OrderPK!&data/budget-recommendations.csvnu[Country,Budget,Currency US,55,AED CH,50,AED AU,50,AED GB,64,AED DE,64,AED DK,51,AED AT,44,AED NO,44,AED CA,51,AED NL,51,AED SE,51,AED IE,40,AED BE,40,AED FI,40,AED FR,51,AED NZ,44,AED HK,22,AED CZ,44,AED SK,40,AED AE,51,AED SG,51,AED IL,51,AED KW,24,AED IT,50,AED JP,64,AED ES,50,AED VE,50,AED HU,40,AED PT,44,AED GR,44,AED KR,22,AED TW,44,AED PL,20,AED ZM,50,AED PR,50,AED CI,50,AED RO,40,AED BH,24,AED PA,50,AED GH,50,AED KH,50,AED SN,50,AED SA,24,AED RU,51,AED UG,50,AED BR,20,AED NI,50,AED MG,50,AED DO,50,AED MX,22,AED OM,24,AED CM,50,AED ZA,50,AED CL,50,AED KE,50,AED NP,50,AED PE,50,AED UA,50,AED MZ,50,AED TZ,50,AED AO,50,AED JO,24,AED ZW,50,AED TR,40,AED MU,50,AED SV,50,AED TH,22,AED CO,50,AED LK,50,AED CR,50,AED AR,24,AED MA,24,AED MY,50,AED KZ,50,AED DZ,24,AED ID,24,AED NG,50,AED GT,50,AED VN,20,AED BY,50,AED EG,24,AED UY,50,AED BD,50,AED PK,50,AED PH,50,AED ET,50,AED TN,24,AED LB,24,AED IN,20,AED PY,50,AED UZ,50,AED EC,50,AED MM,50,AED GE,50,AED US,5347,ARS CH,4812,ARS AU,4812,ARS GB,6238,ARS DE,6238,ARS DK,4991,ARS AT,4278,ARS NO,4278,ARS CA,4991,ARS NL,4991,ARS SE,4991,ARS IE,3921,ARS BE,3921,ARS FI,3921,ARS FR,4991,ARS NZ,4278,ARS HK,2139,ARS CZ,4278,ARS SK,3921,ARS AE,4991,ARS SG,4991,ARS IL,4991,ARS KW,2317,ARS IT,4812,ARS JP,6238,ARS ES,4812,ARS VE,4812,ARS HU,3921,ARS PT,4278,ARS GR,4278,ARS KR,2139,ARS TW,4278,ARS PL,1961,ARS ZM,4812,ARS PR,4812,ARS CI,4812,ARS RO,3921,ARS BH,2317,ARS PA,4812,ARS GH,4812,ARS KH,4812,ARS SN,4812,ARS SA,2317,ARS RU,4991,ARS UG,4812,ARS BR,1961,ARS NI,4812,ARS MG,4812,ARS DO,4812,ARS MX,2139,ARS OM,2317,ARS CM,4812,ARS ZA,4812,ARS CL,4812,ARS KE,4812,ARS NP,4812,ARS PE,4812,ARS UA,4812,ARS MZ,4812,ARS TZ,4812,ARS AO,4812,ARS JO,2317,ARS ZW,4812,ARS TR,3921,ARS MU,4812,ARS SV,4812,ARS TH,2139,ARS CO,4812,ARS LK,4812,ARS CR,4812,ARS AR,2317,ARS MA,2317,ARS MY,4812,ARS KZ,4812,ARS DZ,2317,ARS ID,2317,ARS NG,4812,ARS GT,4812,ARS VN,1961,ARS BY,4812,ARS EG,2317,ARS UY,4812,ARS BD,4812,ARS PK,4812,ARS PH,4812,ARS ET,4812,ARS TN,2317,ARS LB,2317,ARS IN,1961,ARS PY,4812,ARS UZ,4812,ARS EC,4812,ARS MM,4812,ARS GE,4812,ARS US,23,AUD CH,21,AUD AU,21,AUD GB,27,AUD DE,27,AUD DK,21,AUD AT,18,AUD NO,18,AUD CA,21,AUD NL,21,AUD SE,21,AUD IE,17,AUD BE,17,AUD FI,17,AUD FR,21,AUD NZ,18,AUD HK,9,AUD CZ,18,AUD SK,17,AUD AE,21,AUD SG,21,AUD IL,21,AUD KW,10,AUD IT,21,AUD JP,27,AUD ES,21,AUD VE,21,AUD HU,17,AUD PT,18,AUD GR,18,AUD KR,9,AUD TW,18,AUD PL,8,AUD ZM,21,AUD PR,21,AUD CI,21,AUD RO,17,AUD BH,10,AUD PA,21,AUD GH,21,AUD KH,21,AUD SN,21,AUD SA,10,AUD RU,21,AUD UG,21,AUD BR,8,AUD NI,21,AUD MG,21,AUD DO,21,AUD MX,9,AUD OM,10,AUD CM,21,AUD ZA,21,AUD CL,21,AUD KE,21,AUD NP,21,AUD PE,21,AUD UA,21,AUD MZ,21,AUD TZ,21,AUD AO,21,AUD JO,10,AUD ZW,21,AUD TR,17,AUD MU,21,AUD SV,21,AUD TH,9,AUD CO,21,AUD LK,21,AUD CR,21,AUD AR,10,AUD MA,10,AUD MY,21,AUD KZ,21,AUD DZ,10,AUD ID,10,AUD NG,21,AUD GT,21,AUD VN,8,AUD BY,21,AUD EG,10,AUD UY,21,AUD BD,21,AUD PK,21,AUD PH,21,AUD ET,21,AUD TN,10,AUD LB,10,AUD IN,8,AUD PY,21,AUD UZ,21,AUD EC,21,AUD MM,21,AUD GE,21,AUD US,27,BGN CH,24,BGN AU,24,BGN GB,31,BGN DE,31,BGN DK,25,BGN AT,22,BGN NO,22,BGN CA,25,BGN NL,25,BGN SE,25,BGN IE,20,BGN BE,20,BGN FI,20,BGN FR,25,BGN NZ,22,BGN HK,11,BGN CZ,22,BGN SK,20,BGN AE,25,BGN SG,25,BGN IL,25,BGN KW,12,BGN IT,24,BGN JP,31,BGN ES,24,BGN VE,24,BGN HU,20,BGN PT,22,BGN GR,22,BGN KR,11,BGN TW,22,BGN PL,10,BGN ZM,24,BGN PR,24,BGN CI,24,BGN RO,20,BGN BH,12,BGN PA,24,BGN GH,24,BGN KH,24,BGN SN,24,BGN SA,12,BGN RU,25,BGN UG,24,BGN BR,10,BGN NI,24,BGN MG,24,BGN DO,24,BGN MX,11,BGN OM,12,BGN CM,24,BGN ZA,24,BGN CL,24,BGN KE,24,BGN NP,24,BGN PE,24,BGN UA,24,BGN MZ,24,BGN TZ,24,BGN AO,24,BGN JO,12,BGN ZW,24,BGN TR,20,BGN MU,24,BGN SV,24,BGN TH,11,BGN CO,24,BGN LK,24,BGN CR,24,BGN AR,12,BGN MA,12,BGN MY,24,BGN KZ,24,BGN DZ,12,BGN ID,12,BGN NG,24,BGN GT,24,BGN VN,10,BGN BY,24,BGN EG,12,BGN UY,24,BGN BD,24,BGN PK,24,BGN PH,24,BGN ET,24,BGN TN,12,BGN LB,12,BGN IN,10,BGN PY,24,BGN UZ,24,BGN EC,24,BGN MM,24,BGN GE,24,BGN US,104,BOB CH,93,BOB AU,93,BOB GB,121,BOB DE,121,BOB DK,97,BOB AT,83,BOB NO,83,BOB CA,97,BOB NL,97,BOB SE,97,BOB IE,76,BOB BE,76,BOB FI,76,BOB FR,97,BOB NZ,83,BOB HK,41,BOB CZ,83,BOB SK,76,BOB AE,97,BOB SG,97,BOB IL,97,BOB KW,45,BOB IT,93,BOB JP,121,BOB ES,93,BOB VE,93,BOB HU,76,BOB PT,83,BOB GR,83,BOB KR,41,BOB TW,83,BOB PL,38,BOB ZM,93,BOB PR,93,BOB CI,93,BOB RO,76,BOB BH,45,BOB PA,93,BOB GH,93,BOB KH,93,BOB SN,93,BOB SA,45,BOB RU,97,BOB UG,93,BOB BR,38,BOB NI,93,BOB MG,93,BOB DO,93,BOB MX,41,BOB OM,45,BOB CM,93,BOB ZA,93,BOB CL,93,BOB KE,93,BOB NP,93,BOB PE,93,BOB UA,93,BOB MZ,93,BOB TZ,93,BOB AO,93,BOB JO,45,BOB ZW,93,BOB TR,76,BOB MU,93,BOB SV,93,BOB TH,41,BOB CO,93,BOB LK,93,BOB CR,93,BOB AR,45,BOB MA,45,BOB MY,93,BOB KZ,93,BOB DZ,45,BOB ID,45,BOB NG,93,BOB GT,93,BOB VN,38,BOB BY,93,BOB EG,45,BOB UY,93,BOB BD,93,BOB PK,93,BOB PH,93,BOB ET,93,BOB TN,45,BOB LB,45,BOB IN,38,BOB PY,93,BOB UZ,93,BOB EC,93,BOB MM,93,BOB GE,93,BOB US,73,BRL CH,66,BRL AU,66,BRL GB,86,BRL DE,86,BRL DK,69,BRL AT,59,BRL NO,59,BRL CA,69,BRL NL,69,BRL SE,69,BRL IE,54,BRL BE,54,BRL FI,54,BRL FR,69,BRL NZ,59,BRL HK,29,BRL CZ,59,BRL SK,54,BRL AE,69,BRL SG,69,BRL IL,69,BRL KW,32,BRL IT,66,BRL JP,86,BRL ES,66,BRL VE,66,BRL HU,54,BRL PT,59,BRL GR,59,BRL KR,29,BRL TW,59,BRL PL,27,BRL ZM,66,BRL PR,66,BRL CI,66,BRL RO,54,BRL BH,32,BRL PA,66,BRL GH,66,BRL KH,66,BRL SN,66,BRL SA,32,BRL RU,69,BRL UG,66,BRL BR,27,BRL NI,66,BRL MG,66,BRL DO,66,BRL MX,29,BRL OM,32,BRL CM,66,BRL ZA,66,BRL CL,66,BRL KE,66,BRL NP,66,BRL PE,66,BRL UA,66,BRL MZ,66,BRL TZ,66,BRL AO,66,BRL JO,32,BRL ZW,66,BRL TR,54,BRL MU,66,BRL SV,66,BRL TH,29,BRL CO,66,BRL LK,66,BRL CR,66,BRL AR,32,BRL MA,32,BRL MY,66,BRL KZ,66,BRL DZ,32,BRL ID,32,BRL NG,66,BRL GT,66,BRL VN,27,BRL BY,66,BRL EG,32,BRL UY,66,BRL BD,66,BRL PK,66,BRL PH,66,BRL ET,66,BRL TN,32,BRL LB,32,BRL IN,27,BRL PY,66,BRL UZ,66,BRL EC,66,BRL MM,66,BRL GE,66,BRL US,21,CAD CH,18,CAD AU,18,CAD GB,24,CAD DE,24,CAD DK,19,CAD AT,16,CAD NO,16,CAD CA,19,CAD NL,19,CAD SE,19,CAD IE,15,CAD BE,15,CAD FI,15,CAD FR,19,CAD NZ,16,CAD HK,8,CAD CZ,16,CAD SK,15,CAD AE,19,CAD SG,19,CAD IL,19,CAD KW,9,CAD IT,18,CAD JP,24,CAD ES,18,CAD VE,18,CAD HU,15,CAD PT,16,CAD GR,16,CAD KR,8,CAD TW,16,CAD PL,8,CAD ZM,18,CAD PR,18,CAD CI,18,CAD RO,15,CAD BH,9,CAD PA,18,CAD GH,18,CAD KH,18,CAD SN,18,CAD SA,9,CAD RU,19,CAD UG,18,CAD BR,8,CAD NI,18,CAD MG,18,CAD DO,18,CAD MX,8,CAD OM,9,CAD CM,18,CAD ZA,18,CAD CL,18,CAD KE,18,CAD NP,18,CAD PE,18,CAD UA,18,CAD MZ,18,CAD TZ,18,CAD AO,18,CAD JO,9,CAD ZW,18,CAD TR,15,CAD MU,18,CAD SV,18,CAD TH,8,CAD CO,18,CAD LK,18,CAD CR,18,CAD AR,9,CAD MA,9,CAD MY,18,CAD KZ,18,CAD DZ,9,CAD ID,9,CAD NG,18,CAD GT,18,CAD VN,8,CAD BY,18,CAD EG,9,CAD UY,18,CAD BD,18,CAD PK,18,CAD PH,18,CAD ET,18,CAD TN,9,CAD LB,9,CAD IN,8,CAD PY,18,CAD UZ,18,CAD EC,18,CAD MM,18,CAD GE,18,CAD US,13,CHF CH,12,CHF AU,12,CHF GB,15,CHF DE,15,CHF DK,12,CHF AT,11,CHF NO,11,CHF CA,12,CHF NL,12,CHF SE,12,CHF IE,10,CHF BE,10,CHF FI,10,CHF FR,12,CHF NZ,11,CHF HK,5,CHF CZ,11,CHF SK,10,CHF AE,12,CHF SG,12,CHF IL,12,CHF KW,6,CHF IT,12,CHF JP,15,CHF ES,12,CHF VE,12,CHF HU,10,CHF PT,11,CHF GR,11,CHF KR,5,CHF TW,11,CHF PL,5,CHF ZM,12,CHF PR,12,CHF CI,12,CHF RO,10,CHF BH,6,CHF PA,12,CHF GH,12,CHF KH,12,CHF SN,12,CHF SA,6,CHF RU,12,CHF UG,12,CHF BR,5,CHF NI,12,CHF MG,12,CHF DO,12,CHF MX,5,CHF OM,6,CHF CM,12,CHF ZA,12,CHF CL,12,CHF KE,12,CHF NP,12,CHF PE,12,CHF UA,12,CHF MZ,12,CHF TZ,12,CHF AO,12,CHF JO,6,CHF ZW,12,CHF TR,10,CHF MU,12,CHF SV,12,CHF TH,5,CHF CO,12,CHF LK,12,CHF CR,12,CHF AR,6,CHF MA,6,CHF MY,12,CHF KZ,12,CHF DZ,6,CHF ID,6,CHF NG,12,CHF GT,12,CHF VN,5,CHF BY,12,CHF EG,6,CHF UY,12,CHF BD,12,CHF PK,12,CHF PH,12,CHF ET,12,CHF TN,6,CHF LB,6,CHF IN,5,CHF PY,12,CHF UZ,12,CHF EC,12,CHF MM,12,CHF GE,12,CHF US,13081,CLP CH,11773,CLP AU,11773,CLP GB,15261,CLP DE,15261,CLP DK,12209,CLP AT,10464,CLP NO,10464,CLP CA,12209,CLP NL,12209,CLP SE,12209,CLP IE,9592,CLP BE,9592,CLP FI,9592,CLP FR,12209,CLP NZ,10464,CLP HK,5232,CLP CZ,10464,CLP SK,9592,CLP AE,12209,CLP SG,12209,CLP IL,12209,CLP KW,5668,CLP IT,11773,CLP JP,15261,CLP ES,11773,CLP VE,11773,CLP HU,9592,CLP PT,10464,CLP GR,10464,CLP KR,5232,CLP TW,10464,CLP PL,4796,CLP ZM,11773,CLP PR,11773,CLP CI,11773,CLP RO,9592,CLP BH,5668,CLP PA,11773,CLP GH,11773,CLP KH,11773,CLP SN,11773,CLP SA,5668,CLP RU,12209,CLP UG,11773,CLP BR,4796,CLP NI,11773,CLP MG,11773,CLP DO,11773,CLP MX,5232,CLP OM,5668,CLP CM,11773,CLP ZA,11773,CLP CL,11773,CLP KE,11773,CLP NP,11773,CLP PE,11773,CLP UA,11773,CLP MZ,11773,CLP TZ,11773,CLP AO,11773,CLP JO,5668,CLP ZW,11773,CLP TR,9592,CLP MU,11773,CLP SV,11773,CLP TH,5232,CLP CO,11773,CLP LK,11773,CLP CR,11773,CLP AR,5668,CLP MA,5668,CLP MY,11773,CLP KZ,11773,CLP DZ,5668,CLP ID,5668,CLP NG,11773,CLP GT,11773,CLP VN,4796,CLP BY,11773,CLP EG,5668,CLP UY,11773,CLP BD,11773,CLP PK,11773,CLP PH,11773,CLP ET,11773,CLP TN,5668,CLP LB,5668,CLP IN,4796,CLP PY,11773,CLP UZ,11773,CLP EC,11773,CLP MM,11773,CLP GE,11773,CLP US,107,CNY CH,97,CNY AU,97,CNY GB,125,CNY DE,125,CNY DK,100,CNY AT,86,CNY NO,86,CNY CA,100,CNY NL,100,CNY SE,100,CNY IE,79,CNY BE,79,CNY FI,79,CNY FR,100,CNY NZ,86,CNY HK,43,CNY CZ,86,CNY SK,79,CNY AE,100,CNY SG,100,CNY IL,100,CNY KW,47,CNY IT,97,CNY JP,125,CNY ES,97,CNY VE,97,CNY HU,79,CNY PT,86,CNY GR,86,CNY KR,43,CNY TW,86,CNY PL,39,CNY ZM,97,CNY PR,97,CNY CI,97,CNY RO,79,CNY BH,47,CNY PA,97,CNY GH,97,CNY KH,97,CNY SN,97,CNY SA,47,CNY RU,100,CNY UG,97,CNY BR,39,CNY NI,97,CNY MG,97,CNY DO,97,CNY MX,43,CNY OM,47,CNY CM,97,CNY ZA,97,CNY CL,97,CNY KE,97,CNY NP,97,CNY PE,97,CNY UA,97,CNY MZ,97,CNY TZ,97,CNY AO,97,CNY JO,47,CNY ZW,97,CNY TR,79,CNY MU,97,CNY SV,97,CNY TH,43,CNY CO,97,CNY LK,97,CNY CR,97,CNY AR,47,CNY MA,47,CNY MY,97,CNY KZ,97,CNY DZ,47,CNY ID,47,CNY NG,97,CNY GT,97,CNY VN,39,CNY BY,97,CNY EG,47,CNY UY,97,CNY BD,97,CNY PK,97,CNY PH,97,CNY ET,97,CNY TN,47,CNY LB,47,CNY IN,39,CNY PY,97,CNY UZ,97,CNY EC,97,CNY MM,97,CNY GE,97,CNY US,61099,COP CH,54989,COP AU,54989,COP GB,71282,COP DE,71282,COP DK,57026,COP AT,48879,COP NO,48879,COP CA,57026,COP NL,57026,COP SE,57026,COP IE,44806,COP BE,44806,COP FI,44806,COP FR,57026,COP NZ,48879,COP HK,24440,COP CZ,48879,COP SK,44806,COP AE,57026,COP SG,57026,COP IL,57026,COP KW,26476,COP IT,54989,COP JP,71282,COP ES,54989,COP VE,54989,COP HU,44806,COP PT,48879,COP GR,48879,COP KR,24440,COP TW,48879,COP PL,22403,COP ZM,54989,COP PR,54989,COP CI,54989,COP RO,44806,COP BH,26476,COP PA,54989,COP GH,54989,COP KH,54989,COP SN,54989,COP SA,26476,COP RU,57026,COP UG,54989,COP BR,22403,COP NI,54989,COP MG,54989,COP DO,54989,COP MX,24440,COP OM,26476,COP CM,54989,COP ZA,54989,COP CL,54989,COP KE,54989,COP NP,54989,COP PE,54989,COP UA,54989,COP MZ,54989,COP TZ,54989,COP AO,54989,COP JO,26476,COP ZW,54989,COP TR,44806,COP MU,54989,COP SV,54989,COP TH,24440,COP CO,54989,COP LK,54989,COP CR,54989,COP AR,26476,COP MA,26476,COP MY,54989,COP KZ,54989,COP DZ,26476,COP ID,26476,COP NG,54989,COP GT,54989,COP VN,22403,COP BY,54989,COP EG,26476,COP UY,54989,COP BD,54989,COP PK,54989,COP PH,54989,COP ET,54989,COP TN,26476,COP LB,26476,COP IN,22403,COP PY,54989,COP UZ,54989,COP EC,54989,COP MM,54989,COP GE,54989,COP US,337,CZK CH,303,CZK AU,303,CZK GB,393,CZK DE,393,CZK DK,314,CZK AT,269,CZK NO,269,CZK CA,314,CZK NL,314,CZK SE,314,CZK IE,247,CZK BE,247,CZK FI,247,CZK FR,314,CZK NZ,269,CZK HK,135,CZK CZ,269,CZK SK,247,CZK AE,314,CZK SG,314,CZK IL,314,CZK KW,146,CZK IT,303,CZK JP,393,CZK ES,303,CZK VE,303,CZK HU,247,CZK PT,269,CZK GR,269,CZK KR,135,CZK TW,269,CZK PL,124,CZK ZM,303,CZK PR,303,CZK CI,303,CZK RO,247,CZK BH,146,CZK PA,303,CZK GH,303,CZK KH,303,CZK SN,303,CZK SA,146,CZK RU,314,CZK UG,303,CZK BR,124,CZK NI,303,CZK MG,303,CZK DO,303,CZK MX,135,CZK OM,146,CZK CM,303,CZK ZA,303,CZK CL,303,CZK KE,303,CZK NP,303,CZK PE,303,CZK UA,303,CZK MZ,303,CZK TZ,303,CZK AO,303,CZK JO,146,CZK ZW,303,CZK TR,247,CZK MU,303,CZK SV,303,CZK TH,135,CZK CO,303,CZK LK,303,CZK CR,303,CZK AR,146,CZK MA,146,CZK MY,303,CZK KZ,303,CZK DZ,146,CZK ID,146,CZK NG,303,CZK GT,303,CZK VN,124,CZK BY,303,CZK EG,146,CZK UY,303,CZK BD,303,CZK PK,303,CZK PH,303,CZK ET,303,CZK TN,146,CZK LB,146,CZK IN,124,CZK PY,303,CZK UZ,303,CZK EC,303,CZK MM,303,CZK GE,303,CZK US,103,DKK CH,92,DKK AU,92,DKK GB,120,DKK DE,120,DKK DK,96,DKK AT,82,DKK NO,82,DKK CA,96,DKK NL,96,DKK SE,96,DKK IE,75,DKK BE,75,DKK FI,75,DKK FR,96,DKK NZ,82,DKK HK,41,DKK CZ,82,DKK SK,75,DKK AE,96,DKK SG,96,DKK IL,96,DKK KW,45,DKK IT,92,DKK JP,120,DKK ES,92,DKK VE,92,DKK HU,75,DKK PT,82,DKK GR,82,DKK KR,41,DKK TW,82,DKK PL,38,DKK ZM,92,DKK PR,92,DKK CI,92,DKK RO,75,DKK BH,45,DKK PA,92,DKK GH,92,DKK KH,92,DKK SN,92,DKK SA,45,DKK RU,96,DKK UG,92,DKK BR,38,DKK NI,92,DKK MG,92,DKK DO,92,DKK MX,41,DKK OM,45,DKK CM,92,DKK ZA,92,DKK CL,92,DKK KE,92,DKK NP,92,DKK PE,92,DKK UA,92,DKK MZ,92,DKK TZ,92,DKK AO,92,DKK JO,45,DKK ZW,92,DKK TR,75,DKK MU,92,DKK SV,92,DKK TH,41,DKK CO,92,DKK LK,92,DKK CR,92,DKK AR,45,DKK MA,45,DKK MY,92,DKK KZ,92,DKK DZ,45,DKK ID,45,DKK NG,92,DKK GT,92,DKK VN,38,DKK BY,92,DKK EG,45,DKK UY,92,DKK BD,92,DKK PK,92,DKK PH,92,DKK ET,92,DKK TN,45,DKK LB,45,DKK IN,38,DKK PY,92,DKK UZ,92,DKK EC,92,DKK MM,92,DKK GE,92,DKK US,463,EGP CH,417,EGP AU,417,EGP GB,541,EGP DE,541,EGP DK,433,EGP AT,371,EGP NO,371,EGP CA,433,EGP NL,433,EGP SE,433,EGP IE,340,EGP BE,340,EGP FI,340,EGP FR,433,EGP NZ,371,EGP HK,185,EGP CZ,371,EGP SK,340,EGP AE,433,EGP SG,433,EGP IL,433,EGP KW,201,EGP IT,417,EGP JP,541,EGP ES,417,EGP VE,417,EGP HU,340,EGP PT,371,EGP GR,371,EGP KR,185,EGP TW,371,EGP PL,170,EGP ZM,417,EGP PR,417,EGP CI,417,EGP RO,340,EGP BH,201,EGP PA,417,EGP GH,417,EGP KH,417,EGP SN,417,EGP SA,201,EGP RU,433,EGP UG,417,EGP BR,170,EGP NI,417,EGP MG,417,EGP DO,417,EGP MX,185,EGP OM,201,EGP CM,417,EGP ZA,417,EGP CL,417,EGP KE,417,EGP NP,417,EGP PE,417,EGP UA,417,EGP MZ,417,EGP TZ,417,EGP AO,417,EGP JO,201,EGP ZW,417,EGP TR,340,EGP MU,417,EGP SV,417,EGP TH,185,EGP CO,417,EGP LK,417,EGP CR,417,EGP AR,201,EGP MA,201,EGP MY,417,EGP KZ,417,EGP DZ,201,EGP ID,201,EGP NG,417,EGP GT,417,EGP VN,170,EGP BY,417,EGP EG,201,EGP UY,417,EGP BD,417,EGP PK,417,EGP PH,417,EGP ET,417,EGP TN,201,EGP LB,201,EGP IN,170,EGP PY,417,EGP UZ,417,EGP EC,417,EGP MM,417,EGP GE,417,EGP US,14,EUR CH,12,EUR AU,12,EUR GB,16,EUR DE,16,EUR DK,13,EUR AT,11,EUR NO,11,EUR CA,13,EUR NL,13,EUR SE,13,EUR IE,10,EUR BE,10,EUR FI,10,EUR FR,13,EUR NZ,11,EUR HK,6,EUR CZ,11,EUR SK,10,EUR AE,13,EUR SG,13,EUR IL,13,EUR KW,6,EUR IT,12,EUR JP,16,EUR ES,12,EUR VE,12,EUR HU,10,EUR PT,11,EUR GR,11,EUR KR,6,EUR TW,11,EUR PL,5,EUR ZM,12,EUR PR,12,EUR CI,12,EUR RO,10,EUR BH,6,EUR PA,12,EUR GH,12,EUR KH,12,EUR SN,12,EUR SA,6,EUR RU,13,EUR UG,12,EUR BR,5,EUR NI,12,EUR MG,12,EUR DO,12,EUR MX,6,EUR OM,6,EUR CM,12,EUR ZA,12,EUR CL,12,EUR KE,12,EUR NP,12,EUR PE,12,EUR UA,12,EUR MZ,12,EUR TZ,12,EUR AO,12,EUR JO,6,EUR ZW,12,EUR TR,10,EUR MU,12,EUR SV,12,EUR TH,6,EUR CO,12,EUR LK,12,EUR CR,12,EUR AR,6,EUR MA,6,EUR MY,12,EUR KZ,12,EUR DZ,6,EUR ID,6,EUR NG,12,EUR GT,12,EUR VN,5,EUR BY,12,EUR EG,6,EUR UY,12,EUR BD,12,EUR PK,12,EUR PH,12,EUR ET,12,EUR TN,6,EUR LB,6,EUR IN,5,EUR PY,12,EUR UZ,12,EUR EC,12,EUR MM,12,EUR GE,12,EUR US,12,GBP CH,11,GBP AU,11,GBP GB,14,GBP DE,14,GBP DK,11,GBP AT,10,GBP NO,10,GBP CA,11,GBP NL,11,GBP SE,11,GBP IE,9,GBP BE,9,GBP FI,9,GBP FR,11,GBP NZ,10,GBP HK,5,GBP CZ,10,GBP SK,9,GBP AE,11,GBP SG,11,GBP IL,11,GBP KW,5,GBP IT,11,GBP JP,14,GBP ES,11,GBP VE,11,GBP HU,9,GBP PT,10,GBP GR,10,GBP KR,5,GBP TW,10,GBP PL,4,GBP ZM,11,GBP PR,11,GBP CI,11,GBP RO,9,GBP BH,5,GBP PA,11,GBP GH,11,GBP KH,11,GBP SN,11,GBP SA,5,GBP RU,11,GBP UG,11,GBP BR,4,GBP NI,11,GBP MG,11,GBP DO,11,GBP MX,5,GBP OM,5,GBP CM,11,GBP ZA,11,GBP CL,11,GBP KE,11,GBP NP,11,GBP PE,11,GBP UA,11,GBP MZ,11,GBP TZ,11,GBP AO,11,GBP JO,5,GBP ZW,11,GBP TR,9,GBP MU,11,GBP SV,11,GBP TH,5,GBP CO,11,GBP LK,11,GBP CR,11,GBP AR,5,GBP MA,5,GBP MY,11,GBP KZ,11,GBP DZ,5,GBP ID,5,GBP NG,11,GBP GT,11,GBP VN,4,GBP BY,11,GBP EG,5,GBP UY,11,GBP BD,11,GBP PK,11,GBP PH,11,GBP ET,11,GBP TN,5,GBP LB,5,GBP IN,4,GBP PY,11,GBP UZ,11,GBP EC,11,GBP MM,11,GBP GE,11,GBP US,117,HKD CH,105,HKD AU,105,HKD GB,136,HKD DE,136,HKD DK,109,HKD AT,94,HKD NO,94,HKD CA,109,HKD NL,109,HKD SE,109,HKD IE,86,HKD BE,86,HKD FI,86,HKD FR,109,HKD NZ,94,HKD HK,47,HKD CZ,94,HKD SK,86,HKD AE,109,HKD SG,109,HKD IL,109,HKD KW,51,HKD IT,105,HKD JP,136,HKD ES,105,HKD VE,105,HKD HU,86,HKD PT,94,HKD GR,94,HKD KR,47,HKD TW,94,HKD PL,43,HKD ZM,105,HKD PR,105,HKD CI,105,HKD RO,86,HKD BH,51,HKD PA,105,HKD GH,105,HKD KH,105,HKD SN,105,HKD SA,51,HKD RU,109,HKD UG,105,HKD BR,43,HKD NI,105,HKD MG,105,HKD DO,105,HKD MX,47,HKD OM,51,HKD CM,105,HKD ZA,105,HKD CL,105,HKD KE,105,HKD NP,105,HKD PE,105,HKD UA,105,HKD MZ,105,HKD TZ,105,HKD AO,105,HKD JO,51,HKD ZW,105,HKD TR,86,HKD MU,105,HKD SV,105,HKD TH,47,HKD CO,105,HKD LK,105,HKD CR,105,HKD AR,51,HKD MA,51,HKD MY,105,HKD KZ,105,HKD DZ,51,HKD ID,51,HKD NG,105,HKD GT,105,HKD VN,43,HKD BY,105,HKD EG,51,HKD UY,105,HKD BD,105,HKD PK,105,HKD PH,105,HKD ET,105,HKD TN,51,HKD LB,51,HKD IN,43,HKD PY,105,HKD UZ,105,HKD EC,105,HKD MM,105,HKD GE,105,HKD US,5254,HUF CH,4729,HUF AU,4729,HUF GB,6130,HUF DE,6130,HUF DK,4904,HUF AT,4203,HUF NO,4203,HUF CA,4904,HUF NL,4904,HUF SE,4904,HUF IE,3853,HUF BE,3853,HUF FI,3853,HUF FR,4904,HUF NZ,4203,HUF HK,2102,HUF CZ,4203,HUF SK,3853,HUF AE,4904,HUF SG,4904,HUF IL,4904,HUF KW,2277,HUF IT,4729,HUF JP,6130,HUF ES,4729,HUF VE,4729,HUF HU,3853,HUF PT,4203,HUF GR,4203,HUF KR,2102,HUF TW,4203,HUF PL,1926,HUF ZM,4729,HUF PR,4729,HUF CI,4729,HUF RO,3853,HUF BH,2277,HUF PA,4729,HUF GH,4729,HUF KH,4729,HUF SN,4729,HUF SA,2277,HUF RU,4904,HUF UG,4729,HUF BR,1926,HUF NI,4729,HUF MG,4729,HUF DO,4729,HUF MX,2102,HUF OM,2277,HUF CM,4729,HUF ZA,4729,HUF CL,4729,HUF KE,4729,HUF NP,4729,HUF PE,4729,HUF UA,4729,HUF MZ,4729,HUF TZ,4729,HUF AO,4729,HUF JO,2277,HUF ZW,4729,HUF TR,3853,HUF MU,4729,HUF SV,4729,HUF TH,2102,HUF CO,4729,HUF LK,4729,HUF CR,4729,HUF AR,2277,HUF MA,2277,HUF MY,4729,HUF KZ,4729,HUF DZ,2277,HUF ID,2277,HUF NG,4729,HUF GT,4729,HUF VN,1926,HUF BY,4729,HUF EG,2277,HUF UY,4729,HUF BD,4729,HUF PK,4729,HUF PH,4729,HUF ET,4729,HUF TN,2277,HUF LB,2277,HUF IN,1926,HUF PY,4729,HUF UZ,4729,HUF EC,4729,HUF MM,4729,HUF GE,4729,HUF US,234395,IDR CH,210956,IDR AU,210956,IDR GB,273461,IDR DE,273461,IDR DK,218769,IDR AT,187516,IDR NO,187516,IDR CA,218769,IDR NL,218769,IDR SE,218769,IDR IE,171890,IDR BE,171890,IDR FI,171890,IDR FR,218769,IDR NZ,187516,IDR HK,93758,IDR CZ,187516,IDR SK,171890,IDR AE,218769,IDR SG,218769,IDR IL,218769,IDR KW,101571,IDR IT,210956,IDR JP,273461,IDR ES,210956,IDR VE,210956,IDR HU,171890,IDR PT,187516,IDR GR,187516,IDR KR,93758,IDR TW,187516,IDR PL,85945,IDR ZM,210956,IDR PR,210956,IDR CI,210956,IDR RO,171890,IDR BH,101571,IDR PA,210956,IDR GH,210956,IDR KH,210956,IDR SN,210956,IDR SA,101571,IDR RU,218769,IDR UG,210956,IDR BR,85945,IDR NI,210956,IDR MG,210956,IDR DO,210956,IDR MX,93758,IDR OM,101571,IDR CM,210956,IDR ZA,210956,IDR CL,210956,IDR KE,210956,IDR NP,210956,IDR PE,210956,IDR UA,210956,IDR MZ,210956,IDR TZ,210956,IDR AO,210956,IDR JO,101571,IDR ZW,210956,IDR TR,171890,IDR MU,210956,IDR SV,210956,IDR TH,93758,IDR CO,210956,IDR LK,210956,IDR CR,210956,IDR AR,101571,IDR MA,101571,IDR MY,210956,IDR KZ,210956,IDR DZ,101571,IDR ID,101571,IDR NG,210956,IDR GT,210956,IDR VN,85945,IDR BY,210956,IDR EG,101571,IDR UY,210956,IDR BD,210956,IDR PK,210956,IDR PH,210956,IDR ET,210956,IDR TN,101571,IDR LB,101571,IDR IN,85945,IDR PY,210956,IDR UZ,210956,IDR EC,210956,IDR MM,210956,IDR GE,210956,IDR US,56,ILS CH,50,ILS AU,50,ILS GB,65,ILS DE,65,ILS DK,52,ILS AT,45,ILS NO,45,ILS CA,52,ILS NL,52,ILS SE,52,ILS IE,41,ILS BE,41,ILS FI,41,ILS FR,52,ILS NZ,45,ILS HK,22,ILS CZ,45,ILS SK,41,ILS AE,52,ILS SG,52,ILS IL,52,ILS KW,24,ILS IT,50,ILS JP,65,ILS ES,50,ILS VE,50,ILS HU,41,ILS PT,45,ILS GR,45,ILS KR,22,ILS TW,45,ILS PL,21,ILS ZM,50,ILS PR,50,ILS CI,50,ILS RO,41,ILS BH,24,ILS PA,50,ILS GH,50,ILS KH,50,ILS SN,50,ILS SA,24,ILS RU,52,ILS UG,50,ILS BR,21,ILS NI,50,ILS MG,50,ILS DO,50,ILS MX,22,ILS OM,24,ILS CM,50,ILS ZA,50,ILS CL,50,ILS KE,50,ILS NP,50,ILS PE,50,ILS UA,50,ILS MZ,50,ILS TZ,50,ILS AO,50,ILS JO,24,ILS ZW,50,ILS TR,41,ILS MU,50,ILS SV,50,ILS TH,22,ILS CO,50,ILS LK,50,ILS CR,50,ILS AR,24,ILS MA,24,ILS MY,50,ILS KZ,50,ILS DZ,24,ILS ID,24,ILS NG,50,ILS GT,50,ILS VN,21,ILS BY,50,ILS EG,24,ILS UY,50,ILS BD,50,ILS PK,50,ILS PH,50,ILS ET,50,ILS TN,24,ILS LB,24,ILS IN,21,ILS PY,50,ILS UZ,50,ILS EC,50,ILS MM,50,ILS GE,50,ILS US,1250,INR CH,1125,INR AU,1125,INR GB,1458,INR DE,1458,INR DK,1166,INR AT,1000,INR NO,1000,INR CA,1166,INR NL,1166,INR SE,1166,INR IE,916,INR BE,916,INR FI,916,INR FR,1166,INR NZ,1000,INR HK,500,INR CZ,1000,INR SK,916,INR AE,1166,INR SG,1166,INR IL,1166,INR KW,541,INR IT,1125,INR JP,1458,INR ES,1125,INR VE,1125,INR HU,916,INR PT,1000,INR GR,1000,INR KR,500,INR TW,1000,INR PL,458,INR ZM,1125,INR PR,1125,INR CI,1125,INR RO,916,INR BH,541,INR PA,1125,INR GH,1125,INR KH,1125,INR SN,1125,INR SA,541,INR RU,1166,INR UG,1125,INR BR,458,INR NI,1125,INR MG,1125,INR DO,1125,INR MX,500,INR OM,541,INR CM,1125,INR ZA,1125,INR CL,1125,INR KE,1125,INR NP,1125,INR PE,1125,INR UA,1125,INR MZ,1125,INR TZ,1125,INR AO,1125,INR JO,541,INR ZW,1125,INR TR,916,INR MU,1125,INR SV,1125,INR TH,500,INR CO,1125,INR LK,1125,INR CR,1125,INR AR,541,INR MA,541,INR MY,1125,INR KZ,1125,INR DZ,541,INR ID,541,INR NG,1125,INR GT,1125,INR VN,458,INR BY,1125,INR EG,541,INR UY,1125,INR BD,1125,INR PK,1125,INR PH,1125,INR ET,1125,INR TN,541,INR LB,541,INR IN,458,INR PY,1125,INR UZ,1125,INR EC,1125,INR MM,1125,INR GE,1125,INR US,2244,JPY CH,2020,JPY AU,2020,JPY GB,2618,JPY DE,2618,JPY DK,2095,JPY AT,1795,JPY NO,1795,JPY CA,2095,JPY NL,2095,JPY SE,2095,JPY IE,1646,JPY BE,1646,JPY FI,1646,JPY FR,2095,JPY NZ,1795,JPY HK,898,JPY CZ,1795,JPY SK,1646,JPY AE,2095,JPY SG,2095,JPY IL,2095,JPY KW,973,JPY IT,2020,JPY JP,2618,JPY ES,2020,JPY VE,2020,JPY HU,1646,JPY PT,1795,JPY GR,1795,JPY KR,898,JPY TW,1795,JPY PL,823,JPY ZM,2020,JPY PR,2020,JPY CI,2020,JPY RO,1646,JPY BH,973,JPY PA,2020,JPY GH,2020,JPY KH,2020,JPY SN,2020,JPY SA,973,JPY RU,2095,JPY UG,2020,JPY BR,823,JPY NI,2020,JPY MG,2020,JPY DO,2020,JPY MX,898,JPY OM,973,JPY CM,2020,JPY ZA,2020,JPY CL,2020,JPY KE,2020,JPY NP,2020,JPY PE,2020,JPY UA,2020,JPY MZ,2020,JPY TZ,2020,JPY AO,2020,JPY JO,973,JPY ZW,2020,JPY TR,1646,JPY MU,2020,JPY SV,2020,JPY TH,898,JPY CO,2020,JPY LK,2020,JPY CR,2020,JPY AR,973,JPY MA,973,JPY MY,2020,JPY KZ,2020,JPY DZ,973,JPY ID,973,JPY NG,2020,JPY GT,2020,JPY VN,823,JPY BY,2020,JPY EG,973,JPY UY,2020,JPY BD,2020,JPY PK,2020,JPY PH,2020,JPY ET,2020,JPY TN,973,JPY LB,973,JPY IN,823,JPY PY,2020,JPY UZ,2020,JPY EC,2020,JPY MM,2020,JPY GE,2020,JPY US,19538,KRW CH,17584,KRW AU,17584,KRW GB,22794,KRW DE,22794,KRW DK,18235,KRW AT,15630,KRW NO,15630,KRW CA,18235,KRW NL,18235,KRW SE,18235,KRW IE,14328,KRW BE,14328,KRW FI,14328,KRW FR,18235,KRW NZ,15630,KRW HK,7815,KRW CZ,15630,KRW SK,14328,KRW AE,18235,KRW SG,18235,KRW IL,18235,KRW KW,8466,KRW IT,17584,KRW JP,22794,KRW ES,17584,KRW VE,17584,KRW HU,14328,KRW PT,15630,KRW GR,15630,KRW KR,7815,KRW TW,15630,KRW PL,7164,KRW ZM,17584,KRW PR,17584,KRW CI,17584,KRW RO,14328,KRW BH,8466,KRW PA,17584,KRW GH,17584,KRW KH,17584,KRW SN,17584,KRW SA,8466,KRW RU,18235,KRW UG,17584,KRW BR,7164,KRW NI,17584,KRW MG,17584,KRW DO,17584,KRW MX,7815,KRW OM,8466,KRW CM,17584,KRW ZA,17584,KRW CL,17584,KRW KE,17584,KRW NP,17584,KRW PE,17584,KRW UA,17584,KRW MZ,17584,KRW TZ,17584,KRW AO,17584,KRW JO,8466,KRW ZW,17584,KRW TR,14328,KRW MU,17584,KRW SV,17584,KRW TH,7815,KRW CO,17584,KRW LK,17584,KRW CR,17584,KRW AR,8466,KRW MA,8466,KRW MY,17584,KRW KZ,17584,KRW DZ,8466,KRW ID,8466,KRW NG,17584,KRW GT,17584,KRW VN,7164,KRW BY,17584,KRW EG,8466,KRW UY,17584,KRW BD,17584,KRW PK,17584,KRW PH,17584,KRW ET,17584,KRW TN,8466,KRW LB,8466,KRW IN,7164,KRW PY,17584,KRW UZ,17584,KRW EC,17584,KRW MM,17584,KRW GE,17584,KRW US,152,MAD CH,137,MAD AU,137,MAD GB,177,MAD DE,177,MAD DK,142,MAD AT,122,MAD NO,122,MAD CA,142,MAD NL,142,MAD SE,142,MAD IE,111,MAD BE,111,MAD FI,111,MAD FR,142,MAD NZ,122,MAD HK,61,MAD CZ,122,MAD SK,111,MAD AE,142,MAD SG,142,MAD IL,142,MAD KW,66,MAD IT,137,MAD JP,177,MAD ES,137,MAD VE,137,MAD HU,111,MAD PT,122,MAD GR,122,MAD KR,61,MAD TW,122,MAD PL,56,MAD ZM,137,MAD PR,137,MAD CI,137,MAD RO,111,MAD BH,66,MAD PA,137,MAD GH,137,MAD KH,137,MAD SN,137,MAD SA,66,MAD RU,142,MAD UG,137,MAD BR,56,MAD NI,137,MAD MG,137,MAD DO,137,MAD MX,61,MAD OM,66,MAD CM,137,MAD ZA,137,MAD CL,137,MAD KE,137,MAD NP,137,MAD PE,137,MAD UA,137,MAD MZ,137,MAD TZ,137,MAD AO,137,MAD JO,66,MAD ZW,137,MAD TR,111,MAD MU,137,MAD SV,137,MAD TH,61,MAD CO,137,MAD LK,137,MAD CR,137,MAD AR,66,MAD MA,66,MAD MY,137,MAD KZ,137,MAD DZ,66,MAD ID,66,MAD NG,137,MAD GT,137,MAD VN,56,MAD BY,137,MAD EG,66,MAD UY,137,MAD BD,137,MAD PK,137,MAD PH,137,MAD ET,137,MAD TN,66,MAD LB,66,MAD IN,56,MAD PY,137,MAD UZ,137,MAD EC,137,MAD MM,137,MAD GE,137,MAD US,258,MXN CH,232,MXN AU,232,MXN GB,301,MXN DE,301,MXN DK,241,MXN AT,206,MXN NO,206,MXN CA,241,MXN NL,241,MXN SE,241,MXN IE,189,MXN BE,189,MXN FI,189,MXN FR,241,MXN NZ,206,MXN HK,103,MXN CZ,206,MXN SK,189,MXN AE,241,MXN SG,241,MXN IL,241,MXN KW,112,MXN IT,232,MXN JP,301,MXN ES,232,MXN VE,232,MXN HU,189,MXN PT,206,MXN GR,206,MXN KR,103,MXN TW,206,MXN PL,95,MXN ZM,232,MXN PR,232,MXN CI,232,MXN RO,189,MXN BH,112,MXN PA,232,MXN GH,232,MXN KH,232,MXN SN,232,MXN SA,112,MXN RU,241,MXN UG,232,MXN BR,95,MXN NI,232,MXN MG,232,MXN DO,232,MXN MX,103,MXN OM,112,MXN CM,232,MXN ZA,232,MXN CL,232,MXN KE,232,MXN NP,232,MXN PE,232,MXN UA,232,MXN MZ,232,MXN TZ,232,MXN AO,232,MXN JO,112,MXN ZW,232,MXN TR,189,MXN MU,232,MXN SV,232,MXN TH,103,MXN CO,232,MXN LK,232,MXN CR,232,MXN AR,112,MXN MA,112,MXN MY,232,MXN KZ,232,MXN DZ,112,MXN ID,112,MXN NG,232,MXN GT,232,MXN VN,95,MXN BY,232,MXN EG,112,MXN UY,232,MXN BD,232,MXN PK,232,MXN PH,232,MXN ET,232,MXN TN,112,MXN LB,112,MXN IN,95,MXN PY,232,MXN UZ,232,MXN EC,232,MXN MM,232,MXN GE,232,MXN US,70,MYR CH,63,MYR AU,63,MYR GB,82,MYR DE,82,MYR DK,65,MYR AT,56,MYR NO,56,MYR CA,65,MYR NL,65,MYR SE,65,MYR IE,51,MYR BE,51,MYR FI,51,MYR FR,65,MYR NZ,56,MYR HK,28,MYR CZ,56,MYR SK,51,MYR AE,65,MYR SG,65,MYR IL,65,MYR KW,30,MYR IT,63,MYR JP,82,MYR ES,63,MYR VE,63,MYR HU,51,MYR PT,56,MYR GR,56,MYR KR,28,MYR TW,56,MYR PL,26,MYR ZM,63,MYR PR,63,MYR CI,63,MYR RO,51,MYR BH,30,MYR PA,63,MYR GH,63,MYR KH,63,MYR SN,63,MYR SA,30,MYR RU,65,MYR UG,63,MYR BR,26,MYR NI,63,MYR MG,63,MYR DO,63,MYR MX,28,MYR OM,30,MYR CM,63,MYR ZA,63,MYR CL,63,MYR KE,63,MYR NP,63,MYR PE,63,MYR UA,63,MYR MZ,63,MYR TZ,63,MYR AO,63,MYR JO,30,MYR ZW,63,MYR TR,51,MYR MU,63,MYR SV,63,MYR TH,28,MYR CO,63,MYR LK,63,MYR CR,63,MYR AR,30,MYR MA,30,MYR MY,63,MYR KZ,63,MYR DZ,30,MYR ID,30,MYR NG,63,MYR GT,63,MYR VN,26,MYR BY,63,MYR EG,30,MYR UY,63,MYR BD,63,MYR PK,63,MYR PH,63,MYR ET,63,MYR TN,30,MYR LB,30,MYR IN,26,MYR PY,63,MYR UZ,63,MYR EC,63,MYR MM,63,MYR GE,63,MYR US,12242,NGN CH,11017,NGN AU,11017,NGN GB,14282,NGN DE,14282,NGN DK,11426,NGN AT,9793,NGN NO,9793,NGN CA,11426,NGN NL,11426,NGN SE,11426,NGN IE,8977,NGN BE,8977,NGN FI,8977,NGN FR,11426,NGN NZ,9793,NGN HK,4897,NGN CZ,9793,NGN SK,8977,NGN AE,11426,NGN SG,11426,NGN IL,11426,NGN KW,5305,NGN IT,11017,NGN JP,14282,NGN ES,11017,NGN VE,11017,NGN HU,8977,NGN PT,9793,NGN GR,9793,NGN KR,4897,NGN TW,9793,NGN PL,4489,NGN ZM,11017,NGN PR,11017,NGN CI,11017,NGN RO,8977,NGN BH,5305,NGN PA,11017,NGN GH,11017,NGN KH,11017,NGN SN,11017,NGN SA,5305,NGN RU,11426,NGN UG,11017,NGN BR,4489,NGN NI,11017,NGN MG,11017,NGN DO,11017,NGN MX,4897,NGN OM,5305,NGN CM,11017,NGN ZA,11017,NGN CL,11017,NGN KE,11017,NGN NP,11017,NGN PE,11017,NGN UA,11017,NGN MZ,11017,NGN TZ,11017,NGN AO,11017,NGN JO,5305,NGN ZW,11017,NGN TR,8977,NGN MU,11017,NGN SV,11017,NGN TH,4897,NGN CO,11017,NGN LK,11017,NGN CR,11017,NGN AR,5305,NGN MA,5305,NGN MY,11017,NGN KZ,11017,NGN DZ,5305,NGN ID,5305,NGN NG,11017,NGN GT,11017,NGN VN,4489,NGN BY,11017,NGN EG,5305,NGN UY,11017,NGN BD,11017,NGN PK,11017,NGN PH,11017,NGN ET,11017,NGN TN,5305,NGN LB,5305,NGN IN,4489,NGN PY,11017,NGN UZ,11017,NGN EC,11017,NGN MM,11017,NGN GE,11017,NGN US,161,NOK CH,145,NOK AU,145,NOK GB,188,NOK DE,188,NOK DK,151,NOK AT,129,NOK NO,129,NOK CA,151,NOK NL,151,NOK SE,151,NOK IE,118,NOK BE,118,NOK FI,118,NOK FR,151,NOK NZ,129,NOK HK,65,NOK CZ,129,NOK SK,118,NOK AE,151,NOK SG,151,NOK IL,151,NOK KW,70,NOK IT,145,NOK JP,188,NOK ES,145,NOK VE,145,NOK HU,118,NOK PT,129,NOK GR,129,NOK KR,65,NOK TW,129,NOK PL,59,NOK ZM,145,NOK PR,145,NOK CI,145,NOK RO,118,NOK BH,70,NOK PA,145,NOK GH,145,NOK KH,145,NOK SN,145,NOK SA,70,NOK RU,151,NOK UG,145,NOK BR,59,NOK NI,145,NOK MG,145,NOK DO,145,NOK MX,65,NOK OM,70,NOK CM,145,NOK ZA,145,NOK CL,145,NOK KE,145,NOK NP,145,NOK PE,145,NOK UA,145,NOK MZ,145,NOK TZ,145,NOK AO,145,NOK JO,70,NOK ZW,145,NOK TR,118,NOK MU,145,NOK SV,145,NOK TH,65,NOK CO,145,NOK LK,145,NOK CR,145,NOK AR,70,NOK MA,70,NOK MY,145,NOK KZ,145,NOK DZ,70,NOK ID,70,NOK NG,145,NOK GT,145,NOK VN,59,NOK BY,145,NOK EG,70,NOK UY,145,NOK BD,145,NOK PK,145,NOK PH,145,NOK ET,145,NOK TN,70,NOK LB,70,NOK IN,59,NOK PY,145,NOK UZ,145,NOK EC,145,NOK MM,145,NOK GE,145,NOK US,56,PEN CH,50,PEN AU,50,PEN GB,65,PEN DE,65,PEN DK,52,PEN AT,45,PEN NO,45,PEN CA,52,PEN NL,52,PEN SE,52,PEN IE,41,PEN BE,41,PEN FI,41,PEN FR,52,PEN NZ,45,PEN HK,22,PEN CZ,45,PEN SK,41,PEN AE,52,PEN SG,52,PEN IL,52,PEN KW,24,PEN IT,50,PEN JP,65,PEN ES,50,PEN VE,50,PEN HU,41,PEN PT,45,PEN GR,45,PEN KR,22,PEN TW,45,PEN PL,21,PEN ZM,50,PEN PR,50,PEN CI,50,PEN RO,41,PEN BH,24,PEN PA,50,PEN GH,50,PEN KH,50,PEN SN,50,PEN SA,24,PEN RU,52,PEN UG,50,PEN BR,21,PEN NI,50,PEN MG,50,PEN DO,50,PEN MX,22,PEN OM,24,PEN CM,50,PEN ZA,50,PEN CL,50,PEN KE,50,PEN NP,50,PEN PE,50,PEN UA,50,PEN MZ,50,PEN TZ,50,PEN AO,50,PEN JO,24,PEN ZW,50,PEN TR,41,PEN MU,50,PEN SV,50,PEN TH,22,PEN CO,50,PEN LK,50,PEN CR,50,PEN AR,24,PEN MA,24,PEN MY,50,PEN KZ,50,PEN DZ,24,PEN ID,24,PEN NG,50,PEN GT,50,PEN VN,21,PEN BY,50,PEN EG,24,PEN UY,50,PEN BD,50,PEN PK,50,PEN PH,50,PEN ET,50,PEN TN,24,PEN LB,24,PEN IN,21,PEN PY,50,PEN UZ,50,PEN EC,50,PEN MM,50,PEN GE,50,PEN US,832,PHP CH,749,PHP AU,749,PHP GB,971,PHP DE,971,PHP DK,777,PHP AT,666,PHP NO,666,PHP CA,777,PHP NL,777,PHP SE,777,PHP IE,610,PHP BE,610,PHP FI,610,PHP FR,777,PHP NZ,666,PHP HK,333,PHP CZ,666,PHP SK,610,PHP AE,777,PHP SG,777,PHP IL,777,PHP KW,361,PHP IT,749,PHP JP,971,PHP ES,749,PHP VE,749,PHP HU,610,PHP PT,666,PHP GR,666,PHP KR,333,PHP TW,666,PHP PL,305,PHP ZM,749,PHP PR,749,PHP CI,749,PHP RO,610,PHP BH,361,PHP PA,749,PHP GH,749,PHP KH,749,PHP SN,749,PHP SA,361,PHP RU,777,PHP UG,749,PHP BR,305,PHP NI,749,PHP MG,749,PHP DO,749,PHP MX,333,PHP OM,361,PHP CM,749,PHP ZA,749,PHP CL,749,PHP KE,749,PHP NP,749,PHP PE,749,PHP UA,749,PHP MZ,749,PHP TZ,749,PHP AO,749,PHP JO,361,PHP ZW,749,PHP TR,610,PHP MU,749,PHP SV,749,PHP TH,333,PHP CO,749,PHP LK,749,PHP CR,749,PHP AR,361,PHP MA,361,PHP MY,749,PHP KZ,749,PHP DZ,361,PHP ID,361,PHP NG,749,PHP GT,749,PHP VN,305,PHP BY,749,PHP EG,361,PHP UY,749,PHP BD,749,PHP PK,749,PHP PH,749,PHP ET,749,PHP TN,361,PHP LB,361,PHP IN,305,PHP PY,749,PHP UZ,749,PHP EC,749,PHP MM,749,PHP GE,749,PHP US,4272,PKR CH,3845,PKR AU,3845,PKR GB,4984,PKR DE,4984,PKR DK,3987,PKR AT,3418,PKR NO,3418,PKR CA,3987,PKR NL,3987,PKR SE,3987,PKR IE,3133,PKR BE,3133,PKR FI,3133,PKR FR,3987,PKR NZ,3418,PKR HK,1709,PKR CZ,3418,PKR SK,3133,PKR AE,3987,PKR SG,3987,PKR IL,3987,PKR KW,1851,PKR IT,3845,PKR JP,4984,PKR ES,3845,PKR VE,3845,PKR HU,3133,PKR PT,3418,PKR GR,3418,PKR KR,1709,PKR TW,3418,PKR PL,1566,PKR ZM,3845,PKR PR,3845,PKR CI,3845,PKR RO,3133,PKR BH,1851,PKR PA,3845,PKR GH,3845,PKR KH,3845,PKR SN,3845,PKR SA,1851,PKR RU,3987,PKR UG,3845,PKR BR,1566,PKR NI,3845,PKR MG,3845,PKR DO,3845,PKR MX,1709,PKR OM,1851,PKR CM,3845,PKR ZA,3845,PKR CL,3845,PKR KE,3845,PKR NP,3845,PKR PE,3845,PKR UA,3845,PKR MZ,3845,PKR TZ,3845,PKR AO,3845,PKR JO,1851,PKR ZW,3845,PKR TR,3133,PKR MU,3845,PKR SV,3845,PKR TH,1709,PKR CO,3845,PKR LK,3845,PKR CR,3845,PKR AR,1851,PKR MA,1851,PKR MY,3845,PKR KZ,3845,PKR DZ,1851,PKR ID,1851,PKR NG,3845,PKR GT,3845,PKR VN,1566,PKR BY,3845,PKR EG,1851,PKR UY,3845,PKR BD,3845,PKR PK,3845,PKR PH,3845,PKR ET,3845,PKR TN,1851,PKR LB,1851,PKR IN,1566,PKR PY,3845,PKR UZ,3845,PKR EC,3845,PKR MM,3845,PKR GE,3845,PKR US,60,PLN CH,54,PLN AU,54,PLN GB,70,PLN DE,70,PLN DK,56,PLN AT,48,PLN NO,48,PLN CA,56,PLN NL,56,PLN SE,56,PLN IE,44,PLN BE,44,PLN FI,44,PLN FR,56,PLN NZ,48,PLN HK,24,PLN CZ,48,PLN SK,44,PLN AE,56,PLN SG,56,PLN IL,56,PLN KW,26,PLN IT,54,PLN JP,70,PLN ES,54,PLN VE,54,PLN HU,44,PLN PT,48,PLN GR,48,PLN KR,24,PLN TW,48,PLN PL,22,PLN ZM,54,PLN PR,54,PLN CI,54,PLN RO,44,PLN BH,26,PLN PA,54,PLN GH,54,PLN KH,54,PLN SN,54,PLN SA,26,PLN RU,56,PLN UG,54,PLN BR,22,PLN NI,54,PLN MG,54,PLN DO,54,PLN MX,24,PLN OM,26,PLN CM,54,PLN ZA,54,PLN CL,54,PLN KE,54,PLN NP,54,PLN PE,54,PLN UA,54,PLN MZ,54,PLN TZ,54,PLN AO,54,PLN JO,26,PLN ZW,54,PLN TR,44,PLN MU,54,PLN SV,54,PLN TH,24,PLN CO,54,PLN LK,54,PLN CR,54,PLN AR,26,PLN MA,26,PLN MY,54,PLN KZ,54,PLN DZ,26,PLN ID,26,PLN NG,54,PLN GT,54,PLN VN,22,PLN BY,54,PLN EG,26,PLN UY,54,PLN BD,54,PLN PK,54,PLN PH,54,PLN ET,54,PLN TN,26,PLN LB,26,PLN IN,22,PLN PY,54,PLN UZ,54,PLN EC,54,PLN MM,54,PLN GE,54,PLN US,69,RON CH,62,RON AU,62,RON GB,80,RON DE,80,RON DK,64,RON AT,55,RON NO,55,RON CA,64,RON NL,64,RON SE,64,RON IE,50,RON BE,50,RON FI,50,RON FR,64,RON NZ,55,RON HK,27,RON CZ,55,RON SK,50,RON AE,64,RON SG,64,RON IL,64,RON KW,30,RON IT,62,RON JP,80,RON ES,62,RON VE,62,RON HU,50,RON PT,55,RON GR,55,RON KR,27,RON TW,55,RON PL,25,RON ZM,62,RON PR,62,RON CI,62,RON RO,50,RON BH,30,RON PA,62,RON GH,62,RON KH,62,RON SN,62,RON SA,30,RON RU,64,RON UG,62,RON BR,25,RON NI,62,RON MG,62,RON DO,62,RON MX,27,RON OM,30,RON CM,62,RON ZA,62,RON CL,62,RON KE,62,RON NP,62,RON PE,62,RON UA,62,RON MZ,62,RON TZ,62,RON AO,62,RON JO,30,RON ZW,62,RON TR,50,RON MU,62,RON SV,62,RON TH,27,RON CO,62,RON LK,62,RON CR,62,RON AR,30,RON MA,30,RON MY,62,RON KZ,62,RON DZ,30,RON ID,30,RON NG,62,RON GT,62,RON VN,25,RON BY,62,RON EG,30,RON UY,62,RON BD,62,RON PK,62,RON PH,62,RON ET,62,RON TN,30,RON LB,30,RON IN,25,RON PY,62,RON UZ,62,RON EC,62,RON MM,62,RON GE,62,RON US,1612,RSD CH,1451,RSD AU,1451,RSD GB,1880,RSD DE,1880,RSD DK,1504,RSD AT,1289,RSD NO,1289,RSD CA,1504,RSD NL,1504,RSD SE,1504,RSD IE,1182,RSD BE,1182,RSD FI,1182,RSD FR,1504,RSD NZ,1289,RSD HK,645,RSD CZ,1289,RSD SK,1182,RSD AE,1504,RSD SG,1504,RSD IL,1504,RSD KW,698,RSD IT,1451,RSD JP,1880,RSD ES,1451,RSD VE,1451,RSD HU,1182,RSD PT,1289,RSD GR,1289,RSD KR,645,RSD TW,1289,RSD PL,591,RSD ZM,1451,RSD PR,1451,RSD CI,1451,RSD RO,1182,RSD BH,698,RSD PA,1451,RSD GH,1451,RSD KH,1451,RSD SN,1451,RSD SA,698,RSD RU,1504,RSD UG,1451,RSD BR,591,RSD NI,1451,RSD MG,1451,RSD DO,1451,RSD MX,645,RSD OM,698,RSD CM,1451,RSD ZA,1451,RSD CL,1451,RSD KE,1451,RSD NP,1451,RSD PE,1451,RSD UA,1451,RSD MZ,1451,RSD TZ,1451,RSD AO,1451,RSD JO,698,RSD ZW,1451,RSD TR,1182,RSD MU,1451,RSD SV,1451,RSD TH,645,RSD CO,1451,RSD LK,1451,RSD CR,1451,RSD AR,698,RSD MA,698,RSD MY,1451,RSD KZ,1451,RSD DZ,698,RSD ID,698,RSD NG,1451,RSD GT,1451,RSD VN,591,RSD BY,1451,RSD EG,698,RSD UY,1451,RSD BD,1451,RSD PK,1451,RSD PH,1451,RSD ET,1451,RSD TN,698,RSD LB,698,RSD IN,591,RSD PY,1451,RSD UZ,1451,RSD EC,1451,RSD MM,1451,RSD GE,1451,RSD US,1326,RUB CH,1193,RUB AU,1193,RUB GB,1547,RUB DE,1547,RUB DK,1238,RUB AT,1061,RUB NO,1061,RUB CA,1238,RUB NL,1238,RUB SE,1238,RUB IE,972,RUB BE,972,RUB FI,972,RUB FR,1238,RUB NZ,1061,RUB HK,530,RUB CZ,1061,RUB SK,972,RUB AE,1238,RUB SG,1238,RUB IL,1238,RUB KW,575,RUB IT,1193,RUB JP,1547,RUB ES,1193,RUB VE,1193,RUB HU,972,RUB PT,1061,RUB GR,1061,RUB KR,530,RUB TW,1061,RUB PL,486,RUB ZM,1193,RUB PR,1193,RUB CI,1193,RUB RO,972,RUB BH,575,RUB PA,1193,RUB GH,1193,RUB KH,1193,RUB SN,1193,RUB SA,575,RUB RU,1238,RUB UG,1193,RUB BR,486,RUB NI,1193,RUB MG,1193,RUB DO,1193,RUB MX,530,RUB OM,575,RUB CM,1193,RUB ZA,1193,RUB CL,1193,RUB KE,1193,RUB NP,1193,RUB PE,1193,RUB UA,1193,RUB MZ,1193,RUB TZ,1193,RUB AO,1193,RUB JO,575,RUB ZW,1193,RUB TR,972,RUB MU,1193,RUB SV,1193,RUB TH,530,RUB CO,1193,RUB LK,1193,RUB CR,1193,RUB AR,575,RUB MA,575,RUB MY,1193,RUB KZ,1193,RUB DZ,575,RUB ID,575,RUB NG,1193,RUB GT,1193,RUB VN,486,RUB BY,1193,RUB EG,575,RUB UY,1193,RUB BD,1193,RUB PK,1193,RUB PH,1193,RUB ET,1193,RUB TN,575,RUB LB,575,RUB IN,486,RUB PY,1193,RUB UZ,1193,RUB EC,1193,RUB MM,1193,RUB GE,1193,RUB US,56,SAR CH,51,SAR AU,51,SAR GB,66,SAR DE,66,SAR DK,53,SAR AT,45,SAR NO,45,SAR CA,53,SAR NL,53,SAR SE,53,SAR IE,41,SAR BE,41,SAR FI,41,SAR FR,53,SAR NZ,45,SAR HK,23,SAR CZ,45,SAR SK,41,SAR AE,53,SAR SG,53,SAR IL,53,SAR KW,24,SAR IT,51,SAR JP,66,SAR ES,51,SAR VE,51,SAR HU,41,SAR PT,45,SAR GR,45,SAR KR,23,SAR TW,45,SAR PL,21,SAR ZM,51,SAR PR,51,SAR CI,51,SAR RO,41,SAR BH,24,SAR PA,51,SAR GH,51,SAR KH,51,SAR SN,51,SAR SA,24,SAR RU,53,SAR UG,51,SAR BR,21,SAR NI,51,SAR MG,51,SAR DO,51,SAR MX,23,SAR OM,24,SAR CM,51,SAR ZA,51,SAR CL,51,SAR KE,51,SAR NP,51,SAR PE,51,SAR UA,51,SAR MZ,51,SAR TZ,51,SAR AO,51,SAR JO,24,SAR ZW,51,SAR TR,41,SAR MU,51,SAR SV,51,SAR TH,23,SAR CO,51,SAR LK,51,SAR CR,51,SAR AR,24,SAR MA,24,SAR MY,51,SAR KZ,51,SAR DZ,24,SAR ID,24,SAR NG,51,SAR GT,51,SAR VN,21,SAR BY,51,SAR EG,24,SAR UY,51,SAR BD,51,SAR PK,51,SAR PH,51,SAR ET,51,SAR TN,24,SAR LB,24,SAR IN,21,SAR PY,51,SAR UZ,51,SAR EC,51,SAR MM,51,SAR GE,51,SAR US,157,SEK CH,142,SEK AU,142,SEK GB,183,SEK DE,183,SEK DK,147,SEK AT,126,SEK NO,126,SEK CA,147,SEK NL,147,SEK SE,147,SEK IE,115,SEK BE,115,SEK FI,115,SEK FR,147,SEK NZ,126,SEK HK,63,SEK CZ,126,SEK SK,115,SEK AE,147,SEK SG,147,SEK IL,147,SEK KW,68,SEK IT,142,SEK JP,183,SEK ES,142,SEK VE,142,SEK HU,115,SEK PT,126,SEK GR,126,SEK KR,63,SEK TW,126,SEK PL,58,SEK ZM,142,SEK PR,142,SEK CI,142,SEK RO,115,SEK BH,68,SEK PA,142,SEK GH,142,SEK KH,142,SEK SN,142,SEK SA,68,SEK RU,147,SEK UG,142,SEK BR,58,SEK NI,142,SEK MG,142,SEK DO,142,SEK MX,63,SEK OM,68,SEK CM,142,SEK ZA,142,SEK CL,142,SEK KE,142,SEK NP,142,SEK PE,142,SEK UA,142,SEK MZ,142,SEK TZ,142,SEK AO,142,SEK JO,68,SEK ZW,142,SEK TR,115,SEK MU,142,SEK SV,142,SEK TH,63,SEK CO,142,SEK LK,142,SEK CR,142,SEK AR,68,SEK MA,68,SEK MY,142,SEK KZ,142,SEK DZ,68,SEK ID,68,SEK NG,142,SEK GT,142,SEK VN,58,SEK BY,142,SEK EG,68,SEK UY,142,SEK BD,142,SEK PK,142,SEK PH,142,SEK ET,142,SEK TN,68,SEK LB,68,SEK IN,58,SEK PY,142,SEK UZ,142,SEK EC,142,SEK MM,142,SEK GE,142,SEK US,20,SGD CH,18,SGD AU,18,SGD GB,23,SGD DE,23,SGD DK,19,SGD AT,16,SGD NO,16,SGD CA,19,SGD NL,19,SGD SE,19,SGD IE,15,SGD BE,15,SGD FI,15,SGD FR,19,SGD NZ,16,SGD HK,8,SGD CZ,16,SGD SK,15,SGD AE,19,SGD SG,19,SGD IL,19,SGD KW,9,SGD IT,18,SGD JP,23,SGD ES,18,SGD VE,18,SGD HU,15,SGD PT,16,SGD GR,16,SGD KR,8,SGD TW,16,SGD PL,7,SGD ZM,18,SGD PR,18,SGD CI,18,SGD RO,15,SGD BH,9,SGD PA,18,SGD GH,18,SGD KH,18,SGD SN,18,SGD SA,9,SGD RU,19,SGD UG,18,SGD BR,7,SGD NI,18,SGD MG,18,SGD DO,18,SGD MX,8,SGD OM,9,SGD CM,18,SGD ZA,18,SGD CL,18,SGD KE,18,SGD NP,18,SGD PE,18,SGD UA,18,SGD MZ,18,SGD TZ,18,SGD AO,18,SGD JO,9,SGD ZW,18,SGD TR,15,SGD MU,18,SGD SV,18,SGD TH,8,SGD CO,18,SGD LK,18,SGD CR,18,SGD AR,9,SGD MA,9,SGD MY,18,SGD KZ,18,SGD DZ,9,SGD ID,9,SGD NG,18,SGD GT,18,SGD VN,7,SGD BY,18,SGD EG,9,SGD UY,18,SGD BD,18,SGD PK,18,SGD PH,18,SGD ET,18,SGD TN,9,SGD LB,9,SGD IN,7,SGD PY,18,SGD UZ,18,SGD EC,18,SGD MM,18,SGD GE,18,SGD US,529,THB CH,476,THB AU,476,THB GB,617,THB DE,617,THB DK,494,THB AT,423,THB NO,423,THB CA,494,THB NL,494,THB SE,494,THB IE,388,THB BE,388,THB FI,388,THB FR,494,THB NZ,423,THB HK,212,THB CZ,423,THB SK,388,THB AE,494,THB SG,494,THB IL,494,THB KW,229,THB IT,476,THB JP,617,THB ES,476,THB VE,476,THB HU,388,THB PT,423,THB GR,423,THB KR,212,THB TW,423,THB PL,194,THB ZM,476,THB PR,476,THB CI,476,THB RO,388,THB BH,229,THB PA,476,THB GH,476,THB KH,476,THB SN,476,THB SA,229,THB RU,494,THB UG,476,THB BR,194,THB NI,476,THB MG,476,THB DO,476,THB MX,212,THB OM,229,THB CM,476,THB ZA,476,THB CL,476,THB KE,476,THB NP,476,THB PE,476,THB UA,476,THB MZ,476,THB TZ,476,THB AO,476,THB JO,229,THB ZW,476,THB TR,388,THB MU,476,THB SV,476,THB TH,212,THB CO,476,THB LK,476,THB CR,476,THB AR,229,THB MA,229,THB MY,476,THB KZ,476,THB DZ,229,THB ID,229,THB NG,476,THB GT,476,THB VN,194,THB BY,476,THB EG,229,THB UY,476,THB BD,476,THB PK,476,THB PH,476,THB ET,476,THB TN,229,THB LB,229,THB IN,194,THB PY,476,THB UZ,476,THB EC,476,THB MM,476,THB GE,476,THB US,433,TRY CH,389,TRY AU,389,TRY GB,505,TRY DE,505,TRY DK,404,TRY AT,346,TRY NO,346,TRY CA,404,TRY NL,404,TRY SE,404,TRY IE,317,TRY BE,317,TRY FI,317,TRY FR,404,TRY NZ,346,TRY HK,173,TRY CZ,346,TRY SK,317,TRY AE,404,TRY SG,404,TRY IL,404,TRY KW,187,TRY IT,389,TRY JP,505,TRY ES,389,TRY VE,389,TRY HU,317,TRY PT,346,TRY GR,346,TRY KR,173,TRY TW,346,TRY PL,159,TRY ZM,389,TRY PR,389,TRY CI,389,TRY RO,317,TRY BH,187,TRY PA,389,TRY GH,389,TRY KH,389,TRY SN,389,TRY SA,187,TRY RU,404,TRY UG,389,TRY BR,159,TRY NI,389,TRY MG,389,TRY DO,389,TRY MX,173,TRY OM,187,TRY CM,389,TRY ZA,389,TRY CL,389,TRY KE,389,TRY NP,389,TRY PE,389,TRY UA,389,TRY MZ,389,TRY TZ,389,TRY AO,389,TRY JO,187,TRY ZW,389,TRY TR,317,TRY MU,389,TRY SV,389,TRY TH,173,TRY CO,389,TRY LK,389,TRY CR,389,TRY AR,187,TRY MA,187,TRY MY,389,TRY KZ,389,TRY DZ,187,TRY ID,187,TRY NG,389,TRY GT,389,TRY VN,159,TRY BY,389,TRY EG,187,TRY UY,389,TRY BD,389,TRY PK,389,TRY PH,389,TRY ET,389,TRY TN,187,TRY LB,187,TRY IN,159,TRY PY,389,TRY UZ,389,TRY EC,389,TRY MM,389,TRY GE,389,TRY US,475,TWD CH,427,TWD AU,427,TWD GB,554,TWD DE,554,TWD DK,443,TWD AT,380,TWD NO,380,TWD CA,443,TWD NL,443,TWD SE,443,TWD IE,348,TWD BE,348,TWD FI,348,TWD FR,443,TWD NZ,380,TWD HK,190,TWD CZ,380,TWD SK,348,TWD AE,443,TWD SG,443,TWD IL,443,TWD KW,206,TWD IT,427,TWD JP,554,TWD ES,427,TWD VE,427,TWD HU,348,TWD PT,380,TWD GR,380,TWD KR,190,TWD TW,380,TWD PL,174,TWD ZM,427,TWD PR,427,TWD CI,427,TWD RO,348,TWD BH,206,TWD PA,427,TWD GH,427,TWD KH,427,TWD SN,427,TWD SA,206,TWD RU,443,TWD UG,427,TWD BR,174,TWD NI,427,TWD MG,427,TWD DO,427,TWD MX,190,TWD OM,206,TWD CM,427,TWD ZA,427,TWD CL,427,TWD KE,427,TWD NP,427,TWD PE,427,TWD UA,427,TWD MZ,427,TWD TZ,427,TWD AO,427,TWD JO,206,TWD ZW,427,TWD TR,348,TWD MU,427,TWD SV,427,TWD TH,190,TWD CO,427,TWD LK,427,TWD CR,427,TWD AR,206,TWD MA,206,TWD MY,427,TWD KZ,427,TWD DZ,206,TWD ID,206,TWD NG,427,TWD GT,427,TWD VN,174,TWD BY,427,TWD EG,206,TWD UY,427,TWD BD,427,TWD PK,427,TWD PH,427,TWD ET,427,TWD TN,206,TWD LB,206,TWD IN,174,TWD PY,427,TWD UZ,427,TWD EC,427,TWD MM,427,TWD GE,427,TWD US,541,UAH CH,487,UAH AU,487,UAH GB,631,UAH DE,631,UAH DK,505,UAH AT,432,UAH NO,432,UAH CA,505,UAH NL,505,UAH SE,505,UAH IE,396,UAH BE,396,UAH FI,396,UAH FR,505,UAH NZ,432,UAH HK,216,UAH CZ,432,UAH SK,396,UAH AE,505,UAH SG,505,UAH IL,505,UAH KW,234,UAH IT,487,UAH JP,631,UAH ES,487,UAH VE,487,UAH HU,396,UAH PT,432,UAH GR,432,UAH KR,216,UAH TW,432,UAH PL,198,UAH ZM,487,UAH PR,487,UAH CI,487,UAH RO,396,UAH BH,234,UAH PA,487,UAH GH,487,UAH KH,487,UAH SN,487,UAH SA,234,UAH RU,505,UAH UG,487,UAH BR,198,UAH NI,487,UAH MG,487,UAH DO,487,UAH MX,216,UAH OM,234,UAH CM,487,UAH ZA,487,UAH CL,487,UAH KE,487,UAH NP,487,UAH PE,487,UAH UA,487,UAH MZ,487,UAH TZ,487,UAH AO,487,UAH JO,234,UAH ZW,487,UAH TR,396,UAH MU,487,UAH SV,487,UAH TH,216,UAH CO,487,UAH LK,487,UAH CR,487,UAH AR,234,UAH MA,234,UAH MY,487,UAH KZ,487,UAH DZ,234,UAH ID,234,UAH NG,487,UAH GT,487,UAH VN,198,UAH BY,487,UAH EG,234,UAH UY,487,UAH BD,487,UAH PK,487,UAH PH,487,UAH ET,487,UAH TN,234,UAH LB,234,UAH IN,198,UAH PY,487,UAH UZ,487,UAH EC,487,UAH MM,487,UAH GE,487,UAH US,15,USD CH,13.5,USD AU,13.5,USD GB,17.5,USD DE,17.5,USD DK,14,USD AT,12,USD NO,12,USD CA,14,USD NL,14,USD SE,14,USD IE,11,USD BE,11,USD FI,11,USD FR,14,USD NZ,12,USD HK,6,USD CZ,12,USD SK,11,USD AE,14,USD SG,14,USD IL,14,USD KW,6.5,USD IT,13.5,USD JP,17.5,USD ES,13.5,USD VE,13.5,USD HU,11,USD PT,12,USD GR,12,USD KR,6,USD TW,12,USD PL,5.5,USD ZM,13.5,USD PR,13.5,USD CI,13.5,USD RO,11,USD BH,6.5,USD PA,13.5,USD GH,13.5,USD KH,13.5,USD SN,13.5,USD SA,6.5,USD RU,14,USD UG,13.5,USD BR,5.5,USD NI,13.5,USD MG,13.5,USD DO,13.5,USD MX,6,USD OM,6.5,USD CM,13.5,USD ZA,13.5,USD CL,13.5,USD KE,13.5,USD NP,13.5,USD PE,13.5,USD UA,13.5,USD MZ,13.5,USD TZ,13.5,USD AO,13.5,USD JO,6.5,USD ZW,13.5,USD TR,11,USD MU,13.5,USD SV,13.5,USD TH,6,USD CO,13.5,USD LK,13.5,USD CR,13.5,USD AR,6.5,USD MA,6.5,USD MY,13.5,USD KZ,13.5,USD DZ,6.5,USD ID,6.5,USD NG,13.5,USD GT,13.5,USD VN,5.5,USD BY,13.5,USD EG,6.5,USD UY,13.5,USD BD,13.5,USD PK,13.5,USD PH,13.5,USD ET,13.5,USD TN,6.5,USD LB,6.5,USD IN,5.5,USD PY,13.5,USD UZ,13.5,USD EC,13.5,USD MM,13.5,USD GE,13.5,USD US,363225,VND CH,326903,VND AU,326903,VND GB,423763,VND DE,423763,VND DK,339010,VND AT,290580,VND NO,290580,VND CA,339010,VND NL,339010,VND SE,339010,VND IE,266365,VND BE,266365,VND FI,266365,VND FR,339010,VND NZ,290580,VND HK,145290,VND CZ,290580,VND SK,266365,VND AE,339010,VND SG,339010,VND IL,339010,VND KW,157398,VND IT,326903,VND JP,423763,VND ES,326903,VND VE,326903,VND HU,266365,VND PT,290580,VND GR,290580,VND KR,145290,VND TW,290580,VND PL,133183,VND ZM,326903,VND PR,326903,VND CI,326903,VND RO,266365,VND BH,157398,VND PA,326903,VND GH,326903,VND KH,326903,VND SN,326903,VND SA,157398,VND RU,339010,VND UG,326903,VND BR,133183,VND NI,326903,VND MG,326903,VND DO,326903,VND MX,145290,VND OM,157398,VND CM,326903,VND ZA,326903,VND CL,326903,VND KE,326903,VND NP,326903,VND PE,326903,VND UA,326903,VND MZ,326903,VND TZ,326903,VND AO,326903,VND JO,157398,VND ZW,326903,VND TR,266365,VND MU,326903,VND SV,326903,VND TH,145290,VND CO,326903,VND LK,326903,VND CR,326903,VND AR,157398,VND MA,157398,VND MY,326903,VND KZ,326903,VND DZ,157398,VND ID,157398,VND NG,326903,VND GT,326903,VND VN,133183,VND BY,326903,VND EG,157398,VND UY,326903,VND BD,326903,VND PK,326903,VND PH,326903,VND ET,326903,VND TN,157398,VND LB,157398,VND IN,133183,VND PY,326903,VND UZ,326903,VND EC,326903,VND MM,326903,VND GE,326903,VND US,283,ZAR CH,255,ZAR AU,255,ZAR GB,330,ZAR DE,330,ZAR DK,264,ZAR AT,227,ZAR NO,227,ZAR CA,264,ZAR NL,264,ZAR SE,264,ZAR IE,208,ZAR BE,208,ZAR FI,208,ZAR FR,264,ZAR NZ,227,ZAR HK,113,ZAR CZ,227,ZAR SK,208,ZAR AE,264,ZAR SG,264,ZAR IL,264,ZAR KW,123,ZAR IT,255,ZAR JP,330,ZAR ES,255,ZAR VE,255,ZAR HU,208,ZAR PT,227,ZAR GR,227,ZAR KR,113,ZAR TW,227,ZAR PL,104,ZAR ZM,255,ZAR PR,255,ZAR CI,255,ZAR RO,208,ZAR BH,123,ZAR PA,255,ZAR GH,255,ZAR KH,255,ZAR SN,255,ZAR SA,123,ZAR RU,264,ZAR UG,255,ZAR BR,104,ZAR NI,255,ZAR MG,255,ZAR DO,255,ZAR MX,113,ZAR OM,123,ZAR CM,255,ZAR ZA,255,ZAR CL,255,ZAR KE,255,ZAR NP,255,ZAR PE,255,ZAR UA,255,ZAR MZ,255,ZAR TZ,255,ZAR AO,255,ZAR JO,123,ZAR ZW,255,ZAR TR,208,ZAR MU,255,ZAR SV,255,ZAR TH,113,ZAR CO,255,ZAR LK,255,ZAR CR,255,ZAR AR,123,ZAR MA,123,ZAR MY,255,ZAR KZ,255,ZAR DZ,123,ZAR ID,123,ZAR NG,255,ZAR GT,255,ZAR VN,104,ZAR BY,255,ZAR EG,123,ZAR UY,255,ZAR BD,255,ZAR PK,255,ZAR PH,255,ZAR ET,255,ZAR TN,123,ZAR LB,123,ZAR IN,104,ZAR PY,255,ZAR UZ,255,ZAR EC,255,ZAR MM,255,ZAR GE,255,ZARPK!IS,S,Vjs/build/images/js/src/images/campaign-preview/093d4a30c2447b174c17.map-background.pngnu[PNG  IHDROj pHYs  sRGBgAMA a+IDATx}r$vβ(GgՈ.CWA A>A3_ s掻i?WޘD̬4\'FDYgM\~ϻwD ޞ`0SX[[r<^7h@)_=(l欌YbvAz#۶DdYn7kk̢a rvQ_ѾtTy }R(Wm:V)ExX/S9;K 8[5vLT5 qYdY켟'LCFR&Oٕ8^T$6D Ug[> qrKJǞ|2>eUT\vk"<̯jh8C!FAf \.Up~Bv/S@X-SƚL 69B!fnv4GLFxlz>TQ [ENqlZ-,klNJu֫]8YC GԣuѭRN:Ã-JF{LS=jHᲗ>ĺsj*q+mdO:UMׇSPZTęN`V=j43L7cЩ#g]$jot'4d5jZ8Ȉ^ʕc2q"eE'q7:BFAӜGrCNL'N;麪c THnh4o&CڠI]>t.)ExWWWc8;=q&5ib NPC%HI8q VV0SIas8d TH'뗤yRŸDYF xIO09#Zõ'QRºu8^j2L&I)S@YNіH kJAPKа[U3 ORvJBögF,9'ƙX#>/ii34pMM܂=1Kҭp1L 7slڨ&f HnB8E#7ܒ $rK.MS+c&>itO`7q$Z2Nrn2$$JЀ3sU`0t(v-3Jm*OkpN^W3 _P֬ZX )n kzyylq$մGthQiW^4_Y2r>>zi]m iGؔD4Cw&$Q\,${!iRMjg;$:W ve#0.QqȞeY! $ΪtǀҒԌR@qd <8W*C"x*:-HRH YmrAT0Dw]8!G+ı*~X}$ұ"ߐFGS-;9-<$v&Qi͗D[GCjbA"jPX?qOImI*$I0+*v TưT;tD2Olcw$Sgmltѐ$.,q̚1UDYtF2HH[,<q&?e^X0 ](7&Ņ8$R)  Edy#yN`9낎硻74M ֬@Z4iz[ɾiqCz$qq pAi4ʱҁOoLKNJ &X:lyc*8Ev+i)BY"U;Ym$Zq6*ှ'=nj(YrFjdQ?4 qLOe"qb,#;, ΆMgP"]*'k屫t c BE'-[\S 3Դz=>e^<rpv,& qHMm:wԺ3Gj9cP~p dܫC|)i̓嬼>f>80উl' /$e1 l2-H*tH EuVW~Am T̓ı K ADr$ҜH0%xwZիXR5p^-4Rb?3m&NjI(w8p$R]IH`D +{KZJ8P>08OhTF HQK$fGHhĕ@=6>ƤƽoZ6PѤTLBR#MHƆ!@!W"u7tRmryRjW՜ 7So=I\,W|Jw$}o"1DU4L4پIA,..:4u8 J^#mjTK3oDIuC7F|84oǼ4ϑJ 5m` IMV'2$UTMqk27cV7tw RHmCRդq즞^I%'Hv5̦7-|lbFAB "19ys%1+Hdm]$qp5qe1oi^&'%M8姍5qPC/2UMN8:t@՚0pMrl9b"LNfTzV ai5qN3MN2\*Ut@UOs!Zfozߘf}EL3Hk?o–8E3ZE J:(AVx 0$eSL}&RJYd"g5t DYt)rQqt qVDi=n6>܅VP&KzȂu֋:IZY)i,q1 mL,$5-%N8Y5 ڙ\ P36N3t@5"0s3퓮ݴ^Ԡ$#itb.JZMK@Oa-Mmz|||nOZ=\I@$z8T X:oq'/];SL8oFwf51i@LDQڴCXL>epޞs0!{D⃊462Jq>bLLL*GIvDӌNH%NB՜-xיlH4id Pб:y\{ /RI'#~eD@M3ɐzD ^ǬDk}T TV$qkL$N*ms[84dxA&[U&s ɼa])RL$N. ؅ΛͽTL2`wzqm/7xkz;Zm)81U`i  pF2A-#6\.;'$Oj?YL*|dBb|%CJI+ Ǎ ^[*҇Iğ0l+x۵=J8DU ^< 7|R2h~^l_O7h x`<\n%A@(|LD87ɰn6jp Kn|XKKî9Ht'E[āˢ5  ƐRT(;ݸEKD'ȃ lvO^Z\\tq{r6tuE>S.-yxs/(@P֝ tCVn;Ɓh p $&>* 21j/qS/kq\zU8%) %ԡlAՎ7qM+>GS> F T·Q{m@tĠY훘k$m p^Q/n:X}t{n8O?84y-!cv>0 7O?`  KPkG@T_ 3tmTLL!~ ƻ^1m-~ø&,?RZu>2ҏ,)An⥙!iq7 ]Y"j^$4S4=␥)j{ia4)}Ԉpm80|`-ȄMţ,[[d7 L'(ZF?J&T;1&vqgm0n{RT+j=oҳ}Ef ۡ`*'J6qbgxbZrBlve9-nUIdž{uH&H%N6io}rCa~[T9C89&PܪscGqH$qǻ;,m_(&}2vVaq9;bCqrRi81Aߜu.?NwcgA{2i1hC QrrIt#qbo}PmRsWܢB;aU J 7Ӫi^vX9!2Vv铹YXN*fm!q8QAp ,Cl襊 a-gR吡P}}#I2n$eH~J(!msIO Wg=UJ{䷾bLں"T#iinŀ0īOmtV/ `a+[dfܩ0푰p*ͼ%aLC{нNY.3Pxl4H^yti>bg3y /sv_}eILEFh@ߨtnOq[ruC{R 2'FL:)7*;.j pe=jrD ;^.v(N $]CP6ȥ]W!HҸbC?4_9 ^/OAa3*7:|CnNTM8  ҅Mf~G*ZP/JOinHB͠svjp@o ! A ݕL)Yo{Nwxb-C +B8\$q9yj6KS w*l,u;{秙<1dGj~prz:dmP(ycQoQEbhA{8R8,I}][%AsmKi$Ն^qy;\/:Wϩ/ح T8^Q*b^jLrơTCYJ tVM$BKs =%r #(ӭ}6Nݼ0w׆&F ɾqofI#j^qe$‚D^ 21ouR֐Dn[5M埩`TzCy}ԇa4WTV)iw!xvvv= jt`C^m-ª7y Bo eTE) #M1؎ݥg-(WgzRڡR&xB!džt(psRy}l9$ ?*Pg֮-eی=vl6b’6:ʙHyn2;xYyr^Fk@ OHtqq,{V O† mhǞLw5rJĆ"IjZWAZZXCACKق@ATID/ cJd~_zo`SF]߄q+!Im0UuA]HMųAm֋gX'H⮮͋]Û#s2 hJRKaRl# M!}žfu.D &!)Ҧ*ϚM'acK9X"h$gJ ם?tvjY P&N?"q ^)@ڠ):T1s`Ӻ d!<@q6P+X80 LK[gX|Ӻu2R1yc~ J -[V!@ƞ{T:%5}nDk#l~ݟOvˈv^mrJB'Qռlη69isɢ59?IvkK85?X-i4Դ$r V4O$d7CtՏ^%ɝTmgs"Ѷr(ڱ6R&y13#T=^cg%`oJSQMs*[ҢnoX69T0(tҹ^Zs !c 6ʒրk:>>v<}c /u`dN?Pb] v +~V΄o~ЋZZ_n26 9jqKxĆc%ѿ4~jQ`ל(tBCBqT/Z?( hv'IGc}úRӲdm#*N E!_ HiTpA^D既,/@qbV|to:W*ܟ^/#Pօ 4ih-xK|m3Es 9:S2a}_'0BzMz9vHTcK_J9IsnpCҁKLg J&Rjbƚ^ iz ^NjHCv?+Z%M۾A/u ?^xM q+%KpbL_z}k*7a`DGO WhX&l yH|+sIORSCLxXvR~lrl`v k+>dsQ\̓:|dΘH_wN^./ 9oP;P MH6,їJ 8&Hhṉo8St [mQiPX,É3rqqXi4[t6 j٘5Y0')qpPbb:p|$|}KB9Ǹ^T'⾃DA> 3L[z:Jh  :9"m^4QlnO=MմrIo6^D‚ƒDGFUgB8罪QIȞ+pPIAJerVqXmLm^?=üSp8zaxF;H8Oݒb0O7a[aZcТ6&O< LyjK'<+&i7Z-qN>P)7Qo0'Ùlz'TsQ4^kVxmc_cܤ6}Zz0傇g{ t"X Xp1O QKɶNqyۄY.8:mKM\VaPƭ]Tv*;.Gk:\JNG)qdA[ v o{;eoLw(^H:)ܘ=)K%^qxzO6m;u80,𢦙P4`2 i!x/[u{/l;vԜ}ι X칐hToI)?-6R?N|IޡB/ZnGz>' hsg1;DM <0V5)ØeH-By^sKšجRΎc86}UD(yuadi^M"CkYǷX9 nY9aMUVP)DFCc w&LPܿGi`x-z:t?Ծw-=Ծtpf[ 0>auuUt"*6]OZN'R4ski>4'u lʼVdk8nt%ښ]|y^)K V] ܾ"IDޓK˃Q@ΊDև 匈 &II t½φ]n+Zf<}q;\M(deM4Lf#.\B*C,2,PdGN`z?0tuCQT|K"8@D2d*0!8"TUw 2mz0[k9N0r! Vp<oxӂof M6ljzlUux:BTi?¬¬¬¬¬}k<l-䴦“Zlx !`hR*I;!3ǿ*eIENDB`PK!Jww\js/build/images/js/src/images/campaign-preview/53f7ebba3e0e05545002.google-shopping-logo.svgnu[ PK!}  Tjs/build/images/js/src/images/campaign-preview/6ad8e32cee58c14f05a5.youtube-logo.svgnu[ PK!iD]"]"\js/build/images/js/src/images/campaign-preview/8955ab13b4b35353af90.product-sample-image.jpgnu[JFIFHHExifMM*JR(iZHHv8Photoshop 3.08BIM8BIM%ُ B~v" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzC   """""""""))))))000006666666666C   8&&88888888888888888888888888888888888888888888888888( ?( (((((((((((((((((((((((((((((((((((((((( (((((((((((((((((((((((((((((((((((((((( (((((((((((((((((((((((((((((((((((((((( (((((((((((((((((((((((((((((((((((((((( ((((((((((((F`,p(h$T'ggv u?X.J (ТY\eNih(((((cȩdU`p* 6AdQTBjsq8@FEQEQEQEQEQEQEQEQEQEQEQEQEQEQE( ((((((((((k:PFX{).Hjs$zcC>*9 u!pjR9CSِTV2:Ծ#T=Rh\FYB:Pբ7H;w'#Fd4l4hҞHh]GR*&AӚOZwn nz0PCe8wҜܓH0) 5?8VzJ6^PȢ38_\tct"*r)h((((((((((((( (((((((((8cϠUf;!8* 2\>_U'֛E!Eb(i()ȠeÏjdF5FT7Q@8ãwH;ʀ.`Qj(?(_@0*/G֣͗ xUO2oQQCտ!@) "s݉i@P5'UiSSGA=Wh!4 i)(uIv)1@!i*)1@ *r)Վ S8>j;8^#K@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@( ((((((((3Mu>]Mϑ*p0:P0'LsMRb@zZ)q@bKAӿ)^yNriޚh3KJ( JJw4fZZi$N4=8n{ 3@[ZJiJ(ZJZ(:PqIOQNF=+F9AHm@TqH%]¤ (((((((((( (((((((CH݅KXWsOȧSހ A8jjCIr)MGKN1@;LPKI(E0NWO@ WVb[S>ub"}i੠4m47 6@.>!8[OZ@sI/M4P91@RF)h(.()qINO [qR -v5T]Fܹ!QEQEQEQEQEQEQEQEQE( (((((((ԾTGo< čq.j{h3(h)i)q@Q@ KEQERR@Q@PE(Z( Z(((P( &9qM^9@ө#llW*X 袊b ( ( ( ( ( ( (?( (((((dй@n2Ļu4Wo [JTQ>yy\3ĵf3ǥ!IAQK@Q@-P@((( Z1EQEPERQ@-PIҊZ)qINZctI@SH(JSH(RG#'J(0u:F[պb ( ( ( ( ( ( (?( (((((˙|<\ʏ#VZ wJN*vU~&Ǩ֢ZhQO=)O XN d24KHzE-QE-Q@Q@Q@Q@ E%-RQ@Q@-PEPG4QE-fPҎ !h@PhRPVa|(%aڀ/QH0w ((((((( ((((y67 SIMW@}!sڣiEH2#ޥS@ h=( LnZ)iZ))hZJ(()҃@ (PRQ-QG4QE-%PI֖~CON(SM-ZP_izY\=E1QEQEQEQEQEQE( ((((c+ s#u?y/^Z91┞)4x x Jj:hQH)E 8 ǭ(MnQEQE%:N JZJJZJ(i)i(Q@u-%)(ii(( ZJZ(g!nS4S>Nh4RPSN;P JUT* 4QEQEQEQEQE( (((SyIjwu AY,R]Ҁ+\R">) (hPZjje E)x O⣗x ZAK@ INZu6(QFh ))hi(I@өZZm(QE%- ZJ(hRQ@ )›J(▐uP֣5$P1 TCPZ)i(ӳM$M0Ӊ4$05sY#O O QE1Q@Q@Q@Q@( (((:U'V 23;4:zSY{E9N9"ȳF)Fg֞AE:qڐ7H:Ԫj!R-J)›@<Y~<$!QH:RSM:h J(ii(E%RHii 4(4$ӨE-6(▒ m:@-%(JZNKMS4<n?QU{@CR*1RKH)@ZC@i3IRZjy=*`8y4M)D$,vB7^$lLrhJ5-"QKLAEPEPEPEP( (((ee\ m+Ve~ o ʸ(4XuXoă""Ni F=@sV.TjkhA$䚒F77& QEQEQEQEQEQEQEQE( (((((((aýk$A"4ĴLJop3]sZ[f}Т)((((((((((( ((((((((VjJ_f9zUU!V!1TmҀi jQQ-J(RQڀM4@-%--!4ڌ醀jAQJZCM4ZQM ZCKM4La*:xp u:(ZJ)(өL ~iKHhJ(Oo=֢)((((((((((( (((((((([VjP4}OPGҦ1QJ&4֤R-J):hCMm4ii pS>)@ EQIERPM4JJ) )Lȩm**uPSQEQEQEQEQEQEQEQEQEQE( (((((((([jUPC@j8 TFcQ"u"MZ(iƙ@RRPM00{GOxKIE-PQEQ@ IJi$QO1Hz}:t?nP:QLAEPEPEPEPEPEPEPEPEPEP( ((((((((7պ}Rtjt4FƣNcM@JR/JS0SRQ@iiK@ IKI@iiޤ}b)iZb ( ( ( ( ( ( ( ( ( (?( ((((((((CW?hVvPҘi623IA@ p P:)P)())M% Z(4i@ =i=i*AQxRQ@ E%RQ@CK@ 4 4PE-6t}~tRSu1Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@( ((((((((Gִ3QZڣjۭ! ZQ@ )*EZAK@ EPm8{ҊJZZCKHhO4@8S[8PH*1R(Q@ EFi Pj>ҘhMCJx}i * S#LAEPEPEPEPEPEPEPEPEPEPPK!مRjs/build/images/js/src/images/campaign-preview/ccb636afd5f179a2a92c.gmail-logo.svgnu[ PK!#_js/build/images/js/src/images/campaign-preview/e398c276792a4a469b92.ad-corner-buttons-image.svgnu[ PK!tsLjs/build/images/js/src/images/get-started/1ad852c48821e91bfd6e.img-quote.svgnu[ PK!1Pjs/build/images/js/src/images/get-started/3905a197de7922d5b82a.img-dashboard.svgnu[ PK!9800Tjs/build/images/js/src/images/get-started/563fd40c029bebb36783.img-free-listings.svgnu[ PK!iGjs/build/images/js/src/images/get-started/75da8376b1a005182ed2.hero.pngnu[PNG  IHDR )&" pHYs.#.#x?v IDATxܿk񯎴P)ʔK}v$-;\ZZLW.6g~˷ϝ?jc2=[.U#m9u詑yoISSs3Rub2w7.vt»-5U.U'KF+`hxחG/^}\䐚^(9^X[?Kw} zgs]SSvwr|zz[vʱަf'SFwWECÃ?phOoUjN*^[\uzt~ORS~f ^sSSӷ;ٞ d~lN;Qf:q7ƾW3<3TݽzLNx }YXZZOubc{Rqnf1677xD-_TXf2n^) @n]\qTu [ 䐁}y!o}vAjS}VK?ƭk_K}T-_TXVv =}iwS)S#'aaOowݫnLzjcm-55QZ~;8ߏu=}r4UdcDqjO65UK;{@f*rw괻?$23qN-]&SFzgOM,VJ@fLvsRSvw8$23e֟sRSF+IdfƝ羳Sj֧'+ 3Kr;;v /7t^xfj\.K{0 llwowxᙩR/KgwTTm6Z=@f&K>PfWkkTTm6Z=@f&K>&5W8*N'SSs3F/LMUJվ K*RVkw4kS*ӳ}@gxXjGڣO~LMz$23??*վ KNjR?.>2G>LMziajj~f11dѮԣ)GRSSK"XP*$I?5=diMcR-o($Iݷ:,_H~$K KZ$2oZIy/ cK i$2io$IRw?y/ ct%Iw~2XK5$I @P&Cyo {^{=iw’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’Kj ,)’kae ..vɯa/QPt,hXpNv$0Aa&HҖi|ٳŌBUv5^ru~_bIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbI<# IDATMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj @,)XRSbIMĒ%5Kj Οt+ᐚ _ȭHM0t>_0:R>;ٸr)zmt| 50z|{gO;ݕXv?#0RS O>׺3׺\XGtW?}ݑ?= NM*ˏ 3kp 15ɿ׍/՘Jh~3:5اO˝xLM Wjcy/cg><)xjG'_ N [X*Mm48+bj LoeHw_LMcP>_ufj 6X.1fSm淕Sm׷ufj ܏ŗזο*.n~J NLMoVc<˝oQ,.)x&.#})xbP,Tcep ej ~ŵ˵1jcO3#๘_49@_7tN[lh~MLM|J%]uOJ'vhp '15/wnST)hp `15`pv1޴K'So N:`{M6NM̍]HN t7) ahKLRS/Ml~];3ɆlNA`p `'35fymX}rTXeWupjqmupZ{)N#rl*bU,ο|7cZA҇Oaj ݹk㻇΄Y^[_|T$ɦ N`oVcc?>HT\|P:Q6=*LMKm˝ɫVf[[N!Sa15oz!\h8b:3oSWkLM+ýK3ӛlhLT7V7m7m<kK{Ou LN _ٍ-s t7׍)xU޿j̦ڳ\}lbn28hh~+ej ^k㻇΄$,sc|$Uiuq *S&_`35j56ƛZCbp֗OʦO78LMĥؗ;VUX28ul*Wn{)xBiuAld}B4X*˛R`ЩޏZ_`15wdCsXev7cNz|lp eWnymX}rTXevѩhhO:Nֿ`j a$\.Wc6՞MBӌN _ٍ-A3A1\z)s?w Ɏ517vg#JAK>w~CSxj56ƛZCcK˷|T$ɦO?Q6s7]_jc_DXMvw_K3[3o.053/.Vc4 t η4U.74ƛ}c[25uݯkcg7V]dtjO'oon~&:3_8)嵥biɱSauF/]o ;?) SSPo#rl*b]gbnۛ*.nyM;# S?߮U,.r$U4A*:]usN oVc*6bHw_L*˛s t=Йp~p6<V=`ymitjO'onxUEXg & , scjG'C7N _ٍGAM;l*Wzgj B3:ر'&{ط7?U;U+HvyZ LM@hB4SA vb=X*\w_K3ryAchO:Nֿ@}0]\dCsXe嵥o>T4ufz?0|`15aZ^[UwGVmtjOǒon~A6~ޏ\" ntjJL%TX*j˦rO%қR`W'`{E.D Wj]gj( o}L\|&~ad K<@dbnx4Qhbn,V{Hb$8DcAtfz;3- ax)Ѱ H$ĥvwa5y N ϱ~cfdeіdv{TG]uLMP,@&ƾH?ZDhO))~w6vfz aymK/ߍQXW.7DXgOUgT+j:?5`05;Wkca5aKS×ߕVy*Ӕ ృhvfz7Z,B a0:xa5זFtr7dv{_z`'* L5A?W_7:5|g]iuq˃x/wɆ:^[ [6ytj̥'זBį^O/ :OerA\Z]sT']'>VH7cgonyJξ@:^+ O4:5ܱ'JL'TX*ۊg7^2^M:~J7ģLoGۅ.vH$rհ[X/}wFC X^[XC1[ a_GPrPz{3Ѱ;O317Z–זF-oMG??&ؓa~_=t{ /ibnlbn,Ϧ7AKŸ= IDATVo~^׽#wD.|iu@-Hףmr7DC2q2\`j vѩ?Ǝ=a5aۍN _HAZ ֙=wTGTRX ? ۮX*\w_L(M*:'\ؽLMpmrm̦ړ aY^[cHiuq˛x4љ?[MuLM49φUWmbn/x\.oyNfξ@:vxg5:5ܱ'JL'TX*ۊWgymk$˝x3o|&:3y n:Vipۛ*f7dv{;ts=`']xsc=ǣJl7uO̍ۊ( #H/wLOciMk5fRڎZV7tߧ"H<8"ז4԰ H64?Xn+v嵥śwfƂXS"6l 1ԑM۱ľZbxmVe\||5vfz<b%v••dC7} &:3baxJ{*vjGoۇ]g}c-<ܸ+o,J+ 䁆Xg L*V\Kʼ39ΟOSSH\姥 /lf!V *a_qiyp5oG6AoXSbrq}cUZ[LM%M~V!e$-՘Ia{=Xwn45-iZAmI8񦟖:&أT"JN tţJL%TX*ۊX*T~hɽT"vr1?{mz!_ז@$Db^xQie|(mQhQ!ao{2w\M647ƛц-M-Gj;n:^Z^[sOxm ?7إ&ƺ[ox4?17n+ѩH$MZ; rùG' >K(2p^^[[cO??o =M^>wdCU,*gotO%[EXesje;х;wB$/-G"4^1६o%[OZLT=\;ƂX"k,mhI8&8ΟVB\.Wcfv ؍FkGEl*blp/br'cSu׳!vha*|njF`;,|}:~֗!---N G"l*:ж#ĶL%G|~BEaOSSm* x"&zGJ튰KbD7} 6ANf=tXWoLb^D"w\D"?-M|.ON6m:uߛCbym~٫T1޴YLoڎ4ז\vg=ZYTm617v8{4HWbNZT b\FهSsREaz|jjdg9IJ-ۍNy{r'LMD"܉73=-ۜ1זvǧcvuj*15]TX\.JSᶂSٜJ64ux۾_ݜZY4p΃6x==>5U,6ʿDX%{%`pرRg9&l7ufz;36x==>5D~~4MW>?i ]זH/ ^^esjtj8u-/mNzbjO7D"܉•:V^k_ l=lj=X*KH$bs jm155?VC߃\SS,-M|3[}rHbU9D"禆}uBS{ l=UFy`9H$vH$rڧxS&|pe>vvEcuH$Iz}זfgoťx)/[^ƣ -GZvuuds]m/ WێUoO/T x Wx;MTb*~bP,"H6jh b[^FX.}8>Qeh®T$YYo|V>+wգ_LMyͩ@|}竱c)UݜJ64ux۾ΧLcmV>RD̠Tl.[Y@ B\5595*~se|wsonӲڐ\fowmsU(YMSYp ?X;WM]8B5h蔯ӹh0eMhɥЀbil7(UugSZܘ|/|q<:RЖ;WM%⋙Ⱥ"G /ڝv:7S5xt$P]m=u5MfcG٠T9fڼINBkcɅxݹjJ19pZK3@cƣ#[U-;4Lm^ڦ$BDHa:vs?lM;6gޯnjOW'NDK}WnhWhJk6^4`m|Gb@'ҋ XՉTT5٫Lb*޾i~XJbcsH"dEjs1S#ŒőMxtd<:"p.w}gSl;+lM;6gr gYUSBtj-{mrÖN)"P$^X5+Q7L.[ֺ1sl$qBTyQt^qug0T5նŅFPrŅ뚚PQztDj(@"NǍUfch;+R]e[k66ֺo̦Kccy)_wsonR9:+N*2?{cj&#Ȋ$BD({{k[c\(GߠT9fڼIN&g+GN,jJ&[v8yH\/{rѳH>RhRh@P]m=u5Mfcmo6x<]Lf65=5\ENVP5^hژ({xt(H58Rש(JvP-V*iI"Pz׶ZO>a,Ѵ18KF _PHêt^qu7zsCE1\;(j4N]c Xp|ū#fcP}f{{s]U|:A\sfcMvTRT m P5+M`"P벵4|X㲵lLfna6OjJ137鲵fo*^ 2q77iy ygt7:BX]ZEQ>&jy37Ԭ,r1/ajʪFsUSBw}'USVN:Svlmla J\_ \ !l͗*a8:6fk3Wx,+$B*;l!r 릻v熝TI"Pv[ekYj[Nu[z{Ӌd$5jJ137PK5/t*( VwnhXL+RwدW冦rKrGG:緪&[vh443:\[k=VݠTKCmvNWMB:(h$(M-V{M dV&LFs٤vtRMgħ=BKV+p_čkFTӬ;Tl١`i`T-;Tӡ6^ > SehUSBԴ޾:Hcn)T`՞HrL7%7կtQճӗ/Vj:ES?uR5J$$Bkuu5MfcꖵShr!^ȸ)YjJ1)X"(M]vP"`pOw:eǹaty}{ϱwVr!I\w١(=-m ЧHt(ک̍x*:5\v򩚺hژv4mj @9hi6ZPdg1;qgzwb));K>پGxRys׷U_xٹƻ3?z껮N#йj JYM37 HzpJ$*zb]ʧjJ&[vpy`0u5;1;9;TӲjHM IDAT}M/{_w19TB<sZsѳHJ޵SɦlL&3#937y}Ca򬚊$窦EYc_@0JɆ %q)o\Pد~^QС6[ɅT-kLU -)b6ָl.[&;8KFBPtȳjyO}Wn\Gޱ^u qq&=|馤,%yswNx@v(+xdgWIySt5յB &P{37h49IZ[J.SM-hmdz3I%Pq{'}"8xߤ 79xߤ ջ~yݧ,xEv"Vn!' 6uB?91GmfuulQQi!D|5@8BNBkbCYndӀ}[/sg.7BU|"_L͜[}fS;ٖ\ACmv.j_,S$ʽb8TWSc5 JРT,őmKPlltWЕOO+-tZsyP$뜛ñE@yJ7?bchrcVW5%}ܠTeos_^d{n + mQ]5-8bG ֭Nӧ[=s_@pGۢl [S;hI[77{@8Tniʣed2s H蔯H jz;97\ӯo='e;٠zFQ= ą)|˵-~c_M !"?,3V[˼Jağ~q nOy~v]t.W;ٰvF}!aaJ.-X ӧ#=]3 w21q,VB*;4κi<ڤB MCW_ ]dώ֍J"9ܞm] 1#ČDUR}#ajϺ0BL!~,;1cjo۱?7tnj @ Nr6[fA*a8<]|\Slܯ(Jv買S5o7a@ Uc\X݊*{H؅ Gy kk SO#kCo| HOTYۢ[|Qs:f!A|$G<:TӲhtSe`j'D(1SMh05ziPJD(yFB[ekUMvS%׌tFf1yjUUSBԴν!?5SO{E]j`l ɓްzf6̠\(ȏ/{At,[^Z{j1";klm{Рd4;.M7%eѦaN`O͕Ȏ@ AT ~))zn!Kx)xg/t[CpK~c8T3].SLP;+;BY*)-o-;0%v7؟v:7S5@)_Uȡj=ufq |V[5m itC&OzCV?ρ91ƄWC!7ǽj;_5%0l)J1ǿwYv %U]v Oga w`q/w|T< ;*﬏-;C >*;4Zu 5f0aK;Bh4|u>cBɎS\Aq@WUg,j 2nhR$%#6#;4L=- D(k ndͻyj>MMG+giUSSUS-ui~T|k\a=[z͍_mo_;]Bƾ}Q%%;E)6ND:9N)<:aAt$_=0=GRv ~cl֙cQuZ_^sm*LR,Xshǵ"dHY:r{2͕_A\?49vycj1o{caK]'USxtd<:w4uԚ-ժAZ# &ɤl.[kwsb|4 EG eqUSB|L5ٲk@{vEn!(A\AqHؿn&S8hR!KXvs4 ~X ~)Jq*otB!FC@DM7%eѣ`?;>ߦ.Ȏ)[hF?oaP$[~,avTTInE7sCoN*/ֿ 2Y%^eo @ S;mH.#9;TM(shjk:5O*lqͽB|,I³^ P5^hژ({G% C{cNn19=*~PLG~|Yv"=?7 $!﬏ʎP 3*=tn^";~3}h "=26kyۏ,@>Y[v[m5QeRv7d }w z"9xk5{H9Y#-mE6HOTvͷ(=v@ٽܰVS)_nGh09f$v/fn$t"MEd_^?XbjJqWTMC޾&޶;1hI{Łz)Ja|wYvrSoNQ .S3(7P-;=c}MIA M;w|M]X1#=Z-Zm87+cL]!hJTI#(~YеoGX6NH Y:pME5` j3k|$g#Cw_I@/ڕS;ʕ\Gjsv(FϮrS@>Z E w[_sS ~)J0US5Z[m#;W z`Xv |P#c^~Qv`e"{!#c,_d_t{@еNA?X$P&Lnwz^l!jxjsk8NOa\B t]1P#'~^QС6[lQGGrCjԩNQT^h0,&Ჵv7OjJ19weo*$X.‰?n&Sl>ҴIǩ8$;DIXu;룲#Bw!;S(U@"䏾r>>+;nm)>oJv`ۦHۦdG@ed(H@owHTa#xZl,nb\vrԷaR9L$bjPq3Wr $B]#'%"$BK,Vek5UKjPP%+*LՉK_{e"^L;Ck~0uЧ{hiЗWrH ?3hҺwWeg(uZƘ=c@A1Niwa_RM;K==[vg˿Xj8~fPvk8ULn{^)"[8oG+g:"9ҫzkJy$uJP]#)d1+L*`lK](١CmT[ Ue+NrPVM6QT^ST*ƣ#y,LTr!+j6a _l͑o-; #Pv^hz뗝u9.=dGPx֖{N/\{dLv ,}X{EYe쵊gd(^TMvjᨢ2oE'~0oegJEr%S;Beh%(V琅ꏤԮ0+g.,-T[]6\S][c =`2LP̍̍X2ͦC;jJ19pZso??yeNDm{y{&dA>,e(>]J'D l3f5ukeT>y~;-;Vf)>"; pgmSګoH_\}Q%Ž~``:"9bz O9ʵətX G9t{`U(%LS;78lo˵&CuE!@B|t7:I|ek5UѼ)RePjs|W^x#1ILn$>PBNх&뜛 7Ǝ<H0`}Ac)J!*;BYIȎ3Lrguoz-wٕD(G֭׷=who΂c~}uSvDX$P$u߼Ӌ:Yկ\Љ3YwGeG(wC]# ]E`jPL_:i)+ lT3Ov᧧yyz$q=9h05icwsޞ{{>X`USDh1s#7l${` %EMn|*ʎ)PmS#6J1T=޿4kd4;ߏnJU9@9d g /4`|ڲ6`3@?X$PX&5uĞqA\V^~,SÑzcZPL*S;5:[Zl%1TH"-z\:>t?$3Lퟚ jJO< @cL^pGvNF~n_ X#BTI,;(;ByR"R:@G_y'eA {_̆=,;p;}fv>WPv }76oŽ`M,B?X$PuZG~pɞM | yZTCuSv 0PL֕K[*Wr!>:{{W#'_W|?:5w#^;*#.wm+{NO?U8Ev@!5֭!KXvR#ǁhmsPEv ONB ~[ XuɎPPj{EՒ!KӋ(RÑOU3\Kh 6%;EeP]v@U:vehtʗJF+1hRhv᧧ͥCӗ#̍[~a!ƣ#L&7tY"f:';Ln@WJI6U5Y<1$3}EvRm3'dP֖{Na߹8Zjȧe>j C3^ݾ^/ Ž`M`@G#XMԭSgj;}绢=wGʖPoC_gr[S'{Otbxd/k ,;&۶Y^AʈusL!;`eiS;1[:\&+ USBK-u}|R^3ۡ+#?[FPӈd+քV)TlT}Vvs}M,ﮖa4W*k}V&M+xuZeJErˡm ew&!}'^cw+*STIxz,=tbfOeG0Q%8Ev 0 vୱז[:e%+|T(z-w[Q5?@ CgpGvDid{VvRH>AӶg]@BEgJώ56S #䯢c緟3.;JĞqS"9Bޜ3:٫}TA''@cj7LJ.#빡(=-tUScKw5](zЌ';dJdfX|ڝTɎ AvJwq>Q@g )Wк);vΧvҽ?tR)+ ą IDATY᫦ T涟?,/y76qE tt{4e.DwAvu٫SLB*ڳOg /;pSnuY²S@}ʿ Yʖ<rBnEԉy MɎPdG|$vztɅx$q=7Tc<OBLo>RP]x),׿os<%;P \XSyQyˍ/Å-̠hm}@ {e@Yx]>i)*O*9:TrOUN=`@VÑ&ed'+o7uqXHVÑRvˎ5vwg2LnP-öƞb< ,Q%uksl-N3p%eJAobM#=Sh="ow= *Oj8n99=umdGRRv OtʎP,}S>r6(Ʌx`?%+ SQ ̍ܰ W`+)R8$W`k_[SNd.1>5,;|@=g T1!A!*us%8I)P.m z"9g&zJ6Ԣ%br[\J ?@Y d2CmTi(EBLn>Rŋ;ФLn>qQ%z=eq*.;B)ɎP:mS#]w!քVl/Pd4;qOv woT\`=S&y@qHY^Uе7ޖDjRjmvXJWS;rtѳKVСbUMN .sn.5d l yC{dg̲Sݵz9Hx1Ɏ`3X/lg ldB&\q-3<]v X$qtK+(;Ew]#Tj8#;}Lަvf0p6١$])ֵDh1s#7ln_ @qs!lyIvtt0dG2<5SyJgI)7*hn_%@ ?IOű|_.ZuɎP ﬏ʎPt;Cn*KeMZ)=馤??xi݁ONnb?At{S"9++d@xmk t{vXJoS;4:K/熪P]~jťu{.hDT^)p 3'Ǻ Sjn_ߨ4۾Gv /~ҰwZv T>л ꟭X -/yS2;n("}(]k|m)J{t1R:+{ZvJRĪH& js 6=sN[1ɎPt}OM>sA)_ʎow% $s";toUNr8I)PIm "9֭Ed@N6T GzxW@ԢkLvltʗ^ U͡$(bՔb6ulXd+sڦo|qQu?(SpFthV`{~R܏8d@ NڊUg m "9yۣJJv {`\v\! Y֭N)@!GS;2weֶe%(u#.wm+ڦy~cw^)A&vw|wETUgDq\vhm0T}|RMNXGdUD mEjkg y;~`Erh䀶9se@OcSmoBkjtNRh05 Wqƣ#K.;@mSh vN^-Gk~ ;Bѽ ˎPLI۸n ߫S=|{7ʼE'UzVSZ{F p$Er@ÆdG@x ?3(;EYuEZs^*@v"KͲNjJyUjbuETIlmSЪJ]7޶g̲SWTI5鑝X|eG(7#P1#ylI5S@ע_^#;y[)$[=&:7wN9TH"h05z%+赥5b?#@G~Nv XN8 6ʎPtܠhe(#XNfZv OI5}Ov hwZ}S@ۦdG2U n=EE Y¡-;P,,N&ž1?<.;֭N*9d@w!KS; 2[:tnUSBKMcV.;P,^贺xMvuߚsn+nJNx=,;t*Nqk Gz ۴4-G C쐝(q7d@YO}~dGP!GS; I7'Fik<mEB,}Y5ח@7( l/C_mSW9o^ ߮ynE,˃Bv W{"}NHЯ^Vz[^eּ;f mbИEl!#>OA)'#hVTI1+Rbj,=O*`o鰽YI@JQ5I\VeO P>Nw<%;P^=W+;BioWuss{epgFC@ɎNO~Rv T۔VJuZ_y,v+c}SE"9ǻ4PN6>[)7򎠈w%YzTH"ZKV;6I+"ZC8 e}Mvh2wӧ[dG( Ga`cQ^=0<ݔᮮS@JHp'd@VED&"9!_3e&5#h\+ȴJv]ÖNYI@JQ5\sl/˄6).;Hd7NQHz8q)sEW NmJ7Щ޲e)H( Y²S@ˆ,]}P,`se|{kBv iۦdG>6@!KS;J\GsCEQzZ>.1hU)OT9TWi| Y쐝(_ogbٹ%Z([v1ѓ4Z[TΌA%;v5(s>У)Ln+{5Hо큆#MAX$48!yYFÑB|eGcj@zf鰥SVаm7:q~鰭EUR[dEc=%;BmDy P=(;މaи_W-S@wJ2XuX{Ln/%e^R`Ѐsʎɺ Vv ʎ CpݾV)@ځ@J.#빡($M*QTr!^yBS-_b# 4ixoVLC4 74sj԰ӹ٠ /\l鏆e Pb? *S"9P1) =c^x\v Lnk+(;^$pˎZL*]L&:fK5pJv8~>РT9TWɞ|4`Y:eqˎPtڸ[w'oMN97ˎMI) /~RW);}`9n% :n(0Ɂ{ɢ2jAv0);hS;0Ѐ̕Í]&jjtjp鰭>N GXɎ?~fО1NQ\kdG(#Ivwc@j̣cS@/j@.(( c1)G<+=X$*;룲#@G|3wvˎ#Q%#PR09vZtѳKVОUM !&gfc ݁@HFXdGX)kg=Nvw_ę|З>9);}rL]q<ſ)$swN@O ~3e/keGНo@S; d2Cm USSK뜛KL\3x<`& uc'Mk%n*v7DwUJ~_ܨ\RWdӦn4k:0xga?bfΙ9|͇ |8 9DG?hnLK0Vﶔto9S+tXY#װ0LAĉ _}X:lǻOxI<tǿuJGqB= V萎1;wKw!&#е3v8Ru$&³kU%;02+7j:꒎`>OR)"{ѝ/˸#{9[<"^2yb{H:j 2'->3W#vC>& }:✔NqN*u[G{iZf. D|8];ۢkߧgI\YVٌX?,z3OD]e$¾N7utP ],еfsUс]jJӴ+bWiʑoT~nd)-6;ԬWJIu1ǴtyէNlbu߉}q ETϠtKZ_I:dqRSPuԼ7iզn,N{ڥ;D$-P߯4Vy]/^?Yv3xЗ\]\𕕚v^׃zLM6\;[:.~JJM$C 3H,&9o0}Eie;'^85~_'8UiښdO]tzҴ%8lT$8'L}t:]ztrG&4tw}&gkY* ny@JM]tz7]`ўTNaM>ƻ~Ž@H:S^y"̢1֡#u) 4`[:r1xzɼʔl/l``e=b5' Sqhv/RK|3զn7ߪ.Ull{krմ<?SnoΗq3^8~h?W1J[ikZٮ@xteS7 f}r$g|9rƺT2/LSx`vPKӶ nK`c|i|RՁ+OOTB/W ];}ѵyfsUс ,C1d_]3ʑQoKt )Gt|W8e,W'{g#F+ %uBW:$ JG0X [i@/KGP_,5ʹHƢfmKg [*t|=|νsb}x]o>Pَ*[.'z$`~ޟҌ,uz[oW7&˥iTѽιV:@t E.N7]My"{:3Uѐ@G_oNwRgJ .VϜ-`%J`$%9 Sun z]?hz[,>{+M@nqNJ1w_}Ip!&XkߟU`qjKGw9zetDzRތi{ruJy" -m4Jȅ|a&9XlɌv.ogy`vP_/{~gA? ~>zx@: 9r5ig)O NwJx=^i -N7%uj7~cͱ֬^#uMRsy -V/3` #,Cٮ+i?PԜVnM՛jޛi{>Q8=Eꦚt K :]V܎oUj.'Wn_b IDATv VIe0NQ1Hf/3_ƝyWŻǧ뼫Wz:r';535Um#Zw9];Y4Ng9 \Mydg6+JEb&r!`3]t ysE2O~΢i<1tgSX 0k:k;'?~W|ԚlLO?},;VKGoZO:!%ѵBy3ڱrT:V ,ql}xΪ_xU BB̓=w+oTm`,x9aa'[͔d?jS 6H8'vg_ejFVp)tacSZhX@:0NT;`=:%aR j=P _]Dߨt\2. :p0v7E΢%WmE`.RSkñڷF* {`+Ej(XuOh9؀p~Dwz#,Rfn[i- [9ts|;զ%߼S:%lPhޖ#u)TKwRr{ L-_k+Ev^Arf פ#,IIGF`\/Ƞ? ;1y?07_uBӴCAtл͒`*tL 3ڱrT4Eᮅ5JdE-ho/㖎#-*B'?6t 7Y2ƲʯK']&^nʪt umwM;A4שb;/ {̨H=O ?[-DOWatg>ϗ?6oUG{YB.qMr,DvH ]/kh"HM7ڊ&<`Q//qLŻǧ>jbc)'>S̢1T(J0P1]Y:_:uSs;#;aJ~\>7%uoK8'0lW͈sr\AodžK)2ݯM?},KgYT(>ݯ:xx{.^@_ޖJwX1Y>ܷW_=_iK6.Ϫ|v7k9= wJ%Yj*8pL!=^B֋qOеT([) q!7UB:;>RH0Pc|" y$IOt{EsEcmSmJږ{T*û>昖Noc]ၭ]Py1W/y 9լHy /nQw#߄ƞAn!vMoR\U0Xds,lYuT`*ۻ*Su-DvH@DN+8&~hL*rG[+Xj, (Y:d1|tk({O)Pmt˩@~ذۻRt\Bۻv VI7v 9hk+#i}xݯ5&'Mݖ̿hV;sLgp]`#G6K%,5O7KJEl}b[8(D@Y;m+TOO j;J:biW oT|<#8 #i$AMÏvx$^ զBuSt Ȉs9GmySNW\ycz<7U2c@/'ၪoW/k1'#{Ke]ޖJ5.67(+`"tѵ 3h|p4Wm#YjJӴKS#8 M*woWKɷ ̠聓zzȑtcZ+|T(.-$IO+}+:^ڔQPyeT&*̗F聓)=pnզv{Sb[cj)o L=it$JV:|CJGF]A.k7nхͪRIKMdaEMp|+EfsLgFZ*#ltDpuT:N5Ĥ#ܒw_tchF:۩%^n*YL t6Uq.,hȳ8VEU35nզluBc'vH0D*i7`t2n'jp;#;PH:6DͻvAyJ})`#+/̥lT[Ub=asLQio.zx3O P<,|sR:ō'ݧJG];v#G6K%.5iڥ``Zk}Mu^cD:%Ɛtc%Sh\AoG@C*;` zU4I{V<´FXAʩ (PgjM 9j$E΋ı`Map%'զXV&ڇ#^ma3c˩t`,OƤ#-kd/Z MIjSWW,=Mo"bPrj+&?'mRgjN'<3&'00H971ުp͚{~z+c:#+U8'ڇ}w|tеnB{&s}zePb`4RSəxz65\UR%M(H^a!?Rys҄]iuIG0Pu ɕXMQk(XզqyeD1Ǵۄ(w Vps;ʎArW\!XЯrrKٮ,HOs̗q2SI'25z5.o U3ANG}];kN]ؼzTRSE_8VaɥBb)j)%Z?Y&?4sot~߉k"vfJRm*WV%~X:%boRPȾ1Y^2 c%pct8R7+XMPG"{?;㥙T(.4̑5&U8FgSеS];,W3|_|;S0'~j+X DԟsL!Bq5m)_PYo%tS᭒\sT:[mr_tBnJގ@HA%5MeMpŶl$1y牼_5%NxDt STv)ttu9أFS~Sk,4:qvamRI*LQj*f7W;XDw~Z:O.=h/e/T.牘jNW fifrt {UI6cuHW#vt+V lW%5MqoYg;(>=Vl$nAr$!A@'_=޵n{k##ߠZ;T\,еk}&o64M45:o)u~U=XmT{Єkqtc/N!ʟAHJ:c#zco2Awm.y|V^>~(.AE>¿4|t vt`gg6&,,.,l$XH_2nD\AJr ecO:z91Ǵt  HGY Y]CasզӡMM]YՇS^:!]F\AoG v`mR:ԗq8(,_T{\$ "|wo?wZ7W|D+lW͈sR:d~o`Ϣ*Arѵ[gLf/S4%GӴfr>LCX4oc؈Q`%{4mB:Q/Na./o#<%{"{SX  es, K>ODhZ@4ͥi՛c-8> Dq3묽,A҉3Kyf=FZt했3`\ Jt.quk)qЗ}2I_{U: zOW*; w&%֝qNJ0Ju,Y{:/~3]fڞONerߔ9q":!WЛ\A&D:'KoBâ) Ar`Q]ѷ1֋W+?pwK^k-mN7)t߷w $oz#е];?ܵyN$`rNE̵JJew?q;9YV¼cK)tV.Na@AM `>f'Y(Veꦤ#F/{4Myo&g۞OtZ8}+ȓ*c?*qsLw<zsį:޵6? ziZutYVktH>m\];еdaM* ,S䳅ukI}\.(3_[)F)#Ne*JHG0؃{/UxcF )>kw{rߎզ'KTYD-}ʼn|?`  DH0ͱt (|bAr&ڇ^<?ltKTw#ccoYR¡cå/tlroz:[ŮRP k*55\tUI``-5 ` oKQG =~~j&V󵿸CpFk:꒎P_nJ:niBMFxݴti}:]*bqνy|NRv).[Y‡H$`+iy"4֞K _n6AzȩP\21tf`%t' UmRIUjJӴ+"OW0 T(:T!LV?s9{QYk;J:lse#$p;:Vzho?#ZmIGX\ֳ-1?3Pl}u4zx@:EX.^T= 93V҈sH\m?ltыϳ M˸t3eɄXv .g?Q*Ptd N/l$XN/HG0ĈSٵ-]t -` 'yT(.\+t(H9UmhlyM긖WG#,bx\XG`/Ams 0C>OߚF7Awt{kُ/SWHݨ+H)X*e>@Ύ\&tb2WT oLK0zГ[ԍJ0P Xtt t^ٯso~cG3,U"^8-KW©JզIЙUƝKc<=.\Ae}wЀt ke65db$`VMG 7~s6ZF:jIGЇl>QWۻE`TW ];ljY*٘ԔiSW_EI;r7{t*?FӴThgˇ֋97Rɚ|wIpHG~'3%~Tm*JM)K:>~hSp7׃ :T YD5T(:T!6 9Z{ͫyۯ~ֹP`ԫQ֗qGd\Hߩc0`vfFysv6$`BftpfC`T`9.n*(Us_)?FӴxt,[!w!-oKe'捪sV%94޴t,VtJ/XtM#e)Ucǖ8fjzԙZU0H?:/qN[ۯ̎acُTJ'2+O##{R\Hvœtt,6t7={}bW8 LŌb2|3 %\OIG@N\Aoko2pyc܆xs:k/yνyc>~2&` : >BMUbY{׼gqq7K PcU+#ϗqGH_-BJ޶ 93w7&˥S`<1럻[/+nj\Aoo$:*j;gN ,Jv@`asۥ٘ԔiSW_J ,&I3wIg)YZьtej\+,Vut}X:0)=M}/Q;I@.־?+aqbY%I\d` IDAT'MUHG%M0H~+8sx׺Z-qLĻ>昖N7sJ"}SJY];.'RW:t;j+yt¥ RI?;͎_n?̳K`U 2nFI>۾\AoG@ىs(P,IjS0ijSwO.&XMЀtō8'XllW͈'%IJGПtQ)yz+Ar-<]g؃B9l=+rnWѵzF;6WnJbRSc Lf NJП+蕎[T?SޒZno8R7j3s OHG@RxPt jm_}*?y5SJ:dqZ:4WmjHz)'W6kK,ϚY}jXvrMu)yД})Xm]),պʍW7۞OgsT ?l mtŜjݵ%g?Jg oKt5&S֓ < :TƻǥS 9`Q~N\u]{'7|聓q8jژt }'ucoIG:k//, w=ko4tW0¥ RILIGljs, 9Pfi/K>^\A2Es-E+GǫűKhaݙܷg5*cėEe)1,Usq,eA 2[{H:U)y<ɡwN}8's Βo?p0y]v1Ǵ g];عkEDWmE`0]X ꄂjk61R(`S <Ż>{,Et@jSsN>]-NKGQZ*#,.9 ,qMdW:G#XNœa_=U%F3*߻Y5hGhVff2еS vKhMRI$[jJӴ]Rx[8((νmo [UJ#@i/sLo7ϳ,҉aV89+8@1H(/N[wjT* z;iLǻݦ|V۸`vSk#DDjr,pV4 ANwYu|3w qV'-_:px)MW mtcnKU1DŽ1֘,h 2?L׼;SuM:}ռ7Sq()qHgP 0}'JF\};}Q3}'J3XN¥Z" t]%\`PI{|Ǒڷ1&qN=SuJgсw_]N./Y#{|:]^S 3еźv0HhGG+7 ]R ,naub0O=BawkF,]`:y"ޖJ#5bft%8q&3%xlv緦|gA@FA묽 ~Rv^ʳt8fHGIcP A0;[onOCFluˆ߈s|t v6L&U[$Xv8G Kti;J:Q\OTپdMWt̢ݙ%ԙgu+{6I8#XdtE} Woxt KLHG!0H/{,@¿+U#${ޖJerVK \ٙ2b"^C&PG.lR=YTka3૓Jv VBqЙue#=X6vՌ8,5 _l)p#GF{OH`tK:n2Hw8*y $ ŷ_q_-DLo5Kk#fcϠ-THݨ+H)Xva GU[ 4->?Eq\V"*hVuEǎ@HǯݠyhF:fqΔI?7#,n+X%, 1Hj}ԼWoxAq!&A7ƭ |,];tF;=rtalTbRSfC`T7NF,J/˸NJ5R|]ZW qKSLԶp0=.e`P[oNQ41V:Q?)7(rtY""*0y 9`'?$}X$+,B7׍hM*t< %vb&9&.7GsV<g)5 .l ງ>.O)`HG0J>^2tDt ̢p2#(H90H▘WW/bq;gX }PVR>昖N_ƝRS oJ:X];[JtzfUz$)5Z\$2{SXC[:!;lR)Dt}KGQ"huiS:K# O[wZsy vy'S OslWtE=X(A7CyK<%?F?Eي%vȧL<8t8U[@>YyJ$ΖNHGjv `wu^e;νlDŽ2JGs㧤#JeaRSEqsq"{_S,—qGHYJGP0 M=׹U)'G~TmNqKޖʎ@H:nSz8=ޘ,Î#يɻvȿ#G6K%<ҳ9-co\`1Ǵt@en+`c|sݖ}#&9wKQRƶS65UK 8'a 9`['*3Y.jW+TZ %@mDt c6kߟ`t/H +N[=nBt䏪%z;Lc\: TKCgCyۗbiJˣkgf@鑣 Ue륒@>YԔieJ+v֘,8)'z7;O)F{!7qu#t'kF.ћ7a.C 95~-5a5]5#gW6+=R1):kg@Jr&M\o:檭y ?,Vj*p8Ve^:tG#"Y˜ʎTHUdRS3%W6e6bǿKt\RDopeW:y[*#*ciPܽ;]˸X{leܑ=cQW>wg@K'];0aN]ج*[/b] kV#G{S [w\Ao/c 2/JGXCo;Xt|nr}֌u4M?# <#X_:Eˤ#bB/ҌQ0';S|AdžKtsx}~ ALصL<8t8U[@nE%RŮ).q Lk2MSjAgYÆӥT:nA8+4uW.dt KIogϔ8_|Upn㸦gWu8nr}&]]ܴ'03>\lqNJtDט,DSjLkڈt Ar71>\>|knq`._xFiV9昐NDG.{3Jفvwz Ue{F/fGñ5{p8V֌.&fZ!AgDt@4+- ťS@@ЀG#-j&ڇޱ uc}<, (#*vSY HGDoIYT.u3yc&?a-zG<RݐnwеI$gh* eRSkñڷF. 0#oKe1-Bgwa $2",#rG'Fej]cAv#p]pD:m{>Q4f:S.IGߵ88hz @o,'#z'9Ar5\.R@11tkd3|,?4 >]n9YY:еy9YU^* ͒j6׬G* 0'UKJTk̦B{&sa8V-5 οv8}k髞\MVSp|B:l-~t DpuZ}e30U]xG4sMx,;dmz֙z}+tIDzY8\z:`Ѯ6>QS$%0H@P|έ]*%oʇ["R>ڵo h /z`tT"ص Np8 XTkasͪ{T8,5hLet ˖լ[7w^6._=pR:EcNj# {'Xg*`L,V_"_?4 BK՟_{U:U)yOCu\Ao'"BJ09'zx[zX:Mbs)wj.Z`Jբ|aoKt #vVGF,4:qvh*`plphl cB܆/nSheq%cDuts\}?MT HGռ;d۱h)M* >WKҝ3JIG%7LAryz3_)1XR忦+=$(V-`trXggD-[6HqCHl04fk%im4is3;۽{gNL٥lNҦK۝Mۘ@ &6Pmגd!ˮù!}GLJیb9Nxd[, IDAT` |WdN8n|XaO( K>1^){ ZQ;"!0H'"&vjPܫ-HWXzL:Ȝ23LF9S1)EQF:vhZttT&J'hϪueɿ4KadlȱMW5^ܲf\qmK\KY A~wt)xi .L|p8kc@s(sB"u~|0}B [h:)[J սN Zntů_N2WY*YjT|霷_1)^=O:+ؚN~_%_0,6r[h0TH-G*C>ԟ/719~kO`Z#- x0SΙ:|H#SEqI'Ezp(cz 3){>О%_0&6`<}cdtn Os%*Trg?"pCsR?ctf,/K;B0vacQ_+2sɇ`eO:+t*![K鋩*(YT:AmKK)ɥd~5(dT:ӻ35\ 0XkϨBh1#<0DL:Aчke՗7Mrkz?ktyaE[rt s.xgD:AKm r)Gġd+Ϛ?`C5(J"u|V@:A/5GhPZB9ơ-PməiVZIOIQ- x0E?Ke ̙R?K:FG:щz0$>.۝|۪7[ CaBjtw7yK)f(7,ϓK'h)_0 v"0X^N04dGɇK+J,wCE',]C=ΪBY1U8R}]@bt LagW#ٟsj;K` g.I'%F"R-yCZ1޶Dۖګ-fzcf|.Y&e;jOޣP0RÖ! K;&Mo<[8ߕXGM O>HY%.<-K?wL:cջvْWtߒN2*r%5LIV &9M0cN]܍|a'] f}g]9S,(b%\\1uFNPnv0`UJ@C5(J"u|.VɆ5A T]qI'e-O Hzt[_xnUe}̙@}[tnA9S;\3(JE hh|#EW.K9ݺD[:< F&9ICOlh[x/.xdD9SٟJF2JJ7}[:~sMY#K_+.#alV(ץ+tk &6XQ;"**Eq7TחIi]JWِUTتEejٓqpnTHK.Sg:ץ mv0`BU鍙˫ֵ]nYaԹpQSFM`+ޭKz,;gJQ-KuP5ZF驻+[J+4w =YLR"=j>S>\|W:~5+ շU(Jt>7nzsn,SYiΔ tE4ۭs5KJ {t )ۭV}\'y!e}r SG16 0 >OG)PnEn]2$J\2ߡC^(5w[LV%]qK֙3(Y ձX"7XA((Q.l9(hWFs܏3/6;ai=]v0`BU钉U. @,rf8uWå<[(zik+㕊]4F]sڡ-/6-__q~a.Yk=-] FFs+ ᜩ/ ?q+?/]#RwWps W{ieh7o]ëfޭKzz4I nP;"S;"r,ݟI) .[>%†2tzin05v6,v8Ϻct2Jt\9%dq՟(/mL]ia+j>nzYci`UJ K5u.>y`F0ͻ5A hlg֞b :K6T!,xgD:{H'ocL/NL{t<rSVnw <˵ax&Б_6Fٓ^;W]a /jG$d?k[{i.M~, 1K>StҎpKL]yxz3jJQk[&0dC[JWB^sл~ -}G*C- ;OJWѯE&ʕp<@ޜ37 ϙReOix1䂿Wu2T* y%ϭޮM,b$>$?g]cc- R;O89-]~sn]\zR7 XM`i|T0vaa݂R% K_åR%@?%鷩{tI s+-NL9|s|]E'OHWv< IttZ{,;}U@KTB_tAG&9 pYqo5g]nkIo:ؗRL%صz`ힰBLG&GWRU)Ԩߚ+=51@>wߕz-V;lviWJWh9XA D(D0ߒN2(-꒮-L=_ؿMiVI'ϔK'H/Sο+Cdd/ctUܪ#)>!~uV,8ԎHzǷϩKILݶ_:wK;`\0vacQ_+Ԩ)EQM|\_,X4s_Eg]!9G:3ۑƤ̟:B3-9/]@؈tuY-`}Ι/ڕ  +`bVlStw50BtUn1rMr:q7Thѣ^[7d)7I{E.rH:LLK%c 3V&2˰ۜ)َPw +4 ~+^ntyzI'[6ׯ͑?9S3/HTjԦOM-HWާԁ`ыMVbӁt<邁] XtOw|Б{oTl'6-0`)A~ci7s,`s؅#Mծ_<J0hdCLq3ۑ=OK?%@^TlR\ '}SH'?ML)'# 0~.&X-9lFΰI@sE/6{³-מ+ynNIy +LPq=RSai7[,`g#M%c N _+vy?.pu幓#lFɆ63(ʊsnzO4jrwC/_T\bm!8>U&)%Ԏ bԢ ݐYݏsǝ3u}cεÜ'OHWLw)k,0-&JW\ݬyR:DwtEx@$UL?PcSP;"V_'[45*JtmaMG K;Y0vt޿6<*3g+;/_}oᢊJ@;?L^aKCiWJW>t,n`WjXSM \@f.DNI'F\V՜9oRh9SEtLF%mK>+ +r-W;,>JVs<邁] bsw]V𡗊K x6w [d]6Xtz_X0f˂w/O\^T%2s;^_ꖫ63U5Zk푮En͗\1e< tK'[V:R43u‘"G2JW|ΗN5~Ɇ|\:!w=mVst Mr{u dyc}5*42 #IWWɆ^t<0"v`i; .L|p8kc`V,8jJQk8x&S,:Pmv3$ TKWg#\NmtS! ߒN2()ӘP{9SXJ+ f/P\/rʪӦ6gJKh$)x.^ z&Nh N0 K;V x4NOK XsM>\TqT @ыM=;wH'؝U}vKIWּ3fa sJ'XA?He͙u~PdǼ7}jGDb=v{ꖫWKWhj͙KZ{+Mr|;X}=ޭK=q]u{-cs0v`i .L|p8kc`9j/~y*0sޭK~vkl.*®'] ] َXv8_:I HW0ߒN2(,07G6gJQF9[=V_a5`+, s@MP:$l5 6,̽W_vP %I'h,&){^0s2Xi,<:yd` ̜e6qs++0 5zsPEXϢ PR!{t.xf;rwH'ϔK'0kt{kL,o(}?jpE#yv3UYHW6SV_a[u^κjwy.9g]!I$HLjr֞JtI'T `qg]%+Xil.0쨩}&.OLdCկ=;wjbD/K'gCoIWfJytEomgtYU97H^L) 07ijR!UJ*sp1?Yc|'k^~=`(4)[AI*kst&xjNj55{(J˶ؑK;ȩɇR%0s5M8,/TdC=;v&(b{B4ƭy0lG~N%b=/vI: f,0>gJ,}tL&ole~7[H̔wި^1v~ 0Y?ݙ)qkBG5ߒS ˤxD󸷙f S;4.Xil.9FW&G`̄eGM)ruMι<`R~vmvOX:@<Γaѿ3eVtEor S?9gJQX HÛ&O``W@P;"-]~ 1W_vP!2wCŵCnnW;"MrW*kO%x?K6JfVtsK?:&[u=Y/]Xi^ɇR%0CV5*l.z)|95!;-s;nUc-lG4uϓN tK'0S @ c9S+xGa7MM>a/_7| r>f(YT:&9(RK5_?ݺD${%ϭإw#*κ?0ч Xi(JrD&L:zZGME􍧰|Hn]Rܪ֦^{v5!O N lۖ5!V+VhԎt952䍸]-]a#?o9Si XǩX 4XkOcRB|851d@MPE^c2tn&9t]tT^~m<*g%X>]I:P4K; &{5c IDATxdT ̄GM)ru%8swx ^ x.n]qыMíM߮8Q믭;R;e*]]O@B/%+TKW@Qe;# ̡tO֕pJWLc&OfawIS[w 3;rg^O N B*6uϳ^j5TXݽEP']yR9p#d•p>?of!?tSz)k.wcF(z0[:u9;#Ir,cDqeNC,zK7OKG{` sM7.-/ CR!UV :+ PI'0^N2򐯶TLz)r>e{:Bq,V>]ͮ1^.]۲. [a93eN3MPu *e[UsWy:VywMŃ%ٟma'f At*7zrYi0z?tġdq xTnEY8QSP:^6(],mI|.eB/ڷHGXhqt7x̫ۯK'c S ;w LU~o-CLѼyW#SLE kdV5mwLSJG^1 Q5(RE)-L̝;ܡ(į q7wPWaplC'le({~oo<@syle%7ע{,P1t{WsCٝ~-]9tt`Ur3*]X3Q+޿iS We{&-zM=>1ЕgcX:ĦV[ep2pOt{kMP TW;ἡG9;Ɨ}q<+W}[:S|ct:{=7vXJK?z\`F1|2T*E^U,r_IZ*[wƲKϺWۓSDg{оӽ!9zCuG:.c, {'%5?jJQCUK?[XMe=ݽy3ۡTHmRwI'0*20Gr.x{tѼX|霄oNCξ⢂)ԟ(cLAGjG.:Cu2tQ˞ # }YLI>?;g]gkFQ((Uuɲ”s~o^X^ψrAUe& }n'|I:;?y5(m2`ެ~n8X*7(`u,X\K;@QM\?t8L`4*bĨ)EQ_ kWj\O|ހ3ۡ+>U&aK "Ϋwɓ2*ZvK!f2Ry4QW¹[A_@o]/ec yxk:^6qQ3&9(ynAOtEu (.E(2>`\Tw_vT,KBE FӶt芥(,f(0.C7J;ZEQ6|ls)e *+`Y+yH}^:۵_:w+ᔮqXTP*p?]`<k[HWԎH]L^3@/52o!ìΖtlD}ޓ.Ѐ1&SNk`:tA;+ 6Xw7ք+`&a_8rUbl{@c,0[/{x|B(RPc.`kv-ѥYelē.`;M:c?Z*7:ӵ v@K;v#j4qe9'S#Ul9}t4P '] ]a}K'CXzL++`\s_HD:~tBLEQv>Y%]k HW@l.;+`2tAtlXzeOKW@X!/)$Gdt9'e=0^EI'>.j#uɲ/vJW@jG1^)]#4uϓNWXStϼU$]z䇋oZ{FMy)KhqGJu2 XY],k̙6XCsT+`>gʳbDvK'@VX!3Y.i;6pq}R%0vɇK+J}xZ*J@3Љ}  /c s_ݹiSrw_JW(+y2al$R5O@:&I Zg.e_ѪP|ӦK;d,˥0h"H N:/L:n|XG5@wk푮vF| lNb=oG܂wF»] ϼPJ8C` +3pNȄIQ֞Ov⓯In1l0s/,;,>5zhg̏wWih(vocX!3/L>\\qT ݨ)EQ&+̷߿``W@ y^<Γl_ 2m *ΕL¸|%OIW7Z{eS,c|DwTgtSUlYX̫Ŧ RϢv%?pa;^X!cY.&“'8/L|!1޿VX^K?Ѕ'mi='+`AtK'XVS<6ү/FWfŴ)ci|1]05cJ XGcrsǤ+ 6TɆ7ք+`JٜgZk 0;,퐱vG&.O{_NE<1^t\zI3Ƥ`AjGVu؅ȩ% Cot$;+[0|^c&*?dt.YVKW@/l0|?+`V/ fy:q~atK;d#0sD8u_8o`)EQM|\c_ h[hg+:;-UEw6Z~.ٱ)_WѪmj$"܂1шdJ '] s TH$`:rd1^b;;p!V̄2@IMGM]E|#KBcyjG9쓮6q3>ݪ[ntbLm!Ӧl9S0{4<&JlZ{Z;HGb=Ye0ӤOsdCtK;d#0[D81-9'<[ æ{/*`1̙/_NlR!{tդN`_LOr%!LݦyRXk'_-Ym[Μ)`YYtՎ6 G*CZctLL xdaNtԔ(C7o(puce0gn{WZ;=x}RKu2. t_2;W2mV1g at>6,/0{ˣ;y`l0.; $/,$[~K;dLh"H N{ +{ؖ}GM;6pQ}R%23eO{@:A_<zht`"/]dUĴ)[yWTs` N)"=Ma3eCl0>T+`V-_й.I cz K;dC@zT ;'Dx,ljC_I` Lٖ'mixf;G::@Q_ 2m 5>mK:3%]BtI6xΓꬒ9|o6&9s7T?xe'7K!Jã'KV -nyzgpnK^=sKWR|HH'X],#]WǤCL֚R̔+|_<`hXDǀ3;Fy'n2J}<8sXKWp%_ݹrZLU_7!VH'̀IsNCCn|<=C`,l0 #rק/tC`D4_ږl@XmQsz K;LAۥ SR%QS |T 0|oOTQ*6)lxq,dt[v헮2~oo,m5Up%鄛0 Rb=7%ˤCku2ǀ[bBjz{-?#zM*κ:Ѩ: Od>̚9-i49:vc{a~+)%9^8,*(U5Z9\|fKy'`JaWHX>5'<[S#`AjGPs|edtK_:? IDATp1v$`.ѝ'[1ԒZM5<5P,'aفbm DXa2͗v.DNM>\^N0jJQ%tc<.v̄i+t3ۡXkOht,xA-^2J8?} W)݂ԟ(DOI:Zv8_:C8қ?tr IScZ{OuVI@X]Luλ+=q>{{;ۖHW,8=v;GR<~@wl+ O>I .)391C ^&;ͭ~͎']ɔ&v!rj_1*?\\:\ gY<+'HD+ 7*ǷaU?}N'C`l0THܱ-K:,ӪKڞӻuIsPs o=O:A/#&z,\|jT `ԍsPEi>re`W@:fsvhgg~n~ (nܔ! ԎHs']( wmZ{o1jo쉵H$`RkbƆZNkkj{BK&}}tY'nYcig[:-twCo|W?/FM} |QAhglhdc[x<R;"J 3)YT:ѯ^Kox[k]-b/W]۱hWNi,xgD:AQ #KԂ m_ -=[-6bfO0˟t\3-FP{it L v6U0vauR%lQS8n|X驑*6-s^:f>oxf;UoL:4FG ־+ci-~Rs%-{k?yw[\Sw-A2`|ox|dtU,{|7OH|$`jk>Y0a=lB at^?Y&>[z Co|W?#FM0b 9{K'xB; gʥ+t3ۡ-~I'.wۻ]%=*Εފҫ.Zy#! #ܳc8eﵶ|NZ{C`Jl0THMo:ٹU\RnYN'{0O3 X"Ril:K6TKW@XفK;  W*`&JܫKm~vn6#"+p>sI:!Xr7THW"R!{t9N}_2QV{k] tv~%O I|H<f4D%ˤC0;uɲǷzM:&9 9wә|<*b<ǷωkPq2}[:AGo,Nl*v@nG̽]ynƨ'-/9S5Zyo=[`jG9쓮ȅwNux &P,c*"˻9/ʵDX: Jmggϝw}#!(7AyR:L-c(|NZ{C`zl'>.t\]W;/,q m[/`ig19X9]|>Ǩ6qs.!Sw7VÂMIw&3ۡ]b M6o1p &J88C~;@5 =wUwr"'X(l VjEx͌e:{ݭ=cNw{Y:{ڙVNkZ3HP * &XHI`]]V9u|z~k}/$YyK?<4@s 9/С $9P4&//)tiạ%mZ4K [1XCt9Sd]rd2[ǵ 7w{^LU@R2E$R"9N!t|rjpmQ|? DQ2o!d{f5E} ]u9cVX&_u_: EIr$2uW~yl|s+2G?pSYZ5-JlK( {ZBGkc;t,~MdiWrۚ:b ǒGSĆ0@)rh[3ɠ۶޳{z7$NQhRST>Ӣd ھ{:4y~=_Ҿ{:_ekZ%q,5$.[2}%N'|w&{ٮUr^QeL~::B^[3<]R:@Y\. :K&}d븶a"jJUk2=;ZS2E߻Dv2}B_'L%mkv<Ž9: ϴte_9MC/W]S3JmDޔvk&RnwA(fN*qaiȟٓ<}ǞܼQn !, c#d]roq,ko]0 PTMν|C%2bHekZ.P_rcdՕ#䩆dվ{:]ϼ';|o[%?|Ҿc.\9z}o8۶#NTܕp~oOv^+>~wθꚚiҁ|&t(0}vM;Nwӿ7y rid*yΙ3NE/qc/-4y?&Ww%O9hHVN-Tӭ =K<jiA 'vX,޺ `(z{ubc5<?t%wOG8%rg)C%Us:@zo? ;~sӾiξf}= p9ݚ}C'B5޺㷣4) %S$9P"oZ-4fȟ[Ws뉞 ùE#=3t\vЋqmŪ! ȃ;?AN.クOw v$t"ѐZiC7 uw'P$JGO{~WNTB޳fχZ~(*S:oUoOBȚs`z 9ciP48ܟ8:bslY6PLTM##ǎuUœg<_a|xx򃡃dݏ)}vBwOGGrOiyO8NΩ(Λ57Q+  Ss* z_k߿mfCs6b}ul0տwyii7}gſwf 6+KܺPZ,:'ɁR3?t[K E˳GY'M/cA?qwKtUށY~?ݾAr.diaع]\O?m) TM=:'?{8MjA6y~Fg}C1ݩz{Z*h(4w p ?>+t<2 ;-y.Uǩ}mSڦ(Mf&کu^7ᅣ{(q$lY2su+7rP&-|ǯ!Yu U?O)NoծhծwFinO>2kHꆖo DP{~Sqפ|oqs`A%!B˪Z@p񚉣cyY 3w6P4TMz7(x)k^jڡu9E|&E- WN,;5\yىBk_G_"o,i_6t 'Rj[j:alcY,`$8xog(kF6s]g86ۙƷlZyK>a/G0>GoA·go{_Y߭s4{ u'|$9P]R[ѳ_WųݡeO gDQomHVh;!g<_.' AwAKPtYeV. ع]\ۚf2Eԛ:XSY?:U< hYQБ v Pw—W $A[kY~0t0>2koxp[j[e1[guꈧ~D`kFܟ:[ݲPل%ױ9:(:E_33۷0-i.S信[75?i+՞Dk3Or9@r ժ*.njN[ 5|2y~ uɿߑ˞ّ vNHFNQHFAer vgݺ!t,NQ>/ 𤼬rꄙڦP5;1X,vN=F6yy`/5|?EG۶EZp s4 [/qBZiƁ<0PCgw쯯W8зjWjWEQTR[{Q˱wl{@+rii5C;+(DQ>`yoծhUEQUUEEZDmS*' 691c&~7׿7:Q:@>Zbg``MeXSYiN XM-Wb_|(x0t<5ay+5v\<d/oዢwhl9Y~^mH}㉝EQTQZrcܒ)eUѯ9Q%ik`8(ݝbp?[N%6J:OX<@ACW5;sY,=96ˏeq''Y5 < @_*5Eokua$@P5vHNh(;n96k*+j 9<ZJލ㹓J:USǥo[,6&^0 Pv4rl(5k&VW.USl1i~$@'N^* PTM##Sc]aRsd25+j Pg8Mj. PN^* PTMz7mJdjL (DpXSY0 P"g5(PN@ρ9sB%JDGsd25k (DNucsok8.(8ND긦6`tt=>k (DN=/g5(ݩX,޺ `:1؞L&Sccabu\[$@Q5uIW4 ŦN0P@TM=ǩC%JNjǶ١E K KMu%bp?1;5UN03`P:{ubc5%bkiv$@Q5u2^yqƤ#130t05UN03` :GFƺqڄYBIs3u&5L (|l9` :InLۚ()]Ϧ C% 48ܟWSY0 P:=o(?49US'9%eWua$@S5u6K&e|[0@عwȱXSYigNɡ#SUc+jJލc[yN)پ١%eM#džRcfbuEm<@R5uJ^?&e IDAT=LƉ SJލ㹓J3US/Reaұsd25+j򓪩SsoG8uB{$@ۚ>;yQ$@R5u=#džRcS%d2ǏmO2`X,6aZ,@iۚ:b 򐪩 xwc8PIR\2L򐪩 ?2r85U(5~:b 򍪩޿5ui0 PR^yu\[$@R5{֥ gJŦN0WTMeqMe}uEm0@Iysu4;T ߨʘ=dz|rep?1;5UN03` ʘlO&nr0@ڳ>}lk* WTMe=a:*'5L 2 ~S'JgǶC%򇪩LJ KMu&130t05Tk򁪩 ۖ:.0-\tt=>. 2ލsC%JPbgPj4j*NuUJލIC%򁪩s3ui0 Pjv4rl(5Wǫ+jR5y]ϥ gJs'/ NTV LT֫riMd25k&jAj*+v%^Ij* P3& KTVүnr0@ z.aTMeˡ#SUc5%ok8.ETqPI\2LBQ5-cɣ5`4?ܓ:b P5E}Re1;W- ETֻ1}xn$@iO Ne1S' =USY48dpj0 P^\>5BTvuߚ:b4 (ACScyY 3rLTvmY>67* P:MM* {.ϯ6`%zҋPkrITmY>4;Tdut=>. 1USYd25Nl0 P=GFƚxMs<@Ψʅ{Re}KIC%rIT.lY>N* Pv4rl(5Wǫ+jrCT.$z%ƦJލ㹓J䌪ۖ:b{7%X]Q0r,xn$@)ۚ>Θ4/T 7TMpX_(Y]%Twz0@ʝ;sJX,޺ ` TMucPIR\:N JD}u||luEpHK8>8(jtڸG~\m,vB62|t-;(:x``MQ}U @~ 7[gnE(jkZ4VM1'j8ݻzDQ?h_ړeK(vΔg4GQ4֪jם%%nэE$۳k2 kL %1;^3q88%tb_Ip9[Xd*j7YݟfpCT;Baa984z0z9DE(|]{5(`)c=wǟ{zq2NK9KѰ3DYee~rT.$8!@Me}j\@O<@}ϗPr=GFWƇR^1f' )m-sJցġ}_{v ?u?:E5įX~>kR(GE|ǩ񃟸{gN*ۀUT͘}g.݉j?ÿ@̘}f na}%E"oA5g%%R:-PowcotZ!("JKz瞍k6ŒB1/5XJ,_2o%v 8 vP ut={Sc{[ US99y٣DZXlꄙ;n  5FyQHұcG}hՒ+5cac_eBGȅ_B K|XSKk.욋 @}?/X[R:++ӿEB4{gWTΒk#FwXYl\|ɼhpȎ-]}c?=JaMN 5S@ 33Y@>H k*5͉R5@Gs(g KCnYslaoZ~]|XY,ȻLC3|#W,[R J&%S@)_R5tW^1ϗU ]+ӿEC']uMK Pkf>s3/梑[7|5ȱ+-Mik $WֲS/座tؙ3X@̘xAjlo]zcj*74UWpF8z+WowLӵk.%W_X:W pOIٷ6zM|ҍk^]ərWʜd_+ӿE߂0y YQO>"V^1fv6Fsk'.z1 |mBQݙ 5L);SJ9νښfUP M~g5z.`S_?{ǃc}-:H]{E3#/NA,^:>IX,bE.۝x݇VN@'o=r},>NR]Su5]'Ŷ׿w5EEq5gM_ؔA M(>v'Δmk:s'/Z8PsGpOikq ij_Co3|$t L,S@fW4bo1q79iؔ+P"FWwn5gz8+-y+uSkI34,ٹwӱX]Q0Y'u\U>6^0 @ƍؚpuQXM_X[>R^1&t۵i/fL/ꚪq Wu<28NHSK|7N'Ͼ2S+ӿEC'SF?w~ʃwR((qE1bْ|jjRt2+Vh#O+o//jRr .ì9GKux]lŪ iؙR :K&aR5LbX ©\/sV_?O}M_X)tjJ[ˊo^qQК[GWbJrcletUxq_3 ;SJ9dJWq,ko]0 ABۖ:buSZ~ !{o݇VڝSZ9 CȺCw./t 2l֜>b]c}rp<uw)W}er9?ʛ{n()ڋrQǩ[~ |N+ ,_qLsΔdeu\[$@f ލc[F,yZ.YkoKKfg27bSd?ȤoM-YJ@1tE\AN> ž@9[ͭ_έn eޯ~V=!^|v[t[|ɼRXT)4,;SJ9q,:af0@ ip``j guw?wߐky!^Wd͚3}3CȺVҺWC 3\|;@mބ}e>,8N(OY #܌g~;s ;A?|5UK ;"O $ee'#dM_XY72|OSoyb5_5gz,*Lo+}eq9ֵ7^べfbEQ+]4kUxu,d],v5qK?Ᾱf›HN-^:?t(rvΔde'gp`"5UN03` #TMg]2L-ȱ?-ly3bL7׆u݉>*t NIsk+on\ .8~OcMᲯo(wߺ!^:oE!@鈕.V^k7cnB8UW,[RW%%SpJo>u甶Ax=R^17NA鲯,Bo|}sylh~ dݔpEoඦx XYlYoYٙ;SJ9gօj*@ϱ䯯~kk  ˮ(wpt9/,h/g+}8t{扵/{5t Nf}XY,tG̘}=ʂc_Y-NnkBTT-_q.t滮o[ĮzpWY[Ysδ@ٙRj%zN5ySj*/9ԕ:.0-\`26݇Vv'2y(V }斏uEGn)8Ix7f G>zgžpW?ӿ9x77wѬA8ysz|}>}h[>l84V|f_pL)%/mJ:USyןO5* @XoOnIKfД}%W_:B폆hnm|.hƫC b+Y:%W?ӿ軽JAuM/AȺf=)Ȥ~ˮ(t 2I ;ӢagJ2s撚cC1^36`T GFjO4JWfۦzuG%ٞkmEþ29Ϝ͙rŗ LZ|ɼGY:5偕_XygNA83oδ(ٙRRxb߫㹓J"US{9w5]lI^8WOM- Fsk_:Ev%%_wWѧ,p%ţxKݳLe_Y|+ӿߜQnXĚZ|K@vMik=)m-SEsz|}fL)%ud250 p*TM=ǖmO2xmLT ŏbSdIOמ)8XĦ<>0*VBW1|or77XYl}C dWuM{ Q]SemδٙRRmumMb'MT98HW  XY쳷}"S?}zW6nK孆xgnHk֜sN]\~_K硙" yh&{z@m Ka[5KiML1)9pYH(rllx`h5e;_^kvH0 Č4jg`yMwǐC 6}}yT&ң)04_vƎ}j_]@bRB֘&pJ!kvRsicZ65K5v?œ,vq}t Tݦ+in0?RIlP/f$`[Xƨ=EeO9YsΒNqrtszVB~`nE?ͭW@T8l<ԕ҆XIvn^\V;oN0K'6T(J]tGzʪSV2ŕyh&)p$UlZMpg\ZJՂa&rr 9- 9m-ZtUS;X^#C`?0.IfW0\.ׂe2-Đ+q)JaW˿&ٹcNnEbO/(vSPwS.^f[(!0)Ge '`f\։ϛs})ZMщo,Jz寨ѣQGtolΘ=ot sU&C`?0$œnt Znt u%nX˿&͋!fmS)f/rr 9T(رk+ʶ=@:V_3[sͱ7\,/%ƕPWbpԕX5Ce|R!aX&N K߲"ݛ*vq}t tT>*S(9pY~:px ӥZMTGW0ڭ%x=>0`7,1Put: %&%_vFѴ|&ңF:~` r,ר>P u%D]i+,Z_3T<4"yhM/͸NHOUYt tT)*S(9pC dž^ES >jʦZN=p8i|T,7|>9=u|)\[Na][_ ;S`0;od?0wh[{zۣ)`;ԕ&J[a,ݼ;LlmMړ^ իl(Ce 13.<{iZ~40VS6{05Z0`+cԡZ4u4Kd3M*(Tᮺ{S`0;o͒N'{[.ݛ߲B:lG]i+,Z_Õ6硙t<%%&%OlYY^][_9t \T *S9p]1` 0L/G|Ea>`V-{MѴ|ٽm7('>' #*f}'{!Џ#E]i+,_cPښ iEy)j#.Z(2(cGe 13.WkqVSaaЗ~TͭU < X5˿?M$[yF*œ*`<͞7C:lAUS2QL6fE{υ#ņq7]w`CՔxB$`+Y^c{UUVGuh7Oo{]8K{n]K:-bxptq2Ju}k >ݯRҽ[t VoX"Y*I )` *0)Ee 13.zw0;I* Ք</ĆYL `}-xu[nҽsβK3Zt{xo~1JLJԮNatq,Ju}k T(T]a0ؤ‰rfIp` -6Nf2˜ GBgcCϸ4'h5ew]k0>}\2hV2PL=WHJ &N~&lN1D] PW˿b@%̿˲QWIit VoX").Y)sQxpx!e޽}zT}iRDvƮޠ3wm}%N8mw|ENPWlԕ僧k@yfךs2rä{SimKHG][_ijhN֣2 i4-?{`EpDTff p ;Gq؂,3lᮺ{SV8MskHto 0u%,@]i,˿Feg3.k thz*ϖoI-ǹ9ʣ2*S(9pO7f]3Q* +T ^]L-Ǥ#tL:,3t06G'>'X^p,CwɴP1ԕ u}; ,v_skZnt a]R(AM%ʼnI ) ;ҳ'S2*S(9psg[KF˩'d!㆛}|:.n=܅2>475JjWp,J 6X#F+a(j,"R<ɴ5]ߟ&b^Z:`:*SȢ2zzs3 FwaZ k:c?zpyǗM-[9y>z/ZA:.xRDp=;T1? R=((+r oOKܒ̺JwgكOJLGe 2b GBg=qi^/ ɦЏVSq˴?=,%9*Ot# f\rR'Y:6>w12P^UV-W4{ Pɶg{p%)@ 65J2.;$zͭYv:`[[aɢ* uHP]ߟ&t}ovGG/65WukoA5{sK7#==W-8)af\dewƆYPG8r o.VS5/~X:|`fIis_֭ݲI%?Rͭ^d >/;Cm^'Mֳ;ǻTm;suOwɏWyr2coPf` Ӥ‰sκhuXPW2;RRZe͹{!UlQ\.׭7'*~G!'7x &64 n[n`p-fBV a ]^x̼][_PILf>Ds}JݽdSpj*#~i23U?hPn{|/)55zShZ~AQިo[~tkS|ES==s?4Q̈npքrMl6Omt0e/81ᣮx X5J2b8Cć'G4egL)n˹f߭7]s5 R26 ,Oޟ]扣6lJiLGCcTnٶhi}©N(رj񆂢;8 ,yJ~[[aɢҪQjoܴN:h%܏5=zԻ/Xv^l弩^{Mx,Lz/bljwzFt u+G2{Y,eYi7 zTw?z#c\ v\ \RZ|O5kѿto܅0j6eg,XzwfNFXAJNnEH#\ozB[]ݑe>) LQ& 3s[Mr&-,a 4OuqfꍲyTtcՔMjjhwߘ%EkkK0ENnl]}0-yfmm,z/}c;>K1Ao6`2e+ϖ+rt+X5ʢXvx͠?l弒'WE 2BM.˗ziڧAQ #BEbӳl;ңzj eL/EejTaf.XwM5F@{.rg\Z3.-%s֢*M-'Nb=z#W T4z/+jSV_Pd[w]$v3\e/Mf`(/n[(a4^:a:]#\1q_c@YW_z3auPWߋk g{S-;u{{': ̞#1T@PO(رjmSHDZBa|"`t;d>S55w-( dWFѓ(EBez*S[2U3sGb\.W5i5sKh ޒY(@\*HPt Sߴ1e%&篲=ض|ES؅ՏnEUhU^x3eǢ;Sx⇹_t u@ԕF]$b(?`ٹld[{_Ży~?Xw]*pPU@')qKލQX|tQǟNqEM o6ӳ"+!1T%6f\.W8Y[K:cC^~` vT&B`*,Kzo.)-2/p7قinM:?YjG[k/n̾ V9`to ue?QT˿1,eYTNg[[yVkjh]TZŶ~hEqgQ ?3`OCXTZ5d}o[2Y*~TvFe{s3 GxWD8'T?Ӟgۼ3eII ͕6J IDATL #EO,}N:3{ bmugՇ~ >jqEy)0JuhQW:˿X5 PgV[ymUBPcQi7KĤx#F# v_T_:Cӣzj ~*Ӹ'=9@h5B&K%/yy;} Olmfϛm ?H +BLe<1)A:x=pl54q u%uPW: ˿,(/'7˲M_ ?l% {A:,P{&7ĭ7KG0XtѨۼfb9*` *S*xAejn0?RIh5‘P_|lxU0Ht 456iNaĤ %UmA^Xxt dTb^_8ྷ\.{L'IQWRWuQJK  w-ǥq 9V٭ &HG|R{,*:cQ4ۼ6>iq4g GBgcCϸ4)ZMű:?46_`q#;9TU4-'WS<ɖ#`0EyT2:]?_*Q]*Տn p) JJS2++GX,,^eg13ڄk/,{PC:Ȅ|6O;& j|a`GEyƪSݑe>9ʔ4P: 3sN[s}Tk8,@jd/::_:4]1fϛaY+lk9 K#8v⇛ZG8o5KaCԕAFXU˿ ާ5+hά~83%j[[>`PoI0EZG:(`tc7 Tl^uJ [Y_vt ueXU%s,>M)ɿi{zvpMv.P(NaάBAgk fGi{*Se8u8f3ϴ^k=M0 X{>vk 'e`wy#ݏ#6wSb K1զu0,t tGz^V3v675{d`6֭NRԕ˿ Yͻc'n~NK}zt]st=*)-NaPcQiUm M?-K؇T*qԥ 3sG(p2ZMŽSoNȸC* p[t#5@:r=9Hޠ3n:t ~<[:uToQi!b30BNnR+A] DVSü,wsԞ^ͭ1w#9Tmr?0+ѱMWz?> UIAP* cf8S8Y[KD{0-Ab0RMF[@:r55rZ\{_fɆ1C_g^:BQ{?pHX Tla30łe3S8uJ+a1/ݼ90;vJGmJoTlp??v&wsĞTIιbf8< 5׷Qf mU5&0~$غ~tS̸β2Aw=\yt ~<[:nE55ʈf`: xԕꡮX;R˿e++9Mo8.t[+[9O:Xatk^־v%YL v,{ZzT/Ae0*S(9L gW* LRDWϗɉWѷJfO`}Csk +x/S`Xst+Vy'PÌ,(ʓNPԕJX/˿n;SUV-BFUYuwG:p:{tɏ]{[V(ZUyO97p)'LLS 1bz =hXVSh9?|$2PMNaHOSCtUU+%%tpن.PJZm#j~_f`AskOo{T:QW*c7.8qy-G5i?^MɯJ$̞Z~ESRp\d$jJH/z>6LQ0 )(ʻg]).#xu[)0HϪS`X,O:#=|R:P/^ <+&vSI>1C]0JXx![RZd뮁7H0k6儮w? œ?=㱲 'TLj rݒY(pZMӍ9IRI؇/;cǾOt#n75JMk#`0'Nʺ'\^_h*֣ң:kJGpJQWb,ڜo~ѭR׭NaVJ\.mqY8&FҽS,|ڗG/;OU9'TQB L-kkr(=93.tuw9T?#I A 0;4ezt\իʪS`JJ3ųCCyC8R<[ԲԕN@] kg˿ qzwJfObO/;V 2IGw%[<5ezf\H3ZN0QlQLfgnO5MϞ<" pZM)dewƆdM 8/;#PbRD Pyt!<2ƩWw (qO`Mk#4 ']8ko ꣮tJX_ے]3NmH_ ,y 38̜,6JJ)Orf7և3u拳~xo @PB||1jr]~ R H|a5 *0_vFy;o}p}\.׾=# -غ~t \3ܵɿ]:F C)n4?t 2=!B}ԕ=/NB/JG#?/iȸZ#ītojNnV>@Ηӳ;+[9(O:jO.FMEK.f\.{.,6Lt(;/-[Kz|HH6t!8Ɋ}U2'3+_۴N:Fƛ.,zTWE8KG'qB\I ,*8J&a׆ėo5[6y:]{H-@?)|IinM:z/HGPD'T}ҽSL^~̹G?^SCt@@M OLqq#وp#P^[;'x'}y'Քj>9jrM_>~U0 U_9$a*M- 5 f]+r=NNimt;ۼG:F#ǟ.2rj`d7l}SB =QͭI0S_HGPGIGpĤܬS}믞*[9O:TS)3sj;8px ӥN@)5^''^QZ:(t ң)0iרm`t 6r S(Xe_oFtSsH:B|PRX;oLJ0Έtu??I$O*C-+XC^Ղp~P^e}dž^EDRS˩RI`vm}%w+jT}n5injhN+(SeuHG`/[{zۣ)TC] 2, /;#1)A:kۯSć]~Go|);o͒NaƣHGPGSCkwG:.-=K'T "x-ZӴiajJMH/z>6ixS.koYsM#`LJJ#OJ>9+=t8F] @ ˿oAQtuGzz9 `Gw'œ,`EyM )KG? AM_ԾV:*ݛ:硙ط7#LCeL탙9`7Ms6`$t2JCңz~]=KPֱMYムqtu|tqcRIJS1JX5  &HG0> JGWJ~tJ^pte55lkNc$&%yh杕r,񔜴+Y^YCɏNɋaf?苞 SVS=Ul УuH1](رk+){ǟNp[t5td{})u%A,Ǟ˿J╒9i*{1?1 ^{o܅FRrҮdye %?:%/r9`:?8%E? IDATP* $ZM˶iRIJ#=?YypQ ۼGa]`t 7#ͭKWԕdk{.*IGWJ~tJ^%IGSCkKq0X'SAQtJNڕ,GE03 )0RY@t u%_X5˿ѣzSCtxm#Aq~ߕM}i;egHgp1*ӱ203 wkMn C)GPu{CCw{6²"Ht uQ}݊Z0F- 鑎 .3<8e_~tΞ9'!wJN#/8;)`) sΒzv +wC=3z{op`ZM9B[݁\d$x|!_xrwx2S#왈tqIskOo{T:E`,˿负<-!wJϛ&Y`yMgK:̒P_=ΐzv +wC=3t;u_DJՔ##ګ|a3ز'C,žttGzkSWtt*ݛ߲B:QWq_Cksgo8&|+x?#8Kӣt hRCIit.՘cfс\d$bh5u~{im"03Ux?:`om)[3.Y'!7;6"ԕl_C|Wÿ/MPijhNM ^G:̕]Ǥ#ޤ]b}]P 3s;[@:V-Na,th;$Gg^:JhC3KJS[`fX/ EΟ =Ҽ` jʉNw}{?*N\~(!JBА'Я7u=OڄFǵ,*dV &TBv1' ш1äcGd8y{Y* _LN" xǿ/򸼇~O¸+XQI6 <+xW\T#WWYC 2JWc 8"k ~i=,X5e].zDK fӢS˻un (L ;> `L+o(5~) 4WU+]*.,aUEG::Ңlt,&1b20:B/o;cՔuy^N<[B=yQUr7[@x,.fа` <d q}i:u5 ݷX(8҃Aphx 8{$x5nrX5e]>brb\` 6]5+ UUYV-]90?H'qywo}S\ ,xf/J'tHozr0ww/ŭTN_toS+ V7P9L9`m㡗R%IjҼ] Ǟ%UHKOa۔fs6~= aϭxE:'K'VWYVIW\ mx?nbb]0VMYZHeʭR%ElP\Xsll$%-Y:`q17+ٟN3~錑z@`"GAt zAƒc@@a5'#3ktNCzU TAdPxe}T `FW޼w|M`(i)/\/]!htk7IW  a6񖛥<.֍+ ̅̕M#tBKg Jy\^͚MߛSѢuKW;7XEz@0N1<)x?).VlC%̹IWihi:)]!{ytBiQod ZkV\ \x/̚EiEݺ?qyo*\bKIN)#-=Ş$](t,Ld'C/J.U&` \#OΙ-]!QP{Y":ܾ}UpS[m6?f'f|mA̕Lǂyo{_:N9_KR غ ߛXSjXqt,MCUd(SAdYH p)[csaZקi=, ,wߒ@`qtd45NY_C?ѫs%_:AtBKgԔ) pV>Ak7}o{YmzdH'Ҕ<+9^E:%0Nut ~̦`X5[Heʭ`8Z?VYV뒮oqy+ݩ >WWp)] s%<5V)Ψ)SAFQf)':v,XRQO:EiIW<+9^E:%0>N)=zyJª)l6cnY0+!qc* aϭx%]\bUJ'_\x{jtϭ^+OU:!3:Ԥ ߓ[RNȾ+( }s0bٹ`]JڙLGNYt^8}eLԸ Sr`l6sapsi9zn!U`lg;O<|2~ĸ؛{S`>dcM^ IW۾ z +~InNpr%\ xt wGoS^x /2p7z9eա;z/q(ytWrқ4%0,NIZB/R%Yj )2-qT ߰&IWHWUtr3[K:!(My<@@\ xtMRZEfEZ&]~J>KIV0nnv i`Ԃ%[;=EVB <3)0F:鉾KI1)j t>x?nb\,oB(m ,!4P:A_?C:Uo}W\ x\NH'";gtR -Rr'SWӰ&C-[́=}𹇿3_:]QK'~T}xh8fIˌ9R%)j Wy^N zaL7GS:C/R%j u&q=9a\ 0(.,/\&IH5U.NKLlc* ((W)kbD*=g@B+y [CZg+ Yu{* lI,ZYV&w,X8_yjpnNu)|g2&S`8q'&` `Xu=zn!U \EJ*?ȕNJ'hރS+\m@x0W"t|wHo˘qt|GoپZΙ]*jC7OUFKN_tFK'>ƫ=҈N>k ~iZF*\US K烗&, ,<.ۦ1{t]# :Ң͕EXSa\ ^?$*H ǖUq+tCZeInNЗ>lX: ;7uqat $S{i͖;kWf]x\ʲꕏo~󞼁S* 0&Sa2fUpT `XZB/Jm@wm~I? Nq3JWl: IDAT~^]#-=ea ױ&)1Wüb2{~ u{<.NN -J[" SS+p7M YW8-=EmSN=t߲p`ϡOH O@ 0dXI㘨qwv` `@D pE)) ~O" ttV>=5IumS{KE0W"CiQF|yIWHNsfK'J'|_:jJLJVvl,6ާ;,2;;;`q3R{ `4jLC1dX\xeZ4X5:8J0E@8^wK'ş,&6zset먫iPo[1W"K'+9%ѱet<ǖU)V?\wNNϐNES )&6zu2 &=[_qw,Qx;y\Rǎ[V\-XE0CdX_:711. pC?mLVNIJ'E?J'.-=׵T\X뒮0W"!׼r/Ι=+4HW)y$̚@.'a{ZvW\ -p%9k  1Z)0Lk\,KBH|~OO17 \Ə( vo{'KWez!zx7!v/4s3ayn+[n0>J=0W9y6-=EBGZVTDGWE=5d'3'%:}] *;ԱC"~q쫪NbbW/SɱeK(G Krf.*Ys5 &S1ǘqbo [0)I_ ^fiv.SMhVMJﺦYҢ^y:b/[)_epJ:gc[{Uia1Ws%H]MCӏJW+.~ʢ%9kCl,Nm',[#{jR֜ ] P\ynMYS=xp>3;w֯W,.wg4&S1#3o ]rWt`hx+>)K:!zt][K<`=5I:J;:> s% b*˪$-=+"S+tTUU>lX/!eIc nr-Jxj+&6zu2 ɶ 'grJb;?Ιkat`! @ML c2F9`q,KI%]00VMa.zDK w"N|it]4^7]pps+tYH97R.^6sntE$D|""!q¼aOMʚ!]~^:]4O: FvT:)J;d$&S`t8u&iwv` `106<6y?zKk`"Ν%] i)j6/Y+*w7~T:\ ybf+uee!_g>V" 6WiQt^:]:A߼'/,NfԊןe(1/\&t"$;gds+^q6 }"9%Q:P zL1NdX\٣i?=!Dt] ^NrE*PYSA+X*˪%J@TUW/З *˪#p,_E-c0oAZzttp6v+t5'Þ$]H&˟8a -•d"/Nd&SD'sJpo US\[eFa_N͜"@ntE Oi.-,]`DR"G{z-u7-]˫zgOMZjuNU{]hQں˥+ ǏѢY^ڻ9[;ܾ?1*Z6^fT `>4W`R1e7gJ'J'I:!Sv,`P<.֍+0J̕ٻtBDzOE%W6Ib#&6ZBGUW,Vu]֜ 40-[0m'lٻŸNd&SD'sJ==}Wv2~ĸ؛{#`EO㘨qvattBMLN<ŏz秦~s@=j[NJW`4+SWGftE8ly=u5 C^|qľK+FS+u wI'Bm\.]SugjQ̹ͻ-lX>hNd&Sd@I>u^~sJ`sջL̫Q˘qtBk ~iZF@0\smL-3k_j=2S_ 7 ;go o4wJW`Ę++ b<.o+hQZ㩭yt+I[b)-Jn)ɯqyz/G+JIKO a=S6l:3oNK֜ HefM}x7"S>Ԫ) na1&i'S'sJjv pT `p}i ?=Sǥ`~O bbb Ol׻ QQȢCaM&.-25JJ bM\+-zӦ)"F>`IfTU:{z/sa9[;}]ТuKW ~GEi3fP ,tB8tp)Xj4$qLԸ SZax1Q1F͞5'CFԱ"s~#i)/ {j;P_%m < s%s%D-]!f`׌^fefMZ㩘h1mngckQ)qݰE!̚ۿHKO837^:A/Ys2[de-'D,[WfOY#~/o ɔ,&S'sJjLϐ*ıj #pOigKH"QSn>6Z]LeYSn97;PO]Mߓ1W2W@TUK'KHP\5uWןa(~'ukQJѢ زj qC"R;̨ZZnr GvY i)? Ȗc*V|tpq0Td@I>|2~x` USw1x91.Q0.^aY@@B^Ll#O̯mƹ/%haނq30V\X뒮0W`i`s[oV޴y}U"_تmHNIաR¼ͻsgY{ B;Ǥ2snA~bJVH'|.9%Tm.ǖU#;g~*jLmL&8b(s<2>CŪ)\[eFx#O̗󸼍G+"&6z_*U9;gt-;g߼Xۼ/E< se(Jۮ-%`oxmu27ƭdm'N ~u)Z6pY8E^+p</m+]!l O6-NXm͋q+;ko]ٮKY$39//PoaS ZLALCԘ8PS}cA10<_-S>KI w4&&LPKnաR^r:jZ(vj"E>=;yk ,wy\+\ WU !F8'?gNi,]̢ٷ9u 8Ǧi 4pjo]Y{{"3k? w3-~CtqyϜ<慴̬V¼F͕OV"?ڏ¼JpL:5L E|2ōp2O:Oֻsؗ bFEOp?_LԸxM08{j"Ux\#3fJNLlw>9Sz&&3k?fߗ13‹Rǎ͟(bJî-UxJXB~rHx!dCs2fܝ4V㽑][K6ɕ߲@i)V>y\p"c3Mm oxy ۴|IZ =5iOJW MBGP>{jҺU}Z&r L 'S 9%5ޒi)ɪ)X0bmOy0x9m?`A B+kN/VCdG{l|CQ¼6mp䉷w0@ˆ:OEbsas%Ѿ%Yﺴ(-9%1;eVv@g>lo{N}^cÿKߞ43ܓ k5_awY,N_yO??j=pb.[tBOEF_.~.UudfM3눉_.n+TFauE]NtL 2F8C?lx!@I4q}mlaˆcn3{kg$pb xYw: āoTv{.?m6[]b0~M [±zM`X<.o=tr+`JtTJW%$N9737N_e ƹ;+¯Q+}R:fj8E_#.~Ydv';.ƍ/j*m'NK'`{8۴|IZ g7+TkU_"#3k~u[WΫ11id2 8C'lx!@Xrmհj >6%ˌ9ͮ#=@oxM:ah1/,eM]TktLJj`*˪sfH`+ǂF9abb'/.m f,~r巔:1F/C?~@:#PYV_Vu^KKO̚llpS\-Т:S;:Oa0o_ݩy-Sb25&Sg2 8PRw&\)1{dHbFOWnԴ_1)VM@yf`G/][ꝟ{%1W"۷>A?W9yݡn:Ν%]u/Y+]z\np=>hlBeOM͙6m_)W쫪mi:)]`D̚M}+`u}oJeY5j@o?/0bɳ:Zit3{ރKW4>% F`d@Ug~ٻKǪ)(6yJ:@4p7CYntI[7NA? PY:x^Z]:AGi)YS+0Z&]8&`X9Rx'sJ ƪ)\[eFlD&GAy?0@mN0W"f_Um']pUJW\F:*;xT:lluIW'~$s}\x [N ;bLcM^ X5ѻfQߤ;cX `w7+cN s% "llmosKW@M@ctF=NQB℅y +p} ̜)]3p<.t0\Lbd@I-gzÒR%@j cr'qLԸx` +,ʚM s% bXq)i~'55iNTU^ђǤpԤ>)] 0B'S 9%u^8}5cKuj c/E2RI}zF:$qѬ/aN1J@İUUKW f_UmK z wI'(!q¼ʢh moH'#d3dq2 KEj c{z./MYe# $z+0\̕Hb!")(X\,]1G6啮Xmj\RtEi)080#&SDY&S 9%/Ī)\[]ӥJXV?PW0PZeYu{[(.,uIW`+ÊCDobAcG_e (ۥɳ:JH0ot>gOMzr巤+`>>t\: &SDY&S 9Uz9m, bX5\Jr˝19vi 70V<+1e-M'+6weYtp>,u巆-)|L:Т4 L?P!])"\)Nz"t_Jc`EO(-zrV|Cqy. IDATW+ gc+ o"̕Hrw{+nڥ%#PO@[%] HWز*9%Q)`LL7M'sr}XӴ{Y1@j aC/3JXσ Q 퓮p1W"m T`mW:ddn-7^:A_K N̬_R:&Sʤ)>h KSJ` eq7 n/-];`WD+̕l6[]M[{ߕn]MtňuIWKK+fe}+t8aa KۼHҤ+`>,7LN'sJp{IDK zc£zyWtV.viIG*kos||tLtFzcTtRii:Y!]1JWJ'4{\^ ZRtus}\x I'd =z2 8PR٫v%gT BxD pmxSnS0ǿN`8wm~I?0#ZV&SYVͽ:&\ ]1W7t+N_4prƨ^^Iؽ g KY$]aE9g͔)uUJWad2dAp2$w1x?nb\͂=X5:8J0EUt{YE˷]((,]bN+\׿|߻=Phj߶ lݸ[:z񸼭yX:rIϽtjזNŒdd@U>u^N*u쐮3gcoJW:ܾʲj D¾ھ]4O:*:(M]Z"]dd@Iݽ|O1Q&'Lª)YGיǚq۽1;KK=0%@4wJW`d+1j̕F{z1R [YVO )(X\,]9X}X:A_1ѫIW(nuD cƇ0bLq#(s<2>Cf-g^%N*oN`&7?1Rn@k71 s%F:v494Tn޵KKz/KW~/9nYmo]4O:AeYS^ U\&S )9%qbo)Yw~t[{*]d+#nPN(|0Wb+7+`tmoЗɏ^9VWU+]򸼭WLle=ݗ+`Js+^"a}-_\@'By^N'cMN" K烗&&{,%9kQt{7u]Mt,퓮h0W"s%]6-(El[{vtdS+pr 1dd@Im㡗R%@رj zG'  KWPMqa=xE-,.f01)J `j_XU{Y:2z/b_UtmS`ev ؽ g1-J[qtEmnѱz'wPLdd@I>|2~ĸ؛{0brTǷdKRǎVU(z %9k-Aw7+0J̕`1u5 y[G:{jC Gr&L!m}tdS+qye4^I0&Sb2ōp2v_KWR21~` sxݰU:4qwZN28wU IH "A4J$i0a*Sw-uj[Wz׆ݾ]ݳv]/\`jnsQBI h&$p?;=E Iy~ :0ԗ+bY;qk>mAEO<ز1C ݉~K@N۶y)1֕źuвNs=-<ڲށAx=wy>:X<'tBm[EkM]xGW Ls)#Ι9^ܚ9ΜlE@S5w=sl,T ;~ l~]๡chQ-<>۶yghj:irٲR׬JH 3aeJ(|V{z;Κ27`8=SZu'h ZfC,%u%Wݻicԍ(n^mkS0+Gu%@Aş۴~4iʛ>L1ƞgBG#H5LjླྀhJreYA+FD+չ<5xsL{ݳ6 =_~FD9`h ;KAwt|iWüsC q7Wdyv~W/yB(b)>LgeJNpf䫽wΞx^֎EՔ(:.5N* 8݉t]"8E 78ŝ|Z)ƺ+Fɶ;S7>“yo^{^o?*炬?bc*6K\]N$Y|ko kxCڷvbeʔK/73)}ڳUMCTMƾ^;{h4zAMa#wsWwݺaD;T;cVMqɸй}lH]6 uk6lZmA̺Һ`̜pgoNaDW[>D"ퟻq*3w嗂m}9Zz]7ِ֭jPJ\1k Q%-Uo?}A2+S'"g@<.FgM"֎USHdƤzUSUǓ=oD"{^ݺam\0NxPoƋϟ:mR= =y<-}= ߬M `] IDnXd WeAt]$SyΒ미7/,WZ__闂>lgB U7,]3dӵS3 O]f IDAT굠To= "V[ymtT$5"WDk"Z: >VYJ_Do<`l=93sf6`1f] ب_8ʳ&ba),&I.U;5chX{ϫοwII|i)0}EiezTBNUj`b5HzOOfKΫ<";Vod JLfAߛwߞΝ/i3dğwys.b /<Ϋ_VsS݉oyuޭ+7FPN[@N^53H7ˋd;USvՅ*.*ME'52ȅq:ӹ`Ww7\a] H[8wSgU/ށtk K7,]2ϩI5d]߈#=?vrORg_OjRWZ_ݺafC`,YAe 9G]P<{^ix!`\?.N}4n_INDZz˝'yɺ7,oD" 3SX^\2}mhXv='D"%;N"ȼs+*"H*-HNNox==Hd*R(h^8yE~}DbOoϩv Yg'UTMއ.F߾uvC<ȬL9pPIT"'X=qf0}`8&`8)USd{ht֔ o[5TMob Tk sB%R5EV<{'=VV*j^O"]\T2jlߒ9\* Iߐ96^* ObGr}@JcEieYɄyཨ"ٽ;Fk/ `xlϫ* CYqZլPINjd2=֞U0 USdDz4VQ0 :MoGYS J٥m̱aPINmqPIཨ"$zC}鱲,VV2!`a=ړ*jwR5Ew=s=L34USdDz4VQ0 :jOoGYS )jm7d JpRvngOi TM񁡾XY++00Ic5@DoߡyTk3dž C%USd㛓dzqֹ /:~4=V)rBYS 07wgJUS䄶3ǺXC$'B2L3:7`P5E<{'=VV*jޡxz(:nzpY(t -c}š)r]CǏI5 /=ړ+J+J&C!S5Ex{wz;6^0 $2dž B%"gv9N* I>=L3"$zLo*j^[h4:k܀a(X%-cc塒ԫ[3SB%"$zC}鱢dB<<{'=/.UCaR5Eu`[0}A$'̱PI(X1lO&z̀a?c0"t՞ގF `xovk/ ¤jjqPINd2U j?xhOz_\ `xUeਚ"'vH..*U 0Dz{|qF)A{|KX{y$'նC0}a$USDo|`/=VVL`鱲,0 yOybׁmcރ;3B%!署"O>=L3͙)Ӫf C~S5Ex=F/i`xUe!"ڹ5s9yN$'ߒ9\* MHO"=*jF7{'=VVL|jҶC0}a$''і9W=/T)J7>0ԗ+bZd2kϪ|U:}gO@z=l$)LiFzUSYx\ieY,!UV2tb$UL;7DH?1thOGSۡs0oLWME"X)F)>,0r҉eʲE% 'λ, @HxO<8ft#G{*J+ScEieYɄ#aSOTMNh=IgK" >&ԝW;bޒǓ=oeWZ_Omny%Cl0K&&L,VY{TM-Z^]W9zV%ugC9wG"m;8;عL JqQIbZfTTTț}f`Ƿ|pa_pq7C'/.U$zaSCnX$U):&E3w;Q )݉ÑHdߞ#=?UPǕT*M0dBWWϪ0zVeaj4TM)RDf6LD"H} ;{;wrNfTG'zߌ:v4l0F]MˋRcęagTMZ;~~쏤dž _|'nXdч~ʳ**Ƈ+:?-T=oyu+;^W>@(ek*+*byʙNUj %UAS{w$=;0)+P;vR}S$ӟ0t.F^;{h4zAMa#7TMGJSceYdBMs\Ŝ>ڬ:e*Wψ˧Ү?mw@ޫ,\;dBճ*g6Ly暀ن7xf #H$ҹg{w%Ieʲy{tj9^ܚD"3&իb"o=sv0ݸyX/5ĪĆy^髒Ǔ]ojʿuk6Ə7|*61t Bڴ~}=4zg NCăS}yU#;qwNAx׆5f^R67Թ{wswO$=;DR<ŝ̼prjr|EIeUSO1Lu]eu]eωD"{vwlPV2a gM09áC]U64xp_$y`Ooveg~˕y&ܗԕBk:ŨfǡC#*6qzP/{{$Z=bm+C#7,]RwJO~+B_y{ٓOmG j_o_$ y0N]}柽j^r;Ɨ7.m\T۾9gq0HT3Q\Tr^YS*-6;wع/>zX?bs!Leݳv=~+_>/0,̼p8sE%M_qk*BgF]Uloްtɨn{hhبl0QS]S; b%'S{ j,T) ¡_P7k܀a8m5S?-_Դ__0tJ(|  Tl´Kf/idK;J]7_^|w)Z*Cxߥl+]8U5| %T)҅*G&_ybv=xF_d\'O[+t xO/7'8jV0USX9B%-#u3B)Du3g o @AM|!t (,Jj/%iHοŗ^WίvG_Ϸccf;>b<ߺ㝟0](lg.V1 Ǖ=>;J_j}QW?#/SS;)F]w#WN'qWI-.*U CS5EA<{'=VV ,zOr5 EY Z A5M8j'_qk'G"5?|Տ/?)U2nܻ` IDAT9oz©E?`#b 5u.Z]|͂ofS5y?_;zu'.85?~~MTg䢺nx\i,E-̽-w><6;B%ʝS.M bt%'SֶcX_sq$4USX9B%n܍_|tYYA8M%u?ͦ9{杫\eKYR;>4O~~au]e ?=2濁ޡ+G_}/ˋ7_88T631k򅗜$[HH$rxG7H-k5_XS;5t3r a=Oضyg=+J+J&CR5Eay_ ώ ܱ􏮋ECLU&;_sݿ@X/,6e͋op|Um;o;>/>~߮U*b.t`ԍlUk;%Fjo)Z}χNqm# ;rϣSiړhϫ* K=%s8TҾ_WNH駿\ꅡKEO.rUMTϪ_{@кn +OϻO~~a㢳C8E%v}EndۦԒ<eMsB8MxeCGEu 3 AfdR@0&㪫 l/Mw6>rlӪ'&t1h47i15}iNkcH @%*  01 wsgkwrg>ڒqÍ1PB9s|pb`i5{ۜ;&CiŚ^{B:IlV߹}|٩yN]I't,=B(0u^oH,.E &&۬۲'J-My:*[i3[WK'Gk Twlώ* &|{q.>{ D"FM!m^d%Ux},gtBhvao?,Z4O:1}Ƚ;~fcꂛE).de'۲`OWDVw:-4Ra06s֜iI )!BybU*[Se+ͲeXrݑvs+B?/]Uj:A2˚+UŨ)Ģx~?tXƜa4J+֔V:tqC 2s &:?|=*˴,=|Жe^Ȗm0!BOZ)*h#k-p}6Zf++Vׂ|>Rithn ^N+UHĨ)Ĩ/O.*Ĝd˰/uc@t+g.dpLr8*2PPFcU'o!I_\2iS">.֬;!Bh2T*Ue[.fݳvkaJ]N"zETz!ȥ[J` ".x/}d9S1K+XSZ@XtQSϙ/y$}G*?X( ת[ӦE).Y\rTLPc!Ӧf˰rѵgN|-'Ŀc+pD5f tܱcN=y` " :/s̓*%}N +g6i!M3 V  SE/Z\23xΔop6Ԝulݾ7`~iS"WcaBr&-;OuO|-s-gNK?t*.xe͕*AabW38t14ƄdE-]M hB3got@fܿXАB$s?VymgkU>ZU^dWX(] TtqCUzvmȯVڤmxnth_Vl˰JWֶvz:+PYő K5bbZMYR% 쎴t!Њ} \3ks; wCMGBǃLz*$'Ӧ(fvԻѲم銫zdͷBTUDH2w\DFM!Gn)QbWdVzR e*m t9S%=9S>PTPU{#nǴ)iS@t9!l~ohنM+lcK,][!]@9>XR2cA5Xy=XeO!Ql]Y-*]֯(DY7xlt )0gBGj:vdT~#,3ӦD.$gv*.NyFUzuYYΌKW\C߻W:!aY:uX1Rp5XtR% \9>tt?/K:Iݑ&]N1ĈϑeR%n7X2TTʹ9Њ~or @[ꛪs fc̼TLŚ+Nf۲4S+oæ}lMue%(vGkAtEJW&{/LDFM(/jyfj]No|%`8O^tI )slV?0g*|u©R=0۲'JPY3C.`?q!IPg[/sbGCɆ#%33Rz`" sKWPT٧~AIJنWNeB-+:낗'H "0j BǹcN5k5ߖNV4E~E+Xh6Z8gZwP aVUeӦ[ez`"Fkct̜V/48pI4ȵ H ذIxU= [ҠO=8FM_lP_fjT Due%kt4[衋m,TV9KQgزF& SfT -I[ &(N!]@M8ݺvN ]nR^h^3CpM}Y-#K6h/\,q g r @1 /t>krMY_ U^'IoOe[ȝ2lLwv=;ZNe93 \2w] K/$]=LJII KU1j t6xX(Un]Y>!^s;< ͓Ԕr[֝d=_6v^|ʏ}}#,1gz>>QjlV#M֯(M~/+XSa H:uXN{5z|#7% ɂ={(TwlԋWK[%LV- 5'p]D$}\mYwHWPG]Rk7 XS_V']y{ώ* $}Y̘*1j gꃗ3J@Yd~oOKW>!btVѪ(}]$}xshQ.hH{7bIKҳSgHWPmn/VtWqCydͷtJ ȿ2 \^ , $ 5|IOIIN<r @˝ucoΚ~m(ߜ8sc.h{Ґ/.);e٘:y"ASG_TwCMXS֕Yr{؞+Ky-#K6h˝:w,X;@)p2s;f @LdwIW㧏O(T%3/ubg~SyQo|6rשo(p,'JWP.NWhֻԫքmSk yl@N_ ,'%cY.~2ݒ-U}@:m߲[+tq]1&$L⒙ǫhlg!fUi6KDVV *.}^d'Ŀ~#Yo|E: 9uXN%mbpqٓg@d5/O:U<-]- ǖHWa7g9&9E)zxerRxB [FcV &TLDvj59] oY==S=õ H Slxn.NҧTwz_tYg]2cSŨ) Z)sEqζ:}}Uu!hq{K3'LG>>QDY,gGUnշ]bm/pnY!hKҠO= FMW4% ⡕I'@mԻ+H+?v1!`|7nwP c_ωiy=0nƄi3+LTnJ2?;kACw Vt!\mx:+Мϑ~T QS9#[:m߲[,֔җCxx@] ˙]l]?hשsu:.h 5xX UowS//@6Q[v Ѫ(Jyǫ*?y*?_촤% YMIN 2uZn3C`Z#PC;2sS_ü @%LKtqȲn|uMUǖX)lA۞)@h%0FMi:A`Ԕ('0j h<{]8psͣ_)(ʬy3SM1!j^zi .O>ttI3S=<#xҡF$wڛ> IDAT[9ps29Iݲ̶,s~CQf=-=̜ ~qE1j @6-}Chk]!!O_WVRiķz{N|mjk9gGtt8d@;5b)gIVY}Uw|ctB \mͮ?T﫽Ow7Ϣe# onrX,x^r h2Ue"4(wLζ^rQ/*f2ht}}y-= E^ /-\wM*&۬кnM#K4$M(}}-*>tWN J"ׯ4ԹcS'`/Kf/ Iz]=*[;e· ھe-펴 ϭt-ȏIaVn>އE̕ ׂWθoṕTȢۜ̐ljκQ:$$~5S \9TztE_[&0t_ԹcR'h=ldIκe%[4hּ *U銫w7?|Sq#e?}m߲;O Dt?{_+뜶[EK :'5!lҲ35̐/.Y\rtC{[ DMK+P+>銈Wzϋ@kˊS6lZq~.w`.e͕*F0j &#G%NLm1[L *kxD\uώG}R{}x: O,֔җJW vYӭtEQ/v5ML~7γLNC~s &tHX7mo:XW@԰R+y:7"]<]~tE\~rѢyYΌ>m ދ)lLH8^U \ϑ9,N_'ilVEQV<kt_^Sʩw7K 9np*8>\N-.9:z;[{;[{:z}ށN0leY2l!i53sS8jϡ-n͐w5ޣI0>7fwWT:wR۷QAFa]YKrgWGԣ/JWZg|C0?ixܳv#m_VöEqi&t!Җ)qC[wSiMR{vTI'ߞU,ZUwTSݱ[v!g4^x Ĝ3E%:j:z35IL~q wCIwpO[Fz6ucCIgAΐNBIWDzw銐'oxn嘧\a K/$]@8uXN~y3bzՔ.WtDջU8zمEIW |ޣBE79S=g*?پ{MƳUopzNڴ=gFƙr2h_@TjpG#]+OM:˵ Hue%F!l= D:낗Y\cp}>i ^;HB'`t:N~7//W Y9,}\(_8t꟤Elu͙ ۲}ꫪx7ϲeKn E65yd-˜*fLH+Rg*n~SyCM+X; ,=l1rjώ* GҔh6&$ @,7p4B7 jWy\QwwͪW h2@E٩3E/jH+KZ҃]m;K{4tI_߳T[Sݱ[v nYt٘j6ZE#pL_a\xܷ<*kt'v ieVymϙk}F]Ӝ!Mw#/tL}MR+I f F-^e_=hw~DŽ>ttIWmNG.8F9QQSuk:Ai%U}B銐([;׵{ϯzP' bՓK4 D9$(?w\3hmYk<7CsE;K7Kn%4I#/tῲ(SʑNPS9{:'бeXz ϭDzB|ٚDq)r-è)\^ ,qVSLbʽQhtEHx:YF9?޷%lU>!~rou5-`CIKu>[4\$/Cdfm䪖x('1j Q)`t xlC<WkATLHvVNmR%Ĩ)`<:/ J!=uuNReώG_/<]L ̲+["]e7gEq_E'uݮqoޱO ʏqTfnj~PhAßGٲ̖$7cB٘*]lײTWtԣ/J'Ś7lZ }D ދ#/5% ɂ=)`<[A QixTtOx|/\=;DiwIW :YdZ&Li9+n5ڙ35ڧM}'`҇G\CiR%0AIN@emtpy<ʽv"C߻wA+'˙! U \[wS&,Ha0N|R̩R%}* \9-RvMuGz?|`aU*@t[E/8l菖LzW5$evL m$C~#lq8sζ DY 2o"S _x|h' K_^(3[WKĮ蒮 ʵ7(Ʃ̧CKѓx]>N-[WϼosѢy-!Z;^?޷EeXߟn%(_rsМgcvho6gU^so\wM%GVϞer-,fLH6S+լ+++TptˆmN% t:Ouo߲[y=8N?2]5ߩsu:]c` jC:!k~Qek \99۾e~  5='*q 9KQ̼iM-劮%-)1iMg{p_˙qN* rLrJ')gtI'سttq)]>ٵĊ/ۤJ QS5t̸F@M{V~TkO-']zwsZ M+?@T[Eq#s-|}:IJgk-c_0c|ȏ_f^` Lݜ%c]YŚ"]~,]%W~Ak??D ދ)lLHA1j n]tٓg#GMمk7T6NS,֔^{BQlL%(=2j*L)*69տly397iEXO-lIKq3&$'%DtĬWν!][]'p9OG׻$]{}/$]@l^d%Uc0! ^fYsJ@Pw9I8-ZtҊ5ˎkkOxlI+G: \م *,EQlfi>-ϯڒ_8Ր 5'3AUyNa(uOeڙq6st0!vG6t!jz?~'p/Vt^t1/,-ق11^&`t{M)l5ٻtgr?^]] XSf.,X! GN<9jh6?ʿט᷼flo<+sݜBthoKJTxeߎX&'eFh|ζ^[2RO)Mg?H{wFA:DMW3K+HWZNQ%]avO|Ct})}>.I)QSD5t32wʼC+N(].0yܩ(J{YGf_u @l2 ~U<-O4ʗ67y墮w5t62FM)_85ơ'~>ix9|]Ă8 t/s̕*):"Ś(gR۵?}'V<#]@,ZR#Mʚ!~,Z/d5+_l+> H3(JCɢF6Aߩioektgz:+(J38t1dAx0j PMH2˚+U"Ni,gة~T?^("-úDl*b˲loc‘vdYG?'<%sos2R#(h˶r&hڳJ⺽vXy{`=:kv` €QSjő;VĐw dȝu/_vy("ҽ?vfc(lsH{S̙mca=]c2)x eop"C+ʥ+ƣz_mS1?B|ICˌIN5x/s̓*w7{}-Zt܊_=ۏ*_x퉢E u~+X IDATƄd{:[#i6SfomoYSȖe Oζc[D**ĮttIWSꭃ+bzwt\o+l $.Pc=.fu]%#NNUMمk^3s kdV M=]}B9w@:Ae2Ƙ/e6 -+ѵgJWĢ~囤+4 ^fOΗ*A0j PY/o'w\_H'`lY7VG/-pHuEeJ#d6*bIK >{#O{Y鄉a0S"gIW;4.@a*]1Q۷<=yPիߔNW /L%CĔzws']ke4[g銘?y:+h.x/D1!Yè)@}3{ <]͟2KvOוH-\jwIW@ڲ́#W?xl2r&hՔ.Ċ?_*P߫ߔN~=9̝2W!Ũ) $.e͕*tT`;^gТSB;(沟LKZT оh3(ʞUMuǤ+S(~?LKq t5D8rrSj @8ջZNIW@xN,w֍+["]1&&?e;zrƣtnp{ͨ)˚.D3s}pM}Y\q)r-FM9̝2O'^NNK ooiЖįy_: < ^:hڊ[vKkw+eD/ۤJ:Bth5&$ @8ջIW@eFauw߮i+GI8]ŮR h٘(Jfޗ1u l tQS@(4ƭ%15$G_Ҷ:OuQ%][[wS&,\{d @y:tBevaˊCsCwr+w1!2!Șbs%-@-*~ꁧC­~A"zϮz1)l8x c l>/]P'į._Hn@.NW~(]HJLQŖe io:+3NݣP%9*7<1₿ז4` LhN?8B,-m "@O8M[&CTr!(= PN\ҚBKL㘞K̭1HXےx%KDV%[qWm^?:} !S#߽g7l=ߟ]#^ ;CW'Du~Ң1L8SS0iߞ~L7#k- w+^:()=BW; g;x/c'+SfN\u;D#tKH?z_B'@ݝkCW'%FqY5SLMJOf.sa)ذ_ɕ?uwo C䕟ErF{ko(SX2pjb&cLM陚 up]?~~_mٴ5tKxnڲ/tEֳٮ+}~m,~T-L. UDݝku`-]_^ZW^17tG:ab gEQAQ~qi37t{*˧&fU.sjJ2@r0:{zv-&[w}E@~KաJp`u%C.Y\Zo8957 ]+W׬zY2?ۻ ~7CWجFQT8umGV~ٷcnz9SS2]P8ؘ﹤u[W%sLMt1|dy5#S)ӿ ] ֬ ]L_'ҏΨ U„35S%FqYs L/xwCf|{m7 @fɥ˗[$7lK9Ϟ TcҐHMM pi ; nmٷ`џo56 ]]u0tEVӿL;BWEc@`*׽kȡԱxV-z4ˋ~UC xǣ{2tW ]9=_e&xEcc@cxplG7<[ ] ٧CCWKC0LMiiߞ~JByvb ȵ_)=aswCWLԭ+tWwGߘkS͞x妌26@PK2K{?uT_(;46V"ӕL 9:QX0 dj Ȟo ~j׷ ]6Qjk ]1]r7 ]1s)s_<{1Z%CS0gr޵_j}#wٜ=ޟ5{bmoll`^bs. D15S'X,VUqEPn^kSӇ)%CWL֟W ]ҰoݥfW.͎ji4kC›UTF6|ޓ͏BWB0LMi~wyJ²65X ݖM[7DغkCW $c\uqAQם_5w陚 1/}5BWdGd"xC?d[4x֌S0!LMJO΋p΅c{s{ft~+^Lqϭ'BWLy`^==+W]<1'laUy۫[  bK/yŇ N}놏 hݰ9t0YZo?QvI&)RoO?;gILoYR}A 2ŚՏN>>mW3='(ښ1okkl9=L RSw׭ ݒY~z5|dƺ$z{9~^&)RC=cA~aiQy]= v:xb!0b˦[~Z X^@ИiųD][S]2p^21L wb~mW/ ݒA\巚/^O;BW`[u^씳J 05S)rӡJ2?'B0f<d !FQԾgi˙} wϝT-,r=To9=lduwo } ?9Pwk~C,4T Ldb`hGfϚqjlz5ua-wwݪdnpp }(HH /:V/˜.Z^1m}C[zk z2SAьSS,]乭!a˦CWd5 L̔䘚vз/:{y wٕ5[~7J`DcΗ~z c $6x̜ܙẖi0 TRz#OW ]AeItN?VU\359֑o90 sݹ;wOab}+uwmXm;:^=}TN؀kksg>޲-1ILCC'@.jjrwݪ!%~bpmuaxx8ux7*pBt[CGEgΚqjlw}Oϓ/oyj@P}}OּvWdm~hmG z+)=U+B:a Y8N4 g, É25+?:/v9d 01F6VVRWȶ~l*)=bn 2}?8|dx 3 >?EQţf}r[ w˫p@o>~K-w%=Ǹ ߛ{vO>D#tEc8! S>x%JrՖM[?U7bk˾ágC'qv_}xjjrHݲ&ӵ5wL5fkS#/.MNZ.#2A,/e芐jkS>3+ Ւx#XY~iN)C?__h`$ծ{WVR1; .KB']߾+t([/_<֦(H5{__k M>ǿOּv<#S)֦ϟ>Ɲl@jI+I;|+&n0t#R3 d\hjM?uRǪO׿\頱agcΑK/X߽s9bVQAذ+B'jn{Ňby!0{.i37TP Jێζh͞4}?Zsgێ{zƾcLS\u<65[Jc{+?t0*y 񇟹ׅ0=M}#s珼b \[a8 t%C}#Ǣųf? `H*.s<üdy |.eƫ IDATK\t6/pQͩQEe -,[X\PEQ’27wFQ4j=yL絩z2˂ys!Lwڷ(ZPTV05eׁEg~*u`J;ݰ lIuT]zU~p+S ?e *w*mѕ>56)Ӵݨ3mצҿGQWr#|{yϭ`*&,9,5ްUd=ު,$?oȱLdƆ ;S%\_|tnISK]hj smk6w.+xVi[sgU\)(/[X|\LϵMM X0]~7N[y忿4*+ ]4 .+()=-t%7uD%MWp~7w6p}]CCey B'ўݰy+` #/ZK0׿y7Me|/~~7N䏒۲ip,(tie[ίMҐU0N#ϐ\+. ~KաJ8 C$H?;gI)V^P9/WצҍNשׁf|{m7lzw NQ\u0t|DOБ(Z%R+z>OvȽ}{zޞ0)~K&3J& >[c86SSZڷ/:{yV]eJqC,/VSGذﭫy?92}?8|d8t|D(W-?;\' ֦ +GK5tL':a"b+^7^bJ ±[}EgΚqjI+?s5K+NvRڲiΪmC S46ӿ ]5R+_Afə^`ﷄNɒw ]1~y}{P%)t}Q}P%经}=;Λn½CW@xv}i썢e["udna(NLnMU<'7dޞ0Z߉NH`~N`oPOX48` `j 2]SԱʀ1O|KgRG P-?-td놏 }LDOkE /NX@9l_*[X\2gtݬac ﷄNU;tD:}7L/]Ǫ+Bp 3S2X@.Yq˗_{em%LlRVsm'i {EQwG_[sgbg*q2zmsӏ- ).'!ݞo >C̒c8 Ӿ=S gW]|y[5QIY;w im7x+.)\Z.kSEUG͚ɡ='o` \n:aM'Lg%Rfcj @`oWr)y3ι0`D[w׬q~X^l5=+76l2Ž<(NT_uN"NR6MU_un~o:&]y %N`J$H?V_cj Û{ӏY`\E7QGD}o}Lxǣ{2t-:td0]hvpQ[LEu{kOێ΀1'7&_:a]CUdbȡ1|$SS{zRǂ"Yb_'cbK/o^99cwVg7lnn ÇDQ_w~"%֦]2gtѬac Fd 9t&XqIQ=ױXl', k4_N?VU|:T {6?㔚گ<T_0?l:a=as[m@ =ފ>XdnEdlY}CM{`B|+tLKsmbSS;ӏ J*#ѕL E3KV+OQRzں7=ۃSs<#tdDcBW EQ[?_k>2mj%sF60 >BUuǢy B'0{zRǢųfl@cP%'mI|MZP9'Y^1wj>=7 N lٴu[}%m͝K˧x~ kS_Wz=7®)BZR}0Zӏ($T d=:2Ң3Y经}=Oˋ-]n脉 {n?9FuJՍ-Wqصg_4;ulؼ{ 94`µ  ]S/B{5<<:Y0` G15Yf㢳. Upj#SOoyǛXR}L 6hkV?6|dx`*m͝+)[.:ѵkk'gU^ 5k >)dV s ]`[u~Ң135Y)Jz_iW/[qMu߼2l>rWSBWLvN wlٴ_FuJՍ-_w~(& M_4r~jN6HMg$]]CWTXtm,/&̞MEg- UQLMAf](X⊀1oC'|l^iMW67+9+^v>bt'C'S\u0tjiUEm;:ۚ;SW- 8%s+S^5`9ru敆ԕL 0t 4_I?;P% BW|HSV\s[nC眀W/ښ{oN wGCWzw%GQƖ+nZ sBkSsJ.5l= pҺzIwwݪk0:Mb,C)J#E;e W.$swrTλz튫7ɝ(m5 ;/CW_EQԶ3u|ᢘצ /@P K{NIgg %X^rNҙce%JT^1r花b.9vݝ~M/.:Zy<͚گD#t9BWzZs;S䡞뢙ųf0cd䡞#g^Qmӵ3N ]qgr_^׾鍷olٴ5luO7tB敡] ;CWd _c'XvɭFQޗ?4ǫW\,[P\}9 /5h媋>W[{Z%&-,jYO|9`W*i?@VkIqM?QvIS=D {5_ԹK+/55d۾Ųy+N^ټ.[qeG;N6OY .:7&X5_ }7s+k,$O>ӛz](<-OU]$1~65`ߜO0i7x0tLW/[zŗҬ)P9 ~@^?}ygj*8SS䡞#ǢťE]Iy6);$tĈ敖+]|IMW:= +sίUT09c5vr?e *(,ѫϽZ*(_5mcB4>W}9e K5wlKLޔX/:5w蜌O0ن |+tṭ:al3 fLo6O`[yySgj XK⍋nXU pBWLYE *-t+ɁCZ{{{w'v5Y*FQKY͞?q}1t9l˟qJjIjYjxawg.[S%*/m$(7혢_Z~e7L^:|(t١ hJMMEQe=,^ #Ǣųf? `D#wMUT0`o9?qO$+qxǣ{+C hO[gT*ԎqYU=Y@r(`e媋 GƖ='+?~K `y]W210W_8r<=ƥ}{񢳗*HeW~S+ߎN`Zxv4- /N],Sxu燋"T.-.O[{^vh~ D qpou,dyuLMAvsԱ̀1)ˋ kn&w|[ɁEQջO(^vΙxv.FAQU_ԎP1NDٸ+t5_I?oZҏUW*'9 tgb5 hiU`@r訅kk CU-Vpm;+J; ]LƆC=E3g85`4gj ^SԱʀ1߼r%+&L#[6mV(j~(Z%^؝^PrWSY]:v{u{xC+u0t%Fe*mױX⊀1tV^1r ̫/1M0@ (JoՍowK].p]d+nZ~u ɡP=㱧sGWMg70=׽kxx8uwyc9SS FyrfP%4g,cC'0{#cyɡLe CU W8ʫ&zZxl&?ʋRZT0f:35+9` \nT ]Aiוw`:jl/^]QECmG[ښꂢ@]d..[0AƷGcÇBWJ2ȞMEg- U2͙ce%J驼bdW_|#tWݝk(oՍo5w)\p]dΩ}@o^> ޲ik 2HW210ԗ:VpF䡞1?ofiQ1X랮BWe ?T>t}?8|d8tDQ+5 WV_~ ^q+gwGɺt@87:}:`̴ej rGScUŧCM#w+ ]A Lw ;|+jБү_~]eCU!_[oe["Txdiz d+sBLg wt%C=cҢ=4+?d:~}_W (xbu,/ڲag (np (N(^TO]/(̿anl^{ƣ1rOg `'|Ԓx#KBL[ 9Бѧ*L_ȵkk1:稝l`<{_) ;CWhJ?.:kYoZҏUW*rXݺgu L@I;ޓ+}_ߵy`5EW8TSj+nZ~Ս-M{CБ7Z9tP`rˡh]P_xZc!SS Wrm_K ]AzۏN ]l~(vtn^f/U-6۪rEWn|;T8ksOg `r ~򱍡+tG>Y^0f15`[u,"` c+~? ]Ajmٷe·+[/GQT;Sgqjlg wK;BW⯤K U2 4y=랮BW {ۚ05 4zdmjڰ3[BWSQCPOuY3N 3?ەܟ:NY8€=@θnUټd~Glں1tțrnyjG/Y13ڳ婷B)>Z[56 ]@vhI~D%JSS[~+0nK//t٪`ݝkCWq+`Tꭹ /nR/}(?PfM{g'k$B%G;S0}|? @x{pxf€1ӊ)YC=c~̳J BW w+{#cS%6yn^f/-(a2kSYm媋:'J[s) {oi"-v ߗHϛYZT0f05?SY~i 7$^}_WKxϗ]vCW֦ԗ>]8P'(ښ?Y) KٙQLKEaj rYW2<ԓ:,6S"gWִ B6iUwp\u0t|1֦Jްzųq2 oXC߾io^v`ZV3dbȡBs(S丣f*>%\}b BWI isGM ~0o\Vpuŷ=lAqE;S@VkpNq @ Ԩ@̸T2J欭nr~^ju~=mvkW:{w,J]e@= $@BNcI9ޯ$<Ӝw>w݉H#nmD"XƙjX9ֿ8Mv&݉c)1ÿ_pn^mߙ;ΫZ*IP5/֥sVJG75u˗0~Omټ=t 8/P*t xıxm{u羷aeݭ.G;޳o|[;Zp>^l{JC?:U@OwAJ*) 34|*;VfM0Ovx?:ͻ[!t w޲~xh8t xıWLԁ}ﭙWqV]<=T'I jm_ FF29t 87uӃlKs"7}~7\*oQE믮+/ֽ-oQYw_j['}P |?H&:B`bk=gxcS STMAAhI<{m:7` /ݷn}A>)`$m};_{|TH$i_Mv>o]ueuyD"HixG_KˋsҜxp} xM'Bͻ:D캴*V0LS5"q<]GцҖo *K /B/+mݏܻ``cphŶ/ЗS?R ]=.`j H*}$F6 䱍?kn? ?tu_H&:B覦};_ VXM}]4W7^ T‚2wwG$yKm$:=뷁C߼K(:iVpY)( 6s_* ?zY#mCXs;{ȗS|ıx$[i+Fū>/,_(c+=7}~yܷ>[۟|Z;n{''By`B ߼ھ3w\X4Tj H@O*}$;̟$` m^sw[P };_Ě{B1r6tx'NNl}mx-hy.. ]pV5{;>$yx} #3eAC}=onEYU0MxܱfY$@H&:>Y-O㓟ȗBewN"ݶ-KPl{rk{[[R9!桹ߺU]\Z+^O|kOp>ڻ۞~A0L0ڒ]玗6JTMAaIM~E%Uڀy覦 xyp,w~uB۾җE7;@{kn{<+mGūXz?wP9@euM_X~̫x˻Z>$ywڞm!yCo7=€aUq@G,+.*ɌSz¦ ͣt;œ'ٷuw~==y_(:S'w'&k~*6sk^yC[9w>?h-.~ sΦ :z8HI;\w_g @HC~7p ڊyuQtRU6NgBˆY* P[au''N /ݿo~#_3ϡSIyn־__4'~3W߱S{WpqeuG'Jc +:>Ƚ&۞zn)(pRn=Sz%w\4kE$8t ֣{.m,NʌUe Mmo݆߱USCW  _[7Bqjﯙ]:ıxX7Xi_.nXYG_yC7w$[Ɵ/_x+jKO{:=?Il`wıx @`C~LP*--~%r^6OQ5\pqv\4kmOFԍkW+k'|mݝH&:Bq[?}Ѣh p/޹g%S.h9^TX[X;[v<*kW/QviiN4T2L\CG޹gYNqlJ4?cI=a#USPZMF8T3un<njztSe^tʆP⇾R 0$\A\ :©-+.l_Z*-/nvA ƟOSTa̫xƟOn{ޱ0LY]-H$jlT$WHR5-q<[PwUK♰2vxSRm]rõ,V:QQ2MMo|ߢe([8U7~%mWViT(vN؛8>9_ƙWԾCT$:ڻ10z/&CJ7覦AM}=ݱ7n.$-Vj Wmo݆UkV/]0Z *)olSXY]x킆ߞSZ~_/V(oXYް.יjog|W]<_TDK{;&hH$ŕLHP)ƧԾE3ߛ҅2RTMAKTffhl޾eH$rgo^fż١S;}7&d>:㊲/wC{<ƙVӚhEӯ>ک{;Ӄcm-^3bgR/DlD|Ǒ  $Z[;vυB7<4Ү_?3gG\Zh43VO6O>Q5n]8r)`'@m_ylw  Sw_gw_gI%b3OU6yJnTif~EkWTV̫˄]G{:zMuvwu0z:{wu ::0D"W]{/bgUSHdaRE(/W9C7Ow<_Sd<a՚W\K.橾t$^ڵ鱧V@(哧VMYQ6*6PY]*M_Q+.M> LT$97DOv?R#}TTq`ԽқJ36Dכ\X{~0;Ov7X<c`՚4\~J&Oԉݯt`=nj Nj̊%S+ʪ*ʦM~]~3|=H$vy`"<Ʃdwۯj1H5FFMC*;S "};V XIE)}=a#Mh7S_$3Vfdmټ=$B봍T@ދ'_xeNjj$ g+ _;sǥsVJ]c9USh='"[Y6yJ<@:֛̮K˫bLtJ王f-(L  US[$Ύ5S T:94|*;VO fS5FxJX0Ph^Nk+J2֮͹c}ͲPI”>ٝ]J*&O fR5 IDATlqQK MٝK˫bXo2VrTM%5wl:Tœ/g4J2AE*͎ ~ICçcԺa&"UStwPwu$@:t(:iVpY&USk=gpdvTM0PP^mߙ;.^*Dj 8#_.2T fǩ 3ᨚH=C'cUlf):^R0Ģj 8SGZsǥsVJ3cmPI&USjI<3<<b3&O ((u"`EpE5B% M[j_PwU$),$Ύu J='S 3Nxٝ]J*&O f\Q5]sǺi"|!wfY$㍪)` Gc41` ?<< f\Q5]sǹ/ ('eץe 3~FE@O*}$;̟$` ;=w\4ky$㊪)`:؜;, (trhTvZ0j -}=ٱd%yD".N 3NFQKbkX_,TmhdP5T:>ٝJX0RX3un0ㄪ)`t$J#]u4U0\qA0RddwvTTj۫;sDžKC%'TM%5wl:T fǩ 3F]*LΎXm<@~;t,]G3 )`, uWJ佗;ryUB%TMc1VRQ ou"`TMc$޾3wl:T %^/m $8USi=gpdvTTjٱrA0aN}gX_{y$@;,TM0L@ztXY_`ؕ;^T,TTMc*޾3w\:ge$@~;ܵxx8;VO0L@1ztXY6yJ<@;,.-./̶sks.8;.MPVY1i _DjfM6"iSEѳl'z3WZ^D"]D"G7Ƶ#ȲK**cHN.է>ޓYn?DZ^x%qwxi$sLdkKYPD]TWZ69ʪgr/_> ql#`U+"f$R=&ɓnEjkNG"g$i'#&}wE5ZD`s$YB' F~'\+?.\ghpTDq/spY㥿7.œ'NtC{^iyWcˋo{1lכ8t@$VqnZ)X^ʪgG"+V^D\=ع}U'7\[^8+rJ-pѲ ~CmO>Lt0vjYfœ'3*EњU?vMxE@XW^sy̥x嫮['.kv]uEv7uo?jyG_t2a {3&(#ґ9\YE'ͪ\xkDcJLx<{F uWiEUS+.Zv߹/r@FLWK E:BPhg2=pVS+os&l=i8noZV+ ee}زSxUkV³UkVk{2ތqvDv@VD"KUM3uhfVj "9~Ǯɜ1~8t.tlT[Ж]|LQVIΩo:S׮kd|)t΋g#vH 0z'c%TM:K>ի3-.Zv?Cnj e|hQ4t,VzǮÿp ^ǪWVM dEXy?/~i" ݸv'+uNlg= I#"! Qd~+@ތsf;b4؎)ɮV/͎60Ϙ) (t}=F &UwcۿsաYo?+V^dH)<]: ^GS5y_|jCgƵ/}lw\qWSY5]o~}KCN/-ۍY.ok gx?{s\ٝ]J*&KzR#ٱ(:i%ĕ dO5^uX(.͝*a>kw̑N'F"g3؎0#o)ɟq:& $HGO[e}´]/dfv[(-U.> j ĎyzSJuޭ/<&@Ң֧!Ů*+WK|:8O:#587s \"6g3؎0(#>͉?.?^ U/?.* @9])6W3\Zsi"݂{-pK,ʛ sCSug\ɎxǼ>cSrR+te a8lFvvR)icQS2>a|"eU6ΥFdZ~#rR~da\Qbֳ9Sk_N0ZL>5YymIDTrtB|?0~;vippq}NsLlVqκ 0ZNZ=u+WK+yȏߠR7etc)@<1oUL>5+%X TD^Gb~ifa>lGD9v9sR% )s|tl0s `VپuLppE^!7NbW*ۖ\]e#iTKl9m,kg݆ӥ+y+|c\]d}lt)P66m eLp6b; mH.=~1j 5tϔ*2};7F nY]HAܘ=u@m}᱊+vE:q{m|'κ > hhVq+on Iz{K~`#~2@#>v`ڻv% hQS5a_tD#xfK ;.{^~WaгlGX>h]N\T5@]o*M%+ IDAT"'+`Ңs3Bh׫?LsJ u67vZMlskY(nt$ cgx@blVvKO_0FSS=m=G .`tpFbauhJ;ՄmNv$I@+]a`iM>lU{^ys{mnY7 1 #`eGەiYr-bj!vE+D#Ԅ*vE:HbVܲj=%\Ӧ Uqce)v%X2et #PZ2ނoeNWJ^atXz!C`;؎e'_DS_=mCKׄInWqV f ͻD ,kg݆@l $W}U.qf{CZX1wdGtdD/+wW&0H #`c;XRoԔɂ1a]kؿ\-R%XDlvr κ 3 KW@suxzЧ@iߐ.df~+ĩ UUUt$)v:xoMJ:vc@:a, ۳lG `AG+25-KE+k>860t1tMvy{+ ƈ=/1Xym\YW}x3fQ^N&T5 ż{J6e5NWJ^at8]) #p #ռ{1vɗ*]'_sH`]Y~<߳c4wtEE9ʘ6J Ɉ:zfcpt̙SܲJ u}e c`Nlv =G)c4¨)zwcCKׄInW+pRBܹau@,Ůط^W@q퇈ȴjBU>toMz vm`(P6{g $NmFp #`Ym]'E%ע FM0s\-R%X̂镫˥+ Yӕ´)Lk?RʆkܾV :_d[]:κ > WfiS֑H IWT(ّ$]00`;؎Vv[C2{TF5N}pl`bt0 `W|Y4NWʆk+@wr $0,n4jwç%0g^zӷrtPռ{J+`$)as I"JI3tt-P6{a\ BmVttARׯ}-.h9SMYĢegHQH`;QlGD\<}0I$50N}pl`bt0(E$;2 $κ > bW?`ĎH#W#]XŠLD3[_;"]h+PZĜ)ټ{JozON!"F!r6b; d Bc#i=(*R obӫ U1kHɋߓcQU']cPʆk+KX[Ƿo#]h+PZnstmjM[#IāҢdGtT6DUUKou1Ũ)FrcCKk1QPC'3HS"]Is>a 0:"0*NWL&TN. @[^g])ve^G:\].p5:Qf(#.cׄI%Ũ)z1vQ,UdfH@iߐQ*Xy96WKWxfL&j@Ses ꐺfz @s?y{NWtJɋߓ>U:jt0D`;lG\щ?.s7K̩ ].ݮ)ND#o=k/Wt #]{6X˵foph&TH.< @[[_x,͝*]Hsn}1 hE+z[I #v:jtI#FM0sb,%otR+`lɎP* 0 r1SJt`N_]ptt7푮UY0]f3`zMJZ I'\n&lG [qJg1¨)scCKk9^пļy&&eU-]3Y0}a\ p55lt-~ Ve l6[oOߊ;(-}ѭ0vT+m>v"؎]~>@$5T2?XKI̛g"1E0Y{+qW?ErעoϓNvllGٮF xaCj}Ev8{V)8=HWTIUʸؕA < s}n {fKM -6l_tHW̜ JW@"K0 `>ڝ`;lGDwߢS?c&`Te~FT Rxstvw0ّ$]Y0}a\ p/<]trut`Lf{M{+mU.IW2}\ҵӧz銏ykOvg3؎; JQS9|XUbbWjBUI)4l7',;`tfѷo՝ހq}Q:z{Vt-߳x7+`W|+UQ+>x|ŮHWOlG;ubH.=1q)l]gK`) ;삦U#^gwKWIZ %;+ IR,]/]hvw,HŮJW n*A:cg3؎>NI1Q0f5ÇUU.ݮ)FQܩy9#|~tlP*FfLC`-'al}EVM*疮5* Yϭmg a8v؎S4.?^ U`le~FT r}e )veb̂0c˾~t$x|=~nݾV%B-^>_:fM:~6c;` :zچwOf2j 5]]S=Xčc&߳x7+` پ.Y,KsV.*PZHޞw>"]hvwP+"ŮJW n*A:fM:{6c;`ڻv% ƌ^5vQ,Uu8])̑1P*B¬\-=anB, Q FR,]/]hnat[֕s/+]xJ~l `=v0>h]N\T1j 5]]S=X]I'`E3 KW!衦E!+0fc$;jBU!0C:bRSCtY,Ih>Ϸxx6+c;`<:zچғ'FM0pgk2?X(zBͣ 0Ȑotdx|=p#IVHzb@l)g3؎,#Ư;}lW.`x$K@4VN̻]Sd04wa!Ԅܩ0ӭgT5q +r)ّ=qHE ;/|w:;[N45\ srl6[A\8[q# s oΕl6ܼœIs `,:vU> Iv D5c2?{.m |tt<kKV!)9I47?x//M}ݑQ¦7{w}G.~'-ّ܄P5KKW"]l6bWz|ْ5!+ڋ2 -Ng3؎ #0!uЮ|GIDslE蔒5A sFM;6yGyI._fzLFcÐ)&Te등}tNI6#?zu]y\t`ϹV:AC];;3/rg+̹1_.nJ3[_;hXA(3']aH=F_R?NdfE5ݺ1P`O @\f1lG V{w;)3خ$]ގ6٤1`wfL]g~"?*Dmif+&NrY]7è3ޜ+]a 4l~㝎QB]X17 9Yt!`TV>G:Ϝj;w^￶mGm`T$MC_Z9+rg>[s%"4nxG+ ɶo>ۣmZSYY_̸;#ILT슦&Ph%LJ+It^aBtsscii:!`$:yq6#.g3؎?#,QS6mr5Çd+GGgܮ)N WU#M4,^>}n٤9Ny˸*!g_ܚrɋR_.Ⱦ~nOF Ư&Teˏ;6K^E#vT.m6bWjBUۤCc0buH}tU pM{"wNJn7 tmӷG+DXX1ה/qq7+jndk.җ{ ϔ\^Q;ua\4M?j Zsq&הlG\q7aJg"KOjf'J5c2?lAy oεܞg^\ =l=vjS/kt4Der)t|thw{QrfNl9F"0G:AR7v^H$#0p:Y^H%+NНȰ#KG-D_l\~%+`c4n X.ٌ1r#4vuwibW.oGOlh1j 4V~bvMq:&_XG[G5*ΓI=C2뜬ӭg45.E81d /TH'o%7] F&THБsg:~̜=+uqm,H U1m VN^tB=Ԥ;c5545lœVYPEg),=ŠiT ZPkBUDSy pR sk^aq81ZlG$)6s5e8 w.3J9'ݢ\++=7JW:=T}KJ$lTm_RUm鮎 =~@H'3w?9 k Vng &SGlt`EK'ę:;OIW|>>7wImӭgj<.X\.(_p EWh_twğ0r9#7atI ƌ Msѥ5(#K֚.tgpeWtȐoG:A^tȔΘݵn}C-!2djnm-`Da]ݺfm n_jIӭgjCO/()5FQh Vnd[OkfAW4*:pJ=CjɎ@iQb>WͰ #x'ߧvMq:& ,' spP|٥!SB+7\nbFb2R]EpKUon3 FnFu q)07YPtmi6n4W퓮3?Z"{-b2}D4-gO?f\:v _0LM,3`Ne~FT 6mI ,\*P6[:Ag \"rsWפCl6MRƐ)hAWĜ;񭒚H\M}ݑ&xRԣ"C @k6w%k~4wxx\=OG1o5_ Jw!bt5맙₳a˱`tG+25-Ke5ÇUU.ݮ)ND:PA œ4wt3͡-1* ,,0'3']! 2`s-#kCOKgjjh=}+|d`^''?KB:ֆ^q#F"j s\AN~@$;EZ@iQ#I/fX`; =KV5vQ,Ufk j+41)%Oxe 2jZPtow;򭒚ޞ1#DܷN=}5րQͶw~x|0zp,=DR,]*@/DVI]C.ru:W!5(ඁAKY|µӧJ'$:ֆ~p!cn_Z6.V: 4 (ht,-[[_wD:d,|/!Sssl nnGŷtB<YPtm |fKF W"h[,(jmEt,5맙S +`;"~(SӲZFQS,(* bߞ wg'I'c5*ŮHW$@࣫j/2^m%kpg3 *W';+toΩ6eL®⢫64C\)ݵ_:CM7Ѯ С@iӕ"]P/?{`IxZRgHW$ӕ(-@$;*Wk[n0fQlGx\c2˓/U2Z`fê&5(QF릒;"78zG1tef?mܙ 7PWZKv$Մ+4Z[%5#|~pZ'zh<鄄z1jӦ51M/7`20=#"؎o25e`̨0j Ʌ;[cR%DwtBMzWsKW$NoOҲ曚܁萩@IW$,]O6tjLjY m%kjCO^! *@?rǬs"6mRO]svTlvE%2 `rê]S~Ɵ"g]NGL:!q:g*bIsg:[1d Z|tB~]MRԷs`lnsYol{wퟛ衦O=3QWQ:f"eiS]\].]8R/q`V0=#؎e'_dT5bl6#Ӻ1P 3Q(7ߡ;TIM%7H'$3&I隮nuH=z[%5+|KGcc;\xz@%衦$L|G%0g*RӦ6=(y p6 ؎I@DoԔɂ1#Ǩ)>jtvMq:& ,t)0JHuH5)n_t: =~^p0+ߓsKW$3KJHWh ;c6?URM RHhHmV?~pIkONH,2g*bcpCM`'hG3LZ~(SӲZFQS,!(*fmv puwW&Z֗FKCxRm ӷrt>#C.fk@?zs>ng݆@it&+|dRR! +]Ȩ\] ͝n=c\3SJr āy+lFFv>{1vɗ*dHa5يݮ)N U:y!͝*])zDxfKzr̂~dWG-`qS Sԥek-23[_x ĸnĝ~7sKWh.3]龦8۾iϟLl˙X|zEk*طJVh=1'_cN7 fDalGMo]&E)e{FQS"ٚ1yFtQ O=V8#Vq |ۺ91-U@n c?27 ef xNWJlvlvoO_s;/GU`T>U:A[ꐺ~퓖"n_u"ݢ!?#=XnL$R[,O~䱢(SӲv-]:9|XUtLXF!//ޞw>"]!-Ԇ_~%̙R*WK'hr@3w?~ ~Uj~NWʬ`߼ IG@i'l~LA}ݑ~]B[]#ӈOIfDv4v[C2:vJ qgDů,50nt!ubڿ⫙پ?ܷ _.NЖgɯ*]?x)ЕNI]Iف6o7y dޢNֹ3+Vn~`m-y3L㦒lVlG@;=ϻ&LoҰx4lG`nklY|FM^4VftL [JnNqv]`ls|Y:AC5OSC[gL/ܷ]  +JIgEIWGw_Ͽ^c~{e]"ޞ>v.Z7xwlߴGqpS `<~Y~ #FA "SS&\XN5c2?{{;E:_.NSNУ6XNslEd5A `F1?(`unOtV!5XY@G|Ӽ Z&] m?Y0}؏޹{w:q +ّNU78zzZn_:A+]\Y]]ì L/uHOVʱLaſ.cF Dۻ+_N@p6rlG m QS6-˓Ϩ)Бpgke~F'8$H9 t$;+py<@`2^~NБ}gϯn=󃇟%yHWbWxWoąqR+s/k_TЄӕ{kbXNqc@'{ff+8q9#4v"z.wMy2Y6* 9|XUtLXDp*)7$uHwБ6@ۤ+tt:n=S}KJ0g 0#:a|zV]7m}ᱚPU^at``_%xOwn GzY:axNWʘ+1(H'p6؎H #,d(SӲZQS,*(*XD^aΜt&:z^:ro!ͼ'@,_sP:Aw.!衦o0d 0t8])3 ϻuw}w Zٹ zg/qT_wdPbxwW_H|g3c;:nk]fyJ.Y:d4V%t8{/VLߟgŮHWhٿJ'Xڌ<%/f&];7YۓIG<$VLx'-]a]Ɏl_fo=%=}' Hwyt&/]w{w_Ɏ$3;ZY0]b7  )0f\؎0]&EL4vl]gKLog݆4wtVwH'Xڤk&J'htnYE:Azg]ǹ<L~O:qRfLx`oUy9Q~=i طt1 e'v{?Nӕ2 s-z Wv~`Jm]'E%򩒥@LslEMKܮ)N UY!3']#Ǥ+0Ǭ[w\率_NЩ gs|e{{yyoHG:{IO +mc%g/>HlȱglG@ǩ)K> XZsѥ5(0@iю}Mfq[N:A^=*;f7@Ə3 /ّ4`zv+on nYW#]H*(^:AL~>%;F5!PZHҮ'^ # j:}(Դ,+c w.3J{pvӕ"ݢnKKYMp&NrI'h')2Zz{+0RNWJlv;X[dLH'ğ:nߴGHoڣј魅C`hѲy}n.ٌ1`;X؎0bY|O,šÇd+Y]Sd{›s"ݒ =/`i|vtB~߾K:$:/]2ӥܙpt?/a=%t{=%/pO7M 1LyE >83[#^L4`)^N}lxaDlGH 쓚2Y6rv߱bqU.Y6*γYFn{ 6隉 F=r~F:!x ̓TT bWf> +J b+# c/)y_J' /ّ(-GJIZ!?#Ch:}(Դ,+Hy>8^lY]SdWeٲqwFuiT(1GI'X) zd{_GkNIm?Y0]:csW]}m ne+_=@:x~KWę)i ;ӑsK cѲy#hټČӹ3mv ٌXq(~~] ]|-fm,:{?Ɣ~M - GY+0^iyٷ wx¬ s⯷-cnsRC,0PFޘwOt0oH'.fG|falGV "SS&\.pcCKk:,}m7EM:@LywTtkjh9zFHwO5* r;a2ΔOrl nSTa$;(-Jv$%g!`;L؎СNJLM˒k#c[J03I'XZ2wȔ~o/ ?x)SWܶbt O"}Ĕ_:S>ɵ ë\]>C|Zl&vK\QSS]&Lr=)o&1LwxtN]hjhytJHa+?տ)`TҙI# ûvq~K Кg3؎0%#Ԕɂ%`|'~ECؾitf Hn`fuH@Sk_Ď~t KCjSCtQ557mߴgPb]\]irubW30`lfvu>VejZ\'0j >vcCKׄInT;!`uK'YoOtNȽN:!κ:@Gt28o׫?\X1W:F `lIc(-ÿC|ZlvbY|K0j >\c2T :oH'X]!g;{ =/Z1|&H޻Ǽ~t 0Fۍu J? EhMӳlG>>NM,XQS >860t1tMvy{Э/^:fsݰtٿJ'PT hhf]0PZ$f.gD}ݑAa(vrurubW3*uG+>v cEQeɵ|QSps\-R%VoO_SCtMH1[M:@w; ;M -tH ֮|b!Ы-`lIG"0+NU0ĨC|yZK #~rtI'_$R>860t1tMvy{С`sRۧt|~ Gϝ鐮f=wH`+R<৛_N޵ӧ^wvT3ėĜ`;"؎H }F)c5W逹[JЧgtLN@4_?:c$ (,E@ks$"h)vi)ir܀@9ʜh+ PC5 xJ6jchl %A# N0$$dL0"dr}_p?\;|>{B:Tᜢ)LOXwTC 3%v}2oL.:] uM)4[A9"(Ghq:nߖ:J.?j ٣="Cg3C0S+j#@#yoC! P]3iY_4 IDAT k_]Io0'CXڴ\A9:(Gh银/gTڽRI"h5ŷ77{ސ`Z~[:`w =//Z$p_g3]y'6fPs12LIE0LN={)2t&q93=ݗUJ0b϶S@#ΝNA55SmWг@C]t 0#,r6#vmlhn渿!8PtS7$, سU:4N@cܑ*ӈKGק =3)MjmKOطdTڽRIh5u3q˙!q-SΟ  v\0JwZ;-qGN# LW;-Ý7CRdfPjsW)Ij nnSu}^t `{`D>+Ϝxt48 B9@mӑvQrYh57Gđ @P7S[SX+4HF;5d LuEtϘ?ctdT#,r>i~h$6ZM@FI%@c3anI gM`V,XWt hdk +TFDn(S!ՕJNчPotCt 0͠a)#}bd840>a#q䰱yfĒI8`<Şmeݧ&<þ~ B#U>=~F:pz[A9:(Gq:as93j }7z>N* R^N `MʟYV%;֗N-{C:p:\A9:(G瓶a9RIh5} }K#xGac`Wgt ]0cU[k@:Tht kOYeOe}yOYt $ݮ͠a#T}bdJB)FqRIHG%dӤ#@#GKGy?RX}PoH: Ȝ j /k>zJ:qz^A9:(Gq:as93Dbj %/ #f>?#oN%hgt@EvW[~*0--=lg#rJ>i#VS_w㤒˞)pm<,:̷uǕ6T:^uE[tR-Nlf#E>x=ݗSUOZ1ZA9 (GBW0--VS_/x|dH9l`4>_: JGЩK]s$V,Xtγ P#spILxGJ5rwwytnmӣs83C h=N' |T|OYt XCnPz/KgϞt F'HG0*NXAfP07j8u1z8rXh5"xGb<3)(֒.@:#(lC#ٔn(3PMC7$ @N)`u#9H;E45uSOgI:@fP01 }+Rg ̑5Cl$'Zjimi!CoS-x#(,Օ"ił?&cw)`u#9H;ȥ]I2 D9@ +t\ j B Ydhs@ ~V:p|P:;铥#9 .GiYٙ4Pt -#' ,Lk3(Gŝ:=9,G;r&z-5|TسM:vll6GO#(o_fP״{OpC5 4=c4SޑR)O)/r>5 HZ>_:@)fP0 }ˑ78`:/Ev=`X򽧥S_kJGPޤIGУHGP_'`~>ł?|}~%͔wT#cfSgʋ\{U# k3(G ;䄛VS#kRIPPgpNt Xw#QS !{M˙_Rm8a_^H:˔wT#cfSgʋ\-B! ܲO:@kYA9(G(]>VS gnq?  v-KA)dgR;M:45IGP^|B\)ZU`Ja9T}Y:,$.SޑRM;-ٙ$ByȥONa ?j,6rQPV ])rvZM@fK%`B Yj-#tkȔ;p}A`E uM+3[[C!DTW !Ȕw|t1I3-V:1pj6rqQPySr͚MM)Qg@n@Bgn#jtHGPק/@#({?r/' 1#N}Qvł߾oќ'jin, JGE4dFbe[VuE-7}#`?zF9@Y'Gwf^ZM`y5G'kine- 4xR:*3]GKЗ? @|^g+/3ԃIjYHo wt cp~m)Ta[\SڌP!:~" Ei)#VS0Xރ.䄛Чϟ&`]۲IGo֍4]gӲES@yt-*&kkjG;]ҹ,玻`Qf#;ϔNa ϴ;)}ߘ7|I:~޺_:@uP#ofxmskqݑ#zR0 Zr PL{#Օ"By&t v\R5!>!n)>VЕ wZ7'}Ywӆb.$L 3])7cb60cۥ#VC]Sg\_2D6fr nw+?ޓ1c_ړ:Ԟ{0 E.䄛pMgdZ[ڤ#">!ny"rtUpwM g&\UչGKB,rKG¸c#@SVbL)1?*3ew= 1p IGE|╎8ӬiT1녭WKG#N k3A9B{|nP֙эJ2R`x +kqݑ#zR0Wp591fܝ)T1cb6־yOt]+E:C͛+B}YC>!/wB^nKs65IC uJGm#Ҥ#@/ꚢߦrǏ;*[_ͺJK5kV76cRߤ#_Ty\:*JYٙOYٙ%ik f-Aa$ZJL"3-kVq6"-3+γ4Օ2`YoV4ct3kkjtA{P_k3tr(G(Wn02'cǾ:Ug(c_ݘ#7jfZ@uEm7dwإb)y./ڿG7LWK{m[ʵIO. B v\`Z{*?kw]nWZj|B`60_s7H/}?c6cw(j>YHG(G9b(G(LخHVS`NH C\Ό@' `Y_ac?|۰<-(@/Z)B qˋmfeF;?_J)xb期`t.^J:If-Ϧe$*2%U 0͠(c_]pXU( O9qC* jZ?|ł< j+jC!*ΜU0M:.~sf3铥SHZp0O-yu͎m8📎<þ)ԒNKtIe qYٙf--*3wUZ)#DG).w)ON`RFgOPgt `6r(+x|uJ5(٣="Cg3C0:\i]gKGPגeӤSc^Znt1 T̒W׼N ;m-)Aۃc'YٙmPT)sܿ܍oi;&AEv}?dB.BE澘g#$9X|mPdn-uzsj V=S,%>!ny"^aO[_J:.Ttu%;6Y2is~Ltz[OVȪSV)Ayt|}TQ_{T:d-Η<ٙ:62_njinXoIGPWz˳it yM˔j[uV:!N#R Yٸ;jYvݔG >kdgҚ|#?}ea'ZߑNyxYS(B]}To7 n(NPefP2PDt[ԛ(^CߧLpd>Y]?`g{I:]$ zdCf4h3 L˕g3SߟQ͟~ YlM;u)>V@G+E:gL2g[=jSV)<ݸ×a/<>nآ5K uM̟>yպŦ/LGjwW[|5$Ýj50O`9MGNwt 1MGNHG(ƚk3(G [:~Bh5}K睉CxGac6SaWϘ$B]v~B]+`e~Csolv}u f-6s'M}`PפͼKu*FN@t UzC+w)OWϘ;%s^z3ڂ'fJPW3i}WJtkTD޺ST:[KG(jk3(G쓶H)6*^ZM98(f5zߍfJ3S@:(A qpIs>ZN wڪu y%Q>wh-c'#eǤ#(gXW3]p]LWٛ?}tkłwUI̩tC)%+;ShMfzCʥSXNuEmOe2z/ Ljk3(GĠBWT&%/ #HGаHymZQ^:k/IRgegwJfLpMۃԒJَY;m9)r" /l_J_O,lZR$+l>t >=~F:&ZErSh"~(Aeq07͠1P#k=Pcx5 k;qÑavSg ='wW(r(]O{a JjSZPNրژf EPЕu<\)h5~yW;ښON5 -5κL i4Ý:YW)rXc%%ٙd^ˋM˕N:u:+;j9>!iNN{M '#(lOYtfteegNn3 YաKGЎY1f EP@;䄛=>@ GNDCĭyLXrsXo%/l_-蝉wg@3i…OΖ20 \ULLWx6'W4E\VvM-/Zܩ)NgˋgT"ՕRͽ3m-sڹe5H_*ֲ LpwPoh})`ԿY1fP@:=+}ǧhB YdhzH.jϯ*A-|x&n3%BFVvfٛi-^a'M˕Ļn<1wJy =v Hu<1qEN{/:NdegbN):Ȝ++;s_ؾZWclnM3N vn#'S;셞7Yýmf6YܝTӑ>_:l>5 B m.9 C @p.-eǧhPJa=cHVr͂y)Tag?t1 qwT7RF/Odgt` &ޝէLU -FE4>!ѹS+w O#MzkuGKg~jObw'\Ulf=(AI]\_$A̘qwieoqwJcUoHGЂEM?fQ@ ]x))&e;j 4}{9O'ezK:ZLSg[O徿μ3KQ @;_*F/^̽a 'EOp?n%;6~ZrǏnΔ8lZJ~vTk̲ǎVC]SKst 1#;~zO"I:tCy7Y7T\:k3tr(G  ּ+}h{06l@`n(7kk3=<W_k-^Zٸcy",}?:djB^c#KuFW{-*{?'JB;O+E\u+gSgӲM6qkIgL͐YW)U0M6y6-3Ykc'#_@/ޑFb}ɫkDZ޲7# >=~F:L @lXg#E99~$ VS)#C2!/W[jG|BܣsV6xaj֎U0-ˋ&S`0U:S A޿hQeGN5&" uMfyx陮?*Vٸ#$ ~Tٹetf[ntHtzlٴL#)ONATWrGjiDTVvty큎=e)oeob ݢ #C<2@SރWLNY0 iylQy]^@>/|rfgS%+;D jWgegzJV6g9Scim@%Şm&ޠLW3[_*Ο1x큎&_?QN*tGۥ<13ՕtuW:mV1Nq]YٙO|룗D>YԬiij|H:p=e:JytԷ>zI][ӷ>zѹS4'5  3'B+2RPJOރSG;!.3#ɦ=zkɫkSgk<JЎ;8!/W:'čwdiOSX/j?k9w m^LrՍ_uEV9ÝO|rP vl-P>ow,~3 =/yjqX}Kv&)uLC۽ul wڒf~lrP_Ns?6kU$R}PЎM,<.B_ 9ow}x:X]Q9ߝ65݀J7[{JJP%k3trR(G@A;C¯#/VS ~a5 D45:&L5@jU/̬GN|m<|,?#gLlyCYC̙Z*Pl֏3+xbf3ېͿy*6Ɡn7My>=~&+;SHejk'Mmj81O ҆[*=ݗݞ8 }ehEf̞b39 ]\۞K{z+?sRIs5<}w㖚? wZ7''k4'UoV1H=ݗS[7/0  6CW(G(rk?9*p䰱ai5N=>.ޑ:O6 ]o<1S:Z L ;hgf*+>!.=ӕ B63u;5OlI ?tdբXvlc677Oo|AkKۅsY)hǦ?wZNM3)|Ϙk^^h֧棧d,|rvzK6`DPy'ޚ7ek?}(w] (}*^S)Y*B{+eB^nt)rbkGߔRYR7EnkMC`bpał)`6C7(G c_]t)lZM5Տ>12q^< s~Y7l V60%dgRflvSV9cEVȐ8PUnn(;ߩ5H\ƊؽuyK#@Av=͹xgmg^ߚN(^U0mڬm~jIG5+;S:'EZ߿x|~`1 /w& NIEcj d:{4;}\#1;qf϶ wZ7'lo}5[3o~hZ%WoݴiXBWoN>45u33إ"G9rDQО䨴{ïv"=Ih5bzFc_9f]NggBAŞm3&R@u uM3N @ǞJg-xbڳ⚏ݳi>';Io9y)ik;Ѓ6ޯ7S#a6C"l6ۨ{ߓ1ȿٙK#xGac"|^HPX:VT:4) IU|5tCyOe &/IM?}'J9y+ۊk q=ݗW,X+?vJGPɾPk3QPs1VS n0;}TU_U:ϔNMUW~T\:_t XBKskC]6s~D@fZK/~V @|^[k@:2Z>_: k3PP8yǹ< @R ^:;G+PN_mthS1N-{ Zpfӽ >/l_LFU~SVi10JKGPi k3DP@QI3j 5zߍfJkޒTWʬi))׿sk)5`ru}^f554j6,%*lzV y"SàZZT8 )Nٹe91vn' BW0-=j WHq? Qt _thtCy[kUWiN龼bZ'[3"6̛N[gS VKs+ĀZPu@9q.g`F)w*z-p7IGP )2ǃЧ vS6?SIU:S[E]_*>!Ndj-#C{h5P\:bQ]Q+aL-fhrb@9@֩_x^-jHB)}!,29"6Ns73 ׿sk)uHG UL]}yab-ͭ>S'ggegj?/ 5 rD{h)*S$1*Pn跋Po6g3fhrb@9@\ ] RF h]8r&z:>ON"]=<)P*Z5%{سr_j?i;mޒh?/XZڹ5+g P7:)䄛c>@:/Evp%<<[p۳,h@t iinmk~ޒî0PoسM: Nkin-P.bAC=`m(G #t]b>@/f L+AR̽+[τ|^~[OPoH:̠=!}سιP.VϦe.59!O`t Ygku)0X\]Q+PЉ@ ]Y2"Cj ϏEɽ#-x&DTWV@:K)`xO 6Ͷ`-ͭ uMO;~'j<)7G9[PI7n/ h>zJ:B, *QЏ#o8in%rONY0 tCAOτ[[SZ[ڤ#۞C5 )`l;7{*)`lʅ봟wUv]yat"UWԾm0w]]Q+ؽutX46 k3TB9+'Gd8}k;"O* ځw#Kr`t YKs=-l6ۊkZ)`T-ͭʥS\QT:`^ؾ:ٙ0eoHGQQq0{Ib+j k`@)PČri? "Ì;b;@_].g`@-BΰR]) -|^}yCP{c)s ez}yu)FYO j_:<x wƓ4vlt@a?/ EP4߿x@:.0k3D9A9@|'# W{)2t&q93~KO[^H:d)ܿJ:L3ص`*#tŀUU]Q+|^ߖNik )xҢe qO shthj) [ )55!]gI: k3B91Oꢇ1VSGm7"Z)5cSQq0 LstE4{Ib϶@t I7T8HI>9{̸;5QT:gm ̲}vD"t2PT0 f(r-t>:%VSywwi9Fp2:uhOЙ8_#KgB|V02w$6V0 @;ԥ3+ vI!g á('zfYQnJ7s%OP"']^(=ӥ0wFynS(LY{U#!X3 :}bd8ậ?5eN5-acnh55zߍfJعe_7$B]><XE) T[k>SݦpM~V:\t o}I,G?2!-g:nNhس)` ϔu{Wr$`ӑ{˙1VS=zz/ŖaK>▻ =KΏQF;L@t>2w$6V0f#'SKg(6jinBL`PtU2~iC5 {*5wuƸ  |SZ򽧹#EZ[|iԧHGcmFl(G f#⓶a9h5U+UFK}#gKc?Ht&q9>q⨇%Gjy>W~_;90[Q boM\ ϾOE2ڦB1 r&'NuECs Wnl|h0#/Gib2=S|fhȌ 0˸7ZXJX(DLj|Nk5\kjQ{>W;=V:sblW"VT/M$6m^yO|jSkoZ\4f73yX?=ȽܫQGvoZ;Ϥ{C_M))3#EAg*Tw}w}]?/fǖwzstfr3OO)vo6[ oܛq-,G,GqY_8USֽwW??g~w엝}o3'F^ETM#m6O|O,]F 0[ 3'r0 5a w~27E4=56~nbNJJ㥉dquݦ+}Ͷ[V馦;.\SON<߹k&^_ ZCotFbM3)D=#_yre~7) {۟o֛=SYffTV;Rv}Ff/vW3#Q!߆3#nEo#\hFܛq,GpI# Z@dղ+|a4US˕O^ngc ð,VQ[]iSO–vS+5OtẆJ@\|ZI!Ҿoy9hb|=oz7➩/Ӗ;䞩l ,Yj " _~oyx: юEpvo_uz/:3ׁC3ǿx@ˡSE͘Z#,L`x^Y.ΤZwz_n?:xb5O۴y=v˪?6OMkrupmǣJ@֋ϽuigRvᄏ~0m0pf[R}wxюQa gFʓ}b>{D;q;:s:Si-u}ף3&ތe9b03'ra.jzߗ̞ u皧~SG3'Qa*S\wJo>]crxiu ZΜ.oϤऺoAhsַmkopf$ \ȎOx[Q0|;?:B̘s{3刹rtun~dVM]GC&&h(,&?rO–vS+K*;9:ߏ* AuiχgR߹[=:`n;wE{>́p&'^x/Q{۟ 炃o}@-w=cW = 2L/Ԇ/@Az9 _}G/=Mo> {3刹rl5<:42v67W/CUS5Oܾtt+ljm^yk|O,jA]sat5o#|WE+[prwo*-/F_7ys|᧢2~i<.MOOfwc9;LO 2LO gF3ٱhA(#f+(,'/?VMM6<:߾s׫~m{#916~J^a4L4~͟\-즦;V&7TNwr`jEJ@ZE GL ;wm[ߎ:k{ܱw:PHv<>M}=3k{|j:Ȍ֥E1>Edz}G7v:Sc }[Ҿl}:W*EX~11>1߂|͘n#fsAWjGZwz?{[+ljm^ymk_H:ʜ cE+##C%x&'Oc'yk=u h9tOFG'w|ۼvlb83u>/#jZ{COׅ=TwO{tgqC2%357cY5,GLj|oଝp_:j#?6Oނ0>c;VA'w|B46z~'@@FOC2%vo~k&soF~X(h#暾3ݹXEyIe&{;wt+ymY"h\]kßt҅k._ཤ3tDn"Ͻu|~*[:01s{S%Gz2 gFvl˟ _۟H:su{:l\>o!~;k5rxmρ;EYǟy1ތQ,GA'NwN6\niQ5IgRS-;w~SGGNLm ҚxMWsۚ?2Q+J@OhIɖ|~˃G;G-x[Qgf->{'w|WeOex>6]/_x9Ѿ8#Lc?egAPʮFl}; sַ?hAm:ڻ7ླྀ7#,G sV@䦣dղLV55{;wʼnuWsm77߽x26<:Μ 0tGyTwߡ7:N3Tw߽w>3(Jdk=R`Z=s7޿?(otL~[|mρ9_qHyrw!a.oR3D˟Ȩ834߽0G)Ž1Yȩq4qK::jC?o{̉s0 㥉MM[NwLוTF`~&y2CI9rHT)"y} l#xPk=cKFϿ+wx;wEez'w|x:,o+jdeK-¬}y96z^kLތX,GAå ׼!={n~0 /lTyjbbtSL*/0<:}9u 7t-@4=^& .L{E:-^o\W~-.LʵhO=S'xy,8@ďL[W,:26z^}᧢R0߹߸_/Z#vTvOY+ɯp%$ڭw},.o +ތhY!  }uݦܰ~\TMEgg3{\O.^Q(v :m{]Cju%ãCѦ/l_F"/{CQex%k?|iGhV=QS{f^q LO>\-o$k3:PL^xZ᧊{"ÿ1US?r͈YbsDx^Y{MS5t&*/lYp^CeYUQx]Uj v]ne֏#0=sחͿ 9+z獻_u [k=;x];pz/fܳ/]Ŷ_o"biO*l߭w/uMNieW'ɫ6_+5D@ˡH~~3#~L͘!,GKj\Hz:/jj:j9j vHoP;jnXQiv/Mēso:E><_)8O߸_/*o뿹r=u&'cQ4SL9'Ҿ_z獍+DhV~OR}Qg^n3_iA@~9:@qww},?(ތrĴ:> nrSFϼ{4}H6k?G`.w~G\ؚRU3O]Qa8r˟F[-:Q8q|K~|uo{ek?~#w~K' ~7Oǟ{~Q}᧢Lfuֻ>VU3/D3(?~/?US?~/ތrԲ;[k_rpuSEaq&Q^Tn|}]J [WEcEa4t?HuzsE$}B{;oT8ua˵^q՟weQA3# 3Jxz?BO[ܛ1YF# }gmn5d:`sյS5xMcN8qg3O|iǏ\97\h)_?NL OuNeif Fk澣=Ϩ/ O~ö]l83[TE.ݗ=c sجN彌hrœrPvΩ@ׯeŴ\;f|#>\:rUSA,^jjVکXQi2јL4~$؜97Μ8~3(D=+/Mdҥ hāCyx QpϤ]%K˭&cwv/{.,{_}lk7~y(Z `.{<[)f/nY~˚ G/߽t\vAj78{tWM};?ֿkތb9X`J3ޡy Q55KL'TSB IDATU =*3^&'&&Ό?ѡ|^[65m W&7˨SC>vP@tFwKủja>ـk鎚x]nںF ZO-^0A"_\Ol{7߱ `ɶ;V66,KFh83rndw}'yC\ ޙPT=y;S v=gO'ҲdZE){m\ KErayu_{fpy5R)US%Uu]gҙtNyIL/hxt(<3Wv ބԝmg h{ti'`kU .~*6 Ϝ9=A Lǖ` -i|^E0vqdVA%kw7.?A\u`vf8 p*V&Ke*~{S d9 XT.7ݱ_ tfUM?Y\T7i~EmYJOLL9é|P?L4̉fOBxp8#?6Q$f\҅k篨,* yYa&k5MFΞ>ߖΤkaK*GML̹xi"{Pݜи%5&̉\MP5d:UM>ҙTtY\t݂U+j_'kVoZ]idlߙw+/ں.lf/`8Pݜ.]VWgg3rfThnnn8l߱/peuݦpmaxthl\4;W#US\ѡUM-X5(,~EaqM&^nώI s8Ւ0uX> ^X\դ fC'qQX\^R9<:AmAp5t3#OuSXQIIq{Mð:\hu5*揝?=i2>1>bp%}"Lsc0V\;xl=",3<:ֽ7{v]eYUQX^b O;vP@g\X:^cEKo60ҙ\O쁪)E@g7&\Rv^CY&5xݺ?<;z&5TKMM[sÕɍ`Hds?ŷr1 {HokqX\axaUǓ+mfUeh33<:d0 K*{E ZcU- m]j99sGZ3#XEXQIUe ,[f~Ţscѡ|Yw貅krDY͑>W~UΌZhCnX\Trű1g t tAPO.^Q;,V^cEDc28>q~hdkdxt(9Q0E)97/MdUAP|mAm2sPwێťEeek'VTgZ(t'z-\V>16>a$`j*qK7S5Lqr7fFU%ťaXtəa*6,VT*.\m*`>W߶z[Gz[#WȩcCGZ3#aXTQ:/[v0,,__uU ύeGfәܰ:^w <?_W,EcQ)33AMqLwW[:kVZ:ʜ&XQéhSSgsa)ft&Ω3#:ȖU\MM[se W53Dŷ֨#:1vOuĊKbK ð,VQWC* 9-Ђ%0 K*{E 届xrrUS c㣽oΩʲ: N,[&7*_}cl|4H89ԳbцEcQ'jAyIEk5*.9(,N&}gҙT~´JgNr_Yp]F V5s w}Ͷ0 Úx]yIPkw XR|mAm:Dcl|w;mݧ:b%eXQ%gaQeծXxlht(/*_.\|,}(<;nוֹEfѡAT._v^CY3d1h;w#ãC S{* DyMM<ΤM\t&URQj .tAT]XQ%g*SWA!:jY])7\[=o8<WNJo[-#Qgl|wXg߁POt^]rf|E_^89ԓpN[pMQX>m*gĢ I3 jɥ Ϋ5\ ^h]T.sn#z@g^5hǏ\97\h}W{ykW6X|tN-w]/M|&ȩξ7tN1 t8wC4ƊJ.\m )ΩUMMU_s* DyG|twe_3S[65m W%?j )&==J\rZQXL4&珟~HoP^HgR̉x]vںF `j:V&7&KV7T77T7=~p%)+ 5@AS5Sp%[ y eKN+U4ծk]97x4}=1҆Gҙ5W5ܼsWkj K[ K*/Z_W4VTziM=3r`Ͼt&ߘpn_- &^W^R9<:m*߶z[Gz[Nhξ類XqYE0,xZeծXxlhBVyieUpQ⺣'ߌ0pusK, 0W3t&⪦uʪ0xZQXL4&#cgμֽ7I!h޻d0^''0PXTMA t tA21Y,^䴲XECusCus`GK oZVm ?r-ayj "s8r8j5*.9-^uMᩳL*1W,Z+*bKo6Au rΫ/ /aMnSֱs'=)s]ljښX^US0S / X\Tn^YuO6T77T7g MTIgRsDv+*][S (` `ercjYM=u6Ցz=I5%sC[65m P5E\S-S-%Z+*xN5MM[ϝ::<:VL*9Q0E@tãCm{ۺē+7TW$0xZ9snh`W{2++ JfP(o[-#Q'P#}cDIq%Ϋ_^>QXF%hl||A+U OuD B[%uoo//\h}]XQs0L&Ƒ}gmޛm{0;ēL*T(m F`G{uHT&c&bE%U V,ڐL,;ȩGe(.*'sxѓoF[%uZ3 XpMcxi5n?~#ãCIa;jnXQiv/M,j4TM^^R|VɊ̹]Im]])7\S`+m Fc㣽:z K0xZIqY ΍eGrlߒK˲âxl|پhS[bQgK:A2qIXs0L&ƑOuN=&W憫o0TMw8r8R^Rzͻ. Ë*j-[xS{3dKgRDyMvںF y2<:ۣx}2sns0Lljںß][SR^?aCusyIeTa:o==%_2CEasbE ̓=ҙTs23 3'juky#Qu ãCm{?N״Dyͦ[>ٵ733]aM&0p`KgR>ӟ=]rNkHAt<\[:0Sum[OHP] 9MM[ϝjޛm{몖ƊJxi&LgRѦ.V|mAm: 0# u8ZW&bE%) X!X:4rjxt(9x0Y@ymXpp&\"s5;1ֽ7!PW{bat5]Ѧ.j t&sW+T M+*mnnnNٗΤhث+UMLj "S-S-5 0 /(Դul܉I3t&;, ?=(EQ I:߹}oXQiCukmj&sBL_Q^RUbq8Y:sbbb a65me՟LnBcxt(9aP5\t&s;;;{9euٶ%yNH5n&^?4ŷ֨P;m類DEɼ'aXY65K/پdVV/ %;zY[%u`HgR;wAҋK6Ln<~#ãCyk;x4QO3hSAEںwlp8} EaqCu?':yx-Q%&S5Lt&#?{󗜓(Դuˇ?21Z]#cgsXQ҅k"uyPpbTP($6QE&7sE"Un"\MҌxy*&BW.Eċ 4$c?Sm{RSln̯%gҾtuF}lrf\ 9(UM}`WWvYɳ|7  dbr\| e'X.*xr `b\u=h/͍{˷o~|bbt.OןC}cRS@X w柲Jq:7>d"yFVdXc) \r_O~OfF|t ZmL& DRS@S_Kϔ}?xȱoω k Yɳ|dߑT}PPXR|˅PvbsotuvDF<ˇ: TJ IDATgWo]`k7dfG?O7x/3=QXKIMkpglFZ7ȜM_<i<^`i=W36:h[RS@+ gJ˵ZmA !H5~!_ڙ*o]`k7dfG?O7x{˅bym O›mJj h1 ;f7Ҿu@pPZj{? l⏙sD lnJON M5ړ*o]`k7@f؅m:::::-߮o%_HRS@,L?(WK~Ι&O7x?㰖@X..^YV,jtuƆΦ/0I znk{ud?h+RS@T67rWV.vA>3~C2j6[kCXKHM5:7ꣵlZx23z~rH͓g ]!!5Dznvz6X^D@f%Cw_֟]'1>P,3 V.jtuƆΦ/0aln׾ 814hRS@ln̯]`k Rg/H&R_mVf;_ĀpؤvP3j6\-Ao<=?9va?mVX۷O&11&U,f,\+ =6:~i)U67_ibVAWgl'Φ/O w4~a|Z? k )6:w+wZvkmC}cg/fF N܏[?]ԩ@IM/KW?)WK~L 89v!H5x[dUn{a-ȓǓgbym~>ҟn( a Ȍ qD* ͙krV]G d_;BiyPSK빭Oj_&'5|{fZUSwwߋw4O@U67O:%r~C'&@Kbpj'_O+ZmL&ԡEh RS:c2ln'!-h!RSpSs[ͮ#WV!5 18[k9:uZeSZ;\* F HMږs;Zܫ >M1ҟy`edb H5n+4Qtqop3x@ftes{rd3Ѥչ[+W]cɱ vxT7@CIM5o,\+Jz3{n{LF *͙kJqۮXFM *(WK/2H&RY$5@˅lmW_ɑM-|7@%5-)hIMDKj ZRSђ@%5-)hIMDKj ZRSђ@%5-)hIMDKj ZRSђ@%5-)hIMDKj ZRSђ@%5-)hIMDKj ZRSђ@%5-)hIMDKj Np/1H_< Y@KrRS+rSYO@ BOf= )+C=ۗ$#zR6<{el؟F'*b_yHde͗/ #6@::d|ܚt]֫HMMMV=mLՎ~T>~R3-{}T5+~$QINjj5SBw_맦x_#L_맦6QOT5>RnH+_m౩B}&I{R3=tv7XQjHVJ{$R08_nfT>S(U@&fϞt&NRy`eBcщJ#LB'= t̞=ŕ[JMuv_ݩ]@:N+xKB93RLr?T8\GW~GVg*phrЍ~s_i_C]soϕiZj*pK]I@_;U(xfwX|= nSѓs/vv=6T7?OdttOX5o-IM^=+8 C/vPmJM5]pe~;+`g^TWϼ-`>R={PmE-OM5]o6g{3I5oumJM5-.+ 7?oۍ{DGWjmٝoۥmMM5Xwgd"TSŅ|eF2KM5-.;KbO}&x.TSƩ˟ŅYzʏMV#g aV]p{^pEs _F'*:YJMzy҅^^gX__gl2k_B" !tz PK!Mjs/build/images/js/src/images/get-started/d7e2d04ea7d6535fecf8.motivation.svgnu[ PK!$~ZZKjs/build/images/js/src/images/get-started/ff593be89bcb7ad1ab7e.benefits.pngnu[PNG  IHDR pHYs  sRGBgAMA aZIDATx[eu&9D& $R%"nGu;rvo~_';/GW媰]ѥ$Q")H$$@ L91>D&!6x˺˸|c?W?*9gR{g׊3o=$s{=M2kY,2տg= ^zl_c-˭-G}pEWw`mNqu])~py%Z>zq>0\]E}ZH\'t9_ïϮq uK\Jl1꼬|<~^g/F繞7}K=Ts9{ø3pNϣ{3ٜ y{1KحuU|l{غ)>sY0z/ܺt*_=݌usX>ޏ`p잇rb&lb&̕ޛq[]<ݘ+__3ݓڝgxYu=xgF7g}i΁Gnj{A偞ou}{}r'|]ўIMq9xT= r8"@~blU'۪p d ~h†OX0XT'+ah{ B1I\uOl<-\i) A;u +~6j\&lM8eS(ðyR,f!]m#eo^Q.pdvA`Q#0bn 1Ac|ӱKDYLHt3m[ԶuìW=4,DA)iu^n.aPF0{j & ]Ols.w6#+&[{]K4KuԍKEkƍ+kF 82Mu|sq!LpduAaz`0G &y.\C gp>c}5S_xKyzeϐ-u}p Ӯ sAǐ Ägogh<\;_t-̒dh?ilJ3ōaL3d~\n}{SV?M>==W&`H\:փ! ጱ̀PiSׯb9y 2LpBV4K#.\3b:B ;-*a. : g轍[PޯYYfIJ|Z}z俱ªd^[&i,v&u~ØT:yk7֘t=AqL۞Y7 ~LԽ-{;JClÛΙK6a+3c)k.f!W o)c}KH&L3z5'ފ~ʛ^u2)6znqγ(8g3ӚGkkY 8g Tx!HP)S^,Mqy G]S7~NǜJt{2A]b;~=a]N q/n'(DF16 x1:#7RSgӨXҌZR1Y|n3~:8\C9g(4ws7yQnJNixh"=q~\dPla&]= yA~nIb nA\8 EO.2]:3N҄2B0 JǴ[%6fՂ1Xְ 5ݒ)Gpӻ{,=UIjWyݍm=hX~]-0o6hace(XzH l\ܭ (iH2Npͱ~7CW4Lcc 6%B03=&.(-}o.>)ptC K! i)p/^bsZ,=`kcۄ^f@-ɍ$t y3Uy"Tz!tluMxD1́^^u}C ם`n޸4$KPE4xoc[5/f8L7, Krau5 XT|]cG?C/ 7)* c:"ygQ6sqH>].Bqח:7(-ְ97eQ)A{@}3BE\H ?zg)@|Na)DU1`NP\8/SyXvϱ7Btx†!ģҥEVwy)F҂Q0t;Gl,| BLW‘@ H#G1 $`<ǫVD/Ǡ@{P>F3J3/2P`جT~3TTM1L1 O<м c^'=|]v` OoXdc/܄U'D7tΰo@Qe Vf΁,( K3Hr}S2N4FGP9z0 ZLC/ par ԺcSblZ愮`{ɰwsmv|Q=2$A!.ꕀJ˖zc9K隣+| 3OS{a/!p᪺  zT rW5F '*'S VƁʞ 1 @C 7!H:J v T)p-=Ø5ϦNH^X#xt͸O@ q!PrOi !{J==4\: # m@YdzݍYSCA2&O<| E@yc9Zv*4Mx!2ʯ AxɨJ=4L= !nm)dV0$0xJ'4w=Ƈl_e:u8P>gM:4a5%kα:狔=3,e{ LN@$|52(3@Y0_~iVW}`S1̴9L[K}0hꒆ%ν^J_yokr@18bZ#X8:J#4Qӥx]UӖl. ;.x)< 9 % U1\v< r*pZX.7e0)pD 8f8|vA-/8[>PuC/e KZ{6  ymÔ.`x3x%Mnj`;-JAkYRE<9Z ssZ kľ4t>u\S7HĀcDNTxx& %W[gnCӽ> Z! |o5Bh/sƀVңҍ`y)wL>sRЄXj3v# ?5ȚYHS1m'ed@X)&VpSjs>XGrAH,d60u~C 6zZ0>p击N 0:X@)xr&\elz1b`6S4ўݒl1g~Ҷfirq >+$Nf́{KW+ư`SY:oYk!I@b2DQ$E=X2&?"6 a8 829ŵ5=ʼn 8y߫i.!֭wҨTd E35?m~)8K t E i:"gI1dqYSXs}whZ90:3mO iLY@P# { 9I}. > 7t3)uPͫBC8C0V\j^ W~Mcxhy}B!IVi >)-/2׍j4;$;Pe'!r QU͛hfSX*DN|]-@1itMosb!th8gD,"77\P$rFo$Sz3߃X -Nku| ъ+^ȹ҂n=D9.!hH-!{7ԣ덈!ᑒ > OWk4b|MhJ2<428*V㚳 6ؽsk p U]8 c=,h1>@= bFGuqg9bd״ C m|gY|޼dbmsEX4ͬ 2Ȼymg{Ʋ(mbֿ{n92IrpdC`XZ]0>V SPIDFtj,4OH{nE%H){OZf6;1}j(B"2FDb9i zψq;,N.'q.٭xЕ#0`Qdq}w%U2ǏdPXisS0.:!\8*|p.i7BZ]\Ÿ= ]/\uaҒ̸c=KZz+ f 0q@+#azӘϥCڑqw*|c˥( p@@w<(ޣO]"Ъf CcQD?53`J{rI ˭(bŸh0e@ch פg 2N)`u0X 1?cz 6glF sl<8 oM&̝ěM(nAGĝjn-#F(cagi 1en ܮ!`(+"YIIÌVXp<.5Hf{(& S$9EAx $E>CtbftȜ1I,^òGp8B-fd`O_iV\Z3@3;jcuIv! |pW} Ɇgd+P6,?( %s:g>0PP fb>ޜ *h Z x2 InLbM= ReTXWj48`>Cc'bwpQ}bj3=Hc0YiA] JNT_J۸L(F`fgyIȡQNS5#p@kmRX4Vmb`[2a)mԎ)HEFYsV Ofs9Y8A,88Td:vCYL,+| n9rz+B O^ q/&]>W{XL!8y.l8`;|6"pkf4oSҕD`yjxzlDQ'lZ=6ITp;KF(gK*4VB<}V.aBw#G%CN5P{$' LT6:aAw kX\eZ!6vפՂdm L0o,X8 cfϥ{^Eߚ[:Kyq ;^+d?_J#(?y&C G »w=#r`I;;*GLrizE=2G;$lr`Cʧx351Ist M(83SC&m8stF{5ŽWBNINy#Z0v21bu.v@@9Xc{rzC``*>Lfv9M`?7Y0"ԓd¼H5%7뙀em4_0Buh" -E0(HaL̋Ŗw:WٽU|.ǥ}h|bMD/clǬ0sуe5݂ɐI KYk P=# Tb"- 8o"i蕷%ק׌~Lp"F8Z, EOV9(!JFDY)Z+ ?CqnZ球\B^ a M ʵ>kU3s#XQJ6⵰>V`Nr%t d#2!}"d! { zS uĞ/5Tej[u-d߂ @|*Nv&-][Z-Pkq2 ZD9L0Խg x~^}^3ĉ;7ezF,U "}q繑\(R)ym"\+x)V{ 2?<.캳]9S8`$X_.ƍ@.YtYczUni=k $rCPiIյݬQ_G"Yx=/WfP-tt 6?7e߀"`dWsL8*S p?xXi8en9qMͤ_\I[Pb7h6Zd, TP9 'W €SgP7:fᑔ 7/qZ93ϝ(ZGE8 R0iOD9ݞ|~\AzmdRj `LRYӧ>+P< m  $/?KpUvlj\v((ӌ-ifmZ@șDDnrB,1>ʱLT4'~GjoVC9`!̡gY"A (AS`Փ.KX𑄅S4ol! 0so妨w5i ^s$Pq\Pؗlx<:ėbfx&^F=2Tȳ )u,_EJ(L<1-8 % AKk/#c՝02gNiā! {\{D.7:$틮Tз`t8۞zre&$*j RF.rlG"2q>ÃTp)DF vYS2ygQar!C }g'B&@ YZZn-l>IV!Ao qº譴ύ{ھD Z[brБEg$ N@J^ 4zoAl=mriDi|mDeڜ>nj>/4$\Z{^%qHT%$Ix͛FYc,IlMq&H,(a;Ue#ƶlbzOOl̵O*Qjr}pJ" {BXƣelȨaמ3&8lI'RJ+; a+0C&i2O2+C%aCҗ`/ tm;XˤLt6! Rc +Q ,>AJƎ=KlZZ{&؋)z.a:QX|;(toŜC"u_En0,AgX=әPy`z!ܾuW].c.l!a =_,lwKWJ! z,1h̒8C;ȷx 7zW\O^R:/,DPQf׼(5@(m3^x>py)3t s*wOjn`r8$L&A*:B7) f~<758% J1a9,\ahĔNŌㆌqCU,|-#ec$I>dz:cϘfiU[ҷ麡}nYLL!fF%az 6pjkw=-@\ FB@/zi B*S`Eq]& g 9B sw`m3 6HjR8l) kT>kTCՅFxVo0H+L)&U/ׄUCܢj> RY7-=&Qj@b𾡸0m\#Qz/9y=yyE$5qOF fQ9 eڍ VL$!q'$mXYs~_5Q6̈́EH~N%__ nVvjzxDa蒄$L5܉FXOgz쇇HD̒kk@)nsj:-r/~dP|- - TZGM 4yeq eTA~'e,b: !\]f.'#P^;uF@!6D͆O<3{qδC$L )[ϼE#_(H`) !VME˅bZvb';L@ Ci BVb }0;p4&/t0.JZà m=tBKXZ }Mz{p=qHc EEa 3 w]%m!a!>|AfȩŚT1E $}1Br ަY)~?s")-z T;_5b)l(8OhNK=O wblͷw]X9oqƼ <52ܣ\ ]K9H `0t*C1Fȗ0)pyfxWT3($P"o:b~f4Pl%e8aD3v{Ɣbyc+3U[=pG'+ԐYP#-e&*u-,ldžH=uZ"Nk Ś;͢,LxZ1 un]X!&3NRؓGmZq}_Rܒ7"lQalx9A+ň2utlh= c*[6ԝS=䟕$gy)jd^X|LΟ$EfֱEIwxVjnoz{fLe].QG 0;[ Z m:!)K5BΤ/Q2bxmF ׸&&Òu yTQ4HhR jdncaS^:6iQQS6MV^c]HM@IsqdH++ӒrDvO?t>ѼG?6ar}OTf(YCk{= fвDJ~_s:>14?j(Y9XAfA1|_cD{[/IBkʸ喵腅μ K\5֓aYglVɠ=F2=k}M:=Hוe,ɌIc PqXzB2=9*os_)&=6ox.VsNx//c^==lJ .2oh{l%m34ϭpB.?lF/A=OiƇ&(4.Xm%,8W>$$۱!'9~K#σit9Ti'F\L½I(Y7W18`Ř!8Vj}N&zZ!SKP(,`5_\,FJ.琰gm1>9rmqcE SԷ\-p% m1 >=5V>~y 9 @ysYX5C3Q|^7 @הk]HMlz4^('{Eg~߂A<;ѠODE0L1%!4hz% y( QG j~;L <0cYM8ݻN^<# =VZL"iN5]F"OrQB}&+8{Ų(7L7K}yo0:-gfbH&7"y%DQs\E"=qcuq-U0<n>*BК` (ण$o)+Vޯ =2jRZqs8KuI9Χ,Q#̫g2Kj {gtx蔃*Q͑vuT| [F06rR %=HQ=dNXgZƷd0 ( `)Ψ0Dpl,(qlp% *+ ͱXa:=R1ϭŃ?拴 9!ɳo1邁~1PPCXnҿ"iƍ.$), z{EX ֡up/bScs%LGUqi,/l(c$ ƭ$2+cNW:[wV0DC4 og$)Z07 H/Zf ~d0ײ &O/2@*2F_[EA9 $Z|Zi`ьf(f7*zA6+~P]if9 {2z. Gc]-qdae*eVW"6bAFuvT6+E=W1`MAX3*VÈ}ѓ3D6^ZG#r8̥C9=YܕɕKlT/˫?>'w"ˣbNNp'_SNytG .-A}A+HG@rA\W@V9.콿.ƅUǙ7S %U,R h+ ޷ƾ+[' O1bEk8B!4>>G*ciDq] g iB܀FX: -TE x ,@")S3Q*e~-r(a_筤ϡ*:+?amHXz3+KV.U[{ϑ`7YRP`z^gh+Z 65 }2ݢ3w@&Z6OO.#G2@ฉ@Ĝ<؄>&ޙ-vA& i GATUM>$*x0-׏h,y =z$4 Zt5͸urEh2z=znXŠhғl³l@!=QО`'ya, |WŽwtC ^̩網DJy4s(` C5R +} 4X{YHpeRλrsh5n6AX*#!%؛n `,;I=`$HcV$`ie aS]ݽIH/ޫ rHJ'/ xk}Ebь؋tKF ij!wc3tfk&O3 y_HX^Oq40MH!0x12\LԣV{ RlQ^t, L)y/ wg@pV  ؾfMt_bPCk苫{Rp=T9v`5zY6`e0Dnq FSl3ƒ٘!H_Ɏlގ{1I3-WZ-wRD|~:GO6=S w5( ")ru:@yr O]zS~7o{,5:9E^|i-?xu-NiCRR+H[ZKRq>2#%.0 kIw7[X4zÙ p^MQ03u/j)Vb/b%dԛѢB{]֊_[Y ٞݠ3d7^T7Yq!,!VG<|~F /<Znc;0lYV*b$Lgǎxx&?䛵YRRۏ>qAu4pX HAX(*&.$5 oCyE`e"B*ʻ4,wۂ7Lusy9b4OF_YO@yFrPmY*쇡U ֹP!{e6 nmmZ{KضR[m\A%V:gx𪔭{{9cq.Ɏ˵|K?= 1^e_ 6q(=Z '6ꄲ F쩃 2OT &z-2/4a.o:jUQiu`qZ8%DZ=;x\|!."Ѝ=ܩD"4=}zr?+Q08N-։!(iq.2947// "-(.d پ9%OaRB̬5 dX,!VL֙L%FsL&d2P9nl ǮTAqnhTf e֧a鄔rdN >J0RXtVVJ+G*2|f]"\&ZY}l&B$$r2I_ƚ,;ht$O"=I"Ǚ JT,(({_ߩΔaP1X|Ewee;%/=wh c1jJ!Jˎx g V{3rqx?Nu HI)O ] ~=Kj=C<}OeØ*Mgn~bgQ9QlTcC=CjɇN9o_w[{)hR54%Ǹ iךA8ЪzNxrVt627k^k g0t30M!:F 4! slǍȩѦGZV$+Iw4ZG 2 QuL5bVmviKͲcCˮ)u&=pf>h lDbj W(h:l&QR峌O MI8փ8a}dm'<²:=ׇ!͘q.v3,A{f !1!B4#t*9X^&옜9 hmaAjn^wfy\{Ed\徜Y/*ymxu%_D..Zd7ͥiy3; =s=Nzҽ@չ"W!v=3R-H鏘8{`'APܓZ"W|Y_AD% e뼾Q'k~xj$%G4͑ '&Дa6`@&U 'c04 zp(x ?hUYiEXqV4a0`K{{]!Ftb QפB޷}j!zG:7>+r|A *mkg+װNËHd0[1,О/l:2̴ 8KBO_}v$T$6ŻTiÎn)V"V͒)ՠQ;<>o`PaQlĪ:^fgm鑏A+$7WOB̙ʖ3g on(Jجf2b.yN ~`x\v9 5`c丮h5ÚJ)Tik;iU˽×dqc%?QߓuC+W0ΰ_=9].兯TȪz\؁@CbfɗtU^j^p_>F)'T"8 *\-3`%ip;,?:z5x(a42.n;=*`T0eٿ_q;D>Dcৰ1e}+fJk.(uP"5pbUϔOMgZ,j1KݾZVZ1݀x)$%xU׭w%6 gvB'N]B ?-hN{V<%g.[rF˯\~Mt$SS|g*3zsT6'< ; g$U Bt1yuvXc]k#3<Ȫp ."QV[#iO|mgJGuOZ.W@*~1;qněiU%-wvhb8Lg_3GCYuh-nrcPEg\r\bag gdkE\ 0˪jSg?gwY]k\7ƍ[%6h&6E@K&-v]<0QgD~PO?uc(ش$%f.PP %#0n"p=)"Յ課\ vj)e03ge\:gqK;{*;rn.f{C+u'N|/ j\/1(S?h[g Y*Hv"x!c/}@x@̰'ȄQ#%J,:E0Q[I!X].ʓkmhiDAүYGen1  7MUe/^^X{DH<򸯉FuTP0G)b*H/8e3`]M0>p5#KDٲW՘;uZuK[?+4Ec *cb+hVP,bm5<3G='nf&s{#Y $)Jxc,<&p%)\ևix,(I]8%lsgW.^:owKXǦR pw{sO0IDJ>ñF߽kc'ŗ -kJҐrx7vijF()XT#?3Xޫӽu7 _KPкʕJϨx `VE)ywA,rnjNAo.s`S͍[.[Cz;;6 0E̚@uBu >؂EHX͆GRJۣMIs"DIu/FבΞZcPrM.(o[1uwkttٲ+ qNxH?_L̹.p}zLکSگ\ZdY)~hŠHCrckX.(fªLyqVݚZr̙$YQO򄀮pc5 h[2-UR2=r;`< K{}V(vJ]jFq㬊s&F=-Wfká Y[E /Z@.+kp kh ;6ETZ_2Zih/~5iIelJ-_Q}^8`zb%_گZy^{Xx<1I?O , +]xzN($\*s$y xx3V+6eR6[C^$! yL{JiPFF|@tMry,fX{3xMLܣuVrA,{TF׋zb^C<ӈVLSN(P$)5R!p.SץTLj "GE"GBy_hzsF \΢5Dճ73;(h5gY0ͩu,FZ}3;\X"A+di3VC|zBs vyLLe6\t AJݳҪ\@A&)溏XD1o@Y. ]=ǃ{âF[Ɩmsͥƍ*3p;{nJ'䧯_w:POrJUy)'r}抧EzaLMF;\ZʱjS\V 9EI<{5RzXc[1GKAwq/v6Vp)0: |V Gջxğ>?F_K[{_ŧ\4806.L C[*M4&rD] / ˖HW]?BW~b 69C"]Mn)YK/BM4"r' +6'EIڤ$TY T 6}pn ,|-8'L:k|"l2,rB}]3<3_dXVC`g+|GT|~>ag{RyQc6%T|Y9 ]Z*lmAY2wMZRN?霄kT(m[ݻ5F+OKS>L@;ɂau%A8 ȌklʹGʻ,P=#^YXȱΫ)D\rXaO;w S7#X0sSLV(("9!=%POPkQXui7Kfi ퟿#ͿpYτ[ YM3>uio.:;KdPX,EW@Ǧ)}Cܾu(7޿[= W,S1CQ$`ayMmy.zŎwS %V֭_P` Sivٳg/@h7IvtBcK/G{WnU(A3*jiާy֒ݓgz{[~R{;=g =I u $'kT}  ,b:ւI. Xxr^'74(y EBAջ` gU%pJ޹9FV_λ ,Em7MPPW4/*o?ޖ?#oߗ[w6M{)~ƚ >2tC#,V 0翰4#+3(Ls g>[=sxc/嫬C٩_7x{8%-/]WÐS٬C\$J⃱$s5ƑuCPm6^ɼ1Е'1N;)Dzv[] į`̤]%Ǵ5rzuڠ;]%<jiÚX 1cYg/zɫgw/$̐Ww޺&G,{+2Z;Rc$xq}hM$IzgFy BE'+"t]dww+Բf[nޅpFyقl9>SCZ!O53xHh^ZY*\XBa@ɀ]H"AsXaG?iU؟-/}yy{;ʗE6tym}M>OޔGR-t%y]y?Cxl,k`F .f`5ֽ y]S;-8 JɄ,j$HҌpu]-5)Z  kYi 됼zøU*wg'luLkU8Z fronEcTaFKTZ|=~?}qxһ='Iô?YK™=.n-Vj&6p'4Mw/Zb-Vp|Y.ltl/y- ,*X_zޑ5HsʼҌD+Rb. kĺRy #76e%\Ua+j/査\ yVo3_߀PZ> 6w,3=l,! eL7ZBeAӘ@l[?NZ& hZ͸ܪ[U<v~gP֐m#X[K.h;X^*$q#gil߹%wxyc_/k\kޕ|W8rLI9 (K_~QU/os]v\-B4*209Th\@vph$F_ .0H>V(o "UK嶚5˿T ~~ǐt-j(egh?[7jOLG;T&s h0$:iAvoWDZܒs|\=S}E̘ u7Ő[HY5̋ʮB" KXo}?'x, 뾉giS>CL|u~wl.](Οg_x—cgOpC.T]콰 $ 3M7wgԟ*3Pp<~&>Dz=u pigHf \rl F$g*bx5Ohȓ\9?ɽ=ټ!@jR,25ʻ sF-bJ_Չq<ԪNQh:˅@ܵM.uVepm1k IAjIy!_2(&oր[rgΝJّe T2r.BKcKw#A0s|eȌ9! Pz^zF ˄ ^@1q?o2)3.lhVIK/()u:Cܒ0U)>3?/|6O>}Q~]G`-+ChPMznADTH`LZBqYP_hpֆ#+r{2ݪmQ!m!"wR!C9ܙel We2* b1woܮ*'5H^(C[.)PAFfXZ=UXޝ\{rXql%,}@S%ruWΝ;rw>'Xװ{s´) @I9<hj9v\p u9]S`:G;{7ޭQfWrɠ!U„O6d+5 k͝)a9\vz2i۔ 9Z,.ΕB?HRP,#.!3l&qzZ' ۹s)$fe<""~6#X5ils8VúI*U&}=0F@(3hF6b8슚 ڻ/oͺ/C]X<3ywZ1L\G'iEMQƅN\{1A!@@tTǹkV/ݺyn] 7B0^  %Cy٥\}R uHPl<6L0qJVx6 JDshLAXs@nRYsrXB#ʧ{%ަ@H 8#n#CNS'Z<2=\[\xA5Z"O=\֣g."F9oO^})›aP\-n%f x~XxDcFYy)3 E0QW s^`UaYWh*}wSDurZZb ɘLZq{F[NYr KKvmWn[Ѫ:Yªx"gP߲5~yhɺls k4Y%1q{Dd2vÊp~,Ck y;2ТyOXUa^Z*?W_}jY/?Qa;>mW\o5zC92Pn i®50oCW\ ]ہs*zƐu\>¿)"d1V @])Nzʖe\XcZTQxn MZlL?Ơ2,/o-'5K 2[&zW,Î&ĠAl0 A`)njt*A.Tw%Kb:sF*$sފi12b6gdslNM|!*Dĕ'l޹}*{3ijΝgSYި:/rz,w^8{Gq}zf|eW y1\bk˙V-SXz D =ޡʿY"?J6({t ڽPQU^mn̅Ȝ_ԭ!%u]?o~[ϯqe-^V/m- lpM"!za՛[YRkfBc X˂) 2ַRuo޼%{{rj93^4sun,(?In*,גRxOIrHA)rTx%OAX.%o#tpK3+~\ǚH Qaz6٘aM 6Z[uhƍPŊa;+{ 7 1%m=D<\Kg{xZ,y 60DVx8V|t * WTEVaR$WAWWklQ=Ö>#.ޑU&BzƪiNn(4|i7W@,` Zl)` 5jy Wc e3bzx,z|gr`$>+O\9y:L ƅf_Ӟ=? )؏~.~7ݖ#YUki$b7`I# zZt 6A]8xj=J䁜l.(82^g3ݛsy#)zn2xeLZrھ"/ީr2őxOX ll  3`ԜD:.3;Xp/x_xIdÊv>@LD`$_֭[7ɧ.U brU oV NmI#SUmq4Q`Gcd[B 4& eFIU6Q+JgN]vjI4;z0roZo3K2]).auB itJܺ}G0K)IXy#3˔2k!,X˛lKHJIR6jAW=bQuMdG~IUyL6'#XLd 4i)H#XJTJ(EFr.qS>$g$gG+-V%K2"=ˎt,XiXtT Kݮ IL=>KF,=Tr/O)SM(FɳadQ1*ai>Ȍ}44{ {"ڼ/M)д*PN2"oq :>z Cyv<OL0E"qR'%s=ogoYjH5Lv ][1BކC.gė糒g<ᇜe]0t7ϫDn"]K.^8g0ʻޗ۷DPAli& m k3~P ,p3,tA{9p×=󰐼d jE j{<&8u췶G dyWoR2w\ <:- Bwoh3D/\8c1trbnaUHQQyrnF$koh=ZZ,xgÉ5Qi^? w'7Mg t7'OVklq8#u=yq9vlWu64+Oh&w➓[ݩ B &y a eK_L.1%!Pݼjlf@rA@US @KW,1MKh͖oK(S Bb="Pºf_B Sev+=-'PdR}o$1'($KܲZ!4L,KlI*-mUz&0d1 JIF|c<> nJ"<`${LjL|K^q+wAo!)yY~gJihI[ %~19 l)nշլ_$z& qHXmV̠&nmR>+U<5CϝAu`bFs]6BH Il~fMXi@i1fmvpד{,\gVټ6Dh7uQ]zYG9Uϔ6R]Nt71 {ֿs_Ic.Ln9L%u Je ]1(l,v!ӑ9mMј@EYTj @0 J9 Ԓz(L"pyXg#x>{ ƷIW6]zvӁˠ6hF9ZZ3iahG)S S'?i{<74\c4VaRWAX6\~ѝQü'g!RzFUT e/9GaEQp˒5gyWr5F+i}l4zN]t$7f {]LtuaԝR tecK|i]ǎ?ojg_Rm\m>Fl2M51DYjO_몧n0EZyfiފўK_o$)Ҩ2n{KS,Bf&U)\ն{F)>X ٕ.xKU])3h WKGjg8NT6<ӉC9 {Q!L5aTz,{{{ĭUe8bc}񦓴JRo+ f&bÝۨsy EqMG(jwn.EnZPP7!"W=\QXQiHAV յ?,{4ѽ4ǣ,\m]f`7x$Yt2mŖx}sJo(s/nr:Fޔ|cjY\ I3:9 /*Ps>uF8tAӔzbXex wY\X@x6a5cD6K[B\u] *O/JeI \s|JJ0~ۘ.t IN+W룲}E5c60fqk<맯f`}QptӉN9C9 CnW j5ap0)4oI"yflF,[is\J1o?&Kτem-q tٴB%F'7G6G?ź"6ls7mέ1)k?),D#Ѹ[yނ^ A΄0 ޻2|^Ԣk* lJKkŹ°Lt6y IRa(q.%*\dfNJ/l@X%b c6hM[69t}"3)%<zp\eT6|9e7ń J݆M')'.{m>>str&%`2f+%` i6~9&Ak$_ƣ ̠e&zDǔ,˝,^zDX^cA<X:(Ho7K7' c?Qhs EqOHl`3ēxngrOHt,µ 1[Hdm;k.͒P3ټ k,sh}gvnlRjK~0zo|[BU51~k1/" W`lx0ng3Nyn5wꄽғrplq4pVOJK.)DD#B{EX =>J˿օ en͚jN!kR>xƮ1tQyg_Bk7k^5m(ZK-FBjw=Hd&[:ffL.{ )_';ƶϢen)<,,Snyu8{5l4@ /=9<}sxg&rK˲u5e) n m8o_+/*d$p B\Gآj;vc%0{ q6'wK,gIn%9[6,cG Auz&yKqh 2=r . PE43oSDCSz '.f7tmVI;p=g܎O(\C5ٰ QYC&ߵdJK5&E q0rĒ7I,0oʣQ8HC:R@ؽ{=?9S -mG&MИ*Dp ıF˱ ;YdUWZ_q е9JQ 2Ua` m(,̕?vGKk$mx ;&2WI8/JTwzU`Up\&llt9/}.'nx?_qu:x]:-6Drl ʒRXɭ尠dea*+e K=(''a;Q?nؽކVDLjRK3 Rs b@;sO̧zxai=/cD𪽸#|b[O %lV"gcb>t!BwtGK=ƌkXgV3Zoܔo?ݻ Vy W(&RPe>Ԍqh`,^GTg M375nڏ@TLy:mc?ٓg->k_$H>K9y.zUVu5šHc[KocZ֒la/iS`/8VW7s/]4Nӑs{k.lF @wgJN)YFSpqU,|7O9 ՝7=BxMoèԈu:y\DP2ָhdcn^.@x`?$>G8{V4m@ Fm/cČ\v8nƸkQjGUmo|:ūOSI1nm F2 ̛{fA-O:1n0l*.u3{)OLiX%,T׿;!,JE7G/w0rh|ѷT4V&U|! ?2|y7O>ؕ.lģWxݿUQ%{ ./lI95elZ@ϿpżRIXd@;"'.;R F=Xh@ ڵlvA,g?Bw8q:L# ckgo{FdJ9ڌq٦I \mxy[y:$QQrɫ™vA8vtK}b`ГáϨ{[H@ʼ[V(ȘgF-|,glˋ/*?oc & Q3&_풤ky?[7oށdz{1 ZE$hkk5:. Ъ^~qrKdz)2 xVJL,u:WAfd>(ʦZg/nGo/kߘL7?e (k {'{1tv)m(DZHr#*ZvA22NѦ-kOWxDR[ek>fŤ0b]T69TXb^p_]ZqkRWFbg;4HHLs0˝;{~R[q裨?)m{P#BoxZZ\JvLEO+W/G [&F\fJkk3԰#ˈǵh/r؊@"φt{7*b(V1P*sA6i}̙A}G[`xDŽyBv1,R{ fB&5Yo-腾ˇa!1[rI!C36b )M (wlV+utb-9Qϙ_'y>@ qà8CU<\!ҲM˺{MH@J}V}_@vʪ6Z6c!P G>GGQ HyK{3=,97,p`̭FIMYChŴ`a&>@I0s緂dDޛo%nݮ:bi[' oKLѹgmmMV-^E]?>@&:zԺw̡:r?}{okcsHxGآKHN<`LSOt%Djs&%4:lJdJmO͋Cef.Pww7N<+6ͿUyʼn 68ϱ!!G=9NT!/A ehX6J`u&=1h̝Lge+Urϫ+`=OZWf;{r9BoTHފ0:L< r/wDfMj:3{9Jzw<,oO 'I⑑HOAuF(kqFiEʮ@8r(e\ 7\Mpøg#4aPNqZ;/J8eo)6Nc5eJG&[KDW;7]/\,>2j7&+>kq~ ;Nzô!}';Y(KxlQ{)# צWs(v/l0?_>ήplV6o^X>"f]s;z"Ǽ{Xql8+ qLnT4 <"MfOF1fP,76ل|'ϧ('ƧgΫ\ȭ^u:*.rVTiűkqxb1ִ6gS32ρ8@xi ݦHMU(ך"tgݘ''%+5i<\6i,'8y0 _V/9IeZ*3Ȑh;:fxm`=l&0\RgDAe>`Hy0PY|Nds3dOe޿뉅,Vrp'Rob{9#ӍW{Q=Z˒Y;8;&׼)e3O' `?-1&_SrN(dnnꡯ;ޫ~ecU`i?2mg9$6M6_IYk]oDz.{ ߻!7~JڼoM9Jd"Q>16 W ٛl}Tɦ8˕9tE{|c[.{NqkޑuxZdQe͟J{G'ߨkqvu_\Mhh*))) 綍t#h%ٍ7O W \RqA[ʦJBaY:u1nL;oUZSdQOdqmYimR(~ ˽e=Vk9ӡ T'd)^ݕ齿1a:/d8B:;ѿ LPgc{M?d멳u.e{CWl?s{Jw{|"ߒuM5ɼܭnɯ4F  캱 Tr8Rj{PzM8YCWWyanP/^ݒ?dY}M:X,]\߿g9ah@[I{7d}U+WfmپR}mW7'*/T#Up-P]d߼AωW Zc 7nKIq4F.Ž|S:AHDB4Zַ.ki_7tMtqd_;|UK9yCغj,ؾ.ΛrޕŕGrK6lӌEX2R$ dwV6O '^id8b [%{n%[`sp)uEUhgܫ}n\U^?]ږ؀l %S0%0hAzsAQO+RNiv.W9 s}mqU\TQe1^\n u {/UpۺvIZږɥ+"'"mG9t {,ǶwI gx\Mլ9jԩ7+<\[{I2hA7/2KyC9?8'tYp%Գ9njcp/DtNml }LJ"P25byBz,}t3E20ى k#2ڒtK{ed}rg<G:lq/E4^`EPYf,k}˿Hc5㜻0*6zG/zJ1V_7NI+OZ|Uݎ㟠Qv?mmTmN R:k#o0U|?S1ңYu>s2VN M4$q_{gxxQWZ:daD r~x}vǦ:rPBؕ>p{"_گ}I.]( wV6[ ˅**uh?h=WP=~Y:8ׁmڵuv["1I>Q~Bh)Gy3$5}w{Y)9|ZB||Bt]%cDBoJ{ߗͯX߾],Y=*Yp|̷k|3:ݿ<y;4"2EEpF TlNNa ]&}@2:woɷײZc{<qG?s V 8{0ݸ6IK1OPh?o#́?;-qup,-p.縷&XxB)wpB^bP@}hl.TE^svN>b..s4gQڒBV޾%#BI._ّ;K$ԼYo;h59lYYqO_*4kfkufqYp.& /=FnZ`YIUF{?\p^~7ebBP6} hOro+{y=ygr-پ[WEqdloo~C8Lc}GOL²B] ={odp^mqzzj*Zo.wn/W۲󰕲*ߕ}Ps qd5O'oӺKE{<ߑcv4ZG=ӃI2똍 xB/!,mU^[C<`'K!#қ?dM=dMk:9d/܍`eJNPpC)2 ê!*\i1`!Y%-qw;}4=V}[oPJRV\C~ brt\(;Re3Lz?YeRŵKlk+nL.gU27(m ?;_=/8\ 7!;U 9jxi-G_/{o~SΗrߒGg9ei6Tao'p{G| /[Y9G?>Ǽ&:RfvG(ԝ{=R$-G٠{Mb:!ǘUE:H{ m(8Aӊsrߖ[ߨo/eKcUSrߒ[7\ L]l?Xn,^^Pd+r_WŲ>uoVzg^vJq=WbyNY<Zd1G`m#כGs4>IP; X|#q0AÒD5´m|ZK_<^>z<\:xhO}ʺ5U=)$OzO=+b]|%2r=`xeBkQ*?:~GV'~dڃS7wܛ˹ #lf9ɓö3d)EY|?P؎\_F4kpxA('彷ߩeY0oK~s/TOHVżzu|7ɧ#y;U|l>`*`M)w:ާGڝ֮z)MQxrIX$1>'pxt,H)>a֏|lo kZuvϱH'r\؞YoH~Q_8ͬElt)Bn%y5xllsx%VQ 5j:a4˃ڃa߾e4+N:PRS6%"W}w쳳6) 2d[qCeݻۑ!RC'V'}=KK&}c=Q] rqdqCX>;7 ȧ7:9`H=B;|^m불%#)V>!'Y瑓)=㑓'ﷇ:lH:xLh t0=TV'5:7.YVnfo^jEP;Ԡ@5\u<*vJc8[c/{Jp=أʡ;g>:nbZ;g/hk$Յ,ӿ?<;>UXkZJ?~ެjWB y!ᑏ{>tC[?t϶gGwG ?;>;Y0A"|^Ű%ZoVx?+ZSOh3g?O~Gy*?> S:2K{|@>;>54y\Y,_ލ)^HG>ұ =#Tώ_cD<״;-eF|>5bPJ.xui.xGБWA~??Y+oߖsW] r:Eci'*O=ǁפ~n})IN*}X%XNWa=ucS|ԇz-$G+Ǿsc{{ q!RJB+[??S~6z=)rV(b|}ͫ$wɝ;Vj…pq1W6B>V~ygǧ(C4ddݹ|#`|8>՞%=*o_~K|~Kx[h̖|ŧw7ԄuHIe>E~?;>Y Suh_:Wm{ԋQNj/(d7;!q(+h|CK._2YU%%U|M,sq}g/U@NL`bi^GZKGpO>*dWul&k!{j])핟oK~(j;b7]'Y9YSaIh7Tn߼+\~lmVGώOk}%Vc[ҩ1>&›(s;|8ƫZk顡r$Wq&keb{VEgS7^_ʯ oϿuLI!WO\}Oߓ?LP>;O>>;>EljOOУBJ唠iI E.Kk7O5O6, !r?v3^Aujde\U5T'v~|v-;Z53F(#XZ;op\#f&Ti8O%C|U+^g_卟ٍQ\睷Ch~](nrYo|ޔJk{t&K>GzMGOz4|6Hn| j$>Q> |i\[e!oB5pw2V~l-7v'y+/e xǸ>O2!b0lg8^ʻn^1x삮>=GoK7~Qm?$;@{&|zSi*My_( (!IR{IݽFޙvuv4%cvl6 HxC*T7YY]xo^Ȍ̌Ȍ,G\ *<滮sWy}n%0SCq]*RApv#3+m 1JD5ta=t+ 5*/1>Wv ܵUdMWn~\% c;V<`{L$tlqa^m!LeB9q5ǜ("nT1f fוȮGzƕ3ثG3K>!8yW,;j[*lS[wRrd/|*uF!cnh>0t V^c`j"! crbbtm"{: 3V˨fOgqTt U;WJ ׸cOF("ǽacdx\6\+~!6*J~r&GWY &B <22pY"-̙KL"uD.W[Q\*`ոwnynlyBD*Y  ݋(*AkMpU/\v/p 7 ˿`CZ\H?r-x䱻☺ 5f*Bn h]+Zh Cgˇky. 5 fա_,^zBT].ec@e[H8z$A))|3 9yfJҫ2,f:3WR- "14ˠV/B]Ry|ǘE:7r+:jT%Ϸb|$Er:_OǸx~\paEf@1т>_Cs;Y#kot]/TSqn1ٶY4!~=!*(قiRj& bt"$ˑ0-NcpUKMx4H[Me9Z~M({jx' o{g/꜕t?g'wqYW/ʢf4@/ů wbWjX |L#;Z5  :6hnBB#4YuqrϦp}Gs`bXߪ.X9;6/f۴د'*w<}A:uس7sy.&T5G^+M՝ːLŏ)LN-ZW sY??_6ZVc(2]%c\@~jz>-xa,BA?]jtu'M2Y=1: 'NQ̓Ţ WV}yCtnK \fJL9BVg%4ߗ;>s1|#AXJvuuk:#Lq''{ae.8_}#~Շ k.@aϳC`?J誩TPj._ &&uFvr._9dT H[8??A.c52_ֶ6~arjcc$lt.Aa)\{#7$# %GΦqzdzQXVX@ K썀0C4#G3 5U֊˗. k %"DO:OJ(b3i.R30 QMcsr&:p Y4 ?_ēZ.xzY!5  c|bmZ), FcqI3Sģ-tᶝ{Ќh'ՒD"%Q|Xv.Bk nKmz$. g9 ԋ!&\g;B +C "1 u {ܙAT6$pBskXʑlց[F'q8{ǦI@q i;D8eq(dAi)woa^+֜*?kCИx9t^ʷ߭H#c8p(34H4 FIئxN>sPPbƤuF.~ዛKgplfiD]ʇڹp~@nħ[,讅3kss+_'^#K My 8~64DA,@_oARW06>T*%ۆ#6b:;Vx"xU45[$Tr3XDU+Ý{ L39ljc#RsGmBa)9>:L&$bA'z N$~IPdkW4ƪ&_{| 38t V'`(7Z 4&{ Jqo)֐lhXzzT6U㯕\016#¤}YKwzU*?G*]})+pYl߾?䃈Ʒfy؜2$TK~Yq56l%!͑-\61 6䯓Ɍ5Ԅݔ[+I Y[8rI %Š; :jmk^.l Adz\ulim )ͤJ95@ gsnMq|^;.f G,rT9Q ][G>]>2|m6L|+I2g[oVsQf짎. :.ާL%gz [o~<[} R,*UTRX`_|>OB)D]]`054?/v#;A5Ǹ~Kq1g̉AI=A|FBqGC2Oam|u{Д$IJ).-WZy4ŧo sLD~_@,Zf b*GX3E+=OYrY%%ER8v*_Yτ󫶉6n`PjPW|c*i\*W^|WVAL]Q y25{كu{YYU@򴈜D<EN$BFxa.IӂUG:C䊖*+AFeYXSVL^ O Cw$8~2HÁ+-4pNҸre---`Άn4k<GݍQL$ǰwu3yV TZN1Fӧ1J&\d㱟FfhZy(z[sͰ5q؞1,s94H5 $ hi,8.LZ%P\rgF! r_Ʃg#gA?$˥ [ tఘI븞bY"H&b8DgY^äLm;N nQ'U.8kK͐b!dG3t4 d`J E0mmR06Mq,/9t[^8S򑄋aæ~~(೻ɶ,ͨR4&?P I\j~zd kIG(n6vcsM8!yCTE2=2Dh&SI $LX@vjLZ7YC+4w²Veo! zց'e(;a%$tttXhZߎ]Iv:pt ^ƥ+SF liKx1bQ|n t%3{^C@+z! k(ˠ+g*UDT> wj|}(=RZ9ۦj{ZtN\GR&diϿ-/[eB^“O>IG:JW_=4YmGE,÷Ţf>53'.''߂Lt4%)戰|_zv`igwrg[aS*}f YSlŵ]Z7UdX h]r uxC(V~ҟ2{gŎ㣏O] vh`G~^O)-|;oَԘ@UfT bY"}Ra Ub - qri.TEE.=9$Z\!k0L) -s.;{%2'gMDgO =X3֠`Zp|{1&_-Us>l~>}[rtƮ^s"D+RCQtLmX+W_y8_en &Z4F#*҅+8zV6TYA^oa!|W'LҶ,-K0M>[2k2LLI 5O"!2]$BI)G֌rzlhrވb4}ݘjzVy3="8Sp,M`UW;%QȓaCIg^%ː4=+ąPƆ pI\p<2I ?D]Tp=2;9"']S' _r~ױi9R3Ep{qmbye]]cJ[Oaj$ .QrY>smQ綪 uY&>6ӅsW U%er ѲGXKW=ßkLg,Tq[%;L6)|֗b(WCc=:W Ł%<\)wz$<Ҧ֕"-=%o8 ,U u7"AI`S &һ\>3 |ĵ(ITQTJpX"89g3ubAe}cY|]glj!L<_I%8{jXn^GA}]l3G%zQLvB\/:\C{K\KJw 8t]/szU|O |a5U&v Kabk 7DPW4y4HӘk( h&?x *J钼Π9?D!w]hljדFٷ? 0""TɦLa䍄BVErPȫd^`c\8+z˱̶>8>aɆcz.z֚V츴 YW&cxvKaI%g`\ COUCA&;҅V0Qɯ m W|( Y,u< > ]F׸żqKukH 94S[7&lU‡S's5ݿ9+}f콪l N⊺5kWCWW$?ɓǰv}?sםO~3\CjEu9r,GB00P^~pSdf X7OH;EH,u8==˃N"f nٽ:K%f>4l5\*SLDղXz#)ތ*}}:ۃE8/#wgG 7v+ j;{bb<'l`[q)-=/xy<^wcxt>9H֭[Ո;e'x c5c$Qr&J;/ skB(MTU4q]RK0 }ޑԾN<槹QjZ"M!ǵ&&G8GZƆKWShG[[7] B %Pߴ/rY@>ă0mN-JWʯ,ucZhh'ҳN*A<ٌ|EӅBxx̷m*CӟWaX4Ft9޻8AaN|sAZF7HO$N򨚉qtDҳ3$ᡇbtlB!D̟%V2#!ea2197}GݙuӭXv;b/}k[lܰUrF3x<$)zB᫸eVS(vb@?FtU%rlC;1i/om{v[6o8Dz8ڸ/oqyLsعk {27XAʐaP;8rXi?xcw/9&W4h^LZWPk܎Qd2dEN~D#qlݗ=L3/-Nabb}' [38~n/>&~7*T*@/hN>")e"5ݿ~Y v5tBGCcD#-rV#Q8mC&a{AܱA>_.BvWgWa=Mrjj-:Tr)]k) E Ac>EٶLrH,pQ^f*ۿyz&T3##ڲj>9to>>$&SPq$4BpDP~\ p'RyaŒ0Bm_y5475K_fFEk1y7^_kq]@=zz7ty#SoQ#bѭqy|WZHq{V[Yew Wzk^r+ Ԁ/@ ӭX%%_h}8 Uڡzrv\g3 bM.o&H1^C8q#뢉B:-,4.n:d4 _dQ|( ȱX$>_Jhq* B^Z8Ky[i,q*2D$bjJK7kau\em$g η_&l@>qxwkuTA,eӮᐍO~O#9Ak*q 9]V(T/jZ6SaE<~eFa+  woBCC7+yBrѿ×/#4 e=زv7-1r7 Jsć(.}4޼da v5z\f$'Fps֒ߟu"&qEԔ?ç13J =B7*o"3061@##c1:~wxhxfW$Q=6$Qj5]aj6ivlĦO1Cg?{0ʋq!'ɤINKXYUyVB[Y*Z*?foJħ392T+gbSs=R(u뷊IEMe_po|WT xDxpnϣC6r~X2FSHr>&w4*~gB7((kaMܶY1:_q{1Ǿ#Ru؃VWF4Ĕ:a_ 4u|kTs0edsrq:V˙sbU2rJOEUc, ͐+e0JlnV#x}ͰDcUK\vkO#|iŐMxA|Gj1rpIǿ~uHرc8qD#a<#mtri4k?+~#;"Q&r\}*h ;d]I~/> 3]LNMah"֬YòT ފ R.^) :ox2S*5Jr ]s~H?rUv# =;q=[ 2p$OSt&Fo>ΏvM֬AԑSR*r\3$ Gشq#ϒ<tkh+5N"$㉸pk>(Pv"L2%kAcc r||"͍m:AeJaiiilтYK:Q)Վ `PX0*Y a+D~᜺4m?x4׼kc\ pE9|U=Mu.¼wo**\c IЉJ6kf( ˶>؊a#f:;|Ռ^Sbˊ`~Cq *^ty7'R{[') Mp,.^=mU%`%VczY hjfk6vADt^тjh&KՀnl봲UlնXYx>6zLX,wu&gRWT,ZV, CDͅB P43$@dsX9#v֭J.]Ρ.QOK\rF44FL{7᳟ێXYPruW׍H.[ivtd,pn_鶼L >\yIZp1|k 3+< L%1SU|" }7Pje}~3]qMu<jA FKJ"$[#BaQ0Lѳ5 R kĆ e$hQ +3=LX]|񫷃$m^KuՌOQvx=k!j'VqsHEپ M-?ᅤF!kH! y)=milK9J> ӕK#C{3p["HDbEag47Gq5"Zqs[>bļc76H'24=tbԂz5ӷV'xfg Kpԕ[ρqV\+21̠mp[U*I“#*ZWa޽22hTu83 O哋=ǟxH5Z2N"M#{)& Kri*x-̌T:-|g5譺zXU͡ ?ќ-@#a+26#x_ &9Z}ՕeWqzE,D9rɮLW-4JcJFmUEeИfd ,0?:v"QYF,eJ{$tބ.7JeghQkPZJAJΡ?_Gww 7&r0jA{,vIg_>{> F}h_c"C )bzj&c:ߣZ.(w1[ID#Ĕ#VM9PƧQgXwP/Ch|b4͙r5;j~8 J/lTc.Sz:̏jnmSK%ke8pYQ߭Rk|GS^xD(ycQMA އow} LiRxF|4"beCIBCsK[_KqmHŐN!֓_$.Νy/_0a rg&j5gFΦpɉIq/4.Ȭ8QuT/8wI£FbgٮKHop~&"O!g(`aG?? h8%Kg64rǩۅȘ+m*jm[ĭd#E'?՜Xj">8rRbsx+G?z sgObhNhjX,zԴ@_FjS>Ȣ񏾁u;\$k* $߻3ѻ3xIF?C\< HtduqgðίB^^pR7Z5zOп YT7aR10tO04[iH7?Zd|QaATSҳ@Z)Qÿ|**y8G1<4!ؽ,*!K 5NXC5xQG89rvp};  C3_CΨJy N^ĶUŢ`U I0e̐!{7b`ƴC+eEZ[qM7Ys.S%fL&)gG`-/w#;?x}=j/W<)Lj}uef;D/|>R@2C.Kk)rUbdZ{-ɮ=T]nMES[Dtѫ-x >~nۈCÅm}Q T^9]Ut㟞%_NF#m|.F2h6$.**ꥱF>ܵ d?&/8c =drI ǿ'i.R{a6^33D9:s].B1s~CbPˊrZNEvesS=*8Gpj=T[ajʱ:Gk>-yv$,=LKi|pV0AC;+)X.cqۖh)Ȃ}%%) W J/k51< DW 70w^nyE7 _Z䧒UM)ԵuَP#ONTQWX1R%u%㨁 gb5رo~{OhoYx#r *PHcp4o/ⶻ7i~!SA%t%yR} jh=oO} WFW"ڈV'Yy0FLA|:FS[=n\=hhJH{VWZΟR))Tp8֭Y+9fu"khdpDǔ,8\\$Bd%i?Vqy~8>2i[zp0C\=(`x/LDXY3IVVgU>OH-hHq^sqeb&f&3OF-i\qTj u4=k#26nN!Qcg/#jD6YCpמ@*Ǒwr]\1M"KC*t&Ų!_ЕQ5yzUy|XA TqZuJ EGeɆ[|iAR~J-U%@TاZdW1Q69Q8ohBV%:3C "ZX^)=NWg?D1ftBO'm.(ќHj!őǰC5GyM'~sU!&F& ]1BxKy'I{FvvsZ -^=m*g lלg!"n O%O7R#ErSeEU"0-;؊m#qd)>z ckDwk"u$L>MHz׫\Sc38IW8Xi=mXk6tÎhC5rut4 N^Ďi*FGoe[Ic$E1r6 $سYH¾Ɋ֪< ;ģh#CVSEFB%KCLԷp˽p%;:0cNq!X!t5cQFL9Z=ތXrtT.sS8p ^J׭ƚ}j.ARoth+ !S[eiꆾɑiLO&]ƅ-\,X Pt&s(}i]C -u! 8"̎ss9o y3TCwqMπ ĖM[F9>vNJ׺[aoǫ`"%q(&"/6m65\ތ3z%-\$=Gu8/SxaX؁H!KcZ Ȟc|z|!k͆^jC,/mC  ^j&!س<1O½. UwTrdRp&y|Q(CQ(7vé ߉$dzs9ƕںs$2˽ q* J:L|-8kNƙn{WZu9+}NU,x30Kc8QG8{xizzHKM^J/uCsTvW˜LN–-HHy|03pG<99`Ώbf:XmimVkWz֣dk`rUR?CXB{swalh?>w_=ChkB} mЭ%8.:mrȷn {NzLe'GH2v& :Ԃ.L ߏ=24!ku@׊|]qc׸bHU;&`?G4۱m>}~C<:r _C⤔_BX@]&4 Ӥ[^/OLx}q VkβZϫzMB^F6u1cE<ք $J #ÓRyͺB۪&9( @d _LZw5| FK!y',[[k+*]C#^|<B.J &CGcNxG8bM *X[?BJw`|bxC@BojЋu֠^դY{lݱ jd1_KY=Z0g7]nxU67^Ɯ6,pUiK)I;C[` sAZt1,dq:3ilnPɏ>>Ώ?:s`CP!rd2,5R Lb>%@$vU1ϜȦ뱡7Pl}bt/q*$vNNSO aúd01 cތT2%w=ݵwJԵZom^}uP61zĭsHTa%=4?M[Zs+.ٮ"O[,K7y&gf019.UUEUUE͟6)Н)Eˤ#ƸfM'to6.ſ>Y!>ICj\xͤOrK[kA+ WxN/m=^/&Gy4z.'h!A>/|t>4kٸOa!!-۵mM@KpI ظa3F=߲|C##CcC\Y*A bUb۴,U'¥khP]oX4G2h~Zy/ڠQNJ8#R iIWM(/"XFu#ߊ y~ն+㚓F& Z*Zmk<< `;~ӻPù*u>䨮#.$eǠBZE "" wLZ)d3HIƒ- &3,6b bYW!M2)O@ʙx&u5i= =iO8+L ^\8 Ut*õC $X*eE)j~a)u_?ΐZrʎRy|E:0J)wWCɴGL(#%BZZ;y'?XȧX8 A\xWe=ҖB Ξ=!o.324mj*+qxm:mdm[Lڏ׿w|QOEeixd#kif: ͦ3ȧaqZE*pm!>& fVN!q LWsm,0P}dUfh`So1<9Nn\{RB퓃'޺ /ҕD0c&UE]Fb3|P%TuKgObI…s=hW'1=9c1rLLizGѢ%<mт[_dI )?}08F֬l"ˬ:Ϗ㧯~@c iR["sr)3'(ֵWߝݰn ~Y5m[{"aՁwڏ{' I&1'&qL%:W1k⌮K SfZbqě"$maSjAmj>ah_!n WyD.R.btyKU qnh {? r\2O~@~~h4$q|lInl"UGjtu;x<6Ŏ[;p޵RLuPk<$бi{ &ǰy㭈FI"zErdŶDIq4w c@Yd{ /LܷEK09'sa(o`^}='ɟ$kqx%Z7Sw]Mnuo~!텓d*;'qחnEݪh>*`0(d]D!`jFro/y<?mw". FSkx1-!sss3GML}xQ[J<) ?s6cf6 rn ȣc_JsQ֜RO9*\ \u>{)_._ EϬɳj6]w: =t/NaӀT^10l ǔ͓C2Ұ֔hi.Ӫo aKNj bx,Kғ"B[($(YQሞĞ9܂kL4k4{~ZP xZZ堪أ4Xi4gݎN#d-,ħa9h͖"-C\>*9C߭YՆ)‰I?_BKguEӭNaWpogTϭl< gp t99ף CB9g*/^G_^t4:49?vġi]_:C2.HPq< p_)locyd<"yn̴!ˇ0:0ތx[X!,<~>GOuМT pD?[/Ƕ[נ]Iϒ6Ƹcf^ec[qe9zoGt= PΚoi=  gR]4pa4x([LH`tu ә+08 jN1/xz&VhhűgTB+z|>g&'Fk];֜"fRlVj(]Jc?5pK_ ]9V((d\KVY(^RWTLmE`}M' =\G1 119&s{۶ۑ7_O>©3EPn۶KNK0==ȁNر>5Tiљou BQKipөwO7>Qxk,x~S*C7O}4Ͳgk ڴLy:̊ςI$GO? F\[C:Π||V47,`@ Lw.. #Gm58?t =dzLefL,(yv96\Ɋ&a14nGM#$mT8-;|}G}Ba@WtTqũIj0Rqr?7Gp% A8@s<(YtŸ,樂%Kx WՎхNR3dM\SGJC+SOJUKK;BVLAcHxlٴ]Yef,V9ciubmMظc͒-[n@+|aÆܽ(rZAUlp,i |8$<an1?h@U|ͻbMp/p N0+0"lsa`ZsaIFiK\Rx O'o|M\c+G ?Əgv%Gp=iJäu7(=Ӭc,%bQY;]7#gZ(l"kIhEq;8Ӄxc}3%YYȆabW-#8y}G>;O2Qzs+hVcQU !ӡ8]Y gm&o2Hx8y(Z[VHn(O v!C1>v$L-&bv' wu΀ yW Np>s-TfiRr@ÏaFo4qW!|g+qZ[:2Dҧ EB-^΂\Xh| WrTxi"rK<*YW,LUþwN̆eزҦLqaB|Tخ M{,xkǪmajW*HC3ԈcO2{rh¥s#xٷsg/3o,YSRtK+Y/_"vځ?!3Mj7}F0W-;odžsџȠ)4||P^&]) *,@v&h]S_NO Y[UO.w5#C`+ˈ,ޖ.rOD:C,[|_}=**k$4ڀuWU1wݍ[z%O+~G7d:ߦHA!jl0˵CG;i@'+o~X }=a2 l s=ÍUnв },/J;%0<he'啁L'QaR^ÿ8nFp YyM^|uN[pݚ41E*M4UN$sx2 V$B=iFr; WXzz[T$Z64bO}FM?~];ko7c|| ux{gde$F洄}!&=zT-O\ Ʈr֬KϿ+qZ4`Z\!.__{)Ӥ9*\)ė:DUk, w)@")52c\5ohƑNջ`D\+Hg#4X*^)+" m~BF/8皨5I}/v-M~豻|B2|*ATz7_ޏ1";ۜ/{(ǔc 1ð]x…A4P.a#TV8? 5{* 2|ay៦J9#M{t gO^D-+1yoЂ[C\GY O

|t5~N @E8Z@+mØrYm4pcG#S9ݪƛK)& ߾W{f<ջwᬪ840z릱>.tuAy(33Tq{?=WR׃[=ChDQ{%-_s a!TIE\G;o-H¡Nkx"Rvp5G ێ?*%Up+wu4vmwIJ8Zñ0V7na.y`*Lz臤-%y;!S=$0SHh vIJL=!\( mdc3 Y^#)Gz" 1u? ng V ٧rMUA]՞if-p [qd1l#lwSY*NI4er_,ȲL@|ͬj"Dhނpk~ uro @Σ\&Νk/C# ZA9Hd>|wܹ+YVmX8S|/+?9 {ȩLU+ƕpP0RLo}yFJBH$x΍bzvtudgOOHٴ b\\qĝ!+c Y َ@Х 4EPg'C LFPޞKq s^E;YC7pע:q-r-aźJ.R# ґDϨNrJ[UV(O1]Y"lH m|9󒈷ɳ<${Wd5Z(JzFCÒ,ummUg*0̠Co-^3!&޵{'yb0RwC鞐1u%' 9$zub|$wzF])BXZ8[a {X'2QUqʲ,v4;p9o4X!u0:4t}2K" /h bd4&#& OU!2$Lo *ʐR<,^ole1\C0Q=g$7%p|a9rnYS)y{o" m|k_ eך\7qݷ_[-l.(T!OE:P7r߾= F@ƒ O?4)LZ9H!|\ΑNdZr &ǦqE _bԒ۳v9;JNi̹35=HIHI/趻Ww?}{m\."8!q܃u`ᖝ!54B.g4/PW, xw6.W? cyIOU0 qHԓ) Ǹ̐Px MZµ]GΣy^%2bKkJySp]46A'`S#^>+jvd% w"nSڀ_FtNP3'EI/QaEcڱG\pЖ45fGb VE]ݗ`~uFm3_{3%9Wg#^X ɀ܋¥YƎzn)+Res~:w+rLU5玸"\0i *1U5lDX5T4[C=|3ꌼ[`tt`\Bg=.?C{&G17_Dk8C qo+By1mjQh?+*F+Ox8ixG+)"s|k6 0V7u[RԹ;5~Ԥ |GXT hyCKB,H6++DBF6#o"׽ʅ7es4ud3 @`!̨Lu!M4&axTk0Ý"߳Lx壽+jxMȴ|!7w_Oh.W%w 'X'_zLZVu4KZ]{X]wzNOjsna[ی{VHJrG$8j4HN6 5u;u+\LJ"Ɲ{o~Z)ݴe>>z%.! '>DټPe!+ЊFiGjlTA~B)9MN>(VEШHPqPpE [Bذ`vDI USdkQ@g/}ͷwrb >Ju Y\% +Grne0TЃ17 2(ZJd\6d #RV)V{,+ "t|?h se_%p2m[U9M8dZ+ $9ſ&r.L0(.u1D"bˎo_ptO (F񘯂],ϪJ@2c6; MPڃmS<'k4i]ߍu[zpˮuXd}zĠ%J!Ҟ:= d)ܓboN.P<`K6 !FKk0$l볬 k{{3e*}4"׍ Z)/eil 1̴zކct8^TE@yX(*9]+?\/c.QSvp*"^t0uGA/R+ "+ 7S0$9w`͛KrxDBN1U紨-J+ d+tr 0?'tv04碿dVՑhpսRx[ cJ;n,e'L¦TZ`,z=4Y^ǥ-dg\|ﯞOJ6犪g`G(M{?W;T?8!CLp SjiM`mMl&S%B*158uL kDpLJy¤EȪXEE^3u!O[!V_[;Gqm}s3^\?Ophqyxwk#I)oje(ŪPEXϜTE\z] 0w#QLg6;ob{6b#{D<@ElO;vnOx;o,\Gz*)vRIqU12,Եb"$E5S+$.A@ +T* է.޵ <͈8ɩ֖*Tn황y^ss@* 7T)&3Y,w2 CL?Gz)CI;B6UX(RR40VuL]eTֈ7y|a k4 K |١5u)#rA?Dx_S(Q5#E|c.@8t/aڜI+k/ǻy@  Nv|qj*MP@V:ZJ-}n;DbQac.q*n._Urm_H~ 3#)4wKCיZ2gr#8E|aԇ0f3,>pj;ǿsdf=eh ^ $g24g֗2kGwabhJ,< =%A,ua"Uρ"WŹ.m Zi 2ExHLL*AX@pB&-#/K,jak{2+S,Ki0Ҷ81TZE*tXY!a)=TElP͉:)921T!_"x'JbtxH% "1nG;g#&ų)}hWM0fmڲFGzם!YUk0tv^9!ni>*Ѝǚ[0, i 1T<GW wÎE׊M%3ֶ@[CF4n-% Sk1 (~E Gu(DرFENs3={wp5x#Cl$K ƤKv(t ex + cw0>EjsNJ8Tr.g:TVNiL*0 f$V~iwi8Ж[aa7e(A%3*̲|aysDxGn2e]NQa^{=F rr-<7^o3@?Wj!"]q~7%ikGO{֬m]ݨkm#)bp\; !I~B!w{5tD`ΏPJ5lP{Z/ȧknFNC"C@TW-&N1;i 7΄y$>#sUD_<9ՓS3bO8xQZ^6ni7Aξ|E[>:1DŽhv@^|12RbÀW ʜrUB$4 %Mf$*C){Bp%D'?z֚'\o*^>gNj|YX0W~au@oUظO~vH+\A2f ^xOz;e )B3-0zJt$_ڋr8^qU`D:Tq[&Z3v9ԓ{ֈ F0:BW JD/$M `(w\I0}~| , exbnk{xˆ~o|R!veVJ«mO3#۴_UXAةPPKHt~)ϯgKhºJ2Vva}Ĺ#賊 N~94ʃbx: i(/ejūd  цV7b}.p LJ`,i,G#tu:j|ns>|g/cgSBe&'3'-UQ3=wEcsT㞧B&<팙HWz2\|-dV~mQ^q9/ ]k%e"b!N7!A#+$k%ViF-lGͰUxnC3 dHA5mГ2YK:O=3\_u-8~,T2c c'y"珐-?$]Q8SG?O$˃Eq}YEf@+Fog U!$r0ř\ܣ/RWG^X荣3^#foaf=$DK:ETƐgVWDc#JW-;|ݘd>tir걷[W8l,Dhq 10BJxTxp][eՄzM>Br^WegsȡwdF毢+6*o ;;ZcS԰U(?^5L&!F5y/Rn;Sf`Qp_U;V,U4Đ-aՐ냄P>o|" 嗒9j O:R(4Už \l~"y %z7*'U-tn/ &q{# _+@=xUdzLK a*U(ţx$Կ XՁq$ZQ0kTS(p& :"Ь0 ~}k -%ZT KBᐔ# l466 EiRjIE2UuMkʇzR2ۏR3y>jheHmHWӆ3 /9<7Eb)( KfTz!J$TiP/ ,.(ź;JUȉu o*AϐT̔Rv" >-&C Sg4@* e}2]?(ڕp~o hlK57Jg C(djix\it$u0&FgJʥ&U y6kmJ7غݽ7=qkv^׶x2uYH.GWd&- rp 2 145aUWZڛQ,6t I{}GlZd12Lm%J؊I`xr?m-x%(7LhfςNH CaQƈtp <&Ӈ7UwJ(^OVװ(!k0LXb)>ඎڻb8nxg {`,&z+p !UطrmMTpD.V0VO%ȧYѲs*ρfɏNhhG.:KDTOrs #a҅ybVHs9b:ab.CKRB~ZVF %U"\eO$ v+C҂ =UJm1btw: @04e96sx/į?e,Yk <#}Th87{I ԑ5^4% 2kr|wBNWjap 1TB-S+U4/l%]/]-8{n<8IyvX/c8AaI,FvX|V}7%hhq7.Hbtt CCHP!3[Up=.vbdjrwÈ!hikD{G+mwj<0h}xHywD 9CBp=Hxn;kh*f} U@wA>e<.⊖*[F1q;wo ?w_9>g aGzYȜ՟Ͻ,HIaRJ cOpmRB!$9Wb^E؄ҽ]ٳd}+h![琟q0,Ԗ;]­wuG(:uCc(W0H!Cؾz3;pDU^̘f'8!D:'=ܑN3K.cjíIL Oh]Ռu[֡gMBqbdmز^Y< y43ݴd srCwr%LVYVD4>H2u'K+7GZr^]?NΆ/^ v:IK4}|ѧ, /|}lc9>Kɴ?”7>JIp=!V9fZJTL`{|DǢSL-83| 1C+Al[׈'56"8r2vѻpqkL: k7t"I_}[(V @2{ t FZiz2<ŚXR6rʖH$ gLp2ɷaOA9y)c'Fy-o[ɑI9qO_$Z\sp`W.{됈GN M9D7ʒ$ fp1= b|:6^2*OmgKy{ʂQ*R1-T1@ I7~w? CZК"f,~3tlw};^8k J3#+t>I,K^-TS"ه( T0C[,4nQ!_@|acV^a|9<һc9 gQH҉:DɱQs},BF Mnc;U Qri8%*zeEqMeʟ@;-DIpUk{ZgR) rٌ[͸tvИ@I:(]));*K 6®}Cc2d*JEtU^` yL4R]¥(StP%Y5zKS8)yʝ$Gw8Z~#12NK{^"hqrs)D~$e*V􅁡~-beXr-6J4X=R^ t?yd{Td`Kt?__WЙmKp 8 ;8 ~ҝ90=GKǽ[ҿd2fҳ_\Va NN rvJiX$sMb8Ν$Rf²TE4^44cfz\,_T"Kc4%[< əa)H.w: z?zMC<:N= ܛ7DMjJtlf9?IDqsx ݫ"u~\-w29)_ՕvW]_KՒ;]o.gv/uኆ39VG>.=geF1:6AV1iBf05Av*E&E?TqR.+SȻI*Y֩T1'V…/T^X4 vJ_?G+ួ['^(U{\4N 89ρz\IJ\mZ+iy<| 1tYpVvUEfjt¹pIbnX]Suj`-0m+:quCMreYp)s=^j \7[)F guG޲2ti~r wc'nj20/v̢-/DA'c55c2b.Hpd> a1Zx֩"ʀ j6>Y0y;hl adKf2/S(<«Ϟ+UV>wM%w(~.oQ I@D's E"زu+gtT` ;= V2CGo_5F~0n՘Q(+xskOh$=oEVo}zlu͐䉲'Ŀ$S/]ඛ mk./hƙ31Iat4AVHa.Էe[AֵX !dy˘ߚHhh+ <¶UWA?Iu=^Cl4,<%4{ `YEF}@)ac$]nzh&O(> CnZ1| ؂L(Ԫ#|A/drVz3\\8QU<<[T"WmiK#]eֹg\8+Nؤ܎cjסxFڹG+ $kSCZ h%YY4}й }n F̺[0ŕ;36\'$YXZm"uk[KQ峿U=B=SdjM.|aQQI[(V.Lq.jHnA} y(hp"kN܉K#8e5TbxGu)tEKp^ׯȫrsh%y_Dz{Vݩf6zj~&,9%פTG!'u~s!F/BfK8x01"ڲP \F{[ysIBu<׶ugN<G2;+rx8y(%@ |m/Z׷@孒\lюB!Tf騺(^UE35^B(9;G |v,A0 SYha(6c>C C]ZF$*(t3 kDFU^.ORUk6&1'gf/–|^/$򍘆`v53|rR;:6IѤ={ .kM;o]xr62R~%@w4'D,PH繹|'j4S}9r jM?xL<-%f^Sn^D0,m4L#(PޡՄ$aȈqڦsRf&3P Z'$o`└*W!:(qw@06X~(< QQIyd x=P4ȧ%LP-iwGѺ*si3i cK0 PYf"o'o<[h ."qXcc2Y| 7R9kɏ/blڍ=d׭ E~&Og2HݗWN6'#J7|+CqQm^ \3 b AQŇD8! ݰ 7-b7aIqİ4zٷ0zA 5*Ti>q=49_@8B@55tj1eM>C (8 d:^n*AbFt4j󄡯M[q9yٓV\#LrO#56BV #آ-<\u 2i ]Ed{!?DF.=mc׭MI"F9P|O>:A/Z+у~5*> DxrjMb+=5{ɩK}hE)iE͈c lތw_=L71%57DXakd2 5|},:wS8u v jzLwhj;*ԘR/8PboiDBmubf*3/rCd0r9>u wΎa4q ZTczb'%%CkR˰Jʆ0yNO`o{Q3@,x`EDMΝkzLz#xo 5N"goّmNTbC^.A&;M򝏞oJBTD9 eФe*1 afh]݅~>%ˊSÚ̀9WVhdy m4Q/6|a#PhYh<TYՋȂy1g}E_Ŝ].AoM=Cfl^G5@2iWGK)؃= lBmyc_,A@r8k_LO2PprSBS@k1TxN4U=/WA:n4iUzh&D| [Y݊t:$ G$ Ԃ)j\`|B.a n%Zxw/:,-)ɎE?bבb]2 .~'z WE&N9QohbéILȐҢ;N1jNGU A *z֟/{ ?`$(/Oaoj8*dpPXKK"gmgsqVNtDck74` o|Tp] a\49WLWGyqU* P2YsF~^G4EZ*<9:g:tWwJ%NYxu܁3qClO#+0[$-qB{xރ4TSj|%$\RYleq4xQd1fS /i*-oRj 4|xh5(pFe3QL 5[|6B0v'N `<5%)ι=6q,qZb[.J\ #bcqrPE;Ub NtOX9$6B+U ݚtM;qaɐ&׆56a ƊGcRH / %G]86@~@wu7bIENDB`PK!!Cjs/build/images/js/src/images/logo/25a37606f64ef10ff60e.wp-logo.svgnu[ PK!oKjs/build/images/js/src/images/logo/389bc604a859dff92f15.google-ads-logo.svgnu[ PK! BIjs/build/images/js/src/images/logo/5585d65b9d8c575e5a1f.gogole-g-logo.svgnu[ PK!orrGjs/build/images/js/src/images/logo/64742f6405be8486218c.google-logo.svgnu[ PK!W Wjs/build/images/js/src/images/logo/6daf36ba57db9c82b6e0.google-merchant-center-logo.svgnu[ PK!ttALjs/build/images/js/src/images/logo/9a968634c60ce598aae3.woocommerce-logo.svgnu[ PK!], Ejs/build/images/js/src/images/829c5735b6338e133556.final-url-icon.svgnu[ PK!'WWKjs/build/images/js/src/images/c4325f35cdc65f85a7c1.success-guide-header.svgnu[ PK!G8%%Kjs/build/images/js/src/images/fb15f79da797ad9cca81.google-free-listings.pngnu[PNG  IHDRPS}H pHYs  sRGBgAMA a%nIDATx}dNNm;.(RT`A`+| X>ӿ {bE+>AQ@QPiJ N$2sgfeI%97YYY{o"BP( BP( BP( BP( NA;o6N0e6ߨh  2VGbH[H2xhCNHku{D6ߦytD@ӗ)9sNY(i%^t 3fY{zR(Җ|o]Fͤ'7M5ݺGE۱koio_Ͽzq OR ug7| ش5{7 }HItNLbۭjms}! -cGb~xe?'"gsCx2q72M|&.v0|ɩvڵQ~6fڜIZ>A;N翃m\qm\uXw͎?ښ׷n}k{;]?=q1H)a֏8; 85`!dp#,]tǔGN9k۶y룞Η_Ͽǒx;Rܒ,/4uNE "ʩ *;xS^g I:LV:BmDB9U'aP`B ; `jpW^YZ:tڋ=ܳi~uuN>m0L׋n;/q.cמY{oSϿ=zgn >*s}zYgyvQ}0#FMVeDNu,oJh)_spl~s?~1nIZU뮻&;i0I':ݱE]/i_[m_Ͽz vɃD6iO5,l8fq9nuڢ[rD6''mb @|h瓍J.?Uʋg>a:2k֬hv%ͷ7wʕat{'/w/zLhM{;=Ic>$ަV}__b3I?i]{zwktNhӎ}tv{ҾDkժUa0wvrYwk6;9D̓jC2l^{-z]z5iXtKJ$Z({u&6!.T<O7B˗/o8~:A=Zv7qe6E1ֵmmŵma4o#OY.1i։;IGE\?cni=zGWo=zww&;I6Ӣj24,e M"HpG[rwL;!I8*N܏ʞtRZ~=ŵժ]{z\;yiIm7&Ǥ?H3mxx1C;vq1i)vr@Ͽ=zڊ볞=z%x%)6Q_)A8r0 {{nZXw% !NPLNNƐ<2l^!?i)P6mÊf]n?Y&?h;8iٲe^-|b;XmF:K~%e|}1 :Ft_?3>m_zwMyC~o =s7ߍl|ށ[jU R˙TuX,@1-ƃ&w0=D''ɱsZ4x;P$|}_JgN}P( BaP~Rr5Րc&l+QN9v:eM؃>7ɜB BP(r?o}8yQE__AO> 7c;(ϔmt 8PX>Tb|w'2Qĩ?O}Ͻ݅ BP(;<x_g?O7Mrgx$ES2D2V8B=v;84Me3u) &E`1e.ڛ/+ BPpuU>O)}=v$ 4t CgPp:.,cfX788*Ac[v_p+ '?J BP(,/Y?__AB24t|fD,qf0?o,ǥk3dnӛ>B BP( ottGN.S}fr)};8rXT(&KP-.yNe^I6WFi8Y?={hQ BP(]IR֯_2,׮]顧 > Q-lے$wǥrl ҷ!qFDv7n~THT*r tr-kێS@|PH=-JJmʏ|ymPL )6XBZe˝t#_CräP( B ˗/Nʤ91xA.]i,߃<'vc;ae,<|rg\ߘI}/|I׮blC?{۾uQ1I=yuR( BPt(W >õOrXtl9]$>n"wMIG;t(XA];v-rwrk\+9Zrr BP(ApGѭ]6کmu4P"D9niV6>l*'kl\x.k#6˹R'p'[&Np) BP(eSa5;HQRlÉNwcHZ9\K./ R0s. ###-Ţ֡Ԝjú;9xNv:VV( BP3"ْ%KM R|r}q#夛>;exlm+Na"}4'.[G˥,W,׳GNr BP(i3<'CJ7jf8$4(c*xҥA/^,F(u~%oz5A|, CV\fw?/P( B)7m `u/R0 U*3Q)7Q7};N*x5hLq͏x -,2R( BPYܸqLC?p7GU@8r'E7Ea͎4XlcHᆒtei+W2 һy<>xH"^qM7 ^FômsN4Ǟ{)SlH! BP( EL*`d:w(YP|.gedMqJP /_ް !r2) BP(I ^ͶPR4rxm$,H9nAwy2$az!by[KYY*0Ο3+ BP(v<v |nڴIA9g& J l-I2*ہ #m Xcu% BP(X8Љ ʋ;h4k{+qlʋ:J> B0Y*l a`BP( b'(PyVߝ',ކ)4(RqG7 w5ȧT$_f " 0Й) BP(vNHg|4ծg@t$[qK[EF+8|2iFBP( B)P 0c8@x7n¤~|pp{zÕع馛… Qy;WP( Bh  T`3MORѴlR<{]x;2Un#7P( BP@5f|slJ;Sa[%+;E$ m '2 r3K + ST*rX:`V( BP<`(ƒk8$c0V(88wRA4* ekC O2I'BP( " UdbMI jlWr}c9j&1%I5L) BP(ZA16L\S+>#1)Jl[rXrlLv(C(vcSP( BPtQ ;ci^{mj߰Zp0^ o[$xyG i_П];*ә-'c ^" rhoӦ/`X\636M|#DG>R?||'onF7cEq?^i㢋.>`Y6 x^'BOL3@K^!m8{ͯ}kta6|"8cBP(.pf2|h ~x;cmuQ-pjlT{ܿl u-nђIN'OW># j,˗/OB2?]9`AaQٵ&E)Qj{שЖ@kWDIGG{M_Bo94t'o^T/^ÒL?PIrkh/$C-wD!e;-F>vYN$wpXU2oߣ.Q'`Z b(3 RG?PEqX+WJԧҊ+6q1}3!OwQ`[ H71!8я~T^oqYX2^W8Hw7!WBpQ16D3OPsABP(6p++T([.r :4KRqȬ9g/i<6l#LsU`q UOy4Ę胟G=/| ~abo2qz1G}I^J2ȟ/* bˠC_|uN;:+?wkwoĘ /~z_*p V2E?RIb5T׾o}+vYSN9El D#&sNl/ dr#x S#6l~8hx,!o)OyJh26{ BX}ZkOpdgR %)rlibsva:zhgu=ol-7" qk(v C$lk?̉r|t7Q#>5C~na8L8P_"cC+ɀ,Ux{wW@Eoc=T_}dcdx3[ OeTw6!3{}Nz%PhA^ =cկ駟NX `83lvt+[]P(!:SkR5:{Wb`VMFAyy6;2F|]TOZ+mS\A4T\Dg}X.`&Mֻ[$7'>@ BP^U>e׿ o7a{%P/ bu3@3J+9j ek,qti2"%*d ypjVۮb HdsMMŮ}ïQ U,yVwsߟr-9APk=@Va%s WFsx 1Fڋ_b p1BX.@T\ ns=W|DILWjH* bE. aCQܴi]q;4(o+&Éd@$즣خ0Tq0Q~b qƦs@2A$aIMXN<̷Wy|oUQQ*?Azo^.h~nF-]8УVanedU#{m-o]F?y4Ȳ $Cf÷ 7zꩳN\.BP,\~`|> Y p|."A-@YV0$]vi&ccO?ꨣV T1h!aU5d@cIX5vy9翸ӱ~oOL5u:©P=Q)?joYxxN7] _ $(ܓObigȼs?N#jh{I#Ya{=y~#~V+o I3m#O '~W^Nc?Or*uB#[$3))֬]ϪgZ@@n4\:^`x) babQW.ϰ]@tO) 6ᇟX5[A\H,3L}q* )+^P^` }xd2K],rk7< b@J-* A6 9L?5]ҧ_YY~xģ&C^sd&$@qR/K.ד38Lae8V166dq?E\A'0a1t៖苿*SO9cտ 2.!9}=+hcxh1wZh@v:N/D+ņP(d9uO!JB$y5[?jb ;}EIJFzo;?j]7dD=U,1I~IP( Ŷ@bFvd,[Ax\@#OZư0js=c|"ƍN1L!I(,=eP_C 22F/םxJBP(f.WL46>AjMބ.qc ߙƊȸAK[ER%aH'|w% ![;v `tb| 0=%?}%ɢaH1 ӟmko,^/)l2*.* B 9*r4x"d0I BglALu`pB50x 1:rJ Lb<88(c@X"y'iG}{.s9)SȹT( B) "11jҷiCV,ANedjgt*_۩vvdS2,$bP( BK.۱|5krVoj&Zz 45vQG:" Q |Glz~g+ BP(ch7d(s8)BqhF [&f+qBP( Ŏކ(4ʱ؀JGShiJs 2V*-BP( B3ږ(τrTgnUXhYcpZ[ۅ@;Fs,PXP( Bd)nBmRy'tRMnJd:묳@6[R%:L*71| " !}l߈BQ^O]P( Eool&Si;L=2sw#Fcd`6?ʕ+E9Az$d, #w;ۉO)|(v`j4򑳩|D?\.zAbHz]ʥt-BP( i3m8gRa5j ȳh" Qb0ϱIɛ=&NI_u]0/y8"o~ڤ-Qā}`{?CcOH\Ow/l>~FOF 7J^-ig~|;#!GM?0) bM ׌bR. s oB cd+t&(2~G~} =stlLD.Ѝ?G;{?/Dv5PX$BP\`ܐ8u9㤕:*TTnQҌB 5\@[oݮNE =IOo|s=7\ӟ4va<>яӯ*jSԦu0=<ZO1;E~^zlG'=9ӏ2H e{z1a(#0(_oWv|+ BPl]s 0BVa)Yy wތU74jl*z <84l )1ncg=Y; wtꩧ 1>GrZmPov3]x2^wY ;_~_W2|y(/|A6aMYkc7Q汗_4^ȥ#2z/veiMBj"V.& iOhuQb3顑ܿF}twWNq|o~Ai7e{;^}͒qZM#n:~vm'1L icX(yApč.'pBE;蠃8H,1 G%TbcJ3 z-8㌰MX#3և*+#,blb|֥E:Li+ _~6NO`5~,3Yӯ, iwEZ^Bu0KP?5=GD)Gz6^}D3n'ö@ɇhG~/| /qؤ^W[<e@{58à۟nH+b B10V\[d}džsx#DXqTn&qܼx!_į5CP|Ig> d25nT9 _=.B {nPo?jrBg]6FK:x/("&:e}06bEv:(/?2L 7XRlcuA @w oo}[;QmD BK }0> bnNqgSɃm9kECFdVK|_Y^z5|rkr 3)?(/ev~py-C=6>cPqAABaW&SN3 Rqe ,vPԂcH_͇wo~Ow *H+#)@/~#bihV%[7_(vKn>e[G:h,$|9^뫴u{ŰO}SP lC?яb?o+ Bc;qPx~q`&kiBP,D@9>>l8uB8A7nؠ3IJxtMqywJ (v t #DZa X0W/TnΡxHcBP,d1rKF vl):A91s[rv&1\h BP( vcT[fMҪ!EVpcG$E6qv( oo޼;nxTS(:y` @u:-i,oF^tNM^R[(}k׏e!ĭן:֪}=zjժ˲+ ;⶚iMsFe#=FR@%ypp07 .IiL$m;vldVۤcN6ڭ?i3j<z}^tNM^R[?~ڎx;}}dC-Yg%qşl v8ȋ*ǭ:lSkb`&Vvhw!lwAHvp3knЪ6O'7v#=Բ}=zڋ[^WI p$D& yD؉;h5Ӓc1er4ƦM塚g9jP, $]@n^0DNo.I7BtWv;]nU?]mI7w 9zMsyssIn*q#Q<"W!KX)v:ņ|ƦBә bkg#4{eBk֋CZ͏kƣv[vǠ]ݸwVtϝU4=zoϷϳ}▉oh/P[@ v HX+(:ޠ0ǑNr `zlrc [lI[hm^R( bw={7-Z$ J1X=v֮]뮴{t&6 {aփEC4 PZײǦJ6l?{Bݨ|gtN.QۋMvEnn^ܲ@H;ե_Ͽ=n]{QD%(ۊ?j@LE(]3.[;CS䤓Nr.RC`^?nw7VKh{b* 6EnltC;ntN'˙efsLw{ܓwjl1L?zΧ3Af)yIGs:ݒd]{6FFFd]6mBepc!ėqöfEߍDߍRYONN ż}; @F񳓛IMӣ[/2mjNn5Zlw(\Gڬ4M|"=YNϿp}m6;im6GwjG䤈`0l7n ǍnUәX!Z;S$.ϱAyB*H^첸K8xVqܦ!n^i\dZ!dNЪvVٮ=e-:߿wz۷轧~/j5ݭjn^'±bՐ S!/ pTVѢ؎ ;sd0D-QMcb-!N5OwKrO(q%BP(#w0ǙV{Q5^'w㑎X-q1HBlT96088 kqA"^2& X<TU)BϱLBʗ4 Hӻ B#nh>AjcʠwQw -.1e˖5o." AcvI'߱iQrH~`{׬Y-_\Ɔ#),Y.z$UaadBP( I+y˝ǝ)ɱ7o3˃K0.YcEHA{v@ib%wRj^nj`+bag&כ,{fjXSlW3@<RB.C0^C*7jN2R|t%4A¶S!ܢE9E<Q/cy^J܎h'M BXuLuKoZ&\`;wNl+pf+A`4gwwM9'u8t!yevDNJJ<1>v۵[3m{ʉ BYwFOz㗉<^I3!NDL] X<g9q8*3F96yAe&''= SID=Tf4Ѿ- i7+9V( v(Våll+N{IaWC%foV^ݔr8|aЩp$*lV%8 u-ZZ)Rѕm*Oc<8>ױH'@P(&iVF_B! >mĸQ6. [j8b>>>.y*,L(nAS8>wNJc8qt0z 1B1WH7ez鉱8 {1d{hX BX@ʹzjpHEYrcXB94my8i[+v*7,bG]tbbĴ5kh c}UsvQb# m-}Mo  B3A7UE@nGsll!B9Ƨ]2uLfW(f26l@ccc[AǶT*ɓBC,IӦm@)K>o BP4JGYժir5776A*Tn@ hB|/vL8zD56@cр4*V5Y\.+1g= ng0%c|IOXP(.foO)>CdPVnb(Q 0*s֮8rۈB$jC4 Q jyM$>q+߉bcWG1q] ' Bp4D1|w96)р9jNVv'ـnF}QGrq`Qb VLA:*xI=o&rYB^0[U) [1XǹQ!&Qx {ͩ=F) BgKN4Y.4ylBs ܚ jԌҙ9fnIY1jܰZ+rdSPp|p/PtIj7?+ BdmrC -u3c vPHmԑ0_r]tRv^c0M6+C5^l(;.d!ɐ5 \O.$&BP("ݛc2S8^-,tyn4*XЍjÌl6:*iRgHTrl6A6zzzf#8E2Z*qR(nJ3S( B1HXA6ޛi^װ]GT GKlTI'D^zi iA7rw'Kc&[4 ϨF6yccc"ÊqC։qɿjBP(s fdҶQ4IE z'.ÙMT3壍ؤr q)nĕ6`-_vL* VBEf"^­Kwӗ-[6O N2l l!~&5Bn`n5R[b@j`M6JrG:Vq9P+ZrJ+0M6yQt,r+v$cfi질[8١mvxB["LKa˅کAR*dٝ}͵)[G*NzjАcA ;cvt6yDxb7N0b Amxk (ٟZ b! Te^P" ZT_ lH=ۥT#k'8[AqKR03\=c+`.lmYݚd!MZP( GCY@Vяm"-dy)?VfcXR(^dj}]6 (v5fz(|@na*V`3@9ǍD9ģ;PaNrAwOZ Quuk3ڊ[vϖGDg>Cx;VqW 1E, d(/| łpuɫI, r[hǣD;nVFLy#袋DGU:H>C{s\.+b o4$l V P<@:׵6`rwM]vva43(x8ySBrJA/p{/e/ݔP.T3 |K" k G [UBZևj!ǡ(ϱb6t8⨝B`<k,w[cY)X$я~4cke~}O<1=D&~_$o}+mC9>[1ywu} _'?tǗ%7M3Cfd@a]y?!daߒ ŎqTnd89M@ Hap#1q;Qe19A+abnڴ) gpPMd&)LF^W/HbG:8t.Ҝ3L;hG]/4GU=? <装Ă0ˀ?OE?i4t^W qB^ 4i|_Ogqb/Jʕ+e^kך5kp?7M|EΧzjr8~eoY[Wc?S7M7}W*r嗋ҍ`=7c_");$:I@ttM.Rڪ|G[UZ9zV՝壱a\4ƮYݭqJQsA#Qؾ/}CBзmz+_dyxs+J3eA;ьn,b ` Qr&Xl(hL= mb_LL$-Yd BG(L9h0qH]U:m ͝n'Cq8Q)nl|ƍ=I יݘj% _*|42sw犭~w]Gq_WH%Qq(-GhCCCb^׊ e.PlTQNڈ2SÎ :4t] ä$.t`08&pDQK^IF|[q{P(9^ݴ iNo-LC~$酆[B! D@HAFC{PYSg?){6(D~-vCZ6q(?x+4o'%#Cۃ%e&S(4" 7nlZ>A96Ў.Nծ_u9#U0ͯ$<ӂq~;sxxtC}=)P E}6]>$USEP{Eթ|xj;fVU}ll/\k+*w6Z n7a;2`GwvX@*DZ퀶fR`hC{$a5WxLf >5j\::: lٲM"Jʹ$[GݎِdvJ % b{5WN8kVa\Ǘ-[&6Pxz4L]KDǡ`QKkؤ0q`| E VӚɪQ2 '>seu̧XWZ4aYث06xt0FI=[+ bq".bժU ਅB!*PG/R$q0;=f0[lqaz;/t(>!/%^5zO+V,ka߸>TsIʱ unߗIB`Qَ[[):lK[D&<'I1o5_P(v&؞c⦦*qh WFe Ȭ{ׯ 2lkt{zzD9vGYbFbgF;%c^ DDR ~<}g~t#T)iȠ.j&kC:a>dFCkmu+O?i]֩UӶmTw;0hGc6[:d[P(v4D=ǘ4# rG.[E\Zp>#s ylˎ:ZD13lOv*%HaRJS*tLK9wci}y]yůW]M|qc=x]b.]BT(S2 ?4 t.R Zv}_7=â0ו?G<\j)5*OMQR\)U^56\!LyȤTKDVǧP(!İU lҒ[f[p"U:R텛zH&ȱYhÃkLe$ T:@mGB0rYVhaYrBLV_zygQTRʄv-[nV0XY/\>q^YvyY\'&'&iʹifr|'+sN9<>}e3T+qBQ '4${jr\ŞWWIQ5d,/talgfT}!Cn,rwsl[kTGZhJK$B~+M BG'}…m+.m%O-JmΉ s9 |OQVTZb:Y8-xD _T%2qMg2$7MZʢƲzq˪pI'Ux &5;>1N6n&?/nOtկKWj⊫?ѝwްJe8ђXe _RZ & Bo-Zkhk=C vP- .x]wm~ K`|z뭴co@ipn"2;[^j?壣< ڳbdkv$&J+Wx(+f g V+ժ)>p[d"mRߪS@W.+rVsXB}ӓ"*NIsE׽Z`sO&ҨHXěqQxy@V Yc?'Sx6`T]&.\ .%Po Sl&XT3Y.@!JNoճ4K|GA7K_=wS]|w _BvTwqg>C 8^zqqQ~o 6oD `3uLE_rYWʺۓ@="m<.ww La|gSDv_P'2vqE@ ixxN%Ƨ&Vi̤ d]_k3Τ#"\B=<-[ 2mU˪eKihq/ 婯7K}=)*e&ekYނC)^.GK{iP?.NyĞ}Oz~񧼌/*r)Nt%W\i\; =!& ת5(Wn>9. YƸZ( Ttjy ehǫnM]uU2`X@%~cCOzғ{S;W\q/ g^W,>z'"HyUR>Χ?~]Ӟ4^x1> $|Ǿ8ҏ??v{կև>eo,7Iű̱/Z{ߓ1'}1}0*?apyaO`9{g1Ķq.ַ_.㘇o 8.wqr p@n>shX{̛G-hooߴu_HR(`}^ q7Ǜ.}h%~3m:'>G;7@_{'3?k|)o@=ƹaU MThк +C슖Y~TIp2>11a'bX(6ْnk ښGxlM;;<ŕ9Li(-E0I#%*:RD2LKce LT}/{]g{y:ϣ:jwZ>*Zl9Z 3?#hՊnn G>{>#i]wŴ߾{=9'^&Z10@sY*Ғe 3L`iyNap)ês:Roo 2dV6 Q͉.<3P* kJZmn ,n6zLgyfzP>O~ɧ_-B+ @?ODM tg:^FB_ lu2|qR |weIݐ5k_zYO(|Qrկ_Oᄏ*?+hDD^hO~r4OD>þ] W_M?OCl 8 RM# D Dd9F_QO}St '8 C-}AOSO=Uq |m_]t O`=q܋9(lF6kԷaD?-Pc*p/7`Pi ~ K8N菉a2c=: '`٧]vEj~g}s鳟lCpN:I(@AbїG<1 +_Jy o/6yC }d~XC$ևߗy˱09~}!#o8j[nU¡A f]9>9ܴ<@1}A=)H6XԮFp%V7|y P] ) qElj6H{vX'zoi=-Ԍ"I`A붪kx*~oA"腺%}27\dq3c;qIEM;DzY15q;u})(r[(2`9g: ҟ|x YUf.Kn SPv-GSwR-2Pj䔐^(Sܷ3yy%KeeRb1MUe_@l+~1zpEI."z"OÓL>ͰZS \)O Sckxx .&U?.]dk6A$ݙecvo}[k^c?,"Un^'P`@yoΣh2oӌ~Njb$(z\C,^ɂ ~`sC ̠Xd1Ww^ r\V@&p͡  WaAzm=ϖD8 NJ2H?ccxh~ 5+u4ClBH_n|3 5YX[k_7s/~i۰c`߱,~C(w 9&c]w%qIn}ƒF!x0w~1{=3NB\54;bb?6&|a]-؍F;2_8N̞\ <;?.ӱ^oOP'ϦMhG"n I|NQ\Y>RSb%~q7(p!3DJUnJ`|_`ų8UCPֹ:r KE(.x$/iQ?v1(b=W|.eEąyykؖ Zic垌LIeߋ)r$q8o&eYvRm(˔-+> )_ 1Fkl`vr ]cY\SAf!bn{} 9nƎw VUVYk" b*Tß#ܠ(%PL&05$;f8ȁ\)9ۃryZr(n=m~pṮEdS\2 e+ (=wg"͓ hƥYFl"5LCtXcb#7%Kiair4FSSL@6 q1g "Vղ"K=}&Ь;i3GXpvbD}|ª'v*73 fV@'`6ڑ}ؿڹUN6WY* ҇KL+. baDZ;+S: K atœI<)F$JJn^\#Ї L̋@8Vz=b W*ՠZ.ês^Ŋlo&)dbv-$>OvX)NQ\Q_}(y+)eZDʓyjL6+Ժ;Pm ǩdXJwND탈ǂW;./HLca{&Orvw'D!2( qQǙeh&`^k()h=)T3*< BqV|0ʱ=)Qk7C.Q*C|ĨX xFvO;.@nV[k1R!#ҒԌ)&njԿ(*kK뤫r`sh!m{{eS+2lC q^5]Ӓ[ύZU5vtge2mA9hh~.ȱjL3YygK~ʺ@HW9&ˬ Oh+I>댛>SYH )*~"sߧP(xt9 DyIVa)DA :7+fg;*Řf+vDIc4`i[ t [Iϛ:`%TLX'J 5Gr zB"--rF^G)P^;/dUt'z磕ZM2\KL6YF 6WX4jVbKQBӟ\>C,HzM<ƒ~.,"vSWc(~"*^*8R\V=SڛrD W*EvPYP윘}3ZQVZ9P(xliZ8r ɕc0h;fQCs&H8-#sN-ۗa.Ĵ\3T[8T' jUxE2L&)#8MTC歂ྚg*VCj6p*T P%0.ru@(}1;ffϕZP9H 'NPY(`>>ʱ7Qj+=[衁L`s|b0<BPN& \ KBXtGr25'fccS1*@-uIuUAV;/YP7KE9rL>N1aɢ-M1Q.|bE+HH#HW5W(zЏmCgZ]@aHP&:  J!~>eW,SyKyZl b囕bͺ4dj]RĽ|rx0z)%uY2}me&[+W)gbܛP /EbI1; W 2ȱm#SEXP(Z;a Z8<Ӥ VawC5cEa!([;`]dE֌>)Τ\2&:TdXb2XrFSŊ(LJE=*p _hQӅ3j;yu:A9nyWn r٤qp E; əiz&g|HMŰ.FQZ*3Vh]TAg]dpZ. L(ߛ%fTZ19-ݐIIZji~]t Y@kLīLxSQ&C*TLʯFWIn& ɶYQ*hI@us~ j$p)u8l> Y'Knr<߇BP(Z#I5ʱ1ZvyD9S4:n;[LW[B!Ph"<2I+Hv2%#DUט)U)WTY6BlU= lk~t6#9EM.%|2L RL2 y.X#Dʓ)RiD|;&q=0I{prA@t6<0y 痏v/4tю+3%޾EC{b]h_y¯0 __DY@5T2xyRP(qIcx㚉֠.qHhH?( e(2]cĒ,Eӷe3yLSu!؂(xiQ/U~V5DQЁ.=o/I v*k`;5M'lm"k͉x-u8b@~䔛+%P˺EX#<ƊkBɒ,|X)d)I!niL$x,d~&7MPTE^99% e7'A⒤v. o~nVR(7oVʱ('*s:'a)iA*gQdX|9U&dyll fT9Vtn o,̶~id~mWŘ dUeioƥ<5 zL:ΦొߖrJwu?/7NCqiplRtj75R>j?3%Y.-U5R/#tu<ሢ u5L>+R')? k #*n 2^To_Bd~*^ElbWv TCW.tT%頀NmBַJ\X,ʐ|>/ R)8EpUW]E%c!sI뮻P<)P(v, [Ep|)! Dh eej 69pdeJ֎qZc> s eIRإJă{E]D7x_tǒO/-O~w#v>nfj?җK8 j׿uzs?A:se >hwK+V Bc&''q| xߠncP<96Ml~cxڵ_P VB NG.7/gpI';HyF8׽ mr0$xem>Ol79EO9zݴ< &=u_*-B~E/z7i%ozSJDam(x6xT(;rlVŷ5zM7.g˃uX~V TjS@iEt=wzL/-)*d(2-$VILfړM~Lm_/x r^s zקT'*<08e^KR; 0鮗89%8+RmK|_b |gҩJ{f?ou@ 6l o{}в"/H6>&>K=/X5PA_WBؕ%dڴ9BC6J4,jρ!x(H b@|4Tc;E+P(߶rʙ74lcc4CCCa;|k#[?PI?HL&PFM)@as2᫒WdR8$/U(#;~?GOXAL[0A1Ii 8B^OxE$x_|l{3hLy̱zng*w QHg܃`Ewh/!c[4P~{qɝtIMpO~!P2{Q@ƾB1GZP8Ikp(sVM &<'^RU@f͚puY޸qcK_B ɘOc T o i #W7nfн}LjL\=) ډ`;THYո#1LTD{}9(X>$keGx Psyz _4@9޷< @z~aɽ,&i׷el:%aC6G}__3>ع^q'6VY( @v7Ql k$=  ƿ/Dyc#G{;D۹{iwD+&Z%Z$[m"lMb6c,qw]7CѼ&e&1p  )ßi02?HTdrN\xx)x|!#y{ .~i!ŚKLV3PZڝ2 ρdއ{7C $W0hfaةruI5 R5YTrd@B&dp\'0M1y~R"[WDNOMZ^RcE&ž;P*;8qw)rx~voOVe fsZJKz\*)9>$p +^ k~ߗnV-il"ћ&z_O?я$0̫$[ƒ%K'yCd;`TmB0{߇z(7+V5nW% Uvc0㡡!t);T  h6zܘԇK2 Mts0OՆ{2\I#Yr9(2)E;&):=PLz<i՘0|00A. x{T6ʘM+cO>MM?W!*JH:Q"x1P$!:˵dpߐR $ABM ѣNu;SfR<1A5&NOec[)V”pP='(E:toxHydpX隆{gxXؖA7H6}{b@>`Al3J+rwuawg(z׻ցGP(_/A 䧝v, .nLR)ϭЉCP,|e8kJzrV,cۉU؃!2-itCh@r{UB-~r1̩#SB52i~ 2z*Ter1#v~J'\O*{Hφ\b6>/*|̄Z(E#3L<PםbevjI& ^⚿@M!2b`D*J_ϤK qjَO' I|&Km| P3>sJ? QNm㼠( ed R?dTr r-D4i _|qa~5yuCEdc QO232JKQ,&r8>)рVw}S// 87nL\ƨ;?c?;#I:h!b;kʱc2sRYpS$[6 ł/N)gb6Xh lȺ@ޝJN2|~З) +~[dTHf\/1#VS[Ad&z5(Lt%uDaE~`w_ yf֘OBor+ZҟUT6K=d(R~aXy<9KcYM_4ŠD~E@`i~%J'^ dWPi_C<:\yղ\ LQŪJ\pY )~mX+@|r&)x~#)`,Bn0Lq0Ԍ r'8 (]vم kKoySy2?ylAB/.[͵ʤrCl#? Y& L lEhb&رcǶV z= ɢ1a^(*[ixzz$F({&ɞ`e 2 f|qD|%9`M^ax3Loy=C r;W&+bˢ6g{a揠; %Dd(4:I,|A嬞o/ Z~'"Z#/\2`;q οsSCKE/EN6K^8L襠 s̮ǵ ʱQ/i꜌_?4d (+#|~*5'ؔwԧ";,H1H;N"Z @L :(({<ʨ:"Q@~V-ɛ|KbHH/b DL:Kw^~b|+EFe<_פ3m+qcx7՗yj,qU4,L;']r%‘[a@gM*5 Mb\1kq/KH]j* o?oo]@9bLtLfiBL U{MvQE8!)VTqeFXj+p:DGw4%!Lë=;nʕ+sWgݺu+.AVIvY p+:mr7LFT)ny!yl() azU;LnVD·k7[kx@mUL iPxpe<7QO_)7"y6 2 t4 TSO=U*͵\X26Ȃ6qc9XCR5 `kW'hpBض{&򾛷ga/AV:uf::#+unb % $աíOL NqWB$\ű!0q1R8< Pcbuqܖ \c&v[7$i"eHYFHƢІ'jGNc^>Y&Kԡ"PwJcBﭐbsBl'@i|y"|ĪȈMKJ 'Z^$% iZ2m x㹢\0Y1rŕ|YG" SpXwDKPYTn9L%͝+gr>c{8&u&8t&GQR,EGeg3Jll:o+87Mebk[.$CZ^\cSPl8N 7)dXpccCütM6 VQ1:l#!9VtfCq; ~'rP]ħ!l ĒM fϚ~u<7X/3ͱ*"Bк(hXԑ7WKI26EG4I麰KVHr|]l BA|XD]F1< 24n dYM巌MG\u;t#)!)R1acK$חuMpq2ie%٭vQ@<8 c.P Y*OT6\,+9VDs^v9&PCP(=` IJ5dZrk!1l#;Pa#m y`ʕ+J>hEH"QҼ c&VcPmNtYp^GKH{nE9I)jObR7=toN'') e/! Loңqi+JkU[L~Q8P3=.d僗ͤ x_*~Qe G#sϑ\cb[%t~&G2KT|gY5R}p@(-\E2W0dC rA=q7ThqIl&xi'm̺P(k lfy7t .VCR*7 ./\&OsUhcLnW6Ib`}=_MF`OTs(NQ"t }&%rM' ȟ_5Rs]6п￟6o"/Z<Ԁ'7V( Ewg;Dy&:sX3^9J،=T!/q%;ZxB ZqIhW;RuAQծm V ~_Tc9QB1XEe- Oa6@#= 26 0 ;$DAv  -(̻ Qq|6n!oa-MN,3 nّ~03$2n`NuuU<'\zx=ߒM=L{঑apu$Vp rݫPP-G=XTL2WJ ǴN%or >96y U bVg%&z ֶUX?0 R_ʜQWyZqc ֹ?Ai)V}" T*,PGЁO׷4$[CA&&0a&&[ yzD`nҶAF ɱLM+#O2Fc=Yz*fX&܎*4vAv3~_Az`=C KD;a.g΁I&0 vR$dB=[YS)E#s+e,XhԊ}[37 stwTPm܀GML{Z*ǎu") ONNU@6)`^h,[ xO4VGqDTMWz:D(JZ6 [/,{p=#(U2ױr``buqbTr v]F4 S|=egV=+nXe؃ ;rn٣ Fט(?/]gjc(Ǫq@v~~b~W>h,%tFxSͧ#2|3ypSXP\ya0"mH6[ǀr A׬YC˗/s4""PmNJֈ`CW][g2^#hu~< LTkLJ l x)!W*~ih$Wdm1s]&L, Y$؉k .p̀Ҋ #=W+칒{'ژClJIF\TCR_Edr5AV? c {A!=VW>QNM`A?20NP^Q7QKzPDU" T`&T!L6bF*{㇀b PA)3Q(T3yHw-Ucdm9QAF*7AX!amhG5_P_bK*P-?JtL8Y Faר0CTA9e&u&zn!/z&g6UUNB"%&ü RadIUAø(U?;Fg@KrD>}I ]}wOJ2_ x("W˦HT_EJ>O)'9=?B #g_7]5&H!( Rn1+ JEZ?([M* #:v`K77؃㚷G*qc: X,Q[<Ǩ\Bv  Sthяhh 3927dWPed7bڗg"UOu飜97ǪFIF@.,)q *OiL^&嚡Kc^ ehT$8ydKE;&5T˥(4RGI:&S\~ r?k~L\R3y~]LZD IV,&2RlNTu14e Q#PV) bn)bO3ߑbҥ(P!=j7šUr1c&C=0cl {Le|.GcCR! en ol@ɄajkF L2{L8my!ƮdaN;&A|d@:eaTX#plc}b)*#fi*LJr/=c \=pDL1acZ3M2庯r_?C5뗑6kJ+2="?xcB cpMdB*իWxgsww- kc ;mٙfy(D$ms4ulU {f_ #%uɷ3g b ĺ?Y)ZM d!C/ᱚ s ɧ&RǤX V1S )r`;5VrL\ =/h<2Dzї.6xׅ=\| $-[JH.旙ؗuGҬnېb% B10YAg q9&2wVa>馛#AjEƬr|hh=h >5[B7ubhc.sV$EǙl_VIxEMZDɃ=SHa)r)j(X0R(NԨ>$tLU'∀ uۨH^PJ&AwZT4q+uI.!37J7HA PӬxYBL1ĶkU4{% o]%93YU~IݭQ%c,ƒ ,lc^Xۘcc}H6,̀  00 4OI%>sYYUY};UY%*"/)jEk[',>@]O$}ox9=qo9QӁePb%LBa:1Lu4|&do/RLJ>i HcL;aKy7sg%V K6U35buT HMf-]RƓL#^o0T֕wdNXP1O"7;!we VVH#""" fYy*J*z>@(XǀUཱིO FBv~cYeۋ`"LFYNq$Nޚ06sCMNysM)%Ix- $SI,g^dJI Tc녛qp;Łx1؀q%!QӃ?Y1!NeA8kDL ɐ|} c$ D4 XwڝZBNN%3xOx)B=""""bkc:q"vNqX|kvh*D\Ep/8t# ;KF#x ^',1ط ~vϋy"0NoؑO!PMF1 tҨ iEQ+=1Y5;K)e&\R˙쉍-J Ps$ظX' b0 3E +`>GFg ~lL\jZ]3{|eCA}]t{5j{"""""ws@1׀Y'xY#"V`p8L@'ncZ.}4m#O#N>@P ȓQ66/z 4iUڞ6dQ%r`cmTXxAnl ̤$Eub%^n/!g~gn<lOmc1hYDbȮ/Kؚ)@ALP٧¢uEqz*[x5C|Srs)^lV46bctI݇yG@9q0^DKȟTq(;FY&P{zͭ=CBe= !L;PnF=jnxbR:RwO_d,(kcڐ(.Yk z42J3o9h@pC mv`bmN IN&ĚLDgN9t8v`\ߘh^ϱUFVR_~y 2s ҌD qC yYx)Pl1q,ԽwBH:k/Hӄl0cŢTI ;`OBlL,v0(J4pJK!V?QA|@/yj C I<;78Tj%/!DS##Y ٌxWiX3p3`#& :?g:Oh|Kkk#Ǿrm&1#GJR99-{gŨs\ G `ץIQ4/תZ\Fmȋ}l}T>VonMpa2a0:9s>Td<#Vx IHN=L,JrxlI5K2gʆC8XAZcú!UB{l&"""dG5 @`:a8j ND6OpVaɱ VsW 0(um;W(B 9SPxJ 4.I@ 31m0Z jz"1L X*0@OsriPce8_Kt ie_ʀ:k9u9Ӻ4ڷD֑o#n1b$0pȤױC*] ,v&Fu|Hq cbۯ$*[9> *l i&sm gr ĝ1kKKKJn"*J*LEh˃ʱt3Q42q"9v*4A!iIu&rwkC}BApg)5ݥIa:b|d +ȄILU}?zJ0cP, 徧ipTFu/O;O_Lv4׆W䪌\nk30$`;L]"*KC֦%5X>飧vb" H2U#[]9sY 4Ҁe~l:"֋SlVBN*CT&ld'UOA$E]A &LKܑM-t2bEN0:,,.Rcb#{1Sy=`XVINTacSHn?^J O)Yy|#D-HUTDbq.,csWKSU4^܀<rG޻w=;:rX/Nm YsI]KPDHD| (7QuX qFi/5eQ~O,J(l1 o`GT#K7nJ4/[^@6X3*-*D^+8O:%}1 Z: Iz.d&9}r~Y"""""=w׮]2HЀ696O;X8r >U F ;(`#ʻDcBjۓgDj2t۬ۍL$b10*Lh8'N%P .l F6GCcܕHM@7#0bLVY9VȼHP%Ɇ;Gc@^j? bВZKDh9+eɞ(k]W#[rhDDDzr[eȫQ9Is ?!{UK[D^6!96Ic93=S \ix]wuRcۃm1BHOB`'ȇ9ύ" bL[X$pϊ2Fv! C)eS[X- Vz;d;:321nOVVPR1c4 $~Od=Zu l_ߧSU5^oB0-B\Ĵ:YvidY u6>/ȫ@NE7"""b^, X8:T/UeiԻ1e u=w]w`[g"1Xb0)3(j|+YTRfC"s7c5NLCҌvDAzyʠ8!ذ?&.El1V@'H2q{Ȭ07 _A1Ȏ @ VяIGk0ab; wDjx]FwQ|ţG:q^yi2‹U &+6 ?aD[B1Bt*T΍x+x4oMu`K s2PFuLg3/"NY2#S5<46c$`x-$MG?^ū#RC$h,:YMh D{~3p3kNGhF2؅-3P~$IZ""""bcp)š8lc %" | 61!1ry^O9u5­GO%3!71U2yxq2Eylh(5ޢŇ%sbHa8>Db/k1EHh G&a{vΓ:u7H_ZY2`F@r:=O+).I4<16~b(C 5KM YF .$ uL7\-rvYX48 N&`{<1IO%m W8镍*lLf#QFҒd]( ۊ)IgG]ӈ-Eh)y`Ո\`!Pi5AuQTԅ%U'ZYtk^p{.fBab[LE.ccaBF(9KO[qDDDΝ;}b1/.<?P2OXCq[)I2#\d[5"N V}ֈ[`b KrBP$!k .J5c;drD-;`H-om@t)Ȋ,Т$IIL (3&FАSV MGhЦWJU놲ȏ}V;b  ٞeg(BDCʼn zd}{w!"+T X~nfuX4 )~ V{@#@kz)&"r$IʳTLj4)눈8=V$Kszhg0>ݏy!"$ 8u72 /vQ#6}ɘl6ZDJ h[5ݥŝ"U M]rg8_pO S^dcn%'ۘ(uPĤcb #[^6 w:T̑CBCȬ Y/xZJ$+ImJd@9kјvQ8UK ܐh@QطUArtbx5ػwoىfvGLGx*w$q,9V %NIl8 !`V&z8bWFŻԔ)H9mXpiYK ,b +IODŽuu$5'Y$+)=}!"9+$cLGr yyF4B䌥<|啡 X,V2m,xT*械7|M^L<q2`s#ᮻ*Cye+yuu=Ĵ8]m;VVVt^'b=$ @n|BhCr%YۛlgxYXp&0H$@f?#g~D#-GYIAtRu$#Td2r6s&HsŬ/ iFbd-$fOXIJ}uXםJYol!ʞ=u&Sb3n}#""mȐWdb.kЀ:s\;)h>`)H98mDd>~&VW ~Vzn僙/i5MF:[Ris:"̀=0EM%;HWHB H!RD@T fK^6x!Lpt*HbknH^ItV!>b6vp]j6cg tFֵYMF>g:Q:a8""|便% >0 k_ZA{Ub|b \P~績r^>,J.0&8:_:K„o.lc@Nguxs\,=޽B7ґ#G(bs ~E,yJib9FO0 \0Q*`gpMLgJ6%DH%p!یUZc3cOH^ `K Ib1cšPwn\cQs [ ]b Pwl8奄W(grlR^MRɰH]ZQ؟rnDDDĦ+[/Ts:tH;[mN &PG#3_ #9So7ʍ y$[ _㍦F~X3 @A,y8c9(d|Jzgm3ɉR̺.<VnbHp &~qDm BgfWTܻBtdC"Q8.&Ȼ,d v 0p&3U]ńFzHbG}.\ѣCo]4 o{cb=ZU8.BCY.,yL+*r(ZTctđa6ѣ q[YY*oIيKsLLƈ!~gnX,yRU9r=uF#""""6ΧƩ'}ˑD$0{Pnr|WA9vqA#$ *s`;lU_狆`hPQ9wB@Gd^3Hn (P*Z6awRFyßSdPSqU{X_A" RFHL;Ud'îB G5VfbRVrD@Qk۵hgH:M.2&`؊GDDD̆=/O,](7n,sk-7:6=^Z믿Rʱ|`yU,..RDۙ# 3z!%" &Fa 6*V9bB0 Bf''0 &>Ȩ­a_640)׆4ZQJr MhmwHIP >`HK?%421r6s.@FtG6 FNVѠOmsIXI:~75L\((P;/m wk񰰭dmdE""ޜEl_;~(7>Fryg}}hϱR(oc4 ^] _!޷o_Yf3@דh [nK |i["x^aVCtLfsU`4$ r_^c+`zB1FLkk4=b 8*VymbIq*1lc:uAŒ4_3):w;@Fӥ^\}^O> c#x;=͟s^s9{f?=z'no_ B>Ї?~~m7V鎣q*=]7^ z؟?7ʝG滣MD<~5ښb:I‹X!5N b\ϐ!@WL~z,b&ҰAf<^7|w7 pnl 0kM:lIQ"76 iF $22 CpՕj kpƪpUbfC "†!mO>WER3 "`d\W}a\)hD4KH7ԗr'L&>eCǮDsaBQ+6Nݽx 3>'> Q._WM]anF |ӟg>";g8+{>=w_҇ L0,{鯟~!=gо]yv/_AЭWk.Ϸ%@ܯA@ckwqYBѵl[ȃ/\!UQ`;t1|㍆tÅJfuͣ9k{֫B e1|L*\xsqXA6%sVwzt*IJR7PЕkIWS$9I!g<M$"vl*jX(Dc%-%鰇@'wbОmKr% G,C)*e&'`<#b,GTQWyż<9BA~ӛ$2>[ߢ/}K<*yrUx<|ЃD\0WW_:_=ϕ@I2ĺG}3ϔv1S}ߔpL+O$moDEnV ŀԅpN륝}cr6>Yϒu}OӃ'XG=JcO;C}c'F/1Qp{{G}@Ї8:>grOɼn?B/g=ALX-sPZ_K/|37?}>{iW/ [Ttݫ>wO|xoa~y˺7eYdy̹1$zRvq{N7}ĺv_$13*[>}W]t߉*?}ʸ?~'Xm7O |]fs?8,#z﫿xCl^ u 0x(8!.Z#.ID,H:piIZVO9u)]aGFbi3.X-֌W9X̤W}.ʤu#~k. OpGG 81GSQD=ט0](| Dd/6loL@Dx/=iя~ q)okg-?ډmE?ٟY!'Wz{#uɟo|׼FaAQOy_ڽ㷔Z_, ?\/a:헼3\ѿ$Dʟ: +(=hy/*~'3ߣhYxLZQ9v+8&Gl.C+$ uIt1U5ʯ` W\qbj(@yS"˰>ڀ \|"@EG>RH{/x j0H_ Ox0|36caa~W5g~Bn׆j,p=t_u6`SnHk ȳBHeyG\p? 1:B%+C5g@ĊS)b`WY]$b-dPk{@aV^a;`guϤxe%C}ZgHkG@r BuXc .UncuJb'gV|0X>k/?*]B=,sfZc&)gR::GK?kr;YHh9cR%TKo/|AFLBi`ȩÍvBAGY ֈlx^I͓M2)0cH cxXXU"jEZH, ??.AA,BB,GCmXIf]D(Pݶ z׻Jkw7OOVօ- ?>2/5:~o*r6A Oօe=iX B%I& O2/k?yyuvK?q˨ E=/<.E[@}7~s L1/zrgF7p?rͅ)G"긶X\ifU%s "jY],'yiTXǐ1  ,pLu*22aؑ#G4El=AFpˎAS]Uބթڀ2vC\2ʁO&*2<#$IT47 s#3` 6@ty=\l2mf=x1u&3RU#:F"ژ@8'#Q#JQǀ”`,\ddPQCw#!pP#|ށq|KF&6"Ej:m;@o; _-oyQxW-.Bz- {D &l wꫯ;? V xÛ.xcg, GO|>!yXbvMoW;n]L._ _:"DC8W}2 ^6@/S&}Ϧ1v9?<'*"5?w>]ҏSF9:PXi7Eb~mĽ|x?%"ȣ3,:"!Abq~2MEZBSP@B!A5"G=T3B(\0f7oZ}^/צy >`Sq&BE]r6@y(-d~fgqe#}7Mi@]v] PzX7l쫦>ETqgxrtH,}9 U^z8 =oAsuq-@ūKN\vpIؖB˟cV :Z7IO43 6RD !,[QD3dxxm1/MêqC}V <-M$3+/:P @PgBSRzxtX.ձbain72➳JS̪q wtM}Hlg9M,2p}<}F@d(ƈ)QeP$p >!V$<)eLW5zKi6V颧:6RBA4/ÄB6Jg(@*UےV1TM'~]y1MQ, m+B$ [[1ПPizP@^Og@k.nkoꪫ/0VM|{*c"Ā䓯 dm#W~"( 0q@^DЕw|"#>0Lq;*W2L@Cy"|5"v .$esMRlu@9cD F&ޠBw!əL3MYFr 4;$7]4%WYJȚ7)! YW| )BA}Rxd-&Ӟ( ~nRt囅.^dB]n_e8~wo^LD grs ,298% ].\sMj j@G6K^9TVneo,Gĩq6$YbcesJohG$,ɲ#C\3K`Ĥ5{$PK:&)HI9tCt`;.8+IOt)˺;|HDW^W>2!I;0¼[%OO//UHߴeMO|T2p1F}c}ۮq}ex%/k?|~g>SRBqlQס]bB9[׳1Hnm"zضPJ󤏞6c2Y!/XISDlU+ޮv<mfP>avK9ωLc-T8Msh~-HfL' sܖ*=9ϡZI~|_2]wu2$<ϕs'=(᷊8etM7+*K_H#=n`e6}C7e>7^/R@37 c8 Ϙ57??FYl'uX-oo=&/퓟dَ720g}m!bP? ,Me tVm=zu9nDQ>3";w&bnL#O HIq]:DzԣJ|e T5xwy#)Jޥ^*$Q@*YP2w]QX!EDwҋ^@&=B}{ߓ"ShY=g}vyÀ????}zC\\v 85?z`PRs㘀CǍ&w3~ac^?iO~W2/~񋅜Aew)C:}2 @E9Cl'.U|=K.w]C_h?|{#} {Wn3_r%`3 m[Va,\*ByCzֳdꄢPBMu&@B cB@q}acAF6AG'nZm 3Ȧ U#+ ea=8wAĺ8.nr{eB9,B䚀cup?E۾mXշ7!; 7%>SPTX( 1M?*>nYV@;~B-n|= v}jO2q+"9n`:ZÅr\<|Yl:$|Bv|DgW2u v"ӹ>V8F4 rN'u/[ZZ >r zn0!um{[H K_Ry .5H#PaH.Cuh2\q#h ȣS]>=XCe g M u_|Gچ&tzmǍTUJ?aQi֏y7\6(Ίreߟz8; ;nqӂsp/`Wrށ cʬ]+[!V>ۧxRӵLLPeE?[O<uBgѿ4=X#n@ SS\~b?uW z( MVDj'ua8ׅ, 4&Ͽ[%'|p.)Vh70N;~ΚگL߿ͨ9,w>P#6NTI@p3^&5(Ǫڲ|HC(r*GnVa,N;M9%%g?r'}{ ;w,QD6qv7=Y?ekŅG!`D:lì! vyډ7J{#Ix[*dc> uA:TϦr}22 {ߟz} :0I[4ġN\C.n:PFC9xa83PCiR7 zl˱Ĵm;'=I7~=^GL*LI4#HL{#l_؏j7ku|,`5X-gY.^B |s wa(}'h1ybNg/5Y[Su8x2A_/sp`!hCCu4a@=!b i>l01mY&F1k% A;RV_Snciǯ H܂P}bnv¢2OM^r_|c<ɂʱƍKS;| .H.Tl,@%Af)qUDl >+G1oZil6iJTsmTv j$ aBx _Hc ʳ[\bX'C#pbP ߦ]]vU?ks' t7#]@(a&4/pKꅏ iE\\\$~\Cb3ɕS4USuݛ7R""""N̼u~<#4ɐhnmzsg|}9obϕqA)bTkĩAye +JyqC{FDDDDDJ0+H5g_Wfvޭo &>QQJ\' NE(Mω'%#lS!3QWGl&?R |Mx9OfQǸiA&|̴UV |7kX*0: ΍r WD:PU41>UUW@PЈgPcxL0l+kR<+opG*;mD,ab?9ݻ7HW U1 HdQ :Nߣ*i?֕`g>;SlXÏT1o:]M RG~q,w 5oi6S%2Ɇx p=u]o 0M:؅rj?TG׷sMUlSm\ Yztb2Ju7h&b~!\ 潘oܖ1P=vf\`)T޼ۇiN>[oCh~5;S${L|J^_W}g??Zr/0P=嚪eݲLoD45l{z]g .ou˾P`z<V}ȫU^F;UCФeV}N 飽'Er b [,j 򶹃{@,1qhRoPuồ/SBDU#1;o(؎UNJvm~juzq15n;q}wqmDOe~?\9zUe[-n($7DMyiOLJVV~|e_}i x?cru)@U_m*HrwMWKM؇޳gyuvi <\veUL@P9vFz7_9NOm""E$%hXEb 7PYǃW+e*AlKB3PdGD̃jtNzM uM͢V$Ȝ]"?}rs'JS XmlYrǨFQɵӐ&cUeTC(n#TvZzzJBUiu膺B}l{=M:z7>?x'(TSoz}oƯ~<5J2<ѧ/k/Um? .|AeX11nk QTU@ǹe!?\GGlӽŌh*ߤD{ld'SQ7(𙦔 -W՞lԣZԳgioڬ?mچY^Qԧxmϳ~jmUf72 x~{߫PnXlPiF!@V1[&MNW aqGETyTUjrw8ȏ#""""3ƶ_.f6 jr剭&g)EPk>䅷(AVͨkWm>@/P=Iڐ#"6=R%Ib , rSuF5ˍ3PQTUڼ͹ڛi7-є:k=.(xiF]UzuJf\vWrkRF+Ѡ/9*<р7] M1UWr&~s9G|>j4"k(ۏr'|XݻW\ w}NB.HcNKmE 1s b|mkYg%Mcl#֋iISEˉ.Ψ?;0T* ]u*P^M)ZGiiμmmml[~ֲy6ͣ)ǟZ:ۖx9 c-Fݾ1KW?*Pmu q_' cNzWIr0r٩R+dU< u 0×sGGn' ֥o^83*md$4y9P1[l$ZQFZ ǭ乐cAD+I}˷ޭحmvv<gx7W&e-ĵȍ1ؐc_Ǥ ZI~JkUӘ3bݻ'9vk_@g<!qko~9υr^\\Q;8\V]{_b][-l*f-)}T} /3Pru᳞VG}=w?^;{RAh= o7ݰj[zl~jP)tþ6Ѐx.GZ,r\b3BHy2Y+lPc@=t[~NwL(KS|GDDDDDu4/ $pHE^#uj2T"C&,:tHC5t=ʱ]+khj9i~Uovnr""6:8NW{T߻hiqvE FbEGNڹ͵ =^[x|ՕkKB3)!Ү|= ]ԋz7v,s[ , ؐXbZ໚$;vbܡvv:rLՎX^K/szpJ'@|TP[4@Vu+v~t:VR-VݻwSDF0sL!k?eޑ#G0)"b )~]kuq!w:'""""""6\GALR?/D PR+~z^d?_ix1^[IdW_}u峟m ·yٳR HH\ݣ) AVTXbAt1@Au*p-қPj_5`܉(js/build/product-select-field/block.jsonnu[{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "google-listings-and-ads/product-select-field", "title": "Product select field", "textdomain": "google-listings-and-ads", "attributes": { "label": { "type": "string" }, "tooltip": { "type": "string" }, "property": { "type": "string", "__experimentalRole": "content" }, "options": { "type": "array", "items": { "type": "object" }, "default": [] } }, "supports": { "html": false, "inserter": false, "lock": false, "__experimentalToolbar": false } }PK!p2js/build/product-select-with-text-field/block.jsonnu[{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "google-listings-and-ads/product-select-with-text-field", "title": "Product select with text field", "textdomain": "google-listings-and-ads", "attributes": { "label": { "type": "string" }, "tooltip": { "type": "string" }, "property": { "type": "string", "__experimentalRole": "content" }, "options": { "type": "array", "items": { "type": "object" }, "default": [] }, "customInputValue": { "type": "string" } }, "supports": { "html": false, "inserter": false, "lock": false, "__experimentalToolbar": false } }PK!LA??js/build/ads-onboarding.cssnu[.gla-stepper-top-bar{align-items:center;background-color:#fff;box-shadow:inset 0 -1px 0 0 #ccc;display:flex;min-height:64px}.gla-stepper-top-bar .components-button{align-self:stretch;height:auto}.gla-stepper-top-bar__back-button{padding:0 calc(var(--main-gap)/2)}.gla-stepper-top-bar__title{flex:1;font-size:16px;letter-spacing:0} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .AnC9WXFuKgCBURYIRcRY{display:flex;flex-direction:column;gap:4px;justify-content:center} .gla-step-content{display:flex;justify-content:center}.gla-step-content .gla-step-content__container{flex:1;margin:var(--large-gap);max-width:1032px} .gla-step-content-header{align-items:center;display:flex;flex-direction:column;gap:12px;margin:auto;margin-bottom:var(--large-gap);max-width:600px;text-align:center}.gla-step-content-header h1{font-size:32px;padding:0}.gla-step-content-header__description{line-height:16px} .gla-step-content-actions{display:flex;gap:20px;justify-content:flex-end}.gla-step-content-actions[hidden]{display:none} .gla-section-card-body{padding:var(--large-gap)} .gla-section-card-footer{padding:calc(var(--large-gap)/2) var(--large-gap)}.gla-section-card-footer[hidden]{display:none} .gla-subsection-title{font-size:14px;font-style:normal;font-weight:600;letter-spacing:0;line-height:20px;margin-bottom:8px;position:relative;text-align:left} .gla-subsection-body{font-size:13px;font-style:normal;font-weight:400;line-height:16px} .gla-subsection-helper-text{font-size:12px;font-style:italic;font-weight:400;letter-spacing:0;line-height:16px} .gla-subsection-subtitle{color:#757575;font-size:12px;line-height:16px;margin-bottom:4px} .gla-subsection:not(:first-child){margin-top:var(--main-gap)} .gla-section-card-title{margin-bottom:calc(var(--main-gap)/3*2)} .gla-section{display:flex;flex-direction:column;margin-bottom:var(--large-gap)}.gla-section--is-disabled,.gla-section--is-disabled-left .gla-section__header{opacity:.5}@media(min-width:600px){.gla-section{flex-direction:row;gap:var(--main-gap)}.gla-section__header{padding-top:var(--main-gap);width:33%}}.gla-section__header h1{font-size:16px;font-style:normal;font-weight:600;margin-bottom:8px;padding:0}.gla-section__header p{line-height:16px;margin:0 0 8px}.gla-section .gla-section__body{flex:1} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .gla-account-card{line-height:16px}.gla-account-card--is-disabled{opacity:.5}.gla-account-card--is-expanded-detail .gla-account-card__indicator{grid-area:1/3}.gla-account-card--is-expanded-detail .gla-account-card__detail{grid-area:2/2/auto/span 2}.gla-account-card__styled--align-top{align-self:flex-start}.gla-account-card__body-layout{align-items:center;display:grid;grid-template-columns:auto 1fr auto}.gla-account-card__icon{grid-area:1/1/span 2;line-height:0;margin-right:16px}.gla-account-card__subject{grid-area:1/2}.gla-account-card__indicator{grid-area:1/3/span 2;margin-left:16px}.gla-account-card__indicator--align-to-detail{grid-area:2/3;margin-top:12px}.gla-account-card__detail{grid-area:2/2;margin-top:12px}.gla-account-card__actions{grid-area:3/2/auto/span 2;margin-top:12px}.gla-account-card__title{color:#000;margin:0}.gla-account-card__description{align-items:flex-start;color:#1e1e1e;display:flex;flex-direction:column;gap:1em;margin-top:4px}.gla-account-card__description>p{margin:0}.gla-account-card__helper{color:#757575;font-size:12px;font-style:italic;margin-top:4px}.gla-account-card .components-card__footer{padding:calc(var(--main-gap)/3*2) var(--main-gap)}.gla-account-card .components-card__footer>.components-button.is-link{min-height:36px;padding:6px 12px}.gla-account-card .components-notice.is-error{background-color:#f8ebea;margin:0}.gla-account-card .components-notice.is-success{background-color:#edfaef;border:0;font-size:12px;margin:0 var(--large-gap) var(--main-gap);padding:16px}@media(max-width:600px){.gla-account-card__body-layout{align-items:flex-start;display:flex;flex-direction:column}.gla-account-card__body-layout>div{margin:8px}} .gla-authorize-google-account-card__error-text{color:#cc1818;font-weight:500} .gla-connected-icon-label{fill:currentcolor;color:#23a713}.gla-connected-icon-label svg{display:block} .gla-ads-claim-account-notice{background-color:#ffeec1;font-size:12px;margin:0 var(--large-gap) var(--large-gap);padding:16px} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-ads-terms-modal{max-width:600px}.gla-ads-terms-modal .main{font-weight:700} .gla-content-button-layout{align-items:center;display:flex;gap:calc(var(--main-gap)/2);justify-content:space-between} .gla-loading-label{align-items:center;color:var(--wp-admin-theme-color);display:inline-flex;gap:8px;height:36px}.gla-loading-label .woocommerce-spinner{height:24px;min-width:24px;width:24px}.gla-loading-label .woocommerce-spinner__circle{stroke:currentcolor;stroke-width:8px} .app-select-control .components-base-control__field{margin-bottom:0}.app-select-control.app-select-control--is-non-interactive{pointer-events:none} .gla-connect-ads .app-select-control{flex-grow:1}.gla-connect-ads .gla-subsection-body{margin-bottom:8px} .gla-free-ad-credit{align-items:center;background-color:#dcf7dd;color:#000;display:flex;gap:calc(var(--main-gap)/3*2);padding:calc(var(--main-gap)/3*2)}.gla-free-ad-credit svg{flex:0 0 auto;fill:#008a20}.gla-free-ad-credit__title{font-size:13px;font-weight:600;margin-bottom:8px}.gla-free-ad-credit__description{font-size:12px;font-style:italic} .gla-free-ad-credit-country-modal{max-width:600px;overflow:auto}.gla-free-ad-credit-country-modal table{border-spacing:calc(var(--main-gap)/3*2) 0;margin-left:calc(var(--large-gap)*2)}.gla-free-ad-credit-country-modal table tbody td:first-child{font-weight:600;text-align:right} .gla-google-ads-billing-setup-card .components-card__body{display:flex;gap:16px}.gla-google-ads-billing-setup-card__description{color:#000;display:flex;flex-direction:column;gap:16px}.gla-google-ads-billing-setup-card__description__helper{color:#757575;font-style:italic} .gla-google-ads-billing-card__success-status{background-color:#eff9f1;color:#1e1e1e;line-height:2em;padding:12px}.gla-google-ads-billing-card__success-status .gridicon{fill:#008a20;margin:0 12px 0 4px} .gla-budget-section__card-body{flex-direction:column}.gla-budget-section__card-body,.gla-budget-section__card-body__cost{display:flex;gap:var(--main-gap)}.gla-budget-section__card-body__cost>*{flex:1}.gla-budget-section .components-input-control__suffix{margin-right:16px} .gla-budget-recommendation__low-budget{align-items:center;display:flex;font-style:italic;gap:calc(var(--main-gap)/3);margin-bottom:calc(var(--main-gap)/2)}.gla-budget-recommendation__low-budget>svg{flex:0 0 auto}.gla-budget-recommendation .components-tip{background-color:#f0f6fc;padding:12px 16px}.gla-budget-recommendation .components-tip>p{font-size:inherit;line-height:20px}.gla-budget-recommendation .components-tip>svg{align-self:auto;margin:4px 10px 0 0}.gla-budget-recommendation .components-tip,.gla-budget-recommendation__low-budget{color:#000;font-size:12px}.gla-budget-recommendation .components-tip>svg,.gla-budget-recommendation__low-budget>svg{fill:#1e1e1e} .app-input-control .components-flex-item{margin-right:0;max-width:100%;width:100%}.app-input-control .components-flex-item label.components-input-control__label{color:#757575!important;white-space:normal!important}.app-input-control--no-pointer-events{pointer-events:none}.app-input-control__character-count{color:#757575;font-size:12px;line-height:16px;margin-top:2px;text-align:right}.app-input-control--error-character-count .components-input-control .components-input-control__container .components-input-control__backdrop,.app-input-control.has-error .components-input-control__backdrop{border-color:#cc1818;box-shadow:none}.app-input-control--error-character-count .app-input-control__character-count,.app-input-control.has-error .components-base-control__help{color:#cc1818} .gla-campaign-preview{height:270px;position:relative;width:205px}.gla-campaign-preview .gla-ads-mockup{position:absolute}.gla-campaign-preview__transition-blur-enter{opacity:0}.gla-campaign-preview__transition-blur-enter-active{opacity:1;transition:opacity .5s ease-in-out}.gla-campaign-preview__transition-blur-exit{opacity:1}.gla-campaign-preview__transition-blur-exit-active{opacity:0;transition:opacity .5s ease-in-out}.gla-ads-mockup{background-color:#fff;border:1px solid #e0e0e0;border-radius:4px;height:270px;overflow:hidden;padding:10px;width:205px}.gla-ads-mockup .app-spinner{align-items:center;height:100%}.gla-ads-mockup__placeholder{border-radius:4px;height:3px}.gla-ads-mockup__placeholder--thinnest{height:1px}.gla-ads-mockup__placeholder--thinner{height:2px}.gla-ads-mockup__placeholder--thicker{height:4px}.gla-ads-mockup__placeholder--gray-100{background-color:#f0f0f0}.gla-ads-mockup__placeholder--gray-200{background-color:#e0e0e0}.gla-ads-mockup__placeholder--gray-300{background-color:#ddd}.gla-ads-mockup__placeholder--gray-400{background-color:#ccc}.gla-ads-mockup__placeholder--gray-500{background-color:#bbb}.gla-ads-mockup__placeholder--blue{background-color:#4285f4}.gla-ads-mockup__scaled-text{font-size:20px;height:1em;line-height:.9;margin-bottom:-.5em;margin-right:-100%;overflow:hidden;text-overflow:ellipsis;transform:scale(.5);transform-origin:top left;white-space:nowrap}.gla-ads-mockup__scaled-text--smaller{font-size:18px}.gla-ads-mockup__scaled-text--larger{font-size:22px}.gla-ads-mockup__scaled-text--gray-700{color:#757575}.gla-ads-mockup__scaled-text--gray-800{color:#2f2f2f}.gla-ads-mockup__scaled-text--blue{color:#4285f4}.gla-ads-mockup__scaled-text--ad-badge{height:auto;line-height:1;margin-bottom:-.6em}.gla-ads-mockup__scaled-text--ad-badge:before{background-color:#f0b849;border-radius:6px;color:#fff;content:"AD";display:inline-block;font-size:16px;margin-right:12px;padding:3px}.gla-ads-mockup__product-cover{aspect-ratio:186/143;background:50%/cover no-repeat;width:100%}.gla-ads-mockup-display .gla-ads-mockup__product-cover{height:126px}.gla-ads-mockup__shop-logo{background:50%/cover no-repeat;flex:0 0 auto;height:44px;width:44px}.gla-ads-mockup__search-bar{align-items:center;border:1px solid #e0e0e0;display:flex;flex-direction:row-reverse;height:23px;justify-content:space-between;padding:0 8px;fill:#ccc;background-color:#fff}.gla-ads-mockup-search .gla-ads-mockup__search-bar{border-radius:29px}.gla-ads-mockup-map .gla-ads-mockup__search-bar{border-radius:4.6px;height:27px;margin:10px}.gla-ads-mockup-gmail .gla-ads-mockup__search-bar{border-radius:3.7px;flex:1 1;height:19px}.gla-ads-mockup__search-bar-menu{display:flex;flex-direction:column;height:9px;justify-content:space-between;width:13px}.gla-ads-mockup__search-bar-menu[hidden]{display:none}.gla-ads-mockup__product-banner{align-items:flex-start;display:flex;gap:10px;padding:10px}.gla-ads-mockup-gmail .gla-ads-mockup__product-banner{border:1px solid #e0e0e0;border-radius:2px;flex-direction:row-reverse}.gla-ads-mockup__product-banner-info{display:flex;flex-direction:column;height:42px;justify-content:space-between;overflow:hidden}.gla-ads-mockup__tab-list{align-items:center;display:flex;justify-content:space-between}.gla-ads-mockup__tab-list>.gla-ads-mockup__placeholder{margin-top:16px;min-width:25px}.gla-ads-mockup__tab-item-with-logo{text-align:center;width:44px}.gla-ads-mockup__tab-item-with-logo>img{display:block;margin:0 auto 6px}.gla-ads-mockup__shopping-product{border:1px solid #e0e0e0;border-radius:4px;margin-top:6px;overflow:hidden}.gla-ads-mockup__shopping-product-info{display:flex;flex-direction:column;gap:8px;padding:8px 10px 10px}.gla-ads-mockup__youtube-header{padding:4px 0}.gla-ads-mockup__youtube-header>img{display:block}.gla-ads-mockup__youtube-product{margin-top:6px}.gla-ads-mockup__youtube-learn-more-row{align-items:center;background-color:#f0f6fc;display:flex;font-weight:600;padding:3px 6px;fill:#4285f4}.gla-ads-mockup__youtube-learn-more-row>div{flex:1 0;line-height:0}.gla-ads-mockup__youtube-product-info{display:flex;flex-direction:column;gap:8px;padding:8px 0}.gla-ads-mockup__search-header{display:flex;justify-content:center;margin:4px 0 8px}.gla-ads-mockup__search-keywords{display:flex;flex-wrap:wrap;gap:4px 11px;justify-content:space-between;margin:8px 0}.gla-ads-mockup__search-card{border:1px solid #ddd;border-radius:4px;margin-top:8px;padding:7.5px}.gla-ads-mockup__search-card+.gla-ads-mockup__search-card{border-color:#f0f0f0}.gla-ads-mockup__search-card:last-child{opacity:.5}.gla-ads-mockup__search-card-header{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}.gla-ads-mockup__search-card-placeholders{min-height:38px}.gla-ads-mockup-map,.gla-ads-mockup__search-card-placeholders{display:flex;flex-direction:column;justify-content:space-between}.gla-ads-mockup-map{background:top no-repeat border-box;padding:0;position:relative}.gla-ads-mockup-map .gridicons-location{left:99px;position:absolute;top:116px;fill:#f86368}.gla-ads-mockup__display-product{border:1px solid #e0e0e0;margin:12px 0}.gla-ads-mockup__display-product .gla-ads-mockup__placeholder{margin:18px 10px 12px}.gla-ads-mockup__display-product-locator{position:relative}.gla-ads-mockup__display-corner-buttons{position:absolute;right:0;top:0}.gla-ads-mockup__display-chevron-button{align-items:center;aspect-ratio:1/1;background-color:#4285f4;border-radius:50%;display:flex;height:22px;justify-content:center;left:50%;position:absolute;top:100%;transform:translate(-50%,-50%);fill:#fff}.gla-ads-mockup__display-placeholders{display:flex;flex-direction:column;gap:4.66px}.gla-ads-mockup__gmail-header{align-items:center;display:flex;gap:10px;margin:5px 0 12px;padding-left:1px}.gla-ads-mockup__mail-item{display:flex;flex-direction:column;height:19px;justify-content:space-around;margin-top:10px;padding-left:28px;position:relative}.gla-ads-mockup__mail-item:before{aspect-ratio:1/1;background-color:#f0f0f0;border-radius:50%;content:"";height:100%;left:0;position:absolute} .gla-campaign-preview-card .gla-section-card-title{margin-bottom:8px}.gla-campaign-preview-card__moving-button.components-button.has-icon{background-color:#f0f0f0;border-radius:50%;height:auto;min-width:0;padding:4px} .gla-faqs-panel .components-panel__row{align-items:flex-start;flex-direction:column;gap:1.5em}.gla-faqs-panel .components-panel__row p{margin:0}.gla-faqs-panel .components-panel__body-title .components-panel__body-toggle.components-button{padding-bottom:20px;padding-top:20px} .gla-vertical-gap-layout{display:flex;flex-direction:column;gap:calc(var(--main-gap)/2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__medium{gap:calc(var(--main-gap)/3*2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__large{gap:var(--main-gap)}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap{gap:0}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap>:not(:first-child){margin-top:-1px} .gla-paid-ads-features-section .woocommerce-pill{color:#757575}.gla-paid-ads-features-section .gla-section-card-title{color:#1e1e1e;font-size:20px;font-weight:400;line-height:28px;margin-bottom:12px}@media(max-width:600px){.gla-paid-ads-features-section__content{flex-direction:column;gap:16px}}.gla-paid-ads-features-section__subtitle{color:#2f2f2f;font-size:14px;line-height:20px}.gla-paid-ads-features-section__feature-list{color:#2f2f2f;display:flex;flex-direction:column;gap:16px;line-height:16px;margin:24px 0 16px}.gla-paid-ads-features-section__feature-list .gridicon{fill:#5ec862}.gla-paid-ads-features-section__cite{color:#949494;font-size:12px;font-style:normal} PK!FFjs/build/ads-onboarding.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[663],{923:(e,t,n)=>{n.r(t),n.d(t,{default:()=>M});var a=n(1609),o=n(6474),l=n(6476),s=n(7723),i=n(7539),c=n(2455),u=n(6473);const g=()=>(0,a.createElement)(i.A,{title:(0,s.__)("Set up your campaign","google-listings-and-ads"),helpButton:(0,a.createElement)(c.A,{eventContext:"setup-ads"}),backHref:(0,l.getNewPath)({},"/google/dashboard"),onBackButtonClick:()=>{(0,u.ce)("gla_setup_ads",{triggered_by:"back-button",action:"leave"})}});var r=n(8846),d=n(6087),m=n(7892),p=n(9370),_=n(3164),A=n(3704),h=n(9826),E=n(8678),y=n(458),b=n(6141),C=n(1378),k=n(8e3),f=n(3741),S=n(8242),v=n(1351);const w=e=>{const{onContinue:t=()=>{}}=e,{google:n}=(0,k.A)(),{googleAdsAccount:o}=(0,C.A)(),l=(0,v.A)();if(!n||"yes"===n.active&&!o)return(0,a.createElement)(f.A,null);const i=!l;return(0,a.createElement)(p.A,null,(0,a.createElement)(_.A,{title:(0,s.__)("Set up your accounts","google-listings-and-ads"),description:(0,s.__)("Connect your Google account and your Google Ads account to set up a Performance Max campaign.","google-listings-and-ads")}),(0,a.createElement)(S.A,{title:(0,s.__)("Connect accounts","google-listings-and-ads"),description:(0,s.__)("Any campaigns created through this app will appear in your Google Ads account. You will be billed directly through Google.","google-listings-and-ads")},(0,a.createElement)(E.Az,{googleAccount:n,hideAccountSwitch:!0,helper:(0,s.__)("This Google account is connected to your store’s product feed.","google-listings-and-ads")}),(0,a.createElement)(y.Ay,null),(0,a.createElement)(b.A,null)),(0,a.createElement)(h.A,null,(0,a.createElement)(A.A,null,(0,a.createElement)(m.A,{isPrimary:!0,disabled:i,onClick:t},(0,s.__)("Continue","google-listings-and-ads")))))};var P=n(7541),B=n(5992),G=n(8468),T=n(1203),x=n(6893),F=n(1968),R=n(1650),D=n(5847),N=n(8519),V=n(8473),Y=n(4679),j=n(3905);const{APPROVED:q}=j.CX,z=()=>{const{billingStatus:e}=(0,x.A)(),[t,n]=(0,d.useState)(!1),[o,i]=(0,d.useState)(!1),[c,g]=(0,N.A)(),r=(0,F.A)(),{data:p}=(0,D.A)(),{highestDailyBudget:_,hasFinishedResolution:A}=(0,Y.A)(p),h={amount:_};(0,d.useEffect)((()=>{if(o){const e=(0,l.getNewPath)({guide:"campaign-creation-success"},"/google/dashboard");window.location.href=r+e}}),[o,r]);const E=t&&!o;return(0,R.A)((0,s.__)("You have unsaved campaign data. Are you sure you want to leave?","google-listings-and-ads"),E),p&&A?(0,a.createElement)(V.A,{initialCampaign:h,onChange:(e,t)=>{n(!(0,G.isEqual)(h,t))},onSubmit:e=>{const{amount:t}=e;(0,u.ce)("gla_launch_paid_campaign_button_click",{audiences:p.join(","),budget:t}),c(t,p,(()=>{i(!0)}))},recommendedDailyBudget:_},(0,a.createElement)(T.A,{headerTitle:(0,s.__)("Create your campaign","google-listings-and-ads"),context:"setup-ads",continueButton:t=>(0,a.createElement)(m.A,{isPrimary:!0,text:(0,s.__)("Create campaign","google-listings-and-ads"),disabled:!t.isValidForm||e?.status!==q,loading:g,onClick:t.handleSubmit})})):(0,a.createElement)(f.A,null)},H=()=>{const[e,t]=(0,d.useState)("1"),n=(0,d.useRef)(null),{hasFinishedResolution:o,hasGoogleAdsConnection:l}=(0,C.A)(),{hasAccess:i,hasFinishedResolution:c,step:g}=(0,B.A)();if((0,P.A)(u.T1,{context:u.lr,step:e}),null===n.current){if(!o||!c)return(0,a.createElement)(f.A,null);const e=l&&!0===i&&"conversion_action"!==g;n.current=e}const m=n=>{n{(()=>{const n=e;(0,u.dQ)("gla_setup_ads",n,"2"),t("2")})()}}),onClick:m},{key:"2",label:(0,s.__)("Create your campaign","google-listings-and-ads"),content:(0,a.createElement)(z,null),onClick:m}];return n.current&&(p.shift(),p=p.map(((e,t)=>({...e,key:(t+1).toString()})))),(0,a.createElement)(r.Stepper,{className:"gla-setup-stepper",currentStep:e,steps:p})},M=()=>((0,o.A)("full-page"),(0,a.createElement)(a.Fragment,null,(0,a.createElement)(g,null),(0,a.createElement)(H,null)))}}]);PK!av$v$js/build/attribute-mapping.cssnu[.gla-section-card-body{padding:var(--large-gap)} .gla-section-card-footer{padding:calc(var(--large-gap)/2) var(--large-gap)}.gla-section-card-footer[hidden]{display:none} .gla-subsection-title{font-size:14px;font-style:normal;font-weight:600;letter-spacing:0;line-height:20px;margin-bottom:8px;position:relative;text-align:left} .gla-subsection-body{font-size:13px;font-style:normal;font-weight:400;line-height:16px} .gla-subsection-helper-text{font-size:12px;font-style:italic;font-weight:400;letter-spacing:0;line-height:16px} .gla-subsection-subtitle{color:#757575;font-size:12px;line-height:16px;margin-bottom:4px} .gla-subsection:not(:first-child){margin-top:var(--main-gap)} .gla-section-card-title{margin-bottom:calc(var(--main-gap)/3*2)} .gla-section{display:flex;flex-direction:column;margin-bottom:var(--large-gap)}.gla-section--is-disabled,.gla-section--is-disabled-left .gla-section__header{opacity:.5}@media(min-width:600px){.gla-section{flex-direction:row;gap:var(--main-gap)}.gla-section__header{padding-top:var(--main-gap);width:33%}}.gla-section__header h1{font-size:16px;font-style:normal;font-weight:600;margin-bottom:8px;padding:0}.gla-section__header p{line-height:16px;margin:0 0 8px}.gla-section .gla-section__body{flex:1} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .app-table-card-div .components-card__header h2{margin-right:8px}.app-table-card-div .components-card__header .woocommerce-table__actions{justify-content:flex-start}.app-table-card-div .components-card__body .woocommerce-table__table .components-base-control__field{margin-bottom:0} .gla-tooltip__children-container{cursor:help;display:inline-block}.gla-tooltip__children-container>span{cursor:auto}.gla-admin-page .disabled-element-wrapper:has(.gla-tooltip__children-container){display:inline-block} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .app-select-control .components-base-control__field{margin-bottom:0}.app-select-control.app-select-control--is-non-interactive{pointer-events:none} .app-radio-content-control{display:grid;gap:8px;column-gap:10px;grid-template-columns:[input-start] auto [text-start] 1fr}.app-radio-content-control .components-base-control__field,.app-radio-content-control .components-base-control__field .components-flex,.app-radio-content-control .components-base-control__label,.app-radio-content-control .components-radio-control,.app-radio-content-control .components-radio-control .components-flex,.app-radio-content-control .components-radio-control__option{display:contents}.app-radio-content-control .components-radio-control__input[type=radio]{margin:0}.app-radio-content-control .app-radio-content-control__content{grid-column:text-start}.app-radio-content-control .app-radio-content-control__content:empty{display:none} .app-input-control .components-flex-item{margin-right:0;max-width:100%;width:100%}.app-input-control .components-flex-item label.components-input-control__label{color:#757575!important;white-space:normal!important}.app-input-control--no-pointer-events{pointer-events:none}.app-input-control__character-count{color:#757575;font-size:12px;line-height:16px;margin-top:2px;text-align:right}.app-input-control--error-character-count .components-input-control .components-input-control__container .components-input-control__backdrop,.app-input-control.has-error .components-input-control__backdrop{border-color:#cc1818;box-shadow:none}.app-input-control--error-character-count .app-input-control__character-count,.app-input-control.has-error .components-base-control__help{color:#cc1818} .help-popover{display:inline-flex;font-size:13px;font-weight:400;line-height:1.4}.help-popover button{align-items:center;background:none;border:none;display:inline-flex;padding:1px;fill:#949494}.help-popover button:not(:disabled){cursor:pointer}.help-popover button:hover:not(:disabled){fill:#616161}.help-popover .components-popover__content{padding:16px;width:300px}@media(min-width:960px){.help-popover .components-popover__content{width:360px}}@media(min-width:1080px){.help-popover .components-popover__content{width:480px}} .gla-searchable-select-control__helper-text:not(:last-child),.gla-searchable-select-control__input:not(:last-child){margin-bottom:calc(var(--main-gap)/3)}.gla-searchable-select-control__label{color:#757575;padding-bottom:4px}.gla-searchable-select-control__helper-text{font-size:12px;font-style:italic;line-height:16px}.gla-searchable-select-control i.material-icons-outlined{display:none}.gla-searchable-select-control .woocommerce-select-control .components-base-control{height:unset}.gla-searchable-select-control .woocommerce-select-control .components-base-control .woocommerce-select-control__control-input,.gla-searchable-select-control .woocommerce-select-control .components-base-control .woocommerce-select-control__tags{margin:0}.gla-searchable-select-control .woocommerce-select-control .components-base-control .woocommerce-tag{max-height:24px}.gla-searchable-select-control .woocommerce-select-control .woocommerce-select-control__listbox{top:unset}.gla-searchable-select-control .woocommerce-select-control .is-disabled .woocommerce-select-control__tags .woocommerce-tag__remove,.gla-searchable-select-control .woocommerce-select-control .is-disabled+.woocommerce-select-control__tags .woocommerce-tag__remove{display:none}.gla-searchable-select-control .woocommerce-select-control .is-disabled .woocommerce-select-control__tags .woocommerce-tag__text,.gla-searchable-select-control .woocommerce-select-control .is-disabled+.woocommerce-select-control__tags .woocommerce-tag__text{border-radius:12px;padding:0 8px} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .app-tab-nav__tabs{align-items:stretch;box-shadow:inset 0 -1px 0 #ccc;display:flex;flex-wrap:wrap;margin-bottom:var(--main-gap)}.app-tab-nav__tabs-item{background:#0000;border:none;border-radius:0;box-shadow:inset 0 -1px 0 #ccc;box-sizing:border-box;cursor:pointer;font-weight:500;height:48px;margin-left:0;padding:3px 16px}.app-tab-nav__tabs-item:after{content:attr(data-label);display:block;height:0;overflow:hidden;speak:none;visibility:hidden}.app-tab-nav__tabs-item:focus:not(:disabled){box-shadow:inset 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) #0000,inset 0 -1.5px 0 0 var(--wp-admin-theme-color);position:relative}.app-tab-nav__tabs-item.is-active:before{border-bottom:1.5px solid #0000;bottom:1px;content:"";left:0;position:absolute;right:0;top:0}.app-tab-nav__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),inset 0 -1.5px 0 0 var(--wp-admin-theme-color)} .gla-gtin-migration__link{cursor:pointer;text-decoration:underline} .gla-rebranding-tour__heading{align-items:center;display:flex;gap:8px;fill:var(--wp-admin-theme-color)}.gla-rebranding-tour .components-card__footer{padding-top:0}.gla-rebranding-tour .components-card__footer>*{display:none} .gla-attribute-mapping__pagination{width:100%}.gla-attribute-mapping__sync-status{color:#757575;font-size:13px}.gla-attribute-mapping__table-footer{padding:16px}.gla-attribute-mapping__table-footer .components-button{min-width:auto}.gla-attribute-mapping__radio-control{margin-top:16px}.gla-attribute-mapping__radio-control .help-popover{margin-left:8px}.gla-attribute-mapping__radio-control .help-popover .components-popover{position:absolute}.gla-attribute-mapping__radio-control .help-popover .components-popover__content{max-width:350px}.gla-attribute-mapping__rule-modal .app-select-control{margin-bottom:8px}.gla-attribute-mapping__rule-modal .app-select-control select.components-select-control__input{height:40px;max-width:100%}.gla-attribute-mapping__rule-modal .app-input-control{margin-bottom:8px}.gla-attribute-mapping__rule-modal .app-input-control input.components-input-control__input{height:40px}.gla-attribute-mapping__table-label{background:#f0f0f0;border-radius:2px;font-size:12px;line-height:16px;padding:4px 8px}.gla-attribute-mapping__table-categories{color:#757575}.gla-attribute-mapping__table-categories-tooltip{display:block;font-weight:600;white-space:break-spaces;width:150px}.gla-attribute-mapping__table-categories-tooltip span{font-weight:400;margin-left:4px}.gla-attribute-mapping__table-categories-help{border-bottom:1px dashed;margin-left:4px;word-wrap:break-word}.gla-attribute-mapping__syncing{color:#757575;font-size:11px} PK!ʀ44js/build/attribute-mapping.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[456],{8885:(e,t,a)=>{a.r(t),a.d(t,{default:()=>te});var l=a(1609),n=a(7723),i=a(8242);const o=()=>(0,l.createElement)("p",null,(0,n.__)("Create attribute rules to control what product data gets sent to Google and to manage product attributes in bulk.","google-listings-and-ads"));var s=a(8846),r=a(6427),g=a(6087),c=a(7892),d=a(2159),u=a(6494),p=a(9039),m=a(3905),_=a(1340);const b=({categories:e})=>(0,l.createElement)("div",{className:"gla-attribute-mapping__table-categories-help"},(0,n.sprintf)( // translators: %d: number of categories. // translators: %d: number of categories. (0,n._n)("%d category","%d categories",e.length,"google-listings-and-ads"),e.length)),E=({categories:e,condition:t})=>{const a=e?.split(",")||[],{names:i}=(0,_.A)(a);if(t===m.aL.ALL)return(0,n.__)("All","google-listings-and-ads");const o=a.length>m.vL?(0,n.sprintf)( // translators: %d: The number of categories. // translators: %d: The number of categories. (0,n.__)("+ %d more","google-listings-and-ads"),a.length-m.vL):"";return(0,l.createElement)(l.Fragment,null,(0,l.createElement)("span",null,t===m.aL.ONLY?(0,n.__)("Only in","google-listings-and-ads"):(0,n.__)("All except","google-listings-and-ads")),(0,l.createElement)(p.A,{text:(0,l.createElement)("div",{className:"gla-attribute-mapping__table-categories-tooltip"},i,o&&(0,l.createElement)("span",null,o))},(0,l.createElement)(b,{categories:a})))};var h=a(8468),v=a(8771),y=a(9457),A=a(7568),C=a(7437),f=a(771);const L=({sources:e=[],help:t,...a})=>(0,l.createElement)(l.Fragment,null,(0,l.createElement)(A.A,{options:[{value:"",label:(0,n.__)("Select an option","google-listings-and-ads")},...e.map((e=>({...e,disabled:e.value.includes("disabled:")})))],...a}),t);var x=a(8683),k=a(5092),N=a(1177),w=a(6257);const P="fixed",T="field",S=(0,n.__)("Set a fixed value","google-listings-and-ads"),F=(0,n.__)("Use value from existing product field","google-listings-and-ads"),R=({sources:e=[],onChange:t=h.noop,value:a})=>{const[i,o]=(0,g.useState)(!a||a?.includes(":")?T:P),s=e=>{t(""),o(e)};return(0,l.createElement)(l.Fragment,null,(0,l.createElement)(v.A.Subtitle,{className:"gla_attribute_mapping_helper-text"},(0,n.__)("Choose how to assign a value to the target attribute","google-listings-and-ads")),(0,l.createElement)(x.A,{className:"gla-attribute-mapping__radio-control",label:(0,l.createElement)(l.Fragment,null,F,(0,l.createElement)(w.A,{id:`${T}-helper-popover`},(0,l.createElement)("div",null,(0,n.__)("Auto-populate the target attribute with the value of the field you link it to.","google-listings-and-ads")))),onChange:s,value:T,selected:i,collapsible:!0},(0,l.createElement)(L,{value:a,onChange:t,"aria-label":F,sources:e,help:(0,l.createElement)(v.A.HelperText,{className:"gla-attribute-mapping__help-text"},(0,g.createInterpolateElement)((0,n.__)("Can’t find an appropriate field? Create a new attribute","google-listings-and-ads"),{link:(0,l.createElement)(N.A,{context:"attribute-mapping",linkId:"create-new-attribute",href:"/wp-admin/edit.php?post_type=product&page=product_attributes"})}))})),(0,l.createElement)(x.A,{className:"gla-attribute-mapping__radio-control",label:(0,l.createElement)(l.Fragment,null,S,(0,l.createElement)(w.A,{id:`${P}-helper-popover`},(0,l.createElement)("div",null,(0,g.createInterpolateElement)((0,n.__)("Use fixed values to populate the target attribute with a value you specify. For example, you can enter a fixed value of 'White' to specify a single color for all your products.","google-listings-and-ads"),{em:(0,l.createElement)("em",null)})))),onChange:s,value:P,selected:i,collapsible:!0},(0,l.createElement)(k.A,{value:a,maxLength:100,onChange:t,"aria-label":S,placeholder:(0,n.__)("Enter a value","google-listings-and-ads")})))};var q=a(4301);const M=({selectedConditionalType:e=m.aL.ALL,selectedCategories:t,onConditionalTypeChange:a=h.noop,onCategoriesChange:i=h.noop})=>{const{categories:o,selected:s}=(0,_.A)(t);return(0,l.createElement)(l.Fragment,null,(0,l.createElement)(A.A,{options:[{value:m.aL.ALL,label:(0,n.__)("Apply to all categories","google-listings-and-ads")},{value:m.aL.EXCEPT,label:(0,n.__)("Apply to all categories EXCEPT","google-listings-and-ads")},{value:m.aL.ONLY,label:(0,n.__)("Apply ONLY to these categories","google-listings-and-ads")}],value:e,onChange:a}),(e===m.aL.ONLY||e===m.aL.EXCEPT)&&(0,l.createElement)(q.A,{options:o,isSearchable:!0,placeholder:(0,n.__)("Type for search","google-listings-and-ads"),selected:s,onChange:e=>i(e.map((e=>e.value))),multiple:!0,inlineTags:!0}))};var D=a(3741),j=a(3658),G=a(6473);const I=(0,n.__)("Select default value","google-listings-and-ads"),O=(0,n.__)("Select a Google attribute that you want to manage","google-listings-and-ads"),Y=(e=[])=>[...e.map((e=>({value:e.id,label:e.label})))],X=({rule:e,onRequestClose:t=h.noop})=>{const[a,i]=(0,g.useState)(e?{...e}:{category_condition_type:m.aL.ALL}),[o,s]=(0,g.useState)(!1),{updateMappingRule:r,createMappingRule:d}=(0,j.j)(),{data:u}=(0,C.A)(),{data:p=[],hasFinishedResolution:_}=(0,f.A)(a.attribute),b=u.find((({id:e})=>e===a.attribute))?.enum||!1,E=Y(p),x=[{value:"",label:(0,n.__)("Select an attribute","google-listings-and-ads")},...Y(u)],k=a.source&&a.attribute&&(a.category_condition_type===m.aL.ALL||a.categories?.length>0)&&!(0,h.isEqual)(a,e),N=e=>{i((e=>({...e,categories:e.category_condition_type===m.aL.ALL?"":e.categories}))(e))},w=e=>{N({...a,source:e})},P=()=>{o||t("dismiss")};return(0,l.createElement)(y.A,{overflow:"visible",onRequestClose:P,className:"gla-attribute-mapping__rule-modal",title:e?(0,n.__)("Manage attribute rule","google-listings-and-ads"):(0,n.__)("Create attribute rule","google-listings-and-ads"),buttons:[(0,l.createElement)(c.A,{disabled:o,key:"cancel",isLink:!0,onClick:P},(0,n.__)("Cancel","google-listings-and-ads")),(0,l.createElement)(c.A,{disabled:!k||o,key:"save-rule",isPrimary:!0,text:o?(0,n.__)("Saving…","google-listings-and-ads"):(0,n.__)("Save rule","google-listings-and-ads"),eventName:"gla_attribute_mapping_save_rule_click",eventProps:{context:e?"attribute-mapping-manage-rule-modal":"attribute-mapping-create-rule-modal"},onClick:async()=>{s(!0);try{e?(await r(a),(0,G.ce)("gla_attribute_mapping_update_rule",{context:"attribute-mapping-manage-rule-modal"})):(await d(a),(0,G.ce)("gla_attribute_mapping_create_rule",{context:"attribute-mapping-create-rule-modal"})),t("confirm")}catch(e){s(!1)}}})]},(0,l.createElement)(v.A,null,(0,l.createElement)(v.A.Title,null,(0,n.__)("Target attribute","google-listings-and-ads")),(0,l.createElement)(v.A.Subtitle,{className:"gla_attribute_mapping_helper-text"},O),(0,l.createElement)(A.A,{value:a.attribute,"aria-label":O,onChange:e=>{N({...a,attribute:e,source:""})},options:x})),!_&&(0,l.createElement)(D.A,null),E.length>0&&_&&(0,l.createElement)(l.Fragment,null,(0,l.createElement)(v.A,null,(0,l.createElement)(v.A.Title,null,b?I:(0,n.__)("Assign value","google-listings-and-ads")),b?(0,l.createElement)(L,{sources:E,onChange:w,value:a.source,"aria-label":I}):(0,l.createElement)(R,{sources:E,onChange:w,value:a.source})),(0,l.createElement)(v.A,null,(0,l.createElement)(v.A.Title,null,(0,n.__)("Categories","google-listings-and-ads")),(0,l.createElement)(M,{selectedConditionalType:a.category_condition_type,selectedCategories:a.categories?.length?a.categories.split(","):[],onConditionalTypeChange:e=>{N({...a,category_condition_type:e})},onCategoriesChange:e=>{N({...a,categories:e.join(",")})}}))))},$=({onRequestClose:e=h.noop,rule:t})=>{const[a,i]=(0,g.useState)(!1),{deleteMappingRule:o}=(0,j.j)(),s=()=>{a||e("dismiss")};return(0,l.createElement)(y.A,{onRequestClose:s,title:(0,n.__)("Delete attribute rule?","google-listings-and-ads"),buttons:[(0,l.createElement)(c.A,{disabled:a,key:"cancel",isLink:!0,onClick:s},(0,n.__)("Cancel","google-listings-and-ads")),(0,l.createElement)(c.A,{disabled:a,key:"delete-rule",isPrimary:!0,text:a?(0,n.__)("Deleting…","google-listings-and-ads"):(0,n.__)("Delete attribute rule","google-listings-and-ads"),eventName:"gla_attribute_mapping_delete_rule_click",eventProps:{context:"attribute-mapping-delete-rule-modal"},onClick:async()=>{i(!0);try{await o(t),(0,G.ce)("gla_attribute_mapping_delete_rule",{context:"attribute-mapping-delete-rule-modal"}),e("confirm")}catch(e){i(!1)}}})]},(0,l.createElement)("div",null,(0,l.createElement)("p",null,(0,n.__)("Deleting a rule does’t affect any data that has already been submitted to Google.","google-listings-and-ads")),(0,l.createElement)("p",null,(0,n.__)("Product data is re-submitted to Google every 30 days to ensure that the information in your product listings are up-to-date.","google-listings-and-ads")),(0,l.createElement)("p",null,(0,n.__)("To ensure your products continue to be approved and promoted by Google, make sure that your product fields include all the required information.","google-listings-and-ads"))))};var U=a(8443),V=a(33),W=a(6520),z=a(3376);const B=()=>{const{data:e,start:t}=(0,V.A)({path:`${W.RV}/mc/mapping/sync`});if((0,g.useEffect)((()=>{t()}),[t]),!e)return null;const a=(0,n.__)("Scheduled for sync","google-listings-and-ads"),i=e.last_sync?(0,U.format)(z.Z,new Date(1e3*e.last_sync)):(0,n.__)("Never","google-listings-and-ads");return(0,l.createElement)(r.Flex,{justify:"end",align:"center"},(0,l.createElement)("strong",null,(0,n.__)("Last sync:","google-listings-and-ads")),(0,l.createElement)("span",{className:"gla-attribute-mapping__sync-status"},e.is_scheduled?a:i),(0,l.createElement)(w.A,{id:"gla-attribute-mapping-last-sync-helper-popover",position:"bottom left"},(0,l.createElement)("div",null,(0,n.__)("When an attribute rule is added or changed, data will be synced to Google Merchant Center via an async job. Note that it may take a while for the update to show up on Merchant Center, especially if it involves products that have not been synced and approved before.","google-listings-and-ads"))))};var H=a(993),Z=a(1396);const J=[{key:"attribute",label:(0,n.__)("Target Attribute","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"source",label:(0,n.__)("Data Source / Default Value","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"categories",label:(0,n.__)("Categories","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"controls",label:"",required:!0}],K=()=>{const{page:e,setPage:t}=(0,Z.A)("attribute-mapping"),{data:{rules:a,total:o},hasFinishedResolution:p}=(0,H.A)({page:e,perPage:10}),{data:m,hasFinishedResolution:_}=(0,C.A)(),b=!_||!p;return(0,g.useEffect)((()=>{p&&0===a?.length&&e>1&&t(e-1)}),[e,a,p,t]),(0,l.createElement)(d.A,null,(0,l.createElement)(i.A.Card,null,(0,l.createElement)(r.CardBody,{size:null},b?(0,l.createElement)(s.TablePlaceholder,{headers:J,caption:(0,n.__)("Loading Attribute Mapping rules","google-listings-and-ads")}):(0,l.createElement)(s.Table,{emptyMessage:(0,n.__)("You have no attribute rules","google-listings-and-ads"),caption:(0,n.__)("Attribute Mapping configuration","google-listings-and-ads"),headers:J,rows:a.map((e=>{return[{display:(t=e.attribute,m.find((e=>e.id===t))?.label||"")},{display:(0,l.createElement)("span",{className:"gla-attribute-mapping__table-label"},e.source)},{display:(0,l.createElement)("span",{className:"gla-attribute-mapping__table-categories"},(0,l.createElement)(E,{categories:e.categories,condition:e.category_condition_type}))},{display:(0,l.createElement)(r.Flex,{justify:"end"},(0,l.createElement)(r.FlexItem,null,(0,l.createElement)(u.A,{button:(0,l.createElement)(c.A,{isLink:!0,text:(0,n.__)("Edit","google-listings-and-ads"),eventName:"gla_modal_open",eventProps:{context:"attribute-mapping-manage-rule-modal"}}),modal:(0,l.createElement)(X,{rule:e,onRequestClose:e=>{(0,G.ce)("gla_modal_closed",{context:"attribute-mapping-manage-rule-modal",action:e})}})})),(0,l.createElement)(r.FlexItem,null,(0,l.createElement)(u.A,{button:(0,l.createElement)(c.A,{isLink:!0,text:(0,n.__)("Delete","google-listings-and-ads"),eventName:"gla_modal_open",eventProps:{context:"attribute-mapping-delete-rule-modal"}}),modal:(0,l.createElement)($,{rule:e,onRequestClose:e=>{(0,G.ce)("gla_modal_closed",{context:"attribute-mapping-delete-rule-modal",action:e})}})})))}];var t}))})),(0,l.createElement)(r.CardFooter,{align:"between",className:"gla-attribute-mapping__table-footer"},(0,l.createElement)(u.A,{button:(0,l.createElement)(c.A,{isSecondary:!0,text:(0,n.__)("Create attribute rule","google-listings-and-ads"),eventName:"gla_modal_open",eventProps:{context:"attribute-mapping-create-rule-modal"}}),modal:(0,l.createElement)(X,{onRequestClose:e=>{(0,G.ce)("gla_modal_closed",{context:"attribute-mapping-create-rule-modal",action:e})}})}),(0,l.createElement)(s.Pagination,{className:"gla-attribute-mapping__pagination",page:e,perPage:10,total:o,showPagePicker:!1,showPerPagePicker:!1,onPageChange:(e,a)=>{t(e),(0,G.Xh)("attribute-mapping-rules",e,a)}}),(0,l.createElement)(B,null))))};var Q=a(9927),ee=a(5246);const te=()=>(0,l.createElement)("div",{className:"gla-attribute-mapping"},(0,l.createElement)(Q.A,null),(0,l.createElement)(ee.A,null),(0,l.createElement)(i.A,{title:(0,n.__)("Manage attributes","google-listings-and-ads"),description:(0,l.createElement)(o,null)},(0,l.createElement)(K,null)))}}]);PK!&<js/build/blocks.asset.phpnu[ array('react', 'wc-block-templates', 'wc-product-editor', 'wc-settings', 'wp-components', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => 'a375c9c5a10aa76a05a0'); PK! :99js/build/blocks.cssnu[.Dz8NM01hSQyVP7lytEaS{margin-top:64px;text-align:center}.iKH0I_4hIpw3xXPSFYR4{margin-bottom:24px} .xrM9YQcns2lLIJoZocoC{margin:1em 0 0;padding-bottom:0;padding-top:0}.xrM9YQcns2lLIJoZocoC h2{font-size:1em}.FNzfRIMkTNDr5hdJSQJS:first-letter{text-transform:uppercase} .p9THnpoXkzl3X4b4PYpg label{visibility:hidden} PK!]3js/build/blocks.jsnu[(()=>{"use strict";const e=window.wc.productEditor,t=window.React,l=window.wp.i18n,o=window.wc.blockTemplates,s=window.wp.components,n=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"google-listings-and-ads/product-onboarding-prompt","title":"Product onboarding prompt","textdomain":"google-listings-and-ads","attributes":{"startUrl":{"type":"string","__experimentalRole":"content"}},"supports":{"html":false,"inserter":false,"lock":false,"__experimentalToolbar":false}}'),a=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"google-listings-and-ads/product-channel-visibility","title":"Product channel visibility","textdomain":"google-listings-and-ads","attributes":{"property":{"type":"string","__experimentalRole":"content"},"options":{"type":"array","items":{"type":"object"},"default":[]},"valueOfSync":{"type":"string"},"valueOfDontSync":{"type":"string"},"statusOfSynced":{"type":"string"},"statusOfHasErrors":{"type":"string"}},"supports":{"html":false,"inserter":false,"lock":false,"__experimentalToolbar":false}}'),i=window.wp.date,r=window.wp.element,p=window.wc.wcSettings;async function c(e,t){const l=e.current;if(!l.validity.valid)return(0,p.isWcVersion)("9.2.0","<")?l.validationMessage:{message:l.validationMessage,context:t}}const d=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"google-listings-and-ads/product-date-time-field","title":"Product date and time fields","textdomain":"google-listings-and-ads","attributes":{"label":{"type":"string"},"tooltip":{"type":"string"},"property":{"type":"string","__experimentalRole":"content"}},"supports":{"html":false,"inserter":false,"lock":false,"__experimentalToolbar":false}}'),u=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"google-listings-and-ads/product-select-field","title":"Product select field","textdomain":"google-listings-and-ads","attributes":{"label":{"type":"string"},"tooltip":{"type":"string"},"property":{"type":"string","__experimentalRole":"content"},"options":{"type":"array","items":{"type":"object"},"default":[]}},"supports":{"html":false,"inserter":false,"lock":false,"__experimentalToolbar":false}}'),m=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","apiVersion":2,"name":"google-listings-and-ads/product-select-with-text-field","title":"Product select with text field","textdomain":"google-listings-and-ads","attributes":{"label":{"type":"string"},"tooltip":{"type":"string"},"property":{"type":"string","__experimentalRole":"content"},"options":{"type":"array","items":{"type":"object"},"default":[]},"customInputValue":{"type":"string"}},"supports":{"html":false,"inserter":false,"lock":false,"__experimentalToolbar":false}}');function g({name:t,...l},o){(0,e.registerProductEditorBlockType)({name:t,metadata:l,settings:{edit:o}})}g(n,(function({attributes:e}){const n=(0,o.useWooBlockProps)(e);return(0,t.createElement)("div",{...n},(0,t.createElement)("div",{className:"Dz8NM01hSQyVP7lytEaS"},(0,t.createElement)("p",{className:"iKH0I_4hIpw3xXPSFYR4"},(0,l.__)("Complete setup to get your products listed on Google for free.","google-listings-and-ads")),(0,t.createElement)(s.Button,{isPrimary:!0,href:e.startUrl},(0,l.__)("Get Started","google-listings-and-ads"))))})),g(a,(function({attributes:n,context:a}){const{valueOfSync:i,valueOfDontSync:r,statusOfSynced:p,statusOfHasErrors:c}=n,d=(0,o.useWooBlockProps)(n),[u,m]=(0,e.__experimentalUseProductEntityProp)(n.property,{postType:a.postType}),{is_visible:g,sync_status:y,issues:f}=u,b=g?u.channel_visibility:r;let _=null;y===c?_=(0,l.__)("Issues detected","google-listings-and-ads"):y&&(_=y.replace("-"," "));const h=g?"":(0,l.__)("This product cannot be shown on any channel because it is hidden from your store catalog. To enable this option, please change this product to be shown in the product catalog, and save the changes.","google-listings-and-ads"),x=g&&null!==_&&b===i&&y!==p,E=f.length>0;return(0,t.createElement)("div",{...d},(0,t.createElement)(s.SelectControl,{disabled:!g,options:n.options,value:b,onChange:e=>{m({...u,channel_visibility:e})},help:h}),x&&(0,t.createElement)(s.Notice,{className:"xrM9YQcns2lLIJoZocoC",status:E?"warning":"info",isDismissible:!1},(0,t.createElement)("section",null,(0,t.createElement)("h2",null,(0,l.__)("Google sync status","google-listings-and-ads")),(0,t.createElement)("p",{className:"FNzfRIMkTNDr5hdJSQJS"},_)),E&&(0,t.createElement)("section",null,(0,t.createElement)("h2",null,(0,l.__)("Issues","google-listings-and-ads")),(0,t.createElement)("ul",null,f.map(((e,l)=>(0,t.createElement)("li",{key:l},e)))))))})),g(d,(function({attributes:l,context:n,clientId:a}){const{property:p}=l,d=(0,o.useWooBlockProps)(l),[u,m]=(0,e.__experimentalUseProductEntityProp)(p,{postType:n.postType,fallbackValue:""}),g=(0,r.useRef)(null),y=(0,r.useRef)(null),[f,b]=(0,r.useState)((()=>u?(0,i.date)("Y-m-d",u):"")),[_,h]=(0,r.useState)((()=>u?(0,i.date)("H:i",u):"")),x=(e,t)=>{let l="";if(e){const o=`${e}T${t||"00:00:00"}`,s=(0,i.getDate)(o);l=(0,i.date)("c",s,"UTC")}b(e),h(t),u!==l&&m(l)},E=(0,e.useValidation)(`${p}-date`,(()=>c(g,a))),w=(0,e.useValidation)(`${p}-time`,(()=>c(y,a)));return(0,t.createElement)("div",{...d},(0,t.createElement)(s.Flex,{align:"flex-start"},(0,t.createElement)(s.FlexBlock,null,(0,t.createElement)(e.__experimentalTextControl,{ref:g,type:"date",pattern:"\\d{4}-\\d{2}-\\d{2}",label:l.label,tooltip:l.tooltip,value:f,error:E.error,onChange:e=>x(e,_),onBlur:E.validate})),(0,t.createElement)(s.FlexBlock,null,(0,t.createElement)(e.__experimentalTextControl,{className:"p9THnpoXkzl3X4b4PYpg",label:" ",tooltip:"‎ ",ref:y,type:"time",pattern:"[0-9]{2}:[0-9]{2}",value:_,error:w.error,onChange:e=>x(f,e),onBlur:w.validate}))))})),g(u,(function({attributes:l,context:n}){const a=(0,o.useWooBlockProps)(l),[i,r]=(0,e.__experimentalUseProductEntityProp)(l.property,{postType:n.postType});return(0,t.createElement)("div",{...a},(0,t.createElement)(s.SelectControl,{label:(0,t.createElement)(e.__experimentalLabel,{label:l.label,tooltip:l.tooltip}),options:l.options,value:i,onChange:r}))})),g(m,(function({attributes:l,context:n}){const{options:a,customInputValue:i}=l,p=(0,o.useWooBlockProps)(l),[c,d]=(0,e.__experimentalUseProductEntityProp)(l.property,{postType:n.postType,fallbackValue:""}),[u,m]=(0,r.useState)((()=>{var e;const t=null!=c?c:"",l=a.find((e=>e.value===t));return null!==(e=l?.value)&&void 0!==e?e:i})),g=u===i,[y,f]=(0,r.useState)(g?c:"");return(0,t.createElement)("div",{...p},(0,t.createElement)(s.SelectControl,{label:(0,t.createElement)(e.__experimentalLabel,{label:l.label,tooltip:l.tooltip}),options:a,value:u,onChange:e=>{m(e),d(e===i?y:e)},disabled:l.disabled}),g&&(0,t.createElement)(e.__experimentalTextControl,{type:"text",value:y,onChange:e=>{f(e),d(e)},disabled:l.disabled}))}))})();PK! :99js/build/blocks-rtl.cssnu[.Dz8NM01hSQyVP7lytEaS{margin-top:64px;text-align:center}.iKH0I_4hIpw3xXPSFYR4{margin-bottom:24px} .xrM9YQcns2lLIJoZocoC{margin:1em 0 0;padding-bottom:0;padding-top:0}.xrM9YQcns2lLIJoZocoC h2{font-size:1em}.FNzfRIMkTNDr5hdJSQJS:first-letter{text-transform:uppercase} .p9THnpoXkzl3X4b4PYpg label{visibility:hidden} PK!@Ȉjs/build/commons.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[223],{559:(e,t,n)=>{n.d(t,{x:()=>_,A:()=>w});var a=n(1609),o=n(7723),s=n(6942),l=n.n(s),r=n(7677),i=n(8690),c=n(8242),d=n(8771);const u=n.p+"images/js/src/images/logo/5585d65b9d8c575e5a1f.gogole-g-logo.svg",g=n.p+"images/js/src/images/logo/6daf36ba57db9c82b6e0.google-merchant-center-logo.svg",m=n.p+"images/js/src/images/logo/389bc604a859dff92f15.google-ads-logo.svg",p=n.p+"images/js/src/images/logo/25a37606f64ef10ff60e.wp-logo.svg",h=n.p+"images/js/src/images/829c5735b6338e133556.final-url-icon.svg",_={EMPTY:"empty",WPCOM:"wpcom",GOOGLE:"google",GOOGLE_MERCHANT_CENTER:"google_merchant_center",GOOGLE_ADS:"google_ads",ADDRESS:"address",FINAL_URL:"final_url"},E=(0,a.createElement)("img",{src:u,alt:(0,o.__)("Google Logo","google-listings-and-ads"),width:"40",height:"40"}),f=(0,a.createElement)("img",{src:g,alt:(0,o.__)("Google Merchant Center Logo","google-listings-and-ads"),width:"40",height:"40"}),A=(0,a.createElement)("img",{src:m,alt:(0,o.__)("Google Ads Logo","google-listings-and-ads"),width:"40",height:"40"}),y=(0,a.createElement)("img",{src:p,alt:(0,o.__)("WordPress.com Logo","google-listings-and-ads"),width:"40",height:"40"}),v=(0,a.createElement)("img",{src:h,alt:(0,o.__)("Final URL icon","google-listings-and-ads"),width:"50"}),b={[_.EMPTY]:{},[_.WPCOM]:{icon:y,title:"WordPress.com"},[_.GOOGLE]:{icon:E,title:(0,o.__)("Google","google-listings-and-ads")},[_.GOOGLE_MERCHANT_CENTER]:{icon:f,title:(0,o.__)("Google Merchant Center","google-listings-and-ads"),description:(0,o.__)("Required to sync products and list on Google.","google-listings-and-ads")},[_.GOOGLE_ADS]:{icon:A,title:(0,o.__)("Google Ads","google-listings-and-ads"),description:(0,o.__)("Required to set up conversion measurement and create campaigns.","google-listings-and-ads")},[_.ADDRESS]:{icon:(0,a.createElement)(r.A,{icon:i.A,size:32}),title:(0,o.__)("Store address","google-listings-and-ads")},[_.FINAL_URL]:{icon:v,title:(0,o.__)("Final URL","google-listings-and-ads")}},C={center:!1,top:"gla-account-card__styled--align-top"},k={...C,toDetail:"gla-account-card__indicator--align-to-detail"};function w({className:e,disabled:t=!1,appearance:n=_.EMPTY,icon:o=b[n].icon,title:s=b[n].title,description:r=b[n].description,helper:i,alignIcon:u="center",indicator:g,alignIndicator:m="center",detail:p,expandedDetail:h=!1,actions:E,children:f,...A}){const y=l()("gla-account-card",!!t&&"gla-account-card--is-disabled",!!h&&"gla-account-card--is-expanded-detail",e),v=l()("gla-account-card__icon",C[u]),w=l()("gla-account-card__indicator",k[m]);return(0,a.createElement)(c.A.Card,{className:y,...A},(0,a.createElement)(c.A.Card.Body,null,(0,a.createElement)("div",{className:"gla-account-card__body-layout"},o&&(0,a.createElement)("div",{className:v},o),(0,a.createElement)("div",{className:"gla-account-card__subject"},s&&(0,a.createElement)(d.A.Title,{className:"gla-account-card__title"},s),r&&(0,a.createElement)("div",{className:"gla-account-card__description"},r),i&&(0,a.createElement)("div",{className:"gla-account-card__helper"},i)),p&&(0,a.createElement)("div",{className:"gla-account-card__detail"},p),g&&(0,a.createElement)("div",{className:w},g),E&&(0,a.createElement)("div",{className:"gla-account-card__actions"},E))),f)}},6960:(e,t,n)=>{n.d(t,{Ay:()=>g,h5:()=>i,Gl:()=>c});var a=n(1609),o=n(6087),s=n(8846),l=n(8468);const r=(0,o.createContext)(null);function i(){const e=(0,o.useContext)(r);if(null===e)throw new Error("useAdaptiveFormContext was used outside of its context provider AdaptiveForm.");return e}function c(e,t=e){const{getInputProps:n,adapter:a}=i();return{...n(e),helper:a.renderRequestedValidation(t)}}const d="submitting",u="submitted",g=(0,o.forwardRef)((function({onSubmit:e,extendAdapter:t,children:n,...i},c){const g=(0,o.useRef)(),m=(0,o.useRef)({submitter:null}),[p,h]=(0,o.useState)([]),[_,E]=(0,o.useState)(),f=(0,o.useCallback)(((...e)=>{h((t=>[...t,e]))}),[]);(0,o.useEffect)((()=>{_&&m.current.setValueCompatibly(..._)}),[_]),(0,o.useImperativeHandle)(c,(()=>({setValue:f,...g.current})));const A=function(){const e=(0,o.useRef)(!1);return(0,o.useEffect)((()=>(e.current=!0,()=>{e.current=!1})),[]),(0,o.useCallback)((()=>e.current),[])}(),[y,v]=(0,o.useState)(0),b=(0,o.useCallback)((()=>{v((e=>e+1))}),[]),C=(0,o.useCallback)((()=>{v(0)}),[]),[k,w]=(0,o.useState)(null),N=k===d,S=k===u;return e&&(i.onSubmit=async function(t){w(d);let n=!1;const a={submitter:m.current.submitter,signalFailedSubmission(){n=!0}};await e.call(this,t,a),A()&&(m.current.submitter=null,w(n?null:u))}),(0,a.createElement)(s.Form,{...i,ref:g},(({setValue:e,setValues:o,getInputProps:s,handleSubmit:i,...c})=>{if(m.current.setValueCompatibly=(t,n)=>{o?o({[t]:n}):e(t,n)},c.setValue=f,c.getInputProps=e=>{const t=s(e);return{...t,onChange:function(n){(function(e){return(e?.nativeEvent||e)instanceof Event})(n)&&(n="checkbox"===n.target.type?!(0,l.get)(t.values,e):n.target.value),m.current.setValueCompatibly(e,n)}}},p.length&&setTimeout((()=>E(p.shift()))),c.handleSubmit=function(e){return m.current.submitter=e?.currentTarget||null,i.call(this,e)},c.adapter={isSubmitting:N,isSubmitted:S,submitter:m.current.submitter,validationRequestCount:y,requestedShowValidation:y>0,showValidation:b,hideValidation:C},"function"==typeof t){const e=t(c);Object.assign(c.adapter,e)}return(0,a.createElement)(r.Provider,{value:c},"function"==typeof n?n(c):n)}))}))},2661:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(7723),s=n(5703),l=n(7568),r=n(5530),i=n(1378);const c=e=>{const{existingAccounts:t}=(0,r.A)(),{googleAdsAccount:n,hasGoogleAdsConnection:c}=(0,i.A)(),d=t?.some((e=>e.id===n?.id));if(!d&&c){const e=new URL((0,s.getSetting)("homeUrl")).host;return(0,a.createElement)(l.A,{autoSelectFirstOption:!0,nonInteractive:!0,value:n.id,options:[{value:n.id,label:(0,o.sprintf)( // translators: 1: account domain, 2: account ID. // translators: 1: account domain, 2: account ID. (0,o.__)("%1$s (%2$s)","google-listings-and-ads"),e,n.id)}]})}const u=t?.map((e=>({value:e.id,label:`${e.name} (${e.id})`})));return(0,a.createElement)(l.A,{options:u,autoSelectFirstOption:!0,...e})}},6494:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(6087);const s=e=>{const{button:t,modal:n}=e,{onClick:s=()=>{}}=t.props,{onRequestClose:l=()=>{}}=n.props,[r,i]=(0,o.useState)(!1);return(0,a.createElement)(a.Fragment,null,(0,o.cloneElement)(t,{onClick:(...e)=>{i(!0),s(...e)}}),r&&(0,o.cloneElement)(n,{onRequestClose:(...e)=>{i(!1),l(...e)}}))}},7892:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(6427),s=n(8846),l=n(6942),r=n.n(l),i=n(6473);const c=e=>{const{className:t,disabled:n,loading:l,eventName:c,eventProps:d,text:u,onClick:g=()=>{},...m}=e,p=n||l,h=["app-button",t];let _;return l&&(_=(0,a.createElement)(s.Spinner,null)),u&&(_=(0,a.createElement)(a.Fragment,null,l&&(0,a.createElement)(s.Spinner,null),u),m.icon&&h.push("app-button--icon-with-text"),"right"===m.iconPosition&&h.push("app-button--icon-position-right")),(0,a.createElement)(o.Button,{className:r()(...h),disabled:p,"aria-disabled":p,text:_,onClick:(...e)=>{c&&(0,i.ce)(c,d),g(...e)},...m})}},1177:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(2848);const s=e=>{const{context:t,linkId:n,href:s,...l}=e;return(0,a.createElement)(o.A,{eventProps:{context:t,link_id:n,href:s},type:"external",target:"_blank",href:s,...l,eventName:"gla_documentation_link_click"})}},5092:(e,t,n)=>{n.d(t,{A:()=>u});var a=n(1609),o=n(6942),s=n.n(o),l=n(7723),r=n(6087),i=n(6427),c=n(399);const d="app-input-control",u=(0,r.forwardRef)((({className:e,noPointerEvents:t=!1,maxCharacterCount:n=0,kindCharacterCount:o,...r},u)=>{const g=[d,e];let m;if(t&&g.push(`${d}--no-pointer-events`),n>0&&o){const e=(0,c.A)(o)(r.value?.trim()||"");m=(0,l.sprintf)( // translators: 1: number of character count. 2: the maximum number of character count. // translators: 1: number of character count. 2: the maximum number of character count. (0,l.__)("%1$d/%2$d characters","google-listings-and-ads"),e,n),e>n&&g.push(`${d}--error-character-count`)}return(0,a.createElement)("div",{className:s()(g)},(0,a.createElement)(i.__experimentalInputControl,{ref:u,...r}),m&&(0,a.createElement)("div",{className:"app-input-control__character-count"},m))}))},6499:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(1609),o=n(6087),s=n(5092);const l=(e,t)=>{const{decimalSeparator:n}=t,a=new RegExp("[^0-9-"+n+"]",["g"]),o=parseFloat((""+e).replace(/\((.*)\)/,"-$1").replace(a,"").replace(n,"."));return isNaN(o)?0:o};var r=n(3772);const i=e=>({...(0,r.A)(),precision:0,...e});var c=n(5128);const d=e=>{const{value:t,numberSettings:n,onChange:r=()=>{},onBlur:d=()=>{},...u}=e,g=i(n),m=(e=>{const t=i(e);return(0,c.A)(t)})(n),[,p]=(0,o.useReducer)((e=>e+1),0),h=m(t),_=e=>{const t=l(e,g),n=m(t);return l(n,g)};return(0,a.createElement)(s.A,{value:h,onChange:e=>{const t=_(e);r(t),p()},onBlur:e=>{const t=_(e.target.value);d(e,t)},...u})}},5588:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(3772),s=n(6499);const l=e=>{const t=(0,o.A)();return(0,a.createElement)(s.A,{suffix:t.code,numberSettings:t,...e})}},9457:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(1609),o=n(6427),s=n(6942),l=n.n(s);const r={auto:!1,visible:"app-modal__styled--overflow-visible"},i=({className:e,overflow:t="auto",buttons:n=[],children:s,...i})=>{const c=l()("gla-admin-page","app-modal",r[t],e);return(0,a.createElement)(o.Modal,{className:c,...i},s,n.length>=1&&(0,a.createElement)("div",{className:"app-modal__footer"},n))}},8683:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(1609),o=n(6942),s=n.n(o),l=n(6427);const r=e=>{const{className:t,label:n,value:o,selected:r,collapsible:i=!1,children:c,...d}=e,u=r===o;return(0,a.createElement)("div",{className:s()("app-radio-content-control",t)},(0,a.createElement)(l.RadioControl,{...d,selected:r,checked:u,options:[{label:n,value:o}],help:""}),(!i||u)&&(0,a.createElement)("div",{className:"app-radio-content-control__content"},c))}},7568:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(6427),s=n(6087),l=n(6942),r=n.n(l),i=n(8468);const c=e=>{const{options:t=[],className:n,onChange:l=i.noop,value:c,autoSelectFirstOption:d=!1,nonInteractive:u=!1,...g}=e,m=(0,s.useRef)(!0===d);(0,s.useEffect)((()=>{m.current&&void 0===c&&t.length&&(m.current=!1,l(t[0].value))}),[c,t,l]);let p={options:t,value:c,onChange:l,...g};const h=d&&1===t?.length||u;return h&&(p={...p,readOnly:!0,suffix:" ",tabIndex:"-1"}),(0,a.createElement)("div",{className:r()("app-select-control",n,{"app-select-control--is-non-interactive":h})},(0,a.createElement)(o.SelectControl,{...p}))}},3741:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(8846);const s=()=>(0,a.createElement)("div",{className:"app-spinner"},(0,a.createElement)(o.Spinner,null))},2118:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(6427);const s=e=>(0,a.createElement)("div",{className:"app-standalone-toggle-control"},(0,a.createElement)(o.ToggleControl,{...e}))},1670:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(6087),s=n(6427),l=n(8846),r=n(6942),i=n.n(r);const c=e=>{const{selectedKey:t,tabs:n}=e;return(0,a.createElement)(s.NavigableMenu,{role:"tablist",orientation:"horizontal",className:"subsubsub gla-sub-nav"},n.map(((e,s)=>{const r=e.key===t;return(0,a.createElement)(o.Fragment,{key:e.key},(0,a.createElement)(l.Link,{className:i()({current:r}),tabIndex:r?null:-1,id:`${e.key}`,href:e.href,role:"tab","aria-selected":r,"aria-controls":`${e.key}-view`,"aria-current":!!r&&"page"},e.title+" "),s{n.d(t,{A:()=>c});var a=n(1609),o=n(6427),s=n(8846),l=n(6942),r=n.n(l);const i=({tabId:e,href:t,children:n,selected:o,...l})=>(0,a.createElement)(s.Link,{role:"tab",tabIndex:o?null:-1,"aria-selected":o,id:e,href:t,...l},n),c=e=>{const{selectedKey:t,tabs:n}=e;return(0,a.createElement)("div",{className:"app-tab-nav"},(0,a.createElement)(o.NavigableMenu,{role:"tablist",orientation:"horizontal",className:"app-tab-nav__tabs"},n.map((e=>(0,a.createElement)(i,{className:r()("components-button","app-tab-nav__tabs-item",{"is-active":e.key===t}),tabId:`${e.key}`,"aria-controls":`${e.key}-view`,selected:e.key===t,key:e.key,href:e.href},e.title)))))}},2159:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(6942),s=n.n(o);const l=e=>{const{className:t,...n}=e;return(0,a.createElement)("div",{className:s()("app-table-card-div",t),...n})}},8237:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(8846),s=n(2159),l=n(6473);const r=(e,t,n)=>{const a=t.includes(n)?"on":"off";(0,l.ce)("gla_table_header_toggle",{report:e,column:n,status:a})},i=(e,t,n)=>{(0,l.ce)("gla_table_sort",{report:e,column:t,direction:n})},c=e=>{const{trackEventReportId:t,...n}=e;function l(e,n){return function(...a){t&&e(t,...a),n&&n(...a)}}return(0,a.createElement)(s.A,null,(0,a.createElement)(o.TableCard,{...n,onColumnsChange:l(r,e.onColumnsChange),onSort:l(i,e.onSort)}))}},6459:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(6942),s=n.n(o);const l=({variant:e,className:t="",children:n,as:o="p",...l})=>{const r=o;return(0,a.createElement)(r,{...l,className:s()("gla-app-text",t,{[`gla-app-text--${e}`]:e})},n)}},9039:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(6427),s=n(6087),l=n(5703);const r={"top-start":"top right",top:"top center"};function i(e){if((0,l.isWpVersion)("6.4","<")){const{placement:t,...n}=e,a=r[t];if(a)return{...n,position:a}}return e}const c=e=>{const{children:t,...n}=e;let l;const r=s.Children.toArray(t);return 1===r.length&&(l=r[0].props?.disabled),(0,a.createElement)(o.Tooltip,{...i(n)},(0,a.createElement)("div",{className:"gla-tooltip__children-container",disabled:l},t))}},4566:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(6942),s=n.n(o),l=n(7723),r=n(6427),i=n(4848);const c=e=>{const{className:t}=e;return(0,a.createElement)(r.Flex,{className:s()("gla-connected-icon-label",t),align:"center",gap:1},(0,a.createElement)(r.FlexItem,null,(0,a.createElement)(i.A,null)),(0,a.createElement)(r.FlexItem,null,(0,l.__)("Connected","google-listings-and-ads")))}},3354:(e,t,n)=>{n.d(t,{h:()=>x,S:()=>A});var a=n(1609),o=n(7723),s=n(6087),l=n(8846),r=n(4818),i=n(6476),c=n(5595),d=n(559),u=n(7892),g=n(6319),m=n(7677),p=n(5603);function h({editHref:e,editEventName:t,loading:n,content:s,appearance:l,warning:r}){const{subpath:c}=(0,i.getQuery)(),g=(0,a.createElement)(u.A,{isSecondary:!0,href:e,text:(0,o.__)("Edit","google-listings-and-ads"),eventName:t,eventProps:{path:(0,i.getPath)(),subpath:c}});let h,_;return n?h=(0,a.createElement)("span",{className:"gla-contact-info-preview-card__placeholder","aria-busy":"true",title:(0,o.__)("Loading…","google-listings-and-ads")}):r?(_=(0,a.createElement)(a.Fragment,null,(0,a.createElement)(m.A,{icon:p.A,size:24,className:"gla-contact-info-preview-card__notice-icon"}),r),h=(0,a.createElement)("span",{className:"gla-contact-info-preview-card__notice-details"},s)):h=s,(0,a.createElement)(d.A,{appearance:l,className:"gla-contact-info-preview-card",icon:null,title:_,description:h,indicator:g})}var _=n(2848);function E(e){const t={address_1:(0,o._x)("address line","The field name of the address line in store address","google-listings-and-ads"),city:(0,o._x)("city","The field name of the city in store address","google-listings-and-ads"),country:(0,o._x)("country/state","The field name of the country in store address","google-listings-and-ads"),postcode:(0,o._x)("postcode/zip","The field name of the postcode in store address","google-listings-and-ads")};return e.missingRequiredFields.map((e=>{const n=t[e]||e;return(0,o.sprintf)( // translators: %s: The missing field name of store address. // translators: %s: The missing field name of store address. (0,o.__)("The %s of store address is required.","google-listings-and-ads"),n)}))}var f=n(6473);const A=()=>{const{loaded:e,data:t,refetch:n}=(0,c.A)(),{isAddressFilled:m}=t,p=(0,i.getPath)(),{subpath:h}=(0,i.getQuery)(),A=(0,s.useRef)(null);e&&A.current&&(A.current(t),A.current=null);const y=(0,a.createElement)(u.A,{isSecondary:!0,icon:r.A,iconSize:20,iconPosition:"right",text:(0,o.__)("Update store address","google-listings-and-ads"),onClick:()=>{n(),A.current=e=>{const t={path:p,subpath:h,country_code:e.countryCode,missing_fields:e.missingRequiredFields.join(",")};(0,f.ce)("gla_wc_store_address_validation",t)}},disabled:!e}),v=(0,a.createElement)(_.A,{target:"_blank",type:"external",href:"admin.php?page=wc-settings",eventName:"gla_edit_wc_store_address",eventProps:{path:p,subpath:h}});let b=(0,a.createElement)(l.Spinner,null);if(e){const{address:e,address2:n,city:o,state:s,country:l,postcode:r}=t,i=[o,s?`${s} - ${l}`:l,r].filter(Boolean).join(", ");b=(0,a.createElement)(a.Fragment,null,e&&(0,a.createElement)("div",null,e),n&&(0,a.createElement)("div",null,n),(0,a.createElement)("div",null,i))}const C=(0,a.createElement)("p",null,m?(0,s.createInterpolateElement)((0,o.__)("We’re using your store address for Google verification. This information won’t be public. Edit in WooCommerce settings if needed and update to review the changes.","google-listings-and-ads"),{link:v}):(0,s.createInterpolateElement)((0,o.__)("Your store address is required by Google for verification. This information won’t be public. Complete that in WooCommerce settings and update to review the changes.","google-listings-and-ads"),{link:v})),k=(0,a.createElement)(a.Fragment,null,b,!m&&(0,a.createElement)(g.A,{messages:E(t)}));return(0,a.createElement)(d.A,{className:"gla-store-address-card",appearance:d.x.ADDRESS,alignIcon:"top",alignIndicator:"top",description:C,detail:k,indicator:y})};function y({editHref:e,learnMore:t}){const{loaded:n,data:s}=(0,c.A)("mc");let l,r;if(n){const{isAddressFilled:e,isMCAddressDifferent:n,address:i,address2:c,city:d,state:u,country:g,postcode:m}=s;e&&!n?l=[i,c,d,u?`${u} - ${g}`:g,m].filter(Boolean).join(", "):(r=(0,o.__)("Please add your store address","google-listings-and-ads"),l=(0,a.createElement)(a.Fragment,null,(0,o.__)("Google requires the store address for all stores using Google Merchant Center. ","google-listings-and-ads"),t))}return(0,a.createElement)(h,{appearance:d.x.ADDRESS,editHref:e,editEventName:"gla_edit_mc_store_address",loading:!n,warning:r,content:l})}var v=n(3666),b=n(8242),C=n(1177);const k="contact-information-read-more",w="https://woocommerce.com/document/google-for-woocommerce/get-started/requirements/#contact-information",N=(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,o.__)("Your contact information is required for verification by Google.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,o.__)("It would be shared with Google Merchant Center for store verification and would not be displayed to customers.","google-listings-and-ads"))),S=(0,o.__)("Contact information","google-listings-and-ads");function x(){return(0,a.createElement)(b.A,{title:S,description:N},(0,a.createElement)(y,{editHref:(0,v.Xb)(),learnMore:(0,a.createElement)(C.A,{context:"settings-no-store-address-notice",linkId:k,href:w},(0,o.__)("Learn more","google-listings-and-ads"))}))}},8463:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(6942),s=n.n(o);const l=e=>{const{className:t,...n}=e;return(0,a.createElement)("div",{className:s()("gla-content-button-layout",t),...n})}},1787:(e,t,n)=>{n.d(t,{A:()=>m});var a=n(1609),o=n(7723),s=n(7752),l=n(3905),r=n(5744),i=n(6087),c=n(7143),d=n(6520);const u=(e,t=d.mY)=>{const n=((e=d.mY)=>(0,c.useSelect)((t=>t(e).getNotices()),[e]))(t).find((t=>t.content===e));return(0,i.useEffect)((()=>{const{removeNotice:e}=(0,c.dispatch)(t);return()=>{n&&e(n.id)}}),[n,t]),n||null};var g=n(6473);const m=({eventContext:e,label:t,secondLabel:n})=>(u(t,"core/notices2"),(0,a.createElement)(s.CustomerEffortScore,{label:t,title:t,firstQuestion:t,secondQuestion:n,recordScoreCallback:(t,n,a)=>{(0,g.ce)("gla_ces_feedback",{context:e,score:t,comments:a||""})},onNoticeShownCallback:()=>{r.A.remove(l.rS.CAN_ONBOARDING_SETUP_CES_PROMPT_OPEN),(0,g.ce)("gla_ces_snackbar_open",{context:e})},onNoticeDismissedCallback:()=>{(0,g.ce)("gla_ces_snackbar_closed",{context:e})},onModalShownCallback:()=>{(0,g.ce)("gla_ces_modal_open",{context:e})},icon:(0,a.createElement)("span",{style:{height:21,width:21},role:"img","aria-label":(0,o.__)("Pencil icon","google-listings-and-ads")},"✏️")}))},2047:(e,t,n)=>{n.d(t,{A:()=>u});var a=n(1609),o=n(7723),s=n(6427),l=n(6087),r=n(1378),i=n(3772),c=n(1177),d=n(3905);const u=({context:e})=>{const{googleAdsAccount:t}=(0,r.A)(),{code:n}=(0,i.A)();return t&&t.status===d.Wn.CONNECTED&&t.currency!==n?(0,a.createElement)(s.Notice,{className:"gla-different-currency-notice",status:"warning",isDismissible:!1},(0,l.createInterpolateElement)((0,o.__)("Note: The currency set in your Google Ads account is , which is different from your store currency, . Read more","google-listings-and-ads"),{adsCurrency:(0,a.createElement)("strong",null,t.currency),storeCurrency:(0,a.createElement)("strong",null,n),readMoreLink:(0,a.createElement)(c.A,{className:"gla-different-currency-notice__link",href:"https://support.google.com/google-ads/answer/9841530",context:e,linkId:"setting-up-currency"})})):null}},8987:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(1609),o=n(8846),s=n(7723),l=n(6473);const r=()=>{},i=({productId:e,eventName:t,eventProps:n})=>{const i=`post.php?action=edit&post=${e}`,c=t?()=>(0,l.Ff)(t,n):r;return(0,a.createElement)(o.Link,{href:i,onClick:c,type:"wp-admin"},(0,s.__)("Edit","google-listings-and-ads"))}},6588:(e,t,n)=>{n.d(t,{A:()=>g});var a=n(1609),o=n(7723),s=n(3832),l=n(6087),r=n(7892),i=n(3905),c=n(6520),d=n(6599),u=n(5640);const g=e=>{const{createNotice:t}=(0,u.A)(),[n,g]=(0,l.useState)(!1),m={next_page_name:i.Th.mcSetupComplete?"settings":"setup-mc"},p=(0,s.addQueryArgs)(`${c.RV}/rest-api/authorize`,m),[h]=(0,d.A)({path:p});return(0,a.createElement)(r.A,{isSecondary:!0,loading:n,onClick:async()=>{try{g(!0);const e=await h();g(!1),window.location.href=e.auth_url}catch(e){g(!1),t("error",(0,o.__)("Unable to enable new product sync. Please try again later.","google-listings-and-ads"))}},...e})}},332:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(7723),s=n(6427),l=n(6087),r=n(7916),i=n(6588);const c=()=>{const{hasFinishedResolution:e,googleMCAccount:t}=(0,r.A)();return e&&t?.notification_service_enabled&&!t?.wpcom_rest_api_status?(0,a.createElement)(s.Notice,{status:"info",isDismissible:!1},(0,l.createInterpolateElement)((0,o.__)("

We will soon transition to a new and improved method for synchronizing product data with Google.

Get early access","google-listings-and-ads"),{enableButton:(0,a.createElement)(i.A,{eventName:"gla_enable_product_sync_click",eventProps:{context:"banner"}}),p:(0,a.createElement)("p",null)})):null}},2178:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(3207);const s=({size:e=18})=>(0,a.createElement)(o.A,{className:"gla-error-icon",size:e})},2448:(e,t,n)=>{n.d(t,{A:()=>N});var a=n(1609),o=n(7723),s=n(6087),l=n(8468),r=n(5556),i=n.n(r),c=n(6476),d=n(7374),u=n(4111),g=n.n(u),m=n(8846),p=n(6427),h=n(6459);class _ extends s.Component{constructor({getLabels:e,param:t,query:n}){super(...arguments),this.state={selected:[]},this.clearQuery=this.clearQuery.bind(this),this.updateQuery=this.updateQuery.bind(this),this.updateLabels=this.updateLabels.bind(this),this.onButtonClicked=this.onButtonClicked.bind(this),n[t]&&e(n[t],n).then(this.updateLabels)}componentDidUpdate({param:e,query:t},{selected:n}){const{getLabels:a,param:o,query:s}=this.props,{selected:r}=this.state;if(e!==o||n.length>0&&0===r.length)return void this.clearQuery();const i=(0,c.getIdsFromQuery)(t[o]),d=(0,c.getIdsFromQuery)(s[o]);(0,l.isEqual)(i.sort(),d.sort())||a(s[o],s).then(this.updateLabels)}clearQuery(){const{param:e,path:t,query:n}=this.props;this.setState({selected:[]}),(0,c.updateQueryString)({[e]:void 0},t,n)}updateLabels(e){this.setState({selected:e})}updateQuery(){const{param:e,path:t,query:n}=this.props,{selected:a}=this.state,o=a.map((e=>e.key));(0,c.updateQueryString)({[e]:o.join(",")},t,n)}onButtonClicked(e){this.updateQuery(e),(0,l.isFunction)(this.props.onClick)&&this.props.onClick(e)}render(){const{labels:e,type:t,autocompleter:n}=this.props,{selected:s}=this.state;return(0,a.createElement)(p.Card,{className:"woocommerce-filters__compare"},(0,a.createElement)(p.CardHeader,null,(0,a.createElement)(h.A,{variant:"subtitle-small"},e.title)),(0,a.createElement)(p.CardBody,null,(0,a.createElement)(m.Search,{autocompleter:n,type:t,selected:s,placeholder:e.placeholder,onChange:e=>{this.setState({selected:e})}})),(0,a.createElement)(p.CardFooter,{justify:"flex-start"},(0,a.createElement)(m.CompareButton,{count:s.length,helpText:e.helpText,onClick:this.onButtonClicked},e.update),s.length>0&&(0,a.createElement)(p.Button,{isLink:!0,onClick:this.clearQuery},(0,o.__)("Clear all","woocommerce"))))}}_.propTypes={getLabels:i().func.isRequired,labels:i().shape({placeholder:i().string,title:i().string,update:i().string}),param:i().string.isRequired,path:i().string.isRequired,query:i().object,type:i().string.isRequired,autocompleter:i().object},_.defaultProps={labels:{},query:{}};var E=n(8107),f=n(6942),A=n.n(f),y=n(7677),v=n(3988);const b="all";class C extends s.Component{constructor(e){super(e);const t=this.getFilter();if(this.state={nav:t.path||[],animate:null,selectedTag:null},this.selectSubFilter=this.selectSubFilter.bind(this),this.getVisibleFilters=this.getVisibleFilters.bind(this),this.updateSelectedTag=this.updateSelectedTag.bind(this),this.onTagChange=this.onTagChange.bind(this),this.onContentMount=this.onContentMount.bind(this),this.goBack=this.goBack.bind(this),t.settings&&t.settings.getLabels){const{query:e}=this.props,{param:n,getLabels:a}=t.settings;a(e[n],e).then(this.updateSelectedTag)}}componentDidUpdate({query:e}){const{query:t,config:n}=this.props;if(e[n.param]!==t[[n.param]]){const e=this.getFilter();if(e&&"Search"===e.component){this.setState({nav:e.path||[]});const{param:n,getLabels:a}=e.settings;a(t[n],t).then(this.updateSelectedTag)}}}updateSelectedTag(e){this.setState({selectedTag:e[0]})}getFilter(e){const{config:t,query:n}=this.props,a=(0,c.flattenFilters)(t.filters);return e=e||n[t.param]||t.defaultValue||b,(0,l.find)(a,{value:e})||{}}getButtonLabel(e){if("Search"===e.component){const{selectedTag:t}=this.state;return[t&&t.label,(0,l.get)(e,"settings.labels.button")]}return e?[e.label]:[]}getVisibleFilters(e,t){if(0===t.length)return e;const n=t[0],a=(0,l.find)(e,{value:n});return this.getVisibleFilters(a&&a.subFilters,t.slice(1))}selectSubFilter(e){this.setState((t=>({nav:[...t.nav,e],animate:"left"})))}goBack(){this.setState((e=>({nav:e.nav.slice(0,-1),animate:"right"})))}update(e,t={}){const{path:n,query:a,config:o,onFilterSelect:s}=this.props,l=(0,c.getPersistedQuery)(a),r={[o.param]:(o.defaultValue||b)===e?void 0:e,...t};o.staticParams.forEach((e=>{r[e]=a[e]})),(0,c.updateQueryString)(r,n,l),s(r)}onTagChange(e,t,n,a){const o=(0,l.last)(a),{value:s,settings:r}=e,{param:i}=r;o?(this.update(s,{[i]:o.key}),t()):this.update(n.defaultValue||b),this.updateSelectedTag([o])}renderButton(e,t,n){if(e.component){const{type:o,labels:s,autocompleter:r}=e.settings,i=this.getFilter().value===e.value?this.state.selectedTag:null;return(0,a.createElement)(m.Search,{autocompleter:r,className:"woocommerce-filters-filter__search",type:o,placeholder:s.placeholder,selected:i?[i]:[],onChange:(0,l.partial)(this.onTagChange,e,t,n),inlineTags:!0,staticResults:!0})}const o=n=>{t(n),this.update(e.value,e.query||{}),this.setState({selectedTag:null})},s=(0,l.partial)(this.selectSubFilter,e.value),r=this.getFilter(),i=r.value===e.value||r.path&&(0,l.includes)(r.path,e.value);return(0,a.createElement)(p.Button,{className:"woocommerce-filters-filter__button",onClick:n=>{i?t(n):e.subFilters?s(n):o(n)}},e.label)}onContentMount(e){const{nav:t}=this.state,n=t.length&&this.getFilter(t[t.length-1])?1:0,a=E.focus.tabbable.find(e)[n];setTimeout((()=>{a.focus()}),0)}render(){const{config:e}=this.props,{nav:t,animate:n}=this.state,s=this.getVisibleFilters(e.filters,t),r=!!t.length&&this.getFilter(t[t.length-1]),i=this.getFilter();return(0,a.createElement)("div",{className:"woocommerce-filters-filter"},e.label&&(0,a.createElement)("span",{className:"woocommerce-filters-label"},e.label,":"),(0,a.createElement)(p.Dropdown,{contentClassName:"woocommerce-filters-filter__content",position:"bottom",expandOnMobile:!0,headerTitle:(0,o.__)("filter report to show:","woocommerce"),renderToggle:({isOpen:e,onToggle:t})=>(0,a.createElement)(m.DropdownButton,{onClick:t,isOpen:e,labels:this.getButtonLabel(i)}),renderContent:({onClose:o})=>(0,a.createElement)(m.AnimationSlider,{animationKey:t,animate:n,onExited:this.onContentMount},(()=>(0,a.createElement)("ul",{className:"woocommerce-filters-filter__content-list"},r&&(0,a.createElement)("li",{className:"woocommerce-filters-filter__content-list-item"},(0,a.createElement)(p.Button,{className:"woocommerce-filters-filter__button",onClick:this.goBack},(0,a.createElement)(y.A,{icon:v.A}),r.label)),s.map((t=>(0,a.createElement)("li",{key:t.value,className:A()("woocommerce-filters-filter__content-list-item",{"is-selected":i.value===t.value||i.path&&(0,l.includes)(i.path,t.value)})},this.renderButton(t,o,e)))))))}))}}C.propTypes={config:i().shape({label:i().string,staticParams:i().array.isRequired,param:i().string.isRequired,defaultValue:i().string,showFilters:i().func.isRequired,filters:i().arrayOf(i().shape({chartMode:i().oneOf(["item-comparison","time-comparison"]),component:i().string,label:i().string,path:i().string,subFilters:i().array,value:i().string.isRequired}))}).isRequired,path:i().string.isRequired,query:i().object,onFilterSelect:i().func},C.defaultProps={query:{},onFilterSelect:()=>{}};const k=C;class w extends s.Component{constructor(){super(),this.renderCard=this.renderCard.bind(this),this.onRangeSelect=this.onRangeSelect.bind(this)}renderCard(e){const{siteLocale:t,advancedFilters:n,query:o,path:s,onAdvancedFilterAction:r,currency:i}=this.props,{filters:c,param:d}=e;if(!o[d])return null;if(0===o[d].indexOf("compare")){const e=(0,l.find)(c,{value:o[d]});if(!e)return null;const{settings:t={}}=e;return(0,a.createElement)("div",{key:d,className:"woocommerce-filters__advanced-filters"},(0,a.createElement)(_,{path:s,query:o,...t}))}return"advanced"===o[d]?(0,a.createElement)("div",{key:d,className:"woocommerce-filters__advanced-filters"},(0,a.createElement)(m.AdvancedFilters,{siteLocale:t,currency:i,config:n,path:s,query:o,onAdvancedFilterAction:r})):void 0}onRangeSelect(e){const{query:t,path:n,onDateSelect:a}=this.props;(0,c.updateQueryString)(e,n,t),a(e)}getDateQuery(e){const{period:t,compare:n,before:a,after:o}=(0,d.getDateParamsFromQuery)(e),{primary:s,secondary:l}=(0,d.getCurrentDates)(e);return{period:t,compare:n,before:a,after:o,primaryDate:s,secondaryDate:l}}render(){const{dateQuery:e,filters:t,query:n,path:l,showDatePicker:r,onFilterSelect:i,isoDateFormat:c}=this.props;return(0,a.createElement)(s.Fragment,null,(0,a.createElement)(m.H,{className:"screen-reader-text"},(0,o.__)("Filters","woocommerce")),(0,a.createElement)(m.Section,{component:"div",className:"woocommerce-filters"},(0,a.createElement)("div",{className:"woocommerce-filters__basic-filters"},r&&(0,a.createElement)(m.DateRangeFilterPicker,{key:JSON.stringify(n),dateQuery:e||this.getDateQuery(n),onRangeSelect:this.onRangeSelect,isoDateFormat:c}),t.map((e=>e.showFilters(n)?(0,a.createElement)(k,{key:e.param,config:e,query:n,path:l,onFilterSelect:i}):null))),t.map(this.renderCard)))}}w.propTypes={siteLocale:i().string,advancedFilters:i().object,filters:i().array,path:i().string.isRequired,query:i().object,showDatePicker:i().bool,onDateSelect:i().func,onFilterSelect:i().func,onAdvancedFilterAction:i().func,currency:i().object,dateQuery:i().shape({period:i().string.isRequired,compare:i().string.isRequired,before:i().object,after:i().object,primaryDate:i().shape({label:i().string.isRequired,range:i().string.isRequired}).isRequired,secondaryDate:i().shape({label:i().string.isRequired,range:i().string.isRequired})}),isoDateFormat:i().string},w.defaultProps={siteLocale:"en_US",advancedFilters:{},filters:[],query:{},showDatePicker:!0,onDateSelect:()=>{},currency:g()().getCurrencyConfig()};const N=w},8428:(e,t,n)=>{n.d(t,{A:()=>p});var a=n(1609),o=n(6942),s=n.n(o),l=n(6087),r=n(7723),i=n(6427),c=n(8468),d=n(5573);const u=()=>(0,a.createElement)(d.SVG,{width:"8",height:"8",fill:"none",xmlns:"http://www.w3.org/2000/svg"},(0,a.createElement)(d.Circle,{cx:"4",cy:"4",r:"4"}));function g({currentPage:e,numberOfPages:t,setCurrentPage:n}){return(0,a.createElement)("ul",{className:"components-guide__page-control","aria-label":(0,r.__)("Guide controls")},(0,c.times)(t,(o=>(0,a.createElement)("li",{key:o,"aria-current":o===e?"step":void 0},(0,a.createElement)(i.Button,{key:o,icon:(0,a.createElement)(u,null),"aria-label":(0,r.sprintf)(/* translators: 1: current page number 2: total number of pages */ /* translators: 1: current page number 2: total number of pages */ (0,r.__)("Page %1$d of %2$d"),o+1,t),onClick:()=>n(o)})))))}function m(e){const t=(0,l.useRef)();return(0,l.useLayoutEffect)((()=>{const{ownerDocument:e}=t.current,{activeElement:n,body:a}=e;n&&n!==a||t.current.focus()}),[]),(0,a.createElement)(i.Button,{...e,ref:t})}function p({className:e,contentLabel:t,backButtonText:n,finishButtonText:o,renderFinish:c=e=>e,onFinish:d,pages:u}){const[p,h]=(0,l.useState)(0),_=p>0,E=p{_&&h(p-1)},A=()=>{E&&h(p+1)};if(0===u.length)return null;let y=null;E||(y=c((0,a.createElement)(m,{className:"components-guide__finish-button",onClick:d},o||(0,r.__)("Finish"))));const v=s()("gla-admin-page","components-guide",e);return(0,a.createElement)(i.Modal,{className:v,contentLabel:t,onRequestClose:d},(0,a.createElement)(i.KeyboardShortcuts,{key:p,shortcuts:{left:f,right:A}}),(0,a.createElement)("div",{className:"components-guide__container"},(0,a.createElement)("div",{className:"components-guide__page"},u[p].image,u.length>1&&(0,a.createElement)(g,{currentPage:p,numberOfPages:u.length,setCurrentPage:h}),u[p].content),(0,a.createElement)("div",{className:"components-guide__footer"},_&&(0,a.createElement)(i.Button,{className:"components-guide__back-button",onClick:f},n||(0,r.__)("Previous")),E&&(0,a.createElement)(i.Button,{className:"components-guide__forward-button",onClick:A},(0,r.__)("Next")),y)))}},9452:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(1609),o=n(7723),s=n(6427),l=n(6942),r=n.n(l),i=n(6473);const c=(e,t,n)=>a=>{(0,i.ce)(e,{id:t,action:a?"expand":"collapse",context:n})};function d({trackName:e,faqItems:t,className:n,context:l}){return(0,a.createElement)(s.Panel,{className:r()("gla-faqs-panel",n),header:(0,o.__)("Frequently asked questions","google-listings-and-ads")},t.map((({trackId:t,question:n,answer:o})=>(0,a.createElement)(s.PanelBody,{key:t,title:n,initialOpen:!1,onToggle:c(e,t,l)},(0,a.createElement)(s.PanelRow,null,o)))))}},6141:(e,t,n)=>{n.d(t,{A:()=>m});var a=n(1609),o=n(7723),s=n(6087),l=n(5170),r=n(1177),i=n(2848),c=n(9457);const d=[["DZ",250,"USD"],["AR",25e3,"ARS"],["AM",300,"USD"],["AU",600,"AUD"],["AT",400,"EUR"],["AZ",300,"USD"],["BH",300,"USD"],["BY",300,"USD"],["BE",400,"EUR"],["BO",350,"USD"],["BA",350,"EUR"],["BR",1200,"BRL"],["BG",700,"BGN"],["CA",600,"CAD"],["CL",350,"USD"],["CN",3e3,"CNY"],["CO",350,"USD"],["CR",350,"USD"],["HR",2500,"HRK"],["CY",350,"EUR"],["CZ",8500,"CZK"],["DK",3e3,"DKK"],["DO",350,"USD"],["EC",350,"USD"],["EG",3e3,"EGP"],["SV",350,"USD"],["EE",350,"EUR"],["FI",400,"EUR"],["FR",400,"EUR"],["GE",300,"USD"],["DE",400,"EUR"],["GR",350,"EUR"],["GT",350,"USD"],["HN",350,"USD"],["HK",3e3,"HKD"],["HU",12e4,"HUF"],["IS",400,"EUR"],["IN",2e4,"INR"],["ID",3e6,"IDR"],["IE",400,"EUR"],["IL",1500,"ILS"],["IT",400,"EUR"],["JP",6e4,"JPY"],["JO",250,"USD"],["KZ",300,"USD"],["KE",300,"USD"],["KR",6e5,"KRW"],["KW",300,"USD"],["LV",350,"EUR"],["LB",250,"USD"],["LY",250,"USD"],["LT",350,"EUR"],["LU",400,"EUR"],["MK",300,"USD"],["MY",1500,"MYR"],["MT",350,"EUR"],["MX",7e3,"MXN"],["ME",350,"EUR"],["MA",250,"USD"],["NL",400,"EUR"],["NZ",600,"NZD"],["NI",350,"USD"],["NG",300,"USD"],["NO",4e3,"NOK"],["OM",250,"USD"],["PK",400,"USD"],["PS",250,"USD"],["PA",350,"USD"],["PY",350,"USD"],["PE",350,"USD"],["PH",2e4,"PHP"],["PL",1200,"PLN"],["PT",400,"EUR"],["PR",350,"USD"],["QA",300,"USD"],["RO",1500,"RON"],["RU",3e4,"RUB"],["SA",1300,"SAR"],["RS",350,"EUR"],["SG",600,"SGD"],["SK",350,"EUR"],["SI",350,"EUR"],["ZA",6e3,"ZAR"],["ES",400,"EUR"],["LK",400,"USD"],["SE",4e3,"SEK"],["CH",400,"CHF"],["TW",12e3,"TWD"],["TH",12e3,"THB"],["TN",250,"USD"],["TR",2500,"TRY"],["UA",1e4,"UAH"],["AE",1600,"AED"],["GB",400,"GBP"],["US",500,"USD"],["UY",350,"USD"],["VE",350,"USD"],["VN",56e5,"VND"]];var u=n(6734);const g=e=>{const t=(0,u.A)();return(0,a.createElement)(c.A,{className:"gla-free-ad-credit-country-modal",title:(0,o.__)("Check your maximum free credit","google-listings-and-ads"),...e},(0,a.createElement)("p",null,(0,o.__)("Whatever you spend in the next month will be added back to your Google Ads account as free credit, up to a maximum limit depending on your store’s country.","google-listings-and-ads")),(0,a.createElement)("table",null,(0,a.createElement)("tbody",null,d.map(((e,n)=>{const[o,s,l]=e;return(0,a.createElement)("tr",{key:n},(0,a.createElement)("td",null,t[o]),(0,a.createElement)("td",null,`${s} ${l}`))})))))},m=()=>{const[e,t]=(0,s.useState)(!1);return(0,a.createElement)("div",{className:"gla-free-ad-credit"},(0,a.createElement)(l.A,null),(0,a.createElement)("div",null,(0,a.createElement)("div",{className:"gla-free-ad-credit__title"},(0,o.__)("Spend $500 to get $500 in Google Ads credits!","google-listings-and-ads")),(0,a.createElement)("div",{className:"gla-free-ad-credit__description"},(0,s.createInterpolateElement)((0,o.__)("New to Google Ads? Get $500 in ad credit when you spend $500 within your first 60 days. Check how much credit you can receive in your country here. Terms and conditions apply.","google-listings-and-ads"),{checkLink:(0,a.createElement)(i.A,{eventName:"gla_free_ad_credit_country_click",eventProps:{context:"setup-ads"},href:"#",type:"wp-admin",onClick:()=>{t(!0)}}),termLink:(0,a.createElement)(r.A,{context:"setup-ads",linkId:"free-ad-credit-terms",href:"https://www.google.com/ads/coupons/terms/"})})),e&&(0,a.createElement)(g,{onRequestClose:()=>{t(!1)}})))}},7343:(e,t,n)=>{n.d(t,{A:()=>ze});var a=n(1609),o=n(6087),s=n(6427),l=n(8846),r=n(8468),i=n(3741),c=n(7892),d=n(6960),u=n(6319),g=n(7723);const m=new Set(["all","selected"]),p=new Set(["automatic","flat","manual"]),h=new Set(["flat","manual"]),_=e=>e.rate>0,E=e=>!!e.some(_)&&e.some((e=>e.options.free_shipping_threshold>0));var f=n(8683),A=n(8242),y=n(8771),v=n(8864),b=n(9491),C=n(6942),k=n.n(C),w=n(8107),N=n(2224);const S=({tags:e=[],disabled:t,maxVisibleTags:n=0,onChange:s=()=>{}})=>{const[r,i]=(0,o.useState)(!1),d=Math.max(0,n),u=r||!d?e:e.slice(0,d);if(!e.length)return null;const m=n=>()=>{t||s(e.filter((e=>e.id!==n)))};return(0,a.createElement)("div",{className:"woocommerce-tree-select-control__tags"},u.map(((t,n)=>{if(!t.label)return null;const o=(0,g.sprintf)( // translators: 1: Tag Label, 2: Current Tag index, 3: Total amount of tags. // translators: 1: Tag Label, 2: Current Tag index, 3: Total amount of tags. (0,g.__)("%1$s (%2$d of %3$d)","google-listings-and-ads"),t.label,n+1,e.length);return(0,a.createElement)(l.Tag,{key:t.id,id:t.id,label:t.label,screenReaderLabel:o,remove:m})})),d>0&&e.length>d&&(0,a.createElement)(c.A,{isTertiary:!0,className:"woocommerce-tree-select-control__show-more",onClick:()=>{i(!r)}},r?(0,g.__)("Show less","google-listings-and-ads"):(0,g.sprintf)( // translators: %d: The number of extra tags to show // translators: %d: The number of extra tags to show (0,g.__)("+ %d more","google-listings-and-ads"),e.length-d)))},x="__WC_TREE_SELECT_COMPONENT_ROOT__",R="ArrowUp",T="ArrowDown",I=(0,o.forwardRef)((({tags:e=[],instanceId:t,placeholder:n,isExpanded:o,disabled:s,maxVisibleTags:l,value:i="",onFocus:c=()=>{},onTagsChange:d=()=>{},onInputChange:u=()=>{},onControlClick:g=r.noop},m)=>{const p=e.length>0,h=!p&&!o;return(0,a.createElement)("div",{className:k()("components-base-control","woocommerce-tree-select-control__control",{"is-disabled":s,"has-tags":p}),onClick:e=>{m.current.focus(),g(e)}},p&&(0,a.createElement)(S,{disabled:s,tags:e,maxVisibleTags:l,onChange:d}),(0,a.createElement)("div",{className:"components-base-control__field"},(0,a.createElement)("input",{ref:m,id:`woocommerce-tree-select-control-${t}__control-input`,type:"search",placeholder:h?n:"",autoComplete:"off",className:"woocommerce-tree-select-control__control-input",role:"combobox","aria-autocomplete":"list",value:i,"aria-expanded":o,disabled:s,onFocus:c,onChange:u,onKeyDown:t=>{if("Backspace"===t.key){if(i)return;d(e.slice(0,-1)),t.preventDefault()}}})))}));var G=n(7677),F=n(3756),P=n(2485),M=n(8351);const O=({option:e,checked:t,className:n,...o})=>{var s,l;return e?(0,a.createElement)("div",{className:n},(0,a.createElement)("div",{className:"components-base-control__field"},(0,a.createElement)("span",{className:"components-checkbox-control__input-container"},(0,a.createElement)("input",{id:`inspector-checkbox-control-${null!==(s=e.key)&&void 0!==s?s:e.value}`,className:"components-checkbox-control__input",type:"checkbox",tabIndex:"-1",value:e.value,checked:t,...o}),t&&(0,a.createElement)(G.A,{icon:M.A,role:"presentation",className:"components-checkbox-control__checked"})),(0,a.createElement)("label",{className:"components-checkbox-control__label",htmlFor:`inspector-checkbox-control-${null!==(l=e.key)&&void 0!==l?l:e.value}`},e.label))):null},D=({options:e=[],onChange:t=()=>{},onExpanderClick:n=r.noop,onToggleExpanded:o=r.noop})=>e.map((e=>{var l;const r=e.value===x,{hasChildren:i,checked:c,partialChecked:d,expanded:u}=e;return(0,a.createElement)("div",{key:`${null!==(l=e.key)&&void 0!==l?l:e.value}`,role:i?"treegroup":"treeitem","aria-expanded":i?u:void 0,className:k()("woocommerce-tree-select-control__node",i&&"has-children")},(0,a.createElement)(s.Flex,{justify:"flex-start"},!r&&(0,a.createElement)("button",{className:k()("woocommerce-tree-select-control__expander",!i&&"is-hidden"),tabIndex:"-1",onClick:t=>{n(t),o(e)}},(0,a.createElement)(G.A,{icon:u?F.A:P.A})),(0,a.createElement)(O,{className:k()("components-base-control","woocommerce-tree-select-control__option",d&&"is-partially-checked"),option:e,checked:c,onChange:n=>{t(n.target.checked,e)},onKeyDown:t=>{((e,t)=>{t.hasChildren&&("ArrowRight"!==e.key||t.expanded?"ArrowLeft"===e.key&&t.expanded&&o(t):o(t))})(t,e)}})),i&&u&&(0,a.createElement)("div",{className:k()("woocommerce-tree-select-control__children",r&&"woocommerce-tree-select-control__main")},(0,a.createElement)(D,{options:e.children,onChange:t,onExpanderClick:n,onToggleExpanded:o})))})),U=D,L=({id:e,label:t,selectAllLabel:n=(0,g.__)("All","google-listings-and-ads"),help:s,placeholder:l,className:i,disabled:c,options:d=[],value:u=[],maxVisibleTags:m,onChange:p=()=>{},onDropdownVisibilityChange:h=r.noop})=>{let _=(0,b.useInstanceId)(L);_=null!=e?e:_;const[E,f]=(0,o.useState)(!1),[A,y]=(0,o.useState)([]),[v,C]=(0,o.useState)(""),S=(0,o.useRef)(),G=(0,o.useRef)(),F=(0,o.useRef)();F.current=h;const P=(0,o.useRef)({filteredOptionsMap:new Map});P.current.expandedValues=A,P.current.selectedValues=u;const M=!c&&E,O=!1!==n?{label:n,value:x,children:d}:null,D=(0,N.A)(O?[O]:d),q=(0,b.__experimentalUseFocusOutside)((()=>{f(!1)})),B=v.trim().toLowerCase(),$=B.length>=3?B:"",V=(0,o.useMemo)((()=>{const e={};return P.current.filteredOptionsMap.clear(),D.forEach((function t(n,a){const{children:o=[]}=n;var s;n.parent=a,o.forEach((e=>t(e,n.value))),o.length||(e[null!==(s=n.key)&&void 0!==s?s:n.value]=n)})),e}),[D]),j=(0,o.useMemo)((()=>{const{current:e}=P,t=e.filteredOptionsMap.get($);if(t)return t;const n=Boolean($),o={hasChildren:{get(){return this.children?.length>0}},leaves:{get(){return this.hasChildren?this.children.flatMap((e=>e.hasChildren?e.leaves:e)):[]}},checked:{get(){return this.hasChildren?this.leaves.every((e=>e.checked)):e.selectedValues.includes(this.value)}},partialChecked:{get(){return!!this.hasChildren&&!this.checked&&this.leaves.some((e=>e.checked||e.partialChecked))}},expanded:{get(){return n||this.value===x||e.expandedValues.includes(this.value)}}},s=(e,{children:t=[],...l})=>{if(t.length){if(l.children=t.reduce(s,[]),!l.children.length)return e}else if(n){const t=l.label.toLowerCase().indexOf($);if(-1===t)return e;l.label=((e,t)=>{const o=t+$.length;return n?(0,a.createElement)("span",null,(0,a.createElement)("span",null,e.substring(0,t)),(0,a.createElement)("strong",null,e.substring(t,o)),(0,a.createElement)("span",null,e.substring(o))):e})(l.label,t)}return Object.defineProperties(l,o),e.push(l),e},l=D.reduce(s,[]);return e.filteredOptionsMap.set($,l),l}),[D,$]);(0,o.useEffect)((()=>{F.current(M)}),[M]);const W=e=>{y(e.expanded?A.filter((t=>e.value!==t)):[...A,e.value])};return(0,a.createElement)("div",{...q,onKeyDown:e=>{if(c)return;"Escape"===e.key&&f(!1),"Enter"===e.key&&(f(!0),e.preventDefault());const t={[R]:-1,[T]:1}[e.key];if(t&&G.current&&j.length){const n=w.focus.focusable.find(G.current).filter((e=>"checkbox"===e.type)),a=n.indexOf(e.target),o=Math.max(a+t,-1)%n.length;n.at(o).focus(),e.preventDefault()}},className:k()("woocommerce-tree-select-control",i)},!!t&&(0,a.createElement)("label",{htmlFor:`woocommerce-tree-select-control-${_}__control-input`,className:"woocommerce-tree-select-control__label"},t),(0,a.createElement)(I,{ref:S,disabled:c,tags:d.length?u.map((e=>{const t=V[e];return{id:e,label:t?.label}})):[],isExpanded:M,onFocus:()=>{f(!0)},onControlClick:()=>{c||f(!0)},instanceId:_,placeholder:l,label:t,maxVisibleTags:m,value:v,onTagsChange:e=>{p([...e.map((e=>e.id))])},onInputChange:e=>{C(e.target.value)}}),M&&(0,a.createElement)("div",{ref:G,className:"woocommerce-tree-select-control__tree",role:"tree",tabIndex:"-1"},(0,a.createElement)(U,{options:j,onChange:(e,t)=>{t.hasChildren?((e,t)=>{let n;const a=t.leaves.filter((t=>t.checked!==e)).map((e=>e.value));e?(t.expanded||W(t),n=u.concat(a)):n=u.filter((e=>!a.includes(e))),p(n)})(e,t):((e,t)=>{const n=e?[...u,t.value]:u.filter((e=>e!==t.value));p(n)})(e,t),C(""),A.includes(t.parent)||S.current.focus()},onExpanderClick:e=>{const t=w.focus.focusable.find(G.current),n=t.indexOf(e.currentTarget)+1;t.at(n).focus()},onToggleExpanded:W})),s&&(0,a.createElement)("div",{className:"woocommerce-tree-select-control__help"},s))},q=L;var B=n(9415);const $=[];function V({countryCodes:e,className:t,...n}){const s=(0,b.useViewportMatch)("medium","<")?5:10,l=function(e){const{data:{countries:t,continents:n},hasFinishedResolution:a}=(0,B.A)("getMCCountriesAndContinents");return(0,o.useMemo)((()=>{if(!a)return $;const o=e||Object.keys(t);return Object.entries(n).reduce(((e,[n,a])=>{const s=a.countries.reduce(((e,n)=>(o.includes(n)&&e.push({value:n,label:t[n].name}),e)),[]);return s.length&&e.push({value:n,label:a.name,children:s}),e}),[])}),[e,t,n,a])}(e);return(0,a.createElement)(q,{className:k()("gla-supported-country-select",t),placeholder:(0,g.__)("Start typing to filter countries…","google-listings-and-ads"),selectAllLabel:(0,g.__)("All countries","google-listings-and-ads"),maxVisibleTags:s,options:l,...n})}var j=n(850);const W=()=>{const{getInputProps:e,adapter:{renderRequestedValidation:t}}=(0,d.h5)();return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(A.A,{className:"gla-choose-audience-section",title:(0,g.__)("Audience","google-listings-and-ads"),description:(0,a.createElement)("p",null,(0,g.__)("Where do you want to sell your products?","google-listings-and-ads"))},(0,a.createElement)(A.A.Card,null,(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(y.A,null,(0,a.createElement)(y.A.Title,null,(0,g.__)("Location","google-listings-and-ads")),(0,a.createElement)(y.A.HelperText,null,(0,g.__)("Your store should already have the appropriate shipping and tax rates (if required) for potential customers in your selected location(s).","google-listings-and-ads")),(0,a.createElement)(j.A,{size:"medium"},(0,a.createElement)(f.A,{...e("location"),collapsible:!0,label:(0,g.__)("Selected countries only","google-listings-and-ads"),value:"selected"},(0,a.createElement)(V,{multiple:!0,...e("countries"),help:(0,g.__)("Can’t find a country? Only supported countries can be selected.","google-listings-and-ads")}),t("countries")),(0,a.createElement)(f.A,{...e("location"),label:(0,g.__)("All countries","google-listings-and-ads"),value:"all"},(0,a.createElement)(v.A,null,(0,g.__)("Your listings will be shown in all supported countries.","google-listings-and-ads")))))))))};var z=n(1177),Y=n(4978),H=n(6494),Q=n(3772);var K=n(5588);const J=e=>{const t={};return 0===e.countries.length&&(t.countries=(0,g.__)("Please specify at least one country.","google-listings-and-ads")),Number.isFinite(e.rate)||(t.rate=(0,g.__)("Please enter the estimated shipping rate.","google-listings-and-ads")),e.rate<0&&(t.rate=(0,g.__)("The estimated shipping rate cannot be less than 0.","google-listings-and-ads")),t};var Z=n(9457);const X=({countryOptions:e,initialValues:t,renderButtons:n=r.noop,onSubmit:s,onRequestClose:i})=>{const[c,d]=(0,o.useState)(!1);return(0,a.createElement)(l.Form,{initialValues:t,validate:J,onSubmit:s},(t=>{const{values:o,getInputProps:s}=t;return(0,a.createElement)(Z.A,{overflow:"visible",shouldCloseOnEsc:!c,shouldCloseOnClickOutside:!c,title:(0,g.__)("Estimate a shipping rate","google-listings-and-ads"),buttons:n(t),onRequestClose:i},(0,a.createElement)(j.A,null,(0,a.createElement)(V,{label:(0,g.__)("If customer is in","google-listings-and-ads"),countryCodes:e,onDropdownVisibilityChange:d,...s("countries")}),(0,a.createElement)(K.A,{label:(0,g.__)("Then the estimated shipping rate displayed in the product listing is","google-listings-and-ads"),suffix:o.currency,...s("rate")})))}))},ee=({countryOptions:e,initialValues:t,onSubmit:n,onRequestClose:o=r.noop})=>(0,a.createElement)(X,{countryOptions:e,initialValues:t,renderButtons:e=>{const{isValidForm:t,handleSubmit:n}=e;return[(0,a.createElement)(c.A,{key:"submit",isPrimary:!0,disabled:!t,onClick:()=>{o(),n()}},(0,g.__)("Add shipping rate","google-listings-and-ads"))]},onSubmit:n,onRequestClose:o}),te=({countryOptions:e,initialValues:t,onSubmit:n,onRequestClose:o=r.noop,onDelete:s=r.noop})=>{const l=()=>{o(),s()};return(0,a.createElement)(X,{countryOptions:e,initialValues:t,renderButtons:e=>{const{isValidForm:t,handleSubmit:n}=e;return[(0,a.createElement)(c.A,{key:"delete",isTertiary:!0,isDestructive:!0,onClick:l},(0,g.__)("Delete","google-listings-and-ads")),(0,a.createElement)(c.A,{key:"submit",isPrimary:!0,disabled:!t,onClick:()=>{o(),n()}},(0,g.__)("Update shipping rate","google-listings-and-ads"))]},onSubmit:n,onRequestClose:o})};var ne=n(6734);const ae=5,oe=e=>{const{countries:t,firstN:n=ae,textWithMore:s,textWithoutMore:l}=e,r=(0,ne.A)(),i=t.slice(0,n).map((e=>r[e])),c=t.length>i.length?s:l;return(0,o.createInterpolateElement)((0,g.sprintf)(c,i.join(", "),t.length-i.length),{strong:(0,a.createElement)("strong",null)})},se=e=>{const{countries:t}=e;return(0,a.createElement)("div",null,(0,a.createElement)(oe,{countries:t,textWithMore: // translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. // translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. (0,g.__)("Shipping rate for %1$s + %2$d more","google-listings-and-ads"),textWithoutMore: // translators: 1: list of country names separated by comma. // translators: 1: list of country names separated by comma. (0,g.__)("Shipping rate for %1$s","google-listings-and-ads")}))},le=({countryOptions:e,value:t,onChange:n,onDelete:o})=>{const{countries:s,currency:r,rate:i}=t;return(0,a.createElement)("div",{className:"gla-shipping-rate-input-control"},(0,a.createElement)(K.A,{label:(0,a.createElement)("div",{className:"label"},(0,a.createElement)(se,{countries:s}),(0,a.createElement)(H.A,{button:(0,a.createElement)(c.A,{isTertiary:!0},(0,g.__)("Edit","google-listings-and-ads")),modal:(0,a.createElement)(te,{countryOptions:e,initialValues:t,onSubmit:n,onDelete:o})})),suffix:r,value:i,onBlur:(e,a)=>{i!==a&&n({...t,rate:a})}}),0===i&&(0,a.createElement)("div",{className:"gla-input-pill-div"},(0,a.createElement)(l.Pill,null,(0,g.__)("Free shipping for all orders","google-listings-and-ads"))))},re={options:{}};function ie({audienceCountries:e,value:t,helper:n,onChange:o}){const{code:s}=(0,Q.A)(),{handleAddSubmit:l,getChangeHandler:r,getDeleteHandler:i}=(({value:e,onChange:t})=>({handleAddSubmit:({countries:n,currency:a,rate:o})=>{const s=n.map((e=>({...re,country:e,currency:a,rate:o})));t(e.concat(s))},getChangeHandler:n=>a=>{const o=e.filter((e=>!(n.countries.includes(e.country)&&!a.countries.includes(e.country))));a.countries.forEach((e=>{const t=o.findIndex((t=>t.country===e)),n=o[t],s={...re,...n,country:e,currency:a.currency,rate:a.rate};_(s)||(s.options.free_shipping_threshold=void 0),t>=0?o[t]=s:o.push(s)})),t(o)},getDeleteHandler:n=>()=>{const a=e.filter((e=>!n.countries.includes(e.country)));t(a)}}))({value:t,onChange:o});return(0,a.createElement)(A.A.Card,null,(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(A.A.Card.Title,null,(0,g.__)("Estimated shipping rates","google-listings-and-ads")),(0,a.createElement)(j.A,{size:"large"},(()=>{const n=(e=>{const t=new Map;return e.forEach((e=>{const{country:n,currency:a,rate:o}=e,s=`${a} ${o} `,l=t.get(s)||{countries:[],currency:a,rate:o};l.countries.push(n),t.set(s,l)})),Array.from(t.values())})(t);if(0===n.length){const t={countries:e,currency:s,rate:void 0};return(0,a.createElement)(le,{countryOptions:e,value:t,onChange:r(t),onDelete:i(t)})}const o=e.filter((e=>!t.some((t=>t.country===e))));return(0,a.createElement)(a.Fragment,null,n.map((t=>(0,a.createElement)(le,{key:t.countries.join("-"),countryOptions:e,value:t,onChange:r(t),onDelete:i(t)}))),o.length>=1&&(0,a.createElement)("div",null,(0,a.createElement)(H.A,{button:(0,a.createElement)(c.A,{isSecondary:!0,icon:(0,a.createElement)(Y.A,null)},(0,g.__)("Add another rate","google-listings-and-ads")),modal:(0,a.createElement)(ee,{countryOptions:o,initialValues:{countries:o,currency:s,rate:0},onSubmit:l})})))})()),n))}const ce=()=>{const{adapter:e}=(0,d.h5)(),t=(0,d.Gl)("shipping_country_rates");return(0,a.createElement)(ie,{audienceCountries:e.audienceCountries,...t})};var de=n(873),ue=n(3027);const ge=()=>{const{getInputProps:e,values:t}=(0,d.h5)(),{settings:n}=(0,de.A)(),{hasFinishedResolution:s,data:l}=(0,ue.A)(),r=e("shipping_rate"),i=!n?.shipping_rates_count&&s&&"incomplete"===l?.status;return(0,a.createElement)(A.A,{title:(0,g.__)("Shipping rates","google-listings-and-ads"),description:(0,a.createElement)("div",null,(0,a.createElement)("p",null,(0,g.__)("Your estimated shipping rates and times will be shown to potential customers on Google.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,a.createElement)(z.A,{context:"setup-mc-shipping",linkId:"shipping-read-more",href:"https://support.google.com/merchants/answer/7050921"},(0,g.__)("Read more","google-listings-and-ads"))))},(0,a.createElement)(A.A.Card,null,(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(j.A,{size:"large"},!i&&(0,a.createElement)(f.A,{...r,label:(0,g.__)("Automatically sync my store’s shipping settings to Google.","google-listings-and-ads"),value:"automatic",collapsible:!0},(0,a.createElement)(v.A,null,(0,g.__)("My current settings and any future changes to my store’s shipping rates and classes will be automatically synced to Google Merchant Center.","google-listings-and-ads"))),(0,a.createElement)(f.A,{...r,label:(0,g.__)("My shipping settings are simple. I can manually estimate flat shipping rates.","google-listings-and-ads"),value:"flat",collapsible:!0}),(0,a.createElement)(f.A,{...r,label:(0,g.__)("My shipping settings are complex. I will enter my shipping rates and times manually in Google Merchant Center.","google-listings-and-ads"),value:"manual",collapsible:!0},(0,a.createElement)(v.A,null,(0,o.createInterpolateElement)((0,g.__)("I understand that if I don’t set this up manually in Google Merchant Center, my products will be disapproved by Google.","google-listings-and-ads"),{link:(0,a.createElement)(z.A,{context:"setup-mc-shipping",linkId:"shipping-manual",href:"https://www.google.com/retail/solutions/merchant-center/"})})))))),"flat"===t.shipping_rate&&(0,a.createElement)(ce,null))},me=e=>{const t={};return 0===e.countries.length&&(t.countries=(0,g.__)("Please specify at least one country.","google-listings-and-ads")),Number.isInteger(e.time)||(t.time=(0,g.__)("Please enter the estimated shipping time.","google-listings-and-ads")),(e.time<0||e.maxTime<0)&&(t.time=(0,g.__)("The estimated shipping time cannot be less than 0.","google-listings-and-ads")),e.time>e.maxTime&&(t.time=(0,g.__)("The minimum shipping time must not be more than the maximum shipping time.","google-listings-and-ads")),t};var pe=n(5095),he=n(4596),_e=n(6499);const Ee=({step:e=1,min:t=0,max:n=250,time:l,handleBlur:r,handleIncrement:i,field:c="time"})=>{const[d,u]=(0,o.useState)(l),m=(0,o.useRef)(null);function p(e){const a=parseFloat(d||0)+e;a>=t&&a<=n&&(u(a),m.current&&clearTimeout(m.current),m.current=setTimeout((()=>{i(a,c)}),600))}return(0,o.useEffect)((()=>{u(0===l?"":l)}),[l]),(0,a.createElement)(_e.A,{step:e,placeholder:null===l?"":(0,g.__)("Same Day","google-listings-and-ads"),suffix:(0,a.createElement)(a.Fragment,null,parseInt(d,10)>=1&&(0,a.createElement)("span",{className:"gla-countries-time-suffix"},(0,g._n)("day","days",parseInt(d,10),"google-listings-and-ads")),(0,a.createElement)(a.Fragment,null,(0,a.createElement)(s.Button,{className:"woocommerce-number-control__increment",icon:pe.A,size:"small",onMouseDown:()=>p(e),"aria-label":(0,g.__)("Increment","google-listings-and-ads"),tabIndex:-1}),(0,a.createElement)(s.Button,{icon:he.A,className:"woocommerce-number-control__decrement",size:"small",onMouseDown:()=>p(-e),"aria-label":(0,g.__)("Decrement","google-listings-and-ads"),tabIndex:-1}))),value:d,onBlur:(e,a)=>{a>=t&&a<=n&&r(a,c)},className:"gla-countries-time-stepper"})},fe=({handleBlur:e,handleIncrement:t,time:n,maxTime:o})=>(0,a.createElement)(s.Flex,{justify:"space-between",gap:"4"},(0,a.createElement)(s.FlexItem,null,(0,a.createElement)("div",{className:"gla-countries-time-input"},(0,a.createElement)(Ee,{handleBlur:e,time:n,handleIncrement:t,field:"time"}))),(0,a.createElement)(s.FlexItem,null,(0,a.createElement)("span",null,(0,g.__)("to","google-listings-and-ads"))),(0,a.createElement)(s.FlexItem,null,(0,a.createElement)("div",{className:"gla-countries-time-input"},(0,a.createElement)(Ee,{handleBlur:e,handleIncrement:t,time:o,field:"maxTime"})))),Ae=({countries:e,onRequestClose:t,onSubmit:n})=>{const[r,i]=(0,o.useState)(!1);return(0,a.createElement)(l.Form,{initialValues:{countries:e,time:0,maxTime:0},validate:me,onSubmit:e=>{n(e),t()}},(n=>{const{getInputProps:o,isValidForm:l,handleSubmit:d,errors:u}=n,m=(e,t)=>{o(t).onChange(e)};return(0,a.createElement)(Z.A,{overflow:"visible",shouldCloseOnEsc:!r,shouldCloseOnClickOutside:!r,title:(0,g.__)("Estimate shipping time","google-listings-and-ads"),buttons:[(0,a.createElement)(c.A,{key:"save",isPrimary:!0,disabled:!l,onClick:d},(0,g.__)("Add shipping time","google-listings-and-ads"))],onRequestClose:t},(0,a.createElement)(j.A,null,(0,a.createElement)(V,{label:(0,g.__)("If customer is in","google-listings-and-ads"),countryCodes:e,onDropdownVisibilityChange:i,...o("countries")}),(0,a.createElement)("div",{className:"label"},(0,g.__)("Then the estimated shipping times displayed in the product listing are:","google-listings-and-ads")),(0,a.createElement)(s.Flex,{direction:"column",className:"gla-countries-time-input-container"},(0,a.createElement)(s.FlexItem,null,(0,a.createElement)(fe,{time:o("time").value,maxTime:o("maxTime").value,handleBlur:m,handleIncrement:m}),(0,a.createElement)("ul",{className:"gla-validation-errors"},(0,a.createElement)("li",null,u.time))))))}))},ye=e=>{const[t,n]=(0,o.useState)(!1);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(c.A,{isSecondary:!0,icon:(0,a.createElement)(Y.A,null),onClick:()=>{n(!0)}},(0,g.__)("Add another time","google-listings-and-ads")),t&&(0,a.createElement)(Ae,{onRequestClose:()=>{n(!1)},...e}))},ve=e=>{const{countries:t}=e;return(0,a.createElement)("div",null,(0,a.createElement)(oe,{countries:t,textWithMore: // translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. // translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. (0,g.__)("Shipping time for %1$s + %2$d more","google-listings-and-ads"),textWithoutMore: // translators: 1: list of country names separated by comma. // translators: 1: list of country names separated by comma. (0,g.__)("Shipping time for %1$s","google-listings-and-ads")}))},be=({audienceCountries:e,time:t,onDelete:n,onSubmit:r,onRequestClose:i})=>{const[d,u]=(0,o.useState)(!1),m=Array.from(new Set([...t.countries,...e])),p=()=>{n(t.countries)};return(0,a.createElement)(l.Form,{initialValues:{countries:t.countries,time:t.time,maxTime:t.maxTime},validate:me,onSubmit:e=>{const n=new Set(e.countries),a=t.countries.filter((e=>!n.has(e)));r(e,a)}},(e=>{const{getInputProps:t,isValidForm:n,handleSubmit:o,errors:l}=e,r=(e,n)=>{t(n).onChange(e)};return(0,a.createElement)(Z.A,{overflow:"visible",shouldCloseOnEsc:!d,shouldCloseOnClickOutside:!d,title:(0,g.__)("Estimate shipping time","google-listings-and-ads"),buttons:[(0,a.createElement)(c.A,{key:"delete",isTertiary:!0,isDestructive:!0,onClick:p},(0,g.__)("Delete","google-listings-and-ads")),(0,a.createElement)(c.A,{key:"save",isPrimary:!0,disabled:!n,onClick:o},(0,g.__)("Update shipping time","google-listings-and-ads"))],onRequestClose:i},(0,a.createElement)(j.A,null,(0,a.createElement)(V,{label:(0,g.__)("If customer is in","google-listings-and-ads"),countryCodes:m,onDropdownVisibilityChange:u,...t("countries")}),(0,a.createElement)("div",{className:"label"},(0,g.__)("Then the estimated shipping times displayed in the product listing are","google-listings-and-ads")),(0,a.createElement)(s.Flex,{direction:"column",className:"gla-countries-time-input-container"},(0,a.createElement)(s.FlexItem,null,(0,a.createElement)(fe,{time:t("time").value,maxTime:t("maxTime").value,handleBlur:r,handleIncrement:r}),(0,a.createElement)("ul",{className:"gla-validation-errors"},(0,a.createElement)("li",null,l.time))))))}))},Ce=({audienceCountries:e,time:t,onChange:n,onDelete:s})=>{const[l,r]=(0,o.useState)(!1);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(c.A,{className:"gla-edit-time-button",isTertiary:!0,onClick:()=>{r(!0)}},(0,g.__)("Edit","google-listings-and-ads")),l&&(0,a.createElement)(be,{audienceCountries:e,time:t,onSubmit:(...e)=>{n(...e),r(!1)},onDelete:e=>{s(e),r(!1)},onRequestClose:()=>{r(!1)}}))},ke=({value:e,audienceCountries:t,onChange:n,onDelete:o})=>{const{countries:l,time:r,maxTime:c}=e;return t?(0,a.createElement)(s.Flex,{direction:"column",className:"gla-countries-time-input-container"},(0,a.createElement)(s.FlexItem,null,(0,a.createElement)("div",{className:"label"},(0,a.createElement)(ve,{countries:l}),(0,a.createElement)(Ce,{audienceCountries:t,onChange:n,onDelete:o,time:e}))),(0,a.createElement)(s.FlexItem,null,(0,a.createElement)(fe,{time:r,maxTime:c,handleBlur:(t,a)=>{e[a]!==t&&n({...e,[a]:t})},handleIncrement:(t,a)=>{n({...e,[a]:t})}}))):(0,a.createElement)(i.A,null)};var we=n(172);function Ne({value:e,audienceCountries:t,onChange:n}){const o=e.length,s=new Map(e.map((e=>[e.countryCode,e]))),l=t.filter((e=>!s.has(e))),r=l.length,i=(0,we.A)(e);function c(t){n(e.filter((e=>!t.includes(e.countryCode))))}function d({countries:e,time:t,maxTime:a},o=[]){o.forEach((e=>s.delete(e))),e.forEach((e=>{s.set(e,{countryCode:e,time:t,maxTime:a})})),n(Array.from(s.values()))}return 0===i.length&&i.push({countries:t,time:null,maxTime:null}),(0,a.createElement)("div",{className:"countries-time"},(0,a.createElement)(j.A,null,i.map((e=>(0,a.createElement)("div",{key:e.countries.join("-"),className:"countries-time-input-form"},(0,a.createElement)(ke,{value:e,audienceCountries:t,onChange:d,onDelete:c})))),o>=1&&r>=1&&(0,a.createElement)("div",{className:"add-time-button"},(0,a.createElement)(ye,{countries:l,onSubmit:function({countries:t,time:a,maxTime:o}){const s=t.map((e=>({countryCode:e,time:a,maxTime:o})));n(e.concat(s))}}))))}const Se=()=>{const{getInputProps:e,adapter:{audienceCountries:t,renderRequestedValidation:n}}=(0,d.h5)();return t?(0,a.createElement)(A.A.Card,null,(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(A.A.Card.Title,null,(0,g.__)("Estimated shipping times","google-listings-and-ads")),(0,a.createElement)(Ne,{...e("shipping_country_times"),audienceCountries:t}),n("shipping_country_times"))):(0,a.createElement)(i.A,null)},xe=()=>(0,a.createElement)(A.A,{title:(0,g.__)("Shipping times","google-listings-and-ads"),description:(0,a.createElement)("div",null,(0,a.createElement)("p",null,(0,g.__)("Your shipping times will be shown to potential customers on Google.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,a.createElement)(z.A,{context:"setup-mc-shipping",linkId:"shipping-read-more",href:"https://support.google.com/merchants/answer/7050921"},(0,g.__)("Read more","google-listings-and-ads"))))},(0,a.createElement)(Se,null)),Re=({value:e,onChange:t})=>(0,a.createElement)(s.CheckboxControl,{label:(0,g.__)("Free shipping over a specific order value","google-listings-and-ads"),checked:e,onChange:t}),Te=e=>{const t={};return 0===e.countries.length&&(t.countries=(0,g.__)("Please specify at least one country.","google-listings-and-ads")),e.threshold>0||(t.threshold=(0,g.__)("The minimum order amount must be greater than 0.","google-listings-and-ads")),t},Ie=({countryOptions:e,renderButtons:t,initialValues:n,onSubmit:s,onRequestClose:r})=>{const[i,c]=(0,o.useState)(!1);return(0,a.createElement)(l.Form,{initialValues:n,validate:Te,onSubmit:s},(n=>{const{getInputProps:o,values:s,setValue:l}=n;return(0,a.createElement)(Z.A,{overflow:"visible",shouldCloseOnEsc:!i,shouldCloseOnClickOutside:!i,title:(0,g.__)("Minimum order to qualify for free shipping","google-listings-and-ads"),buttons:t(n),onRequestClose:r},(0,a.createElement)(j.A,null,(0,a.createElement)(V,{label:(0,g.__)("If customer is in","google-listings-and-ads"),countryCodes:e,onDropdownVisibilityChange:c,...o("countries")}),(0,a.createElement)(K.A,{label:(0,g.__)("Then they qualify for free shipping if their order is over","google-listings-and-ads"),suffix:s.currency,...o("threshold"),onBlur:(e,t)=>{o("threshold").onBlur(e),l("threshold",t>0?t:void 0)}})))}))},Ge=({countryOptions:e,initialValues:t,onSubmit:n,onRequestClose:o})=>(0,a.createElement)(Ie,{countryOptions:e,initialValues:t,renderButtons:e=>{const{isValidForm:t,handleSubmit:n}=e;return[(0,a.createElement)(c.A,{key:"save",isPrimary:!0,disabled:!t,onClick:()=>{o(),n()}},(0,g.__)("Add minimum order","google-listings-and-ads"))]},onSubmit:n,onRequestClose:o}),Fe=({countryOptions:e,initialValues:t,onSubmit:n,onRequestClose:o,onDelete:s})=>{const l=()=>{o(),s()};return(0,a.createElement)(Ie,{countryOptions:e,initialValues:t,renderButtons:e=>{const{isValidForm:t,handleSubmit:n}=e;return[(0,a.createElement)(c.A,{key:"delete",isTertiary:!0,isDestructive:!0,onClick:l},(0,g.__)("Delete","google-listings-and-ads")),(0,a.createElement)(c.A,{key:"save",isPrimary:!0,disabled:!t,onClick:()=>{o(),n()}},(0,g.__)("Update minimum order","google-listings-and-ads"))]},onSubmit:n,onRequestClose:o})},Pe=e=>{const{countries:t}=e;return(0,a.createElement)("div",null,(0,a.createElement)(oe,{countries:t,textWithMore: // translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. // translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. (0,g.__)("Minimum order for %1$s + %2$d more","google-listings-and-ads"),textWithoutMore: // translators: 1: list of country names separated by comma. // translators: 1: list of country names separated by comma. (0,g.__)("Minimum order for %1$s","google-listings-and-ads")}))},Me=e=>{const{countryOptions:t,value:n,onChange:o,onDelete:s}=e,{countries:l,threshold:r,currency:i}=n,{values:u}=(0,d.h5)();return u.offer_free_shipping?(0,a.createElement)(K.A,{className:"gla-minimum-order-input-control",label:(0,a.createElement)("div",{className:"gla-minimum-order-input-control__label"},(0,a.createElement)("div",{className:"gla-minimum-order-input-control__label_country"},(0,a.createElement)(Pe,{countries:l}),(0,a.createElement)(H.A,{button:(0,a.createElement)(c.A,{isTertiary:!0},(0,g.__)("Edit","google-listings-and-ads")),modal:(0,a.createElement)(Fe,{countryOptions:t,initialValues:n,onSubmit:o,onDelete:s})})),(0,a.createElement)(a.Fragment,null,`Cost (${i})`)),value:r,onBlur:(e,t)=>{t!==n.threshold&&o({countries:l,threshold:t>0?t:void 0,currency:i})}}):null},Oe=(e,t,n)=>e.map((e=>{const a={...e,options:{...e.options}};return n?.countries.includes(a.country)?a.options.free_shipping_threshold=n.threshold:t?.countries.includes(a.country)&&(a.options.free_shipping_threshold=void 0),a})),De=({value:e=[],helper:t,onChange:n})=>{const o=(0,d.Gl)("offer_free_shipping");return(0,a.createElement)(A.A.Card,{className:"gla-minimum-order-card"},(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(A.A.Card.Title,null,(0,g.__)("Only select if applicable","google-listings-and-ads")),(0,a.createElement)(j.A,{size:"large"},(0,a.createElement)(Re,{...o}),(()=>{const t=e.filter(_),o=(e=>{const t=new Map;return e.forEach((e=>{const{options:{free_shipping_threshold:n},currency:a}=e,o=`${n} ${a}`,s=t.get(o)||{countries:[],threshold:n,currency:a};s.countries.push(e.country),t.set(o,s)})),Array.from(t.values())})(t),s=t.map((e=>e.country)),l=t=>a=>{n(Oe(e,t,a))},r=t=>()=>{n(Oe(e,t))};if(1===o.length)return(0,a.createElement)(Me,{countryOptions:s,value:o[0],onChange:l(o[0]),onDelete:r(o[0])});const i=o.filter((e=>void 0!==e.threshold)),d=o.find((e=>void 0===e.threshold));return(0,a.createElement)(a.Fragment,null,i.map((e=>(0,a.createElement)(Me,{key:e.countries.join("-"),countryOptions:s,value:e,onChange:l(e),onDelete:r(e)}))),d&&(0,a.createElement)("div",null,(0,a.createElement)(H.A,{button:(0,a.createElement)(c.A,{isSecondary:!0,icon:(0,a.createElement)(Y.A,null)},(0,g.__)("Add another condition","google-listings-and-ads")),modal:(0,a.createElement)(Ge,{countryOptions:d.countries,initialValues:d,onSubmit:t=>{n(Oe(e,null,t))}})})))})()),t))},Ue=()=>{const e=(0,d.Gl)("shipping_country_rates","free_shipping_threshold");return(0,a.createElement)(A.A,{title:(0,g.__)("Order value condition","google-listings-and-ads"),description:(0,a.createElement)("div",null,(0,a.createElement)("p",null," ",(0,g.__)("Optional","google-listings-and-ads")," "))},(0,a.createElement)(De,{...e}))},Le=()=>{const{values:e}=(0,d.h5)(),t="flat"===e.shipping_time,n="flat"===e.shipping_rate&&e.shipping_country_rates.some(_);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(W,null),(0,a.createElement)(ge,null),n&&(0,a.createElement)(Ue,null),t&&(0,a.createElement)(xe,null))},qe=["locale","language","location","countries"],Be=["shipping_rate","shipping_time"],$e=()=>!0,{Fill:Ve,Slot:je}=(0,s.createSlotFill)("gla/SetupFreeListings/SubmitButton"),We=({targetAudience:e,resolveFinalCountries:t,onTargetAudienceChange:n=r.noop,settings:s,onSettingsChange:l=r.noop,shippingRates:f,onShippingRatesChange:A=r.noop,shippingTimes:y,onShippingTimesChange:v=r.noop,onRequestSubmit:b=$e,onContinue:C=r.noop,submitLabel:k})=>{const w=(0,o.useRef)();if(!(e&&s&&f&&y))return(0,a.createElement)(i.A,null);const N=e=>{const n=t(e),{shipping_country_times:a}=e;return((e,t,n)=>{const a={};return m.has(e.location)||(a.location=(0,g.__)("Please select a location option.","google-listings-and-ads")),"selected"===e.location&&0===e.countries.length&&(a.countries=(0,g.__)("Please select at least one country.","google-listings-and-ads")),p.has(e.shipping_rate)||(a.shipping_rate=(0,g.__)("Please select a shipping rate option.","google-listings-and-ads")),"flat"===e.shipping_rate&&(e.shipping_country_rates.lengthe.rate<0)))&&(a.shipping_country_rates=(0,g.__)("Please specify estimated shipping rates for all the countries, and the rate cannot be less than 0.","google-listings-and-ads")),"flat"===e.shipping_rate&&!0===e.offer_free_shipping&&e.shipping_country_rates.every((e=>void 0===e.options.free_shipping_threshold))&&(a.free_shipping_threshold=(0,g.__)("Please enter minimum order for free shipping.","google-listings-and-ads")),h.has(e.shipping_time)||(a.shipping_time=(0,g.__)("Please select a shipping time option.","google-listings-and-ads")),"flat"===e.shipping_time&&(t.lengthe.time<0||e.maxTime<0)))&&(a.shipping_country_times=(0,g.__)("Please specify estimated shipping times for all the countries, and the time cannot be less than 0.","google-listings-and-ads")),"flat"===e.shipping_time&&t.some((e=>e.time>e.maxTime))&&(a.shipping_country_times=(0,g.__)("The minimum shipping time must not be more than the maximum shipping time.","google-listings-and-ads")),a})(e,a,n)};return(0,a.createElement)("div",{className:"gla-setup-free-listings"},(0,a.createElement)(d.Ay,{ref:w,initialValues:{locale:e.locale,language:e.language,location:e.location,countries:e.countries||[],shipping_rate:s.shipping_rate,shipping_time:s.shipping_time,offer_free_shipping:E(f),shipping_country_rates:f,shipping_country_times:y},extendAdapter:e=>({audienceCountries:t(e.values),renderRequestedValidation:t=>e.adapter.requestedShowValidation?(0,a.createElement)(u.A,{messages:e.errors[t]}):null}),onChange:(e,a)=>{const{setValue:o}=w.current;if("shipping_country_rates"===e.name)A(a.shipping_country_rates),e.value.some(_)||o("offer_free_shipping",void 0);else if("offer_free_shipping"===e.name){if(!1===e.value){const e=a.shipping_country_rates.map((e=>({...e,options:{...e.options,free_shipping_threshold:void 0}})));o("shipping_country_rates",e)}}else if("shipping_country_times"===e.name){const t=!N(a).hasOwnProperty(e.name);!a.shipping_country_times.some((e=>null===e.time||null===e.maxTime))&&t&&v(a.shipping_country_times)}else if(Be.includes(e.name)){let t=!0;if("shipping_rate"===e.name){const n="manual"===e.value?"manual":"flat";n!==a.shipping_time&&(t=!1,o("shipping_time",n))}t&&l((e=>(0,r.pick)(e,Be))(a))}else qe.includes(e.name)&&(n((0,r.pick)(a,qe)),["shipping_country_rates","shipping_country_times"].forEach((e=>{const n=t(a),s=a[e],l=s.filter((e=>n.includes(e.country||e.countryCode)));l.length!==s.length&&o(e,l)})))},validate:N,onSubmit:C},(e=>{const{isValidForm:t,handleSubmit:n,adapter:o}=e;return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(Le,null),(0,a.createElement)(Ve,null,(0,a.createElement)(c.A,{isPrimary:!0,loading:o.isSubmitting,onClick:async e=>{if(t){if(!await b())return;return n(e)}o.showValidation()}},k)))})))};We.SubmitButton=je;const ze=We},8678:(e,t,n)=>{n.d(t,{Az:()=>b,mU:()=>g,dR:()=>_,w5:()=>v,Ay:()=>k,TQ:()=>h});var a=n(1609),o=n(8e3),s=n(3741),l=n(559),r=n(7723),i=n(6087),c=n(3905),d=n(7892),u=n(1177);const g=(0,a.createElement)(u.A,{context:"setup-mc-accounts",linkId:"required-google-permissions",href:"https://woocommerce.com/document/google-for-woocommerce/get-started/setup-and-configuration/#required-google-permissions"});var m=n(5640),p=n(1830);const h=(e,t)=>{const{createNotice:n}=(0,m.A)(),[a,o]=(0,p.A)(e,t);return[async()=>{try{const{url:e}=await a();window.location.href=e}catch(e){n("error",(0,r.__)("Unable to connect your Google account. Please try again later.","google-listings-and-ads"))}},o]},_=({additionalScopeEmail:e})=>{const t=c.Th.mcSetupComplete?"reconnect":"setup-mc",[n,{loading:o,data:s}]=h(t,e);return(0,a.createElement)(l.A,{appearance:l.x.GOOGLE,alignIcon:"top",description:(0,a.createElement)(a.Fragment,null,e,(0,a.createElement)("p",null,(0,a.createElement)("em",null,(0,i.createInterpolateElement)((0,r.__)("Uh-oh! You did not allow WooCommerce sufficient access to your Google account. You must allow all required permissions in the Google authorization page to proceed. Read more","google-listings-and-ads"),{alert:(0,a.createElement)("span",{className:"gla-authorize-google-account-card__error-text"}),link:g})))),alignIndicator:"top",indicator:(0,a.createElement)(d.A,{isSecondary:!0,isDestructive:!0,loading:o||s,eventName:"gla_google_account_connect_button_click",eventProps:{context:t,action:"scope"},text:(0,r.__)("Allow full access","google-listings-and-ads"),onClick:n})})};var E=n(4566),f=n(8242),A=n(6520),y=n(6599);const v=({text:e=(0,r.__)("Or, connect to a different Google account","google-listings-and-ads"),...t})=>{const[n,{loading:o}]=(()=>{const{createNotice:e,removeNotice:t}=(0,m.A)(),[n,{loading:a}]=(0,y.A)({path:`${A.RV}/mc/connection`,method:"DELETE"}),[o,{loading:s}]=(0,y.A)({path:`${A.RV}/ads/connection`,method:"DELETE"}),[l,{loading:i}]=(0,y.A)({path:`${A.RV}/google/connect`,method:"DELETE"}),[c,{loading:d,data:u}]=(0,p.A)("setup-mc");return[async()=>{const{notice:a}=await e("info",(0,r.__)("Connecting to a different Google account, please wait…","google-listings-and-ads"));try{await n(),await o(),await l();const{url:e}=await c();window.location.href=e}catch(n){t(a.id),e("error",(0,r.__)("Unable to connect to a different Google account. Please try again later.","google-listings-and-ads"))}},{loading:a||s||i||d||u}]})();return(0,a.createElement)(d.A,{isLink:!0,disabled:o,text:e,eventName:"gla_google_account_connect_different_account_button_click",onClick:n,...t})},b=({googleAccount:e,helper:t,hideAccountSwitch:n=!1})=>(0,a.createElement)(l.A,{appearance:l.x.GOOGLE,description:e.email,helper:t,indicator:(0,a.createElement)(E.A,null)},!n&&(0,a.createElement)(f.A.Card.Footer,null,(0,a.createElement)(v,null))),C=({disabled:e})=>{const t=c.Th.mcSetupComplete?"reconnect":"setup-mc",[n,{loading:o,data:s}]=h(t);return(0,a.createElement)(l.A,{appearance:l.x.GOOGLE,disabled:e,alignIcon:"top",description:(0,a.createElement)(a.Fragment,null,(0,r.__)("Required to sync with Google Merchant Center and Google Ads.","google-listings-and-ads"),(0,a.createElement)("p",null,(0,a.createElement)("em",null,(0,i.createInterpolateElement)((0,r.__)("You will be prompted to give WooCommerce access to your Google account. Please check all the checkboxes to give WooCommerce all required permissions. Read more","google-listings-and-ads"),{link:g})))),alignIndicator:"top",indicator:(0,a.createElement)(d.A,{isSecondary:!0,disabled:e,loading:o||s,eventName:"gla_google_account_connect_button_click",eventProps:{context:t,action:"authorization"},text:(0,r.__)("Connect","google-listings-and-ads"),onClick:n})})};function k({disabled:e=!1}){const{google:t,scope:n,hasFinishedResolution:r}=(0,o.A)();if(!r)return(0,a.createElement)(l.A,{description:(0,a.createElement)(s.A,null)});const i="yes"===t?.active;return i&&n.glaRequired?(0,a.createElement)(b,{googleAccount:t}):i&&!n.glaRequired?(0,a.createElement)(_,{additionalScopeEmail:t.email}):(0,a.createElement)(C,{disabled:e})}},458:(e,t,n)=>{n.d(t,{My:()=>w,AV:()=>P,Ez:()=>y,iZ:()=>A,Ay:()=>Y});var a=n(1609),o=n(6427),s=n(7723),l=n(3905),r=n(6028),i=n(8e3),c=n(1378),d=n(559),u=n(4566),g=n(8242),m=n(8468),p=n(6087),h=n(3658),_=n(7892),E=n(7541),f=n(6473);const A=({onDisconnected:e=m.noop})=>{const{disconnectGoogleAdsAccount:t}=(0,h.j)(),[n,o]=(0,p.useState)(!1),l=(0,E.A)(f.T1);return(0,a.createElement)(_.A,{isTertiary:!0,loading:n,text:(0,s.__)("Or, connect to a different Google Ads account","google-listings-and-ads"),eventName:"gla_ads_account_disconnect_button_click",eventProps:l(),onClick:()=>{o(!0),t(!0).then((()=>e())).catch((()=>o(!1)))}})};function y({googleAdsAccount:e,hideAccountSwitch:t=!1,children:n,...l}){return(0,a.createElement)(d.A,{appearance:d.x.GOOGLE_ADS,description:(0,a.createElement)(o.ExternalLink,{href:"https://ads.google.com/aw/overview"},(r=e.id,(0,s.sprintf)((0,s.__)("Account %s","google-listings-and-ads"),r))),indicator:(0,a.createElement)(u.A,null),...l},n,!t&&(0,a.createElement)(g.A.Card.Footer,null,(0,a.createElement)(A,null)));var r}var v=n(5559);const b=()=>{const{fetchGoogleAdsAccountStatus:e}=(0,h.j)();return(0,v.A)(e,30),(0,a.createElement)(p.Fragment,null,(0,a.createElement)("p",{className:"gla-ads-claim-account-notice"},(0,s.__)("Claim your new Google Ads account to complete this setup.","google-listings-and-ads")),(0,a.createElement)(g.A.Card.Footer,null,(0,a.createElement)(A,null)))};var C=n(4297),k=n(5992);const w=({onClick:e=m.noop,...t})=>{const{inviteLink:n}=(0,k.A)(),o=(0,E.A)(f.T1);return n?(0,a.createElement)(_.A,{...t,eventName:"gla_open_ads_account_claim_invitation_button_click",eventProps:o(),onClick:t=>{const{defaultView:a}=t.target.ownerDocument,o=(0,C.A)(a,600,800);a.open(n,"_blank",o),e(t)}}):null};var N=n(9457);const S=({onRequestClose:e})=>{const{hasAccess:t}=(0,k.A)();return(0,p.useEffect)((()=>{t&&e()}),[e,t]),(0,a.createElement)(N.A,{className:"gla-ads-invite-modal",title:(0,s.__)("Claim your Google Ads account","google-listings-and-ads"),buttons:[(0,a.createElement)(w,{key:"1",isPrimary:!0,onClick:e},(0,s.__)("Claim account in Google Ads","google-listings-and-ads"))],onRequestClose:e},(0,a.createElement)("p",null,(0,s.__)("Claiming your account lets you access Google Ads and sets up conversion measurement. You must claim your account in the next 20 days.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,s.__)("When you claim your account, you’ll be asked to set up billing. This step is optional and you only need to complete it if you want to create Google Ads campaigns. If you don’t want to set up billing, close the window after you’ve clicked ‘Continue’ on the next page.","google-listings-and-ads")))};var x=n(1177);const R=({onCreateAccount:e=()=>{},onRequestClose:t=()=>{}})=>{const[n,l]=(0,p.useState)(!1),r=(0,E.A)(f.T1);return(0,a.createElement)(N.A,{className:"gla-ads-terms-modal",title:(0,s.__)("Create Google Ads Account","google-listings-and-ads"),buttons:[(0,a.createElement)(_.A,{key:"1",isPrimary:!0,disabled:!n,eventName:"gla_ads_account_create_button_click",eventProps:r(),onClick:()=>{e(),t()}},(0,s.__)("Create account","google-listings-and-ads"))],onRequestClose:t},(0,a.createElement)("p",{className:"main"},(0,s.__)("By creating a Google Ads account, you agree to the following terms and conditions:","google-listings-and-ads")),(0,a.createElement)("p",null,(0,p.createInterpolateElement)((0,s.__)("You agree to comply with Google’s terms and policies, including Shopping ads policies and Google Ads Terms and Conditions.","google-listings-and-ads"),{policylink:(0,a.createElement)(x.A,{context:"setup-ads",linkId:"shopping-ads-policies",href:"https://support.google.com/merchants/answer/6149970"}),termslink:(0,a.createElement)(x.A,{context:"setup-ads",linkId:"google-ads-terms-of-service",href:"https://support.google.com/adspolicy/answer/54818"})})),(0,a.createElement)(o.CheckboxControl,{label:(0,s.__)("I have read and accept these terms","google-listings-and-ads"),checked:n,onChange:l}))},T=({onCreateAccount:e})=>{const[t,n]=(0,p.useState)(!1);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(_.A,{isSecondary:!0,text:(0,s.__)("Create account","google-listings-and-ads"),onClick:()=>{n(!0)}}),t&&(0,a.createElement)(R,{onCreateAccount:e,onRequestClose:()=>{n(!1)}}))};var I=n(70);const G=e=>{const{allowShowExisting:t,onShowExisting:n}=e,[o,l]=(0,p.useState)(!1),{googleAdsAccount:r}=(0,c.A)(),{hasAccess:i,step:u}=(0,k.A)(),[m,{loading:h,action:E}]=(0,I.A)(),f=Boolean(r.id&&!1===i),A=async()=>{await m(),l(!0)};return(0,p.useEffect)((()=>{!0===i&&"conversion_action"===u&&m()}),[i,m,u]),(0,a.createElement)(d.A,{appearance:d.x.GOOGLE_ADS,alignIcon:"top",indicator:(()=>{if(h){const e="create"===E?(0,s.__)("Creating…","google-listings-and-ads"):(0,s.__)("Updating…","google-listings-and-ads");return(0,a.createElement)(_.A,{loading:!0,text:e})}return f?(0,a.createElement)(w,{isSecondary:!0},(0,s.__)("Claim Account","google-listings-and-ads")):(0,a.createElement)(T,{onCreateAccount:A})})()},t&&!f&&(0,a.createElement)(g.A.Card.Footer,null,(0,a.createElement)(_.A,{isLink:!0,disabled:h,onClick:n},(0,s.__)("Or, use your existing Google Ads account","google-listings-and-ads"))),f&&(0,a.createElement)(p.Fragment,null,o&&(0,a.createElement)(S,{onRequestClose:()=>{l(!1)}}),(0,a.createElement)(b,null)))};var F=n(5530);const P=({accountID:e,...t})=>{const n=(0,E.A)(f.T1);return(0,a.createElement)(_.A,{isSecondary:!0,disabled:!e,eventName:"gla_ads_account_connect_button_click",eventProps:n({id:Number(e)}),...t},(0,s.__)("Connect","google-listings-and-ads"))};var M=n(8463),O=n(8441),D=n(8771),U=n(6599),L=n(5640),q=n(2661);const B=e=>{const{accounts:t,onCreateNew:n=()=>{}}=e,[l,r]=(0,p.useState)(),[i,u]=(0,p.useState)(!1),[m]=(0,U.A)({path:"/wc/gla/ads/accounts",method:"POST",data:{id:l}}),{refetchGoogleAdsAccount:E}=(0,c.A)(),{createNotice:f}=(0,L.A)(),{fetchGoogleAdsAccountStatus:A}=(0,h.j)(),y=t.length>1;return(0,a.createElement)(d.A,{className:"gla-connect-ads",alignIcon:"top",appearance:d.x.GOOGLE_ADS},(0,a.createElement)(o.CardDivider,null),(0,a.createElement)(g.A.Card.Body,null,(0,a.createElement)(D.A.Title,null,(0,s.__)("Connect to an existing account","google-listings-and-ads")),y&&(0,a.createElement)(D.A.Body,null,(0,p.createInterpolateElement)((0,s.__)("If you manage multiple sub-accounts in Google Ads, please connect the relevant sub-account, not a manager account. Learn more","google-listings-and-ads"),{link:(0,a.createElement)(x.A,{context:"setup-ads-connect-account",linkId:"connect-sub-account",href:"https://support.google.com/google-ads/answer/6139186"})})),(0,a.createElement)(M.A,null,(0,a.createElement)(q.A,{value:l,onChange:r}),i?(0,a.createElement)(O.A,{text:(0,s.__)("Connecting…","google-listings-and-ads")}):(0,a.createElement)(P,{accountID:l,onClick:async()=>{if(l){u(!0);try{await m(),await A(),await E()}catch(e){u(!1),f("error",(0,s.__)("Unable to connect your Google Ads account. Please try again later.","google-listings-and-ads"))}}}}))),(0,a.createElement)(g.A.Card.Footer,null,(0,a.createElement)(_.A,{isTertiary:!0,disabled:i,onClick:n},(0,s.__)("Or, create a new Google Ads account","google-listings-and-ads"))))},$=()=>{const{existingAccounts:e}=(0,F.A)(),[t,n]=(0,p.useState)(!1),{googleAdsAccount:o}=(0,c.A)(),{hasAccess:s,step:l}=(0,k.A)();return e?0===e.length||t||o.id&&!0!==s||!0===s&&"conversion_action"===l?(0,a.createElement)(G,{allowShowExisting:t,onShowExisting:()=>{n(!1)}}):(0,a.createElement)(B,{accounts:e,onCreateNew:()=>{n(!0)}}):(0,a.createElement)(r.A,null)};var V=n(1830);const j=({additionalScopeEmail:e})=>{const t=l.Th.mcSetupComplete?"setup-ads":"setup-mc",{createNotice:n}=(0,L.A)(),[o,{loading:r,data:i}]=(0,V.A)(t,e);return(0,a.createElement)(d.A,{appearance:d.x.GOOGLE_ADS,alignIcon:"top",indicator:(0,a.createElement)(_.A,{isSecondary:!0,loading:r||i,onClick:()=>{o().then((({url:e})=>{window.location.href=e})).catch((()=>{n("error",(0,s.__)("Unable to get Google authorization page. Please try again later.","google-listings-and-ads"))}))},text:(0,s.__)("Allow full access","google-listings-and-ads"),eventName:"gla_google_account_connect_button_click",eventProps:{context:"setup-ads",action:"scope"}})})},W=()=>(0,a.createElement)(d.A,{disabled:!0,appearance:d.x.GOOGLE_ADS});var z=n(4391);function Y(){const{google:e,scope:t,hasFinishedResolution:n}=(0,i.A)(),{googleAdsAccount:d,hasFinishedResolution:u}=(0,c.A)(),{hasAccess:g,step:m,hasFinishedResolution:p}=(0,k.A)();if(!n||!u||!p||null===d)return(0,a.createElement)(r.A,null);if(!e||"no"===e.active)return(0,a.createElement)(W,null);if(!t.adsRequired)return(0,a.createElement)(j,{additionalScopeEmail:e.email});if(d.status===l.Wn.DISCONNECTED||!0!==g||!0===g&&"conversion_action"===m)return(0,a.createElement)($,null);const h=(0,z.A)(d);return(0,a.createElement)(y,{googleAdsAccount:d},h?(0,a.createElement)(o.Notice,{status:"success",isDismissible:!1},(0,s.__)("Conversion measurement has been set up. You can create a campaign later.","google-listings-and-ads")):null)}},1274:(e,t,n)=>{n.d(t,{A:()=>K});var a=n(1609),o=n(8e3),s=n(3741),l=n(559),r=n(8678),i=n(7723),c=n(6087),d=n(6427),u=n(3905),g=n(7892),m=n(1177);const p=({disabled:e})=>{const t=u.Th.mcSetupComplete?"reconnect":"setup-mc",[n,{loading:o,data:s}]=(0,r.TQ)(t),[p,h]=(0,c.useState)(!1);return(0,a.createElement)(l.A,{appearance:l.x.GOOGLE,disabled:e,alignIcon:"top",className:"gla-google-combo-service-account-card--google",description:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,i.__)("Required to sync with Google Merchant Center and Google Ads.","google-listings-and-ads")),(0,a.createElement)(d.CheckboxControl,{label:(0,c.createInterpolateElement)((0,i.__)("I accept the terms and conditions of Merchant Center and Google Ads","google-listings-and-ads"),{linkMerchant:(0,a.createElement)(m.A,{context:"setup-mc-accounts",linkId:"google-mc-terms-of-service",href:"https://support.google.com/merchants/answer/160173"}),linkAds:(0,a.createElement)(m.A,{context:"setup-ads",linkId:"google-ads-terms-of-service",href:"https://support.google.com/adspolicy/answer/54818"})}),checked:p,onChange:h,disabled:e})),helper:(0,c.createInterpolateElement)((0,i.__)("You will be prompted to give WooCommerce access to your Google account. Please check all the checkboxes to give WooCommerce all required permissions. Read more","google-listings-and-ads"),{link:r.mU}),alignIndicator:"top",indicator:(0,a.createElement)(g.A,{isSecondary:!0,disabled:e||!p,loading:o||s,eventName:"gla_google_account_connect_button_click",eventProps:{context:t,action:"authorization"},text:(0,i.__)("Connect","google-listings-and-ads"),onClick:n})})};var h=n(3658),_=n(9457),E=n(7792);const f=({onContinue:e,onRequestClose:t})=>(0,a.createElement)(_.A,{className:"gla-ads-warning-modal",title:(0,i.__)("Create Google Ads Account","google-listings-and-ads"),buttons:[(0,a.createElement)(g.A,{key:"confirm",isSecondary:!0,onClick:e},(0,i.__)("Yes, I want a new account","google-listings-and-ads")),(0,a.createElement)(g.A,{key:"cancel",isPrimary:!0,onClick:t},(0,i.__)("Cancel","google-listings-and-ads"))],onRequestClose:t},(0,a.createElement)("p",{className:"gla-ads-warning-modal__warning-text"},(0,a.createElement)(E.A,null),(0,a.createElement)("span",null,(0,i.__)("Are you sure you want to create a new Google Ads account?","google-listings-and-ads"))),(0,a.createElement)("p",null,(0,i.__)("You already have another Ads account associated with this Google account.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,i.__)("If you create a new Google Ads account, you will need to accept an invite to the account before it can be used.","google-listings-and-ads")));var A=n(458),y=n(5530),v=n(5992),b=n(1378);const C=({isConnected:e,onCreateNewClick:t,onDisconnected:n,disabled:o,...s})=>{const{existingAccounts:l}=(0,y.A)(),{googleAdsAccount:r}=(0,b.A)(),{hasAccess:c}=(0,v.A)(),d=Boolean(r?.id&&!1===c);if(e&&l.length>0)return(0,a.createElement)(A.iZ,{onDisconnected:n});const u=o||d&&!l.length;return(0,a.createElement)(g.A,{isTertiary:!0,onClick:t,disabled:u,...s},(0,i.__)("Or, create a new Google Ads account","google-listings-and-ads"))};var k=n(8441),w=n(6599),N=n(5640),S=n(1351),x=n(2661),R=n(4566);const T=({onCreateClick:e})=>{const[t,n]=(0,c.useState)(),[o,s]=(0,c.useState)(!1),{createNotice:r}=(0,N.A)(),{fetchGoogleAdsAccountStatus:d}=(0,h.j)(),u=(0,S.A)(),{googleAdsAccount:g,hasFinishedResolution:m,hasGoogleAdsConnection:p,refetchGoogleAdsAccount:_}=(0,b.A)(),[E]=(0,w.A)({path:"/wc/gla/ads/accounts",method:"POST",data:{id:t}});(0,c.useEffect)((()=>{p&&n(g.id)}),[g,p]);return(0,a.createElement)(l.A,{className:"gla-google-combo-account-card gla-google-combo-service-account-card--ads",title:(0,i.__)("Connect to existing Google Ads account","google-listings-and-ads"),helper:(0,i.__)("Required to set up conversion measurement for your store.","google-listings-and-ads"),alignIndicator:"toDetail",indicator:m?o?(0,a.createElement)(k.A,{text:(0,i.__)("Connecting…","google-listings-and-ads")}):u?(0,a.createElement)(R.A,null):(0,a.createElement)(A.AV,{disabled:p,accountID:t,onClick:async()=>{if(t){s(!0);try{await E(),await d(),await _()}catch(e){r("error",(0,i.__)("Unable to connect your Google Ads account. Please try again later.","google-listings-and-ads"))}finally{s(!1)}}}}):(0,a.createElement)(k.A,null),detail:(0,a.createElement)(x.A,{value:t,onChange:n,autoSelectFirstOption:!0,nonInteractive:p}),actions:(0,a.createElement)(C,{disabled:o,isConnected:p,onCreateNewClick:e,onDisconnected:()=>{n(void 0)}})})},I=({upsertingAction:e})=>{const t="update"===e;let n=(0,i.__)("Creating a new Google Ads account","google-listings-and-ads"),o=(0,i.__)("Creating…","google-listings-and-ads");return t&&(n=(0,i.__)("Connecting your Google Ads account","google-listings-and-ads"),o=(0,i.__)("Connecting…","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-google-combo-service-account-card--ads",title:n,helper:(0,i.__)("This may take a few moments, please wait…","google-listings-and-ads"),indicator:(0,a.createElement)(k.A,{text:o})})},G=({onRequestCreate:e,upsertingAction:t})=>{const[n,o]=(0,c.useState)(!1);if(t)return(0,a.createElement)(I,{upsertingAction:t});const s=()=>{o(!1)};return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(T,{onCreateClick:()=>{o(!0)}}),n&&(0,a.createElement)(f,{onContinue:()=>{e(),s()},onRequestClose:s}))};var F=n(7916);const P=()=>{const{google:e}=(0,o.A)(),{googleAdsAccount:t}=(0,b.A)(),{googleMCAccount:n,isReady:s}=(0,F.A)();return(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,e.email),(0,a.createElement)("p",null,s&&(0,i.sprintf)( // Translators: %s is the Merchant Center ID // Translators: %s is the Merchant Center ID (0,i.__)("Merchant Center ID: %s","google-listings-and-ads"),n.id)),(0,a.createElement)("p",null,t?.id>0&&(0,i.sprintf)( // Translators: %s is the Google Ads ID // Translators: %s is the Google Ads ID (0,i.__)("Google Ads ID: %s","google-listings-and-ads"),t.id)))};var M=n(7108),O=n(5559);const D=()=>{const{fetchGoogleAdsAccountStatus:e}=(0,h.j)();return(0,O.A)(e,30),(0,a.createElement)("div",{className:"gla-claim-ads-account-box"},(0,a.createElement)("h4",null,(0,i.__)("Claim your Google Ads account","google-listings-and-ads")),(0,a.createElement)("p",null,(0,i.__)("You need to accept the invitation to the Google Ads account we created for you. This gives you access to Google Ads and sets up conversion measurement. You must claim your account in the next 20 days.","google-listings-and-ads")),(0,a.createElement)("p",{className:"gla-ads-post-claim-instructions"},(0,i.__)("After accepting the invitation, you’ll be prompted to set up billing. We highly recommend doing this to avoid having to do it later on.","google-listings-and-ads")),(0,a.createElement)(A.My,{isPrimary:!0},(0,i.__)("Claim account in Google Ads","google-listings-and-ads"),(0,a.createElement)(d.Icon,{icon:M.A,size:20})))},U=({claimGoogleAdsAccount:e,showConversionMeasurementNotice:t})=>e||t?(0,a.createElement)("div",{className:"gla-connected-ads-account-detail"},e&&(0,a.createElement)(D,null),t&&(0,a.createElement)(d.Notice,{status:"success",isDismissible:!1},(0,i.__)("Google Ads conversion measurement has been set up for your store.","google-listings-and-ads"))):null,L=({showSpinner:e})=>{const t=(0,S.A)(),{isReady:n}=(0,F.A)();return e?(0,a.createElement)(k.A,{text:(0,i.__)("Creating…","google-listings-and-ads")}):t&&n?(0,a.createElement)(R.A,null):null},q="both",B="ads",$="mc";var V=n(6028),j=n(3354),W=n(2722),z=n(70);var Y=n(1666),H=n(4391);const Q=()=>{const[e,t]=(0,c.useState)(!1),[n,o]=(()=>{const{createNotice:e}=(0,N.A)(),{invalidateResolution:t}=(0,h.j)(),[n,a]=(0,w.A)({path:"/wc/gla/mc/accounts",method:"POST"});return[async()=>{try{await n({data:a.error?.id&&{id:a.error.id},parse:!1}),t("getGoogleMCAccount",[])}catch(t){if(![403,503].includes(t.status)){const n=(await t.json()).message||(0,i.__)("Unable to create Merchant Center account. Please try again later.","google-listings-and-ads");e("error",n)}}},a]})(),{data:s}=(0,W.A)(),{hasDetermined:d,creatingWhich:u}=(e=>{const t=(0,c.useRef)(!1),[n,a]=(0,c.useState)(null),[o,s]=(0,c.useState)(!1),l=(()=>{const{hasFinishedResolution:e,hasGoogleAdsConnection:t}=(0,b.A)(),{hasFinishedResolution:n,existingAccounts:a}=(0,y.A)();return e&&n?!t&&0===a?.length:null})(),r=(()=>{const{hasFinishedResolution:e,hasGoogleMCConnection:t}=(0,F.A)(),{hasFinishedResolution:n,data:a}=(0,W.A)();return e&&n?!t&&0===a?.length:null})(),[i]=(0,z.A)();return(0,c.useEffect)((()=>{if(null===r||null===l||t.current)return;let n=null;t.current=!0,r&&l?n=q:r?n=$:l&&(n=B),a(n),s(!0),n&&(async()=>{n===q?(await e(),await i()):n===$?await e():n===B&&await i(),a(null)})()}),[e,l,r,i]),{hasDetermined:o,creatingWhich:n}})(n),{text:m,subText:p}=(e=>{let t=null,n=null;switch(e){case q:t=(0,i.__)("You don’t have Merchant Center nor Google Ads accounts, so we’re creating them for you.","google-listings-and-ads"),n=(0,i.__)("Merchant Center is required to sync products so they show on Google. Google Ads is required to set up conversion measurement for your store.","google-listings-and-ads");break;case B:t=(0,i.__)("You don’t have Google Ads account, so we’re creating one for you.","google-listings-and-ads"),n=(0,i.__)("Required to set up conversion measurement for your store.","google-listings-and-ads");break;case $:t=(0,i.__)("You don’t have Merchant Center account, so we’re creating one for you.","google-listings-and-ads"),n=(0,i.__)("Required to sync products so they show on Google.","google-listings-and-ads")}return{text:t,subText:n}})(u),{existingAccounts:_}=(0,y.A)(),{isReady:E,hasGoogleMCConnection:f,hasFinishedResolution:A}=(0,F.A)(),{invalidateResolution:C}=(0,h.j)(),{googleAdsAccount:k,hasGoogleAdsConnection:S}=(0,b.A)(),{hasAccess:x,step:R}=(0,v.A)(),[T,{action:I,loading:M}]=(0,z.A)(),O=s?.length>0,D=_?.length>0,Q=Boolean(!M&&k?.id&&!1===x),K=!0===x&&"conversion_action"===R;(0,c.useEffect)((()=>{(async()=>{K&&(await T(),C("getExistingGoogleAdsAccounts",[]))})()}),[K,T,C]);const J=[409,403].includes(o.response?.status)||O||f,Z=J&&(e||!E),X=S||D,ee=X&&(e||!S);if((0,c.useEffect)((()=>{!e||f||S||t(!1)}),[e,S,f]),!d)return(0,a.createElement)(V.A,null);const te=(0,a.createElement)(r.w5,{isTertiary:!0,text:(0,i.__)("Or, connect to a different Google account","google-listings-and-ads")}),ne=Boolean(u)&&!Q||!ee&&K,ae=(0,H.A)(k),oe=A&&E;return(0,a.createElement)("div",{className:"gla-google-combo-account-card-wrapper"},(0,a.createElement)(l.A,{appearance:l.x.GOOGLE,alignIcon:"top",className:"gla-google-combo-account-card gla-google-combo-account-card--connected gla-google-combo-service-account-card--google",description:m||(0,a.createElement)(P,null),actions:e?(0,a.createElement)("div",{className:"gla-google-combo-account-card__description-actions"},te,(0,a.createElement)(g.A,{isTertiary:!0,onClick:()=>{t(!1)}},(0,i.__)("Cancel","google-listings-and-ads"))):(0,a.createElement)("div",{className:"gla-google-combo-account-card__description-actions"},!ee&&X||!Z&&J?(0,a.createElement)(g.A,{isTertiary:!0,text:(0,i.__)("Edit","google-listings-and-ads"),onClick:()=>{t(!0)}}):te),helper:p,indicator:(0,a.createElement)(L,{showSpinner:ne}),detail:(0,a.createElement)(U,{showConversionMeasurementNotice:ae,claimGoogleAdsAccount:Q}),expandedDetail:!0}),ee&&(0,a.createElement)(G,{onRequestCreate:T,upsertingAction:I}),Z&&(0,a.createElement)(Y.b,{createAccount:n,resultCreateAccount:o,className:"gla-google-combo-account-card gla-google-combo-service-account-card--mc"}),oe&&(0,a.createElement)(j.S,null))};function K({disabled:e=!1}){const{google:t,scope:n,hasFinishedResolution:i}=(0,o.A)();if(!i)return(0,a.createElement)(l.A,{description:(0,a.createElement)(s.A,null)});const c="yes"===t?.active;return c&&n.gmcRequired&&n.adsRequired?(0,a.createElement)(Q,null):!c||n.gmcRequired&&n.adsRequired?(0,a.createElement)(p,{disabled:e}):(0,a.createElement)(r.dR,{additionalScopeEmail:t.email})}},1666:(e,t,n)=>{n.d(t,{b:()=>V,D:()=>Q});var a=n(1609),o=n(6942),s=n.n(o),l=n(6087),r=n(7723),i=n(5703),c=n(2722),d=n(7568);const u=e=>{const{data:t}=(0,c.A)(),n=t?.map((e=>({value:e.id,label:(0,r.sprintf)( // translators: 1: account name, 2: account domain, 3: account ID. // translators: 1: account name, 2: account domain, 3: account ID. (0,r.__)("%1$s ・ %2$s (%3$s)","google-listings-and-ads"),e.name,e.domain,e.id)})));return n?.sort(((e,t)=>e.label.localeCompare(t.label))),(0,a.createElement)(d.A,{options:n,autoSelectFirstOption:!0,...e})};var g=n(7916);const m=({isConnected:e,...t})=>{const{data:n}=(0,c.A)(),{googleMCAccount:o}=(0,g.A)(),s=n?.some((e=>e.id===o.id));if(!s&&e){const e=new URL((0,i.getSetting)("homeUrl")).host;return(0,a.createElement)(d.A,{autoSelectFirstOption:!0,nonInteractive:!0,value:o.id,options:[{value:o.id,label:(0,r.sprintf)( // translators: 1: account domain, 2: account ID. // translators: 1: account domain, 2: account ID. (0,r.__)("%1$s (%2$s)","google-listings-and-ads"),e,o.id)}]})}return(0,a.createElement)(u,{nonInteractive:e,...t})};var p=n(7892),h=n(6599),_=n(5640),E=n(3658);var f=n(6427),A=n(8242),y=n(8771),v=n(8463),b=n(8468),C=n(1177),k=n(559),w=n(7677),N=n(9703),S=n(5092);const x=e=>{const{className:t,...n}=e;return(0,a.createElement)(S.A,{className:s()("app-input-link-control",t),prefix:(0,a.createElement)(w.A,{icon:N.A,size:24}),...n})},R=({id:e,websiteUrl:t,onSwitchAccount:n=b.noop})=>{const{invalidateResolution:o}=(0,E.j)(),[s,{loading:d,error:u,reset:g}]=(0,h.A)({path:"/wc/gla/mc/accounts/claim-overwrite",method:"POST",data:{id:e}}),m=(0,i.getSetting)("homeUrl"),{data:_}=(0,c.A)(),w=_?.length>0;return(0,a.createElement)(k.A,{className:"gla-reclaim-url-card",appearance:k.x.GOOGLE_MERCHANT_CENTER,description:(0,r.sprintf)( // translators: 1: website URL, 2: account ID. // translators: 1: website URL, 2: account ID. (0,r.__)("%1$s (%2$s)","google-listings-and-ads"),t,e),indicator:w?(0,a.createElement)(p.A,{isSecondary:!0,disabled:d,eventName:"gla_mc_account_switch_account_button_click",eventProps:{context:"reclaim-url"},onClick:n},(0,r.__)("Switch account","google-listings-and-ads")):null},(0,a.createElement)(f.CardDivider,null),(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(y.A.Title,null,(0,r.__)("Reclaim your URL","google-listings-and-ads")),(0,a.createElement)(y.A.Body,null,(0,r.__)("Your URL is currently claimed by another Merchant Center account.","google-listings-and-ads")),(0,a.createElement)(v.A,null,(0,a.createElement)(x,{disabled:!0,value:m}),(0,a.createElement)(p.A,{isSecondary:!0,loading:d,eventName:"gla_mc_account_reclaim_url_button_click",onClick:async()=>{g(),await s({parse:!1}),o("getGoogleMCAccount",[])}},(0,r.__)("Reclaim my URL","google-listings-and-ads"))),(0,a.createElement)(y.A.HelperText,null,(0,l.createInterpolateElement)((0,r.__)("If you reclaim this URL, it will cause any existing product listings or ads to stop running, and the other verified account will be notified that they have lost their claim. Learn more.","google-listings-and-ads"),{link:(0,a.createElement)(C.A,{context:"setup-mc",linkId:"claim-url",href:"https://support.google.com/merchants/answer/176793"})})),u&&(0,a.createElement)(f.Notice,{status:"error",isDismissible:!1},(0,l.createInterpolateElement)((0,r.__)("We were unable to reclaim this URL. You may not have permission to reclaim this URL, or an error might have occurred. Try again later or contact your Google account administrator.","google-listings-and-ads"),{strong:(0,a.createElement)("strong",null)}))))},T=({id:e,claimedUrl:t,newUrl:n,onSelectAnotherAccount:o=()=>{}})=>{const{createNotice:s}=(0,_.A)(),{invalidateResolution:l}=(0,E.j)(),[c,{loading:d,error:u,response:g}]=(0,h.A)({path:"/wc/gla/mc/accounts/switch-url",method:"POST",data:{id:e}}),m=(0,i.getSetting)("homeUrl"),b=()=>{o()};return g&&403===g.status?(0,a.createElement)(R,{id:u.id,websiteUrl:u.website_url,onSwitchAccount:b}):(0,a.createElement)(k.A,{className:"gla-switch-url-card",appearance:k.x.GOOGLE_MERCHANT_CENTER,description:(0,r.sprintf)( // translators: 1: the new URL, 2: account ID. // translators: 1: the new URL, 2: account ID. (0,r.__)("%1$s (%2$s)","google-listings-and-ads"),n,e),indicator:(0,a.createElement)(p.A,{isSecondary:!0,disabled:d,eventName:"gla_mc_account_switch_account_button_click",eventProps:{context:"switch-url"},onClick:b},(0,r.__)("Switch account","google-listings-and-ads"))},(0,a.createElement)(f.CardDivider,null),(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(y.A.Title,null,(0,r.__)("Switch to this new URL","google-listings-and-ads")),(0,a.createElement)(y.A.Body,null,(0,r.sprintf)( // translators: %s: claimed URL. // translators: %s: claimed URL. (0,r.__)("This Merchant Center account already has a verified and claimed URL, %s.","google-listings-and-ads"),t)),(0,a.createElement)(v.A,null,(0,a.createElement)(x,{disabled:!0,value:m}),(0,a.createElement)(p.A,{isSecondary:!0,loading:d,eventName:"gla_mc_account_switch_url_button_click",onClick:async()=>{try{await c({parse:!1}),l("getGoogleMCAccount",[])}catch(e){if(403!==e.status){const t=(await e.json()).message||(0,r.__)("Unable to switch to your new URL. Please try again later.","google-listings-and-ads");s("error",t)}}}},(0,r.__)("Switch to this new URL","google-listings-and-ads"))),(0,a.createElement)(y.A.HelperText,null,(0,r.sprintf)(/* translators: 1: new URL. 2: claimed URL. */ /* translators: 1: new URL. 2: claimed URL. */ (0,r.__)("If you switch your claimed URL to %1$s, you will lose your claim to %2$s. This will cause any existing product listings tied to %2$s to stop running.","google-listings-and-ads"),n,t))))};var I=n(8441),G=n(4566),F=n(9457),P=n(7792);const M=({existingAccount:e,onContinue:t=()=>{},onRequestClose:n=()=>{}})=>(0,a.createElement)(F.A,{className:"gla-mc-warning-modal",title:(0,r.__)("Create Google Merchant Center Account","google-listings-and-ads"),buttons:[(0,a.createElement)(p.A,{key:"confirm",isSecondary:!0,eventName:"gla_mc_account_warning_modal_confirm_button_click",onClick:()=>{t()}},(0,r.__)("Yes, I want a new account","google-listings-and-ads")),(0,a.createElement)(p.A,{key:"cancel",isPrimary:!0,onClick:n},(0,r.__)("Cancel","google-listings-and-ads"))],onRequestClose:n},(0,a.createElement)("p",{className:"gla-mc-warning-modal__warning-text"},(0,a.createElement)(P.A,null),(0,a.createElement)("span",null,(0,r.__)("Are you sure you want to create a new Google Merchant Center account?","google-listings-and-ads"))),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,r.__)("You already have another verified account, , which is connected to this store’s URL, .","google-listings-and-ads"),{storename:(0,a.createElement)("strong",null,e.name),storeurl:(0,a.createElement)("strong",null,e.domain)})),(0,a.createElement)("p",null,(0,r.__)("If you create a new Google Merchant Center account, you will have to reclaim this store’s URL with the new account. This will cause any existing product listings or ads to stop running, and the other verified account will lose its claim.","google-listings-and-ads"))),O=({onCreateAccount:e=()=>{},onRequestClose:t=()=>{}})=>{const[n,o]=(0,l.useState)(!1);return(0,a.createElement)(F.A,{className:"gla-mc-terms-modal",title:(0,r.__)("Create Google Merchant Center Account","google-listings-and-ads"),buttons:[(0,a.createElement)(p.A,{key:"1",isPrimary:!0,disabled:!n,eventName:"gla_mc_account_create_button_click",onClick:()=>{e(),t()}},(0,r.__)("Create account","google-listings-and-ads"))],onRequestClose:t},(0,a.createElement)("p",{className:"main"},(0,r.__)("By creating a Google Merchant Center account, you agree to the following terms and conditions:","google-listings-and-ads")),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,r.__)("You agree to comply with Google’s terms and policies, including Google Merchant Center Terms of Service.","google-listings-and-ads"),{link:(0,a.createElement)(C.A,{context:"setup-mc",linkId:"google-mc-terms-of-service",href:"https://support.google.com/merchants/answer/160173"})})),(0,a.createElement)(f.CheckboxControl,{label:(0,r.__)("I have read and accept these terms","google-listings-and-ads"),checked:n,onChange:o}))},D=Object.freeze({NONE:"NONE",WARNING:"WARNING",TERMS:"TERMS"}),U=e=>{const{onCreateAccount:t=b.noop,onClick:n=b.noop,...o}=e,[s,r]=(0,l.useState)(D.NONE),{data:d}=(0,c.A)(),u=((e=[])=>{const t=new URL((0,i.getSetting)("homeUrl")).toString();return e.find((e=>{try{return new URL(e.domain).toString()===t}catch(e){return!1}}))})(d),g=()=>{r(D.NONE)};return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(p.A,{onClick:()=>{r(u?D.WARNING:D.TERMS),n()},...o}),s===D.WARNING&&(0,a.createElement)(M,{existingAccount:u,onContinue:()=>{r(D.TERMS)},onRequestClose:g}),s===D.TERMS&&(0,a.createElement)(O,{onCreateAccount:t,onRequestClose:g}))};var L=n(6520);const q=({onDisconnected:e=b.noop,...t})=>{const{createNotice:n,removeNotice:o}=(0,_.A)(),{invalidateResolution:s}=(0,E.j)(),[l,{loading:i}]=(0,h.A)({path:`${L.RV}/mc/connection`,method:"DELETE"});return(0,a.createElement)(p.A,{disabled:i,text:(0,r.__)("Or, connect to a different Google Merchant Center account","google-listings-and-ads"),eventName:"gla_mc_account_connect_different_account_button_click",onClick:async()=>{const{notice:t}=await n("info",(0,r.__)("Disconnecting your Google Merchant Center account, please wait…","google-listings-and-ads"));try{await l(),s("getExistingGoogleMCAccounts",[]),s("getGoogleMCAccount",[]),e()}catch(e){n("error",(0,r.__)("Unable to disconnect your Google Merchant Center account. Please try again later.","google-listings-and-ads"))}o(t.id)},...t})},B=({isConnected:e,resultConnectMC:t,resultCreateAccount:n,onCreateAccount:o})=>{const{data:s}=(0,c.A)();if(e&&s.length>0){const e=()=>{t.reset(),n.reset()};return(0,a.createElement)(q,{onDisconnected:e,isTertiary:!0})}return(0,a.createElement)(U,{isTertiary:!0,disabled:t.loading,onCreateAccount:o},(0,r.__)("Or, create a new Merchant Center account","google-listings-and-ads"))},$=e=>{const{retryAfter:t,onRetry:n=()=>{}}=e;return(0,l.useEffect)((()=>{if(!t)return;const e=setInterval((()=>{n()}),1e3*t);return()=>clearInterval(e)}),[t,n]),(0,a.createElement)(k.A,{appearance:k.x.GOOGLE_MERCHANT_CENTER,description:(0,r.__)("This may take a few moments, please wait…","google-listings-and-ads"),indicator:(0,a.createElement)(p.A,{loading:!0},(0,r.__)("Creating…","google-listings-and-ads"))})},V=({createAccount:e,resultCreateAccount:t,className:n})=>{const[o,i]=(0,l.useState)(),[c,d]=(e=>{const{createNotice:t}=(0,_.A)(),[n,a]=(0,h.A)({path:"/wc/gla/mc/accounts",method:"POST",data:{id:e}}),{invalidateResolution:o}=(0,E.j)();return[async()=>{if(e)try{await n({parse:!1}),o("getGoogleMCAccount",[])}catch(e){if(![409,403].includes(e.status)){const n=(await e.json()).message||(0,r.__)("Unable to connect Merchant Center account. Please try again later.","google-listings-and-ads");t("error",n)}}},a]})(o),{googleMCAccount:u,hasFinishedResolution:f,isReady:A,hasGoogleMCConnection:y}=(0,g.A)();if((0,l.useEffect)((()=>{y&&i(u.id)}),[u,y]),!A){if(409===d.response?.status)return(0,a.createElement)(T,{id:d.error.id,message:d.error.message,claimedUrl:d.error.claimed_url,newUrl:d.error.new_url,onSelectAnotherAccount:d.reset});if(403===d.response?.status||403===t.response?.status)return(0,a.createElement)(R,{id:d.error?.id||t.error?.id,websiteUrl:d.error?.website_url||t.error?.website_url,onSwitchAccount:()=>{d.reset(),t.reset()}});if(t.loading||503===t.response?.status)return(0,a.createElement)($,{retryAfter:t.error?.retry_after,onRetry:e})}return(0,a.createElement)(k.A,{className:s()("gla-connect-mc-card",n),title:(0,r.__)("Connect to existing Merchant Center account","google-listings-and-ads"),helper:(0,r.__)("Required to sync products so they show on Google.","google-listings-and-ads"),alignIndicator:"toDetail",indicator:f?A?(0,a.createElement)(G.A,null):d.loading?(0,a.createElement)(I.A,{text:(0,r.__)("Connecting…","google-listings-and-ads")}):(0,a.createElement)(p.A,{isSecondary:!0,eventName:"gla_mc_account_connect_button_click",eventProps:{id:Number(o)},onClick:c},(0,r.__)("Connect","google-listings-and-ads")):(0,a.createElement)(I.A,null),detail:(0,a.createElement)(m,{isConnected:y,value:o,onChange:i}),actions:(0,a.createElement)(B,{isConnected:y,resultConnectMC:d,resultCreateAccount:t,onCreateAccount:e})})};var j=n(3905),W=n(6588);const z=e=>{const{className:t,...n}=e,o=["app-notice",t];return(0,a.createElement)(f.Notice,{className:s()(...o),...n})};var Y=n(4876),H=n(3666);const Q=({googleMCAccount:e,hideNotificationService:t=!1})=>{const{createNotice:n,removeNotice:o}=(0,_.A)(),{invalidateResolution:s}=(0,E.j)(),[c,{loading:d}]=(0,h.A)({path:`${L.RV}/rest-api/authorize`,method:"DELETE"}),[u,g]=(0,l.useState)(null),m=new URL((0,i.getSetting)("homeUrl")).host,f=!t&&e.wpcom_rest_api_status===j.ac.APPROVED,y=!t&&e.wpcom_rest_api_status&&e.notification_service_enabled&&e.wpcom_rest_api_status!==j.ac.APPROVED;return(0,a.createElement)(k.A,{appearance:k.x.GOOGLE_MERCHANT_CENTER,description:(0,r.sprintf)( // translators: 1: account domain, 2: account ID. // translators: 1: account domain, 2: account ID. (0,r.__)("%1$s (%2$s)","google-listings-and-ads"),m,e.id),indicator:y?(0,a.createElement)(W.A,{text:(0,r.__)("Grant access","google-listings-and-ads"),eventName:"gla_enable_product_sync_click",eventProps:{context:"mc_card"}}):(0,a.createElement)(G.A,null)},f&&(0,a.createElement)(z,{status:"success",isDismissible:!1},(0,r.__)("Google has been granted access to fetch your product data.","google-listings-and-ads")),y&&(0,a.createElement)(z,{status:"warning",isDismissible:!1},(0,r.__)("There was an issue granting access to Google for fetching your products.","google-listings-and-ads")),u&&(0,a.createElement)(Y.Ay,{onRequestClose:()=>g(null),onDisconnected:()=>{window.location.href=(0,H.FN)()},disconnectTarget:u,disconnectAction:async()=>{const{notice:e}=await n("info",(0,r.__)("Disabling the new Product Sync feature, please wait…","google-listings-and-ads"));try{await c(),s("getGoogleMCAccount",[])}catch(e){n("error",(0,r.__)("Unable to disable new product sync. Please try again later.","google-listings-and-ads"))}o(e.id)}}),f&&(0,a.createElement)(A.A.Card.Footer,null,(0,a.createElement)(p.A,{isDestructive:!0,isLink:!0,disabled:d,text:(0,r.__)("Disable product data fetch","google-listings-and-ads"),eventName:"gla_disable_product_sync_click",onClick:()=>g(Y.Me)})))}},4270:(e,t,n)=>{n.d(t,{A:()=>r,i:()=>i});var a=n(1609),o=n(6942),s=n.n(o),l=n(2848);function r({title:e,children:t}){return(0,a.createElement)("div",{className:"gla-guide__page-content"},(0,a.createElement)("h2",{className:"gla-guide__page-content__header"},e),(0,a.createElement)("div",{className:"gla-guide__page-content__body"},t))}function i(e){const{context:t,href:n,className:o,...r}=e;return(0,a.createElement)(l.A,{className:s()("gla-guide__page-content__link",o),eventName:"gla_modal_content_link_click",eventProps:{context:t,href:n},type:"external",target:"_blank",href:n,...r})}},2455:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(1609),o=n(7723),s=n(4236),l=n(7892);const r=({eventContext:e})=>(0,a.createElement)(l.A,{className:"AnC9WXFuKgCBURYIRcRY",href:"https://woocommerce.com/document/google-for-woocommerce/",target:"_blank",eventName:"gla_help_click",eventProps:{context:e}},(0,a.createElement)(s.A,null),(0,o.__)("Help","google-listings-and-ads"))},6257:(e,t,n)=>{n.d(t,{A:()=>u});var a=n(1609),o=n(6942),s=n.n(o),l=n(7723),r=n(6427),i=n(6087),c=n(4236),d=n(6473);const u=({className:e,id:t,disabled:n=!1,iconSize:o=16,children:u,...g})=>{const[m,p]=(0,i.useState)(!1);return(0,a.createElement)("span",{className:s()("help-popover",e)},(0,a.createElement)("button",{"aria-label":(0,l.__)("Open popover","google-listings-and-ads"),disabled:n,onClick:()=>{p(!0),t&&(0,d.ce)("gla_tooltip_viewed",{id:t})}},(0,a.createElement)(c.A,{size:o})),m&&!n&&(0,a.createElement)(r.Popover,{focusOnMount:"container",inline:!0,onClose:()=>{p(!1)},...g},u))}},8441:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(8846);function s({text:e}){return(0,a.createElement)("div",{className:"gla-loading-label"},(0,a.createElement)(o.Spinner,null),e)}},9927:(e,t,n)=>{n.d(t,{A:()=>w});var a=n(1609),o=n(7723),s=n(6476),l=n(3905),r=n(347),i=n(14),c=n(6427),d=n(6087),u=n(2848),g=n(7892),m=n(9457),p=n(6473),h=n(5640),_=n(9415),E=n(6599);const f="gtin_migration_banner",A="ready",y="started",v=e=>(0,a.createElement)(u.A,{eventName:"gla_gtin_migration_banner_status_link_click",eventProps:{context:f},href:"admin.php?page=wc-status&tab=action-scheduler&s=migrate_gtin&orderby=schedule&order=desc",type:"external",target:"_blank",...e}),b=()=>{const{createNotice:e}=(0,h.A)(),[t,n]=(0,d.useState)(!1),[s,l,r]=(()=>{const{data:e,isResolving:t,invalidateResolution:n}=(0,_.A)("getGtinMigrationStatus"),[a,{loading:o}]=(0,E.A)({path:"/wc/gla/gtin-migration",method:"POST"});return[e,o||t,(0,d.useCallback)((async()=>{await a(),n()}),[a,n])]})();if(s!==A&&s!==y)return null;const i=()=>{(0,p.ce)("gla_modal_closed",{context:f}),n(!1)};return(0,a.createElement)(a.Fragment,null,t&&(0,a.createElement)(m.A,{className:"gla-gtin-migration-banner-modal",title:(0,o.__)("Before you start the migration…","google-listings-and-ads"),buttons:[(0,a.createElement)(g.A,{key:"1",isSecondary:!0,onClick:i},(0,o.__)("Never mind","google-listings-and-ads")),(0,a.createElement)(g.A,{key:"2",disabled:l,isPrimary:!0,onClick:async()=>{(0,p.ce)("gla_gtin_migration_banner_migration_start",{context:f});try{await r(),(0,p.ce)("gla_gtin_migration_banner_migration_scheduled",{context:f}),n(!1),e("info",(0,o.__)("GTIN Migration was successfully scheduled.","google-listings-and-ads"))}catch(t){(0,p.ce)("gla_gtin_migration_banner_migration_failed",{context:f,error:t.message}),e("error",(0,o.__)("Unable to start GTIN Migration.","google-listings-and-ads"))}}},(0,o.__)("Start migration","google-listings-and-ads"))],onRequestClose:i},(0,a.createElement)("p",null,(0,d.createInterpolateElement)((0,o.__)("This migration will copy all GTIN numbers set in the Google for WooCommerce Product tab into the new GTIN field under the Product Inventory tab. If you have already set GTIN numbers in some of your products' Inventory tab, they will not be overridden. The GTIN numbers in the Google for WooCommerce tab will not be removed. The migration will run in the background and is not reversible. You can check the migration process on the WooCommerce Scheduled Actions page.","google-listings-and-ads"),{link:(0,a.createElement)(v,null)}))),s===A&&(0,a.createElement)(c.Notice,{isDismissible:!1},(0,d.createInterpolateElement)((0,o.__)("The GTIN field managed by WooCommerce in the Product's inventory section, will now be used by Google for WooCommerce. It will continue to support the previous field and any mapping rules you have setup for the GTIN field. If you would like to migrate the data click here.","google-listings-and-ads"),{link:(0,a.createElement)(u.A,{className:"gla-gtin-migration__link",eventName:"gla_gtin_migration_banner_click",eventProps:{context:f},onClick:()=>{(0,p.ce)("gla_modal_open",{context:f}),n(!0)}})})),s===y&&(0,a.createElement)(c.Notice,{isDismissible:!1},(0,d.createInterpolateElement)((0,o.__)("Your GTIN Migration is now running in the background. You can check the migration process on the WooCommerce Scheduled Actions page","google-listings-and-ads"),{link:(0,a.createElement)(v,null)})))};var C=n(3666);let k=[{key:"dashboard",title:(0,o.__)("Dashboard","google-listings-and-ads"),href:(0,s.getNewPath)({},"/google/dashboard",{})},{key:"reports",title:(0,o.__)("Reports","google-listings-and-ads"),href:(0,s.getNewPath)({},"/google/reports",{})},{key:"product-feed",title:(0,o.__)("Product Feed","google-listings-and-ads"),href:(0,s.getNewPath)({},"/google/product-feed",{})},{key:"attribute-mapping",title:(0,o.__)("Attributes","google-listings-and-ads"),href:(0,s.getNewPath)({},"/google/attribute-mapping",{})},{key:"settings",title:(0,o.__)("Settings","google-listings-and-ads"),href:(0,s.getNewPath)({},"/google/settings",{})},{key:"shipping",title:(0,o.__)("Shipping","google-listings-and-ads"),href:(0,C.Qk)()}];l.Th.enableReports||(k=k.filter((({key:e})=>"reports"!==e)));const w=()=>{(0,i.A)();const e=(()=>{const e=(0,s.getPath)();return k.find((t=>e.includes(t.key)))?.key})();return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(b,null),(0,a.createElement)(r.A,{tabs:k,selectedKey:e}))}},2391:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(1609),o=n(7723),s=n(6476),l=n(7892),r=n(3905),i=n(3666),c=n(6473);const d=e=>{const{eventName:t="gla_add_paid_campaign_clicked",eventProps:n,children:d,onClick:u=()=>{},...g}=e,{adsSetupComplete:m}=r.Th,p=m?(0,i.uB)():(0,s.getNewPath)({},"/google/setup-ads",{}),h={context:"",href:p};return(0,a.createElement)(l.A,{isSmall:!0,isSecondary:!0,onClick:(...e)=>{(0,c.ce)(t,{...h,...n}),(0,s.getHistory)().push(p),u(...e)},...g},d||(0,o.__)("Add campaign","google-listings-and-ads"))}},1203:(e,t,n)=>{n.d(t,{A:()=>Z});var a=n(1609),o=n(7723),s=n(6087),l=n(9370),r=n(3164),i=n(9826),c=n(3704),d=n(1177),u=n(6960),g=n(5955),m=n(6427),p=n(6893),h=n(6028),_=n(7108),E=n(3741),f=n(1378),A=n(8242),y=n(7892),v=n(7541),b=n(1455),C=n.n(b),k=n(8468),w=n(3658),N=n(5559),S=n(5640),x=n(3905);var R=n(4297),T=n(6473);const I=({billingUrl:e,onSetupComplete:t})=>{const{googleAdsAccount:n}=(0,f.A)(),l=(0,v.A)(T.T1);if(((e=k.noop)=>{const{createNotice:t}=(0,S.A)(),{receiveGoogleAdsAccountBillingStatus:n}=(0,w.j)(),a=(0,s.useRef)(),l=(0,s.useRef)();l.current=e;const r=(0,s.useCallback)((async()=>{const e=a.current,s=await C()({path:"/wc/gla/ads/billing-status"});if(a.current=s.status,e!==s.status&&s.status===x.CX.APPROVED)try{await C()({path:"/wc/gla/ads/accounts",method:"POST"}),await l.current(),n(s)}catch(e){t("error",(0,o.__)("Unable to complete your Google Ads account setup. Please try again later.","google-listings-and-ads"))}}),[t,n]);(0,N.A)(r,30)})(t),!n)return(0,a.createElement)(E.A,null);const r=t=>{const n=l({link_id:"set-up-billing",href:e});if((0,T.ce)("gla_ads_set_up_billing_click",n),"BUTTON"===t.currentTarget.nodeName){const{defaultView:n}=t.target.ownerDocument,a=(0,R.A)(n,600,800);n.open(e,"_blank",a)}};return(0,a.createElement)(A.A.Card,{className:"gla-google-ads-billing-setup-card"},(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)("div",{className:"gla-google-ads-billing-setup-card__description"},(0,o.__)("You do not have billing information set up in your Google Ads account. Once you have set up billing, you can start running ads.","google-listings-and-ads"),(0,a.createElement)("div",{className:"gla-google-ads-billing-setup-card__description__helper"},(0,s.createInterpolateElement)((0,o.__)("You will be directed to Google Ads for this step. In case your browser is unable to open the pop-up, click here instead .","google-listings-and-ads"),{link:(0,a.createElement)("a",{target:"_blank",rel:"external noreferrer noopener",href:e,onClick:r}),icon:(0,a.createElement)(m.Icon,{icon:_.A,size:12})}))),(0,a.createElement)(y.A,{isSecondary:!0,onClick:r},(0,o.__)("Set up billing","google-listings-and-ads"))))},G="https://support.google.com/google-ads/answer/2375375",{APPROVED:F}=x.CX;function P(){const{billingStatus:e,hasFinishedResolution:t}=(0,p.A)();return t?e.status===F?(0,a.createElement)(m.Flex,{className:"gla-google-ads-billing-card__success-status"},(0,a.createElement)(g.A,{size:18}),(0,a.createElement)(m.FlexBlock,null,(0,o.__)("Billing method for Google Ads added successfully","google-listings-and-ads"))):(0,a.createElement)(I,{billingUrl:e.billing_url||G}):(0,a.createElement)(h.A,null)}var M=n(9031),O=n(6734),D=n(4679);const U=e=>{const{countryCodes:t,dailyAverageCost:n=1/0}=e,{data:l,highestDailyBudgetCountryCode:r,highestDailyBudget:i}=(0,D.A)(t),c=(0,O.A)();if(!l)return null;const{currency:d,recommendations:u}=l,g=c[r],p=function(e,...t){const n={strong:(0,a.createElement)("strong",null),em:(0,a.createElement)("em",null),br:(0,a.createElement)("br",null)},l=e? // translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount. // translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount. (0,o.__)("We recommend running campaigns for at least 1 month so it can learn to optimize for your business.
Tip: Most merchants targeting similar countries set a daily budget of %1$f %2$s","google-listings-and-ads"): // translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount 3: a country name selected by the merchant. // translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount 3: a country name selected by the merchant. (0,o.__)("We recommend running campaigns for at least 1 month so it can learn to optimize for your business.
Tip: Most merchants targeting %3$s set a daily budget of %1$f %2$s","google-listings-and-ads");return(0,s.createInterpolateElement)((0,o.sprintf)(l,...t),n)}(u.length>1,i,d,g),h=n{const{getInputProps:l,values:r}=e,{amount:i}=r,{googleAdsAccount:c}=(0,f.A)(),d=30.4*i,u=c?.currency;return(0,a.createElement)("div",{className:"gla-budget-section"},(0,a.createElement)(A.A,{verticalGap:4,disabled:n,title:(0,o.__)("Set your budget","google-listings-and-ads"),description:(0,a.createElement)("p",null,(0,o.__)("With Performance Max campaigns, you can set your own budget and Google’s Smart Bidding technology will serve the most appropriate ad, with the optimal bid, to maximize campaign performance. You only pay when people click on your ads, and you can start or stop your campaign whenever you want.","google-listings-and-ads"))},(0,a.createElement)(A.A.Card,null,(0,a.createElement)(A.A.Card.Body,{className:"gla-budget-section__card-body"},(0,a.createElement)("div",{className:"gla-budget-section__card-body__cost"},(0,a.createElement)(L.A,{label:(0,o.__)("Daily average cost","google-listings-and-ads"),suffix:u,...l("amount"),...n&&q}),(0,a.createElement)(L.A,{disabled:!0,label:(0,o.__)("Monthly max, estimated","google-listings-and-ads"),suffix:u,value:d})),t?.length>0&&(0,a.createElement)(U,{countryCodes:t,dailyAverageCost:i}))),s))};var $=n(1212),V=n(9452);const j=[{trackId:"how-does-google-ads-work",question:(0,o.__)("How does Google Ads work?","google-listings-and-ads"),answer:(0,o.__)("Google Ads works by displaying your ad when people search online for the products and services you offer. By leveraging smart technology, Google Ads helps get your ads in front of potential customers at just the moment they’re ready to take action.","google-listings-and-ads")},{trackId:"what-is-a-product-feed",question:(0,o.__)("What is a product feed?","google-listings-and-ads"),answer:(0,o.__)("Your product feed is the central data source that contains a list of products you want to advertise through Merchant Center. By default, Google syncs all active products from your WooCommerce inventory. You can choose to exclude products later after this setup.","google-listings-and-ads")},{trackId:"how-much-does-google-ads-cost",question:(0,o.__)("How much does Google Ads cost?","google-listings-and-ads"),answer:(0,o.__)("With Google Ads, you decide how much to spend. There’s no minimum spend, and no time commitment. Your costs may vary from day to day, but you won’t be charged more than your daily budget times the number of days in a month. You pay only for the actual clicks and calls that your ad receives.","google-listings-and-ads")},{trackId:"where-will-my-products-appear",question:(0,o.__)("Where will my products appear?","google-listings-and-ads"),answer:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",null,(0,o.__)("If you’re selling in the US, then eligible free listings can appear in search results across Google Search, Google Images, and the Google Shopping tab. If you’re selling outside the US, free listings will appear on the Shopping tab.","google-listings-and-ads")),(0,a.createElement)("div",null,(0,o.__)("If you’re running a Performance Max Campaign, your approved products can appear on Google Search, Google Maps, the Shopping tab, Gmail, Youtube, the Google Display Network, and Discover feed.","google-listings-and-ads")))},{trackId:"how-long-until-i-see-results-with-google-ads",question:(0,o.__)("How long until I see results with Google Ads?","google-listings-and-ads"),answer:(0,o.__)("Google’s Performance Max campaigns are powered by machine learning models. These models train and adapt based on the data you provide in your campaign. This means performance optimization can take time. Typically, this learning process takes 1—2 weeks.","google-listings-and-ads")}],W=()=>(0,a.createElement)(V.A,{trackName:"gla_setup_ads_faq",context:"setup-ads",faqItems:j});var z=n(8846),Y=n(6141),H=n(850);function Q(){const e=[{Icon:g.A,content:(0,o.__)("Set a daily budget, and only pay when people click on your ads.","google-listings-and-ads")}];return(0,a.createElement)("div",{className:"gla-paid-ads-features-section__feature-list"},e.map((({Icon:e,content:t},n)=>(0,a.createElement)(m.Flex,{key:n,align:"flex-start"},(0,a.createElement)(e,{size:"18"}),(0,a.createElement)(m.FlexBlock,null,t)))))}function K(){return(0,a.createElement)(A.A,{className:"gla-paid-ads-features-section",topContent:(0,a.createElement)(z.Pill,null,(0,o.__)("Recommended","google-listings-and-ads")),title:(0,o.__)("Performance Max campaign","google-listings-and-ads"),description:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,o.__)("Performance Max uses the best of Google’s AI to show the most impactful ads for your products at the right time and place. Google will use your product data to create ads for this campaign. ","google-listings-and-ads")),(0,a.createElement)("p",null,(0,a.createElement)(d.A,{context:"setup-paid-ads",linkId:"paid-ads-with-performance-max-campaigns-learn-more",href:"https://support.google.com/google-ads/answer/10724817"},(0,o.__)("Learn more about Performance Max","google-listings-and-ads"))))},(0,a.createElement)(A.A.Card,null,(0,a.createElement)(A.A.Card.Body,null,(0,a.createElement)(H.A,{size:"medium"},(0,a.createElement)(m.Flex,{className:"gla-paid-ads-features-section__content",align:"center",gap:9},(0,a.createElement)(m.FlexBlock,null,(0,a.createElement)(A.A.Card.Title,null,(0,o.__)("Drive more sales with Performance Max","google-listings-and-ads")),(0,a.createElement)("div",{className:"gla-paid-ads-features-section__subtitle"},(0,o.__)("Reach more customers by advertising your products across Google Ads channels like Search, YouTube and Discover. Set up your campaign now so your products are included as soon as they’re approved.","google-listings-and-ads")),(0,a.createElement)(Q,null)),(0,a.createElement)(m.FlexItem,null,(0,a.createElement)($.A,null))),(0,a.createElement)(Y.A,null)))))}var J=n(5847);function Z({campaign:e,headerTitle:t,context:n,skipButton:g,continueButton:m}){const p=(0,u.h5)(),{data:h}=(0,J.A)(),_="setup-mc"===n,E="setup-ads"===n||"create-ads"===n||"edit-ads"===n,f="setup-mc"===n||"setup-ads"===n;let A=(0,s.createInterpolateElement)((0,o.__)("Performance Max campaigns are automatically optimized for you by Google. See what your ads will look like.","google-listings-and-ads"),{link:(0,a.createElement)(d.A,{context:n,linkId:"see-what-ads-look-like",href:"https://support.google.com/google-ads/answer/6275294"})});return _&&(A=(0,o.__)("You’re ready to set up a Performance Max campaign to drive more sales with ads. Your products will be included in the campaign after they’re approved.","google-listings-and-ads")),(0,a.createElement)(l.A,null,(0,a.createElement)(r.A,{title:t,description:A}),_&&(0,a.createElement)(K,null),(0,a.createElement)(B,{formProps:p,countryCodes:"edit-ads"===n?e.displayCountries:h},f&&(0,a.createElement)(P,null),E&&(0,a.createElement)($.B,null)),(0,a.createElement)(i.A,null,(0,a.createElement)(c.A,null,"function"==typeof g?g(p):g,"function"==typeof m?m(p):m),(0,a.createElement)(W,null)))}},8234:(e,t,n)=>{n.d(t,{zK:()=>ee,Ay:()=>ne});var a=n(1609),o=n(7723),s=n(3905),l=n(6960),r=n(9370),i=n(3164),c=n(9826),d=n(3704),u=n(7892),g=n(6087),m=n(6427),p=n(8242),h=n(6942),_=n.n(h),E=n(559),f=n(1455),A=n.n(f),y=n(3832),v=n(8846),b=n(5640),C=n(4301),k=n(6520);function w(){return"."}function N({onAssetsLoaded:e}){const t=(0,g.useRef)({}),n=(0,g.useRef)(),[s,l]=(0,g.useState)([]),[r,i]=(0,g.useState)(!1),[c,d]=(0,g.useState)(!1),{createNotice:m}=(0,b.A)(),{finalUrl:p}=s[0]||{};return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(C.A,{className:"gla-assets-loader",label:(0,a.createElement)(a.Fragment,null,(0,o.__)("Select final URL","google-listings-and-ads"),r&&(0,a.createElement)(v.Spinner,null)),placeholder:(0,o.__)("Search page","google-listings-and-ads"),isSearchable:!0,hideBeforeSearch:!0,excludeSelectedOptions:!1,disabled:c,options:[],selected:s,onSearch:async(e,c)=>{var d;r||i(!0),c!==s[0]?.label&&l([{label:c}]);const u=new Promise((e=>setTimeout(e,300)));if(n.current=u,await u,n.current!==u)return e;const g=t.current,m=c.trim().toLowerCase();return null!==(d=g[m])&&void 0!==d||(g[m]=function(e){const t=`${k.RV}/assets/final-url/suggestions`,n={search:e};return A()({path:(0,y.addQueryArgs)(t,n)})}(m).then((e=>function(e,t){const n=e.map((e=>({finalUrl:e,key:`${e.type}-${e.id}`,keywords:[e.title],label:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:"gla-assets-loader__option-title"},e.title),(0,a.createElement)("div",{className:"gla-assets-loader__option-url"},e.url))})));return""===t&&e.length&&n.unshift({key:"disabled-option-suggestion",label:(0,o.__)("SUGGESTIONS","google-listings-and-ads"),isDisabled:!0}),0===e.length&&n.unshift({key:"disabled-option-no-results",label:(0,o.__)("No matching results","google-listings-and-ads"),keywords:[t],isDisabled:!0}),n}(e,m)))),g[m].finally((()=>{i(!1)})),g[m]},onChange:([e])=>{if(e){const t={...e,label:e.finalUrl.title};l([t])}else l([])},getSearchExpression:w}),(0,a.createElement)(u.A,{isSecondary:!0,text:c?"":(0,o.__)("Select","google-listings-and-ads"),eventName:"gla_import_assets_by_final_url_button_click",eventProps:{type:p?.type},disabled:!p,loading:c,onClick:async()=>{const{finalUrl:t}=s[0];d(!0),function(e,t){const n=`${k.RV}/assets/suggestions`,a={id:e,type:t};return A()({path:(0,y.addQueryArgs)(n,a)})}(t.id,t.type).then(e).catch((()=>{d(!1),m("error",(0,o.__)("Unable to load assets data from the selected page.","google-listings-and-ads"))}))}}))}function S({onAssetsChange:e,initialFinalUrl:t,hideFooter:n=!1}){const[l,r]=(0,g.useState)(t||null),i=l?(0,a.createElement)(m.ExternalLink,{href:l},l):(0,o.__)("Choose a page that you want people to reach after clicking your ad. This might be your homepage, or a more specific page.","google-listings-and-ads"),c=_()({"gla-final-url-card":!0,"gla-final-url-card--has-selected-url":l});return(0,a.createElement)(E.A,{className:c,appearance:E.x.FINAL_URL,alignIcon:"top",description:i},(0,a.createElement)(p.A.Card.Footer,{align:"end",gap:4,hidden:n},l?(0,a.createElement)(u.A,{isTertiary:!0,text:(0,o.__)("Or, select a different Final URL","google-listings-and-ads"),eventName:"gla_reselect_another_final_url_button_click",onClick:()=>{r(null),e(null)}}):(0,a.createElement)(N,{onAssetsLoaded:t=>{r(t[s.ll.FINAL_URL]),e(t)}})))}var x=n(5092),R=n(6319),T=n(8468),I=n(9387);function G({minWidth:e,minHeight:t,suggestedWidth:n,suggestedHeight:a,onSelect:s,onDelete:l,ratioPercentError:r=1}){const i=(0,g.useRef)({});return i.current.onSelect=s,i.current.onDelete=l,{openSelector:(0,g.useCallback)((s=>{const{media:l}=wp,c=(0,o.sprintf)( // translators: 1: Minimum width, 2: Minimum height. // translators: 1: Minimum width, 2: Minimum height. (0,o.__)("Image size needs to be at least %1$d x %2$d","google-listings-and-ads"),e,t),d=l({button:{text:(0,o.__)("Select","google-listings-and-ads"),close:!1},states:[new l.controller.Library({title:(0,o.__)("Select or upload image","google-listings-and-ads"),library:l.query({type:"image"}),date:!1,suggestedWidth:n,suggestedHeight:a}),new l.controller.CustomizeImageCropper({imgSelectOptions:(n,a)=>{const o=[n.get("width"),n.get("height"),e,t],s=function(e,t,n,a){const[o,s]=function(e,t,n,a){const o=n/a;return e/t>o?e=Math.round(t*o):t=Math.round(e/o),[e,t]}(...arguments),l=(e-o)/2,r=(t-s)/2;return{handles:!0,instance:!0,persistent:!0,imageWidth:e,imageHeight:t,minWidth:n,minHeight:a,x1:l,y1:r,x2:l+o,y2:r+s,aspectRatio:`${n}:${a}`}}(...o);if(function(e,t,n,a){const o=e/t,s=n/a;return 100*((o>s?o/s:s/o)-1)}(...o){a.frame.toolbar.get().get("insert").model.set("disabled",e)};a.cropperView.once("image-loaded",(()=>{e(!0)})),s.onSelectEnd=(t,n)=>{e(n.width===s.imageWidth&&n.height===s.imageHeight)}}return s.onSelectChange=(e,t)=>{Number.isNaN(t.width)&&setTimeout((()=>function(e,t,n){const a=getComputedStyle(n),{style:o}=n,s=new Map;["width","height"].forEach((e=>{const t=parseFloat(a[e]);if(Number.isInteger(t)||!Number.isFinite(t))return;s.set(e,[o.getPropertyValue(e),o.getPropertyPriority(e)]);const n=`${Math.ceil(t)}px`;o.setProperty(e,n)}));const{x1:l,y1:r,x2:i,y2:c}=t;e.imgSelect.setSelection(l,r,i,c),e.imgSelect.update(),s.forEach((([e,t],n)=>{o.setProperty(n,e,t)}))}(a,s,e)))},s},control:{params:{}}})]});function u(){if(this===d)return void setTimeout(u);const n=d.state().get("selection"),a=d.toolbar.get();let o;if(n.length){const{width:a,height:s}=n.first().toJSON();o=at.map((e=>({url:e,id:e,alt:""}))))),h=e=>{p(e),r(e.map((e=>e.url)))};i.current=h;const _=e=>{e.id===c?.id&&d(null),h(m.filter((({id:t})=>t!==e.id)))};(0,g.useEffect)((()=>{n>-1&&m.length>n&&i.current(m.slice(0,n))}),[m,n]);const E=G({...e,onDelete:_,onSelect(e){const t=[...m];let n=t.findIndex((({id:t})=>t===e.id));c&&(-1!==n&&e.id!==c.id&&t.splice(n,1,{...c}),n=t.indexOf(c)),-1===n?t.push(e):t.splice(n,1,e),d(null),h(t)}}),f=(e,t=null)=>{d(t),E.openSelector(t?.id)};return(0,a.createElement)("div",{className:"gla-images-selector"},(0,a.createElement)("div",{className:"gla-images-selector__image-list"},m.map((e=>(0,a.createElement)("div",{key:e.url,className:"gla-images-selector__image-item"},(0,a.createElement)(u.A,{className:"gla-images-selector__replace-image-button","aria-label":(0,o.__)("Replace image","google-listings-and-ads"),onClick:()=>f(0,e)},(0,a.createElement)("img",{className:"gla-images-selector__image",alt:e.alt,src:e.url})),(0,a.createElement)(u.A,{className:"gla-images-selector__remove-image-button","aria-label":(0,o.__)("Remove image","google-listings-and-ads"),icon:(0,a.createElement)(I.A,null),iconSize:20,onClick:()=>_(e)}))))),l,(()=>{const e=-1!==n&&m.length>=n,t=(0,a.createElement)(M,{disabled:e,text:(0,o.__)("Add image","google-listings-and-ads"),onClick:f});return e&&s?(0,a.createElement)(F.A,{placement:"top",text:s},t):t})())}var D=n(4788);function U({initialTexts:e=[],minNumberOfTexts:t=0,maxNumberOfTexts:n=0,maxCharacterCounts:s,addButtonText:l,placeholder:r,children:i,onChange:c=T.noop}){const d=(0,g.useRef)(),[m,p]=(0,g.useState)(e),h=e=>{p(e),c(e)};d.current=h,(0,g.useEffect)((()=>{(n>0&&m.length>n||t>0&&m.length"")),s=[0];return n>0&&s.push(n),e.concat(o).slice(...s)}(m,t,n))}),[m,n,t]);const _=(e,{event:t})=>{const{index:n}=t.target.dataset,a=[...m];a[n]=e.trim(),h(a)},E=e=>{const{index:t}=e.currentTarget.dataset,n=[...m];n.splice(t,1),h(n)},f=[s].flat();return(0,a.createElement)("div",{className:"gla-texts-editor"},(0,a.createElement)("div",{className:"gla-texts-editor__text-list"},m.map(((e,n)=>{var s;const l=null!==(s=f[n])&&void 0!==s?s:f[0];return(0,a.createElement)("div",{key:n,className:"gla-texts-editor__text-item"},(0,a.createElement)(x.A,{className:"gla-texts-editor__text-input",value:e,kindCharacterCount:"google-ads",maxCharacterCount:l,placeholder:r,"data-index":n,onChange:_}),(0,a.createElement)("div",{className:"gla-texts-editor__remove-text-button-anchor"},n+1>t&&(0,a.createElement)(u.A,{className:"gla-texts-editor__remove-text-button","aria-label":(0,o.__)("Remove text","google-listings-and-ads"),icon:(0,a.createElement)(D.A,null),iconSize:20,"data-index":n,onClick:E})))}))),i,(0,a.createElement)(M,{hidden:t>0&&t===n,"aria-label":(0,o.__)("Add text","google-listings-and-ads"),disabled:n>0&&m.length>=n,text:l,onClick:()=>{h(m.concat(""))}}))}var L=n(9491),q=n(3756),B=n(2485),$=n(6257);const V=(0,g.forwardRef)((function({className:e,heading:t,subheading:n,help:s,numOfIssues:l=0,initialExpanded:r=!1,markOptional:i=!1,disabled:c=!1,children:d},m){const p=(0,g.useRef)(),[h,E]=(0,g.useState)(r),f=(0,L.useReducedMotion)();(0,g.useImperativeHandle)(m,(()=>({scrollIntoComponent(){p.current.scrollIntoView({behavior:f?"auto":"smooth",inline:"nearest",block:"nearest"})}})));const A=(0,o.sprintf)( // translators: %d: number of issues in an asset field. // translators: %d: number of issues in an asset field. (0,o._n)("%d issue","%d issues",l,"google-listings-and-ads"),l),y=_()("gla-asset-field",e,!!c&&"gla-asset-field--is-disabled"),b=h&&!c;return(0,a.createElement)("div",{className:y,ref:p},(0,a.createElement)("header",{className:"gla-asset-field__header"},(0,a.createElement)("div",{className:"gla-asset-field__heading-part"},(0,a.createElement)("h2",{className:"gla-asset-field__heading"},t,i&&(0,a.createElement)("span",{className:"gla-asset-field__optional-label"},(0,o._x)("(Optional)","A label behind the heading to indicate a field is optional","google-listings-and-ads")),(0,a.createElement)($.A,{className:"gla-asset-field__help-popover",position:"top",iconSize:20,disabled:c},(0,a.createElement)("div",{className:"gla-asset-field__help-popover__content"},s))),n&&(0,a.createElement)("h3",{className:"gla-asset-field__subheading"},n)),l>0&&(0,a.createElement)(v.Pill,{className:"gla-asset-field__issue-pill"},A),(0,a.createElement)("div",{className:"gla-asset-field__toggle-button-anchor"},(0,a.createElement)(u.A,{className:"gla-asset-field__toggle-button",icon:b?q.A:B.A,"aria-expanded":b,"aria-label":(0,o.__)("Toggle asset","google-listings-and-ads"),disabled:c,onClick:()=>{E(!h)}}))),(0,a.createElement)("div",{className:"gla-asset-field__content",hidden:!b},d))}));var j=n(6023);const W=[{label:"Automated",value:""},{label:"Learn more",value:"learn_more"},{label:"Get quote",value:"get_quote"},{label:"Apply now",value:"apply_now"},{label:"Sign up",value:"sign_up"},{label:"Contact us",value:"contact_us"},{label:"Subscribe",value:"subscribe"},{label:"Download",value:"download"},{label:"Book now",value:"book_now"},{label:"Shop now",value:"shop_now"}];function z(){const e=(0,g.useRef)(),{values:t,setValue:n,getInputProps:r,adapter:{baseAssetGroup:i,validationRequestCount:c,assetGroupErrors:d}}=(0,l.h5)(),u=i[s.Ms.FINAL_URL],p=u?new URL(u).hostname:"",h=Boolean(u),_=r(s.Ms.CALL_TO_ACTION_SELECTION);function E(e){if(!h||0===c)return 0;const t=d[e];return Array.isArray(t)?t.length:t?1:0}function f(e){return 0===E(e)?null:(0,a.createElement)(R.A,{messages:d[e]})}function A(t){e.current||0===E(this)||(e.current=t)}e.current=null,(0,g.useEffect)((()=>{c>0&&e.current&&e.current.scrollIntoComponent()}),[c]);const y=(0,g.useRef)(h);h||(y.current=h);const v=y.current?t:i;return(0,a.createElement)("div",{key:u,className:"gla-asset-group-card"},j.om.map((e=>{const n=v[e.key],o=r(e.key);return(0,a.createElement)(V,{key:e.key,ref:A.bind(e.key),heading:e.heading,subheading:e.subheading,help:e.help,numOfIssues:E(e.key),markOptional:0===e.min,disabled:!h,initialExpanded:h},(0,a.createElement)(O,{initialImageUrls:n,maxNumberOfImages:e.getMax(t),reachedMaxNumberTip:e.getMaxNumberTip(t),imageConfig:e.imageConfig,onChange:o.onChange},f(e.key)))})),j.E1.map((e=>{const t=[v[e.key]].flat(),n=r(e.key);return(0,a.createElement)(V,{key:e.key,ref:A.bind(e.key),heading:e.heading,subheading:(0,a.createElement)(g.Fragment,null,e.subheading,h&&e.extraSubheading),help:e.help,numOfIssues:E(e.key),disabled:!h,initialExpanded:h},(0,a.createElement)(U,{initialTexts:t,minNumberOfTexts:e.min,maxNumberOfTexts:e.max,maxCharacterCounts:e.maxCharacterCounts,placeholder:e.capitalizedName,addButtonText:e.addButtonText,onChange:t=>{e.requiredSingleValue?n.onChange(t[0]):n.onChange(t)}},f(e.key)))})),(0,a.createElement)(V,{className:"gla-asset-field-call-to-action",heading:(0,o.__)("Call to action","google-listings-and-ads"),help:(0,o.__)("Select a call to action that aligns with your goals, or use automated call to action which allows Google to automatically choose the most relevant call to action for you.","google-listings-and-ads"),disabled:!h,initialExpanded:h},(0,a.createElement)(m.SelectControl,{options:W,value:_.value||W[0].value,onChange:_.onChange})),(0,a.createElement)(V,{ref:A.bind(s.Ms.DISPLAY_URL_PATH),className:"gla-asset-field-display-url-path",heading:(0,o.__)("Display URL Path","google-listings-and-ads"),subheading:p,help:(0,a.createElement)(g.Fragment,null,(0,a.createElement)("div",null,(0,o.__)("The display URL gives potential customers a clear idea of what webpage they'll reach once they click your ad, so your path text should describe your ad's landing page.","google-listings-and-ads")),(0,a.createElement)("div",null,(0,o.__)('To create your display URL, Google Ads will combine the domain (for example, "www.google.com" in www.google.com/nonprofits) from your final URL and the path text (for example, "nonprofits" in www.google.com/nonprofits).',"google-listings-and-ads"))),numOfIssues:E(s.Ms.DISPLAY_URL_PATH),markOptional:!0,disabled:!h,initialExpanded:h},j.tY.map(((e,o)=>{const l=t[s.Ms.DISPLAY_URL_PATH];return(0,a.createElement)(g.Fragment,{key:o},(0,a.createElement)("span",{className:"gla-asset-field-display-url-path__slash"},"/"),(0,a.createElement)(x.A,{className:"gla-asset-field-display-url-path__text-input",kindCharacterCount:"google-ads",maxCharacterCount:e.maxCharacterCount,value:l[o]||"",onChange:e=>{const t=l.slice();t[o]=e,n(s.Ms.DISPLAY_URL_PATH,t)}}))})),f(s.Ms.DISPLAY_URL_PATH)))}var Y=n(1177);function H(){const{adapter:e}=(0,l.h5)(),t=e.hasImportedAssets;return(0,a.createElement)(p.A,{className:"gla-asset-group-section",verticalGap:4,title:(0,g.createInterpolateElement)((0,o.__)("Add additional assets (Optional)","google-listings-and-ads"),{optional:(0,a.createElement)("span",{className:"gla-asset-group-section__optional-label"})}),description:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",{className:"gla-asset-group-section__primary-description"},(0,o.__)("Upload text and image assets to effectively reach and engage your target shoppers. Google will mix and match your assets, continually testing combinations to create personalized and optimal shopping experiences.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,a.createElement)(Y.A,{context:"asset-group",linkId:"asset-group-learn-more",href:"https://support.google.com/google-ads/answer/10729160"},(0,o.__)("Learn more","google-listings-and-ads"))))},(0,a.createElement)(S,{initialFinalUrl:e.baseAssetGroup[s.Ms.FINAL_URL],onAssetsChange:e.resetAssetGroup,hideFooter:!e.isEmptyAssetEntityGroup}),t&&(0,a.createElement)(m.Tip,null,(0,o.__)("We auto-populated assets directly from your Final URL. We encourage you to edit or add more in order to best showcase your business.","google-listings-and-ads")),(0,a.createElement)(z,null))}var Q=n(9452);const K=[{trackId:"what-will-my-ads-look-like",question:(0,o.__)("What will my ads look like?","google-listings-and-ads"),answer:(0,a.createElement)("div",null,(0,g.createInterpolateElement)((0,o.__)("Google will generate text ads and responsive display ads in various combinations and formats from the headlines, images, and descriptions you add. Your ads will automatically adjust their size, appearance, and format to fit available ad spaces. See common ad formats","google-listings-and-ads"),{link:(0,a.createElement)(Y.A,{context:"assets-faq",linkId:"assets-faq-about-ad-formats-available-in-different-campaign-types",href:"https://support.google.com/google-ads/answer/1722124"})}))},{trackId:"what-makes-these-ads-different-from-product-ads",question:(0,o.__)("What makes these ads different from product ads?","google-listings-and-ads"),answer:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",null,(0,o.__)("Text and image assets can elevate your campaign by offering a variety of ad combinations that capture your audience's attention and generate maximum engagement. By leveraging Google's asset-mixing technology, your ads can be optimized to deliver the right message, to the right people, at the right time.","google-listings-and-ads")),(0,a.createElement)("div",null,(0,o.__)("Compared to product ads—which showcase individual products and are designed to drive direct sales and revenue— ads with creative assets are typically used to highlight your business, generate interest, and attract new customers. While both types of ads can drive conversions, using them together can generate even greater results.","google-listings-and-ads")))}],J=()=>(0,a.createElement)(Q.A,{context:"campaign-management",faqItems:K});var Z=n(6473),X=n(5847);const ee="submit-campaign-and-assets",te="submit-campaign-only";function ne({campaign:e}){const t=!e,{isValidForm:n,handleSubmit:g,adapter:m,values:p}=(0,l.h5)(),{data:h}=(0,X.A)(),{isValidAssetGroup:_,isSubmitting:E,isSubmitted:f,submitter:A}=m,y=A?.dataset.action;function v(n){const a=t?h:e.displayCountries,o=p[s.Ms.FINAL_URL],l={context:t?"campaign-creation":"campaign-editing",action:n.target.dataset.action,audiences:a.join(","),budget:p.amount.toString(),assets_validation:_?"valid":"invalid"};o||(l.assets_validation="unknown"),Object.values(s.Ms).forEach((e=>{const t=`number_of_${e}`,n=[p[e]].flat().filter(Boolean).length;l[t]=o?n.toString():"unknown"})),(0,Z.ce)("gla_submit_campaign_button_click",l)}return(0,a.createElement)(r.A,null,(0,a.createElement)(i.A,{title:(0,o.__)("Optimize your campaign","google-listings-and-ads"),description:(0,o.__)("Drive greater performance by adding text and image assets to create personalized and engaging ads","google-listings-and-ads")}),(0,a.createElement)(H,null),(0,a.createElement)(c.A,null,(0,a.createElement)(d.A,null,(t||m.isEmptyAssetEntityGroup)&&(0,a.createElement)(u.A,{isTertiary:!0,"data-action":te,disabled:!n||f||y===ee,loading:E&&y===te,onClick:e=>{g(e),v(e)}},(0,o.__)("Skip this step","google-listings-and-ads")),(0,a.createElement)(u.A,{isPrimary:!0,"data-action":ee,disabled:!m.baseAssetGroup[s.Ms.FINAL_URL]||f||y===te,loading:E&&y===ee,onClick:e=>{_?g(e):m.showValidation(),v(e)}},t?(0,o.__)("Create campaign","google-listings-and-ads"):(0,o.__)("Save changes","google-listings-and-ads"))),(0,a.createElement)(J,null)))}},8473:(e,t,n)=>{n.d(t,{A:()=>p});var a=n(1609),o=n(6087),s=n(8468),l=n(3905),r=n(6960),i=n(7723);const c=(e,t)=>{if(Number.isFinite(e.amount)&&Number.isFinite(t.dailyBudget)&&t.dailyBudget>0){const{amount:n}=e,{dailyBudget:a,formatAmount:o}=t,s=Math.ceil(.3*a);if(nfunction(e={}){const{assets:t={}}=e,n={...m};return Object.keys(m).forEach((a=>{if(e.hasOwnProperty(a))n[a]=e[a];else if(t.hasOwnProperty(a)){const e=t[a];Array.isArray(e)?n[a]=e.map((({content:e})=>e)):n[a]=e.content}})),n}(t)),[t]),[_,E]=(0,o.useState)(h),[f,A]=(0,o.useState)(!1),{formatAmount:y}=(0,g.A)();return(0,a.createElement)(r.Ay,{initialValues:{...e,...h},validate:e=>c(e,{dailyBudget:n,formatAmount:y}),extendAdapter:e=>{const n=function(e){const t={};u.om.forEach((n=>{e[n.key].length{const o=[],s=[e[a.key]].flat(),l=s.filter(Boolean);if(a.min>=2&&Array.isArray(a.maxCharacterCounts)){const[e,t]=a.maxCharacterCounts;if(e{var s;const l=null!==(s=r[t])&&void 0!==s?s:r[0];if(n(e)>l){const e=a.requiredSingleValue?(0,i.__)("Character limit exceeded","google-listings-and-ads"): // translators: 1: Asset field name. 2: The sequential number of the asset field. // translators: 1: Asset field name. 2: The sequential number of the asset field. (0,i.__)("%1$s %2$d: Character limit exceeded","google-listings-and-ads"),n=(0,i.sprintf)(e,a.capitalizedName,t+1);o.push(n)}})),o.length&&(t[a.key]=o)}));const a=e[l.Ms.DISPLAY_URL_PATH];if(a.length){const e=[],[o,s]=a;if(!o&&s){const t=(0,i.sprintf)( // translators: Asset field name. // translators: Asset field name. (0,i.__)("%s is incomplete","google-listings-and-ads"),u.tY[0].capitalizedName);e.push(t)}u.tY.forEach(((t,o)=>{const s=a[o]||"";if(n(s)>t.maxCharacterCount){const n=(0,i.sprintf)( // translators: Asset field name. // translators: Asset field name. (0,i.__)("%s: Character limit exceeded","google-listings-and-ads"),t.capitalizedName);e.push(n)}})),e.length&&(t[l.Ms.DISPLAY_URL_PATH]=e)}return t}(e.values),a=t?.[l.ll.FINAL_URL];return{isEmptyAssetEntityGroup:!a,baseAssetGroup:_,assetGroupErrors:n,hasImportedAssets:f,isValidAssetGroup:0===Object.keys(n).length,resetAssetGroup(t){const n=(0,s.isPlainObject)(t)?t:h;let a=!1;Object.keys(m).forEach((o=>{t&&t[o]?.length&&(a=!0),e.setValue(o,n[o])})),A(a),E(n),e.adapter.hideValidation()}}},...p})}},1212:(e,t,n)=>{n.d(t,{B:()=>z,A:()=>$});var a=n(1609),o=n(7723),s=n(6087),l=n(4038),r=n(6488),i=n(4111),c=n.n(i),d=n(5703),u=n(7133),g=n(9415),m=n(3741),p=n(6942),h=n.n(p);const _={default:!1,thinnest:"gla-ads-mockup__placeholder--thinnest",thinner:"gla-ads-mockup__placeholder--thinner",thicker:"gla-ads-mockup__placeholder--thicker",blue:"gla-ads-mockup__placeholder--blue","gray-100":"gla-ads-mockup__placeholder--gray-100","gray-200":"gla-ads-mockup__placeholder--gray-200","gray-300":"gla-ads-mockup__placeholder--gray-300","gray-400":"gla-ads-mockup__placeholder--gray-400","gray-500":"gla-ads-mockup__placeholder--gray-500"};function E({width:e,color:t="gray-100",stroke:n="default"}){const o=h()("gla-ads-mockup__placeholder",_[t],_[n]);let s;return void 0!==e&&(s={width:/^\d+$/.test(e)?`${e}px`:e}),(0,a.createElement)("div",{className:o,style:s})}const f={default:!1,adBadge:"gla-ads-mockup__scaled-text--ad-badge",smaller:"gla-ads-mockup__scaled-text--smaller",larger:"gla-ads-mockup__scaled-text--larger",blue:"gla-ads-mockup__scaled-text--blue","gray-700":"gla-ads-mockup__scaled-text--gray-700","gray-800":"gla-ads-mockup__scaled-text--gray-800"};function A({adBadge:e=!1,size:t="default",color:n="gray-700",children:o}){const s=h()("gla-ads-mockup__scaled-text",f[n],f[t],e&&f.adBadge);return(0,a.createElement)("div",{className:s,children:o})}function y({product:e}){const t={backgroundImage:`url(${e.coverUrl})`};return(0,a.createElement)("div",{className:"gla-ads-mockup__product-cover",style:t})}const v=n.p+"images/js/src/images/campaign-preview/53f7ebba3e0e05545002.google-shopping-logo.svg";var b=n(6227);const C=n.p+"images/js/src/images/campaign-preview/6ad8e32cee58c14f05a5.youtube-logo.svg";var k=n(6427),w=n(6740);function N({hideMenu:e=!1}){return(0,a.createElement)("div",{className:"gla-ads-mockup__search-bar"},(0,a.createElement)(w.A,{size:13}),(0,a.createElement)("div",{className:"gla-ads-mockup__search-bar-menu",hidden:e},(0,a.createElement)(E,{stroke:"thinnest",color:"gray-400"}),(0,a.createElement)(E,{stroke:"thinnest",color:"gray-400"}),(0,a.createElement)(E,{stroke:"thinnest",color:"gray-400"})))}function S({product:e}){const t={backgroundImage:`url(${e.shopLogoUrl})`};return(0,a.createElement)("div",{className:"gla-ads-mockup__shop-logo",style:t})}var x=n(9692);function R({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup__product-banner"},(0,a.createElement)("div",{className:"gla-ads-mockup__product-banner-info"},(0,a.createElement)(A,{size:"smaller",adBadge:!0},e.shopName),(0,a.createElement)(E,{stroke:"thinner",width:"85",color:"gray-300"}),(0,a.createElement)(E,{stroke:"thinner",width:"65",color:"gray-300"}),(0,a.createElement)(E,{stroke:"thinner",width:"27",color:"blue"})),(0,a.createElement)(S,{product:e}))}const T=n.p+"images/js/src/images/campaign-preview/ccb636afd5f179a2a92c.gmail-logo.svg";function I(){return(0,a.createElement)("div",{className:"gla-ads-mockup__mail-item"},(0,a.createElement)(E,{stroke:"thinner",color:"gray-200",width:"65"}),(0,a.createElement)(E,{stroke:"thinner",color:"gray-200"}),(0,a.createElement)(E,{stroke:"thinner",width:"122"}))}var G=n(3230);const F=n.p+"images/js/src/images/campaign-preview/e398c276792a4a469b92.ad-corner-buttons-image.svg";var P=n(3423);const M=n.p+"images/js/src/images/campaign-preview/093d4a30c2447b174c17.map-background.png",O=n.p+"images/js/src/images/campaign-preview/8955ab13b4b35353af90.product-sample-image.jpg",D=n.p+"images/js/src/images/campaign-preview/26a3f83547ba2af2f541.shop-sample-logo.png";var U=n(3905);const L={title:(0,o._x)("White tee","A sample product title for demonstrating the paid ads shown on Google services.","google-listings-and-ads"),price:(0,o._x)("$10.00","A sample product price for demonstrating the paid ads shown on Google services.","google-listings-and-ads"),shopName:(0,o._x)("Colleen's Tee Store","A sample name of an online shop for demonstrating the paid ads shown on Google services.","google-listings-and-ads"),coverUrl:O,shopLogoUrl:D,shopUrl:"colleensteestore.com"},q={page:1,per_page:1,orderby:"total_sales",order:"desc"},B=[function({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup"},(0,a.createElement)("div",{className:"gla-ads-mockup__tab-list"},(0,a.createElement)(E,{stroke:"thicker"}),(0,a.createElement)(E,{stroke:"thicker"}),(0,a.createElement)("div",{className:"gla-ads-mockup__tab-item-with-logo"},(0,a.createElement)("img",{height:"30",src:v,alt:(0,o.__)("Google Shopping Logo","google-listings-and-ads")}),(0,a.createElement)(E,{stroke:"thinner",color:"gray-500"})),(0,a.createElement)(E,{stroke:"thicker"})),(0,a.createElement)("div",{className:"gla-ads-mockup__shopping-product"},(0,a.createElement)(y,{product:e}),(0,a.createElement)("div",{className:"gla-ads-mockup__shopping-product-info"},(0,a.createElement)(A,{size:"larger",color:"gray-800"},e.title),(0,a.createElement)(A,{color:"gray-800"},e.price),(0,a.createElement)(A,{size:"smaller"},e.shopName))))},function({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup"},(0,a.createElement)("div",{className:"gla-ads-mockup__youtube-header"},(0,a.createElement)("img",{height:"16",src:C,alt:(0,o.__)("YouTube Logo","google-listings-and-ads")})),(0,a.createElement)("div",{className:"gla-ads-mockup__youtube-product"},(0,a.createElement)(y,{product:e}),(0,a.createElement)("div",{className:"gla-ads-mockup__youtube-learn-more-row"},(0,a.createElement)("div",null,(0,a.createElement)(A,{size:"smaller",color:"blue"},(0,o.__)("LEARN MORE","google-listings-and-ads"))),(0,a.createElement)(b.A,{size:10})),(0,a.createElement)("div",{className:"gla-ads-mockup__youtube-product-info"},(0,a.createElement)(A,{size:"larger",color:"gray-800"},e.title),(0,a.createElement)(E,null),(0,a.createElement)(E,{width:"135"}),(0,a.createElement)(A,{size:"smaller",adBadge:!0},e.shopName))))},function({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup gla-ads-mockup-search"},(0,a.createElement)("div",{className:"gla-ads-mockup__search-header"},(0,a.createElement)("img",{height:"22",src:x,alt:(0,o.__)("Google Logo","google-listings-and-ads")})),(0,a.createElement)(N,{hideMenu:!0}),(0,a.createElement)("div",{className:"gla-ads-mockup__search-keywords"},(0,a.createElement)(E,{width:"30",stroke:"thicker",color:"gray-500"}),(0,a.createElement)(E,{width:"42",stroke:"thicker"}),(0,a.createElement)(E,{width:"32",stroke:"thicker"}),(0,a.createElement)(E,{width:"45",stroke:"thicker"}),(0,a.createElement)(E,{width:"30",stroke:"thinner",color:"gray-500"})),(0,a.createElement)("div",{className:"gla-ads-mockup__search-card"},(0,a.createElement)("div",{className:"gla-ads-mockup__search-card-header"},(0,a.createElement)(A,{size:"smaller",adBadge:!0},e.shopUrl),(0,a.createElement)(E,{stroke:"thinner",width:"79",color:"blue"})),(0,a.createElement)(k.Flex,{align:"stretch"},(0,a.createElement)("div",{className:"gla-ads-mockup__search-card-placeholders"},(0,a.createElement)(E,{width:"100"}),(0,a.createElement)(E,{width:"97"}),(0,a.createElement)(E,{width:"95"}),(0,a.createElement)(E,{width:"99"}),(0,a.createElement)(E,{width:"90"}),(0,a.createElement)(E,{width:"78"})),(0,a.createElement)(S,{product:e}))),(0,a.createElement)("div",{className:"gla-ads-mockup__search-card"},(0,a.createElement)("div",{className:"gla-ads-mockup__search-card-placeholders"},(0,a.createElement)(E,{stroke:"thinner",width:"79",color:"gray-400"}),(0,a.createElement)(E,{stroke:"thinner",color:"gray-300"}),(0,a.createElement)(E,{width:"122"}),(0,a.createElement)(E,{width:"108"}),(0,a.createElement)(E,{width:"100"}),(0,a.createElement)(E,{width:"55"}))),(0,a.createElement)("div",{className:"gla-ads-mockup__search-card"},(0,a.createElement)("div",{className:"gla-ads-mockup__search-card-placeholders"},(0,a.createElement)(E,{stroke:"thinner",width:"79",color:"gray-400"}))))},function({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup gla-ads-mockup-map",style:{backgroundImage:`url(${M})`}},(0,a.createElement)(N,null),(0,a.createElement)(P.A,{size:45}),(0,a.createElement)(R,{product:e}))},function({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup gla-ads-mockup-display"},(0,a.createElement)("div",{className:"gla-ads-mockup__display-placeholders"},(0,a.createElement)(E,{stroke:"thinner",color:"gray-300"}),(0,a.createElement)(E,{stroke:"thinner",color:"gray-300",width:"146"}),(0,a.createElement)(E,{stroke:"thinner",color:"gray-300",width:"149"}),(0,a.createElement)(E,{stroke:"thinner",color:"gray-300",width:"135"})),(0,a.createElement)("div",{className:"gla-ads-mockup__display-product"},(0,a.createElement)("div",{className:"gla-ads-mockup__display-product-locator"},(0,a.createElement)(y,{product:e}),(0,a.createElement)("img",{className:"gla-ads-mockup__display-corner-buttons",src:F,alt:(0,o.__)("Simulated the info and close buttons at the corner of a Google ad","google-listings-and-ads")}),(0,a.createElement)("div",{className:"gla-ads-mockup__display-chevron-button"},(0,a.createElement)(G.A,{size:16}))),(0,a.createElement)(E,{stroke:"thinner",color:"gray-500"})),(0,a.createElement)("div",{className:"gla-ads-mockup__display-placeholders"},(0,a.createElement)(E,null),(0,a.createElement)(E,{width:"151"}),(0,a.createElement)(E,{width:"135"}),(0,a.createElement)(E,null),(0,a.createElement)(E,null),(0,a.createElement)(E,{width:"151"})))},function({product:e}){return(0,a.createElement)("div",{className:"gla-ads-mockup gla-ads-mockup-gmail"},(0,a.createElement)("div",{className:"gla-ads-mockup__gmail-header"},(0,a.createElement)("img",{height:"15",src:T,alt:(0,o.__)("Gmail Logo","google-listings-and-ads")}),(0,a.createElement)(N,{hideMenu:!0})),(0,a.createElement)(R,{product:e}),(0,a.createElement)(I,null),(0,a.createElement)(I,null),(0,a.createElement)(I,null),(0,a.createElement)(I,null),(0,a.createElement)(I,null))}],$=(0,s.forwardRef)((function({autoplay:e=!0},t){const[n,o]=(0,s.useState)(0),{second:i,callCount:p,startCountdown:h}=(0,u.A)(),{hasFinishedResolution:_,data:E}=(0,g.A)("getMCProductFeed",q),f=e&&_,A=(0,s.useCallback)((e=>{o((t=>(t+e+B.length)%B.length))}),[]);if((0,s.useEffect)((()=>{f&&0===i&&(p>0&&A(1),h(5))}),[f,i,p,h,A]),(0,s.useImperativeHandle)(t,(()=>({moveBy:A}))),!_)return(0,a.createElement)("div",{className:"gla-ads-mockup"},(0,a.createElement)(m.A,null));const y=B[n],v=function(e=[]){const t=c()((0,d.getSetting)("currency")),[n={}]=e,{title:a,price:o,image_url:s}=n,l={title:a,coverUrl:s,price:t.formatAmount(o),shopName:(0,d.getSetting)("siteTitle"),shopUrl:new URL((0,d.getSetting)("homeUrl")).host,shopLogoUrl:U.Th.siteLogoUrl};return Object.entries(l).forEach((([e,t])=>{t||(l[e]=L[e])})),l}(E?.products);return(0,a.createElement)(l.A,{className:"gla-campaign-preview"},(0,a.createElement)(r.A,{key:n,classNames:"gla-campaign-preview__transition-blur",timeout:500},(0,a.createElement)(y,{product:v})))}));var V=n(8687),j=n(8242),W=n(7892);function z(){const e=(0,s.useRef)(),t=t=>{const n=Number(t.currentTarget.dataset.step);e.current.moveBy(n)};return(0,a.createElement)(j.A.Card,{className:"gla-campaign-preview-card"},(0,a.createElement)(j.A.Card.Body,null,(0,a.createElement)(k.Flex,{align:"start",gap:9,direction:["column","row"]},(0,a.createElement)(k.FlexBlock,null,(0,a.createElement)(j.A.Card.Title,null,(0,o.__)("Preview product ad","google-listings-and-ads")),(0,a.createElement)("div",null,(0,o.__)("Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats.","google-listings-and-ads"))),(0,a.createElement)(k.FlexItem,null,(0,a.createElement)(k.Flex,{align:"center",gap:5},(0,a.createElement)(W.A,{className:"gla-campaign-preview-card__moving-button",icon:(0,a.createElement)(V.A,null),iconSize:16,onClick:t,"data-step":"-1"}),(0,a.createElement)($,{ref:e,autoplay:!1}),(0,a.createElement)(W.A,{className:"gla-campaign-preview-card__moving-button",icon:(0,a.createElement)(G.A,null),iconSize:16,onClick:t,"data-step":"1"}))))))}},4831:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(7723),s=n(7892);const l=({formProps:e,onClick:t})=>(0,a.createElement)(s.A,{isPrimary:!0,text:(0,o.__)("Continue","google-listings-and-ads"),disabled:!e.isValidForm,onClick:t})},4307:(e,t,n)=>{n.d(t,{A:()=>s,m:()=>o});var a=n(3905);function o(e,t){const n=[],o=e.assets;function s(e,...t){const a=t.map((t=>({...t,content:null,field_type:e})));n.push(...a)}return Object.values(a.ZD).forEach((e=>{const a=[t[e]].flat().filter(Boolean),l=[o[e]].flat().filter(Boolean);let r=0;a.forEach((t=>{do{const n=l[r];if(t===n?.content)break;n&&s(e,n),r+=1}while(r=l.length&&n.push({id:null,content:t,field_type:e}),r+=1})),s(e,...l.slice(r))})),n}function s(e,t){const[n,s]=t[a.Ms.DISPLAY_URL_PATH],l=o(e,t);return{final_url:t[a.Ms.FINAL_URL],path1:n,path2:s,assets:l}}},8864:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(1609);const o=e=>{const{className:t="",...n}=e;return(0,a.createElement)("span",{className:`gla-radio-helper-text ${t}`,...n})}},4301:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(1609),o=n(8846),s=n(6942),l=n.n(s);const r=e=>{const{label:t,helperText:n,className:s,...r}=e;return(0,a.createElement)("div",{className:l()("gla-searchable-select-control",s)},t&&(0,a.createElement)("div",{className:"gla-searchable-select-control__label"},t),(0,a.createElement)("div",{className:"gla-searchable-select-control__input"},(0,a.createElement)(o.SelectControl,{...r,help:""})),n&&(0,a.createElement)("div",{className:"gla-searchable-select-control__helper-text"},n))}},8242:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(1609),o=n(6942),s=n.n(o),l=n(6427);var r=n(8771);const i=({size:e="",...t})=>(0,a.createElement)(l.Card,{...t,size:e});i.Body=e=>{const{className:t,...n}=e;return(0,a.createElement)(l.CardBody,{className:s()("gla-section-card-body",t),...n})},i.Footer=e=>{const{children:t,...n}=e;return(0,a.createElement)(l.CardFooter,{className:"gla-section-card-footer",...n},t)},i.Title=e=>{const{className:t,...n}=e;return(0,a.createElement)(r.A.Title,{className:s()("gla-section-card-title",t),...n})};const c=({className:e,title:t,description:n,topContent:o,children:r,disabled:i,disabledLeft:c,verticalGap:d=6})=>{const u=s()("gla-section",!!i&&"gla-section--is-disabled",!!c&&"gla-section--is-disabled-left",e);return(0,a.createElement)("section",{className:u},(0,a.createElement)("header",{className:"gla-section__header"},o&&(0,a.createElement)("p",null,o),t&&(0,a.createElement)("h1",null,t),n),(0,a.createElement)(l.Flex,{className:"gla-section__body",direction:"column",gap:d},r))};c.Card=i;const d=c},6028:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(3741),s=n(8242);const l=()=>(0,a.createElement)(s.A.Card,null,(0,a.createElement)(s.A.Card.Body,null,(0,a.createElement)(o.A,null)))},3704:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(1609);const o=e=>{const{className:t="",...n}=e;return(0,a.createElement)("div",{className:`gla-step-content-actions ${t}`,...n})}},9826:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(8242);const s=({children:e})=>(0,a.createElement)(o.A,{className:"gla-step-content-footer",verticalGap:10},e)},3164:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(1609);const o=e=>{const{className:t="",title:n,description:o}=e;return(0,a.createElement)("header",{className:`gla-step-content-header ${t}`},(0,a.createElement)("h1",null,n),(0,a.createElement)("div",{className:"gla-step-content-header__description"},o))}},9370:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(1609);const o=e=>{const{className:t="",children:n,...o}=e;return(0,a.createElement)("div",{className:`gla-step-content ${t}`,...o},(0,a.createElement)("div",{className:"gla-step-content__container"},n))}},7539:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(8846),s=n(8687);const l=({title:e,backHref:t,helpButton:n,onBackButtonClick:l})=>(0,a.createElement)("div",{className:"gla-stepper-top-bar"},(0,a.createElement)(o.Link,{className:"components-button gla-stepper-top-bar__back-button",href:t,type:"wc-admin",onClick:l},(0,a.createElement)(s.A,null)),(0,a.createElement)("span",{className:"gla-stepper-top-bar__title"},e),n)},8771:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(1609),o=n(6942),s=n.n(o);const l=e=>{const{className:t="",...n}=e;return(0,a.createElement)("div",{className:`gla-subsection ${t}`,...n})};l.Title=e=>{const{className:t,...n}=e;return(0,a.createElement)("div",{className:s()("gla-subsection-title",t),...n})},l.Subtitle=e=>{const{className:t,...n}=e;return(0,a.createElement)("div",{className:s()("gla-subsection-subtitle",t),...n})},l.Body=e=>{const{children:t}=e;return(0,a.createElement)("div",{className:"gla-subsection-body"},t)},l.HelperText=e=>{const{className:t,children:n}=e;return(0,a.createElement)("div",{className:s()("gla-subsection-helper-text",t)},n)};const r=l},6397:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(4848);const s=({size:e=18})=>(0,a.createElement)(o.A,{className:"gla-success-icon",size:e})},4473:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(1609),o=n(4015),s=n(6942),l=n.n(s);const r=({size:e=18,className:t})=>(0,a.createElement)(o.A,{className:l()("gla-sync-icon",t),size:e})},9788:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(7723),s=n(8846),l=n(6277),r=n(8146);const i="dashboard-feature--campaign-assets";function c({referenceElementCssSelector:e}){const{tourChecked:t,setTourChecked:n}=(0,r.A)(i);if(t)return null;const c={steps:[{referenceElements:{desktop:e},meta:{heading:(0,a.createElement)("div",{className:"gla-campaign-assets-tour__heading"},(0,a.createElement)(l.A,null),(0,o.__)("Optimize your campaign","google-listings-and-ads"),(0,a.createElement)(s.Pill,null,(0,o._x)("New","A highlighting label behind the heading of the new feature","google-listings-and-ads"))),descriptions:{desktop:(0,a.createElement)(a.Fragment,null,(0,o.__)("Add images, headlines, and descriptions to drive better engagement and more sales.","google-listings-and-ads"),(0,a.createElement)("br",null),(0,a.createElement)("br",null),(0,o.__)("Edit your campaign to explore this new feature.","google-listings-and-ads"))}}}],options:{classNames:"gla-admin-page,gla-campaign-assets-tour",effects:{overlay:!1}},placement:"top",closeHandler:()=>n(!0)};return(0,a.createElement)(s.TourKit,{config:c})}},5246:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(1609),o=n(7723),s=n(6087),l=n(8846),r=n(8146);const i="rebranding-tour";function c(){const{tourChecked:e,setTourChecked:t}=(0,r.A)(i);if(e)return null;const n={steps:[{referenceElements:{desktop:'.toplevel_page_woocommerce-marketing a[href*="google"]',mobile:"#wp-admin-bar-menu-toggle"},meta:{heading:(0,a.createElement)("div",{className:"gla-rebranding-tour__heading"},(0,o.__)("New name, same great solution","google-listings-and-ads")),descriptions:{desktop:(0,a.createElement)(a.Fragment,null,(0,s.createInterpolateElement)((0,o.__)("Google Listings & Ads is now Google for WooCommerce.","google-listings-and-ads"),{strong:(0,a.createElement)("strong",null)}))}}}],options:{classNames:"gla-admin-page,gla-rebranding-tour",effects:{overlay:!1}},closeHandler:()=>t(!0)};return(0,a.createElement)(l.TourKit,{config:n})}},2848:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(1609),o=n(8846),s=n(6473);const l=e=>{const{eventName:t,eventProps:n,onClick:l=()=>{},...r}=e;return(0,a.createElement)(o.Link,{...r,onClick:e=>{t&&(0,s.ce)(t,n),l(e)}})}},6319:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(1609);function o({messages:e}){let t=e;return e?.length?(Array.isArray(e)||(t=[e]),(0,a.createElement)("ul",{className:"gla-validation-errors"},t.map((e=>(0,a.createElement)("li",{key:e},e))))):null}},850:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(1609),o=n(6942),s=n.n(o);const l={normal:!1,medium:"gla-vertical-gap-layout__medium",large:"gla-vertical-gap-layout__large",overlap:"gla-vertical-gap-layout__overlap"},r=e=>{const{className:t,size:n="normal",...o}=e;return(0,a.createElement)("div",{className:s()("gla-vertical-gap-layout",l[n],t),...o})}},7792:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1609),o=n(9031);const s=({size:e=18})=>(0,a.createElement)(o.A,{className:"gla-warning-icon",size:e})},4790:(e,t,n)=>{n.d(t,{s9:()=>h,LJ:()=>i,Ay:()=>_});var a=n(1609),o=n(7723);function s(e){return"yes"!==e.active?"":"yes"===e.owner?e.email:(0,o.__)("Successfully connected through Jetpack","google-listings-and-ads")}var l=n(559),r=n(4566);const i=({jetpack:e})=>(0,a.createElement)(l.A,{appearance:l.x.WPCOM,description:s(e),indicator:(0,a.createElement)(r.A,null)});var c=n(3832),d=n(3905),u=n(6520),g=n(7892),m=n(5640),p=n(6599);const h=()=>{const{createNotice:e}=(0,m.A)(),t=d.Th.mcSetupComplete?"reconnect":"setup-mc",n={next_page_name:t},s=(0,c.addQueryArgs)(`${u.RV}/jetpack/connect`,n),[r,{loading:i,data:h}]=(0,p.A)({path:s});return(0,a.createElement)(l.A,{appearance:l.x.WPCOM,description:(0,o.__)("Required to connect with Google","google-listings-and-ads"),indicator:(0,a.createElement)(g.A,{isSecondary:!0,loading:i||h,eventName:"gla_wordpress_account_connect_button_click",eventProps:{context:t},onClick:async()=>{try{const e=await r();window.location.href=e.url}catch(t){e("error",(0,o.__)("Unable to connect your WordPress.com account. Please try again later.","google-listings-and-ads"))}}},(0,o.__)("Connect","google-listings-and-ads"))})},_=({jetpack:e})=>"yes"===e.active?(0,a.createElement)(i,{jetpack:e}):(0,a.createElement)(h,null)},6727:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(6476),o=n(3905),s=n(7951);const l=()=>{const e=(0,s.A)()[o.Tj]?o.Tj:o.ds;return(0,a.getQuery)()?.issueType||e}},1968:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(5703);const o=()=>(0,a.getSetting)("adminUrl")},1209:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(7143),o=n(3658),s=n(3905),l=n(2224);const r="getAdsCampaigns",i=(...e)=>{const t=(0,l.A)(e);return(0,a.useSelect)((e=>{const{adsSetupComplete:n}=s.Th;if(!n)return{loading:!1,loaded:!0,data:[]};const a=e(o.U),l=a[r](...t);return{loading:a.isResolving(r,t),loaded:a.hasFinishedResolution(r,t),data:l}}),[t])}},7419:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(6087),o=n(4111),s=n.n(o),l=n(3772),r=n(1378);function i(){const e=(0,l.A)(),{googleAdsAccount:t}=(0,r.A)(),n=t?.currency||"",o=t?.symbol||"",i=(0,a.useMemo)((()=>({...e,code:n,symbol:o})),[e,n,o]),c=(0,a.useMemo)((()=>s()(i).formatAmount),[i]);return{adsCurrencyConfig:i,formatAmount:c}}},8519:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(7723),o=n(6087),s=n(1455),l=n.n(s),r=n(3658),i=n(5640);function c(){const{createAdsCampaign:e}=(0,r.j)(),{createNotice:t}=(0,i.A)(),[n,s]=(0,o.useState)(!1),c=(0,o.useCallback)((()=>l()({path:"/wc/gla/ads/setup/complete",method:"POST"}).catch((()=>(t("error",(0,a.__)("Unable to complete your ads setup. Please try again later.","google-listings-and-ads")),Promise.reject())))),[t]);return[(0,o.useCallback)(((t,n,a)=>(s(!0),e(t,n).then(c).then(a).catch((()=>s(!1))))),[e,c]),n]}},6599:(e,t,n)=>{n.d(t,{A:()=>p});var a=n(6087),o=n(1455),s=n.n(o),l=n(2224);const r="START",i="FINISH",c="ERROR",d="RESET",u={loading:!1,error:void 0,data:void 0,response:void 0,options:void 0},g=(e,t)=>{switch(t.type){case r:return{...e,loading:!0,options:t.options};case i:return{...e,loading:!1,data:t.data,response:t.response,options:t.options};case c:return{...e,loading:!1,error:t.error,response:t.response,options:t.options};case d:return t.state}},m=e=>{const{parse:t=!0}=e;return t},p=(e,t=u)=>{const n=(0,l.A)(e),o={...u,...t},[p,h]=(0,a.useReducer)(g,o);return[(0,a.useCallback)((async e=>{const t={...n,...e};h({type:r,options:t});try{const e=await s()({...t,parse:!1}),n=e.clone(),a=n.json&&await n.json();return h({type:i,data:a,response:e,options:t}),m(t)?a:e}catch(e){if("fetch_error"===e.code)throw h({type:c,error:e,response:void 0,options:t}),e;const n=e;let a;try{const e=n.clone();a=e.json?await e.json():new Error("No content body in fetch response.")}catch(e){a=new Error("Error parsing response.")}throw h({type:c,error:a,response:n,options:t}),m(t)?a:n}}),[n]),{...p,reset:e=>{h({type:d,state:{...o,...e}})}}]}},6453:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(6087),o=n(6599),s=n(2224);const l=e=>{const t=(0,s.A)(e),[n,l]=(0,o.A)(t,{loading:!0});return(0,a.useEffect)((()=>{t&&n()}),[n,t]),l}},9415:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(7143),o=n(6087),s=n(6520),l=n(3658),r=n(2224);const i=(e,...t)=>{const{invalidateResolution:n}=(0,l.j)(),i=(0,r.A)(t),c=(0,o.useCallback)((()=>{n(e,i)}),[n,e,i]);return(0,a.useSelect)((t=>{const{isResolving:n,hasFinishedResolution:a}=t(s.Ui),o=t(s.Ui)[e](...i);return{isResolving:n(e,i),hasFinishedResolution:a(e,i),data:o,invalidateResolution:c}}),[c,e,i])}},4679:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(7143),o=n(6520);const s=e=>(0,a.useSelect)((t=>{const{getAdsBudgetRecommendations:n,hasFinishedResolution:a}=t(o.Ui),s=n(e);let l,r=0;if(s){const{recommendations:e}=s;({daily_budget:r,country:l}=function(e){return e?e.reduce(((e,t)=>t.daily_budget>e.daily_budget?t:e)):null}(e))}return{data:s,highestDailyBudget:r,highestDailyBudgetCountryCode:l,hasFinishedResolution:a("getAdsBudgetRecommendations",[e])}}),[e])},1340:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(7723),o=n(9415),s=n(3905);const l=(0,a._x)(", ","the separator for concatenating the categories where the Attribute mapping rule is applied.","google-listings-and-ads"),r=e=>({key:e.id,label:e.name,value:e.id}),i=(e=[])=>e.map(r),c=e=>e.slice(0,s.vL).map((e=>e.label)).join(l),d=(e=[])=>{const{data:t,hasFinishedResolution:n}=(0,o.A)("getStoreCategories");if(!n)return{hasFinishedResolution:n,categories:[],selected:[],names:""};const s=e.filter((e=>!t.find((t=>t.id.toString()===e)))).map((e=>{return{id:e,name:(t=e,(0,a.sprintf)( // translators: %d: number of categories. // translators: %d: number of categories. (0,a.__)("Category ID %s (deleted)","google-listings-and-ads"),[t])),parent:0};var t})),l=[...t,...s],d=((e,t)=>e.map((e=>{const n=t.find((t=>t.id.toString()===e));return r(n)})))(e,l);return{hasFinishedResolution:n,selected:d,categories:i(l),names:c(d)}}},7133:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(6087);function o(e="single"){const t=(0,a.useRef)({}),[n,o]=(0,a.useState)(0);t.current.usingHandle=e,t.current[e]=t.current[e]||{callCount:0};const s=(0,a.useCallback)((n=>{o(n);const a=t.current[e],s=(new Date).getTime()+1e3*n;a.id&&clearInterval(a.id),a.updateSecond=()=>{let n=(s-(new Date).getTime())/1e3;n=Math.max(Math.round(n),0),t.current.usingHandle===e&&o(n),0===n&&clearInterval(a.id)},a.id=setInterval(a.updateSecond,1e3),a.callCount+=1}),[e]);(0,a.useEffect)((()=>{const{updateSecond:n}=t.current[e];n&&n()}),[e]),(0,a.useEffect)((()=>{const e=t.current;return()=>{Object.values(e).forEach((e=>clearInterval(e.id)))}}),[]);const{callCount:l}=t.current[e];return{second:n,callCount:l,startCountdown:s}}},6734:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(8537),o=n(5703);const s=()=>{const e={...(0,o.getSetting)("countries")};return Object.keys(e).forEach((t=>{e[t]=(0,a.decodeEntities)(e[t])})),e}},5128:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(6087),o=n(3577),s=n(3772);const l=e=>{const t=(0,s.A)();return(0,a.useMemo)((()=>{const n={...t,...e};return e=>(0,o.numberFormat)(n,e)}),[t,e])}},5640:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(7143);const o=()=>(0,a.useDispatch)("core/notices")},7541:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(6087),o=n(8468),s=n(2224),l=n(6473);function r(e,t){const n=(0,s.A)((0,o.pick)(t,l.E$.get(e))),[,r]=(0,a.useReducer)((e=>e+1),0);return(0,a.useEffect)((()=>{const t=`${l.CU}/${(0,o.uniqueId)()}`;return Object.keys(n).length&&l.JL.addFilter(e,t,(e=>({...e,...n}))),r(),()=>{l.JL.removeFilter(e,t)}}),[e,n]),(0,a.useCallback)((t=>l.JL.applyFilters(e,t)),[e])}},5530:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(7143),o=n(6520);const s=()=>(0,a.useSelect)((e=>{const t=e(o.Ui).getExistingGoogleAdsAccounts(),n=e(o.Ui).isResolving("getExistingGoogleAdsAccounts");return{existingAccounts:t,hasFinishedResolution:e(o.Ui).hasFinishedResolution("getExistingGoogleAdsAccounts"),isResolving:n}}),[])},2722:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>(0,a.A)("getExistingGoogleMCAccounts")},8e3:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(7143),o=n(3905),s=n(6520),l=n(7400),r=n(7401);const i=()=>{const{jetpack:e,isResolving:t,hasFinishedResolution:n}=(0,r.A)();return(0,a.useSelect)((a=>{if(!e||"no"===e.active)return{google:void 0,scope:(0,l.A)(o.Th.adsSetupComplete),isResolving:t,hasFinishedResolution:n};const{getGoogleAccount:r,isResolving:i,hasFinishedResolution:c}=a(s.Ui),d=r();return{google:d,scope:(0,l.A)(o.Th.adsSetupComplete,d?.scope),isResolving:i("getGoogleAccount"),hasFinishedResolution:c("getGoogleAccount")}}),[e,t,n])}},1378:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(7143),o=n(6087),s=n(6520),l=n(3658),r=n(3905),i=n(8e3);const c="getGoogleAdsAccount",d=()=>{const{google:e,isResolving:t,hasFinishedResolution:n}=(0,i.A)(),d=(0,l.j)(),u=(0,o.useCallback)((()=>{d.invalidateResolution(c,[])}),[d]);return(0,a.useSelect)((a=>{if(!e||"no"===e.active)return{googleAdsAccount:void 0,isResolving:t,hasFinishedResolution:n};const o=a(s.Ui),l=o[c](),i=o.isResolving(c),d=[r.Wn.CONNECTED,r.Wn.INCOMPLETE].includes(l?.status);return{googleAdsAccount:l,isResolving:i,refetchGoogleAdsAccount:u,hasFinishedResolution:o.hasFinishedResolution(c),hasGoogleAdsConnection:d}}),[e,t,n,u])}},6893:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(7143),o=n(6520);const s="getGoogleAdsAccountBillingStatus",l=()=>(0,a.useSelect)((e=>{const t=e(o.Ui);return{billingStatus:t[s](),hasFinishedResolution:t.hasFinishedResolution(s,[])}}),[])},1351:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(1378),o=n(5992);const s=()=>{const{hasGoogleAdsConnection:e,hasFinishedResolution:t}=(0,a.A)(),{hasAccess:n,step:s,hasFinishedResolution:l}=(0,o.A)();return t&&l?e&&n&&["","billing","link_merchant"].includes(s):null}},5992:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(7143),o=n(6520);const s="getGoogleAdsAccountStatus",l=()=>(0,a.useSelect)((e=>{const t=e(o.Ui),{hasAccess:n,inviteLink:a,step:l}=t[s]();return{hasAccess:n,inviteLink:a,step:l,hasFinishedResolution:t.hasFinishedResolution(s)}}),[])},1830:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(6087),o=n(3832),s=n(6520),l=n(6599);function r(e,t){const n=(0,a.useMemo)((()=>{const n={next_page_name:e,login_hint:t};return{path:(0,o.addQueryArgs)(`${s.RV}/google/connect`,n)}}),[e,t]);return(0,l.A)(n)}},7916:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(7143),o=n(6520),s=n(8e3),l=n(3905);const r="getGoogleMCAccount",i=()=>{const{google:e,scope:t,isResolving:n,hasFinishedResolution:i}=(0,s.A)();return(0,a.useSelect)((a=>{if(!e||"no"===e.active||!t.gmcRequired)return{googleMCAccount:void 0,isResolving:n,hasFinishedResolution:i,isPreconditionReady:!1,hasGoogleMCConnection:!1,isReady:!1};const s=a(o.Ui),c=s[r](),d=s.isResolving(r),u=Boolean(c?.id)&&[l.WR.CONNECTED,l.WR.INCOMPLETE].includes(c?.status),g=c?.status===l.WR.CONNECTED||c?.status===l.WR.INCOMPLETE&&"link_ads"===c?.step;return{googleMCAccount:c,isResolving:d,hasFinishedResolution:s.hasFinishedResolution(r),isPreconditionReady:!0,hasGoogleMCConnection:u,isReady:g}}),[e,t.gmcRequired,n,i])}},2224:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(8468),o=n(6087);const s=e=>{const t=(0,o.useRef)(e);return(0,a.isEqual)(t.current,e)||(t.current=e),t.current}},7401:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(7143),o=n(6520);const s=()=>(0,a.useSelect)((e=>({jetpack:e(o.Ui).getJetpackAccount(),isResolving:e(o.Ui).isResolving("getJetpackAccount"),hasFinishedResolution:e(o.Ui).hasFinishedResolution("getJetpackAccount")})),[])},6474:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(6087);const o={"full-page":["woocommerce-admin-full-screen","is-wp-toolbar-disabled","gla-full-page"],"full-content":["gla-full-content"]};function s(e){(0,a.useEffect)((()=>{if(!o.hasOwnProperty(e))return;const t=document.body.classList,n=o[e].filter((e=>!t.contains(e)));return t.add(...n),()=>{t.remove(...n)}}),[e])}},7240:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>{const e=(0,a.A)("getMCCountriesAndContinents");return{...e,data:e.data.countries}}},7951:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(3905),o=n(6425);const s=e=>{const t=Object.values(e).reduce(((e,t)=>e+t),0);return Number.isInteger(t)?t:void 0},l=()=>{const e={[a.Tj]:(0,o.A)(a.Tj)?.data?.total,[a.ds]:(0,o.A)(a.ds)?.data?.total};return{...e,total:s(e)}}},6425:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(3905),o=n(9415);const s=a.X4,l=(e=a.Tj,t=1,n=s)=>(0,o.A)("getMCIssues",{page:t,issue_type:e,per_page:n})},614:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(6087),o=n(9415),s=n(7133),l=n(6599),r=n(3658);const i=()=>{const{second:e,callCount:t,startCountdown:n}=(0,s.A)(),{invalidateResolutionForStoreSelector:i}=(0,r.j)(),{data:c,hasFinishedResolution:d,invalidateResolution:u,...g}=(0,o.A)("getMCProductStatistics"),m=!(!d||!c?.loading),p=!(!d||!c?.statistics),[h]=(0,l.A)({path:"/wc/gla/mc/product-statistics/refresh",method:"GET"}),_=(0,a.useCallback)((async()=>{await h(),u()}),[h,u]);return(0,a.useEffect)((()=>{m&&0===e&&(n(15),t>0&&u()),p&&t>0&&(n(0),i("getMCProductFeed"))}),[e,t,m,p,u,i,n]),{data:c,invalidateResolution:u,hasFinishedResolution:d,refreshStats:_,...g}}},3027:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>(0,a.A)("getMCSetup")},7437:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>(0,a.A)("getMappingAttributes")},771:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=e=>(0,a.A)("getMappingSources",e)},993:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=({page:e=1,perPage:t=10})=>(0,a.A)("getMappingRules",{page:e,perPage:t})},14:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(6087);const o={match:{url:"/google/dashboard"},wpOpenMenu:"toplevel_page_woocommerce-marketing"};function s(){return(0,a.useEffect)((()=>{window.wpNavMenuClassChange(o,o.match.url)}))}},1650:(e,t,n)=>{n.d(t,{A:()=>r});var a=n(6087),o=n(6476),s=n(8468);const l=()=>!0;function r(e,t,n=l){const{key:r}=(0,o.getHistory)().location;(0,a.useEffect)((()=>{let a=s.noop;return t&&(a=(0,o.getHistory)().block((t=>{const{location:o,retry:s}=t;let l=!0;n(function(e){return{...e,pathname:e.pathname.replace(/^(\/wp-admin)?\//,"")}}(o))&&(l=window.confirm(e)),l&&(a(),s())}))),()=>{a()}}),[r,e,t,n])}},1396:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(6087);const o=(e,t=1)=>{var n;const[o,s]=(0,a.useState)({}),l=(0,a.useCallback)((t=>{s((n=>({...n,[e]:t})))}),[e]);return{page:null!==(n=o[e])&&void 0!==n?n:t,setPage:l}}},33:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(6087),o=n(6599),s=n(7133);function l(e,t=10,n=!1){const{second:l,callCount:r,startCountdown:i}=(0,s.A)(),[c,{data:d}]=(0,o.A)(e),u=!d||!n,g=(0,a.useCallback)((()=>{const e=c();return e.finally((()=>i(t))),e}),[c,i,t]);return(0,a.useEffect)((()=>{0===l&&r>0&&u&&g()}),[l,r,g,u]),{start:g,data:d}}},5455:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(6087),o=n(3658),s=n(4716),l=n(8468);const r=(e,t)=>void 0!==e.id&&e.id===t.id||e.country===t.country;var i=n(6523);const c=()=>{const{data:e}=(0,i.A)(),{deleteShippingRates:t,upsertShippingRates:n}=(0,o.j)();return{saveShippingRates:(0,a.useCallback)((async a=>{const o=((e,t)=>((e,t)=>(0,l.differenceWith)(t,e,r))(e,t).map((e=>e.id)))(a,e);o.length&&await t(o);const i=(0,s.A)(a,e);i.length&&await n(i)}),[t,e,n])}}},5807:(e,t,n)=>{n.d(t,{A:()=>i});var a=n(6087),o=n(3658),s=n(5622),l=n(8468);var r=n(172);const i=()=>{const{data:e}=(0,s.A)(),{deleteShippingTimes:t,upsertShippingTimes:n}=(0,o.j)();return{saveShippingTimes:(0,a.useCallback)((async a=>{const o=((e,t)=>((e,t)=>(0,l.differenceBy)(t,e,"countryCode"))(e,t).map((e=>e.countryCode)))(a,e);o.length&&await t(o);const s=(i=a,c=e,(0,l.differenceWith)(i,c,l.isEqual));var i,c;if(s.length){const e=(0,r.A)(s).map((e=>n(e)));await Promise.all(e)}}),[t,e,n])}}},873:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(7143),o=n(3658);const s=()=>{const{saveSettings:e,syncSettings:t}=(0,o.j)();return{settings:(0,a.useSelect)((e=>e(o.U).getSettings()),[]),saveSettings:e,syncSettings:t}}},6523:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>(0,a.A)("getShippingRates")},5622:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>(0,a.A)("getShippingTimes")},5595:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(9415),o=n(6734);const s={address:"",address2:"",city:"",state:"",country:"",postcode:"",isMCAddressDifferent:null,isAddressFilled:null,missingRequiredFields:[]};function l(e="wc"){const{data:t,hasFinishedResolution:n,invalidateResolution:l}=(0,a.A)("getGoogleMCContactInformation"),r=(0,o.A)();let i=s;if(n&&t){const{is_mc_address_different:n,wc_address_errors:a}=t,o="wc"===e?t.wc_address:t.mc_address,s=o?.street_address||"",l=o?.locality||"",c=o?.region||"",d=o?.postal_code||"",[u,g=""]=s.split("\n"),m=r[o?.country]||"";i={countryCode:o?.country||"",address:u,address2:g,city:l,state:c,country:m,postcode:d,isAddressFilled:!a.length,isMCAddressDifferent:n,missingRequiredFields:a}}return{refetch:l,loaded:n,data:i}}},1456:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(7143),o=n(7916),s=n(6520);function l(){const{hasGoogleMCConnection:e}=(0,o.A)();return(0,a.useSelect)((t=>{if(!e)return!1;const n=t(s.Ui).getGoogleMCContactInformation();return!!n&&!n.wc_address_errors.length}),[e])}},7337:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(7143),o=n(314),s=n(5703);function l(){return(0,a.useSelect)((e=>{const t=e(o.OPTIONS_STORE_NAME),n="getOption",a=["woocommerce_default_country"],l=t[n](...a);let r=null,i=null;if(t.hasFinishedResolution(n,a)){const e=(0,s.getSetting)("countries");[r]=l.split(":"),i=e[r]}return{code:r,name:i}}),[])}},3772:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(6087),o=n(3577),s=n(5703);const l=()=>{const e=(0,s.getSetting)("currency");return(0,a.useMemo)((()=>({...e,formatNumber:(t,n=e.precision)=>{const a={...e,precision:n};return(0,o.numberFormat)(a,t)}})),[e])}},8859:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(9415);const o=()=>(0,a.A)("getTargetAudience")},5847:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(7143),o=n(3658),s=n(7240);const l=()=>{const{data:e,isResolving:t}=(0,s.A)();return(0,a.useSelect)((function(n){const{getTargetAudience:a,isResolving:s}=n(o.U),l=a(),r=s("getTargetAudience")||t,i=e&&Object.keys(e);function c(e){return"all"===e?.location?i:e?.countries}return{loading:r,data:c(l),targetAudience:l,getFinalCountries:c}}),[e,t])}},8146:(e,t,n)=>{n.d(t,{A:()=>l});var a=n(6087),o=n(3658),s=n(9415);const l=e=>{const t=(0,s.A)("getTour",e),{upsertTour:n}=(0,o.j)(),l=t.data?.checked;return{tourChecked:!t.hasFinishedResolution||Boolean(l),setTourChecked:(0,a.useCallback)((t=>{t!==l&&n({id:e,checked:t},!0)}),[e,l,n])}}},1016:(e,t,n)=>{n.d(t,{A:()=>c});var a=n(6087),o=n(6476),s=n(3905),l=n(3658),r=n(6520),i=n(6599);const c=()=>{const{google_wpcom_app_status:e,nonce:t}=(0,o.getQuery)(),{invalidateResolution:n}=(0,l.j)(),c=`${r.RV}/rest-api/authorize`,[d]=(0,i.A)({path:c,method:"PUT"}),u=(0,a.useCallback)((async()=>{try{await d({data:{status:e,nonce:t}}),n("getGoogleMCAccount",[])}catch(e){console.error(e.message)}}),[d,e,n,t]);(0,a.useEffect)((()=>{Object.values(s.ac).includes(e)&&async function(){await u(e)}()}),[e,u])}},70:(e,t,n)=>{n.d(t,{A:()=>d});var a=n(7723),o=n(6087),s=n(3658),l=n(6520),r=n(1378),i=n(6599),c=n(5640);const d=()=>{const{googleAdsAccount:e}=(0,r.A)(),{createNotice:t}=(0,c.A)(),{fetchGoogleAdsAccount:n,fetchGoogleAdsAccountStatus:d}=(0,s.j)(),[u,g]=(0,o.useState)(null),m=!e?.id,[p]=(0,i.A)({path:`${l.RV}/ads/accounts`,method:"POST",data:{id:e?.id||void 0}});return[(0,o.useCallback)((async()=>{g(m?"create":"update");try{await p({parse:!1})}catch(e){if(428!==e.status){const n=406===e.status?(0,a.__)("Error creating account: Account creation limit reached. Contact support for help.","google-listings-and-ads"):(0,a.__)("Unable to create Google Ads account. Please try again later.","google-listings-and-ads");t("error",n)}}await Promise.all([n(),d()]),g(null)}),[m,t,p,n,d]),{loading:null!==u,action:u}]}},2775:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(6476),o=n(2224);function s(){const e=(0,a.getQuery)();return(0,o.A)(e)}},5559:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(6087);const o=(e,t)=>{const n=(()=>{const[e,t]=(0,a.useState)(document.hasFocus());return(0,a.useEffect)((()=>{const e=()=>{t(!0)},n=()=>{t(!1)};return window.addEventListener("focus",e),window.addEventListener("blur",n),()=>{window.removeEventListener("focus",e),window.removeEventListener("blur",n)}}),[]),e})();(0,a.useEffect)((()=>{if(!n)return;e();const a=setInterval(e,1e3*t);return()=>clearInterval(a)}),[e,t,n])}},8606:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(7723);async function o(e,t){const n=(await Promise.allSettled(e)).reduce(((e,n,a)=>("rejected"===n.status&&t[a]&&e.push(t[a]),e)),[]);return function(e,t=!0){if(e.length){const n=(0,a._x)(", ","the separator for concatenating the messages of failed actions","google-listings-and-ads"),o=(0,a.sprintf)( // translators: 1: optional string when there are multiple failed actions, and it's a concatenated text of failed actions except for the last one. 2: the last one or the only failed action. // translators: 1: optional string when there are multiple failed actions, and it's a concatenated text of failed actions except for the last one. 2: the last one or the only failed action. (0,a._n)("There is an error in the following action: %1$s%2$s.","There are errors in the following actions: %1$s and %2$s.",e.length,"google-listings-and-ads"),e.slice(0,-1).join(n),e.at(-1)),s=t? // translators: text for the failed action(s). // translators: text for the failed action(s). (0,a.__)("%s Other changes have been saved. Please try again later.","google-listings-and-ads"): // translators: text for the failed action(s). // translators: text for the failed action(s). (0,a.__)("%s Please try again later.","google-listings-and-ads");return(0,a.sprintf)(s,o)}return null}(n,n.length{n.d(t,{Z:()=>o});var a=n(3905);const o=a.Th.dateFormat+(a.Th.dateFormat.trim()&&a.Th.timeFormat.trim()?", ":"")+a.Th.timeFormat},4716:(e,t,n)=>{n.d(t,{A:()=>s});var a=n(8468);const o=(e,t)=>{const n=["country","currency","rate","options.free_shipping_threshold"];return(0,a.isEqual)((0,a.at)(e,n),(0,a.at)(t,n))},s=(e,t)=>(0,a.differenceWith)(e,t,o)},9369:(e,t,n)=>{function a(e){return Object.entries(e).reduce(((e,[t,n])=>"not_synced"===t?e:e+n),0)}n.d(t,{A:()=>a})},9269:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(6476);const o=()=>{const e=(0,a.getQuery)();return"products"===e?.reportKey?"products":"programs"}},172:(e,t,n)=>{n.d(t,{A:()=>a});const a=e=>{const t=new Map;return e.forEach((e=>{const{countryCode:n,time:a,maxTime:o}=e,s=`${a}-${o}`,l=t.get(s)||{countries:[],time:a,maxTime:o};l.countries.push(n),t.set(s,l)})),Array.from(t.values())}},4297:(e,t,n)=>{n.d(t,{A:()=>a});const a=(e,t,n)=>{const{innerWidth:a,innerHeight:o,screenX:s,screenY:l,screen:r}=e,i=Math.min(t,r.availWidth),c=Math.min(n,r.availHeight);return`popup=1,left=${(a-i)/2+s},top=${(o-c)/2+l},width=${i},height=${c}`}},3921:(e,t,n)=>{n.d(t,{A:()=>a});const a=()=>!!window.wcTracks?.isEnabled},5744:(e,t,n)=>{n.d(t,{A:()=>o});const a="undefined"!=typeof window&&"localStorage"in window,o={get:e=>a?window.localStorage.getItem(e):null,set:(e,t)=>a?window.localStorage.setItem(e,t):null,remove:e=>a?window.localStorage.removeItem(e):null}},4391:(e,t,n)=>{n.d(t,{A:()=>o});var a=n(3905);function o(e){return e?.status===a.Wn.CONNECTED||["link_merchant","billing"].includes(e?.step)}},7400:(e,t,n)=>{n.d(t,{A:()=>o});const a={CONTENT:"https://www.googleapis.com/auth/content",SITE_VERIFICATION_VERIFY_ONLY:"https://www.googleapis.com/auth/siteverification.verify_only",AD_WORDS:"https://www.googleapis.com/auth/adwords"};function o(e,t=[]){const n={adsRequired:t.includes(a.AD_WORDS)};return n.gmcRequired=t.includes(a.CONTENT)&&t.includes(a.SITE_VERIFICATION_VERIFY_ONLY),n.glaRequired=e?n.gmcRequired&&n.adsRequired:n.gmcRequired,n}},9959:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/ff593be89bcb7ad1ab7e.benefits.png"},9899:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/75da8376b1a005182ed2.hero.png"},4902:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/3905a197de7922d5b82a.img-dashboard.svg"},1034:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/563fd40c029bebb36783.img-free-listings.svg"},9351:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/c94ebde75361208ddf3a.img-product-promotion.svg"},1394:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/1ad852c48821e91bfd6e.img-quote.svg"},1948:(e,t,n)=>{e.exports=n.p+"images/js/src/images/get-started/d7e2d04ea7d6535fecf8.motivation.svg"},8806:(e,t,n)=>{e.exports=n.p+"images/js/src/images/fb15f79da797ad9cca81.google-free-listings.png"},9692:(e,t,n)=>{e.exports=n.p+"images/js/src/images/logo/64742f6405be8486218c.google-logo.svg"},8317:(e,t,n)=>{e.exports=n.p+"images/js/src/images/logo/9a968634c60ce598aae3.woocommerce-logo.svg"},7043:(e,t,n)=>{e.exports=n.p+"images/js/src/images/c4325f35cdc65f85a7c1.success-guide-header.svg"}}]);PK!Jžvvjs/build/dashboard.cssnu[.app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .app-tab-nav__tabs{align-items:stretch;box-shadow:inset 0 -1px 0 #ccc;display:flex;flex-wrap:wrap;margin-bottom:var(--main-gap)}.app-tab-nav__tabs-item{background:#0000;border:none;border-radius:0;box-shadow:inset 0 -1px 0 #ccc;box-sizing:border-box;cursor:pointer;font-weight:500;height:48px;margin-left:0;padding:3px 16px}.app-tab-nav__tabs-item:after{content:attr(data-label);display:block;height:0;overflow:hidden;speak:none;visibility:hidden}.app-tab-nav__tabs-item:focus:not(:disabled){box-shadow:inset 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) #0000,inset 0 -1.5px 0 0 var(--wp-admin-theme-color);position:relative}.app-tab-nav__tabs-item.is-active:before{border-bottom:1.5px solid #0000;bottom:1px;content:"";left:0;position:absolute;right:0;top:0}.app-tab-nav__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),inset 0 -1.5px 0 0 var(--wp-admin-theme-color)} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-gtin-migration__link{cursor:pointer;text-decoration:underline} .gla-summary-card>div:last-child{flex:1}.gla-summary-card__body{align-items:center;background-color:#f8f9fa;border:1px solid #e0e0e0;color:#757575;display:grid;flex:1;gap:16px;grid-auto-flow:row;height:100%;justify-items:center;min-height:117px;padding:var(--main-gap);text-align:center}.gla-summary-card__body>*{margin:0}.gla-summary-card .woocommerce-summary{align-items:center;flex:1;margin:0} .gla-app-text{font-weight:400;margin:0}.gla-app-text--body{font-size:13px;line-height:20px}.gla-app-text--caption,.gla-app-text--label{font-size:12px;line-height:16px}.gla-app-text--label{font-weight:600}.gla-app-text--title-small{font-size:20px;line-height:28px}.gla-app-text--title-medium{font-size:24px;line-height:32px}.gla-app-text--subtitle{font-size:16px;font-weight:600;line-height:24px}.gla-app-text--subtitle-small{font-size:14px;line-height:20px} .gla-guide__page-content{padding:0 24px}.gla-guide__page-content__header{color:#757575;font-size:24px;font-weight:400;line-height:32px;margin:0 0 24px}.gla-guide__page-content__body{color:#757575;font-size:16px;line-height:1.5}.gla-guide__page-content__body>p{font-size:inherit;line-height:inherit;margin:1.5em 0}.gla-guide__page-content__body>cite{display:block;font-size:11px;font-style:normal;line-height:18px}.gla-guide__page-content__link{text-decoration:none} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .gla-campaign-preview{height:270px;position:relative;width:205px}.gla-campaign-preview .gla-ads-mockup{position:absolute}.gla-campaign-preview__transition-blur-enter{opacity:0}.gla-campaign-preview__transition-blur-enter-active{opacity:1;transition:opacity .5s ease-in-out}.gla-campaign-preview__transition-blur-exit{opacity:1}.gla-campaign-preview__transition-blur-exit-active{opacity:0;transition:opacity .5s ease-in-out}.gla-ads-mockup{background-color:#fff;border:1px solid #e0e0e0;border-radius:4px;height:270px;overflow:hidden;padding:10px;width:205px}.gla-ads-mockup .app-spinner{align-items:center;height:100%}.gla-ads-mockup__placeholder{border-radius:4px;height:3px}.gla-ads-mockup__placeholder--thinnest{height:1px}.gla-ads-mockup__placeholder--thinner{height:2px}.gla-ads-mockup__placeholder--thicker{height:4px}.gla-ads-mockup__placeholder--gray-100{background-color:#f0f0f0}.gla-ads-mockup__placeholder--gray-200{background-color:#e0e0e0}.gla-ads-mockup__placeholder--gray-300{background-color:#ddd}.gla-ads-mockup__placeholder--gray-400{background-color:#ccc}.gla-ads-mockup__placeholder--gray-500{background-color:#bbb}.gla-ads-mockup__placeholder--blue{background-color:#4285f4}.gla-ads-mockup__scaled-text{font-size:20px;height:1em;line-height:.9;margin-bottom:-.5em;margin-right:-100%;overflow:hidden;text-overflow:ellipsis;transform:scale(.5);transform-origin:top left;white-space:nowrap}.gla-ads-mockup__scaled-text--smaller{font-size:18px}.gla-ads-mockup__scaled-text--larger{font-size:22px}.gla-ads-mockup__scaled-text--gray-700{color:#757575}.gla-ads-mockup__scaled-text--gray-800{color:#2f2f2f}.gla-ads-mockup__scaled-text--blue{color:#4285f4}.gla-ads-mockup__scaled-text--ad-badge{height:auto;line-height:1;margin-bottom:-.6em}.gla-ads-mockup__scaled-text--ad-badge:before{background-color:#f0b849;border-radius:6px;color:#fff;content:"AD";display:inline-block;font-size:16px;margin-right:12px;padding:3px}.gla-ads-mockup__product-cover{aspect-ratio:186/143;background:50%/cover no-repeat;width:100%}.gla-ads-mockup-display .gla-ads-mockup__product-cover{height:126px}.gla-ads-mockup__shop-logo{background:50%/cover no-repeat;flex:0 0 auto;height:44px;width:44px}.gla-ads-mockup__search-bar{align-items:center;border:1px solid #e0e0e0;display:flex;flex-direction:row-reverse;height:23px;justify-content:space-between;padding:0 8px;fill:#ccc;background-color:#fff}.gla-ads-mockup-search .gla-ads-mockup__search-bar{border-radius:29px}.gla-ads-mockup-map .gla-ads-mockup__search-bar{border-radius:4.6px;height:27px;margin:10px}.gla-ads-mockup-gmail .gla-ads-mockup__search-bar{border-radius:3.7px;flex:1 1;height:19px}.gla-ads-mockup__search-bar-menu{display:flex;flex-direction:column;height:9px;justify-content:space-between;width:13px}.gla-ads-mockup__search-bar-menu[hidden]{display:none}.gla-ads-mockup__product-banner{align-items:flex-start;display:flex;gap:10px;padding:10px}.gla-ads-mockup-gmail .gla-ads-mockup__product-banner{border:1px solid #e0e0e0;border-radius:2px;flex-direction:row-reverse}.gla-ads-mockup__product-banner-info{display:flex;flex-direction:column;height:42px;justify-content:space-between;overflow:hidden}.gla-ads-mockup__tab-list{align-items:center;display:flex;justify-content:space-between}.gla-ads-mockup__tab-list>.gla-ads-mockup__placeholder{margin-top:16px;min-width:25px}.gla-ads-mockup__tab-item-with-logo{text-align:center;width:44px}.gla-ads-mockup__tab-item-with-logo>img{display:block;margin:0 auto 6px}.gla-ads-mockup__shopping-product{border:1px solid #e0e0e0;border-radius:4px;margin-top:6px;overflow:hidden}.gla-ads-mockup__shopping-product-info{display:flex;flex-direction:column;gap:8px;padding:8px 10px 10px}.gla-ads-mockup__youtube-header{padding:4px 0}.gla-ads-mockup__youtube-header>img{display:block}.gla-ads-mockup__youtube-product{margin-top:6px}.gla-ads-mockup__youtube-learn-more-row{align-items:center;background-color:#f0f6fc;display:flex;font-weight:600;padding:3px 6px;fill:#4285f4}.gla-ads-mockup__youtube-learn-more-row>div{flex:1 0;line-height:0}.gla-ads-mockup__youtube-product-info{display:flex;flex-direction:column;gap:8px;padding:8px 0}.gla-ads-mockup__search-header{display:flex;justify-content:center;margin:4px 0 8px}.gla-ads-mockup__search-keywords{display:flex;flex-wrap:wrap;gap:4px 11px;justify-content:space-between;margin:8px 0}.gla-ads-mockup__search-card{border:1px solid #ddd;border-radius:4px;margin-top:8px;padding:7.5px}.gla-ads-mockup__search-card+.gla-ads-mockup__search-card{border-color:#f0f0f0}.gla-ads-mockup__search-card:last-child{opacity:.5}.gla-ads-mockup__search-card-header{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}.gla-ads-mockup__search-card-placeholders{min-height:38px}.gla-ads-mockup-map,.gla-ads-mockup__search-card-placeholders{display:flex;flex-direction:column;justify-content:space-between}.gla-ads-mockup-map{background:top no-repeat border-box;padding:0;position:relative}.gla-ads-mockup-map .gridicons-location{left:99px;position:absolute;top:116px;fill:#f86368}.gla-ads-mockup__display-product{border:1px solid #e0e0e0;margin:12px 0}.gla-ads-mockup__display-product .gla-ads-mockup__placeholder{margin:18px 10px 12px}.gla-ads-mockup__display-product-locator{position:relative}.gla-ads-mockup__display-corner-buttons{position:absolute;right:0;top:0}.gla-ads-mockup__display-chevron-button{align-items:center;aspect-ratio:1/1;background-color:#4285f4;border-radius:50%;display:flex;height:22px;justify-content:center;left:50%;position:absolute;top:100%;transform:translate(-50%,-50%);fill:#fff}.gla-ads-mockup__display-placeholders{display:flex;flex-direction:column;gap:4.66px}.gla-ads-mockup__gmail-header{align-items:center;display:flex;gap:10px;margin:5px 0 12px;padding-left:1px}.gla-ads-mockup__mail-item{display:flex;flex-direction:column;height:19px;justify-content:space-around;margin-top:10px;padding-left:28px;position:relative}.gla-ads-mockup__mail-item:before{aspect-ratio:1/1;background-color:#f0f0f0;border-radius:50%;content:"";height:100%;left:0;position:absolute} .gla-section-card-body{padding:var(--large-gap)} .gla-section-card-footer{padding:calc(var(--large-gap)/2) var(--large-gap)}.gla-section-card-footer[hidden]{display:none} .gla-subsection-title{font-size:14px;font-style:normal;font-weight:600;letter-spacing:0;line-height:20px;margin-bottom:8px;position:relative;text-align:left} .gla-subsection-body{font-size:13px;font-style:normal;font-weight:400;line-height:16px} .gla-subsection-helper-text{font-size:12px;font-style:italic;font-weight:400;letter-spacing:0;line-height:16px} .gla-subsection-subtitle{color:#757575;font-size:12px;line-height:16px;margin-bottom:4px} .gla-subsection:not(:first-child){margin-top:var(--main-gap)} .gla-section-card-title{margin-bottom:calc(var(--main-gap)/3*2)} .gla-section{display:flex;flex-direction:column;margin-bottom:var(--large-gap)}.gla-section--is-disabled,.gla-section--is-disabled-left .gla-section__header{opacity:.5}@media(min-width:600px){.gla-section{flex-direction:row;gap:var(--main-gap)}.gla-section__header{padding-top:var(--main-gap);width:33%}}.gla-section__header h1{font-size:16px;font-style:normal;font-weight:600;margin-bottom:8px;padding:0}.gla-section__header p{line-height:16px;margin:0 0 8px}.gla-section .gla-section__body{flex:1} .gla-campaign-preview-card .gla-section-card-title{margin-bottom:8px}.gla-campaign-preview-card__moving-button.components-button.has-icon{background-color:#f0f0f0;border-radius:50%;height:auto;min-width:0;padding:4px} .gla-vertical-gap-layout{display:flex;flex-direction:column;gap:calc(var(--main-gap)/2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__medium{gap:calc(var(--main-gap)/3*2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__large{gap:var(--main-gap)}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap{gap:0}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap>:not(:first-child){margin-top:-1px} .gla-paid-features__content{flex-direction:column}@media(min-width:600px){.gla-paid-features__content{align-items:flex-start;flex-direction:row;gap:16px}}.gla-paid-features__feature-list{color:#2f2f2f;display:flex;flex-direction:column;gap:16px;line-height:16px;text-align:left}.gla-paid-features__feature-list .gridicon{fill:#4ab866}.gla-paid-features .gla-free-ad-credit-claim{align-items:flex-start;background-color:#fff;color:#1e1e1e;display:flex;gap:8px;padding:16px 12px;text-align:left}.gla-paid-features .gla-free-ad-credit-claim .gridicon{fill:#008a20;flex:0 0 auto}.gla-paid-features .components-button{align-self:flex-end} @media(min-width:600px){.gla-campaign-creation-success-guide{max-width:517px}}.gla-campaign-creation-success-guide .components-modal__header{background:none;border-bottom:none;margin:0;position:absolute;width:100%}.gla-campaign-creation-success-guide .components-modal__content{margin-top:0;padding-left:0;padding-right:0;padding-top:0}.gla-campaign-creation-success-guide .app-modal__footer{margin:var(--main-gap) var(--main-gap) 0}.gla-campaign-creation-success-guide__header-image{background-color:#f7edf7;display:flex;justify-content:center;margin-bottom:var(--main-gap)} .app-table-card-div .components-card__header h2{margin-right:8px}.app-table-card-div .components-card__header .woocommerce-table__actions{justify-content:flex-start}.app-table-card-div .components-card__body .woocommerce-table__table .components-base-control__field{margin-bottom:0} @media(min-width:600px){.gla-remove-program-modal{max-width:360px}} @media(min-width:600px){.gla-edit-program-prompt-modal{max-width:360px}} .gla-all-programs-table-card .components-card__header .woocommerce-table__actions{justify-content:flex-end}.gla-all-programs-table-card .components-card-footer .woocommerce-pagination{display:none}.gla-all-programs-table-card .program-actions{display:flex;gap:12px;justify-content:space-between} @media(min-width:600px){.gla-pause-program-modal{max-width:360px}} .app-standalone-toggle-control{display:inline-block}.app-standalone-toggle-control .components-base-control{margin-bottom:0}.app-standalone-toggle-control .components-toggle-control .components-base-control__field .components-form-toggle{margin-right:0}.app-standalone-toggle-control .components-toggle-control .components-base-control__field .components-form-toggle input[disabled]{pointer-events:none} .gla-tooltip__children-container{cursor:help;display:inline-block}.gla-tooltip__children-container>span{cursor:auto}.gla-admin-page .disabled-element-wrapper:has(.gla-tooltip__children-container){display:inline-block} .gla-campaign-assets-tour__heading{align-items:center;display:flex;gap:8px;fill:#4ab866}.gla-campaign-assets-tour__heading .woocommerce-pill{background-color:var(--wp-admin-theme-color);border:none;color:#fff;margin-left:4px}.gla-campaign-assets-tour .components-card__footer{padding-top:0}.gla-campaign-assets-tour .components-card__footer>*{display:none} .gla-stepper-top-bar{align-items:center;background-color:#fff;box-shadow:inset 0 -1px 0 0 #ccc;display:flex;min-height:64px}.gla-stepper-top-bar .components-button{align-self:stretch;height:auto}.gla-stepper-top-bar__back-button{padding:0 calc(var(--main-gap)/2)}.gla-stepper-top-bar__title{flex:1;font-size:16px;letter-spacing:0} .AnC9WXFuKgCBURYIRcRY{display:flex;flex-direction:column;gap:4px;justify-content:center} .gla-step-content{display:flex;justify-content:center}.gla-step-content .gla-step-content__container{flex:1;margin:var(--large-gap);max-width:1032px} .gla-step-content-header{align-items:center;display:flex;flex-direction:column;gap:12px;margin:auto;margin-bottom:var(--large-gap);max-width:600px;text-align:center}.gla-step-content-header h1{font-size:32px;padding:0}.gla-step-content-header__description{line-height:16px} .gla-step-content-actions{display:flex;gap:20px;justify-content:flex-end}.gla-step-content-actions[hidden]{display:none} .gla-google-ads-billing-setup-card .components-card__body{display:flex;gap:16px}.gla-google-ads-billing-setup-card__description{color:#000;display:flex;flex-direction:column;gap:16px}.gla-google-ads-billing-setup-card__description__helper{color:#757575;font-style:italic} .gla-google-ads-billing-card__success-status{background-color:#eff9f1;color:#1e1e1e;line-height:2em;padding:12px}.gla-google-ads-billing-card__success-status .gridicon{fill:#008a20;margin:0 12px 0 4px} .gla-budget-section__card-body{flex-direction:column}.gla-budget-section__card-body,.gla-budget-section__card-body__cost{display:flex;gap:var(--main-gap)}.gla-budget-section__card-body__cost>*{flex:1}.gla-budget-section .components-input-control__suffix{margin-right:16px} .gla-budget-recommendation__low-budget{align-items:center;display:flex;font-style:italic;gap:calc(var(--main-gap)/3);margin-bottom:calc(var(--main-gap)/2)}.gla-budget-recommendation__low-budget>svg{flex:0 0 auto}.gla-budget-recommendation .components-tip{background-color:#f0f6fc;padding:12px 16px}.gla-budget-recommendation .components-tip>p{font-size:inherit;line-height:20px}.gla-budget-recommendation .components-tip>svg{align-self:auto;margin:4px 10px 0 0}.gla-budget-recommendation .components-tip,.gla-budget-recommendation__low-budget{color:#000;font-size:12px}.gla-budget-recommendation .components-tip>svg,.gla-budget-recommendation__low-budget>svg{fill:#1e1e1e} .app-input-control .components-flex-item{margin-right:0;max-width:100%;width:100%}.app-input-control .components-flex-item label.components-input-control__label{color:#757575!important;white-space:normal!important}.app-input-control--no-pointer-events{pointer-events:none}.app-input-control__character-count{color:#757575;font-size:12px;line-height:16px;margin-top:2px;text-align:right}.app-input-control--error-character-count .components-input-control .components-input-control__container .components-input-control__backdrop,.app-input-control.has-error .components-input-control__backdrop{border-color:#cc1818;box-shadow:none}.app-input-control--error-character-count .app-input-control__character-count,.app-input-control.has-error .components-base-control__help{color:#cc1818} .gla-faqs-panel .components-panel__row{align-items:flex-start;flex-direction:column;gap:1.5em}.gla-faqs-panel .components-panel__row p{margin:0}.gla-faqs-panel .components-panel__body-title .components-panel__body-toggle.components-button{padding-bottom:20px;padding-top:20px} .gla-free-ad-credit{align-items:center;background-color:#dcf7dd;color:#000;display:flex;gap:calc(var(--main-gap)/3*2);padding:calc(var(--main-gap)/3*2)}.gla-free-ad-credit svg{flex:0 0 auto;fill:#008a20}.gla-free-ad-credit__title{font-size:13px;font-weight:600;margin-bottom:8px}.gla-free-ad-credit__description{font-size:12px;font-style:italic} .gla-free-ad-credit-country-modal{max-width:600px;overflow:auto}.gla-free-ad-credit-country-modal table{border-spacing:calc(var(--main-gap)/3*2) 0;margin-left:calc(var(--large-gap)*2)}.gla-free-ad-credit-country-modal table tbody td:first-child{font-weight:600;text-align:right} .gla-paid-ads-features-section .woocommerce-pill{color:#757575}.gla-paid-ads-features-section .gla-section-card-title{color:#1e1e1e;font-size:20px;font-weight:400;line-height:28px;margin-bottom:12px}@media(max-width:600px){.gla-paid-ads-features-section__content{flex-direction:column;gap:16px}}.gla-paid-ads-features-section__subtitle{color:#2f2f2f;font-size:14px;line-height:20px}.gla-paid-ads-features-section__feature-list{color:#2f2f2f;display:flex;flex-direction:column;gap:16px;line-height:16px;margin:24px 0 16px}.gla-paid-ads-features-section__feature-list .gridicon{fill:#5ec862}.gla-paid-ads-features-section__cite{color:#949494;font-size:12px;font-style:normal} .gla-account-card{line-height:16px}.gla-account-card--is-disabled{opacity:.5}.gla-account-card--is-expanded-detail .gla-account-card__indicator{grid-area:1/3}.gla-account-card--is-expanded-detail .gla-account-card__detail{grid-area:2/2/auto/span 2}.gla-account-card__styled--align-top{align-self:flex-start}.gla-account-card__body-layout{align-items:center;display:grid;grid-template-columns:auto 1fr auto}.gla-account-card__icon{grid-area:1/1/span 2;line-height:0;margin-right:16px}.gla-account-card__subject{grid-area:1/2}.gla-account-card__indicator{grid-area:1/3/span 2;margin-left:16px}.gla-account-card__indicator--align-to-detail{grid-area:2/3;margin-top:12px}.gla-account-card__detail{grid-area:2/2;margin-top:12px}.gla-account-card__actions{grid-area:3/2/auto/span 2;margin-top:12px}.gla-account-card__title{color:#000;margin:0}.gla-account-card__description{align-items:flex-start;color:#1e1e1e;display:flex;flex-direction:column;gap:1em;margin-top:4px}.gla-account-card__description>p{margin:0}.gla-account-card__helper{color:#757575;font-size:12px;font-style:italic;margin-top:4px}.gla-account-card .components-card__footer{padding:calc(var(--main-gap)/3*2) var(--main-gap)}.gla-account-card .components-card__footer>.components-button.is-link{min-height:36px;padding:6px 12px}.gla-account-card .components-notice.is-error{background-color:#f8ebea;margin:0}.gla-account-card .components-notice.is-success{background-color:#edfaef;border:0;font-size:12px;margin:0 var(--large-gap) var(--main-gap);padding:16px}@media(max-width:600px){.gla-account-card__body-layout{align-items:flex-start;display:flex;flex-direction:column}.gla-account-card__body-layout>div{margin:8px}} .gla-searchable-select-control__helper-text:not(:last-child),.gla-searchable-select-control__input:not(:last-child){margin-bottom:calc(var(--main-gap)/3)}.gla-searchable-select-control__label{color:#757575;padding-bottom:4px}.gla-searchable-select-control__helper-text{font-size:12px;font-style:italic;line-height:16px}.gla-searchable-select-control i.material-icons-outlined{display:none}.gla-searchable-select-control .woocommerce-select-control .components-base-control{height:unset}.gla-searchable-select-control .woocommerce-select-control .components-base-control .woocommerce-select-control__control-input,.gla-searchable-select-control .woocommerce-select-control .components-base-control .woocommerce-select-control__tags{margin:0}.gla-searchable-select-control .woocommerce-select-control .components-base-control .woocommerce-tag{max-height:24px}.gla-searchable-select-control .woocommerce-select-control .woocommerce-select-control__listbox{top:unset}.gla-searchable-select-control .woocommerce-select-control .is-disabled .woocommerce-select-control__tags .woocommerce-tag__remove,.gla-searchable-select-control .woocommerce-select-control .is-disabled+.woocommerce-select-control__tags .woocommerce-tag__remove{display:none}.gla-searchable-select-control .woocommerce-select-control .is-disabled .woocommerce-select-control__tags .woocommerce-tag__text,.gla-searchable-select-control .woocommerce-select-control .is-disabled+.woocommerce-select-control__tags .woocommerce-tag__text{border-radius:12px;padding:0 8px} .gla-assets-loader.gla-searchable-select-control{flex-grow:1}.gla-assets-loader.gla-searchable-select-control .components-base-control{display:flex;flex-direction:row-reverse;height:36px}.gla-assets-loader.gla-searchable-select-control .woocommerce-select-control__control-input{font-size:13px;padding-left:0}.gla-assets-loader.gla-searchable-select-control .woocommerce-select-control__listbox .components-button{display:block;height:unset;line-height:16px;min-height:unset;padding:12px}.gla-assets-loader.gla-searchable-select-control .woocommerce-select-control__listbox .components-button:disabled{background:unset;color:inherit}.gla-assets-loader .gla-searchable-select-control__label{align-items:center;color:#000;display:flex;gap:8px;padding-bottom:8px}.gla-assets-loader .gla-searchable-select-control__label .woocommerce-spinner{height:1em;min-width:unset;width:1em}.gla-assets-loader__option-title{color:#1e1e1e;font-size:13px;margin-bottom:4px}.gla-assets-loader__option-url{color:#757575;font-size:11px} .gla-final-url-card .components-card__body,.gla-final-url-card .components-card__footer{padding:32px}.gla-final-url-card--has-selected-url .components-card__footer{padding:24px}.gla-final-url-card .gla-account-card__description{color:#757575} .gla-validation-errors{color:#cc1818;font-size:12px;line-height:16px;margin:0;width:100%}.gla-validation-errors:not(:first-child){margin-top:16px}.gla-validation-errors:not(:last-child){margin-bottom:16px}.gla-validation-errors>li{list-style:disc inside;margin:0;padding-left:.5em}.gla-validation-errors>li:only-child{list-style:none;padding-left:0} .gla-decorated-error-message[data-error-message]:before{color:#cc1818;content:attr(data-error-message);float:left;font-size:14px;height:32px;line-height:32px;margin-top:15px} .gla-add-asset-item-button.has-icon{padding:4px} .gla-images-selector__image-list{display:flex;flex-wrap:wrap;gap:12px}.gla-images-selector__image-list:not(:empty){margin-bottom:16px}.gla-images-selector__image-item{display:flex;position:relative}.gla-images-selector__image{max-height:100%;max-width:100%}.gla-admin-page .gla-images-selector__replace-image-button{background-color:#fff;border:1px solid #e0e0e0;border-radius:2px;height:100px;justify-content:center;overflow:hidden;padding:0;width:100px}.gla-admin-page .gla-images-selector__remove-image-button{color:#bbb;display:none;position:absolute;right:0;top:0;transform:translate(50%,-50%)}.gla-admin-page .gla-images-selector__remove-image-button:after{background-color:#fff;content:"";height:30%;margin:auto;position:absolute;width:30%;z-index:-1}.gla-admin-page .gla-images-selector__image-item:hover .gla-images-selector__remove-image-button{display:flex}.gla-images-selector .components-popover__content{text-align:left;white-space:normal;width:200px}.gla-images-selector .components-popover__content>div{padding:.25em .5em} .gla-texts-editor .components-input-control .components-input-control__container .components-input-control__input{height:36px;padding-left:16px;padding-right:16px}.gla-texts-editor__text-list{display:flex;flex-direction:column;gap:16px}.gla-texts-editor__text-list:not(:empty){margin-bottom:16px}.gla-texts-editor__text-item{align-items:flex-start;display:flex;gap:28px}.gla-texts-editor__text-input{flex:1 1}.gla-texts-editor__remove-text-button-anchor{height:36px;position:relative;width:24px}.gla-texts-editor__remove-text-button{left:50%;position:absolute;top:50%;transform:translate(-50%,-50%)} .help-popover{display:inline-flex;font-size:13px;font-weight:400;line-height:1.4}.help-popover button{align-items:center;background:none;border:none;display:inline-flex;padding:1px;fill:#949494}.help-popover button:not(:disabled){cursor:pointer}.help-popover button:hover:not(:disabled){fill:#616161}.help-popover .components-popover__content{padding:16px;width:300px}@media(min-width:960px){.help-popover .components-popover__content{width:360px}}@media(min-width:1080px){.help-popover .components-popover__content{width:480px}} .gla-asset-field{background-color:#fff;border-radius:2px;box-shadow:0 0 0 1px #0000001a;padding:24px}.gla-asset-field--is-disabled{opacity:.5}.gla-asset-field__heading-part{flex:1 1}.gla-asset-field__header{align-items:flex-start;display:flex}.gla-asset-field__heading{align-items:center;color:#1e1e1e;display:flex;font-size:13px;line-height:16px;margin:0}.gla-asset-field__subheading{color:#757575;font-size:13px;font-weight:400;line-height:16px;margin:4px 0 0}.gla-asset-field__optional-label{color:#757575;font-size:12px;font-weight:400;margin-left:4px}.gla-asset-field__help-popover{margin-left:4px}.gla-asset-field__help-popover button{padding:0}.gla-asset-field__help-popover__content{color:#2f2f2f;display:flex;flex-direction:column;gap:1.5em;line-height:16px}.gla-asset-field__help-popover__content ul{color:#757575;margin:0}.gla-asset-field__help-popover__content ul li{margin:0}.gla-asset-field__help-popover .components-popover__content{padding:30px;width:400px}.gla-asset-field .gla-asset-field__issue-pill{background-color:#fcf0f1;border:none;color:#cc1818;margin-right:8px}.gla-asset-field__toggle-button-anchor{height:24px;position:relative;width:24px}.gla-asset-field .gla-asset-field__toggle-button{color:#1e1e1e;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%)}.gla-asset-field__content[hidden]{display:none!important}.gla-asset-field__content:not(:empty){padding-top:16px} .gla-asset-group-card .components-external-link{margin-left:4px}.gla-asset-group-card .gla-validation-errors{margin-top:0}.gla-asset-field-call-to-action .components-base-control{width:240px}.gla-asset-field-call-to-action .components-base-control .components-input-control__container .components-select-control__input{height:36px}.gla-asset-field-call-to-action .components-base-control__field{margin-bottom:0}.gla-asset-field-display-url-path .gla-asset-field__subheading{margin-top:12px}.gla-asset-field-display-url-path .gla-asset-field__content{align-items:flex-start;display:flex;flex-wrap:wrap;gap:16px 8px}.gla-asset-field-display-url-path .gla-asset-field__content:not(:empty){padding-top:8px}.gla-asset-field-display-url-path__slash{height:36px;line-height:36px}.gla-asset-field-display-url-path__text-input{flex:0 1 240px}.gla-asset-field-display-url-path__text-input .components-input-control .components-input-control__container .components-input-control__input{height:36px;padding-left:16px;padding-right:16px} .gla-asset-group-section__optional-label,.gla-asset-group-section__primary-description{color:#757575;font-size:13px;font-weight:400}.gla-asset-group-section .components-tip{background:#f0f6fc;border:1px solid #c5d9ed;color:#1e1e1e;line-height:20px;padding:12px 16px}.gla-asset-group-section .components-tip>svg{fill:#007cba;align-self:auto} .gla-rebranding-tour__heading{align-items:center;display:flex;gap:8px;fill:var(--wp-admin-theme-color)}.gla-rebranding-tour .components-card__footer{padding-top:0}.gla-rebranding-tour .components-card__footer>*{display:none} .gla-dashboard .gla-dashboard__filter{align-items:flex-end;display:flex;flex-direction:column;margin-bottom:var(--main-gap)}@media(min-width:782px){.gla-dashboard .gla-dashboard__filter{flex-direction:row;justify-content:space-between}}.gla-dashboard .gla-dashboard__filter .woocommerce-filters-filter{margin-bottom:calc(var(--main-gap)/2)}@media(min-width:782px){.gla-dashboard .gla-dashboard__filter .woocommerce-filters-filter{margin-bottom:0}}.gla-dashboard .gla-dashboard__performance{display:flex;flex-direction:column;gap:var(--main-gap);margin-bottom:var(--main-gap)}@media(min-width:782px){.gla-dashboard .gla-dashboard__performance{align-items:flex-start;flex-direction:row}}.gla-dashboard .gla-dashboard__performance .woocommerce-summary__item-container{height:100%;margin:0}.gla-dashboard .gla-dashboard__performance .woocommerce-summary__item{height:100%;justify-content:center;min-height:117px}.gla-dashboard .gla-dashboard__performance .components-card{flex:1}.gla-dashboard .gla-dashboard__performance .components-card>div:first-of-type{display:flex;flex-direction:column} PK!paNNjs/build/dashboard.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[945],{6905:(e,t,a)=>{a.r(t),a.d(t,{default:()=>Ve});var n=a(1609),l=a(6087),s=a(7723),o=a(8846),r=a(6476),i=a(7892),c=a(2047),d=a(9927),g=a(1787),m=a(7374);var u=a(6473);const p=e=>{const{trackEventReportId:t}=e,a=(()=>{const e=(0,r.getQuery)(),{period:t,compare:a,before:n,after:l}=(0,m.getDateParamsFromQuery)(e),{primary:s,secondary:o}=(0,m.getCurrentDates)(e);return{period:t,compare:a,before:n,after:l,primaryDate:s,secondaryDate:o}})();return(0,n.createElement)(o.DateRangeFilterPicker,{dateQuery:a,onRangeSelect:e=>{t&&(0,u.ce)("gla_datepicker_update",{report:t,...e}),(0,r.updateQueryString)(e)},isoDateFormat:"YYYY-MM-DD"})};var _=a(3905),y=a(1209),E=a(7419),h=a(5128),A=a(7143),b=a(6520),k=a(7615),C=a(2775);function f(e){const t=(0,C.A)();return(0,A.useSelect)((a=>{const{getDashboardPerformance:n}=a(b.Ui),l=n(e,t,"primary"),s=n(e,t,"secondary");let o=null;const r=l.loaded&&s.loaded;return r&&l.data&&s.data&&(o=(0,k.bM)(l.data,s.data)),{loaded:r,data:o}}),[e,t])}const v=({loaded:e,data:t,children:a,noDataMessage:l})=>{let s;return s=e?t?(0,n.createElement)(o.SummaryList,null,(()=>a(t))):(0,n.createElement)("div",{className:"gla-summary-card__body"},(0,n.createElement)("p",null,l.body),(0,n.createElement)(i.A,{eventName:l.eventName,eventProps:{context:"dashboard",href:l.link},href:l.link,target:"_blank",isSmall:!0,isSecondary:!0},l.buttonLabel)):(0,n.createElement)(o.SummaryListPlaceholder,{numberOfItems:2}),s};var S=a(6427),N=a(6459);const w=({title:e,children:t})=>(0,n.createElement)(S.Card,{className:"gla-summary-card"},(0,n.createElement)(S.CardHeader,{size:"medium"},(0,n.createElement)(N.A,{variant:"title-small"},e)),t);var P=a(1378),R=a(5955),I=a(4270),G=a(1212),x=a(2391),F=a(5170),M=a(1177);const T=()=>(0,n.createElement)("div",{className:"gla-free-ad-credit-claim"},(0,n.createElement)(F.A,null),(0,n.createElement)("div",null,(0,l.createInterpolateElement)((0,s.__)("Claim $500 in ads credit when you spend your first $500 with Google Ads. Terms and conditions apply.","google-listings-and-ads"),{termLink:(0,n.createElement)(M.A,{context:"dashboard",linkId:"free-ad-credit-terms",href:"https://www.google.com/ads/coupons/terms/"})})));var D=a(850);function B(){const e=[{Icon:R.A,content:(0,s.__)("Reach more customer by advertising your products across Google Ads channels like Search, YouTube and Discover.","google-listings-and-ads")},{Icon:R.A,content:(0,s.__)("Set a daily budget and only pay when people click on your ads.","google-listings-and-ads")},{Icon:R.A,content:(0,l.createInterpolateElement)((0,s.__)("Performance Max uses the best of Google's AI to show the most impactful ads for your products at the right time and place. Learn more about Performance Max technology.","google-listings-and-ads"),{link:(0,n.createElement)(I.i,{href:"https://support.google.com/google-ads/answer/10724817",context:"campaign-creation-performance-max"})})}];return(0,n.createElement)("div",{className:"gla-paid-features__feature-list"},e.map((({Icon:e,content:t},a)=>(0,n.createElement)(S.Flex,{key:a,align:"flex-start"},(0,n.createElement)(e,{size:"18"}),(0,n.createElement)(S.FlexBlock,null,t)))))}const L=()=>(0,n.createElement)(D.A,{size:"medium",className:"gla-paid-features"},(0,n.createElement)(S.Flex,{align:"center",gap:9,className:"gla-paid-features__content"},(0,n.createElement)(S.FlexItem,null,(0,n.createElement)(G.A,null)),(0,n.createElement)(S.FlexBlock,null,(0,n.createElement)(B,null))),(0,n.createElement)(T,null),(0,n.createElement)(x.A,{isPrimary:!0,isSecondary:!1,isSmall:!1,eventProps:{context:"add-paid-campaign-promotion"}},(0,s.__)("Create Campaign","google-listings-and-ads"))),Q=function(){const{googleAdsAccount:e}=(0,P.A)();return(0,n.createElement)("div",{className:"gla-summary-card__body"},e?(0,n.createElement)(L,null):(0,n.createElement)(o.Spinner,null))},q={precision:0},O=()=>{const e=(0,h.A)(q),{data:t,loaded:a}=f(_.k1);return(0,n.createElement)(v,{loaded:a,data:t,noDataMessage:{body:(0,s.__)("We're having trouble loading this data. Try again later, or track your performance in Google Merchant Center.","google-listings-and-ads"),link:"https://merchants.google.com/mc/reporting/dashboard",eventName:"gla_google_mc_link_click",buttonLabel:(0,s.__)("Open Google Merchant Center","google-listings-and-ads")}},(t=>[(0,n.createElement)(o.SummaryNumber,{key:"1",label:(0,s.__)("Clicks","google-listings-and-ads"),value:e(t.clicks.value),prevValue:e(t.clicks.prevValue),delta:t.clicks.delta}),(0,n.createElement)(o.SummaryNumber,{key:"2",label:(0,s.__)("Total Spend","google-listings-and-ads"),value:(0,s.__)("Free","google-listings-and-ads"),delta:null})]))},H=()=>{const{formatAmount:e}=(0,E.A)(),{data:t,loaded:a}=f(_.Mx);return(0,n.createElement)(v,{loaded:a,data:t,noDataMessage:{body:(0,s.__)("We're having trouble loading this data. Try again later, or track your performance in Google Ads.","google-listings-and-ads"),link:"https://ads.google.com/",eventName:"gla_google_ads_link_click",buttonLabel:(0,s.__)("Open Google Ads","google-listings-and-ads")}},(t=>[(0,n.createElement)(o.SummaryNumber,{key:"1",label:(0,s.__)("Total Sales","google-listings-and-ads"),value:e(t.sales.value,!0),prevValue:e(t.sales.prevValue,!0),delta:t.sales.delta}),(0,n.createElement)(o.SummaryNumber,{key:"2",label:(0,s.__)("Total Spend","google-listings-and-ads"),value:e(t.spend.value,!0),prevValue:e(t.spend.prevValue,!0),delta:t.spend.delta})]))};function K(){const{loaded:e,data:t}=(0,y.A)();if(!e)return null;const a=!t?.length;return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(w,{title:(0,s.__)("Google Ads","google-listings-and-ads")},a?(0,n.createElement)(Q,null):(0,n.createElement)(H,null)),(0,n.createElement)(w,{title:(0,s.__)("Free Listings (Limited Visibility)","google-listings-and-ads")},(0,n.createElement)(O,null)))}var $=a(9457),U=a(7043);const Z="create-another-campaign",V="confirm";function Y({onGuideRequestClose:e=()=>{}}){(0,l.useEffect)((()=>{(0,u.ce)("gla_modal_open",{context:_.K4.CAMPAIGN_CREATION_SUCCESS})}),[]);const t=(0,l.useCallback)((t=>e(t,"dismiss")),[e]);return(0,n.createElement)($.A,{className:"gla-campaign-creation-success-guide",onRequestClose:t,buttons:[(0,n.createElement)(i.A,{key:"0",isTertiary:!0,"data-action":Z,onClick:e},(0,s.__)("Create another campaign","google-listings-and-ads")),(0,n.createElement)(i.A,{key:"1",isPrimary:!0,"data-action":V,onClick:e},(0,s.__)("Got it","google-listings-and-ads"))]},(0,n.createElement)("div",{className:"gla-campaign-creation-success-guide__header-image"},(0,n.createElement)("img",{src:U,alt:(0,s.__)("Drawing of a person who successfuly launched a campaign","google-listings-and-ads"),width:"413",height:"160"})),(0,n.createElement)(I.A,{title:(0,s.__)("You've set up a Performance Max Campaign!","google-listings-and-ads")},(0,l.createInterpolateElement)((0,s.__)("You can pause or edit your campaign at any time. For best results, we recommend allowing your campaign to run for at least 14 days without pausing or editing. Learn more about Performance Max technology.","google-listings-and-ads"),{link:(0,n.createElement)(I.i,{href:"https://support.google.com/google-ads/answer/10724817",context:"campaign-creation-performance-max"})})))}var z=a(6942),j=a.n(z),W=a(8237),J=a(3658);const X=e=>{const{programId:t,onRequestClose:a}=e,[o,r]=(0,l.useState)(!1),c=(0,J.j)(),d=()=>{o||a()};return(0,n.createElement)($.A,{className:"gla-remove-program-modal",title:(0,s.__)("Permanently Remove?","google-listings-and-ads"),isDismissible:!o,buttons:[(0,n.createElement)(i.A,{key:"keep",isSecondary:!0,disabled:o,onClick:d},(0,s.__)("Keep Campaign","google-listings-and-ads")),(0,n.createElement)(i.A,{key:"remove",isPrimary:!0,isDestructive:!0,loading:o,onClick:()=>{r(!0),c.deleteAdsCampaign(t).then((()=>a())).catch((()=>r(!1)))}},(0,s.__)("Remove Campaign","google-listings-and-ads"))],onRequestClose:d},(0,n.createElement)("p",null,(0,s.__)("Results typically improve with time. Removing a campaign will result in the loss of any optimisations learned from those campaigns.","google-listings-and-ads")),(0,n.createElement)("p",null,(0,s.__)("Once a campaign is removed, it cannot be re-enabled.","google-listings-and-ads")))},ee=e=>{const{programId:t}=e,[a,o]=(0,l.useState)(!1);return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(i.A,{isDestructive:!0,isLink:!0,onClick:()=>{o(!0)}},(0,s.__)("Remove","google-listings-and-ads")),a&&(0,n.createElement)(X,{programId:t,onRequestClose:()=>{o(!1)}}))};var te=a(3666);const ae=({programId:e,onRequestClose:t})=>(0,n.createElement)($.A,{className:"gla-edit-program-prompt-modal",title:(0,s.__)("Before you edit…","google-listings-and-ads"),buttons:[(0,n.createElement)(i.A,{key:"no",isSecondary:!0,onClick:()=>{t()}},(0,s.__)("Don't edit","google-listings-and-ads")),(0,n.createElement)(i.A,{key:"yes",isPrimary:!0,onClick:()=>{const t=(0,te.Q4)(e);(0,r.getHistory)().push(t),(0,u.ce)("gla_dashboard_edit_program_click",{programId:e,url:t})}},(0,s.__)("Continue to edit","google-listings-and-ads"))],onRequestClose:t},(0,n.createElement)("p",null,(0,s.__)("Results typically improve with time.","google-listings-and-ads")),(0,n.createElement)("p",null,(0,s.__)("Editing will result in the loss of any optimisations learned over time.","google-listings-and-ads")),(0,n.createElement)("p",null,(0,s.__)("We recommend allowing your programs to run for at least 14 days after set up, without pausing or editing, for optimal performance.","google-listings-and-ads")));var ne=a(6494);const le=e=>{const{className:t,programId:a,...l}=e;return(0,n.createElement)(ne.A,{button:(0,n.createElement)(i.A,{...l,isLink:!0,className:j()(t)},(0,s.__)("Edit","google-listings-and-ads")),modal:(0,n.createElement)(ae,{programId:a})})};var se=a(6734),oe=a(5847),re=a(3741);const ie=e=>{const{onPauseCampaign:t=()=>{},onRequestClose:a}=e;return(0,n.createElement)($.A,{className:"gla-pause-program-modal",title:(0,s.__)("Before you pause…","google-listings-and-ads"),buttons:[(0,n.createElement)(i.A,{key:"1",isSecondary:!0,onClick:()=>{a()}},(0,s.__)("Keep Active","google-listings-and-ads")),(0,n.createElement)(i.A,{key:"2",isPrimary:!0,onClick:()=>{t()}},(0,s.__)("Pause Campaign","google-listings-and-ads"))],onRequestClose:a},(0,n.createElement)("p",null,(0,s.__)("Results typically improve with time. If you pause, your products won’t be shown to people looking for what you offer.","google-listings-and-ads")),(0,n.createElement)("p",null,(0,s.__)("Pausing a campaign will result in the loss of any optimisations learned from those campaigns.","google-listings-and-ads")))};var ce=a(2118);const de=e=>{const{program:t}=e,[a,s]=(0,l.useState)(t.active),[o,r]=(0,l.useState)(!1),{updateAdsCampaign:i}=(0,J.j)();return(0,n.createElement)(n.Fragment,null,(0,n.createElement)(ce.A,{checked:a,onChange:e=>{!1!==e?(s(e),i(t.id,{status:"enabled"})):r(!0)}}),o&&(0,n.createElement)(ie,{programId:t.id,onPauseCampaign:()=>{r(!1),s(!1),i(t.id,{status:"paused"})},onRequestClose:()=>{r(!1)}}))};var ge=a(9039);const me=()=>(0,n.createElement)(ge.A,{text:(0,s.__)("Free listings cannot be paused through WooCommerce. Go to Google Merchant Center for advanced settings.","google-listings-and-ads")},(0,n.createElement)(ce.A,{checked:!0,disabled:!0}));var ue=a(9788);const pe="gla-all-programs-table-card",_e="gla-campaign-edit-button",ye=[{key:"title",label:(0,s.__)("Program","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"country",label:(0,s.__)("Country","google-listings-and-ads"),isLeftAligned:!0},{key:"dailyBudget",label:(0,s.__)("Daily budget","google-listings-and-ads")},{key:"enabled",label:(0,s.__)("Enabled","google-listings-and-ads")},{key:"actions",label:"",required:!0}];function Ee({countryCodes:e,countryNameMap:t}){const[a]=e;return(0,n.createElement)("span",null,t[a],e.length>=2&&(0,s.sprintf)( // translators: %d: number of countries, with minimum value of 1. // translators: %d: number of countries, with minimum value of 1. (0,s.__)(" + %d more","google-listings-and-ads"),e.length-1))}const he=e=>{const t=(0,r.getQuery)(),{formatAmount:a}=(0,E.A)(),{data:l}=(0,oe.A)(),{data:o}=(0,y.A)(),i=(0,se.A)();if(!l||!o)return(0,n.createElement)(re.A,null);let c=null;if(o.filter((({type:e})=>e===_.$g)).length){const e=`.${pe} .${_e}`;c=(0,n.createElement)(ue.A,{referenceElementCssSelector:e})}const d=[{id:_.Q,title:(0,s.__)("Free listings","google-listings-and-ads"),dailyBudget:(0,s.__)("Free","google-listings-and-ads"),country:(0,n.createElement)(Ee,{countryCodes:l,countryNameMap:i}),active:!0,disabledEdit:!1},...o.map((e=>({id:e.id,title:e.name,dailyBudget:a(e.amount,!0),country:(0,n.createElement)(Ee,{countryCodes:e.displayCountries,countryNameMap:i}),active:"enabled"===e.status,disabledEdit:e.type!==_.$g})))],g=(0,n.createElement)(W.A,{className:pe,title:(0,s.__)("Programs","google-listings-and-ads"),actions:(0,n.createElement)(x.A,{eventProps:{context:"programs-table-card"}}),headers:ye,rowKey:e=>e[0].id,rows:d.map((e=>{const t=e.id===_.Q,a=j()({[_e]:!t&&!e.disabledEdit});return[{display:e.title,id:e.id.toString()},{display:e.country},{display:e.dailyBudget},{display:t?(0,n.createElement)(me,null):(0,n.createElement)(de,{program:e})},{display:e.id!==_.Q&&(0,n.createElement)("div",{className:"program-actions"},(0,n.createElement)(le,{className:a,programId:e.id,disabled:e.disabledEdit}),(0,n.createElement)(ee,{programId:e.id}))}]})),totalRows:d.length,rowsPerPage:d.length,query:t,onQueryChange:r.onQueryChange,...e});return(0,n.createElement)(n.Fragment,null,c,g)};var Ae=a(3921),be=a(8468),ke=a(6474),Ce=a(9415),fe=a(4307),ve=a(7539),Se=a(2455),Ne=a(8473),we=a(1203),Pe=a(4831),Re=a(8234),Ie=a(4679),Ge=a(1650);const xe="gla_paid_campaign_step",Fe="edit-ads",Me=(0,te.uZ)(),Te=(0,n.createElement)(Se.A,{eventContext:Fe});function De(){const{step:e}=(0,r.getQuery)();return Object.values(_.km).includes(e)?e:_.km.CAMPAIGN}function Be(e){const t=new Set([(0,r.getNewPath)({step:_.km.CAMPAIGN}),(0,r.getNewPath)({step:_.km.ASSET_GROUP})]),a=e.pathname+e.search;return!t.has(a)}const Le=()=>{(0,ke.A)("full-content");const[e,t]=(0,l.useState)(!1),[a,i]=(0,l.useState)(!1),{updateAdsCampaign:c,createCampaignAssetGroup:d,updateCampaignAssetGroup:g}=(0,J.j)(),m=Number((0,r.getQuery)().programId),{loaded:p,data:E}=(0,y.A)(),{hasFinishedResolution:h,invalidateResolution:A,data:b}=(0,Ce.A)("getCampaignAssetGroups",m),k=E?.find((e=>e.id===m)),C=b?.at(0),{highestDailyBudget:f,hasFinishedResolution:v}=(0,Ie.A)(k?.displayCountries);(0,l.useEffect)((()=>{k&&k.type!==_.$g&&(0,r.getHistory)().replace(Me)}),[k]);const S=De();(0,Ge.A)((0,s.__)("You have unsaved campaign data. Are you sure you want to leave?","google-listings-and-ads"),e&&!a,Be);const N=e=>{const t=(0,r.getNewPath)({...(0,r.getQuery)(),step:e});(0,r.getHistory)().push(t)};return p&&h&&v?k?(0,n.createElement)(n.Fragment,null,(0,n.createElement)(ve.A,{title:(0,s.sprintf)( // translators: %s: campaign's name. // translators: %s: campaign's name. (0,s.__)("Edit %s","google-listings-and-ads"),k.name),helpButton:Te,backHref:Me}),(0,n.createElement)(Ne.A,{initialCampaign:{amount:k.amount},recommendedDailyBudget:f,assetEntityGroup:C,onSubmit:async(e,t)=>{const{action:a}=t.submitter.dataset,{amount:n}=e;i(!0);try{if(await c(k.id,{amount:n}),a===Re.zK){let t=C;t||(t=(await d(m)).assetGroup);const a=t.id,n=(0,fe.A)(t,e);await g(a,n),A()}}catch(e){return i(!1),void t.signalFailedSubmission()}(0,r.getHistory)().push((0,te.uZ)())},onChange:(e,a)=>{const n=a.amount!==k.amount||!(0,be.isEqual)(C.display_url_path,a.display_url_path)||(0,fe.m)(C,a).length>0;t(n)}},(0,n.createElement)(o.Stepper,{currentStep:De(),steps:[{key:_.km.CAMPAIGN,label:(0,s.__)("Edit campaign","google-listings-and-ads"),content:(0,n.createElement)(we.A,{campaign:k,context:Fe,headerTitle:(0,s.__)("Edit your campaign","google-listings-and-ads"),continueButton:e=>(0,n.createElement)(Pe.A,{formProps:e,onClick:()=>{return e=_.km.ASSET_GROUP,(0,u.dQ)(xe,_.Z3[S],_.Z3[e],Fe),void N(e);var e}})}),onClick:e=>{(0,u.T)(xe,_.Z3[e],Fe),N(e)}},{key:_.km.ASSET_GROUP,label:(0,s.__)("Optimize your campaign","google-listings-and-ads"),content:(0,n.createElement)(Re.Ay,{campaign:k})}]}))):(0,n.createElement)(n.Fragment,null,(0,n.createElement)(ve.A,{title:(0,s.__)("Edit Campaign","google-listings-and-ads"),helpButton:Te,backHref:Me}),(0,n.createElement)("div",null,(0,s.__)("Error in loading your ads campaign. Please try again later.","google-listings-and-ads"))):(0,n.createElement)(n.Fragment,null,(0,n.createElement)(ve.A,{title:(0,s.__)("Loading…","google-listings-and-ads"),helpButton:Te,backHref:Me}),(0,n.createElement)(re.A,null))};var Qe=a(1455),qe=a.n(Qe),Oe=a(5640);const He="gla_paid_campaign_step",Ke="create-ads",$e=(0,te.uZ)(),Ue=()=>{(0,ke.A)("full-content");const[e,t]=(0,l.useState)(_.km.CAMPAIGN),a=(0,l.useRef)(null),{createAdsCampaign:i,updateCampaignAssetGroup:c}=(0,J.j)(),{createNotice:d}=(0,Oe.A)(),{data:g}=(0,oe.A)(),{highestDailyBudget:m,hasFinishedResolution:p}=(0,Ie.A)(g);return g&&p?(0,n.createElement)(n.Fragment,null,(0,n.createElement)(ve.A,{title:(0,s.__)("Create your campaign","google-listings-and-ads"),helpButton:(0,n.createElement)(Se.A,{eventContext:Ke}),backHref:$e}),(0,n.createElement)(Ne.A,{initialCampaign:{amount:m},recommendedDailyBudget:m,onSubmit:async(e,t)=>{const{action:n}=t.submitter.dataset;try{const{amount:t}=e;if(null===a.current){const e=await i(t,g);a.current=e.createdCampaign.id}if(n===Re.zK){const t=a.current,n=`${b.RV}/ads/campaigns/asset-groups?campaign_id=${t}`,[l]=await qe()({path:n}),s=(0,fe.A)(l,e);await c(l.id,s)}d("success",(0,s.__)("You’ve successfully created a campaign!","google-listings-and-ads"))}catch(e){return void t.signalFailedSubmission()}(0,r.getHistory)().push((0,te.uZ)({campaign:"saved"}))}},(0,n.createElement)(o.Stepper,{currentStep:e,steps:[{key:_.km.CAMPAIGN,label:(0,s.__)("Create campaign","google-listings-and-ads"),content:(0,n.createElement)(we.A,{headerTitle:(0,s.__)("Create your campaign","google-listings-and-ads"),context:Ke,continueButton:a=>(0,n.createElement)(Pe.A,{formProps:a,onClick:()=>{var a;a=_.km.ASSET_GROUP,(0,u.dQ)(He,_.Z3[e],_.Z3[a],Ke),t(a)}})}),onClick:e=>{(0,u.T)(He,_.Z3[e],Ke),t(e)}},{key:_.km.ASSET_GROUP,label:(0,s.__)("Optimize your campaign","google-listings-and-ads"),content:(0,n.createElement)(Re.Ay,null)}]}))):null};var Ze=a(5246);const Ve=()=>{const[e,t]=(0,l.useState)(!1),a=(0,l.useCallback)(((e,a)=>{const n=a||e.currentTarget.dataset.action,l={...(0,r.getQuery)(),guide:void 0};(0,r.getHistory)().replace((0,r.getNewPath)(l)),n===Z?(0,r.getHistory)().push((0,te.uB)()):n===V&&t(!0),(0,u.ce)("gla_modal_closed",{context:_.K4.CAMPAIGN_CREATION_SUCCESS,action:n})}),[t]),m=(0,r.getQuery)();switch(m.subpath){case te.$K.editCampaign:return(0,n.createElement)(Le,null);case te.$K.createCampaign:return(0,n.createElement)(Ue,null)}const y="dashboard",{enableReports:E}=_.Th,h=m?.guide===_.K4.CAMPAIGN_CREATION_SUCCESS,A=(0,Ae.A)();return(0,n.createElement)(n.Fragment,null,(0,n.createElement)("div",{className:"gla-dashboard"},(0,n.createElement)(c.A,{context:"dashboard"}),(0,n.createElement)(d.A,null),(0,n.createElement)(Ze.A,null),(0,n.createElement)("div",{className:"gla-dashboard__filter"},(0,n.createElement)(p,{trackEventReportId:y}),E&&(0,n.createElement)((()=>(0,n.createElement)(o.Link,{href:(0,r.getNewPath)(null,"/google/reports")},(0,n.createElement)(i.A,{isPrimary:!0},(0,s.__)("View Reports","google-listings-and-ads")))),null)),(0,n.createElement)("div",{className:"gla-dashboard__performance"},(0,n.createElement)(K,null)),(0,n.createElement)("div",{className:"gla-dashboard__programs"},(0,n.createElement)(he,{trackEventReportId:y}))),h&&(0,n.createElement)(Y,{onGuideRequestClose:a}),e&&A&&(0,n.createElement)(g.A,{label:(0,s.__)("How easy was it to create a Google Ad campaign?","google-listings-and-ads"),secondLabel:(0,s.__)("How easy was it to understand the requirements for the Google Ad campaign creation?","google-listings-and-ads"),eventContext:_.K4.CAMPAIGN_CREATION_SUCCESS}))}}}]);PK!"|' ' js/build/get-started-page.cssnu[.woocommerce-marketing-google-get-started-page{margin:0 auto;max-width:824px}.woocommerce-marketing-google-get-started-page>.components-card{margin-bottom:calc(var(--main-gap)*1.5)}.woocommerce-marketing-google-get-started-page .gla-get-started-notice{margin:0 0 32px} .gla-app-text{font-weight:400;margin:0}.gla-app-text--body{font-size:13px;line-height:20px}.gla-app-text--caption,.gla-app-text--label{font-size:12px;line-height:16px}.gla-app-text--label{font-weight:600}.gla-app-text--title-small{font-size:20px;line-height:28px}.gla-app-text--title-medium{font-size:24px;line-height:32px}.gla-app-text--subtitle{font-size:16px;font-weight:600;line-height:24px}.gla-app-text--subtitle-small{font-size:14px;line-height:20px} .gla-get-started-benefits-card .components-card__body.components-card__body{display:flex;padding:0}@media(max-width:600px){.gla-get-started-benefits-card .components-card__body.components-card__body{flex-direction:column}}.gla-get-started-benefits-card .gla-get-started-benefits-card__image{flex:100%}.gla-get-started-benefits-card .gla-get-started-benefits-card__image>img{object-fit:cover}.gla-get-started-benefits-card .components-flex-item{display:flex;flex:100%;flex-direction:column;gap:16px;justify-content:center;padding:40px}@media(max-width:600px){.gla-get-started-benefits-card .components-flex-item{padding:24px;text-align:center}}.gla-get-started-benefits-card .gla-get-started-benefits-card__description{font-size:14px;line-height:18px}.gla-get-started-benefits-card .gla-get-started-benefits-card__hint{color:#949494;font-size:12px;line-height:16px}@media(max-width:600px){.gla-get-started-benefits-card .gla-get-started-benefits-card__title{font-size:20px}} .gla-get-started-customer-quotes-card .components-flex{align-items:stretch;gap:28px;padding:0 62px 46px}.gla-get-started-customer-quotes-card .components-flex.components-card__header{flex-direction:column;padding:46px 112px 40px;text-align:center}@media(max-width:600px){.gla-get-started-customer-quotes-card .components-flex.components-card__header{padding:24px}}.gla-get-started-customer-quotes-card .components-flex-block{background-color:#f6f7f7;display:flex;flex-direction:column;gap:10px;padding:20px}@media(max-width:600px){.gla-get-started-customer-quotes-card .components-flex{flex-direction:column;gap:24px;padding:0 24px 24px}}.gla-get-started-customer-quotes-card .gla-get-started-customer-quotes-card__content{font-size:14px;line-height:20px}.gla-get-started-customer-quotes-card .gla-get-started-customer-quotes-card__name{color:#949494;font-size:12px;line-height:16px}@media(max-width:600px){.gla-get-started-customer-quotes-card .gla-get-started-customer-quotes-card__title{font-size:20px;line-height:28px}} .gla-faqs-panel .components-panel__row{align-items:flex-start;flex-direction:column;gap:1.5em}.gla-faqs-panel .components-panel__row p{margin:0}.gla-faqs-panel .components-panel__body-title .components-panel__body-toggle.components-button{padding-bottom:20px;padding-top:20px} .gla-get-started-faqs .components-panel__header>h2{font-size:24px;font-weight:400;line-height:32px}@media(max-width:600px){.gla-get-started-faqs .components-panel__header>h2{font-size:20px;line-height:28px}}.gla-get-started-faqs .components-panel__body-title .components-panel__body-toggle.components-button{font-size:14px;font-weight:400;line-height:18px}.gla-get-started-faqs ul{list-style:revert;padding:revert} .gla-get-started-features-card .components-flex{gap:50px;padding:0 88px 48px}.gla-get-started-features-card .components-flex.components-card__header{flex-direction:column;gap:8px;padding:48px 113px 40px;text-align:center}@media(max-width:600px){.gla-get-started-features-card .components-flex.components-card__header{gap:16px;padding:24px 24px 16px}}.gla-get-started-features-card .components-flex-block{align-items:center;display:flex;flex-direction:column;text-align:center}@media(max-width:600px){.gla-get-started-features-card .components-flex{flex-direction:column;gap:40px;padding:0 55px 44px}}.gla-get-started-features-card .gla-get-started-features-card__description{font-size:14px;line-height:18px}.gla-get-started-features-card .gla-get-started-features-card__label{font-size:14px;font-weight:700;line-height:18px;margin:24px 0 16px}.gla-get-started-features-card .gla-get-started-features-card__content{font-size:14px;line-height:18px}.gla-get-started-features-card .gla-get-started-features-card__learn-more{font-size:14px;font-weight:500;line-height:16px;margin-top:8px}.gla-get-started-features-card .gla-get-started-features-card__learn-more a{text-decoration:none}@media(max-width:600px){.gla-get-started-features-card .gla-get-started-features-card__title{font-size:20px;line-height:28px}.gla-get-started-features-card .gla-get-started-features-card__label{margin:20px 0 16px}} .gla-get-started-card>div:first-of-type{display:flex;flex-direction:row-reverse;padding:40px 40px 16px}@media(max-width:600px){.gla-get-started-card>div:first-of-type{flex-direction:column;padding:24px}}.gla-get-started-card .motivation-image{min-width:auto}@media(max-width:600px){.gla-get-started-card .motivation-image{text-align:center}}.gla-get-started-card .components-card__body.components-card__body{align-items:flex-start;display:flex;flex-direction:column;gap:20px;padding:0}@media(max-width:600px){.gla-get-started-card .components-card__body.components-card__body{align-items:center;gap:16px;text-align:center}}.gla-get-started-card .gla-get-started-card__terms-notice{font-size:12px;line-height:16px}.gla-get-started-card .gla-get-started-card__terms-notice a{text-decoration:none}@media(max-width:600px){.gla-get-started-card .gla-get-started-card__title{font-size:20px;line-height:28px}.gla-get-started-card .gla-get-started-card__terms-notice{margin-top:12px}} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .gla-get-started-with-hero-card{flex-direction:column}.gla-get-started-with-hero-card .motivation{margin:0;min-width:100%}.gla-get-started-with-hero-card .components-card__body.components-card__body{align-items:center;display:flex;flex-direction:column;margin:calc(var(--main-gap)*1.67) 0 var(--main-gap);padding:0;text-align:center}@media(max-width:600px){.gla-get-started-with-hero-card .components-card__body.components-card__body{margin:24px}}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__caption{color:#949494;margin-bottom:16px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__title{margin-bottom:16px;max-width:550px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__description{margin-bottom:24px;max-width:550px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__button{display:flex;font-size:14px;height:36px;justify-content:center;margin-bottom:8px;width:188px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__hint{color:#ccc;font-size:12px;margin-bottom:24px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__terms-notice{font-size:12px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__terms-notice a{text-decoration:none}.gla-get-started-with-hero-card .components-tip{border-top:1px solid #ddd;margin:0 40px;padding:24px 12px}.gla-get-started-with-hero-card .components-tip svg{fill:#1e1e1e;margin-right:12px}.gla-get-started-with-hero-card .components-tip p{color:#949494;font-size:12px}@media(max-width:600px){.gla-get-started-with-hero-card .gla-get-started-with-hero-card__title{font-size:20px;line-height:28px}.gla-get-started-with-hero-card .gla-get-started-with-hero-card__description{padding:0 12px}.gla-get-started-with-hero-card .components-tip{margin:0 24px;padding:24px 4px}.gla-get-started-with-hero-card .components-tip svg{align-self:flex-start}} .gla-get-started-notice.is-error{background-color:#ffcbcb}.gla-get-started-notice.is-warning{background-color:#ffeec1}.gla-get-started-notice .components-notice__content{margin-left:16px}.gla-get-started-notice__icon{margin-left:.2em;vertical-align:middle} PK!HL}Z}Zjs/build/get-started-page.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[207],{494:(e,t,o)=>{o.r(t),o.d(t,{default:()=>Y});var a=o(1609),n=o(7723),r=o(6427),s=o(9959),l=o(6459);const i=()=>(0,a.createElement)(r.Card,{className:"gla-get-started-benefits-card",isBorderless:!0},(0,a.createElement)(r.CardBody,null,(0,a.createElement)("div",{className:"gla-get-started-benefits-card__image"},(0,a.createElement)("img",{src:s,alt:(0,n.__)("Google for WooCommerce Benefits","google-listings-and-ads"),width:"100%",height:"100%"})),(0,a.createElement)(r.FlexItem,null,(0,a.createElement)(l.A,{variant:"title-medium",className:"gla-get-started-benefits-card__title"},(0,n.__)("Reach your sales goals by creating a campaign","google-listings-and-ads")),(0,a.createElement)(l.A,{variant:"body",className:"gla-get-started-benefits-card__description"},(0,n.__)("Reach more customers by advertising your products across Google Ads channels like Search, YouTube and Discover. Set up your campaign now so your products are included as soon as they’re approved.","google-listings-and-ads")))));var c=o(1394);const g=({quote:e,name:t})=>(0,a.createElement)(r.FlexBlock,null,(0,a.createElement)("img",{src:c,alt:(0,n.__)("An image of a quote.","google-listings-and-ads"),width:"34",height:"34"}),(0,a.createElement)(l.A,{className:"gla-get-started-customer-quotes-card__content",variant:"body"},e),(0,a.createElement)(l.A,{className:"gla-get-started-customer-quotes-card__name"},t)),d=()=>(0,a.createElement)(r.Card,{className:"gla-get-started-customer-quotes-card",isBorderless:!0},(0,a.createElement)(r.CardHeader,null,(0,a.createElement)(l.A,{className:"gla-get-started-customer-quotes-card__title",variant:"title-medium"},(0,n.__)("21,000+ WooCommerce store owners like you already list products with Google","google-listings-and-ads"))),(0,a.createElement)(r.Flex,{gap:0},(0,a.createElement)(g,{quote:(0,n.__)("Thank you Google and WooCommerce for creating this app. It’s so simple to use and connect your products to Merchant Center.","google-listings-and-ads"),name:(0,n.__)("joshualukewhite","google-listings-and-ads")}),(0,a.createElement)(g,{quote:(0,n.__)("It does everything smoothly. Perfect and must need add-on from WooCommerce. Some things are just “essentials”.","google-listings-and-ads"),name:(0,n.__)("Anonymous","google-listings-and-ads")})));var m=o(6087),u=o(9452),p=o(1177);const h=(0,a.createElement)(p.A,{context:"faqs",linkId:"google-merchant-center-link",href:"https://woocommerce.com/document/google-for-woocommerce/#connect-your-store-with-google-merchant-center"}),y=(0,a.createElement)(p.A,{context:"faqs",linkId:"performance-max-link",href:"https://woocommerce.com/document/google-for-woocommerce/get-started/google-performance-max-campaigns/"}),_=[{trackId:"what-is-google-merchant-center",question:(0,n.__)("What is Google Merchant Center?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("Google Merchant Center is like a digital storefront for your products on Google. It's where you upload and manage information about your products, like titles, descriptions, images, prices, and availability. This data is used to create product listings that can appear across Google.","google-listings-and-ads"),{link:h}))},{trackId:"why-should-i-connect-google-merchant-center",question:(0,n.__)("Why should I connect to Google Merchant Center?","google-listings-and-ads"),answer:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("By syncing your product information to Google Merchant Center, your products can appear in relevant Google searches, Shopping tab, image searches, and even on other platforms like YouTube. When running Performance Max campaigns, Google Merchant Center ensures that shoppers see the most up-to-date and accurate information about your product feed, reducing confusion and improving the chances of a purchase.","google-listings-and-ads"),{linkMc:h,linkPmax:y})))},{trackId:"will-my-deals-and-promotions-display-on-google",question:(0,n.__)("Will my deals and promotions display on Google?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("To show your coupons and promotions on Google Shopping listings, make sure you’re using the latest version of Google for WooCommerce. When you create or update a coupon in your WordPress dashboard under Marketing > Coupons, you’ll see a Channel Visibility settings box on the right: select “Show coupon on Google” to enable it. Learn more about managing promotions for Google for WooCommerce. This feature is currently available in Australia, Canada, Germany, France, India, the United Kingdom, and the United States.","google-listings-and-ads"),{link:(0,a.createElement)(p.A,{context:"faqs",linkId:"google-promotions-using-woocommerce",href:"https://support.google.com/merchants/answer/11338950#zippy=%2Cmanage-promotions-using-woocommerce"})}))},{trackId:"what-is-product-sync",question:(0,n.__)("What is Product Sync?","google-listings-and-ads"),answer:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,n.__)("Product Sync is a feature fully integrated into WooCommerce’s management platform that automatically lets you sync your product feed to Google Merchant Center. It will sync all your WooCommerce product data, and you can also add or edit products individually or in bulk. To ensure products are approved by Google, check that your product feed includes the following information:","google-listings-and-ads")),(0,a.createElement)("ul",null,(0,a.createElement)("li",null,(0,n.__)("General product information","google-listings-and-ads")),(0,a.createElement)("li",null,(0,n.__)("Unique product identifiers","google-listings-and-ads")),(0,a.createElement)("li",null,(0,n.__)("Data requirements for specific categories (auto-assigned by Google):","google-listings-and-ads"),(0,a.createElement)("ul",null,(0,a.createElement)("li",null,(0,n.__)("Apparel & Accessories","google-listings-and-ads")),(0,a.createElement)("li",null,(0,n.__)("Media","google-listings-and-ads")),(0,a.createElement)("li",null,(0,n.__)("Books","google-listings-and-ads"))))))},{trackId:"where-do-i-manage-my-product-feed-and-my-google-ads-campaigns",question:(0,n.__)("Where do I manage my product feed and my Google Ads campaigns?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,n.__)("You can manage and edit all of your products and your Google Ads campaigns right from your WooCommerce dashboard and on the WooCommerce Mobile App.","google-listings-and-ads"))},{trackId:"where-will-my-products-appear",question:(0,n.__)("Where will my products appear?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("Once you start running a Performance Max ads campaign, your approved products will reach more shoppers to help grow your business by being shown on Google Search, Google Maps, the Shopping tab, Gmail, Youtube, the Google Display Network, and Discover feed.","google-listings-and-ads"),{linkPmax:y}))},{trackId:"what-are-performance-max-campaigns",question:(0,n.__)("What are Performance Max campaigns?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("Performance Max campaigns help you combine your expertise with Google AI to reach your most valuable customers and drive sales. Just set your goals and budget and Google AI will get your ads seen by the right customers at the right time across Google Search, Google Maps, the Shopping tab, Gmail, Youtube, the Google Display Network, and Discover feed.","google-listings-and-ads"),{linkPmax:y}))},{trackId:"how-much-do-performance-max-campaigns-cost",question:(0,n.__)("How much do Performance Max campaigns cost?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("Performance Max campaigns are pay-per-click, meaning you only pay when someone clicks on your ads. To get the best results and ensure your products reach the right customers, we recommend starting with the suggested Google for WooCommerce minimum daily budget for your Performance Max campaign. This helps jumpstart your campaign and drive early conversions. You can always adjust your budget later as you see what works best for your business.","google-listings-and-ads"),{linkPmax:y}))},{trackId:"can-i-sync-my-products-and-run-performance-max-campaigns-on-google-for-woocommerce-at-the-same-time",question:(0,n.__)("Can I sync my products and run Performance Max campaigns on Google for WooCommerce at the same time?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("Yes, you can run both at the same time, and we recommend you do! Once you sync your store it’s automatically listed on Google, so you can choose to run a Performance Max campaign as soon as you’d like. In the US, advertisers who sync their products to Google and run Google Ads Performance Max campaigns have seen an average of over 50% increase in clicks and over 100% increase in impressions in both their product listings and their ads on the Shopping tab. ","google-listings-and-ads"),{linkPmax:y}))},{trackId:"how-does-google-for-woocommerce-help-me-drive-sales",question:(0,n.__)("How does Google for WooCommerce help me drive sales?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,n.__)("With Google for WooCommerce, you can serve the best-performing ads more often, by using Google AI to pull headlines, images, product details, and more from your product feed and find more relevant customers. Your campaigns will learn and optimize in real time – to help deliver better performance and boost your ROI.","google-listings-and-ads"))},{trackId:"what-are-enhanced-conversions",question:(0,n.__)("What are Enhanced conversions?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("Enhanced conversions is a feature that can improve the accuracy of your conversion measurement and unlock more powerful bidding. It supplements your existing conversion tags by sending hashed first-party conversion data from your website to Google in a privacy-safe way.","google-listings-and-ads"),{link:(0,a.createElement)(p.A,{context:"faqs",linkId:"google-enhanced-conversions",href:"https://support.google.com/google-ads/answer/9888656?hl=en-GB"})}))},{trackId:"which-countries-are-available-for-google-for-woocommerce",question:(0,n.__)("Which countries are available for Google for WooCommerce?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("For Performance Max campaigns, learn more about supported countries and currencies here.","google-listings-and-ads"),{linkPmax:y,link:(0,a.createElement)(p.A,{context:"faqs",linkId:"google-country-table",href:"https://support.google.com/merchants/answer/160637#countrytable"})}))},{trackId:"what-is-multi-country-advertising",question:(0,n.__)("What is Multi-Country Advertising?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,n.__)("Multi-Country Advertising enables you to create a single Google Ads campaign that targets multiple countries at once. Google for WooCommerce automatically populates eligible countries from your Google Merchant Center account into the plug-in ads campaign creation flow.","google-listings-and-ads"))},{trackId:"can-i-enable-multi-country-advertising-on-my-existing-campaigns",question:(0,n.__)("Can I enable Multi-Country Advertising on my existing campaigns?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,n.__)("If you created a campaign before this feature launched, you’ll need to create a new campaign to target new countries with Multi-Country Advertising","google-listings-and-ads"))},{trackId:"how-is-my-ads-budget-split-between-the-different-countries",question:(0,n.__)("How is my ads budget split between the different countries?","google-listings-and-ads"),answer:(0,a.createElement)("p",null,(0,n.__)("Identify the best performing targeted countries with the help of Google AI, to make your ads reach the right shoppers at the right time.","google-listings-and-ads"))},{trackId:"which-countries-can-i-target",question:(0,n.__)("Which countries can I target?","google-listings-and-ads"),answer:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,n.__)("You can only select the countries that you’re targeting on Google Merchant Center. Your target countries must be eligible for both Google Merchant Center and Google Ads.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,n.__)("To allow your products to appear in all relevant locations, make sure you’ve correctly configured your shipping for countries where your products can be delivered. Keep in mind that shipping services can cover multiple countries. Learn more about multi-country shipping.","google-listings-and-ads"),{linkShipping:(0,a.createElement)(p.A,{context:"faqs",linkId:"google-set-up-shipping",href:"https://support.google.com/merchants/answer/6069284"}),linkMultiCountryShipping:(0,a.createElement)(p.A,{context:"faqs",linkId:"google-set-up-multi-country-shipping",href:"https://support.google.com/merchants/answer/6069284#multicountryshipping"})})))}],f=()=>(0,a.createElement)(u.A,{className:"gla-get-started-faqs",trackName:"gla_faq",context:"get-started",faqItems:_});var w=o(1034),k=o(9351),E=o(4902);const b=({linkId:e,href:t})=>(0,a.createElement)(l.A,{className:"gla-get-started-features-card__learn-more",variant:"body"},(0,m.createInterpolateElement)((0,n.__)("Learn More →","google-listings-and-ads"),{link:(0,a.createElement)(p.A,{context:"get-started",linkId:e,href:t})})),v=()=>(0,a.createElement)(r.Card,{className:"gla-get-started-features-card",isBorderless:!0},(0,a.createElement)(r.CardHeader,null,(0,a.createElement)(l.A,{className:"gla-get-started-features-card__title",variant:"title-medium"},(0,n.__)("49% of shoppers surveyed say they use Google to discover or find a new item or product","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__description",variant:"body"},(0,n.__)("With Google for WooCommerce, connect with the right shoppers at the right moment when they’re looking to buy products like yours.","google-listings-and-ads"))),(0,a.createElement)(r.Flex,{gap:0},(0,a.createElement)(r.FlexBlock,null,(0,a.createElement)("img",{src:w,alt:(0,n.__)("Drawing of WooCommerce and Google","google-listings-and-ads"),width:"183",height:"100"}),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__label",variant:"label"},(0,n.__)("Show products automatically on Google for free","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__content",variant:"body"},(0,n.__)("When your products are added and approved, they’ll be eligible for free listings, reaching shoppers across Google’s network.","google-listings-and-ads")),(0,a.createElement)(b,{linkId:"get-started-features-free-listing-learn-more",href:"https://woocommerce.com/document/google-for-woocommerce/get-started/product-feed-information-and-free-listings/#section-1"})),(0,a.createElement)(r.FlexBlock,null,(0,a.createElement)("img",{src:k,alt:(0,n.__)("Drawing of a mobile and product ads","google-listings-and-ads"),width:"183",height:"100"}),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__label",variant:"label"},(0,n.__)("Promote products and drive more sales with Google Ads","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__content",variant:"body"},(0,n.__)("Connect your Google Ads account, choose a budget, and Google will optimize your ads so they appear at the right time and place. ","google-listings-and-ads")),(0,a.createElement)(b,{linkId:"get-started-features-google-ads-learn-more",href:"https://woocommerce.com/document/google-for-woocommerce/get-started/google-performance-max-campaigns"})),(0,a.createElement)(r.FlexBlock,null,(0,a.createElement)("img",{src:E,alt:(0,n.__)("Drawing of a bar and line charts heading up","google-listings-and-ads"),width:"183",height:"100"}),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__label",variant:"label"},(0,n.__)("Track performance straight from your store dashboard","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-features-card__content",variant:"body"},(0,n.__)("Real-time reporting all within your WooCommerce dashboard means you know how your campaigns are performing at all times.","google-listings-and-ads")),(0,a.createElement)(b,{linkId:"get-started-features-dashboard-learn-more",href:"https://woocommerce.com/document/google-for-woocommerce/get-started/campaign-analytics"}))));var G=o(3905),x=o(1948),I=o(7892),C=o(3666);const A=()=>{const e=!G.Th.mcSupportedLanguage;return(0,a.createElement)(r.Card,{className:"gla-get-started-card",isBorderless:!0},(0,a.createElement)(r.FlexItem,{className:"motivation-image"},(0,a.createElement)("img",{src:x,alt:(0,n.__)("Google Shopping search results example","google-listings-and-ads"),width:"279",height:"185"})),(0,a.createElement)(r.CardBody,null,(0,a.createElement)(l.A,{variant:"title-medium",className:"gla-get-started-card__title"},(0,n.__)("Get your products in front of more shoppers with Google for WooCommerce","google-listings-and-ads")),(0,a.createElement)(I.A,{isPrimary:!0,disabled:e,href:(0,C.xP)(),eventName:"gla_setup_mc",eventProps:{triggered_by:"start-onboarding-button",action:"go-to-onboarding",context:"get-started"}},(0,n.__)("Sell more on Google →","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-card__terms-notice"},(0,m.createInterpolateElement)((0,n.__)("By clicking ‘Sell more on Google‘, you agree to our Terms of Service.","google-listings-and-ads"),{link:(0,a.createElement)(p.A,{context:"get-started",linkId:"wp-terms-of-service",href:"https://wordpress.com/tos/"})}))))};var P=o(9899);const W=()=>{const e=!G.Th.mcSupportedLanguage;return(0,a.createElement)(r.Card,{className:"gla-get-started-with-hero-card",isBorderless:!0},(0,a.createElement)(r.FlexBlock,{className:"motivation"},(0,a.createElement)("div",{className:"gla-get-started-with-hero-card__image"},(0,a.createElement)("img",{src:P,alt:(0,n.__)("Google for WooCommerce","google-listings-and-ads"),width:"100%",height:"100%"}))),(0,a.createElement)(r.CardBody,null,(0,a.createElement)(l.A,{variant:"caption",className:"gla-get-started-with-hero-card__caption"},(0,n.__)("The official extension for WooCommerce, built in collaboration with Google","google-listings-and-ads")),(0,a.createElement)(l.A,{variant:"title-medium",className:"gla-get-started-with-hero-card__title"},(0,n.__)("Connect your WooCommerce store and reach millions of shoppers on Google","google-listings-and-ads")),(0,a.createElement)(l.A,{variant:"body",className:"gla-get-started-with-hero-card__description"},(0,n.__)("Effortlessly sync your WooCommerce product feed across Google and be seen by millions of engaged shoppers with the Google for WooCommerce extension.","google-listings-and-ads")),(0,a.createElement)(I.A,{className:"gla-get-started-with-hero-card__button",isPrimary:!0,disabled:e,href:(0,C.xP)(),eventName:"gla_setup_mc",eventProps:{triggered_by:"start-onboarding-button",action:"go-to-onboarding",context:"get-started-with-hero"}},(0,n.__)("Sell more on Google →","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-with-hero-card__hint"},(0,n.__)("Estimated setup time: 5 min","google-listings-and-ads")),(0,a.createElement)(l.A,{className:"gla-get-started-with-hero-card__terms-notice",variant:"body"},(0,m.createInterpolateElement)((0,n.__)("By clicking ‘Sell more on Google’, you agree to our Terms of Service.","google-listings-and-ads"),{link:(0,a.createElement)(p.A,{context:"get-started-with-hero",linkId:"wp-terms-of-service",href:"https://wordpress.com/tos/"})}))),(0,a.createElement)(r.Tip,null,(0,n.__)("If you’re already using another extension to manage your product feed with Google, make sure to deactivate or uninstall it first to prevent duplicate product feeds.","google-listings-and-ads")))};var N=o(7108),M=o(8846),q=o(8859),S=o(7337);const L=()=>(0,a.createElement)(r.Icon,{className:"gla-get-started-notice__icon",icon:N.A,size:18}),T=()=>{const{data:e}=(0,q.A)();return e?(0,a.createElement)(r.Notice,{className:"gla-get-started-notice",status:"error",isDismissible:!1},(0,m.createInterpolateElement)((0,n.__)("Your site language is . This language is currently not supported by Google for WooCommerce. You can change your site language here. Read more about supported languages","google-listings-and-ads"),{language:(0,a.createElement)("strong",null,e.language),settingsLink:(0,a.createElement)(M.Link,{type:"wp-admin",href:"/wp-admin/options-general.php"}),supportedLanguagesLink:(0,a.createElement)(p.A,{href:"https://support.google.com/merchants/answer/160637",context:"get-started",linkId:"supported-languages"})}),(0,a.createElement)(L,null)):null},B=()=>{const{name:e}=(0,S.A)();return e?(0,a.createElement)(r.Notice,{className:"gla-get-started-notice",status:"warning",isDismissible:!1},(0,m.createInterpolateElement)((0,n.__)("Your store’s country is . This country is currently not supported by Google for WooCommerce. However, you can still choose to list your products in another supported country, if you are able to sell your products to customers there. Change your store’s country here. Read more about supported countries","google-listings-and-ads"),{country:(0,a.createElement)("strong",null,e),settingsLink:(0,a.createElement)(M.Link,{type:"wp-admin",href:"/wp-admin/admin.php?page=wc-settings"}),supportedCountriesLink:(0,a.createElement)(p.A,{href:"https://support.google.com/merchants/answer/160637",context:"get-started",linkId:"supported-countries"})}),(0,a.createElement)(L,null)):null};function F(){const{mcSupportedLanguage:e,mcSupportedCountry:t}=G.Th;return(0,a.createElement)(a.Fragment,null,!e&&(0,a.createElement)(T,null),!t&&(0,a.createElement)(B,null))}const Y=()=>(0,a.createElement)("div",{className:"woocommerce-marketing-google-get-started-page"},(0,a.createElement)(F,null),(0,a.createElement)(W,null),(0,a.createElement)(i,null),(0,a.createElement)(v,null),(0,a.createElement)(d,null),(0,a.createElement)(A,null),(0,a.createElement)(f,null))}}]);PK!Mm^^js/build/gtag-events.asset.phpnu[ array('wp-hooks'), 'version' => '5a4e71ac555fd7fba253'); PK!C js/build/gtag-events.jsnu[(()=>{"use strict";const t=window.wp.hooks,e=(t,e)=>{const a={id:"gla_"+t.id,quantity:e,google_business_vertical:"retail"};return t.name&&(a.name=t.name),t?.categories?.length&&(a.category=t.categories[0].name),t?.prices?.price&&(a.price=parseInt(t.prices.price,10)/10**t.prices.currency_minor_unit),a},a=(t,a=1)=>{((t,e)=>{if("function"!=typeof gtag)throw new Error("Function gtag not implemented.");window.gtag("event","add_to_cart",{send_to:"GLA",...e})})(0,{ecomm_pagetype:"cart",event_category:"ecommerce",items:[e(t,a)]})},n=t=>{var e;return glaGtagData.products[t.id]&&(t.name=glaGtagData.products[t.id].name,t.prices=(e=glaGtagData.products[t.id].price,{price:Math.round(e*10**glaGtagData.currency_minor_unit),currency_minor_unit:glaGtagData.currency_minor_unit})),t};(0,t.addAction)("experimental__woocommerce_blocks-cart-add-item","google-listings-and-ads",(({product:t,quantity:e=1})=>{a(t,e)}));const r=function(t){const e=t.currentTarget.dataset,r=n({id:e.product_id});a(r,e.quantity||1)},o=function(t){const e=t.target.closest("form.cart");if(!e)return;const r=e.querySelector("[name=add-to-cart]");if(!r)return;const o=e.querySelector("[name=variation_id]"),c=e.querySelector("[name=quantity]"),i=n({id:parseInt(o?o.value:r.value,10)});a(i,c?parseInt(c.value,10):1)};document.defaultView.addEventListener("DOMContentLoaded",(function(){document.querySelectorAll(".add_to_cart_button:not( .product_type_variable ):not( .product_type_grouped ):not( .wc-block-components-product-button__button )").forEach((t=>{t.addEventListener("click",r)})),document.querySelectorAll('[data-block-name="woocommerce/product-button"] > .add_to_cart_button:not( .product_type_variable ):not( .product_type_grouped )').forEach((t=>{t.addEventListener("click",r)})),document.querySelectorAll(".single_add_to_cart_button").forEach((t=>{t.addEventListener("click",o)}))})),"function"==typeof jQuery&&jQuery(document).on("found_variation","form.cart",(function(t,e){(t=>{t?.variation_id&&(glaGtagData.products[t.variation_id]={name:t.display_name,price:t.display_price})})(e)}))})();PK!js/build/index.asset.phpnu[ array('lodash', 'react', 'react-dom', 'wc-components', 'wc-currency', 'wc-customer-effort-score', 'wc-date', 'wc-navigation', 'wc-number', 'wc-settings', 'wc-store-data', 'wc-tracks', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'dd19d4caece23e0030ca'); PK!tjs/build/index.cssnu[.gla-admin-page .components-button.is-primary.is-destructive:disabled{color:#fff6}.gla-admin-page .components-button.is-tertiary.is-destructive,.gla-admin-page .components-button.is-tertiary.is-destructive:hover:not(:disabled){box-shadow:none}.gla-admin-page .components-button.is-link{text-decoration:none}.gla-admin-page .components-button.is-link:disabled{color:initial}.gla-admin-page .components-button.is-link.is-destructive:focus{box-shadow:none;color:#cc1818}.gla-admin-page .components-button.is-link.is-destructive:focus:not(:disabled){color:#cc1818}.gla-admin-page .components-card{margin-bottom:0}.gla-admin-page .components-card__header{font-size:inherit}.gla-admin-page .components-input-control__suffix{margin-right:8px}.gla-full-content #wpbody{margin-top:0!important}.gla-full-content .woocommerce-layout{padding-top:0}.gla-full-content .woocommerce-layout .woocommerce-layout__header,.gla-full-content .woocommerce-layout .woocommerce-layout__notice-list,.gla-full-content .woocommerce-layout .woocommerce-store-alerts{display:none}.gla-full-content .woocommerce-layout .woocommerce-layout__primary{margin:0}.gla-full-content .woocommerce-layout .woocommerce-layout__primary .woocommerce-layout__main{padding:0}@media(min-width:600px)and (max-width:782px){.gla-full-page.is-wp-toolbar-disabled{margin-top:-46px}}.gla-full-page .woocommerce-layout{padding-top:0}.gla-full-page .woocommerce-layout .woocommerce-layout__primary{margin:0}.gla-full-page .woocommerce-layout .woocommerce-layout__primary .woocommerce-layout__main{padding:0}.gla-admin-page .woocommerce-stepper__steps{align-items:center;background-color:#fff;box-shadow:inset 0 -1px 0 #ccc;height:64px;justify-content:center;margin-bottom:0}.gla-admin-page .woocommerce-stepper__steps .woocommerce-stepper__step-divider{align-self:auto;margin-top:0;max-width:48px}.gla-admin-page .components-notice:not(.app-notice){margin-bottom:12px}.wp-admin .woocommerce-customer-effort-score__intro{margin-bottom:1em} PK!Yώjs/build/index.jsnu[(()=>{"use strict";var e,t,n={6023:(e,t,n)=>{n.d(t,{E1:()=>E,om:()=>p,tY:()=>d});var a=n(1609),s=n(7723),i=n(6427),o=n(6087),r=n(3905);const c=Symbol("sharedMax"),d=[{maxCharacterCount:15,capitalizedName:(0,s._x)("The first display URL path","Capitalized asset field name as the start of an error message","google-listings-and-ads")},{maxCharacterCount:15,capitalizedName:(0,s._x)("The second display URL path","Capitalized asset field name as the start of an error message","google-listings-and-ads")}],l=[{key:r.Ms.MARKETING_IMAGE,min:1,imageConfig:{minWidth:600,minHeight:314,suggestedWidth:1200,suggestedHeight:628},heading:(0,s._x)("Landscape images","Plural asset field name as the heading","google-listings-and-ads"),helpSubheading:(0,s._x)("Landscape image (1.91:1)","Asset field name with its aspect ratio as the subheading within a help tip","google-listings-and-ads"),lowercaseName:(0,s._x)("landscape","Lowercase asset field name","google-listings-and-ads")},{key:r.Ms.SQUARE_MARKETING_IMAGE,min:1,imageConfig:{minWidth:300,minHeight:300,suggestedWidth:1200,suggestedHeight:1200},heading:(0,s._x)("Square images","Plural asset field name as the heading","google-listings-and-ads"),helpSubheading:(0,s._x)("Square image (1:1)","Asset field name with its aspect ratio as the subheading within a help tip","google-listings-and-ads"),lowercaseName:(0,s._x)("square","Lowercase asset field name","google-listings-and-ads")},{key:r.Ms.PORTRAIT_MARKETING_IMAGE,min:0,imageConfig:{minWidth:480,minHeight:600,suggestedWidth:960,suggestedHeight:1200},heading:(0,s._x)("Portrait images","Plural asset field name as the heading","google-listings-and-ads"),helpSubheading:(0,s._x)("Portrait image (4:5)","Asset field name with its aspect ratio as the subheading within a help tip","google-listings-and-ads"),lowercaseName:(0,s._x)("portrait","Lowercase asset field name","google-listings-and-ads")}],g=[{key:r.Ms.LOGO,min:1,imageConfig:{minWidth:128,minHeight:128,suggestedWidth:1200,suggestedHeight:1200},heading:(0,s._x)("Logo","Plural asset field name as the heading","google-listings-and-ads"),helpSubheading:(0,s._x)("Logo (1:1)","Asset field name with its aspect ratio as the subheading within a help tip","google-listings-and-ads"),lowercaseName:(0,s._x)("logo","Lowercase asset field name","google-listings-and-ads")}];l[c]=20,g[c]=5;const u=[l,g],p=u.flat(),E=[{key:r.Ms.BUSINESS_NAME,min:1,max:1,maxCharacterCounts:25,heading:(0,s._x)("Business name","Plural asset field name as the heading","google-listings-and-ads"),capitalizedName:(0,s._x)("Business name","Capitalized asset field name as the placeholder or the start of an error message","google-listings-and-ads"),lowercaseSingularName:(0,s._x)("business name","Singular and lowercase asset field name","google-listings-and-ads"),help:(0,s.__)("The business name is the name of your business or brand. In certain layouts, it may appear in the text of your ad.","google-listings-and-ads")},{key:r.Ms.HEADLINE,min:3,max:5,maxCharacterCounts:[15,30,30,30,30],heading:(0,s._x)("Headlines","Plural asset field name as the heading","google-listings-and-ads"),extraSubheading:(0,a.createElement)(i.ExternalLink,{href:"https://support.google.com/google-ads/answer/6167101"},(0,s.__)("Learn how to write effective ads","google-listings-and-ads")),addButtonText:(0,s.__)("Add headline","google-listings-and-ads"),capitalizedName:(0,s._x)("Headline","Capitalized asset field name as the placeholder or the start of an error message","google-listings-and-ads"),lowercaseSingularName:(0,s._x)("headline","Singular and lowercase asset field name","google-listings-and-ads"),lowercasePluralName:(0,s._x)("headlines","Plural and lowercase asset field name","google-listings-and-ads"),help:(0,s.__)("The headline is the first line of your ad and is most likely the first thing people notice, so consider including words that people may have entered in their Google search.","google-listings-and-ads")},{key:r.Ms.LONG_HEADLINE,min:1,max:5,maxCharacterCounts:90,heading:(0,s._x)("Long headlines","Plural asset field name as the heading","google-listings-and-ads"),addButtonText:(0,s.__)("Add long headline","google-listings-and-ads"),capitalizedName:(0,s._x)("Long headline","Capitalized asset field name as the placeholder or the start of an error message","google-listings-and-ads"),lowercaseSingularName:(0,s._x)("long headline","Singular and lowercase asset field name","google-listings-and-ads"),lowercasePluralName:(0,s._x)("long headlines","Plural and lowercase asset field name","google-listings-and-ads"),help:(0,a.createElement)(o.Fragment,null,(0,a.createElement)("div",null,(0,s.__)("The long headline is the first line of your ad, and appears instead of your short headline in larger ads. Long headlines can be up to 90 characters, and may appear with or without your description.","google-listings-and-ads")),(0,a.createElement)("div",null,(0,s.__)("The length of the rendered headline will depend on the site it appears on. If shortened, it will end with an ellipsis(…).","google-listings-and-ads")))},{key:r.Ms.DESCRIPTION,min:2,max:5,maxCharacterCounts:[60,90,90,90,90],heading:(0,s._x)("Descriptions","Plural asset field name as the heading","google-listings-and-ads"),addButtonText:(0,s.__)("Add description","google-listings-and-ads"),capitalizedName:(0,s._x)("Description","Capitalized asset field name as the placeholder or the start of an error message","google-listings-and-ads"),lowercaseSingularName:(0,s._x)("description","Singular and lowercase asset field name","google-listings-and-ads"),lowercasePluralName:(0,s._x)("descriptions","Plural and lowercase asset field name","google-listings-and-ads"),help:(0,a.createElement)(o.Fragment,null,(0,a.createElement)("div",null,(0,s.__)("The description adds to the headline and provides additional context or details. It can be up to 90 characters, and may appear after the headline.","google-listings-and-ads")),(0,a.createElement)("div",null,(0,s.__)("The length of the rendered description will depend on the site it appears on. If it's shortened, it will end with an ellipsis(…). The description doesn't show in all sizes and formats.","google-listings-and-ads")))}];{function _(e){const t=(0,s._x)(", ","The separator for concatenating the types of assets","google-listings-and-ads");return(0,s.sprintf)( // translators: 1: Concatenated text for the types of assets except for the last one. 2: The last type of assets. // translators: 1: Concatenated text for the types of assets except for the last one. 2: The last type of assets. (0,s.__)("%1$s and %2$s","google-listings-and-ads"),e.slice(0,-1).join(t),e.at(-1))}function h(e,t){if(t){if(0===e.min)return;return(0,s.sprintf)( // translators: 1: The minimal number of this item. // translators: 1: The minimal number of this item. (0,s.__)("At least %d required","google-listings-and-ads"),e.min)}if(!e.requiredSingleValue)return(0,s.sprintf)( // translators: 1: The minimal number of this item. 2: The maximum number of this item. // translators: 1: The minimal number of this item. 2: The maximum number of this item. (0,s.__)("At least %1$d required. Add up to %2$d.","google-listings-and-ads"),e.min,e.max)}function m(e,t){const{helpSubheading:n,imageConfig:i}=e,r=(0,a.createElement)("ul",null,(0,o.createInterpolateElement)((0,s.sprintf)( // translators: 1: Recommended width. 2: Recommended height. 3: Minimal width. 4: Minimal height. // translators: 1: Recommended width. 2: Recommended height. 3: Minimal width. 4: Minimal height. (0,s.__)("Recommended size: %1$d x %2$dMin. size: %3$d x %4$d","google-listings-and-ads"),i.suggestedWidth,i.suggestedHeight,i.minWidth,i.minHeight),{listItem:(0,a.createElement)("li",null)}));return(0,a.createElement)(o.Fragment,{key:e.key},(0,a.createElement)("div",null,(0,a.createElement)("strong",null,n),t&&r),!t&&r)}function C(e){const t=e.map((e=>e.lowercaseName)),n=(0,s.sprintf)( // translators: 1: The maximum number of this image assets. 2: Text for the types of image assets. // translators: 1: The maximum number of this image assets. 2: Text for the types of image assets. (0,s.__)("You can add up to a maximum of %1$d image assets, which can be a combination of %2$s images.","google-listings-and-ads"),e[c],_(t));return(0,a.createElement)("div",null,n)}function T(e,t){const n=e.map((e=>m(e,t)));return(0,a.createElement)(o.Fragment,null,t&&C(e),(0,a.createElement)("div",null,(0,s.__)("Add images that meet or can be cropped to the recommended sizes. Note: The maximum file size for any image is 5120 KB.","google-listings-and-ads")),n)}function A(e,t,n){return t.reduce(((e,t)=>t.key===this.key?e:e-Math.max(t.min,n[t.key].length)),e)}function S(e,t,n){if(t.reduce(((e,t)=>e+n[t.key].length),0)===e)return(0,s.sprintf)( // translators: The shared maximum number of the grouped types of image assets. // translators: The shared maximum number of the grouped types of image assets. (0,s.__)("The maximum number of images that can be uploaded is %d.","google-listings-and-ads"),e);const a=t.filter((e=>e.min>0)).map((e=>(0,s.sprintf)( // translators: 1: The minimum number of this asset field. 2: Asset field name. // translators: 1: The minimum number of this asset field. 2: Asset field name. (0,s.__)("%1$d %2$s","google-listings-and-ads"),e.min,e.lowercaseName)));return(0,s.sprintf)( // translators: 1: The shared maximum number of the grouped types of image assets. 2: Text for the minimum number and type of each image asset. // translators: 1: The shared maximum number of the grouped types of image assets. 2: Text for the minimum number and type of each image asset. (0,s.__)("Maximum %1$d images can be uploaded, with a minimum of %2$s image.","google-listings-and-ads"),e,_(a))}u.forEach((e=>{const t=e.length>1,n=e[c],a=T(e,t);e.forEach((s=>{!t&&Number.isInteger(n)&&(s.max=n),s.subheading=h(s,t),s.help=a,s.getMax=A.bind(s,n,e),s.getMaxNumberTip=t?S.bind(null,n,e):()=>null}))})),E.forEach((e=>{e.requiredSingleValue=1===e.min&&1===e.max,e.subheading=h(e)}))}},3905:(e,t,n)=>{n.d(t,{$g:()=>y,CX:()=>I,DA:()=>i,K4:()=>l,Ms:()=>G,Mx:()=>o,Q:()=>s,Th:()=>a,Tj:()=>m,WR:()=>S,Wn:()=>A,X4:()=>T,Z3:()=>_,ZD:()=>N,aL:()=>R,ac:()=>P,ds:()=>h,iH:()=>u,k1:()=>r,km:()=>E,ll:()=>O,r6:()=>c,rS:()=>g,s_:()=>d,vL:()=>f,zU:()=>C});const a=window.glaData,s=(window.glaProductData,0),i="report-source",o="paid",r="free",c=o,d="programs",l={SUBMISSION_SUCCESS:"submission-success",CAMPAIGN_CREATION_SUCCESS:"campaign-creation-success"},g={CAN_ONBOARDING_SETUP_CES_PROMPT_OPEN:"gla-can-onboarding-setup-ces-prompt-open"},u={WPCOM_DISCONNECTED:"JETPACK_DISCONNECTED",GOOGLE_DISCONNECTED:"GOOGLE_DISCONNECTED"},p=[["CAMPAIGN","campaign"],["ASSET_GROUP","asset-group"]],E=Object.fromEntries(p),_=p.reduce(((e,t,n)=>{const a=(n+1).toString();return e[t[1]]=a,e}),{}),h="product",m="account",C="request-review",T=5,A={CONNECTED:"connected",DISCONNECTED:"disconnected",INCOMPLETE:"incomplete"},S={CONNECTED:"connected",DISCONNECTED:"disconnected",INCOMPLETE:"incomplete"},I={UNKNOWN:"unknown",PENDING:"pending",APPROVED:"approved",CANCELLED:"cancelled"},R={ALL:"ALL",EXCEPT:"EXCEPT",ONLY:"ONLY"},f=5,y="performance_max",N={BUSINESS_NAME:"business_name",MARKETING_IMAGE:"marketing_image",SQUARE_MARKETING_IMAGE:"square_marketing_image",PORTRAIT_MARKETING_IMAGE:"portrait_marketing_image",LOGO:"logo",HEADLINE:"headline",LONG_HEADLINE:"long_headline",DESCRIPTION:"description",CALL_TO_ACTION_SELECTION:"call_to_action_selection"},O={FINAL_URL:"final_url",DISPLAY_URL_PATH:"display_url_path"},G={...N,...O},P={APPROVED:"approved",DISAPPROVED:"disapproved",ERROR:"error",DISABLED:"disabled"}},6520:(e,t,n)=>{n.d(t,{RV:()=>i,Ui:()=>s,W1:()=>r,fP:()=>c,mY:()=>o});var a=n(3905);const s="wc/gla",i="/wc/gla",o="core/notices",r={DELETE:"DELETE",POST:"POST"},c={assets:{},[a.ll.FINAL_URL]:"",[a.ll.DISPLAY_URL_PATH]:[]}},3658:(e,t,n)=>{n.d(t,{U:()=>g.Ui,j:()=>Wt});var a={};n.r(a),n.d(a,{createAdsCampaign:()=>Y,createCampaignAssetGroup:()=>Z,createMappingRule:()=>pe,deleteAdsCampaign:()=>J,deleteMappingRule:()=>_e,deleteShippingRates:()=>y,deleteShippingTimes:()=>G,disconnectAllAccounts:()=>$,disconnectGoogleAccount:()=>x,disconnectGoogleAdsAccount:()=>F,fetchExistingGoogleAdsAccounts:()=>B,fetchExistingGoogleMCAccounts:()=>L,fetchGoogleAccount:()=>D,fetchGoogleAdsAccount:()=>v,fetchGoogleAdsAccountBillingStatus:()=>H,fetchGoogleAdsAccountStatus:()=>Ae,fetchGoogleMCAccount:()=>V,fetchJetpackAccount:()=>U,fetchMCSetup:()=>ae,fetchSettings:()=>P,fetchShippingRates:()=>R,fetchShippingTimes:()=>N,fetchTargetAudience:()=>W,hydratePrefetchedData:()=>I,receiveAdsAccount:()=>Q,receiveGoogleAccountAccess:()=>b,receiveGoogleAdsAccountBillingStatus:()=>k,receiveGoogleMCContactInformation:()=>j,receiveGtinMigrationStatus:()=>Te,receiveMCAccount:()=>z,receiveMCIssues:()=>oe,receiveMCProductFeed:()=>re,receiveMCProductStatistics:()=>se,receiveMCReviewRequest:()=>ie,receiveMCSetup:()=>ne,receiveMappingAttributes:()=>le,receiveMappingRules:()=>ue,receiveMappingSources:()=>ge,receiveReport:()=>te,receiveStoreCategories:()=>he,receiveTour:()=>me,saveSettings:()=>w,saveTargetAudience:()=>K,sendMCReviewRequest:()=>de,syncSettings:()=>M,updateAdsCampaign:()=>X,updateCampaignAssetGroup:()=>ee,updateGoogleMCContactInformation:()=>q,updateMCProductVisibility:()=>ce,updateMappingRule:()=>Ee,upsertShippingRates:()=>f,upsertShippingTimes:()=>O,upsertTour:()=>Ce});var s={};n.r(s),n.d(s,{getAdsBudgetRecommendations:()=>at,getAdsCampaigns:()=>ke,getCampaignAssetGroups:()=>He,getDashboardPerformance:()=>Ye,getExistingGoogleAdsAccounts:()=>ve,getExistingGoogleMCAccounts:()=>be,getGeneral:()=>Ne,getGoogleAccount:()=>Me,getGoogleAccountAccess:()=>Ue,getGoogleAdsAccount:()=>Ve,getGoogleAdsAccountBillingStatus:()=>Le,getGoogleAdsAccountStatus:()=>nt,getGoogleMCAccount:()=>De,getGoogleMCContactInformation:()=>xe,getGtinMigrationStatus:()=>st,getJetpackAccount:()=>we,getMCCountriesAndContinents:()=>Fe,getMCIssues:()=>We,getMCProductFeed:()=>ze,getMCProductStatistics:()=>je,getMCReviewRequest:()=>qe,getMCSetup:()=>Be,getMappingAttributes:()=>Xe,getMappingRules:()=>Ze,getMappingSources:()=>Je,getReport:()=>Ke,getReportByApiQuery:()=>Qe,getSettings:()=>Pe,getShippingRates:()=>Oe,getShippingTimes:()=>Ge,getStoreCategories:()=>et,getTargetAudience:()=>$e,getTour:()=>tt});var i={};n.r(i),n.d(i,{getAdsBudgetRecommendations:()=>xt,getAdsCampaigns:()=>Rt,getCampaignAssetGroups:()=>ft,getExistingGoogleAdsAccounts:()=>Tt,getExistingGoogleMCAccounts:()=>ht,getGoogleAccount:()=>pt,getGoogleAccountAccess:()=>Et,getGoogleAdsAccount:()=>mt,getGoogleAdsAccountBillingStatus:()=>Ct,getGoogleAdsAccountStatus:()=>vt,getGoogleMCAccount:()=>_t,getGoogleMCContactInformation:()=>At,getGtinMigrationStatus:()=>Ft,getJetpackAccount:()=>ut,getMCCountriesAndContinents:()=>St,getMCIssues:()=>Gt,getMCProductFeed:()=>Pt,getMCProductStatistics:()=>Nt,getMCReviewRequest:()=>Ot,getMCSetup:()=>yt,getMappingAttributes:()=>Ut,getMappingRules:()=>bt,getMappingSources:()=>Dt,getReportByApiQuery:()=>Mt,getSettings:()=>gt,getShippingRates:()=>dt,getShippingTimes:()=>lt,getStoreCategories:()=>Vt,getTargetAudience:()=>It,getTour:()=>Lt});var o=n(7143),r=n(1455),c=n.n(r),d=n(6476),l=n(3905),g=n(6520);const u=window.wp.dataControls;var p=n(7723);const E={RECEIVE_SHIPPING_RATES:"RECEIVE_SHIPPING_RATES",UPSERT_SHIPPING_RATES:"UPSERT_SHIPPING_RATES",DELETE_SHIPPING_RATES:"DELETE_SHIPPING_RATES",RECEIVE_SHIPPING_TIMES:"RECEIVE_SHIPPING_TIMES",UPSERT_SHIPPING_TIMES:"UPSERT_SHIPPING_TIMES",DELETE_SHIPPING_TIMES:"DELETE_SHIPPING_TIMES",RECEIVE_SETTINGS:"RECEIVE_SETTINGS",SAVE_SETTINGS:"SAVE_SETTINGS",RECEIVE_ACCOUNTS_JETPACK:"RECEIVE_ACCOUNTS_JETPACK",RECEIVE_ACCOUNTS_GOOGLE:"RECEIVE_ACCOUNTS_GOOGLE",RECEIVE_ACCOUNTS_GOOGLE_ACCESS:"RECEIVE_ACCOUNTS_GOOGLE_ACCESS",RECEIVE_ACCOUNTS_GOOGLE_MC:"RECEIVE_ACCOUNTS_GOOGLE_MC",RECEIVE_ACCOUNTS_GOOGLE_MC_EXISTING:"RECEIVE_ACCOUNTS_GOOGLE_MC_EXISTING",RECEIVE_ACCOUNTS_GOOGLE_ADS:"RECEIVE_ACCOUNTS_GOOGLE_ADS",DISCONNECT_ACCOUNTS_GOOGLE:"DISCONNECT_ACCOUNTS_GOOGLE",DISCONNECT_ACCOUNTS_GOOGLE_ADS:"DISCONNECT_ACCOUNTS_GOOGLE_ADS",DISCONNECT_ACCOUNTS_ALL:"DISCONNECT_ACCOUNTS_ALL",RECEIVE_ACCOUNTS_GOOGLE_ADS_BILLING_STATUS:"RECEIVE_ACCOUNTS_GOOGLE_ADS_BILLING_STATUS",RECEIVE_ACCOUNTS_GOOGLE_ADS_EXISTING:"RECEIVE_ACCOUNTS_GOOGLE_ADS_EXISTING",RECEIVE_MC_CONTACT_INFORMATION:"RECEIVE_MC_CONTACT_INFORMATION",RECEIVE_MC_COUNTRIES_AND_CONTINENTS:"RECEIVE_MC_COUNTRIES_AND_CONTINENTS",RECEIVE_TARGET_AUDIENCE:"RECEIVE_TARGET_AUDIENCE",SAVE_TARGET_AUDIENCE:"SAVE_TARGET_AUDIENCE",RECEIVE_ADS_CAMPAIGNS:"RECEIVE_ADS_CAMPAIGNS",CREATE_ADS_CAMPAIGN:"CREATE_ADS_CAMPAIGN",UPDATE_ADS_CAMPAIGN:"UPDATE_ADS_CAMPAIGN",DELETE_ADS_CAMPAIGN:"DELETE_ADS_CAMPAIGN",RECEIVE_CAMPAIGN_ASSET_GROUPS:"RECEIVE_CAMPAIGN_ASSET_GROUPS",CREATE_CAMPAIGN_ASSET_GROUP:"CREATE_CAMPAIGN_ASSET_GROUP",UPDATE_CAMPAIGN_ASSET_GROUP:"UPDATE_CAMPAIGN_ASSET_GROUP",RECEIVE_MC_SETUP:"RECEIVE_MC_SETUP",RECEIVE_REPORT:"RECEIVE_REPORT",RECEIVE_MC_PRODUCT_STATISTICS:"RECEIVE_MC_PRODUCT_STATISTICS",RECEIVE_MC_REVIEW_REQUEST:"RECEIVE_MC_REVIEW_REQUEST",RECEIVE_MC_ISSUES:"RECEIVE_MC_ISSUES",RECEIVE_MC_PRODUCT_FEED:"RECEIVE_MC_PRODUCT_FEED",UPDATE_MC_PRODUCTS_VISIBILITY:"UPDATE_MC_PRODUCTS_VISIBILITY",RECEIVE_MAPPING_ATTRIBUTES:"RECEIVE_MAPPING_ATTRIBUTES",RECEIVE_MAPPING_SOURCES:"RECEIVE_MAPPING_SOURCES",RECEIVE_MAPPING_RULES:"RECEIVE_MAPPING_RULES",UPSERT_MAPPING_RULE:"UPSERT_MAPPING_RULE",DELETE_MAPPING_RULE:"DELETE_MAPPING_RULE",RECEIVE_STORE_CATEGORIES:"RECEIVE_STORE_CATEGORIES",RECEIVE_TOUR:"RECEIVE_TOUR",UPSERT_TOUR:"UPSERT_TOUR",HYDRATE_PREFETCHED_DATA:"HYDRATE_PREFETCHED_DATA",RECEIVE_GOOGLE_ADS_ACCOUNT_STATUS:"RECEIVE_GOOGLE_ADS_ACCOUNT_STATUS",RECEIVE_ADS_BUDGET_RECOMMENDATIONS:"RECEIVE_ADS_BUDGET_RECOMMENDATIONS",RECEIVE_GTIN_MIGRATION_STATUS:"RECEIVE_GTIN_MIGRATION_STATUS"};var _=n(8998),h=n(6023),m=n(399);function C(e){const t=e.targeted_locations.length>0,n=t?e.targeted_locations:[e.country];return{...e,allowMultiple:t,displayCountries:n}}function T(e){const t=new Map;h.E1.forEach((e=>{const{maxCharacterCounts:n}=e;if(Array.isArray(n)){const[a,s]=n;a{const s=a[t];if(s&&!(s.length<2)&&n(s[0].content)>e){const i=s.findIndex((({content:t})=>n(t)<=e));i>0&&(s.unshift(...s.splice(i,1)),a[t]=s)}})),{...e,assets:a}}const A=()=>window.navigator.userAgent.toLowerCase().includes("wc-ios"),S=()=>window.navigator.userAgent.toLowerCase().includes("wc-android");function I(e){return{type:E.HYDRATE_PREFETCHED_DATA,data:e}}function*R(){try{const e=(yield(0,u.apiFetch)({path:`${g.RV}/mc/shipping/rates`})).map((e=>({...e,rate:Number(e.rate)})));return{type:E.RECEIVE_SHIPPING_RATES,shippingRates:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading shipping rates.","google-listings-and-ads"))}}function*f(e){const t=(yield(0,u.apiFetch)({path:`${g.RV}/mc/shipping/rates/batch`,method:"POST",data:{rates:e}})).success.map((e=>({...e.rate,rate:Number(e.rate.rate)})));return{type:E.UPSERT_SHIPPING_RATES,shippingRates:t}}function*y(e){return yield(0,u.apiFetch)({path:`${g.RV}/mc/shipping/rates/batch`,method:"DELETE",data:{ids:e}}),{type:E.DELETE_SHIPPING_RATES,ids:e}}function*N(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/shipping/times`}),t=Object.values(e).map((e=>({countryCode:e.country_code,time:Number(e.time),maxTime:Number(e.max_time)})));return{type:E.RECEIVE_SHIPPING_TIMES,shippingTimes:t}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading shipping times.","google-listings-and-ads"))}}function*O(e){const{countries:t,time:n,maxTime:a}=e;return yield(0,u.apiFetch)({path:`${g.RV}/mc/shipping/times/batch`,method:"POST",data:{country_codes:t,time:n,max_time:a}}),{type:E.UPSERT_SHIPPING_TIMES,shippingTime:e}}function*G(e){return yield(0,u.apiFetch)({path:`${g.RV}/mc/shipping/times/batch`,method:"DELETE",data:{country_codes:e}}),{type:E.DELETE_SHIPPING_TIMES,countryCodes:e}}function*P(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/settings`});return{type:E.RECEIVE_SETTINGS,settings:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading merchant center settings.","google-listings-and-ads"))}}function*w(e){return yield(0,u.apiFetch)({path:`${g.RV}/mc/settings`,method:"POST",data:e}),{type:E.SAVE_SETTINGS,settings:e}}function*M(){yield(0,u.apiFetch)({path:`${g.RV}/mc/settings/sync`,method:"POST"})}function*U(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/jetpack/connected`});return{type:E.RECEIVE_ACCOUNTS_JETPACK,account:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading Jetpack account info.","google-listings-and-ads"))}}function*D(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/google/connected`});return{type:E.RECEIVE_ACCOUNTS_GOOGLE,account:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading Google account info.","google-listings-and-ads"))}}function b(e){return{type:E.RECEIVE_ACCOUNTS_GOOGLE_ACCESS,data:e}}function*V(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/connection`}),t=e.id||null;return yield I({mcId:t}),{type:E.RECEIVE_ACCOUNTS_GOOGLE_MC,account:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading Google Merchant Center account info.","google-listings-and-ads"))}}function*L(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/accounts`});return{type:E.RECEIVE_ACCOUNTS_GOOGLE_MC_EXISTING,accounts:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error getting your Google Merchant Center accounts.","google-listings-and-ads"))}}function*v(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/ads/connection`}),t=e.id||null;return yield I({adsId:t}),{type:E.RECEIVE_ACCOUNTS_GOOGLE_ADS,account:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading Google Ads account info.","google-listings-and-ads"))}}function*x(){try{return yield(0,u.apiFetch)({path:`${g.RV}/google/connect`,method:"DELETE"}),{type:E.DISCONNECT_ACCOUNTS_GOOGLE}}catch(e){throw(0,_.h)(e,(0,p.__)("Unable to disconnect your Google account.","google-listings-and-ads")),e}}function*F(e=!1){try{return yield(0,u.apiFetch)({path:`${g.RV}/ads/connection`,method:"DELETE"}),{type:E.DISCONNECT_ACCOUNTS_GOOGLE_ADS,invalidateRelatedState:e}}catch(e){throw(0,_.h)(e,(0,p.__)("Unable to disconnect your Google Ads account.","google-listings-and-ads")),e}}function*$(){try{return yield(0,u.apiFetch)({path:`${g.RV}/connections`,method:"DELETE"}),{type:E.DISCONNECT_ACCOUNTS_ALL}}catch(e){if(e.errors[`${g.RV}/rest-api/authorize`])return{type:E.DISCONNECT_ACCOUNTS_ALL};throw(0,_.h)(e,(0,p.__)("Unable to disconnect all your accounts.","google-listings-and-ads")),e}}function k(e){return{type:E.RECEIVE_ACCOUNTS_GOOGLE_ADS_BILLING_STATUS,billingStatus:e}}function*H(){try{return k(yield(0,u.apiFetch)({path:`${g.RV}/ads/billing-status`}))}catch(e){(0,_.h)(e,(0,p.__)("There was an error getting the billing status of your Google Ads account.","google-listings-and-ads"))}}function*B(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/ads/accounts`});return{type:E.RECEIVE_ACCOUNTS_GOOGLE_ADS_EXISTING,accounts:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error getting your Google Ads accounts.","google-listings-and-ads"))}}function j(e){return{type:E.RECEIVE_MC_CONTACT_INFORMATION,data:e}}function*q(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/contact-information`,method:"POST"});yield j(e)}catch(e){throw(0,_.h)(e,(0,p.__)("Unable to update your Google Merchant Center contact information.","google-listings-and-ads")),e}}function*W(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/target_audience`});return{type:E.RECEIVE_TARGET_AUDIENCE,target_audience:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading target audience.","google-listings-and-ads"))}}function z(e){return{type:E.RECEIVE_ACCOUNTS_GOOGLE_MC,account:e}}function Q(e){return{type:E.RECEIVE_ACCOUNTS_GOOGLE_ADS,account:e}}function*K(e){return yield(0,u.apiFetch)({path:`${g.RV}/mc/target_audience`,method:"POST",data:e}),{type:E.SAVE_TARGET_AUDIENCE,target_audience:e}}function*Y(e,t){let n="wc-web";A()?n="wc-ios":S()&&(n="wc-android");try{const a=yield(0,u.apiFetch)({path:`${g.RV}/ads/campaigns`,method:"POST",data:{amount:e,targeted_locations:t,label:n}});return{type:E.CREATE_ADS_CAMPAIGN,createdCampaign:C(a)}}catch(e){throw(0,_.h)(e),e}}function*X(e,t){try{return yield(0,u.apiFetch)({path:`${g.RV}/ads/campaigns/${e}`,method:"PATCH",data:t}),{type:E.UPDATE_ADS_CAMPAIGN,id:e,data:t}}catch(e){throw(0,_.h)(e),e}}function*J(e){try{return yield(0,u.apiFetch)({path:`${g.RV}/ads/campaigns/${e}`,method:"DELETE"}),{type:E.DELETE_ADS_CAMPAIGN,id:e}}catch(e){throw(0,_.h)(e),e}}function*Z(e){try{const t=yield(0,u.apiFetch)({path:`${g.RV}/ads/campaigns/asset-groups`,method:"POST",data:{campaign_id:e}});return{type:E.CREATE_CAMPAIGN_ASSET_GROUP,campaignId:e,assetGroup:{...g.fP,id:t.id}}}catch(e){const t=(0,p.__)("There was an error creating the assets of the campaign.","google-listings-and-ads");throw(0,_.h)(e,null,t),e}}function*ee(e,t){try{return yield(0,u.apiFetch)({path:`${g.RV}/ads/campaigns/asset-groups/${e}`,method:"PUT",data:t}),{type:E.UPDATE_CAMPAIGN_ASSET_GROUP,assetGroupId:e}}catch(e){const t=(0,p.__)("There was an error updating the assets of the campaign.","google-listings-and-ads");throw(0,_.h)(e,null,t),e}}function te(e,t){return{type:E.RECEIVE_REPORT,reportKey:e,data:t}}function*ne(e){return{type:E.RECEIVE_MC_SETUP,mcSetup:e}}function*ae(){try{return ne(yield(0,u.apiFetch)({path:`${g.RV}/mc/setup`}))}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading your merchant center setup status.","google-listings-and-ads"))}}function*se(e){return{type:E.RECEIVE_MC_PRODUCT_STATISTICS,mcProductStatistics:e}}function*ie(e){return{type:E.RECEIVE_MC_REVIEW_REQUEST,mcReviewRequest:e}}function*oe(e,t){return{type:E.RECEIVE_MC_ISSUES,query:e,data:t}}function*re(e,t){return{type:E.RECEIVE_MC_PRODUCT_FEED,query:e,data:t}}function*ce(e,t){try{return yield(0,u.apiFetch)({path:`${g.RV}/mc/product-visibility`,method:"POST",data:{ids:e,visible:t}}),{type:E.UPDATE_MC_PRODUCTS_VISIBILITY}}catch(e){throw(0,_.h)(e,(0,p.__)("Unable to update the channel visibility of products.","google-listings-and-ads")),e}}function*de(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/review`,method:"POST"});return yield ie(e)}catch(e){throw(0,_.h)(e),e}}function*le(e){return{type:E.RECEIVE_MAPPING_ATTRIBUTES,attributes:e}}function*ge(e,t){return{type:E.RECEIVE_MAPPING_SOURCES,sources:e,attributeKey:t}}function*ue(e,t){return{type:E.RECEIVE_MAPPING_RULES,rules:e,pagination:t}}function*pe(e){try{const t=yield(0,u.apiFetch)({path:`${g.RV}/mc/mapping/rules`,method:"POST",data:e});return{type:E.UPSERT_MAPPING_RULE,rule:t}}catch(e){throw(0,_.h)(e,(0,p.__)("There was an error creating the rule.","google-listings-and-ads")),e}}function*Ee(e){try{const t=yield(0,u.apiFetch)({path:`${g.RV}/mc/mapping/rules/${e.id}`,method:g.W1.POST,data:e});return{type:E.UPSERT_MAPPING_RULE,rule:t}}catch(e){throw(0,_.h)(e,(0,p.__)("There was an error updating the rule.","google-listings-and-ads")),e}}function*_e(e){try{const t=yield(0,u.apiFetch)({path:`${g.RV}/mc/mapping/rules/${e.id}`,method:g.W1.DELETE,data:e});return{type:E.DELETE_MAPPING_RULE,rule:t}}catch(e){throw(0,_.h)(e,(0,p.__)("There was an error deleting the rule.","google-listings-and-ads")),e}}function*he(e){return{type:E.RECEIVE_STORE_CATEGORIES,storeCategories:e}}function*me(e){return{type:E.RECEIVE_TOUR,tour:e}}function*Ce(e,t=!1){const n=[(0,u.apiFetch)({path:`${g.RV}/tours`,method:g.W1.POST,data:e})],a={type:E.UPSERT_TOUR,tour:e};!0===t?n.unshift(a):n.push(a);try{for(const e of n)yield e}catch(e){(0,_.h)(e,(0,p.__)("There was an error updating the tour.","google-listings-and-ads"))}}function*Te(e){return{type:E.RECEIVE_GTIN_MIGRATION_STATUS,data:e}}function*Ae(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/ads/account-status`});return{type:E.RECEIVE_GOOGLE_ADS_ACCOUNT_STATUS,data:e}}catch(e){(0,_.h)(e,(0,p.__)("There was an error getting the status of your Google Ads account.","google-listings-and-ads"))}}var Se={};function Ie(e){return[e]}function Re(e,t,n){var a;if(e.length!==t.length)return!1;for(a=n;ae.general,Oe=e=>e.mc.shipping.rates,Ge=e=>e.mc.shipping.times,Pe=e=>e.mc.settings,we=e=>e.mc.accounts.jetpack,Me=e=>e.mc.accounts.google,Ue=e=>e.mc.accounts.google_access,De=e=>e.mc.accounts.mc,be=e=>e.mc.accounts.existing_mc,Ve=e=>e.mc.accounts.ads,Le=e=>e.mc.accounts.ads_billing_status,ve=e=>e.mc.accounts.existing_ads,xe=e=>e.mc.contact,Fe=fe((e=>{const{countries:t,continents:n}=e.mc;return{countries:t,continents:n}}),(e=>[e.mc.countries,e.mc.continents])),$e=e=>e.mc.target_audience,ke=(e,t)=>!1===t?.exclude_removed?e.all_ads_campaigns:e.ads_campaigns,He=(e,t)=>e.campaign_asset_groups[t]||null,Be=e=>e.mc_setup,je=e=>e.mc_product_statistics,qe=e=>e.mc_review_request,We=fe(((e,t)=>{const n=e.mc_issues[t.issue_type];if(!n)return n;const a=(t.page-1)*t.per_page,s=a+t.per_page;return{issues:n.issues.slice(a,s),total:n.total}}),(e=>[e.mc_issues])),ze=(e,t)=>e.mc_product_feed?{products:e.mc_product_feed.pages[t.page],total:e.mc_product_feed.total}:e.mc_product_feed,Qe=(e,t,n,a)=>{const s=(0,ye.kj)(t,n,a);return e.report[s]||null},Ke=(0,o.createRegistrySelector)((e=>(t,n,a,s,i)=>{const o=e(g.Ui),r=(0,ye.dh)(n,a,s,i),c=[n,a,r];return{reportQuery:r,loaded:o.hasFinishedResolution("getReportByApiQuery",c),data:o.getReportByApiQuery(...c)}})),Ye=(0,o.createRegistrySelector)((e=>(t,n,a,s)=>{const i=e(g.Ui),o=["programs",n,(0,ye.N2)(n,a,s)],r=i.getReportByApiQuery(...o);return{data:r?r.totals:null,loaded:i.hasFinishedResolution("getReportByApiQuery",o)}})),Xe=e=>e.mc.mapping.attributes,Je=(e,t)=>e.mc.mapping.sources[t],Ze=fe(((e,t)=>{const n={...e.mc.mapping.rules},{page:a,perPage:s}=t,i=(a-1)*s,o=i+s;return{rules:n?.items.slice(i,o)||[],total:n.total,pages:n.pages}}),(e=>[e.mc.mapping.rules])),et=e=>e.store_categories,tt=(e,t)=>e.tours[t]||null,nt=e=>e.ads.accountStatus,at=(e,t=[])=>{const n=(0,ye.eT)(t);return e.ads.budgetRecommendations[n]||null},st=e=>e.gtinMigrationStatus;var it=n(3832);const ot=e=>({type:"FETCH_WITH_HEADERS",options:e}),rt=e=>({type:"GLA_AWAIT_PROMISE",promise:e}),ct={...u.controls,FETCH_WITH_HEADERS:({options:e})=>c()({...e,parse:!1}).then((e=>Promise.all([e.headers,e.status,e.json()]))).then((([e,t,n])=>({headers:e,status:t,data:n}))),GLA_AWAIT_PROMISE:({promise:e})=>e};function*dt(){yield R()}function*lt(){yield N()}function*gt(){yield P()}function*ut(){yield U()}function*pt(){yield D()}function*Et(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/google/reconnected`});yield b(e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading Google account access info.","google-listings-and-ads"))}}function*_t(){yield V()}function*ht(){yield L()}function*mt(){yield v()}function*Ct(){yield H()}function*Tt(){yield B()}function*At(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/contact-information`});yield j(e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading Google Merchant Center contact information.","google-listings-and-ads"))}}function*St(){try{const e={continents:!0},t=(0,it.addQueryArgs)(`${g.RV}/mc/countries`,e),n=yield(0,u.apiFetch)({path:t});return{type:E.RECEIVE_MC_COUNTRIES_AND_CONTINENTS,data:n}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading supported country details.","google-listings-and-ads"))}}function*It(){yield W()}function*Rt(e){try{const t=yield(0,u.apiFetch)({path:(0,it.addQueryArgs)(`${g.RV}/ads/campaigns`,e)});return{type:E.RECEIVE_ADS_CAMPAIGNS,query:e,adsCampaigns:t.map(C)}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading ads campaigns.","google-listings-and-ads"))}}function*ft(e){const t=`${g.RV}/ads/campaigns/asset-groups`,n={campaign_id:e},a=(0,it.addQueryArgs)(t,n);try{const t=yield(0,u.apiFetch)({path:a});return{type:E.RECEIVE_CAMPAIGN_ASSET_GROUPS,campaignId:e,assetGroups:t.map(T)}}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading the assets of the campaign.","google-listings-and-ads"))}}function*yt(){yield ae()}function*Nt(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/product-statistics`});yield se(e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading your merchant center product statistics.","google-listings-and-ads"))}}function*Ot(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/review`});yield ie(e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading your merchant center product review request status.","google-listings-and-ads"))}}function*Gt(e){try{const{issue_type:t,...n}=e,a=yield(0,u.apiFetch)({path:(0,it.addQueryArgs)(`${g.RV}/mc/issues/${t||l.Tj}`,n)});yield oe(e,a)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading issues to resolve.","google-listings-and-ads"))}}function*Pt(e){try{const t=yield(0,u.apiFetch)({path:(0,it.addQueryArgs)(`${g.RV}/mc/product-feed`,e)});yield re(e,t)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading product feed.","google-listings-and-ads"))}}pt.shouldInvalidate=e=>e.type===E.DISCONNECT_ACCOUNTS_GOOGLE,Et.shouldInvalidate=e=>e.type===E.DISCONNECT_ACCOUNTS_GOOGLE,mt.shouldInvalidate=e=>e.type===E.DISCONNECT_ACCOUNTS_GOOGLE_ADS&&e.invalidateRelatedState,Ct.shouldInvalidate=e=>e.type===E.RECEIVE_ACCOUNTS_GOOGLE_ADS,Tt.shouldInvalidate=mt.shouldInvalidate,Rt.shouldInvalidate=(e,t)=>(e.type===E.UPDATE_ADS_CAMPAIGN||e.type===E.DELETE_ADS_CAMPAIGN||e.type===E.CREATE_ADS_CAMPAIGN)&&!1===t?.exclude_removed,Gt.shouldInvalidate=e=>e.type===E.UPDATE_MC_PRODUCTS_VISIBILITY,Pt.shouldInvalidate=(e,t)=>e.type===E.UPDATE_MC_PRODUCTS_VISIBILITY||e.type===E.RECEIVE_MC_PRODUCT_FEED&&(e.query.per_page!==t.per_page||e.query.orderby!==t.orderby||e.query.order!==t.order);const wt=new Map([[l.k1,"mc"],[l.Mx,"ads"]]);function*Mt(e,t,n){const a=wt.get(t),s=`${g.RV}/${a}/reports/${e}`,i=(0,it.addQueryArgs)(s,n);try{const a=yield(0,u.apiFetch)({path:i}),s=(0,ye.kj)(e,t,n);yield te(s,a)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading report.","google-listings-and-ads"))}}function*Ut(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/mapping/attributes`});yield le(e.data)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading the mapping attributes.","google-listings-and-ads"))}}function*Dt(e){try{if(!e)return;const t=yield(0,u.apiFetch)({path:(0,it.addQueryArgs)(`${g.RV}/mc/mapping/sources`,{attribute:e})});yield ge(t.data,e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading the mapping sources for the selected attribute.","google-listings-and-ads"))}}function*bt(e){try{const t=yield ot({path:(0,it.addQueryArgs)(`${g.RV}/mc/mapping/rules`,{page:e.page,per_page:e.perPage})}),n=parseInt(t.headers.get("x-wp-total"),10),a=parseInt(t.headers.get("x-wp-totalpages"),10),s=t.data;yield ue(s,{...e,total:n,pages:a})}catch(e){(0,_.h)(e,(0,p.__)("There was an error loading the mapping rules.","google-listings-and-ads"))}}function*Vt(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/mc/mapping/categories`});yield he(e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error getting the store categories.","google-listings-and-ads"))}}function*Lt(e){try{const{data:t}=yield ot({path:`${g.RV}/tours/${e}`});yield me(t)}catch(e){if(404===e.status)return;const t=e?.json()||e?.text(),n=yield rt(t);(0,_.h)(n,(0,p.__)("There was an error getting the tour.","google-listings-and-ads"))}}function*vt(){yield Ae()}function*xt(e){if(!e||!e.length)return;const t=(0,ye.eT)(e),n=`${g.RV}/ads/campaigns/budget-recommendation`,a={country_codes:e},s=(0,it.addQueryArgs)(n,a);try{const{data:e}=yield ot({path:s}),{currency:n,recommendations:a}=e;return{type:E.RECEIVE_ADS_BUDGET_RECOMMENDATIONS,countryCodesKey:t,currency:n,recommendations:a}}catch(e){if(404===e.status)return;const t=e?.json()||e?.text(),n=yield rt(t);(0,_.h)(n,(0,p.__)("There was an error getting the budget recommendation.","google-listings-and-ads"))}}function*Ft(){try{const e=yield(0,u.apiFetch)({path:`${g.RV}/gtin-migration`});yield Te(e)}catch(e){(0,_.h)(e,(0,p.__)("There was an error getting the GTIN Migration Status.","google-listings-and-ads"))}}bt.shouldInvalidate=e=>e.type===E.UPSERT_MAPPING_RULE||e.type===E.DELETE_MAPPING_RULE,vt.shouldInvalidate=e=>e.type===E.DISCONNECT_ACCOUNTS_GOOGLE_ADS,xt.shouldInvalidate=e=>e.type===E.DISCONNECT_ACCOUNTS_GOOGLE_ADS;var $t=n(8468);const kt={general:{version:null,mcId:null,adsId:null},mc:{target_audience:null,countries:null,continents:null,shipping:{rates:[],times:[]},settings:null,accounts:{jetpack:null,google:null,mc:null,ads:null,existing_mc:null,existing_ads:null,ads_billing_status:null,google_access:null},contact:null,mapping:{attributes:[],sources:{},rules:{items:[],total:null,pages:null}}},ads_campaigns:null,all_ads_campaigns:null,campaign_asset_groups:{},mc_setup:null,mc_product_statistics:null,mc_issues:{account:null,product:null},mc_review_request:{status:null,cooldown:null,issues:null,reviewEligibleRegions:[]},mc_product_feed:null,report:{},store_categories:[],tours:{},ads:{accountStatus:{hasAccess:null,inviteLink:null,step:null},budgetRecommendations:{}},gtinMigrationStatus:null};function Ht(e,t=""){const n=Object.assign(e.constructor(),e),a=e=>null==e?{}:(0,$t.clone)(e);return{setIn(e,s){const i=(e=>t?Array.isArray(t)||Array.isArray(e)?[].concat(t,e):`${t}.${e}`:e)(e);return(0,$t.setWith)(n,i,s,a),this},end:()=>n}}function Bt(e,t,n){return Ht(e).setIn(t,n).end()}function jt(e,t){return t?e.json?e.json():Promise.reject(e):e}var qt=n(3666);(0,o.registerStore)(g.Ui,{actions:a,selectors:s,resolvers:i,controls:ct,reducer:(e=kt,t)=>{switch(t.type){case E.RECEIVE_SHIPPING_RATES:return Bt(e,"mc.shipping.rates",t.shippingRates);case E.UPSERT_SHIPPING_RATES:{const{shippingRates:n}=t,a=[...e.mc.shipping.rates];return n.forEach((e=>{const t=a.findIndex((t=>t.id===e.id));t>=0?a[t]=e:a.push(e)})),Bt(e,"mc.shipping.rates",a)}case E.DELETE_SHIPPING_RATES:{const{ids:n}=t,a=e.mc.shipping.rates.filter((e=>!n.includes(e.id)));return Bt(e,"mc.shipping.rates",a)}case E.RECEIVE_SHIPPING_TIMES:return Bt(e,"mc.shipping.times",t.shippingTimes);case E.UPSERT_SHIPPING_TIMES:{const{countries:n,time:a,maxTime:s}=t.shippingTime,i=[...e.mc.shipping.times];return n.forEach((e=>{const t={countryCode:e,time:a,maxTime:s},n=i.findIndex((t=>t.countryCode===e));n>=0?i[n]=t:i.push(t)})),Bt(e,"mc.shipping.times",i)}case E.DELETE_SHIPPING_TIMES:{const n=new Set(t.countryCodes),a=e.mc.shipping.times.filter((e=>!n.has(e.countryCode)));return Bt(e,"mc.shipping.times",a)}case E.RECEIVE_SETTINGS:return Bt(e,"mc.settings",t.settings);case E.SAVE_SETTINGS:return Bt(e,"mc.settings",{...e.mc.settings,...t.settings});case E.RECEIVE_ACCOUNTS_JETPACK:return Bt(e,"mc.accounts.jetpack",t.account);case E.RECEIVE_ACCOUNTS_GOOGLE:return Bt(e,"mc.accounts.google",t.account);case E.RECEIVE_ACCOUNTS_GOOGLE_ACCESS:return Bt(e,"mc.accounts.google_access",t.data);case E.RECEIVE_ACCOUNTS_GOOGLE_MC:return Bt(e,"mc.accounts.mc",t.account);case E.RECEIVE_ACCOUNTS_GOOGLE_MC_EXISTING:return Bt(e,"mc.accounts.existing_mc",t.accounts);case E.RECEIVE_ACCOUNTS_GOOGLE_ADS:return Bt(e,"mc.accounts.ads",t.account);case E.DISCONNECT_ACCOUNTS_GOOGLE_ADS:return Bt(e,"mc.accounts.ads",kt.mc.accounts.ads);case E.RECEIVE_ACCOUNTS_GOOGLE_ADS_BILLING_STATUS:return Bt(e,"mc.accounts.ads_billing_status",t.billingStatus);case E.RECEIVE_ACCOUNTS_GOOGLE_ADS_EXISTING:return Bt(e,"mc.accounts.existing_ads",t.accounts);case E.RECEIVE_MC_CONTACT_INFORMATION:return Bt(e,"mc.contact",t.data);case E.RECEIVE_MC_COUNTRIES_AND_CONTINENTS:{const{data:n}=t;return Ht(e,"mc").setIn("countries",n.countries).setIn("continents",n.continents).end()}case E.RECEIVE_TARGET_AUDIENCE:case E.SAVE_TARGET_AUDIENCE:return Bt(e,"mc.target_audience",t.target_audience);case E.RECEIVE_ADS_CAMPAIGNS:return!1===t.query?.exclude_removed?Bt(e,"all_ads_campaigns",t.adsCampaigns):Bt(e,"ads_campaigns",t.adsCampaigns);case E.CREATE_ADS_CAMPAIGN:return Bt(e,"ads_campaigns",[...e.ads_campaigns||[],t.createdCampaign]);case E.UPDATE_ADS_CAMPAIGN:{const{id:n,data:a}=t,s=e.ads_campaigns.findIndex((e=>e.id===n)),i={...e.ads_campaigns[s],...a},o=[...e.ads_campaigns];return o[s]=i,Bt(e,"ads_campaigns",o)}case E.DELETE_ADS_CAMPAIGN:{const{id:n}=t,a=e.ads_campaigns.filter((e=>e.id!==n));return Bt(e,"ads_campaigns",a)}case E.RECEIVE_CAMPAIGN_ASSET_GROUPS:return Bt(e,["campaign_asset_groups",t.campaignId],t.assetGroups);case E.CREATE_CAMPAIGN_ASSET_GROUP:{const{campaignId:n,assetGroup:a}=t;return Bt(e,["campaign_asset_groups",n],[...e.campaign_asset_groups[n]||[],a])}case E.RECEIVE_MC_SETUP:return Bt(e,"mc_setup",t.mcSetup);case E.RECEIVE_MC_PRODUCT_STATISTICS:return Bt(e,"mc_product_statistics",t.mcProductStatistics);case E.RECEIVE_MC_REVIEW_REQUEST:return Bt(e,"mc_review_request",t.mcReviewRequest);case E.RECEIVE_MC_ISSUES:{const{query:n,data:a}=t,s=e.mc_issues[n.issue_type]?.issues.slice()||[];return s.splice((n.page-1)*n.per_page,n.per_page,...a.issues),Ht(e,`mc_issues.${n.issue_type}`).setIn("issues",s).setIn("total",a.total).end()}case E.RECEIVE_MC_PRODUCT_FEED:{const{query:n,data:a}=t,s=e.mc_product_feed||{},i=Ht(e,"mc_product_feed");return s.per_page===n.per_page&&s.order===n.order&&s.orderby===n.orderby||i.setIn("pages",{}),i.setIn(["pages",n.page],a.products).setIn("per_page",n.per_page).setIn("order",n.order).setIn("orderby",n.orderby).setIn("total",a.total).end()}case E.RECEIVE_REPORT:{const{reportKey:n,data:a}=t;return Bt(e,["report",n],a)}case E.RECEIVE_MAPPING_ATTRIBUTES:return Bt(e,"mc.mapping.attributes",t.attributes);case E.RECEIVE_MAPPING_SOURCES:{const{attributeKey:n,sources:a}=t;return Bt(e,["mc","mapping","sources",n],a)}case E.RECEIVE_MAPPING_RULES:{const{rules:n,pagination:a}=t,s=[...e.mc.mapping.rules.items],i=(a.page-1)*a.perPage,o=a.perPage;return s.splice(i,o,...n),Ht(e,"mc.mapping.rules").setIn("items",s).setIn("total",a.total).setIn("pages",a.pages).end()}case E.UPSERT_MAPPING_RULE:{const{rule:n}=t,a=[...e.mc.mapping.rules.items],s=a.findIndex((e=>e.id===n.id));return s>=0?a[s]=n:a.push(n),Bt(e,"mc.mapping.rules.items",a)}case E.DELETE_MAPPING_RULE:{const n=e.mc.mapping.rules.items.filter((e=>e.id!==t.rule.id));return Bt(e,"mc.mapping.rules.items",n)}case E.RECEIVE_STORE_CATEGORIES:{const{storeCategories:n}=t;return Bt(e,"store_categories",n)}case E.RECEIVE_TOUR:case E.UPSERT_TOUR:{const{tour:n}=t;return Bt(e,["tours",n.id],n)}case E.HYDRATE_PREFETCHED_DATA:{const n=Ht(e,"general");return["version","mcId","adsId"].forEach((e=>{t.data.hasOwnProperty(e)&&n.setIn(e,t.data[e])})),n.end()}case E.RECEIVE_GOOGLE_ADS_ACCOUNT_STATUS:{const{data:{has_access:n,invite_link:a,step:s}}=t;return Ht(e,"ads.accountStatus").setIn("hasAccess",n).setIn("inviteLink",a).setIn("step",s).end()}case E.RECEIVE_ADS_BUDGET_RECOMMENDATIONS:{const{countryCodesKey:n,currency:a,recommendations:s}=t;return Bt(e,["ads","budgetRecommendations",n],{currency:a,recommendations:s})}case E.RECEIVE_GTIN_MIGRATION_STATUS:{const{data:n}=t;return Bt(e,"gtinMigrationStatus",n?.status)}case E.DISCONNECT_ACCOUNTS_ALL:default:return e}}}),(0,o.dispatch)(g.Ui).hydratePrefetchedData(l.Th.initialWpData),c().use(function(e){const t=new RegExp(`^${g.RV}/`);return function(n,a){if(!t.test(n.path))return a(n);const{parse:s=!0}=n;return a({...n,parse:!1}).catch(e).catch((async e=>Promise.reject(await jt(e,s)))).then((e=>s&&204===e.status?null:jt(e,s)))}}((e=>{if(l.Th.mcSetupComplete&&401===e.status)return(e.json||e.text).call(e).then((e=>"string"==typeof e?{message:e}:e)).then((e=>{const t=(0,qt.Ke)(e.code);return t&&(0,d.getHistory)().replace(t),e})).then((t=>Promise.reject({...t,statusCode:e.status})));throw e})));const Wt=()=>(0,o.useDispatch)(g.Ui)},7615:(e,t,n)=>{n.d(t,{XQ:()=>r,jr:()=>p,eT:()=>_,N2:()=>c,kj:()=>g,dh:()=>d,bM:()=>E,YK:()=>o});var a=n(8443),s=n(7374);const i=["clicks","impressions"],o=["sales","conversions","spend",...i],r=Object.freeze({NONE:0,FOR_METRIC:1,FOR_REQUEST:2});function c(e,t,n){const r=(0,s.getCurrentDates)(t);return{after:(0,a.format)("Y-m-d",r[n].after),before:(0,a.format)("Y-m-d",r[n].before),fields:"free"===e?i:o}}function d(e,t,n,a){const s=c(t,n,a),{order:i="desc"}=n;let{orderby:o}=n;o&&s.fields.includes(o)||(o=s.fields[0]);const r={...s,interval:"day",orderby:o,order:i};return"programs"===e&&n.programs?r.ids=n.programs:"products"===e&&n.products&&(r.ids=n.products.replace(/\d+/g,"gla_$&")),r}function l(e,t){if(t){if(Array.isArray(t))return[...t].sort();if("object"==typeof t)return Object.fromEntries(Object.entries(t).sort())}return t}function g(e,t,n){return`${e}:${t}:${JSON.stringify(n,l)}`}function u(e,t){let n=null;if("number"==typeof e&&"number"==typeof t&&(n=0,e!==t)){const a=(e-t)/t*100;n=Number.isFinite(a)?function(e,t=2){const n=Math.pow(10,t);return Math.round(e*n)/n}(a):null}return n}const p=(e,t,n)=>({value:e,delta:u(e,t),prevValue:t,missingFreeListingsData:n});function E(e={},t={},n){return(n||Object.keys(e)).reduce(((n,a)=>({...n,[a]:p(e[a],t[a],e[a]&&t[a]?r.NONE:r.FOR_REQUEST)})),{})}function _(e=[]){return[...e].sort().join("_").toLowerCase()}},399:(e,t,n)=>{n.d(t,{A:()=>s});const a=new Map;function s(e){if(a.has(e))return a.get(e);throw new Error(`The given \`kind\` of character counter is an unknown kind: ${e}`)}a.set("google-ads",function(){const e=[/[\u0000-\u04F9]/,/[\u1E00-\u20BF]/,/[\uFF61-\uFFDC]/,/[\u0E00-\u0E7F]/,/[\u2100-\u213A]/,/[\u0600-\u06FF]/,/[\u0750-\u077F]/,/[\uFB50-\uFDFF]/,/[\uFE70-\uFEFF]/,/[\u05D0-\u05EA]/,/\u05BE|\u05F3|\u05F4/],t=new Set(["ऀ","ँ","ं","ऺ","़","ु","ू","ृ","ॄ","ॅ","ॆ","े","ै","्","॑","॒","॓","॔","ॕ","ॖ","ॗ","ॢ","ॣ"]);return function(n){return n.split("").reduce(((n,a)=>n+function(n){return e.some((e=>e.test(n)))?1:/[\u0900-\u0D7F]/.test(n)?t.has(n)?0:1:2}(a)),0)}}())},8998:(e,t,n)=>{n.d(t,{h:()=>o});var a=n(7143),s=n(7723),i=n(6520);function o(e,t,n){if(401!==e?.statusCode){const o=function(e,t,n){const a=[],i=e?.message;return t&&a.push(t),i&&"string"==typeof i?a.push(i):n&&a.push(n),0===a.length&&a.push((0,s.__)("Unknown error occurred.","google-listings-and-ads")),a.join((0,s._x)(" ","The spacing between sentences. It's a space in English. Please use an empty string if no spacing is needed in that language.","google-listings-and-ads"))}(e,t,n);(0,a.dispatch)(i.mY).createNotice("error",o)}console.error(e)}},6473:(e,t,n)=>{n.d(t,{lr:()=>p,GH:()=>u,T1:()=>l,CU:()=>d,qX:()=>E,E$:()=>g,JL:()=>c,Ff:()=>h,ce:()=>_,dQ:()=>T,T:()=>C,Xh:()=>m});const a=window.wc.tracks;var s=n(7143),i=n(2619),o=n(3905),r=n(3658);const c=(0,i.createHooks)(),d="tracking",l="FILTER_ONBOARDING",g=new Map;g.set(l,["context","step"]);const u="setup-mc",p="setup-ads";function E(e){const{slug:t}=o.Th,{version:n,adsId:a,mcId:i}=(0,s.select)(r.U).getGeneral(),c={...e,[`${t}_version`]:n};return i&&(c[`${t}_mc_id`]=i),a&&(c[`${t}_ads_id`]=a),c}function _(e,t){(0,a.recordEvent)(e,E(t))}function h(e,t){(0,a.queueRecordEvent)(e,E(t))}const m=(e,t,n)=>{const a={context:e};let s;"goto"===n?(s="gla_table_go_to_page",a.page=t):(s="gla_table_page_click",a.direction=n),_(s,a)};function C(e,t,n){_(e,{triggered_by:`stepper-step${t}-button`,action:`go-to-step${t}`,context:n})}function T(e,t,n,a){_(e,{triggered_by:`step${t}-continue-button`,action:`go-to-step${n}`,context:a})}},3666:(e,t,n)=>{n.d(t,{$K:()=>i,FN:()=>E,Ke:()=>m,Q4:()=>c,Qk:()=>_,XG:()=>l,Xb:()=>h,hP:()=>p,uB:()=>d,uZ:()=>u,xP:()=>g});var a=n(6476),s=n(3905);const i={editCampaign:"/campaigns/edit",createCampaign:"/campaigns/create",editStoreAddress:"/edit-store-address",reconnectWPComAccount:"/reconnect-wpcom-account",reconnectGoogleAccount:"/reconnect-google-account"},o="/google/dashboard",r="/google/settings",c=(e,t)=>(0,a.getNewPath)({subpath:i.editCampaign,programId:e,initialStep:t},o),d=()=>(0,a.getNewPath)({subpath:i.createCampaign},o),l=()=>(0,a.getNewPath)(null,"/google/start",null),g=()=>(0,a.getNewPath)(null,"/google/setup-mc",null),u=(e=null)=>(0,a.getNewPath)(e,o,null),p=(e=null)=>(0,a.getNewPath)(e,"/google/product-feed",null),E=()=>(0,a.getNewPath)(null,r,null),_=()=>(0,a.getNewPath)(null,"/google/shipping",null),h=()=>(0,a.getNewPath)({subpath:i.editStoreAddress},r,null),m=e=>{let t;switch(e){case s.iH.WPCOM_DISCONNECTED:t=i.reconnectWPComAccount;break;case s.iH.GOOGLE_DISCONNECTED:t=i.reconnectGoogleAccount;break;default:return}return(0,a.getNewPath)({subpath:t},r,null)}},1609:e=>{e.exports=window.React},5795:e=>{e.exports=window.ReactDOM},8468:e=>{e.exports=window.lodash},8846:e=>{e.exports=window.wc.components},4111:e=>{e.exports=window.wc.currency},7752:e=>{e.exports=window.wc.customerEffortScore},314:e=>{e.exports=window.wc.data},7374:e=>{e.exports=window.wc.date},6476:e=>{e.exports=window.wc.navigation},3577:e=>{e.exports=window.wc.number},5703:e=>{e.exports=window.wc.wcSettings},1455:e=>{e.exports=window.wp.apiFetch},6427:e=>{e.exports=window.wp.components},9491:e=>{e.exports=window.wp.compose},7143:e=>{e.exports=window.wp.data},8443:e=>{e.exports=window.wp.date},8107:e=>{e.exports=window.wp.dom},6087:e=>{e.exports=window.wp.element},2619:e=>{e.exports=window.wp.hooks},8537:e=>{e.exports=window.wp.htmlEntities},7723:e=>{e.exports=window.wp.i18n},5573:e=>{e.exports=window.wp.primitives},3832:e=>{e.exports=window.wp.url}},a={};function s(e){var t=a[e];if(void 0!==t)return t.exports;var i=a[e]={exports:{}};return n[e](i,i.exports,s),i.exports}s.m=n,s.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return s.d(t,{a:t}),t},s.d=(e,t)=>{for(var n in t)s.o(t,n)&&!s.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},s.f={},s.e=e=>Promise.all(Object.keys(s.f).reduce(((t,n)=>(s.f[n](e,t),t)),[])),s.u=e=>({61:"product-feed",96:"vendors",207:"get-started-page",223:"commons",352:"onboarding",456:"attribute-mapping",472:"settings",528:"reports",553:"shipping",663:"ads-onboarding",945:"dashboard"}[e]+".js?ver="+{61:"9dce87931e14ceb96bd0",96:"e76b7f5484d80ac7acad",207:"6b3a36f011299ff5f91d",223:"db137169492470aa2cd3",352:"5298fbd73de5a4b38c02",456:"fdd5f7cb86ca0f23675e",472:"8b9caa7f115a63904e1e",528:"b70e39ef5e61ad918cee",553:"f226be63dbcd2804ae7e",663:"4068323a565e6ad186d5",945:"7315dfa0599400a17466"}[e]),s.miniCssF=e=>({61:"product-feed",207:"get-started-page",352:"onboarding",456:"attribute-mapping",472:"settings",528:"reports",553:"shipping",663:"ads-onboarding",945:"dashboard"}[e]+".css?ver="+{61:"9dce87931e14ceb96bd0",207:"6b3a36f011299ff5f91d",352:"5298fbd73de5a4b38c02",456:"fdd5f7cb86ca0f23675e",472:"8b9caa7f115a63904e1e",528:"b70e39ef5e61ad918cee",553:"f226be63dbcd2804ae7e",663:"4068323a565e6ad186d5",945:"7315dfa0599400a17466"}[e]),s.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="google-listings-and-ads:",s.l=(n,a,i,o)=>{if(e[n])e[n].push(a);else{var r,c;if(void 0!==i)for(var d=document.getElementsByTagName("script"),l=0;l{r.onerror=r.onload=null,clearTimeout(p);var s=e[n];if(delete e[n],r.parentNode&&r.parentNode.removeChild(r),s&&s.forEach((e=>e(a))),t)return t(a)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=u.bind(null,r.onerror),r.onload=u.bind(null,r.onload),c&&document.head.appendChild(r)}},s.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;s.g.importScripts&&(e=s.g.location+"");var t=s.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var n=t.getElementsByTagName("script");if(n.length)for(var a=n.length-1;a>-1&&(!e||!/^http(s?):/.test(e));)e=n[a--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),s.p=e})(),(()=>{if("undefined"!=typeof document){var e={57:0};s.f.miniCss=(t,n)=>{e[t]?n.push(e[t]):0!==e[t]&&{61:1,207:1,352:1,456:1,472:1,528:1,553:1,663:1,945:1}[t]&&n.push(e[t]=(e=>new Promise(((t,n)=>{var a=s.miniCssF(e),i=s.p+a;if(((e,t)=>{for(var n=document.getElementsByTagName("link"),a=0;a{var o=document.createElement("link");o.rel="stylesheet",o.type="text/css",s.nc&&(o.nonce=s.nc),o.onerror=o.onload=n=>{if(o.onerror=o.onload=null,"load"===n.type)a();else{var s=n&&n.type,r=n&&n.target&&n.target.href||t,c=new Error("Loading CSS chunk "+e+" failed.\n("+s+": "+r+")");c.name="ChunkLoadError",c.code="CSS_CHUNK_LOAD_FAILED",c.type=s,c.request=r,o.parentNode&&o.parentNode.removeChild(o),i(c)}},o.href=t,document.head.appendChild(o)})(e,i,0,t,n)})))(t).then((()=>{e[t]=0}),(n=>{throw delete e[t],n})))}}})(),(()=>{var e={57:0};s.f.j=(t,n)=>{var a=s.o(e,t)?e[t]:void 0;if(0!==a)if(a)n.push(a[2]);else{var i=new Promise(((n,s)=>a=e[t]=[n,s]));n.push(a[2]=i);var o=s.p+s.u(t),r=new Error;s.l(o,(n=>{if(s.o(e,t)&&(0!==(a=e[t])&&(e[t]=void 0),a)){var i=n&&("load"===n.type?"missing":n.type),o=n&&n.target&&n.target.src;r.message="Loading chunk "+t+" failed.\n("+i+": "+o+")",r.name="ChunkLoadError",r.type=i,r.request=o,a[1](r)}}),"chunk-"+t,t)}};var t=(t,n)=>{var a,i,[o,r,c]=n,d=0;if(o.some((t=>0!==e[t]))){for(a in r)s.o(r,a)&&(s.m[a]=r[a]);c&&c(s)}for(t&&t(n);dt=>(0,d.createElement)("div",{className:"gla-admin-page"},(0,d.createElement)(e,{...t}))),"withAdminPageShell");s(3658);var g=s(6473);const u=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(945)]).then(s.bind(s,6905)))),p=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(207)]).then(s.bind(s,494)))),E=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(352)]).then(s.bind(s,550)))),_=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(663)]).then(s.bind(s,923)))),h=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(528)]).then(s.bind(s,9531)))),m=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(61)]).then(s.bind(s,9061)))),C=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(456)]).then(s.bind(s,8885)))),T=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(472)]).then(s.bind(s,2625)))),A=(0,o.lazy)((()=>Promise.all([s.e(223),s.e(96),s.e(553)]).then(s.bind(s,6343)))),S=new Set,I=(0,c.getSetting)("admin")?.woocommerceTranslation||(0,i.__)("WooCommerce","google-listings-and-ads");(0,r.addFilter)("woocommerce_admin_pages_list","woocommerce/google-listings-and-ads/add-page-routes",(e=>{const t=[["",I],["/marketing",(0,i.__)("Marketing","google-listings-and-ads")],(0,i.__)("Google for WooCommerce","google-listings-and-ads")],n=[{breadcrumbs:[...t],container:p,path:"/google/start",wpOpenMenu:"toplevel_page_woocommerce-marketing"},{breadcrumbs:[...t,(0,i.__)("Setup Merchant Center","google-listings-and-ads")],container:E,path:"/google/setup-mc"},{breadcrumbs:[...t,(0,i.__)("Setup Google Ads","google-listings-and-ads")],container:_,path:"/google/setup-ads"},{breadcrumbs:[...t,(0,i.__)("Dashboard","google-listings-and-ads")],container:u,path:"/google/dashboard",wpOpenMenu:"toplevel_page_woocommerce-marketing"},{breadcrumbs:[...t,(0,i.__)("Reports","google-listings-and-ads")],container:h,path:"/google/reports",wpOpenMenu:"toplevel_page_woocommerce-marketing"},{breadcrumbs:[...t,(0,i.__)("Product Feed","google-listings-and-ads")],container:m,path:"/google/product-feed",wpOpenMenu:"toplevel_page_woocommerce-marketing"},{breadcrumbs:[...t,(0,i.__)("Attribute Mapping","google-listings-and-ads")],container:C,path:"/google/attribute-mapping",wpOpenMenu:"toplevel_page_woocommerce-marketing"},{breadcrumbs:[...t,(0,i.__)("Settings","google-listings-and-ads")],container:T,path:"/google/settings",wpOpenMenu:"toplevel_page_woocommerce-marketing"},{breadcrumbs:[...t,(0,i.__)("Shipping","google-listings-and-ads")],container:A,path:"/google/shipping",wpOpenMenu:"toplevel_page_woocommerce-marketing"}];return n.forEach((e=>{e.container=l(e.container);const t=e.path.substring(1).replace(/\//g,"_");S.add(t)})),e.concat(n)})),(0,r.addFilter)("woocommerce_tracks_client_event_properties","woocommerce/google-listings-and-ads/add-base-event-properties-to-page-view",((e,t)=>"wcadmin_page_view"===t&&S.has(e.path)?(0,g.qX)(e):e))})();PK!೿js/build/index-rtl.cssnu[.gla-admin-page .components-button.is-primary.is-destructive:disabled{color:#fff6}.gla-admin-page .components-button.is-tertiary.is-destructive,.gla-admin-page .components-button.is-tertiary.is-destructive:hover:not(:disabled){box-shadow:none}.gla-admin-page .components-button.is-link{text-decoration:none}.gla-admin-page .components-button.is-link:disabled{color:initial}.gla-admin-page .components-button.is-link.is-destructive:focus{box-shadow:none;color:#cc1818}.gla-admin-page .components-button.is-link.is-destructive:focus:not(:disabled){color:#cc1818}.gla-admin-page .components-card{margin-bottom:0}.gla-admin-page .components-card__header{font-size:inherit}.gla-admin-page .components-input-control__suffix{margin-left:8px}.gla-full-content #wpbody{margin-top:0!important}.gla-full-content .woocommerce-layout{padding-top:0}.gla-full-content .woocommerce-layout .woocommerce-layout__header,.gla-full-content .woocommerce-layout .woocommerce-layout__notice-list,.gla-full-content .woocommerce-layout .woocommerce-store-alerts{display:none}.gla-full-content .woocommerce-layout .woocommerce-layout__primary{margin:0}.gla-full-content .woocommerce-layout .woocommerce-layout__primary .woocommerce-layout__main{padding:0}@media(min-width:600px)and (max-width:782px){.gla-full-page.is-wp-toolbar-disabled{margin-top:-46px}}.gla-full-page .woocommerce-layout{padding-top:0}.gla-full-page .woocommerce-layout .woocommerce-layout__primary{margin:0}.gla-full-page .woocommerce-layout .woocommerce-layout__primary .woocommerce-layout__main{padding:0}.gla-admin-page .woocommerce-stepper__steps{align-items:center;background-color:#fff;box-shadow:inset 0 -1px 0 #ccc;height:64px;justify-content:center;margin-bottom:0}.gla-admin-page .woocommerce-stepper__steps .woocommerce-stepper__step-divider{align-self:auto;margin-top:0;max-width:48px}.gla-admin-page .components-notice:not(.app-notice){margin-bottom:12px}.wp-admin .woocommerce-customer-effort-score__intro{margin-bottom:1em} PK!48nnjs/build/onboarding.cssnu[.gla-stepper-top-bar{align-items:center;background-color:#fff;box-shadow:inset 0 -1px 0 0 #ccc;display:flex;min-height:64px}.gla-stepper-top-bar .components-button{align-self:stretch;height:auto}.gla-stepper-top-bar__back-button{padding:0 calc(var(--main-gap)/2)}.gla-stepper-top-bar__title{flex:1;font-size:16px;letter-spacing:0} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .AnC9WXFuKgCBURYIRcRY{display:flex;flex-direction:column;gap:4px;justify-content:center} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .gla-step-content{display:flex;justify-content:center}.gla-step-content .gla-step-content__container{flex:1;margin:var(--large-gap);max-width:1032px} .gla-step-content-header{align-items:center;display:flex;flex-direction:column;gap:12px;margin:auto;margin-bottom:var(--large-gap);max-width:600px;text-align:center}.gla-step-content-header h1{font-size:32px;padding:0}.gla-step-content-header__description{line-height:16px} .gla-section-card-body{padding:var(--large-gap)} .gla-section-card-footer{padding:calc(var(--large-gap)/2) var(--large-gap)}.gla-section-card-footer[hidden]{display:none} .gla-subsection-title{font-size:14px;font-style:normal;font-weight:600;letter-spacing:0;line-height:20px;margin-bottom:8px;position:relative;text-align:left} .gla-subsection-body{font-size:13px;font-style:normal;font-weight:400;line-height:16px} .gla-subsection-helper-text{font-size:12px;font-style:italic;font-weight:400;letter-spacing:0;line-height:16px} .gla-subsection-subtitle{color:#757575;font-size:12px;line-height:16px;margin-bottom:4px} .gla-subsection:not(:first-child){margin-top:var(--main-gap)} .gla-section-card-title{margin-bottom:calc(var(--main-gap)/3*2)} .gla-section{display:flex;flex-direction:column;margin-bottom:var(--large-gap)}.gla-section--is-disabled,.gla-section--is-disabled-left .gla-section__header{opacity:.5}@media(min-width:600px){.gla-section{flex-direction:row;gap:var(--main-gap)}.gla-section__header{padding-top:var(--main-gap);width:33%}}.gla-section__header h1{font-size:16px;font-style:normal;font-weight:600;margin-bottom:8px;padding:0}.gla-section__header p{line-height:16px;margin:0 0 8px}.gla-section .gla-section__body{flex:1} .gla-step-content-actions{display:flex;gap:20px;justify-content:flex-end}.gla-step-content-actions[hidden]{display:none} .gla-account-card{line-height:16px}.gla-account-card--is-disabled{opacity:.5}.gla-account-card--is-expanded-detail .gla-account-card__indicator{grid-area:1/3}.gla-account-card--is-expanded-detail .gla-account-card__detail{grid-area:2/2/auto/span 2}.gla-account-card__styled--align-top{align-self:flex-start}.gla-account-card__body-layout{align-items:center;display:grid;grid-template-columns:auto 1fr auto}.gla-account-card__icon{grid-area:1/1/span 2;line-height:0;margin-right:16px}.gla-account-card__subject{grid-area:1/2}.gla-account-card__indicator{grid-area:1/3/span 2;margin-left:16px}.gla-account-card__indicator--align-to-detail{grid-area:2/3;margin-top:12px}.gla-account-card__detail{grid-area:2/2;margin-top:12px}.gla-account-card__actions{grid-area:3/2/auto/span 2;margin-top:12px}.gla-account-card__title{color:#000;margin:0}.gla-account-card__description{align-items:flex-start;color:#1e1e1e;display:flex;flex-direction:column;gap:1em;margin-top:4px}.gla-account-card__description>p{margin:0}.gla-account-card__helper{color:#757575;font-size:12px;font-style:italic;margin-top:4px}.gla-account-card .components-card__footer{padding:calc(var(--main-gap)/3*2) var(--main-gap)}.gla-account-card .components-card__footer>.components-button.is-link{min-height:36px;padding:6px 12px}.gla-account-card .components-notice.is-error{background-color:#f8ebea;margin:0}.gla-account-card .components-notice.is-success{background-color:#edfaef;border:0;font-size:12px;margin:0 var(--large-gap) var(--main-gap);padding:16px}@media(max-width:600px){.gla-account-card__body-layout{align-items:flex-start;display:flex;flex-direction:column}.gla-account-card__body-layout>div{margin:8px}} .gla-connected-icon-label{fill:currentcolor;color:#23a713}.gla-connected-icon-label svg{display:block} .gla-authorize-google-account-card__error-text{color:#cc1818;font-weight:500} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-warning-icon.gridicon.gridicons-notice-outline{fill:#f0b849} .gla-ads-warning-modal .gla-ads-warning-modal__warning-text{align-items:center;display:flex;gap:calc(var(--main-gap)/3)} .gla-ads-claim-account-notice{background-color:#ffeec1;font-size:12px;margin:0 var(--large-gap) var(--large-gap);padding:16px} .gla-ads-terms-modal{max-width:600px}.gla-ads-terms-modal .main{font-weight:700} .gla-content-button-layout{align-items:center;display:flex;gap:calc(var(--main-gap)/2);justify-content:space-between} .gla-loading-label{align-items:center;color:var(--wp-admin-theme-color);display:inline-flex;gap:8px;height:36px}.gla-loading-label .woocommerce-spinner{height:24px;min-width:24px;width:24px}.gla-loading-label .woocommerce-spinner__circle{stroke:currentcolor;stroke-width:8px} .app-select-control .components-base-control__field{margin-bottom:0}.app-select-control.app-select-control--is-non-interactive{pointer-events:none} .gla-connect-ads .app-select-control{flex-grow:1}.gla-connect-ads .gla-subsection-body{margin-bottom:8px} .gla-claim-ads-account-box{border:1px solid #ddd;display:flex;flex-direction:column;gap:8px;padding:24px}.gla-claim-ads-account-box h4{font-size:14px;margin:0}.gla-claim-ads-account-box p{margin:0}.gla-claim-ads-account-box .gla-ads-post-claim-instructions{color:#757575;font-style:italic}.gla-claim-ads-account-box .app-button{align-self:flex-end} .gla-connected-ads-account-detail .components-notice.is-success{margin:0} .gla-validation-errors{color:#cc1818;font-size:12px;line-height:16px;margin:0;width:100%}.gla-validation-errors:not(:first-child){margin-top:16px}.gla-validation-errors:not(:last-child){margin-bottom:16px}.gla-validation-errors>li{list-style:disc inside;margin:0;padding-left:.5em}.gla-validation-errors>li:only-child{list-style:none;padding-left:0} .gla-contact-info-preview-card .gla-subsection-title{align-items:center;display:flex}.gla-contact-info-preview-card__notice-icon{fill:#cc1818;margin:calc(var(--main-gap)/-8) 0}.gla-contact-info-preview-card__notice-details{color:#757575}.gla-contact-info-preview-card__placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:#0000;display:inline-block;width:18em}.gla-contact-info-preview-card__placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.gla-contact-info-preview-card__placeholder{animation:none}} .gla-store-address-card .gla-account-card__indicator .has-icon svg{margin-left:4px}.gla-store-address-card .gla-account-card__description{color:#757575} .gla-reclaim-url-card .gla-content-button-layout{margin:16px 0}.gla-reclaim-url-card .components-notice.is-error{margin-top:16px} .app-input-control .components-flex-item{margin-right:0;max-width:100%;width:100%}.app-input-control .components-flex-item label.components-input-control__label{color:#757575!important;white-space:normal!important}.app-input-control--no-pointer-events{pointer-events:none}.app-input-control__character-count{color:#757575;font-size:12px;line-height:16px;margin-top:2px;text-align:right}.app-input-control--error-character-count .components-input-control .components-input-control__container .components-input-control__backdrop,.app-input-control.has-error .components-input-control__backdrop{border-color:#cc1818;box-shadow:none}.app-input-control--error-character-count .app-input-control__character-count,.app-input-control.has-error .components-base-control__help{color:#cc1818} .app-input-link-control{width:100%}.app-input-link-control svg{display:block;margin:6px 0 6px 6px;fill:currentcolor} .gla-switch-url-card .gla-content-button-layout{margin:16px 0} .gla-mc-warning-modal{max-width:600px}.gla-mc-warning-modal__warning-text{display:flex;font-weight:700;gap:8px}.gla-mc-warning-modal__warning-text svg{flex-shrink:0} .gla-mc-terms-modal{max-width:600px}.gla-mc-terms-modal .main{font-weight:700} .app-notice{border:0;font-size:12px;margin:0 var(--large-gap) var(--main-gap);padding:16px} @media(min-width:600px){.gla-disconnect-accounts-modal{max-width:600px}}.gla-disconnect-accounts-modal .components-modal__header-heading{align-items:center;display:flex}.gla-disconnect-accounts-modal .gridicon.gridicons-notice-outline{margin-right:8px;fill:#cc1818}.gla-disconnect-accounts-modal .components-base-control{margin:var(--main-gap) 0 var(--large-gap)} .gla-google-combo-account-card-wrapper .components-card:not(:last-child){border-bottom-left-radius:0;border-bottom-right-radius:0}.gla-google-combo-account-card-wrapper .components-card:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.gla-google-combo-account-card--connected .gla-account-card__description{gap:0}.gla-google-combo-account-card--connected .gla-google-combo-account-card__description-actions{display:flex;gap:var(--large-gap)} .gla-google-combo-account-card .gla-account-card__actions .app-button{margin-left:calc(var(--main-gap)/-2)} .gla-faqs-panel .components-panel__row{align-items:flex-start;flex-direction:column;gap:1.5em}.gla-faqs-panel .components-panel__row p{margin:0}.gla-faqs-panel .components-panel__body-title .components-panel__body-toggle.components-button{padding-bottom:20px;padding-top:20px} .gla-wp-google-accounts-section{margin-bottom:var(--main-gap)}.gla-google-mc-disclaimer-section header{border-top:1px solid #ddd}.gla-google-mc-disclaimer-section header p{color:#757575;font-size:12px;font-style:italic;margin-bottom:1.5em} .app-radio-content-control{display:grid;gap:8px;column-gap:10px;grid-template-columns:[input-start] auto [text-start] 1fr}.app-radio-content-control .components-base-control__field,.app-radio-content-control .components-base-control__field .components-flex,.app-radio-content-control .components-base-control__label,.app-radio-content-control .components-radio-control,.app-radio-content-control .components-radio-control .components-flex,.app-radio-content-control .components-radio-control__option{display:contents}.app-radio-content-control .components-radio-control__input[type=radio]{margin:0}.app-radio-content-control .app-radio-content-control__content{grid-column:text-start}.app-radio-content-control .app-radio-content-control__content:empty{display:none} .gla-radio-helper-text{color:#757575;font-size:12px;font-style:italic;font-weight:400;line-height:16px} .woocommerce-tree-select-control{position:relative}.woocommerce-tree-select-control__label{color:#757575;display:block;font-size:16px;padding-bottom:8px}.woocommerce-tree-select-control__help{color:#757575;font-size:12px;line-height:16px;margin-top:4px}.woocommerce-tree-select-control .components-base-control{align-items:center;background:#fff;border:1px solid #949494;border-radius:3px;display:flex;flex-wrap:wrap;height:auto;padding:12px;position:relative}.woocommerce-tree-select-control .components-base-control .components-base-control__field{align-items:center;display:flex;flex:1;flex-basis:content;margin-bottom:0;max-width:100%}.woocommerce-tree-select-control .components-base-control .woocommerce-tree-select-control__control-input{background:#0000;border:0;box-shadow:none;color:#2f2f2f;font-size:16px;letter-spacing:inherit;line-height:1.5;margin:0;padding-left:0;padding-right:0;text-align:left;width:100%}.woocommerce-tree-select-control .components-base-control .woocommerce-tree-select-control__control-input::-webkit-search-cancel-button{display:none}.woocommerce-tree-select-control .components-base-control .woocommerce-tree-select-control__control-input:focus{outline:none}.woocommerce-tree-select-control .components-base-control i{color:#636d75;margin-right:12px;width:24px}.woocommerce-tree-select-control .components-base-control.is-active{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color)}.woocommerce-tree-select-control .components-base-control.has-tags .components-base-control__label,.woocommerce-tree-select-control .components-base-control.with-value .components-base-control__label{font-size:12px;margin-top:-12px}.woocommerce-tree-select-control .components-base-control.is-disabled{background:#ffffff80;border:1px solid #a7aaad80}.woocommerce-tree-select-control .components-base-control.is-disabled .components-base-control__field{visibility:hidden}.woocommerce-tree-select-control .components-base-control.is-disabled .components-base-control__label{cursor:default}.woocommerce-tree-select-control .components-base-control.is-disabled .woocommerce-tag__remove{cursor:default;pointer-events:none}.woocommerce-tree-select-control .woocommerce-tree-select-control__autofill-input{position:absolute;z-index:-1}.woocommerce-tree-select-control .woocommerce-tree-select-control__tags{margin:0;position:relative}.woocommerce-tree-select-control .woocommerce-tree-select-control__tags.has-clear{padding-right:24px}.woocommerce-tree-select-control .woocommerce-tag,.woocommerce-tree-select-control .woocommerce-tree-select-control__show-more{max-height:24px}.woocommerce-tree-select-control .woocommerce-tree-select-control__clear{position:absolute;right:10px;top:calc(50% - 10px)}.woocommerce-tree-select-control .woocommerce-tree-select-control__clear>.clear-icon{color:#ccc}.woocommerce-tree-select-control .woocommerce-tree-select-control__tree{align-items:stretch;background:#fff;border-radius:3px;box-shadow:0 3px 5px #0003,0 1px 18px #0000001f,0 6px 10px #00000024;display:flex;flex-direction:column;left:0;max-height:350px;overflow-y:auto;padding:16px;position:absolute;right:0;z-index:10}.woocommerce-tree-select-control .woocommerce-tree-select-control__tree.is-static{position:static}.woocommerce-tree-select-control .woocommerce-tree-select-control__node.has-children{border-bottom:1px solid #eee}.woocommerce-tree-select-control .woocommerce-tree-select-control__node.has-children:last-child{border-bottom:0}.woocommerce-tree-select-control .woocommerce-tree-select-control__children{padding-left:32px}.woocommerce-tree-select-control .woocommerce-tree-select-control__main{border-top:1px solid #e0e0e0;padding-left:0}.woocommerce-tree-select-control .woocommerce-tree-select-control__option{border:none;display:flex;flex:1;font-size:16px;height:auto;min-height:0;padding:0 0 0 8px;text-align:left}.woocommerce-tree-select-control .woocommerce-tree-select-control__option.is-selected,.woocommerce-tree-select-control .woocommerce-tree-select-control__option:hover{color:var(--wp-admin-theme-color)}.woocommerce-tree-select-control .woocommerce-tree-select-control__option.is-partially-checked .components-checkbox-control__input{background:var(--wp-admin-theme-color);border:4px solid #fff;box-shadow:0 0 0 1px #1e1e1e}.woocommerce-tree-select-control .woocommerce-tree-select-control__option.is-partially-checked .components-checkbox-control__input:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px var(--wp-admin-theme-color)}.woocommerce-tree-select-control .woocommerce-tree-select-control__expander{background:#0000;border:none;cursor:pointer;margin-right:0;padding:4px}.woocommerce-tree-select-control .woocommerce-tree-select-control__expander.is-hidden{pointer-events:none;visibility:hidden}.woocommerce-tree-select-control .components-checkbox-control__label{align-items:center;display:flex;min-height:56px;width:100%}.woocommerce-tree-select-control.is-searchable .components-base-control__label{left:48px}.woocommerce-tree-select-control.is-searchable .components-base-control.is-active .components-base-control__label{font-size:12px;margin-top:-12px}.woocommerce-tree-select-control.is-searchable .woocommerce-tree-select-control__control-input{padding-left:12px} .gla-supported-country-select .woocommerce-tree-select-control__label{font-size:13px;padding-bottom:4px}.gla-supported-country-select .woocommerce-tree-select-control__help{font-style:italic} .gla-vertical-gap-layout{display:flex;flex-direction:column;gap:calc(var(--main-gap)/2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__medium{gap:calc(var(--main-gap)/3*2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__large{gap:var(--main-gap)}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap{gap:0}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap>:not(:first-child){margin-top:-1px} .gla-choose-audience-section .gla-radio-helper-text{font-style:normal}.gla-choose-audience-section .gla-subsection-helper-text{margin-bottom:calc(var(--main-gap)/3*2)}.gla-choose-audience-section .woocommerce-tree-select-control__help{margin-top:8px} .gla-shipping-rate-input-control{align-items:flex-end;display:flex;flex-wrap:wrap;gap:8px}.gla-shipping-rate-input-control .label{display:flex;gap:4px;justify-content:space-between}.gla-shipping-rate-input-control .label button{height:-moz-fit-content;height:fit-content;line-height:1.4em;padding:0}.gla-shipping-rate-input-control .app-input-control{width:300px}.gla-shipping-rate-input-control .gla-input-pill-div{padding:2px 0}.gla-shipping-rate-input-control .gla-input-pill-div .woocommerce-pill{border-color:#008a20;color:#008a20} .gla-countries-time-stepper{width:150px}.gla-countries-time-stepper .gla-countries-time-suffix{margin-right:4px} .gla-edit-time-button.components-button.is-tertiary{height:-moz-fit-content;height:fit-content;line-height:1.4em;padding:0} .gla-countries-time-input-container{max-width:360px}.gla-countries-time-input-container .label{display:flex;gap:4px;justify-content:space-between}.gla-countries-time-input-container .gla-validation-errors{min-height:2lh} .gla-minimum-order-input-control .gla-minimum-order-input-control__label{display:flex;flex-direction:column;gap:8px;justify-content:space-between}.gla-minimum-order-input-control .gla-minimum-order-input-control__label_country{display:flex;gap:4px;justify-content:space-between}.gla-minimum-order-input-control .gla-minimum-order-input-control__label_country button{height:-moz-fit-content;height:fit-content;line-height:1.4em;padding:0} .gla-minimum-order-card .app-input-control{width:300px} .gla-setup-free-listing-hero{align-items:center;background-color:#fff;display:flex;flex-direction:column;padding:var(--large-gap);padding-bottom:0}.gla-setup-free-listing-hero .hero-text{margin-bottom:0;max-width:unset}.gla-setup-free-listing-hero .hero-text .hero-text__subtitle{font-size:16px;font-weight:600;line-height:1.5;margin:4px 0 0;padding:0}.gla-setup-free-listing-hero .hero-text .hero-text__body{font-size:14px;margin-bottom:4px}.gla-setup-free-listing-hero__image{max-width:100%} .gla-google-ads-billing-setup-card .components-card__body{display:flex;gap:16px}.gla-google-ads-billing-setup-card__description{color:#000;display:flex;flex-direction:column;gap:16px}.gla-google-ads-billing-setup-card__description__helper{color:#757575;font-style:italic} .gla-google-ads-billing-card__success-status{background-color:#eff9f1;color:#1e1e1e;line-height:2em;padding:12px}.gla-google-ads-billing-card__success-status .gridicon{fill:#008a20;margin:0 12px 0 4px} .gla-budget-section__card-body{flex-direction:column}.gla-budget-section__card-body,.gla-budget-section__card-body__cost{display:flex;gap:var(--main-gap)}.gla-budget-section__card-body__cost>*{flex:1}.gla-budget-section .components-input-control__suffix{margin-right:16px} .gla-budget-recommendation__low-budget{align-items:center;display:flex;font-style:italic;gap:calc(var(--main-gap)/3);margin-bottom:calc(var(--main-gap)/2)}.gla-budget-recommendation__low-budget>svg{flex:0 0 auto}.gla-budget-recommendation .components-tip{background-color:#f0f6fc;padding:12px 16px}.gla-budget-recommendation .components-tip>p{font-size:inherit;line-height:20px}.gla-budget-recommendation .components-tip>svg{align-self:auto;margin:4px 10px 0 0}.gla-budget-recommendation .components-tip,.gla-budget-recommendation__low-budget{color:#000;font-size:12px}.gla-budget-recommendation .components-tip>svg,.gla-budget-recommendation__low-budget>svg{fill:#1e1e1e} .gla-campaign-preview{height:270px;position:relative;width:205px}.gla-campaign-preview .gla-ads-mockup{position:absolute}.gla-campaign-preview__transition-blur-enter{opacity:0}.gla-campaign-preview__transition-blur-enter-active{opacity:1;transition:opacity .5s ease-in-out}.gla-campaign-preview__transition-blur-exit{opacity:1}.gla-campaign-preview__transition-blur-exit-active{opacity:0;transition:opacity .5s ease-in-out}.gla-ads-mockup{background-color:#fff;border:1px solid #e0e0e0;border-radius:4px;height:270px;overflow:hidden;padding:10px;width:205px}.gla-ads-mockup .app-spinner{align-items:center;height:100%}.gla-ads-mockup__placeholder{border-radius:4px;height:3px}.gla-ads-mockup__placeholder--thinnest{height:1px}.gla-ads-mockup__placeholder--thinner{height:2px}.gla-ads-mockup__placeholder--thicker{height:4px}.gla-ads-mockup__placeholder--gray-100{background-color:#f0f0f0}.gla-ads-mockup__placeholder--gray-200{background-color:#e0e0e0}.gla-ads-mockup__placeholder--gray-300{background-color:#ddd}.gla-ads-mockup__placeholder--gray-400{background-color:#ccc}.gla-ads-mockup__placeholder--gray-500{background-color:#bbb}.gla-ads-mockup__placeholder--blue{background-color:#4285f4}.gla-ads-mockup__scaled-text{font-size:20px;height:1em;line-height:.9;margin-bottom:-.5em;margin-right:-100%;overflow:hidden;text-overflow:ellipsis;transform:scale(.5);transform-origin:top left;white-space:nowrap}.gla-ads-mockup__scaled-text--smaller{font-size:18px}.gla-ads-mockup__scaled-text--larger{font-size:22px}.gla-ads-mockup__scaled-text--gray-700{color:#757575}.gla-ads-mockup__scaled-text--gray-800{color:#2f2f2f}.gla-ads-mockup__scaled-text--blue{color:#4285f4}.gla-ads-mockup__scaled-text--ad-badge{height:auto;line-height:1;margin-bottom:-.6em}.gla-ads-mockup__scaled-text--ad-badge:before{background-color:#f0b849;border-radius:6px;color:#fff;content:"AD";display:inline-block;font-size:16px;margin-right:12px;padding:3px}.gla-ads-mockup__product-cover{aspect-ratio:186/143;background:50%/cover no-repeat;width:100%}.gla-ads-mockup-display .gla-ads-mockup__product-cover{height:126px}.gla-ads-mockup__shop-logo{background:50%/cover no-repeat;flex:0 0 auto;height:44px;width:44px}.gla-ads-mockup__search-bar{align-items:center;border:1px solid #e0e0e0;display:flex;flex-direction:row-reverse;height:23px;justify-content:space-between;padding:0 8px;fill:#ccc;background-color:#fff}.gla-ads-mockup-search .gla-ads-mockup__search-bar{border-radius:29px}.gla-ads-mockup-map .gla-ads-mockup__search-bar{border-radius:4.6px;height:27px;margin:10px}.gla-ads-mockup-gmail .gla-ads-mockup__search-bar{border-radius:3.7px;flex:1 1;height:19px}.gla-ads-mockup__search-bar-menu{display:flex;flex-direction:column;height:9px;justify-content:space-between;width:13px}.gla-ads-mockup__search-bar-menu[hidden]{display:none}.gla-ads-mockup__product-banner{align-items:flex-start;display:flex;gap:10px;padding:10px}.gla-ads-mockup-gmail .gla-ads-mockup__product-banner{border:1px solid #e0e0e0;border-radius:2px;flex-direction:row-reverse}.gla-ads-mockup__product-banner-info{display:flex;flex-direction:column;height:42px;justify-content:space-between;overflow:hidden}.gla-ads-mockup__tab-list{align-items:center;display:flex;justify-content:space-between}.gla-ads-mockup__tab-list>.gla-ads-mockup__placeholder{margin-top:16px;min-width:25px}.gla-ads-mockup__tab-item-with-logo{text-align:center;width:44px}.gla-ads-mockup__tab-item-with-logo>img{display:block;margin:0 auto 6px}.gla-ads-mockup__shopping-product{border:1px solid #e0e0e0;border-radius:4px;margin-top:6px;overflow:hidden}.gla-ads-mockup__shopping-product-info{display:flex;flex-direction:column;gap:8px;padding:8px 10px 10px}.gla-ads-mockup__youtube-header{padding:4px 0}.gla-ads-mockup__youtube-header>img{display:block}.gla-ads-mockup__youtube-product{margin-top:6px}.gla-ads-mockup__youtube-learn-more-row{align-items:center;background-color:#f0f6fc;display:flex;font-weight:600;padding:3px 6px;fill:#4285f4}.gla-ads-mockup__youtube-learn-more-row>div{flex:1 0;line-height:0}.gla-ads-mockup__youtube-product-info{display:flex;flex-direction:column;gap:8px;padding:8px 0}.gla-ads-mockup__search-header{display:flex;justify-content:center;margin:4px 0 8px}.gla-ads-mockup__search-keywords{display:flex;flex-wrap:wrap;gap:4px 11px;justify-content:space-between;margin:8px 0}.gla-ads-mockup__search-card{border:1px solid #ddd;border-radius:4px;margin-top:8px;padding:7.5px}.gla-ads-mockup__search-card+.gla-ads-mockup__search-card{border-color:#f0f0f0}.gla-ads-mockup__search-card:last-child{opacity:.5}.gla-ads-mockup__search-card-header{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}.gla-ads-mockup__search-card-placeholders{min-height:38px}.gla-ads-mockup-map,.gla-ads-mockup__search-card-placeholders{display:flex;flex-direction:column;justify-content:space-between}.gla-ads-mockup-map{background:top no-repeat border-box;padding:0;position:relative}.gla-ads-mockup-map .gridicons-location{left:99px;position:absolute;top:116px;fill:#f86368}.gla-ads-mockup__display-product{border:1px solid #e0e0e0;margin:12px 0}.gla-ads-mockup__display-product .gla-ads-mockup__placeholder{margin:18px 10px 12px}.gla-ads-mockup__display-product-locator{position:relative}.gla-ads-mockup__display-corner-buttons{position:absolute;right:0;top:0}.gla-ads-mockup__display-chevron-button{align-items:center;aspect-ratio:1/1;background-color:#4285f4;border-radius:50%;display:flex;height:22px;justify-content:center;left:50%;position:absolute;top:100%;transform:translate(-50%,-50%);fill:#fff}.gla-ads-mockup__display-placeholders{display:flex;flex-direction:column;gap:4.66px}.gla-ads-mockup__gmail-header{align-items:center;display:flex;gap:10px;margin:5px 0 12px;padding-left:1px}.gla-ads-mockup__mail-item{display:flex;flex-direction:column;height:19px;justify-content:space-around;margin-top:10px;padding-left:28px;position:relative}.gla-ads-mockup__mail-item:before{aspect-ratio:1/1;background-color:#f0f0f0;border-radius:50%;content:"";height:100%;left:0;position:absolute} .gla-campaign-preview-card .gla-section-card-title{margin-bottom:8px}.gla-campaign-preview-card__moving-button.components-button.has-icon{background-color:#f0f0f0;border-radius:50%;height:auto;min-width:0;padding:4px} .gla-free-ad-credit{align-items:center;background-color:#dcf7dd;color:#000;display:flex;gap:calc(var(--main-gap)/3*2);padding:calc(var(--main-gap)/3*2)}.gla-free-ad-credit svg{flex:0 0 auto;fill:#008a20}.gla-free-ad-credit__title{font-size:13px;font-weight:600;margin-bottom:8px}.gla-free-ad-credit__description{font-size:12px;font-style:italic} .gla-free-ad-credit-country-modal{max-width:600px;overflow:auto}.gla-free-ad-credit-country-modal table{border-spacing:calc(var(--main-gap)/3*2) 0;margin-left:calc(var(--large-gap)*2)}.gla-free-ad-credit-country-modal table tbody td:first-child{font-weight:600;text-align:right} .gla-paid-ads-features-section .woocommerce-pill{color:#757575}.gla-paid-ads-features-section .gla-section-card-title{color:#1e1e1e;font-size:20px;font-weight:400;line-height:28px;margin-bottom:12px}@media(max-width:600px){.gla-paid-ads-features-section__content{flex-direction:column;gap:16px}}.gla-paid-ads-features-section__subtitle{color:#2f2f2f;font-size:14px;line-height:20px}.gla-paid-ads-features-section__feature-list{color:#2f2f2f;display:flex;flex-direction:column;gap:16px;line-height:16px;margin:24px 0 16px}.gla-paid-ads-features-section__feature-list .gridicon{fill:#5ec862}.gla-paid-ads-features-section__cite{color:#949494;font-size:12px;font-style:normal} PK!B==js/build/onboarding.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[352],{550:(e,t,n)=>{n.r(t),n.d(t,{default:()=>Ce});var a=n(1609),o=n(6474),s=n(7723),i=n(6476),l=n(7539),c=n(2455),r=n(6473);const g=()=>(0,a.createElement)(l.A,{title:(0,s.__)("Get started with Google for WooCommerce","google-listings-and-ads"),helpButton:(0,a.createElement)(c.A,{eventContext:"setup-mc"}),backHref:(0,i.getNewPath)({},"/google/start"),onBackButtonClick:()=>{(0,r.ce)("gla_setup_mc",{triggered_by:"back-button",action:"leave"})}});var d=n(3741),u=n(8846),m=n(6087),p=n(3658),h=n(7541),_=n(6520),f=n(8859),A=n(7240),y=n(6453);var C=n(5847),E=n(873),b=n(6523),S=n(5622),k=n(5455),w=n(5807),v=n(5640),G=n(7401),I=n(8e3),x=n(7916),P=n(7892),T=n(9370),N=n(3164),R=n(9826),F=n(3704),M=n(8242),q=n(1177),B=n(4790),D=n(1274),W=n(9452);const Y=[{trackId:"why-do-i-need-a-wp-account",question:(0,s.__)("Why do I need a WordPress.com account?","google-listings-and-ads"),answer:(0,s.__)("We use a WordPress.com account to connect your site to the WooCommerce and Google servers. It ensures that requests (e.g. product feed, clicks, sales, etc) from your site are securely and correctly attributed to your store. It enables a connection to your self-hosted site, and provides a common authentication interface across disparate server configurations and architectures.","google-listings-and-ads")},{trackId:"why-do-i-need-a-google-mc-account",question:(0,s.__)("Why do I need a Google Merchant Center account?","google-listings-and-ads"),answer:(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,s.__)("Google Merchant Center helps you sync your store and product data with Google and makes the information available for both free listings on the Shopping tab and Google Shopping Ads. That means everything about your stores and products is available to shoppers when they search on a Google property.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,s.__)("If you create a new Merchant Center account through this application, it will be associated with Google’s Comparison Shopping Service (Google Shopping) by default. You can change the CSS associated with your account at any time. Please find more information here.","google-listings-and-ads"),{link:(0,a.createElement)(q.A,{context:"faqs",linkId:"find-a-partner",href:"https://comparisonshoppingpartners.withgoogle.com/find_a_partner/"})})))}],j=()=>(0,a.createElement)(W.A,{trackName:"gla_faq",context:"setup-mc-accounts",faqItems:Y});var O=n(1378),V=n(1351),H=n(1456);const L=()=>(0,a.createElement)(a.Fragment,null,(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,s.__)("If you are in the European Economic Area or Switzerland, your Merchant Center account must be associated with a Comparison Shopping Service (CSS). Please find more information at Google Merchant Center Help website.","google-listings-and-ads"),{link:(0,a.createElement)(q.A,{context:"setup-mc-accounts",linkId:"comparison-shopping-services",href:"https://support.google.com/merchants/topic/9080307"})})),(0,a.createElement)("p",null,(0,m.createInterpolateElement)((0,s.__)("If you create a new Merchant Center account through this application, it will be associated with Google Shopping, Google’s CSS, by default. You can change the CSS associated with your account at any time. Please find more information about our CSS Partners here.","google-listings-and-ads"),{link:(0,a.createElement)(q.A,{context:"setup-mc-accounts",linkId:"comparison-shopping-partners-find-a-partner",href:"https://comparisonshoppingpartners.withgoogle.com/find_a_partner/"})})),(0,a.createElement)("p",null,(0,s.__)("Once you have set up your Merchant Center account you can use our onboarding tool regardless of which CSS you use.","google-listings-and-ads"))),U=e=>{const{onContinue:t=()=>{}}=e,{jetpack:n}=(0,G.A)(),{google:o,scope:i}=(0,I.A)(),{googleMCAccount:l,isPreconditionReady:c,isReady:r}=(0,x.A)(),{hasFinishedResolution:g}=(0,O.A)(),u=(0,H.A)(),h=(0,V.A)(),{updateGoogleMCContactInformation:_}=(0,p.j)(),[f,A]=(0,m.useState)(!1),y=!n,C="yes"===n?.active,E=C&&!o,b="yes"===o?.active&&i.gmcRequired&&!l;if(y||E||b)return(0,a.createElement)(d.A,null);const S=!(g&&h&&r&&u);return(0,a.createElement)(T.A,null,(0,a.createElement)(N.A,{title:(0,s.__)("Set up your accounts","google-listings-and-ads"),description:(0,s.__)("Connect the accounts required to use Google for WooCommerce.","google-listings-and-ads")}),(0,a.createElement)(M.A,{className:"gla-wp-google-accounts-section",title:(0,s.__)("Connect accounts","google-listings-and-ads"),description:(0,s.__)("The following accounts are required to use the Google for WooCommerce plugin.","google-listings-and-ads")},!C&&(0,a.createElement)(B.Ay,{jetpack:n}),(0,a.createElement)(D.A,{disabled:!C})),(0,a.createElement)(R.A,null,(0,a.createElement)(F.A,null,(0,a.createElement)(P.A,{isPrimary:!0,disabled:S,loading:f,text:(0,s.__)("Continue","google-listings-and-ads"),onClick:()=>{A(!0),_().then((()=>t())).catch((()=>A(!1)))}}))),(0,a.createElement)(M.A,{className:"gla-google-mc-disclaimer-section",description:(0,a.createElement)(L,null),disabledLeft:!c},(0,a.createElement)(j,null)))};var z=n(7343),J=n(8806);const K=()=>(0,a.createElement)("div",{className:"gla-setup-free-listing-hero"},(0,a.createElement)(N.A,{className:"hero-text",title:(0,s.__)("Configure your product listings","google-listings-and-ads"),description:(0,a.createElement)("div",null,(0,a.createElement)("p",{className:"hero-text__subtitle"},(0,s.__)("Your product listings will look something like this.","google-listings-and-ads")),(0,a.createElement)("p",{className:"hero-text__body"},(0,s.__)("Your product details, estimated shipping info and tax details will be displayed across Google.","google-listings-and-ads")))}),(0,a.createElement)("img",{className:"gla-setup-free-listing-hero__image",src:J,alt:(0,s.__)("Google Shopping search results example","google-listings-and-ads")}));function Q(e){return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(K,null),(0,a.createElement)(T.A,null,(0,a.createElement)(z.A,{...e}),(0,a.createElement)(R.A,null,(0,a.createElement)(F.A,null,(0,a.createElement)(z.A.SubmitButton,null)))))}var X=n(8468),$=n(1968),Z=n(8519),ee=n(1203),te=n(8473),ne=n(6893),ae=n(3666),oe=n(8998),se=n(3905);const ie="complete-ads",le="skip-ads";var ce=n(9457);const re=({onRequestClose:e,onSkipCreatePaidAds:t})=>(0,a.createElement)(ce.A,{title:(0,s.__)("Skip setting up ads?","google-listings-and-ads"),buttons:[(0,a.createElement)(P.A,{key:"cancel",isSecondary:!0,onClick:e},(0,s.__)("Cancel","google-listings-and-ads")),(0,a.createElement)(P.A,{key:"complete-setup",onClick:t,isPrimary:!0},(0,s.__)("Complete setup without setting up ads","google-listings-and-ads"))],onRequestClose:e},(0,a.createElement)("p",null,(0,s.__)("Enabling Performance Max is highly recommended to drive more sales and reach new audiences across Google channels like Search, YouTube and Discover.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,s.__)("Performance Max uses the best of Google’s AI to show the most impactful ads for your products at the right time and place. Google will use your product data to create ads for this campaign.","google-listings-and-ads")),(0,a.createElement)("p",null,(0,a.createElement)(q.A,{href:"https://support.google.com/google-ads/answer/10724817",context:"skip-paid-ads-modal",linkId:"paid-ads-with-performance-max-campaigns-learn-more"},(0,s.__)("Learn more about Performance Max.","google-listings-and-ads"))));function ge({isValidForm:e,onSkipCreatePaidAds:t=X.noop,loading:n,disabled:o}){const[i,l]=(0,m.useState)(!1),{googleAdsAccount:c}=(0,O.A)(),{billingStatus:g}=(0,ne.A)();return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(P.A,{isTertiary:!0,text:(0,s.__)("Skip ads creation","google-listings-and-ads"),loading:n,disabled:o,onClick:()=>{l(!0)}}),i&&(0,a.createElement)(re,{onRequestClose:()=>{l(!1)},onSkipCreatePaidAds:()=>{l(!1);const n={google_ads_account_status:c?.status,billing_method_status:g?.status||"unknown",campaign_form_validation:e?"valid":"invalid"};(0,r.ce)("gla_onboarding_complete_button_click",n),t()}}))}const de="gla-onboarding-paid-ads",{sessionStorage:ue}=window,me={setCampaign({amount:e}){const t=JSON.stringify({amount:e});ue.setItem(de,t)},getCampaign(){const e=ue.getItem(de);return null===e?null:JSON.parse(e)}};var pe=n(4679);function he(){const e=(0,$.A)(),[t,n]=(0,m.useState)(null),{data:o}=(0,C.A)(),{highestDailyBudget:i,hasFinishedResolution:l}=(0,pe.A)(o),[c]=(0,Z.A)(),{billingStatus:r}=(0,ne.A)(),{syncSettings:g}=(0,p.j)(),u=r?.status===se.CX.APPROVED,h=async(t=X.noop)=>{try{await g(),await t()}catch(e){return n(null),void(0,oe.h)(e,(0,s.__)("Unable to complete your setup.","google-listings-and-ads"))}const a={guide:se.K4.SUBMISSION_SUCCESS};window.location.href=e+(0,ae.hP)(a)},_=async()=>{n(le),await h()},f={amount:i,...me.getCampaign()};return l&&o?(0,a.createElement)(te.A,{initialCampaign:f,recommendedDailyBudget:i,onChange:(e,t)=>{t.amount>=i&&me.setCampaign(t)}},(0,a.createElement)(ee.A,{headerTitle:(0,s.__)("Create a campaign to advertise your products","google-listings-and-ads"),continueButton:e=>{const{isValidForm:i,values:l}=e,{amount:r}=l,g=t===le||!i||!u;return(0,a.createElement)(P.A,{isPrimary:!0,disabled:g,onClick:async()=>{n(ie);const e=c.bind(null,r,o);await h(e)},loading:t===ie,text:(0,s.__)("Complete setup","google-listings-and-ads"),eventName:"gla_onboarding_complete_with_paid_ads_button_click",eventProps:{budget:r,audiences:o?.join(",")}})},skipButton:e=>{const{isValidForm:n}=e;return(0,a.createElement)(ge,{isValidForm:n,onSkipCreatePaidAds:_,disabled:t===ie,loading:t===le})},context:"setup-mc"})):(0,a.createElement)(d.A,null)}const _e={accounts:"1",product_listings:"2",paid_ads:"3"},fe=({savedStep:e})=>{const[t,n]=(0,m.useState)(e),{settings:o,saveSettings:i}=(0,E.A)(),{data:l}=(()=>{const{hasFinishedResolution:e}=(0,A.A)(),{hasFinishedResolution:t,data:n}=(0,f.A)(),{loading:a,data:o}=(0,y.A)({path:`${_.RV}/mc/target_audience/suggestions`});return{loading:!e||!t||a,data:t&&null===n?.location&&null===n?.countries?o:n}})(),{targetAudience:c,getFinalCountries:g}=(0,C.A)(),{hasFinishedResolution:d,data:G}=(0,b.A)(),{hasFinishedResolution:I,data:x}=(0,S.A)(),{saveTargetAudience:P}=(0,p.j)(),{saveShippingRates:T}=(0,k.A)(),{saveShippingTimes:N}=(0,w.A)(),{createNotice:R}=(0,v.A)();(0,h.A)(r.T1,{context:r.GH,step:t}),(0,m.useEffect)((()=>{null===c?.location&&l?.location&&P(l)}),[c,l,P]),(0,m.useEffect)((()=>{null===o?.shipping_rate&&i({...o,shipping_rate:"flat",shipping_time:"flat"})}),[o,i]);const F=e=>{const a=t;(0,r.dQ)("gla_setup_mc",a,e),n(e)},M=e=>{Number(e)R("error",e)))}const B=d?G:null,D=I?x:null,W=c?.location?c:null,Y=o?.shipping_rate?o:null;return(0,a.createElement)(u.Stepper,{className:"gla-setup-stepper",currentStep:t,steps:[{key:_e.accounts,label:(0,s.__)("Set up your accounts","google-listings-and-ads"),content:(0,a.createElement)(U,{onContinue:()=>{F(_e.product_listings)}}),onClick:M},{key:_e.product_listings,label:(0,s.__)("Configure product listings","google-listings-and-ads"),content:(0,a.createElement)(Q,{targetAudience:W,settings:Y,shippingRates:B,shippingTimes:D,resolveFinalCountries:g,onTargetAudienceChange:q.bind(P,(0,s.__)("There was an error saving audience.","google-listings-and-ads")),onSettingsChange:q.bind(i,(0,s.__)("There was an error saving settings.","google-listings-and-ads")),onShippingRatesChange:q.bind(T,(0,s.__)("There was an error saving shipping rates.","google-listings-and-ads")),onShippingTimesChange:q.bind(N,(0,s.__)("There was an error saving shipping times.","google-listings-and-ads")),onContinue:()=>{F(_e.paid_ads)},submitLabel:(0,s.__)("Continue","google-listings-and-ads")}),onClick:M},{key:_e.paid_ads,label:(0,s.__)("Create a campaign","google-listings-and-ads"),content:(0,a.createElement)(he,null),onClick:M}]})};var Ae=n(3027);const ye=()=>{const{hasFinishedResolution:e,data:t}=(0,Ae.A)();if(!e&&!t)return(0,a.createElement)(d.A,null);if(e&&!t)return null;const{status:n,step:o}=t;return"complete"===n?((0,i.getHistory)().replace((0,i.getNewPath)({},"/google/dashboard")),null):(0,a.createElement)(fe,{savedStep:_e[o]})},Ce=()=>((0,o.A)("full-page"),(0,a.createElement)(a.Fragment,null,(0,a.createElement)(g,null),(0,a.createElement)(ye,null)))},4876:(e,t,n)=>{n.d(t,{NS:()=>u,RO:()=>d,Me:()=>m,Ay:()=>_});var a=n(1609),o=n(7723),s=n(6427),i=n(6087),l=n(9457),c=n(7892),r=n(7792),g=n(3658);const d="all-accounts",u="ads-account",m="api-data-fetch-feature",p={[d]:{title:(0,o.__)("Disconnect all accounts","google-listings-and-ads"),confirmButton:(0,o.__)("Disconnect all accounts","google-listings-and-ads"),confirmation:(0,o.__)("Yes, I want to disconnect all my accounts.","google-listings-and-ads"),contents:[(0,o.__)("I understand that I am disconnecting any WordPress.com account, Google account, Google Merchant Center account and Google Ads account connected to this extension.","google-listings-and-ads"),(0,o.__)("Any active product listings will continue to show on Google. They can be managed, edited, or deleted manually from Google Merchant Center (merchants.google.com).","google-listings-and-ads"),(0,o.__)("Any ongoing campaigns will continue to run. They can be managed, edited, or deleted manually from Google Ads (ads.google.com).","google-listings-and-ads")]},[u]:{title:(0,o.__)("Disconnect Google Ads account","google-listings-and-ads"),confirmButton:(0,o.__)("Disconnect Google Ads Account","google-listings-and-ads"),confirmation:(0,o.__)("Yes, I want to disconnect my Google Ads account.","google-listings-and-ads"),contents:[(0,o.__)("I understand that I am disconnecting my Google Ads account from this WooCommerce extension.","google-listings-and-ads"),(0,o.__)("Any ongoing campaigns will continue to run. They can be managed, edited, or deleted manually from Google Ads (ads.google.com).","google-listings-and-ads"),(0,o.__)("Some configurations for Google Ads created through WooCommerce may be lost. This cannot be undone.","google-listings-and-ads")]},[m]:{title:(0,o.__)("Disable data fetching","google-listings-and-ads"),confirmButton:(0,o.__)("Disable data fetching","google-listings-and-ads"),confirmation:(0,o.__)("Yes, I want to disable the data fetching feature.","google-listings-and-ads"),contents:[(0,o.__)("I understand that I am disabling the data fetching feature from this WooCommerce extension.","google-listings-and-ads"),(0,o.__)("Any ongoing campaigns and configuration will continue to run. They will be pushed to Google as in the previous versions of this extension.","google-listings-and-ads")]}};function h({disconnectTarget:e,onRequestClose:t,onDisconnected:n,disconnectAction:u}){const[m,h]=(0,i.useState)(!1),[_,f]=(0,i.useState)(!1),A=(0,g.j)(),{title:y,confirmButton:C,confirmation:E,contents:b}=p[e],S=()=>{_||t()};return(0,a.createElement)(l.A,{className:"gla-disconnect-accounts-modal",title:(0,a.createElement)(a.Fragment,null,(0,a.createElement)(r.A,{size:20}),y),isDismissible:!_,buttons:[(0,a.createElement)(c.A,{key:"1",isSecondary:!0,disabled:_,onClick:S},(0,o.__)("Never mind","google-listings-and-ads")),(0,a.createElement)(c.A,{key:"2",isPrimary:!0,isDestructive:!0,loading:_,disabled:!m,onClick:()=>{let a=e===d?A.disconnectAllAccounts:A.disconnectGoogleAdsAccount;u&&(a=u),f(!0),a().then((()=>{n(),t()})).catch((()=>{f(!1)}))}},C)],onRequestClose:S},b.map(((e,t)=>(0,a.createElement)("p",{key:t},e))),(0,a.createElement)(s.CheckboxControl,{label:E,checked:m,disabled:_,onChange:h}))}function _(e){return(0,a.createElement)(h,{...e})}}}]);PK!9f\\%js/build/product-attributes.asset.phpnu[ array('jquery'), 'version' => '85c56937325ac9e55094'); PK!]7js/build/product-attributes.cssnu[.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox{border:1px solid #ddd;clear:both;margin:16px 0!important;overflow:hidden}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox .wc-metabox-content{overflow:hidden;padding:0 1em}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox h3 .handlediv{margin-top:0;visibility:visible}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox .gla-input .options{border:none}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox .gla-input .options input[type=checkbox]{margin-top:5px!important}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox:last-of-type{border-bottom:1px solid #ddd}#woocommerce-product-data ul.wc-tabs li.gla_attributes_tab a:before{content:""}.gla-channel-visibility-box .form-row>select{display:block;margin:8px 0;max-width:100%} PK!{22js/build/product-attributes.jsnu[(()=>{"use strict";var t={n:o=>{var e=o&&o.__esModule?()=>o.default:()=>o;return t.d(e,{a:e}),e},d:(o,e)=>{for(var n in e)t.o(e,n)&&!t.o(o,n)&&Object.defineProperty(o,n,{enumerable:!0,get:e[n]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o)};const o=window.jQuery;var e=t.n(o);window.glaData;const n=window.glaProductData,c=[["CAMPAIGN","campaign"],["ASSET_GROUP","asset-group"]],a=(Object.fromEntries(c),c.reduce(((t,o,e)=>{const n=(e+1).toString();return t[o[1]]=n,t}),{}),t=>{const o=t.parents("div.select-with-text-input").find(".custom-input");"_gla_custom_value"===t.val()?o.show():o.hide()}),r=t=>{const o=t("div.select-with-text-input select");o.each((function(o,e){a(t(e))})),o.on("change",(function(){a(t(this))}))};e()((function(){const t=()=>{r(e())};e()("#woocommerce-product-data").on("woocommerce_variations_loaded",t),e()(document.body).on("woocommerce_variations_added",t),t()})),e()(document).on("woocommerce-product-type-change","body",((t,o)=>{const c=n.applicableProductTypes.includes(o);e()(".gla_attributes_tab, .gla_meta_box").toggle(c)}))})();PK!]7#js/build/product-attributes-rtl.cssnu[.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox{border:1px solid #ddd;clear:both;margin:16px 0!important;overflow:hidden}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox .wc-metabox-content{overflow:hidden;padding:0 1em}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox h3 .handlediv{margin-top:0;visibility:visible}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox .gla-input .options{border:none}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox .gla-input .options input[type=checkbox]{margin-top:5px!important}.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation .gla-metabox.wc-metabox:last-of-type{border-bottom:1px solid #ddd}#woocommerce-product-data ul.wc-tabs li.gla_attributes_tab a:before{content:""}.gla-channel-visibility-box .form-row>select{display:block;margin:8px 0;max-width:100%} PK!4>.>.js/build/product-feed.cssnu[.app-tab-nav__tabs{align-items:stretch;box-shadow:inset 0 -1px 0 #ccc;display:flex;flex-wrap:wrap;margin-bottom:var(--main-gap)}.app-tab-nav__tabs-item{background:#0000;border:none;border-radius:0;box-shadow:inset 0 -1px 0 #ccc;box-sizing:border-box;cursor:pointer;font-weight:500;height:48px;margin-left:0;padding:3px 16px}.app-tab-nav__tabs-item:after{content:attr(data-label);display:block;height:0;overflow:hidden;speak:none;visibility:hidden}.app-tab-nav__tabs-item:focus:not(:disabled){box-shadow:inset 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) #0000,inset 0 -1.5px 0 0 var(--wp-admin-theme-color);position:relative}.app-tab-nav__tabs-item.is-active:before{border-bottom:1.5px solid #0000;bottom:1px;content:"";left:0;position:absolute;right:0;top:0}.app-tab-nav__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),inset 0 -1.5px 0 0 var(--wp-admin-theme-color)} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-gtin-migration__link{cursor:pointer;text-decoration:underline} .app-table-card-div .components-card__header h2{margin-right:8px}.app-table-card-div .components-card__header .woocommerce-table__actions{justify-content:flex-start}.app-table-card-div .components-card__body .woocommerce-table__table .components-base-control__field{margin-bottom:0} .help-popover{display:inline-flex;font-size:13px;font-weight:400;line-height:1.4}.help-popover button{align-items:center;background:none;border:none;display:inline-flex;padding:1px;fill:#949494}.help-popover button:not(:disabled){cursor:pointer}.help-popover button:hover:not(:disabled){fill:#616161}.help-popover .components-popover__content{padding:16px;width:300px}@media(min-width:960px){.help-popover .components-popover__content{width:360px}}@media(min-width:1080px){.help-popover .components-popover__content{width:480px}} .gla-warning-icon.gridicon.gridicons-notice-outline{fill:#f0b849} .gla-error-icon.gridicon.gridicons-notice{fill:#d94f4f} .gla-app-text{font-weight:400;margin:0}.gla-app-text--body{font-size:13px;line-height:20px}.gla-app-text--caption,.gla-app-text--label{font-size:12px;line-height:16px}.gla-app-text--label{font-weight:600}.gla-app-text--title-small{font-size:20px;line-height:28px}.gla-app-text--title-medium{font-size:24px;line-height:32px}.gla-app-text--subtitle{font-size:16px;font-weight:600;line-height:24px}.gla-app-text--subtitle-small{font-size:14px;line-height:20px} .gla-issues-table-card .woocommerce-table,.gla-issues-table-card .woocommerce-table .woocommerce-pagination{margin-bottom:0}.gla-issues-table-card .app-tab-nav__tabs{margin-bottom:0;padding:0 24px}@media(max-width:480px){.gla-issues-table-card .app-tab-nav__tabs{padding:0}}.gla-issues-table-card .gla-issues-solved{margin:0 auto;max-width:600px;padding:40px;text-align:center}.gla-issues-table-card .gla-issues-solved__icon{color:#bbb;font-size:40px;height:40px;margin-bottom:16px;width:40px}.gla-issues-table-card .gla-issues-solved__body{color:#757575;line-height:1.2;margin-top:8px}.gla-issues-table-data-modal .components-modal__header{border-bottom:1px solid #ccc}.gla-issues-table-data-modal .gridicon{margin-right:.5em;width:12px} .gla-success-icon.gridicon{fill:#008a20} .gla-sync-icon.gridicon{fill:#f0b849} .gla-review-request .gla-sync-icon{fill:#f0b849}.gla-review-request-modal{max-width:600px}.gla-review-request-modal__notice{margin-bottom:24px;margin-left:0}.gla-review-request-modal__issue-list{list-style:initial;padding:inherit;padding-bottom:0}.gla-review-request-modal__checkbox{margin-top:24px}.gla-review-request-notice{background:#fef8ee;flex-wrap:wrap;padding:24px}.gla-review-request-notice__text{padding:0 16px}.gla-review-request-notice__text-body{color:#757575}.gla-review-request-notice__button{margin:8px 0} .gla-product-feed-table-card .components-card__header .woocommerce-table__actions{align-items:center;justify-content:flex-end;min-height:36px}.gla-product-feed-table-card .components-card__header .woocommerce-table__actions>div{width:auto}.gla-product-feed-table-card .gla-tooltip__children-container{line-height:0}.gla-product-feed-table-card .gla-tooltip__children-container>svg{fill:#949494}.gla-product-feed-table-card .woocommerce-table__actions .components-base-control{font-size:13px;height:36px;width:250px}.gla-product-feed-table-card .woocommerce-table__actions .components-base-control__label{font-size:13px}.gla-product-feed-table-card .woocommerce-table__actions .with-value .components-base-control__label{display:none}.gla-product-feed-table-card .woocommerce-table__actions .woocommerce-select-control__listbox{top:100%}.gla-product-feed-table-card .woocommerce-table__actions .woocommerce-select-control__option{font-size:13px;min-height:36px}.gla-product-feed-table-card .woocommerce-table__actions .woocommerce-select-control__control-input{color:#1e1e1e;font-size:13px;margin-top:0}.gla-product-feed-table-card .woocommerce-table{margin-bottom:0} .gla-tooltip__children-container{cursor:help;display:inline-block}.gla-tooltip__children-container>span{cursor:auto}.gla-admin-page .disabled-element-wrapper:has(.gla-tooltip__children-container){display:inline-block} .gla-admin-page .components-guide__page-control>li[aria-current=step] .components-button{color:var(--wp-admin-theme-color)} .gla-guide__page-content{padding:0 24px}.gla-guide__page-content__header{color:#757575;font-size:24px;font-weight:400;line-height:32px;margin:0 0 24px}.gla-guide__page-content__body{color:#757575;font-size:16px;line-height:1.5}.gla-guide__page-content__body>p{font-size:inherit;line-height:inherit;margin:1.5em 0}.gla-guide__page-content__body>cite{display:block;font-size:11px;font-style:normal;line-height:18px}.gla-guide__page-content__link{text-decoration:none} .gla-submission-success-guide.components-guide{height:auto}@media(min-width:600px){.gla-submission-success-guide.components-guide{max-height:none;max-width:517px}}.gla-submission-success-guide .components-modal__header{position:absolute}.gla-submission-success-guide .components-modal__header .components-button{color:#50575e}.gla-submission-success-guide .components-modal__header .components-button:hover svg{fill:currentcolor}.gla-submission-success-guide .components-guide__container{margin-top:0}.gla-submission-success-guide .components-guide__page{flex-grow:1;justify-content:normal}.gla-submission-success-guide .components-guide__page-control{margin:18px 0}.gla-submission-success-guide .components-guide__page-control li{margin-bottom:0}.gla-submission-success-guide .components-guide__page-control .components-button{height:20px;min-width:20px;padding:6px}.gla-submission-success-guide .components-guide__footer{box-sizing:border-box;height:auto;justify-content:flex-end;margin:calc(var(--large-gap)/2) 0 0;padding:0 24px 24px;width:100%}@media(max-width:600px){.gla-submission-success-guide .components-guide__footer{flex-wrap:wrap;position:static}.gla-submission-success-guide .components-guide__footer .components-button{justify-content:center;margin-top:12px;width:100%}}.gla-submission-success-guide .components-guide__footer .components-button{border-radius:3px;box-sizing:border-box;font-size:14px;font-weight:500;height:36px;padding:0 16px;position:static}.gla-submission-success-guide .components-guide__footer .components-button~.components-button{margin-left:calc(var(--main-gap)/2)}.gla-submission-success-guide .components-guide__back-button,.gla-submission-success-guide .components-guide__forward-button{color:var(--wp-admin-theme-color)}.gla-submission-success-guide .components-guide__forward-button{border:1px solid}.gla-submission-success-guide__logo-block{align-items:center;background-color:#f3f4f5;display:flex;height:160px;justify-content:center;margin:0}.gla-submission-success-guide__logo-block+.gla-guide__page-content{margin-top:var(--main-gap)}.gla-submission-success-guide__logo-item{flex:1;line-height:0;margin-bottom:0}.gla-submission-success-guide__logo-item:first-child{text-align:right}.gla-submission-success-guide__logo-separator-line{background-color:#949494;height:63px;margin:0 28px;width:1px}.gla-submission-success-guide__space_holder{flex:1 1 0} .gla-ads-inline-notice{background-color:#f0f6fc;margin-bottom:16px;padding:16px 24px}.gla-ads-inline-notice p{margin-top:0} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .gla-product-statistics__last-updated{align-items:center;display:flex;gap:8px;margin-bottom:calc(var(--main-gap)/3*2)}.gla-product-statistics__summaries .woocommerce-summary{margin:0}.gla-product-statistics__summaries .woocommerce-summary .woocommerce-summary__item-delta{display:none}.gla-product-statistics__summaries .woocommerce-summary .app-spinner{justify-content:left;padding:0}.gla-product-statistics .components-card__footer{flex-direction:column}@media(max-width:960px){.gla-product-statistics .components-card__footer .components-flex{align-items:normal;flex-direction:column;margin:12px 0}.gla-product-statistics .components-card__footer .components-flex-item:not(:first-child){margin-left:16px;margin-top:8px}}.gla-product-statistics .components-card__footer .components-flex{align-items:flex-start}.gla-product-statistics .components-card__footer .components-flex+.components-flex{margin-top:8px}.gla-product-statistics .components-card__footer .components-flex-item{color:#1e1e1e;padding-top:2px}.gla-product-statistics .components-card__footer .components-flex-item:first-child{min-width:130px}.gla-product-statistics .components-card__footer .components-flex-item.gla-status__icon{min-width:30px;padding-top:0}.gla-product-statistics .components-card__footer .components-flex-item.gla-status__icon .gla-success{fill:#008a20}.gla-product-statistics .components-card__footer .components-flex-item .gla-status__label .gla-error{color:#d94f4f}.gla-product-statistics .components-card__footer .components-flex-item .gla-status__label .gla-success{color:#008a20}.gla-product-statistics .components-card__footer .components-flex-item .gla-status__description{color:#757575}.gla-product-statistics .components-card__footer .components-flex-item .gla-status__description:not(:empty):before{content:"•";display:inline-block;margin:0 8px}.gla-product-statistics .components-card__footer .gridicon{margin-right:4px}.gla-product-statistics .components-card__footer .gridicons-sync{transform:rotate(90deg)}.gla-product-statistics .components-card__footer .overview-stats-error-button{height:auto;padding-left:0;padding-top:0} .gla-product-feed{display:flex;flex-direction:column;gap:var(--large-gap);margin-bottom:var(--large-gap)} .gla-rebranding-tour__heading{align-items:center;display:flex;gap:8px;fill:var(--wp-admin-theme-color)}.gla-rebranding-tour .components-card__footer{padding-top:0}.gla-rebranding-tour .components-card__footer>*{display:none} PK!,TnTnjs/build/product-feed.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[61],{9061:(e,t,s)=>{s.r(t),s.d(t,{default:()=>De});var a=s(1609),n=s(7723),l=s(6087),o=s(6476),i=s(9927),r=s(6427),c=s(6942),d=s.n(c),g=s(2159),u=s(6257),m=s(1177),_=s(8846),p=s(3905);const E=[{key:"type",label:(0,n.__)("Type","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"affectedProduct",label:(0,n.__)("Affected product","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"issue",label:(0,n.__)("Issue","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"suggestedAction",label:(0,n.__)("Suggested action","google-listings-and-ads"),isLeftAligned:!0,required:!0},{key:"action",label:"",required:!0}];var v=s(6473),h=s(7892),y=s(7792),b=s(2178),w=s(8987),f=s(6494),A=s(6727),k=s(6459);const S=()=>{const e=(0,A.A)(),t={[p.Tj]:(0,n.__)("All account issues resolved","google-listings-and-ads"),[p.ds]:(0,n.__)("All product issues resolved","google-listings-and-ads")},s={[p.Tj]:(0,l.createInterpolateElement)((0,n.__)("However, there are issues affecting your products that needs to be resolved. Head over to the Product Issues tab to view them.","google-listings-and-ads"),{strong:(0,a.createElement)("strong",null)}),[p.ds]:(0,l.createInterpolateElement)((0,n.__)("However, there are issues affecting your account that needs to be resolved. Head over to the Account Issues tab to view them.","google-listings-and-ads"),{strong:(0,a.createElement)("strong",null)})};return(0,a.createElement)("div",{className:"gla-issues-solved"},(0,a.createElement)(r.Dashicon,{icon:"yes-alt",className:"gla-issues-solved__icon"}),(0,a.createElement)(k.A,{variant:"subtitle"},t[e]),(0,a.createElement)(k.A,{variant:"body",className:"gla-issues-solved__body"},s[e]))};var N=s(6227),C=s(9457);const P=({issue:e,onRequestClose:t=()=>{}})=>(0,a.createElement)(C.A,{className:"gla-issues-table-data-modal",title:e.issue,onRequestClose:t,buttons:[(0,a.createElement)(h.A,{key:"learn-more",isPrimary:!0,target:"_blank",href:e.action_url,text:(0,n.__)("Learn more","google-listings-and-ads"),eventName:"gla_documentation_link_click",eventProps:{context:"issues-data-table-modal",linkId:e.code,href:e.action_url},icon:(0,a.createElement)(N.A,null)})]},(0,a.createElement)("p",null,(0,a.createElement)("strong",null,(0,n.__)("What to do?","google-listings-and-ads"))),(0,a.createElement)("p",null,e.action)),I=({data:e})=>{const t=(0,n.__)("Read more about this issue","google-listings-and-ads");return e?e.issues?.length?(0,a.createElement)(_.Table,{caption:(0,n.__)("Issues to resolve","google-listings-and-ads"),headers:E,rows:e.issues.map((e=>[{display:"warning"===e.severity?(0,a.createElement)(y.A,null):(0,a.createElement)(b.A,null)},{display:e.product},{display:e.issue},{display:e.action?(0,a.createElement)(f.A,{button:(0,a.createElement)(h.A,{isLink:!0,eventName:"gla_click_read_more_about_issue",eventProps:{context:"issues-to-resolve",issue:e.code}},t),modal:(0,a.createElement)(P,{issue:e})}):(0,a.createElement)(m.A,{context:"issues-to-resolve",linkId:e.code,href:e.action_url},t)},{display:e.type===p.ds&&(0,a.createElement)(w.A,{productId:e.product_id,eventName:"gla_edit_product_issue_click",eventProps:{code:e.code,issue:e.issue}})}]))}):(0,a.createElement)(S,null):(0,a.createElement)(_.EmptyTable,{headers:E,numberOfRows:1},(0,n.__)("An error occurred while retrieving issues. Please try again later.","google-listings-and-ads"))};var x=s(6425),q=s(1396);const R=()=>{const e=(0,A.A)(),{page:t,setPage:s}=(0,q.A)(e),{data:l,hasFinishedResolution:o}=(0,x.A)(e,t);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(r.CardBody,{size:null},o?(0,a.createElement)(I,{data:l}):(0,a.createElement)(_.TablePlaceholder,{headers:E,caption:(0,n.__)("Loading Issues To Resolve","google-listings-and-ads")})),l?.total>0&&(0,a.createElement)(r.CardFooter,{justify:"center"},(0,a.createElement)(_.Pagination,{page:t,perPage:p.X4,total:l.total,showPagePicker:!1,showPerPagePicker:!1,onPageChange:(t,a)=>{s(t),(0,v.Xh)(`${e}-issues-to-resolve`,t,a)}})))};var F=s(347),T=s(7951);const G=()=>{const e=(0,T.A)(),t=(0,A.A)(),s=t=>{const s=e[t];return s>=0?`(${s})`:""},l=[{key:p.Tj,title:`${(0,n.__)("Account Issues","google-listings-and-ads")} ${s(p.Tj)}`,href:(0,o.getNewPath)({issueType:p.Tj},"/google/product-feed",{})},{key:p.ds,title:`${(0,n.__)("Product Issues","google-listings-and-ads")} ${s(p.ds)}`,href:(0,o.getNewPath)({issueType:p.ds},"/google/product-feed",{})}];return(0,a.createElement)(F.A,{tabs:l,selectedKey:t})},D=({issues:e=[]})=>{const[t,s]=(0,l.useState)(!1);if(!e.length)return null;const o=t?e:e.slice(0,5);return(0,a.createElement)(a.Fragment,null,(0,a.createElement)(k.A,{variant:"subtitle"},(0,n.__)("Request a review on the following issue(s):","google-listings-and-ads")),(0,a.createElement)("ul",{className:"gla-review-request-modal__issue-list"},o.map((e=>(0,a.createElement)("li",{key:e.code},e.issue)))),e.length>5&&(0,a.createElement)(h.A,{isTertiary:!0,onClick:()=>{(0,v.ce)("gla_request_review_issue_list_toggle_click",{action:t?"collapse":"expand"}),s(!t)}},t?(0,n.__)("Show less","google-listings-and-ads"):(0,n.sprintf)( // translators: %d: The number of extra issues issues // translators: %d: The number of extra issues issues (0,n.__)("+ %d more issue(s)","google-listings-and-ads"),e.length-5)))};var L=s(3658),O=s(5640);const z=({issues:e=[],isActive:t=!1,onClose:s=()=>{}})=>{const[o,i]=(0,l.useState)(!1),[c,d]=(0,l.useState)(!1),{sendMCReviewRequest:g}=(0,L.j)(),{createNotice:u}=(0,O.A)();if(!t)return null;const _=e=>{c||s(e)};return(0,a.createElement)(C.A,{className:"gla-review-request-modal",title:(0,n.__)("Request account review","google-listings-and-ads"),buttons:[(0,a.createElement)(h.A,{key:"secondary",isSecondary:!0,onClick:()=>{_("maybe-later")}},(0,n.__)("Cancel","google-listings-and-ads")),(0,a.createElement)(h.A,{loading:c,key:"primary",isPrimary:!0,disabled:!o&&e.length,onClick:()=>{c||(d(!0),(0,v.ce)("gla_request_review"),g().then((()=>{u("success",(0,n.__)("Your account review was successfully requested.","google-listings-and-ads")),(0,v.ce)("gla_request_review_success"),s("request-review-success")})).catch((()=>{d(!1),(0,v.ce)("gla_request_review_failure")})))}},(0,n.__)("Request account review","google-listings-and-ads"))],onRequestClose:()=>{_("dismiss")}},(0,a.createElement)(r.Notice,{className:"gla-review-request-modal__notice",status:"warning",isDismissible:!1},(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("Please ensure that you have resolved all account suspension issues before requesting for an account review. If some issues are unresolved, you wont be able to request another review for at least 7 days. Learn more","google-listings-and-ads"),{Link:(0,a.createElement)(m.A,{href:"https://support.google.com/merchants/answer/2948694",context:"request-review-modal",linkId:"request-review-modal-learn-more"})}))),(0,a.createElement)(D,{issues:e}),e.length>0&&(0,a.createElement)(r.CheckboxControl,{className:"gla-review-request-modal__checkbox",label:(0,n.__)("I have resolved all the issue(s) listed above.","google-listings-and-ads"),checked:o,onChange:e=>{i(e),(0,v.ce)("gla_request_review_issues_solved_checkbox_click",{action:e?"check":"uncheck"})}}))};var U=s(8443),M=s(6397),$=s(4473);const B={status:(0,a.createElement)("span",{className:"gla-error"},(0,n.__)("Disapproved","google-listings-and-ads")),statusDescription:(0,n.__)("To make products eligible to show on Google, fix all setup and policy issues that were found.","google-listings-and-ads"),title:(0,n.__)("We’ve found unresolved issues in your account.","google-listings-and-ads"),body:(0,n.__)("Fix all account suspension issues listed below to request a review of your account.","google-listings-and-ads"),requestButton:!0,icon:(0,a.createElement)(b.A,{size:24})},W={...B,status:(0,n.__)("Warning","google-listings-and-ads"),statusDescription:(0,n.__)("To keep showing your products on Google, fix your setup and policy issues.","google-listings-and-ads"),icon:(0,a.createElement)(y.A,{size:24})},j={status:(0,n.__)("Pending review","google-listings-and-ads"),statusDescription:(0,n.__)("This may take up to 3 days. If approved, your products will show on Google once it’s completed.","google-listings-and-ads"),icon:(0,a.createElement)($.A,{size:24})},Y={UNDER_REVIEW:{status:(0,n.__)("Under review","google-listings-and-ads"),statusDescription:(0,n.__)("Review requests take at least 7 days.","google-listings-and-ads"),icon:(0,a.createElement)($.A,{size:24})},PENDING_REVIEW:j,DISAPPROVED:B,WARNING:W,APPROVED:{status:(0,a.createElement)("span",{className:"gla-success"},(0,n.__)("Approved","google-listings-and-ads")),statusDescription:(0,n.__)("Your products listings are on Google.","google-listings-and-ads"),icon:(0,a.createElement)(M.A,{size:24})},ONBOARDING:{status:(0,n.__)("No products added","google-listings-and-ads"),statusDescription:(0,n.__)("Add and sync products to Google.","google-listings-and-ads"),icon:(0,a.createElement)(y.A,{size:24})}},H=({account:e,onRequestReviewClick:t=()=>{}})=>{const s=Y[e.status];if(!s)return null;const l=e.cooldown&&(0,n.sprintf)( // translators: %s: Cool down period date. // translators: %s: Cool down period date. (0,n.__)("Your account is under cool down period. You can request a new review on %s.","google-listings-and-ads"),(0,U.format)(`${p.Th.dateFormat}, ${p.Th.timeFormat}`,new Date(e.cooldown)));return(0,a.createElement)(r.Flex,{"data-testid":"gla-review-request-notice",className:"gla-review-request-notice"},(0,a.createElement)(r.FlexItem,null,(0,a.createElement)(r.Flex,null,(0,a.createElement)(r.FlexItem,{className:"gla-review-request-notice__icon"},s.icon),(0,a.createElement)(r.FlexItem,{className:"gla-review-request-notice__text"},(0,a.createElement)(k.A,{variant:"subtitle"},s.title),(0,a.createElement)(k.A,{className:"gla-review-request-notice__text-body",variant:"body"},l||s.body)))),(0,a.createElement)(r.FlexItem,{className:"gla-review-request-notice__button"},s.requestButton&&(e.cooldown||Object.keys(e.reviewEligibleRegions)?.length>0)&&(0,a.createElement)(h.A,{isPrimary:!0,onClick:t,disabled:!!e.cooldown,text:(0,n.__)("Request review","google-listings-and-ads")})))},V=({account:e={}})=>{const[t,s]=(0,l.useState)(!1),n=(0,A.A)(),{data:o,hasFinishedResolution:i}=(0,x.A)(p.Tj,1,200),{data:r,hasFinishedResolution:c}=e;return i&&c&&(d=r.status,Y[d]?.title)&&n===p.Tj?(0,a.createElement)("div",{className:"gla-review-request"},(0,a.createElement)(z,{issues:o.issues.filter((e=>r.issues.includes(e.code))),isActive:t,onClose:e=>{s(!1),(0,v.ce)("gla_modal_closed",{context:p.zU,action:e})}}),(0,a.createElement)(H,{account:r,onRequestReviewClick:()=>{s(!0),(0,v.ce)("gla_modal_open",{context:p.zU})}})):null;var d};var K=s(9415);const X=(0,a.createElement)(u.A,{id:"issues-to-resolve"},(0,l.createInterpolateElement)((0,n.__)("Products and stores must meet Google Merchant Center’s requirements in order to get approved. WooCommerce and Google automatically check your product feed to help you resolve any issues. ","google-listings-and-ads"),{link:(0,a.createElement)(m.A,{context:"product-feed",linkId:"issues-to-resolve",href:"https://support.google.com/merchants/answer/6363310"})})),Q=()=>{const{total:e}=(0,T.A)(),t=(0,K.A)("getMCReviewRequest");return e?(0,a.createElement)(g.A,{className:"gla-issues-table-card"},(0,a.createElement)(r.Card,{className:d()("woocommerce-table",{"has-actions":!!X})},(0,a.createElement)(r.CardHeader,null,(0,a.createElement)(k.A,{variant:"title-small",as:"h2"},(0,n.__)("Issues to resolve","google-listings-and-ads")),(0,a.createElement)("div",{className:"woocommerce-table__actions"},X)),(0,a.createElement)(G,null),(0,a.createElement)(V,{account:t}),(0,a.createElement)(R,null))):null};var Z=s(4772),J=s(9039);const ee=(0,n.__)("Select channel visibility","google-listings-and-ads"),te=[{key:0,value:!0,label:(0,n.__)("Sync and show","google-listings-and-ads")},{key:1,value:!1,label:(0,n.__)("Don’t sync and show","google-listings-and-ads")}];function se({withTooltip:e,children:t,...s}){return e?(0,a.createElement)(J.A,{children:t,...s}):t}function ae({selectedSize:e,onActionClick:t}){const[s,o]=(0,l.useState)(null);return(0,l.useEffect)((()=>{0===e&&o(null)}),[e]),0===e?(0,a.createElement)(J.A,{placement:"top",text:(0,n.__)("Select one or more products to bulk edit","google-listings-and-ads")},(0,a.createElement)(r.Icon,{icon:Z.A})):(0,a.createElement)(a.Fragment,null,(0,a.createElement)(_.SelectControl,{label:ee,options:te,selected:s,onChange:o}),(0,a.createElement)(se,{withTooltip:null===s,placement:"top",text:ee},(0,a.createElement)(h.A,{isSecondary:!0,disabled:null===s,onClick:()=>{const e=te.find((({key:e})=>e===s));t(e.value)}},(0,n.sprintf)( // translators: %d: number of selected products to edit channel visibility, with minimum value of 1. // translators: %d: number of selected products to edit channel visibility, with minimum value of 1. (0,n.__)("Apply to %d selected","google-listings-and-ads"),e))))}const ne={approved:(0,n.__)("Approved","google-listings-and-ads"),partially_approved:(0,n.__)("Partially approved","google-listings-and-ads"),expiring:(0,n.__)("Expiring","google-listings-and-ads"),pending:(0,n.__)("Pending","google-listings-and-ads"),disapproved:(0,n.__)("Disapproved","google-listings-and-ads"),not_synced:(0,n.__)("Not synced","google-listings-and-ads")},le="product-feed",oe=e=>e?"sync_and_show":"dont_sync_and_show",ie=()=>{const[e,t]=(0,l.useState)(new Set),[s,o]=(0,l.useState)({page:1,per_page:10,orderby:"title",order:"asc"}),{hasFinishedResolution:i,data:c}=(0,K.A)("getMCProductFeed",s),{updateMCProductVisibility:u}=(0,L.j)(),{createNotice:m}=(0,O.A)(),p=e=>{if(e){const e=c?.products.map((e=>e.id));t(new Set([...e]))}else t(new Set)},E=[{key:"select",label:(0,a.createElement)(r.CheckboxControl,{disabled:!c?.products,checked:c?.products?.length>0&&c?.products?.every((t=>e.has(t.id))),onChange:p}),isLeftAligned:!0,required:!0},{key:"title",label:(0,n.__)("Product Title","google-listings-and-ads"),isLeftAligned:!0,required:!0,isSortable:!0},{key:"visible",label:(0,n.__)("Channel Visibility","google-listings-and-ads"),isLeftAligned:!0,isSortable:!0},{key:"status",label:(0,n.__)("Status","google-listings-and-ads"),isLeftAligned:!0,isSortable:!0},{key:"action",label:"",required:!0}],h=(0,a.createElement)(ae,{selectedSize:e.size,onActionClick:t=>{const s=Array.from(e),{length:a}=s;u(s,t).then((()=>{const e=(0,n.sprintf)( // translators: %d: number of products are updated successfully, with minimum value of 1. // translators: %d: number of products are updated successfully, with minimum value of 1. (0,n._n)("You successfully changed the channel visibility of %d product","You successfully changed the channel visibility of %d products",a,"google-listings-and-ads"),a);m("success",e)})),(0,v.ce)("gla_bulk_edit_click",{context:le,number_of_items:a,visibility_to:oe(t)}),p(!1)}});return(0,a.createElement)(g.A,{className:"gla-product-feed-table-card"},(0,a.createElement)(r.Card,{className:d()("woocommerce-table",{"has-actions":!!h})},(0,a.createElement)(r.CardHeader,null,(0,a.createElement)(k.A,{variant:"title-small",as:"h2"},(0,n.__)("Product Feed","google-listings-and-ads")),(0,a.createElement)("div",{className:"woocommerce-table__actions"},h)),(0,a.createElement)(r.CardBody,{size:null},!i&&(0,a.createElement)(_.TablePlaceholder,{headers:E,numberOfRows:s.per_page,caption:(0,n.__)("Loading product feed","google-listings-and-ads")}),i&&!c?.products&&(0,a.createElement)(_.EmptyTable,{headers:E,numberOfRows:1},(0,n.__)("An error occurred while retrieving products. Please try again later.","google-listings-and-ads")),i&&c?.products&&(0,a.createElement)(_.Table,{headers:E,rows:c.products.map((s=>{return[{display:(0,a.createElement)(r.CheckboxControl,{checked:e.has(s.id),onChange:(l=s.id,s=>{s?t(new Set([...e,l])):(e.delete(l),t(new Set(e)))})})},{display:s.title},{display:s.visible?(0,n.__)("Sync and show","google-listings-and-ads"):(0,n.__)("Don't sync and show","google-listings-and-ads")},{display:ne[s.status]},{display:(0,a.createElement)(w.A,{productId:s.id,eventName:"gla_edit_product_click",eventProps:{status:s.status,visibility:oe(s.visible)}})}];var l})),query:s,onSort:(e,t)=>{o({...s,orderby:e,order:t})}})),(0,a.createElement)(r.CardFooter,{justify:"center"},c?.total>0&&(0,a.createElement)(_.Pagination,{page:s.page,perPage:s.per_page,total:c.total,showPagePicker:!0,showPerPagePicker:!1,onPageChange:(e,t)=>{o({...s,page:e}),(0,v.Xh)(le,e,t)}}))))};var re=s(8428),ce=s(4270),de=s(2391),ge=s(5744),ue=s(3666),me=s(8317),_e=s(9692);const pe="gla_modal_closed",Ee=(0,a.createElement)("div",{className:"gla-submission-success-guide__logo-block"},(0,a.createElement)("div",{className:"gla-submission-success-guide__logo-item"},(0,a.createElement)("img",{src:me,alt:(0,n.__)("WooCommerce Logo","google-listings-and-ads"),width:"145",height:"31"})),(0,a.createElement)("div",{className:"gla-submission-success-guide__logo-separator-line"}),(0,a.createElement)("div",{className:"gla-submission-success-guide__logo-item"},(0,a.createElement)("img",{src:_e,alt:(0,n.__)("Google Logo","google-listings-and-ads"),width:"106",height:"36"}))),ve=[{image:Ee,content:(0,a.createElement)(ce.A,{title:(0,n.__)("You’ve successfully set up Google for WooCommerce! 🎉","google-listings-and-ads")},(0,a.createElement)("p",null,(0,n.__)("Your products are being synced and reviewed. Google reviews product listings in 3-5 days.","google-listings-and-ads")),(0,a.createElement)("p",null,p.Th.adsSetupComplete?(0,n.__)("No ads will launch yet and you won’t be charged until Google approves your listings. Updates are available in your WooCommerce dashboard.","google-listings-and-ads"):(0,l.createInterpolateElement)((0,n.__)("Manage and edit your product feed in WooCommerce. We will also notify you of any product feed issues to ensure your products get approved and perform well on Google.","google-listings-and-ads"),{productFeedLink:(0,a.createElement)(ce.i,{href:(0,ue.hP)(),context:"product-feed"})})))},{image:Ee,content:(0,a.createElement)(ce.A,{title:(0,n.__)("Spend $500 to get $500 in Google Ads credits","google-listings-and-ads")},(0,a.createElement)("p",null,(0,n.__)("New to Google Ads? Get $500 in ad credit when you spend $500 within your first 60 days* You can edit or cancel your campaign at any time.","google-listings-and-ads")),(0,a.createElement)("cite",null,(0,l.createInterpolateElement)((0,n.__)("*Full terms and conditions here.","google-listings-and-ads"),{link:(0,a.createElement)(ce.i,{href:"https://www.google.com/ads/coupons/terms/",context:"terms-of-ads-coupons"})})))}];p.Th.adsSetupComplete&&ve.pop();const he=e=>{(0,o.getHistory)().replace((0,ue.hP)());let t="dismiss";e&&(t=(e.currentTarget||e.target).dataset.action||t),(0,v.ce)(pe,{context:p.K4.SUBMISSION_SUCCESS,action:t})},ye=()=>{(0,l.useEffect)((()=>{(0,v.ce)("gla_modal_open",{context:p.K4.SUBMISSION_SUCCESS}),ge.A.set(p.rS.CAN_ONBOARDING_SETUP_CES_PROMPT_OPEN,!0)}),[]);const e=(0,l.useCallback)((()=>p.Th.adsSetupComplete?(0,a.createElement)(h.A,{isPrimary:!0,"data-action":"view-product-feed",onClick:he},(0,n.__)("View product feed","google-listings-and-ads")):(0,a.createElement)(a.Fragment,null,(0,a.createElement)("div",{className:"gla-submission-success-guide__space_holder"}),(0,a.createElement)(h.A,{isSecondary:!0,"data-action":"maybe-later",onClick:he},(0,n.__)("Maybe later","google-listings-and-ads")),(0,a.createElement)(de.A,{isPrimary:!0,isSecondary:!1,isSmall:!1,eventName:pe,eventProps:{context:p.K4.SUBMISSION_SUCCESS,action:"create-paid-campaign"}},(0,n.__)("Create campaign","google-listings-and-ads")))),[]);return(0,a.createElement)(re.A,{className:"gla-submission-success-guide",backButtonText:(0,n.__)("Back","google-listings-and-ads"),pages:ve,renderFinish:e,onFinish:he})};var be=s(1787),we=s(614);const fe=()=>{const e={strong:(0,a.createElement)("strong",null)};return(0,a.createElement)(u.A,{id:"product-status"},(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("Your product feed is automatically synced from WooCommerce to Google, every 1-2 days. ","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("‘Not synced’ products do not appear in Google listings. They are queued for submission, or they may be ineligible or excluded from the product feed.","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("After submission, Google assigns each product a status: Active, Expiring, Pending, or Disapproved.","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("‘Active’ products are fully approved and eligible to appear in free listings on Google.","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("‘Expiring’ products will become inactive and no longer appear in Google listings in the next 3 days.","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("‘Pending’ products are being processed by Google. They will not appear in listings until they are approved.","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("‘Disapproved’ products are inactive and do not appear in Google listings.","google-listings-and-ads"),e)),(0,a.createElement)("p",null,(0,l.createInterpolateElement)((0,n.__)("Read more about product sync and statuses","google-listings-and-ads"),{link:(0,a.createElement)(m.A,{context:"product-feed",linkId:"product-sync-statuses",href:"https://support.google.com/merchants/answer/160491"})})))},Ae=({icon:e,title:t,label:s,description:n,className:l})=>(0,a.createElement)(r.Flex,{className:l,justify:"normal",gap:1},(0,a.createElement)(r.FlexItem,null,t),(0,a.createElement)(r.FlexItem,{className:"gla-status__icon"},e),(0,a.createElement)(r.FlexItem,null,(0,a.createElement)("span",{className:"gla-status__label"},s),(0,a.createElement)("span",{className:"gla-status__description"},n)));var ke=s(9369),Se=s(3376);const Ne=function(){const{data:e,hasFinishedResolution:t}=(0,K.A)("getMCProductStatistics");if(!t||!e?.statistics)return null;const{Icon:s,status:l,description:o}=function({scheduled_sync:e,statistics:t,timestamp:s,loading:a}){if(0!==e||a)return{Icon:$.A,status:(0,n.__)("Sync in progress","google-listings-and-ads"),description:null};const l=(0,ke.A)(t);return{Icon:M.A,status:(0,n.__)("Automatically synced to Google","google-listings-and-ads"),description:(0,n.sprintf)( // translators: %s: datetime of last update products sync status, and %d: number of synced products, with minimum value of 1. // translators: %s: datetime of last update products sync status, and %d: number of synced products, with minimum value of 1. (0,n._n)("Last updated: %1$s, containing %2$d product","Last updated: %1$s, containing %2$d products",l,"google-listings-and-ads"),(0,U.format)(Se.Z,new Date(1e3*s)),l)}}(e);return(0,a.createElement)(Ae,{title:(0,n.__)("Sync with Google:","google-listings-and-ads"),icon:(0,a.createElement)(s,{className:"gla-success",size:24}),label:(0,a.createElement)("span",{className:"gla-success"},l),description:o})},Ce=function({refreshStats:e,error:t}){return(0,a.createElement)(Ae,{title:(0,n.__)("Overview Stats:","google-listings-and-ads"),icon:(0,a.createElement)(b.A,{size:24}),label:(0,a.createElement)(h.A,{"aria-label":t,onClick:e,className:"overview-stats-error-button",eventName:"gla_retry_loading_product_stats"},(0,n.__)("There was an error loading the Overview Stats. Click to retry.","google-listings-and-ads")),description:t})},Pe=function(){const{total:e}=(0,T.A)(),t=(s=e,Number.isInteger(s)?0===s?(0,n.__)("No issues to resolve 🎉","google-listings-and-ads"):(0,n.sprintf)( // translators: %d: number of unsolved Merchant Center issues, with minimum value of 1. // translators: %d: number of unsolved Merchant Center issues, with minimum value of 1. (0,n._n)("%d issue to resolve","%d issues to resolve",s,"google-listings-and-ads"),s):"");var s;return(0,a.createElement)(Ae,{title:(0,n.__)("Feed setup:","google-listings-and-ads"),icon:(0,a.createElement)(M.A,{size:24}),label:(0,a.createElement)("span",{className:"gla-success"},(0,n.__)("Free listings setup completed","google-listings-and-ads")),description:t})},Ie=()=>{const e=(0,K.A)("getMCReviewRequest");if(!e.hasFinishedResolution||!e.data?.status)return null;const t=Y[e.data.status];return t?(0,a.createElement)(Ae,{className:"gla-account-status",title:(0,n.__)("Account status:","google-listings-and-ads"),icon:t.icon,label:t.status,description:t.statusDescription}):null};var xe=s(1209);const qe=()=>{const{data:e}=(0,we.A)(),{loaded:t,data:s}=(0,xe.A)();return!e?.statistics?.active||!t||s?.length>0?null:(0,a.createElement)(r.Flex,{className:"gla-ads-inline-notice"},(0,a.createElement)(r.FlexItem,null,(0,a.createElement)("p",null,(0,n.__)("You have approved products. Create a Google Ads campaign to reach more customers and drive more sales.","google-listings-and-ads")),(0,a.createElement)(de.A,{isSmall:!1,eventProps:{context:"product-feed-overview-promotion"}},(0,n.__)("Create Campaign","google-listings-and-ads"))))};var Re=s(3741);const Fe=()=>{const{hasFinishedResolution:e,data:t,refreshStats:s}=(0,we.A)();if(e&&!t)return(0,n.__)("An error occurred while retrieving your product feed. Please try again later.","google-listings-and-ads");const l=!e||t?.loading;let o={};return l&&(o={children:(0,a.createElement)(Re.A,null)}),(0,a.createElement)(r.Card,{className:"gla-product-statistics"},(0,a.createElement)(r.CardHeader,{justify:"normal"},(0,a.createElement)(r.FlexItem,null,(0,a.createElement)(k.A,{variant:"title-small",as:"h2"},(0,n.__)("Overview","google-listings-and-ads"))),(0,a.createElement)(fe,null)),(0,a.createElement)(r.CardBody,{className:"gla-product-statistics__summaries",size:null},!e&&(0,a.createElement)(_.SummaryListPlaceholder,{numberOfItems:5}),e&&(0,a.createElement)(_.SummaryList,null,(()=>[(0,a.createElement)(_.SummaryNumber,{key:"active",label:(0,n.__)("Active","google-listings-and-ads"),value:l?"":t?.statistics?.active,...o}),(0,a.createElement)(_.SummaryNumber,{key:"expiring",label:(0,n.__)("Expiring","google-listings-and-ads"),value:l?"":t?.statistics?.expiring,...o}),(0,a.createElement)(_.SummaryNumber,{key:"pending",label:(0,n.__)("Pending","google-listings-and-ads"),value:l?"":t?.statistics?.pending,...o}),(0,a.createElement)(_.SummaryNumber,{key:"disapproved",label:(0,n.__)("Disapproved","google-listings-and-ads"),value:l?"":t?.statistics?.disapproved,...o}),(0,a.createElement)(_.SummaryNumber,{key:"not_synced",label:(0,n.__)("Not Synced","google-listings-and-ads"),value:l?"":t?.statistics?.not_synced,...o})]))),(0,a.createElement)(r.CardFooter,{gap:0},(0,a.createElement)(qe,null),(0,a.createElement)(Pe,null),(0,a.createElement)(Ne,null),(0,a.createElement)(Ie,null),e&&t?.error&&(0,a.createElement)(Ce,{refreshStats:s,error:t.error})))};var Te=s(3921),Ge=s(5246);const De=()=>{const[e,t]=(0,l.useState)(!1),s=(0,o.getQuery)()?.guide===p.K4.SUBMISSION_SUCCESS,r=(0,Te.A)();return(0,l.useEffect)((()=>{if(!e){const e=ge.A.get(p.rS.CAN_ONBOARDING_SETUP_CES_PROMPT_OPEN);t(!s&&e&&r)}}),[s,e,r]),(0,a.createElement)(a.Fragment,null,(0,a.createElement)(i.A,null),(0,a.createElement)(Ge.A,null),s&&(0,a.createElement)(ye,null),e&&(0,a.createElement)(be.A,{label:(0,n.__)("How easy was it to set up Google for WooCommerce?","google-listings-and-ads"),secondLabel:(0,n.__)("How easy was it to understand the requirements for the Google for WooCommerce setup?","google-listings-and-ads"),eventContext:p.K4.SUBMISSION_SUCCESS}),(0,a.createElement)("div",{className:"gla-product-feed"},(0,a.createElement)(Fe,null),(0,a.createElement)(Q,null),(0,a.createElement)(ie,{trackEventReportId:"product-feed"})))}}}]);PK!h`js/build/reports.cssnu[.app-tab-nav__tabs{align-items:stretch;box-shadow:inset 0 -1px 0 #ccc;display:flex;flex-wrap:wrap;margin-bottom:var(--main-gap)}.app-tab-nav__tabs-item{background:#0000;border:none;border-radius:0;box-shadow:inset 0 -1px 0 #ccc;box-sizing:border-box;cursor:pointer;font-weight:500;height:48px;margin-left:0;padding:3px 16px}.app-tab-nav__tabs-item:after{content:attr(data-label);display:block;height:0;overflow:hidden;speak:none;visibility:hidden}.app-tab-nav__tabs-item:focus:not(:disabled){box-shadow:inset 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) #0000,inset 0 -1.5px 0 0 var(--wp-admin-theme-color);position:relative}.app-tab-nav__tabs-item.is-active:before{border-bottom:1.5px solid #0000;bottom:1px;content:"";left:0;position:absolute;right:0;top:0}.app-tab-nav__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),inset 0 -1.5px 0 0 var(--wp-admin-theme-color)} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-gtin-migration__link{cursor:pointer;text-decoration:underline} .gla-app-text{font-weight:400;margin:0}.gla-app-text--body{font-size:13px;line-height:20px}.gla-app-text--caption,.gla-app-text--label{font-size:12px;line-height:16px}.gla-app-text--label{font-weight:600}.gla-app-text--title-small{font-size:20px;line-height:28px}.gla-app-text--title-medium{font-size:24px;line-height:32px}.gla-app-text--subtitle{font-size:16px;font-weight:600;line-height:24px}.gla-app-text--subtitle-small{font-size:14px;line-height:20px} .gla-reports__metric-label{align-items:center;display:flex;gap:8px}.gla-reports__metric-label .components-tooltip .components-popover__content{max-width:40em;min-width:32em;white-space:normal}.gla-reports__metric-label .components-tooltip .components-popover__content a{color:#9ec2e6}.gla-reports__metric-label .components-tooltip .components-popover__content a:hover{color:#4d8fd1}.gla-reports__metric-label .components-tooltip .components-popover__content a:active{color:#3982cc}.gla-reports__metric-infoicon{align-items:center;display:flex;fill:#949494;cursor:pointer}.gla-reports__metric-infoicon:hover{fill:#616161}.gla-reports__metric-info{margin-bottom:1em}.gla-reports__metric-info:last-child{margin-bottom:0} .gla-tooltip__children-container{cursor:help;display:inline-block}.gla-tooltip__children-container>span{cursor:auto}.gla-admin-page .disabled-element-wrapper:has(.gla-tooltip__children-container){display:inline-block} .app-table-card-div .components-card__header h2{margin-right:8px}.app-table-card-div .components-card__header .woocommerce-table__actions{justify-content:flex-start}.app-table-card-div .components-card__body .woocommerce-table__table .components-base-control__field{margin-bottom:0} .gla-reports__tooltip-campaign-name .gla-tooltip__children-container{width:100%} .gla-sub-nav.subsubsub{float:none;margin:calc(var(--main-gap)*-1/3) 0 var(--main-gap) 0;text-align:left} .gla-rebranding-tour__heading{align-items:center;display:flex;gap:8px;fill:var(--wp-admin-theme-color)}.gla-rebranding-tour .components-card__footer{padding-top:0}.gla-rebranding-tour .components-card__footer>*{display:none} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} PK!rEJJjs/build/reports.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[528],{9531:(e,t,a)=>{a.r(t),a.d(t,{default:()=>Ne});var r=a(1609),o=a(7723),s=a(6087),l=a(6476),n=a(7143),i=a(6520),d=a(7615);function c(e=""){return[...new Set(e.split(",").map((e=>parseInt(e,10))).filter((e=>!isNaN(e))))]}function g(e,t={},a={}){return e.reduce(((e,r)=>(e[r]=(t[r]||0)+(a[r]||0),e)),{})}function p(e,t){if(!t)return e||null;if(!e)return t;const a=new Map(e.map((e=>[e.interval,e.subtotals]))),r=new Map(t.map((e=>[e.interval,e.subtotals])));return[...new Set([...a.keys(),...r.keys()])].sort().map((e=>({interval:e,subtotals:g(d.YK,a.get(e),r.get(e))})))}function u(e,t={},a){const r=e?Object.keys(e):a;return e=e||{},r.reduce(((r,o)=>{let s=d.XQ.NONE,l=e[o];return a&&(a.includes(o)?void 0===t[o]?s=d.XQ.FOR_REQUEST:l=(e[o]||0)+t[o]:s=d.XQ.FOR_METRIC),{...r,[o]:(0,d.jr)(l,void 0,s)}}),{})}var m=a(2775),_=a(3905);const y="programs",f={free_listings:[],campaigns:[],intervals:[],totals:{}},h={loaded:!0,data:{},reportQuery:null};function b(e,t,a){const r=c(t[_.s_]),o=0===r.length||r.includes(_.Q),s=(0===r.length||r.some((e=>e!==_.Q)))&&_.Th.adsSetupComplete;return{free:o&&e(y,"free",t,a)||h,paid:s&&e(y,"paid",t,a)||h}}var v=a(3772),k=a(7419);const E=(0,o.__)("Unavailable","google-listings-and-ads");function S(e){const{formatNumber:t}=(0,v.A)(),{formatAmount:a}=(0,k.A)();return(0,s.useMemo)((()=>{function r(e){return void 0===e?E:this.isCurrency?a(e,!0):t(e,0)}return e.map((e=>({...e,formatFn:r})))}),[e,t,a])}var C=a(2047),A=a(9927),P=a(8468),F=a(7374),M=a(5703);const L=[{id:_.Q,name:(0,o.__)("Free Listings","google-listings-and-ads")}],w=new Set(L.map((e=>e.id)));var T=a(1209),Q=a(2448),N=a(6473);const I=(0,M.getSetting)("locale").siteLocale,x=(()=>{let e,t,a;function r(){e=null,a=new Promise((e=>{t=e})).then((()=>L.concat(e)))}r();const s={name:"programs",options:()=>a,getOptionIdentifier:e=>e.id,getOptionLabel:e=>e.name,getOptionKeywords:e=>[e.name],getOptionCompletion:e=>({key:e.id,label:e.name})};async function l(e){const t=new Set(c(e));let r;return r=function(e,t){if(e.size>t.size)return!1;for(const a of e)if(!t.has(a))return!1;return!0}(t,w)?L:(await a).filter((e=>t.has(e.id))),r.map((e=>({key:e.id,label:e.name})))}const n={label:(0,o.__)("Show","google-listings-and-ads"),staticParams:["period","chartType","paged","per_page","selectedMetric","reportKey","orderby","order"],param:"filter",showFilters:()=>!0,filters:[{label:(0,o.__)("All Google programs","google-listings-and-ads"),value:"all"},{label:(0,o.__)("Single program","google-listings-and-ads"),value:"select_program",subFilters:[{component:"Search",value:"single_program",path:["select_program"],settings:{type:"custom",param:_.s_,getLabels:l,labels:{placeholder:(0,o.__)("Type to search for a program","google-listings-and-ads"),button:(0,o.__)("Single Program","google-listings-and-ads")},autocompleter:s}}]},{label:(0,o.__)("Comparison","google-listings-and-ads"),chartMode:"item-comparison",value:"compare-programs",settings:{type:"custom",param:_.s_,getLabels:l,labels:{helpText:(0,o.__)("Check at least two programs below to compare","google-listings-and-ads"),placeholder:(0,o.__)("Search for programs to compare","google-listings-and-ads"),title:(0,o.__)("Compare Programs","google-listings-and-ads"),update:(0,o.__)("Compare","google-listings-and-ads")},autocompleter:s}}]};return({data:a,loaded:o})=>(o?(e&&e!==a&&r(),e=a,t()):e&&r(),n)})(),D=e=>{const{query:t,trackEventId:a}=e,o=[x((0,T.A)({exclude_removed:!1}))],{period:s,compare:l,before:n,after:i}=(0,F.getDateParamsFromQuery)(t),{primary:d,secondary:c}=(0,F.getCurrentDates)(t),g={period:s,compare:l,before:n,after:i,primaryDate:d,secondaryDate:c},p=(0,v.A)(),u={...t};return(0,r.createElement)(Q.A,{query:u,siteLocale:I,currency:p,filters:o,onDateSelect:e=>(0,N.ce)("gla_datepicker_update",{report:a,...(0,P.omitBy)(e,P.isUndefined)}),onFilterSelect:e=>(0,N.ce)("gla_filter",{report:a,filter:e.filter||"all"}),path:t.path,dateQuery:g,isoDateFormat:F.isoDateFormat})};var O=a(8846),R=a(4275),V=a(9039),q=a(2848);const j="https://merchants.google.com/mc/reporting/dashboard",B=({href:e,selected:t,onLinkClickCallback:a,metric:l,data:{value:n,prevValue:i,delta:c,missingFreeListingsData:g}})=>{const p=(0,s.useMemo)((()=>({value:l.formatFn(n),prevValue:l.formatFn(i)})),[l,n,i]);let u=l.label;const m=[],_=[];if(g!==d.XQ.NONE){const e=(0,o.__)("This data is currently available for Google Ads campaigns only.","google-listings-and-ads");m.push(e),_.push(e)}if(g===d.XQ.FOR_REQUEST){const e=(0,o.__)("Please try again later, or go to to track your performance for Google Free Listings.","google-listings-and-ads");m.push((0,s.createInterpolateElement)(e,{googleMerchantCenterLink:(0,r.createElement)(q.A,{eventName:"gla_google_mc_link_click",eventProps:{context:"reports",href:j},type:"external",target:"_blank",href:j,onClick:e=>e.stopPropagation()},(0,o.__)("Google Merchant Center","google-listings-and-ads"))}));const t=(0,s.createInterpolateElement)(e,{googleMerchantCenterLink:(0,r.createElement)(r.Fragment,null,(0,o.sprintf)( // translators: %s: link to Google Merchant Center. // translators: %s: link to Google Merchant Center. (0,o.__)("Google Merchant Center (%s)","google-listings-and-ads"),j))});_.push((0,s.renderToString)(t))}if(m.length>0){const e=m.map(((e,t)=>(0,r.createElement)("div",{className:"gla-reports__metric-info",key:t},e)));u=(0,r.createElement)("div",{className:"gla-reports__metric-label"},l.label,(0,r.createElement)(V.A,{text:e},(0,r.createElement)(R.A,{className:"gla-reports__metric-infoicon",role:"img","aria-label":_.join(" "),size:16})))}return(0,r.createElement)(O.SummaryNumber,{label:u,href:e,selected:t,delta:c,onLinkClickCallback:a,...p})},K={value:null,preValue:null,delta:null},U=({loaded:e,metrics:t,expectedLength:a=t.length,totals:o,trackEventId:s})=>{const n=(0,m.A)();if(!e)return(0,r.createElement)(O.SummaryListPlaceholder,{numberOfItems:a});const{selectedMetric:i=t[0].key}=n;return(0,r.createElement)(O.SummaryList,null,(()=>t.map((e=>{const{key:t}=e,a=i===t,n=(0,l.getNewPath)({selectedMetric:t});return(0,r.createElement)(B,{key:t,metric:e,href:n,selected:a,data:o[t]||K,onLinkClickCallback:()=>{return e=t,void(0,N.ce)("gla_chart_tab_click",{report:s,context:e});var e}})}))))},G=(0,o.__)("No data for the selected date range","google-listings-and-ads");function z({metrics:e,loaded:t,intervals:a}){const o=(0,m.A)(),l=(0,v.A)(),{selectedMetric:n}=o;let i={};e.length&&(i=n&&e.find((e=>e.key===n))||e[0]);const{key:d,label:c,isCurrency:g=!1,formatFn:p}=i,u={...l,symbol:""},_=(0,F.getChartTypeForQuery)(o),y=g?"currency":"number",f=p.bind(i),h=(0,s.useMemo)((()=>t?a.map((({interval:e,subtotals:t})=>({date:e,[c]:{value:t[d],label:c}}))):[]),[d,c,t,a]);return(0,r.createElement)(O.Chart,{data:h,title:c,query:o,currency:u,chartType:_,valueType:y,tooltipValueFormat:f,isRequesting:!t,emptyMessage:G,layout:"time-comparison",legendPosition:"hidden"})}var X=a(6427),H=a(7892),Y=a(8237);const $=({compareBy:e,compareParam:t,metrics:a,isLoading:n,compareButonTitle:i,data:d,nameHeader:g,nameCell:p,...u})=>{const _=(0,m.A)(),[y,f]=(0,s.useState)((()=>new Set(c(_[e])))),h=d.length||5,b=(0,s.useMemo)((()=>{if(!a.length)return[];const e=a.map((e=>({...e,isSortable:!0,isNumeric:!0})));return e[0].defaultSort=!0,e[0].defaultOrder="desc",e}),[a]),v=e=>a.map((t=>{const a=e.subtotals[t.key];return{display:t.formatFn(a)}})),k=(e,t)=>{t?f(new Set([...y,e])):(y.delete(e),f(new Set(y)))};return(0,r.createElement)(Y.A,{actions:(0,r.createElement)(H.A,{isSecondary:!0,disabled:n||y.size<=1,title:i,onClick:()=>{const a=Array.from(y).join(",");(0,l.onQueryChange)("compare")(e,t,a)}},(0,o.__)("Compare","google-listings-and-ads")),isLoading:n,headers:(E=d,[{key:"compare",label:(0,r.createElement)(X.CheckboxControl,{disabled:n,checked:!n&&E.length&&y.size===E.length,onChange:e=>{if(e){const e=d.map((e=>e.id));f(new Set(e))}else f(new Set)}}),required:!0},{key:"title",label:g,isLeftAligned:!0,required:!0},...b]),rows:(e=>e.map((e=>[{display:(0,r.createElement)(X.CheckboxControl,{checked:y.has(e.id),onChange:k.bind(null,e.id)})},{display:p(e)},...v(e)])))(d),totalRows:d.length,rowsPerPage:h,query:_,compareBy:e,compareParam:t,onQueryChange:l.onQueryChange,onSort:(0,l.onQueryChange)("sort"),...u});var E},J=({isConverted:e,name:t})=>e?(0,r.createElement)("div",{className:"gla-reports__tooltip-campaign-name"},(0,r.createElement)(V.A,{placement:"top-start",text:(0,o.__)("This campaign has been upgraded to Performance Max","google-listings-and-ads")},t)):t,W=({isLoading:e,orderby:t,order:a,metrics:l,freeListings:n,campaigns:i,...d})=>{const c=(0,s.useMemo)((()=>{if(e)return[];if(!n||0===n.length)return i;const r=[{...n[0],name:(0,o.__)("Free Listings","google-listings-and-ads"),id:_.Q},...i];return i.length&&(r.sort(((e,a)=>(e.subtotals[t]||Number.NEGATIVE_INFINITY)-(a.subtotals[t]||Number.NEGATIVE_INFINITY))),"desc"===a&&r.reverse()),r}),[e,n,i,t,a]);return(0,r.createElement)($,{title:(0,o.__)("Programs","google-listings-and-ads"),compareButonTitle:(0,o.__)("Select one or more programs to compare","google-listings-and-ads"),nameHeader:(0,o.__)("Program","google-listings-and-ads"),nameCell:J,compareBy:"programs",compareParam:"filter",metrics:l,isLoading:e,data:c,...d})};var Z=a(1670),ee=a(9269);const te=[{key:"programs",title:(0,o.__)("Programs","google-listings-and-ads"),href:(0,l.getNewPath)({reportKey:"programs"},"/google/reports",{})},{key:"products",title:(0,o.__)("Products","google-listings-and-ads"),href:(0,l.getNewPath)({reportKey:"products"},"/google/reports",{})}],ae=()=>{const e=(0,ee.A)();return(0,r.createElement)(Z.A,{tabs:te,selectedKey:e})};var re=a(5246);const oe=[{key:"sales",label:(0,o.__)("Total Sales","google-listings-and-ads"),isCurrency:!0},{key:"conversions",label:(0,o.__)("Conversions","google-listings-and-ads")},{key:"clicks",label:(0,o.__)("Clicks","google-listings-and-ads")},{key:"impressions",label:(0,o.__)("Impressions","google-listings-and-ads")}],se=[...oe,{key:"spend",label:(0,o.__)("Total Spend","google-listings-and-ads"),isCurrency:!0}],le=[...oe,{key:"spend",label:(0,o.__)("Spend","google-listings-and-ads"),isCurrency:!0}],ne=()=>{const e="reports-programs",{loaded:t,data:{totals:a,intervals:o,freeListings:c,campaigns:g},reportQuery:{fields:_,orderby:y,order:h}}=function(){const e=(0,m.A)(),{paid:t,free:a}=(0,n.useSelect)((t=>{const{getReport:a}=t(i.Ui);return b(a,e,"primary")}),[e]),r=t.loaded&&a.loaded,o=t.reportQuery||a.reportQuery,l=a.reportQuery?.fields,d=(0,s.useMemo)((()=>{const e=a.data,o=t.data;return r&&o&&e?{freeListings:e.free_listings||f.free_listings,campaigns:o.campaigns||f.campaigns,intervals:p(o.intervals,e.intervals)||f.intervals,totals:u(o.totals,e.totals,l)}:f}),[r,t.data,a.data,l]);return{loaded:r,reportQuery:o,data:d}}(),v=(0,s.useMemo)((()=>{const e=t&&Object.keys(a).length>0;return{available:e?se.filter((({key:e})=>a.hasOwnProperty(e))):se.filter((({key:e})=>_.includes(e))),expected:e?le.filter((({key:e})=>a.hasOwnProperty(e))):le.filter((({key:e})=>_.includes(e)))}}),[t,a,_]),k=S(v.available),E=S(v.expected),{loaded:P,data:F}=function(e){const t=(0,m.A)(),{loaded:a,data:r}=function(e){const{paid:t,free:a}=(0,n.useSelect)((t=>{const{getReport:a}=t(i.Ui);return b(a,e,"secondary")}),[e]),r=t.loaded&&a.loaded,o=a.reportQuery?.fields,l=(0,s.useMemo)((()=>{const e=a.data,s=t.data;return r&&s&&e?u(s.totals,e.totals,o):f.totals}),[r,t.data,a.data,o]);return{loaded:r,data:l}}(t),o=(0,s.useMemo)((()=>a?function(e,t){return Object.keys(e).reduce(((a,r)=>({...a,[r]:(0,d.jr)(e[r].value,t[r]?.value,e[r].missingFreeListingsData)})),{})}(e,r):e),[a,r,e]);return{loaded:a,data:o}}(a),M=P?F:a;return(0,r.createElement)(r.Fragment,null,(0,r.createElement)(C.A,{context:e}),(0,r.createElement)(A.A,null),(0,r.createElement)(re.A,null),(0,r.createElement)(ae,null),(0,r.createElement)(D,{query:(0,l.getQuery)(),trackEventId:e}),(0,r.createElement)(U,{loaded:t,metrics:k,expectedLength:se.length,totals:M,trackEventId:e}),(0,r.createElement)(z,{metrics:k,loaded:t,intervals:o}),(0,r.createElement)(W,{trackEventReportId:e,isLoading:!t,orderby:y,order:h,metrics:E,freeListings:c,campaigns:g}))},ie="products",de={products:[],intervals:[],totals:{}};var ce=a(3741),ge=a(314),pe=a(2619),ue=a(3832),me=a(1455),_e=a.n(me);function ye(e,t=P.identity){return function(a="",r){const o="function"==typeof e?e(r):e,s=(0,l.getIdsFromQuery)(a);if(s.length<1)return Promise.resolve([]);const n={include:s.join(","),per_page:s.length};return _e()({path:(0,ue.addQueryArgs)(o,n)}).then((e=>e.map(t)))}}const fe=ye(ge.NAMESPACE+"/products",(e=>({key:e.id,label:e.name,type:e.type})));function he({attributes:e,name:t}){const a=(0,M.getSetting)("admin")?.variationTitleAttributesSeparator||" - ";if(t.indexOf(a)>-1)return t;const r=e.map((({option:e})=>e)).join(", ");return r?t+a+r:t}const be=ye((({products:e})=>e?ge.NAMESPACE+`/products/${e}/variations`:ge.NAMESPACE+"/variations"),(e=>({key:e.id,label:he(e)}))),ve={label:(0,o.__)("Show","google-listings-and-ads"),staticParams:[_.DA,"chartType","orderby","order","paged","per_page","selectedMetric","reportKey"],param:"filter",showFilters:()=>!0,filters:[{label:(0,o.__)("All Products","google-listings-and-ads"),value:"all"},{label:(0,o.__)("Single Product","google-listings-and-ads"),value:"select_product",chartMode:"item-comparison",subFilters:[{component:"Search",value:"single-product",chartMode:"item-comparison",path:["select_product"],settings:{type:"products",param:"products",getLabels:fe,labels:{placeholder:(0,o.__)("Type to search for a product","google-listings-and-ads"),button:(0,o.__)("Single Product","google-listings-and-ads")}}}]},{label:(0,o.__)("Comparison","google-listings-and-ads"),value:"compare-products",chartMode:"item-comparison",settings:{type:"products",param:"products",getLabels:fe,labels:{helpText:(0,o.__)("Check at least two products below to compare","google-listings-and-ads"),placeholder:(0,o.__)("Search for products to compare","google-listings-and-ads"),title:(0,o.__)("Compare Products","google-listings-and-ads"),update:(0,o.__)("Compare","google-listings-and-ads")}}}]},ke={showFilters:e=>"single-product"===e.filter&&!!e.products&&e["is-variable"],staticParams:["filter","products","chartType","orderby","order","paged","per_page","selectedMetric","reportKey"],param:"filter-variations",filters:[{label:(0,o.__)("All Variations","google-listings-and-ads"),chartMode:"item-comparison",value:"all"},{label:(0,o.__)("Single Variation","google-listings-and-ads"),value:"select_variation",subFilters:[{component:"Search",value:"single-variation",path:["select_variation"],settings:{type:"variations",param:"variations",getLabels:be,labels:{placeholder:(0,o.__)("Type to search for a variation","google-listings-and-ads"),button:(0,o.__)("Single Variation","google-listings-and-ads")}}}]},{label:(0,o.__)("Comparison","google-listings-and-ads"),chartMode:"item-comparison",value:"compare-variations",settings:{type:"variations",param:"variations",getLabels:be,labels:{helpText:(0,o.__)("Check at least two variations below to compare","google-listings-and-ads"),placeholder:(0,o.__)("Search for variations to compare","google-listings-and-ads"),title:(0,o.__)("Compare Variations","google-listings-and-ads"),update:(0,o.__)("Compare","google-listings-and-ads")}}}]},Ee={label:(0,o.__)("Show data from","google-listings-and-ads"),param:_.DA,staticParams:["filter","products","orderby","order","chartType","selectedMetric","reportKey"],defaultValue:_.r6,filters:[{value:_.Mx,label:(0,o.__)("Ad campaigns","google-listings-and-ads")},{value:_.k1,label:(0,o.__)("Free listings","google-listings-and-ads")}],showFilters:({hasPaidSource:e})=>e},Se=(0,pe.applyFilters)("gla_products_report_filters",[ve,ke,Ee]),Ce=(0,pe.applyFilters)("gla_products_report_advanced_filters",{}),Ae=(0,M.getSetting)("currency"),Pe=(0,M.getSetting)("locale").siteLocale,Fe=e=>{const{hasPaidSource:t,query:a,trackEventId:o}=e,{period:s,compare:l,before:i,after:d}=(0,F.getDateParamsFromQuery)(a),{primary:c,secondary:g}=(0,F.getCurrentDates)(a),p={period:s,compare:l,before:i,after:d,primaryDate:c,secondaryDate:g},u=(0,n.useSelect)((e=>{if(a.search||!a.products||1!==a.products.split(",").length)return!1;const t=parseInt(a.products,10),r={include:t},{getItems:o}=e(ge.ITEMS_STORE_NAME),s=o("products",r);return s&&s.get(t)&&"variable"===s.get(t).type}),[a.search,a.products]),m={...a,"is-variable":u,hasPaidSource:t};return(0,r.createElement)(O.ReportFilters,{query:m,siteLocale:Pe,currency:Ae,filters:Se,advancedFilters:Ce,onDateSelect:e=>(0,N.ce)("gla_datepicker_update",{report:o,...(0,P.omitBy)(e,P.isUndefined)}),onFilterSelect:e=>(0,N.ce)("gla_filter",{report:o,filter:e.filter||"all",filterVariations:e["filter-variations"]}),dateQuery:p,isoDateFormat:F.isoDateFormat})},Me=({metrics:e,isLoading:t,products:a,...s})=>(0,r.createElement)($,{title:(0,o.__)("Products","google-listings-and-ads"),compareButonTitle:(0,o.__)("Select one or more products to compare","google-listings-and-ads"),nameHeader:(0,o.__)("Product title","google-listings-and-ads"),nameCell:e=>e.name,compareBy:"products",compareParam:"filter",metrics:e,isLoading:t,data:a,...s}),Le=[{key:"clicks",label:(0,o.__)("Clicks","google-listings-and-ads")},{key:"impressions",label:(0,o.__)("Impressions","google-listings-and-ads")}],we=[{key:"sales",label:(0,o.__)("Total Sales","google-listings-and-ads"),isCurrency:!0},{key:"conversions",label:(0,o.__)("Conversions","google-listings-and-ads")},...Le,{key:"spend",label:(0,o.__)("Spend","google-listings-and-ads"),isCurrency:!0}],Te=({hasPaidSource:e})=>{const t="reports-products",a=(0,l.getQuery)(),o=e?a[_.DA]||_.r6:_.k1,s=S(o===_.Mx?we:Le),{loaded:c,data:{totals:g,intervals:p,products:u}}=function(e){const t=(0,m.A)();return(0,n.useSelect)((a=>{const{getReport:r}=a(i.Ui),o=r(ie,e,t,"primary"),s=r(ie,e,t,"secondary"),l=o.loaded&&s.loaded;let n=de;return l&&o.data&&s.data&&(n={products:o.data.products||de.products,intervals:o.data.intervals||de.intervals,totals:(0,d.bM)(o.data.totals,s.data.totals,o.reportQuery.fields)}),{data:n,loaded:l}}),[e,t])}(o);return(0,r.createElement)(r.Fragment,null,(0,r.createElement)(Fe,{hasPaidSource:e,query:a,trackEventId:t}),(0,r.createElement)(U,{metrics:s,loaded:c,totals:g,trackEventId:t}),(0,r.createElement)(z,{metrics:s,loaded:c,intervals:p}),(0,r.createElement)(Me,{trackEventReportId:t,metrics:s,isLoading:!c,products:u}))},Qe=()=>{const{loaded:e,data:t}=(0,T.A)(),a=e&&t.some((({status:e})=>"enabled"===e));return(0,r.createElement)(r.Fragment,null,(0,r.createElement)(C.A,{context:"reports-products"}),(0,r.createElement)(A.A,null),(0,r.createElement)(re.A,null),(0,r.createElement)(ae,null),e?(0,r.createElement)(Te,{hasPaidSource:a}):(0,r.createElement)(ce.A,null))},Ne=()=>"products"===(0,ee.A)()?(0,r.createElement)(Qe,null):(0,r.createElement)(ne,null)}}]);PK! ,,js/build/settings.cssnu[.gla-section-card-body{padding:var(--large-gap)} .gla-section-card-footer{padding:calc(var(--large-gap)/2) var(--large-gap)}.gla-section-card-footer[hidden]{display:none} .gla-subsection-title{font-size:14px;font-style:normal;font-weight:600;letter-spacing:0;line-height:20px;margin-bottom:8px;position:relative;text-align:left} .gla-subsection-body{font-size:13px;font-style:normal;font-weight:400;line-height:16px} .gla-subsection-helper-text{font-size:12px;font-style:italic;font-weight:400;letter-spacing:0;line-height:16px} .gla-subsection-subtitle{color:#757575;font-size:12px;line-height:16px;margin-bottom:4px} .gla-subsection:not(:first-child){margin-top:var(--main-gap)} .gla-section-card-title{margin-bottom:calc(var(--main-gap)/3*2)} .gla-section{display:flex;flex-direction:column;margin-bottom:var(--large-gap)}.gla-section--is-disabled,.gla-section--is-disabled-left .gla-section__header{opacity:.5}@media(min-width:600px){.gla-section{flex-direction:row;gap:var(--main-gap)}.gla-section__header{padding-top:var(--main-gap);width:33%}}.gla-section__header h1{font-size:16px;font-style:normal;font-weight:600;margin-bottom:8px;padding:0}.gla-section__header p{line-height:16px;margin:0 0 8px}.gla-section .gla-section__body{flex:1} .gla-account-card{line-height:16px}.gla-account-card--is-disabled{opacity:.5}.gla-account-card--is-expanded-detail .gla-account-card__indicator{grid-area:1/3}.gla-account-card--is-expanded-detail .gla-account-card__detail{grid-area:2/2/auto/span 2}.gla-account-card__styled--align-top{align-self:flex-start}.gla-account-card__body-layout{align-items:center;display:grid;grid-template-columns:auto 1fr auto}.gla-account-card__icon{grid-area:1/1/span 2;line-height:0;margin-right:16px}.gla-account-card__subject{grid-area:1/2}.gla-account-card__indicator{grid-area:1/3/span 2;margin-left:16px}.gla-account-card__indicator--align-to-detail{grid-area:2/3;margin-top:12px}.gla-account-card__detail{grid-area:2/2;margin-top:12px}.gla-account-card__actions{grid-area:3/2/auto/span 2;margin-top:12px}.gla-account-card__title{color:#000;margin:0}.gla-account-card__description{align-items:flex-start;color:#1e1e1e;display:flex;flex-direction:column;gap:1em;margin-top:4px}.gla-account-card__description>p{margin:0}.gla-account-card__helper{color:#757575;font-size:12px;font-style:italic;margin-top:4px}.gla-account-card .components-card__footer{padding:calc(var(--main-gap)/3*2) var(--main-gap)}.gla-account-card .components-card__footer>.components-button.is-link{min-height:36px;padding:6px 12px}.gla-account-card .components-notice.is-error{background-color:#f8ebea;margin:0}.gla-account-card .components-notice.is-success{background-color:#edfaef;border:0;font-size:12px;margin:0 var(--large-gap) var(--main-gap);padding:16px}@media(max-width:600px){.gla-account-card__body-layout{align-items:flex-start;display:flex;flex-direction:column}.gla-account-card__body-layout>div{margin:8px}} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .gla-validation-errors{color:#cc1818;font-size:12px;line-height:16px;margin:0;width:100%}.gla-validation-errors:not(:first-child){margin-top:16px}.gla-validation-errors:not(:last-child){margin-bottom:16px}.gla-validation-errors>li{list-style:disc inside;margin:0;padding-left:.5em}.gla-validation-errors>li:only-child{list-style:none;padding-left:0} .gla-contact-info-preview-card .gla-subsection-title{align-items:center;display:flex}.gla-contact-info-preview-card__notice-icon{fill:#cc1818;margin:calc(var(--main-gap)/-8) 0}.gla-contact-info-preview-card__notice-details{color:#757575}.gla-contact-info-preview-card__placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:#0000;display:inline-block;width:18em}.gla-contact-info-preview-card__placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.gla-contact-info-preview-card__placeholder{animation:none}} .gla-store-address-card .gla-account-card__indicator .has-icon svg{margin-left:4px}.gla-store-address-card .gla-account-card__description{color:#757575} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .gla-radio-helper-text{color:#757575;font-size:12px;font-style:italic;font-weight:400;line-height:16px} .app-radio-content-control{display:grid;gap:8px;column-gap:10px;grid-template-columns:[input-start] auto [text-start] 1fr}.app-radio-content-control .components-base-control__field,.app-radio-content-control .components-base-control__field .components-flex,.app-radio-content-control .components-base-control__label,.app-radio-content-control .components-radio-control,.app-radio-content-control .components-radio-control .components-flex,.app-radio-content-control .components-radio-control__option{display:contents}.app-radio-content-control .components-radio-control__input[type=radio]{margin:0}.app-radio-content-control .app-radio-content-control__content{grid-column:text-start}.app-radio-content-control .app-radio-content-control__content:empty{display:none} .gla-vertical-gap-layout{display:flex;flex-direction:column;gap:calc(var(--main-gap)/2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__medium{gap:calc(var(--main-gap)/3*2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__large{gap:var(--main-gap)}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap{gap:0}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap>:not(:first-child){margin-top:-1px} .gla-connected-icon-label{fill:currentcolor;color:#23a713}.gla-connected-icon-label svg{display:block} .gla-authorize-google-account-card__error-text{color:#cc1818;font-weight:500} .gla-ads-claim-account-notice{background-color:#ffeec1;font-size:12px;margin:0 var(--large-gap) var(--large-gap);padding:16px} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-ads-terms-modal{max-width:600px}.gla-ads-terms-modal .main{font-weight:700} .gla-content-button-layout{align-items:center;display:flex;gap:calc(var(--main-gap)/2);justify-content:space-between} .gla-loading-label{align-items:center;color:var(--wp-admin-theme-color);display:inline-flex;gap:8px;height:36px}.gla-loading-label .woocommerce-spinner{height:24px;min-width:24px;width:24px}.gla-loading-label .woocommerce-spinner__circle{stroke:currentcolor;stroke-width:8px} .app-select-control .components-base-control__field{margin-bottom:0}.app-select-control.app-select-control--is-non-interactive{pointer-events:none} .gla-connect-ads .app-select-control{flex-grow:1}.gla-connect-ads .gla-subsection-body{margin-bottom:8px} .gla-reclaim-url-card .gla-content-button-layout{margin:16px 0}.gla-reclaim-url-card .components-notice.is-error{margin-top:16px} .app-input-control .components-flex-item{margin-right:0;max-width:100%;width:100%}.app-input-control .components-flex-item label.components-input-control__label{color:#757575!important;white-space:normal!important}.app-input-control--no-pointer-events{pointer-events:none}.app-input-control__character-count{color:#757575;font-size:12px;line-height:16px;margin-top:2px;text-align:right}.app-input-control--error-character-count .components-input-control .components-input-control__container .components-input-control__backdrop,.app-input-control.has-error .components-input-control__backdrop{border-color:#cc1818;box-shadow:none}.app-input-control--error-character-count .app-input-control__character-count,.app-input-control.has-error .components-base-control__help{color:#cc1818} .app-input-link-control{width:100%}.app-input-link-control svg{display:block;margin:6px 0 6px 6px;fill:currentcolor} .gla-switch-url-card .gla-content-button-layout{margin:16px 0} .gla-warning-icon.gridicon.gridicons-notice-outline{fill:#f0b849} .gla-mc-warning-modal{max-width:600px}.gla-mc-warning-modal__warning-text{display:flex;font-weight:700;gap:8px}.gla-mc-warning-modal__warning-text svg{flex-shrink:0} .gla-mc-terms-modal{max-width:600px}.gla-mc-terms-modal .main{font-weight:700} .app-notice{border:0;font-size:12px;margin:0 var(--large-gap) var(--main-gap);padding:16px} @media(min-width:600px){.gla-disconnect-accounts-modal{max-width:600px}}.gla-disconnect-accounts-modal .components-modal__header-heading{align-items:center;display:flex}.gla-disconnect-accounts-modal .gridicon.gridicons-notice-outline{margin-right:8px;fill:#cc1818}.gla-disconnect-accounts-modal .components-base-control{margin:var(--main-gap) 0 var(--large-gap)} .gla-wpcom-connection-lost-card.gla-account-card{background-color:#facfd2;border-radius:0}.gla-wpcom-connection-lost-card .gla-account-card__helper{color:#000} .gla-stepper-top-bar{align-items:center;background-color:#fff;box-shadow:inset 0 -1px 0 0 #ccc;display:flex;min-height:64px}.gla-stepper-top-bar .components-button{align-self:stretch;height:auto}.gla-stepper-top-bar__back-button{padding:0 calc(var(--main-gap)/2)}.gla-stepper-top-bar__title{flex:1;font-size:16px;letter-spacing:0} .AnC9WXFuKgCBURYIRcRY{display:flex;flex-direction:column;gap:4px;justify-content:center} .app-tab-nav__tabs{align-items:stretch;box-shadow:inset 0 -1px 0 #ccc;display:flex;flex-wrap:wrap;margin-bottom:var(--main-gap)}.app-tab-nav__tabs-item{background:#0000;border:none;border-radius:0;box-shadow:inset 0 -1px 0 #ccc;box-sizing:border-box;cursor:pointer;font-weight:500;height:48px;margin-left:0;padding:3px 16px}.app-tab-nav__tabs-item:after{content:attr(data-label);display:block;height:0;overflow:hidden;speak:none;visibility:hidden}.app-tab-nav__tabs-item:focus:not(:disabled){box-shadow:inset 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) #0000,inset 0 -1.5px 0 0 var(--wp-admin-theme-color);position:relative}.app-tab-nav__tabs-item.is-active:before{border-bottom:1.5px solid #0000;bottom:1px;content:"";left:0;position:absolute;right:0;top:0}.app-tab-nav__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),inset 0 -1.5px 0 0 var(--wp-admin-theme-color)} .gla-gtin-migration__link{cursor:pointer;text-decoration:underline} .gla-rebranding-tour__heading{align-items:center;display:flex;gap:8px;fill:var(--wp-admin-theme-color)}.gla-rebranding-tour .components-card__footer{padding-top:0}.gla-rebranding-tour .components-card__footer>*{display:none} .gla-settings .gla-section{margin-left:auto;margin-right:auto;max-width:780px}.gla-settings .gla-section:first-child{margin-top:var(--large-gap)}@media(min-width:600px){.gla-settings .gla-section header{max-width:244px}}.gla-settings .gla-subsection-helper-text{color:#949494;margin-top:8px} PK!{n.d(t,{NS:()=>u,RO:()=>g,Me:()=>m,Ay:()=>A});var o=n(1609),a=n(7723),l=n(6427),s=n(6087),c=n(9457),i=n(7892),r=n(7792),d=n(3658);const g="all-accounts",u="ads-account",m="api-data-fetch-feature",_={[g]:{title:(0,a.__)("Disconnect all accounts","google-listings-and-ads"),confirmButton:(0,a.__)("Disconnect all accounts","google-listings-and-ads"),confirmation:(0,a.__)("Yes, I want to disconnect all my accounts.","google-listings-and-ads"),contents:[(0,a.__)("I understand that I am disconnecting any WordPress.com account, Google account, Google Merchant Center account and Google Ads account connected to this extension.","google-listings-and-ads"),(0,a.__)("Any active product listings will continue to show on Google. They can be managed, edited, or deleted manually from Google Merchant Center (merchants.google.com).","google-listings-and-ads"),(0,a.__)("Any ongoing campaigns will continue to run. They can be managed, edited, or deleted manually from Google Ads (ads.google.com).","google-listings-and-ads")]},[u]:{title:(0,a.__)("Disconnect Google Ads account","google-listings-and-ads"),confirmButton:(0,a.__)("Disconnect Google Ads Account","google-listings-and-ads"),confirmation:(0,a.__)("Yes, I want to disconnect my Google Ads account.","google-listings-and-ads"),contents:[(0,a.__)("I understand that I am disconnecting my Google Ads account from this WooCommerce extension.","google-listings-and-ads"),(0,a.__)("Any ongoing campaigns will continue to run. They can be managed, edited, or deleted manually from Google Ads (ads.google.com).","google-listings-and-ads"),(0,a.__)("Some configurations for Google Ads created through WooCommerce may be lost. This cannot be undone.","google-listings-and-ads")]},[m]:{title:(0,a.__)("Disable data fetching","google-listings-and-ads"),confirmButton:(0,a.__)("Disable data fetching","google-listings-and-ads"),confirmation:(0,a.__)("Yes, I want to disable the data fetching feature.","google-listings-and-ads"),contents:[(0,a.__)("I understand that I am disabling the data fetching feature from this WooCommerce extension.","google-listings-and-ads"),(0,a.__)("Any ongoing campaigns and configuration will continue to run. They will be pushed to Google as in the previous versions of this extension.","google-listings-and-ads")]}};function h({disconnectTarget:e,onRequestClose:t,onDisconnected:n,disconnectAction:u}){const[m,h]=(0,s.useState)(!1),[A,E]=(0,s.useState)(!1),y=(0,d.j)(),{title:f,confirmButton:p,confirmation:b,contents:C}=_[e],x=()=>{A||t()};return(0,o.createElement)(c.A,{className:"gla-disconnect-accounts-modal",title:(0,o.createElement)(o.Fragment,null,(0,o.createElement)(r.A,{size:20}),f),isDismissible:!A,buttons:[(0,o.createElement)(i.A,{key:"1",isSecondary:!0,disabled:A,onClick:x},(0,a.__)("Never mind","google-listings-and-ads")),(0,o.createElement)(i.A,{key:"2",isPrimary:!0,isDestructive:!0,loading:A,disabled:!m,onClick:()=>{let o=e===g?y.disconnectAllAccounts:y.disconnectGoogleAdsAccount;u&&(o=u),E(!0),o().then((()=>{n(),t()})).catch((()=>{E(!1)}))}},p)],onRequestClose:x},C.map(((e,t)=>(0,o.createElement)("p",{key:t},e))),(0,o.createElement)(l.CheckboxControl,{label:b,checked:m,disabled:A,onChange:h}))}function A(e){return(0,o.createElement)(h,{...e})}},2625:(e,t,n)=>{n.r(t),n.d(t,{default:()=>re});var o=n(1609),a=n(6087),l=n(6476),s=n(3905),c=n(14),i=n(8e3),r=n(1016),d=n(3666),g=n(3354),u=n(7723),m=n(6427),_=n(8242),h=n(3741),A=n(7892),E=n(6960),y=n(8864),f=n(8683),p=n(1177),b=n(850);const C=({children:e})=>{const{getInputProps:t,adapter:{isSubmitting:n}}=(0,E.h5)();return(0,o.createElement)(_.A,{title:(0,u.__)("Tax rate (required for U.S. only)","google-listings-and-ads"),description:(0,o.createElement)("div",null,(0,o.createElement)("p",null,(0,u.__)("This tax rate will be shown to potential customers, together with the cost of your product.","google-listings-and-ads")),(0,o.createElement)("p",null,(0,o.createElement)(p.A,{context:"setup-mc-tax-rate",linkId:"tax-rate-read-more",href:"https://support.google.com/merchants/answer/160162"},(0,u.__)("Read more","google-listings-and-ads"))))},(0,o.createElement)(_.A.Card,null,(0,o.createElement)(_.A.Card.Body,null,(0,o.createElement)(b.A,{size:"large"},(0,o.createElement)(f.A,{...t("tax_rate"),label:(0,u.__)("My store uses destination-based tax rates.","google-listings-and-ads"),value:"destination",collapsible:!0,disabled:n},(0,o.createElement)(y.A,null,(0,u.__)("Google’s estimated tax rates will automatically be applied to my product listings.","google-listings-and-ads"))),(0,o.createElement)(f.A,{...t("tax_rate"),label:(0,u.__)("My store does not use destination-based tax rates.","google-listings-and-ads"),value:"manual",collapsible:!0,disabled:n},(0,o.createElement)(y.A,null,(0,a.createInterpolateElement)((0,u.__)("I’ll set my tax rates up manually in Google Merchant Center. I understand that if I don’t set this up, my products will be disapproved.","google-listings-and-ads"),{link:(0,o.createElement)(p.A,{context:"setup-mc-tax-rate",linkId:"tax-rate-manual",href:"https://www.google.com/retail/solutions/merchant-center/"})})))))),e)};var x=n(873),v=n(7337);var w=n(5847),G=n(5640),k=n(8998);const S=new Set(["destination","manual"]);function D(){const{settings:e,saveSettings:t,syncSettings:n}=(0,x.A)(),{data:a}=(0,w.A)(),l=((e=null)=>{const{code:t}=(0,v.A)();return"US"===t||!(!e||!e.includes("US"))||(!t||null===e)&&null})(a),{createNotice:s}=(0,G.A)();return l&&e?.hasOwnProperty("tax_rate")?(0,o.createElement)(E.Ay,{initialValues:{tax_rate:e.tax_rate},validate:e=>{const t={};return S.has(e.tax_rate)||(t.tax_rate=(0,u.__)("Please specify tax rate option.","google-listings-and-ads")),t},onSubmit:async o=>{const a={...e,tax_rate:o.tax_rate};return t(a).then(n,(e=>{(0,k.h)(e,(0,u.__)("There was an error saving tax rate.","google-listings-and-ads"))})).catch((e=>{(0,k.h)(e,(0,u.__)("There was an error synchronizing tax rate to Google Merchant Center.","google-listings-and-ads"))})).then((()=>{s("success",(0,u.__)("Your change to tax rate has been saved and will be synced to your Google Merchant Center.","google-listings-and-ads"))}))}},(t=>{const{values:n,isValidForm:a}=t,l=n.tax_rate,s=!a||l===e.tax_rate;return(0,o.createElement)(C,null,(0,o.createElement)(m.Flex,{justify:"flex-end"},(0,o.createElement)(A.A,{isPrimary:!0,disabled:s,loading:t.adapter.isSubmitting,onClick:t.handleSubmit},(0,u.__)("Save tax rate","google-listings-and-ads"))))})):!1===l?null:(0,o.createElement)(_.A,null,(0,o.createElement)(h.A,null))}var I=n(1968),T=n(7401),N=n(7916),M=n(1378),P=n(6028),F=n(4790),O=n(8678),W=n(458),j=n(1666);function R(e){return(0,o.createElement)(_.A,{title:(0,u.__)("Linked accounts","google-listings-and-ads"),description:(0,u.__)("A WordPress.com account, Google account, Google Merchant Center account, and Google Ads account are required to use this extension in WooCommerce.","google-listings-and-ads"),...e})}var q=n(4876),B=n(6473);const{CONNECTED:z,INCOMPLETE:L}=s.Wn;function H(){const e=(0,I.A)(),{jetpack:t}=(0,T.A)(),{google:n}=(0,i.A)(),{googleMCAccount:l}=(0,N.A)(),{googleAdsAccount:s}=(0,M.A)(),c=!(t&&n&&l&&s),r=[z,L].includes(s?.status),[g,h]=(0,a.useState)(null);return(0,o.createElement)(R,null,g&&(0,o.createElement)(q.Ay,{onRequestClose:()=>h(null),onDisconnected:()=>{(0,B.Ff)("gla_disconnected_accounts",{context:g});const t=g===q.RO?e+(0,d.XG)():window.location.href;window.location.href=t},disconnectTarget:g}),c?(0,o.createElement)(P.A,null):(0,o.createElement)(o.Fragment,null,(0,o.createElement)(F.LJ,{jetpack:t}),(0,o.createElement)(O.Az,{googleAccount:n,hideAccountSwitch:!0}),(0,o.createElement)(j.D,{googleMCAccount:l}),r&&(0,o.createElement)(W.Ez,{googleAdsAccount:s,hideAccountSwitch:!0},(0,o.createElement)(_.A.Card.Footer,null,(0,o.createElement)(A.A,{isDestructive:!0,isLink:!0,onClick:()=>h(q.NS)},(0,u.__)("Disconnect Google Ads account only","google-listings-and-ads")))),(0,o.createElement)(m.Flex,{justify:"flex-end"},(0,o.createElement)(A.A,{isPrimary:!0,isDestructive:!0,onClick:()=>h(q.RO)},(0,u.__)("Disconnect from all accounts","google-listings-and-ads")))))}var Y=n(7677),K=n(1903),$=n(559);function U(){const{jetpack:e}=(0,T.A)(),t="yes"===e?.active;return(0,a.useEffect)((()=>{t&&(0,l.getHistory)().replace((0,d.FN)())}),[t]),e?(0,o.createElement)(R,null,(0,o.createElement)($.A,{className:"gla-wpcom-connection-lost-card",isBorderless:!0,size:"small",icon:(0,o.createElement)(Y.A,{icon:K.A,size:24}),title:(0,u.__)("Your WordPress.com account has been disconnected.","google-listings-and-ads"),helper:(0,u.__)("Connect your WordPress.com account to ensure your products stay listed on Google. If you do not re-connect, your products can’t be automatically synced to Google, and any existing listings may be removed from Google.","google-listings-and-ads")}),(0,o.createElement)(F.s9,null)):(0,o.createElement)(h.A,null)}var V=n(7400),J=n(9415),Q=n(3658);function X({email:e}){const t=(0,I.A)(),[n,s]=(0,a.useState)(null),{disconnectGoogleAccount:c}=(0,Q.j)(),[i,r]=(0,a.useState)(!1);return(0,o.createElement)($.A,{appearance:$.x.GOOGLE,description:e},(0,o.createElement)(m.CardDivider,null),(0,o.createElement)(_.A.Card.Body,null,(0,o.createElement)(m.Notice,{status:"error",isDismissible:!1},(0,o.createElement)("p",null,(0,a.createInterpolateElement)((0,u.__)("This Google account, , was not the Google account previously connected to this integration.","google-listings-and-ads"),{accountEmail:(0,o.createElement)("strong",null,e)})),(0,o.createElement)("p",null,(0,u.__)("Thus, it doesn‘t have access to the Google Merchant Center and/or Google Ads account currently connected to this WooCommerce store.","google-listings-and-ads")),(0,o.createElement)("p",null,(0,u.__)("Try connecting with a different Google account, or completely disconnect all your connected accounts.","google-listings-and-ads")))),(0,o.createElement)(_.A.Card.Footer,{justify:"flex-end"},n&&(0,o.createElement)(q.Ay,{onRequestClose:()=>s(null),onDisconnected:()=>{const e=(0,l.getNewPath)(null,"/google/start",null);window.location.href=t+e},disconnectTarget:n}),(0,o.createElement)(A.A,{isSecondary:!0,isDestructive:!0,disabled:i,onClick:()=>s(q.RO)},(0,u.__)("Disconnect all accounts","google-listings-and-ads")),(0,o.createElement)(A.A,{isPrimary:!0,loading:i,onClick:()=>{r(!0),c().catch((()=>{r(!1)}))}},(0,u.__)("Try another Google account","google-listings-and-ads"))))}function Z(){const{data:e}=(0,J.A)("getGoogleAccountAccess"),t=(0,V.A)(s.Th.adsSetupComplete,e?.scope),n="yes"===e?.active,c=n?"no"===e?.merchant_access||"no"===e?.ads_access:void 0,i=n&&!c&&t.glaRequired;if((0,a.useEffect)((()=>{i&&(0,l.getHistory)().replace((0,d.uZ)())}),[i]),!e)return(0,o.createElement)(h.A,null);if(!i){const t=c?(0,o.createElement)(X,{email:e.email}):(0,o.createElement)(O.Ay,null);return(0,o.createElement)(_.A,{title:(0,u.__)("Connect account","google-listings-and-ads")},t)}return null}var ee=n(6474),te=n(5595),ne=n(7539),oe=n(2455);const ae=()=>{(0,ee.A)("full-content");const{updateGoogleMCContactInformation:e}=(0,Q.j)(),{data:t}=(0,te.A)(),[n,s]=(0,a.useState)(!1),c=t.isAddressFilled&&t.isMCAddressDifferent;return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(ne.A,{title:(0,u.__)("Edit store address","google-listings-and-ads"),helpButton:(0,o.createElement)(oe.A,{eventContext:"edit-store-address"}),backHref:(0,d.FN)()}),(0,o.createElement)("div",{className:"gla-settings"},(0,o.createElement)(_.A,{title:(0,u.__)("Store address","google-listings-and-ads"),description:(0,o.createElement)("div",null,(0,o.createElement)("p",null,(0,u.__)("Your store address is required by Google for verification purposes. It will be shared with the Google Merchant Center and will not be displayed to customers.","google-listings-and-ads")),(0,o.createElement)("p",null,(0,o.createElement)(p.A,{context:"settings-store-address",linkId:"contact-information-read-more",href:"https://woocommerce.com/document/google-for-woocommerce/get-started/requirements/#contact-information"},(0,u.__)("Learn more","google-listings-and-ads"))))},(0,o.createElement)(g.S,null)),(0,o.createElement)(_.A,null,(0,o.createElement)(m.Flex,{justify:"flex-end"},(0,o.createElement)(A.A,{isPrimary:!0,loading:n,disabled:!c,eventName:"gla_contact_information_save_button_click",onClick:()=>{s(!0),e().then((()=>(0,l.getHistory)().push((0,d.FN)()))).catch((()=>s(!1)))}},(0,u.__)("Save details","google-listings-and-ads"))))))};var le=n(332),se=n(9927),ce=n(5246);const ie="gla-settings",re=()=>{const{subpath:e}=(0,l.getQuery)();(0,c.A)(),(0,r.A)();const{google:t}=(0,i.A)(),n=e===d.$K.reconnectGoogleAccount;switch((0,a.useEffect)((()=>{n||"no"!==t?.active||(0,l.getHistory)().replace((0,d.Ke)(s.iH.GOOGLE_DISCONNECTED))}),[n,t]),e){case d.$K.reconnectWPComAccount:return(0,o.createElement)("div",{className:ie},(0,o.createElement)(U,null));case d.$K.reconnectGoogleAccount:return(0,o.createElement)(Z,null);case d.$K.editStoreAddress:return(0,o.createElement)(ae,null)}return(0,o.createElement)("div",{className:ie},(0,o.createElement)(le.A,null),(0,o.createElement)(se.A,null),(0,o.createElement)(ce.A,null),(0,o.createElement)(g.h,null),(0,o.createElement)(D,null),(0,o.createElement)(H,null))}}}]);PK!.]22js/build/shipping.cssnu[.app-tab-nav__tabs{align-items:stretch;box-shadow:inset 0 -1px 0 #ccc;display:flex;flex-wrap:wrap;margin-bottom:var(--main-gap)}.app-tab-nav__tabs-item{background:#0000;border:none;border-radius:0;box-shadow:inset 0 -1px 0 #ccc;box-sizing:border-box;cursor:pointer;font-weight:500;height:48px;margin-left:0;padding:3px 16px}.app-tab-nav__tabs-item:after{content:attr(data-label);display:block;height:0;overflow:hidden;speak:none;visibility:hidden}.app-tab-nav__tabs-item:focus:not(:disabled){box-shadow:inset 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) #0000,inset 0 -1.5px 0 0 var(--wp-admin-theme-color);position:relative}.app-tab-nav__tabs-item.is-active:before{border-bottom:1.5px solid #0000;bottom:1px;content:"";left:0;position:absolute;right:0;top:0}.app-tab-nav__tabs-item:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color)}.app-tab-nav__tabs-item.is-active:focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color),inset 0 -1.5px 0 0 var(--wp-admin-theme-color)} .app-button{white-space:nowrap}.app-button svg{max-height:inherit;max-width:inherit}.app-button svg circle{stroke:currentcolor}.app-button[hidden]{display:none!important}.app-button--icon-position-right.has-icon svg:last-child{margin-left:8px}.app-button--icon-with-text.has-icon{padding:6px 12px} .app-modal{overflow:hidden}@media(min-width:960px){.app-modal{width:600px}}.app-modal .app-modal__footer{display:flex;flex-direction:column-reverse;gap:calc(var(--main-gap)/2);margin-top:var(--large-gap)}@media(min-width:480px){.app-modal .app-modal__footer{flex-direction:row;justify-content:flex-end}}.app-modal .app-modal__footer button{justify-content:center}.app-modal .components-modal__content{overflow:auto}.app-modal__styled--overflow-visible .components-modal__content,.app-modal__styled--overflow-visible.app-modal{overflow:visible} .gla-gtin-migration__link{cursor:pointer;text-decoration:underline} .app-spinner{display:flex;justify-content:center;padding:var(--main-gap)} .gla-validation-errors{color:#cc1818;font-size:12px;line-height:16px;margin:0;width:100%}.gla-validation-errors:not(:first-child){margin-top:16px}.gla-validation-errors:not(:last-child){margin-bottom:16px}.gla-validation-errors>li{list-style:disc inside;margin:0;padding-left:.5em}.gla-validation-errors>li:only-child{list-style:none;padding-left:0} .app-radio-content-control{display:grid;gap:8px;column-gap:10px;grid-template-columns:[input-start] auto [text-start] 1fr}.app-radio-content-control .components-base-control__field,.app-radio-content-control .components-base-control__field .components-flex,.app-radio-content-control .components-base-control__label,.app-radio-content-control .components-radio-control,.app-radio-content-control .components-radio-control .components-flex,.app-radio-content-control .components-radio-control__option{display:contents}.app-radio-content-control .components-radio-control__input[type=radio]{margin:0}.app-radio-content-control .app-radio-content-control__content{grid-column:text-start}.app-radio-content-control .app-radio-content-control__content:empty{display:none} .gla-section-card-body{padding:var(--large-gap)} .gla-section-card-footer{padding:calc(var(--large-gap)/2) var(--large-gap)}.gla-section-card-footer[hidden]{display:none} .gla-subsection-title{font-size:14px;font-style:normal;font-weight:600;letter-spacing:0;line-height:20px;margin-bottom:8px;position:relative;text-align:left} .gla-subsection-body{font-size:13px;font-style:normal;font-weight:400;line-height:16px} .gla-subsection-helper-text{font-size:12px;font-style:italic;font-weight:400;letter-spacing:0;line-height:16px} .gla-subsection-subtitle{color:#757575;font-size:12px;line-height:16px;margin-bottom:4px} .gla-subsection:not(:first-child){margin-top:var(--main-gap)} .gla-section-card-title{margin-bottom:calc(var(--main-gap)/3*2)} .gla-section{display:flex;flex-direction:column;margin-bottom:var(--large-gap)}.gla-section--is-disabled,.gla-section--is-disabled-left .gla-section__header{opacity:.5}@media(min-width:600px){.gla-section{flex-direction:row;gap:var(--main-gap)}.gla-section__header{padding-top:var(--main-gap);width:33%}}.gla-section__header h1{font-size:16px;font-style:normal;font-weight:600;margin-bottom:8px;padding:0}.gla-section__header p{line-height:16px;margin:0 0 8px}.gla-section .gla-section__body{flex:1} .gla-radio-helper-text{color:#757575;font-size:12px;font-style:italic;font-weight:400;line-height:16px} .woocommerce-tree-select-control{position:relative}.woocommerce-tree-select-control__label{color:#757575;display:block;font-size:16px;padding-bottom:8px}.woocommerce-tree-select-control__help{color:#757575;font-size:12px;line-height:16px;margin-top:4px}.woocommerce-tree-select-control .components-base-control{align-items:center;background:#fff;border:1px solid #949494;border-radius:3px;display:flex;flex-wrap:wrap;height:auto;padding:12px;position:relative}.woocommerce-tree-select-control .components-base-control .components-base-control__field{align-items:center;display:flex;flex:1;flex-basis:content;margin-bottom:0;max-width:100%}.woocommerce-tree-select-control .components-base-control .woocommerce-tree-select-control__control-input{background:#0000;border:0;box-shadow:none;color:#2f2f2f;font-size:16px;letter-spacing:inherit;line-height:1.5;margin:0;padding-left:0;padding-right:0;text-align:left;width:100%}.woocommerce-tree-select-control .components-base-control .woocommerce-tree-select-control__control-input::-webkit-search-cancel-button{display:none}.woocommerce-tree-select-control .components-base-control .woocommerce-tree-select-control__control-input:focus{outline:none}.woocommerce-tree-select-control .components-base-control i{color:#636d75;margin-right:12px;width:24px}.woocommerce-tree-select-control .components-base-control.is-active{border-color:var(--wp-admin-theme-color);box-shadow:0 0 0 1px var(--wp-admin-theme-color)}.woocommerce-tree-select-control .components-base-control.has-tags .components-base-control__label,.woocommerce-tree-select-control .components-base-control.with-value .components-base-control__label{font-size:12px;margin-top:-12px}.woocommerce-tree-select-control .components-base-control.is-disabled{background:#ffffff80;border:1px solid #a7aaad80}.woocommerce-tree-select-control .components-base-control.is-disabled .components-base-control__field{visibility:hidden}.woocommerce-tree-select-control .components-base-control.is-disabled .components-base-control__label{cursor:default}.woocommerce-tree-select-control .components-base-control.is-disabled .woocommerce-tag__remove{cursor:default;pointer-events:none}.woocommerce-tree-select-control .woocommerce-tree-select-control__autofill-input{position:absolute;z-index:-1}.woocommerce-tree-select-control .woocommerce-tree-select-control__tags{margin:0;position:relative}.woocommerce-tree-select-control .woocommerce-tree-select-control__tags.has-clear{padding-right:24px}.woocommerce-tree-select-control .woocommerce-tag,.woocommerce-tree-select-control .woocommerce-tree-select-control__show-more{max-height:24px}.woocommerce-tree-select-control .woocommerce-tree-select-control__clear{position:absolute;right:10px;top:calc(50% - 10px)}.woocommerce-tree-select-control .woocommerce-tree-select-control__clear>.clear-icon{color:#ccc}.woocommerce-tree-select-control .woocommerce-tree-select-control__tree{align-items:stretch;background:#fff;border-radius:3px;box-shadow:0 3px 5px #0003,0 1px 18px #0000001f,0 6px 10px #00000024;display:flex;flex-direction:column;left:0;max-height:350px;overflow-y:auto;padding:16px;position:absolute;right:0;z-index:10}.woocommerce-tree-select-control .woocommerce-tree-select-control__tree.is-static{position:static}.woocommerce-tree-select-control .woocommerce-tree-select-control__node.has-children{border-bottom:1px solid #eee}.woocommerce-tree-select-control .woocommerce-tree-select-control__node.has-children:last-child{border-bottom:0}.woocommerce-tree-select-control .woocommerce-tree-select-control__children{padding-left:32px}.woocommerce-tree-select-control .woocommerce-tree-select-control__main{border-top:1px solid #e0e0e0;padding-left:0}.woocommerce-tree-select-control .woocommerce-tree-select-control__option{border:none;display:flex;flex:1;font-size:16px;height:auto;min-height:0;padding:0 0 0 8px;text-align:left}.woocommerce-tree-select-control .woocommerce-tree-select-control__option.is-selected,.woocommerce-tree-select-control .woocommerce-tree-select-control__option:hover{color:var(--wp-admin-theme-color)}.woocommerce-tree-select-control .woocommerce-tree-select-control__option.is-partially-checked .components-checkbox-control__input{background:var(--wp-admin-theme-color);border:4px solid #fff;box-shadow:0 0 0 1px #1e1e1e}.woocommerce-tree-select-control .woocommerce-tree-select-control__option.is-partially-checked .components-checkbox-control__input:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px var(--wp-admin-theme-color)}.woocommerce-tree-select-control .woocommerce-tree-select-control__expander{background:#0000;border:none;cursor:pointer;margin-right:0;padding:4px}.woocommerce-tree-select-control .woocommerce-tree-select-control__expander.is-hidden{pointer-events:none;visibility:hidden}.woocommerce-tree-select-control .components-checkbox-control__label{align-items:center;display:flex;min-height:56px;width:100%}.woocommerce-tree-select-control.is-searchable .components-base-control__label{left:48px}.woocommerce-tree-select-control.is-searchable .components-base-control.is-active .components-base-control__label{font-size:12px;margin-top:-12px}.woocommerce-tree-select-control.is-searchable .woocommerce-tree-select-control__control-input{padding-left:12px} .gla-supported-country-select .woocommerce-tree-select-control__label{font-size:13px;padding-bottom:4px}.gla-supported-country-select .woocommerce-tree-select-control__help{font-style:italic} .gla-vertical-gap-layout{display:flex;flex-direction:column;gap:calc(var(--main-gap)/2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__medium{gap:calc(var(--main-gap)/3*2)}.gla-vertical-gap-layout.gla-vertical-gap-layout__large{gap:var(--main-gap)}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap{gap:0}.gla-vertical-gap-layout.gla-vertical-gap-layout__overlap>:not(:first-child){margin-top:-1px} .gla-choose-audience-section .gla-radio-helper-text{font-style:normal}.gla-choose-audience-section .gla-subsection-helper-text{margin-bottom:calc(var(--main-gap)/3*2)}.gla-choose-audience-section .woocommerce-tree-select-control__help{margin-top:8px} .app-input-control .components-flex-item{margin-right:0;max-width:100%;width:100%}.app-input-control .components-flex-item label.components-input-control__label{color:#757575!important;white-space:normal!important}.app-input-control--no-pointer-events{pointer-events:none}.app-input-control__character-count{color:#757575;font-size:12px;line-height:16px;margin-top:2px;text-align:right}.app-input-control--error-character-count .components-input-control .components-input-control__container .components-input-control__backdrop,.app-input-control.has-error .components-input-control__backdrop{border-color:#cc1818;box-shadow:none}.app-input-control--error-character-count .app-input-control__character-count,.app-input-control.has-error .components-base-control__help{color:#cc1818} .gla-shipping-rate-input-control{align-items:flex-end;display:flex;flex-wrap:wrap;gap:8px}.gla-shipping-rate-input-control .label{display:flex;gap:4px;justify-content:space-between}.gla-shipping-rate-input-control .label button{height:-moz-fit-content;height:fit-content;line-height:1.4em;padding:0}.gla-shipping-rate-input-control .app-input-control{width:300px}.gla-shipping-rate-input-control .gla-input-pill-div{padding:2px 0}.gla-shipping-rate-input-control .gla-input-pill-div .woocommerce-pill{border-color:#008a20;color:#008a20} .gla-countries-time-stepper{width:150px}.gla-countries-time-stepper .gla-countries-time-suffix{margin-right:4px} .gla-edit-time-button.components-button.is-tertiary{height:-moz-fit-content;height:fit-content;line-height:1.4em;padding:0} .gla-countries-time-input-container{max-width:360px}.gla-countries-time-input-container .label{display:flex;gap:4px;justify-content:space-between}.gla-countries-time-input-container .gla-validation-errors{min-height:2lh} .gla-minimum-order-input-control .gla-minimum-order-input-control__label{display:flex;flex-direction:column;gap:8px;justify-content:space-between}.gla-minimum-order-input-control .gla-minimum-order-input-control__label_country{display:flex;gap:4px;justify-content:space-between}.gla-minimum-order-input-control .gla-minimum-order-input-control__label_country button{height:-moz-fit-content;height:fit-content;line-height:1.4em;padding:0} .gla-minimum-order-card .app-input-control{width:300px} @media(min-width:600px){._GQ4sGfGTeKA7JPcBsrS{max-width:360px}} PK!8js/build/shipping.jsnu["use strict";(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[553],{6343:(e,n,t)=>{t.r(n),t.d(n,{default:()=>T});var s=t(1609),a=t(6087),i=t(6427),l=t(7723),o=t(8468),g=t(3658),u=t(9927),r=t(7343),c=t(7892),d=t(9457);const h={confirmationModal:"_GQ4sGfGTeKA7JPcBsrS"};function _({onContinue:e,onRequestClose:n}){return(0,s.createElement)(d.A,{className:h.confirmationModal,title:(0,l.__)("Before you save…","google-listings-and-ads"),buttons:[(0,s.createElement)(c.A,{key:"cancel",isSecondary:!0,onClick:n},(0,l.__)("Don't save","google-listings-and-ads")),(0,s.createElement)(c.A,{key:"continue",isPrimary:!0,onClick:e},(0,l.__)("Continue to save","google-listings-and-ads"))],onRequestClose:n},(0,s.createElement)("p",null,(0,l.__)("Results typically improve with time.","google-listings-and-ads")),(0,s.createElement)("p",null,(0,l.__)("Changes will result in the loss of any optimisations learned over time.","google-listings-and-ads")),(0,s.createElement)("p",null,(0,l.__)("We recommend allowing your listings to run for at least 14 days after set up without changing them for optimal performance.","google-listings-and-ads")))}var m=t(5847),p=t(873),S=t(1650),f=t(6523),A=t(5622),v=t(5640),y=t(4716),C=t(5455),E=t(5807),w=t(8606),b=t(8998),R=t(6473);function T(){const{targetAudience:e,getFinalCountries:n}=(0,m.A)(),{settings:t,saveSettings:c,syncSettings:d}=(0,p.A)(),{saveTargetAudience:h}=(0,g.j)(),{saveShippingRates:T}=(0,C.A)(),{saveShippingTimes:k}=(0,E.A)(),[q,F]=(0,a.useState)(e),[G,M]=(0,a.useState)(t),{hasFinishedResolution:P,data:B}=(0,f.A)(),[j,x]=(0,a.useState)(B),{hasFinishedResolution:N,data:Y}=(0,A.A)(),[D,J]=(0,a.useState)(Y),[K,L]=(0,a.useState)(null);(0,a.useEffect)((()=>M(t)),[t]),(0,a.useEffect)((()=>F(e)),[e]),(0,a.useEffect)((()=>x(B)),[B]),(0,a.useEffect)((()=>J(Y)),[Y]);const{createNotice:Q}=(0,v.A)(),U=!(0,o.isEqual)(...[q,e].map((e=>({...e,countries:new Set(e?.countries)})))),W=!(0,o.isEqual)(G,t),z=(I=B,(H=j).length!==I.length||(0,y.A)(H,I).length>0);var H,I;const O=!(0,o.isEqual)(new Set(D),new Set(Y)),V=U||W||z||O;(0,S.A)((0,l.__)("You have unsaved changes. Are you sure you want to leave?","google-listings-and-ads"),V);const X=q?.countries?q:null,Z=G?.shipping_rate?G:null,$=P?B:null,ee=N?Y:null;return(0,s.createElement)(s.Fragment,null,(0,s.createElement)(u.A,null),(0,s.createElement)(r.A,{targetAudience:X,resolveFinalCountries:n,onTargetAudienceChange:F,settings:Z,onSettingsChange:M,shippingRates:$,onShippingRatesChange:x,shippingTimes:ee,onShippingTimesChange:J,onRequestSubmit:()=>new Promise((e=>{L((()=>n=>{e(n),L(null)}))})),onContinue:async()=>{try{const e=[h(q),c(G),T(j),k(D)],n=await(0,w.A)(e,[(0,l.__)("Target audience","google-listings-and-ads"),(0,l.__)("Merchant Center Settings","google-listings-and-ads"),(0,l.__)("Shipping rates","google-listings-and-ads"),(0,l.__)("Shipping times","google-listings-and-ads")]);await d(),n?Q("error",n):Q("success",(0,l.__)("Your changes have been saved and will be synced to your Google Merchant Center account.","google-listings-and-ads")),(0,R.ce)("gla_free_campaign_edited")}catch(e){(0,b.h)(e,(0,l.__)("Unable to save your changes.","google-listings-and-ads"),(0,l.__)("Something went wrong while saving your changes. Please try again later.","google-listings-and-ads"))}},submitLabel:(0,l.__)("Save changes","google-listings-and-ads")}),(0,s.createElement)(i.Flex,{justify:"flex-end"},(0,s.createElement)(r.A.SubmitButton,null)),K&&(0,s.createElement)(_,{onContinue:()=>K(!0),onRequestClose:()=>K(!1)}))}}}]);PK!3js/build/vendors.jsnu[(globalThis.webpackChunkgoogle_listings_and_ads=globalThis.webpackChunkgoogle_listings_and_ads||[]).push([[96],{7677:(e,t,n)=>{"use strict";n.d(t,{A:()=>i});var r=n(6087);const i=(0,r.forwardRef)((function({icon:e,size:t=24,...n},i){return(0,r.cloneElement)(e,{width:t,height:t,...n,ref:i})}))},8351:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"}))},2485:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,r.createElement)(i.Path,{d:"M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z"}))},3988:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M14.6 7l-1.2-1L8 12l5.4 6 1.2-1-4.6-5z"}))},3756:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},(0,r.createElement)(i.Path,{d:"M6.5 12.4L12 8l5.5 4.4-.9 1.2L12 10l-4.5 3.6-1-1.2z"}))},4772:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"}))},7108:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"}))},9703:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"}))},1903:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M10.5 4v4h3V4H15v4h1.5a1 1 0 011 1v4l-3 4v2a1 1 0 01-1 1h-3a1 1 0 01-1-1v-2l-3-4V9a1 1 0 011-1H9V4h1.5zm.5 12.5v2h2v-2l3-4v-3H8v3l3 4z"}))},5095:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M11 12.5V17.5H12.5V12.5H17.5V11H12.5V6H11V11H6V12.5H11Z"}))},4596:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"M7 11.5h10V13H7z"}))},8690:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{fillRule:"evenodd",d:"M19.75 11H21V8.667L19.875 4H4.125L3 8.667V11h1.25v8.75h15.5V11zm-1.5 0H5.75v7.25H10V13h4v5.25h4.25V11zm-5.5-5.5h2.067l.486 3.24.028.76H12.75v-4zm-3.567 0h2.067v4H8.669l.028-.76.486-3.24zm7.615 3.1l-.464-3.1h2.36l.806 3.345V9.5h-2.668l-.034-.9zM7.666 5.5h-2.36L4.5 8.845V9.5h2.668l.034-.9.464-3.1z",clipRule:"evenodd"}))},4818:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,r.createElement)(i.Path,{d:"m11.3 17.2-5-5c-.1-.1-.1-.3 0-.4l2.3-2.3-1.1-1-2.3 2.3c-.7.7-.7 1.8 0 2.5l5 5H7.5v1.5h5.3v-5.2h-1.5v2.6zm7.5-6.4-5-5h2.7V4.2h-5.2v5.2h1.5V6.8l5 5c.1.1.1.3 0 .4l-2.3 2.3 1.1 1.1 2.3-2.3c.6-.7.6-1.9-.1-2.5z"}))},5603:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(1609),i=n(5573);const o=(0,r.createElement)(i.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},(0,r.createElement)(i.Path,{d:"M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zm1.13 9.38l.35-6.46H8.52l.35 6.46h2.26zm-.09 3.36c.24-.23.37-.55.37-.96 0-.42-.12-.74-.36-.97s-.59-.35-1.06-.35-.82.12-1.07.35-.37.55-.37.97c0 .41.13.73.38.96.26.23.61.34 1.06.34s.8-.11 1.05-.34z"}))},4848:(e,t,n)=>{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";t.A=function(e){var t=e.size,n=void 0===t?24:t,r=e.onClick,a=(e.icon,e.className),l=function(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r{"use strict";var r=n(6925);function i(){}function o(){}o.resetWarningCache=i,e.exports=function(){function e(e,t,n,i,o,s){if(s!==r){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:i};return n.PropTypes=n,n}},5556:(e,t,n)=>{e.exports=n(2694)()},6925:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},6488:(e,t,n)=>{"use strict";n.d(t,{A:()=>x});var r=n(8168),i=n(8587),o=n(5540);function s(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}var a=n(1609),l=n.n(a),c=n(5795),u=n.n(c);var f=n(7241),p=function(e){return e.scrollTop},d="unmounted",h="exited",v="entering",m="entered",g="exiting",b=function(e){function t(t,n){var r;r=e.call(this,t,n)||this;var i,o=n&&!n.isMounting?t.enter:t.appear;return r.appearStatus=null,t.in?o?(i=h,r.appearStatus=v):i=m:i=t.unmountOnExit||t.mountOnEnter?d:h,r.state={status:i},r.nextCallback=null,r}(0,o.A)(t,e),t.getDerivedStateFromProps=function(e,t){return e.in&&t.status===d?{status:h}:null};var n=t.prototype;return n.componentDidMount=function(){this.updateStatus(!0,this.appearStatus)},n.componentDidUpdate=function(e){var t=null;if(e!==this.props){var n=this.state.status;this.props.in?n!==v&&n!==m&&(t=v):n!==v&&n!==m||(t=g)}this.updateStatus(!1,t)},n.componentWillUnmount=function(){this.cancelNextCallback()},n.getTimeouts=function(){var e,t,n,r=this.props.timeout;return e=t=n=r,null!=r&&"number"!=typeof r&&(e=r.exit,t=r.enter,n=void 0!==r.appear?r.appear:t),{exit:e,enter:t,appear:n}},n.updateStatus=function(e,t){if(void 0===e&&(e=!1),null!==t)if(this.cancelNextCallback(),t===v){if(this.props.unmountOnExit||this.props.mountOnEnter){var n=this.props.nodeRef?this.props.nodeRef.current:u().findDOMNode(this);n&&p(n)}this.performEnter(e)}else this.performExit();else this.props.unmountOnExit&&this.state.status===h&&this.setState({status:d})},n.performEnter=function(e){var t=this,n=this.props.enter,r=this.context?this.context.isMounting:e,i=this.props.nodeRef?[r]:[u().findDOMNode(this),r],o=i[0],s=i[1],a=this.getTimeouts(),l=r?a.appear:a.enter;e||n?(this.props.onEnter(o,s),this.safeSetState({status:v},(function(){t.props.onEntering(o,s),t.onTransitionEnd(l,(function(){t.safeSetState({status:m},(function(){t.props.onEntered(o,s)}))}))}))):this.safeSetState({status:m},(function(){t.props.onEntered(o)}))},n.performExit=function(){var e=this,t=this.props.exit,n=this.getTimeouts(),r=this.props.nodeRef?void 0:u().findDOMNode(this);t?(this.props.onExit(r),this.safeSetState({status:g},(function(){e.props.onExiting(r),e.onTransitionEnd(n.exit,(function(){e.safeSetState({status:h},(function(){e.props.onExited(r)}))}))}))):this.safeSetState({status:h},(function(){e.props.onExited(r)}))},n.cancelNextCallback=function(){null!==this.nextCallback&&(this.nextCallback.cancel(),this.nextCallback=null)},n.safeSetState=function(e,t){t=this.setNextCallback(t),this.setState(e,t)},n.setNextCallback=function(e){var t=this,n=!0;return this.nextCallback=function(r){n&&(n=!1,t.nextCallback=null,e(r))},this.nextCallback.cancel=function(){n=!1},this.nextCallback},n.onTransitionEnd=function(e,t){this.setNextCallback(t);var n=this.props.nodeRef?this.props.nodeRef.current:u().findDOMNode(this),r=null==e&&!this.props.addEndListener;if(n&&!r){if(this.props.addEndListener){var i=this.props.nodeRef?[this.nextCallback]:[n,this.nextCallback],o=i[0],s=i[1];this.props.addEndListener(o,s)}null!=e&&setTimeout(this.nextCallback,e)}else setTimeout(this.nextCallback,0)},n.render=function(){var e=this.state.status;if(e===d)return null;var t=this.props,n=t.children,r=(t.in,t.mountOnEnter,t.unmountOnExit,t.appear,t.enter,t.exit,t.timeout,t.addEndListener,t.onEnter,t.onEntering,t.onEntered,t.onExit,t.onExiting,t.onExited,t.nodeRef,(0,i.A)(t,["children","in","mountOnEnter","unmountOnExit","appear","enter","exit","timeout","addEndListener","onEnter","onEntering","onEntered","onExit","onExiting","onExited","nodeRef"]));return l().createElement(f.A.Provider,{value:null},"function"==typeof n?n(e,r):l().cloneElement(l().Children.only(n),r))},t}(l().Component);function O(){}b.contextType=f.A,b.propTypes={},b.defaultProps={in:!1,mountOnEnter:!1,unmountOnExit:!1,appear:!1,enter:!0,exit:!0,onEnter:O,onEntering:O,onEntered:O,onExit:O,onExiting:O,onExited:O},b.UNMOUNTED=d,b.EXITED=h,b.ENTERING=v,b.ENTERED=m,b.EXITING=g;const w=b;var y=function(e,t){return e&&t&&t.split(" ").forEach((function(t){return r=t,void((n=e).classList?n.classList.remove(r):"string"==typeof n.className?n.className=s(n.className,r):n.setAttribute("class",s(n.className&&n.className.baseVal||"",r)));var n,r}))},E=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i{"use strict";n.d(t,{A:()=>h});var r=n(8587),i=n(8168),o=n(5540),s=n(1609),a=n.n(s),l=n(7241);function c(e,t){var n=Object.create(null);return e&&s.Children.map(e,(function(e){return e})).forEach((function(e){n[e.key]=function(e){return t&&(0,s.isValidElement)(e)?t(e):e}(e)})),n}function u(e,t,n){return null!=n[t]?n[t]:e.props[t]}function f(e,t,n){var r=c(e.children),i=function(e,t){function n(n){return n in t?t[n]:e[n]}e=e||{},t=t||{};var r,i=Object.create(null),o=[];for(var s in e)s in t?o.length&&(i[s]=o,o=[]):o.push(s);var a={};for(var l in t){if(i[l])for(r=0;r{"use strict";n.d(t,{A:()=>i});var r=n(1609);const i=n.n(r)().createContext(null)},6942:(e,t)=>{var n;!function(){"use strict";var r={}.hasOwnProperty;function i(){for(var e="",t=0;t{"use strict";function r(){return r=Object.assign?Object.assign.bind():function(e){for(var t=1;tr})},5540:(e,t,n)=>{"use strict";function r(e,t){return r=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},r(e,t)}function i(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,r(e,t)}n.d(t,{A:()=>i})},8587:(e,t,n)=>{"use strict";function r(e,t){if(null==e)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.includes(r))continue;n[r]=e[r]}return n}n.d(t,{A:()=>r})}}]);PK!,JTT!js/build/wp-consent-api.asset.phpnu[ array(), 'version' => 'da7d4a784443814213db'); PK! S44js/build/wp-consent-api.jsnu[(()=>{const e={statistics:["analytics_storage"],marketing:["ad_storage","ad_user_data","ad_personalization"]},t=()=>{if("function"==typeof wp_has_consent){void 0===window.wp_consent_type&&(window.wp_consent_type="optin");const t={};for(const[n,o]of Object.entries(e))if(""!==consent_api_get_cookie(window.consent_api.cookie_prefix+"_"+n)){const e=wp_has_consent(n)?"granted":"denied";o.forEach((n=>{t[n]=e}))}Object.keys(t).length>0&&window.gtag("consent","update",t)}};document.addEventListener("wp_listen_for_consent_change",(t=>{const n={},o=e[Object.keys(t.detail)[0]],a="allow"===Object.values(t.detail)[0]?"granted":"denied";void 0!==o&&(o.forEach((e=>{n[e]=a})),Object.keys(n).length>0&&window.gtag("consent","update",n))})),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()})();PK!d&3[l[l%languages/google-listings-and-ads.potnu[# Copyright (C) 2025 WooCommerce # This file is distributed under the GPLv3. msgid "" msgstr "" "Project-Id-Version: Google for WooCommerce 2.9.7\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/google-listings-and-ads\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "POT-Creation-Date: 2025-01-28T20:47:00+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.9.0\n" "X-Domain: google-listings-and-ads\n" #. Plugin Name of the plugin #: src/Admin/ProductBlocksService.php:148 #: src/Internal/DependencyManagement/ThirdPartyServiceProvider.php:82 #: src/Menu/Dashboard.php:37 #: src/Menu/GetStarted.php:37 #: src/MultichannelMarketing/GLAChannel.php:104 #: views/attributes/variations-form.php:22 #: views/meta-box/channel_visibility.php:63 #: views/meta-box/channel_visibility.php:92 #: views/meta-box/coupon_channel_visibility.php:114 #: views/meta-box/coupon_channel_visibility.php:160 #: js/build/get-started-page.js:1 #: js/build/index.js:25 msgid "Google for WooCommerce" msgstr "" #. Description of the plugin #: src/MultichannelMarketing/GLAChannel.php:113 msgid "Native integration with Google that allows merchants to easily display their products across Google’s network." msgstr "" #. Author of the plugin #: js/build/index.js:25 msgid "WooCommerce" msgstr "" #. Author URI of the plugin msgid "https://woocommerce.com/" msgstr "" #. translators: %s: the action name #: src/ActionScheduler/ActionSchedulerException.php:29 msgid "No action matching %s was found." msgstr "" #: src/Admin/Admin.php:205 #: src/Menu/Settings.php:25 #: js/build/commons.js:60 #: js/build/index.js:25 msgid "Settings" msgstr "" #: src/Admin/Admin.php:211 #: js/build/blocks.js:1 msgid "Get Started" msgstr "" #: src/Admin/Admin.php:218 msgid "Documentation" msgstr "" #. translators: 1) HTML anchor open tag 2) HTML anchor closing tag #: src/Admin/Admin.php:279 msgid "By using this extension, you may be storing personal data or sharing data with an external service. %1$sLearn more about what data is collected by Google and what you may want to include in your privacy policy%2$s." msgstr "" #: src/Admin/Input/BooleanSelect.php:20 #: src/Admin/Product/Attributes/AttributesForm.php:147 msgid "Default" msgstr "" #: src/Admin/Input/BooleanSelect.php:21 #: src/Product/Attributes/Adult.php:82 #: src/Product/Attributes/IsBundle.php:83 msgid "Yes" msgstr "" #: src/Admin/Input/BooleanSelect.php:22 #: src/Product/Attributes/Adult.php:83 #: src/Product/Attributes/IsBundle.php:84 msgid "No" msgstr "" #: src/Admin/Input/SelectWithTextInput.php:27 msgid "Enter your value" msgstr "" #: src/Admin/Input/SelectWithTextInput.php:106 #: src/Admin/Input/SelectWithTextInput.php:165 msgid "Enter a custom value" msgstr "" #: src/Admin/MetaBox/ChannelVisibilityMetaBox.php:73 #: src/Admin/MetaBox/CouponChannelVisibilityMetaBox.php:78 #: src/Admin/ProductBlocksService.php:185 msgid "Channel visibility" msgstr "" #: src/Admin/Product/Attributes/Input/AdultInput.php:25 msgid "Adult content" msgstr "" #: src/Admin/Product/Attributes/Input/AdultInput.php:26 msgid "Whether the product contains nudity or sexually suggestive content" msgstr "" #: src/Admin/Product/Attributes/Input/AgeGroupInput.php:25 msgid "Age Group" msgstr "" #: src/Admin/Product/Attributes/Input/AgeGroupInput.php:26 msgid "Target age group of the item." msgstr "" #: src/Admin/Product/Attributes/Input/AvailabilityDateInput.php:25 #: src/Product/Attributes/AvailabilityDate.php:38 msgid "Availability Date" msgstr "" #: src/Admin/Product/Attributes/Input/AvailabilityDateInput.php:26 #: src/Product/Attributes/AvailabilityDate.php:47 msgid "The date a preordered or backordered product becomes available for delivery. Required if product availability is preorder or backorder" msgstr "" #: src/Admin/Product/Attributes/Input/BrandInput.php:25 #: src/Product/Attributes/Brand.php:61 msgid "Brand" msgstr "" #: src/Admin/Product/Attributes/Input/BrandInput.php:26 msgid "Brand of the product." msgstr "" #: src/Admin/Product/Attributes/Input/ColorInput.php:25 #: src/Product/Attributes/Color.php:61 msgid "Color" msgstr "" #: src/Admin/Product/Attributes/Input/ColorInput.php:26 msgid "Color of the product." msgstr "" #: src/Admin/Product/Attributes/Input/ConditionInput.php:25 #: src/Product/Attributes/Condition.php:76 msgid "Condition" msgstr "" #: src/Admin/Product/Attributes/Input/ConditionInput.php:26 msgid "Condition or state of the item." msgstr "" #: src/Admin/Product/Attributes/Input/GenderInput.php:25 #: src/Product/Attributes/Gender.php:76 msgid "Gender" msgstr "" #: src/Admin/Product/Attributes/Input/GenderInput.php:26 msgid "The gender for which your product is intended." msgstr "" #: src/Admin/Product/Attributes/Input/GTINInput.php:28 msgid "Global Trade Item Number (GTIN)" msgstr "" #: src/Admin/Product/Attributes/Input/GTINInput.php:29 msgid "Global Trade Item Number (GTIN) for your item. These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books)" msgstr "" #: src/Admin/Product/Attributes/Input/GTINInput.php:47 msgid "The Global Trade Item Number (GTIN) for your item can now be entered on the \"Inventory\" tab" msgstr "" #: src/Admin/Product/Attributes/Input/IsBundleInput.php:25 msgid "Is Bundle?" msgstr "" #: src/Admin/Product/Attributes/Input/IsBundleInput.php:26 msgid "Whether the item is a bundle of products. A bundle is a custom grouping of different products sold by a merchant for a single price." msgstr "" #: src/Admin/Product/Attributes/Input/MaterialInput.php:25 #: src/Product/Attributes/Material.php:61 msgid "Material" msgstr "" #: src/Admin/Product/Attributes/Input/MaterialInput.php:26 msgid "The material of which the item is made." msgstr "" #: src/Admin/Product/Attributes/Input/MPNInput.php:25 msgid "Manufacturer Part Number (MPN)" msgstr "" #: src/Admin/Product/Attributes/Input/MPNInput.php:26 msgid "This code uniquely identifies the product to its manufacturer." msgstr "" #: src/Admin/Product/Attributes/Input/MultipackInput.php:25 #: src/Product/Attributes/Multipack.php:72 msgid "Multipack" msgstr "" #: src/Admin/Product/Attributes/Input/MultipackInput.php:26 msgid "The number of identical products in a multipack. Use this attribute to indicate that you've grouped multiple identical products for sale as one item." msgstr "" #: src/Admin/Product/Attributes/Input/PatternInput.php:25 #: src/Product/Attributes/Pattern.php:61 msgid "Pattern" msgstr "" #: src/Admin/Product/Attributes/Input/PatternInput.php:26 msgid "The item's pattern (e.g. polka dots)." msgstr "" #: src/Admin/Product/Attributes/Input/SizeInput.php:25 #: src/Product/Attributes/Size.php:61 msgid "Size" msgstr "" #: src/Admin/Product/Attributes/Input/SizeInput.php:26 msgid "Size of the product." msgstr "" #: src/Admin/Product/Attributes/Input/SizeSystemInput.php:25 msgid "Size system" msgstr "" #: src/Admin/Product/Attributes/Input/SizeSystemInput.php:26 msgid "System in which the size is specified. Recommended for apparel items." msgstr "" #: src/Admin/Product/Attributes/Input/SizeTypeInput.php:25 msgid "Size type" msgstr "" #: src/Admin/Product/Attributes/Input/SizeTypeInput.php:26 msgid "The cut of the item. Recommended for apparel items." msgstr "" #: src/Admin/ProductBlocksService.php:204 #: views/attributes/tab-panel.php:20 msgid "Product attributes" msgstr "" #. translators: 1: is a numeric account ID #: src/Ads/AccountService.php:108 msgid "Ads account %1$d already connected." msgstr "" #. translators: 1: is a string representing an unknown step name #: src/Ads/AccountService.php:185 msgid "Unknown ads account creation step %1$s" msgstr "" #: src/Ads/AccountService.php:249 msgid "Account must be accepted before completing setup." msgstr "" #: src/Ads/AccountService.php:327 msgid "Billing setup must be completed." msgstr "" #. translators: 1: is an integer representing an unknown Term ID #: src/Ads/AssetSuggestionsService.php:173 msgid "Invalid Term ID or Post ID or site url %1$d" msgstr "" #: src/Ads/AssetSuggestionsService.php:242 #: src/Ads/AssetSuggestionsService.php:243 #: src/Ads/AssetSuggestionsService.php:244 #: src/Ads/AssetSuggestionsService.php:721 msgid "Homepage" msgstr "" #. translators: 1: is an integer representing an unknown Post ID #: src/Ads/AssetSuggestionsService.php:266 msgid "Invalid Post ID %1$d" msgstr "" #. translators: 1: is an integer representing an unknown Term ID #: src/Ads/AssetSuggestionsService.php:315 msgid "Invalid Term ID %1$d" msgstr "" #. translators: %s Error message #: src/API/Google/Ads.php:85 msgid "Error retrieving accounts: %s" msgstr "" #: src/API/Google/Ads.php:357 msgid "Merchant link is not available to accept" msgstr "" #. translators: %s: current date time. #: src/API/Google/AdsAssetGroup.php:100 msgid "PMax %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsAssetGroup.php:116 msgid "Error creating asset group: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsAssetGroup.php:262 msgid "Error retrieving asset groups: %s" msgstr "" #: src/API/Google/AdsAssetGroup.php:330 msgid "Each image type (landscape, square, portrait or logo) cannot contain duplicated images." msgstr "" #. translators: %s Error message #: src/API/Google/AdsAssetGroup.php:336 msgid "Error editing asset group: %s" msgstr "" #: src/API/Google/AdsAssetGroup.php:425 msgid "Invalid asset group ID" msgstr "" #. translators: %s Error message #: src/API/Google/AdsAssetGroupAsset.php:140 msgid "Error retrieving asset groups assets: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsAssetGroupAsset.php:206 msgid "Error retrieving asset groups assets by final url: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsCampaign.php:160 msgid "Error retrieving campaigns: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsCampaign.php:203 msgid "Error retrieving campaign: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsCampaign.php:269 msgid "Error creating campaign: %s" msgstr "" #: src/API/Google/AdsCampaign.php:272 msgid "A campaign with this name already exists" msgstr "" #. translators: %s Error message #: src/API/Google/AdsCampaign.php:325 msgid "Error editing campaign: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsCampaign.php:361 msgid "Error deleting campaign: %s" msgstr "" #: src/API/Google/AdsCampaign.php:364 msgid "This campaign has already been deleted" msgstr "" #: src/API/Google/AdsCampaign.php:623 msgid "Invalid campaign ID" msgstr "" #: src/API/Google/AdsCampaign.php:639 msgid "Invalid geo target location ID" msgstr "" #. translators: %d Campaign ID #: src/API/Google/AdsCampaignBudget.php:131 msgid "No budget found for campaign %d" msgstr "" #: src/API/Google/AdsCampaignBudget.php:147 msgid "Invalid campaign budget ID" msgstr "" #. translators: %1 is a random 4-digit string #: src/API/Google/AdsConversionAction.php:70 msgid "[%1$s] Google for WooCommerce purchase action" msgstr "" #: src/API/Google/AdsConversionAction.php:107 msgid "A conversion action with this name already exists" msgstr "" #. translators: %s Error message #: src/API/Google/AdsConversionAction.php:116 msgid "Error creating conversion action: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsConversionAction.php:157 msgid "Error retrieving conversion action: %s" msgstr "" #. translators: %s Error message #: src/API/Google/AdsReport.php:110 msgid "Unable to retrieve report data: %s" msgstr "" #: src/API/Google/Connection.php:68 #: src/API/Google/Connection.php:72 msgid "Unable to connect Google account" msgstr "" #: src/API/Google/Connection.php:123 msgid "Invalid response when retrieving status" msgstr "" #: src/API/Google/Connection.php:128 msgid "Error retrieving status" msgstr "" #: src/API/Google/ExceptionTrait.php:139 msgid "An unknown error occurred in the Shopping Content Service." msgstr "" #: src/API/Google/Merchant.php:98 msgid "Unable to claim website." msgstr "" #: src/API/Google/Merchant.php:100 msgid "Website already claimed, use overwrite to complete the process." msgstr "" #. translators: %s Error message #: src/API/Google/Merchant.php:200 msgid "Unable to retrieve Merchant Center account: %s" msgstr "" #: src/API/Google/Merchant.php:251 msgid "Unable to retrieve Merchant Center account status." msgstr "" #. translators: %s Error message #: src/API/Google/Merchant.php:301 msgid "Unable to update Merchant Center account: %s" msgstr "" #: src/API/Google/MerchantReport.php:116 msgid "Unable to retrieve Product View Report." msgstr "" #: src/API/Google/MerchantReport.php:184 msgid "Unable to retrieve report data." msgstr "" #: src/API/Google/Middleware.php:75 msgid "Error retrieving accounts" msgstr "" #: src/API/Google/Middleware.php:92 #: src/API/Google/Middleware.php:294 msgid "Unable to log accepted TOS" msgstr "" #: src/API/Google/Middleware.php:97 #: src/API/Google/SiteVerification.php:59 #: src/MerchantCenter/AccountService.php:450 msgid "Invalid site URL." msgstr "" #: src/API/Google/Middleware.php:153 #: src/API/Google/Middleware.php:334 msgid "Invalid response when creating account" msgstr "" #: src/API/Google/Middleware.php:156 #: src/API/Google/Middleware.php:340 msgid "Error creating account" msgstr "" #: src/API/Google/Middleware.php:217 msgid "Invalid response when linking merchant to MCA" msgstr "" #: src/API/Google/Middleware.php:223 msgid "Error linking merchant to MCA" msgstr "" #: src/API/Google/Middleware.php:262 msgid "Invalid response when claiming website" msgstr "" #: src/API/Google/Middleware.php:269 msgid "Error claiming website" msgstr "" #: src/API/Google/Middleware.php:288 msgid "Store country is not supported" msgstr "" #: src/API/Google/Middleware.php:384 msgid "Invalid response when linking account" msgstr "" #: src/API/Google/Middleware.php:390 msgid "Error linking account" msgstr "" #. translators: 1: current date in the format Y-m-d #: src/API/Google/Middleware.php:509 msgid "Account %1$s" msgstr "" #: src/API/Google/Middleware.php:586 msgid "Invalid response authenticating partner app." msgstr "" #: src/API/Google/Middleware.php:596 msgid "Error authenticating Google Partner APP." msgstr "" #: src/API/Google/Query/AdsQuery.php:81 msgid "No result from query" msgstr "" #. translators: %s Error message #: src/API/Google/SiteVerification.php:126 msgid "Unable to retrieve site verification token: %s" msgstr "" #. translators: %s Error message #: src/API/Google/SiteVerification.php:168 msgid "Unable to insert site verification: %s" msgstr "" #: src/API/Site/Controllers/Ads/AccountController.php:160 #: src/API/Site/Controllers/Google/AccountController.php:151 #: src/API/Site/Controllers/Jetpack/AccountController.php:169 msgid "Successfully disconnected." msgstr "" #: src/API/Site/Controllers/Ads/AccountController.php:200 msgid "Google Ads Account ID." msgstr "" #: src/API/Site/Controllers/Ads/AccountController.php:207 msgid "Billing Flow URL." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:86 msgid "Final URL." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:90 msgid "Asset Group path 1." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:94 msgid "Asset Group path 2." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:108 msgid "Asset Group ID." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:114 msgid "List of asset to be edited." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:131 msgid "Campaign ID." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:174 msgid "Successfully created asset group." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:197 msgid "No asset group fields to update." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:203 msgid "Successfully edited asset group." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:221 msgid "Asset Group ID" msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:226 #: js/build/commons.js:1 msgid "Final URL" msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:232 msgid "Text that may appear appended to the url displayed in the ad." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:237 msgid "Asset is a part of an ad which can be shared across multiple ads. It can be an image, headlines, descriptions, etc." msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:281 msgid "Asset ID" msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:285 msgid "Asset content" msgstr "" #: src/API/Site/Controllers/Ads/AssetGroupController.php:289 msgid "Asset field type" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:79 msgid "Search for post title or term name" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:86 msgid "The number of items to be return" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:94 msgid "Sort retrieved items by parameter" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:112 msgid "Post ID or Term ID." msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:119 msgid "Type linked to the id." msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:177 msgid "Post ID or Term ID" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:182 msgid "Post, term or homepage" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:189 msgid "The post or term title" msgstr "" #: src/API/Site/Controllers/Ads/AssetSuggestionsController.php:195 msgid "The URL linked to the post/term" msgstr "" #: src/API/Site/Controllers/Ads/BudgetRecommendationController.php:100 msgid "No currency available for the Ads account." msgstr "" #: src/API/Site/Controllers/Ads/BudgetRecommendationController.php:117 msgid "Cannot find any budget recommendations." msgstr "" #: src/API/Site/Controllers/Ads/BudgetRecommendationController.php:154 #: src/API/Site/Controllers/ShippingRateSchemaTrait.php:42 msgid "The currency to use for the shipping rate." msgstr "" #: src/API/Site/Controllers/Ads/BudgetRecommendationController.php:165 #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:255 #: src/API/Site/Controllers/ShippingRateSchemaTrait.php:34 msgid "Country code in ISO 3166-1 alpha-2 format." msgstr "" #: src/API/Site/Controllers/Ads/BudgetRecommendationController.php:170 msgid "The recommended daily budget for a country." msgstr "" #. translators: %s: current date time. #: src/API/Site/Controllers/Ads/CampaignController.php:132 msgid "Campaign %s" msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:186 msgid "Campaign is not available." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:213 msgid "Invalid edit data." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:243 msgid "Successfully edited campaign." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:278 msgid "Successfully deleted campaign." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:320 msgid "Exclude removed campaigns." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:326 #: src/API/Site/Controllers/BaseReportsController.php:79 #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:208 #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:169 msgid "Maximum number of rows to be returned in result data." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:345 #: src/API/Site/Controllers/Ads/ReportsController.php:149 msgid "ID number." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:351 msgid "Descriptive campaign name." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:359 #: src/API/Site/Controllers/Ads/ReportsController.php:160 msgid "Campaign status." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:366 msgid "Campaign type." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:372 msgid "Daily budget amount in the local currency." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:379 msgid "Country code of sale country in ISO 3166-1 alpha-2 format." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:387 msgid "The locations that an Ads campaign is targeting in ISO 3166-1 alpha-2 format." msgstr "" #: src/API/Site/Controllers/Ads/CampaignController.php:399 msgid "The name of the label to assign to the campaign." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:102 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:101 msgid "Time interval to use for segments in the returned data." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:130 #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:92 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:134 msgid "Product ID." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:135 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:139 msgid "Product name." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:154 msgid "Campaign name." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:165 msgid "Whether the campaign has been converted" msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:179 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:153 msgid "ID of this report segment." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:189 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:163 msgid "Token to retrieve the next page of results." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:206 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:180 msgid "Clicks." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:211 #: src/API/Site/Controllers/MerchantCenter/ReportsController.php:185 msgid "Impressions." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:216 msgid "Sales amount." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:221 msgid "Spend amount." msgstr "" #: src/API/Site/Controllers/Ads/ReportsController.php:226 msgid "Conversions." msgstr "" #: src/API/Site/Controllers/Ads/SetupCompleteController.php:85 msgid "Successfully marked Ads setup as completed." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingDataController.php:71 msgid "The attribute key to get the sources." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingDataController.php:125 msgid "The list of attributes or attribute sources." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingRulesController.php:207 msgid "The Id for the rule." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingRulesController.php:213 msgid "The attribute value for the rule." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingRulesController.php:220 msgid "The source value for the rule." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingRulesController.php:226 msgid "The category condition type to apply for this rule." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingRulesController.php:233 msgid "List of category IDs, separated by commas." msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingSyncerController.php:89 msgid "Indicates if the products are currently syncing" msgstr "" #: src/API/Site/Controllers/AttributeMapping/AttributeMappingSyncerController.php:96 msgid "Timestamp with the last sync." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:35 msgid "Limit response to data after a given ISO8601 compliant date." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:42 msgid "Limit response to data before a given ISO8601 compliant date." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:49 msgid "Limit result to items with specified ids." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:58 msgid "Limit totals to a set of fields." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:67 #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:198 msgid "Order sort attribute ascending or descending." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:74 #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:191 msgid "Sort collection by attribute." msgstr "" #: src/API/Site/Controllers/BaseReportsController.php:88 msgid "Token to retrieve the next page." msgstr "" #: src/API/Site/Controllers/BatchSchemaTrait.php:33 #: src/API/Site/Controllers/MerchantCenter/ShippingRateSuggestionsController.php:64 #: src/API/Site/Controllers/MerchantCenter/TargetAudienceController.php:247 msgid "Array of country codes in ISO 3166-1 alpha-2 format." msgstr "" #: src/API/Site/Controllers/CountryCodeTrait.php:72 msgid "Country is not supported" msgstr "" #: src/API/Site/Controllers/Google/AccountController.php:126 msgid "Indicates the next page name mapped to the redirect URL when back from Google authorization." msgstr "" #: src/API/Site/Controllers/Google/AccountController.php:133 msgid "Indicate the Google account to suggest for authorization." msgstr "" #: src/API/Site/Controllers/Google/AccountController.php:206 msgid "The URL for making a connection to Google." msgstr "" #: src/API/Site/Controllers/GTINMigrationController.php:79 msgid "GTIN Migration cannot be scheduled." msgstr "" #: src/API/Site/Controllers/GTINMigrationController.php:89 msgid "GTIN Migration successfully started." msgstr "" #: src/API/Site/Controllers/Jetpack/AccountController.php:147 msgid "Indicates the next page name mapped to the redirect URL when back from Jetpack authorization." msgstr "" #: src/API/Site/Controllers/Jetpack/AccountController.php:268 msgid "The URL for making a connection to Jetpack (wordpress.com)." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AccountController.php:212 msgid "Merchant Center account successfully disconnected." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AccountController.php:226 msgid "Merchant Center Account ID." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AccountController.php:233 msgid "Is a MCA sub account." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AccountController.php:239 msgid "The Merchant Center Account name." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AccountController.php:245 msgid "The domain registered with the Merchant Center Account." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AttributeMappingCategoriesController.php:80 msgid "The Category ID." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AttributeMappingCategoriesController.php:86 msgid "The category name." msgstr "" #: src/API/Site/Controllers/MerchantCenter/AttributeMappingCategoriesController.php:92 msgid "The category parent." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ConnectionController.php:56 msgid "Action that should be completed after connection." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:129 msgid "The Merchant Center account ID." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:135 msgid "The phone number associated with the Merchant Center account." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:140 msgid "The verification status of the phone number associated with the Merchant Center account." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:146 msgid "The address associated with the Merchant Center account." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:152 msgid "The WooCommerce store address." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:158 msgid "Whether the Merchant Center account address is different than the WooCommerce store address." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:163 msgid "The errors associated with the WooCommerce address" msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:177 msgid "Street-level part of the address." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:182 msgid "City, town or commune. May also include dependent localities or sublocalities (e.g. neighborhoods or suburbs)." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:187 msgid "Top-level administrative subdivision of the country. For example, a state like California (\"CA\") or a province like Quebec (\"QC\")." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:192 msgid "Postal code or ZIP (e.g. \"94043\")." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ContactInformationController.php:197 msgid "CLDR country code (e.g. \"US\")." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:117 msgid "The issues related to the Merchant Center account." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:125 msgid "Issue type." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:130 msgid "Affected product." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:135 msgid "The WooCommerce product ID." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:140 msgid "Internal Google code for issue." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:145 msgid "Descriptive text of the issue." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:150 msgid "Descriptive text of action to take." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:155 msgid "Documentation URL for issue and/or action." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:160 msgid "Severity level of the issue: warning or error." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:165 msgid "Country codes of the product audience." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:183 msgid "Whether the product issues are loading." msgstr "" #: src/API/Site/Controllers/MerchantCenter/IssuesController.php:200 #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:161 msgid "Page of data to retrieve." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PhoneVerificationController.php:50 msgid "Method used to verify the phone number." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PhoneVerificationController.php:69 msgid "Two-letter country code (ISO 3166-1 alpha-2) for the phone number." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PhoneVerificationController.php:75 msgid "The phone number to verify." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PhoneVerificationController.php:95 msgid "The verification ID returned by the /request call." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PhoneVerificationController.php:101 msgid "The verification code that was sent to the phone number for validation." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:96 msgid "The store website could be accessed or not by all users." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:101 msgid "The merchant set the restrictions in robots.txt or not in the store." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:106 msgid "The sample of product landing pages leads to a 404 error." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:111 msgid "The sample of product landing pages have redirects through 3P domains." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:116 msgid "The payment gateways associated with onboarding policy checking." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:121 msgid "The store ssl associated with onboarding policy checking." msgstr "" #: src/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.php:126 msgid "The refund returns policy associated with onboarding policy checking." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:84 msgid "The store's products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:97 msgid "Product title." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:102 msgid "Whether the product is set to be visible in the Merchant Center" msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:107 msgid "The current sync status of the product." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:112 msgid "The image url of the product." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:117 msgid "The price of the product." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:122 msgid "Errors preventing the product from being synced to the Merchant Center." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:177 msgid "Text to search for in product names." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductFeedController.php:182 msgid "Limit result to items with specified ids (comma-separated)." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:130 msgid "Timestamp reflecting when the product status statistics were last generated." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:136 msgid "Merchant Center product status statistics." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:142 msgid "Active products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:147 msgid "Expiring products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:152 msgid "Pending products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:157 msgid "Disapproved products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:162 msgid "Products not uploaded." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:169 msgid "Amount of scheduled jobs which will sync products to Google." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:175 msgid "Whether the product statistics are loading." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductStatisticsController.php:181 msgid "Error message in case of failure" msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductVisibilityController.php:117 msgid "Products whose visibility was changed successfully." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductVisibilityController.php:126 msgid "Products whose visibility was not changed." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductVisibilityController.php:145 msgid "IDs of the products to update." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ProductVisibilityController.php:154 msgid "New Visibility status for the specified products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:119 msgid "Your account is under cool down period and cannot request a new review." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:131 msgid "Your account is not eligible for a new request review." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:183 msgid "The status of the last review." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:189 msgid "Timestamp indicating if the user is in cool down period." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:195 msgid "The issues related to the Merchant Center to be reviewed and addressed before approval." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:204 msgid "The region codes in which is allowed to request a new review." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:286 msgid "Error getting account review status." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:318 msgid "Invalid response getting requesting a new review." msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:325 msgid "A new review has been successfully requested" msgstr "" #: src/API/Site/Controllers/MerchantCenter/RequestReviewController.php:331 msgid "Error requesting a new review." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SettingsController.php:103 msgid "Merchant Center Settings successfully updated." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SettingsController.php:118 msgid "Whether shipping rate is a simple flat rate or needs to be configured manually in the Merchant Center." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SettingsController.php:132 msgid "Whether shipping time is a simple flat time or needs to be configured manually in the Merchant Center." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SettingsController.php:145 msgid "Whether tax rate is destination based or need to be configured manually in the Merchant Center." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SettingsController.php:159 msgid "The number of shipping rates in WC ready to be used in the Merchant Center." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SettingsSyncController.php:91 msgid "Successfully synchronized settings with Google." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingRateBatchController.php:123 msgid "Array of shipping rates to create." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingRateBatchController.php:145 msgid "Array of unique shipping rate identification numbers." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingRateController.php:128 msgid "No rate available." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingRateController.php:152 #: src/API/Site/Controllers/MerchantCenter/ShippingRateController.php:238 msgid "No rate found with the given ID." msgstr "" #. translators: %s is the country code in ISO 3166-1 alpha-2 format. #: src/API/Site/Controllers/MerchantCenter/ShippingRateController.php:216 msgid "Successfully added rate for country: \"%s\"." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingRateController.php:249 msgid "Successfully deleted rate." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:119 msgid "No time available." msgstr "" #. translators: %s is the country code in ISO 3166-1 alpha-2 format. #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:170 msgid "Successfully added time for country: \"%s\"." msgstr "" #. translators: %s is the country code in ISO 3166-1 alpha-2 format. #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:204 msgid "Successfully deleted the time for country: \"%s\"." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:263 msgid "The minimum shipping time in days." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:269 msgid "The maximum shipping time in days." msgstr "" #. translators: 1: Parameter, 2: Type name. #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:305 msgid "%1$s is not of type %2$s." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:311 msgid "Shipping times cannot be negative." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:315 msgid "The minimum shipping time cannot be greater than the maximum shipping time." msgstr "" #: src/API/Site/Controllers/MerchantCenter/ShippingTimeController.php:346 msgid "Country in which the shipping time applies." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SupportedCountriesController.php:159 msgid "Include continents data if set to true." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.php:99 msgid "Successfully scheduled a job to update the number of syncable products." msgstr "" #: src/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.php:115 msgid "The number of products that are ready to be synced to Google." msgstr "" #: src/API/Site/Controllers/MerchantCenter/TargetAudienceController.php:143 msgid "Successfully updated the Target Audience settings." msgstr "" #: src/API/Site/Controllers/MerchantCenter/TargetAudienceController.php:165 msgid "The locale for the site." msgstr "" #: src/API/Site/Controllers/MerchantCenter/TargetAudienceController.php:176 msgid "The language to use for product listings." msgstr "" #: src/API/Site/Controllers/MerchantCenter/TargetAudienceController.php:236 msgid "Location where products will be shown." msgstr "" #: src/API/Site/Controllers/RestAPI/AuthController.php:152 msgid "Indicates the next page name mapped to the redirect URL when redirected back from Google WPCOM App authorization." msgstr "" #: src/API/Site/Controllers/RestAPI/AuthController.php:169 #: src/API/Site/Controllers/RestAPI/AuthController.php:198 msgid "The status of the merchant granting access to Google's WPCOM app" msgstr "" #: src/API/Site/Controllers/RestAPI/AuthController.php:176 msgid "The nonce provided by Google in the URL query parameter when Google redirects back to merchant's site" msgstr "" #: src/API/Site/Controllers/RestAPI/AuthController.php:193 msgid "The authorization URL for granting access to Google WPCOM App." msgstr "" #: src/API/Site/Controllers/ShippingRateSchemaTrait.php:28 msgid "The shipping rate unique identification number." msgstr "" #: src/API/Site/Controllers/ShippingRateSchemaTrait.php:50 msgid "The shipping rate." msgstr "" #: src/API/Site/Controllers/ShippingRateSchemaTrait.php:58 msgid "Array of options for the shipping method." msgstr "" #: src/API/Site/Controllers/ShippingRateSchemaTrait.php:66 msgid "Minimum price eligible for free shipping." msgstr "" #: src/API/Site/Controllers/TourController.php:100 msgid "Successfully updated the tour." msgstr "" #: src/API/Site/Controllers/TourController.php:105 msgid "Unable to updated the tour." msgstr "" #: src/API/Site/Controllers/TourController.php:132 msgid "Tour not found" msgstr "" #: src/API/Site/Controllers/TourController.php:146 msgid "The Id for the tour." msgstr "" #: src/API/Site/Controllers/TourController.php:153 msgid "Whether the tour was checked." msgstr "" #: src/Autoloader.php:51 msgid "Your installation of Google for WooCommerce is incomplete. If you installed from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment" msgstr "" #. translators: 1: is a link to a support document. 2: closing link #: src/Autoloader.php:63 msgid "Your installation of Google for WooCommerce is incomplete. If you installed from GitHub, %1$splease refer to this document%2$s to set up your development environment." msgstr "" #: src/Coupon/CouponSyncer.php:472 #: src/Product/ProductSyncer.php:362 msgid "Google Merchant Center has not been set up correctly. Please review your configuration." msgstr "" #: src/Coupon/CouponSyncer.php:487 msgid "Pushing Coupons will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings." msgstr "" #: src/Exception/AccountReconnect.php:27 msgid "Please reconnect your Jetpack account." msgstr "" #: src/Exception/AccountReconnect.php:44 msgid "Please reconnect your Google account." msgstr "" #: src/Exception/ApiNotReady.php:29 msgid "Please retry the request after the indicated number of seconds." msgstr "" #. translators: 1 the missing plugin name #: src/Exception/ExtensionRequirementException.php:36 msgid "Google for WooCommerce requires %1$s to be enabled." msgstr "" #. translators: 1 the incompatible plugin name #: src/Exception/ExtensionRequirementException.php:59 msgid "Google for WooCommerce is incompatible with %1$s." msgstr "" #. translators: 1 top level domain name. #: src/Exception/InvalidDomainName.php:28 msgid "Unable to create an account, the domain name \"%s\" must end with a valid top-level domain name." msgstr "" #. translators: 1 is the required component, 2 is the minimum required version, 3 is the version in use on the site #: src/Exception/InvalidVersion.php:40 msgid "Google for WooCommerce requires %1$s version %2$s or higher. You are using version %3$s." msgstr "" #. translators: 1 is the required component, 2 is the minimum required version #: src/Exception/InvalidVersion.php:67 msgid "Google for WooCommerce requires %1$s version %2$s or higher." msgstr "" #: src/Exception/InvalidVersion.php:85 msgid "Google for WooCommerce requires a 64 bit version of PHP." msgstr "" #. translators: Name/image of the client requesting authorization #: src/Integration/JetpackWPCOM.php:193 msgid "%s wants to access your site’s data. Log in to authorize that access." msgstr "" #: src/Integration/JetpackWPCOM.php:213 msgid "You must connect your Jetpack plugin to WordPress.com to use this feature." msgstr "" #: src/Integration/JetpackWPCOM.php:216 msgid "Someone may be trying to trick you into giving them access to your site. Or it could be you just encountered a bug :). Either way, please close this window." msgstr "" #. translators: %s is a URL #: src/Integration/JetpackWPCOM.php:226 msgid "Your site is incorrectly double-encoding redirects from http to https. This is preventing Jetpack from authenticating your connection. Please visit our support page for details about how to resolve this." msgstr "" #: src/Integration/JetpackWPCOM.php:323 msgid "The authorization process expired. Please go back and try again." msgstr "" #: src/Integration/YoastWooCommerceSeo.php:215 msgid "- Yoast SEO -" msgstr "" #: src/Integration/YoastWooCommerceSeo.php:224 msgid "GTIN Field" msgstr "" #: src/Integration/YoastWooCommerceSeo.php:233 msgid "MPN Field" msgstr "" #: src/Internal/Requirements/GoogleProductFeedValidator.php:84 msgid "The Google Product Feed plugin may cause conflicts or unexpected results." msgstr "" #: src/Internal/Requirements/GoogleProductFeedValidator.php:85 msgid "Deactivate the Google Product Feed plugin from your store" msgstr "" #: src/Jobs/JobException.php:24 msgid "Job item not found." msgstr "" #. translators: %s: the job item name #: src/Jobs/JobException.php:38 msgid "Required job item \"%s\" not provided." msgstr "" #. translators: %s: the job name #: src/Jobs/JobException.php:55 msgid "The \"%s\" job was stopped because its failure rate is above the allowed threshold." msgstr "" #. translators: %s: the job classname #: src/Jobs/JobException.php:72 msgid "The job \"%s\" does not exist." msgstr "" #: src/Menu/AttributeMapping.php:25 #: js/build/index.js:25 msgid "Attribute Mapping" msgstr "" #: src/Menu/ProductFeed.php:25 #: js/build/commons.js:60 #: js/build/index.js:25 #: js/build/product-feed.js:13 msgid "Product Feed" msgstr "" #: src/Menu/Reports.php:25 #: js/build/commons.js:60 #: js/build/index.js:25 msgid "Reports" msgstr "" #: src/Menu/SetupAds.php:25 msgid "Ads Setup Wizard" msgstr "" #: src/Menu/SetupMerchantCenter.php:25 msgid "MC Setup Wizard" msgstr "" #: src/Menu/Shipping.php:27 #: js/build/commons.js:60 #: js/build/index.js:25 msgid "Shipping" msgstr "" #: src/MerchantCenter/AccountService.php:186 msgid "Attempting invalid URL switch." msgstr "" #: src/MerchantCenter/AccountService.php:209 msgid "Attempting invalid claim overwrite." msgstr "" #. translators: 1: is a string representing an unknown step name #: src/MerchantCenter/AccountService.php:365 msgid "Unknown merchant account creation step %1$s" msgstr "" #: src/MerchantCenter/AccountService.php:397 msgid "Unable to claim website URL with this Merchant Center Account." msgstr "" #. translators: 1: is a numeric account ID #: src/MerchantCenter/AccountService.php:429 msgid "Merchant Center account already connected: %d" msgstr "" #. translators: 1: is a website URL (without the protocol) #: src/MerchantCenter/AccountService.php:478 msgid "This Merchant Center account already has a verified and claimed URL, %1$s" msgstr "" #: src/MerchantCenter/AccountService.php:566 msgid "No stored nonce found in the database, skip updating auth status." msgstr "" #: src/MerchantCenter/AccountService.php:572 msgid "Nonce is not provided, skip updating auth status." msgstr "" #: src/MerchantCenter/AccountService.php:579 msgid "Nonces mismatch, skip updating auth status." msgstr "" #: src/MerchantCenter/MerchantCenterService.php:291 msgid "No contact information." msgstr "" #: src/MerchantCenter/MerchantCenterService.php:292 msgid "Add store contact information" msgstr "" #: src/MerchantCenter/MerchantStatuses.php:147 msgid "The scheduled job has been paused due to a high failure rate." msgstr "" #: src/MerchantCenter/MerchantStatuses.php:276 msgid "Google account is not connected." msgstr "" #: src/MerchantCenter/MerchantStatuses.php:279 msgid "Merchant Center account is not set up." msgstr "" #: src/MerchantCenter/MerchantStatuses.php:497 msgid "All products" msgstr "" #: src/MerchantCenter/MerchantStatuses.php:589 msgid "Update this attribute in your product data" msgstr "" #: src/Notes/CompleteSetup.php:40 msgid "Reach more shoppers with free listings on Google" msgstr "" #: src/Notes/CompleteSetup.php:41 msgid "Finish setting up Google for WooCommerce to list your products on Google for free and promote them with ads." msgstr "" #: src/Notes/CompleteSetup.php:50 msgid "Finish setup" msgstr "" #: src/Notes/ContactInformation.php:41 msgid "Please add your contact information" msgstr "" #: src/Notes/ContactInformation.php:42 msgid "Google requires the phone number and store address for all stores using Google Merchant Center. This is required to verify your store, and it will not be shown to customers. If you do not add your contact information, your listings may not appear on Google." msgstr "" #: src/Notes/ContactInformation.php:51 msgid "Add contact information" msgstr "" #: src/Notes/LeaveReviewActionTrait.php:30 msgid "Leave a review" msgstr "" #: src/Notes/ReconnectWordPress.php:59 msgid "Re-connect your store to Google for WooCommerce" msgstr "" #: src/Notes/ReconnectWordPress.php:62 msgid "Your WordPress.com account has been disconnected from Google for WooCommerce. Connect your WordPress.com account again to ensure your products stay listed on Google through the Google for WooCommerce extension.

If you do not re-connect, any existing listings may be removed from Google." msgstr "" #: src/Notes/ReconnectWordPress.php:70 msgid "Go to Google for WooCommerce" msgstr "" #. translators: %s number of clicks #: src/Notes/ReviewAfterClicks.php:76 msgid "You’ve gotten %s+ clicks on your free listings! 🎉" msgstr "" #: src/Notes/ReviewAfterClicks.php:81 #: src/Notes/ReviewAfterConversions.php:83 msgid "Congratulations! Tell us what you think about Google for WooCommerce by leaving a review. Your feedback will help us make WooCommerce even better for you." msgstr "" #: src/Notes/ReviewAfterConversions.php:81 msgid "You got your first conversion on Google Ads! 🎉" msgstr "" #: src/Notes/SetupCampaign.php:48 msgid "Launch ads to drive traffic and grow sales" msgstr "" #: src/Notes/SetupCampaign.php:50 msgid "Your products are ready for Google Ads! Get your products shown on Google exactly when shoppers are searching for the products you offer. For new Google Ads accounts, get $500 in ad credit when you spend $500 within your first 60 days. T&Cs apply." msgstr "" #: src/Notes/SetupCampaign.php:57 #: src/Notes/SetupCampaignTwoWeeks.php:57 msgid "Set up Google Ads" msgstr "" #: src/Notes/SetupCampaign.php:63 msgid "Finish connecting your Google Ads account" msgstr "" #: src/Notes/SetupCampaign.php:65 msgid "Your products are ready for Google Ads! Finish connecting your account, create your campaign, pick your budget, and easily measure the impact of your ads. Plus, Google will give you $500 USD in ad credit when you spend $500 for new accounts. T&Cs apply." msgstr "" #: src/Notes/SetupCampaign.php:72 #: src/Notes/SetupCampaignTwoWeeks.php:74 msgid "Complete Setup" msgstr "" #: src/Notes/SetupCampaign.php:81 #: js/build/commons.js:10 #: js/build/commons.js:72 #: js/build/product-feed.js:1 #: js/build/settings.js:1 msgid "Learn more" msgstr "" #: src/Notes/SetupCampaignTwoWeeks.php:48 msgid "Reach more shoppers with Google Ads" msgstr "" #: src/Notes/SetupCampaignTwoWeeks.php:50 msgid "Your products are ready for Google Ads! Connect with the right shoppers at the right moment when they’re searching for products like yours. Connect your Google Ads account to create your first campaign." msgstr "" #: src/Notes/SetupCampaignTwoWeeks.php:64 msgid "Finish setting up your ads campaign and boost your sales" msgstr "" #: src/Notes/SetupCampaignTwoWeeks.php:67 msgid "You're just a few steps away from reaching new shoppers across Google. Finish connecting your account, create your campaign, pick your budget, and easily measure the impact of your ads." msgstr "" #: src/Notes/SetupCouponSharing.php:59 msgid "Show your store coupons on your Google listings" msgstr "" #: src/Notes/SetupCouponSharing.php:61 msgid "Sync your store promotions and coupons directly with Google to showcase on your product listings across the Google Shopping tab.

When creating a coupon, you’ll see a Channel Visibility settings box on the right; select \"Show coupon on Google\" to enable." msgstr "" #: src/Notes/SetupCouponSharing.php:74 msgid "Go to coupons" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:70 msgid "- Global attributes -" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:80 msgid "- Taxonomies -" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:96 msgid "Allow backorders setting" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:97 #: js/build/reports.js:4 msgid "Product title" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:98 msgid "SKU" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:99 msgid "Stock Qty" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:100 msgid "Stock Status" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:101 msgid "Tax class" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:102 msgid "Variation title" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:103 msgid "Weight (raw value, no units)" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:104 msgid "Weight (with units)" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:110 msgid "- Product fields -" msgstr "" #: src/Product/AttributeMapping/Traits/IsFieldTrait.php:133 msgid "- Custom Attributes -" msgstr "" #: src/Product/Attributes/Adult.php:72 #: src/Product/Attributes/AgeGroup.php:46 msgid "Adult" msgstr "" #: src/Product/Attributes/AgeGroup.php:42 msgid "Newborn" msgstr "" #: src/Product/Attributes/AgeGroup.php:43 msgid "Infant" msgstr "" #: src/Product/Attributes/AgeGroup.php:44 msgid "Toddler" msgstr "" #: src/Product/Attributes/AgeGroup.php:45 msgid "Kids" msgstr "" #: src/Product/Attributes/AgeGroup.php:79 msgid "Age group" msgstr "" #: src/Product/Attributes/Condition.php:51 msgid "New" msgstr "" #: src/Product/Attributes/Condition.php:52 msgid "Refurbished" msgstr "" #: src/Product/Attributes/Condition.php:53 msgid "Used" msgstr "" #: src/Product/Attributes/Gender.php:42 msgid "Male" msgstr "" #: src/Product/Attributes/Gender.php:43 msgid "Female" msgstr "" #: src/Product/Attributes/Gender.php:44 msgid "Unisex" msgstr "" #: src/Product/Attributes/GTIN.php:61 msgid "GTIN" msgstr "" #: src/Product/Attributes/IsBundle.php:72 msgid "Is Bundle" msgstr "" #: src/Product/Attributes/MPN.php:61 msgid "MPN" msgstr "" #: src/Product/Attributes/SizeSystem.php:51 msgid "US" msgstr "" #: src/Product/Attributes/SizeSystem.php:52 msgid "EU" msgstr "" #: src/Product/Attributes/SizeSystem.php:53 msgid "UK" msgstr "" #: src/Product/Attributes/SizeSystem.php:54 msgid "DE" msgstr "" #: src/Product/Attributes/SizeSystem.php:55 msgid "FR" msgstr "" #: src/Product/Attributes/SizeSystem.php:56 msgid "IT" msgstr "" #: src/Product/Attributes/SizeSystem.php:57 msgid "AU" msgstr "" #: src/Product/Attributes/SizeSystem.php:58 msgid "BR" msgstr "" #: src/Product/Attributes/SizeSystem.php:59 msgid "CN" msgstr "" #: src/Product/Attributes/SizeSystem.php:60 msgid "JP" msgstr "" #: src/Product/Attributes/SizeSystem.php:61 msgid "MEX" msgstr "" #: src/Product/Attributes/SizeSystem.php:84 msgid "Size System" msgstr "" #: src/Product/Attributes/SizeType.php:52 msgid "Regular" msgstr "" #: src/Product/Attributes/SizeType.php:53 msgid "Petite" msgstr "" #: src/Product/Attributes/SizeType.php:54 msgid "Plus" msgstr "" #: src/Product/Attributes/SizeType.php:55 msgid "Tall" msgstr "" #: src/Product/Attributes/SizeType.php:56 msgid "Big" msgstr "" #: src/Product/Attributes/SizeType.php:57 msgid "Maternity" msgstr "" #: src/Product/Attributes/SizeType.php:80 msgid "Size Type" msgstr "" #: src/Product/ProductSyncer.php:373 msgid "Pushing products will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings." msgstr "" #. translators: %1 is the shipping rate, %2 is the currency (e.g. USD) #: src/Shipping/GoogleAdapter/DBShippingSettingsAdapter.php:122 msgid "Flat rate - %1$s %2$s" msgstr "" #. translators: %1 is a random 4-digit string, %2 is the rate, %3 is the currency, %4 is the country code #: src/Shipping/GoogleAdapter/DBShippingSettingsAdapter.php:150 msgid "[%1$s] Google for WooCommerce generated service - %2$s %3$s to %4$s" msgstr "" #. translators: %1 is a random 4-digit string, %2 is the country code #: src/Shipping/GoogleAdapter/WCShippingSettingsAdapter.php:149 msgid "[%1$s] Google for WooCommerce generated service - %2$s" msgstr "" #: src/TaskList/CompleteSetupTask.php:53 msgid "Set up Google for WooCommerce" msgstr "" #: src/TaskList/CompleteSetupTask.php:74 msgid "20 minutes" msgstr "" #: src/Utility/WPCLIMigrationGTIN.php:89 msgid "No GTIN were migrated." msgstr "" #. Translators: %1$d is the number of migrated GTINS and %2$d is the execution time in seconds. #: src/Utility/WPCLIMigrationGTIN.php:96 msgid "%1$d GTIN was migrated in %2$d seconds." msgid_plural "%1$d GTIN were migrated in %2$d seconds." msgstr[0] "" msgstr[1] "" #: src/Value/ChannelVisibility.php:74 #: js/build/product-feed.js:7 #: js/build/product-feed.js:13 msgid "Sync and show" msgstr "" #: src/Value/ChannelVisibility.php:75 msgid "Don't Sync and show" msgstr "" #: views/attributes/tab-panel.php:21 msgid "As this is a variable product, you can add additional product attributes by going to Variations > Select one variation > Google for WooCommerce." msgstr "" #: views/bulk-edit/shop_coupon.php:19 msgid "Google visibility" msgstr "" #: views/bulk-edit/shop_coupon.php:24 msgid "— No change —" msgstr "" #: views/bulk-edit/shop_coupon.php:25 msgid "Show coupon" msgstr "" #: views/bulk-edit/shop_coupon.php:26 msgid "Don't show coupon" msgstr "" #: views/meta-box/channel_visibility.php:28 #: views/meta-box/coupon_channel_visibility.php:60 #: js/build/blocks.js:1 msgid "Issues detected" msgstr "" #: views/meta-box/channel_visibility.php:47 msgid "This product cannot be shown on any channel because it is hidden from your store catalog." msgstr "" #: views/meta-box/channel_visibility.php:77 #: js/build/blocks.js:1 msgid "Google sync status" msgstr "" #: views/meta-box/channel_visibility.php:81 #: js/build/blocks.js:1 msgid "Issues" msgstr "" #: views/meta-box/channel_visibility.php:93 #: js/build/blocks.js:1 msgid "Complete setup to get your products listed on Google for free." msgstr "" #: views/meta-box/channel_visibility.php:95 #: views/meta-box/coupon_channel_visibility.php:163 #: js/build/onboarding.js:1 msgid "Complete setup" msgstr "" #: views/meta-box/coupon_channel_visibility.php:62 msgid "Pending for sync" msgstr "" #: views/meta-box/coupon_channel_visibility.php:65 msgid "Sent to Google" msgstr "" #: views/meta-box/coupon_channel_visibility.php:73 msgid "Check your email for updates." msgstr "" #: views/meta-box/coupon_channel_visibility.php:89 msgid "This coupon cannot be shown on public channel because it is hidden from your store." msgstr "" #: views/meta-box/coupon_channel_visibility.php:90 msgid "This coupon cannot be shown because the coupon restrictions are not supported to share in Google channel." msgstr "" #: views/meta-box/coupon_channel_visibility.php:95 msgid "This coupon visibility channel has not been supported in your store base country yet." msgstr "" #: views/meta-box/coupon_channel_visibility.php:118 msgid "Show coupon on Google" msgstr "" #: views/meta-box/coupon_channel_visibility.php:122 msgid "Don't show coupon on Google" msgstr "" #: views/meta-box/coupon_channel_visibility.php:138 msgid "Coupon issues" msgstr "" #: views/meta-box/coupon_channel_visibility.php:162 msgid "Complete setup to get your coupon listed on Google for free." msgstr "" #: js/build/ads-onboarding.js:1 msgid "Set up your campaign" msgstr "" #: js/build/ads-onboarding.js:1 #: js/build/onboarding.js:1 msgid "Set up your accounts" msgstr "" #: js/build/ads-onboarding.js:1 msgid "Connect your Google account and your Google Ads account to set up a Performance Max campaign." msgstr "" #: js/build/ads-onboarding.js:1 #: js/build/onboarding.js:1 msgid "Connect accounts" msgstr "" #: js/build/ads-onboarding.js:1 msgid "Any campaigns created through this app will appear in your Google Ads account. You will be billed directly through Google." msgstr "" #: js/build/ads-onboarding.js:1 msgid "This Google account is connected to your store’s product feed." msgstr "" #: js/build/ads-onboarding.js:1 #: js/build/commons.js:97 #: js/build/onboarding.js:1 msgid "Continue" msgstr "" #: js/build/ads-onboarding.js:1 #: js/build/dashboard.js:4 msgid "You have unsaved campaign data. Are you sure you want to leave?" msgstr "" #: js/build/ads-onboarding.js:1 #: js/build/dashboard.js:7 msgid "Create your campaign" msgstr "" #: js/build/ads-onboarding.js:1 #: js/build/commons.js:72 #: js/build/dashboard.js:7 #: js/build/product-feed.js:13 msgid "Create campaign" msgstr "" #: js/build/attribute-mapping.js:1 msgid "Create attribute rules to control what product data gets sent to Google and to manage product attributes in bulk." msgstr "" #. translators: %d: number of categories. #: js/build/attribute-mapping.js:4 msgid "%d category" msgid_plural "%d categories" msgstr[0] "" msgstr[1] "" #: js/build/attribute-mapping.js:4 #: js/build/commons.js:17 msgid "All" msgstr "" #. translators: %d: The number of categories. #. translators: %d: The number of extra tags to show #: js/build/attribute-mapping.js:7 #: js/build/commons.js:17 msgid "+ %d more" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Only in" msgstr "" #: js/build/attribute-mapping.js:7 msgid "All except" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Select an option" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Set a fixed value" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Use value from existing product field" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Choose how to assign a value to the target attribute" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Auto-populate the target attribute with the value of the field you link it to." msgstr "" #: js/build/attribute-mapping.js:7 msgid "Can’t find an appropriate field? Create a new attribute" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Use fixed values to populate the target attribute with a value you specify. For example, you can enter a fixed value of 'White' to specify a single color for all your products." msgstr "" #: js/build/attribute-mapping.js:7 msgid "Enter a value" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Apply to all categories" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Apply to all categories EXCEPT" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Apply ONLY to these categories" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Type for search" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Select default value" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Select a Google attribute that you want to manage" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Select an attribute" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Manage attribute rule" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Create attribute rule" msgstr "" #: js/build/attribute-mapping.js:7 #: js/build/commons.js:35 #: js/build/commons.js:41 #: js/build/commons.js:57 #: js/build/onboarding.js:1 #: js/build/product-feed.js:4 msgid "Cancel" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Saving…" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Save rule" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Target attribute" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Assign value" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Categories" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Delete attribute rule?" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Deleting…" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Delete attribute rule" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Deleting a rule does’t affect any data that has already been submitted to Google." msgstr "" #: js/build/attribute-mapping.js:7 msgid "Product data is re-submitted to Google every 30 days to ensure that the information in your product listings are up-to-date." msgstr "" #: js/build/attribute-mapping.js:7 msgid "To ensure your products continue to be approved and promoted by Google, make sure that your product fields include all the required information." msgstr "" #: js/build/attribute-mapping.js:7 msgid "Scheduled for sync" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Never" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Last sync:" msgstr "" #: js/build/attribute-mapping.js:7 msgid "When an attribute rule is added or changed, data will be synced to Google Merchant Center via an async job. Note that it may take a while for the update to show up on Merchant Center, especially if it involves products that have not been synced and approved before." msgstr "" #: js/build/attribute-mapping.js:7 msgid "Target Attribute" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Data Source / Default Value" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Loading Attribute Mapping rules" msgstr "" #: js/build/attribute-mapping.js:7 msgid "You have no attribute rules" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Attribute Mapping configuration" msgstr "" #: js/build/attribute-mapping.js:7 #: js/build/commons.js:7 #: js/build/commons.js:10 #: js/build/commons.js:23 #: js/build/commons.js:29 #: js/build/commons.js:35 #: js/build/commons.js:41 #: js/build/dashboard.js:1 msgid "Edit" msgstr "" #: js/build/attribute-mapping.js:7 #: js/build/commons.js:17 #: js/build/commons.js:29 msgid "Delete" msgstr "" #: js/build/attribute-mapping.js:7 msgid "Manage attributes" msgstr "" #: js/build/blocks.js:1 msgid "This product cannot be shown on any channel because it is hidden from your store catalog. To enable this option, please change this product to be shown in the product catalog, and save the changes." msgstr "" #: js/build/commons.js:1 #: js/build/commons.js:97 #: js/build/product-feed.js:13 msgid "Google Logo" msgstr "" #: js/build/commons.js:1 msgid "Google Merchant Center Logo" msgstr "" #: js/build/commons.js:1 msgid "Google Ads Logo" msgstr "" #: js/build/commons.js:1 msgid "WordPress.com Logo" msgstr "" #: js/build/commons.js:1 msgid "Final URL icon" msgstr "" #: js/build/commons.js:1 msgid "Google" msgstr "" #: js/build/commons.js:1 #: js/build/reports.js:1 msgid "Google Merchant Center" msgstr "" #: js/build/commons.js:1 msgid "Required to sync products and list on Google." msgstr "" #: js/build/commons.js:1 #: js/build/dashboard.js:1 msgid "Google Ads" msgstr "" #: js/build/commons.js:1 msgid "Required to set up conversion measurement and create campaigns." msgstr "" #: js/build/commons.js:1 #: js/build/settings.js:1 msgid "Store address" msgstr "" #. translators: 1: account domain, 2: account ID. #. translators: 1: website URL, 2: account ID. #. translators: 1: the new URL, 2: account ID. #: js/build/commons.js:4 #: js/build/commons.js:47 #: js/build/commons.js:50 #: js/build/commons.js:53 #: js/build/commons.js:60 msgid "%1$s (%2$s)" msgstr "" #. translators: 1: number of character count. 2: the maximum number of character count. #: js/build/commons.js:7 msgid "%1$d/%2$d characters" msgstr "" #: js/build/commons.js:7 msgid "Connected" msgstr "" #: js/build/commons.js:7 #: js/build/dashboard.js:7 msgid "Loading…" msgstr "" #: js/build/commons.js:7 msgctxt "The field name of the address line in store address" msgid "address line" msgstr "" #: js/build/commons.js:7 msgctxt "The field name of the city in store address" msgid "city" msgstr "" #: js/build/commons.js:7 msgctxt "The field name of the country in store address" msgid "country/state" msgstr "" #: js/build/commons.js:7 msgctxt "The field name of the postcode in store address" msgid "postcode/zip" msgstr "" #. translators: %s: The missing field name of store address. #: js/build/commons.js:10 msgid "The %s of store address is required." msgstr "" #: js/build/commons.js:10 msgid "Update store address" msgstr "" #: js/build/commons.js:10 msgid "We’re using your store address for Google verification. This information won’t be public. Edit in WooCommerce settings if needed and update to review the changes." msgstr "" #: js/build/commons.js:10 msgid "Your store address is required by Google for verification. This information won’t be public. Complete that in WooCommerce settings and update to review the changes." msgstr "" #: js/build/commons.js:10 msgid "Please add your store address" msgstr "" #: js/build/commons.js:10 msgid "Google requires the store address for all stores using Google Merchant Center. " msgstr "" #: js/build/commons.js:10 msgid "Your contact information is required for verification by Google." msgstr "" #: js/build/commons.js:10 msgid "It would be shared with Google Merchant Center for store verification and would not be displayed to customers." msgstr "" #: js/build/commons.js:10 msgid "Contact information" msgstr "" #: js/build/commons.js:10 msgid "Pencil icon" msgstr "" #: js/build/commons.js:10 msgid "Note: The currency set in your Google Ads account is , which is different from your store currency, . Read more" msgstr "" #: js/build/commons.js:10 msgid "Unable to enable new product sync. Please try again later." msgstr "" #: js/build/commons.js:10 msgid "

We will soon transition to a new and improved method for synchronizing product data with Google.

Get early access" msgstr "" #. translators: 1: current page number 2: total number of pages #: js/build/commons.js:11 msgid "Frequently asked questions" msgstr "" #: js/build/commons.js:11 msgid "Check your maximum free credit" msgstr "" #: js/build/commons.js:11 msgid "Whatever you spend in the next month will be added back to your Google Ads account as free credit, up to a maximum limit depending on your store’s country." msgstr "" #: js/build/commons.js:11 msgid "Spend $500 to get $500 in Google Ads credits!" msgstr "" #: js/build/commons.js:11 msgid "New to Google Ads? Get $500 in ad credit when you spend $500 within your first 60 days. Check how much credit you can receive in your country here. Terms and conditions apply." msgstr "" #. translators: 1: Tag Label, 2: Current Tag index, 3: Total amount of tags. #: js/build/commons.js:14 msgid "%1$s (%2$d of %3$d)" msgstr "" #: js/build/commons.js:14 #: js/build/product-feed.js:1 msgid "Show less" msgstr "" #: js/build/commons.js:17 msgid "Start typing to filter countries…" msgstr "" #: js/build/commons.js:17 msgid "All countries" msgstr "" #: js/build/commons.js:17 msgid "Audience" msgstr "" #: js/build/commons.js:17 msgid "Where do you want to sell your products?" msgstr "" #: js/build/commons.js:17 msgid "Location" msgstr "" #: js/build/commons.js:17 msgid "Your store should already have the appropriate shipping and tax rates (if required) for potential customers in your selected location(s)." msgstr "" #: js/build/commons.js:17 msgid "Selected countries only" msgstr "" #: js/build/commons.js:17 msgid "Can’t find a country? Only supported countries can be selected." msgstr "" #: js/build/commons.js:17 msgid "Your listings will be shown in all supported countries." msgstr "" #: js/build/commons.js:17 #: js/build/commons.js:23 #: js/build/commons.js:29 msgid "Please specify at least one country." msgstr "" #: js/build/commons.js:17 msgid "Please enter the estimated shipping rate." msgstr "" #: js/build/commons.js:17 msgid "The estimated shipping rate cannot be less than 0." msgstr "" #: js/build/commons.js:17 msgid "Estimate a shipping rate" msgstr "" #: js/build/commons.js:17 #: js/build/commons.js:23 #: js/build/commons.js:29 msgid "If customer is in" msgstr "" #: js/build/commons.js:17 msgid "Then the estimated shipping rate displayed in the product listing is" msgstr "" #: js/build/commons.js:17 msgid "Add shipping rate" msgstr "" #: js/build/commons.js:17 msgid "Update shipping rate" msgstr "" #. translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. #: js/build/commons.js:20 msgid "Shipping rate for %1$s + %2$d more" msgstr "" #. translators: 1: list of country names separated by comma. #: js/build/commons.js:23 msgid "Shipping rate for %1$s" msgstr "" #: js/build/commons.js:23 msgid "Free shipping for all orders" msgstr "" #: js/build/commons.js:23 msgid "Estimated shipping rates" msgstr "" #: js/build/commons.js:23 msgid "Add another rate" msgstr "" #: js/build/commons.js:23 #: js/build/shipping.js:1 msgid "Shipping rates" msgstr "" #: js/build/commons.js:23 msgid "Your estimated shipping rates and times will be shown to potential customers on Google." msgstr "" #: js/build/commons.js:23 #: js/build/commons.js:29 #: js/build/settings.js:1 msgid "Read more" msgstr "" #: js/build/commons.js:23 msgid "Automatically sync my store’s shipping settings to Google." msgstr "" #: js/build/commons.js:23 msgid "My current settings and any future changes to my store’s shipping rates and classes will be automatically synced to Google Merchant Center." msgstr "" #: js/build/commons.js:23 msgid "My shipping settings are simple. I can manually estimate flat shipping rates." msgstr "" #: js/build/commons.js:23 msgid "My shipping settings are complex. I will enter my shipping rates and times manually in Google Merchant Center." msgstr "" #: js/build/commons.js:23 msgid "I understand that if I don’t set this up manually in Google Merchant Center, my products will be disapproved by Google." msgstr "" #: js/build/commons.js:23 msgid "Please enter the estimated shipping time." msgstr "" #: js/build/commons.js:23 msgid "The estimated shipping time cannot be less than 0." msgstr "" #: js/build/commons.js:23 #: js/build/commons.js:35 msgid "The minimum shipping time must not be more than the maximum shipping time." msgstr "" #: js/build/commons.js:23 msgid "Same Day" msgstr "" #: js/build/commons.js:23 msgid "day" msgid_plural "days" msgstr[0] "" msgstr[1] "" #: js/build/commons.js:23 msgid "Increment" msgstr "" #: js/build/commons.js:23 msgid "Decrement" msgstr "" #: js/build/commons.js:23 msgid "to" msgstr "" #: js/build/commons.js:23 #: js/build/commons.js:29 msgid "Estimate shipping time" msgstr "" #: js/build/commons.js:23 msgid "Add shipping time" msgstr "" #: js/build/commons.js:23 msgid "Then the estimated shipping times displayed in the product listing are:" msgstr "" #: js/build/commons.js:23 msgid "Add another time" msgstr "" #. translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. #: js/build/commons.js:26 msgid "Shipping time for %1$s + %2$d more" msgstr "" #. translators: 1: list of country names separated by comma. #: js/build/commons.js:29 msgid "Shipping time for %1$s" msgstr "" #: js/build/commons.js:29 msgid "Update shipping time" msgstr "" #: js/build/commons.js:29 msgid "Then the estimated shipping times displayed in the product listing are" msgstr "" #: js/build/commons.js:29 msgid "Estimated shipping times" msgstr "" #: js/build/commons.js:29 #: js/build/shipping.js:1 msgid "Shipping times" msgstr "" #: js/build/commons.js:29 msgid "Your shipping times will be shown to potential customers on Google." msgstr "" #: js/build/commons.js:29 msgid "Free shipping over a specific order value" msgstr "" #: js/build/commons.js:29 msgid "The minimum order amount must be greater than 0." msgstr "" #: js/build/commons.js:29 msgid "Minimum order to qualify for free shipping" msgstr "" #: js/build/commons.js:29 msgid "Then they qualify for free shipping if their order is over" msgstr "" #: js/build/commons.js:29 msgid "Add minimum order" msgstr "" #: js/build/commons.js:29 msgid "Update minimum order" msgstr "" #. translators: 1: list of country names separated by comma, up to 5 countries; 2: the remaining count of countries. #: js/build/commons.js:32 msgid "Minimum order for %1$s + %2$d more" msgstr "" #. translators: 1: list of country names separated by comma. #: js/build/commons.js:35 msgid "Minimum order for %1$s" msgstr "" #: js/build/commons.js:35 msgid "Only select if applicable" msgstr "" #: js/build/commons.js:35 msgid "Add another condition" msgstr "" #: js/build/commons.js:35 msgid "Order value condition" msgstr "" #: js/build/commons.js:35 msgid "Optional" msgstr "" #: js/build/commons.js:35 msgid "Please select a location option." msgstr "" #: js/build/commons.js:35 msgid "Please select at least one country." msgstr "" #: js/build/commons.js:35 msgid "Please select a shipping rate option." msgstr "" #: js/build/commons.js:35 msgid "Please specify estimated shipping rates for all the countries, and the rate cannot be less than 0." msgstr "" #: js/build/commons.js:35 msgid "Please enter minimum order for free shipping." msgstr "" #: js/build/commons.js:35 msgid "Please select a shipping time option." msgstr "" #: js/build/commons.js:35 msgid "Please specify estimated shipping times for all the countries, and the time cannot be less than 0." msgstr "" #: js/build/commons.js:35 msgid "Unable to connect your Google account. Please try again later." msgstr "" #: js/build/commons.js:35 msgid "Uh-oh! You did not allow WooCommerce sufficient access to your Google account. You must allow all required permissions in the Google authorization page to proceed. Read more" msgstr "" #: js/build/commons.js:35 msgid "Allow full access" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:41 msgid "Or, connect to a different Google account" msgstr "" #: js/build/commons.js:35 msgid "Connecting to a different Google account, please wait…" msgstr "" #: js/build/commons.js:35 msgid "Unable to connect to a different Google account. Please try again later." msgstr "" #: js/build/commons.js:35 msgid "Required to sync with Google Merchant Center and Google Ads." msgstr "" #: js/build/commons.js:35 msgid "You will be prompted to give WooCommerce access to your Google account. Please check all the checkboxes to give WooCommerce all required permissions. Read more" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:57 #: js/build/commons.js:97 msgid "Connect" msgstr "" #: js/build/commons.js:35 msgid "Or, connect to a different Google Ads account" msgstr "" #: js/build/commons.js:35 msgid "Account %s" msgstr "" #: js/build/commons.js:35 msgid "Claim your new Google Ads account to complete this setup." msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:41 msgid "Claim your Google Ads account" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:41 msgid "Claim account in Google Ads" msgstr "" #: js/build/commons.js:35 msgid "Claiming your account lets you access Google Ads and sets up conversion measurement. You must claim your account in the next 20 days." msgstr "" #: js/build/commons.js:35 msgid "When you claim your account, you’ll be asked to set up billing. This step is optional and you only need to complete it if you want to create Google Ads campaigns. If you don’t want to set up billing, close the window after you’ve clicked ‘Continue’ on the next page." msgstr "" #: js/build/commons.js:35 msgid "Create Google Ads Account" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:57 msgid "Create account" msgstr "" #: js/build/commons.js:35 msgid "By creating a Google Ads account, you agree to the following terms and conditions:" msgstr "" #: js/build/commons.js:35 msgid "You agree to comply with Google’s terms and policies, including Shopping ads policies and Google Ads Terms and Conditions." msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:57 msgid "I have read and accept these terms" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:41 #: js/build/commons.js:57 msgid "Creating…" msgstr "" #: js/build/commons.js:35 msgid "Updating…" msgstr "" #: js/build/commons.js:35 msgid "Claim Account" msgstr "" #: js/build/commons.js:35 msgid "Or, use your existing Google Ads account" msgstr "" #: js/build/commons.js:35 msgid "Connect to an existing account" msgstr "" #: js/build/commons.js:35 msgid "If you manage multiple sub-accounts in Google Ads, please connect the relevant sub-account, not a manager account. Learn more" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:57 msgid "Connecting…" msgstr "" #: js/build/commons.js:35 msgid "Unable to connect your Google Ads account. Please try again later." msgstr "" #: js/build/commons.js:35 msgid "Or, create a new Google Ads account" msgstr "" #: js/build/commons.js:35 msgid "Unable to get Google authorization page. Please try again later." msgstr "" #: js/build/commons.js:35 msgid "Conversion measurement has been set up. You can create a campaign later." msgstr "" #: js/build/commons.js:35 msgid "I accept the terms and conditions of Merchant Center and Google Ads" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:57 msgid "Yes, I want a new account" msgstr "" #: js/build/commons.js:35 msgid "Are you sure you want to create a new Google Ads account?" msgstr "" #: js/build/commons.js:35 msgid "You already have another Ads account associated with this Google account." msgstr "" #: js/build/commons.js:35 msgid "If you create a new Google Ads account, you will need to accept an invite to the account before it can be used." msgstr "" #: js/build/commons.js:35 msgid "Connect to existing Google Ads account" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:41 msgid "Required to set up conversion measurement for your store." msgstr "" #: js/build/commons.js:35 msgid "Creating a new Google Ads account" msgstr "" #: js/build/commons.js:35 msgid "Connecting your Google Ads account" msgstr "" #: js/build/commons.js:35 #: js/build/commons.js:57 msgid "This may take a few moments, please wait…" msgstr "" #. Translators: %s is the Merchant Center ID #: js/build/commons.js:38 msgid "Merchant Center ID: %s" msgstr "" #. Translators: %s is the Google Ads ID #: js/build/commons.js:41 msgid "Google Ads ID: %s" msgstr "" #: js/build/commons.js:41 msgid "You need to accept the invitation to the Google Ads account we created for you. This gives you access to Google Ads and sets up conversion measurement. You must claim your account in the next 20 days." msgstr "" #: js/build/commons.js:41 msgid "After accepting the invitation, you’ll be prompted to set up billing. We highly recommend doing this to avoid having to do it later on." msgstr "" #: js/build/commons.js:41 msgid "Google Ads conversion measurement has been set up for your store." msgstr "" #: js/build/commons.js:41 msgid "Unable to create Merchant Center account. Please try again later." msgstr "" #: js/build/commons.js:41 msgid "You don’t have Merchant Center nor Google Ads accounts, so we’re creating them for you." msgstr "" #: js/build/commons.js:41 msgid "Merchant Center is required to sync products so they show on Google. Google Ads is required to set up conversion measurement for your store." msgstr "" #: js/build/commons.js:41 msgid "You don’t have Google Ads account, so we’re creating one for you." msgstr "" #: js/build/commons.js:41 msgid "You don’t have Merchant Center account, so we’re creating one for you." msgstr "" #: js/build/commons.js:41 #: js/build/commons.js:57 msgid "Required to sync products so they show on Google." msgstr "" #. translators: 1: account name, 2: account domain, 3: account ID. #: js/build/commons.js:44 msgid "%1$s ・ %2$s (%3$s)" msgstr "" #: js/build/commons.js:50 #: js/build/commons.js:53 msgid "Switch account" msgstr "" #: js/build/commons.js:50 msgid "Reclaim your URL" msgstr "" #: js/build/commons.js:50 msgid "Your URL is currently claimed by another Merchant Center account." msgstr "" #: js/build/commons.js:50 msgid "Reclaim my URL" msgstr "" #: js/build/commons.js:50 msgid "If you reclaim this URL, it will cause any existing product listings or ads to stop running, and the other verified account will be notified that they have lost their claim. Learn more." msgstr "" #: js/build/commons.js:50 msgid "We were unable to reclaim this URL. You may not have permission to reclaim this URL, or an error might have occurred. Try again later or contact your Google account administrator." msgstr "" #: js/build/commons.js:53 #: js/build/commons.js:56 msgid "Switch to this new URL" msgstr "" #. translators: %s: claimed URL. #: js/build/commons.js:56 msgid "This Merchant Center account already has a verified and claimed URL, %s." msgstr "" #: js/build/commons.js:56 msgid "Unable to switch to your new URL. Please try again later." msgstr "" #. translators: 1: new URL. 2: claimed URL. #: js/build/commons.js:57 msgid "If you switch your claimed URL to %1$s, you will lose your claim to %2$s. This will cause any existing product listings tied to %2$s to stop running." msgstr "" #: js/build/commons.js:57 msgid "Create Google Merchant Center Account" msgstr "" #: js/build/commons.js:57 msgid "Are you sure you want to create a new Google Merchant Center account?" msgstr "" #: js/build/commons.js:57 msgid "You already have another verified account, , which is connected to this store’s URL, ." msgstr "" #: js/build/commons.js:57 msgid "If you create a new Google Merchant Center account, you will have to reclaim this store’s URL with the new account. This will cause any existing product listings or ads to stop running, and the other verified account will lose its claim." msgstr "" #: js/build/commons.js:57 msgid "By creating a Google Merchant Center account, you agree to the following terms and conditions:" msgstr "" #: js/build/commons.js:57 msgid "You agree to comply with Google’s terms and policies, including Google Merchant Center Terms of Service." msgstr "" #: js/build/commons.js:57 msgid "Or, connect to a different Google Merchant Center account" msgstr "" #: js/build/commons.js:57 msgid "Disconnecting your Google Merchant Center account, please wait…" msgstr "" #: js/build/commons.js:57 msgid "Unable to disconnect your Google Merchant Center account. Please try again later." msgstr "" #: js/build/commons.js:57 msgid "Or, create a new Merchant Center account" msgstr "" #: js/build/commons.js:57 msgid "Unable to connect Merchant Center account. Please try again later." msgstr "" #: js/build/commons.js:57 msgid "Connect to existing Merchant Center account" msgstr "" #: js/build/commons.js:60 msgid "Grant access" msgstr "" #: js/build/commons.js:60 msgid "Google has been granted access to fetch your product data." msgstr "" #: js/build/commons.js:60 msgid "There was an issue granting access to Google for fetching your products." msgstr "" #: js/build/commons.js:60 msgid "Disabling the new Product Sync feature, please wait…" msgstr "" #: js/build/commons.js:60 msgid "Unable to disable new product sync. Please try again later." msgstr "" #: js/build/commons.js:60 msgid "Disable product data fetch" msgstr "" #: js/build/commons.js:60 msgid "Help" msgstr "" #: js/build/commons.js:60 msgid "Open popover" msgstr "" #: js/build/commons.js:60 msgid "Before you start the migration…" msgstr "" #: js/build/commons.js:60 #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Never mind" msgstr "" #: js/build/commons.js:60 msgid "GTIN Migration was successfully scheduled." msgstr "" #: js/build/commons.js:60 msgid "Unable to start GTIN Migration." msgstr "" #: js/build/commons.js:60 msgid "Start migration" msgstr "" #: js/build/commons.js:60 msgid "This migration will copy all GTIN numbers set in the Google for WooCommerce Product tab into the new GTIN field under the Product Inventory tab. If you have already set GTIN numbers in some of your products' Inventory tab, they will not be overridden. The GTIN numbers in the Google for WooCommerce tab will not be removed. The migration will run in the background and is not reversible. You can check the migration process on the WooCommerce Scheduled Actions page." msgstr "" #: js/build/commons.js:60 msgid "The GTIN field managed by WooCommerce in the Product's inventory section, will now be used by Google for WooCommerce. It will continue to support the previous field and any mapping rules you have setup for the GTIN field. If you would like to migrate the data click here." msgstr "" #: js/build/commons.js:60 msgid "Your GTIN Migration is now running in the background. You can check the migration process on the WooCommerce Scheduled Actions page" msgstr "" #: js/build/commons.js:60 #: js/build/index.js:25 msgid "Dashboard" msgstr "" #: js/build/commons.js:60 msgid "Attributes" msgstr "" #: js/build/commons.js:60 msgid "Add campaign" msgstr "" #: js/build/commons.js:60 msgid "Unable to complete your Google Ads account setup. Please try again later." msgstr "" #: js/build/commons.js:60 msgid "You do not have billing information set up in your Google Ads account. Once you have set up billing, you can start running ads." msgstr "" #: js/build/commons.js:60 msgid "You will be directed to Google Ads for this step. In case your browser is unable to open the pop-up, click here instead ." msgstr "" #: js/build/commons.js:60 msgid "Set up billing" msgstr "" #: js/build/commons.js:60 msgid "Billing method for Google Ads added successfully" msgstr "" #. translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount. #: js/build/commons.js:63 msgid "We recommend running campaigns for at least 1 month so it can learn to optimize for your business.
Tip: Most merchants targeting similar countries set a daily budget of %1$f %2$s" msgstr "" #. translators: it's a range of recommended budget amount. 1: the value of the budget, 2: the currency of amount 3: a country name selected by the merchant. #: js/build/commons.js:66 msgid "We recommend running campaigns for at least 1 month so it can learn to optimize for your business.
Tip: Most merchants targeting %3$s set a daily budget of %1$f %2$s" msgstr "" #: js/build/commons.js:66 msgid "With a budget lower than your competitor range, your campaign may not get noticeable results." msgstr "" #: js/build/commons.js:66 msgid "Set your budget" msgstr "" #: js/build/commons.js:66 msgid "With Performance Max campaigns, you can set your own budget and Google’s Smart Bidding technology will serve the most appropriate ad, with the optimal bid, to maximize campaign performance. You only pay when people click on your ads, and you can start or stop your campaign whenever you want." msgstr "" #: js/build/commons.js:66 msgid "Daily average cost" msgstr "" #: js/build/commons.js:66 msgid "Monthly max, estimated" msgstr "" #: js/build/commons.js:66 msgid "How does Google Ads work?" msgstr "" #: js/build/commons.js:66 msgid "Google Ads works by displaying your ad when people search online for the products and services you offer. By leveraging smart technology, Google Ads helps get your ads in front of potential customers at just the moment they’re ready to take action." msgstr "" #: js/build/commons.js:66 msgid "What is a product feed?" msgstr "" #: js/build/commons.js:66 msgid "Your product feed is the central data source that contains a list of products you want to advertise through Merchant Center. By default, Google syncs all active products from your WooCommerce inventory. You can choose to exclude products later after this setup." msgstr "" #: js/build/commons.js:66 msgid "How much does Google Ads cost?" msgstr "" #: js/build/commons.js:66 msgid "With Google Ads, you decide how much to spend. There’s no minimum spend, and no time commitment. Your costs may vary from day to day, but you won’t be charged more than your daily budget times the number of days in a month. You pay only for the actual clicks and calls that your ad receives." msgstr "" #: js/build/commons.js:66 #: js/build/get-started-page.js:1 msgid "Where will my products appear?" msgstr "" #: js/build/commons.js:66 msgid "If you’re selling in the US, then eligible free listings can appear in search results across Google Search, Google Images, and the Google Shopping tab. If you’re selling outside the US, free listings will appear on the Shopping tab." msgstr "" #: js/build/commons.js:66 msgid "If you’re running a Performance Max Campaign, your approved products can appear on Google Search, Google Maps, the Shopping tab, Gmail, Youtube, the Google Display Network, and Discover feed." msgstr "" #: js/build/commons.js:66 msgid "How long until I see results with Google Ads?" msgstr "" #: js/build/commons.js:66 msgid "Google’s Performance Max campaigns are powered by machine learning models. These models train and adapt based on the data you provide in your campaign. This means performance optimization can take time. Typically, this learning process takes 1—2 weeks." msgstr "" #: js/build/commons.js:66 msgid "Set a daily budget, and only pay when people click on your ads." msgstr "" #: js/build/commons.js:66 msgid "Recommended" msgstr "" #: js/build/commons.js:66 msgid "Performance Max campaign" msgstr "" #: js/build/commons.js:66 msgid "Performance Max uses the best of Google’s AI to show the most impactful ads for your products at the right time and place. Google will use your product data to create ads for this campaign. " msgstr "" #: js/build/commons.js:66 msgid "Learn more about Performance Max" msgstr "" #: js/build/commons.js:66 msgid "Drive more sales with Performance Max" msgstr "" #: js/build/commons.js:66 #: js/build/get-started-page.js:1 msgid "Reach more customers by advertising your products across Google Ads channels like Search, YouTube and Discover. Set up your campaign now so your products are included as soon as they’re approved." msgstr "" #: js/build/commons.js:66 msgid "Performance Max campaigns are automatically optimized for you by Google. See what your ads will look like." msgstr "" #: js/build/commons.js:66 msgid "You’re ready to set up a Performance Max campaign to drive more sales with ads. Your products will be included in the campaign after they’re approved." msgstr "" #: js/build/commons.js:66 msgid "Select final URL" msgstr "" #: js/build/commons.js:66 msgid "Search page" msgstr "" #: js/build/commons.js:66 msgid "SUGGESTIONS" msgstr "" #: js/build/commons.js:66 msgid "No matching results" msgstr "" #: js/build/commons.js:66 #: js/build/commons.js:69 msgid "Select" msgstr "" #: js/build/commons.js:66 msgid "Unable to load assets data from the selected page." msgstr "" #: js/build/commons.js:66 msgid "Choose a page that you want people to reach after clicking your ad. This might be your homepage, or a more specific page." msgstr "" #: js/build/commons.js:66 msgid "Or, select a different Final URL" msgstr "" #. translators: 1: Minimum width, 2: Minimum height. #: js/build/commons.js:69 msgid "Image size needs to be at least %1$d x %2$d" msgstr "" #: js/build/commons.js:69 msgid "Select or upload image" msgstr "" #: js/build/commons.js:69 msgid "Replace image" msgstr "" #: js/build/commons.js:69 msgid "Remove image" msgstr "" #: js/build/commons.js:69 msgid "Add image" msgstr "" #: js/build/commons.js:69 msgid "Remove text" msgstr "" #: js/build/commons.js:69 msgid "Add text" msgstr "" #. translators: %d: number of issues in an asset field. #: js/build/commons.js:72 msgid "%d issue" msgid_plural "%d issues" msgstr[0] "" msgstr[1] "" #: js/build/commons.js:72 msgctxt "A label behind the heading to indicate a field is optional" msgid "(Optional)" msgstr "" #: js/build/commons.js:72 msgid "Toggle asset" msgstr "" #: js/build/commons.js:72 msgid "Call to action" msgstr "" #: js/build/commons.js:72 msgid "Select a call to action that aligns with your goals, or use automated call to action which allows Google to automatically choose the most relevant call to action for you." msgstr "" #: js/build/commons.js:72 msgid "Display URL Path" msgstr "" #: js/build/commons.js:72 msgid "The display URL gives potential customers a clear idea of what webpage they'll reach once they click your ad, so your path text should describe your ad's landing page." msgstr "" #: js/build/commons.js:72 msgid "To create your display URL, Google Ads will combine the domain (for example, \"www.google.com\" in www.google.com/nonprofits) from your final URL and the path text (for example, \"nonprofits\" in www.google.com/nonprofits)." msgstr "" #: js/build/commons.js:72 msgid "Add additional assets (Optional)" msgstr "" #: js/build/commons.js:72 msgid "Upload text and image assets to effectively reach and engage your target shoppers. Google will mix and match your assets, continually testing combinations to create personalized and optimal shopping experiences." msgstr "" #: js/build/commons.js:72 msgid "We auto-populated assets directly from your Final URL. We encourage you to edit or add more in order to best showcase your business." msgstr "" #: js/build/commons.js:72 msgid "What will my ads look like?" msgstr "" #: js/build/commons.js:72 msgid "Google will generate text ads and responsive display ads in various combinations and formats from the headlines, images, and descriptions you add. Your ads will automatically adjust their size, appearance, and format to fit available ad spaces. See common ad formats" msgstr "" #: js/build/commons.js:72 msgid "What makes these ads different from product ads?" msgstr "" #: js/build/commons.js:72 msgid "Text and image assets can elevate your campaign by offering a variety of ad combinations that capture your audience's attention and generate maximum engagement. By leveraging Google's asset-mixing technology, your ads can be optimized to deliver the right message, to the right people, at the right time." msgstr "" #: js/build/commons.js:72 msgid "Compared to product ads—which showcase individual products and are designed to drive direct sales and revenue— ads with creative assets are typically used to highlight your business, generate interest, and attract new customers. While both types of ads can drive conversions, using them together can generate even greater results." msgstr "" #: js/build/commons.js:72 #: js/build/commons.js:97 #: js/build/dashboard.js:7 msgid "Optimize your campaign" msgstr "" #: js/build/commons.js:72 msgid "Drive greater performance by adding text and image assets to create personalized and engaging ads" msgstr "" #: js/build/commons.js:72 msgid "Skip this step" msgstr "" #: js/build/commons.js:72 #: js/build/shipping.js:1 msgid "Save changes" msgstr "" #. translators: %1$s: minimum daily budget #: js/build/commons.js:73 msgid "Please make sure daily average cost is at least %s" msgstr "" #: js/build/commons.js:73 msgid "Please make sure daily average cost is greater than 0." msgstr "" #. translators: 1: The minimal number of this item. 2: Asset field name. #: js/build/commons.js:76 msgid "Add at least %1$d %2$s image" msgid_plural "Add at least %1$d %2$s images" msgstr[0] "" msgstr[1] "" #. translators: Asset field name. #: js/build/commons.js:79 msgid "The %s in the first field is required" msgstr "" #. translators: 1: Asset field name. #: js/build/commons.js:82 msgid "The %1$s is required" msgstr "" #. translators: 1: Asset field name. 2: The minimal number of this item. #: js/build/commons.js:85 msgid "Add at least %2$d %1$s" msgstr "" #. translators: Asset field name. #: js/build/commons.js:88 msgid "%s are identical" msgstr "" #: js/build/commons.js:88 msgid "Character limit exceeded" msgstr "" #. translators: 1: Asset field name. 2: The sequential number of the asset field. #: js/build/commons.js:91 msgid "%1$s %2$d: Character limit exceeded" msgstr "" #. translators: Asset field name. #: js/build/commons.js:94 msgid "%s is incomplete" msgstr "" #. translators: Asset field name. #: js/build/commons.js:97 msgid "%s: Character limit exceeded" msgstr "" #: js/build/commons.js:97 msgctxt "A sample product title for demonstrating the paid ads shown on Google services." msgid "White tee" msgstr "" #: js/build/commons.js:97 msgctxt "A sample product price for demonstrating the paid ads shown on Google services." msgid "$10.00" msgstr "" #: js/build/commons.js:97 msgctxt "A sample name of an online shop for demonstrating the paid ads shown on Google services." msgid "Colleen's Tee Store" msgstr "" #: js/build/commons.js:97 msgid "Google Shopping Logo" msgstr "" #: js/build/commons.js:97 msgid "YouTube Logo" msgstr "" #: js/build/commons.js:97 msgid "LEARN MORE" msgstr "" #: js/build/commons.js:97 msgid "Simulated the info and close buttons at the corner of a Google ad" msgstr "" #: js/build/commons.js:97 msgid "Gmail Logo" msgstr "" #: js/build/commons.js:97 msgid "Preview product ad" msgstr "" #: js/build/commons.js:97 msgid "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." msgstr "" #: js/build/commons.js:97 msgctxt "A highlighting label behind the heading of the new feature" msgid "New" msgstr "" #: js/build/commons.js:97 msgid "Add images, headlines, and descriptions to drive better engagement and more sales." msgstr "" #: js/build/commons.js:97 msgid "Edit your campaign to explore this new feature." msgstr "" #: js/build/commons.js:97 msgid "New name, same great solution" msgstr "" #: js/build/commons.js:97 msgid "Google Listings & Ads is now Google for WooCommerce." msgstr "" #: js/build/commons.js:97 msgid "Successfully connected through Jetpack" msgstr "" #: js/build/commons.js:97 msgid "Required to connect with Google" msgstr "" #: js/build/commons.js:97 msgid "Unable to connect your WordPress.com account. Please try again later." msgstr "" #: js/build/commons.js:97 msgid "Unable to complete your ads setup. Please try again later." msgstr "" #: js/build/commons.js:97 msgctxt "the separator for concatenating the categories where the Attribute mapping rule is applied." msgid ", " msgstr "" #. translators: %d: number of categories. #: js/build/commons.js:100 msgid "Category ID %s (deleted)" msgstr "" #: js/build/commons.js:100 msgid "Error creating account: Account creation limit reached. Contact support for help." msgstr "" #: js/build/commons.js:100 msgid "Unable to create Google Ads account. Please try again later." msgstr "" #: js/build/commons.js:100 msgctxt "the separator for concatenating the messages of failed actions" msgid ", " msgstr "" #. translators: 1: optional string when there are multiple failed actions, and it's a concatenated text of failed actions except for the last one. 2: the last one or the only failed action. #: js/build/commons.js:103 msgid "There is an error in the following action: %1$s%2$s." msgid_plural "There are errors in the following actions: %1$s and %2$s." msgstr[0] "" msgstr[1] "" #. translators: text for the failed action(s). #: js/build/commons.js:106 msgid "%s Other changes have been saved. Please try again later." msgstr "" #. translators: text for the failed action(s). #: js/build/commons.js:109 msgid "%s Please try again later." msgstr "" #: js/build/dashboard.js:1 msgid "Claim $500 in ads credit when you spend your first $500 with Google Ads. Terms and conditions apply." msgstr "" #: js/build/dashboard.js:1 msgid "Reach more customer by advertising your products across Google Ads channels like Search, YouTube and Discover." msgstr "" #: js/build/dashboard.js:1 msgid "Set a daily budget and only pay when people click on your ads." msgstr "" #: js/build/dashboard.js:1 msgid "Performance Max uses the best of Google's AI to show the most impactful ads for your products at the right time and place. Learn more about Performance Max technology." msgstr "" #: js/build/dashboard.js:1 #: js/build/product-feed.js:19 msgid "Create Campaign" msgstr "" #: js/build/dashboard.js:1 msgid "We're having trouble loading this data. Try again later, or track your performance in Google Merchant Center." msgstr "" #: js/build/dashboard.js:1 msgid "Open Google Merchant Center" msgstr "" #: js/build/dashboard.js:1 #: js/build/reports.js:4 msgid "Clicks" msgstr "" #: js/build/dashboard.js:1 #: js/build/reports.js:4 msgid "Total Spend" msgstr "" #: js/build/dashboard.js:1 #: js/build/dashboard.js:4 msgid "Free" msgstr "" #: js/build/dashboard.js:1 msgid "We're having trouble loading this data. Try again later, or track your performance in Google Ads." msgstr "" #: js/build/dashboard.js:1 msgid "Open Google Ads" msgstr "" #: js/build/dashboard.js:1 #: js/build/reports.js:4 msgid "Total Sales" msgstr "" #: js/build/dashboard.js:1 msgid "Free Listings (Limited Visibility)" msgstr "" #: js/build/dashboard.js:1 msgid "Create another campaign" msgstr "" #: js/build/dashboard.js:1 msgid "Got it" msgstr "" #: js/build/dashboard.js:1 msgid "Drawing of a person who successfuly launched a campaign" msgstr "" #: js/build/dashboard.js:1 msgid "You've set up a Performance Max Campaign!" msgstr "" #: js/build/dashboard.js:1 msgid "You can pause or edit your campaign at any time. For best results, we recommend allowing your campaign to run for at least 14 days without pausing or editing. Learn more about Performance Max technology." msgstr "" #: js/build/dashboard.js:1 msgid "Permanently Remove?" msgstr "" #: js/build/dashboard.js:1 msgid "Keep Campaign" msgstr "" #: js/build/dashboard.js:1 msgid "Remove Campaign" msgstr "" #: js/build/dashboard.js:1 msgid "Results typically improve with time. Removing a campaign will result in the loss of any optimisations learned from those campaigns." msgstr "" #: js/build/dashboard.js:1 msgid "Once a campaign is removed, it cannot be re-enabled." msgstr "" #: js/build/dashboard.js:1 msgid "Remove" msgstr "" #: js/build/dashboard.js:1 msgid "Before you edit…" msgstr "" #: js/build/dashboard.js:1 msgid "Don't edit" msgstr "" #: js/build/dashboard.js:1 msgid "Continue to edit" msgstr "" #: js/build/dashboard.js:1 #: js/build/shipping.js:1 msgid "Results typically improve with time." msgstr "" #: js/build/dashboard.js:1 msgid "Editing will result in the loss of any optimisations learned over time." msgstr "" #: js/build/dashboard.js:1 msgid "We recommend allowing your programs to run for at least 14 days after set up, without pausing or editing, for optimal performance." msgstr "" #: js/build/dashboard.js:1 msgid "Before you pause…" msgstr "" #: js/build/dashboard.js:1 msgid "Keep Active" msgstr "" #: js/build/dashboard.js:1 msgid "Pause Campaign" msgstr "" #: js/build/dashboard.js:1 msgid "Results typically improve with time. If you pause, your products won’t be shown to people looking for what you offer." msgstr "" #: js/build/dashboard.js:1 msgid "Pausing a campaign will result in the loss of any optimisations learned from those campaigns." msgstr "" #: js/build/dashboard.js:1 msgid "Free listings cannot be paused through WooCommerce. Go to Google Merchant Center for advanced settings." msgstr "" #: js/build/dashboard.js:1 #: js/build/reports.js:4 msgid "Program" msgstr "" #: js/build/dashboard.js:1 msgid "Country" msgstr "" #: js/build/dashboard.js:1 msgid "Daily budget" msgstr "" #: js/build/dashboard.js:1 msgid "Enabled" msgstr "" #. translators: %d: number of countries, with minimum value of 1. #: js/build/dashboard.js:4 msgid " + %d more" msgstr "" #: js/build/dashboard.js:4 #: js/build/reports.js:4 msgid "Free listings" msgstr "" #: js/build/dashboard.js:4 #: js/build/reports.js:4 msgid "Programs" msgstr "" #. translators: %s: campaign's name. #: js/build/dashboard.js:7 msgid "Edit %s" msgstr "" #: js/build/dashboard.js:7 msgid "Edit campaign" msgstr "" #: js/build/dashboard.js:7 msgid "Edit your campaign" msgstr "" #: js/build/dashboard.js:7 msgid "Edit Campaign" msgstr "" #: js/build/dashboard.js:7 msgid "Error in loading your ads campaign. Please try again later." msgstr "" #: js/build/dashboard.js:7 msgid "You’ve successfully created a campaign!" msgstr "" #: js/build/dashboard.js:7 msgid "View Reports" msgstr "" #: js/build/dashboard.js:7 msgid "How easy was it to create a Google Ad campaign?" msgstr "" #: js/build/dashboard.js:7 msgid "How easy was it to understand the requirements for the Google Ad campaign creation?" msgstr "" #: js/build/get-started-page.js:1 msgid "Google for WooCommerce Benefits" msgstr "" #: js/build/get-started-page.js:1 msgid "Reach your sales goals by creating a campaign" msgstr "" #: js/build/get-started-page.js:1 msgid "An image of a quote." msgstr "" #: js/build/get-started-page.js:1 msgid "21,000+ WooCommerce store owners like you already list products with Google" msgstr "" #: js/build/get-started-page.js:1 msgid "Thank you Google and WooCommerce for creating this app. It’s so simple to use and connect your products to Merchant Center." msgstr "" #: js/build/get-started-page.js:1 msgid "joshualukewhite" msgstr "" #: js/build/get-started-page.js:1 msgid "It does everything smoothly. Perfect and must need add-on from WooCommerce. Some things are just “essentials”." msgstr "" #: js/build/get-started-page.js:1 msgid "Anonymous" msgstr "" #: js/build/get-started-page.js:1 msgid "What is Google Merchant Center?" msgstr "" #: js/build/get-started-page.js:1 msgid "Google Merchant Center is like a digital storefront for your products on Google. It's where you upload and manage information about your products, like titles, descriptions, images, prices, and availability. This data is used to create product listings that can appear across Google." msgstr "" #: js/build/get-started-page.js:1 msgid "Why should I connect to Google Merchant Center?" msgstr "" #: js/build/get-started-page.js:1 msgid "By syncing your product information to Google Merchant Center, your products can appear in relevant Google searches, Shopping tab, image searches, and even on other platforms like YouTube. When running Performance Max campaigns, Google Merchant Center ensures that shoppers see the most up-to-date and accurate information about your product feed, reducing confusion and improving the chances of a purchase." msgstr "" #: js/build/get-started-page.js:1 msgid "Will my deals and promotions display on Google?" msgstr "" #: js/build/get-started-page.js:1 msgid "To show your coupons and promotions on Google Shopping listings, make sure you’re using the latest version of Google for WooCommerce. When you create or update a coupon in your WordPress dashboard under Marketing > Coupons, you’ll see a Channel Visibility settings box on the right: select “Show coupon on Google” to enable it. Learn more about managing promotions for Google for WooCommerce. This feature is currently available in Australia, Canada, Germany, France, India, the United Kingdom, and the United States." msgstr "" #: js/build/get-started-page.js:1 msgid "What is Product Sync?" msgstr "" #: js/build/get-started-page.js:1 msgid "Product Sync is a feature fully integrated into WooCommerce’s management platform that automatically lets you sync your product feed to Google Merchant Center. It will sync all your WooCommerce product data, and you can also add or edit products individually or in bulk. To ensure products are approved by Google, check that your product feed includes the following information:" msgstr "" #: js/build/get-started-page.js:1 msgid "General product information" msgstr "" #: js/build/get-started-page.js:1 msgid "Unique product identifiers" msgstr "" #: js/build/get-started-page.js:1 msgid "Data requirements for specific categories (auto-assigned by Google):" msgstr "" #: js/build/get-started-page.js:1 msgid "Apparel & Accessories" msgstr "" #: js/build/get-started-page.js:1 msgid "Media" msgstr "" #: js/build/get-started-page.js:1 msgid "Books" msgstr "" #: js/build/get-started-page.js:1 msgid "Where do I manage my product feed and my Google Ads campaigns?" msgstr "" #: js/build/get-started-page.js:1 msgid "You can manage and edit all of your products and your Google Ads campaigns right from your WooCommerce dashboard and on the WooCommerce Mobile App." msgstr "" #: js/build/get-started-page.js:1 msgid "Once you start running a Performance Max ads campaign, your approved products will reach more shoppers to help grow your business by being shown on Google Search, Google Maps, the Shopping tab, Gmail, Youtube, the Google Display Network, and Discover feed." msgstr "" #: js/build/get-started-page.js:1 msgid "What are Performance Max campaigns?" msgstr "" #: js/build/get-started-page.js:1 msgid "Performance Max campaigns help you combine your expertise with Google AI to reach your most valuable customers and drive sales. Just set your goals and budget and Google AI will get your ads seen by the right customers at the right time across Google Search, Google Maps, the Shopping tab, Gmail, Youtube, the Google Display Network, and Discover feed." msgstr "" #: js/build/get-started-page.js:1 msgid "How much do Performance Max campaigns cost?" msgstr "" #: js/build/get-started-page.js:1 msgid "Performance Max campaigns are pay-per-click, meaning you only pay when someone clicks on your ads. To get the best results and ensure your products reach the right customers, we recommend starting with the suggested Google for WooCommerce minimum daily budget for your Performance Max campaign. This helps jumpstart your campaign and drive early conversions. You can always adjust your budget later as you see what works best for your business." msgstr "" #: js/build/get-started-page.js:1 msgid "Can I sync my products and run Performance Max campaigns on Google for WooCommerce at the same time?" msgstr "" #: js/build/get-started-page.js:1 msgid "Yes, you can run both at the same time, and we recommend you do! Once you sync your store it’s automatically listed on Google, so you can choose to run a Performance Max campaign as soon as you’d like. In the US, advertisers who sync their products to Google and run Google Ads Performance Max campaigns have seen an average of over 50% increase in clicks and over 100% increase in impressions in both their product listings and their ads on the Shopping tab. " msgstr "" #: js/build/get-started-page.js:1 msgid "How does Google for WooCommerce help me drive sales?" msgstr "" #: js/build/get-started-page.js:1 msgid "With Google for WooCommerce, you can serve the best-performing ads more often, by using Google AI to pull headlines, images, product details, and more from your product feed and find more relevant customers. Your campaigns will learn and optimize in real time – to help deliver better performance and boost your ROI." msgstr "" #: js/build/get-started-page.js:1 msgid "What are Enhanced conversions?" msgstr "" #: js/build/get-started-page.js:1 msgid "Enhanced conversions is a feature that can improve the accuracy of your conversion measurement and unlock more powerful bidding. It supplements your existing conversion tags by sending hashed first-party conversion data from your website to Google in a privacy-safe way." msgstr "" #: js/build/get-started-page.js:1 msgid "Which countries are available for Google for WooCommerce?" msgstr "" #: js/build/get-started-page.js:1 msgid "For Performance Max campaigns, learn more about supported countries and currencies here." msgstr "" #: js/build/get-started-page.js:1 msgid "What is Multi-Country Advertising?" msgstr "" #: js/build/get-started-page.js:1 msgid "Multi-Country Advertising enables you to create a single Google Ads campaign that targets multiple countries at once. Google for WooCommerce automatically populates eligible countries from your Google Merchant Center account into the plug-in ads campaign creation flow." msgstr "" #: js/build/get-started-page.js:1 msgid "Can I enable Multi-Country Advertising on my existing campaigns?" msgstr "" #: js/build/get-started-page.js:1 msgid "If you created a campaign before this feature launched, you’ll need to create a new campaign to target new countries with Multi-Country Advertising" msgstr "" #: js/build/get-started-page.js:1 msgid "How is my ads budget split between the different countries?" msgstr "" #: js/build/get-started-page.js:1 msgid "Identify the best performing targeted countries with the help of Google AI, to make your ads reach the right shoppers at the right time." msgstr "" #: js/build/get-started-page.js:1 msgid "Which countries can I target?" msgstr "" #: js/build/get-started-page.js:1 msgid "You can only select the countries that you’re targeting on Google Merchant Center. Your target countries must be eligible for both Google Merchant Center and Google Ads." msgstr "" #: js/build/get-started-page.js:1 msgid "To allow your products to appear in all relevant locations, make sure you’ve correctly configured your shipping for countries where your products can be delivered. Keep in mind that shipping services can cover multiple countries. Learn more about multi-country shipping." msgstr "" #: js/build/get-started-page.js:1 msgid "Learn More →" msgstr "" #: js/build/get-started-page.js:1 msgid "49% of shoppers surveyed say they use Google to discover or find a new item or product" msgstr "" #: js/build/get-started-page.js:1 msgid "With Google for WooCommerce, connect with the right shoppers at the right moment when they’re looking to buy products like yours." msgstr "" #: js/build/get-started-page.js:1 msgid "Drawing of WooCommerce and Google" msgstr "" #: js/build/get-started-page.js:1 msgid "Show products automatically on Google for free" msgstr "" #: js/build/get-started-page.js:1 msgid "When your products are added and approved, they’ll be eligible for free listings, reaching shoppers across Google’s network." msgstr "" #: js/build/get-started-page.js:1 msgid "Drawing of a mobile and product ads" msgstr "" #: js/build/get-started-page.js:1 msgid "Promote products and drive more sales with Google Ads" msgstr "" #: js/build/get-started-page.js:1 msgid "Connect your Google Ads account, choose a budget, and Google will optimize your ads so they appear at the right time and place. " msgstr "" #: js/build/get-started-page.js:1 msgid "Drawing of a bar and line charts heading up" msgstr "" #: js/build/get-started-page.js:1 msgid "Track performance straight from your store dashboard" msgstr "" #: js/build/get-started-page.js:1 msgid "Real-time reporting all within your WooCommerce dashboard means you know how your campaigns are performing at all times." msgstr "" #: js/build/get-started-page.js:1 #: js/build/onboarding.js:1 msgid "Google Shopping search results example" msgstr "" #: js/build/get-started-page.js:1 msgid "Get your products in front of more shoppers with Google for WooCommerce" msgstr "" #: js/build/get-started-page.js:1 msgid "Sell more on Google →" msgstr "" #: js/build/get-started-page.js:1 msgid "By clicking ‘Sell more on Google‘, you agree to our Terms of Service." msgstr "" #: js/build/get-started-page.js:1 msgid "The official extension for WooCommerce, built in collaboration with Google" msgstr "" #: js/build/get-started-page.js:1 msgid "Connect your WooCommerce store and reach millions of shoppers on Google" msgstr "" #: js/build/get-started-page.js:1 msgid "Effortlessly sync your WooCommerce product feed across Google and be seen by millions of engaged shoppers with the Google for WooCommerce extension." msgstr "" #: js/build/get-started-page.js:1 msgid "Estimated setup time: 5 min" msgstr "" #: js/build/get-started-page.js:1 msgid "By clicking ‘Sell more on Google’, you agree to our Terms of Service." msgstr "" #: js/build/get-started-page.js:1 msgid "If you’re already using another extension to manage your product feed with Google, make sure to deactivate or uninstall it first to prevent duplicate product feeds." msgstr "" #: js/build/get-started-page.js:1 msgid "Your site language is . This language is currently not supported by Google for WooCommerce. You can change your site language here. Read more about supported languages" msgstr "" #: js/build/get-started-page.js:1 msgid "Your store’s country is . This country is currently not supported by Google for WooCommerce. However, you can still choose to list your products in another supported country, if you are able to sell your products to customers there. Change your store’s country here. Read more about supported countries" msgstr "" #: js/build/index.js:1 msgctxt "Capitalized asset field name as the start of an error message" msgid "The first display URL path" msgstr "" #: js/build/index.js:1 msgctxt "Capitalized asset field name as the start of an error message" msgid "The second display URL path" msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Landscape images" msgstr "" #: js/build/index.js:1 msgctxt "Asset field name with its aspect ratio as the subheading within a help tip" msgid "Landscape image (1.91:1)" msgstr "" #: js/build/index.js:1 msgctxt "Lowercase asset field name" msgid "landscape" msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Square images" msgstr "" #: js/build/index.js:1 msgctxt "Asset field name with its aspect ratio as the subheading within a help tip" msgid "Square image (1:1)" msgstr "" #: js/build/index.js:1 msgctxt "Lowercase asset field name" msgid "square" msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Portrait images" msgstr "" #: js/build/index.js:1 msgctxt "Asset field name with its aspect ratio as the subheading within a help tip" msgid "Portrait image (4:5)" msgstr "" #: js/build/index.js:1 msgctxt "Lowercase asset field name" msgid "portrait" msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Logo" msgstr "" #: js/build/index.js:1 msgctxt "Asset field name with its aspect ratio as the subheading within a help tip" msgid "Logo (1:1)" msgstr "" #: js/build/index.js:1 msgctxt "Lowercase asset field name" msgid "logo" msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Business name" msgstr "" #: js/build/index.js:1 msgctxt "Capitalized asset field name as the placeholder or the start of an error message" msgid "Business name" msgstr "" #: js/build/index.js:1 msgctxt "Singular and lowercase asset field name" msgid "business name" msgstr "" #: js/build/index.js:1 msgid "The business name is the name of your business or brand. In certain layouts, it may appear in the text of your ad." msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Headlines" msgstr "" #: js/build/index.js:1 msgid "Learn how to write effective ads" msgstr "" #: js/build/index.js:1 msgid "Add headline" msgstr "" #: js/build/index.js:1 msgctxt "Capitalized asset field name as the placeholder or the start of an error message" msgid "Headline" msgstr "" #: js/build/index.js:1 msgctxt "Singular and lowercase asset field name" msgid "headline" msgstr "" #: js/build/index.js:1 msgctxt "Plural and lowercase asset field name" msgid "headlines" msgstr "" #: js/build/index.js:1 msgid "The headline is the first line of your ad and is most likely the first thing people notice, so consider including words that people may have entered in their Google search." msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Long headlines" msgstr "" #: js/build/index.js:1 msgid "Add long headline" msgstr "" #: js/build/index.js:1 msgctxt "Capitalized asset field name as the placeholder or the start of an error message" msgid "Long headline" msgstr "" #: js/build/index.js:1 msgctxt "Singular and lowercase asset field name" msgid "long headline" msgstr "" #: js/build/index.js:1 msgctxt "Plural and lowercase asset field name" msgid "long headlines" msgstr "" #: js/build/index.js:1 msgid "The long headline is the first line of your ad, and appears instead of your short headline in larger ads. Long headlines can be up to 90 characters, and may appear with or without your description." msgstr "" #: js/build/index.js:1 msgid "The length of the rendered headline will depend on the site it appears on. If shortened, it will end with an ellipsis(…)." msgstr "" #: js/build/index.js:1 msgctxt "Plural asset field name as the heading" msgid "Descriptions" msgstr "" #: js/build/index.js:1 msgid "Add description" msgstr "" #: js/build/index.js:1 msgctxt "Capitalized asset field name as the placeholder or the start of an error message" msgid "Description" msgstr "" #: js/build/index.js:1 msgctxt "Singular and lowercase asset field name" msgid "description" msgstr "" #: js/build/index.js:1 msgctxt "Plural and lowercase asset field name" msgid "descriptions" msgstr "" #: js/build/index.js:1 msgid "The description adds to the headline and provides additional context or details. It can be up to 90 characters, and may appear after the headline." msgstr "" #: js/build/index.js:1 msgid "The length of the rendered description will depend on the site it appears on. If it's shortened, it will end with an ellipsis(…). The description doesn't show in all sizes and formats." msgstr "" #: js/build/index.js:1 msgctxt "The separator for concatenating the types of assets" msgid ", " msgstr "" #. translators: 1: Concatenated text for the types of assets except for the last one. 2: The last type of assets. #: js/build/index.js:4 msgid "%1$s and %2$s" msgstr "" #. translators: 1: The minimal number of this item. #: js/build/index.js:7 msgid "At least %d required" msgstr "" #. translators: 1: The minimal number of this item. 2: The maximum number of this item. #: js/build/index.js:10 msgid "At least %1$d required. Add up to %2$d." msgstr "" #. translators: 1: Recommended width. 2: Recommended height. 3: Minimal width. 4: Minimal height. #: js/build/index.js:13 msgid "Recommended size: %1$d x %2$dMin. size: %3$d x %4$d" msgstr "" #. translators: 1: The maximum number of this image assets. 2: Text for the types of image assets. #: js/build/index.js:16 msgid "You can add up to a maximum of %1$d image assets, which can be a combination of %2$s images." msgstr "" #: js/build/index.js:16 msgid "Add images that meet or can be cropped to the recommended sizes. Note: The maximum file size for any image is 5120 KB." msgstr "" #. translators: The shared maximum number of the grouped types of image assets. #: js/build/index.js:19 msgid "The maximum number of images that can be uploaded is %d." msgstr "" #. translators: 1: The minimum number of this asset field. 2: Asset field name. #: js/build/index.js:22 msgid "%1$d %2$s" msgstr "" #. translators: 1: The shared maximum number of the grouped types of image assets. 2: Text for the minimum number and type of each image asset. #: js/build/index.js:25 msgid "Maximum %1$d images can be uploaded, with a minimum of %2$s image." msgstr "" #: js/build/index.js:25 msgid "There was an error loading shipping rates." msgstr "" #: js/build/index.js:25 msgid "There was an error loading shipping times." msgstr "" #: js/build/index.js:25 msgid "There was an error loading merchant center settings." msgstr "" #: js/build/index.js:25 msgid "There was an error loading Jetpack account info." msgstr "" #: js/build/index.js:25 msgid "There was an error loading Google account info." msgstr "" #: js/build/index.js:25 msgid "There was an error loading Google Merchant Center account info." msgstr "" #: js/build/index.js:25 msgid "There was an error getting your Google Merchant Center accounts." msgstr "" #: js/build/index.js:25 msgid "There was an error loading Google Ads account info." msgstr "" #: js/build/index.js:25 msgid "Unable to disconnect your Google account." msgstr "" #: js/build/index.js:25 msgid "Unable to disconnect your Google Ads account." msgstr "" #: js/build/index.js:25 msgid "Unable to disconnect all your accounts." msgstr "" #: js/build/index.js:25 msgid "There was an error getting the billing status of your Google Ads account." msgstr "" #: js/build/index.js:25 msgid "There was an error getting your Google Ads accounts." msgstr "" #: js/build/index.js:25 msgid "Unable to update your Google Merchant Center contact information." msgstr "" #: js/build/index.js:25 msgid "There was an error loading target audience." msgstr "" #: js/build/index.js:25 msgid "There was an error creating the assets of the campaign." msgstr "" #: js/build/index.js:25 msgid "There was an error updating the assets of the campaign." msgstr "" #: js/build/index.js:25 msgid "There was an error loading your merchant center setup status." msgstr "" #: js/build/index.js:25 msgid "Unable to update the channel visibility of products." msgstr "" #: js/build/index.js:25 msgid "There was an error creating the rule." msgstr "" #: js/build/index.js:25 msgid "There was an error updating the rule." msgstr "" #: js/build/index.js:25 msgid "There was an error deleting the rule." msgstr "" #: js/build/index.js:25 msgid "There was an error updating the tour." msgstr "" #: js/build/index.js:25 msgid "There was an error getting the status of your Google Ads account." msgstr "" #: js/build/index.js:25 msgid "There was an error loading Google account access info." msgstr "" #: js/build/index.js:25 msgid "There was an error loading Google Merchant Center contact information." msgstr "" #: js/build/index.js:25 msgid "There was an error loading supported country details." msgstr "" #: js/build/index.js:25 msgid "There was an error loading ads campaigns." msgstr "" #: js/build/index.js:25 msgid "There was an error loading the assets of the campaign." msgstr "" #: js/build/index.js:25 msgid "There was an error loading your merchant center product statistics." msgstr "" #: js/build/index.js:25 msgid "There was an error loading your merchant center product review request status." msgstr "" #: js/build/index.js:25 msgid "There was an error loading issues to resolve." msgstr "" #: js/build/index.js:25 msgid "There was an error loading product feed." msgstr "" #: js/build/index.js:25 msgid "There was an error loading report." msgstr "" #: js/build/index.js:25 msgid "There was an error loading the mapping attributes." msgstr "" #: js/build/index.js:25 msgid "There was an error loading the mapping sources for the selected attribute." msgstr "" #: js/build/index.js:25 msgid "There was an error loading the mapping rules." msgstr "" #: js/build/index.js:25 msgid "There was an error getting the store categories." msgstr "" #: js/build/index.js:25 msgid "There was an error getting the tour." msgstr "" #: js/build/index.js:25 msgid "There was an error getting the budget recommendation." msgstr "" #: js/build/index.js:25 msgid "There was an error getting the GTIN Migration Status." msgstr "" #: js/build/index.js:25 msgid "Unknown error occurred." msgstr "" #: js/build/index.js:25 msgctxt "The spacing between sentences. It's a space in English. Please use an empty string if no spacing is needed in that language." msgid " " msgstr "" #: js/build/index.js:25 msgid "Marketing" msgstr "" #: js/build/index.js:25 msgid "Setup Merchant Center" msgstr "" #: js/build/index.js:25 msgid "Setup Google Ads" msgstr "" #: js/build/onboarding.js:1 msgid "Get started with Google for WooCommerce" msgstr "" #: js/build/onboarding.js:1 msgid "Why do I need a WordPress.com account?" msgstr "" #: js/build/onboarding.js:1 msgid "We use a WordPress.com account to connect your site to the WooCommerce and Google servers. It ensures that requests (e.g. product feed, clicks, sales, etc) from your site are securely and correctly attributed to your store. It enables a connection to your self-hosted site, and provides a common authentication interface across disparate server configurations and architectures." msgstr "" #: js/build/onboarding.js:1 msgid "Why do I need a Google Merchant Center account?" msgstr "" #: js/build/onboarding.js:1 msgid "Google Merchant Center helps you sync your store and product data with Google and makes the information available for both free listings on the Shopping tab and Google Shopping Ads. That means everything about your stores and products is available to shoppers when they search on a Google property." msgstr "" #: js/build/onboarding.js:1 msgid "If you create a new Merchant Center account through this application, it will be associated with Google’s Comparison Shopping Service (Google Shopping) by default. You can change the CSS associated with your account at any time. Please find more information here." msgstr "" #: js/build/onboarding.js:1 msgid "If you are in the European Economic Area or Switzerland, your Merchant Center account must be associated with a Comparison Shopping Service (CSS). Please find more information at Google Merchant Center Help website." msgstr "" #: js/build/onboarding.js:1 msgid "If you create a new Merchant Center account through this application, it will be associated with Google Shopping, Google’s CSS, by default. You can change the CSS associated with your account at any time. Please find more information about our CSS Partners here." msgstr "" #: js/build/onboarding.js:1 msgid "Once you have set up your Merchant Center account you can use our onboarding tool regardless of which CSS you use." msgstr "" #: js/build/onboarding.js:1 msgid "Connect the accounts required to use Google for WooCommerce." msgstr "" #: js/build/onboarding.js:1 msgid "The following accounts are required to use the Google for WooCommerce plugin." msgstr "" #: js/build/onboarding.js:1 msgid "Configure your product listings" msgstr "" #: js/build/onboarding.js:1 msgid "Your product listings will look something like this." msgstr "" #: js/build/onboarding.js:1 msgid "Your product details, estimated shipping info and tax details will be displayed across Google." msgstr "" #: js/build/onboarding.js:1 msgid "Skip setting up ads?" msgstr "" #: js/build/onboarding.js:1 msgid "Complete setup without setting up ads" msgstr "" #: js/build/onboarding.js:1 msgid "Enabling Performance Max is highly recommended to drive more sales and reach new audiences across Google channels like Search, YouTube and Discover." msgstr "" #: js/build/onboarding.js:1 msgid "Performance Max uses the best of Google’s AI to show the most impactful ads for your products at the right time and place. Google will use your product data to create ads for this campaign." msgstr "" #: js/build/onboarding.js:1 msgid "Learn more about Performance Max." msgstr "" #: js/build/onboarding.js:1 msgid "Skip ads creation" msgstr "" #: js/build/onboarding.js:1 msgid "Unable to complete your setup." msgstr "" #: js/build/onboarding.js:1 msgid "Create a campaign to advertise your products" msgstr "" #: js/build/onboarding.js:1 msgid "Configure product listings" msgstr "" #: js/build/onboarding.js:1 msgid "There was an error saving audience." msgstr "" #: js/build/onboarding.js:1 msgid "There was an error saving settings." msgstr "" #: js/build/onboarding.js:1 msgid "There was an error saving shipping rates." msgstr "" #: js/build/onboarding.js:1 msgid "There was an error saving shipping times." msgstr "" #: js/build/onboarding.js:1 msgid "Create a campaign" msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Disconnect all accounts" msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Yes, I want to disconnect all my accounts." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "I understand that I am disconnecting any WordPress.com account, Google account, Google Merchant Center account and Google Ads account connected to this extension." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Any active product listings will continue to show on Google. They can be managed, edited, or deleted manually from Google Merchant Center (merchants.google.com)." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Any ongoing campaigns will continue to run. They can be managed, edited, or deleted manually from Google Ads (ads.google.com)." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Disconnect Google Ads account" msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Disconnect Google Ads Account" msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Yes, I want to disconnect my Google Ads account." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "I understand that I am disconnecting my Google Ads account from this WooCommerce extension." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Some configurations for Google Ads created through WooCommerce may be lost. This cannot be undone." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Disable data fetching" msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Yes, I want to disable the data fetching feature." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "I understand that I am disabling the data fetching feature from this WooCommerce extension." msgstr "" #: js/build/onboarding.js:1 #: js/build/settings.js:1 msgid "Any ongoing campaigns and configuration will continue to run. They will be pushed to Google as in the previous versions of this extension." msgstr "" #: js/build/product-feed.js:1 msgid "Type" msgstr "" #: js/build/product-feed.js:1 msgid "Affected product" msgstr "" #: js/build/product-feed.js:1 msgid "Issue" msgstr "" #: js/build/product-feed.js:1 msgid "Suggested action" msgstr "" #: js/build/product-feed.js:1 msgid "All account issues resolved" msgstr "" #: js/build/product-feed.js:1 msgid "All product issues resolved" msgstr "" #: js/build/product-feed.js:1 msgid "However, there are issues affecting your products that needs to be resolved. Head over to the Product Issues tab to view them." msgstr "" #: js/build/product-feed.js:1 msgid "However, there are issues affecting your account that needs to be resolved. Head over to the Account Issues tab to view them." msgstr "" #: js/build/product-feed.js:1 msgid "What to do?" msgstr "" #: js/build/product-feed.js:1 msgid "Read more about this issue" msgstr "" #: js/build/product-feed.js:1 #: js/build/product-feed.js:7 msgid "Issues to resolve" msgstr "" #: js/build/product-feed.js:1 msgid "An error occurred while retrieving issues. Please try again later." msgstr "" #: js/build/product-feed.js:1 msgid "Loading Issues To Resolve" msgstr "" #: js/build/product-feed.js:1 msgid "Account Issues" msgstr "" #: js/build/product-feed.js:1 msgid "Product Issues" msgstr "" #: js/build/product-feed.js:1 msgid "Request a review on the following issue(s):" msgstr "" #. translators: %d: The number of extra issues issues #: js/build/product-feed.js:4 msgid "+ %d more issue(s)" msgstr "" #: js/build/product-feed.js:4 msgid "Request account review" msgstr "" #: js/build/product-feed.js:4 msgid "Your account review was successfully requested." msgstr "" #: js/build/product-feed.js:4 msgid "Please ensure that you have resolved all account suspension issues before requesting for an account review. If some issues are unresolved, you wont be able to request another review for at least 7 days. Learn more" msgstr "" #: js/build/product-feed.js:4 msgid "I have resolved all the issue(s) listed above." msgstr "" #: js/build/product-feed.js:4 #: js/build/product-feed.js:10 #: js/build/product-feed.js:19 msgid "Disapproved" msgstr "" #: js/build/product-feed.js:4 msgid "To make products eligible to show on Google, fix all setup and policy issues that were found." msgstr "" #: js/build/product-feed.js:4 msgid "We’ve found unresolved issues in your account." msgstr "" #: js/build/product-feed.js:4 msgid "Fix all account suspension issues listed below to request a review of your account." msgstr "" #: js/build/product-feed.js:4 msgid "Warning" msgstr "" #: js/build/product-feed.js:4 msgid "To keep showing your products on Google, fix your setup and policy issues." msgstr "" #: js/build/product-feed.js:4 msgid "Pending review" msgstr "" #: js/build/product-feed.js:4 msgid "This may take up to 3 days. If approved, your products will show on Google once it’s completed." msgstr "" #: js/build/product-feed.js:4 msgid "Under review" msgstr "" #: js/build/product-feed.js:4 msgid "Review requests take at least 7 days." msgstr "" #: js/build/product-feed.js:4 #: js/build/product-feed.js:10 msgid "Approved" msgstr "" #: js/build/product-feed.js:4 msgid "Your products listings are on Google." msgstr "" #: js/build/product-feed.js:4 msgid "No products added" msgstr "" #: js/build/product-feed.js:4 msgid "Add and sync products to Google." msgstr "" #. translators: %s: Cool down period date. #: js/build/product-feed.js:7 msgid "Your account is under cool down period. You can request a new review on %s." msgstr "" #: js/build/product-feed.js:7 msgid "Request review" msgstr "" #: js/build/product-feed.js:7 msgid "Products and stores must meet Google Merchant Center’s requirements in order to get approved. WooCommerce and Google automatically check your product feed to help you resolve any issues. " msgstr "" #: js/build/product-feed.js:7 msgid "Select channel visibility" msgstr "" #: js/build/product-feed.js:7 msgid "Don’t sync and show" msgstr "" #: js/build/product-feed.js:7 msgid "Select one or more products to bulk edit" msgstr "" #. translators: %d: number of selected products to edit channel visibility, with minimum value of 1. #: js/build/product-feed.js:10 msgid "Apply to %d selected" msgstr "" #: js/build/product-feed.js:10 msgid "Partially approved" msgstr "" #: js/build/product-feed.js:10 #: js/build/product-feed.js:19 msgid "Expiring" msgstr "" #: js/build/product-feed.js:10 #: js/build/product-feed.js:19 msgid "Pending" msgstr "" #: js/build/product-feed.js:10 msgid "Not synced" msgstr "" #: js/build/product-feed.js:10 msgid "Product Title" msgstr "" #: js/build/product-feed.js:10 msgid "Channel Visibility" msgstr "" #: js/build/product-feed.js:10 msgid "Status" msgstr "" #. translators: %d: number of products are updated successfully, with minimum value of 1. #: js/build/product-feed.js:13 msgid "You successfully changed the channel visibility of %d product" msgid_plural "You successfully changed the channel visibility of %d products" msgstr[0] "" msgstr[1] "" #: js/build/product-feed.js:13 msgid "Loading product feed" msgstr "" #: js/build/product-feed.js:13 msgid "An error occurred while retrieving products. Please try again later." msgstr "" #: js/build/product-feed.js:13 msgid "Don't sync and show" msgstr "" #: js/build/product-feed.js:13 msgid "WooCommerce Logo" msgstr "" #: js/build/product-feed.js:13 msgid "You’ve successfully set up Google for WooCommerce! 🎉" msgstr "" #: js/build/product-feed.js:13 msgid "Your products are being synced and reviewed. Google reviews product listings in 3-5 days." msgstr "" #: js/build/product-feed.js:13 msgid "No ads will launch yet and you won’t be charged until Google approves your listings. Updates are available in your WooCommerce dashboard." msgstr "" #: js/build/product-feed.js:13 msgid "Manage and edit your product feed in WooCommerce. We will also notify you of any product feed issues to ensure your products get approved and perform well on Google." msgstr "" #: js/build/product-feed.js:13 msgid "Spend $500 to get $500 in Google Ads credits" msgstr "" #: js/build/product-feed.js:13 msgid "New to Google Ads? Get $500 in ad credit when you spend $500 within your first 60 days* You can edit or cancel your campaign at any time." msgstr "" #: js/build/product-feed.js:13 msgid "*Full terms and conditions here." msgstr "" #: js/build/product-feed.js:13 msgid "View product feed" msgstr "" #: js/build/product-feed.js:13 msgid "Maybe later" msgstr "" #: js/build/product-feed.js:13 msgid "Back" msgstr "" #: js/build/product-feed.js:13 msgid "Your product feed is automatically synced from WooCommerce to Google, every 1-2 days. " msgstr "" #: js/build/product-feed.js:13 msgid "‘Not synced’ products do not appear in Google listings. They are queued for submission, or they may be ineligible or excluded from the product feed." msgstr "" #: js/build/product-feed.js:13 msgid "After submission, Google assigns each product a status: Active, Expiring, Pending, or Disapproved." msgstr "" #: js/build/product-feed.js:13 msgid "‘Active’ products are fully approved and eligible to appear in free listings on Google." msgstr "" #: js/build/product-feed.js:13 msgid "‘Expiring’ products will become inactive and no longer appear in Google listings in the next 3 days." msgstr "" #: js/build/product-feed.js:13 msgid "‘Pending’ products are being processed by Google. They will not appear in listings until they are approved." msgstr "" #: js/build/product-feed.js:13 msgid "‘Disapproved’ products are inactive and do not appear in Google listings." msgstr "" #: js/build/product-feed.js:13 msgid "Read more about product sync and statuses" msgstr "" #: js/build/product-feed.js:13 msgid "Sync in progress" msgstr "" #: js/build/product-feed.js:13 msgid "Automatically synced to Google" msgstr "" #. translators: %s: datetime of last update products sync status, and %d: number of synced products, with minimum value of 1. #: js/build/product-feed.js:16 msgid "Last updated: %1$s, containing %2$d product" msgid_plural "Last updated: %1$s, containing %2$d products" msgstr[0] "" msgstr[1] "" #: js/build/product-feed.js:16 msgid "Sync with Google:" msgstr "" #: js/build/product-feed.js:16 msgid "Overview Stats:" msgstr "" #: js/build/product-feed.js:16 msgid "There was an error loading the Overview Stats. Click to retry." msgstr "" #: js/build/product-feed.js:16 msgid "No issues to resolve 🎉" msgstr "" #. translators: %d: number of unsolved Merchant Center issues, with minimum value of 1. #: js/build/product-feed.js:19 msgid "%d issue to resolve" msgid_plural "%d issues to resolve" msgstr[0] "" msgstr[1] "" #: js/build/product-feed.js:19 msgid "Feed setup:" msgstr "" #: js/build/product-feed.js:19 msgid "Free listings setup completed" msgstr "" #: js/build/product-feed.js:19 msgid "Account status:" msgstr "" #: js/build/product-feed.js:19 msgid "You have approved products. Create a Google Ads campaign to reach more customers and drive more sales." msgstr "" #: js/build/product-feed.js:19 msgid "An error occurred while retrieving your product feed. Please try again later." msgstr "" #: js/build/product-feed.js:19 msgid "Overview" msgstr "" #: js/build/product-feed.js:19 msgid "Active" msgstr "" #: js/build/product-feed.js:19 msgid "Not Synced" msgstr "" #: js/build/product-feed.js:19 msgid "How easy was it to set up Google for WooCommerce?" msgstr "" #: js/build/product-feed.js:19 msgid "How easy was it to understand the requirements for the Google for WooCommerce setup?" msgstr "" #: js/build/reports.js:1 msgid "Unavailable" msgstr "" #: js/build/reports.js:1 #: js/build/reports.js:4 msgid "Free Listings" msgstr "" #: js/build/reports.js:1 #: js/build/reports.js:4 msgid "Show" msgstr "" #: js/build/reports.js:1 msgid "All Google programs" msgstr "" #: js/build/reports.js:1 msgid "Single program" msgstr "" #: js/build/reports.js:1 msgid "Type to search for a program" msgstr "" #: js/build/reports.js:1 msgid "Single Program" msgstr "" #: js/build/reports.js:1 #: js/build/reports.js:4 msgid "Comparison" msgstr "" #: js/build/reports.js:1 msgid "Check at least two programs below to compare" msgstr "" #: js/build/reports.js:1 msgid "Search for programs to compare" msgstr "" #: js/build/reports.js:1 msgid "Compare Programs" msgstr "" #: js/build/reports.js:1 #: js/build/reports.js:4 msgid "Compare" msgstr "" #: js/build/reports.js:1 msgid "This data is currently available for Google Ads campaigns only." msgstr "" #: js/build/reports.js:1 msgid "Please try again later, or go to to track your performance for Google Free Listings." msgstr "" #. translators: %s: link to Google Merchant Center. #: js/build/reports.js:4 msgid "Google Merchant Center (%s)" msgstr "" #: js/build/reports.js:4 msgid "No data for the selected date range" msgstr "" #: js/build/reports.js:4 msgid "This campaign has been upgraded to Performance Max" msgstr "" #: js/build/reports.js:4 msgid "Select one or more programs to compare" msgstr "" #: js/build/reports.js:4 msgid "Products" msgstr "" #: js/build/reports.js:4 msgid "Conversions" msgstr "" #: js/build/reports.js:4 msgid "Impressions" msgstr "" #: js/build/reports.js:4 msgid "Spend" msgstr "" #: js/build/reports.js:4 msgid "All Products" msgstr "" #: js/build/reports.js:4 msgid "Single Product" msgstr "" #: js/build/reports.js:4 msgid "Type to search for a product" msgstr "" #: js/build/reports.js:4 msgid "Check at least two products below to compare" msgstr "" #: js/build/reports.js:4 msgid "Search for products to compare" msgstr "" #: js/build/reports.js:4 msgid "Compare Products" msgstr "" #: js/build/reports.js:4 msgid "All Variations" msgstr "" #: js/build/reports.js:4 msgid "Single Variation" msgstr "" #: js/build/reports.js:4 msgid "Type to search for a variation" msgstr "" #: js/build/reports.js:4 msgid "Check at least two variations below to compare" msgstr "" #: js/build/reports.js:4 msgid "Search for variations to compare" msgstr "" #: js/build/reports.js:4 msgid "Compare Variations" msgstr "" #: js/build/reports.js:4 msgid "Show data from" msgstr "" #: js/build/reports.js:4 msgid "Ad campaigns" msgstr "" #: js/build/reports.js:4 msgid "Select one or more products to compare" msgstr "" #: js/build/settings.js:1 msgid "Tax rate (required for U.S. only)" msgstr "" #: js/build/settings.js:1 msgid "This tax rate will be shown to potential customers, together with the cost of your product." msgstr "" #: js/build/settings.js:1 msgid "My store uses destination-based tax rates." msgstr "" #: js/build/settings.js:1 msgid "Google’s estimated tax rates will automatically be applied to my product listings." msgstr "" #: js/build/settings.js:1 msgid "My store does not use destination-based tax rates." msgstr "" #: js/build/settings.js:1 msgid "I’ll set my tax rates up manually in Google Merchant Center. I understand that if I don’t set this up, my products will be disapproved." msgstr "" #: js/build/settings.js:1 msgid "Please specify tax rate option." msgstr "" #: js/build/settings.js:1 msgid "There was an error saving tax rate." msgstr "" #: js/build/settings.js:1 msgid "There was an error synchronizing tax rate to Google Merchant Center." msgstr "" #: js/build/settings.js:1 msgid "Your change to tax rate has been saved and will be synced to your Google Merchant Center." msgstr "" #: js/build/settings.js:1 msgid "Save tax rate" msgstr "" #: js/build/settings.js:1 msgid "Linked accounts" msgstr "" #: js/build/settings.js:1 msgid "A WordPress.com account, Google account, Google Merchant Center account, and Google Ads account are required to use this extension in WooCommerce." msgstr "" #: js/build/settings.js:1 msgid "Disconnect Google Ads account only" msgstr "" #: js/build/settings.js:1 msgid "Disconnect from all accounts" msgstr "" #: js/build/settings.js:1 msgid "Your WordPress.com account has been disconnected." msgstr "" #: js/build/settings.js:1 msgid "Connect your WordPress.com account to ensure your products stay listed on Google. If you do not re-connect, your products can’t be automatically synced to Google, and any existing listings may be removed from Google." msgstr "" #: js/build/settings.js:1 msgid "This Google account, , was not the Google account previously connected to this integration." msgstr "" #: js/build/settings.js:1 msgid "Thus, it doesn‘t have access to the Google Merchant Center and/or Google Ads account currently connected to this WooCommerce store." msgstr "" #: js/build/settings.js:1 msgid "Try connecting with a different Google account, or completely disconnect all your connected accounts." msgstr "" #: js/build/settings.js:1 msgid "Try another Google account" msgstr "" #: js/build/settings.js:1 msgid "Connect account" msgstr "" #: js/build/settings.js:1 msgid "Edit store address" msgstr "" #: js/build/settings.js:1 msgid "Your store address is required by Google for verification purposes. It will be shared with the Google Merchant Center and will not be displayed to customers." msgstr "" #: js/build/settings.js:1 msgid "Save details" msgstr "" #: js/build/shipping.js:1 msgid "Before you save…" msgstr "" #: js/build/shipping.js:1 msgid "Don't save" msgstr "" #: js/build/shipping.js:1 msgid "Continue to save" msgstr "" #: js/build/shipping.js:1 msgid "Changes will result in the loss of any optimisations learned over time." msgstr "" #: js/build/shipping.js:1 msgid "We recommend allowing your listings to run for at least 14 days after set up without changing them for optimal performance." msgstr "" #: js/build/shipping.js:1 msgid "You have unsaved changes. Are you sure you want to leave?" msgstr "" #: js/build/shipping.js:1 msgid "Target audience" msgstr "" #: js/build/shipping.js:1 msgid "Merchant Center Settings" msgstr "" #: js/build/shipping.js:1 msgid "Your changes have been saved and will be synced to your Google Merchant Center account." msgstr "" #: js/build/shipping.js:1 msgid "Unable to save your changes." msgstr "" #: js/build/shipping.js:1 msgid "Something went wrong while saving your changes. Please try again later." msgstr "" #: js/build/product-channel-visibility/block.json msgctxt "block title" msgid "Product channel visibility" msgstr "" #: js/build/product-date-time-field/block.json msgctxt "block title" msgid "Product date and time fields" msgstr "" #: js/build/product-onboarding-prompt/block.json msgctxt "block title" msgid "Product onboarding prompt" msgstr "" #: js/build/product-select-field/block.json msgctxt "block title" msgid "Product select field" msgstr "" #: js/build/product-select-with-text-field/block.json msgctxt "block title" msgid "Product select with text field" msgstr "" PK!).src/API/Google/Query/AdsAccountAccessQuery.phpnu[columns( [ 'customer_user_access.resource_name', 'customer_user_access.access_role' ] ); } } PK!~j(src/API/Google/Query/AdsAccountQuery.phpnu[columns( [ 'customer.id', 'customer.descriptive_name', 'customer.manager', 'customer.test_account' ] ); } } PK!ܤ0src/API/Google/Query/AdsAssetGroupAssetQuery.phpnu[columns( [ 'asset.id', 'asset.name', 'asset.type', 'asset.text_asset.text', 'asset.image_asset.full_size.url', 'asset.call_to_action_asset.call_to_action', 'asset_group_asset.field_type' ] ); } } PK!_ss+src/API/Google/Query/AdsAssetGroupQuery.phpnu[columns( [ 'asset_group.resource_name' ] ); $this->search_args = $search_args; } } PK!f5kk.src/API/Google/Query/AdsBillingStatusQuery.phpnu[columns( [ 'status' => 'billing_setup.status', 'start_date_time' => 'billing_setup.start_date_time', ] ); $this->set_order( 'start_date_time', 'DESC' ); } } PK!1a/src/API/Google/Query/AdsCampaignBudgetQuery.phpnu[columns( [ 'campaign.campaign_budget' ] ); } } PK!**2src/API/Google/Query/AdsCampaignCriterionQuery.phpnu[columns( [ 'campaign.id', 'campaign_criterion.location.geo_target_constant', ] ); } } PK!<.src/API/Google/Query/AdsCampaignLabelQuery.phpnu[columns( [ 'label.id', ] ); } } PK!. tt)src/API/Google/Query/AdsCampaignQuery.phpnu[columns( [ 'campaign.id', 'campaign.name', 'campaign.status', 'campaign.advertising_channel_type', 'campaign.shopping_setting.feed_label', 'campaign_budget.amount_micros', ] ); } } PK!O?qq/src/API/Google/Query/AdsCampaignReportQuery.phpnu[columns( [ 'id' => 'campaign.id', 'name' => 'campaign.name', 'status' => 'campaign.status', 'type' => 'campaign.advertising_channel_type', ] ); } /** * Filter the query by a list of ID's. * * @param array $ids list of ID's to filter by. * * @return $this */ public function filter( array $ids ): QueryInterface { if ( empty( $ids ) ) { return $this; } return $this->where( 'campaign.id', $ids, 'IN' ); } } PK!y1src/API/Google/Query/AdsConversionActionQuery.phpnu[columns( [ 'id' => 'conversion_action.id', 'name' => 'conversion_action.name', 'status' => 'conversion_action.status', 'tag_snippets' => 'conversion_action.tag_snippets', ] ); } } PK!z/HH6src/API/Google/Query/AdsProductLinkInvitationQuery.phpnu[columns( [ 'product_link_invitation.merchant_center.merchant_center_id', 'product_link_invitation.status' ] ); } } PK!0l?66.src/API/Google/Query/AdsProductReportQuery.phpnu[columns( [ 'id' => 'segments.product_item_id', 'name' => 'segments.product_title', ] ); } /** * Filter the query by a list of ID's. * * @param array $ids list of ID's to filter by. * * @return $this */ public function filter( array $ids ): QueryInterface { if ( empty( $ids ) ) { return $this; } return $this->where( 'segments.product_item_id', $ids, 'IN' ); } } PK! @  !src/API/Google/Query/AdsQuery.phpnu[client = $client; $this->id = $id; return $this; } /** * Get the first row from the results. * * @return GoogleAdsRow * @throws ApiException When no results returned or an error occurs. */ public function get_result(): GoogleAdsRow { $results = $this->get_results(); if ( $results ) { foreach ( $results->iterateAllElements() as $row ) { return $row; } } throw new ApiException( __( 'No result from query', 'google-listings-and-ads' ), 404, '' ); } /** * Perform the query and save it to the results. * * @throws ApiException If the search call fails. * @throws InvalidProperty If the client is not set. */ protected function query_results() { if ( ! $this->client || ! $this->id ) { throw InvalidProperty::not_null( get_class( $this ), 'client' ); } $request = new SearchGoogleAdsRequest(); if ( ! empty( $this->search_args['pageToken'] ) ) { $request->setPageToken( $this->search_args['pageToken'] ); } // Allow us to get the total number of results. $request->setSearchSettings( new SearchSettings( [ 'return_total_results_count' => true, ] ) ); $request->setQuery( $this->build_query() ); $request->setCustomerId( $this->id ); $this->results = $this->client->getGoogleAdsServiceClient()->search( $request ); } } PK!~('src/API/Google/Query/AdsReportQuery.phpnu[set_initial_columns(); $this->handle_query_args( $args ); } /** * Add all the requested fields. * * @param array $fields List of fields. * * @return $this */ public function fields( array $fields ): QueryInterface { $map = [ 'clicks' => 'metrics.clicks', 'impressions' => 'metrics.impressions', 'spend' => 'metrics.cost_micros', 'sales' => 'metrics.conversions_value', 'conversions' => 'metrics.conversions', ]; $this->add_columns( array_intersect_key( $map, array_flip( $fields ) ) ); return $this; } /** * Add a segment interval to the query. * * @param string $interval Type of interval. * * @return $this */ public function segment_interval( string $interval ): QueryInterface { $map = [ 'day' => 'segments.date', 'week' => 'segments.week', 'month' => 'segments.month', 'quarter' => 'segments.quarter', 'year' => 'segments.year', ]; if ( isset( $map[ $interval ] ) ) { $this->add_columns( [ $interval => $map[ $interval ] ] ); } return $this; } /** * Set the initial columns for this query. */ abstract protected function set_initial_columns(); /** * Filter the query by a list of ID's. * * @param array $ids list of ID's to filter by. * * @return $this */ abstract public function filter( array $ids ): QueryInterface; } PK!]%s7src/API/Google/Query/MerchantFreeListingReportQuery.phpnu[columns( [ 'id' => 'segments.offer_id', ] ); } /** * Filter the query by a list of ID's. * * @param array $ids list of ID's to filter by. * * @return $this */ public function filter( array $ids ): QueryInterface { if ( empty( $ids ) ) { return $this; } return $this->where( 'segments.offer_id', $ids, 'IN' ); } } PK!f7src/API/Google/Query/MerchantProductViewReportQuery.phpnu[set_initial_columns(); $this->handle_query_args( $args ); } /** * Filter the query by a list of ID's. * * @param array $ids list of ID's to filter by. * * @return $this */ public function filter( array $ids ): QueryInterface { // No filtering used for product view report. return $this; } /** * Set the initial columns for this query. */ protected function set_initial_columns() { $this->columns( [ 'id' => 'product_view.id', 'expiration_date' => 'product_view.expiration_date', 'status' => 'product_view.aggregated_destination_status', ] ); } } PK!5/ -__&src/API/Google/Query/MerchantQuery.phpnu[client = $client; $this->id = $id; return $this; } /** * Perform the query and save it to the results. * * @throws GoogleException If the search call fails. * @throws InvalidProperty If the client is not set. */ protected function query_results() { if ( ! $this->client || ! $this->id ) { throw InvalidProperty::not_null( get_class( $this ), 'client' ); } $request = new SearchRequest(); $request->setQuery( $this->build_query() ); if ( ! empty( $this->search_args['pageSize'] ) ) { $request->setPageSize( $this->search_args['pageSize'] ); } if ( ! empty( $this->search_args['pageToken'] ) ) { $request->setPageToken( $this->search_args['pageToken'] ); } /** @var SearchResponse $this->results */ $this->results = $this->client->reports->search( $this->id, $request ); } } PK!T$$,src/API/Google/Query/MerchantReportQuery.phpnu[set_initial_columns(); $this->handle_query_args( $args ); $this->where( 'segments.program', 'FREE_PRODUCT_LISTING' ); } /** * Add all the requested fields. * * @param array $fields List of fields. * * @return $this */ public function fields( array $fields ): QueryInterface { $map = [ 'clicks' => 'metrics.clicks', 'impressions' => 'metrics.impressions', ]; $this->add_columns( array_intersect_key( $map, array_flip( $fields ) ) ); return $this; } /** * Add a segment interval to the query. * * @param string $interval Type of interval. * * @return $this */ public function segment_interval( string $interval ): QueryInterface { $map = [ 'day' => 'segments.date', 'week' => 'segments.week', 'month' => 'segments.month', 'quarter' => 'segments.quarter', 'year' => 'segments.year', ]; if ( isset( $map[ $interval ] ) ) { $this->add_columns( [ $interval => $map[ $interval ] ] ); } return $this; } /** * Set the initial columns for this query. */ abstract protected function set_initial_columns(); /** * Filter the query by a list of ID's. * * @param array $ids list of ID's to filter by. * * @return $this */ abstract public function filter( array $ids ): QueryInterface; } PK!L'src/API/Google/Query/QueryInterface.phpnu[, IN, NOT IN. * * @return $this */ public function where( string $column, $value, string $compare = '=' ): QueryInterface; /** * Set the where relation for the query. * * @param string $relation * * @return QueryInterface */ public function set_where_relation( string $relation ): QueryInterface; /** * Get the results of the query. * * @return mixed */ public function get_results(); } PK!I$VVsrc/API/Google/Query/Query.phpnu[resource = $resource_name; } /** * Set columns to retrieve in the query. * * @param array $columns List of column names. * * @return QueryInterface */ public function columns( array $columns ): QueryInterface { $this->validate_columns( $columns ); $this->columns = $columns; return $this; } /** * Add a set columns to retrieve in the query. * * @param array $columns List of column names. * * @return QueryInterface */ public function add_columns( array $columns ): QueryInterface { $this->validate_columns( $columns ); $this->columns = array_merge( $this->columns, array_filter( $columns ) ); return $this; } /** * Add a where clause to the query. * * @param string $column The column name. * @param mixed $value The where value. * @param string $compare The comparison to use. Valid values are =, <, >, IN, NOT IN. * * @return QueryInterface */ public function where( string $column, $value, string $compare = '=' ): QueryInterface { $this->validate_compare( $compare ); $this->where[] = [ 'column' => $column, 'value' => $value, 'compare' => $compare, ]; return $this; } /** * Add a where date between clause to the query. * * @since 1.7.0 * * @link https://developers.google.com/shopping-content/guides/reports/query-language/date-ranges * * @param string $after Start of date range. In ISO 8601(YYYY-MM-DD) format. * @param string $before End of date range. In ISO 8601(YYYY-MM-DD) format. * * @return QueryInterface */ public function where_date_between( string $after, string $before ): QueryInterface { return $this->where( 'segments.date', [ $after, $before ], 'BETWEEN' ); } /** * Set the where relation for the query. * * @param string $relation * * @return QueryInterface */ public function set_where_relation( string $relation ): QueryInterface { $this->validate_where_relation( $relation ); $this->where_relation = $relation; return $this; } /** * Set ordering information for the query. * * @param string $column * @param string $order * * @return QueryInterface * @throws InvalidQuery When the given column is not in the list of included columns. */ public function set_order( string $column, string $order = 'ASC' ): QueryInterface { if ( ! array_key_exists( $column, $this->columns ) ) { throw InvalidQuery::invalid_order_column( $column ); } $this->orderby = $this->columns[ $column ]; $this->order = $this->normalize_order( $order ); return $this; } /** * Get the results of the query. * * @return mixed */ public function get_results() { if ( null === $this->results ) { $this->query_results(); } return $this->results; } /** * Perform the query and save it to the results. */ protected function query_results() { $this->results = []; } /** * Validate a set of columns. * * @param array $columns * * @throws InvalidQuery When one of columns in the set is not valid. */ protected function validate_columns( array $columns ) { array_walk( $columns, [ $this, 'validate_column' ] ); } /** * Validate that a given column is using a valid name. * * @param string $column * * @throws InvalidQuery When the given column is not valid. */ protected function validate_column( string $column ) { if ( ! preg_match( '/^[a-zA-Z0-9\._]+$/', $column ) ) { throw InvalidQuery::invalid_column( $column ); } } /** * Validate that a compare operator is valid. * * @param string $compare * * @throws InvalidQuery When the compare value is not valid. */ protected function validate_compare( string $compare ) { switch ( $compare ) { case '=': case '>': case '<': case '!=': case 'IN': case 'NOT IN': case 'BETWEEN': case 'IS NOT NULL': case 'CONTAINS ANY': // These are all valid. return; default: throw InvalidQuery::from_compare( $compare ); } } /** * Validate that a where relation is valid. * * @param string $relation * * @throws InvalidQuery When the relation value is not valid. */ protected function validate_where_relation( string $relation ) { switch ( $relation ) { case 'AND': case 'OR': // These are all valid. return; default: throw InvalidQuery::where_relation( $relation ); } } /** * Normalize the string for the order. * * Converts the string to uppercase, and will return only DESC or ASC. * * @param string $order * * @return string */ protected function normalize_order( string $order ): string { $order = strtoupper( $order ); return 'DESC' === $order ? $order : 'ASC'; } /** * Build the query and return the query string. * * @return string * * @throws InvalidQuery When the set of columns is empty. */ protected function build_query(): string { if ( empty( $this->columns ) ) { throw InvalidQuery::empty_columns(); } $columns = join( ',', $this->columns ); $pieces = [ "SELECT {$columns} FROM {$this->resource}" ]; $pieces = array_merge( $pieces, $this->generate_where_pieces() ); if ( $this->orderby ) { $pieces[] = "ORDER BY {$this->orderby} {$this->order}"; } return join( ' ', $pieces ); } /** * Generate the pieces for the WHERE part of the query. * * @return string[] */ protected function generate_where_pieces(): array { if ( empty( $this->where ) ) { return []; } $where_pieces = [ 'WHERE' ]; foreach ( $this->where as $where ) { $column = $where['column']; $compare = $where['compare']; if ( 'IN' === $compare || 'NOT_IN' === $compare || 'CONTAINS ANY' === $compare ) { $value = sprintf( "('%s')", join( "','", array_map( function ( $value ) { return $this->escape( $value ); }, $where['value'] ) ) ); } elseif ( 'BETWEEN' === $compare ) { $value = "'{$this->escape( $where['value'][0] )}' AND '{$this->escape( $where['value'][1] )}'"; } elseif ( 'IS NOT NULL' === $compare ) { $value = ''; } else { $value = "'{$this->escape( $where['value'] )}'"; } if ( count( $where_pieces ) > 1 ) { $where_pieces[] = $this->where_relation ?? 'AND'; } $where_pieces[] = "{$column} {$compare} {$value}"; } return $where_pieces; } /** * Escape the value to a string which can be used in a query. * * @param mixed $value Original value to escape. * * @return string */ protected function escape( $value ): string { if ( $value instanceof DateTime ) { return $value->format( 'Y-m-d' ); } if ( ! is_numeric( $value ) ) { return (string) $value; } return addslashes( (string) $value ); } } PK!. 3??)src/API/Google/Query/ReportQueryTrait.phpnu[fields( $args['fields'] ); } if ( ! empty( $args['interval'] ) ) { $this->segment_interval( $args['interval'] ); } if ( ! empty( $args['after'] ) && ! empty( $args['before'] ) ) { $after = $args['after']; $before = $args['before']; $this->where_date_between( $after instanceof DateTime ? $after->format( 'Y-m-d' ) : $after, $before instanceof DateTime ? $before->format( 'Y-m-d' ) : $before ); } if ( ! empty( $args['ids'] ) ) { $this->filter( $args['ids'] ); } if ( ! empty( $args['orderby'] ) ) { $this->set_order( $args['orderby'], $args['order'] ); } if ( ! empty( $args['per_page'] ) ) { $this->search_args['pageSize'] = $args['per_page']; } if ( ! empty( $args['next_page'] ) ) { $this->search_args['pageToken'] = $args['next_page']; } } } PK!)33%src/API/Google/AdsAssetGroupAsset.phpnu[client = $client; $this->asset = $asset; } /** * Get the asset field types to use for the asset group assets query. * * @return string[] */ protected function get_asset_field_types_query(): array { return [ AssetFieldType::name( AssetFieldType::BUSINESS_NAME ), AssetFieldType::name( AssetFieldType::CALL_TO_ACTION_SELECTION ), AssetFieldType::name( AssetFieldType::DESCRIPTION ), AssetFieldType::name( AssetFieldType::HEADLINE ), AssetFieldType::name( AssetFieldType::LOGO ), AssetFieldType::name( AssetFieldType::LONG_HEADLINE ), AssetFieldType::name( AssetFieldType::MARKETING_IMAGE ), AssetFieldType::name( AssetFieldType::SQUARE_MARKETING_IMAGE ), AssetFieldType::name( AssetFieldType::PORTRAIT_MARKETING_IMAGE ), ]; } /** * Get Assets for specific asset groups ids. * * @param array $asset_groups_ids The asset groups ids. * @param array $fields The asset field types to get. * * @return array The assets for the asset groups. * @throws ExceptionWithResponseData When an ApiException is caught. */ public function get_assets_by_asset_group_ids( array $asset_groups_ids, array $fields = [] ): array { try { if ( empty( $asset_groups_ids ) ) { return []; } if ( empty( $fields ) ) { $fields = $this->get_asset_field_types_query(); } $asset_group_assets = []; $asset_results = ( new AdsAssetGroupAssetQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->add_columns( [ 'asset_group.id' ] ) ->where( 'asset_group.id', $asset_groups_ids, 'IN' ) ->where( 'asset_group_asset.field_type', $fields, 'IN' ) ->where( 'asset_group_asset.status', 'REMOVED', '!=' ) ->get_results(); /** @var GoogleAdsRow $row */ foreach ( $asset_results->iterateAllElements() as $row ) { /** @var AssetGroupAsset $asset_group_asset */ $asset_group_asset = $row->getAssetGroupAsset(); $field_type = AssetFieldType::label( $asset_group_asset->getFieldType() ); switch ( $field_type ) { case AssetFieldType::BUSINESS_NAME: case AssetFieldType::CALL_TO_ACTION_SELECTION: $asset_group_assets[ $row->getAssetGroup()->getId() ][ $field_type ] = $this->asset->convert_asset( $row ); break; default: $asset_group_assets[ $row->getAssetGroup()->getId() ][ $field_type ][] = $this->asset->convert_asset( $row ); } } return $asset_group_assets; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error retrieving asset groups assets: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors ] ); } } /** * Get Assets for specific final URL. * * @param string $url The final url. * @param bool $only_first_asset_group Whether to return only the first asset group found. * * @return array The assets for the asset groups with a specific final url. * @throws ExceptionWithResponseData When an ApiException is caught. */ public function get_assets_by_final_url( string $url, bool $only_first_asset_group = false ): array { try { $asset_group_assets = []; // Search urls with and without trailing slash. $asset_results = ( new AdsAssetGroupAssetQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->add_columns( [ 'asset_group.id', 'asset_group.path1', 'asset_group.path2' ] ) ->where( 'asset_group.final_urls', [ trailingslashit( $url ), untrailingslashit( $url ) ], 'CONTAINS ANY' ) ->where( 'asset_group_asset.field_type', $this->get_asset_field_types_query(), 'IN' ) ->where( 'asset_group_asset.status', 'REMOVED', '!=' ) ->where( 'asset_group.status', 'REMOVED', '!=' ) ->where( 'campaign.status', 'REMOVED', '!=' ) ->get_results(); /** @var GoogleAdsRow $row */ foreach ( $asset_results->iterateAllElements() as $row ) { /** @var AssetGroupAsset $asset_group_asset */ $asset_group_asset = $row->getAssetGroupAsset(); $field_type = AssetFieldType::label( $asset_group_asset->getFieldType() ); switch ( $field_type ) { case AssetFieldType::BUSINESS_NAME: case AssetFieldType::CALL_TO_ACTION_SELECTION: $asset_group_assets[ $row->getAssetGroup()->getId() ][ $field_type ] = $this->asset->convert_asset( $row )['content']; break; default: $asset_group_assets[ $row->getAssetGroup()->getId() ][ $field_type ][] = $this->asset->convert_asset( $row )['content']; } $asset_group_assets[ $row->getAssetGroup()->getId() ]['display_url_path'] = [ $row->getAssetGroup()->getPath1(), $row->getAssetGroup()->getPath2(), ]; } if ( $only_first_asset_group ) { return reset( $asset_group_assets ) ?: []; } return $asset_group_assets; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error retrieving asset groups assets by final url: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors ] ); } } /** * Get assets to be deleted. * * @param array $assets A list of assets. * * @return array The assets to be deleted. */ public function get_assets_to_be_deleted( array $assets ): array { return array_values( array_filter( $assets, function ( $asset ) { return ! empty( $asset['id'] ); } ) ); } /** * Get assets to be created. * * @param array $assets A list of assets. * * @return array The assets to be created. */ public function get_assets_to_be_created( array $assets ): array { return array_values( array_filter( $assets, function ( $asset ) { return ! empty( $asset['content'] ); } ) ); } /** * Get specific assets by asset types. * * @param int $asset_group_id The asset group id. * @param array $asset_field_types The asset field types types. * * @return array The assets. */ protected function get_specific_assets( int $asset_group_id, array $asset_field_types ): array { $result = $this->get_assets_by_asset_group_ids( [ $asset_group_id ], $asset_field_types ); $asset_group_assets = $result[ $asset_group_id ] ?? []; $specific_assets = []; foreach ( $asset_group_assets as $field_type => $assets ) { foreach ( $assets as $asset ) { $specific_assets[] = array_merge( $asset, [ 'field_type' => $field_type ] ); } } return $specific_assets; } /** * Check if a asset type will be edited. * * @param string $field_type The asset field type. * @param array $assets The assets. * * @return bool True if the asset type is edited. */ protected function maybe_asset_type_is_edited( string $field_type, array $assets ): bool { return in_array( $field_type, array_column( $assets, 'field_type' ), true ); } /** * Get override asset operations. * * @param int $asset_group_id The asset group id. * @param array $asset_field_types The asset field types. * * @return array The asset group asset operations. */ protected function get_override_operations( int $asset_group_id, array $asset_field_types ): array { return array_map( function ( $asset ) use ( $asset_group_id ) { return $this->delete_operation( $asset_group_id, $asset['field_type'], $asset['id'] ); }, $this->get_specific_assets( $asset_group_id, $asset_field_types ) ); } /** * Edit assets group assets. * * @param int $asset_group_id The asset group id. * @param array $assets The assets to create. * * @return array The asset group asset operations. * @throws Exception If the asset type is not supported. */ public function edit_operations( int $asset_group_id, array $assets ): array { if ( empty( $assets ) ) { return []; } $asset_group_assets_operations = []; $assets_for_creation = $this->get_assets_to_be_created( $assets ); $asset_arns = $this->asset->create_assets( $assets_for_creation ); $total_assets = count( $assets_for_creation ); $delete_asset_group_assets_operations = []; if ( $this->maybe_asset_type_is_edited( AssetFieldType::LOGO, $assets ) ) { // As we are not working with the LANDSCAPE_LOGO, we delete it so it does not interfere with the maximum quantities of logos. $delete_asset_group_assets_operations = $this->get_override_operations( $asset_group_id, [ AssetFieldType::name( AssetFieldType::LANDSCAPE_LOGO ) ] ); } // The asset mutation operation results (ARNs) are returned in the same order as the operations are specified. // See: https://youtu.be/9KaVjqW5tVM?t=103 for ( $i = 0; $i < $total_assets; $i++ ) { $asset_group_assets_operations[] = $this->create_operation( $asset_group_id, $assets_for_creation[ $i ]['field_type'], $asset_arns[ $i ] ); } foreach ( $this->get_assets_to_be_deleted( $assets ) as $asset ) { $delete_asset_group_assets_operations[] = $this->delete_operation( $asset_group_id, $asset['field_type'], $asset['id'] ); } // The delete operations must be executed first otherwise will cause a conflict with existing assets with identical content. // See here: https://github.com/woocommerce/google-listings-and-ads/pull/1870 return array_merge( $delete_asset_group_assets_operations, $asset_group_assets_operations ); } /** * Creates an operation for an asset group asset. * * @param int $asset_group_id The ID of the asset group. * @param string $asset_field_type The field type of the asset. * @param string $asset_arn The the asset ARN. * * @return MutateOperation The mutate create operation for the asset group asset. */ protected function create_operation( int $asset_group_id, string $asset_field_type, string $asset_arn ): MutateOperation { $operation = new AssetGroupAssetOperation(); $new_asset_group_asset = new AssetGroupAsset( [ 'asset' => $asset_arn, 'asset_group' => ResourceNames::forAssetGroup( $this->options->get_ads_id(), $asset_group_id ), 'field_type' => AssetFieldType::number( $asset_field_type ), ] ); return ( new MutateOperation() )->setAssetGroupAssetOperation( $operation->setCreate( $new_asset_group_asset ) ); } /** * Returns a delete operation for asset group asset. * * @param int $asset_group_id The ID of the asset group. * @param string $asset_field_type The field type of the asset. * @param int $asset_id The ID of the asset. * * @return MutateOperation The remove operation for the asset group asset. */ protected function delete_operation( int $asset_group_id, string $asset_field_type, int $asset_id ): MutateOperation { $asset_group_asset_resource_name = ResourceNames::forAssetGroupAsset( $this->options->get_ads_id(), $asset_group_id, $asset_id, AssetFieldType::name( $asset_field_type ) ); $operation = ( new AssetGroupAssetOperation() )->setRemove( $asset_group_asset_resource_name ); return ( new MutateOperation() )->setAssetGroupAssetOperation( $operation ); } } PK!4dD7D7 src/API/Google/AdsAssetGroup.phpnu[client = $client; $this->asset_group_asset = $asset_group_asset; } /** * Create an asset group. * * @since 2.4.0 * * @param int $campaign_id * * @return int id The asset group id. * @throws ExceptionWithResponseData When an ApiException or Exception is caught. */ public function create_asset_group( int $campaign_id ): int { try { $campaign_resource_name = ResourceNames::forCampaign( $this->options->get_ads_id(), $campaign_id ); $current_date_time = ( new DateTime( 'now', wp_timezone() ) )->format( 'Y-m-d H:i:s' ); $asset_group_name = sprintf( /* translators: %s: current date time. */ __( 'PMax %s', 'google-listings-and-ads' ), $current_date_time ); $operations = $this->create_operations( $campaign_resource_name, $asset_group_name ); return $this->mutate( $operations ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $message = $e->getMessage(); $code = $e->getCode(); $data = []; if ( $e instanceof ApiException ) { $errors = $this->get_exception_errors( $e ); /* translators: %s Error message */ $message = sprintf( __( 'Error creating asset group: %s', 'google-listings-and-ads' ), reset( $errors ) ); $code = $this->map_grpc_code_to_http_status_code( $e ); $data = [ 'errors' => $errors, ]; } throw new ExceptionWithResponseData( $message, $code, null, $data ); } } /** * Returns a set of operations to create an asset group. * * @param string $campaign_resource_name * @param string $asset_group_name The asset group name. * @return array */ public function create_operations( string $campaign_resource_name, string $asset_group_name ): array { // Asset must be created before listing group. return [ $this->asset_group_create_operation( $campaign_resource_name, $asset_group_name ), $this->listing_group_create_operation(), ]; } /** * Returns an asset group create operation. * * @param string $campaign_resource_name * @param string $campaign_name * * @return MutateOperation */ protected function asset_group_create_operation( string $campaign_resource_name, string $campaign_name ): MutateOperation { $asset_group = new AssetGroup( [ 'resource_name' => $this->temporary_resource_name(), 'name' => $campaign_name . ' Asset Group', 'campaign' => $campaign_resource_name, 'status' => AssetGroupStatus::ENABLED, ] ); $operation = ( new AssetGroupOperation() )->setCreate( $asset_group ); return ( new MutateOperation() )->setAssetGroupOperation( $operation ); } /** * Returns an asset group listing group filter create operation. * * @return MutateOperation */ protected function listing_group_create_operation(): MutateOperation { $listing_group = new AssetGroupListingGroupFilter( [ 'asset_group' => $this->temporary_resource_name(), 'type' => ListingGroupFilterType::UNIT_INCLUDED, 'listing_source' => ListingGroupFilterListingSource::SHOPPING, ] ); $operation = ( new AssetGroupListingGroupFilterOperation() )->setCreate( $listing_group ); return ( new MutateOperation() )->setAssetGroupListingGroupFilterOperation( $operation ); } /** * Returns an asset group delete operation. * * @param string $campaign_resource_name * * @return MutateOperation[] */ protected function asset_group_delete_operations( string $campaign_resource_name ): array { $operations = []; $this->asset_groups = []; $results = ( new AdsAssetGroupQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->where( 'asset_group.campaign', $campaign_resource_name ) ->get_results(); /** @var GoogleAdsRow $row */ foreach ( $results->iterateAllElements() as $row ) { $resource_name = $row->getAssetGroup()->getResourceName(); $this->asset_groups[] = $resource_name; $operation = ( new AssetGroupOperation() )->setRemove( $resource_name ); $operations[] = ( new MutateOperation() )->setAssetGroupOperation( $operation ); } return $operations; } /** * Return a temporary resource name for the asset group. * * @return string */ protected function temporary_resource_name() { return ResourceNames::forAssetGroup( $this->options->get_ads_id(), self::TEMPORARY_ID ); } /** * Get Asset Groups for a specific campaign. Limit to first AdsAssetGroup. * * @since 2.4.0 * * @param int $campaign_id The campaign ID. * @param bool $include_assets Whether to include the assets in the response. * * @return array The asset groups for the campaign. * @throws ExceptionWithResponseData When an ApiException is caught. */ public function get_asset_groups_by_campaign_id( int $campaign_id, bool $include_assets = true ): array { try { $asset_groups_converted = []; $asset_group_results = ( new AdsAssetGroupQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->add_columns( [ 'asset_group.path1', 'asset_group.path2', 'asset_group.id', 'asset_group.final_urls' ] ) ->where( 'campaign.id', $campaign_id ) ->where( 'asset_group.status', 'REMOVED', '!=' ) ->get_results(); /** @var GoogleAdsRow $row */ foreach ( $asset_group_results->getPage()->getIterator() as $row ) { $asset_groups_converted[ $row->getAssetGroup()->getId() ] = $this->convert_asset_group( $row ); break; // Limit to only first asset group. } if ( $include_assets ) { return array_values( $this->get_assets( $asset_groups_converted ) ); } return array_values( $asset_groups_converted ); } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error retrieving asset groups: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors ] ); } } /** * Get assets for asset groups. * * @since 2.4.0 * * @param array $asset_groups The asset groups converted. * * @return array The asset groups with assets. */ protected function get_assets( array $asset_groups ): array { $asset_group_ids = array_keys( $asset_groups ); $assets = $this->asset_group_asset->get_assets_by_asset_group_ids( $asset_group_ids ); foreach ( $asset_group_ids as $asset_group_id ) { $asset_groups[ $asset_group_id ]['assets'] = $assets[ $asset_group_id ] ?? (object) []; } return $asset_groups; } /** * Edit an asset group. * * @param int $asset_group_id The asset group ID. * @param array $data The asset group data. * @param array $assets A list of assets data. * * @return int The asset group ID. * @throws ExceptionWithResponseData When an ApiException is caught. */ public function edit_asset_group( int $asset_group_id, array $data, array $assets = [] ): int { try { $operations = $this->asset_group_asset->edit_operations( $asset_group_id, $assets ); // PMax only supports one final URL but it is required to be an array. if ( ! empty( $data['final_url'] ) ) { $data['final_urls'] = [ $data['final_url'] ]; unset( $data['final_url'] ); } if ( ! empty( $data ) ) { // If the asset group does not contain a final URL, it is required to update first the asset group with the final URL and then the assets. $operations = [ $this->edit_operation( $asset_group_id, $data ), ...$operations ]; } if ( ! empty( $operations ) ) { $this->mutate( $operations ); } return $asset_group_id; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); if ( $e->getCode() === 413 ) { $errors = [ 'Request entity too large' ]; $code = $e->getCode(); } else { $errors = $this->get_exception_errors( $e ); $code = $this->map_grpc_code_to_http_status_code( $e ); if ( array_key_exists( 'DUPLICATE_ASSETS_WITH_DIFFERENT_FIELD_VALUE', $errors ) ) { $errors['DUPLICATE_ASSETS_WITH_DIFFERENT_FIELD_VALUE'] = __( 'Each image type (landscape, square, portrait or logo) cannot contain duplicated images.', 'google-listings-and-ads' ); } } throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error editing asset group: %s', 'google-listings-and-ads' ), reset( $errors ) ), $code, null, [ 'errors' => $errors, 'id' => $asset_group_id, ] ); } } /** * Returns an asset group edit operation. * * @param integer $asset_group_id The Asset Group ID * @param array $fields The fields to update. * * @return MutateOperation */ protected function edit_operation( int $asset_group_id, array $fields ): MutateOperation { $fields['resource_name'] = ResourceNames::forAssetGroup( $this->options->get_ads_id(), $asset_group_id ); $asset_group = new AssetGroup( $fields ); $operation = new AssetGroupOperation(); $operation->setUpdate( $asset_group ); // We create the FieldMask manually because empty paths (path1 and path2) are not processed by the library. // See similar issue here: https://github.com/googleads/google-ads-php/issues/487 $operation->setUpdateMask( ( new FieldMask() )->setPaths( [ 'resource_name', ...array_keys( $fields ) ] ) ); return ( new MutateOperation() )->setAssetGroupOperation( $operation ); } /** * Convert Asset Group data to an array. * * @since 2.4.0 * * @param GoogleAdsRow $row Data row returned from a query request. * * @return array */ protected function convert_asset_group( GoogleAdsRow $row ): array { return [ 'id' => $row->getAssetGroup()->getId(), 'final_url' => iterator_to_array( $row->getAssetGroup()->getFinalUrls() )[0] ?? '', 'display_url_path' => [ $row->getAssetGroup()->getPath1(), $row->getAssetGroup()->getPath2() ], ]; } /** * Send a batch of operations to mutate an asset group. * * @since 2.4.0 * * @param MutateOperation[] $operations * * @return int If the asset group operation is present, it will return the asset group id otherwise 0 for other operations. * @throws ApiException If any of the operations fail. * @throws Exception If the resource name is not in the expected format. */ protected function mutate( array $operations ): int { $request = new MutateGoogleAdsRequest(); $request->setCustomerId( $this->options->get_ads_id() ); $request->setMutateOperations( $operations ); $responses = $this->client->getGoogleAdsServiceClient()->mutate( $request ); foreach ( $responses->getMutateOperationResponses() as $response ) { if ( 'asset_group_result' === $response->getResponse() ) { $asset_group_result = $response->getAssetGroupResult(); return $this->parse_asset_group_id( $asset_group_result->getResourceName() ); } } return 0; } /** * Convert ID from a resource name to an int. * * @since 2.4.0 * * @param string $name Resource name containing ID number. * * @return int The asset group ID. * @throws Exception When unable to parse resource ID. */ protected function parse_asset_group_id( string $name ): int { try { $parts = AssetGroupServiceClient::parseName( $name ); return absint( $parts['asset_group_id'] ); } catch ( ValidationException $e ) { throw new Exception( __( 'Invalid asset group ID', 'google-listings-and-ads' ) ); } } } PK!~.##src/API/Google/AdsAsset.phpnu[client = $client; $this->wp = $wp; } /** * Temporary ID to use within a batch job. * A negative number which is unique for all the created resources. * * @var int */ protected static $temporary_id = -5; /** * Return a temporary resource name for the asset. * * @param int $temporary_id The temporary ID to use for the asset. * * @return string The Asset resource name. */ protected function temporary_resource_name( int $temporary_id ): string { return ResourceNames::forAsset( $this->options->get_ads_id(), $temporary_id ); } /** * Returns the asset type for the given field type. * * @param string $field_type The field type. * * @return int The asset type. * @throws Exception If the field type is not supported. */ protected function get_asset_type_by_field_type( string $field_type ): int { switch ( $field_type ) { case AssetFieldType::LOGO: case AssetFieldType::MARKETING_IMAGE: case AssetFieldType::SQUARE_MARKETING_IMAGE: case AssetFieldType::PORTRAIT_MARKETING_IMAGE: return AssetType::IMAGE; case AssetFieldType::CALL_TO_ACTION_SELECTION: return AssetType::CALL_TO_ACTION; case AssetFieldType::HEADLINE: case AssetFieldType::LONG_HEADLINE: case AssetFieldType::DESCRIPTION: case AssetFieldType::BUSINESS_NAME: return AssetType::TEXT; default: throw new Exception( 'Asset Field type not supported' ); } } /** * Returns the image data. * * @param string $url The image url. * * @return array The image data. * @throws Exception If the image url is not a valid url or the image size is too large. */ protected function get_image_data( string $url ): array { $image_data = $this->wp->wp_remote_get( $url ); if ( is_wp_error( $image_data ) || empty( $image_data['body'] ) ) { throw new Exception( sprintf( 'There was a problem loading the url: %s', $url ) ); } $size = $image_data['headers']->offsetGet( 'content-length' ); if ( $size > self::MAX_IMAGE_SIZE_BYTES ) { throw new Exception( 'Image size is too large.' ); } return [ 'body' => $image_data['body'], 'size' => $size, ]; } /** * Returns a list of batches of assets. * * @param array $assets A list of assets. * @param int $max_size The maximum size of the payload in bytes. * * @return array A list of batches of assets. * @throws Exception If the image url is not a valid url, if the field type is not supported or the image size is too big. */ protected function create_batches( array $assets, int $max_size = self::MAX_PAYLOAD_BYTES ): array { $batch_size = 0; $index = 0; $batches = []; foreach ( $assets as $asset ) { if ( $this->get_asset_type_by_field_type( $asset['field_type'] ) === AssetType::IMAGE ) { $image_data = $this->get_image_data( $asset['content'] ); $asset['body'] = $image_data['body']; $batch_size += $image_data['size']; if ( $batch_size > $max_size ) { $batches[ ++$index ][] = $asset; $batch_size = $image_data['size']; continue; } } $batches[ $index ][] = $asset; } return $batches; } /** * Creates the assets so they can be used in the asset groups. * * @param array $assets The assets to create. * @param int $batch_size The maximum size of the payload in bytes. * * @return array A list of Asset's ARN created. * * @throws Exception If the asset type is not supported or if the image url is not a valid url. * @throws ApiException If any of the operations fail. */ public function create_assets( array $assets, int $batch_size = self::MAX_PAYLOAD_BYTES ): array { if ( empty( $assets ) ) { return []; } $batches = $this->create_batches( $assets, $batch_size ); $arns = []; foreach ( $batches as $batch ) { $operations = []; foreach ( $batch as $asset ) { $operations[] = $this->create_operation( $asset, self::$temporary_id-- ); } // If the mutate operation fails, it will throw an exception that will be caught by the caller. $arns = [ ...$arns, ...$this->mutate( $operations ) ]; } return $arns; } /** * Returns an operation to create a text asset. * * @param array $data The asset data. * @param int $temporary_id The temporary ID to use for the asset. * * @return MutateOperation The create asset operation. * @throws Exception If the asset type is not supported. */ protected function create_operation( array $data, int $temporary_id ): MutateOperation { $asset = new Asset( [ 'resource_name' => $this->temporary_resource_name( $temporary_id ), ] ); switch ( $this->get_asset_type_by_field_type( $data['field_type'] ) ) { case AssetType::CALL_TO_ACTION: $asset->setCallToActionAsset( new CallToActionAsset( [ 'call_to_action' => CallToActionType::number( $data['content'] ) ] ) ); break; case AssetType::IMAGE: $asset->setImageAsset( new ImageAsset( [ 'data' => $data['body'] ] ) ); $asset->setName( basename( $data['content'] ) ); break; case AssetType::TEXT: $asset->setTextAsset( new TextAsset( [ 'text' => $data['content'] ] ) ); break; default: throw new Exception( 'Asset type not supported' ); } $operation = ( new AssetOperation() )->setCreate( $asset ); return ( new MutateOperation() )->setAssetOperation( $operation ); } /** * Returns the asset content for the given row. * * @param GoogleAdsRow $row Data row returned from a query request. * * @return string The asset content. */ protected function get_asset_content( GoogleAdsRow $row ): string { /** @var Asset $asset */ $asset = $row->getAsset(); switch ( $asset->getType() ) { case AssetType::IMAGE: return $asset->getImageAsset()->getFullSize()->getUrl(); case AssetType::TEXT: return $asset->getTextAsset()->getText(); case AssetType::CALL_TO_ACTION: // When CallToActionType::UNSPECIFIED is returned, does not have a CallToActionAsset. if ( ! $asset->getCallToActionAsset() ) { return CallToActionType::UNSPECIFIED; } return CallToActionType::label( $asset->getCallToActionAsset()->getCallToAction() ); default: return ''; } } /** * Convert Asset data to an array. * * @param GoogleAdsRow $row Data row returned from a query request. * * @return array The asset data converted. */ public function convert_asset( GoogleAdsRow $row ): array { return [ 'id' => $row->getAsset()->getId(), 'content' => $this->get_asset_content( $row ), ]; } /** * Send a batch of operations to mutate assets. * * @param MutateOperation[] $operations * * @return array A list of Asset's ARN created. * @throws ApiException If any of the operations fail. */ protected function mutate( array $operations ): array { $arns = []; $request = new MutateGoogleAdsRequest(); $request->setCustomerId( $this->options->get_ads_id() ); $request->setMutateOperations( $operations ); $responses = $this->client->getGoogleAdsServiceClient()->mutate( $request ); foreach ( $responses->getMutateOperationResponses() as $response ) { if ( 'asset_result' === $response->getResponse() ) { $asset_result = $response->getAssetResult(); $arns[] = $asset_result->getResourceName(); } } return $arns; } } PK!PP$src/API/Google/AdsCampaignBudget.phpnu[client = $client; } /** * Returns a new campaign budget create operation. * * @param string $campaign_name New campaign name. * @param float $amount Budget amount in the local currency. * * @return MutateOperation */ public function create_operation( string $campaign_name, float $amount ): MutateOperation { $budget = new CampaignBudget( [ 'resource_name' => $this->temporary_resource_name(), 'name' => $campaign_name . ' Budget', 'amount_micros' => $this->to_micro( $amount ), 'explicitly_shared' => false, ] ); $operation = ( new CampaignBudgetOperation() )->setCreate( $budget ); return ( new MutateOperation() )->setCampaignBudgetOperation( $operation ); } /** * Updates a new campaign budget. * * @param int $campaign_id Campaign ID. * @param float $amount Budget amount in the local currency. * * @return string Resource name of the updated budget. * @throws Exception If no linked budget has been found. */ public function edit_operation( int $campaign_id, float $amount ): MutateOperation { $budget_id = $this->get_budget_from_campaign( $campaign_id ); $budget = new CampaignBudget( [ 'resource_name' => ResourceNames::forCampaignBudget( $this->options->get_ads_id(), $budget_id ), 'amount_micros' => $this->to_micro( $amount ), ] ); $operation = new CampaignBudgetOperation(); $operation->setUpdate( $budget ); $operation->setUpdateMask( FieldMasks::allSetFieldsOf( $budget ) ); return ( new MutateOperation() )->setCampaignBudgetOperation( $operation ); } /** * Return a temporary resource name for the campaign budget. * * @return string */ public function temporary_resource_name() { return ResourceNames::forCampaignBudget( $this->options->get_ads_id(), self::TEMPORARY_ID ); } /** * Retrieve the linked budget ID from a campaign ID. * * @param int $campaign_id Campaign ID. * * @return int * @throws Exception If no linked budget has been found. */ protected function get_budget_from_campaign( int $campaign_id ): int { $results = ( new AdsCampaignBudgetQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->where( 'campaign.id', $campaign_id ) ->get_results(); foreach ( $results->iterateAllElements() as $row ) { $campaign = $row->getCampaign(); return $this->parse_campaign_budget_id( $campaign->getCampaignBudget() ); } /* translators: %d Campaign ID */ throw new Exception( sprintf( __( 'No budget found for campaign %d', 'google-listings-and-ads' ), $campaign_id ) ); } /** * Convert ID from a resource name to an int. * * @param string $name Resource name containing ID number. * * @return int * @throws Exception When unable to parse resource ID. */ protected function parse_campaign_budget_id( string $name ): int { try { $parts = CampaignBudgetServiceClient::parseName( $name ); return absint( $parts['campaign_budget_id'] ); } catch ( ValidationException $e ) { throw new Exception( __( 'Invalid campaign budget ID', 'google-listings-and-ads' ) ); } } } PK!6'src/API/Google/AdsCampaignCriterion.phpnu[create_operation( $campaign_resource_name, $location_id ); }, $location_ids ); } /** * Returns a new campaign criterion create operation. * * @param string $campaign_resource_name Campaign resource name. * @param int $location_id Targeted location ID. * * @return MutateOperation */ protected function create_operation( string $campaign_resource_name, int $location_id ): MutateOperation { $campaign_criterion = new CampaignCriterion( [ 'campaign' => $campaign_resource_name, 'negative' => false, 'status' => CampaignCriterionStatus::ENABLED, 'location' => new LocationInfo( [ 'geo_target_constant' => ResourceNames::forGeoTargetConstant( $location_id ), ] ), ] ); $operation = ( new CampaignCriterionOperation() )->setCreate( $campaign_criterion ); return ( new MutateOperation() )->setCampaignCriterionOperation( $operation ); } } PK!:u#src/API/Google/AdsCampaignLabel.phpnu[client = $client; } /** * Get the label ID by name. * * @param string $name The label name. * * @return null|int The label ID. * * @throws ApiException If the search call fails. */ protected function get_label_id_by_name( string $name ) { $query = new AdsCampaignLabelQuery(); $query->set_client( $this->client, $this->options->get_ads_id() ); $query->where( 'label.name', $name, '=' ); $label_results = $query->get_results(); foreach ( $label_results->iterateAllElements() as $row ) { return $row->getLabel()->getId(); } return null; } /** * Assign a label to a campaign by label name. * * @param int $campaign_id The campaign ID. * @param string $label_name The label name. * * @throws ApiException If searching for the label fails. */ public function assign_label_to_campaign_by_label_name( int $campaign_id, string $label_name ) { $label_id = $this->get_label_id_by_name( $label_name ); $operations = []; if ( ! $label_id ) { $operations[] = $this->create_operation( $label_name ); $label_id = self::TEMPORARY_ID; } $operations[] = $this->assign_label_to_campaign_operation( $campaign_id, $label_id ); $this->mutate( $operations ); } /** * Create a label operation. * * @param string $name The label name. * * @return MutateOperation */ protected function create_operation( string $name ): MutateOperation { $label = new Label( [ 'name' => $name, 'resource_name' => $this->temporary_resource_name(), ] ); $operation = ( new LabelOperation() )->setCreate( $label ); return ( new MutateOperation() )->setLabelOperation( $operation ); } /** * Return a temporary resource name for the label. * * @return string */ protected function temporary_resource_name() { return ResourceNames::forLabel( $this->options->get_ads_id(), self::TEMPORARY_ID ); } /** * Creates a campaign label operation. * * @param int $campaign_id The campaign ID. * @param int $label_id The label ID. * * @return MutateOperation */ protected function assign_label_to_campaign_operation( int $campaign_id, int $label_id ): MutateOperation { $label_resource_name = ResourceNames::forLabel( $this->options->get_ads_id(), $label_id ); $campaign_label = new CampaignLabel( [ 'campaign' => ResourceNames::forCampaign( $this->options->get_ads_id(), $campaign_id ), 'label' => $label_resource_name, ] ); $operation = ( new CampaignLabelOperation() )->setCreate( $campaign_label ); return ( new MutateOperation() )->setCampaignLabelOperation( $operation ); } /** * Mutate the operations. * * @param array $operations The operations to mutate. * * @throws ApiException — Thrown if the API call fails. */ protected function mutate( array $operations ) { $request = new MutateGoogleAdsRequest(); $request->setCustomerId( $this->options->get_ads_id() ); $request->setMutateOperations( $operations ); $this->client->getGoogleAdsServiceClient()->mutate( $request ); } } PK!QQsrc/API/Google/AdsCampaign.phpnu[client = $client; $this->budget = $budget; $this->criterion = $criterion; $this->google_helper = $google_helper; $this->campaign_label = $campaign_label; } /** * Returns a list of campaigns with targeted locations retrieved from campaign criterion. * * @param bool $exclude_removed Exclude removed campaigns (default true). * @param bool $fetch_criterion Combine the campaign data with criterion data (default true). * @param array $args Arguments for fetching campaigns, for example: per_page for limiting the number of results. * * @return array * @throws ExceptionWithResponseData When an ApiException is caught. */ public function get_campaigns( bool $exclude_removed = true, bool $fetch_criterion = true, array $args = [] ): array { try { $query = ( new AdsCampaignQuery() )->set_client( $this->client, $this->options->get_ads_id() ); if ( $exclude_removed ) { $query->where( 'campaign.status', 'REMOVED', '!=' ); } $count = 0; $campaign_results = $query->get_results(); $converted_campaigns = []; foreach ( $campaign_results->iterateAllElements() as $row ) { ++$count; $campaign = $this->convert_campaign( $row ); $converted_campaigns[ $campaign['id'] ] = $campaign; // Break early if we request a limited result. if ( ! empty( $args['per_page'] ) && $count >= $args['per_page'] ) { break; } } if ( $exclude_removed ) { // Cache campaign count. $campaign_count = $campaign_results->getPage()->getResponseObject()->getTotalResultsCount(); $this->container->get( TransientsInterface::class )->set( TransientsInterface::ADS_CAMPAIGN_COUNT, $campaign_count, HOUR_IN_SECONDS * 12 ); } if ( $fetch_criterion ) { $converted_campaigns = $this->combine_campaigns_and_campaign_criterion_results( $converted_campaigns ); } return array_values( $converted_campaigns ); } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error retrieving campaigns: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors ] ); } } /** * Retrieve a single campaign with targeted locations retrieved from campaign criterion. * * @param int $id Campaign ID. * * @return array * @throws ExceptionWithResponseData When an ApiException is caught. */ public function get_campaign( int $id ): array { try { $campaign_results = ( new AdsCampaignQuery() )->set_client( $this->client, $this->options->get_ads_id() ) ->where( 'campaign.id', $id, '=' ) ->get_results(); $converted_campaigns = []; // Get only the first element from campaign results foreach ( $campaign_results->iterateAllElements() as $row ) { $campaign = $this->convert_campaign( $row ); $converted_campaigns[ $campaign['id'] ] = $campaign; break; } if ( ! empty( $converted_campaigns ) ) { $combined_results = $this->combine_campaigns_and_campaign_criterion_results( $converted_campaigns ); return reset( $combined_results ); } return []; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error retrieving campaign: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors, 'id' => $id, ] ); } } /** * Create a new campaign. * * @param array $params Request parameters. * * @return array * @throws ExceptionWithResponseData When an ApiException is caught. */ public function create_campaign( array $params ): array { try { $base_country = $this->container->get( WC::class )->get_base_country(); $location_ids = array_map( function ( $country_code ) { return $this->google_helper->find_country_id_by_code( $country_code ); }, $params['targeted_locations'] ); $location_ids = array_filter( $location_ids ); // Operations must be in a specific order to match the temporary ID's. $operations = array_merge( [ $this->budget->create_operation( $params['name'], $params['amount'] ) ], [ $this->create_operation( $params['name'], $base_country ) ], $this->container->get( AdsAssetGroup::class )->create_operations( $this->temporary_resource_name(), $params['name'] ), $this->criterion->create_operations( $this->temporary_resource_name(), $location_ids ) ); $campaign_id = $this->mutate( $operations ); if ( isset( $params['label'] ) ) { $this->campaign_label->assign_label_to_campaign_by_label_name( $campaign_id, $params['label'] ); } // Clear cached campaign count. $this->container->get( TransientsInterface::class )->delete( TransientsInterface::ADS_CAMPAIGN_COUNT ); return [ 'id' => $campaign_id, 'status' => CampaignStatus::ENABLED, 'type' => CampaignType::PERFORMANCE_MAX, 'country' => $base_country, ] + $params; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); /* translators: %s Error message */ $message = sprintf( __( 'Error creating campaign: %s', 'google-listings-and-ads' ), reset( $errors ) ); if ( isset( $errors['DUPLICATE_CAMPAIGN_NAME'] ) ) { $message = __( 'A campaign with this name already exists', 'google-listings-and-ads' ); } throw new ExceptionWithResponseData( $message, $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors ] ); } } /** * Edit a campaign. * * @param int $campaign_id Campaign ID. * @param array $params Request parameters. * * @return int * @throws ExceptionWithResponseData When an ApiException is caught. */ public function edit_campaign( int $campaign_id, array $params ): int { try { $operations = []; $campaign_fields = []; if ( ! empty( $params['name'] ) ) { $campaign_fields['name'] = $params['name']; } if ( ! empty( $params['status'] ) ) { $campaign_fields['status'] = CampaignStatus::number( $params['status'] ); } if ( ! empty( $params['amount'] ) ) { $operations[] = $this->budget->edit_operation( $campaign_id, $params['amount'] ); } if ( ! empty( $campaign_fields ) ) { $operations[] = $this->edit_operation( $campaign_id, $campaign_fields ); } if ( ! empty( $operations ) ) { return $this->mutate( $operations ) ?: $campaign_id; } return $campaign_id; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error editing campaign: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors, 'id' => $campaign_id, ] ); } } /** * Delete a campaign. * * @param int $campaign_id Campaign ID. * * @return int * @throws ExceptionWithResponseData When an ApiException is caught. */ public function delete_campaign( int $campaign_id ): int { try { $campaign_resource_name = ResourceNames::forCampaign( $this->options->get_ads_id(), $campaign_id ); $operations = [ $this->delete_operation( $campaign_resource_name ), ]; // Clear cached campaign count. $this->container->get( TransientsInterface::class )->delete( TransientsInterface::ADS_CAMPAIGN_COUNT ); return $this->mutate( $operations ); } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); /* translators: %s Error message */ $message = sprintf( __( 'Error deleting campaign: %s', 'google-listings-and-ads' ), reset( $errors ) ); if ( isset( $errors['OPERATION_NOT_PERMITTED_FOR_REMOVED_RESOURCE'] ) ) { $message = __( 'This campaign has already been deleted', 'google-listings-and-ads' ); } throw new ExceptionWithResponseData( $message, $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors, 'id' => $campaign_id, ] ); } } /** * Retrieves the status of converting campaigns. * The status is cached for an hour during unconverted. * * - unconverted - Still need to convert some older campaigns * - converted - All campaigns are converted to PMax campaigns * - not-applicable - User never had any older campaign types * * @since 2.0.3 * * @return string */ public function get_campaign_convert_status(): string { $convert_status = $this->options->get( OptionsInterface::CAMPAIGN_CONVERT_STATUS ); if ( ! is_array( $convert_status ) || empty( $convert_status['status'] ) ) { $convert_status = [ 'status' => 'unknown' ]; } // Refetch if status is unconverted and older than an hour. if ( in_array( $convert_status['status'], [ 'unconverted', 'unknown' ], true ) && ( empty( $convert_status['updated'] ) || time() - $convert_status['updated'] > HOUR_IN_SECONDS ) ) { $old_campaigns = 0; $old_removed_campaigns = 0; $convert_status['status'] = 'unconverted'; try { foreach ( $this->get_campaigns( false, false ) as $campaign ) { if ( CampaignType::PERFORMANCE_MAX !== $campaign['type'] ) { if ( CampaignStatus::REMOVED === $campaign['status'] ) { ++$old_removed_campaigns; } else { ++$old_campaigns; } } } // No old campaign types means we don't need to convert. if ( ! $old_removed_campaigns && ! $old_campaigns ) { $convert_status['status'] = 'not-applicable'; } // All old campaign types have been removed, means we converted. if ( ! $old_campaigns && $old_removed_campaigns > 0 ) { $convert_status['status'] = 'converted'; } } catch ( Exception $e ) { // Error when retrieving campaigns, do not handle conversion. $convert_status['status'] = 'unknown'; } $convert_status['updated'] = time(); $this->options->update( OptionsInterface::CAMPAIGN_CONVERT_STATUS, $convert_status ); } return $convert_status['status']; } /** * Return a temporary resource name for the campaign. * * @return string */ protected function temporary_resource_name() { return ResourceNames::forCampaign( $this->options->get_ads_id(), self::TEMPORARY_ID ); } /** * Returns a campaign create operation. * * @param string $campaign_name * @param string $country * * @return MutateOperation */ protected function create_operation( string $campaign_name, string $country ): MutateOperation { $campaign = new Campaign( [ 'resource_name' => $this->temporary_resource_name(), 'name' => $campaign_name, 'advertising_channel_type' => AdvertisingChannelType::PERFORMANCE_MAX, 'status' => CampaignStatus::number( 'enabled' ), 'campaign_budget' => $this->budget->temporary_resource_name(), 'maximize_conversion_value' => new MaximizeConversionValue(), 'url_expansion_opt_out' => true, 'shopping_setting' => new ShoppingSetting( [ 'merchant_id' => $this->options->get_merchant_id(), 'feed_label' => $country, ] ), ] ); $operation = ( new CampaignOperation() )->setCreate( $campaign ); return ( new MutateOperation() )->setCampaignOperation( $operation ); } /** * Returns a campaign edit operation. * * @param integer $campaign_id * @param array $fields * * @return MutateOperation */ protected function edit_operation( int $campaign_id, array $fields ): MutateOperation { $fields['resource_name'] = ResourceNames::forCampaign( $this->options->get_ads_id(), $campaign_id ); $campaign = new Campaign( $fields ); $operation = new CampaignOperation(); $operation->setUpdate( $campaign ); $operation->setUpdateMask( FieldMasks::allSetFieldsOf( $campaign ) ); return ( new MutateOperation() )->setCampaignOperation( $operation ); } /** * Returns a campaign delete operation. * * @param string $campaign_resource_name * * @return MutateOperation */ protected function delete_operation( string $campaign_resource_name ): MutateOperation { $operation = ( new CampaignOperation() )->setRemove( $campaign_resource_name ); return ( new MutateOperation() )->setCampaignOperation( $operation ); } /** * Convert campaign data to an array. * * @param GoogleAdsRow $row Data row returned from a query request. * * @return array */ protected function convert_campaign( GoogleAdsRow $row ): array { $campaign = $row->getCampaign(); $data = [ 'id' => $campaign->getId(), 'name' => $campaign->getName(), 'status' => CampaignStatus::label( $campaign->getStatus() ), 'type' => CampaignType::label( $campaign->getAdvertisingChannelType() ), 'targeted_locations' => [], ]; $budget = $row->getCampaignBudget(); if ( $budget ) { $data += [ 'amount' => $this->from_micro( $budget->getAmountMicros() ), ]; } $shopping = $campaign->getShoppingSetting(); if ( $shopping ) { $data += [ 'country' => $shopping->getFeedLabel(), ]; } return $data; } /** * Combine converted campaigns data with campaign criterion results data * * @param array $campaigns Campaigns data returned from a query request and converted by convert_campaign function. * * @return array */ protected function combine_campaigns_and_campaign_criterion_results( array $campaigns ): array { if ( empty( $campaigns ) ) { return []; } $campaign_criterion_results = ( new AdsCampaignCriterionQuery() )->set_client( $this->client, $this->options->get_ads_id() ) ->where( 'campaign.id', array_keys( $campaigns ), 'IN' ) // negative: Whether to target (false) or exclude (true) the criterion. ->where( 'campaign_criterion.negative', 'false', '=' ) ->where( 'campaign_criterion.status', 'REMOVED', '!=' ) ->where( 'campaign_criterion.location.geo_target_constant', '', 'IS NOT NULL' ) ->get_results(); /** @var GoogleAdsRow $row */ foreach ( $campaign_criterion_results->iterateAllElements() as $row ) { $campaign = $row->getCampaign(); $campaign_id = $campaign->getId(); if ( ! isset( $campaigns[ $campaign_id ] ) ) { continue; } $campaign_criterion = $row->getCampaignCriterion(); $location = $campaign_criterion->getLocation(); $geo_target_constant = $location->getGeoTargetConstant(); $location_id = $this->parse_geo_target_location_id( $geo_target_constant ); $country_code = $this->google_helper->find_country_code_by_id( $location_id ); if ( $country_code ) { $campaigns[ $campaign_id ]['targeted_locations'][] = $country_code; } } return $campaigns; } /** * Send a batch of operations to mutate a campaign. * * @param MutateOperation[] $operations * * @return int Campaign ID from the MutateOperationResponse. * @throws ApiException If any of the operations fail. */ protected function mutate( array $operations ): int { $request = new MutateGoogleAdsRequest(); $request->setCustomerId( $this->options->get_ads_id() ); $request->setMutateOperations( $operations ); $responses = $this->client->getGoogleAdsServiceClient()->mutate( $request ); foreach ( $responses->getMutateOperationResponses() as $response ) { if ( 'campaign_result' === $response->getResponse() ) { $campaign_result = $response->getCampaignResult(); return $this->parse_campaign_id( $campaign_result->getResourceName() ); } } // When editing only the budget there is no campaign mutate result. return 0; } /** * Convert ID from a resource name to an int. * * @param string $name Resource name containing ID number. * * @return int * @throws Exception When unable to parse resource ID. */ protected function parse_campaign_id( string $name ): int { try { $parts = CampaignServiceClient::parseName( $name ); return absint( $parts['campaign_id'] ); } catch ( ValidationException $e ) { throw new Exception( __( 'Invalid campaign ID', 'google-listings-and-ads' ) ); } } /** * Convert location ID from a geo target constant resource name to an int. * * @param string $geo_target_constant Resource name containing ID number. * * @return int * @throws Exception When unable to parse resource ID. */ protected function parse_geo_target_location_id( string $geo_target_constant ): int { if ( 1 === preg_match( '#geoTargetConstants/(?\d+)#', $geo_target_constant, $parts ) ) { return absint( $parts['id'] ); } else { throw new Exception( __( 'Invalid geo target location ID', 'google-listings-and-ads' ) ); } } } PK!2LV7&src/API/Google/AdsConversionAction.phpnu[client = $client; } /** * Create the 'Google for WooCommerce purchase action' conversion action. * * @return array An array with some conversion action details. * @throws Exception If the conversion action can't be created or retrieved. */ public function create_conversion_action(): array { try { $unique = sprintf( '%04x', wp_rand( 0, 0xffff ) ); $conversion_action_operation = new ConversionActionOperation(); $conversion_action_operation->setCreate( new ConversionAction( [ 'name' => apply_filters( 'woocommerce_gla_conversion_action_name', sprintf( /* translators: %1 is a random 4-digit string */ __( '[%1$s] Google for WooCommerce purchase action', 'google-listings-and-ads' ), $unique ) ), 'category' => ConversionActionCategory::PURCHASE, 'type' => ConversionActionType::WEBPAGE, 'status' => ConversionActionStatus::ENABLED, 'value_settings' => new ValueSettings( [ 'default_value' => 0, 'always_use_default_value' => false, ] ), ] ) ); // Create the conversion. $request = new MutateConversionActionsRequest(); $request->setCustomerId( $this->options->get_ads_id() ); $request->setOperations( [ $conversion_action_operation ] ); $response = $this->client->getConversionActionServiceClient()->mutateConversionActions( $request ); /** @var MutateConversionActionResult $added_conversion_action */ $added_conversion_action = $response->getResults()->offsetGet( 0 ); return $this->get_conversion_action( $added_conversion_action->getResourceName() ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $message = $e->getMessage(); $code = $e->getCode(); if ( $e instanceof ApiException ) { if ( $this->has_api_exception_error( $e, 'DUPLICATE_NAME' ) ) { $message = __( 'A conversion action with this name already exists', 'google-listings-and-ads' ); } else { $message = $e->getBasicMessage(); } $code = $this->map_grpc_code_to_http_status_code( $e ); } throw new Exception( /* translators: %s Error message */ sprintf( __( 'Error creating conversion action: %s', 'google-listings-and-ads' ), $message ), $code ); } } /** * Retrieve a Conversion Action. * * @param string|int $resource_name The Conversion Action to retrieve (also accepts the Conversion Action ID). * * @return array An array with some conversion action details. * @throws Exception If the Conversion Action can't be retrieved. */ public function get_conversion_action( $resource_name ): array { try { // Accept IDs too if ( is_numeric( $resource_name ) ) { $resource_name = ConversionActionServiceClient::conversionActionName( strval( $this->options->get_ads_id() ), strval( $resource_name ) ); } $results = ( new AdsConversionActionQuery() )->set_client( $this->client, $this->options->get_ads_id() ) ->where( 'conversion_action.resource_name', $resource_name, '=' ) ->get_results(); // Get only the first element from results. foreach ( $results->iterateAllElements() as $row ) { return $this->convert_conversion_action( $row ); } } catch ( Exception $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $message = $e->getMessage(); $code = $e->getCode(); if ( $e instanceof ApiException ) { $message = $e->getBasicMessage(); $code = $this->map_grpc_code_to_http_status_code( $e ); } throw new Exception( /* translators: %s Error message */ sprintf( __( 'Error retrieving conversion action: %s', 'google-listings-and-ads' ), $message ), $code ); } } /** * Convert conversion action data to an array. * * @param GoogleAdsRow $row Data row returned from a query request. * * @return array An array with some conversion action details. */ private function convert_conversion_action( GoogleAdsRow $row ): array { $conversion_action = $row->getConversionAction(); $return = [ 'id' => $conversion_action->getId(), 'name' => $conversion_action->getName(), 'status' => ConversionActionStatus::name( $conversion_action->getStatus() ), ]; foreach ( $conversion_action->getTagSnippets() as $t ) { /** @var TagSnippet $t */ if ( $t->getType() !== TrackingCodeType::WEBPAGE ) { continue; } if ( $t->getPageFormat() !== TrackingCodePageFormat::HTML ) { continue; } preg_match( "#send_to': '([^/]+)/([^']+)'#", $t->getEventSnippet(), $matches ); $return['conversion_id'] = $matches[1]; $return['conversion_label'] = $matches[2]; break; } return $return; } } PK!LiV*V*src/API/Google/Ads.phpnu[client = $client; } /** * Get Ads accounts associated with the connected Google account. * * @return array * @throws ExceptionWithResponseData When an ApiException is caught. */ public function get_ads_accounts(): array { try { $customers = $this->client->getCustomerServiceClient()->listAccessibleCustomers( new ListAccessibleCustomersRequest() ); $accounts = []; foreach ( $customers->getResourceNames() as $name ) { $account = $this->get_account_details( $name ); if ( $account ) { $accounts[] = $account; } } return $accounts; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); // Return an empty list if the user has not signed up to ads yet. if ( isset( $errors['NOT_ADS_USER'] ) ) { return []; } throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Error retrieving accounts: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors ] ); } } /** * Get billing status. * * @return string */ public function get_billing_status(): string { $ads_id = $this->options->get_ads_id(); if ( ! $ads_id ) { return BillingSetupStatus::UNKNOWN; } try { $results = ( new AdsBillingStatusQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->get_results(); foreach ( $results->iterateAllElements() as $row ) { $billing_setup = $row->getBillingSetup(); $status = BillingSetupStatus::label( $billing_setup->getStatus() ); return apply_filters( 'woocommerce_gla_ads_billing_setup_status', $status, $ads_id ); } } catch ( ApiException | ValidationException $e ) { // Do not act upon error as we might not have permission to access this account yet. if ( 'PERMISSION_DENIED' !== $e->getStatus() ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); } } return apply_filters( 'woocommerce_gla_ads_billing_setup_status', BillingSetupStatus::UNKNOWN, $ads_id ); } /** * Accept a link from a merchant account. * * @param int $merchant_id Merchant Center account id. * @throws Exception When a link is unavailable. */ public function accept_merchant_link( int $merchant_id ) { $link = $this->get_merchant_link( $merchant_id, 3 ); $link_status = $link->getStatus(); if ( $link_status === ProductLinkInvitationStatus::ACCEPTED ) { return; } $request = new UpdateProductLinkInvitationRequest(); $request->setCustomerId( $this->options->get_ads_id() ); $request->setResourceName( $link->getResourceName() ); $request->setProductLinkInvitationStatus( ProductLinkInvitationStatus::ACCEPTED ); $this->client->getProductLinkInvitationServiceClient()->updateProductLinkInvitation( $request ); } /** * Check if we have access to the ads account. * * @param string $email Email address of the connected account. * * @return bool */ public function has_access( string $email ): bool { $ads_id = $this->options->get_ads_id(); try { $results = ( new AdsAccountAccessQuery() ) ->set_client( $this->client, $ads_id ) ->where( 'customer_user_access.email_address', $email ) ->get_results(); foreach ( $results->iterateAllElements() as $row ) { $access = $row->getCustomerUserAccess(); if ( AccessRole::ADMIN === $access->getAccessRole() ) { return true; } } } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); } return false; } /** * Get the ads account currency. * * @since 1.4.1 * * @return string */ public function get_ads_currency(): string { // Retrieve account currency from the API if we haven't done so previously. if ( $this->options->get_ads_id() && ! $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ) ) { $this->request_ads_currency(); } return strtoupper( $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ) ?? get_woocommerce_currency() ); } /** * Request the Ads Account currency, and cache it as an option. * * @since 1.1.0 * * @return boolean */ public function request_ads_currency(): bool { try { $ads_id = $this->options->get_ads_id(); $account = ResourceNames::forCustomer( $ads_id ); $customer = ( new AdsAccountQuery() ) ->set_client( $this->client, $ads_id ) ->columns( [ 'customer.currency_code' ] ) ->where( 'customer.resource_name', $account, '=' ) ->get_result() ->getCustomer(); $currency = $customer->getCurrencyCode(); } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $currency = null; } return $this->options->update( OptionsInterface::ADS_ACCOUNT_CURRENCY, $currency ); } /** * Save the Ads account currency to the same value as the Store currency. * * @since 1.1.0 * * @return boolean */ public function use_store_currency(): bool { return $this->options->update( OptionsInterface::ADS_ACCOUNT_CURRENCY, get_woocommerce_currency() ); } /** * Convert ads ID from a resource name to an int. * * @param string $name Resource name containing ID number. * * @return int */ public function parse_ads_id( string $name ): int { return absint( str_replace( 'customers/', '', $name ) ); } /** * Update the Ads ID to use for requests. * * @param int $id Ads ID number. * * @return bool */ public function update_ads_id( int $id ): bool { return $this->options->update( OptionsInterface::ADS_ID, $id ); } /** * Returns true if the Ads id exists in the options. * * @return bool */ public function ads_id_exists(): bool { return ! empty( $this->options->get( OptionsInterface::ADS_ID ) ); } /** * Update the billing flow URL so we can retrieve it again later. * * @param string $url Billing flow URL. * * @return bool */ public function update_billing_url( string $url ): bool { return $this->options->update( OptionsInterface::ADS_BILLING_URL, $url ); } /** * Update the OCID for the account so that we can reference it later in order * to link to accept invite link or to send customer to conversion settings page * in their account. * * @param string $url Billing flow URL. * * @return bool */ public function update_ocid_from_billing_url( string $url ): bool { $query_string = wp_parse_url( $url, PHP_URL_QUERY ); // Return if no params. if ( empty( $query_string ) ) { return false; } parse_str( $query_string, $params ); if ( empty( $params['ocid'] ) ) { return false; } return $this->options->update( OptionsInterface::ADS_ACCOUNT_OCID, $params['ocid'] ); } /** * Fetch the account details. * Returns null for any account that fails or is not the right type. * * @param string $account Customer resource name. * @return null|array */ private function get_account_details( string $account ): ?array { try { $customer = ( new AdsAccountQuery() ) ->set_client( $this->client, $this->parse_ads_id( $account ) ) ->where( 'customer.resource_name', $account, '=' ) ->get_result() ->getCustomer(); if ( ! $customer || $customer->getManager() || $customer->getTestAccount() ) { return null; } return [ 'id' => $customer->getId(), 'name' => $customer->getDescriptiveName(), ]; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); } return null; } /** * Get the link from a merchant account. * * The invitation link may not be available in Google Ads immediately after * the invitation is sent from Google Merchant Center, so this method offers * a parameter to specify the number of retries. * * @param int $merchant_id Merchant Center account id. * @param int $attempts_left The number of attempts left to get the link. * * @return ProductLinkInvitation * @throws Exception When the merchant link hasn't been created. */ private function get_merchant_link( int $merchant_id, int $attempts_left = 0 ): ProductLinkInvitation { $res = ( new AdsProductLinkInvitationQuery() ) ->set_client( $this->client, $this->options->get_ads_id() ) ->where( 'product_link_invitation.status', [ ProductLinkInvitationStatus::name( ProductLinkInvitationStatus::ACCEPTED ), ProductLinkInvitationStatus::name( ProductLinkInvitationStatus::PENDING_APPROVAL ) ], 'IN' ) ->get_results(); foreach ( $res->iterateAllElements() as $row ) { $link = $row->getProductLinkInvitation(); $mc = $link->getMerchantCenter(); $mc_id = $mc->getMerchantCenterId(); if ( absint( $mc_id ) === $merchant_id ) { return $link; } } if ( $attempts_left > 0 ) { return $this->get_merchant_link( $merchant_id, $attempts_left - 1 ); } throw new Exception( __( 'Merchant link is not available to accept', 'google-listings-and-ads' ) ); } } PK!zFsrc/API/Google/AdsReport.phpnu[client = $client; } /** * Get report data for campaigns. * * @param string $type Report type (campaigns or products). * @param array $args Query arguments. * * @return array * @throws ExceptionWithResponseData If the report data can't be retrieved. */ public function get_report_data( string $type, array $args ): array { try { $this->has_converted = 'converted' === $this->container->get( AdsCampaign::class )->get_campaign_convert_status(); if ( 'products' === $type ) { $query = new AdsProductReportQuery( $args ); } else { $query = new AdsCampaignReportQuery( $args ); } $results = $query ->set_client( $this->client, $this->options->get_ads_id() ) ->get_results(); $page = $results->getPage(); $this->init_report_totals( $args['fields'] ?? [] ); // Iterate only this page (iterateAllElements will iterate all pages). foreach ( $page->getIterator() as $row ) { $this->add_report_row( $type, $row, $args ); } if ( $page->hasNextPage() ) { $this->report_data['next_page'] = $page->getNextPageToken(); } // Sort intervals to generate an ordered graph. if ( isset( $this->report_data['intervals'] ) ) { ksort( $this->report_data['intervals'] ); } $this->remove_report_indexes( [ 'products', 'campaigns', 'intervals' ] ); return $this->report_data; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Unable to retrieve report data: %s', 'google-listings-and-ads' ), reset( $errors ) ), $this->map_grpc_code_to_http_status_code( $e ), null, [ 'errors' => $errors, 'report_type' => $type, 'report_query_args' => $args, ] ); } } /** * Add data for a report row. * * @param string $type Report type (campaigns or products). * @param GoogleAdsRow $row Report row. * @param array $args Request arguments. */ protected function add_report_row( string $type, GoogleAdsRow $row, array $args ) { $campaign = $row->getCampaign(); $segments = $row->getSegments(); $metrics = $this->get_report_row_metrics( $row, $args ); if ( 'products' === $type && $segments ) { $product_id = $segments->getProductItemId(); $this->increase_report_data( 'products', (string) $product_id, [ 'id' => $product_id, 'name' => $segments->getProductTitle(), 'subtotals' => $metrics, ] ); } if ( 'campaigns' === $type && $campaign ) { $campaign_id = $campaign->getId(); $campaign_name = $campaign->getName(); $campaign_type = CampaignType::label( $campaign->getAdvertisingChannelType() ); $is_converted = $this->has_converted && CampaignType::PERFORMANCE_MAX !== $campaign_type; $this->increase_report_data( 'campaigns', (string) $campaign_id, [ 'id' => $campaign_id, 'name' => $campaign_name, 'status' => CampaignStatus::label( $campaign->getStatus() ), 'isConverted' => $is_converted, 'subtotals' => $metrics, ] ); } if ( $segments && ! empty( $args['interval'] ) ) { $interval = $this->get_segment_interval( $args['interval'], $segments ); $this->increase_report_data( 'intervals', $interval, [ 'interval' => $interval, 'subtotals' => $metrics, ] ); } $this->increase_report_totals( $metrics ); } /** * Get metrics for a report row. * * @param GoogleAdsRow $row Report row. * @param array $args Request arguments. * * @return array */ protected function get_report_row_metrics( GoogleAdsRow $row, array $args ): array { $metrics = $row->getMetrics(); if ( ! $metrics || empty( $args['fields'] ) ) { return []; } $data = []; foreach ( $args['fields'] as $field ) { switch ( $field ) { case 'clicks': $data['clicks'] = $metrics->getClicks(); break; case 'impressions': $data['impressions'] = $metrics->getImpressions(); break; case 'spend': $data['spend'] = $this->from_micro( $metrics->getCostMicros() ); break; case 'sales': $data['sales'] = $metrics->getConversionsValue(); break; case 'conversions': $data['conversions'] = $metrics->getConversions(); break; } } return $data; } /** * Get a unique interval index based on the segments data. * * Types: * day = -- * week = - * month = - * quarter = - * year = * * @param string $interval Interval type. * @param Segments $segments Report segment data. * * @return string * @throws InvalidValue When invalid interval type is given. */ protected function get_segment_interval( string $interval, Segments $segments ): string { switch ( $interval ) { case 'day': $date = new DateTime( $segments->getDate() ); break; case 'week': $date = new DateTime( $segments->getWeek() ); break; case 'month': $date = new DateTime( $segments->getMonth() ); break; case 'quarter': $date = new DateTime( $segments->getQuarter() ); break; case 'year': $date = DateTime::createFromFormat( 'Y', (string) $segments->getYear() ); break; default: throw InvalidValue::not_in_allowed_list( $interval, [ 'day', 'week', 'month', 'quarter', 'year' ] ); } return TimeInterval::time_interval_id( $interval, $date ); } } PK!!src/API/Google/AssetFieldType.phpnu[ self::UNSPECIFIED, AdsAssetFieldType::UNKNOWN => self::UNKNOWN, AdsAssetFieldType::HEADLINE => self::HEADLINE, AdsAssetFieldType::DESCRIPTION => self::DESCRIPTION, AdsAssetFieldType::MARKETING_IMAGE => self::MARKETING_IMAGE, AdsAssetFieldType::LONG_HEADLINE => self::LONG_HEADLINE, AdsAssetFieldType::BUSINESS_NAME => self::BUSINESS_NAME, AdsAssetFieldType::SQUARE_MARKETING_IMAGE => self::SQUARE_MARKETING_IMAGE, AdsAssetFieldType::LOGO => self::LOGO, AdsAssetFieldType::CALL_TO_ACTION_SELECTION => self::CALL_TO_ACTION_SELECTION, AdsAssetFieldType::PORTRAIT_MARKETING_IMAGE => self::PORTRAIT_MARKETING_IMAGE, AdsAssetFieldType::LANDSCAPE_LOGO => self::LANDSCAPE_LOGO, AdsAssetFieldType::YOUTUBE_VIDEO => self::YOUTUBE_VIDEO, AdsAssetFieldType::MEDIA_BUNDLE => self::MEDIA_BUNDLE, ]; /** * Get the enum name for the given label. * * @param string $label The label. * @return string The enum name. * * @throws UnexpectedValueException If the label does not exist. */ public static function name( string $label ): string { return AdsAssetFieldType::name( self::number( $label ) ); } } PK!1??%src/API/Google/BillingSetupStatus.phpnu[ self::PENDING, AdsBillingSetupStatus::APPROVED => self::APPROVED, AdsBillingSetupStatus::CANCELLED => self::CANCELLED, ]; } PK!2 #src/API/Google/CallToActionType.phpnu[ self::UNSPECIFIED, AdsCallToActionType::UNKNOWN => self::UNKNOWN, AdsCallToActionType::LEARN_MORE => self::LEARN_MORE, AdsCallToActionType::GET_QUOTE => self::GET_QUOTE, AdsCallToActionType::APPLY_NOW => self::APPLY_NOW, AdsCallToActionType::SIGN_UP => self::SIGN_UP, AdsCallToActionType::CONTACT_US => self::CONTACT_US, AdsCallToActionType::SUBSCRIBE => self::SUBSCRIBE, AdsCallToActionType::DOWNLOAD => self::DOWNLOAD, AdsCallToActionType::BOOK_NOW => self::BOOK_NOW, AdsCallToActionType::SHOP_NOW => self::SHOP_NOW, ]; } PK!ijrr!src/API/Google/CampaignStatus.phpnu[ self::ENABLED, AdsCampaignStatus::PAUSED => self::PAUSED, AdsCampaignStatus::REMOVED => self::REMOVED, ]; } PK!<I src/API/Google/CampaignType.phpnu[ self::UNSPECIFIED, AdsCampaignType::UNKNOWN => self::UNKNOWN, AdsCampaignType::SEARCH => self::SEARCH, AdsCampaignType::DISPLAY => self::DISPLAY, AdsCampaignType::SHOPPING => self::SHOPPING, AdsCampaignType::HOTEL => self::HOTEL, AdsCampaignType::VIDEO => self::VIDEO, AdsCampaignType::MULTI_CHANNEL => self::MULTI_CHANNEL, AdsCampaignType::LOCAL => self::LOCAL, AdsCampaignType::SMART => self::SMART, AdsCampaignType::PERFORMANCE_MAX => self::PERFORMANCE_MAX, ]; } PK!V%src/API/Google/Connection.phpnu[ $return_url ]; if ( ! empty( $login_hint ) ) { $post_body['loginHint'] = $login_hint; } /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_connection_url(), [ 'body' => wp_json_encode( $post_body ), ] ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 === $result->getStatusCode() && ! empty( $response['oauthUrl'] ) ) { $this->options->update( OptionsInterface::GOOGLE_CONNECTED, true ); return $response['oauthUrl']; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); throw new Exception( __( 'Unable to connect Google account', 'google-listings-and-ads' ) ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( __( 'Unable to connect Google account', 'google-listings-and-ads' ) ); } } /** * Disconnect from the Google account. * * @return string */ public function disconnect(): string { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->delete( $this->get_connection_url() ); $this->options->update( OptionsInterface::GOOGLE_CONNECTED, false ); return $result->getBody()->getContents(); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); return $e->getMessage(); } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); return $e->getMessage(); } } /** * Get the status of the connection. * * @return array * @throws Exception When a ClientException is caught or the response contains an error. */ public function get_status(): array { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->get( $this->get_connection_url() ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 === $result->getStatusCode() ) { $connected = isset( $response['status'] ) && 'connected' === $response['status']; $this->options->update( OptionsInterface::GOOGLE_CONNECTED, $connected ); return $response; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); $message = $response['message'] ?? __( 'Invalid response when retrieving status', 'google-listings-and-ads' ); throw new Exception( $message, $result->getStatusCode() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $this->client_exception_message( $e, __( 'Error retrieving status', 'google-listings-and-ads' ) ) ); } } /** * Get the reconnect status which checks: * - The Google account is connected * - We have access to the connected MC account * - We have access to the connected Ads account * * @return array * @throws Exception When a ClientException is caught or the response contains an error. */ public function get_reconnect_status(): array { $status = $this->get_status(); $email = $status['email'] ?? ''; if ( ! isset( $status['status'] ) || 'connected' !== $status['status'] ) { return $status; } $merchant_id = $this->options->get_merchant_id(); if ( $merchant_id ) { /** @var Merchant $merchant */ $merchant = $this->container->get( Merchant::class ); $status['merchant_account'] = $merchant_id; $status['merchant_access'] = $merchant->has_access( $email ) ? 'yes' : 'no'; } $ads_id = $this->options->get_ads_id(); if ( $ads_id ) { /** @var Ads $ads */ $ads = $this->container->get( Ads::class ); $status['ads_account'] = $ads_id; $status['ads_access'] = $ads->has_access( $email ) ? 'yes' : 'no'; } return $status; } /** * Get the Google connection URL. * * @return string */ protected function get_connection_url(): string { return "{$this->container->get( 'connect_server_root' )}google/connection/google-mc"; } } PK!t!src/API/Google/ExceptionTrait.phpnu[getMetadata(); if ( empty( $meta ) || ! is_array( $meta ) ) { return false; } foreach ( $meta as $data ) { if ( empty( $data['errors'] ) || ! is_array( $data['errors'] ) ) { continue; } foreach ( $data['errors'] as $error ) { if ( in_array( $error_code, $error['errorCode'], true ) ) { return true; } } } return false; } /** * Returns a list of detailed errors from an exception instance that extends ApiException * or GoogleServiceException. Other Exception instances will also be converted to an array * in the same structure. * * The following are the example sources of ApiException, GoogleServiceException, * and other Exception in order: * * @link https://github.com/googleads/google-ads-php/blob/v25.0.0/src/Google/Ads/GoogleAds/V18/Services/Client/CustomerServiceClient.php#L303 * @link https://github.com/googleapis/google-api-php-client/blob/v2.16.1/src/Http/REST.php#L119-L135 * @link https://github.com/googleapis/google-api-php-client/blob/v2.16.1/src/Service/Resource.php#L86-L174 * * @param ApiException|GoogleServiceException|Exception $exception Exception to check. * * @return array */ protected function get_exception_errors( Exception $exception ): array { if ( $exception instanceof ApiException ) { return $this->get_api_exception_errors( $exception ); } if ( $exception instanceof GoogleServiceException ) { return $this->get_google_service_exception_errors( $exception ); } // Fallback for handling other Exception instances. $code = $exception->getCode(); return [ $code => $exception->getMessage() ]; } /** * Returns a list of detailed errors from an ApiException. * If no errors are found the default Exception message is returned. * * @param ApiException $exception Exception to check. * * @return array */ private function get_api_exception_errors( ApiException $exception ): array { $errors = []; $meta = $exception->getMetadata(); if ( is_array( $meta ) ) { foreach ( $meta as $data ) { if ( empty( $data['errors'] ) || ! is_array( $data['errors'] ) ) { continue; } foreach ( $data['errors'] as $error ) { if ( empty( $error['message'] ) ) { continue; } if ( ! empty( $error['errorCode'] ) && is_array( $error['errorCode'] ) ) { $error_code = reset( $error['errorCode'] ); } else { $error_code = 'ERROR'; } $errors[ $error_code ] = $error['message']; } } } $errors[ $exception->getStatus() ] = $exception->getBasicMessage(); return $errors; } /** * Returns a list of detailed errors from a GoogleServiceException. * * @param GoogleServiceException $exception Exception to check. * * @return array */ private function get_google_service_exception_errors( GoogleServiceException $exception ): array { $errors = []; if ( ! is_null( $exception->getErrors() ) ) { foreach ( $exception->getErrors() as $error ) { if ( ! isset( $error['message'] ) ) { continue; } $error_code = $error['reason'] ?? 'ERROR'; $errors[ $error_code ] = $error['message']; } } if ( 0 === count( $errors ) ) { $errors['unknown'] = __( 'An unknown error occurred in the Shopping Content Service.', 'google-listings-and-ads' ); } return $errors; } /** * Get an error message from a ClientException. * * @param ClientExceptionInterface $exception Exception to check. * @param string $default_error Default error message. * * @return string */ protected function client_exception_message( ClientExceptionInterface $exception, string $default_error ): string { if ( $exception instanceof BadResponseException ) { $response = json_decode( $exception->getResponse()->getBody()->getContents(), true ); $message = $response['message'] ?? false; return $message ? $default_error . ': ' . $message : $default_error; } return $default_error; } /** * Map a gRPC code to HTTP status code. * * @param ApiException $exception Exception to check. * * @return int The HTTP status code. * * @see Google\Rpc\Code for the list of gRPC codes. */ protected function map_grpc_code_to_http_status_code( ApiException $exception ) { switch ( $exception->getCode() ) { case Code::OK: return 200; case Code::CANCELLED: return 499; case Code::UNKNOWN: return 500; case Code::INVALID_ARGUMENT: return 400; case Code::DEADLINE_EXCEEDED: return 504; case Code::NOT_FOUND: return 404; case Code::ALREADY_EXISTS: return 409; case Code::PERMISSION_DENIED: return 403; case Code::UNAUTHENTICATED: return 401; case Code::RESOURCE_EXHAUSTED: return 429; case Code::FAILED_PRECONDITION: return 400; case Code::ABORTED: return 409; case Code::OUT_OF_RANGE: return 400; case Code::UNIMPLEMENTED: return 501; case Code::INTERNAL: return 500; case Code::UNAVAILABLE: return 503; case Code::DATA_LOSS: return 500; default: return 500; } } } PK!"src/API/Google/LocationIDTrait.phpnu[ 21133, 'AK' => 21132, 'AZ' => 21136, 'AR' => 21135, 'CA' => 21137, 'CO' => 21138, 'CT' => 21139, 'DE' => 21141, 'DC' => 21140, 'FL' => 21142, 'GA' => 21143, 'HI' => 21144, 'ID' => 21146, 'IL' => 21147, 'IN' => 21148, 'IA' => 21145, 'KS' => 21149, 'KY' => 21150, 'LA' => 21151, 'ME' => 21154, 'MD' => 21153, 'MA' => 21152, 'MI' => 21155, 'MN' => 21156, 'MS' => 21158, 'MO' => 21157, 'MT' => 21159, 'NE' => 21162, 'NV' => 21166, 'NH' => 21163, 'NJ' => 21164, 'NM' => 21165, 'NY' => 21167, 'NC' => 21160, 'ND' => 21161, 'OH' => 21168, 'OK' => 21169, 'OR' => 21170, 'PA' => 21171, 'RI' => 21172, 'SC' => 21173, 'SD' => 21174, 'TN' => 21175, 'TX' => 21176, 'UT' => 21177, 'VT' => 21179, 'VA' => 21178, 'WA' => 21180, 'WV' => 21183, 'WI' => 21182, 'WY' => 21184, ]; /** * Get the location ID for a given state. * * @param string $state * * @return int * @throws InvalidState When the provided state is not found in the mapping. */ protected function get_state_id( string $state ): int { if ( ! array_key_exists( $state, $this->mapping ) ) { throw InvalidState::from_state( $state ); } return $this->mapping[ $state ]; } } PK!@C"src/API/Google/MerchantMetrics.phpnu[shopping_client = $shopping_client; $this->ads_client = $ads_client; $this->wp = $wp; $this->transients = $transients; } /** * Get free listing metrics. * * @return array Of metrics or empty if no metrics were available. * @type int $clicks Number of free clicks. * @type int $impressions NUmber of free impressions. * * @throws Exception When unable to get clicks data. */ public function get_free_listing_metrics(): array { if ( ! $this->options->get_merchant_id() ) { // Merchant account not set up return []; } // Google API requires a date clause to be set but there doesn't seem to be any limits on how wide the range $query = ( new MerchantFreeListingReportQuery( [] ) ) ->set_client( $this->shopping_client, $this->options->get_merchant_id() ) ->where_date_between( self::MAX_QUERY_START_DATE, $this->get_tomorrow() ) ->fields( [ 'clicks', 'impressions' ] ); /** @var SearchResponse $response */ $response = $query->get_results(); if ( empty( $response ) || empty( $response->getResults() ) ) { return []; } $report_row = $response->getResults()[0]; return [ 'clicks' => (int) $report_row->getMetrics()->getClicks(), 'impressions' => (int) $report_row->getMetrics()->getImpressions(), ]; } /** * Get free listing metrics but cached for 12 hours. * * PLEASE NOTE: These metrics will not be 100% accurate since there is no invalidation apart from the 12 hour refresh. * * @return array Of metrics or empty if no metrics were available. * @type int $clicks Number of free clicks. * @type int $impressions NUmber of free impressions. * * @throws Exception When unable to get data. */ public function get_cached_free_listing_metrics(): array { $value = $this->transients->get( TransientsInterface::FREE_LISTING_METRICS ); if ( $value === null ) { $value = $this->get_free_listing_metrics(); $this->transients->set( TransientsInterface::FREE_LISTING_METRICS, $value, HOUR_IN_SECONDS * 12 ); } return $value; } /** * Get ads metrics across all campaigns. * * @return array Of metrics or empty if no metrics were available. * * @throws Exception When unable to get data. */ public function get_ads_metrics(): array { if ( ! $this->options->get_ads_id() ) { // Ads account not set up return []; } // Google API requires a date clause to be set but there doesn't seem to be any limits on how wide the range $query = ( new AdsCampaignReportQuery( [] ) ) ->set_client( $this->ads_client, $this->options->get_ads_id() ) ->where_date_between( self::MAX_QUERY_START_DATE, $this->get_tomorrow() ) ->fields( [ 'clicks', 'conversions', 'impressions' ] ); /** @var PagedListResponse $response */ $response = $query->get_results(); $page = $response->getPage(); if ( $page && $page->getIterator()->current() ) { /** @var GoogleAdsRow $row */ $row = $page->getIterator()->current(); $metrics = $row->getMetrics(); if ( $metrics ) { return [ 'clicks' => $metrics->getClicks(), 'conversions' => (int) $metrics->getConversions(), 'impressions' => $metrics->getImpressions(), ]; } } return []; } /** * Get ads metrics across all campaigns but cached for 12 hours. * * PLEASE NOTE: These metrics will not be 100% accurate since there is no invalidation apart from the 12 hour refresh. * * @return array Of metrics or empty if no metrics were available. * * @throws Exception When unable to get data. */ public function get_cached_ads_metrics(): array { $value = $this->transients->get( TransientsInterface::ADS_METRICS ); if ( $value === null ) { $value = $this->get_ads_metrics(); $this->transients->set( TransientsInterface::ADS_METRICS, $value, HOUR_IN_SECONDS * 12 ); } return $value; } /** * Return amount of active campaigns for the connected Ads account. * * @since 2.5.11 * * @return int */ public function get_campaign_count(): int { if ( ! $this->options->get_ads_id() ) { return 0; } $campaign_count = 0; $cached_count = $this->transients->get( TransientsInterface::ADS_CAMPAIGN_COUNT ); if ( null !== $cached_count ) { return (int) $cached_count; } try { $query = ( new AdsCampaignQuery() )->set_client( $this->ads_client, $this->options->get_ads_id() ); $query->where( 'campaign.status', 'REMOVED', '!=' ); $campaign_results = $query->get_results(); // Iterate through all paged results (total results count is not set). foreach ( $campaign_results->iterateAllElements() as $row ) { ++$campaign_count; } } catch ( Exception $e ) { $campaign_count = 0; } $this->transients->set( TransientsInterface::ADS_CAMPAIGN_COUNT, $campaign_count, HOUR_IN_SECONDS * 12 ); return $campaign_count; } /** * Get tomorrow's date to ensure we include any metrics from the current day. * * @return string */ protected function get_tomorrow(): string { return ( new DateTime( 'tomorrow', $this->wp->wp_timezone() ) )->format( 'Y-m-d' ); } } PK!}99src/API/Google/Merchant.phpnu[service = $service; } /** * @return Product[] */ public function get_products(): array { $products = $this->service->products->listProducts( $this->options->get_merchant_id() ); $return = []; while ( ! empty( $products->getResources() ) ) { foreach ( $products->getResources() as $product ) { $return[] = $product; } if ( empty( $products->getNextPageToken() ) ) { break; } $products = $this->service->products->listProducts( $this->options->get_merchant_id(), [ 'pageToken' => $products->getNextPageToken() ] ); } return $return; } /** * Claim a website for the user's Merchant Center account. * * @param bool $overwrite Whether to include the overwrite directive. * @return bool * @throws Exception If the website claim fails. */ public function claimwebsite( bool $overwrite = false ): bool { try { $id = $this->options->get_merchant_id(); $params = $overwrite ? [ 'overwrite' => true ] : []; $this->service->accounts->claimwebsite( $id, $id, $params ); do_action( 'woocommerce_gla_site_claim_success', [ 'details' => 'google_proxy' ] ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'google_proxy' ] ); $error_message = __( 'Unable to claim website.', 'google-listings-and-ads' ); if ( 403 === $e->getCode() ) { $error_message = __( 'Website already claimed, use overwrite to complete the process.', 'google-listings-and-ads' ); } throw new Exception( $error_message, $e->getCode() ); } return true; } /** * Request verification code to start phone verification. * * @param string $region_code Two-letter country code (ISO 3166-1 alpha-2) for the phone number, for * example CA for Canadian numbers. * @param string $phone_number Phone number to be verified. * @param string $verification_method Verification method to receive verification code. * @param string $language_code Language code IETF BCP 47 syntax (for example, en-US). Language code is used * to provide localized SMS and PHONE_CALL. Default language used is en-US if * not provided. * * @return string The verification ID to use in subsequent calls to * `Merchant::verify_phone_number`. * * @throws GoogleServiceException If there are any Google API errors. * * @see https://tools.ietf.org/html/bcp47 IETF BCP 47 language codes. * @see https://wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements ISO 3166-1 alpha-2 * officially assigned codes. * * @since 1.5.0 */ public function request_phone_verification( string $region_code, string $phone_number, string $verification_method, string $language_code = 'en-US' ): string { $merchant_id = $this->options->get_merchant_id(); $request = new RequestPhoneVerificationRequest( [ 'phoneRegionCode' => $region_code, 'phoneNumber' => $phone_number, 'phoneVerificationMethod' => $verification_method, 'languageCode' => $language_code, ] ); try { return $this->service->accounts->requestphoneverification( $merchant_id, $merchant_id, $request )->getVerificationId(); } catch ( GoogleServiceException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw $e; } } /** * Validates verification code to verify phone number for the account. * * @param string $verification_id The verification ID returned by * `Merchant::request_phone_verification`. * @param string $verification_code The verification code that was sent to the phone number for validation. * @param string $verification_method Verification method used to receive verification code. * * @return string Verified phone number if verification is successful. * * @throws GoogleServiceException If there are any Google API errors. * * @since 1.5.0 */ public function verify_phone_number( string $verification_id, string $verification_code, string $verification_method ): string { $merchant_id = $this->options->get_merchant_id(); $request = new VerifyPhoneNumberRequest( [ 'verificationId' => $verification_id, 'verificationCode' => $verification_code, 'phoneVerificationMethod' => $verification_method, ] ); try { return $this->service->accounts->verifyphonenumber( $merchant_id, $merchant_id, $request )->getVerifiedPhoneNumber(); } catch ( GoogleServiceException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw $e; } } /** * Retrieve the user's Merchant Center account information. * * @param int $id Optional - the Merchant Center account to retrieve * * @return Account The user's Merchant Center account. * @throws ExceptionWithResponseData If the account can't be retrieved. */ public function get_account( int $id = 0 ): Account { $id = $id ?: $this->options->get_merchant_id(); try { $mc_account = $this->service->accounts->get( $id, $id ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Unable to retrieve Merchant Center account: %s', 'google-listings-and-ads' ), reset( $errors ) ), $e->getCode(), null, [ 'errors' => $errors ] ); } return $mc_account; } /** * Get hash of the site URL we used during onboarding. * If not available in a local option, it's fetched from the Merchant Center account. * * @since 1.13.0 * @return string|null */ public function get_claimed_url_hash(): ?string { $claimed_url_hash = $this->options->get( OptionsInterface::CLAIMED_URL_HASH ); if ( empty( $claimed_url_hash ) && $this->options->get_merchant_id() ) { try { $account_url = $this->get_account()->getWebsiteUrl(); if ( empty( $account_url ) || ! $this->get_accountstatus()->getWebsiteClaimed() ) { return null; } $claimed_url_hash = md5( untrailingslashit( $account_url ) ); $this->options->update( OptionsInterface::CLAIMED_URL_HASH, $claimed_url_hash ); } catch ( Exception $e ) { return null; } } return $claimed_url_hash; } /** * Retrieve the user's Merchant Center account information. * * @param int $id Optional - the Merchant Center account to retrieve * @return AccountStatus The user's Merchant Center account status. * @throws Exception If the account can't be retrieved. */ public function get_accountstatus( int $id = 0 ): AccountStatus { $id = $id ?: $this->options->get_merchant_id(); try { $mc_account_status = $this->service->accountstatuses->get( $id, $id ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw new Exception( __( 'Unable to retrieve Merchant Center account status.', 'google-listings-and-ads' ), $e->getCode() ); } return $mc_account_status; } /** * Retrieve a batch of Merchant Center Product Statuses using the provided Merchant Center product IDs. * * @since 1.1.0 * * @param string[] $mc_product_ids * * @return ProductstatusesCustomBatchResponse; */ public function get_productstatuses_batch( array $mc_product_ids ): ProductstatusesCustomBatchResponse { $merchant_id = $this->options->get_merchant_id(); $entries = []; foreach ( $mc_product_ids as $index => $id ) { $entries[] = [ 'batchId' => $index + 1, 'productId' => $id, 'method' => 'GET', 'merchantId' => $merchant_id, ]; } // Retrieve batch. $request = new ProductstatusesCustomBatchRequest(); $request->setEntries( $entries ); return $this->service->productstatuses->custombatch( $request ); } /** * Update the provided Merchant Center account information. * * @param Account $account The Account data to update. * * @return Account The user's Merchant Center account. * @throws ExceptionWithResponseData If the account can't be updated. */ public function update_account( Account $account ): Account { try { $account = $this->service->accounts->update( $account->getId(), $account->getId(), $account ); } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Unable to update Merchant Center account: %s', 'google-listings-and-ads' ), reset( $errors ) ), $e->getCode(), null, [ 'errors' => $errors ] ); } return $account; } /** * Link a Google Ads ID to this Merchant account. * * @param int $ads_id Google Ads ID to link. * * @return bool * @throws ExceptionWithResponseData When unable to retrieve or update account data. */ public function link_ads_id( int $ads_id ): bool { $account = $this->get_account(); $ads_links = $account->getAdsLinks() ?? []; // Stop early if we already have a link setup. foreach ( $ads_links as $link ) { if ( $ads_id === absint( $link->getAdsId() ) ) { return false; } } $link = new AccountAdsLink(); $link->setAdsId( $ads_id ); $link->setStatus( 'active' ); $account->setAdsLinks( array_merge( $ads_links, [ $link ] ) ); $this->update_account( $account ); return true; } /** * Check if we have access to the merchant account. * * @param string $email Email address of the connected account. * * @return bool */ public function has_access( string $email ): bool { $id = $this->options->get_merchant_id(); try { $account = $this->service->accounts->get( $id, $id ); foreach ( $account->getUsers() as $user ) { if ( $email === $user->getEmailAddress() && $user->getAdmin() ) { return true; } } } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); } return false; } /** * Update the Merchant Center ID to use for requests. * * @param int $id Merchant ID number. * * @return bool */ public function update_merchant_id( int $id ): bool { return $this->options->update( OptionsInterface::MERCHANT_ID, $id ); } /** * Get the review status for an MC account * * @since 2.7.1 * * @return array An array with the status for freeListingsProgram and shoppingAdsProgram * @throws Exception When an exception happens in the Google API. */ public function get_account_review_status() { try { $id = $this->options->get_merchant_id(); return [ 'freeListingsProgram' => $this->service->freelistingsprogram->get( $id ), 'shoppingAdsProgram' => $this->service->shoppingadsprogram->get( $id ), ]; } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw new Exception( $e->getMessage(), $e->getCode() ); } } /** * Request a review for an MC account * * @since 2.7.1 * * @param string $region_code The region code to request the review * @param array $types The types of programs to request the review * * @return ResponseInterface The Google API response * @throws Exception When the request review produces an exception in the Google side or when * the programs are not supported. */ public function account_request_review( $region_code, $types ) { try { $id = $this->options->get_merchant_id(); if ( in_array( 'freelistingsprogram', $types, true ) ) { $request = new RequestReviewFreeListingsRequest(); $request->setRegionCode( $region_code ); return $this->service->freelistingsprogram->requestreview( $id, $request ); } elseif ( in_array( 'shoppingadsprogram', $types, true ) ) { $request = new RequestReviewShoppingAdsRequest(); $request->setRegionCode( $region_code ); return $this->service->shoppingadsprogram->requestreview( $id, $request ); } else { throw new Exception( 'Program type not supported', 400 ); } } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw new Exception( $e->getMessage(), $e->getCode() ); } } } PK!Ǐ!!!src/API/Google/MerchantReport.phpnu[service = $service; $this->product_helper = $product_helper; } /** * Get ProductView Query response. * * @param string|null $next_page_token The next page token. * @return array Associative array with product statuses and the next page token. * * @throws Exception If the product view report data can't be retrieved. */ public function get_product_view_report( $next_page_token = null ): array { $batch_size = apply_filters( 'woocommerce_gla_product_view_report_page_size', 500 ); try { $product_view_data = [ 'statuses' => [], 'next_page_token' => null, ]; $query = new MerchantProductViewReportQuery( [ 'next_page' => $next_page_token, 'per_page' => $batch_size, ] ); $response = $query ->set_client( $this->service, $this->options->get_merchant_id() ) ->get_results(); $results = $response->getResults() ?? []; foreach ( $results as $row ) { /** @var ProductView $product_view */ $product_view = $row->getProductView(); $wc_product_id = $this->product_helper->get_wc_product_id( $product_view->getId() ); $mc_product_status = $this->convert_aggregated_status_to_mc_status( $product_view->getAggregatedDestinationStatus() ); // Skip if the product id does not exist if ( ! $wc_product_id ) { continue; } $product_view_data['statuses'][ $wc_product_id ] = [ 'mc_id' => $product_view->getId(), 'product_id' => $wc_product_id, 'status' => $mc_product_status, 'expiration_date' => $this->convert_shopping_content_date( $product_view->getExpirationDate() ), ]; } $product_view_data['next_page_token'] = $response->getNextPageToken(); return $product_view_data; } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw new Exception( __( 'Unable to retrieve Product View Report.', 'google-listings-and-ads' ) . $e->getMessage(), $e->getCode() ); } } /** * Convert the product view aggregated status to the MC status. * * @param string $status The aggregated status of the product. * * @return string The MC status. */ protected function convert_aggregated_status_to_mc_status( string $status ): string { switch ( $status ) { case 'ELIGIBLE': return MCStatus::APPROVED; case 'ELIGIBLE_LIMITED': return MCStatus::PARTIALLY_APPROVED; case 'NOT_ELIGIBLE_OR_DISAPPROVED': return MCStatus::DISAPPROVED; case 'PENDING': return MCStatus::PENDING; default: return MCStatus::NOT_SYNCED; } } /** * Get report data for free listings. * * @param string $type Report type (free_listings or products). * @param array $args Query arguments. * * @return array * @throws Exception If the report data can't be retrieved. */ public function get_report_data( string $type, array $args ): array { try { if ( 'products' === $type ) { $query = new MerchantProductReportQuery( $args ); } else { $query = new MerchantFreeListingReportQuery( $args ); } $results = $query ->set_client( $this->service, $this->options->get_merchant_id() ) ->get_results(); $this->init_report_totals( $args['fields'] ?? [] ); foreach ( $results->getResults() as $row ) { $this->add_report_row( $type, $row, $args ); } if ( $results->getNextPageToken() ) { $this->report_data['next_page'] = $results->getNextPageToken(); } // Sort intervals to generate an ordered graph. if ( isset( $this->report_data['intervals'] ) ) { ksort( $this->report_data['intervals'] ); } $this->remove_report_indexes( [ 'products', 'free_listings', 'intervals' ] ); return $this->report_data; } catch ( GoogleException $e ) { do_action( 'woocommerce_gla_mc_client_exception', $e, __METHOD__ ); throw new Exception( __( 'Unable to retrieve report data.', 'google-listings-and-ads' ), $e->getCode() ); } } /** * Add data for a report row. * * @param string $type Report type (free_listings or products). * @param ReportRow $row Report row. * @param array $args Request arguments. */ protected function add_report_row( string $type, ReportRow $row, array $args ) { $segments = $row->getSegments(); $metrics = $this->get_report_row_metrics( $row, $args ); if ( 'free_listings' === $type ) { $this->increase_report_data( 'free_listings', 'free', [ 'subtotals' => $metrics, ] ); } if ( 'products' === $type && $segments ) { $product_id = $segments->getOfferId(); $this->increase_report_data( 'products', (string) $product_id, [ 'id' => $product_id, 'subtotals' => $metrics, ] ); // Retrieve product title and add to report. if ( empty( $this->report_data['products'][ $product_id ]['name'] ) ) { $name = $this->product_helper->get_wc_product_title( (string) $product_id ); $this->report_data['products'][ $product_id ]['name'] = $name; } } if ( $segments && ! empty( $args['interval'] ) ) { $interval = $this->get_segment_interval( $args['interval'], $segments ); $this->increase_report_data( 'intervals', $interval, [ 'interval' => $interval, 'subtotals' => $metrics, ] ); } $this->increase_report_totals( $metrics ); } /** * Get metrics for a report row. * * @param ReportRow $row Report row. * @param array $args Request arguments. * * @return array */ protected function get_report_row_metrics( ReportRow $row, array $args ): array { $metrics = $row->getMetrics(); if ( ! $metrics || empty( $args['fields'] ) ) { return []; } $data = []; foreach ( $args['fields'] as $field ) { switch ( $field ) { case 'clicks': $data['clicks'] = (int) $metrics->getClicks(); break; case 'impressions': $data['impressions'] = (int) $metrics->getImpressions(); break; } } return $data; } /** * Get a unique interval index based on the segments data. * * Types: * day = -- * * @param string $interval Interval type. * @param Segments $segments Report segment data. * * @return string * @throws InvalidValue When invalid interval type is given. */ protected function get_segment_interval( string $interval, Segments $segments ): string { if ( 'day' !== $interval ) { throw InvalidValue::not_in_allowed_list( $interval, [ 'day' ] ); } $date = $segments->getDate(); $date = new DateTime( "{$date->getYear()}-{$date->getMonth()}-{$date->getDay()}" ); return TimeInterval::time_interval_id( $interval, $date ); } } PK!XJXJsrc/API/Google/Middleware.phpnu[container->get( Client::class ); $result = $client->get( $this->get_manager_url( 'merchant-accounts' ) ); $response = json_decode( $result->getBody()->getContents(), true ); $accounts = []; if ( 200 === $result->getStatusCode() && is_array( $response ) ) { foreach ( $response as $account ) { $accounts[] = $account; } } return $accounts; } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $this->client_exception_message( $e, __( 'Error retrieving accounts', 'google-listings-and-ads' ) ), $e->getCode() ); } } /** * Create a new Merchant Center account. * * @return int Created merchant account ID * * @throws Exception When an Exception is caught or we receive an invalid response. */ public function create_merchant_account(): int { $user = wp_get_current_user(); $tos = $this->mark_tos_accepted( 'google-mc', $user->user_email ); if ( ! $tos->accepted() ) { throw new Exception( __( 'Unable to log accepted TOS', 'google-listings-and-ads' ) ); } $site_url = esc_url_raw( $this->get_site_url() ); if ( ! wc_is_valid_url( $site_url ) ) { throw new Exception( __( 'Invalid site URL.', 'google-listings-and-ads' ) ); } try { return $this->create_merchant_account_request( $this->new_account_name(), $site_url ); } catch ( InvalidTerm $e ) { // Try again with a default account name. return $this->create_merchant_account_request( $this->default_account_name(), $site_url ); } } /** * Send a request to create a merchant account. * * @param string $name Site name * @param string $site_url Website URL * * @return int Created merchant account ID * * @throws Exception When an Exception is caught or we receive an invalid response. * @throws InvalidTerm When the account name contains invalid terms. * @throws InvalidDomainName When the site URL ends with an invalid top-level domain. * @since 1.5.0 */ protected function create_merchant_account_request( string $name, string $site_url ): int { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_manager_url( 'create-merchant' ), [ 'body' => wp_json_encode( [ 'name' => $name, 'websiteUrl' => $site_url, ] ), ] ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 === $result->getStatusCode() && isset( $response['id'] ) ) { $id = absint( $response['id'] ); $this->container->get( Merchant::class )->update_merchant_id( $id ); return $id; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); $error = $response['message'] ?? __( 'Invalid response when creating account', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } catch ( ClientExceptionInterface $e ) { $message = $this->client_exception_message( $e, __( 'Error creating account', 'google-listings-and-ads' ) ); if ( preg_match( '/terms?.* are|is not allowed/', $message ) ) { throw InvalidTerm::contains_invalid_terms( $name ); } if ( strpos( $message, 'URL ends with an invalid top-level domain name' ) !== false ) { throw InvalidDomainName::create_account_failed_invalid_top_level_domain_name( $this->strip_url_protocol( esc_url_raw( $this->get_site_url() ) ) ); } do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $message, $e->getCode() ); } } /** * Link an existing Merchant Center account. * * @param int $id Existing account ID. * * @return int */ public function link_merchant_account( int $id ): int { $this->container->get( Merchant::class )->update_merchant_id( $id ); return $id; } /** * Link Merchant Center account to MCA. * * @return bool * @throws Exception When a ClientException is caught or we receive an invalid response. */ public function link_merchant_to_mca(): bool { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_manager_url( 'link-merchant' ), [ 'body' => wp_json_encode( [ 'accountId' => $this->options->get_merchant_id(), ] ), ] ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 === $result->getStatusCode() && isset( $response['status'] ) && 'success' === $response['status'] ) { return true; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); $error = $response['message'] ?? __( 'Invalid response when linking merchant to MCA', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $this->client_exception_message( $e, __( 'Error linking merchant to MCA', 'google-listings-and-ads' ) ), $e->getCode() ); } } /** * Claim the website for a MCA. * * @param bool $overwrite To enable claim overwriting. * @return bool * @throws Exception When an Exception is caught or we receive an invalid response. */ public function claim_merchant_website( bool $overwrite = false ): bool { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_manager_url( 'claim-website' ), [ 'body' => wp_json_encode( [ 'accountId' => $this->options->get_merchant_id(), 'overwrite' => $overwrite, ] ), ] ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 === $result->getStatusCode() && isset( $response['status'] ) && 'success' === $response['status'] ) { do_action( 'woocommerce_gla_site_claim_success', [ 'details' => 'google_manager' ] ); return true; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'google_manager' ] ); $error = $response['message'] ?? __( 'Invalid response when claiming website', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'google_manager' ] ); throw new Exception( $this->client_exception_message( $e, __( 'Error claiming website', 'google-listings-and-ads' ) ), $e->getCode() ); } } /** * Create a new Google Ads account. * * @return array * @throws Exception When a ClientException is caught, unsupported store country, or we receive an invalid response. */ public function create_ads_account(): array { try { $country = $this->container->get( WC::class )->get_base_country(); /** @var GoogleHelper $google_helper */ $google_helper = $this->container->get( GoogleHelper::class ); if ( ! $google_helper->is_country_supported( $country ) ) { throw new Exception( __( 'Store country is not supported', 'google-listings-and-ads' ) ); } $user = wp_get_current_user(); $tos = $this->mark_tos_accepted( 'google-ads', $user->user_email ); if ( ! $tos->accepted() ) { throw new Exception( __( 'Unable to log accepted TOS', 'google-listings-and-ads' ) ); } /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_manager_url( $country . '/create-customer' ), [ 'body' => wp_json_encode( [ 'descriptive_name' => $this->new_account_name(), 'currency_code' => get_woocommerce_currency(), 'time_zone' => $this->get_site_timezone_string(), ] ), ] ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 === $result->getStatusCode() && isset( $response['resourceName'] ) ) { /** @var Ads $ads */ $ads = $this->container->get( Ads::class ); $id = $ads->parse_ads_id( $response['resourceName'] ); $ads->update_ads_id( $id ); $ads->use_store_currency(); $billing_url = $response['invitationLink'] ?? ''; $ads->update_billing_url( $billing_url ); $ads->update_ocid_from_billing_url( $billing_url ); return [ 'id' => $id, 'billing_url' => $billing_url, ]; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); $error = $response['message'] ?? __( 'Invalid response when creating account', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $this->client_exception_message( $e, __( 'Error creating account', 'google-listings-and-ads' ) ), $e->getCode() ); } } /** * Link an existing Google Ads account. * * @param int $id Existing account ID. * * @return array * @throws Exception When a ClientException is caught or we receive an invalid response. */ public function link_ads_account( int $id ): array { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_manager_url( 'link-customer' ), [ 'body' => wp_json_encode( [ 'client_customer' => $id, ] ), ] ); $response = json_decode( $result->getBody()->getContents(), true ); $name = "customers/{$id}"; if ( 200 === $result->getStatusCode() && isset( $response['resourceName'] ) && 0 === strpos( $response['resourceName'], $name ) ) { /** @var Ads $ads */ $ads = $this->container->get( Ads::class ); $ads->update_ads_id( $id ); $ads->request_ads_currency(); return [ 'id' => $id ]; } do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); $error = $response['message'] ?? __( 'Invalid response when linking account', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $this->client_exception_message( $e, __( 'Error linking account', 'google-listings-and-ads' ) ), $e->getCode() ); } } /** * Determine whether the TOS have been accepted. * * @param string $service Name of service. * * @return TosAccepted */ public function check_tos_accepted( string $service ): TosAccepted { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->get( $this->get_tos_url( $service ) ); return new TosAccepted( 200 === $result->getStatusCode(), $result->getBody()->getContents() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); return new TosAccepted( false, $e->getMessage() ); } } /** * Record TOS acceptance for a particular email address. * * @param string $service Name of service. * @param string $email * * @return TosAccepted */ public function mark_tos_accepted( string $service, string $email ): TosAccepted { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->post( $this->get_tos_url( $service ), [ 'body' => wp_json_encode( [ 'email' => $email, ] ), ] ); return new TosAccepted( 200 === $result->getStatusCode(), $result->getBody()->getContents() ?? $result->getReasonPhrase() ); } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); return new TosAccepted( false, $e->getMessage() ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); return new TosAccepted( false, $e->getMessage() ); } } /** * Get the TOS endpoint URL * * @param string $service Name of service. * * @return string */ protected function get_tos_url( string $service ): string { $url = $this->container->get( 'connect_server_root' ) . 'tos'; return $service ? trailingslashit( $url ) . $service : $url; } /** * Get the manager endpoint URL * * @param string $name Resource name. * * @return string */ protected function get_manager_url( string $name = '' ): string { $url = $this->container->get( 'connect_server_root' ) . 'google/manager'; return $name ? trailingslashit( $url ) . $name : $url; } /** * Get the Google Shopping Data Integration auth endpoint URL * * @return string */ public function get_sdi_auth_endpoint(): string { return $this->container->get( 'connect_server_root' ) . 'google/google-sdi/v1/credentials/partners/WOO_COMMERCE/merchants/' . $this->strip_url_protocol( $this->get_site_url() ) . '/oauth/redirect:generate' . '?merchant_id=' . $this->options->get_merchant_id(); } /** * Generate a descriptive name for a new account. * Use site name if available. * * @return string */ protected function new_account_name(): string { $site_name = get_bloginfo( 'name' ); return ! empty( $site_name ) ? $site_name : $this->default_account_name(); } /** * Generate a default account name based on the date. * * @return string */ protected function default_account_name(): string { return sprintf( /* translators: 1: current date in the format Y-m-d */ __( 'Account %1$s', 'google-listings-and-ads' ), ( new DateTime() )->format( 'Y-m-d' ) ); } /** * Get a timezone string from WP Settings. * * @return string * @throws Exception If the DateTime instantiation fails. */ protected function get_site_timezone_string(): string { /** @var WP $wp */ $wp = $this->container->get( WP::class ); $timezone = $wp->wp_timezone_string(); /** @var DateTimeUtility $datetime_util */ $datetime_util = $this->container->get( DateTimeUtility::class ); return $datetime_util->maybe_convert_tz_string( $timezone ); } /** * This function detects if the current account is a sub-account * This function is cached in the MC_IS_SUBACCOUNT transient * * @return bool True if it's a standalone account. */ public function is_subaccount(): bool { /** @var TransientsInterface $transients */ $transients = $this->container->get( TransientsInterface::class ); $is_subaccount = $transients->get( $transients::MC_IS_SUBACCOUNT ); if ( is_null( $is_subaccount ) ) { $is_subaccount = 0; $merchant_id = $this->options->get_merchant_id(); $accounts = $this->get_merchant_accounts(); foreach ( $accounts as $account ) { if ( $account['id'] === $merchant_id && $account['subaccount'] ) { $is_subaccount = 1; } } $transients->set( $transients::MC_IS_SUBACCOUNT, $is_subaccount ); } // since transients don't support booleans, we save them as 0/1 and do the conversion here return boolval( $is_subaccount ); } /** * Performs a request to Google Shopping Data Integration (SDI) to get required information in order to form an auth URL. * * @return array An array with the JSON response from the WCS server. * @throws NotFoundExceptionInterface When the container was not found. * @throws ContainerExceptionInterface When an error happens while retrieving the container. * @throws Exception When the response status is not successful. * @see google-sdi in google/services inside WCS */ public function get_sdi_auth_params() { try { /** @var Client $client */ $client = $this->container->get( Client::class ); $result = $client->get( $this->get_sdi_auth_endpoint() ); $response = json_decode( $result->getBody()->getContents(), true ); if ( 200 !== $result->getStatusCode() ) { do_action( 'woocommerce_gla_partner_app_auth_failure', [ 'error' => 'response', 'response' => $response, ] ); do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ ); $error = $response['message'] ?? __( 'Invalid response authenticating partner app.', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } return $response; } catch ( ClientExceptionInterface $e ) { do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ ); throw new Exception( $this->client_exception_message( $e, __( 'Error authenticating Google Partner APP.', 'google-listings-and-ads' ) ), $e->getCode() ); } } } PK!%%%src/API/Google/ReportTrait.phpnu[report_data[ $field ][ $index ] ) ) { $this->report_data[ $field ][ $index ] = $data; } elseif ( ! empty( $data['subtotals'] ) ) { foreach ( $data['subtotals'] as $name => $subtotal ) { $this->report_data[ $field ][ $index ]['subtotals'][ $name ] += $subtotal; } } } /** * Initialize report totals to 0 values. * * @param array $fields List of field names. */ protected function init_report_totals( array $fields ) { foreach ( $fields as $name ) { $this->report_data['totals'][ $name ] = 0; } } /** * Increase report totals. * * @param array $data Totals data. */ protected function increase_report_totals( array $data ) { foreach ( $data as $name => $total ) { if ( ! isset( $this->report_data['totals'][ $name ] ) ) { $this->report_data['totals'][ $name ] = $total; } else { $this->report_data['totals'][ $name ] += $total; } } } /** * Remove indexes from report data to conform to schema. * * @param array $fields Fields to reindex. */ protected function remove_report_indexes( array $fields ) { foreach ( $fields as $key ) { if ( isset( $this->report_data[ $key ] ) ) { $this->report_data[ $key ] = array_values( $this->report_data[ $key ] ); } } } } PK!d00src/API/Google/Settings.phpnu[get_settings(); return [ 'shipping_rate' => $settings['shipping_rate'] ?? '', 'offers_free_shipping' => (bool) ( $settings['offers_free_shipping'] ?? false ), 'free_shipping_threshold' => (float) ( $settings['free_shipping_threshold'] ?? 0 ), 'shipping_time' => $settings['shipping_time'] ?? '', 'tax_rate' => $settings['tax_rate'] ?? '', 'target_countries' => join( ',', $this->get_target_countries() ), ]; } /** * Sync the shipping settings with Google. */ public function sync_shipping() { if ( ! $this->should_sync_shipping() ) { return; } $settings = $this->generate_shipping_settings(); $this->get_shopping_service()->shippingsettings->update( $this->get_merchant_id(), $this->get_account_id(), $settings ); } /** * Whether we should synchronize settings with the Merchant Center * * @return bool */ protected function should_sync_shipping(): bool { $shipping_rate = $this->get_settings()['shipping_rate'] ?? ''; $shipping_time = $this->get_settings()['shipping_time'] ?? ''; return in_array( $shipping_rate, [ 'flat', 'automatic' ], true ) && 'flat' === $shipping_time; } /** * Whether we should get the shipping settings from the WooCommerce settings. * * @return bool * * @since 1.12.0 */ public function should_get_shipping_rates_from_woocommerce(): bool { return 'automatic' === ( $this->get_settings()['shipping_rate'] ?? '' ); } /** * Generate a ShippingSettings object for syncing the store shipping settings to Merchant Center. * * @return ShippingSettings * * @since 2.1.0 */ protected function generate_shipping_settings(): ShippingSettings { $times = $this->get_shipping_times(); /** @var WC $wc_proxy */ $wc_proxy = $this->container->get( WC::class ); $currency = $wc_proxy->get_woocommerce_currency(); if ( $this->should_get_shipping_rates_from_woocommerce() ) { return new WCShippingSettingsAdapter( [ 'currency' => $currency, 'rates_collections' => $this->get_shipping_rates_collections_from_woocommerce(), 'delivery_times' => $times, 'accountId' => $this->get_account_id(), ] ); } return new DBShippingSettingsAdapter( [ 'currency' => $currency, 'db_rates' => $this->get_shipping_rates_from_database(), 'delivery_times' => $times, 'accountId' => $this->get_account_id(), ] ); } /** * Get the current tax settings from the API. * * @return AccountTax */ public function get_taxes(): AccountTax { return $this->get_shopping_service()->accounttax->get( $this->get_merchant_id(), $this->get_account_id() ); } /** * Whether we should sync tax settings. * * This depends on the store being in the US * * @return bool */ protected function should_sync_taxes(): bool { if ( 'US' !== $this->get_store_country() ) { return false; } return 'destination' === ( $this->get_settings()['tax_rate'] ?? 'destination' ); } /** * Sync tax setting with Google. */ public function sync_taxes() { if ( ! $this->should_sync_taxes() ) { return; } $taxes = new AccountTax(); $taxes->setAccountId( $this->get_account_id() ); $tax_rule = new TaxRule(); $tax_rule->setUseGlobalRate( true ); $tax_rule->setLocationId( $this->get_state_id( $this->get_store_state() ) ); $tax_rule->setCountry( $this->get_store_country() ); $taxes->setRules( [ $tax_rule ] ); $this->get_shopping_service()->accounttax->update( $this->get_merchant_id(), $this->get_account_id(), $taxes ); } /** * Get shipping time data. * * @return array */ protected function get_shipping_times(): array { static $times = null; if ( null === $times ) { $time_query = $this->container->get( ShippingTimeQuery::class ); $times = $time_query->get_all_shipping_times(); } return $times; } /** * Get shipping rate data. * * @return array */ protected function get_shipping_rates_from_database(): array { $rate_query = $this->container->get( ShippingRateQuery::class ); return $rate_query->get_results(); } /** * Get shipping rate data from WooCommerce shipping settings. * * @return CountryRatesCollection[] Array of rates collections for each target country specified in settings. */ protected function get_shipping_rates_collections_from_woocommerce(): array { /** @var TargetAudience $target_audience */ $target_audience = $this->container->get( TargetAudience::class ); $target_countries = $target_audience->get_target_countries(); /** @var ShippingZone $shipping_zone */ $shipping_zone = $this->container->get( ShippingZone::class ); $rates = []; foreach ( $target_countries as $country ) { $location_rates = $shipping_zone->get_shipping_rates_for_country( $country ); $rates[ $country ] = new CountryRatesCollection( $country, $location_rates ); } return $rates; } /** * @return OptionsInterface */ protected function get_options_object(): OptionsInterface { return $this->container->get( OptionsInterface::class ); } /** * Get the Merchant ID * * @return int */ protected function get_merchant_id(): int { return $this->get_options_object()->get( OptionsInterface::MERCHANT_ID ); } /** * Get the account ID. * * @return int */ protected function get_account_id(): int { // todo: there are some cases where this might be different than the Merchant ID. return $this->get_merchant_id(); } /** * Get the Shopping Service object. * * @return ShoppingContent */ protected function get_shopping_service(): ShoppingContent { return $this->container->get( ShoppingContent::class ); } /** * Get the country for the store. * * @return string */ protected function get_store_country(): string { return $this->container->get( WC::class )->get_base_country(); } /** * Get the state for the store. * * @return string */ protected function get_store_state(): string { /** @var WC $wc */ $wc = $this->container->get( WC::class ); return $wc->get_wc_countries()->get_base_state(); } /** * Get the WooCommerce store physical address. * * @return AccountAddress * * @since 1.4.0 */ public function get_store_address(): AccountAddress { /** @var WC $wc */ $wc = $this->container->get( WC::class ); $countries = $wc->get_wc_countries(); $postal_code = ! empty( $countries->get_base_postcode() ) ? $countries->get_base_postcode() : null; $locality = ! empty( $countries->get_base_city() ) ? $countries->get_base_city() : null; $country = ! empty( $countries->get_base_country() ) ? $countries->get_base_country() : null; $region = ! empty( $countries->get_base_state() ) ? $countries->get_base_state() : null; $mc_address = new AccountAddress(); $mc_address->setPostalCode( $postal_code ); $mc_address->setLocality( $locality ); $mc_address->setCountry( $country ); if ( ! empty( $region ) && ! empty( $country ) ) { $mc_address->setRegion( $this->maybe_get_state_name( $region, $country ) ); } $address = ! empty( $countries->get_base_address() ) ? $countries->get_base_address() : null; $address_2 = ! empty( $countries->get_base_address_2() ) ? $countries->get_base_address_2() : null; $separator = ! empty( $address ) && ! empty( $address_2 ) ? "\n" : ''; $address = sprintf( '%s%s%s', $countries->get_base_address(), $separator, $countries->get_base_address_2() ); if ( ! empty( $address ) ) { $mc_address->setStreetAddress( $address ); } return $mc_address; } /** * Check whether the address has errors * * @param AccountAddress $address to be validated. * * @return array */ public function wc_address_errors( AccountAddress $address ): array { /** @var WC $wc */ $wc = $this->container->get( WC::class ); $countries = $wc->get_wc_countries(); $locale = $countries->get_country_locale(); $locale_settings = $locale[ $address->getCountry() ] ?? []; $fields_to_validate = [ 'address_1' => $address->getStreetAddress(), 'city' => $address->getLocality(), 'country' => $address->getCountry(), 'postcode' => $address->getPostalCode(), ]; return $this->validate_address( $fields_to_validate, $locale_settings ); } /** * Check whether the required address fields are empty * * @param array $address_fields to be validated. * @param array $locale_settings locale settings * @return array */ public function validate_address( array $address_fields, array $locale_settings ): array { $errors = array_filter( $address_fields, function ( $field ) use ( $locale_settings, $address_fields ) { $is_required = $locale_settings[ $field ]['required'] ?? true; return $is_required && empty( $address_fields[ $field ] ); }, ARRAY_FILTER_USE_KEY ); return array_keys( $errors ); } /** * Return a state name. * * @param string $state_code State code. * @param string $country Country code. * * @return string * * @since 1.4.0 */ protected function maybe_get_state_name( string $state_code, string $country ): string { /** @var WC $wc */ $wc = $this->container->get( WC::class ); $states = $country ? array_filter( (array) $wc->get_wc_countries()->get_states( $country ) ) : []; if ( ! empty( $states ) ) { $state_code = wc_strtoupper( $state_code ); if ( isset( $states[ $state_code ] ) ) { return $states[ $state_code ]; } } return $state_code; } /** * Get the array of settings for the Merchant Center. * * @return array */ protected function get_settings(): array { $settings = $this->get_options_object()->get( OptionsInterface::MERCHANT_CENTER ); return is_array( $settings ) ? $settings : []; } /** * Return a list of target countries or all. * * @return array */ protected function get_target_countries(): array { $target_audience = $this->get_options_object()->get( OptionsInterface::TARGET_AUDIENCE ); if ( isset( $target_audience['location'] ) && 'all' === $target_audience['location'] ) { return [ 'all' ]; } return $target_audience['countries'] ?? []; } } PK! b+src/API/Google/ShoppingContentDateTrait.phpnu[getYear()}-{$date->getMonth()}-{$date->getDay()}" ); } } PK!,yy#src/API/Google/SiteVerification.phpnu[ 'site-url' ] ); throw new Exception( __( 'Invalid site URL.', 'google-listings-and-ads' ) ); } // Retrieve the meta tag with verification token. try { $meta_tag = $this->get_token( $site_url ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_site_verify_failure', [ 'step' => 'token' ] ); throw $e; } // Store the meta tag in the options table and mark as unverified. $site_verification_options = [ 'verified' => self::VERIFICATION_STATUS_UNVERIFIED, 'meta_tag' => $meta_tag, ]; $this->options->update( OptionsInterface::SITE_VERIFICATION, $site_verification_options ); // Attempt verification. try { $this->insert( $site_url ); $site_verification_options['verified'] = self::VERIFICATION_STATUS_VERIFIED; $this->options->update( OptionsInterface::SITE_VERIFICATION, $site_verification_options ); do_action( 'woocommerce_gla_site_verify_success', [] ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_site_verify_failure', [ 'step' => 'meta-tag' ] ); throw $e; } } /** * Get the META token for site verification. * https://developers.google.com/site-verification/v1/webResource/getToken * * @param string $identifier The URL of the site to verify (including protocol). * * @return string The meta tag to be used for verification. * @throws ExceptionWithResponseData When unable to retrieve meta token. */ protected function get_token( string $identifier ): string { /** @var SiteVerificationService $service */ $service = $this->container->get( SiteVerificationService::class ); $post_body = new GetTokenRequest( [ 'verificationMethod' => self::VERIFICATION_METHOD, 'site' => new GetTokenRequestSite( [ 'type' => 'SITE', 'identifier' => $identifier, ] ), ] ); try { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $response = $service->webResource->getToken( $post_body ); } catch ( GoogleServiceException $e ) { do_action( 'woocommerce_gla_sv_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Unable to retrieve site verification token: %s', 'google-listings-and-ads' ), reset( $errors ) ), $e->getCode(), null, [ 'errors' => $errors ] ); } return $response->getToken(); } /** * Instructs the Google Site Verification API to verify site ownership * using the META method. * * @param string $identifier The URL of the site to verify (including protocol). * * @throws ExceptionWithResponseData When unable to verify token. */ protected function insert( string $identifier ) { /** @var SiteVerificationService $service */ $service = $this->container->get( SiteVerificationService::class ); $post_body = new WebResource( [ 'site' => new WebResourceSite( [ 'type' => 'SITE', 'identifier' => $identifier, ] ), ] ); try { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $service->webResource->insert( self::VERIFICATION_METHOD, $post_body ); } catch ( GoogleServiceException $e ) { do_action( 'woocommerce_gla_sv_client_exception', $e, __METHOD__ ); $errors = $this->get_exception_errors( $e ); throw new ExceptionWithResponseData( /* translators: %s Error message */ sprintf( __( 'Unable to insert site verification: %s', 'google-listings-and-ads' ), reset( $errors ) ), $e->getCode(), null, [ 'errors' => $errors ] ); } } } PK!xv"RR2src/API/Site/Controllers/Ads/AccountController.phpnu[account = $account; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'ads/accounts', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_accounts_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->create_or_link_account_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'ads/connection', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connected_ads_account_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->disconnect_ads_account_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); $this->register_route( 'ads/billing-status', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_billing_status_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); $this->register_route( 'ads/account-status', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_ads_account_has_access(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback function for the list accounts request. * * @return callable */ protected function get_accounts_callback(): callable { return function () { try { return new Response( $this->account->get_accounts() ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for creating or linking an account. * * @return callable */ protected function create_or_link_account_callback(): callable { return function ( Request $request ) { try { $link_id = absint( $request['id'] ); if ( $link_id ) { $this->account->use_existing_account( $link_id ); } $account_data = $this->account->setup_account(); return $this->prepare_item_for_response( $account_data, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the connected ads account. * * @return callable */ protected function get_connected_ads_account_callback(): callable { return function () { return $this->account->get_connected_account(); }; } /** * Get the callback function for disconnecting a merchant. * * @return callable */ protected function disconnect_ads_account_callback(): callable { return function () { $this->account->disconnect(); return [ 'status' => 'success', 'message' => __( 'Successfully disconnected.', 'google-listings-and-ads' ), ]; }; } /** * Get the callback function for retrieving the billing setup status. * * @return callable */ protected function get_billing_status_callback(): callable { return function () { return $this->account->get_billing_status(); }; } /** * Get the callback function for retrieving the account access status for ads. * * @return callable */ protected function get_ads_account_has_access(): callable { return function () { try { return $this->account->get_ads_account_has_access(); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'id' => [ 'type' => 'number', 'description' => __( 'Google Ads Account ID.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => false, ], 'billing_url' => [ 'type' => 'string', 'description' => __( 'Billing Flow URL.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'account'; } } PK!e6c$c$5src/API/Site/Controllers/Ads/AssetGroupController.phpnu[ads_asset_group = $ads_asset_group; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'ads/campaigns/asset-groups/(?P[\d]+)', [ [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->edit_asset_group_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->edit_asset_group_params(), ], ] ); $this->register_route( 'ads/campaigns/asset-groups', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_asset_groups_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_asset_group_params(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->create_asset_group_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_asset_group_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the schema for the asset group. * * @return array The asset group schema. */ public function get_asset_group_fields(): array { return [ 'final_url' => [ 'type' => 'string', 'description' => __( 'Final URL.', 'google-listings-and-ads' ), ], 'path1' => [ 'type' => 'string', 'description' => __( 'Asset Group path 1.', 'google-listings-and-ads' ), ], 'path2' => [ 'type' => 'string', 'description' => __( 'Asset Group path 2.', 'google-listings-and-ads' ), ], ]; } /** * Get the edit asset group params params to update an asset group. * * @return array The edit asset group params. */ public function edit_asset_group_params(): array { return array_merge( [ 'id' => [ 'description' => __( 'Asset Group ID.', 'google-listings-and-ads' ), 'type' => 'integer', 'required' => true, ], 'assets' => [ 'type' => 'array', 'description' => __( 'List of asset to be edited.', 'google-listings-and-ads' ), 'items' => $this->get_schema_asset(), 'default' => [], ], ], $this->get_asset_group_fields() ); } /** * Get the assets groups params. * * @return array */ public function get_asset_group_params(): array { return [ 'campaign_id' => [ 'description' => __( 'Campaign ID.', 'google-listings-and-ads' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], ]; } /** * Get Asset Groups by Campaign ID. * * @return callable */ protected function get_asset_groups_callback(): callable { return function ( Request $request ) { try { $campaign_id = $request->get_param( 'campaign_id' ); return array_map( function ( $item ) use ( $request ) { $data = $this->prepare_item_for_response( $item, $request ); return $this->prepare_response_for_collection( $data ); }, $this->ads_asset_group->get_asset_groups_by_campaign_id( $campaign_id ) ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Create asset group. * * @return callable */ public function create_asset_group_callback(): callable { return function ( Request $request ) { try { $asset_group_id = $this->ads_asset_group->create_asset_group( $request->get_param( 'campaign_id' ) ); return [ 'status' => 'success', 'message' => __( 'Successfully created asset group.', 'google-listings-and-ads' ), 'id' => $asset_group_id, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Edit asset group. * * @return callable */ public function edit_asset_group_callback(): callable { return function ( Request $request ) { try { $asset_group_fields = array_intersect_key( $request->get_params(), $this->get_asset_group_fields() ); if ( empty( $asset_group_fields ) && empty( $request->get_param( 'assets' ) ) ) { throw new Exception( __( 'No asset group fields to update.', 'google-listings-and-ads' ) ); } $asset_group_id = $this->ads_asset_group->edit_asset_group( $request->get_param( 'id' ), $asset_group_fields, $request->get_param( 'assets' ) ); return [ 'status' => 'success', 'message' => __( 'Successfully edited asset group.', 'google-listings-and-ads' ), 'id' => $asset_group_id, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'id' => [ 'type' => 'number', 'description' => __( 'Asset Group ID', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'final_url' => [ 'type' => 'string', 'description' => __( 'Final URL', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'display_url_path' => [ 'type' => 'array', 'description' => __( 'Text that may appear appended to the url displayed in the ad.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'assets' => [ 'type' => 'array', 'description' => __( 'Asset is a part of an ad which can be shared across multiple ads. It can be an image, headlines, descriptions, etc.', 'google-listings-and-ads' ), 'items' => [ 'type' => 'object', 'properties' => [ AssetFieldType::SQUARE_MARKETING_IMAGE => $this->get_schema_field_type_asset(), AssetFieldType::MARKETING_IMAGE => $this->get_schema_field_type_asset(), AssetFieldType::PORTRAIT_MARKETING_IMAGE => $this->get_schema_field_type_asset(), AssetFieldType::LOGO => $this->get_schema_field_type_asset(), AssetFieldType::BUSINESS_NAME => $this->get_schema_field_type_asset(), AssetFieldType::HEADLINE => $this->get_schema_field_type_asset(), AssetFieldType::DESCRIPTION => $this->get_schema_field_type_asset(), AssetFieldType::LONG_HEADLINE => $this->get_schema_field_type_asset(), AssetFieldType::CALL_TO_ACTION_SELECTION => $this->get_schema_field_type_asset(), ], ], ], ]; } /** * Get the item schema for the field type asset. * * @return array the field type asset schema. */ protected function get_schema_field_type_asset(): array { return [ 'type' => 'array', 'items' => $this->get_schema_asset(), 'required' => false, ]; } /** * Get the item schema for the asset. * * @return array */ protected function get_schema_asset() { return [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => [ 'integer', 'null' ], 'description' => __( 'Asset ID', 'google-listings-and-ads' ), ], 'content' => [ 'type' => [ 'string', 'null' ], 'description' => __( 'Asset content', 'google-listings-and-ads' ), ], 'field_type' => [ 'type' => 'string', 'description' => __( 'Asset field type', 'google-listings-and-ads' ), 'required' => true, 'context' => [ 'edit' ], 'enum' => [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION, AssetFieldType::BUSINESS_NAME, AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::LOGO, AssetFieldType::CALL_TO_ACTION_SELECTION, AssetFieldType::PORTRAIT_MARKETING_IMAGE, ], ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'asset-group'; } } PK!p]];src/API/Site/Controllers/Ads/AssetSuggestionsController.phpnu[asset_suggestions_service = $asset_suggestions; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'assets/suggestions', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_assets_suggestions_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_assets_suggestions_params(), ], ] ); $this->register_route( 'assets/final-url/suggestions', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_final_url_suggestions_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { return [ 'search' => [ 'description' => __( 'Search for post title or term name', 'google-listings-and-ads' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ], 'per_page' => [ 'description' => __( 'The number of items to be return', 'google-listings-and-ads' ), 'type' => 'number', 'default' => 30, 'sanitize_callback' => 'absint', 'minimum' => 1, 'validate_callback' => 'rest_validate_request_arg', ], 'order_by' => [ 'description' => __( 'Sort retrieved items by parameter', 'google-listings-and-ads' ), 'type' => 'string', 'default' => 'title', 'sanitize_callback' => 'sanitize_text_field', 'enum' => [ 'type', 'title', 'url' ], 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Get the assets suggestions params. * * @return array */ public function get_assets_suggestions_params(): array { return [ 'id' => [ 'description' => __( 'Post ID or Term ID.', 'google-listings-and-ads' ), 'type' => 'number', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], 'type' => [ 'description' => __( 'Type linked to the id.', 'google-listings-and-ads' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'enum' => [ 'post', 'term', 'homepage' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], ]; } /** * Get the callback function for the assets suggestions request. * * @return callable */ protected function get_assets_suggestions_callback(): callable { return function ( Request $request ) { try { $id = $request->get_param( 'id' ); $type = $request->get_param( 'type' ); return $this->asset_suggestions_service->get_assets_suggestions( $id, $type ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the list of final-url suggestions request. * * @return callable */ protected function get_final_url_suggestions_callback(): callable { return function ( Request $request ) { $search = $request->get_param( 'search' ); $per_page = $request->get_param( 'per_page' ); $order_by = $request->get_param( 'order_by' ); return array_map( function ( $item ) use ( $request ) { $data = $this->prepare_item_for_response( $item, $request ); return $this->prepare_response_for_collection( $data ); }, $this->asset_suggestions_service->get_final_url_suggestions( $search, $per_page, $order_by ) ); }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'id' => [ 'type' => 'number', 'description' => __( 'Post ID or Term ID', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'type' => [ 'type' => 'string', 'description' => __( 'Post, term or homepage', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'enum' => [ 'post', 'term', 'homepage' ], 'readonly' => true, ], 'title' => [ 'type' => 'string', 'description' => __( 'The post or term title', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'url' => [ 'type' => 'string', 'description' => __( 'The URL linked to the post/term', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'asset_final_url_suggestions'; } } PK!Ks?src/API/Site/Controllers/Ads/BudgetRecommendationController.phpnu[budget_recommendation_query = $budget_recommendation_query; $this->ads = $ads; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'ads/campaigns/budget-recommendation', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_budget_recommendation_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), 'country_codes' => [ 'type' => 'array', 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_country_code_validate_callback(), 'items' => [ 'type' => 'string', ], 'required' => true, 'minItems' => 1, ], ]; } /** * @return callable */ protected function get_budget_recommendation_callback(): callable { return function ( Request $request ) { $country_codes = $request->get_param( 'country_codes' ); $currency = $this->ads->get_ads_currency(); if ( ! $currency ) { return new Response( [ 'message' => __( 'No currency available for the Ads account.', 'google-listings-and-ads' ), 'currency' => $currency, 'country_codes' => $country_codes, ], 400 ); } $recommendations = $this ->budget_recommendation_query ->where( 'country', $country_codes, 'IN' ) ->where( 'currency', $currency ) ->get_results(); if ( ! $recommendations ) { return new Response( [ 'message' => __( 'Cannot find any budget recommendations.', 'google-listings-and-ads' ), 'currency' => $currency, 'country_codes' => $country_codes, ], 404 ); } $returned_recommendations = array_map( function ( $recommendation ) { return [ 'country' => $recommendation['country'], 'daily_budget' => (int) $recommendation['daily_budget'], ]; }, $recommendations ); return $this->prepare_item_for_response( [ 'currency' => $currency, 'recommendations' => $returned_recommendations, ], $request ); }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'currency' => [ 'type' => 'string', 'description' => __( 'The currency to use for the shipping rate.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'validate_callback' => 'rest_validate_request_arg', ], 'recommendations' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'country' => [ 'type' => 'string', 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'daily_budget' => [ 'type' => 'number', 'description' => __( 'The recommended daily budget for a country.', 'google-listings-and-ads' ), ], ], ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'budget-recommendation'; } } PK!sp113src/API/Site/Controllers/Ads/CampaignController.phpnu[ads_campaign = $ads_campaign; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'ads/campaigns', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_campaigns_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->create_campaign_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'ads/campaigns/(?P[\d]+)', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_campaign_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->edit_campaign_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_edit_schema(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->delete_campaign_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for listing campaigns. * * @return callable */ protected function get_campaigns_callback(): callable { return function ( Request $request ) { try { $exclude_removed = $request->get_param( 'exclude_removed' ); return array_map( function ( $campaign ) use ( $request ) { $data = $this->prepare_item_for_response( $campaign, $request ); return $this->prepare_response_for_collection( $data ); }, $this->ads_campaign->get_campaigns( $exclude_removed, true, $request->get_params() ) ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for creating a campaign. * * @return callable */ protected function create_campaign_callback(): callable { return function ( Request $request ) { try { $fields = array_intersect_key( $request->get_json_params(), $this->get_schema_properties() ); // Set the default value of campaign name. if ( empty( $fields['name'] ) ) { $current_date_time = ( new DateTime( 'now', wp_timezone() ) )->format( 'Y-m-d H:i:s' ); $fields['name'] = sprintf( /* translators: %s: current date time. */ __( 'Campaign %s', 'google-listings-and-ads' ), $current_date_time ); } $campaign = $this->ads_campaign->create_campaign( $fields ); /** * When a campaign has been successfully created. * * @event gla_created_campaign * @property int id Campaign ID. * @property string status Campaign status, `enabled` or `paused`. * @property string name Campaign name, generated based on date. * @property float amount Campaign budget. * @property string country Base target country code. * @property string targeted_locations Additional target country codes. * @property string source The source of the campaign creation. */ do_action( 'woocommerce_gla_track_event', 'created_campaign', [ 'id' => $campaign['id'], 'status' => $campaign['status'], 'name' => $campaign['name'], 'amount' => $campaign['amount'], 'country' => $campaign['country'], 'targeted_locations' => join( ',', $campaign['targeted_locations'] ), 'source' => $fields['label'] ?? '', ] ); return $this->prepare_item_for_response( $campaign, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for listing a single campaign. * * @return callable */ protected function get_campaign_callback(): callable { return function ( Request $request ) { try { $id = absint( $request['id'] ); $campaign = $this->ads_campaign->get_campaign( $id ); if ( empty( $campaign ) ) { return new Response( [ 'message' => __( 'Campaign is not available.', 'google-listings-and-ads' ), 'id' => $id, ], 404 ); } return $this->prepare_item_for_response( $campaign, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for editing a campaign. * * @return callable */ protected function edit_campaign_callback(): callable { return function ( Request $request ) { try { $fields = array_intersect_key( $request->get_json_params(), $this->get_edit_schema() ); if ( empty( $fields ) ) { return new Response( [ 'status' => 'invalid_data', 'message' => __( 'Invalid edit data.', 'google-listings-and-ads' ), ], 400 ); } $campaign_id = $this->ads_campaign->edit_campaign( absint( $request['id'] ), $fields ); /** * When a campaign has been successfully edited. * * @event gla_edited_campaign * @property int id Campaign ID. * @property string status Campaign status, `enabled` or `paused`. * @property string name Campaign name, generated based on date. * @property float amount Campaign budget. */ do_action( 'woocommerce_gla_track_event', 'edited_campaign', array_merge( [ 'id' => $campaign_id, ], $fields, ) ); return [ 'status' => 'success', 'message' => __( 'Successfully edited campaign.', 'google-listings-and-ads' ), 'id' => $campaign_id, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for deleting a campaign. * * @return callable */ protected function delete_campaign_callback(): callable { return function ( Request $request ) { try { $deleted_id = $this->ads_campaign->delete_campaign( absint( $request['id'] ) ); /** * When a campaign has been successfully deleted. * * @event gla_deleted_campaign * @property int id Campaign ID. */ do_action( 'woocommerce_gla_track_event', 'deleted_campaign', [ 'id' => $deleted_id, ] ); return [ 'status' => 'success', 'message' => __( 'Successfully deleted campaign.', 'google-listings-and-ads' ), 'id' => $deleted_id, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the schema for fields we are allowed to edit. * * @return array */ protected function get_edit_schema(): array { $allowed = [ 'name', 'status', 'amount', ]; $fields = array_intersect_key( $this->get_schema_properties(), array_flip( $allowed ) ); // Unset required to allow editing individual fields. array_walk( $fields, function ( &$value ) { unset( $value['required'] ); } ); return $fields; } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { return [ 'exclude_removed' => [ 'description' => __( 'Exclude removed campaigns.', 'google-listings-and-ads' ), 'type' => 'boolean', 'default' => true, 'validate_callback' => 'rest_validate_request_arg', ], 'per_page' => [ 'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ), 'type' => 'integer', 'minimum' => 1, 'maximum' => 10000, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'id' => [ 'type' => 'integer', 'description' => __( 'ID number.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'name' => [ 'type' => 'string', 'description' => __( 'Descriptive campaign name.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => false, ], 'status' => [ 'type' => 'string', 'enum' => CampaignStatus::labels(), 'description' => __( 'Campaign status.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', ], 'type' => [ 'type' => 'string', 'enum' => CampaignType::labels(), 'description' => __( 'Campaign type.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', ], 'amount' => [ 'type' => 'number', 'description' => __( 'Daily budget amount in the local currency.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], 'country' => [ 'type' => 'string', 'description' => __( 'Country code of sale country in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_supported_country_code_validate_callback(), 'readonly' => true, ], 'targeted_locations' => [ 'type' => 'array', 'description' => __( 'The locations that an Ads campaign is targeting in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_supported_country_code_validate_callback(), 'required' => true, 'minItems' => 1, 'items' => [ 'type' => 'string', ], ], 'label' => [ 'type' => 'string', 'description' => __( 'The name of the label to assign to the campaign.', 'google-listings-and-ads' ), 'context' => [ 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => false, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'campaign'; } } PK!u{{2src/API/Site/Controllers/Ads/ReportsController.phpnu[register_route( 'ads/reports/programs', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_programs_report_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'ads/reports/products', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_products_report_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for the programs report request. * * @return callable */ protected function get_programs_report_callback(): callable { return function ( Request $request ) { try { /** @var AdsReport $ads */ $ads = $this->container->get( AdsReport::class ); $data = $ads->get_report_data( 'campaigns', $this->prepare_query_arguments( $request ) ); return $this->prepare_item_for_response( $data, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the products report request. * * @return callable */ protected function get_products_report_callback(): callable { return function ( Request $request ) { try { /** @var AdsReport $ads */ $ads = $this->container->get( AdsReport::class ); $data = $ads->get_report_data( 'products', $this->prepare_query_arguments( $request ) ); return $this->prepare_item_for_response( $data, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { $params = parent::get_collection_params(); $params['interval'] = [ 'description' => __( 'Time interval to use for segments in the returned data.', 'google-listings-and-ads' ), 'type' => 'string', 'enum' => [ 'day', 'week', 'month', 'quarter', 'year', ], 'validate_callback' => 'rest_validate_request_arg', ]; return $params; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'products' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string', 'description' => __( 'Product ID.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'name' => [ 'type' => 'string', 'description' => __( 'Product name.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], ], 'subtotals' => $this->get_totals_schema(), ], ], ], 'campaigns' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => __( 'ID number.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'name' => [ 'type' => 'string', 'description' => __( 'Campaign name.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], ], 'status' => [ 'type' => 'string', 'enum' => CampaignStatus::labels(), 'description' => __( 'Campaign status.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'isConverted' => [ 'type' => 'boolean', 'description' => __( 'Whether the campaign has been converted', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'subtotals' => $this->get_totals_schema(), ], ], ], 'intervals' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'interval' => [ 'type' => 'string', 'description' => __( 'ID of this report segment.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'subtotals' => $this->get_totals_schema(), ], ], ], 'totals' => $this->get_totals_schema(), 'next_page' => [ 'type' => 'string', 'description' => __( 'Token to retrieve the next page of results.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ]; } /** * Return schema for total fields. * * @return array */ protected function get_totals_schema(): array { return [ 'type' => 'object', 'properties' => [ 'clicks' => [ 'type' => 'integer', 'description' => __( 'Clicks.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'impressions' => [ 'type' => 'integer', 'description' => __( 'Impressions.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'sales' => [ 'type' => 'number', 'description' => __( 'Sales amount.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'spend' => [ 'type' => 'number', 'description' => __( 'Spend amount.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'conversions' => [ 'type' => 'number', 'description' => __( 'Conversions.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'reports'; } } PK!Y% % 8src/API/Site/Controllers/Ads/SetupCompleteController.phpnu[metrics = $metrics; } /** * Registers the routes for the objects of the controller. */ public function register_routes() { $this->register_route( 'ads/setup/complete', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_setup_complete_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback function for marking setup complete. * * @return callable */ protected function get_setup_complete_callback(): callable { return function ( Request $request ) { do_action( 'woocommerce_gla_ads_setup_completed' ); /** * Ads onboarding has been successfully completed. * * @event gla_ads_setup_completed * @property int campaign_count Number of campaigns for the connected Ads account. */ do_action( 'woocommerce_gla_track_event', 'ads_setup_completed', [ 'campaign_count' => $this->metrics->get_campaign_count(), ] ); return new Response( [ 'status' => 'success', 'message' => __( 'Successfully marked Ads setup as completed.', 'google-listings-and-ads' ), ] ); }; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'ads_setup_complete'; } } PK!/WLsrc/API/Site/Controllers/AttributeMapping/AttributeMappingDataController.phpnu[attribute_mapping_helper = $attribute_mapping_helper; } /** * Register rest routes with WordPress. */ public function register_routes(): void { /** * GET the destination fields for Google Shopping */ $this->register_route( 'mc/mapping/attributes', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_mapping_attributes_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); /** * GET for getting the source data for a specific destination */ $this->register_route( 'mc/mapping/sources', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_mapping_sources_read_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => [ 'attribute' => [ 'description' => __( 'The attribute key to get the sources.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], ], ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Callback function for returning the attributes * * @return callable */ protected function get_mapping_attributes_read_callback(): callable { return function ( Request $request ) { try { return $this->prepare_item_for_response( $this->get_attributes(), $request ); } catch ( Exception $e ) { return new Response( [ 'message' => $e->getMessage() ], $e->getCode() ?: 400 ); } }; } /** * Callback function for returning the sources. * * @return callable */ protected function get_mapping_sources_read_callback(): callable { return function ( Request $request ) { try { $attribute = $request->get_param( 'attribute' ); return [ 'data' => $this->attribute_mapping_helper->get_sources_for_attribute( $attribute ), ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'data' => [ 'type' => 'array', 'description' => __( 'The list of attributes or attribute sources.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'attribute_mapping_data'; } /** * Attributes getter * * @return array The attributes available for mapping */ private function get_attributes(): array { return [ 'data' => $this->attribute_mapping_helper->get_attributes(), ]; } } PK!ަ%#%#Msrc/API/Site/Controllers/AttributeMapping/AttributeMappingRulesController.phpnu[attribute_mapping_helper = $attribute_mapping_helper; $this->attribute_mapping_rules_query = $attribute_mapping_rules_query; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/mapping/rules', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_rule_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->create_rule_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); $this->register_route( 'mc/mapping/rules/(?P[\d]+)', [ [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->update_rule_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->delete_rule_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Callback function for getting the Attribute Mapping rules from DB * * @return callable */ protected function get_rule_callback(): callable { return function ( Request $request ) { try { $page = $request->get_param( 'page' ); $per_page = $request->get_param( 'per_page' ); $this->attribute_mapping_rules_query->set_limit( $per_page ); $this->attribute_mapping_rules_query->set_offset( $per_page * ( $page - 1 ) ); $rules = $this->attribute_mapping_rules_query->get_results(); $total_rules = $this->attribute_mapping_rules_query->get_count(); $response_data = []; foreach ( $rules as $rule ) { $item_data = $this->prepare_item_for_response( $rule, $request ); $response_data[] = $this->prepare_response_for_collection( $item_data ); } return new Response( $response_data, 200, [ 'X-WP-Total' => $total_rules, 'X-WP-TotalPages' => ceil( $total_rules / $per_page ), ] ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Callback function for saving an Attribute Mapping rule in DB * * @return callable */ protected function create_rule_callback(): callable { return function ( Request $request ) { try { if ( ! $this->attribute_mapping_rules_query->insert( $this->prepare_item_for_database( $request ) ) ) { return $this->response_from_exception( new Exception( 'Unable to create the new rule.' ) ); } $response = $this->prepare_item_for_response( $this->attribute_mapping_rules_query->get_rule( $this->attribute_mapping_rules_query->last_insert_id() ), $request ); do_action( 'woocommerce_gla_mapping_rules_change' ); return $response; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Callback function for saving an Attribute Mapping rule in DB * * @return callable */ protected function update_rule_callback(): callable { return function ( Request $request ) { try { $rule_id = $request->get_url_params()['id']; if ( ! $this->attribute_mapping_rules_query->update( $this->prepare_item_for_database( $request ), [ 'id' => $rule_id ] ) ) { return $this->response_from_exception( new Exception( 'Unable to update the new rule.' ) ); } $response = $this->prepare_item_for_response( $this->attribute_mapping_rules_query->get_rule( $rule_id ), $request ); do_action( 'woocommerce_gla_mapping_rules_change' ); return $response; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Callback function for deleting an Attribute Mapping rule in DB * * @return callable */ protected function delete_rule_callback(): callable { return function ( Request $request ) { try { $rule_id = $request->get_url_params()['id']; if ( ! $this->attribute_mapping_rules_query->delete( 'id', $rule_id ) ) { return $this->response_from_exception( new Exception( 'Unable to delete the rule' ) ); } do_action( 'woocommerce_gla_mapping_rules_change' ); return [ 'id' => $rule_id, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema properties for the controller. * * @return array The Schema properties */ protected function get_schema_properties(): array { return [ 'id' => [ 'description' => __( 'The Id for the rule.', 'google-listings-and-ads' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', 'readonly' => true, ], 'attribute' => [ 'description' => __( 'The attribute value for the rule.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, 'enum' => array_column( $this->attribute_mapping_helper->get_attributes(), 'id' ), ], 'source' => [ 'description' => __( 'The source value for the rule.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], 'category_condition_type' => [ 'description' => __( 'The category condition type to apply for this rule.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, 'enum' => $this->attribute_mapping_helper->get_category_condition_types(), ], 'categories' => [ 'description' => __( 'List of category IDs, separated by commas.', 'google-listings-and-ads' ), 'type' => 'string', 'required' => false, 'validate_callback' => function ( $param ) { return $this->validate_categories_param( $param ); }, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'attribute_mapping_rules'; } /** * @param string $categories Categories to validate * @return bool|WP_Error True if it's validated * * @throw Exception when invalid categories are provided */ public function validate_categories_param( string $categories ) { if ( $categories === '' ) { return true; } $categories_array = explode( ',', $categories ); foreach ( $categories_array as $category ) { if ( ! is_numeric( $category ) ) { return new WP_Error( 'woocommerce_gla_attribute_mapping_invalid_categories_schema', 'categories should be a string of category IDs separated by commas.', [ 'categories' => $categories, ] ); } } return true; } } PK!r  Nsrc/API/Site/Controllers/AttributeMapping/AttributeMappingSyncerController.phpnu[sync_stats = $sync_stats; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/mapping/sync', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_sync_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Callback function for getting the Attribute Mapping Sync State * * @return callable */ protected function get_sync_callback(): callable { return function ( Request $request ) { try { $state = [ 'is_scheduled' => (bool) $this->sync_stats->get_count(), 'last_sync' => $this->options->get( OptionsInterface::UPDATE_ALL_PRODUCTS_LAST_SYNC ), ]; return $this->prepare_item_for_response( $state, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema properties for the controller. * * @return array The Schema properties */ protected function get_schema_properties(): array { return [ 'is_scheduled' => [ 'description' => __( 'Indicates if the products are currently syncing', 'google-listings-and-ads' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', 'readonly' => true, 'context' => [ 'view' ], ], 'last_sync' => [ 'description' => __( 'Timestamp with the last sync.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'readonly' => true, 'context' => [ 'view' ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'attribute_mapping_syncer'; } } PK!W=PP5src/API/Site/Controllers/Google/AccountController.phpnu[ '/google/setup-mc', 'setup-ads' => '/google/setup-ads', 'reconnect' => '/google/settings&subpath=/reconnect-google-account', ]; /** * AccountController constructor. * * @param RESTServer $server * @param Connection $connection */ public function __construct( RESTServer $server, Connection $connection ) { parent::__construct( $server ); $this->connection = $connection; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'google/connect', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connect_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_connect_params(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_disconnect_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'google/connected', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connected_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); $this->register_route( 'google/reconnected', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_reconnected_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback function for the connection request. * * @return callable */ protected function get_connect_callback(): callable { return function ( Request $request ) { try { $next = $request->get_param( 'next_page_name' ); $login_hint = $request->get_param( 'login_hint' ) ?: ''; $path = self::NEXT_PATH_MAPPING[ $next ]; return [ 'url' => $this->connection->connect( admin_url( "admin.php?page=wc-admin&path={$path}" ), $login_hint ), ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the query params for the connection request. * * @return array */ protected function get_connect_params(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), 'next_page_name' => [ 'description' => __( 'Indicates the next page name mapped to the redirect URL when back from Google authorization.', 'google-listings-and-ads' ), 'type' => 'string', 'default' => array_key_first( self::NEXT_PATH_MAPPING ), 'enum' => array_keys( self::NEXT_PATH_MAPPING ), 'validate_callback' => 'rest_validate_request_arg', ], 'login_hint' => [ 'description' => __( 'Indicate the Google account to suggest for authorization.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'is_email', ], ]; } /** * Get the callback function for the disconnection request. * * @return callable */ protected function get_disconnect_callback(): callable { return function () { $this->connection->disconnect(); return [ 'status' => 'success', 'message' => __( 'Successfully disconnected.', 'google-listings-and-ads' ), ]; }; } /** * Get the callback function to determine if Google is currently connected. * * Uses consistent properties to the Jetpack connected callback * * @return callable */ protected function get_connected_callback(): callable { return function () { try { $status = $this->connection->get_status(); return [ 'active' => array_key_exists( 'status', $status ) && ( 'connected' === $status['status'] ) ? 'yes' : 'no', 'email' => array_key_exists( 'email', $status ) ? $status['email'] : '', 'scope' => array_key_exists( 'scope', $status ) ? $status['scope'] : [], ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function to determine if we have access to the dependent services. * * @return callable */ protected function get_reconnected_callback(): callable { return function () { try { $status = $this->connection->get_reconnect_status(); $status['active'] = array_key_exists( 'status', $status ) && ( 'connected' === $status['status'] ) ? 'yes' : 'no'; unset( $status['status'] ); return $status; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'url' => [ 'type' => 'string', 'description' => __( 'The URL for making a connection to Google.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'google_account'; } } PK!?V  6src/API/Site/Controllers/Jetpack/AccountController.phpnu[ '/google/setup-mc', 'reconnect' => '/google/settings&subpath=/reconnect-wpcom-account', ]; /** * AccountController constructor. * * @param RESTServer $server * @param Manager $manager * @param Middleware $middleware */ public function __construct( RESTServer $server, Manager $manager, Middleware $middleware ) { parent::__construct( $server ); $this->manager = $manager; $this->middleware = $middleware; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'jetpack/connect', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connect_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_connect_params(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_disconnect_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'jetpack/connected', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connected_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback function for the connection request. * * @return callable */ protected function get_connect_callback(): callable { return function ( Request $request ) { // Register the site to WPCOM. if ( $this->manager->is_connected() ) { $result = $this->manager->reconnect(); } else { $result = $this->manager->register(); } if ( is_wp_error( $result ) ) { return new Response( [ 'status' => 'error', 'message' => $result->get_error_message(), ], 400 ); } // Get an authorization URL which will redirect back to our page. $next = $request->get_param( 'next_page_name' ); $path = self::NEXT_PATH_MAPPING[ $next ]; $redirect = admin_url( "admin.php?page=wc-admin&path={$path}" ); $auth_url = $this->manager->get_authorization_url( null, $redirect ); // Payments flow allows redirect back to the site without showing plans. Escaping the URL preventing XSS. $auth_url = esc_url( add_query_arg( [ 'from' => 'google-listings-and-ads' ], $auth_url ), null, 'db' ); return [ 'url' => $auth_url, ]; }; } /** * Get the query params for the connection request. * * @return array */ protected function get_connect_params(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), 'next_page_name' => [ 'description' => __( 'Indicates the next page name mapped to the redirect URL when back from Jetpack authorization.', 'google-listings-and-ads' ), 'type' => 'string', 'default' => array_key_first( self::NEXT_PATH_MAPPING ), 'enum' => array_keys( self::NEXT_PATH_MAPPING ), 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Get the callback function for the disconnection request. * * @return callable */ protected function get_disconnect_callback(): callable { return function () { $this->manager->remove_connection(); $this->options->delete( OptionsInterface::WP_TOS_ACCEPTED ); $this->options->delete( OptionsInterface::JETPACK_CONNECTED ); return [ 'status' => 'success', 'message' => __( 'Successfully disconnected.', 'google-listings-and-ads' ), ]; }; } /** * Get the callback function to determine if Jetpack is currently connected. * * @return callable */ protected function get_connected_callback(): callable { return function () { if ( $this->is_jetpack_connected() && ! $this->options->get( OptionsInterface::WP_TOS_ACCEPTED ) ) { $this->log_wp_tos_accepted(); } // Update connection status. $this->options->update( OptionsInterface::JETPACK_CONNECTED, $this->is_jetpack_connected() ); $user_data = $this->get_jetpack_user_data(); return [ 'active' => $this->display_boolean( $this->is_jetpack_connected() ), 'owner' => $this->display_boolean( $this->is_jetpack_connection_owner() ), 'displayName' => $user_data['display_name'] ?? '', 'email' => $user_data['email'] ?? '', ]; }; } /** * Determine whether Jetpack is connected. * Check if manager is active and we have a valid token. * * @return bool */ protected function is_jetpack_connected(): bool { if ( null !== $this->jetpack_connected_state ) { return $this->jetpack_connected_state; } if ( ! $this->manager->has_connected_owner() || ! $this->manager->is_connected() ) { $this->jetpack_connected_state = false; return false; } // Send an external request to validate the token. $this->jetpack_connected_state = $this->manager->get_tokens()->validate_blog_token(); return $this->jetpack_connected_state; } /** * Determine whether user is the current Jetpack connection owner. * * @return bool */ protected function is_jetpack_connection_owner(): bool { return $this->manager->is_connection_owner(); } /** * Format boolean for display. * * @param bool $value * * @return string */ protected function display_boolean( bool $value ): string { return $value ? 'yes' : 'no'; } /** * Get the wpcom user data of the current connected user. * * @return array */ protected function get_jetpack_user_data(): array { $user_data = $this->manager->get_connected_user_data(); // adjust for $user_data returning false return is_array( $user_data ) ? $user_data : []; } /** * Log accepted TOS for WordPress. */ protected function log_wp_tos_accepted() { $user = wp_get_current_user(); $this->middleware->mark_tos_accepted( 'wp-com', $user->user_email ); $this->options->update( OptionsInterface::WP_TOS_ACCEPTED, true ); } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'url' => [ 'type' => 'string', 'description' => __( 'The URL for making a connection to Jetpack (wordpress.com).', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'jetpack_account'; } } PK!"ww=src/API/Site/Controllers/MerchantCenter/AccountController.phpnu[account = $account; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/accounts', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_accounts_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->setup_account_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'mc/accounts/claim-overwrite', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->overwrite_claim_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'mc/accounts/switch-url', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->switch_url_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'mc/connection', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connected_merchant_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->disconnect_merchant_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); $this->register_route( 'mc/setup', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_setup_merchant_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback function for the list accounts request. * * @return callable */ protected function get_accounts_callback(): callable { return function ( Request $request ) { try { return array_map( function ( $account ) use ( $request ) { $data = $this->prepare_item_for_response( $account, $request ); return $this->prepare_response_for_collection( $data ); }, $this->account->get_accounts() ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback for creating or linking an account, overwriting the website claim during the claim step. * * @return callable */ protected function overwrite_claim_callback(): callable { return $this->setup_account_callback( 'overwrite_claim' ); } /** * Get the callback for creating or linking an account, switching the URL during the set_id step. * * @return callable */ protected function switch_url_callback(): callable { return $this->setup_account_callback( 'switch_url' ); } /** * Get the callback function for creating or linking an account. * * @param string $action Action to call while setting up account (default is normal setup). * @return callable */ protected function setup_account_callback( string $action = 'setup_account' ): callable { return function ( Request $request ) use ( $action ) { try { $account_id = absint( $request['id'] ); if ( $account_id && 'setup_account' === $action ) { $this->account->use_existing_account_id( $account_id ); } $account = $this->account->{$action}( $account_id ); return $this->prepare_item_for_response( $account, $request ); } catch ( ApiNotReady $e ) { return $this->get_time_to_wait_response( $e ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the connected merchant account. * * @return callable */ protected function get_connected_merchant_callback(): callable { return function () { return $this->account->get_connected_status(); }; } /** * Get the callback function for the merchant setup status. * * @return callable */ protected function get_setup_merchant_callback(): callable { return function () { return $this->account->get_setup_status(); }; } /** * Get the callback function for disconnecting a merchant. * * @return callable */ protected function disconnect_merchant_callback(): callable { return function () { $this->account->disconnect(); return [ 'status' => 'success', 'message' => __( 'Merchant Center account successfully disconnected.', 'google-listings-and-ads' ), ]; }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'id' => [ 'type' => 'number', 'description' => __( 'Merchant Center Account ID.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => false, ], 'subaccount' => [ 'type' => 'boolean', 'description' => __( 'Is a MCA sub account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'name' => [ 'type' => 'string', 'description' => __( 'The Merchant Center Account name.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'required' => false, ], 'domain' => [ 'type' => 'string', 'description' => __( 'The domain registered with the Merchant Center Account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'account'; } /** * Return a 503 Response with Retry-After header and message. * * @param ApiNotReady $wait Exception containing the time to wait. * * @return Response */ private function get_time_to_wait_response( ApiNotReady $wait ): Response { $data = $wait->get_response_data( true ); return new Response( $data, $wait->getCode() ?: 503, [ 'Retry-After' => $data['retry_after'], ] ); } } PK!P P Psrc/API/Site/Controllers/MerchantCenter/AttributeMappingCategoriesController.phpnu[register_route( 'mc/mapping/categories', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_categories_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Callback function for getting the category tree * * @return callable */ protected function get_categories_callback(): callable { return function ( Request $request ) { try { $cats = $this->get_category_tree(); return array_map( function ( $cats ) use ( $request ) { $response = $this->prepare_item_for_response( $cats, $request ); return $this->prepare_response_for_collection( $response ); }, $cats ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema properties for the controller. * * @return array The Schema properties */ protected function get_schema_properties(): array { return [ 'id' => [ 'description' => __( 'The Category ID.', 'google-listings-and-ads' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', 'readonly' => true, ], 'name' => [ 'description' => __( 'The category name.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'readonly' => true, ], 'parent' => [ 'description' => __( 'The category parent.', 'google-listings-and-ads' ), 'type' => 'integer', 'validate_callback' => 'rest_validate_request_arg', 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'attribute_mapping_categories'; } /** * Function to get all the categories * * @return array The categories */ private function get_category_tree(): array { $categories = get_categories( [ 'taxonomy' => 'product_cat', 'hide_empty' => false, ] ); return array_map( function ( $category ) { return [ 'id' => $category->term_id, 'name' => $category->name, 'parent' => $category->parent, ]; }, $categories ); } } PK!¬>src/API/Site/Controllers/MerchantCenter/BatchShippingTrait.phpnu[get_param( 'country_codes' ); $responses = []; $errors = []; foreach ( $country_codes as $country_code ) { $route = "/{$this->get_namespace()}/{$this->route_base}/{$country_code}"; $delete_request = new Request( 'DELETE', $route ); $response = $this->server->dispatch_request( $delete_request ); if ( 200 !== $response->get_status() ) { $errors[] = $response->get_data(); } else { $responses[] = $response->get_data(); } } return new Response( [ 'errors' => $errors, 'success' => $responses, ], ); }; } } PK!O@@src/API/Site/Controllers/MerchantCenter/ConnectionController.phpnu[register_route( 'mc/connect', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_connect_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for the connection request. * * @return callable */ protected function get_connect_callback(): callable { return function () { return [ 'url' => 'example.com', ]; }; } /** * Get the schema for settings endpoints. * * @return array */ protected function get_schema_properties(): array { return [ 'url' => [ 'description' => __( 'Action that should be completed after connection.', 'google-listings-and-ads' ), 'type' => 'string', 'context' => [ 'view', 'edit' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'merchant_center_connection'; } } PK!IF'F'Hsrc/API/Site/Controllers/MerchantCenter/ContactInformationController.phpnu[contact_information = $contact_information; $this->settings = $settings; $this->address_utility = $address_utility; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/contact-information', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_contact_information_endpoint_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_contact_information_endpoint_edit_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_update_args(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get a callback for the contact information endpoint. * * @return callable */ protected function get_contact_information_endpoint_read_callback(): callable { return function ( Request $request ) { try { return $this->get_contact_information_response( $this->contact_information->get_contact_information(), $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get a callback for the edit contact information endpoint. * * @return callable */ protected function get_contact_information_endpoint_edit_callback(): callable { return function ( Request $request ) { try { return $this->get_contact_information_response( $this->contact_information->update_address_based_on_store_settings(), $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the schema for contact information endpoints. * * @return array */ protected function get_schema_properties(): array { return [ 'id' => [ 'type' => 'integer', 'description' => __( 'The Merchant Center account ID.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', ], 'phone_number' => [ 'type' => 'string', 'description' => __( 'The phone number associated with the Merchant Center account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'phone_verification_status' => [ 'type' => 'string', 'description' => __( 'The verification status of the phone number associated with the Merchant Center account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'enum' => [ 'verified', 'unverified' ], ], 'mc_address' => [ 'type' => 'object', 'description' => __( 'The address associated with the Merchant Center account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'properties' => $this->get_address_schema(), ], 'wc_address' => [ 'type' => 'object', 'description' => __( 'The WooCommerce store address.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'properties' => $this->get_address_schema(), ], 'is_mc_address_different' => [ 'type' => 'boolean', 'description' => __( 'Whether the Merchant Center account address is different than the WooCommerce store address.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'wc_address_errors' => [ 'type' => 'array', 'description' => __( 'The errors associated with the WooCommerce address', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ]; } /** * Get the schema for addresses returned by the contact information endpoints. * * @return array[] */ protected function get_address_schema(): array { return [ 'street_address' => [ 'description' => __( 'Street-level part of the address.', 'google-listings-and-ads' ), 'type' => 'string', 'context' => [ 'view' ], ], 'locality' => [ 'description' => __( 'City, town or commune. May also include dependent localities or sublocalities (e.g. neighborhoods or suburbs).', 'google-listings-and-ads' ), 'type' => 'string', 'context' => [ 'view' ], ], 'region' => [ 'description' => __( 'Top-level administrative subdivision of the country. For example, a state like California ("CA") or a province like Quebec ("QC").', 'google-listings-and-ads' ), 'type' => 'string', 'context' => [ 'view' ], ], 'postal_code' => [ 'description' => __( 'Postal code or ZIP (e.g. "94043").', 'google-listings-and-ads' ), 'type' => 'string', 'context' => [ 'view' ], ], 'country' => [ 'description' => __( 'CLDR country code (e.g. "US").', 'google-listings-and-ads' ), 'type' => 'string', 'context' => [ 'view' ], ], ]; } /** * Get the arguments for the update endpoint. * * @return array */ public function get_update_args(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), ]; } /** * Get the prepared REST response with Merchant Center account ID and contact information. * * @param AccountBusinessInformation|null $contact_information * @param Request $request * * @return Response */ protected function get_contact_information_response( ?AccountBusinessInformation $contact_information, Request $request ): Response { $phone_number = null; $phone_verification_status = null; $mc_address = null; $wc_address = null; $is_address_diff = false; if ( $this->settings->get_store_address() instanceof AccountAddress ) { $wc_address = $this->settings->get_store_address(); $is_address_diff = true; } if ( $contact_information instanceof AccountBusinessInformation ) { if ( ! empty( $contact_information->getPhoneNumber() ) ) { try { $phone_number = PhoneNumber::cast( $contact_information->getPhoneNumber() )->get(); $phone_verification_status = strtolower( $contact_information->getPhoneVerificationStatus() ); } catch ( InvalidValue $exception ) { // log and fail silently do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); } } if ( $contact_information->getAddress() instanceof AccountAddress ) { $mc_address = $contact_information->getAddress(); $is_address_diff = true; } if ( null !== $mc_address && null !== $wc_address ) { $is_address_diff = ! $this->address_utility->compare_addresses( $contact_information->getAddress(), $this->settings->get_store_address() ); } } $wc_address_errors = $this->settings->wc_address_errors( $wc_address ); return $this->prepare_item_for_response( [ 'id' => $this->options->get_merchant_id(), 'phone_number' => $phone_number, 'phone_verification_status' => $phone_verification_status, 'mc_address' => self::serialize_address( $mc_address ), 'wc_address' => self::serialize_address( $wc_address ), 'is_mc_address_different' => $is_address_diff, 'wc_address_errors' => $wc_address_errors, ], $request ); } /** * @param AccountAddress|null $address * * @return array|null */ protected static function serialize_address( ?AccountAddress $address ): ?array { if ( null === $address ) { return null; } return [ 'street_address' => $address->getStreetAddress(), 'locality' => $address->getLocality(), 'region' => $address->getRegion(), 'postal_code' => $address->getPostalCode(), 'country' => $address->getCountry(), ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'merchant_center_contact_information'; } } PK!Ruvqq<src/API/Site/Controllers/MerchantCenter/IssuesController.phpnu[merchant_statuses = $merchant_statuses; $this->product_helper = $product_helper; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/issues(/(?P[a-z]+))?', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_issues_read_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Get the callback function for returning account and product issues. * * @return callable */ protected function get_issues_read_callback(): callable { return function ( Request $request ) { $type_filter = $request['type_filter']; $per_page = intval( $request['per_page'] ); $page = max( 1, intval( $request['page'] ) ); try { $results = $this->merchant_statuses->get_issues( $type_filter, $per_page, $page ); $results['page'] = $page; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } // Replace variation IDs with parent ID (for Edit links). foreach ( $results['issues'] as &$issue ) { $issue = apply_filters( 'woocommerce_gla_merchant_issue_override', $issue ); if ( empty( $issue['product_id'] ) ) { continue; } try { $issue['product_id'] = $this->product_helper->maybe_swap_for_parent_id( $issue['product_id'] ); } catch ( InvalidValue $e ) { // Don't include invalid products do_action( 'woocommerce_gla_debug_message', sprintf( 'Merchant Center product ID %s not found in this WooCommerce store.', $issue['product_id'] ), __METHOD__, ); continue; } } return $this->prepare_item_for_response( $results, $request ); }; } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'issues' => [ 'type' => 'array', 'description' => __( 'The issues related to the Merchant Center account.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'type' => [ 'type' => 'string', 'description' => __( 'Issue type.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'product' => [ 'type' => 'string', 'description' => __( 'Affected product.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'product_id' => [ 'type' => 'numeric', 'description' => __( 'The WooCommerce product ID.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'code' => [ 'type' => 'string', 'description' => __( 'Internal Google code for issue.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'issue' => [ 'type' => 'string', 'description' => __( 'Descriptive text of the issue.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'action' => [ 'type' => 'string', 'description' => __( 'Descriptive text of action to take.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'action_url' => [ 'type' => 'string', 'description' => __( 'Documentation URL for issue and/or action.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'severity' => [ 'type' => 'string', 'description' => __( 'Severity level of the issue: warning or error.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'applicable_countries' => [ 'type' => 'array', 'description' => __( 'Country codes of the product audience.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ], ], ], 'total' => [ 'type' => 'numeric', 'context' => [ 'view' ], 'readonly' => true, ], 'page' => [ 'type' => 'numeric', 'context' => [ 'view' ], 'readonly' => true, ], 'loading' => [ 'type' => 'boolean', 'description' => __( 'Whether the product issues are loading.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), 'page' => [ 'description' => __( 'Page of data to retrieve.', 'google-listings-and-ads' ), 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], 'per_page' => [ 'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ), 'type' => 'integer', 'default' => 0, 'minimum' => 0, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'merchant_issues'; } } PK!j))Gsrc/API/Site/Controllers/MerchantCenter/PhoneVerificationController.phpnu[phone_verification = $phone_verification; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $verification_method = [ 'description' => __( 'Method used to verify the phone number.', 'google-listings-and-ads' ), 'enum' => [ PhoneVerification::VERIFICATION_METHOD_SMS, PhoneVerification::VERIFICATION_METHOD_PHONE_CALL, ], 'required' => true, 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ]; $this->register_route( '/mc/phone-verification/request', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_request_phone_verification_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => [ 'phone_region_code' => [ 'description' => __( 'Two-letter country code (ISO 3166-1 alpha-2) for the phone number.', 'google-listings-and-ads' ), 'required' => true, 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'phone_number' => [ 'description' => __( 'The phone number to verify.', 'google-listings-and-ads' ), 'required' => true, 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'verification_method' => $verification_method, ], ], ] ); $this->register_route( '/mc/phone-verification/verify', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_verify_phone_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => [ 'verification_id' => [ 'description' => __( 'The verification ID returned by the /request call.', 'google-listings-and-ads' ), 'required' => true, 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'verification_code' => [ 'description' => __( 'The verification code that was sent to the phone number for validation.', 'google-listings-and-ads' ), 'required' => true, 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'verification_method' => $verification_method, ], ], ] ); } /** * Get callback for requesting phone verification endpoint. * * @return callable */ protected function get_request_phone_verification_callback(): callable { return function ( Request $request ) { try { $verification_id = $this->phone_verification->request_phone_verification( $request->get_param( 'phone_region_code' ), new PhoneNumber( $request->get_param( 'phone_number' ) ), $request->get_param( 'verification_method' ), ); return [ 'verification_id' => $verification_id, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get callback for verifying a phone number. * * @return callable */ protected function get_verify_phone_callback(): callable { return function ( Request $request ) { try { $this->phone_verification->verify_phone_number( $request->get_param( 'verification_id' ), $request->get_param( 'verification_code' ), $request->get_param( 'verification_method' ), ); return new Response( null, 204 ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema name for the controller. * * @return string */ protected function get_schema_title(): string { return 'phone_verification'; } } PK!3ɨDDKsrc/API/Site/Controllers/MerchantCenter/PolicyComplianceCheckController.phpnu[policy_compliance_check = $policy_compliance_check; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/policy_check', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_policy_check_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the allowed countries, payment gateways info, store ssl and refund return policy page for the controller. * * @return callable */ protected function get_policy_check_callback(): callable { return function () { try { return new Response( [ 'allowed_countries' => $this->policy_compliance_check->is_accessible(), 'robots_restriction' => $this->policy_compliance_check->has_restriction(), 'page_not_found_error' => $this->policy_compliance_check->has_page_not_found_error(), 'page_redirects' => $this->policy_compliance_check->has_redirects(), 'payment_gateways' => $this->policy_compliance_check->has_payment_gateways(), 'store_ssl' => $this->policy_compliance_check->get_is_store_ssl(), 'refund_returns' => $this->policy_compliance_check->has_refund_return_policy_page(), ] ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the schema for policy compliance check endpoints. * * @return array */ protected function get_schema_properties(): array { return [ 'allowed_countries' => [ 'type' => 'boolean', 'description' => __( 'The store website could be accessed or not by all users.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'robots_restriction' => [ 'type' => 'boolean', 'description' => __( 'The merchant set the restrictions in robots.txt or not in the store.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'page_not_found_error' => [ 'type' => 'boolean', 'description' => __( 'The sample of product landing pages leads to a 404 error.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'page_redirects' => [ 'type' => 'boolean', 'description' => __( 'The sample of product landing pages have redirects through 3P domains.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'payment_gateways' => [ 'type' => 'boolean', 'description' => __( 'The payment gateways associated with onboarding policy checking.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'store_ssl' => [ 'type' => 'boolean', 'description' => __( 'The store ssl associated with onboarding policy checking.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'refund_returns' => [ 'type' => 'boolean', 'description' => __( 'The refund returns policy associated with onboarding policy checking.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'schema' => $this->get_api_response_schema_callback(), ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'policy_check'; } } PK!F\=Asrc/API/Site/Controllers/MerchantCenter/ProductFeedController.phpnu[query_helper = $query_helper; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/product-feed', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_product_feed_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Get the callback function for returning the product feed. * * @return callable */ protected function get_product_feed_read_callback(): callable { return function ( Request $request ) { try { return [ 'products' => $this->query_helper->get( $request ), 'total' => $this->query_helper->count( $request ), 'page' => $request['per_page'] > 0 && $request['page'] > 0 ? $request['page'] : 1, ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'products' => [ 'type' => 'array', 'description' => __( 'The store\'s products.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'numeric', 'description' => __( 'Product ID.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'title' => [ 'type' => 'string', 'description' => __( 'Product title.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'visible' => [ 'type' => 'boolean', 'description' => __( 'Whether the product is set to be visible in the Merchant Center', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'status' => [ 'type' => 'string', 'description' => __( 'The current sync status of the product.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'image_url' => [ 'type' => 'string', 'description' => __( 'The image url of the product.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'price' => [ 'type' => 'string', 'description' => __( 'The price of the product.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'errors' => [ 'type' => 'array', 'description' => __( 'Errors preventing the product from being synced to the Merchant Center.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ], ], ], 'total' => [ 'type' => 'numeric', 'context' => [ 'view' ], 'readonly' => true, ], 'page' => [ 'type' => 'numeric', 'context' => [ 'view' ], 'readonly' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'product_feed'; } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), 'page' => [ 'description' => __( 'Page of data to retrieve.', 'google-listings-and-ads' ), 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], 'per_page' => [ 'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ), 'type' => 'integer', 'default' => 0, 'minimum' => 0, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], 'search' => [ 'description' => __( 'Text to search for in product names.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'ids' => [ 'description' => __( 'Limit result to items with specified ids (comma-separated).', 'google-listings-and-ads' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_list', 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'integer', ], ], 'orderby' => [ 'description' => __( 'Sort collection by attribute.', 'google-listings-and-ads' ), 'type' => 'string', 'default' => 'title', 'enum' => [ 'title', 'id', 'visible', 'status' ], 'validate_callback' => 'rest_validate_request_arg', ], 'order' => [ 'description' => __( 'Order sort attribute ascending or descending.', 'google-listings-and-ads' ), 'type' => 'string', 'default' => 'ASC', 'enum' => [ 'ASC', 'DESC' ], 'validate_callback' => 'rest_validate_request_arg', ], ]; } } PK!Z==Gsrc/API/Site/Controllers/MerchantCenter/ProductStatisticsController.phpnu[merchant_statuses = $merchant_statuses; $this->sync_stats = $sync_stats; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/product-statistics', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_product_statistics_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); $this->register_route( 'mc/product-statistics/refresh', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_product_statistics_refresh_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Get the callback function for returning product statistics. * * @return callable */ protected function get_product_statistics_read_callback(): callable { return function ( Request $request ) { return $this->get_product_status_stats( $request ); }; } /** * Get the callback function for getting re-calculated product statistics. * * @return callable */ protected function get_product_statistics_refresh_callback(): callable { return function ( Request $request ) { return $this->get_product_status_stats( $request, true ); }; } /** * Get the overall product status statistics array. * * @param Request $request * @param bool $force_refresh True to force a refresh of the product status statistics. * * @return Response */ protected function get_product_status_stats( Request $request, bool $force_refresh = false ): Response { try { $response = $this->merchant_statuses->get_product_statistics( $force_refresh ); $response['scheduled_sync'] = $this->sync_stats->get_count(); return $this->prepare_item_for_response( $response, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'timestamp' => [ 'type' => 'number', 'description' => __( 'Timestamp reflecting when the product status statistics were last generated.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'statistics' => [ 'type' => 'object', 'description' => __( 'Merchant Center product status statistics.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, 'properties' => [ 'active' => [ 'type' => 'integer', 'description' => __( 'Active products.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'expiring' => [ 'type' => 'integer', 'description' => __( 'Expiring products.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'pending' => [ 'type' => 'number', 'description' => __( 'Pending products.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'disapproved' => [ 'type' => 'number', 'description' => __( 'Disapproved products.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'not_synced' => [ 'type' => 'number', 'description' => __( 'Products not uploaded.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ], ], 'scheduled_sync' => [ 'type' => 'number', 'description' => __( 'Amount of scheduled jobs which will sync products to Google.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'loading' => [ 'type' => 'boolean', 'description' => __( 'Whether the product statistics are loading.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'error' => [ 'type' => 'string', 'description' => __( 'Error message in case of failure', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, 'default' => null, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'product_statistics'; } } PK!'s((Gsrc/API/Site/Controllers/MerchantCenter/ProductVisibilityController.phpnu[product_helper = $product_helper; $this->issue_query = $issue_query; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/product-visibility', [ [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->get_update_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_update_args(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get a callback for updating products' channel visibility. * * @return callable */ protected function get_update_callback(): callable { return function ( Request $request ) { $ids = $request->get_param( 'ids' ); $visible = $request->get_param( 'visible' ); $success = []; $errors = []; foreach ( $ids as $product_id ) { $product_id = intval( $product_id ); if ( ! $this->change_product_visibility( $product_id, $visible ) ) { $errors[] = $product_id; continue; } if ( ! $visible ) { $this->issue_query->delete( 'product_id', $product_id ); } $success[] = $product_id; } sort( $success ); sort( $errors ); return new Response( [ 'success' => $success, 'errors' => $errors, ], count( $errors ) ? 400 : 200 ); }; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'success' => [ 'type' => 'array', 'description' => __( 'Products whose visibility was changed successfully.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'numeric', ], ], 'errors' => [ 'type' => 'array', 'description' => __( 'Products whose visibility was not changed.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'numeric', ], ], ]; } /** * Get the arguments for the update endpoint. * * @return array */ public function get_update_args(): array { return [ 'context' => $this->get_context_param( [ 'default' => 'view' ] ), 'ids' => [ 'description' => __( 'IDs of the products to update.', 'google-listings-and-ads' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_slug_list', 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'integer', ], ], 'visible' => [ 'description' => __( 'New Visibility status for the specified products.', 'google-listings-and-ads' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'product_visibility'; } /** * Update a product's Merchant Center visibility setting (or parent product, for variations). * * @param int $product_id * @param bool $new_visibility True for visible, false for not visible. * * @return bool True if the product was found and updated correctly. */ protected function change_product_visibility( int $product_id, bool $new_visibility ): bool { try { $product = $this->product_helper->get_wc_product( $product_id ); $product = $this->product_helper->maybe_swap_for_parent( $product ); // Use $product->save() instead of ProductMetaHandler to trigger MC sync. $product->update_meta_data( $this->prefix_meta_key( ProductMetaHandler::KEY_VISIBILITY ), $new_visibility ? ChannelVisibility::SYNC_AND_SHOW : ChannelVisibility::DONT_SYNC_AND_SHOW ); $product->save(); return true; } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); return false; } } } PK!IњRR=src/API/Site/Controllers/MerchantCenter/ReportsController.phpnu[register_route( 'mc/reports/programs', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_programs_report_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'mc/reports/products', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_products_report_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_collection_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for the programs report request. * * @return callable */ protected function get_programs_report_callback(): callable { return function ( Request $request ) { try { /** @var MerchantReport $merchant */ $merchant = $this->container->get( MerchantReport::class ); $data = $merchant->get_report_data( 'free_listings', $this->prepare_query_arguments( $request ) ); return $this->prepare_item_for_response( $data, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the products report request. * * @return callable */ protected function get_products_report_callback(): callable { return function ( Request $request ) { try { /** @var MerchantReport $merchant */ $merchant = $this->container->get( MerchantReport::class ); $data = $merchant->get_report_data( 'products', $this->prepare_query_arguments( $request ) ); return $this->prepare_item_for_response( $data, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the query params for collections. * * @return array */ public function get_collection_params(): array { $params = parent::get_collection_params(); $params['interval'] = [ 'description' => __( 'Time interval to use for segments in the returned data.', 'google-listings-and-ads' ), 'type' => 'string', 'enum' => [ 'day', ], 'validate_callback' => 'rest_validate_request_arg', ]; return $params; } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'free_listings' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'subtotals' => $this->get_totals_schema(), ], ], ], 'products' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string', 'description' => __( 'Product ID.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'name' => [ 'type' => 'string', 'description' => __( 'Product name.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], ], 'subtotals' => $this->get_totals_schema(), ], ], ], 'intervals' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'interval' => [ 'type' => 'string', 'description' => __( 'ID of this report segment.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'subtotals' => $this->get_totals_schema(), ], ], ], 'totals' => $this->get_totals_schema(), 'next_page' => [ 'type' => 'string', 'description' => __( 'Token to retrieve the next page of results.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ]; } /** * Return schema for total fields. * * @return array */ protected function get_totals_schema(): array { return [ 'type' => 'object', 'properties' => [ 'clicks' => [ 'type' => 'integer', 'description' => __( 'Clicks.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'impressions' => [ 'type' => 'integer', 'description' => __( 'Impressions.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'reports'; } } PK!6 Y)Y)Csrc/API/Site/Controllers/MerchantCenter/RequestReviewController.phpnu[middleware = $middleware; $this->merchant = $merchant; $this->request_review_statuses = $request_review_statuses; $this->transients = $transients; } /** * Register rest routes with WordPress. */ public function register_routes(): void { /** * GET information regarding the current Account Status */ $this->register_route( 'mc/review', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_review_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); /** * POST a request review for the current account */ $this->register_route( 'mc/review', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->post_review_request_callback(), 'permission_callback' => $this->get_permission_callback(), ], ], ); } /** * Get the callback function for returning the review status. * * @return callable */ protected function get_review_read_callback(): callable { return function ( Request $request ) { try { return $this->prepare_item_for_response( $this->get_review_status(), $request ); } catch ( Exception $e ) { return new Response( [ 'message' => $e->getMessage() ], $e->getCode() ?: 400 ); } }; } /** * Get the callback function after requesting a review. * * @return callable */ protected function post_review_request_callback(): callable { return function () { try { // getting the current account status $account_review_status = $this->get_review_status(); // Abort if it's in cool down period if ( $account_review_status['cooldown'] ) { do_action( 'woocommerce_gla_request_review_failure', [ 'error' => 'cooldown', 'account_review_status' => $account_review_status, ] ); throw new Exception( __( 'Your account is under cool down period and cannot request a new review.', 'google-listings-and-ads' ), 400 ); } // Abort if there is no eligible region available if ( ! count( $account_review_status['reviewEligibleRegions'] ) ) { do_action( 'woocommerce_gla_request_review_failure', [ 'error' => 'ineligible', 'account_review_status' => $account_review_status, ] ); throw new Exception( __( 'Your account is not eligible for a new request review.', 'google-listings-and-ads' ), 400 ); } $this->account_request_review( $account_review_status['reviewEligibleRegions'] ); return $this->set_under_review_status(); } catch ( Exception $e ) { /** * Catch potential errors in any specific region API call. * * Notice due some inconsistencies with Google API we are not considering [Bad Request -> ...already under review...] * as an exception. This is because we suspect that calling the API of a region is triggering other regions requests as well. * This makes all the calls after the first to fail as they will be under review. * * The undesired call of this function for accounts under review is already prevented in a previous stage, so, there is no danger doing this. */ if ( strpos( $e->getMessage(), 'under review' ) !== false ) { return $this->set_under_review_status(); } return new Response( [ 'message' => $e->getMessage() ], $e->getCode() ?: 400 ); } }; } /** * Set Under review Status in the cache and return the response * * @return Response With the Under review status */ private function set_under_review_status() { $new_status = [ 'issues' => [], 'cooldown' => 0, 'status' => $this->request_review_statuses::UNDER_REVIEW, 'reviewEligibleRegions' => [], ]; // Update Account status when successful response $this->set_cached_review_status( $new_status ); return new Response( $new_status ); } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'status' => [ 'type' => 'string', 'description' => __( 'The status of the last review.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'cooldown' => [ 'type' => 'integer', 'description' => __( 'Timestamp indicating if the user is in cool down period.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'issues' => [ 'type' => 'array', 'description' => __( 'The issues related to the Merchant Center to be reviewed and addressed before approval.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, 'items' => [ 'type' => 'string', ], ], 'reviewEligibleRegions' => [ 'type' => 'array', 'description' => __( 'The region codes in which is allowed to request a new review.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, 'items' => [ 'type' => 'string', ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'merchant_account_review'; } /** * Save the Account Review Status data inside a transient for caching purposes. * * @param array $value The Account Review Status data to save in the transient */ private function set_cached_review_status( $value ): void { $this->transients->set( TransientsInterface::MC_ACCOUNT_REVIEW, $value, $this->request_review_statuses->get_account_review_lifetime() ); } /** * Get the Account Review Status data inside a transient for caching purposes. * * @return null|array Returns NULL in case no data is available or an array with the Account Review Status data otherwise. */ private function get_cached_review_status(): ?array { return $this->transients->get( TransientsInterface::MC_ACCOUNT_REVIEW, ); } /** * Get the Account Review Status. We attempt to get the cached version or create a request otherwise. * * @return null|array Returns NULL in case no data is available or an array with the Account Review Status data otherwise. * @throws Exception If the get_account_review_status API call fails. */ private function get_review_status(): ?array { $review_status = $this->get_cached_review_status(); if ( is_null( $review_status ) ) { $response = $this->get_account_review_status(); $review_status = $this->request_review_statuses->get_statuses_from_response( $response ); $this->set_cached_review_status( $review_status ); } return $review_status; } /** * Get Account Review Status * * @return array the response data * @throws Exception When there is an invalid response. */ public function get_account_review_status() { try { if ( ! $this->middleware->is_subaccount() ) { return []; } $response = $this->merchant->get_account_review_status(); do_action( 'woocommerce_gla_request_review_response', $response ); return $response; } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); throw new Exception( $e->getMessage() ?? __( 'Error getting account review status.', 'google-listings-and-ads' ), $e->getCode() ); } } /** * Request a new account review * * @param array $regions Regions to request a review. * @return array With a successful message * @throws Exception When there is an invalid response. */ public function account_request_review( $regions ) { try { // For each region we request a new review foreach ( $regions as $region_code => $region_types ) { $result = $this->merchant->account_request_review( $region_code, $region_types ); if ( 200 !== $result->getStatusCode() ) { do_action( 'woocommerce_gla_request_review_failure', [ 'error' => 'response', 'region_code' => $region_code, 'response' => $result, ] ); do_action( 'woocommerce_gla_guzzle_invalid_response', $result, __METHOD__ ); $error = $response['message'] ?? __( 'Invalid response getting requesting a new review.', 'google-listings-and-ads' ); throw new Exception( $error, $result->getStatusCode() ); } } // Otherwise, return a successful message and update the account status return [ 'message' => __( 'A new review has been successfully requested', 'google-listings-and-ads' ), ]; } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); throw new Exception( $e->getMessage() ?? __( 'Error requesting a new review.', 'google-listings-and-ads' ), $e->getCode() ); } } } PK!Sx >src/API/Site/Controllers/MerchantCenter/SettingsController.phpnu[shipping_zone = $shipping_zone; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/settings', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_settings_endpoint_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->get_settings_endpoint_edit_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get a callback for the settings endpoint. * * @return callable */ protected function get_settings_endpoint_read_callback(): callable { return function () { $data = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] ); $data['shipping_rates_count'] = $this->shipping_zone->get_shipping_rates_count(); $schema = $this->get_schema_properties(); $items = []; foreach ( $schema as $key => $property ) { $items[ $key ] = $data[ $key ] ?? $property['default'] ?? null; } return $items; }; } /** * Get a callback for editing the settings endpoint. * * @return callable */ protected function get_settings_endpoint_edit_callback(): callable { return function ( Request $request ) { $schema = $this->get_schema_properties(); $options = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] ); if ( ! is_array( $options ) ) { $options = []; } foreach ( $schema as $key => $property ) { if ( ! in_array( 'edit', $property['context'] ?? [], true ) ) { continue; } $options[ $key ] = $request->get_param( $key ) ?? $options[ $key ] ?? $property['default'] ?? null; } $this->options->update( OptionsInterface::MERCHANT_CENTER, $options ); return [ 'status' => 'success', 'message' => __( 'Merchant Center Settings successfully updated.', 'google-listings-and-ads' ), 'data' => $options, ]; }; } /** * Get the schema for settings endpoints. * * @return array */ protected function get_schema_properties(): array { return [ 'shipping_rate' => [ 'type' => 'string', 'description' => __( 'Whether shipping rate is a simple flat rate or needs to be configured manually in the Merchant Center.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'enum' => [ 'automatic', 'flat', 'manual', ], ], 'shipping_time' => [ 'type' => 'string', 'description' => __( 'Whether shipping time is a simple flat time or needs to be configured manually in the Merchant Center.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'enum' => [ 'flat', 'manual', ], ], 'tax_rate' => [ 'type' => 'string', 'description' => __( 'Whether tax rate is destination based or need to be configured manually in the Merchant Center.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'enum' => [ 'destination', 'manual', ], 'default' => 'destination', ], 'shipping_rates_count' => [ 'type' => 'number', 'description' => __( 'The number of shipping rates in WC ready to be used in the Merchant Center.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'validate_callback' => 'rest_validate_request_arg', 'default' => 0, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'merchant_center_settings'; } } PK!1O33Bsrc/API/Site/Controllers/MerchantCenter/SettingsSyncController.phpnu[settings = $settings; } /** * Registers the routes for the objects of the controller. */ public function register_routes() { $this->register_route( 'mc/settings/sync', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_sync_endpoint_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback for syncing shipping. * * @return callable */ protected function get_sync_endpoint_callback(): callable { return function ( Request $request ) { try { $this->settings->sync_taxes(); $this->settings->sync_shipping(); do_action( 'woocommerce_gla_mc_settings_sync' ); /** * MerchantCenter onboarding has been successfully completed. * * @event gla_mc_setup_completed * @property string shipping_rate Shipping rate setup `automatic`, `manual`, `flat`. * @property bool offers_free_shipping Free Shipping is available. * @property float free_shipping_threshold Minimum amount to avail of free shipping. * @property string shipping_time Shipping time setup `flat`, `manual`. * @property string tax_rate Tax rate setup `destination`, `manual`. * @property string target_countries List of target countries or `all`. */ do_action( 'woocommerce_gla_track_event', 'mc_setup_completed', $this->settings->get_settings_for_tracking() ); return new Response( [ 'status' => 'success', 'message' => __( 'Successfully synchronized settings with Google.', 'google-listings-and-ads' ), ], 201 ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); try { $decoded = $this->json_decode_message( $e->getMessage() ); $data = [ 'status' => $decoded['code'] ?? 500, 'message' => $decoded['message'] ?? '', 'data' => $decoded, ]; } catch ( Exception $e2 ) { $data = [ 'status' => 500, ]; } return $this->error_from_exception( $e, 'gla_setting_sync_error', $data ); } }; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'settings_sync'; } } PK!\-WWGsrc/API/Site/Controllers/MerchantCenter/ShippingRateBatchController.phpnu[register_route( "{$this->route_base}/batch", [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_batch_create_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_batch_create_args_schema(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_batch_delete_shipping_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_batch_delete_args_schema(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback for creating items via batch. * * @return callable */ protected function get_batch_create_callback(): callable { return function ( Request $request ) { $rates = $request->get_param( 'rates' ); $responses = []; $errors = []; foreach ( $rates as $rate ) { $new_request = new Request( 'POST', "/{$this->get_namespace()}/{$this->route_base}" ); $new_request->set_body_params( $rate ); $response = $this->server->dispatch_request( $new_request ); if ( 201 !== $response->get_status() ) { $errors[] = $response->get_data(); } else { $responses[] = $response->get_data(); } } return new Response( [ 'errors' => $errors, 'success' => $responses, ], 201 ); }; } /** * Get the callback for deleting shipping items via batch. * * @return callable * * @since 1.12.0 */ protected function get_batch_delete_shipping_callback(): callable { return function ( Request $request ) { $ids = $request->get_param( 'ids' ); $responses = []; $errors = []; foreach ( $ids as $id ) { $route = "/{$this->get_namespace()}/{$this->route_base}/{$id}"; $delete_request = new Request( 'DELETE', $route ); $response = $this->server->dispatch_request( $delete_request ); if ( 200 !== $response->get_status() ) { $errors[] = $response->get_data(); } else { $responses[] = $response->get_data(); } } return new Response( [ 'errors' => $errors, 'success' => $responses, ], ); }; } /** * Get the argument schema for a batch create request. * * @return array * * @since 1.12.0 */ protected function get_batch_create_args_schema(): array { return [ 'rates' => [ 'type' => 'array', 'minItems' => 1, 'uniqueItems' => true, 'description' => __( 'Array of shipping rates to create.', 'google-listings-and-ads' ), 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'object', 'additionalProperties' => false, 'properties' => $this->get_schema_properties(), ], ], ]; } /** * Get the argument schema for a batch delete request. * * @return array * * @since 1.12.0 */ protected function get_batch_delete_args_schema(): array { return [ 'ids' => [ 'type' => 'array', 'description' => __( 'Array of unique shipping rate identification numbers.', 'google-listings-and-ads' ), 'context' => [ 'edit' ], 'minItems' => 1, 'required' => true, 'uniqueItems' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'batch_shipping_rates'; } } PK!u[ [ Bsrc/API/Site/Controllers/MerchantCenter/ShippingRateController.phpnu[query = $query; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( $this->route_base, [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_read_all_rates_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_create_rate_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( "{$this->route_base}/(?P[\d]+)", [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_read_rate_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => [ 'id' => $this->get_schema_properties()['id'] ], ], [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->get_update_rate_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_delete_rate_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => [ 'id' => $this->get_schema_properties()['id'] ], ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for returning the endpoint results. * * @return callable */ protected function get_read_all_rates_callback(): callable { return function ( Request $request ) { $rates = $this->get_all_shipping_rates(); return array_map( function ( $rate ) use ( $request ) { $response = $this->prepare_item_for_response( $rate, $request ); return $this->prepare_response_for_collection( $response ); }, $rates ); }; } /** * @return callable */ protected function get_read_rate_callback(): callable { return function ( Request $request ) { $id = (string) $request->get_param( 'id' ); $rate = $this->get_shipping_rate_by_id( $id ); if ( empty( $rate ) ) { return new Response( [ 'message' => __( 'No rate available.', 'google-listings-and-ads' ), 'id' => $id, ], 404 ); } return $this->prepare_item_for_response( $rate, $request ); }; } /** * @return callable * * @since 1.12.0 */ protected function get_update_rate_callback(): callable { return function ( Request $request ) { $id = (string) $request->get_param( 'id' ); $rate = $this->get_shipping_rate_by_id( $id ); if ( empty( $rate ) ) { return new Response( [ 'message' => __( 'No rate found with the given ID.', 'google-listings-and-ads' ), 'id' => $id, ], 404 ); } $data = $this->prepare_item_for_database( $request ); $this->create_query()->update( $data, [ 'id' => $id, ] ); return new Response( '', 204 ); }; } /** * Get the callback function for creating a new shipping rate. * * @return callable */ protected function get_create_rate_callback(): callable { return function ( Request $request ) { $shipping_rate_query = $this->create_query(); try { $data = $this->prepare_item_for_database( $request ); $country = $data['country']; $existing_query = $this->create_query()->where( 'country', $country ); $existing = ! empty( $existing_query->get_results() ); if ( $existing ) { $rate_id = $existing_query->get_results()[0]['id']; $shipping_rate_query->update( $data, [ 'id' => $rate_id ] ); } else { $shipping_rate_query->insert( $data ); $rate_id = $shipping_rate_query->last_insert_id(); } } catch ( InvalidQuery $e ) { return $this->error_from_exception( $e, 'gla_error_creating_shipping_rate', [ 'code' => 400, 'message' => $e->getMessage(), ] ); } // Fetch updated/inserted rate to return in response. $rate_response = $this->prepare_item_for_response( $this->get_shipping_rate_by_id( (string) $rate_id ), $request ); return new Response( [ 'status' => 'success', 'message' => sprintf( /* translators: %s is the country code in ISO 3166-1 alpha-2 format. */ __( 'Successfully added rate for country: "%s".', 'google-listings-and-ads' ), $country ), 'rate' => $rate_response->get_data(), ], 201 ); }; } /** * @return callable */ protected function get_delete_rate_callback(): callable { return function ( Request $request ) { try { $id = (string) $request->get_param( 'id' ); $rate = $this->get_shipping_rate_by_id( $id ); if ( empty( $rate ) ) { return new Response( [ 'message' => __( 'No rate found with the given ID.', 'google-listings-and-ads' ), 'id' => $id, ], 404 ); } $this->create_query()->delete( 'id', $id ); return [ 'status' => 'success', 'message' => __( 'Successfully deleted rate.', 'google-listings-and-ads' ), ]; } catch ( InvalidQuery $e ) { return $this->error_from_exception( $e, 'gla_error_deleting_shipping_rate', [ 'code' => 400, 'message' => $e->getMessage(), ] ); } }; } /** * Returns the list of all shipping rates stored in the database grouped by their respective country code. * * @return array Array of shipping rates grouped by country code. */ protected function get_all_shipping_rates(): array { return $this->create_query() ->set_order( 'country', 'ASC' ) ->get_results(); } /** * @param string $id * * @return array|null The shipping rate properties as an array or null if it doesn't exist. */ protected function get_shipping_rate_by_id( string $id ): ?array { $results = $this->create_query()->where( 'id', $id )->get_results(); return ! empty( $results ) ? $results[0] : null; } /** * Return a new instance of the shipping rate query object. * * @return ShippingRateQuery */ protected function create_query(): ShippingRateQuery { return clone $this->query; } /** * @return array */ protected function get_schema_properties(): array { return $this->get_shipping_rate_schema(); } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'shipping_rates'; } } PK!R%/Msrc/API/Site/Controllers/MerchantCenter/ShippingRateSuggestionsController.phpnu[shipping_suggestion = $shipping_suggestion; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( "{$this->route_base}", [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_suggestions_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => [ 'country_codes' => [ 'type' => 'array', 'description' => __( 'Array of country codes in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'edit' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_country_code_validate_callback(), 'minItems' => 1, 'required' => true, 'uniqueItems' => true, 'items' => [ 'type' => 'string', ], ], ], ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for returning the endpoint results. * * @return callable */ protected function get_suggestions_callback(): callable { return function ( Request $request ) { $country_codes = $request->get_param( 'country_codes' ); $rates_output = []; foreach ( $country_codes as $country_code ) { $suggestions = $this->shipping_suggestion->get_suggestions( $country_code ); // Prepare the output. $suggestions = array_map( function ( $suggestion ) use ( $request ) { $response = $this->prepare_item_for_response( $suggestion, $request ); return $this->prepare_response_for_collection( $response ); }, $suggestions ); // Merge the suggestions for all countries into one array. $rates_output = array_merge( $rates_output, $suggestions ); } return $rates_output; }; } /** * @return array */ protected function get_schema_properties(): array { $schema = $this->get_shipping_rate_schema(); // Suggested shipping rates don't have an id. unset( $schema['id'] ); // All properties are read-only. return array_map( function ( $property ) { $property['readonly'] = true; $property['context'] = [ 'view' ]; return $property; }, $schema ); } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'shipping_rates_suggestions'; } } PK!xZ Z Gsrc/API/Site/Controllers/MerchantCenter/ShippingTimeBatchController.phpnu[register_route( "{$this->route_base}/batch", [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_batch_create_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_item_schema(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_batch_delete_shipping_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_item_delete_schema(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback for creating items via batch. * * @return callable */ protected function get_batch_create_callback(): callable { return function ( Request $request ) { $country_codes = $request->get_param( 'country_codes' ); $time = $request->get_param( 'time' ); $max_time = $request->get_param( 'max_time' ); $responses = []; $errors = []; foreach ( $country_codes as $country_code ) { $new_request = new Request( 'POST', "/{$this->get_namespace()}/{$this->route_base}" ); $new_request->set_body_params( [ 'country_code' => $country_code, 'time' => $time, 'max_time' => $max_time, ] ); $response = $this->server->dispatch_request( $new_request ); if ( 201 !== $response->get_status() ) { $errors[] = $response->get_data(); } else { $responses[] = $response->get_data(); } } return new Response( [ 'errors' => $errors, 'success' => $responses, ], 201 ); }; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'batch_shipping_times'; } } PK!$ _'_'Bsrc/API/Site/Controllers/MerchantCenter/ShippingTimeController.phpnu[register_route( $this->route_base, [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_read_times_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_create_time_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_args_schema(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( "{$this->route_base}/(?P\\w{2})", [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_read_time_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_delete_time_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for reading times. * * @return callable */ protected function get_read_times_callback(): callable { return function ( Request $request ) { $times = $this->get_all_shipping_times(); $items = []; foreach ( $times as $time ) { $data = $this->prepare_item_for_response( [ 'country_code' => $time['country'], 'time' => $time['time'], 'max_time' => $time['max_time'], ], $request ); $items[ $time['country'] ] = $this->prepare_response_for_collection( $data ); } return $items; }; } /** * Get the callback function for reading a single time. * * @return callable */ protected function get_read_time_callback(): callable { return function ( Request $request ) { $country = $request->get_param( 'country_code' ); $time = $this->get_shipping_time_for_country( $country ); if ( empty( $time ) ) { return new Response( [ 'message' => __( 'No time available.', 'google-listings-and-ads' ), 'country' => $country, ], 404 ); } return $this->prepare_item_for_response( [ 'country_code' => $time[0]['country'], 'time' => $time[0]['time'], 'max_time' => $time[0]['max_time'], ], $request ); }; } /** * Get the callback to crate a new time. * * @return callable */ protected function get_create_time_callback(): callable { return function ( Request $request ) { $query = $this->get_query_object(); $country_code = $request->get_param( 'country_code' ); $existing = ! empty( $query->where( 'country', $country_code )->get_results() ); try { $data = [ 'country' => $country_code, 'time' => $request->get_param( 'time' ), 'max_time' => $request->get_param( 'max_time' ), ]; if ( $existing ) { $query->update( $data, [ 'id' => $query->get_results()[0]['id'], ] ); } else { $query->insert( $data ); } return new Response( [ 'status' => 'success', 'message' => sprintf( /* translators: %s is the country code in ISO 3166-1 alpha-2 format. */ __( 'Successfully added time for country: "%s".', 'google-listings-and-ads' ), $country_code ), ], 201 ); } catch ( InvalidQuery $e ) { return $this->error_from_exception( $e, 'gla_error_creating_shipping_time', [ 'code' => 400, 'message' => $e->getMessage(), ] ); } }; } /** * Get the callback function for deleting a time. * * @return callable */ protected function get_delete_time_callback(): callable { return function ( Request $request ) { try { $country_code = $request->get_param( 'country_code' ); $this->get_query_object()->delete( 'country', $country_code ); return [ 'status' => 'success', 'message' => sprintf( /* translators: %s is the country code in ISO 3166-1 alpha-2 format. */ __( 'Successfully deleted the time for country: "%s".', 'google-listings-and-ads' ), $country_code ), ]; } catch ( InvalidQuery $e ) { return $this->error_from_exception( $e, 'gla_error_deleting_shipping_time', [ 'code' => 400, 'message' => $e->getMessage(), ] ); } }; } /** * @return array */ protected function get_all_shipping_times(): array { return $this->get_query_object()->set_limit( 100 )->get_results(); } /** * @param string $country * * @return array */ protected function get_shipping_time_for_country( string $country ): array { return $this->get_query_object()->where( 'country', $country )->get_results(); } /** * Get the shipping time query object. * * @return ShippingTimeQuery */ protected function get_query_object(): ShippingTimeQuery { return $this->container->get( ShippingTimeQuery::class ); } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'country_code' => [ 'type' => 'string', 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_country_code_validate_callback(), 'required' => true, ], 'time' => [ 'type' => 'integer', 'description' => __( 'The minimum shipping time in days.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => [ $this, 'validate_shipping_times' ], ], 'max_time' => [ 'type' => 'integer', 'description' => __( 'The maximum shipping time in days.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => [ $this, 'validate_shipping_times' ], ], ]; } /** * Get the args schema for the controller. * * @return array */ protected function get_args_schema(): array { $schema = $this->get_schema_properties(); $schema['time']['required'] = true; $schema['max_time']['required'] = true; return $schema; } /** * Validate the shipping times. * * @param mixed $value * @param Request $request * @param string $param * * @return WP_Error|true */ public function validate_shipping_times( $value, $request, $param ) { $time = $request->get_param( 'time' ); $max_time = $request->get_param( 'max_time' ); if ( rest_is_integer( $value ) === false ) { return new WP_Error( 'rest_invalid_type', /* translators: 1: Parameter, 2: Type name. */ sprintf( __( '%1$s is not of type %2$s.', 'google-listings-and-ads' ), $param, 'integer' ), [ 'param' => $param ] ); } if ( $value < 0 ) { return new WP_Error( 'invalid_shipping_times', __( 'Shipping times cannot be negative.', 'google-listings-and-ads' ), [ 'param' => $param ] ); } if ( $time > $max_time ) { return new WP_Error( 'invalid_shipping_times', __( 'The minimum shipping time cannot be greater than the maximum shipping time.', 'google-listings-and-ads' ), [ 'param' => $param ] ); } return true; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'shipping_times'; } /** * Retrieves all of the registered additional fields for a given object-type. * * @param string $object_type Optional. The object type. * * @return array Registered additional fields (if any), empty array if none or if the object type could * not be inferred. */ protected function get_additional_fields( $object_type = null ): array { $fields = parent::get_additional_fields( $object_type ); $fields['country'] = [ 'schema' => [ 'type' => 'string', 'description' => __( 'Country in which the shipping time applies.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'get_callback' => function ( $fields ) { return $this->iso3166_data_provider->alpha2( $fields['country_code'] )['name']; }, ]; return $fields; } } PK!4SAAHsrc/API/Site/Controllers/MerchantCenter/SupportedCountriesController.phpnu[wc = $wc; $this->google_helper = $google_helper; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/countries', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_countries_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_query_args(), ], ] ); } /** * Get the callback function for returning supported countries. * * @return callable */ protected function get_countries_callback(): callable { return function ( Request $request ) { $return = [ 'countries' => $this->get_supported_countries( $request ), ]; if ( $request->get_param( 'continents' ) ) { $return['continents'] = $this->get_supported_continents(); } return $return; }; } /** * Get the array of supported countries. * * @return array */ protected function get_supported_countries(): array { $all_countries = $this->wc->get_countries(); $mc_countries = $this->google_helper->get_mc_supported_countries_currencies(); $supported = []; foreach ( $mc_countries as $country => $currency ) { if ( ! array_key_exists( $country, $all_countries ) ) { continue; } $supported[ $country ] = [ 'name' => $all_countries[ $country ], 'currency' => $currency, ]; } uasort( $supported, function ( $a, $b ) { return $a['name'] <=> $b['name']; } ); return $supported; } /** * Get the array of supported continents. * * @return array */ protected function get_supported_continents(): array { $all_continents = $this->wc->get_continents(); foreach ( $all_continents as $continent_code => $continent ) { $supported_countries_of_continent = $this->google_helper->get_supported_countries_from_continent( $continent_code ); if ( empty( $supported_countries_of_continent ) ) { unset( $all_continents[ $continent_code ] ); } else { $all_continents[ $continent_code ]['countries'] = array_values( $supported_countries_of_continent ); } } return $all_continents; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'supported_countries'; } /** * Get the arguments for the query endpoint. * * @return array */ protected function get_query_args(): array { return [ 'continents' => [ 'description' => __( 'Include continents data if set to true.', 'google-listings-and-ads' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', ], ]; } } PK!fKsrc/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.phpnu[job_repository = $job_repository; } /** * Registers the routes for the objects of the controller. */ public function register_routes() { $this->register_route( 'mc/syncable-products-count', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_syncable_products_count_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->update_syncable_products_count_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback function for marking setup complete. * * @return callable */ protected function get_syncable_products_count_callback(): callable { return function ( Request $request ) { $response = [ 'count' => null, ]; $count = $this->options->get( OptionsInterface::SYNCABLE_PRODUCTS_COUNT ); if ( isset( $count ) ) { $response['count'] = (int) $count; } return $this->prepare_item_for_response( $response, $request ); }; } /** * Get the callback for syncing shipping. * * @return callable */ protected function update_syncable_products_count_callback(): callable { return function ( Request $request ) { $this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT ); $this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA ); $job = $this->job_repository->get( UpdateSyncableProductsCount::class ); $job->schedule(); return new Response( [ 'status' => 'success', 'message' => __( 'Successfully scheduled a job to update the number of syncable products.', 'google-listings-and-ads' ), ], 201 ); }; } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'count' => [ 'type' => 'number', 'description' => __( 'The number of products that are ready to be synced to Google.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'syncable_products_count'; } } PK! PR Dsrc/API/Site/Controllers/MerchantCenter/TargetAudienceController.phpnu[wp = $wp; $this->wc = $wc; $this->shipping_zone = $shipping_zone; $this->google_helper = $google_helper; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'mc/target_audience', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_read_audience_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_update_audience_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); $this->register_route( 'mc/target_audience/suggestions', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_suggest_audience_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for reading the target audience data. * * @return callable */ protected function get_read_audience_callback(): callable { return function ( Request $request ) { return $this->prepare_item_for_response( $this->get_target_audience_option(), $request ); }; } /** * Get the callback function for suggesting the target audience data. * * @return callable * * @since 1.9.0 */ protected function get_suggest_audience_callback(): callable { return function ( Request $request ) { return $this->prepare_item_for_response( $this->get_target_audience_suggestion(), $request ); }; } /** * Get the callback function for updating the target audience data. * * @return callable */ protected function get_update_audience_callback(): callable { return function ( Request $request ) { $data = $this->prepare_item_for_database( $request ); $this->update_target_audience_option( $data ); $this->prepare_item_for_response( $data, $request ); return new Response( [ 'status' => 'success', 'message' => __( 'Successfully updated the Target Audience settings.', 'google-listings-and-ads' ), ], 201 ); }; } /** * Retrieves all of the registered additional fields for a given object-type. * * @param string $object_type Optional. The object type. * * @return array Registered additional fields (if any), empty array if none or if the object type could * not be inferred. */ protected function get_additional_fields( $object_type = null ): array { $fields = parent::get_additional_fields( $object_type ); // Fields are expected to be an array with a 'get_callback' callable that returns the field value. $fields['locale'] = [ 'schema' => [ 'type' => 'string', 'description' => __( 'The locale for the site.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'get_callback' => function () { return $this->wp->get_locale(); }, ]; $fields['language'] = [ 'schema' => [ 'type' => 'string', 'description' => __( 'The language to use for product listings.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'get_callback' => $this->get_language_callback(), ]; return $fields; } /** * Get the option data for the target audience. * * @return array */ protected function get_target_audience_option(): array { return $this->options->get( OptionsInterface::TARGET_AUDIENCE, [] ); } /** * Get the suggested values for the target audience option. * * @return string[] * * @since 1.9.0 */ protected function get_target_audience_suggestion(): array { $countries = $this->shipping_zone->get_shipping_countries(); $base_country = $this->wc->get_base_country(); // Add WooCommerce store country if it's supported and not already in the list. if ( ! in_array( $base_country, $countries, true ) && $this->google_helper->is_country_supported( $base_country ) ) { $countries[] = $base_country; } return [ 'location' => 'selected', 'countries' => $countries, ]; } /** * Update the option data for the target audience. * * @param array $data * * @return bool */ protected function update_target_audience_option( array $data ): bool { return $this->options->update( OptionsInterface::TARGET_AUDIENCE, $data ); } /** * Get the item schema for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'location' => [ 'type' => 'string', 'description' => __( 'Location where products will be shown.', 'google-listings-and-ads' ), 'context' => [ 'edit', 'view' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => true, 'enum' => [ 'all', 'selected', ], ], 'countries' => [ 'type' => 'array', 'description' => __( 'Array of country codes in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'edit', 'view' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_country_code_validate_callback(), ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'target_audience'; } /** * Get the callback to provide the language in use for the site. * * @return callable */ protected function get_language_callback(): callable { $locale = $this->wp->get_locale(); // Default to using the Locale class if it is available. if ( class_exists( Locale::class ) ) { return function () use ( $locale ): string { return Locale::getDisplayLanguage( $locale, $locale ); }; } return function () use ( $locale ): string { // en_US isn't provided by the translations API. if ( 'en_US' === $locale ) { return 'English'; } require_once ABSPATH . 'wp-admin/includes/translation-install.php'; return wp_get_available_translations()[ $locale ]['native_name'] ?? $locale; }; } } PK!=i"3src/API/Site/Controllers/RestAPI/AuthController.phpnu[ '/google/setup-mc', 'settings' => '/google/settings', ]; /** * AuthController constructor. * * @param RESTServer $server * @param OAuthService $oauth_service * @param AccountService $account_service */ public function __construct( RESTServer $server, OAuthService $oauth_service, AccountService $account_service ) { parent::__construct( $server ); $this->oauth_service = $oauth_service; $this->account_service = $account_service; } /** * Registers the routes for the objects of the controller. */ public function register_routes() { $this->register_route( 'rest-api/authorize', [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_authorize_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_auth_params(), ], [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->delete_authorize_callback(), 'permission_callback' => $this->get_permission_callback(), ], [ 'methods' => TransportMethods::EDITABLE, 'callback' => $this->get_update_authorize_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_update_authorize_params(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Get the callback function for the authorization request. * * @return callable */ protected function get_authorize_callback(): callable { return function ( Request $request ) { try { $next = $request->get_param( 'next_page_name' ); $path = self::NEXT_PATH_MAPPING[ $next ]; $auth_url = $this->oauth_service->get_auth_url( $path ); $response = [ 'auth_url' => $auth_url, ]; return $this->prepare_item_for_response( $response, $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the delete authorization request. * * @return callable */ protected function delete_authorize_callback(): callable { return function ( Request $request ) { try { $this->oauth_service->revoke_wpcom_api_auth(); return $this->prepare_item_for_response( [], $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the callback function for the update authorize request. * * @return callable */ protected function get_update_authorize_callback(): callable { return function ( Request $request ) { try { $this->account_service->update_wpcom_api_authorization( $request['status'], $request['nonce'] ); return [ 'status' => $request['status'] ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the query params for the authorize request. * * @return array */ protected function get_auth_params(): array { return [ 'next_page_name' => [ 'description' => __( 'Indicates the next page name mapped to the redirect URL when redirected back from Google WPCOM App authorization.', 'google-listings-and-ads' ), 'type' => 'string', 'default' => array_key_first( self::NEXT_PATH_MAPPING ), 'enum' => array_keys( self::NEXT_PATH_MAPPING ), 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Get the query params for the update authorize request. * * @return array */ protected function get_update_authorize_params(): array { return [ 'status' => [ 'description' => __( 'The status of the merchant granting access to Google\'s WPCOM app', 'google-listings-and-ads' ), 'type' => 'string', 'enum' => OAuthService::ALLOWED_STATUSES, 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], 'nonce' => [ 'description' => __( 'The nonce provided by Google in the URL query parameter when Google redirects back to merchant\'s site', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], ]; } /** * Get the item schema properties for the controller. * * @return array */ protected function get_schema_properties(): array { return [ 'auth_url' => [ 'type' => 'string', 'description' => __( 'The authorization URL for granting access to Google WPCOM App.', 'google-listings-and-ads' ), 'context' => [ 'view' ], ], 'status' => [ 'type' => 'string', 'description' => __( 'The status of the merchant granting access to Google\'s WPCOM app', 'google-listings-and-ads' ), 'enum' => OAuthService::ALLOWED_STATUSES, 'context' => [ 'view' ], ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'rest_api_authorize'; } } PK!`j+src/API/Site/Controllers/BaseController.phpnu[server = $server; $this->namespace = $this->get_namespace(); } /** * Register a service. */ public function register(): void { $this->register_routes(); } /** * Register a single route. * * @param string $route The route name. * @param array $args The arguments for the route. */ protected function register_route( string $route, array $args ): void { $this->server->register_route( $this->get_namespace(), $route, $args ); } /** * Get the namespace for the current controller. * * @return string */ protected function get_namespace(): string { return "wc/{$this->get_slug()}"; } /** * Get the callback to determine the route's permissions. * * @return callable */ protected function get_permission_callback(): callable { return function () { return $this->can_manage(); }; } /** * Prepare an item schema for sending to the API. * * @param array $properties Array of raw properties. * @param string $schema_title Schema title. * * @return array */ protected function prepare_item_schema( array $properties, string $schema_title ): array { return $this->add_additional_fields_schema( [ '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => $schema_title, 'type' => 'object', 'additionalProperties' => false, 'properties' => $properties, ] ); } /** * Retrieves the item's schema, conforming to JSON Schema. * * @return array Item schema data. */ public function get_item_schema(): array { return $this->prepare_item_schema( $this->get_schema_properties(), $this->get_schema_title() ); } /** * Get a callback function for returning the API schema. * * @return callable */ protected function get_api_response_schema_callback(): callable { return function () { return $this->get_item_schema(); }; } /** * Get a route name which is safe to use as a filter (removes namespace prefix). * * @param Request $request Request object. * * @return string */ protected function get_route_name( Request $request ): string { $route = trim( $request->get_route(), '/' ); if ( 0 === strpos( $route, $this->get_namespace() ) ) { $route = substr( $route, strlen( $this->get_namespace() ) ); } return sanitize_title( $route ); } /** * Prepares the item for the REST response. * * @param mixed $item WordPress representation of the item. * @param Request $request Request object. * * @return Response Response object on success, or WP_Error object on failure. */ public function prepare_item_for_response( $item, $request ) { $prepared = []; $context = $request['context'] ?? 'view'; $schema = $this->get_schema_properties(); foreach ( $schema as $key => $property ) { $item_value = $item[ $key ] ?? $property['default'] ?? null; // Cast empty arrays to empty objects if property is supposed to be an object. if ( is_array( $item_value ) && empty( $item_value ) && isset( $property['type'] ) && 'object' === $property['type'] ) { $item_value = (object) []; } $prepared[ $key ] = $item_value; } $prepared = $this->add_additional_fields_to_object( $prepared, $request ); $prepared = $this->filter_response_by_context( $prepared, $context ); $prepared = apply_filters( 'woocommerce_gla_prepared_response_' . $this->get_route_name( $request ), $prepared, $request ); return new Response( $prepared ); } /** * Prepares one item for create or update operation. * * @param Request $request Request object. * * @return array The prepared item, or WP_Error object on failure. */ protected function prepare_item_for_database( $request ): array { $prepared = []; $schema = $this->get_schema_properties(); foreach ( $schema as $key => $property ) { if ( $property['readonly'] ?? false ) { continue; } $prepared[ $key ] = $request[ $key ] ?? $property['default'] ?? null; } return $prepared; } /** * Get the item schema properties for the controller. * * @return array */ abstract protected function get_schema_properties(): array; /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ abstract protected function get_schema_title(): string; } PK! 2src/API/Site/Controllers/BaseOptionsController.phpnu[ $this->get_context_param( [ 'default' => 'view' ] ), 'after' => [ 'description' => __( 'Limit response to data after a given ISO8601 compliant date.', 'google-listings-and-ads' ), 'type' => 'string', 'format' => 'date', 'default' => '-7 days', 'validate_callback' => 'rest_validate_request_arg', ], 'before' => [ 'description' => __( 'Limit response to data before a given ISO8601 compliant date.', 'google-listings-and-ads' ), 'type' => 'string', 'format' => 'date', 'default' => 'now', 'validate_callback' => 'rest_validate_request_arg', ], 'ids' => [ 'description' => __( 'Limit result to items with specified ids.', 'google-listings-and-ads' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_slug_list', 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'string', ], ], 'fields' => [ 'description' => __( 'Limit totals to a set of fields.', 'google-listings-and-ads' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_slug_list', 'validate_callback' => 'rest_validate_request_arg', 'items' => [ 'type' => 'string', ], ], 'order' => [ 'description' => __( 'Order sort attribute ascending or descending.', 'google-listings-and-ads' ), 'type' => 'string', 'default' => 'desc', 'enum' => [ 'asc', 'desc' ], 'validate_callback' => 'rest_validate_request_arg', ], 'orderby' => [ 'description' => __( 'Sort collection by attribute.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], 'per_page' => [ 'description' => __( 'Maximum number of rows to be returned in result data.', 'google-listings-and-ads' ), 'type' => 'integer', 'default' => 200, 'minimum' => 1, 'maximum' => 1000, 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ], 'next_page' => [ 'description' => __( 'Token to retrieve the next page.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ], ]; } /** * Maps query arguments from the REST request. * * @param Request $request REST Request. * @return array */ protected function prepare_query_arguments( Request $request ): array { $args = wp_parse_args( array_intersect_key( $request->get_query_params(), $this->get_collection_params() ), $request->get_default_params() ); $this->normalize_timezones( $args ); return $args; } /** * Converts input datetime parameters to local timezone. * * @param array $query_args Array of query arguments. */ protected function normalize_timezones( &$query_args ) { /** @var WP $wp */ $wp = $this->container->get( WP::class ); $local_tz = $wp->wp_timezone(); foreach ( [ 'before', 'after' ] as $query_arg_key ) { if ( isset( $query_args[ $query_arg_key ] ) && is_string( $query_args[ $query_arg_key ] ) ) { // Assume that unspecified timezone is a local timezone. $datetime = new DateTime( $query_args[ $query_arg_key ], $local_tz ); // In case timezone was forced by using +HH:MM, convert to local timezone. $datetime->setTimezone( $local_tz ); $query_args[ $query_arg_key ] = $datetime; } elseif ( isset( $query_args[ $query_arg_key ] ) && $query_args[ $query_arg_key ] instanceof DateTime ) { // In case timezone is in other timezone, convert to local timezone. $query_args[ $query_arg_key ]->setTimezone( $local_tz ); } } } } PK!$R-src/API/Site/Controllers/BatchSchemaTrait.phpnu[ &$value ) { $value['context'] = [ 'edit' ]; } $schema['country_codes'] = [ 'type' => 'array', 'description' => __( 'Array of country codes in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'edit' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_country_code_validate_callback(), 'minItems' => 1, 'required' => true, 'uniqueItems' => true, 'items' => [ 'type' => 'string', ], ]; return $schema; } /** * Get the schema for a batch DELETE request. * * @return array */ public function get_item_delete_schema(): array { $schema = $this->get_item_schema(); unset( $schema['rate'], $schema['currency'] ); return $schema; } } PK!ﶘ**-src/API/Site/Controllers/CountryCodeTrait.phpnu[iso3166_data_provider->alpha2( $country ); } /** * Validate that a country or a list of countries is valid and supported, * and also validate the data by the built-in validation of WP REST API with parameter’s schema. * * Since this extension's all API endpoints that use this validation function specify both * `validate_callback` and `sanitize_callback`, this makes the built-in schema validation * in WP REST API not to be applied. Therefore, this function calls `rest_validate_request_arg` * first, so that the API endpoints can still benefit from the built-in schema validation. * * @param bool $check_supported_country Whether to check the country is supported. * @param mixed $countries An individual string or an array of strings. * @param Request $request The request to validate. * @param string $param The parameter name, used in error messages. * * @return mixed * @throws Exception When the country is not supported. * @throws OutOfBoundsException When the country code cannot be found. */ protected function validate_country_codes( bool $check_supported_country, $countries, $request, $param ) { $validation_result = rest_validate_request_arg( $countries, $request, $param ); if ( true !== $validation_result ) { return $validation_result; } try { // This is used for individual strings and an array of strings. $countries = (array) $countries; foreach ( $countries as $country ) { $this->validate_country_code( $country ); if ( $check_supported_country ) { $country_supported = $this->google_helper->is_country_supported( $country ); if ( ! $country_supported ) { throw new Exception( __( 'Country is not supported', 'google-listings-and-ads' ) ); } } } return true; } catch ( Throwable $e ) { return $this->error_from_exception( $e, 'gla_invalid_country', [ 'status' => 400, 'country' => $countries, ] ); } } /** * Get the callback to sanitize the country code. * * Necessary because strtoupper() will trigger warnings when extra parameters are passed to it. * * @return callable */ protected function get_country_code_sanitize_callback(): callable { return function ( $value ) { return is_array( $value ) ? array_map( 'strtoupper', $value ) : strtoupper( $value ); }; } /** * Get a callable function for validating that a provided country code is recognized * and fulfilled the given parameter's schema. * * @return callable */ protected function get_country_code_validate_callback(): callable { return function ( ...$args ) { return $this->validate_country_codes( false, ...$args ); }; } /** * Get a callable function for validating that a provided country code is recognized, supported, * and fulfilled the given parameter's schema.. * * @return callable */ protected function get_supported_country_code_validate_callback(): callable { return function ( ...$args ) { return $this->validate_country_codes( true, ...$args ); }; } } PK!Z1src/API/Site/Controllers/DisconnectController.phpnu[register_route( 'connections', [ [ 'methods' => TransportMethods::DELETABLE, 'callback' => $this->get_disconnect_callback(), 'permission_callback' => $this->get_permission_callback(), ], ] ); } /** * Get the callback for disconnecting all the services. * * @return callable */ protected function get_disconnect_callback(): callable { return function ( Request $request ) { $endpoints = [ 'ads/connection', 'mc/connection', 'google/connect', 'jetpack/connect', 'rest-api/authorize', ]; $errors = []; $responses = []; foreach ( $endpoints as $endpoint ) { $response = $this->get_delete_response( $endpoint ); if ( 200 !== $response->get_status() ) { $errors[ $response->get_matched_route() ] = $response->get_data(); } else { $responses[ $response->get_matched_route() ] = $response->get_data(); } } return new Response( [ 'errors' => $errors, 'responses' => $responses, ], empty( $errors ) ? 200 : 400 ); }; } /** * Run a DELETE request for a given path, and return the response. * * @param string $path The relative API path. Based on the shared namespace. * * @return Response */ protected function get_delete_response( string $path ): Response { $path = ltrim( $path, '/' ); return $this->server->dispatch_request( new Request( 'DELETE', "/{$this->get_namespace()}/{$path}" ) ); } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'disconnect_all_accounts'; } } PK!'7src/API/Site/Controllers/EmptySchemaPropertiesTrait.phpnu[job_repository = $job_repository; } /** * Register rest routes with WordPress. */ public function register_routes(): void { $this->register_route( 'gtin-migration', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->start_migration_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_migration_status_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ] ); } /** * Callback function for scheduling GTIN migration job. * * @return callable */ protected function start_migration_callback(): callable { return function () { try { $job = $this->job_repository->get( MigrateGTIN::class ); if ( ! $job->can_schedule( [ 1 ] ) ) { return new Response( [ 'status' => 'error', 'message' => __( 'GTIN Migration cannot be scheduled.', 'google-listings-and-ads' ), ], 400 ); } $job->schedule(); return new Response( [ 'status' => 'success', 'message' => __( 'GTIN Migration successfully started.', 'google-listings-and-ads' ), ], 200 ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Callback function for getting the current migration status. * * @return callable */ protected function get_migration_status_callback(): callable { return function () { return new Response( [ 'status' => $this->get_gtin_migration_status(), ], 200 ); }; } /** * Get Schema title * * @return string */ protected function get_schema_title(): string { return 'gtin_migration'; } } PK!zYf7src/API/Site/Controllers/ResponseFromExceptionTrait.phpnu[getCode(); $status = $code && is_numeric( $code ) ? $code : 400; if ( $exception instanceof ExceptionWithResponseData ) { return new Response( $exception->get_response_data( true ), $status ); } return new Response( [ 'message' => $exception->getMessage() ], $status ); } } PK!ڮ 4src/API/Site/Controllers/ShippingRateSchemaTrait.phpnu[ [ 'type' => 'number', 'description' => __( 'The shipping rate unique identification number.', 'google-listings-and-ads' ), 'context' => [ 'view' ], 'readonly' => true, ], 'country' => [ 'type' => 'string', 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'sanitize_callback' => $this->get_country_code_sanitize_callback(), 'validate_callback' => $this->get_country_code_validate_callback(), 'required' => true, ], 'currency' => [ 'type' => 'string', 'description' => __( 'The currency to use for the shipping rate.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'default' => 'USD', // todo: default to store currency. ], 'rate' => [ 'type' => 'number', 'minimum' => 0, 'description' => __( 'The shipping rate.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], 'options' => [ 'type' => 'object', 'additionalProperties' => false, 'description' => __( 'Array of options for the shipping method.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', 'default' => [], 'properties' => [ 'free_shipping_threshold' => [ 'type' => 'number', 'minimum' => 0, 'description' => __( 'Minimum price eligible for free shipping.', 'google-listings-and-ads' ), 'context' => [ 'view', 'edit' ], 'validate_callback' => 'rest_validate_request_arg', ], ], ], ]; } } PK!i11+src/API/Site/Controllers/TourController.phpnu[register_route( "/tours/(?P{$this->get_tour_id_regex()})", [ [ 'methods' => TransportMethods::READABLE, 'callback' => $this->get_tours_read_callback(), 'permission_callback' => $this->get_permission_callback(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); /** * POST Update the tour visualizations */ $this->register_route( '/tours', [ [ 'methods' => TransportMethods::CREATABLE, 'callback' => $this->get_tours_create_callback(), 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_schema_properties(), ], 'schema' => $this->get_api_response_schema_callback(), ], ); } /** * Callback function for returning the tours * * @return callable */ protected function get_tours_read_callback(): callable { return function ( Request $request ) { try { $tour_id = $request->get_url_params()['id']; return $this->prepare_item_for_response( $this->get_tour( $tour_id ), $request ); } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Callback function for saving the Tours * * @return callable */ protected function get_tours_create_callback(): callable { return function ( Request $request ) { try { $tour_id = $request->get_param( 'id' ); $tours = $this->get_tours(); $tours[ $tour_id ] = $this->prepare_item_for_database( $request ); if ( $this->options->update( OptionsInterface::TOURS, $tours ) ) { return new Response( [ 'status' => 'success', 'message' => __( 'Successfully updated the tour.', 'google-listings-and-ads' ), ], 200 ); } else { throw new Exception( __( 'Unable to updated the tour.', 'google-listings-and-ads' ), 400 ); } } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } /** * Get the tours * * @return array|null The tours saved in databse */ private function get_tours(): ?array { return $this->options->get( OptionsInterface::TOURS ); } /** * Get the tour by Id * * @param string $tour_id The tour ID * @return array The tour * @throws Exception In case the tour is not found. */ private function get_tour( string $tour_id ): array { $tours = $this->get_tours(); if ( ! isset( $tours[ $tour_id ] ) ) { throw new Exception( __( 'Tour not found', 'google-listings-and-ads' ), 404 ); } return $tours[ $tour_id ]; } /** * Get the item schema properties for the controller. * * @return array The Schema properties */ protected function get_schema_properties(): array { return [ 'id' => [ 'description' => __( 'The Id for the tour.', 'google-listings-and-ads' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, 'pattern' => "^{$this->get_tour_id_regex()}$", ], 'checked' => [ 'description' => __( 'Whether the tour was checked.', 'google-listings-and-ads' ), 'type' => 'boolean', 'validate_callback' => 'rest_validate_request_arg', 'required' => true, ], ]; } /** * Get the item schema name for the controller. * * Used for building the API response schema. * * @return string */ protected function get_schema_title(): string { return 'tours'; } /** * Get the regex used for the Tour ID * * @return string The regex */ private function get_tour_id_regex(): string { return '[a-zA-z0-9-_]+'; } } PK!"z src/API/Site/RESTControllers.phpnu[register_controllers(); } ); } /** * Register our individual rest controllers. */ protected function register_controllers(): void { /** @var BaseController[] $controllers */ $controllers = $this->container->get( 'rest_controller' ); foreach ( $controllers as $controller ) { $this->validate_instanceof( $controller, BaseController::class ); $controller->register(); } } } PK! ݘ#src/API/WP/NotificationsService.phpnu[merchant_center = $merchant_center; $this->account_service = $account_service; $this->notification_url = "https://public-api.wordpress.com/wpcom/v2/sites/{$blog_id}/partners/google/notifications"; } /** * Calls the Notification endpoint in WPCOM. * https://public-api.wordpress.com/wpcom/v2/sites/{site}/partners/google/notifications * * @param string $topic The topic to use in the notification. * @param int|null $item_id The item ID to notify. It can be null for topics that doesn't need Item ID * @param array $data Optional data to send in the request. * @return bool True is the notification is successful. False otherwise. */ public function notify( string $topic, $item_id = null, $data = [] ): bool { /** * Allow users to disable the notification request. * * @since 2.8.0 * * @param bool $value The current filter value. True by default. * @param int $item_id The item_id for the notification. * @param string $topic The topic for the notification. */ if ( ! apply_filters( 'woocommerce_gla_notify', $this->is_ready() && in_array( $topic, self::ALLOWED_TOPICS, true ), $item_id, $topic ) ) { $this->notification_error( $topic, 'Notification was not sent because the Notification Service is not ready or the topic is not valid.', $item_id ); return false; } $remote_args = [ 'method' => 'POST', 'timeout' => 30, 'headers' => [ 'x-woocommerce-topic' => $topic, 'Content-Type' => 'application/json', ], 'body' => array_merge( $data, [ 'item_id' => $item_id ] ), 'url' => $this->get_notification_url(), ]; $response = $this->do_request( $remote_args ); if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) >= 400 ) { $error = is_wp_error( $response ) ? $response->get_error_message() : wp_remote_retrieve_body( $response ); $this->notification_error( $topic, $error, $item_id ); return false; } do_action( 'woocommerce_gla_debug_message', sprintf( 'Notification - Item ID: %s - Topic: %s - Data %s', $item_id, $topic, wp_json_encode( $data ) ), __METHOD__ ); return true; } /** * Logs an error. * * @param string $topic * @param string $error * @param int|null $item_id */ private function notification_error( string $topic, string $error, $item_id = null ): void { do_action( 'woocommerce_gla_error', sprintf( 'Error sending notification for Item ID %s with topic %s. %s', $item_id, $topic, $error ), __METHOD__ ); } /** * Performs a Remote Request * * @param array $args * @return array|\WP_Error */ protected function do_request( array $args ) { return Client::remote_request( $args, wp_json_encode( $args['body'] ) ); } /** * Get the route * * @return string The route. */ public function get_notification_url(): string { return $this->notification_url; } /** * If the Notifications are ready * This happens when the WPCOM API is Authorized and the feature is enabled. * * @param bool $with_health_check If true. Performs a remote request to WPCOM API to get the status. * @return bool */ public function is_ready( bool $with_health_check = true ): bool { return $this->options->is_wpcom_api_authorized() && $this->is_enabled() && $this->merchant_center->is_ready_for_syncing() && ( $with_health_check === false || $this->account_service->is_wpcom_api_status_healthy() ); } /** * If the Notifications are enabled * * @return bool */ public function is_enabled(): bool { return apply_filters( 'woocommerce_gla_notifications_enabled', true ); } } PK!f8!8!src/API/WP/OAuthService.phpnu[get_data_from_google(); $store_url = urlencode_deep( admin_url( "admin.php?page=wc-admin&path={$path}" ) ); $state = $this->base64url_encode( build_query( [ 'nonce' => $google_data['nonce'], 'store_url' => $store_url, ] ) ); $auth_url = esc_url_raw( add_query_arg( [ 'blog' => Jetpack_Options::get_option( 'id' ), 'client_id' => $google_data['client_id'], 'redirect_uri' => $google_data['redirect_uri'], 'response_type' => self::RESPONSE_TYPE, 'scope' => self::SCOPE, 'state' => $state, ], $this->get_wpcom_api_url( self::AUTH_URL ) ) ); return $auth_url; } /** * Get a WPCOM REST API URl concatenating the endpoint with the API Domain * * @param string $endpoint The endpoint to get the URL for * * @return string The WPCOM endpoint with the domain. */ protected function get_wpcom_api_url( string $endpoint ): string { return self::WPCOM_API_URL . $endpoint; } /** * Calls an API by Google via WCS to get required information in order to form an auth URL. * * @return array{client_id: string, redirect_uri: string, nonce: string} An associative array contains required information that is retrived from Google. * client_id: Google's WPCOM app client ID, will be used to form the authorization URL. * redirect_uri: A Google's URL that will be redirected to when the merchant approve the app access. Note that it needs to be matched with the Google WPCOM app client settings. * nonce: A string returned by Google that we will put it in the auth URL and the redirect_uri. Google will use it to verify the call. * @throws ContainerExceptionInterface When get_sdi_auth_params throws an exception. */ protected function get_data_from_google(): array { /** @var Middleware $middleware */ $middleware = $this->container->get( Middleware::class ); $response = $middleware->get_sdi_auth_params(); $nonce = $response['nonce']; $this->options->update( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE, $nonce ); return [ 'client_id' => $response['clientId'], 'redirect_uri' => $response['redirectUri'], 'nonce' => $nonce, ]; } /** * Perform a remote request for revoking OAuth access for the current user. * * @return string The body of the response * @throws Exception If the remote request fails. */ public function revoke_wpcom_api_auth(): string { $args = [ 'method' => 'DELETE', 'timeout' => 30, 'url' => $this->get_wpcom_api_url( '/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/revoke-token' ), 'user_id' => get_current_user_id(), ]; $request = $this->container->get( Jetpack::class )->remote_request( $args ); if ( is_wp_error( $request ) ) { /** * When the WPCOM token has been revoked with errors. * * @event revoke_wpcom_api_authorization * @property int status The status of the request. * @property string error The error message. * @property int|null blog_id The blog ID. */ do_action( 'woocommerce_gla_track_event', 'revoke_wpcom_api_authorization', [ 'status' => 400, 'error' => $request->get_error_message(), 'blog_id' => Jetpack_Options::get_option( 'id' ), ] ); throw new Exception( $request->get_error_message(), 400 ); } else { $body = wp_remote_retrieve_body( $request ); $status = wp_remote_retrieve_response_code( $request ); if ( ! $status || $status !== 200 ) { $data = json_decode( $body, true ); $message = $data['message'] ?? 'Error revoking access to WPCOM.'; /** * * When the WPCOM token has been revoked with errors. * * @event revoke_wpcom_api_authorization * @property int status The status of the request. * @property string error The error message. * @property int|null blog_id The blog ID. */ do_action( 'woocommerce_gla_track_event', 'revoke_wpcom_api_authorization', [ 'status' => $status, 'error' => $message, 'blog_id' => Jetpack_Options::get_option( 'id' ), ] ); throw new Exception( $message, $status ); } /** * When the WPCOM token has been revoked successfully. * * @event revoke_wpcom_api_authorization * @property int status The status of the request. * @property int|null blog_id The blog ID. */ do_action( 'woocommerce_gla_track_event', 'revoke_wpcom_api_authorization', [ 'status' => 200, 'blog_id' => Jetpack_Options::get_option( 'id' ), ] ); $this->container->get( AccountService::class )->reset_wpcom_api_authorization_data(); return $body; } } /** * Deactivate the service. * * Revoke token on deactivation. */ public function deactivate(): void { // Try to revoke the token on deactivation. If no token is available, it will throw an exception which we can ignore. try { $this->revoke_wpcom_api_auth(); } catch ( Exception $e ) { do_action( 'woocommerce_gla_error', sprintf( 'Error revoking the WPCOM token: %s', $e->getMessage() ), __METHOD__ ); } } } PK!:dsrc/API/MicroTrait.phpnu[async_runner = $async_runner; } /** * Schedule an action to run once at some time in the future * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array|null $args Arguments to pass when the hook triggers. * * @return int The action ID. */ public function schedule_single( int $timestamp, string $hook, $args = [] ): int { return as_schedule_single_action( $timestamp, $hook, $args, $this->get_slug() ); } /** * Schedule an action to run now i.e. in the next available batch. * * This differs from async actions by having a scheduled time rather than being set for '0000-00-00 00:00:00'. * We could use an async action instead but they can't be viewed easily in the admin area * because the table is sorted by schedule date. * * @param string $hook The hook to trigger. * @param array|null $args Arguments to pass when the hook triggers. * * @return int The action ID. */ public function schedule_immediate( string $hook, $args = [] ): int { return as_schedule_single_action( gmdate( 'U' ) - 1, $hook, $args, $this->get_slug() ); } /** * Schedule a recurring action to run now (i.e. in the next available batch), and in the given intervals. * * @param int $timestamp When the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array|null $args Arguments to pass when the hook triggers. * * @return int The action ID. */ public function schedule_recurring( int $timestamp, int $interval_in_seconds, string $hook, $args = [] ): int { return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $this->get_slug() ); } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The first instance of the action will be scheduled to run at a time * calculated after this timestamp matching the cron expression. This * can be used to delay the first instance of the action. * @param string $schedule A cron-link schedule string * @param string $hook The hook to trigger. * @param array|null $args Arguments to pass when the hook triggers. * * @return int The action ID. * * @see https://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) */ public function schedule_cron( int $timestamp, string $schedule, string $hook, $args = [] ): int { return as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $this->get_slug() ); } /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array|null $args Arguments to pass when the hook triggers. * * @return int The action ID. */ public function enqueue_async_action( string $hook, $args = [] ): int { $this->async_runner->attach_shutdown_hook(); return $this->schedule_immediate( $hook, $args ); } /** * Check if there is an existing action in the queue with a given hook and args combination. * * An action in the queue could be pending, in-progress or async. If the action is pending for a time in * future, currently being run, or an async action sitting in the queue waiting to be processed, boolean * true will be returned. Or there may be no async, in-progress or pending action for this hook, in which * case, boolean false will be the return value. * * @param string $hook * @param array|null $args * * @return bool True if there is a pending scheduled, async or in-progress action in the queue or false if there is no matching action. */ public function has_scheduled_action( string $hook, $args = [] ): bool { return ( false !== as_next_scheduled_action( $hook, $args, $this->get_slug() ) ); } /** * Search for scheduled actions. * * @param array|null $args See as_get_scheduled_actions() for possible arguments. * @param string $return_format OBJECT, ARRAY_A, or ids. * * @return array */ public function search( $args = [], $return_format = OBJECT ): array { $args['group'] = $this->get_slug(); return as_get_scheduled_actions( $args, $return_format ); } /** * Cancel the next scheduled instance of an action with a matching hook (and optionally matching args). * * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. * * @param string $hook The hook that the job will trigger. * @param array|null $args Args that would have been passed to the job. * * @return int The scheduled action ID if a scheduled action was found. * * @throws ActionSchedulerException If no matching action found. */ public function cancel( string $hook, $args = [] ) { $action_id = as_unschedule_action( $hook, $args, $this->get_slug() ); if ( null === $action_id ) { throw ActionSchedulerException::action_not_found( $hook ); } return $action_id; } /** * Retrieve an action. * * @param int $action_id Action ID. * * @return ActionScheduler_Action * * @since 1.7.0 */ public function fetch_action( int $action_id ): ActionScheduler_Action { return ActionSchedulerCore::store()->fetch_action( $action_id ); } } PK!)src/ActionScheduler/AsyncActionRunner.phpnu[async_request = $async_request; $this->locker = $locker; } /** * Attach async runner shutdown hook before ActionScheduler shutdown hook. * * The shutdown hook should only be attached if an async event has been created in the current request. * The hook is only attached if it hasn't already been attached. * * @see ActionScheduler_QueueRunner::hook_dispatch_async_request */ public function attach_shutdown_hook() { if ( $this->has_attached_shutdown_hook ) { return; } $this->has_attached_shutdown_hook = true; add_action( 'shutdown', [ $this, 'maybe_dispatch_async_request' ], 9 ); } /** * Dispatches an async queue runner request if various conditions are met. * * Note: This is a temporary solution. In the future (probably ActionScheduler 3.2) we should use the filter * added in https://github.com/woocommerce/action-scheduler/pull/628. */ public function maybe_dispatch_async_request() { if ( is_admin() ) { // ActionScheduler will dispatch an async runner request on it's own. return; } if ( $this->locker->is_locked( 'async-request-runner' ) ) { // An async runner request has already occurred in the last 60 seconds. return; } $this->locker->set( 'async-request-runner' ); $this->async_request->maybe_dispatch(); } } PK!ypJJ*src/Admin/BulkEdit/BulkEditInitializer.phpnu[meta_handler = $meta_handler; $this->merchant_center = $merchant_center; $this->target_audience = $target_audience; } /** * Register a service. */ public function register(): void { add_action( 'bulk_edit_custom_box', [ $this, 'render_view' ], 10, 2 ); add_action( 'bulk_edit_save_post', [ $this, 'handle_submission' ], 10, 2 ); } /** * The screen on which to show the bulk edit view. * * @return string */ public function get_screen(): string { return self::SCREEN_COUPON; } /** * Render the coupon bulk edit view. * * @param string $column_name Column being shown. * @param string $post_type Post type being shown. */ public function render_view( $column_name, $post_type ) { if ( $this->get_screen() !== $post_type || self::TARGET_COLUMN !== $column_name ) { return; } if ( ! $this->merchant_center->is_setup_complete() ) { return; } $target_country = $this->target_audience->get_main_target_country(); if ( ! $this->merchant_center->is_promotion_supported_country( $target_country ) ) { return; } include path_join( dirname( __DIR__, 3 ), self::VIEW_PATH ); } /** * Handle the coupon bulk edit submission. * * @param int $post_id Post ID being saved. * @param object $post Post object being saved. * * @return int $post_id */ public function handle_submission( int $post_id, $post ): int { $request_data = $this->request_data(); // If this is an autosave, our form has not been submitted, so we don't want to do anything. if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) { return $post_id; } // Don't save revisions and autosaves. if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || $this->get_screen() !== $post->post_type || ! current_user_can( 'edit_post', $post_id ) ) { return $post_id; } // Check nonce. if ( ! isset( $request_data['woocommerce_gla_bulk_edit'] ) || ! wp_verify_nonce( $request_data['woocommerce_gla_bulk_edit_nonce'], 'woocommerce_gla_bulk_edit_nonce' ) ) { return $post_id; } if ( ! empty( $request_data['change_channel_visibility'] ) ) { // Get the coupon and save. $coupon = new WC_Coupon( $post_id ); $visibility = ChannelVisibility::cast( sanitize_key( $request_data['change_channel_visibility'] ) ); if ( $this->meta_handler->get_visibility( $coupon ) !== $visibility ) { $this->meta_handler->update_visibility( $coupon, $visibility ); do_action( 'woocommerce_gla_bulk_update_coupon', $post_id ); } } return $post_id; } /** * Get the current request data ($_REQUEST superglobal). * This method is added to ease unit testing. * * @return array The $_REQUEST superglobal. */ protected function request_data(): array { // Nonce must be verified manually. // phpcs:ignore WordPress.Security.NonceVerification.Recommended return $_REQUEST; } } PK!566!src/Admin/Input/BooleanSelect.phpnu[ __( 'Default', 'google-listings-and-ads' ), 'yes' => __( 'Yes', 'google-listings-and-ads' ), 'no' => __( 'No', 'google-listings-and-ads' ), ]; } /** * Return the data used for the input's view. * * @return array */ public function get_view_data(): array { $view_data = parent::get_view_data(); if ( is_bool( $view_data['value'] ) ) { $view_data['value'] = wc_bool_to_string( $view_data['value'] ); } return $view_data; } } PK!ii)src/Admin/Input/DateTime.phpnu[get_value() ) ) { try { // Display the time in site's local timezone. $datetime = new WC_DateTime( $this->get_value(), new DateTimeZone( 'UTC' ) ); $datetime->setTimezone( new DateTimeZone( $this->get_local_tz_string() ) ); $view_data['value'] = $datetime->format( 'Y-m-d H:i:s' ); $view_data['date'] = $datetime->format( 'Y-m-d' ); $view_data['time'] = $datetime->format( 'H:i' ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); $view_data['value'] = ''; $view_data['date'] = ''; $view_data['time'] = ''; } } return $view_data; } /** * Set the form's data. * * @param mixed $data * * @return void */ public function set_data( $data ): void { if ( is_array( $data ) ) { if ( ! empty( $data['date'] ) ) { $date = $data['date'] ?? ''; $time = $data['time'] ?? ''; $data = sprintf( '%s%s', $date, $time ); } else { $data = ''; } } if ( ! empty( $data ) ) { try { // Store the time in UTC. $datetime = new WC_DateTime( $data, new DateTimeZone( $this->get_local_tz_string() ) ); $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); $data = (string) $datetime; } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); $data = ''; } } parent::set_data( $data ); } /** * Get site's local timezone string from WordPress settings. * * @return string */ protected function get_local_tz_string(): string { return wp_timezone_string(); } } PK!!)GK!src/Admin/Input/FormException.phpnu[set_data( $data ); } /** * @return string */ public function get_name(): string { return $this->name; } /** * @param string $name * * @return FormInterface */ public function set_name( string $name ): FormInterface { $this->name = $name; return $this; } /** * @return FormInterface[] */ public function get_children(): array { return $this->children; } /** * Add a child form. * * @param FormInterface $form * * @return FormInterface * * @throws FormException If form is already submitted. */ public function add( FormInterface $form ): FormInterface { if ( $this->is_submitted ) { throw FormException::cannot_modify_submitted(); } $this->children[ $form->get_name() ] = $form; $form->set_parent( $this ); return $this; } /** * Remove a child with the given name from the form's children. * * @param string $name * * @return FormInterface * * @throws FormException If form is already submitted. */ public function remove( string $name ): FormInterface { if ( $this->is_submitted ) { throw FormException::cannot_modify_submitted(); } if ( isset( $this->children[ $name ] ) ) { $this->children[ $name ]->set_parent( null ); unset( $this->children[ $name ] ); } return $this; } /** * Whether the form contains a child with the given name. * * @param string $name * * @return bool */ public function has( string $name ): bool { return isset( $this->children[ $name ] ); } /** * @param FormInterface|null $form * * @return void */ public function set_parent( ?FormInterface $form ): void { $this->parent = $form; } /** * @return FormInterface|null */ public function get_parent(): ?FormInterface { return $this->parent; } /** * Return the form's data. * * @return mixed */ public function get_data() { return $this->data; } /** * Set the form's data. * * @param mixed $data * * @return void */ public function set_data( $data ): void { if ( is_array( $data ) && ! empty( $this->children ) ) { $this->data = $this->map_children_data( $data ); } else { if ( is_string( $data ) ) { $data = trim( $data ); } $this->data = $data; } } /** * Maps the data to each child and returns the mapped data. * * @param array $data * * @return array */ protected function map_children_data( array $data ): array { $children_data = []; foreach ( $data as $key => $datum ) { if ( isset( $this->children[ $key ] ) ) { $this->children[ $key ]->set_data( $datum ); $children_data[ $key ] = $this->children[ $key ]->get_data(); } } return $children_data; } /** * Submit the form. * * @param array $submitted_data */ public function submit( array $submitted_data = [] ): void { // todo: add form validation if ( ! $this->is_submitted ) { $this->is_submitted = true; $this->set_data( $submitted_data ); } } /** * Return the data used for the form's view. * * @return array */ public function get_view_data(): array { $view_data = [ 'name' => $this->get_view_name(), 'is_root' => $this->is_root(), 'children' => [], ]; foreach ( $this->get_children() as $index => $form ) { $view_data['children'][ $index ] = $form->get_view_data(); } return $view_data; } /** * Return the name used for the form's view. * * @return string */ public function get_view_name(): string { return $this->is_root() ? sprintf( 'gla_%s', $this->get_name() ) : sprintf( '%s[%s]', $this->get_parent()->get_view_name(), $this->get_name() ); } /** * Whether this is the root form (i.e. has no parents). * * @return bool */ public function is_root(): bool { return null === $this->parent; } /** * Whether the form has been already submitted. * * @return bool */ public function is_submitted(): bool { return $this->is_submitted; } } PK!(yy"src/Admin/Input/InputInterface.phpnu[type = $type; $this->block_name = $block_name; parent::__construct(); } /** * @return string|null */ public function get_id(): ?string { return $this->id; } /** * @return string */ public function get_type(): string { return $this->type; } /** * @return string|null */ public function get_label(): ?string { return $this->label; } /** * @return string|null */ public function get_description(): ?string { return $this->description; } /** * @return mixed */ public function get_value() { return $this->get_data(); } /** * @param string|null $id * * @return InputInterface */ public function set_id( ?string $id ): InputInterface { $this->id = $id; return $this; } /** * @param string|null $label * * @return InputInterface */ public function set_label( ?string $label ): InputInterface { $this->label = $label; return $this; } /** * @param string|null $description * * @return InputInterface */ public function set_description( ?string $description ): InputInterface { $this->description = $description; return $this; } /** * @param mixed $value * * @return InputInterface */ public function set_value( $value ): InputInterface { $this->set_data( $value ); return $this; } /** * @return bool */ public function is_readonly(): bool { return $this->is_readonly; } /** * @param bool $value * * @return InputInterface */ public function set_readonly( bool $value ): InputInterface { $this->is_readonly = $value; return $this; } /** * @param bool $value * * @return InputInterface */ public function set_hidden( bool $value ): InputInterface { $this->is_hidden = $value; return $this; } /** * @return bool */ public function is_hidden(): bool { return $this->is_hidden; } /** * Return the data used for the input's view. * * @return array */ public function get_view_data(): array { $view_data = [ 'id' => $this->get_view_id(), 'type' => $this->get_type(), 'label' => $this->get_label(), 'value' => $this->get_value(), 'description' => $this->get_description(), 'desc_tip' => true, ]; if ( $this->is_readonly ) { $view_data['custom_attributes'] = [ 'readonly' => 'readonly', ]; } return array_merge( parent::get_view_data(), $view_data ); } /** * Return the id used for the input's view. * * @return string */ public function get_view_id(): string { $parent = $this->get_parent(); if ( $parent instanceof InputInterface ) { return sprintf( '%s_%s', $parent->get_view_id(), $this->get_id() ); } elseif ( $parent instanceof FormInterface ) { return sprintf( '%s_%s', $parent->get_view_name(), $this->get_id() ); } return sprintf( 'gla_%s', $this->get_name() ); } /** * Return the name of a generic product block in WooCommerce core or a custom block in this extension. * * @return string */ public function get_block_name(): string { return $this->block_name; } /** * Add or update a block attribute used for block config. * * @param string $key The attribute key defined in the corresponding block.json * @param mixed $value The attribute value defined in the corresponding block.json * * @return InputInterface */ public function set_block_attribute( string $key, $value ): InputInterface { $this->block_attributes[ $key ] = $value; return $this; } /** * Return the attributes of block config used for the input's view within the Product Block Editor. * * @return array */ public function get_block_attributes(): array { $meta_key = $this->prefix_meta_key( $this->get_id() ); $block_attributes = array_merge( [ 'property' => "meta_data.{$meta_key}", 'label' => $this->get_label(), 'tooltip' => $this->get_description(), ], $this->block_attributes ); // Set boolean disabled property only if it's needed. if ( $this->is_readonly() ) { $block_attributes['disabled'] = true; } return $block_attributes; } /** * Return the config used for the input's block within the Product Block Editor. * * @return array */ public function get_block_config(): array { $id = $this->get_id(); return [ 'id' => "google-listings-and-ads-product-attributes-{$id}", 'blockName' => $this->get_block_name(), 'attributes' => $this->get_block_attributes(), ]; } } PK!V+src/Admin/Input/Integer.phpnu[set_block_attribute( 'pattern', [ 'value' => '0|[1-9]\d*', ] ); } } PK!:(src/Admin/Input/Select.phpnu[options; } /** * @param array $options * * @return $this */ public function set_options( array $options ): Select { $this->options = $options; return $this; } /** * Return the data used for the input's view. * * @return array */ public function get_view_data(): array { $view_data = parent::get_view_data(); $view_data['options'] = $this->get_options(); // add custom class $view_data['class'] = 'select short'; return $view_data; } /** * Return the attributes of block config used for the input's view within the Product Block Editor. * * @return array */ public function get_block_attributes(): array { $options = []; foreach ( $this->get_options() as $key => $value ) { $options[] = [ 'label' => $value, 'value' => $key, ]; } $this->set_block_attribute( 'options', $options ); return parent::get_block_attributes(); } } PK!OV@@'src/Admin/Input/SelectWithTextInput.phpnu[set_id( self::SELECT_INPUT_KEY ) ->set_name( self::SELECT_INPUT_KEY ); $this->add( $select_input ); $custom_input = ( new Text() )->set_id( self::CUSTOM_INPUT_KEY ) ->set_label( __( 'Enter your value', 'google-listings-and-ads' ) ) ->set_name( self::CUSTOM_INPUT_KEY ); $this->add( $custom_input ); parent::__construct( 'select-with-text-input', 'google-listings-and-ads/product-select-with-text-field' ); } /** * @return array */ public function get_options(): array { return $this->get_select_input()->get_options(); } /** * @param array $options * * @return $this */ public function set_options( array $options ): SelectWithTextInput { $this->get_select_input()->set_options( $options ); return $this; } /** * @param string|null $label * * @return InputInterface */ public function set_label( ?string $label ): InputInterface { $this->get_select_input()->set_label( $label ); return parent::set_label( $label ); } /** * @param string|null $description * * @return InputInterface */ public function set_description( ?string $description ): InputInterface { $this->get_select_input()->set_description( $description ); return parent::set_description( $description ); } /** * @return Select */ protected function get_select_input(): Select { return $this->children[ self::SELECT_INPUT_KEY ]; } /** * @return Text */ protected function get_custom_input(): Text { return $this->children[ self::CUSTOM_INPUT_KEY ]; } /** * Return the data used for the input's view. * * @return array */ public function get_view_data(): array { $view_data = parent::get_view_data(); $select_input = $view_data['children'][ self::SELECT_INPUT_KEY ]; $custom_input = $view_data['children'][ self::CUSTOM_INPUT_KEY ]; // add custom classes $view_data['gla_wrapper_class'] = $view_data['gla_wrapper_class'] ?? ''; $view_data['gla_wrapper_class'] .= ' select-with-text-input'; $custom_input['wrapper_class'] = 'custom-input'; // add custom value option $select_input['options'][ self::CUSTOM_INPUT_KEY ] = __( 'Enter a custom value', 'google-listings-and-ads' ); if ( $this->is_readonly ) { $select_input['custom_attributes'] = [ 'disabled' => 'disabled', ]; $custom_input['custom_attributes'] = [ 'readonly' => 'readonly', ]; } $view_data['children'][ self::CUSTOM_INPUT_KEY ] = $custom_input; $view_data['children'][ self::SELECT_INPUT_KEY ] = $select_input; return $view_data; } /** * Set the form's data. * * @param mixed $data * * @return void */ public function set_data( $data ): void { if ( empty( $data ) ) { $this->get_select_input()->set_data( null ); $this->get_custom_input()->set_data( null ); return; } $select_value = is_array( $data ) ? $data[ self::SELECT_INPUT_KEY ] ?? '' : $data; $custom_value = is_array( $data ) ? $data[ self::CUSTOM_INPUT_KEY ] ?? '' : $data; if ( ! isset( $this->get_options()[ $select_value ] ) ) { $this->get_select_input()->set_data( self::CUSTOM_INPUT_KEY ); $this->get_custom_input()->set_data( $custom_value ); $this->data = $custom_value; } else { $this->get_select_input()->set_data( $select_value ); $this->data = $select_value; } } /** * Return the attributes of block config used for the input's view within the Product Block Editor. * * @return array */ public function get_block_attributes(): array { $options = []; foreach ( $this->get_options() as $key => $value ) { $options[] = [ 'label' => $value, 'value' => $key, ]; } $options[] = [ 'label' => __( 'Enter a custom value', 'google-listings-and-ads' ), 'value' => self::CUSTOM_INPUT_KEY, ]; $this->set_block_attribute( 'options', $options ); $this->set_block_attribute( 'customInputValue', self::CUSTOM_INPUT_KEY ); return parent::get_block_attributes(); } } PK!Csrc/Admin/Input/Text.phpnu[admin = $admin; } /** * The context within the screen where the box should display. Available contexts vary from screen to * screen. Post edit screen contexts include 'normal', 'side', and 'advanced'. Comments screen contexts * include 'normal' and 'side'. Menus meta boxes (accordion sections) all use the 'side' context. * * Global default is 'advanced'. * * @return string */ public function get_context(): string { return self::CONTEXT_ADVANCED; } /*** * The priority within the context where the box should show. * * Accepts 'high', 'core', 'default', or 'low'. Default 'default'. * * @return string */ public function get_priority(): string { return self::PRIORITY_DEFAULT; } /** * Data that should be set as the $args property of the box array (which is the second parameter passed to your callback). * * @return array */ public function get_callback_args(): array { return []; } /** * Returns an array of CSS classes to apply to the box. * * @return array */ public function get_classes(): array { return []; } /** * Function that fills the box with the desired content. * * The function should echo its output. * * @return callable */ public function get_callback(): callable { return [ $this, 'handle_callback' ]; } /** * Called by WordPress when rendering the meta box. * * The function should echo its output. * * @param WP_Post $post The WordPress post object the box is loaded for. * @param array $data Array of box data passed to the callback by WordPress. * * @return void * * @throws ViewException If the meta box view can't be rendered. */ public function handle_callback( WP_Post $post, array $data ): void { $args = $data['args'] ?? []; $context = $this->get_view_context( $post, $args ); echo wp_kses( $this->render( $context ), $this->get_allowed_html_form_tags() ); } /** * Render the meta box. * * The view templates need to be placed under 'views/meta-box' and named * using the meta box ID specified by the `get_id` method. * * @param array $context Optional. Contextual information to use while * rendering. Defaults to an empty array. * * @return string Rendered result. * * @throws ViewException If the view doesn't exist or can't be loaded. * * @see self::get_id To see and modify the view file name. */ public function render( array $context = [] ): string { $view_path = path_join( self::VIEW_PATH, $this->get_id() ); return $this->admin->get_view( $view_path, $context ); } /** * Appends a prefix to the given field ID and returns it. * * @param string $field_id * * @return string * * @since 1.1.0 */ protected function prefix_field_id( string $field_id ): string { $box_id = $this->prefix_id( $this->get_id() ); return "{$box_id}_{$field_id}"; } /** * Returns an array of variables to be used in the view. * * @param WP_Post $post The WordPress post object the box is loaded for. * @param array $args Array of data passed to the callback. Defined by `get_callback_args`. * * @return array */ abstract protected function get_view_context( WP_Post $post, array $args ): array; } PK!Ke.src/Admin/MetaBox/ChannelVisibilityMetaBox.phpnu[meta_handler = $meta_handler; $this->product_helper = $product_helper; $this->merchant_center = $merchant_center; parent::__construct( $admin ); } /** * Meta box ID (used in the 'id' attribute for the meta box). * * @return string */ public function get_id(): string { return 'channel_visibility'; } /** * Title of the meta box. * * @return string */ public function get_title(): string { return __( 'Channel visibility', 'google-listings-and-ads' ); } /** * The screen on which to show the box (such as a post type, 'link', or 'comment'). * * Default is the current screen. * * @return string */ public function get_screen(): string { return self::SCREEN_PRODUCT; } /** * The context within the screen where the box should display. Available contexts vary from screen to * screen. Post edit screen contexts include 'normal', 'side', and 'advanced'. Comments screen contexts * include 'normal' and 'side'. Menus meta boxes (accordion sections) all use the 'side' context. * * Global default is 'advanced'. * * @return string */ public function get_context(): string { return self::CONTEXT_SIDE; } /** * Returns an array of CSS classes to apply to the box. * * @return array */ public function get_classes(): array { return [ 'gla_meta_box' ]; } /** * Returns an array of variables to be used in the view. * * @param WP_Post $post The WordPress post object the box is loaded for. * @param array $args Array of data passed to the callback. Defined by `get_callback_args`. * * @return array */ protected function get_view_context( WP_Post $post, array $args ): array { $product_id = absint( $post->ID ); $product = $this->product_helper->get_wc_product( $product_id ); return [ 'field_id' => $this->get_visibility_field_id(), 'product_id' => $product_id, 'product' => $product, 'channel_visibility' => $this->product_helper->get_channel_visibility( $product ), 'sync_status' => $this->meta_handler->get_sync_status( $product ), 'issues' => $this->product_helper->get_validation_errors( $product ), 'is_setup_complete' => $this->merchant_center->is_setup_complete(), 'get_started_url' => $this->get_start_url(), ]; } /** * Register a service. */ public function register(): void { add_action( 'woocommerce_new_product', [ $this, 'handle_submission' ], 10, 2 ); add_action( 'woocommerce_update_product', [ $this, 'handle_submission' ], 10, 2 ); } /** * @param int $product_id * @param WC_Product $product */ public function handle_submission( int $product_id, WC_Product $product ) { /** * Array of `true` values for each product IDs already handled by this method. Used to prevent double submission. * * @var bool[] $already_updated */ static $already_updated = []; $field_id = $this->get_visibility_field_id(); // phpcs:disable WordPress.Security.NonceVerification // nonce is verified by self::verify_nonce if ( ! $this->verify_nonce() || ! isset( $_POST[ $field_id ] ) || isset( $already_updated[ $product_id ] ) ) { return; } // only update the value for supported product types if ( ! in_array( $product->get_type(), ProductSyncer::get_supported_product_types(), true ) ) { return; } try { $visibility = empty( $_POST[ $field_id ] ) ? ChannelVisibility::cast( ChannelVisibility::SYNC_AND_SHOW ) : ChannelVisibility::cast( sanitize_key( $_POST[ $field_id ] ) ); // phpcs:enable WordPress.Security.NonceVerification $this->meta_handler->update_visibility( $product, $visibility ); $already_updated[ $product_id ] = true; } catch ( InvalidValue $exception ) { // silently log the exception and do not set the product's visibility if an invalid visibility value is sent. do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); } } /** * @return string * * @since 1.1.0 */ protected function get_visibility_field_id(): string { return $this->prefix_field_id( 'visibility' ); } } PK!p4src/Admin/MetaBox/CouponChannelVisibilityMetaBox.phpnu[meta_handler = $meta_handler; $this->coupon_helper = $coupon_helper; $this->merchant_center = $merchant_center; $this->target_audience = $target_audience; parent::__construct( $admin ); } /** * Meta box ID (used in the 'id' attribute for the meta box). * * @return string */ public function get_id(): string { return 'coupon_channel_visibility'; } /** * Title of the meta box. * * @return string */ public function get_title(): string { return __( 'Channel visibility', 'google-listings-and-ads' ); } /** * The screen on which to show the box (such as a post type, 'link', or 'comment'). * * Default is the current screen. * * @return string */ public function get_screen(): string { return self::SCREEN_COUPON; } /** * The context within the screen where the box should display. Available contexts vary from screen to * screen. Post edit screen contexts include 'normal', 'side', and 'advanced'. Comments screen contexts * include 'normal' and 'side'. Menus meta boxes (accordion sections) all use the 'side' context. * * Global default is 'advanced'. * * @return string */ public function get_context(): string { return self::CONTEXT_SIDE; } /** * Returns an array of CSS classes to apply to the box. * * @return array */ public function get_classes(): array { $shown_types = array_map( function ( string $coupon_type ) { return "show_if_{$coupon_type}"; }, CouponSyncer::get_supported_coupon_types() ); $hidden_types = array_map( function ( string $coupon_type ) { return "hide_if_{$coupon_type}"; }, CouponSyncer::get_hidden_coupon_types() ); return array_merge( $shown_types, $hidden_types ); } /** * Returns an array of variables to be used in the view. * * @param WP_Post $post The WordPress post object the box is loaded for. * @param array $args Array of data passed to the callback. Defined by `get_callback_args`. * * @return array */ protected function get_view_context( WP_Post $post, array $args ): array { $coupon_id = absint( $post->ID ); $coupon = $this->coupon_helper->get_wc_coupon( $coupon_id ); $target_country = $this->target_audience->get_main_target_country(); return [ 'field_id' => $this->get_visibility_field_id(), 'coupon_id' => $coupon_id, 'coupon' => $coupon, 'channel_visibility' => $this->coupon_helper->get_channel_visibility( $coupon ), 'sync_status' => $this->meta_handler->get_sync_status( $coupon ), 'issues' => $this->coupon_helper->get_validation_errors( $coupon ), 'is_setup_complete' => $this->merchant_center->is_setup_complete(), 'is_channel_supported' => $this->merchant_center->is_promotion_supported_country( $target_country ), 'get_started_url' => $this->get_start_url(), ]; } /** * Register a service. */ public function register(): void { add_action( 'woocommerce_new_coupon', [ $this, 'handle_submission' ], 10, 2 ); add_action( 'woocommerce_update_coupon', [ $this, 'handle_submission' ], 10, 2 ); } /** * @param int $coupon_id * @param WC_Coupon $coupon */ public function handle_submission( int $coupon_id, WC_Coupon $coupon ) { /** * Array of `true` values for each coupon IDs already handled by this method. Used to prevent double submission. * * @var bool[] $already_updated */ static $already_updated = []; $field_id = $this->get_visibility_field_id(); // phpcs:disable WordPress.Security.NonceVerification // nonce is verified by self::verify_nonce if ( ! $this->verify_nonce() || ! isset( $_POST[ $field_id ] ) || isset( $already_updated[ $coupon_id ] ) ) { return; } // Only update the value for supported coupon types if ( ! CouponSyncer::is_coupon_supported( $coupon ) ) { return; } try { $visibility = empty( $_POST[ $field_id ] ) ? ChannelVisibility::cast( ChannelVisibility::DONT_SYNC_AND_SHOW ) : ChannelVisibility::cast( sanitize_key( $_POST[ $field_id ] ) ); // phpcs:enable WordPress.Security.NonceVerification $this->meta_handler->update_visibility( $coupon, $visibility ); $already_updated[ $coupon_id ] = true; } catch ( InvalidValue $exception ) { // silently log the exception and do not set the coupon's visibility if an invalid visibility value is sent. do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); } } /** * @return string * * @since 1.1.0 */ protected function get_visibility_field_id(): string { return $this->prefix_field_id( 'visibility' ); } } PK!4ncc(src/Admin/MetaBox/MetaBoxInitializer.phpnu[admin = $admin; $this->meta_boxes = $meta_boxes; } /** * Register a service. */ public function register(): void { add_action( 'add_meta_boxes', [ $this, 'register_meta_boxes' ] ); } /** * Registers the meta boxes. */ public function register_meta_boxes() { array_walk( $this->meta_boxes, [ $this->admin, 'add_meta_box' ] ); } } PK!g  &src/Admin/MetaBox/MetaBoxInterface.phpnu[set_label( __( 'Adult content', 'google-listings-and-ads' ) ); $this->set_description( __( 'Whether the product contains nudity or sexually suggestive content', 'google-listings-and-ads' ) ); } } PK!K4src/Admin/Product/Attributes/Input/AgeGroupInput.phpnu[set_label( __( 'Age Group', 'google-listings-and-ads' ) ); $this->set_description( __( 'Target age group of the item.', 'google-listings-and-ads' ) ); } } PK!`†>src/Admin/Product/Attributes/Input/AttributeInputInterface.phpnu[set_label( __( 'Availability Date', 'google-listings-and-ads' ) ); $this->set_description( __( 'The date a preordered or backordered product becomes available for delivery. Required if product availability is preorder or backorder', 'google-listings-and-ads' ) ); } } PK!`5kpp1src/Admin/Product/Attributes/Input/BrandInput.phpnu[set_label( __( 'Brand', 'google-listings-and-ads' ) ); $this->set_description( __( 'Brand of the product.', 'google-listings-and-ads' ) ); } } PK!qKpp1src/Admin/Product/Attributes/Input/ColorInput.phpnu[set_label( __( 'Color', 'google-listings-and-ads' ) ); $this->set_description( __( 'Color of the product.', 'google-listings-and-ads' ) ); } } PK!_? 5src/Admin/Product/Attributes/Input/ConditionInput.phpnu[set_label( __( 'Condition', 'google-listings-and-ads' ) ); $this->set_description( __( 'Condition or state of the item.', 'google-listings-and-ads' ) ); } } PK!52src/Admin/Product/Attributes/Input/GenderInput.phpnu[set_label( __( 'Gender', 'google-listings-and-ads' ) ); $this->set_description( __( 'The gender for which your product is intended.', 'google-listings-and-ads' ) ); } } PK!0UU0src/Admin/Product/Attributes/Input/GTINInput.phpnu[set_label( __( 'Global Trade Item Number (GTIN)', 'google-listings-and-ads' ) ); $this->set_description( __( 'Global Trade Item Number (GTIN) for your item. These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books)', 'google-listings-and-ads' ) ); $this->set_field_visibility(); } /** * Controls the inputs visibility based on the WooCommerce version and the * initial version of Google for WooCommerce at the time of installation. * * @since 2.9.0 * @return void */ public function set_field_visibility(): void { if ( $this->is_gtin_available_in_core() ) { // For versions after the GTIN changes are published. Hide the GTIN field from G4W tab. Otherwise, set as readonly. if ( $this->should_hide_gtin() ) { $this->set_hidden( true ); } else { $this->set_readonly( true ); $this->set_description( __( 'The Global Trade Item Number (GTIN) for your item can now be entered on the "Inventory" tab', 'google-listings-and-ads' ) ); } } } } PK! f4src/Admin/Product/Attributes/Input/IsBundleInput.phpnu[set_label( __( 'Is Bundle?', 'google-listings-and-ads' ) ); $this->set_description( __( 'Whether the item is a bundle of products. A bundle is a custom grouping of different products sold by a merchant for a single price.', 'google-listings-and-ads' ) ); } } PK!dc4src/Admin/Product/Attributes/Input/MaterialInput.phpnu[set_label( __( 'Material', 'google-listings-and-ads' ) ); $this->set_description( __( 'The material of which the item is made.', 'google-listings-and-ads' ) ); } } PK!o۬/src/Admin/Product/Attributes/Input/MPNInput.phpnu[set_label( __( 'Manufacturer Part Number (MPN)', 'google-listings-and-ads' ) ); $this->set_description( __( 'This code uniquely identifies the product to its manufacturer.', 'google-listings-and-ads' ) ); } } PK!M@@5src/Admin/Product/Attributes/Input/MultipackInput.phpnu[set_label( __( 'Multipack', 'google-listings-and-ads' ) ); $this->set_description( __( 'The number of identical products in a multipack. Use this attribute to indicate that you\'ve grouped multiple identical products for sale as one item.', 'google-listings-and-ads' ) ); $this->set_block_attribute( 'min', [ 'value' => 0 ] ); } } PK!-݉3src/Admin/Product/Attributes/Input/PatternInput.phpnu[set_label( __( 'Pattern', 'google-listings-and-ads' ) ); $this->set_description( __( 'The item\'s pattern (e.g. polka dots).', 'google-listings-and-ads' ) ); } } PK!%xkk0src/Admin/Product/Attributes/Input/SizeInput.phpnu[set_label( __( 'Size', 'google-listings-and-ads' ) ); $this->set_description( __( 'Size of the product.', 'google-listings-and-ads' ) ); } } PK!l6src/Admin/Product/Attributes/Input/SizeSystemInput.phpnu[set_label( __( 'Size system', 'google-listings-and-ads' ) ); $this->set_description( __( 'System in which the size is specified. Recommended for apparel items.', 'google-listings-and-ads' ) ); } } PK!@4src/Admin/Product/Attributes/Input/SizeTypeInput.phpnu[set_label( __( 'Size type', 'google-listings-and-ads' ) ); $this->set_description( __( 'The cut of the item. Recommended for apparel items.', 'google-listings-and-ads' ) ); } } PK!6E--/src/Admin/Product/Attributes/AttributesForm.phpnu[add_attribute( $attribute_type ); } parent::__construct( $data ); } /** * Return the data used for the input's view. * * @return array */ public function get_view_data(): array { $view_data = parent::get_view_data(); // add classes to hide/display attributes based on product type foreach ( $view_data['children'] as $index => $input ) { if ( ! isset( $this->attribute_types[ $index ] ) ) { continue; } $attribute_type = $this->attribute_types[ $index ]; $attribute_product_types = self::get_attribute_product_types( $attribute_type ); $hidden_types = $attribute_product_types['hidden']; $visible_types = $attribute_product_types['visible']; $input['gla_wrapper_class'] = $input['gla_wrapper_class'] ?? ''; if ( ! empty( $visible_types ) ) { $input['gla_wrapper_class'] .= ' show_if_' . join( ' show_if_', $visible_types ); } if ( ! empty( $hidden_types ) ) { $input['gla_wrapper_class'] .= ' hide_if_' . join( ' hide_if_', $hidden_types ); } $view_data['children'][ $index ] = $input; } return $view_data; } /** * Get the hidden and visible types of an attribute's applicable product types. * * @param string $attribute_type The name of an attribute class extending AttributeInterface. * * @return array */ public static function get_attribute_product_types( string $attribute_type ): array { $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); $applicable_product_types = call_user_func( [ $attribute_type, 'get_applicable_product_types' ] ); /** * This filter is documented in AttributeManager::map_attribute_types * * @see AttributeManager::map_attribute_types */ $applicable_product_types = apply_filters( "woocommerce_gla_attribute_applicable_product_types_{$attribute_id}", $applicable_product_types, $attribute_type ); /** * Filters the list of product types to hide the attribute for. */ $hidden_product_types = apply_filters( "woocommerce_gla_attribute_hidden_product_types_{$attribute_id}", [] ); $visible_product_types = array_diff( $applicable_product_types, $hidden_product_types ); return [ 'hidden' => $hidden_product_types, 'visible' => $visible_product_types, ]; } /** * @param InputInterface $input * @param AttributeInterface $attribute * * @return InputInterface */ public static function init_input( InputInterface $input, AttributeInterface $attribute ) { $input->set_id( $attribute::get_id() ) ->set_name( $attribute::get_id() ); $value_options = []; if ( $attribute instanceof WithValueOptionsInterface ) { $value_options = $attribute::get_value_options(); } $value_options = apply_filters( "woocommerce_gla_product_attribute_value_options_{$attribute::get_id()}", $value_options ); if ( ! empty( $value_options ) ) { if ( ! $input instanceof Select && ! $input instanceof SelectWithTextInput ) { $new_input = new SelectWithTextInput(); $new_input->set_label( $input->get_label() ) ->set_description( $input->get_description() ); // When GTIN uses the SelectWithTextInput field, copy the readonly/hidden attributes from the GTINInput field. if ( $input->name === 'gtin' ) { $gtin_input = new GTINInput(); $new_input->set_hidden( $gtin_input->is_hidden() ); $new_input->set_readonly( $gtin_input->is_readonly() ); } return self::init_input( $new_input, $attribute ); } // add a 'default' value option $value_options = [ '' => __( 'Default', 'google-listings-and-ads' ) ] + $value_options; $input->set_options( $value_options ); } return $input; } /** * Add an attribute to the form * * @param string $attribute_type The name of an attribute class extending AttributeInterface. * @param string|null $input_type The name of an input class extending InputInterface to use for attribute input. * * @return AttributesForm * * @throws InvalidValue If the attribute type is invalid or an invalid input type is specified for the attribute. * @throws FormException If form is already submitted. */ public function add_attribute( string $attribute_type, ?string $input_type = null ): AttributesForm { $this->validate_interface( $attribute_type, AttributeInterface::class ); // use the attribute's default input type if none provided. if ( empty( $input_type ) ) { $input_type = call_user_func( [ $attribute_type, 'get_input_type' ] ); } $this->validate_interface( $input_type, InputInterface::class ); $attribute_input = self::init_input( new $input_type(), new $attribute_type() ); if ( ! $attribute_input->is_hidden() ) { $this->add( $attribute_input ); $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); $this->attribute_types[ $attribute_id ] = $attribute_type; } return $this; } /** * Remove an attribute from the form * * @param string $attribute_type The name of an attribute class extending AttributeInterface. * * @return AttributesForm * * @throws InvalidValue If the attribute type is invalid or an invalid input type is specified for the attribute. * @throws FormException If form is already submitted. */ public function remove_attribute( string $attribute_type ): AttributesForm { $this->validate_interface( $attribute_type, AttributeInterface::class ); $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); unset( $this->attribute_types[ $attribute_id ] ); $this->remove( $attribute_id ); return $this; } /** * Sets the input type for the given attribute. * * @param string $attribute_type The name of an attribute class extending AttributeInterface. * @param string $input_type The name of an input class extending InputInterface to use for attribute input. * * @return $this * * @throws FormException If form is already submitted. */ public function set_attribute_input( string $attribute_type, string $input_type ): AttributesForm { if ( $this->is_submitted ) { throw FormException::cannot_modify_submitted(); } $this->validate_interface( $attribute_type, AttributeInterface::class ); $this->validate_interface( $input_type, InputInterface::class ); $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); if ( $this->has( $attribute_id ) ) { $this->children[ $attribute_id ] = self::init_input( new $input_type(), new $attribute_type() ); } return $this; } } PK!w.src/Admin/Product/Attributes/AttributesTab.phpnu[admin = $admin; $this->attribute_manager = $attribute_manager; $this->merchant_center = $merchant_center; } /** * Register a service. */ public function register(): void { // Register the hooks only if Merchant Center is set up. if ( ! $this->merchant_center->is_setup_complete() ) { return; } add_action( 'woocommerce_new_product', function ( int $product_id, WC_Product $product ) { $this->handle_update_product( $product ); }, 10, 2 ); add_action( 'woocommerce_update_product', function ( int $product_id, WC_Product $product ) { $this->handle_update_product( $product ); }, 10, 2 ); add_action( 'woocommerce_product_data_tabs', function ( array $tabs ) { return $this->add_tab( $tabs ); } ); add_action( 'woocommerce_product_data_panels', function () { $this->render_panel(); } ); } /** * Adds the Google for WooCommerce tab to the WooCommerce product data box. * * @param array $tabs The current product data tabs. * * @return array An array with product tabs with the Yoast SEO tab added. */ private function add_tab( array $tabs ): array { $tabs['gla_attributes'] = [ 'label' => 'Google for WooCommerce', 'class' => 'gla', 'target' => 'gla_attributes', ]; return $tabs; } /** * Render the product attributes tab. */ private function render_panel() { $product = wc_get_product( get_the_ID() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->admin->get_view( 'attributes/tab-panel', [ 'form' => $this->get_form( $product )->get_view_data() ] ); } /** * Handle form submission and update the product attributes. * * @param WC_Product $product */ private function handle_update_product( WC_Product $product ) { /** * Array of `true` values for each product IDs already handled by this method. Used to prevent double submission. * * @var bool[] $already_updated */ static $already_updated = []; if ( isset( $already_updated[ $product->get_id() ] ) ) { return; } $form = $this->get_form( $product ); $form_view_data = $form->get_view_data(); // phpcs:disable WordPress.Security.NonceVerification if ( empty( $_POST[ $form_view_data['name'] ] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $submitted_data = (array) wc_clean( wp_unslash( $_POST[ $form_view_data['name'] ] ) ); // phpcs:enable WordPress.Security.NonceVerification $form->submit( $submitted_data ); $this->update_data( $product, $form->get_data() ); $already_updated[ $product->get_id() ] = true; } /** * @param WC_Product $product * * @return AttributesForm */ protected function get_form( WC_Product $product ): AttributesForm { $attribute_types = $this->attribute_manager->get_attribute_types_for_product_types( $this->get_applicable_product_types() ); $form = new AttributesForm( $attribute_types, $this->attribute_manager->get_all_values( $product ) ); $form->set_name( 'attributes' ); return $form; } /** * @param WC_Product $product * @param array $data * * @return void */ protected function update_data( WC_Product $product, array $data ): void { foreach ( $this->attribute_manager->get_attribute_types_for_product( $product ) as $attribute_id => $attribute_type ) { if ( isset( $data[ $attribute_id ] ) ) { $this->attribute_manager->update( $product, new $attribute_type( $data[ $attribute_id ] ) ); } } } } PK!H~zz0src/Admin/Product/Attributes/AttributesTrait.phpnu[admin = $admin; $this->attribute_manager = $attribute_manager; $this->merchant_center = $merchant_center; } /** * Register a service. */ public function register(): void { // Register the hooks only if Merchant Center is set up. if ( ! $this->merchant_center->is_setup_complete() ) { return; } add_action( 'woocommerce_product_after_variable_attributes', function ( int $variation_index, array $variation_data, WP_Post $variation ) { $this->render_attributes_form( $variation_index, $variation ); }, 90, 3 ); add_action( 'woocommerce_save_product_variation', function ( int $variation_id, int $variation_index ) { $this->handle_save_variation( $variation_id, $variation_index ); }, 10, 2 ); } /** * Render the attributes form for variations. * * @param int $variation_index Position in the loop. * @param WP_Post $variation Post data. */ private function render_attributes_form( int $variation_index, WP_Post $variation ) { /** * @var WC_Product_Variation $product */ $product = wc_get_product( $variation->ID ); $data = $this->get_form( $product, $variation_index )->get_view_data(); // Do not render the form if it doesn't contain any child attributes. $attributes = reset( $data['children'] ); if ( empty( $data['children'] ) || empty( $attributes['children'] ) ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->admin->get_view( 'attributes/variations-form', $data ); } /** * Handle form submission and update the product attributes. * * @param int $variation_id * @param int $variation_index */ private function handle_save_variation( int $variation_id, int $variation_index ) { /** * @var WC_Product_Variation $variation */ $variation = wc_get_product( $variation_id ); $form = $this->get_form( $variation, $variation_index ); $form_view_data = $form->get_view_data(); // phpcs:disable WordPress.Security.NonceVerification if ( empty( $_POST[ $form_view_data['name'] ] ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $submitted_data = (array) wc_clean( wp_unslash( $_POST[ $form_view_data['name'] ] ) ); // phpcs:enable WordPress.Security.NonceVerification $form->submit( $submitted_data ); $form_data = $form->get_data(); if ( ! empty( $form_data[ $variation_index ] ) ) { $this->update_data( $variation, $form_data[ $variation_index ] ); } } /** * @param WC_Product_Variation $variation * @param int $variation_index * * @return Form */ protected function get_form( WC_Product_Variation $variation, int $variation_index ): Form { $attribute_types = $this->attribute_manager->get_attribute_types_for_product( $variation ); $attribute_form = new AttributesForm( $attribute_types ); $attribute_form->set_name( (string) $variation_index ); $form = new Form(); $form->set_name( 'variation_attributes' ) ->add( $attribute_form ) ->set_data( [ (string) $variation_index => $this->attribute_manager->get_all_values( $variation ) ] ); return $form; } /** * @param WC_Product_Variation $variation * @param array $data * * @return void */ protected function update_data( WC_Product_Variation $variation, array $data ): void { foreach ( $this->attribute_manager->get_attribute_types_for_product( $variation ) as $attribute_id => $attribute_type ) { if ( isset( $data[ $attribute_id ] ) ) { $this->attribute_manager->update( $variation, new $attribute_type( $data[ $attribute_id ] ) ); } } } } PK!v##,src/Admin/Product/ChannelVisibilityBlock.phpnu[product_helper = $product_helper; $this->merchant_center = $merchant_center; } /** * Register hooks for querying and updating product via REST APIs. */ public function register(): void { if ( ! $this->merchant_center->is_setup_complete() ) { return; } // https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php#L182-L192 add_filter( 'woocommerce_rest_prepare_product_object', [ $this, 'prepare_data' ], 10, 2 ); // https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php#L200-L207 // https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php#L247-L254 add_action( 'woocommerce_rest_insert_product_object', [ $this, 'update_data' ], 10, 2 ); } /** * Get channel visibility data from the given product and add it to the given response. * * @param Response $response Response to be added channel visibility data. * @param WC_Product|WC_Data $product WooCommerce product to get data. * * @return Response */ public function prepare_data( Response $response, WC_Data $product ): Response { if ( ! $product instanceof WC_Product ) { return $response; } $response->data[ self::PROPERTY ] = [ 'is_visible' => $product->is_visible(), 'channel_visibility' => $this->product_helper->get_channel_visibility( $product ), 'sync_status' => $this->product_helper->get_sync_status( $product ), 'issues' => $this->product_helper->get_validation_errors( $product ), ]; return $response; } /** * Get channel visibility data from the given request and update it to the given product. * * @param WC_Product|WC_Data $product WooCommerce product to be updated. * @param Request $request Response to get the channel visibility data. */ public function update_data( WC_Data $product, Request $request ): void { if ( ! $product instanceof WC_Product || ! in_array( $product->get_type(), $this->get_visible_product_types(), true ) ) { return; } $params = $request->get_params(); if ( ! isset( $params[ self::PROPERTY ] ) ) { return; } $channel_visibility = $params[ self::PROPERTY ]['channel_visibility']; if ( $channel_visibility !== $this->product_helper->get_channel_visibility( $product ) ) { $this->product_helper->update_channel_visibility( $product, $channel_visibility ); } } /** * Return the visible product types to control the hidden condition of the channel visibility block * in the Product Block Editor. * * @return array */ public function get_visible_product_types(): array { return array_diff( ProductSyncer::get_supported_product_types(), [ 'variation' ] ); } /** * Return the config used for the input's block within the Product Block Editor. * * @return array */ public function get_block_config(): array { $options = []; foreach ( ChannelVisibility::get_value_options() as $key => $value ) { $options[] = [ 'label' => $value, 'value' => $key, ]; } $attributes = [ 'property' => self::PROPERTY, 'options' => $options, 'valueOfSync' => ChannelVisibility::SYNC_AND_SHOW, 'valueOfDontSync' => ChannelVisibility::DONT_SYNC_AND_SHOW, 'statusOfSynced' => SyncStatus::SYNCED, 'statusOfHasErrors' => SyncStatus::HAS_ERRORS, ]; return [ 'id' => 'google-listings-and-ads-product-channel-visibility', 'blockName' => 'google-listings-and-ads/product-channel-visibility', 'attributes' => $attributes, ]; } } PK!**src/Admin/Admin.phpnu[assets_handler = $assets_handler; $this->view_factory = $view_factory; $this->merchant_center = $merchant_center; $this->ads = $ads; } /** * Register a service. */ public function register(): void { add_action( 'admin_enqueue_scripts', function () { if ( PageController::is_admin_page() ) { // Enqueue the required JavaScript scripts and CSS styles of the Media library. wp_enqueue_media(); } $assets = $this->get_assets(); $this->assets_handler->register_many( $assets ); $this->assets_handler->enqueue_many( $assets ); } ); add_action( "plugin_action_links_{$this->get_plugin_basename()}", function ( $links ) { return $this->add_plugin_links( $links ); } ); add_action( 'wp_default_scripts', function ( $scripts ) { $this->inject_fast_refresh_for_dev( $scripts ); }, 20 ); add_action( 'admin_init', [ $this, 'privacy_policy' ] ); } /** * Return an array of assets. * * @return Asset[] */ protected function get_assets(): array { $wc_admin_condition = function () { return PageController::is_admin_page(); }; $assets[] = ( new AdminScriptWithBuiltDependenciesAsset( 'google-listings-and-ads', 'js/build/index', "{$this->get_root_dir()}/js/build/index.asset.php", new BuiltScriptDependencyArray( [ 'dependencies' => [], 'version' => (string) filemtime( "{$this->get_root_dir()}/js/build/index.js" ), ] ), $wc_admin_condition ) )->add_inline_script( 'glaData', [ 'slug' => $this->get_slug(), 'mcSetupComplete' => $this->merchant_center->is_setup_complete(), 'mcSupportedCountry' => $this->merchant_center->is_store_country_supported(), 'mcSupportedLanguage' => $this->merchant_center->is_language_supported(), 'adsCampaignConvertStatus' => $this->options->get( OptionsInterface::CAMPAIGN_CONVERT_STATUS ), 'adsSetupComplete' => $this->ads->is_setup_complete(), 'enableReports' => $this->enableReports(), 'dateFormat' => get_option( 'date_format' ), 'timeFormat' => get_option( 'time_format' ), 'siteLogoUrl' => wp_get_attachment_image_url( get_theme_mod( 'custom_logo' ), 'full' ), 'initialWpData' => [ 'version' => $this->get_version(), 'mcId' => $this->options->get_merchant_id() ?: null, 'adsId' => $this->options->get_ads_id() ?: null, ], ] ); $assets[] = ( new AdminStyleAsset( 'google-listings-and-ads-css', '/js/build/index', defined( 'WC_ADMIN_PLUGIN_FILE' ) ? [ 'wc-admin-app' ] : [], (string) filemtime( "{$this->get_root_dir()}/js/build/index.css" ), $wc_admin_condition ) ); $product_condition = function () { $screen = get_current_screen(); return ( null !== $screen && 'product' === $screen->id ); }; $assets[] = ( new AdminScriptWithBuiltDependenciesAsset( 'gla-product-attributes', 'js/build/product-attributes', "{$this->get_root_dir()}/js/build/product-attributes.asset.php", new BuiltScriptDependencyArray( [ 'dependencies' => [], 'version' => (string) filemtime( "{$this->get_root_dir()}/js/build/product-attributes.js" ), ] ), $product_condition ) )->add_inline_script( 'glaProductData', [ 'applicableProductTypes' => ProductSyncer::get_supported_product_types(), ] ); $assets[] = ( new AdminStyleAsset( 'gla-product-attributes-css', 'js/build/product-attributes', [], '', $product_condition ) ); return $assets; } /** * Adds links to the plugin's row in the "Plugins" wp-admin page. * * @see https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name) * @param array $links The existing list of links that will be rendered. */ protected function add_plugin_links( $links ): array { $plugin_links = []; // Display settings url if setup is complete otherwise link to get started page if ( $this->merchant_center->is_setup_complete() ) { $plugin_links[] = sprintf( '%2$s', esc_attr( $this->get_settings_url() ), esc_html__( 'Settings', 'google-listings-and-ads' ) ); } else { $plugin_links[] = sprintf( '%2$s', esc_attr( $this->get_start_url() ), esc_html__( 'Get Started', 'google-listings-and-ads' ) ); } $plugin_links[] = sprintf( '%2$s', esc_attr( $this->get_documentation_url() ), esc_html__( 'Documentation', 'google-listings-and-ads' ) ); // Add new links to the beginning return array_merge( $plugin_links, $links ); } /** * Adds a meta box. * * @param MetaBoxInterface $meta_box */ public function add_meta_box( MetaBoxInterface $meta_box ) { add_filter( "postbox_classes_{$meta_box->get_screen()}_{$meta_box->get_id()}", function ( array $classes ) use ( $meta_box ) { return array_merge( $classes, $meta_box->get_classes() ); } ); add_meta_box( $meta_box->get_id(), $meta_box->get_title(), $meta_box->get_callback(), $meta_box->get_screen(), $meta_box->get_context(), $meta_box->get_priority(), $meta_box->get_callback_args() ); } /** * @param string $view Name of the view * @param array $context_variables Array of variables to pass to the view * * @return string The rendered view * * @throws ViewException If the view doesn't exist or can't be loaded. */ public function get_view( string $view, array $context_variables = [] ): string { return $this->view_factory->create( $view ) ->render( $context_variables ); } /** * Only show reports if we enable it through a snippet. * * @return bool Whether reports should be enabled . */ protected function enableReports(): bool { return apply_filters( 'woocommerce_gla_enable_reports', true ); } /** * Add suggested privacy policy content * * @return void */ public function privacy_policy() { $policy_text = sprintf( /* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */ esc_html__( 'By using this extension, you may be storing personal data or sharing data with an external service. %1$sLearn more about what data is collected by Google and what you may want to include in your privacy policy%2$s.', 'google-listings-and-ads' ), '', '' ); // As the extension doesn't offer suggested privacy policy text, the button to copy it is hidden. $content = '

' . $policy_text . '

'; wp_add_privacy_policy_content( 'Google for WooCommerce', wpautop( $content, false ) ); } /** * This method is ONLY used during development. * * The runtime.js file is created when the front-end is developed in Fast Refresh mode * and must be loaded together to enable the mode. * * When Gutenberg is not installed or not activated, the react dependency will not have * the 'wp-react-refresh-entry' handle, so here injects the Fast Refresh scripts we built. * * The Fast Refresh also needs the development version of React and ReactDOM. * They will be replaced if the SCRIPT_DEBUG flag is not enabled. * * @param WP_Scripts $scripts WP_Scripts instance. */ private function inject_fast_refresh_for_dev( $scripts ) { $runtime_path = "{$this->get_root_dir()}/js/build/runtime.js"; if ( ! file_exists( $runtime_path ) ) { return; } $react_script = $scripts->query( 'react', 'registered' ); if ( ! $react_script ) { return; } if ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ) { $react_dom_script = $scripts->query( 'react-dom', 'registered' ); $react_dom_script->src = str_replace( '.min', '', $react_dom_script->src ); $react_script->src = str_replace( '.min', '', $react_script->src ); } $plugin_url = $this->get_plugin_url(); $scripts->add( 'gla-webpack-runtime', "{$plugin_url}/js/build/runtime.js", [], (string) filemtime( $runtime_path ) ); $react_script->deps[] = 'gla-webpack-runtime'; if ( ! in_array( 'wp-react-refresh-entry', $react_script->deps, true ) ) { $scripts->add( 'wp-react-refresh-runtime', "{$plugin_url}/js/build-dev/react-refresh-runtime.js", [] ); $scripts->add( 'wp-react-refresh-entry', "{$plugin_url}/js/build-dev/react-refresh-entry.js", [ 'wp-react-refresh-runtime' ] ); $react_script->deps[] = 'wp-react-refresh-entry'; } } } PK!H022"src/Admin/ProductBlocksService.phpnu[assets_handler = $assets_handler; $this->attribute_manager = $attribute_manager; $this->merchant_center = $merchant_center; $this->channel_visibility_block = $channel_visibility_block; } /** * Return whether this service is needed to be registered. * * @return bool Whether this service is needed to be registered. */ public static function is_needed(): bool { // compatibility-code "WC >= 8.6" -- The Block Template API used requires at least WooCommerce 8.6 return version_compare( WC_VERSION, '8.6', '>=' ); } /** * Register a service. */ public function register(): void { if ( PageController::is_admin_page() ) { add_action( 'init', [ $this, 'hook_init' ] ); } // https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/AbstractProductFormTemplate.php#L19 $template_area = 'product-form'; // https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php#L19 // https://github.com/woocommerce/woocommerce/blob/8.6.0/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/ProductVariationTemplate.php#L19 $block_id = 'general'; add_action( "woocommerce_block_template_area_{$template_area}_after_add_block_{$block_id}", [ $this, 'hook_block_template' ] ); } /** * Action hanlder for the 'init' hook. */ public function hook_init(): void { $build_path = "{$this->get_root_dir()}/js/build"; $uri = 'js/build/blocks'; $this->register_custom_blocks( BlockRegistry::get_instance(), $build_path, $uri, self::CUSTOM_BLOCKS ); } /** * Action hanlder for the "woocommerce_block_template_area_{$template_area}_after_add_block_{$block_id}" hook. * * @param BlockInterface $block The block just added to get its root template to add this extension's group and blocks. */ public function hook_block_template( BlockInterface $block ): void { /** @var Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface */ $template = $block->get_root_template(); $is_variation_template = $this->is_variation_template( $block ); // Please note that the simple, variable, grouped, and external product types // use the same product block template 'simple-product'. Their dynamic hidden // conditions are added below. if ( 'simple-product' !== $template->get_id() && ! $is_variation_template ) { return; } /** @var Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\GroupInterface */ $group = $template->add_group( [ 'id' => 'google-listings-and-ads-group', 'order' => 100, 'attributes' => [ 'title' => __( 'Google for WooCommerce', 'google-listings-and-ads' ), ], ] ); $visible_product_types = ProductSyncer::get_supported_product_types(); if ( $is_variation_template ) { // The property of `editedProduct.type` doesn't exist in the variation product. // The condition returned from `get_hide_condition` won't work, so it uses 'true' directly. if ( ! in_array( 'variation', $visible_product_types, true ) ) { $group->add_hide_condition( 'true' ); } } else { $group->add_hide_condition( $this->get_hide_condition( $visible_product_types ) ); } if ( ! $this->merchant_center->is_setup_complete() ) { $group->add_block( [ 'id' => 'google-listings-and-ads-product-onboarding-prompt', 'blockName' => 'google-listings-and-ads/product-onboarding-prompt', 'attributes' => [ 'startUrl' => $this->get_start_url(), ], ] ); return; } /** @var SectionInterface */ $channel_visibility_section = $group->add_section( [ 'id' => 'google-listings-and-ads-channel-visibility-section', 'order' => 1, 'attributes' => [ 'title' => __( 'Channel visibility', 'google-listings-and-ads' ), ], ] ); if ( ! $is_variation_template ) { $this->add_channel_visibility_block( $channel_visibility_section ); } // Add the hidden condition to the channel visibility section because it only has one block. $visible_product_types = $this->channel_visibility_block->get_visible_product_types(); $channel_visibility_section->add_hide_condition( $this->get_hide_condition( $visible_product_types ) ); /** @var SectionInterface */ $product_attributes_section = $group->add_section( [ 'id' => 'google-listings-and-ads-product-attributes-section', 'order' => 2, 'attributes' => [ 'title' => __( 'Product attributes', 'google-listings-and-ads' ), ], ] ); $this->add_product_attribute_blocks( $product_attributes_section ); } /** * Register the custom blocks and their assets. * * @param BlockRegistry $block_registry BlockRegistry instance getting from Woo Core for registering custom blocks. * @param string $build_path The absolute path to the build directory of the assets. * @param string $uri The script URI of the custom blocks. * @param string[] $custom_blocks The directory names of each custom block under the build path. */ public function register_custom_blocks( BlockRegistry $block_registry, string $build_path, string $uri, array $custom_blocks ): void { foreach ( $custom_blocks as $custom_block ) { $block_json_file = "{$build_path}/{$custom_block}/block.json"; if ( ! file_exists( $block_json_file ) ) { continue; } $block_registry->register_block_type_from_metadata( $block_json_file ); } $assets[] = new AdminScriptWithBuiltDependenciesAsset( 'google-listings-and-ads-product-blocks', $uri, "{$build_path}/blocks.asset.php", new BuiltScriptDependencyArray( [ 'dependencies' => [], 'version' => (string) filemtime( "{$build_path}/blocks.js" ), ] ) ); $assets[] = new AdminStyleAsset( 'google-listings-and-ads-product-blocks-css', $uri, [], (string) filemtime( "{$build_path}/blocks.css" ) ); $this->assets_handler->register_many( $assets ); $this->assets_handler->enqueue_many( $assets ); } /** * Add the channel visibility block to the given section block. * * @param SectionInterface $section The section block to add the channel visibility block */ private function add_channel_visibility_block( SectionInterface $section ): void { $section->add_block( $this->channel_visibility_block->get_block_config() ); } /** * Add product attribute blocks to the given section block. * * @param SectionInterface $section The section block to add product attribute blocks */ private function add_product_attribute_blocks( SectionInterface $section ): void { $is_variation_template = $this->is_variation_template( $section ); $product_types = $is_variation_template ? [ 'variation' ] : $this->get_applicable_product_types(); $attribute_types = $this->attribute_manager->get_attribute_types_for_product_types( $product_types ); foreach ( $attribute_types as $attribute_type ) { $input_type = call_user_func( [ $attribute_type, 'get_input_type' ] ); $input = AttributesForm::init_input( new $input_type(), new $attribute_type() ); // Avoid to render Inputs that are defined as hidden in the Input. // i.e We don't render GTIN for new WC versions anymore. if ( $input->is_hidden() ) { continue; } if ( $is_variation_template ) { // When editing a variation, its product type on the frontend side won't be changed dynamically. // In addition, the property of `editedProduct.type` doesn't exist in the variation product. // Therefore, instead of using the ProductTemplates API `add_hide_condition` to conditionally // hide attributes, it doesn't add invisible attribute blocks from the beginning. if ( $this->is_visible_for_variation( $attribute_type ) ) { $section->add_block( $input->get_block_config() ); } } else { $visible_product_types = AttributesForm::get_attribute_product_types( $attribute_type )['visible']; // When editing a simple, variable, grouped, or external product, its product type on the // frontend side can be changed dynamically. So, it needs to use the ProductTemplates API // `add_hide_condition` to conditionally hide attributes. /** @var BlockInterface */ $block = $section->add_block( $input->get_block_config() ); $block->add_hide_condition( $this->get_hide_condition( $visible_product_types ) ); } } } /** * Determine if the product block template of the given block is the variation template. * * @param BlockInterface $block The block to be checked * * @return boolean */ private function is_variation_template( BlockInterface $block ): bool { return 'product-variation' === $block->get_root_template()->get_id(); } /** * Determine if the given attribute is visible for variation product after applying related filters. * * @param string $attribute_type An attribute class extending AttributeInterface * * @return bool */ private function is_visible_for_variation( string $attribute_type ): bool { $attribute_product_types = AttributesForm::get_attribute_product_types( $attribute_type ); return in_array( 'variation', $attribute_product_types['visible'], true ); } /** * Get the expression of the hide condition to a block based on the visible product types. * e.g. "editedProduct.type !== 'simple' && ! editedProduct.parent_id > 0" * * The hide condition is a JavaScript-like expression that will be evaluated on the client to determine if the block should be hidden. * See [@woocommerce/expression-evaluation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/expression-evaluation/README.md) for more details. * * @param array $visible_product_types The visible product types to be converted to a hidden condition * * @return string */ public function get_hide_condition( array $visible_product_types ): string { $conditions = array_map( function ( $type ) { return "editedProduct.type !== '{$type}'"; }, $visible_product_types ); return implode( ' && ', $conditions ) ?: 'true'; } } PK!"=src/Admin/Redirect.phpnu[ Dashboard::PATH, 'get_started' => GetStarted::PATH, ]; /** * @var WP */ protected $wp; /** * Redirect constructor. * * @param WP $wp */ public function __construct( WP $wp ) { $this->wp = $wp; } /** * Register a service. * * @return void */ public function register(): void { add_action( 'admin_init', function () { $this->maybe_redirect(); } ); } /** * Activate a service. * * @return void */ public function activate(): void { // Do not take any action if activated in a REST request (via wc-admin). if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } if ( // Only redirect to onboarding when activated on its own. Either with a link... ( isset( $_GET['action'] ) && 'activate' === $_GET['action'] ) // phpcs:ignore WordPress.Security.NonceVerification // ...or with a bulk action. || ( isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) && 1 === count( $_POST['checked'] ) ) // phpcs:ignore WordPress.Security.NonceVerification ) { $this->options->update( self::OPTION, 'yes' ); } } /** * Checks if merchant should be redirected to the onboarding page if it is not. * * @return void */ public function maybe_redirect() { if ( $this->wp->wp_doing_ajax() ) { return; } // Maybe redirect to onboarding after activation if ( 'yes' === $this->options->get( self::OPTION ) ) { return $this->maybe_redirect_after_activation(); } // If setup ISNT complete then redirect from dashboard to onboarding if ( ! $this->merchant_center->is_setup_complete() && $this->is_current_wc_admin_page( self::PATHS['dashboard'] ) ) { return $this->redirect_to( self::PATHS['get_started'] ); } // If setup IS complete then redirect from onboarding to dashboard if ( $this->merchant_center->is_setup_complete() && $this->is_current_wc_admin_page( self::PATHS['get_started'] ) ) { return $this->redirect_to( self::PATHS['dashboard'] ); } return false; } /** * Checks if merchant should be redirected to the onboarding page after extension activation. * * @return bool True if the redirection should have happened */ protected function maybe_redirect_after_activation(): bool { // Do not redirect if setup is already complete if ( $this->merchant_center->is_setup_complete() ) { $this->options->update( self::OPTION, 'no' ); return false; } // if we are on the get started page don't redirect again if ( $this->is_current_wc_admin_page( self::PATHS['get_started'] ) ) { $this->options->update( self::OPTION, 'no' ); return false; } // Redirect if setup is not complete $this->redirect_to( self::PATHS['get_started'] ); return true; } /** * Utility function to immediately redirect to a given WC Admin path. * Note that this function immediately ends the execution. * * @param string $path The WC Admin path to redirect to * * @return void */ public function redirect_to( $path ): void { // If we are already on this path, do nothing. if ( $this->is_current_wc_admin_page( $path ) ) { return; } $params = [ 'page' => PageController::PAGE_ROOT, 'path' => $path, ]; wp_safe_redirect( admin_url( add_query_arg( $params, 'admin.php' ) ) ); exit(); } /** * Check if the current WC Admin page matches the given path. * * @param string $path The path to check. * * @return bool */ public function is_current_wc_admin_page( $path ): bool { $params = [ 'page' => PageController::PAGE_ROOT, 'path' => $path, ]; return 2 === count( array_intersect_assoc( $_GET, $params ) ); // phpcs:disable WordPress.Security.NonceVerification.Recommended } } PK!P@ - -src/Ads/AccountService.phpnu[state = $state; } /** * Get Ads accounts associated with the connected Google account. * * @return array * @throws Exception When an API error occurs. */ public function get_accounts(): array { return $this->container->get( Ads::class )->get_ads_accounts(); } /** * Get the connected ads account. * * @return array */ public function get_connected_account(): array { $id = $this->options->get_ads_id(); $status = [ 'id' => $id, 'currency' => $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ), 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $this->options->get( OptionsInterface::ADS_ACCOUNT_CURRENCY ) ), ENT_QUOTES ), 'status' => $id ? 'connected' : 'disconnected', ]; $incomplete = $this->state->last_incomplete_step(); if ( ! empty( $incomplete ) ) { $status['status'] = 'incomplete'; $status['step'] = $incomplete; } $status += $this->state->get_step_data( 'set_id' ); return $status; } /** * Use an existing Ads account. Mark the 'set_id' step as done and sets the Ads ID. * * @param int $account_id The Ads account ID to use. * * @throws Exception If there is already an Ads account ID. */ public function use_existing_account( int $account_id ) { $ads_id = $this->options->get_ads_id(); if ( $ads_id && $ads_id !== $account_id ) { throw new Exception( /* translators: 1: is a numeric account ID */ sprintf( __( 'Ads account %1$d already connected.', 'google-listings-and-ads' ), $ads_id ) ); } $state = $this->state->get(); // Don't do anything if this step was already finished. if ( AdsAccountState::STEP_DONE === $state['set_id']['status'] ) { return; } $this->container->get( Middleware::class )->link_ads_account( $account_id ); // Skip billing setup flow when using an existing account. $state['set_id']['status'] = AdsAccountState::STEP_DONE; $state['billing']['status'] = AdsAccountState::STEP_DONE; $this->state->update( $state ); } /** * Performs the steps necessary to setup an ads account. * Should always resume up at the last pending or unfinished step. * If the Ads account has already been created, the ID is simply returned. * * @return array The newly created (or pre-existing) Ads ID. * @throws Exception If an error occurs during any step. */ public function setup_account(): array { $state = $this->state->get(); $ads_id = $this->options->get_ads_id(); $account = [ 'id' => $ads_id ]; foreach ( $state as $name => &$step ) { if ( AdsAccountState::STEP_DONE === $step['status'] ) { continue; } try { switch ( $name ) { case 'set_id': // Just in case, don't create another Ads ID. if ( ! empty( $ads_id ) ) { break; } $account = $this->container->get( Middleware::class )->create_ads_account(); $step['data']['sub_account'] = true; $step['data']['created_timestamp'] = time(); break; case 'billing': $this->check_billing_status( $account ); break; case 'conversion_action': $this->create_conversion_action(); break; case 'link_merchant': // Continue to next step if the MC account is not connected yet. if ( ! $this->options->get_merchant_id() ) { // Save step as pending and continue the foreach loop with `continue 2`. $state[ $name ]['status'] = AdsAccountState::STEP_PENDING; $this->state->update( $state ); continue 2; } $this->link_merchant_account(); break; case 'account_access': $this->check_ads_account_has_access(); break; default: throw new Exception( /* translators: 1: is a string representing an unknown step name */ sprintf( __( 'Unknown ads account creation step %1$s', 'google-listings-and-ads' ), $name ) ); } $step['status'] = AdsAccountState::STEP_DONE; $step['message'] = ''; $this->state->update( $state ); } catch ( Exception $e ) { $step['status'] = AdsAccountState::STEP_ERROR; $step['message'] = $e->getMessage(); $this->state->update( $state ); throw $e; } } return $account; } /** * Gets the billing setup status and returns a setup URL if available. * * @return array */ public function get_billing_status(): array { $status = $this->container->get( Ads::class )->get_billing_status(); if ( BillingSetupStatus::APPROVED === $status ) { $this->state->complete_step( 'billing' ); return [ 'status' => $status ]; } $billing_url = $this->options->get( OptionsInterface::ADS_BILLING_URL ); // Check if user has provided the access and ocid is present. $connection_status = $this->container->get( Connection::class )->get_status(); $email = $connection_status['email'] ?? ''; $has_access = $this->container->get( Ads::class )->has_access( $email ); $ocid = $this->options->get( OptionsInterface::ADS_ACCOUNT_OCID, null ); // Link directly to the payment page if the customer already has access. if ( $has_access ) { $billing_url = add_query_arg( [ 'ocid' => $ocid ?: 0, ], 'https://ads.google.com/aw/signup/payment' ); } return [ 'status' => $status, 'billing_url' => $billing_url, ]; } /** * Check if the Ads account has access. * * @throws ExceptionWithResponseData If the account doesn't have access. */ private function check_ads_account_has_access() { $access_status = $this->get_ads_account_has_access(); if ( ! $access_status['has_access'] ) { throw new ExceptionWithResponseData( __( 'Account must be accepted before completing setup.', 'google-listings-and-ads' ), 428, null, $access_status ); } } /** * Gets the Ads account access status. * * @return array { * Returns the access status, last completed account setup step, * and invite link if available. * * @type bool $has_access Whether the customer has access to the account. * @type string $step The last completed setup step for the Ads account. * @type string $invite_link The URL to the invite link. * } */ public function get_ads_account_has_access() { $has_access = false; // Check if an Ads ID is present. if ( $this->options->get_ads_id() ) { $connection_status = $this->container->get( Connection::class )->get_status(); $email = $connection_status['email'] ?? ''; } // If no email, means google account is not connected. if ( ! empty( $email ) ) { $has_access = $this->container->get( Ads::class )->has_access( $email ); } // If we have access, complete the step so that it won't be called next time. if ( $has_access ) { $this->state->complete_step( 'account_access' ); } return [ 'has_access' => $has_access, 'step' => $this->state->last_incomplete_step(), 'invite_link' => $this->options->get( OptionsInterface::ADS_BILLING_URL, '' ), ]; } /** * Disconnect Ads account */ public function disconnect() { $this->options->delete( OptionsInterface::ADS_ACCOUNT_CURRENCY ); $this->options->delete( OptionsInterface::ADS_ACCOUNT_OCID ); $this->options->delete( OptionsInterface::ADS_ACCOUNT_STATE ); $this->options->delete( OptionsInterface::ADS_BILLING_URL ); $this->options->delete( OptionsInterface::ADS_CONVERSION_ACTION ); $this->options->delete( OptionsInterface::ADS_ID ); $this->options->delete( OptionsInterface::ADS_SETUP_COMPLETED_AT ); $this->options->delete( OptionsInterface::CAMPAIGN_CONVERT_STATUS ); $this->container->get( TransientsInterface::class )->delete( TransientsInterface::ADS_CAMPAIGN_COUNT ); } /** * Confirm the billing flow has been completed. * * @param array $account Account details. * * @throws ExceptionWithResponseData If this step hasn't been completed yet. */ private function check_billing_status( array $account ) { $status = BillingSetupStatus::UNKNOWN; // Only check billing status if we haven't just created the account. if ( empty( $account['billing_url'] ) ) { $status = $this->container->get( Ads::class )->get_billing_status(); } if ( BillingSetupStatus::APPROVED !== $status ) { throw new ExceptionWithResponseData( __( 'Billing setup must be completed.', 'google-listings-and-ads' ), 428, null, [ 'billing_url' => $this->options->get( OptionsInterface::ADS_BILLING_URL ), 'billing_status' => $status, ] ); } } /** * Get the callback function for linking a merchant account. * * @throws Exception When the ads account hasn't been set yet. */ private function link_merchant_account() { if ( ! $this->options->get_ads_id() ) { throw new Exception( 'An Ads account must be connected' ); } $mc_state = $this->container->get( MerchantAccountState::class ); // Create link for Merchant and accept it in Ads. $this->container->get( Merchant::class )->link_ads_id( $this->options->get_ads_id() ); $this->container->get( Ads::class )->accept_merchant_link( $this->options->get_merchant_id() ); $mc_state->complete_step( 'link_ads' ); } /** * Create the generic GLA conversion action and store the details as an option. * * @throws Exception If the conversion action can't be created. */ private function create_conversion_action(): void { $action = $this->container->get( AdsConversionAction::class )->create_conversion_action(); $this->options->update( OptionsInterface::ADS_CONVERSION_ACTION, $action ); } } PK!sssrc/Ads/AdsAwareInterface.phpnu[ads_service = $ads_service; } } PK!?bAsrc/Ads/AdsService.phpnu[account_state = $account_state; } /** * Determine whether Ads setup has been started. * * @since 1.11.0 * @return bool */ public function is_setup_started(): bool { return $this->account_state->last_incomplete_step() !== '' && ! $this->is_setup_complete(); } /** * Determine whether Ads setup has completed. * * @return bool */ public function is_setup_complete(): bool { return boolval( $this->options->get( OptionsInterface::ADS_SETUP_COMPLETED_AT, false ) ); } /** * Determine whether Ads has connected. * * @return bool */ public function is_connected(): bool { $google_connected = boolval( $this->options->get( OptionsInterface::GOOGLE_CONNECTED, false ) ); return $google_connected && $this->is_setup_complete(); } /** * Determine whether the Ads account is connected, even when pending billing. * * @return bool */ public function connected_account(): bool { $id = $this->options->get_ads_id(); $last_step = $this->account_state->last_incomplete_step(); return $id && ( $last_step === '' || $last_step === 'billing' ); } } PK!kO6_6_#src/Ads/AssetSuggestionsService.phpnu[ [ 'minimum' => [ 600, 314 ], 'recommended' => [ 1200, 628 ], 'max_qty' => 8, ], self::SQUARE_MARKETING_IMAGE_KEY => [ 'minimum' => [ 300, 300 ], 'recommended' => [ 1200, 1200 ], 'max_qty' => 8, ], self::PORTRAIT_MARKETING_IMAGE_KEY => [ 'minimum' => [ 480, 600 ], 'recommended' => [ 960, 1200 ], 'max_qty' => 4, ], self::LOGO_IMAGE_KEY => [ 'minimum' => [ 128, 128 ], 'recommended' => [ 1200, 1200 ], 'max_qty' => 20, ], ]; /** * Default maximum marketing images. */ protected const DEFAULT_MAXIMUM_MARKETING_IMAGES = 20; /** * The subsize key for the square marketing image. */ protected const SQUARE_MARKETING_IMAGE_KEY = 'gla_square_marketing_asset'; /** * The subsize key for the marketing image. */ protected const MARKETING_IMAGE_KEY = 'gla_marketing_asset'; /** * The subsize key for the portrait marketing image. */ protected const PORTRAIT_MARKETING_IMAGE_KEY = 'gla_portrait_marketing_asset'; /** * The subsize key for the logo image. */ protected const LOGO_IMAGE_KEY = 'gla_logo_asset'; /** * The homepage key ID. */ protected const HOMEPAGE_KEY_ID = 0; /** * AssetSuggestionsService constructor. * * @param WP $wp WP Proxy. * @param WC $wc WC Proxy. * @param ImageUtility $image_utility Image utility. * @param wpdb $wpdb WordPress database access abstraction class. * @param AdsAssetGroupAsset $asset_group_asset The AdsAssetGroupAsset class. */ public function __construct( WP $wp, WC $wc, ImageUtility $image_utility, wpdb $wpdb, AdsAssetGroupAsset $asset_group_asset ) { $this->wp = $wp; $this->wc = $wc; $this->wpdb = $wpdb; $this->image_utility = $image_utility; $this->asset_group_asset = $asset_group_asset; } /** * Get WP and other campaigns' assets from the specific post or term. * * @param int $id Post ID, Term ID or self::HOMEPAGE_KEY_ID if it's the homepage. * @param string $type Only possible values are post, term and homepage. */ public function get_assets_suggestions( int $id, string $type ): array { $asset_group_assets = $this->get_asset_group_asset_suggestions( $id, $type ); if ( ! empty( $asset_group_assets ) ) { return $asset_group_assets; } return $this->get_wp_assets( $id, $type ); } /** * Get URL for a specific post or term. * * @param int $id Post ID, Term ID or self::HOMEPAGE_KEY_ID * @param string $type Only possible values are post, term and homepage. * * @return string The URL. * @throws Exception If the ID is invalid. */ protected function get_url( int $id, string $type ): string { if ( $type === 'post' ) { $url = get_permalink( $id ); } elseif ( $type === 'term' ) { $url = get_term_link( $id ); } else { $url = get_bloginfo( 'url' ); } if ( is_wp_error( $url ) || empty( $url ) ) { throw new Exception( /* translators: 1: is an integer representing an unknown Term ID */ sprintf( __( 'Invalid Term ID or Post ID or site url %1$d', 'google-listings-and-ads' ), $id ) ); } return $url; } /** * Get other campaigns' assets from the specific url. * * @param int $id Post or Term ID. * @param string $type Only possible values are post or term. */ protected function get_asset_group_asset_suggestions( int $id, string $type ): array { $final_url = $this->get_url( $id, $type ); // Suggest the assets from the first asset group if exists. $asset_group_assets = $this->asset_group_asset->get_assets_by_final_url( $final_url, true ); if ( empty( $asset_group_assets ) ) { return []; } return array_merge( $this->get_suggestions_common_fields( [] ), [ 'final_url' => $final_url ], $asset_group_assets ); } /** * Get assets from specific post or term. * * @param int $id Post or Term ID, or self::HOMEPAGE_KEY_ID. * @param string $type Only possible values are post or term. * * @return array All assets available for specific term, post or homepage. * @throws Exception If the ID is invalid. */ protected function get_wp_assets( int $id, string $type ): array { if ( $type === 'post' ) { return $this->get_post_assets( $id ); } elseif ( $type === 'term' ) { return $this->get_term_assets( $id ); } else { return $this->get_homepage_assets(); } } /** * Get assets from the homepage. * * @return array Assets available for the homepage. * @throws Exception If the homepage id is invalid. */ protected function get_homepage_assets(): array { $home_page = $this->wp->get_static_homepage(); // Static homepage. if ( $home_page ) { return $this->get_post_assets( $home_page->ID ); } // Get images from the latest posts. $posts = $this->wp->get_posts( [] ); $inserted_images_ids = array_map( [ $this, 'get_html_inserted_images' ], array_column( $posts, 'post_content' ) ); $ids = array_merge( $this->get_post_image_attachments( [ 'post_parent__in' => array_column( $posts, 'ID' ) ] ), ...$inserted_images_ids ); $marketing_images = $this->get_url_attachments_by_ids( $ids ); // Non static homepage. return array_merge( [ AssetFieldType::HEADLINE => [ __( 'Homepage', 'google-listings-and-ads' ) ], AssetFieldType::LONG_HEADLINE => [ get_bloginfo( 'name' ) . ': ' . __( 'Homepage', 'google-listings-and-ads' ) ], AssetFieldType::DESCRIPTION => ArrayUtil::remove_empty_values( [ __( 'Homepage', 'google-listings-and-ads' ), get_bloginfo( 'description' ) ] ), 'display_url_path' => [], 'final_url' => get_bloginfo( 'url' ), ], $this->get_suggestions_common_fields( $marketing_images ) ); } /** * Get assets from specific post. * * @param int $id Post ID. * * @return array All assets for specific post. * @throws Exception If the Post ID is invalid. */ protected function get_post_assets( int $id ): array { $post = get_post( $id ); if ( ! $post || $post->post_status === 'trash' ) { throw new Exception( /* translators: 1: is an integer representing an unknown Post ID */ sprintf( __( 'Invalid Post ID %1$d', 'google-listings-and-ads' ), $id ) ); } $attachments_ids = $this->get_post_image_attachments( [ 'post_parent' => $id, ] ); if ( $id === wc_get_page_id( 'shop' ) ) { $attachments_ids = [ ...$attachments_ids, ...$this->get_shop_attachments() ]; } if ( $post->post_type === 'product' || $post->post_type === 'product_variation' ) { $product = $this->wc->maybe_get_product( $id ); $attachments_ids = [ ...$attachments_ids, ...$product->get_gallery_image_ids() ]; } $attachments_ids = [ ...$attachments_ids, ...$this->get_gallery_images_ids( $id ), ...$this->get_html_inserted_images( $post->post_content ), get_post_thumbnail_id( $id ) ]; $marketing_images = $this->get_url_attachments_by_ids( $attachments_ids ); $long_headline = get_bloginfo( 'name' ) . ': ' . $post->post_title; return array_merge( [ AssetFieldType::HEADLINE => [ $post->post_title ], AssetFieldType::LONG_HEADLINE => [ $long_headline ], AssetFieldType::DESCRIPTION => ArrayUtil::remove_empty_values( [ $post->post_excerpt, get_bloginfo( 'description' ) ] ), 'display_url_path' => [ $post->post_name ], 'final_url' => get_permalink( $id ), ], $this->get_suggestions_common_fields( $marketing_images ) ); } /** * Get assets from specific term. * * @param int $id Term ID. * * @return array All assets for specific term. * @throws Exception If the Term ID is invalid. */ protected function get_term_assets( int $id ): array { $term = get_term( $id ); if ( ! $term ) { throw new Exception( /* translators: 1: is an integer representing an unknown Term ID */ sprintf( __( 'Invalid Term ID %1$d', 'google-listings-and-ads' ), $id ) ); } $posts_assigned_to_term = $this->get_posts_assigned_to_a_term( $term->term_id, $term->taxonomy ); $posts_ids_assigned_to_term = []; $attachments_ids = []; foreach ( $posts_assigned_to_term as $post ) { $attachments_ids[] = get_post_thumbnail_id( $post->ID ); $posts_ids_assigned_to_term[] = $post->ID; } if ( count( $posts_assigned_to_term ) ) { $attachments_ids = [ ...$this->get_post_image_attachments( [ 'post_parent__in' => $posts_ids_assigned_to_term ] ), ...$attachments_ids ]; } $marketing_images = $this->get_url_attachments_by_ids( $attachments_ids ); return array_merge( [ AssetFieldType::HEADLINE => [ $term->name ], AssetFieldType::LONG_HEADLINE => [ get_bloginfo( 'name' ) . ': ' . $term->name ], AssetFieldType::DESCRIPTION => ArrayUtil::remove_empty_values( [ wp_strip_all_tags( $term->description ), get_bloginfo( 'description' ) ] ), 'display_url_path' => [ $term->slug ], 'final_url' => get_term_link( $term->term_id ), ], $this->get_suggestions_common_fields( $marketing_images ) ); } /** * Get inserted images from HTML. * * @param string $html HTML string. * * @return array Array of image IDs. */ protected function get_html_inserted_images( string $html ): array { if ( empty( $html ) ) { return []; } // Malformed HTML can cause DOMDocument to throw warnings. With the below line, we can suppress them and work only with the HTML that has been parsed. libxml_use_internal_errors( true ); $dom = new DOMDocument(); if ( $dom->loadHTML( $html ) ) { $images = $dom->getElementsByTagName( 'img' ); $images_ids = []; $pattern = '/-\d+x\d+\.(jpg|jpeg|png)$/i'; foreach ( $images as $image ) { $url_unscaled = preg_replace( $pattern, '.${1}', $image->getAttribute( 'src' ), ); $image_id = attachment_url_to_postid( $url_unscaled ); // Look for scaled image if the original image is not found. if ( $image_id === 0 ) { $url_scaled = preg_replace( $pattern, '-scaled.${1}', $image->getAttribute( 'src' ), ); $image_id = attachment_url_to_postid( $url_scaled ); } if ( $image_id > 0 ) { $images_ids[] = $image_id; } } } return $images_ids; } /** * Get logo images urls. * * @return array Logo images urls. */ protected function get_logo_images(): array { $logo_images = $this->get_url_attachments_by_ids( [ get_theme_mod( 'custom_logo' ) ], [ self::LOGO_IMAGE_KEY ] ); return $logo_images[ self::LOGO_IMAGE_KEY ] ?? []; } /** * Get posts linked to a specific term. * * @param int $term_id Term ID. * @param string $taxonomy_name Taxonomy name. * * @return array List of posts assigned to the term. */ protected function get_posts_assigned_to_a_term( int $term_id, string $taxonomy_name ): array { $args = [ 'post_type' => 'any', 'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES, 'tax_query' => [ [ 'taxonomy' => $taxonomy_name, 'terms' => $term_id, 'field' => 'term_id', 'include_children' => false, ], ], ]; return $this->wp->get_posts( $args ); } /** * Get attachments related to the shop page. * * @return array Shop attachments. */ protected function get_shop_attachments(): array { return $this->get_post_image_attachments( [ 'post_parent__in' => $this->get_shop_products(), ] ); } /** * * Get products that will be use to offer image assets. * * @param array $args See WP_Query::parse_query() for all available arguments. * @return array Shop products. */ protected function get_shop_products( array $args = [] ): array { $defaults = [ 'post_type' => 'product', 'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES, 'fields' => 'ids', ]; $args = wp_parse_args( $args, $defaults ); return $this->wp->get_posts( $args ); } /** * Get gallery images ids. * * @param int $post_id Post ID that contains the gallery. * * @return array List of gallery images ids. */ protected function get_gallery_images_ids( int $post_id ): array { $gallery = get_post_gallery( $post_id, false ); if ( ! $gallery || ! isset( $gallery['ids'] ) ) { return []; } return explode( ',', $gallery['ids'] ); } /** * Get unique attachments ids converted to int values. * * @param array $ids Attachments ids. * @param int $maximum_images Maximum number of images to return. * * @return array List of unique attachments ids converted to int values. */ protected function prepare_image_ids( array $ids, int $maximum_images = self::DEFAULT_MAXIMUM_MARKETING_IMAGES ): array { $ids = array_unique( ArrayUtil::remove_empty_values( $ids ) ); $ids = array_map( 'intval', $ids ); return array_slice( $ids, 0, $maximum_images ); } /** * Get URL for each attachment using an array of attachment ids and a list of subsizes. * * @param array $ids Attachments ids. * @param array $size_keys Image subsize keys. * @param int $maximum_images Maximum number of images to return. * * @return array A list of attachments urls. */ protected function get_url_attachments_by_ids( array $ids, array $size_keys = [ self::SQUARE_MARKETING_IMAGE_KEY, self::MARKETING_IMAGE_KEY, self::PORTRAIT_MARKETING_IMAGE_KEY ], $maximum_images = self::DEFAULT_MAXIMUM_MARKETING_IMAGES ): array { $ids = $this->prepare_image_ids( $ids, $maximum_images ); $marketing_images = []; foreach ( $ids as $id ) { $metadata = wp_get_attachment_metadata( $id ); if ( ! $metadata ) { continue; } foreach ( $size_keys as $size_key ) { if ( count( $marketing_images[ $size_key ] ?? [] ) >= self::IMAGE_REQUIREMENTS[ $size_key ]['max_qty'] ) { continue; } $minimum_size = new DimensionUtility( ...self::IMAGE_REQUIREMENTS[ $size_key ]['minimum'] ); $recommended_size = new DimensionUtility( ...self::IMAGE_REQUIREMENTS[ $size_key ]['recommended'] ); $image_size = new DimensionUtility( $metadata['width'], $metadata['height'] ); $suggested_size = $this->image_utility->recommend_size( $image_size, $recommended_size, $minimum_size ); // If the original size matches the suggested size with a precision of +-1px. if ( $suggested_size && $suggested_size->equals( $image_size ) ) { $marketing_images[ $size_key ][] = wp_get_attachment_url( $id ); } elseif ( isset( $metadata['sizes'][ $size_key ] ) ) { // use the sub size. $marketing_images[ $size_key ][] = wp_get_attachment_image_url( $id, $size_key ); } elseif ( $suggested_size && $this->image_utility->maybe_add_subsize_image( $id, $size_key, $suggested_size ) ) { // use the resized image. $marketing_images[ $size_key ][] = wp_get_attachment_image_url( $id, $size_key ); } } } return $marketing_images; } /** * Get Attachmets for specific posts. * * @param array $args See WP_Query::parse_query() for all available arguments. * * @return array List of attachments */ protected function get_post_image_attachments( array $args = [] ): array { $defaults = [ 'post_type' => 'attachment', 'post_mime_type' => [ 'image/jpeg', 'image/png', 'image/jpg' ], 'fields' => 'ids', 'numberposts' => self::DEFAULT_MAXIMUM_MARKETING_IMAGES, ]; $args = wp_parse_args( $args, $defaults ); return $this->wp->get_posts( $args ); } /** * Get posts that can be used to suggest assets * * @param string $search The search query. * @param int $per_page Number of items per page. * @param int $offset Used in the get_posts query. * * @return array formatted post suggestions */ protected function get_post_suggestions( string $search, int $per_page, int $offset = 0 ): array { if ( $per_page <= 0 ) { return []; } $post_suggestions = []; $excluded_post_types = [ 'attachment' ]; $post_types = $this->wp->get_post_types( [ 'exclude_from_search' => false, 'public' => true, ] ); // Exclude attachment post_type $filtered_post_types = array_diff( $post_types, $excluded_post_types ); $args = [ 'post_type' => $filtered_post_types, 'posts_per_page' => $per_page, 'post_status' => 'publish', 'search_title' => $search, 'offset' => $offset, 'suppress_filters' => false, ]; add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 ); $posts = $this->wp->get_posts( $args ); remove_filter( 'posts_where', [ $this, 'title_filter' ] ); foreach ( $posts as $post ) { $post_suggestions[] = $this->format_final_url_response( $post->ID, 'post', $post->post_title, get_permalink( $post->ID ) ); } return $post_suggestions; } /** * Filter for the posts_where hook, adds WHERE clause to search * for the 'search_title' parameter in the post titles (when present). * * @param string $where The WHERE clause of the query. * @param WP_Query $wp_query The WP_Query instance (passed by reference). * * @return string The updated WHERE clause. */ public function title_filter( string $where, WP_Query $wp_query ): string { $search_title = $wp_query->get( 'search_title' ); if ( $search_title ) { $title_search = '%' . $this->wpdb->esc_like( $search_title ) . '%'; $where .= $this->wpdb->prepare( " AND `{$this->wpdb->posts}`.`post_title` LIKE %s", $title_search ); // phpcs:ignore WordPress.DB.PreparedSQL } return $where; } /** * Get terms that can be used to suggest assets * * @param string $search The search query * @param int $per_page Number of items per page * * @return array formatted terms suggestions */ protected function get_terms_suggestions( string $search, int $per_page ): array { $terms_suggestions = []; // get_terms evaluates $per_page_terms = 0 as a falsy, therefore it will not add the LIMIT clausure returning all the results. // See: https://github.com/WordPress/WordPress/blob/abe134c2090e84080adc46187884201a4badd649/wp-includes/class-wp-term-query.php#L868 if ( $per_page <= 0 ) { return []; } // Get all taxonomies that are public, show_in_menu = true helps to exclude taxonomies such as "product_shipping_class". $taxonomies = $this->wp->get_taxonomies( [ 'public' => true, 'show_in_menu' => true, ], ); $terms = $this->wp->get_terms( [ 'taxonomy' => $taxonomies, 'hide_empty' => false, 'number' => $per_page, 'name__like' => $search, ] ); foreach ( $terms as $term ) { $terms_suggestions[] = $this->format_final_url_response( $term->term_id, 'term', $term->name, get_term_link( $term->term_id, $term->taxonomy ) ); } return $terms_suggestions; } /** * Return a list of final urls that can be used to suggest assets. * * @param string $search The search query * @param int $per_page Number of items per page * @param string $order_by Order by: type, title, url * * @return array final urls with their title, id & type. */ public function get_final_url_suggestions( string $search = '', int $per_page = 30, string $order_by = 'title' ): array { if ( empty( $search ) ) { return $this->get_defaults_final_url_suggestions(); } $homepage = []; // If the search query contains the word "homepage" add the homepage to the results. if ( strpos( 'homepage', strtolower( $search ) ) !== false ) { $homepage[] = $this->get_homepage_final_url(); --$per_page; } // Split possible results between posts and terms. $per_page_posts = (int) ceil( $per_page / 2 ); $posts = $this->get_post_suggestions( $search, $per_page_posts ); // Try to get more results using the terms $per_page_terms = $per_page - count( $posts ); $terms = $this->get_terms_suggestions( $search, $per_page_terms ); $pending_results = $per_page - count( $posts ) - count( $terms ); $more_results = []; // Try to get more results using posts if ( $pending_results > 0 && count( $posts ) === $per_page_posts ) { $more_results = $this->get_post_suggestions( $search, $pending_results, $per_page_posts ); } $result = array_merge( $homepage, $posts, $terms, $more_results ); return $this->sort_results( $result, $order_by ); } /** * Get the final url for the homepage. * * @return array final url for the homepage. */ protected function get_homepage_final_url(): array { return $this->format_final_url_response( self::HOMEPAGE_KEY_ID, 'homepage', __( 'Homepage', 'google-listings-and-ads' ), get_bloginfo( 'url' ) ); } /** * Get defaults final urls suggestions. * * @return array default final urls. */ protected function get_defaults_final_url_suggestions(): array { $defaults = [ $this->get_homepage_final_url() ]; $shop_page = $this->wp->get_shop_page(); if ( $shop_page ) { $defaults[] = $this->format_final_url_response( $shop_page->ID, 'post', $shop_page->post_title, get_permalink( $shop_page->ID ) ); } return $defaults; } /** * Order suggestions alphabetically * * @param array $results Results as an associative array * @param string $field Sort by a specific field * * @return array response sorted alphabetically */ protected function sort_results( array $results, string $field ): array { usort( $results, function ( $a, $b ) use ( $field ) { return strcmp( strtolower( (string) $a[ $field ] ), strtolower( (string) $b[ $field ] ) ); } ); return $results; } /** * Return an assotiave array with the page suggestion response format. * * @param int $id post id, term id or self::HOMEPAGE_KEY_ID. * @param string $type post|term * @param string $title page|term title * @param string $url page|term url * * @return array response formated. */ protected function format_final_url_response( int $id, string $type, string $title, string $url ): array { return [ 'id' => $id, 'type' => $type, 'title' => $title, 'url' => $url, ]; } /** * Get the suggested common fieds. * * @param array $marketing_images Marketing images. * * @return array Suggested common fields. */ protected function get_suggestions_common_fields( array $marketing_images ): array { return [ AssetFieldType::LOGO => $this->get_logo_images(), AssetFieldType::BUSINESS_NAME => get_bloginfo( 'name' ), AssetFieldType::SQUARE_MARKETING_IMAGE => $marketing_images[ self::SQUARE_MARKETING_IMAGE_KEY ] ?? [], AssetFieldType::MARKETING_IMAGE => $marketing_images [ self::MARKETING_IMAGE_KEY ] ?? [], AssetFieldType::PORTRAIT_MARKETING_IMAGE => $marketing_images [ self::PORTRAIT_MARKETING_IMAGE_KEY ] ?? [], AssetFieldType::CALL_TO_ACTION_SELECTION => null, ]; } } PK!HE66src/Assets/AdminAssetHelper.phpnu[validate_handle_not_exists( $asset->get_handle() ); $this->assets[ $asset->get_handle() ] = $asset; $asset->register(); } /** * Register multiple assets. * * @param Asset[] $assets Array of assets to register. */ public function register_many( array $assets ): void { foreach ( $assets as $asset ) { $this->register( $asset ); } } /** * Enqueue a single asset. * * @param Asset $asset Asset to enqueue. * * @throws InvalidAsset If the passed-in asset is not valid. * * @see AssetsHandlerInterface::register To register assets. * @see AssetsHandlerInterface::register_many To register multiple assets. */ public function enqueue( Asset $asset ): void { $this->enqueue_handle( $asset->get_handle() ); } /** * Enqueue multiple assets. * * @param Asset[] $assets Array of assets to enqueue. * * @throws InvalidAsset If any of the passed-in assets are not valid. * * @see AssetsHandlerInterface::register To register assets. * @see AssetsHandlerInterface::register_many To register multiple assets. */ public function enqueue_many( array $assets ): void { foreach ( $assets as $asset ) { $this->enqueue( $asset ); } } /** * Enqueue a single asset based on its handle. * * @param string $handle Handle of the asset to enqueue. * * @throws InvalidAsset If the passed-in asset handle is not valid. */ public function enqueue_handle( string $handle ): void { $this->validate_handle_exists( $handle ); $this->assets[ $handle ]->enqueue(); } /** * Enqueue multiple assets based on their handles. * * @param string[] $handles Array of asset handles to enqueue. * * @throws InvalidAsset If any of the passed-in asset handles are not valid. */ public function enqueue_many_handles( array $handles ): void { foreach ( $handles as $handle ) { $this->enqueue_handle( $handle ); } } /** * Dequeue a single asset based on its handle. * * @param string $handle Handle of the asset to enqueue. * * @throws InvalidAsset If the passed-in asset handle is not valid. */ public function dequeue_handle( string $handle ): void { $this->validate_handle_exists( $handle ); $this->assets[ $handle ]->dequeue(); } /** * Enqueue all assets known to this asset handler. */ public function enqueue_all(): void { foreach ( $this->assets as $asset_object ) { $asset_object->enqueue(); } } /** * Validate that a given asset handle is known to the object. * * @param string $handle The asset handle to validate. * * @throws InvalidAsset When the asset handle is unknown to the object. */ protected function validate_handle_exists( string $handle ): void { if ( ! array_key_exists( $handle, $this->assets ) ) { throw InvalidAsset::invalid_handle( $handle ); } } /** * Validate that a given asset handle does not already exist. * * @param string $handle * * @throws InvalidAsset When the handle exists. */ protected function validate_handle_not_exists( string $handle ): void { if ( array_key_exists( $handle, $this->assets ) ) { throw InvalidAsset::handle_exists( $handle ); } } } PK!JIsrc/Assets/BaseAsset.phpnu[file_extension = $file_extension; $this->handle = $handle; $this->uri = $this->get_uri_from_path( $uri ); $this->dependencies = $dependencies; $this->version = $version ?: $this->get_version(); $this->enqueue_condition_callback = $enqueue_condition_callback; } /** * Get the handle of the asset. The handle serves as the ID within WordPress. * * @return string */ public function get_handle(): string { return $this->handle; } /** * Get the URI for the asset. * * @return string */ public function get_uri(): string { return $this->uri; } /** * Get the condition callback to run when enqueuing the asset. * * The asset will only be enqueued if the callback returns true. * * @return bool */ public function can_enqueue(): bool { if ( is_null( $this->enqueue_condition_callback ) ) { return true; } return (bool) call_user_func( $this->enqueue_condition_callback, $this ); } /** * Enqueue the asset within WordPress. */ public function enqueue(): void { if ( ! $this->can_enqueue() ) { return; } $this->defer_action( $this->get_enqueue_action(), $this->get_enqueue_callback(), $this->enqueue_priority ); } /** * Dequeue the asset within WordPress. */ public function dequeue(): void { $this->defer_action( $this->get_dequeue_action(), $this->get_dequeue_callback(), $this->dequeue_priority ); } /** * Register a service. */ public function register(): void { $this->defer_action( $this->get_register_action(), $this->get_register_callback(), $this->registration_priority ); } /** * Get the register action to use. * * @since 0.1.0 * * @return string Register action to use. */ protected function get_register_action(): string { return $this->get_enqueue_action(); } /** * Get the enqueue action to use. * * @return string */ protected function get_enqueue_action(): string { return 'wp_enqueue_scripts'; } /** * Get the dequeue action to use. * * @return string */ protected function get_dequeue_action(): string { return 'wp_print_scripts'; } /** * Add a callable to an action, or run it immediately if the action has already fired. * * @param string $action * @param callable $callback * @param int $priority */ protected function defer_action( string $action, callable $callback, int $priority = 10 ): void { if ( did_action( $action ) ) { try { $callback(); } catch ( InvalidAsset $exception ) { do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); } return; } add_action( $action, $callback, $priority ); } /** * Convert a file path to a URI for a source. * * @param string $path The source file path. * * @return string */ protected function get_uri_from_path( string $path ): string { $path = $this->normalize_source_path( $path ); $path = str_replace( $this->get_root_dir(), '', $path ); return $this->get_plugin_url( $path ); } /** * Normalize a source path with a given file extension. * * @param string $path The path to normalize. * * @return string */ protected function normalize_source_path( string $path ): string { $path = ltrim( $path, '/' ); $path = $this->maybe_add_extension( $path ); $path = "{$this->get_root_dir()}/{$path}"; return $this->maybe_add_minimized_extension( $path ); } /** * Possibly add an extension to a path. * * @param string $path Path where an extension may be needed. * * @return string */ protected function maybe_add_extension( string $path ): string { $detected_extension = pathinfo( $path, PATHINFO_EXTENSION ); if ( $this->file_extension !== $detected_extension ) { $path .= ".{$this->file_extension}"; } return $path; } /** * Possibly add a minimized extension to a path. * * @param string $path Path where a minimized extension may be needed. * * @return string * @throws InvalidAsset When no asset can be found. */ protected function maybe_add_minimized_extension( string $path ): string { $minimized_path = str_replace( ".{$this->file_extension}", ".min.{$this->file_extension}", $path ); // Validate that at least one version of the file exists. $path_readable = is_readable( $path ); $minimized_readable = is_readable( $minimized_path ); if ( ! $path_readable && ! $minimized_readable ) { throw InvalidAsset::invalid_path( $path ); } // If we only have one available, return the available one no matter what. if ( ! $minimized_readable ) { return $path; } elseif ( ! $path_readable ) { return $minimized_path; } return defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? $path : $minimized_path; } /** * Get the register callback to use. * * @return callable */ abstract protected function get_register_callback(): callable; /** * Get the enqueue callback to use. * * @return callable */ abstract protected function get_enqueue_callback(): callable; /** * Get the dequeue callback to use. * * @return callable */ abstract protected function get_dequeue_callback(): callable; } PK!$'src/Assets/ScriptAsset.phpnu[in_footer = $in_footer; parent::__construct( 'js', $handle, $uri, $dependencies, $version, $enqueue_condition_callback ); } /** * Add a localization to the script. * * @param string $object_name The object name. * @param array $data Array of data for the object. * * @return $this */ public function add_localization( string $object_name, array $data ): ScriptAsset { $this->localizations[ $object_name ] = $data; return $this; } /** * Add a inline script to pass generic data from PHP to JavaScript. * * @param string $variable_name The global JavaScript variable name. * @param array $data Array of data to be encoded to JSON format. * * @return $this */ public function add_inline_script( string $variable_name, array $data ): ScriptAsset { $this->inline_scripts[ $variable_name ] = $data; return $this; } /** * Get the register callback to use. * * @return callable */ protected function get_register_callback(): callable { return function () { if ( wp_script_is( $this->handle, 'registered' ) ) { return; } wp_register_script( $this->handle, $this->uri, $this->dependencies, $this->version, $this->in_footer ); }; } /** * Get the enqueue callback to use. * * @return callable */ protected function get_enqueue_callback(): callable { return function () { if ( ! wp_script_is( $this->handle, 'registered' ) ) { throw InvalidAsset::asset_not_registered( $this->handle ); } foreach ( $this->localizations as $object_name => $data_array ) { wp_localize_script( $this->handle, $object_name, $data_array ); } foreach ( $this->inline_scripts as $variable_name => $data_array ) { $inline_script = "var $variable_name = " . wp_json_encode( $data_array ); wp_add_inline_script( $this->handle, $inline_script, 'before' ); } wp_enqueue_script( $this->handle ); if ( in_array( 'wp-i18n', $this->dependencies, true ) ) { wp_set_script_translations( $this->handle, 'google-listings-and-ads' ); } }; } /** * Get the dequeue callback to use. * * @return callable */ protected function get_dequeue_callback(): callable { return function () { wp_dequeue_script( $this->handle ); }; } } PK!uD /src/Assets/ScriptWithBuiltDependenciesAsset.phpnu[ instead of in * the (default: true). */ public function __construct( string $handle, string $uri, string $build_dependency_path, DependencyArray $fallback_dependency_data, ?callable $enqueue_condition_callback = null, bool $in_footer = true ) { $dependency_data = $this->get_dependency_data( $build_dependency_path, $fallback_dependency_data ); parent::__construct( $handle, $uri, $dependency_data->get_dependencies(), $dependency_data->get_version(), $enqueue_condition_callback, $in_footer ); } /** * Get usable dependency data from an asset path or from the fallback. * * @param string $build_dependency_path * @param DependencyArray $fallback * * @return DependencyArray */ protected function get_dependency_data( string $build_dependency_path, DependencyArray $fallback ): DependencyArray { try { if ( ! is_readable( $build_dependency_path ) ) { return $fallback; } // Reason of exclusion: These files are being loaded manually in the function call with no user input // nosemgrep: audit.php.lang.security.file.inclusion-arg return new DependencyArray( include $build_dependency_path ); } catch ( Throwable $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); return $fallback; } } } PK!-|Qw w src/Assets/StyleAsset.phpnu[media = $media; parent::__construct( 'css', $handle, $uri, $dependencies, $version, $enqueue_condition_callback ); } /** * Get the register callback to use. * * @return callable */ protected function get_register_callback(): callable { return function () { if ( wp_style_is( $this->handle, 'registered' ) ) { return; } wp_register_style( $this->handle, $this->uri, $this->dependencies, $this->version, $this->media ); }; } /** * Get the enqueue callback to use. * * @return callable */ protected function get_enqueue_callback(): callable { return function () { if ( ! wp_style_is( $this->handle, 'registered' ) ) { throw InvalidAsset::asset_not_registered( $this->handle ); } wp_enqueue_style( $this->handle ); }; } /** * Get the dequeue callback to use. * * @return callable */ protected function get_dequeue_callback(): callable { return function () { wp_dequeue_style( $this->handle ); }; } } PK!8meta_handler = $meta_handler; $this->wc = $wc; $this->merchant_center = $merchant_center; } /** * Mark the item as notified. * * @param WC_Coupon $coupon * * @return void */ public function mark_as_notified( $coupon ): void { $this->meta_handler->update_synced_at( $coupon, time() ); $this->meta_handler->update_sync_status( $coupon, SyncStatus::SYNCED ); $this->update_empty_visibility( $coupon ); } /** * Mark a coupon as synced. This function accepts nullable $google_id, * which guarantees version compatibility for Alpha, Beta and stable verison promtoion APIs. * * @param WC_Coupon $coupon * @param string|null $google_id * @param string $target_country */ public function mark_as_synced( WC_Coupon $coupon, ?string $google_id, string $target_country ) { $this->meta_handler->update_synced_at( $coupon, time() ); $this->meta_handler->update_sync_status( $coupon, SyncStatus::SYNCED ); $this->update_empty_visibility( $coupon ); // merge and update all google ids $current_google_ids = $this->meta_handler->get_google_ids( $coupon ); $current_google_ids = ! empty( $current_google_ids ) ? $current_google_ids : []; $google_ids = array_unique( array_merge( $current_google_ids, [ $target_country => $google_id, ] ) ); $this->meta_handler->update_google_ids( $coupon, $google_ids ); } /** * * @param WC_Coupon $coupon */ public function mark_as_unsynced( $coupon ): void { $this->meta_handler->delete_synced_at( $coupon ); $this->meta_handler->update_sync_status( $coupon, SyncStatus::NOT_SYNCED ); $this->meta_handler->delete_google_ids( $coupon ); $this->meta_handler->delete_errors( $coupon ); $this->meta_handler->delete_failed_sync_attempts( $coupon ); $this->meta_handler->delete_sync_failed_at( $coupon ); } /** * * @param WC_Coupon $coupon * @param string $target_country */ public function remove_google_id_by_country( WC_Coupon $coupon, string $target_country ) { $google_ids = $this->meta_handler->get_google_ids( $coupon ); if ( empty( $google_ids ) ) { return; } unset( $google_ids[ $target_country ] ); if ( ! empty( $google_ids ) ) { $this->meta_handler->update_google_ids( $coupon, $google_ids ); } else { // if there are no Google IDs left then this coupon is no longer considered "synced" $this->mark_as_unsynced( $coupon ); } } /** * Marks a WooCommerce coupon as invalid and stores the errors in a meta data key. * * @param WC_Coupon $coupon * @param InvalidCouponEntry[] $errors */ public function mark_as_invalid( WC_Coupon $coupon, array $errors ) { // bail if no errors exist if ( empty( $errors ) ) { return; } $this->meta_handler->update_errors( $coupon, $errors ); $this->meta_handler->update_sync_status( $coupon, SyncStatus::HAS_ERRORS ); $this->update_empty_visibility( $coupon ); // TODO: Update failed sync attempts count in case of internal errors } /** * Marks a WooCommerce coupon as pending synchronization. * * @param WC_Coupon $coupon */ public function mark_as_pending( WC_Coupon $coupon ) { $this->meta_handler->update_sync_status( $coupon, SyncStatus::PENDING ); $this->meta_handler->delete_errors( $coupon ); } /** * Update empty (NOT EXIST) visibility meta values to SYNC_AND_SHOW. * * @param WC_Coupon $coupon */ protected function update_empty_visibility( WC_Coupon $coupon ): void { $visibility = $this->meta_handler->get_visibility( $coupon ); if ( empty( $visibility ) ) { $this->meta_handler->update_visibility( $coupon, ChannelVisibility::SYNC_AND_SHOW ); } } /** * * @param WC_Coupon $coupon * * @return string[]|null An array of Google IDs stored for each WooCommerce coupon */ public function get_synced_google_ids( WC_Coupon $coupon ): ?array { return $this->meta_handler->get_google_ids( $coupon ); } /** * Get WooCommerce coupon * * @param int $coupon_id * * @return WC_Coupon * * @throws InvalidValue If the given ID doesn't reference a valid coupon. */ public function get_wc_coupon( int $coupon_id ): WC_Coupon { $coupon = $this->wc->maybe_get_coupon( $coupon_id ); if ( ! $coupon instanceof WC_Coupon ) { throw InvalidValue::not_valid_coupon_id( $coupon_id ); } return $coupon; } /** * * @param WC_Coupon $coupon * * @return bool */ public function is_coupon_synced( WC_Coupon $coupon ): bool { $synced_at = $this->meta_handler->get_synced_at( $coupon ); $google_ids = $this->meta_handler->get_google_ids( $coupon ); return ! empty( $synced_at ) && ! empty( $google_ids ); } /** * * @param WC_Coupon $coupon * * @return bool */ public function is_sync_ready( WC_Coupon $coupon ): bool { return ( ChannelVisibility::SYNC_AND_SHOW === $this->get_channel_visibility( $coupon ) ) && ( CouponSyncer::is_coupon_supported( $coupon ) ) && ( ! $coupon->get_virtual() ); } /** * Whether the sync has failed repeatedly for the coupon within the given timeframe. * * @param WC_Coupon $coupon * * @return bool * * @see CouponSyncer::FAILURE_THRESHOLD The number of failed attempts allowed per timeframe * @see CouponSyncer::FAILURE_THRESHOLD_WINDOW The specified timeframe */ public function is_sync_failed_recently( WC_Coupon $coupon ): bool { $failed_attempts = $this->meta_handler->get_failed_sync_attempts( $coupon ); $failed_at = $this->meta_handler->get_sync_failed_at( $coupon ); // if it has failed more times than the specified threshold AND if syncing it has failed within the specified window return $failed_attempts > CouponSyncer::FAILURE_THRESHOLD && $failed_at > strtotime( sprintf( '-%s', CouponSyncer::FAILURE_THRESHOLD_WINDOW ) ); } /** * * @param WC_Coupon $coupon * * @return string */ public function get_channel_visibility( WC_Coupon $coupon ): string { $visibility = $this->meta_handler->get_visibility( $coupon ); if ( empty( $visibility ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Channel visibility forced to "%s" for visibility unknown (Post ID: %s).', ChannelVisibility::DONT_SYNC_AND_SHOW, $coupon->get_id() ), __METHOD__ ); return ChannelVisibility::DONT_SYNC_AND_SHOW; } return $visibility; } /** * Return a string indicating sync status based on several factors. * * @param WC_Coupon $coupon * * @return string|null */ public function get_sync_status( WC_Coupon $coupon ): ?string { return $this->meta_handler->get_sync_status( $coupon ); } /** * Return the string indicating the coupon status as reported by the Merchant Center. * * @param WC_Coupon $coupon * * @return string|null */ public function get_mc_status( WC_Coupon $coupon ): ?string { try { return $this->meta_handler->get_mc_status( $coupon ); } catch ( InvalidValue $exception ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Coupon status returned null for invalid coupon (ID: %s).', $coupon->get_id() ), __METHOD__ ); return null; } } /** * Get validation errors for a specific coupon. * Combines errors for variable coupons, which have a variation-indexed array of errors. * * @param WC_Coupon $coupon * * @return array */ public function get_validation_errors( WC_Coupon $coupon ): array { $errors = $this->meta_handler->get_errors( $coupon ) ?: []; $first_key = array_key_first( $errors ); if ( ! empty( $errors ) && is_array( $errors[ $first_key ] ) ) { $errors = array_unique( array_merge( ...$errors ) ); } return $errors; } /** * Indicates if a coupon is ready for sending Notifications. * A coupon is ready to send notifications if its sync ready and the post status is publish. * * @param WC_Coupon $coupon * * @return bool */ public function is_ready_to_notify( WC_Coupon $coupon ): bool { $is_ready = $this->is_sync_ready( $coupon ) && $coupon->get_status() === 'publish'; /** * Allow users to filter if a coupon is ready to notify. * * @since 2.8.0 * * @param bool $value The current filter value. * @param WC_Coupon $coupon The coupon for the notification. */ return apply_filters( 'woocommerce_gla_coupon_is_ready_to_notify', $is_ready, $coupon ); } /** * Indicates if a coupon was already notified about its creation. * Notice we consider synced coupons in MC as notified for creation. * * @param WC_Coupon $coupon * * @return bool */ public function has_notified_creation( WC_Coupon $coupon ): bool { $valid_has_notified_creation_statuses = [ NotificationStatus::NOTIFICATION_CREATED, NotificationStatus::NOTIFICATION_UPDATED, NotificationStatus::NOTIFICATION_PENDING_UPDATE, NotificationStatus::NOTIFICATION_PENDING_DELETE, ]; return in_array( $this->meta_handler->get_notification_status( $coupon ), $valid_has_notified_creation_statuses, true ) || $this->is_coupon_synced( $coupon ); } /** * Set the notification status for a WooCommerce coupon. * * @param WC_Coupon $coupon * @param string $status */ public function set_notification_status( $coupon, $status ): void { $this->meta_handler->update_notification_status( $coupon, $status ); } /** * Indicates if a coupon is ready for sending a create Notification. * A coupon is ready to send create notifications if is ready to notify and has not sent create notification yet. * * @param WC_Coupon $coupon * * @return bool */ public function should_trigger_create_notification( $coupon ): bool { return $this->is_ready_to_notify( $coupon ) && ! $this->has_notified_creation( $coupon ); } /** * Indicates if a coupon is ready for sending an update Notification. * A coupon is ready to send update notifications if is ready to notify and has sent create notification already. * * @param WC_Coupon $coupon * * @return bool */ public function should_trigger_update_notification( $coupon ): bool { return $this->is_ready_to_notify( $coupon ) && $this->has_notified_creation( $coupon ); } /** * Indicates if a coupon is ready for sending a delete Notification. * A coupon is ready to send delete notifications if it is not ready to notify and has sent create notification already. * * @param WC_Coupon $coupon * * @return bool */ public function should_trigger_delete_notification( $coupon ): bool { return ! $this->is_ready_to_notify( $coupon ) && $this->has_notified_creation( $coupon ); } } PK!C? Mbb src/Coupon/CouponMetaHandler.phpnu[ 'int', self::KEY_GOOGLE_IDS => 'array', self::KEY_VISIBILITY => 'string', self::KEY_ERRORS => 'array', self::KEY_FAILED_SYNC_ATTEMPTS => 'int', self::KEY_SYNC_FAILED_AT => 'int', self::KEY_SYNC_STATUS => 'string', self::KEY_MC_STATUS => 'string', self::KEY_NOTIFICATION_STATUS => 'string', ]; /** * * @param string $name * @param mixed $arguments * * @return mixed * * @throws BadMethodCallException If the method that's called doesn't exist. * @throws InvalidMeta If the meta key is invalid. */ public function __call( string $name, $arguments ) { $found_matches = preg_match( '/^([a-z]+)_([\w\d]+)$/i', $name, $matches ); if ( ! $found_matches ) { throw new BadMethodCallException( sprintf( 'The method %s does not exist in class CouponMetaHandler', $name ) ); } [ $function_name, $method, $key ] = $matches; // validate the method if ( ! in_array( $method, [ 'update', 'delete', 'get', ], true ) ) { throw new BadMethodCallException( sprintf( 'The method %s does not exist in class CouponMetaHandler', $function_name ) ); } // set the value as the third argument if method is `update` if ( 'update' === $method ) { $arguments[2] = $arguments[1]; } // set the key as the second argument $arguments[1] = $key; return call_user_func_array( [ $this, $method, ], $arguments ); } /** * * @param WC_Coupon $coupon * @param string $key * @param mixed $value * * @throws InvalidMeta If the meta key is invalid. */ public function update( WC_Coupon $coupon, string $key, $value ) { self::validate_meta_key( $key ); if ( isset( self::TYPES[ $key ] ) ) { if ( in_array( self::TYPES[ $key ], [ 'bool', 'boolean', ], true ) ) { $value = wc_bool_to_string( $value ); } else { settype( $value, self::TYPES[ $key ] ); } } $coupon->update_meta_data( $this->prefix_meta_key( $key ), $value ); $coupon->save_meta_data(); } /** * * @param WC_Coupon $coupon * @param string $key * * @throws InvalidMeta If the meta key is invalid. */ public function delete( WC_Coupon $coupon, string $key ) { self::validate_meta_key( $key ); $coupon->delete_meta_data( $this->prefix_meta_key( $key ) ); $coupon->save_meta_data(); } /** * * @param WC_Coupon $coupon * @param string $key * * @return mixed The value, or null if the meta key doesn't exist. * * @throws InvalidMeta If the meta key is invalid. */ public function get( WC_Coupon $coupon, string $key ) { self::validate_meta_key( $key ); $value = null; if ( $coupon->meta_exists( $this->prefix_meta_key( $key ) ) ) { $value = $coupon->get_meta( $this->prefix_meta_key( $key ), true ); if ( isset( self::TYPES[ $key ] ) && in_array( self::TYPES[ $key ], [ 'bool', 'boolean', ], true ) ) { $value = wc_string_to_bool( $value ); } } return $value; } /** * * @param string $key * * @throws InvalidMeta If the meta key is invalid. */ protected static function validate_meta_key( string $key ) { if ( ! self::is_meta_key_valid( $key ) ) { do_action( 'woocommerce_gla_error', sprintf( 'Coupon meta key is invalid: %s', $key ), __METHOD__ ); throw InvalidMeta::invalid_key( $key ); } } /** * * @param string $key * * @return bool Whether the meta key is valid. */ public static function is_meta_key_valid( string $key ): bool { return isset( self::TYPES[ $key ] ); } /** * Returns all available meta keys. * * @return array */ public static function get_all_meta_keys(): array { return array_keys( self::TYPES ); } } PK!\$src/Coupon/CouponSyncerException.phpnu[google_service = $google_service; $this->coupon_helper = $coupon_helper; $this->validator = $validator; $this->merchant_center = $merchant_center; $this->target_audience = $target_audience; $this->wc = $wc; } /** * Submit a WooCommerce coupon to Google Merchant Center. * * @param WC_Coupon $coupon * * @throws CouponSyncerException If there are any errors while syncing coupon with Google Merchant Center. */ public function update( WC_Coupon $coupon ) { $this->validate_merchant_center_setup(); if ( ! $this->coupon_helper->is_sync_ready( $coupon ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Skipping coupon (ID: %s) because it is not ready to be synced.', $coupon->get_id() ), __METHOD__ ); return; } $target_country = $this->target_audience->get_main_target_country(); if ( ! $this->merchant_center->is_promotion_supported_country( $target_country ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Skipping coupon (ID: %s) because it is not supported in main target country %s.', $coupon->get_id(), $target_country ), __METHOD__ ); return; } $adapted_coupon = new WCCouponAdapter( [ 'wc_coupon' => $coupon, 'targetCountry' => $target_country, ] ); $validation_result = $this->validate_coupon( $adapted_coupon ); if ( $validation_result instanceof InvalidCouponEntry ) { $this->coupon_helper->mark_as_invalid( $coupon, $validation_result->get_errors() ); do_action( 'woocommerce_gla_debug_message', sprintf( 'Skipping coupon (ID: %s) because it does not pass validation: %s', $coupon->get_id(), wp_json_encode( $validation_result ) ), __METHOD__ ); return; } try { do_action( 'woocommerce_gla_debug_message', sprintf( 'Start to upload coupon (ID: %s) as promotion structure: %s', $coupon->get_id(), wp_json_encode( $adapted_coupon ) ), __METHOD__ ); $response = $this->google_service->create( $adapted_coupon ); $this->coupon_helper->mark_as_synced( $coupon, $response->getId(), $target_country ); do_action( 'woocommerce_gla_updated_coupon', $adapted_coupon ); do_action( 'woocommerce_gla_debug_message', sprintf( "Submitted promotion:\n%s", wp_json_encode( $adapted_coupon ) ), __METHOD__ ); } catch ( GoogleException $google_exception ) { $invalid_promotion = new InvalidCouponEntry( $coupon->get_id(), [ $google_exception->getCode() => $google_exception->getMessage(), ], $target_country ); $this->coupon_helper->mark_as_invalid( $coupon, $invalid_promotion->get_errors() ); $this->handle_update_errors( [ $invalid_promotion ] ); do_action( 'woocommerce_gla_debug_message', sprintf( "Promotion failed to sync with Merchant Center:\n%s", wp_json_encode( $invalid_promotion ) ), __METHOD__ ); } catch ( Exception $exception ) { do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); throw new CouponSyncerException( sprintf( 'Error updating Google promotion: %s', $exception->getMessage() ), 0, $exception ); } } /** * * @param WCCouponAdapter $coupon * * @return InvalidCouponEntry|true */ protected function validate_coupon( WCCouponAdapter $coupon ) { $violations = $this->validator->validate( $coupon ); if ( 0 !== count( $violations ) ) { $invalid_promotion = new InvalidCouponEntry( $coupon->get_wc_coupon_id() ); $invalid_promotion->map_validation_violations( $violations ); return $invalid_promotion; } return true; } /** * Delete a WooCommerce coupon from Google Merchant Center. * * @param DeleteCouponEntry $coupon * * @throws CouponSyncerException If there are any errors while deleting coupon from Google Merchant Center. */ public function delete( DeleteCouponEntry $coupon ) { $this->validate_merchant_center_setup(); $deleted_promotions = []; $invalid_promotions = []; $synced_google_ids = $coupon->get_synced_google_ids(); $wc_coupon = $this->wc->maybe_get_coupon( $coupon->get_wc_coupon_id() ); $wc_coupon_exist = $wc_coupon instanceof WC_Coupon; foreach ( $synced_google_ids as $target_country => $google_id ) { try { $adapted_coupon = $coupon->get_google_promotion(); $adapted_coupon->setTargetCountry( $target_country ); do_action( 'woocommerce_gla_debug_message', sprintf( 'Start to delete coupon (ID: %s) as promotion structure: %s', $coupon->get_wc_coupon_id(), wp_json_encode( $adapted_coupon ) ), __METHOD__ ); // DeleteCouponEntry is generated with promotion effective date expired // when WC coupon is able to be deleted. // To soft-delete the promotion from Google side, // we will update Google promotion with expired effective date. $response = $this->google_service->create( $adapted_coupon ); array_push( $deleted_promotions, $response ); if ( $wc_coupon_exist ) { $this->coupon_helper->remove_google_id_by_country( $wc_coupon, $target_country ); } } catch ( GoogleException $google_exception ) { array_push( $invalid_promotions, new InvalidCouponEntry( $coupon->get_wc_coupon_id(), [ $google_exception->getCode() => $google_exception->getMessage(), ], $target_country, $google_id ) ); } catch ( Exception $exception ) { do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); throw new CouponSyncerException( sprintf( 'Error deleting Google promotion: %s', $exception->getMessage() ), 0, $exception ); } } if ( ! empty( $invalid_promotions ) ) { $this->handle_delete_errors( $invalid_promotions ); do_action( 'woocommerce_gla_debug_message', sprintf( "Failed to delete %s promotions from Merchant Center:\n%s", count( $invalid_promotions ), wp_json_encode( $invalid_promotions ) ), __METHOD__ ); } elseif ( $wc_coupon_exist ) { $this->coupon_helper->mark_as_unsynced( $wc_coupon ); } do_action( 'woocommerce_gla_deleted_promotions', $deleted_promotions, $invalid_promotions ); do_action( 'woocommerce_gla_debug_message', sprintf( "Deleted %s promoitons:\n%s", count( $deleted_promotions ), wp_json_encode( $deleted_promotions ) ), __METHOD__ ); } /** * Return whether coupon is supported as visible on Google. * * @param WC_Coupon $coupon * * @return bool */ public static function is_coupon_supported( WC_Coupon $coupon ): bool { if ( $coupon->get_virtual() ) { return false; } if ( ! empty( $coupon->get_email_restrictions() ) ) { return false; } if ( ! empty( $coupon->get_exclude_sale_items() ) && $coupon->get_exclude_sale_items() ) { return false; } return true; } /** * Return the list of supported coupon types. * * @return array */ public static function get_supported_coupon_types(): array { return (array) apply_filters( 'woocommerce_gla_supported_coupon_types', [ 'percent', 'fixed_cart', 'fixed_product' ] ); } /** * Return the list of coupon types we will hide functionality for (default none). * * @since 1.2.0 * * @return array */ public static function get_hidden_coupon_types(): array { return (array) apply_filters( 'woocommerce_gla_hidden_coupon_types', [] ); } /** * * @param InvalidCouponEntry[] $invalid_coupons */ protected function handle_update_errors( array $invalid_coupons ) { // Get a coupon id to country mappings. $internal_error_coupon_ids = []; foreach ( $invalid_coupons as $invalid_coupon ) { if ( $invalid_coupon->has_error( GooglePromotionService::INTERNAL_ERROR_CODE ) ) { $coupon_id = $invalid_coupon->get_wc_coupon_id(); $internal_error_coupon_ids[ $coupon_id ] = $invalid_coupon->get_target_country(); } } if ( ! empty( $internal_error_coupon_ids ) && apply_filters( 'woocommerce_gla_coupons_update_retry_on_failure', true, $internal_error_coupon_ids ) ) { do_action( 'woocommerce_gla_retry_update_coupons', $internal_error_coupon_ids ); do_action( 'woocommerce_gla_error', sprintf( 'Internal API errors while submitting the following coupons: %s', join( ', ', $internal_error_coupon_ids ) ), __METHOD__ ); } } /** * * @param BatchInvalidCouponEntry[] $invalid_coupons */ protected function handle_delete_errors( array $invalid_coupons ) { // Get all wc coupon id to google id mappings that have internal errors. $internal_error_coupon_ids = []; foreach ( $invalid_coupons as $invalid_coupon ) { if ( $invalid_coupon->has_error( GooglePromotionService::INTERNAL_ERROR_CODE ) ) { $coupon_id = $invalid_coupon->get_wc_coupon_id(); $internal_error_coupon_ids[ $coupon_id ] = $invalid_coupon->get_google_promotion_id(); } } if ( ! empty( $internal_error_coupon_ids ) && apply_filters( 'woocommerce_gla_coupons_delete_retry_on_failure', true, $internal_error_coupon_ids ) ) { do_action( 'woocommerce_gla_retry_delete_coupons', $internal_error_coupon_ids ); do_action( 'woocommerce_gla_error', sprintf( 'Internal API errors while deleting the following coupons: %s', join( ', ', $internal_error_coupon_ids ) ), __METHOD__ ); } } /** * Validates whether Merchant Center is connected and ready for pushing data. * * @throws CouponSyncerException If Google Merchant Center is not set up and connected or is not ready for pushing data. */ protected function validate_merchant_center_setup(): void { if ( ! $this->merchant_center->is_ready_for_syncing() ) { do_action( 'woocommerce_gla_error', 'Cannot sync any coupons before setting up Google Merchant Center.', __METHOD__ ); throw new CouponSyncerException( __( 'Google Merchant Center has not been set up correctly. Please review your configuration.', 'google-listings-and-ads' ) ); } if ( ! $this->merchant_center->should_push() ) { do_action( 'woocommerce_gla_error', 'Cannot push any coupons because they are being fetched automatically.', __METHOD__ ); throw new CouponSyncerException( __( 'Pushing Coupons will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings.', 'google-listings-and-ads' ) ); } } } PK!/L55src/Coupon/SyncerHooks.phpnu[coupon_helper = $coupon_helper; $this->job_repository = $job_repository; $this->merchant_center = $merchant_center; $this->notifications_service = $notifications_service; $this->wc = $wc; $this->wp = $wp; } /** * Register a service. */ public function register(): void { // only register the hooks if Merchant Center is set up correctly. if ( ! $this->merchant_center->is_ready_for_syncing() ) { return; } // when a coupon is added / updated, schedule a update job. add_action( 'woocommerce_new_coupon', [ $this, 'update_by_id' ], 90, 2 ); add_action( 'woocommerce_update_coupon', [ $this, 'update_by_id' ], 90, 2 ); add_action( 'woocommerce_gla_bulk_update_coupon', [ $this, 'update_by_id' ], 90 ); // when a coupon is trashed or removed, schedule a delete job. add_action( 'wp_trash_post', [ $this, 'pre_delete' ], 90 ); add_action( 'before_delete_post', [ $this, 'pre_delete' ], 90 ); add_action( 'trashed_post', [ $this, 'delete_by_id' ], 90 ); add_action( 'deleted_post', [ $this, 'delete_by_id' ], 90 ); add_action( 'woocommerce_delete_coupon', [ $this, 'delete_by_id' ], 90, 2 ); add_action( 'woocommerce_trash_coupon', [ $this, 'delete_by_id' ], 90, 2 ); // when a coupon is restored from trash, schedule a update job. add_action( 'untrashed_post', [ $this, 'update_by_id' ], 90 ); // Update coupons when object terms get updated. add_action( 'set_object_terms', [ $this, 'maybe_update_by_id_when_terms_updated' ], 90, 6 ); } /** * Update a coupon by the ID * * @param int $coupon_id */ public function update_by_id( int $coupon_id ) { $coupon = $this->wc->maybe_get_coupon( $coupon_id ); if ( $coupon instanceof WC_Coupon ) { $this->handle_update_coupon( $coupon ); } } /** * Update a coupon by the ID when the terms get updated. * * @param int $object_id The object ID. * @param array $terms An array of object term IDs or slugs. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy The taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ public function maybe_update_by_id_when_terms_updated( int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids ) { $this->handle_update_coupon_when_product_brands_updated( $taxonomy, $tt_ids, $old_tt_ids ); } /** * Delete a coupon by the ID * * @param int $coupon_id */ public function delete_by_id( int $coupon_id ) { $this->handle_delete_coupon( $coupon_id ); } /** * Pre Delete a coupon by the ID * * @param int $coupon_id */ public function pre_delete( int $coupon_id ) { $this->handle_pre_delete_coupon( $coupon_id ); } /** * Handle updating of a coupon. * * @param WC_Coupon $coupon * The coupon being saved. * * @return void */ protected function handle_update_coupon( WC_Coupon $coupon ) { $coupon_id = $coupon->get_id(); if ( $this->notifications_service->is_ready() ) { $this->handle_update_coupon_notification( $coupon ); } // Schedule an update job if product sync is enabled. if ( $this->coupon_helper->is_sync_ready( $coupon ) ) { $this->coupon_helper->mark_as_pending( $coupon ); $this->job_repository->get( UpdateCoupon::class )->schedule( [ [ $coupon_id ], ] ); } elseif ( $this->coupon_helper->is_coupon_synced( $coupon ) ) { // Delete the coupon from Google Merchant Center if it's already synced BUT it is not sync ready after the edit. $coupon_to_delete = new DeleteCouponEntry( $coupon_id, $this->get_coupon_to_delete( $coupon ), $this->coupon_helper->get_synced_google_ids( $coupon ) ); $this->job_repository->get( DeleteCoupon::class )->schedule( [ $coupon_to_delete, ] ); do_action( 'woocommerce_gla_debug_message', sprintf( 'Deleting coupon (ID: %s) from Google Merchant Center because it is not ready to be synced.', $coupon->get_id() ), __METHOD__ ); } else { $this->coupon_helper->mark_as_unsynced( $coupon ); } } /** * Create request entries for the coupon (containing its Google ID), * so we can schedule a delete job when it is actually trashed / deleted. * * @param int $coupon_id */ protected function handle_pre_delete_coupon( int $coupon_id ) { $coupon = $this->wc->maybe_get_coupon( $coupon_id ); if ( $coupon instanceof WC_Coupon && $this->coupon_helper->is_coupon_synced( $coupon ) ) { $this->delete_requests_map[ $coupon_id ] = new DeleteCouponEntry( $coupon_id, $this->get_coupon_to_delete( $coupon ), $this->coupon_helper->get_synced_google_ids( $coupon ) ); } } /** * @param WC_Coupon $coupon * * @return WCCouponAdapter */ protected function get_coupon_to_delete( WC_Coupon $coupon ): WCCouponAdapter { $adapted_coupon_to_delete = new WCCouponAdapter( [ 'wc_coupon' => $coupon, ] ); // Promotion stored in Google can only be soft-deleted to keep historical records. // Instead of 'delete', we update the promotion with effective dates expired. // Here we reset an expiring date based on WooCommerce coupon source. $adapted_coupon_to_delete->disable_promotion( $coupon ); return $adapted_coupon_to_delete; } /** * Handle deleting of a coupon. * * @param int $coupon_id */ protected function handle_delete_coupon( int $coupon_id ) { if ( $this->notifications_service->is_ready() ) { $this->maybe_send_delete_notification( $coupon_id ); } if ( ! isset( $this->delete_requests_map[ $coupon_id ] ) ) { return; } $coupon_to_delete = $this->delete_requests_map[ $coupon_id ]; if ( ! empty( $coupon_to_delete->get_synced_google_ids() ) && ! $this->is_already_scheduled_to_delete( $coupon_id ) ) { $this->job_repository->get( DeleteCoupon::class )->schedule( [ $coupon_to_delete, ] ); $this->set_already_scheduled_to_delete( $coupon_id ); } } /** * Send the notification for coupon deletion * * @since 2.8.0 * @param int $coupon_id */ protected function maybe_send_delete_notification( int $coupon_id ): void { $coupon = $this->wc->maybe_get_coupon( $coupon_id ); if ( $coupon instanceof WC_Coupon && $this->coupon_helper->should_trigger_delete_notification( $coupon ) ) { $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_DELETE ); $this->job_repository->get( CouponNotificationJob::class )->schedule( [ 'item_id' => $coupon->get_id(), 'topic' => NotificationsService::TOPIC_COUPON_DELETED, ] ); } } /** * * @param int $coupon_id * @param string $schedule_type * * @return bool */ protected function is_already_scheduled( int $coupon_id, string $schedule_type ): bool { return isset( $this->already_scheduled[ $coupon_id ] ) && $this->already_scheduled[ $coupon_id ] === $schedule_type; } /** * * @param int $coupon_id * * @return bool */ protected function is_already_scheduled_to_update( int $coupon_id ): bool { return $this->is_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_UPDATE ); } /** * * @param int $coupon_id * * @return bool */ protected function is_already_scheduled_to_delete( int $coupon_id ): bool { return $this->is_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_DELETE ); } /** * * @param int $coupon_id * @param string $schedule_type * * @return void */ protected function set_already_scheduled( int $coupon_id, string $schedule_type ): void { $this->already_scheduled[ $coupon_id ] = $schedule_type; } /** * * @param int $coupon_id * * @return void */ protected function set_already_scheduled_to_update( int $coupon_id ): void { $this->set_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_UPDATE ); } /** * * @param int $coupon_id * * @return void */ protected function set_already_scheduled_to_delete( int $coupon_id ): void { $this->set_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_DELETE ); } /** * Schedules notifications for an updated coupon * * @param WC_Coupon $coupon */ protected function handle_update_coupon_notification( WC_Coupon $coupon ) { if ( $this->coupon_helper->should_trigger_create_notification( $coupon ) ) { $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_CREATE ); $this->job_repository->get( CouponNotificationJob::class )->schedule( [ 'item_id' => $coupon->get_id(), 'topic' => NotificationsService::TOPIC_COUPON_CREATED, ] ); } elseif ( $this->coupon_helper->should_trigger_update_notification( $coupon ) ) { $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_UPDATE ); $this->job_repository->get( CouponNotificationJob::class )->schedule( [ 'item_id' => $coupon->get_id(), 'topic' => NotificationsService::TOPIC_COUPON_UPDATED, ] ); } elseif ( $this->coupon_helper->should_trigger_delete_notification( $coupon ) ) { $this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_DELETE ); $this->job_repository->get( CouponNotificationJob::class )->schedule( [ 'item_id' => $coupon->get_id(), 'topic' => NotificationsService::TOPIC_COUPON_DELETED, ] ); } } /** * If product to brands relationship is updated, update the coupons that are related to the brands. * * @param string $taxonomy The taxonomy slug. * @param array $tt_ids An array of term taxonomy IDs. * @param array $old_tt_ids Old array of term taxonomy IDs. */ protected function handle_update_coupon_when_product_brands_updated( string $taxonomy, array $tt_ids, array $old_tt_ids ) { if ( 'product_brand' !== $taxonomy ) { return; } // Convert term taxonomy IDs to integers. $tt_ids = array_map( 'intval', $tt_ids ); $old_tt_ids = array_map( 'intval', $old_tt_ids ); // Find the difference between the new and old term taxonomy IDs. $diff1 = array_diff( $tt_ids, $old_tt_ids ); $diff2 = array_diff( $old_tt_ids, $tt_ids ); $diff = array_merge( $diff1, $diff2 ); if ( empty( $diff ) ) { return; } // Serialize the diff to use in the meta query. // This is needed because the meta value is serialized. $serialized_diff = maybe_serialize( $diff ); $args = [ 'post_type' => 'shop_coupon', 'meta_query' => [ 'relation' => 'OR', [ 'key' => 'product_brands', 'value' => $serialized_diff, 'compare' => 'LIKE', ], [ 'key' => 'exclude_product_brands', 'value' => $serialized_diff, 'compare' => 'LIKE', ], ], ]; // Get coupon posts based on the above query args. $posts = $this->wp->get_posts( $args ); if ( empty( $posts ) ) { return; } foreach ( $posts as $post ) { $this->update_by_id( $post->ID ); } } } PK!`44src/Coupon/WCCouponAdapter.phpnu[wc_coupon_id = $wc_coupon->get_id(); $this->map_woocommerce_coupon( $wc_coupon, $this->get_coupon_destinations( $properties ) ); // Google doesn't expect extra fields, so it's best to remove them unset( $properties['wc_coupon'] ); parent::mapTypes( $properties ); } /** * Map the WooCommerce coupon attributes to the current class. * * @param WC_Coupon $wc_coupon * @param string[] $destinations The destination ID's for the coupon * * @return void */ protected function map_woocommerce_coupon( WC_Coupon $wc_coupon, array $destinations ) { $this->setRedemptionChannel( self::CHANNEL_ONLINE ); $this->setPromotionDestinationIds( $destinations ); $content_language = empty( get_locale() ) ? 'en' : strtolower( substr( get_locale(), 0, 2 ) ); // ISO 639-1. $this->setContentLanguage( $content_language ); $this->map_wc_coupon_id( $wc_coupon ) ->map_wc_general_attributes( $wc_coupon ) ->map_wc_usage_restriction( $wc_coupon ); } /** * Map the WooCommerce coupon ID. * * @param WC_Coupon $wc_coupon * * @return $this */ protected function map_wc_coupon_id( WC_Coupon $wc_coupon ): WCCouponAdapter { $coupon_id = "{$this->get_slug()}_{$wc_coupon->get_id()}"; $this->setPromotionId( $coupon_id ); return $this; } /** * Map the general WooCommerce coupon attributes. * * @param WC_Coupon $wc_coupon * * @return $this */ protected function map_wc_general_attributes( WC_Coupon $wc_coupon ): WCCouponAdapter { $this->setOfferType( self::OFFER_TYPE_GENERIC_CODE ); $this->setGenericRedemptionCode( $wc_coupon->get_code() ); $coupon_amount = $wc_coupon->get_amount(); if ( $wc_coupon->is_type( self::WC_DISCOUNT_TYPE_PERCENT ) ) { $this->setCouponValueType( self::COUPON_VALUE_TYPE_PERCENT_OFF ); $percent_off = round( floatval( $coupon_amount ) ); $this->setPercentOff( $percent_off ); $this->setLongtitle( sprintf( '%d%% off', $percent_off ) ); } elseif ( $wc_coupon->is_type( [ self::WC_DISCOUNT_TYPE_FIXED_CART, self::WC_DISCOUNT_TYPE_FIXED_PRODUCT, ] ) ) { $this->setCouponValueType( self::COUPON_VALUE_TYPE_MONEY_OFF ); $this->setMoneyOffAmount( $this->map_google_price_amount( $coupon_amount ) ); $this->setLongtitle( sprintf( '%d %s off', $coupon_amount, get_woocommerce_currency() ) ); } $this->setPromotionEffectiveTimePeriod( $this->get_wc_coupon_effective_dates( $wc_coupon ) ); return $this; } /** * Return the effective time period for the WooCommerce coupon. * * @param WC_Coupon $wc_coupon * * @return GoogleTimePeriod */ protected function get_wc_coupon_effective_dates( WC_Coupon $wc_coupon ): GoogleTimePeriod { $start_date = $this->get_wc_coupon_start_date( $wc_coupon ); $end_date = $wc_coupon->get_date_expires(); // If there is no expiring date, set to promotion maximumal effective days allowed by Google.\ // Refer to https://support.google.com/merchants/answer/2906014?hl=en if ( empty( $end_date ) ) { $end_date = clone $start_date; $end_date->add( new DateInterval( 'P183D' ) ); } // If the coupon is already expired. set the coupon expires immediately after start date. if ( $end_date < $start_date ) { $end_date = clone $start_date; $end_date->add( new DateInterval( 'PT1S' ) ); } return new GoogleTimePeriod( [ 'startTime' => (string) $start_date, 'endTime' => (string) $end_date, ] ); } /** * Return the start date for the WooCommerce coupon. * * @param WC_Coupon $wc_coupon * * @return WC_DateTime */ protected function get_wc_coupon_start_date( $wc_coupon ): WC_DateTime { new WC_DateTime(); $post_time = get_post_time( self::DATE_TIME_FORMAT, true, $wc_coupon->get_id(), false ); if ( ! empty( $post_time ) ) { return new WC_DateTime( $post_time ); } else { return new WC_DateTime(); } } /** * Map the WooCommerce coupon usage restriction. * * @param WC_Coupon $wc_coupon * * @return $this */ protected function map_wc_usage_restriction( WC_Coupon $wc_coupon ): WCCouponAdapter { $minimal_spend = $wc_coupon->get_minimum_amount(); if ( ! empty( $minimal_spend ) ) { $this->setMinimumPurchaseAmount( $this->map_google_price_amount( $minimal_spend ) ); } $maximal_spend = $wc_coupon->get_maximum_amount(); if ( ! empty( $maximal_spend ) ) { $this->setLimitValue( $this->map_google_price_amount( $maximal_spend ) ); } $has_product_restriction = false; $get_offer_id = function ( int $product_id ) { return WCProductAdapter::get_google_product_offer_id( $this->get_slug(), $product_id ); }; $wc_product_ids = $wc_coupon->get_product_ids(); if ( ! empty( $wc_product_ids ) ) { $google_product_ids = array_map( $get_offer_id, $wc_product_ids ); $has_product_restriction = true; $this->setItemId( $google_product_ids ); } // Currently the brand inclusion restriction will override the product inclustion restriction. // It's align with the current coupon discounts behaviour in WooCommerce. $wc_product_ids_in_brand = $this->get_product_ids_in_brand( $wc_coupon ); if ( ! empty( $wc_product_ids_in_brand ) ) { $google_product_ids = array_map( $get_offer_id, $wc_product_ids_in_brand ); $has_product_restriction = true; $this->setItemId( $google_product_ids ); } // Get excluded product IDs and excluded product IDs in brand. $wc_excluded_product_ids = $wc_coupon->get_excluded_product_ids(); $wc_excluded_product_ids_in_brand = $this->get_product_ids_in_brand( $wc_coupon, true ); if ( ! empty( $wc_excluded_product_ids ) || ! empty( $wc_excluded_product_ids_in_brand ) ) { $google_product_ids = array_merge( array_map( $get_offer_id, $wc_excluded_product_ids ), array_map( $get_offer_id, $wc_excluded_product_ids_in_brand ) ); $google_product_ids = array_values( array_unique( $google_product_ids ) ); $has_product_restriction = true; $this->setItemIdExclusion( $google_product_ids ); } $wc_product_catetories = $wc_coupon->get_product_categories(); if ( ! empty( $wc_product_catetories ) ) { $str_product_categories = WCProductAdapter::convert_product_types( $wc_product_catetories ); $has_product_restriction = true; $this->setProductType( $str_product_categories ); } $wc_excluded_product_catetories = $wc_coupon->get_excluded_product_categories(); if ( ! empty( $wc_excluded_product_catetories ) ) { $str_product_categories = WCProductAdapter::convert_product_types( $wc_excluded_product_catetories ); $has_product_restriction = true; $this->setProductTypeExclusion( $str_product_categories ); } if ( $has_product_restriction ) { $this->setProductApplicability( self::PRODUCT_APPLICABILITY_SPECIFIC_PRODUCTS ); } else { $this->setProductApplicability( self::PRODUCT_APPLICABILITY_ALL_PRODUCTS ); } return $this; } /** * Map WooCommerce price number to Google price structure. * * @param float $wc_amount * * @return GooglePriceAmount */ protected function map_google_price_amount( $wc_amount ): GooglePriceAmount { return new GooglePriceAmount( [ 'currency' => get_woocommerce_currency(), 'value' => $wc_amount, ] ); } /** * Disable promotion shared in Google by only updating promotion effective end_date * to make the promotion expired. * * @param WC_Coupon $wc_coupon */ public function disable_promotion( WC_Coupon $wc_coupon ) { $start_date = $this->get_wc_coupon_start_date( $wc_coupon ); // Set promotion to be disabled immediately. $end_date = new WC_DateTime(); // If this coupon is scheduled in the future, disable it right after start date. if ( $start_date >= $end_date ) { $end_date = clone $start_date; $end_date->add( new DateInterval( 'PT1S' ) ); } $this->setPromotionEffectiveTimePeriod( new GoogleTimePeriod( [ 'startTime' => (string) $start_date, 'endTime' => (string) $end_date, ] ) ); } /** * * @param ClassMetadata $metadata */ public static function load_validator_metadata( ClassMetadata $metadata ) { $metadata->addPropertyConstraint( 'targetCountry', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'promotionId', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'genericRedemptionCode', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'productApplicability', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'offerType', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'redemptionChannel', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'couponValueType', new Assert\NotBlank() ); } /** * * @return int $wc_coupon_id */ public function get_wc_coupon_id(): int { return $this->wc_coupon_id; } /** * * @param string $targetCountry * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase */ public function setTargetCountry( $targetCountry ) { // set the new target country parent::setTargetCountry( $targetCountry ); } /** * Get the destinations allowed per specific country. * * @param array $coupon_data The coupon data to get the allowed destinations. * @return string[] The destinations country based. */ private function get_coupon_destinations( array $coupon_data ): array { $destinations = [ self::PROMOTION_DESTINATION_ADS ]; if ( isset( $coupon_data['targetCountry'] ) && in_array( $coupon_data['targetCountry'], self::COUNTRIES_WITH_FREE_SHIPPING_DESTINATION, true ) ) { $destinations[] = self::PROMOTION_DESTINATION_FREE_LISTING; } return apply_filters( 'woocommerce_gla_coupon_destinations', $destinations, $coupon_data ); } /** * Get the product IDs that belongs to a brand. * * @param WC_Coupon $wc_coupon The WC coupon object. * @param bool $is_exclude If the product IDs are for exclusion. * @return string[] The product IDs that belongs to a brand. */ private function get_product_ids_in_brand( WC_Coupon $wc_coupon, bool $is_exclude = false ) { $coupon_id = $wc_coupon->get_id(); $meta_key = $is_exclude ? 'exclude_product_brands' : 'product_brands'; // Get the brand term IDs if brand restriction is set. $brand_term_ids = get_post_meta( $coupon_id, $meta_key ); if ( ! is_array( $brand_term_ids ) ) { return []; } $product_ids = []; foreach ( $brand_term_ids as $brand_term_id ) { // Get the product IDs that belongs to the brand. $object_ids = get_objects_in_term( $brand_term_id, 'product_brand' ); if ( is_wp_error( $object_ids ) ) { continue; } $product_ids = array_merge( $product_ids, $object_ids ); } return $product_ids; } } PK!!&src/DB/Migration/AbstractMigration.phpnu[wpdb = $wpdb; } } PK!ף}  1src/DB/Migration/Migration20211228T1640692399.phpnu[shipping_rate_table = $shipping_rate_table; $this->options = $options; } /** * Returns the version to apply this migration for. * * @return string A version number. For example: 1.4.1 */ public function get_applicable_version(): string { return '1.12.2'; } /** * Apply the migrations. * * @return void */ public function apply(): void { if ( $this->shipping_rate_table->exists() ) { $mc_settings = $this->options->get( OptionsInterface::MERCHANT_CENTER ); if ( ! is_array( $mc_settings ) ) { return; } if ( isset( $mc_settings['offers_free_shipping'] ) && false !== boolval( $mc_settings['offers_free_shipping'] ) && isset( $mc_settings['free_shipping_threshold'] ) ) { // Move the free shipping threshold from the options to the shipping rate table. $options_json = wp_json_encode( [ 'free_shipping_threshold' => (float) $mc_settings['free_shipping_threshold'] ] ); // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $this->wpdb->query( $this->wpdb->prepare( "UPDATE `{$this->wpdb->_escape( $this->shipping_rate_table->get_name() )}` SET `options`=%s WHERE 1=1", $options_json ) ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } // Remove the free shipping threshold from the options. unset( $mc_settings['free_shipping_threshold'] ); unset( $mc_settings['offers_free_shipping'] ); $this->options->update( OptionsInterface::MERCHANT_CENTER, $mc_settings ); } } } PK!=pp1src/DB/Migration/Migration20220524T1653383133.phpnu[budget_rate_table = $budget_rate_table; } /** * Returns the version to apply this migration for. * * @return string A version number. For example: 1.4.1 */ public function get_applicable_version(): string { return '1.13.3'; } /** * Apply the migrations. * * @return void */ public function apply(): void { $this->budget_rate_table->reload_data(); } } PK!նbb1src/DB/Migration/Migration20231109T1653383133.phpnu[budget_rate_table = $budget_rate_table; } /** * Returns the version to apply this migration for. * * @return string A version number. For example: 1.4.1 */ public function get_applicable_version(): string { return '2.5.13'; } /** * Apply the migrations. * * @return void */ public function apply(): void { if ( $this->budget_rate_table->exists() && $this->budget_rate_table->has_column( 'daily_budget_low' ) ) { $this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->budget_rate_table->get_name() )}` DROP COLUMN `daily_budget_low`" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } if ( $this->budget_rate_table->exists() && $this->budget_rate_table->has_column( 'daily_budget_high' ) ) { $this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->budget_rate_table->get_name() )}` DROP COLUMN `daily_budget_high`" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } $this->budget_rate_table->reload_data(); } } PK!׍1src/DB/Migration/Migration20240813T1653383133.phpnu[shipping_time_table = $shipping_time_table; } /** * Returns the version to apply this migration for. * * @return string A version number. For example: 1.4.1 */ public function get_applicable_version(): string { return '2.9.1'; } /** * Apply the migrations. * * @return void */ public function apply(): void { if ( $this->shipping_time_table->exists() && ! $this->shipping_time_table->has_column( 'max_time' ) ) { $this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->shipping_time_table->get_name() )}` Add COLUMN `max_time` bigint(20) NOT NULL default 0" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } // Fill the new column with the current values $this->wpdb->query( "UPDATE `{$this->wpdb->_escape( $this->shipping_time_table->get_name() )}` SET `max_time`=`time` WHERE 1=1" ); } } PK!y22'src/DB/Migration/MigrationInterface.phpnu[mc_issues_table = $mc_issues_table; } /** * Returns the version to apply this migration for. * * @return string A version number. For example: 1.4.1 */ public function get_applicable_version(): string { return '1.4.1'; } /** * Apply the migrations. * * @return void */ public function apply(): void { if ( $this->mc_issues_table->exists() && $this->mc_issues_table->has_index( 'product_issue' ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $this->wpdb->query( "ALTER TABLE `{$this->wpdb->_escape( $this->mc_issues_table->get_name() )}` DROP INDEX `product_issue`" ); } } } PK!Nssrc/DB/Migration/Migrator.phpnu[migrations = $migrations; // Sort migrations by version. uasort( $this->migrations, function ( MigrationInterface $migration_a, MigrationInterface $migration_b ) { return version_compare( $migration_a->get_applicable_version(), $migration_b->get_applicable_version() ); } ); } /** * Run migrations. * * @param string $old_version Previous version before updating. * @param string $new_version Current version after updating. */ public function migrate( string $old_version, string $new_version ): void { // bail if both versions are equal. if ( 0 === version_compare( $old_version, $new_version ) ) { return; } foreach ( $this->migrations as $migration ) { if ( $this->can_apply( $migration->get_applicable_version(), $old_version, $new_version ) ) { $migration->apply(); } } } /** * @param string $migration_version The applicable version of the migration. * @param string $old_version Previous version before updating. * @param string $new_version Current version after updating. * * @return bool True if migration should be applied. */ protected function can_apply( string $migration_version, string $old_version, string $new_version ): bool { return version_compare( $old_version, $new_version, '<' ) && version_compare( $old_version, $migration_version, '<' ) && version_compare( $migration_version, $new_version, '<=' ); } } PK!I+src/DB/Query/AttributeMappingRulesQuery.phpnu[where( 'id', $rule_id )->get_row(); } } PK!z]7*src/DB/Query/BudgetRecommendationQuery.phpnu[results = array_map( function ( $row ) { $row['options'] = ! empty( $row['options'] ) ? json_decode( $row['options'], true ) : $row['options']; return $row; }, $this->results ); } } PK!Dget_results(); $items = []; foreach ( $times as $time ) { $data = [ 'country_code' => $time['country'], 'time' => $time['time'], 'max_time' => $time['max_time'] ?: $time['time'], ]; $items[ $time['country'] ] = $data; } return $items; } } PK!N1NN+src/DB/Table/AttributeMappingRulesTable.phpnu[get_sql_safe_name()}` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `attribute` varchar(255) NOT NULL, `source` varchar(100) NOT NULL, `category_condition_type` varchar(10) NOT NULL, `categories` text NOT NULL, PRIMARY KEY `id` (`id`) ) {$this->get_collation()}; "; } /** * Get the un-prefixed (raw) table name. * * @return string */ public static function get_raw_name(): string { return 'attribute_mapping_rules'; } /** * Get the columns for the table. * * @return array */ public function get_columns(): array { return [ 'id' => true, 'attribute' => true, 'source' => true, 'category_condition_type' => true, 'categories' => true, ]; } } PK!Ϡ11*src/DB/Table/BudgetRecommendationTable.phpnu[get_sql_safe_name()}` ( id bigint(20) NOT NULL AUTO_INCREMENT, currency varchar(3) NOT NULL, country varchar(2) NOT NULL, daily_budget int(11) NOT NULL, PRIMARY KEY (id), UNIQUE KEY country_currency (country, currency) ) {$this->get_collation()}; "; } /** * Install the Database table. * * Add data if there is none. */ public function install(): void { parent::install(); // Load the data if the table is empty. // phpcs:ignore WordPress.DB.PreparedSQL $result = $this->wpdb->get_row( "SELECT COUNT(*) AS count FROM `{$this->get_sql_safe_name()}`" ); if ( empty( $result->count ) ) { $this->load_initial_data(); } } /** * Reload initial data. * * @return void */ public function reload_data(): void { if ( $this->exists() && ! $this->has_loaded_initial_data ) { $this->truncate(); $this->load_initial_data(); } } /** * Get the un-prefixed (raw) table name. * * @return string */ public static function get_raw_name(): string { return 'budget_recommendations'; } /** * Get the columns for the table. * * @return array */ public function get_columns(): array { return [ 'id' => true, 'currency' => true, 'country' => true, 'daily_budget' => true, ]; } /** * Load packaged recommendation data on the first install of GLA. * * Inserts 500 records at a time. */ private function load_initial_data(): void { $path = $this->get_root_dir() . '/data/budget-recommendations.csv'; $chunk_size = 500; if ( file_exists( $path ) ) { $csv = array_map( function ( $row ) { return str_getcsv( $row, ',', '"', '\\' ); }, file( $path ) ); // Remove the headers array_shift( $csv ); if ( empty( $csv ) ) { return; } $values = []; $placeholders = []; // Build placeholders for each row, and add values to data array foreach ( $csv as $row ) { if ( empty( $row ) ) { continue; } $row_placeholders = []; foreach ( $row as $value ) { $values[] = $value; $row_placeholders[] = is_numeric( $value ) ? '%d' : '%s'; } $placeholders[] = '(' . implode( ', ', $row_placeholders ) . ')'; if ( count( $placeholders ) >= $chunk_size ) { $this->insert_chunk( $placeholders, $values ); $placeholders = []; $values = []; } } $this->insert_chunk( $placeholders, $values ); } $this->has_loaded_initial_data = true; } /** * Insert a chunk of budget recommendations * * @param string[] $placeholders * @param array $values */ private function insert_chunk( array $placeholders, array $values ): void { $sql = "INSERT INTO `{$this->get_sql_safe_name()}` (country,daily_budget,currency) VALUES\n"; $sql .= implode( ",\n", $placeholders ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->wpdb->query( $this->wpdb->prepare( $sql, $values ) ); } } PK!& #src/DB/Table/MerchantIssueTable.phpnu[get_sql_safe_name()}` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` bigint(20) NOT NULL, `issue` varchar(200) NOT NULL, `code` varchar(100) NOT NULL, `severity` varchar(20) NOT NULL DEFAULT 'warning', `product` varchar(100) NOT NULL, `action` text NOT NULL, `action_url` varchar(1024) NOT NULL, `applicable_countries` text NOT NULL, `source` varchar(10) NOT NULL DEFAULT 'mc', `type` varchar(10) NOT NULL DEFAULT 'product', `created_at` datetime NOT NULL, PRIMARY KEY `id` (`id`) ) {$this->get_collation()}; "; } /** * Get the un-prefixed (raw) table name. * * @return string */ public static function get_raw_name(): string { return 'merchant_issues'; } /** * Delete stale issue records. * * @param DateTime $created_before Delete all records created before this. */ public function delete_stale( DateTime $created_before ): void { $query = "DELETE FROM `{$this->get_sql_safe_name()}` WHERE `created_at` < '%s'"; $this->wpdb->query( $this->wpdb->prepare( $query, $created_before->format( 'Y-m-d H:i:s' ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * Delete product issues for specific products and source. * * @param array $products_ids Array of product IDs to delete issues for. * @param string $source The source of the issues. Default is 'mc'. */ public function delete_specific_product_issues( array $products_ids, string $source = 'mc' ): void { if ( empty( $products_ids ) ) { return; } $placeholder = '(' . implode( ',', array_fill( 0, count( $products_ids ), '%d' ) ) . ')'; $this->wpdb->query( $this->wpdb->prepare( "DELETE FROM `{$this->get_sql_safe_name()}` WHERE `product_id` IN {$placeholder} AND `source` = %s", array_merge( $products_ids, [ $source ] ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * Get the columns for the table. * * @return array */ public function get_columns(): array { return [ 'id' => true, 'product_id' => true, 'code' => true, 'severity' => true, 'issue' => true, 'product' => true, 'action' => true, 'action_url' => true, 'applicable_countries' => true, 'source' => true, 'type' => true, 'created_at' => true, ]; } } PK!"src/DB/Table/ShippingRateTable.phpnu[get_sql_safe_name()}` ( id bigint(20) NOT NULL AUTO_INCREMENT, country varchar(2) NOT NULL, currency varchar(3) NOT NULL, rate double NOT NULL default 0, options text DEFAULT NULL, PRIMARY KEY (id), KEY country (country), KEY currency (currency) ) {$this->get_collation()}; "; } /** * Get the un-prefixed (raw) table name. * * @return string */ public static function get_raw_name(): string { return 'shipping_rates'; } /** * Get the columns for the table. * * @return array */ public function get_columns(): array { return [ 'id' => true, 'country' => true, 'currency' => true, 'rate' => true, 'options' => true, ]; } } PK!8G"src/DB/Table/ShippingTimeTable.phpnu[get_sql_safe_name()}` ( id bigint(20) NOT NULL AUTO_INCREMENT, country varchar(2) NOT NULL, time bigint(20) NOT NULL default 0, max_time bigint(20) NOT NULL default 0, PRIMARY KEY (id), KEY country (country) ) {$this->get_collation()}; "; } /** * Get the un-prefixed (raw) table name. * * @return string */ public static function get_raw_name(): string { return 'shipping_times'; } /** * Get the columns for the table. * * @return array */ public function get_columns(): array { return [ 'id' => true, 'country' => true, 'time' => true, 'max_time' => true, ]; } } PK!I2src/DB/Installer.phpnu[table_manager = $table_manager; $this->migrator = $migrator; } /** * Run installation logic for this class. * * @param string $old_version Previous version before updating. * @param string $new_version Current version after updating. */ public function install( string $old_version, string $new_version ): void { foreach ( $this->table_manager->get_tables() as $table ) { $table->install(); } // Run migrations. $this->migrator->migrate( $old_version, $new_version ); } /** * Logic to run when the plugin is first installed. */ public function first_install(): void { foreach ( $this->table_manager->get_tables() as $table ) { if ( $table instanceof FirstInstallInterface ) { $table->first_install(); } } } } PK!)Y!src/DB/ProductFeedQueryHelper.phpnu[wpdb = $wpdb; $this->product_repository = $product_repository; } /** * Retrieve an array of product information using the request params. * * @param WP_REST_Request $request * * @return array * * @throws InvalidValue If the orderby value isn't valid. * @throws Exception If the status data can't be retrieved from Google. */ public function get( WP_REST_Request $request ): array { $this->request = $request; $products = []; $args = $this->prepare_query_args(); $refresh_status_data_job = null; list( $limit, $offset ) = $this->prepare_query_pagination(); $mc_service = $this->container->get( MerchantCenterService::class ); if ( $mc_service->is_connected() ) { $refresh_status_data_job = $this->container->get( MerchantStatuses::class )->maybe_refresh_status_data(); } /** @var ProductHelper $product_helper */ $product_helper = $this->container->get( ProductHelper::class ); add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 ); foreach ( $this->product_repository->find( $args, $limit, $offset ) as $product ) { $id = $product->get_id(); $errors = $product_helper->get_validation_errors( $product ); $mc_status = $product_helper->get_mc_status( $product ) ?: $product_helper->get_sync_status( $product ); // If the refresh_status_data_job is scheduled, we don't know the status yet as it is being refreshed. if ( $refresh_status_data_job && $refresh_status_data_job->is_scheduled() ) { $mc_status = null; } $products[ $id ] = [ 'id' => $id, 'title' => $product->get_name(), 'visible' => $product_helper->get_channel_visibility( $product ) !== ChannelVisibility::DONT_SYNC_AND_SHOW, 'status' => $mc_status, 'image_url' => wp_get_attachment_image_url( $product->get_image_id(), 'full' ), 'price' => $product->get_price(), 'errors' => array_values( $errors ), ]; } remove_filter( 'posts_where', [ $this, 'title_filter' ] ); return array_values( $products ); } /** * Count the number of products (using title filter if present). * * @param WP_REST_Request $request * * @return int * * @throws InvalidValue If the orderby value isn't valid. */ public function count( WP_REST_Request $request ): int { $this->request = $request; $args = $this->prepare_query_args(); add_filter( 'posts_where', [ $this, 'title_filter' ], 10, 2 ); $ids = $this->product_repository->find_ids( $args ); remove_filter( 'posts_where', [ $this, 'title_filter' ] ); return count( $ids ); } /** * Prepare the args to be used to retrieve the products, namely orderby, meta_query and type. * * @return array * * @throws InvalidValue If the orderby value isn't valid. */ protected function prepare_query_args(): array { $product_types = ProductSyncer::get_supported_product_types(); $product_types = array_diff( $product_types, [ 'variation' ] ); $args = [ 'type' => $product_types, 'status' => 'publish', 'orderby' => [ 'title' => 'ASC' ], ]; if ( ! empty( $this->request['ids'] ) ) { $args['include'] = explode( ',', $this->request['ids'] ); } if ( ! empty( $this->request['search'] ) ) { $args['gla_search'] = $this->request['search']; } if ( empty( $this->request['orderby'] ) ) { return $args; } switch ( $this->request['orderby'] ) { case 'title': $args['orderby']['title'] = $this->get_order(); break; case 'id': $args['orderby'] = [ 'ID' => $this->get_order() ] + $args['orderby']; break; case 'visible': $args['meta_key'] = $this->prefix_meta_key( ProductMetaHandler::KEY_VISIBILITY ); $args['orderby'] = [ 'meta_value' => $this->get_order() ] + $args['orderby']; break; case 'status': $args['meta_key'] = $this->prefix_meta_key( ProductMetaHandler::KEY_MC_STATUS ); $args['orderby'] = [ 'meta_value' => $this->get_order() ] + $args['orderby']; break; case 'total_sales': $args['meta_key'] = self::META_KEY_TOTAL_SALES; $args['orderby'] = [ 'meta_value_num' => $this->get_order() ] + $args['orderby']; break; default: throw InvalidValue::not_in_allowed_list( 'orderby', [ 'title', 'id', 'visible', 'status', 'total_sales' ] ); } return $args; } /** * Convert the per_page and page parameters into limit and offset values. * * @return array Containing limit and offset values. */ protected function prepare_query_pagination(): array { $limit = -1; $offset = 0; if ( ! empty( $this->request['per_page'] ) ) { $limit = intval( $this->request['per_page'] ); $page = max( 1, intval( $this->request['page'] ) ); $offset = $limit * ( $page - 1 ); } return [ $limit, $offset ]; } /** * Filter for the posts_where hook, adds WHERE clause to search * for the 'search' parameter in the product titles (when present). * * @param string $where The WHERE clause of the query. * @param WP_Query $wp_query The WP_Query instance (passed by reference). * * @return string The updated WHERE clause. */ public function title_filter( string $where, WP_Query $wp_query ): string { $gla_search = $wp_query->get( 'gla_search' ); if ( $gla_search ) { $title_search = '%' . $this->wpdb->esc_like( $gla_search ) . '%'; $where .= $this->wpdb->prepare( " AND `{$this->wpdb->posts}`.`post_title` LIKE %s", $title_search ); // phpcs:ignore WordPress.DB.PreparedSQL } return $where; } /** * Return the ORDER BY order based on the order request parameter value. * * @return string */ protected function get_order(): string { return strtoupper( $this->request['order'] ?? '' ) === 'DESC' ? 'DESC' : 'ASC'; } } PK!$P !src/DB/ProductMetaQueryHelper.phpnu[wpdb = $wpdb; } /** * Get all values for a given meta_key as post_id=>meta_value. * * @param string $meta_key The meta value to retrieve for all posts. * @return array Meta values by post ID. * * @throws InvalidMeta If the meta key isn't valid. */ public function get_all_values( string $meta_key ): array { self::validate_meta_key( $meta_key ); $query = "SELECT post_id, meta_value FROM {$this->wpdb->postmeta} WHERE meta_key = %s"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $results = $this->wpdb->get_results( $this->wpdb->prepare( $query, $this->prefix_meta_key( $meta_key ) ) ); $return = []; foreach ( $results as $r ) { $return[ $r->post_id ] = maybe_unserialize( $r->meta_value ); } return $return; } /** * Delete all values for a given meta_key. * * @since 2.6.4 * * @param string $meta_key The meta key to delete. * * @throws InvalidMeta If the meta key isn't valid. */ public function delete_all_values( string $meta_key ) { self::validate_meta_key( $meta_key ); $meta_key = $this->prefix_meta_key( $meta_key ); $query = "DELETE FROM {$this->wpdb->postmeta} WHERE meta_key = %s"; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->wpdb->query( $this->wpdb->prepare( $query, $meta_key ) ); } /** * @param string $meta_key The meta key to validate * * @throws InvalidMeta If the meta key isn't valid. */ protected static function validate_meta_key( string $meta_key ) { if ( ! ProductMetaHandler::is_meta_key_valid( $meta_key ) ) { do_action( 'woocommerce_gla_error', sprintf( 'Product meta key is invalid: %s', $meta_key ), __METHOD__ ); throw InvalidMeta::invalid_key( $meta_key ); } } } PK!"( src/DB/QueryInterface.phpnu[, IN, NOT IN. * * @return $this */ public function where( string $column, $value, string $compare = '=' ): QueryInterface; /** * Set the where relation for the query. * * @param string $relation * * @return QueryInterface */ public function set_where_relation( string $relation ): QueryInterface; /** * @param string $column * @param string $order * * @return QueryInterface */ public function set_order( string $column, string $order = 'DESC' ): QueryInterface; /** * Limit the number of results for the query. * * @param int $limit * * @return QueryInterface */ public function set_limit( int $limit ): QueryInterface; /** * Set an offset for the results. * * @param int $offset * * @return QueryInterface */ public function set_offset( int $offset ): QueryInterface; /** * Get the results of the query. * * @return mixed */ public function get_results(); /** * Get the number of results returned by the query. * * @return int */ public function get_count(): int; /** * Gets the first result of the query. * * @return array */ public function get_row(): array; /** * Insert a row of data into the table. * * @param array $data * * @return int * @throws InvalidQuery When there is an error inserting the data. */ public function insert( array $data ): int; /** * Returns the last inserted ID. Must be called after insert. * * @since 1.12.0 * * @return int|null */ public function last_insert_id(): ?int; /** * Delete rows from the database. * * @param string $where_column Column to use when looking for values to delete. * @param mixed $value Value to use when determining what rows to delete. * * @return int The number of rows deleted. * @throws InvalidQuery When there is an error deleting data. */ public function delete( string $where_column, $value ): int; /** * Update data in the database. * * @param array $data Array of columns and their values. * @param array $where Array of where conditions for updating values. * * @return int * @throws InvalidQuery When there is an error updating data, or when an empty where array is provided. */ public function update( array $data, array $where ): int; /** * Batch update or insert a set of records. * * @param array $records Array of records to be updated or inserted. * * @throws InvalidQuery If an invalid column name is provided. */ public function update_or_insert( array $records ): void; } PK!GgJ.J.src/DB/Query.phpnu[wpdb = $wpdb; $this->table = $table; } /** * Add a where clause to the query. * * @param string $column The column name. * @param mixed $value The where value. * @param string $compare The comparison to use. Valid values are =, <, >, IN, NOT IN. * * @return $this */ public function where( string $column, $value, string $compare = '=' ): QueryInterface { $this->validate_column( $column ); $this->validate_compare( $compare ); $this->where[] = [ 'column' => $column, 'value' => $value, 'compare' => $compare, ]; return $this; } /** * Add a group by clause to the query. * * @param string $column The column name. * * @return $this * * @since 1.12.0 */ public function group_by( string $column ): QueryInterface { $this->validate_column( $column ); $this->groupby[] = "`{$column}`"; return $this; } /** * Set the where relation for the query. * * @param string $relation * * @return QueryInterface */ public function set_where_relation( string $relation ): QueryInterface { $this->validate_where_relation( $relation ); $this->where_relation = $relation; return $this; } /** * Set ordering information for the query. * * @param string $column * @param string $order * * @return QueryInterface */ public function set_order( string $column, string $order = 'ASC' ): QueryInterface { $this->validate_column( $column ); $this->orderby[] = "`{$column}` {$this->normalize_order( $order )}"; return $this; } /** * Limit the number of results for the query. * * @param int $limit * * @return QueryInterface */ public function set_limit( int $limit ): QueryInterface { $this->limit = ( new PositiveInteger( $limit ) )->get(); return $this; } /** * Set an offset for the results. * * @param int $offset * * @return QueryInterface */ public function set_offset( int $offset ): QueryInterface { $this->offset = ( new PositiveInteger( $offset ) )->get(); return $this; } /** * Get the results of the query. * * @return mixed */ public function get_results() { if ( null === $this->results ) { $this->query_results(); } return $this->results; } /** * Get the number of results returned by the query. * * @return int */ public function get_count(): int { if ( null === $this->count ) { $this->count_results(); } return $this->count; } /** * Gets the first result of the query. * * @return array */ public function get_row(): array { if ( null === $this->results ) { $old_limit = $this->limit ?? 0; $this->set_limit( 1 ); $this->query_results(); $this->set_limit( $old_limit ); } return $this->results[0] ?? []; } /** * Perform the query and save it to the results. */ protected function query_results() { $this->results = $this->wpdb->get_results( $this->build_query(), // phpcs:ignore WordPress.DB.PreparedSQL ARRAY_A ); } /** * Count the results and save the result. */ protected function count_results() { $this->count = (int) $this->wpdb->get_var( $this->build_query( true ) ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * Validate that a given column is valid for the current table. * * @param string $column * * @throws InvalidQuery When the given column is not valid for the current table. */ protected function validate_column( string $column ) { if ( ! array_key_exists( $column, $this->table->get_columns() ) ) { throw InvalidQuery::from_column( $column, get_class( $this->table ) ); } } /** * Validate that a compare operator is valid. * * @param string $compare * * @throws InvalidQuery When the compare value is not valid. */ protected function validate_compare( string $compare ) { switch ( $compare ) { case '=': case '>': case '<': case 'IN': case 'NOT IN': // These are all valid. return; default: throw InvalidQuery::from_compare( $compare ); } } /** * Validate that a where relation is valid. * * @param string $relation * * @throws InvalidQuery When the relation value is not valid. */ protected function validate_where_relation( string $relation ) { switch ( $relation ) { case 'AND': case 'OR': // These are all valid. return; default: throw InvalidQuery::where_relation( $relation ); } } /** * Normalize the string for the order. * * Converts the string to uppercase, and will return only DESC or ASC. * * @param string $order * * @return string */ protected function normalize_order( string $order ): string { $order = strtoupper( $order ); return 'DESC' === $order ? $order : 'ASC'; } /** * Build the query and return the query string. * * @param bool $get_count False to build a normal query, true to build a COUNT(*) query. * * @return string */ protected function build_query( bool $get_count = false ): string { $columns = $get_count ? 'COUNT(*)' : '*'; $pieces = [ "SELECT {$columns} FROM `{$this->table->get_name()}`" ]; $pieces = array_merge( $pieces, $this->generate_where_pieces() ); if ( ! empty( $this->groupby ) ) { $pieces[] = 'GROUP BY ' . implode( ', ', $this->groupby ); } if ( ! $get_count ) { if ( $this->orderby ) { $pieces[] = 'ORDER BY ' . implode( ', ', $this->orderby ); } if ( $this->limit ) { $pieces[] = "LIMIT {$this->limit}"; } if ( $this->offset ) { $pieces[] = "OFFSET {$this->offset}"; } } return join( "\n", $pieces ); } /** * Generate the pieces for the WHERE part of the query. * * @return string[] */ protected function generate_where_pieces(): array { if ( empty( $this->where ) ) { return []; } $where_pieces = [ 'WHERE' ]; foreach ( $this->where as $where ) { $column = $where['column']; $compare = $where['compare']; if ( $compare === 'IN' || $compare === 'NOT IN' ) { $value = sprintf( "('%s')", join( "','", array_map( function ( $value ) { return $this->wpdb->_escape( $value ); }, $where['value'] ) ) ); } else { $value = "'{$this->wpdb->_escape( $where['value'] )}'"; } if ( count( $where_pieces ) > 1 ) { $where_pieces[] = $this->where_relation ?? 'AND'; } $where_pieces[] = "{$column} {$compare} {$value}"; } return $where_pieces; } /** * Insert a row of data into the table. * * @param array $data * * @return int * @throws InvalidQuery When there is an error inserting the data. */ public function insert( array $data ): int { foreach ( $data as $column => &$value ) { $this->validate_column( $column ); $value = $this->sanitize_value( $column, $value ); } $result = $this->wpdb->insert( $this->table->get_name(), $data ); if ( false === $result ) { throw InvalidQuery::from_insert( $this->wpdb->last_error ?: 'Error inserting data.' ); } // Save a local copy of the last inserted ID. $this->last_insert_id = $this->wpdb->insert_id; return $result; } /** * Returns the last inserted ID. Must be called after insert. * * @since 1.12.0 * * @return int|null */ public function last_insert_id(): ?int { return $this->last_insert_id; } /** * Delete rows from the database. * * @param string $where_column Column to use when looking for values to delete. * @param mixed $value Value to use when determining what rows to delete. * * @return int The number of rows deleted. * @throws InvalidQuery When there is an error deleting data. */ public function delete( string $where_column, $value ): int { $this->validate_column( $where_column ); $result = $this->wpdb->delete( $this->table->get_name(), [ $where_column => $value ] ); if ( false === $result ) { throw InvalidQuery::from_delete( $this->wpdb->last_error ?: 'Error deleting data.' ); } return $result; } /** * Update data in the database. * * @param array $data Array of columns and their values. * @param array $where Array of where conditions for updating values. * * @return int * @throws InvalidQuery When there is an error updating data, or when an empty where array is provided. */ public function update( array $data, array $where ): int { if ( empty( $where ) ) { throw InvalidQuery::empty_where(); } foreach ( $data as $column => &$value ) { $this->validate_column( $column ); $value = $this->sanitize_value( $column, $value ); } $result = $this->wpdb->update( $this->table->get_name(), $data, $where ); if ( false === $result ) { throw InvalidQuery::from_update( $this->wpdb->last_error ?: 'Error updating data.' ); } return $result; } /** * Batch update or insert a set of records. * * @param array $records Array of records to be updated or inserted. * * @throws InvalidQuery If an invalid column name is provided. */ public function update_or_insert( array $records ): void { if ( empty( $records ) ) { return; } $update_values = []; $columns = array_keys( reset( $records ) ); foreach ( $columns as $c ) { $this->validate_column( $c ); $update_values[] = "`$c`=VALUES(`$c`)"; } $single_placeholder = '(' . implode( ',', array_fill( 0, count( $columns ), "'%s'" ) ) . ')'; $chunk_size = 200; $num_issues = count( $records ); for ( $i = 0; $i < $num_issues; $i += $chunk_size ) { $all_values = []; $all_placeholders = []; foreach ( array_slice( $records, $i, $chunk_size ) as $issue ) { if ( array_keys( $issue ) !== $columns ) { throw new InvalidQuery( 'Not all records contain the same columns' ); } $all_placeholders[] = $single_placeholder; array_push( $all_values, ...array_values( $issue ) ); } $column_names = '(`' . implode( '`, `', $columns ) . '`)'; $query = "INSERT INTO `{$this->table->get_name()}` $column_names VALUES "; $query .= implode( ', ', $all_placeholders ); $query .= ' ON DUPLICATE KEY UPDATE ' . implode( ', ', $update_values ); $this->wpdb->query( $this->wpdb->prepare( $query, $all_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL } } /** * Sanitize a value for a given column before inserting it into the DB. * * @param string $column The column name. * @param mixed $value The value to sanitize. * * @return mixed The sanitized value. */ abstract protected function sanitize_value( string $column, $value ); } PK!'src/DB/TableInterface.phpnu[ true, BudgetRecommendationTable::class => true, MerchantIssueTable::class => true, ShippingRateTable::class => true, ShippingTimeTable::class => true, ]; /** * @var Table[] */ protected $tables; /** * TableManager constructor. * * @param Table[] $tables */ public function __construct( array $tables ) { $this->setup_tables( $tables ); } /** * @return Table[] * * @see \Automattic\WooCommerce\GoogleListingsAndAds\DB\Installer::install for installing these tables. */ public function get_tables(): array { return $this->tables; } /** * Returns a list of table names to be installed. * * @return string[] Table names * * @see TableManager::VALID_TABLES for the list of valid table classes. */ public static function get_all_table_names(): array { $tables = []; foreach ( array_keys( self::VALID_TABLES ) as $table_class ) { $table_name = call_user_func( [ $table_class, 'get_raw_name' ] ); $tables[ $table_name ] = $table_name; } return $tables; } /** * Set up each of the table controllers. * * @param Table[] $tables */ protected function setup_tables( array $tables ) { foreach ( $tables as $table ) { $this->validate_instanceof( $table, Table::class ); // only include tables from the installable tables list. if ( isset( self::VALID_TABLES[ get_class( $table ) ] ) ) { $this->tables[] = $table; } } } } PK!#0_src/DB/Table.phpnu[wp = $wp; $this->wpdb = $wpdb; } /** * Install the Database table. */ public function install(): void { $this->wp->db_delta( $this->get_install_query() ); } /** * Determine whether the table actually exists in the DB. * * @return bool */ public function exists(): bool { $result = $this->wpdb->get_var( "SHOW TABLES LIKE '{$this->wpdb->esc_like( $this->get_name() )}'" // phpcs:ignore WordPress.DB.PreparedSQL ); return $result === $this->get_name(); } /** * Delete the Database table. */ public function delete(): void { $this->wpdb->query( "DROP TABLE IF EXISTS `{$this->get_sql_safe_name()}`" ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * Truncate the Database table. */ public function truncate(): void { $this->wpdb->query( "TRUNCATE TABLE `{$this->get_sql_safe_name()}`" ); // phpcs:ignore WordPress.DB.PreparedSQL } /** * Get the SQL escaped version of the table name. * * @return string */ protected function get_sql_safe_name(): string { return $this->wpdb->_escape( $this->get_name() ); } /** * Get the name of the Database table. * * The name is prefixed with the wpdb prefix, and our plugin prefix. * * @return string */ public function get_name(): string { return "{$this->wpdb->prefix}{$this->get_slug()}_{$this->get_raw_name()}"; } /** * Get the primary column name for the table. * * @return string */ public function get_primary_column(): string { return 'id'; } /** * Checks whether an index exists for the table. * * @param string $index_name The index name. * * @return bool True if the index exists on the table and False if not. * * @since 1.4.1 */ public function has_index( string $index_name ): bool { $result = $this->wpdb->get_results( $this->wpdb->prepare( "SHOW INDEX FROM `{$this->get_sql_safe_name()}` WHERE Key_name = %s ", [ $index_name ] ) // phpcs:ignore WordPress.DB.PreparedSQL ); return ! empty( $result ); } /** * Get the DB collation. * * @return string */ protected function get_collation(): string { return $this->wpdb->has_cap( 'collation' ) ? $this->wpdb->get_charset_collate() : ''; } /** * Checks whether a column exists for the table. * * @param string $column_name The column name. * * @return bool True if the column exists on the table or False if not. * * @since 2.5.13 */ public function has_column( string $column_name ): bool { // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $this->wpdb->get_results( $this->wpdb->prepare( "SHOW COLUMNS FROM `{$this->get_sql_safe_name()}` WHERE Field = %s", [ $column_name ] ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared return (bool) $this->wpdb->num_rows; } /** * Get the schema for the DB. * * This should be a SQL string for creating the DB table. * * @return string */ abstract protected function get_install_query(): string; /** * Get the un-prefixed (raw) table name. * * @return string */ abstract public static function get_raw_name(): string; } PK!a?v$src/Event/ClearProductStatsCache.phpnu[merchant_statuses = $merchant_statuses; } /** * Register a service. */ public function register(): void { add_action( 'woocommerce_gla_batch_updated_products', function () { $this->clear_stats_cache(); } ); add_action( 'woocommerce_gla_batch_deleted_products', function () { $this->clear_stats_cache(); } ); } /** * Clears the product statistics cache */ protected function clear_stats_cache() { try { $this->merchant_statuses->clear_cache(); } catch ( Exception $exception ) { // log and fail silently do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); } } } PK!0src/Event/StartProductSync.phpnu[job_repository = $job_repository; } /** * Register a service. */ public function register(): void { add_action( 'woocommerce_gla_mc_settings_sync', function () { $this->on_settings_sync(); } ); add_action( 'woocommerce_gla_mapping_rules_change', function () { $this->on_rules_change(); } ); } /** * Start the cleanup and update all products. */ protected function on_settings_sync() { $cleanup = $this->job_repository->get( CleanupProductsJob::class ); $cleanup->schedule(); $update = $this->job_repository->get( UpdateAllProducts::class ); $update->schedule(); } /** * Creates a Job for updating all products with a 30 minutes delay. */ protected function on_rules_change() { $update = $this->job_repository->get( UpdateAllProducts::class ); $update->schedule_delayed( 1800 ); // 30 minutes } } PK!9"src/Exception/AccountReconnect.phpnu[ 401, 'code' => 'JETPACK_DISCONNECTED', ] ); } /** * Create a new instance of the exception when the Google account is not connected. * * @return static */ public static function google_disconnected(): AccountReconnect { return new static( __( 'Please reconnect your Google account.', 'google-listings-and-ads' ), 401, null, [ 'status' => 401, 'code' => 'GOOGLE_DISCONNECTED', ] ); } } PK!`3Cbbsrc/Exception/ApiNotReady.phpnu[ $wait, ] ); } } PK!+src/Exception/ExceptionWithResponseData.phpnu[response_data = $data; } } /** * @param bool $with_message include the message in the returned data array. * * @return array */ public function get_response_data( bool $with_message = false ): array { if ( $with_message ) { return array_merge( [ 'message' => $this->getMessage() ], $this->response_data ); } return $this->response_data; } /** * @param array $response_data */ public function set_response_data( array $response_data ) { $this->response_data = $response_data; } } PK!%Jz/src/Exception/ExtensionRequirementException.phpnu[ sprintf( /* translators: 1 the missing plugin name */ __( 'Google for WooCommerce requires %1$s to be enabled.', 'google-listings-and-ads' ), $plugin_name ) ); } /** * Create a new instance of the exception when an incompatible plugin/extension is activated. * * @param string $plugin_name The name of the incompatible plugin. * * @return static */ public static function incompatible_plugin( string $plugin_name ): ExtensionRequirementException { return new static( sprintf( 'Google for WooCommerce is incompatible with %1$s.', // Fallback exception message. $plugin_name ), 0, null, fn () => sprintf( /* translators: 1 the incompatible plugin name */ __( 'Google for WooCommerce is incompatible with %1$s.', 'google-listings-and-ads' ), $plugin_name ) ); } } PK!C`""/src/Exception/GoogleListingsAndAdsException.phpnu[ sprintf( /* translators: 1 is the required component, 2 is the minimum required version, 3 is the version in use on the site */ __( 'Google for WooCommerce requires %1$s version %2$s or higher. You are using version %3$s.', 'google-listings-and-ads' ), $requirement, $minimum_version, $found_version ) ); } /** * Create a new instance of the exception when a requirement is missing. * * @param string $requirement * @param string $minimum_version * * @return InvalidVersion */ public static function requirement_missing( string $requirement, string $minimum_version ): InvalidVersion { return new static( sprintf( 'Google for WooCommerce requires %1$s version %2$s or higher.', // Fallback exception message. $requirement, $minimum_version ), 0, null, fn () => sprintf( /* translators: 1 is the required component, 2 is the minimum required version */ __( 'Google for WooCommerce requires %1$s version %2$s or higher.', 'google-listings-and-ads' ), $requirement, $minimum_version ) ); } /** * Create a new instance of the exception when an invalid architecture is detected. * * @since 2.3.9 * @return InvalidVersion */ public static function invalid_architecture(): InvalidVersion { return new static( 'Google for WooCommerce requires a 64 bit version of PHP.', // Fallback exception message. 0, null, fn () => __( 'Google for WooCommerce requires a 64 bit version of PHP.', 'google-listings-and-ads' ) ); } } PK![[5src/Exception/RuntimeExceptionWithMessageFunction.phpnu[message_function = $message_function; } /** * Override getMessage function to return message from function if available. * * @return string Exception message. */ public function get_formatted_message(): string { if ( is_callable( $this->message_function ) ) { return ( $this->message_function )(); } return parent::getMessage(); } } PK!&#src/Exception/ValidateInterface.phpnu[get_error_message(); $code = $error->get_error_code(); $string_code = ''; if ( ! is_numeric( $code ) ) { $string_code = $code; $code = 0; } return new static( sprintf( 'A WP Error was generated. Code: "%s" Message: "%s".', $string_code, $message ), $code ); } } PK!îXsrc/Exception/WPErrorTrait.phpnu[has_errors() ) { throw WPError::from_error( $maybe_error ); } } /** * Create a WP_Error from an exception. * * @param Throwable $e * @param string $code * @param array $data * * @return WP_Error */ protected function error_from_exception( Throwable $e, string $code, array $data = [] ): WP_Error { return new WP_Error( $code, $data['message'] ?? $e->getMessage(), $data ); } /** * Try to decode a JSON string. * * @param string $message * * @return array * @throws Exception When the JSON could not be decoded. */ protected function json_decode_message( string $message ): array { $decoded = json_decode( $message, true ); if ( null === $decoded || JSON_ERROR_NONE !== json_last_error() ) { throw new Exception( 'Could not decode JSON' ); } return $decoded; } } PK!\O"src/Google/Ads/GoogleAdsClient.phpnu[oAuth2Credential = new InsecureCredentials(); $this->endpoint = $endpoint; } /** * Set a guzzle client to use for requests. * * @param Client $client Guzzle client. */ public function setHttpClient( Client $client ) { $this->httpClient = $client; } /** * Build a HTTP Handler to handle the requests. */ protected function buildHttpHandler() { return [ HttpHandlerFactory::build( $this->httpClient ), 'async' ]; } } PK!.44,src/Google/Ads/ServiceClientFactoryTrait.phpnu[ $this->getOAuth2Credential(), self::$DEVELOPER_TOKEN_KEY => '', self::$TRANSPORT_KEY => 'rest', 'libName' => Constants::LIBRARY_NAME, 'libVersion' => Constants::LIBRARY_VERSION, ]; if ( ! empty( $this->getEndpoint() ) ) { $clientOptions += [ self::$SERVICE_ADDRESS_KEY => $this->getEndpoint() ]; } if ( isset( $this->httpClient ) ) { $clientOptions['transportConfig'] = [ 'rest' => [ 'httpHandler' => $this->buildHttpHandler(), ], ]; } return $clientOptions; } /** * @return AccountLinkServiceClient */ public function getAccountLinkServiceClient(): AccountLinkServiceClient { return new AccountLinkServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AdGroupAdLabelServiceClient */ public function getAdGroupAdLabelServiceClient(): AdGroupAdLabelServiceClient { return new AdGroupAdLabelServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AdGroupAdServiceClient */ public function getAdGroupAdServiceClient(): AdGroupAdServiceClient { return new AdGroupAdServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AdGroupCriterionServiceClient */ public function getAdGroupCriterionServiceClient(): AdGroupCriterionServiceClient { return new AdGroupCriterionServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AdGroupServiceClient */ public function getAdGroupServiceClient(): AdGroupServiceClient { return new AdGroupServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AdServiceClient */ public function getAdServiceClient(): AdServiceClient { return new AdServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AssetGroupListingGroupFilterServiceClient */ public function getAssetGroupListingGroupFilterServiceClient(): AssetGroupListingGroupFilterServiceClient { return new AssetGroupListingGroupFilterServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return AssetGroupServiceClient */ public function getAssetGroupServiceClient(): AssetGroupServiceClient { return new AssetGroupServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return BillingSetupServiceClient */ public function getBillingSetupServiceClient(): BillingSetupServiceClient { return new BillingSetupServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return CampaignBudgetServiceClient */ public function getCampaignBudgetServiceClient(): CampaignBudgetServiceClient { return new CampaignBudgetServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return CampaignCriterionServiceClient */ public function getCampaignCriterionServiceClient(): CampaignCriterionServiceClient { return new CampaignCriterionServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return CampaignServiceClient */ public function getCampaignServiceClient(): CampaignServiceClient { return new CampaignServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return ConversionActionServiceClient */ public function getConversionActionServiceClient(): ConversionActionServiceClient { return new ConversionActionServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return CustomerServiceClient */ public function getCustomerServiceClient(): CustomerServiceClient { return new CustomerServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return CustomerUserAccessServiceClient */ public function getCustomerUserAccessServiceClient(): CustomerUserAccessServiceClient { return new CustomerUserAccessServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return GeoTargetConstantServiceClient */ public function getGeoTargetConstantServiceClient(): GeoTargetConstantServiceClient { return new GeoTargetConstantServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return GoogleAdsServiceClient */ public function getGoogleAdsServiceClient(): GoogleAdsServiceClient { return new GoogleAdsServiceClient( $this->getGoogleAdsClientOptions() ); } /** * @return ProductLinkInvitationServiceClient */ public function getProductLinkInvitationServiceClient(): ProductLinkInvitationServiceClient { return new ProductLinkInvitationServiceClient( $this->getGoogleAdsClientOptions() ); } } PK!Q$  'src/Google/BatchInvalidProductEntry.phpnu[wc_product_id = $wc_product_id; $this->google_product_id = $google_product_id; $this->errors = $errors; } /** * @return int */ public function get_wc_product_id(): int { return $this->wc_product_id; } /** * @return string|null */ public function get_google_product_id(): ?string { return $this->google_product_id; } /** * @return string[] */ public function get_errors(): array { return $this->errors; } /** * @param string $error_reason * * @return bool */ public function has_error( string $error_reason ): bool { return ! empty( $this->errors[ $error_reason ] ); } /** * @param ConstraintViolationListInterface $violations * * @return BatchInvalidProductEntry */ public function map_validation_violations( ConstraintViolationListInterface $violations ): BatchInvalidProductEntry { $validation_errors = []; foreach ( $violations as $violation ) { $validation_errors[] = sprintf( '[%s] %s', $violation->getPropertyPath(), $violation->getMessage() ); } $this->errors = $validation_errors; return $this; } /** * @return array */ public function jsonSerialize(): array { $data = [ 'woocommerce_id' => $this->get_wc_product_id(), 'errors' => $this->get_errors(), ]; if ( null !== $this->get_google_product_id() ) { $data['google_id'] = $this->get_google_product_id(); } return $data; } } PK!vZ src/Google/BatchProductEntry.phpnu[wc_product_id = $wc_product_id; $this->google_product = $google_product; } /** * @return int */ public function get_wc_product_id(): int { return $this->wc_product_id; } /** * @return GoogleProduct|null */ public function get_google_product(): ?GoogleProduct { return $this->google_product; } /** * @return array */ public function jsonSerialize(): array { $data = [ 'woocommerce_id' => $this->get_wc_product_id() ]; if ( null !== $this->get_google_product() ) { $data['google_id'] = $this->get_google_product()->getId(); } return $data; } } PK!~q)src/Google/BatchProductIDRequestEntry.phpnu[wc_product_id = $wc_product_id; $this->product_id = $product_id; } /** * @return int */ public function get_wc_product_id(): int { return $this->wc_product_id; } /** * @return string */ public function get_product_id(): string { return $this->product_id; } /** * @param ProductIDMap $product_id_map * * @return BatchProductIDRequestEntry[] */ public static function create_from_id_map( ProductIDMap $product_id_map ): array { $product_entries = []; foreach ( $product_id_map as $google_product_id => $wc_product_id ) { $product_entries[] = new BatchProductIDRequestEntry( $wc_product_id, $google_product_id ); } return $product_entries; } /** * @param BatchProductIDRequestEntry[] $request_entries * * @return ProductIDMap $product_id_map */ public static function convert_to_id_map( array $request_entries ): ProductIDMap { $id_map = []; foreach ( $request_entries as $request_entry ) { $id_map[ $request_entry->get_product_id() ] = $request_entry->get_wc_product_id(); } return new ProductIDMap( $id_map ); } } PK!C3'src/Google/BatchProductRequestEntry.phpnu[wc_product_id = $wc_product_id; $this->product = $product; } /** * @return int */ public function get_wc_product_id(): int { return $this->wc_product_id; } /** * @return WCProductAdapter */ public function get_product(): WCProductAdapter { return $this->product; } } PK!#src/Google/BatchProductResponse.phpnu[products = $products; $this->errors = $errors; } /** * @return BatchProductEntry[] */ public function get_products(): array { return $this->products; } /** * @return BatchInvalidProductEntry[] */ public function get_errors(): array { return $this->errors; } } PK!A src/Google/DeleteCouponEntry.phpnu[wc_coupon_id = $wc_coupon_id; $this->google_promotion = $google_promotion; $this->synced_google_ids = $synced_google_ids; } /** * * @return int */ public function get_wc_coupon_id(): int { return $this->wc_coupon_id; } /** * * @return GooglePromotion */ public function get_google_promotion(): GooglePromotion { return $this->google_promotion; } /** * * @return array */ public function get_synced_google_ids(): array { return $this->synced_google_ids; } } PK!,%BBsrc/Google/GlobalSiteTag.phpnu[assets_handler = $assets_handler; $this->gtag_js = $gtag_js; $this->product_helper = $product_helper; $this->wc = $wc; $this->wp = $wp; } /** * Register the service. */ public function register(): void { $conversion_action = $this->options->get( OptionsInterface::ADS_CONVERSION_ACTION ); // No snippets without conversion action info. if ( ! $conversion_action ) { return; } $ads_conversion_id = $conversion_action['conversion_id']; $ads_conversion_label = $conversion_action['conversion_label']; add_action( 'wp_head', function () use ( $ads_conversion_id ) { $this->activate_global_site_tag( $ads_conversion_id ); }, 999999 ); add_action( 'woocommerce_before_thankyou', function ( $order_id ) use ( $ads_conversion_id, $ads_conversion_label ) { $this->maybe_display_conversion_and_purchase_event_snippets( $ads_conversion_id, $ads_conversion_label, $order_id ); }, ); add_action( 'woocommerce_after_single_product', function () { $this->display_view_item_event_snippet(); } ); add_action( 'wp_body_open', function () { $this->display_page_view_event_snippet(); } ); $this->product_data_hooks(); $this->register_assets(); } /** * Attach filters to add product data required for tracking events. */ protected function product_data_hooks() { // Add product data for any add_to_cart link. add_filter( 'woocommerce_loop_add_to_cart_link', function ( $link, $product ) { $this->add_product_data( $product ); return $link; }, 10, 2 ); // Add display name for an available variation. add_filter( 'woocommerce_available_variation', function ( $data, $instance, $variation ) { $data['display_name'] = $variation->get_name(); return $data; }, 10, 3 ); } /** * Register and enqueue assets for gtag events in blocks. */ protected function register_assets() { $gtag_events = new ScriptWithBuiltDependenciesAsset( 'gla-gtag-events', 'js/build/gtag-events', "{$this->get_root_dir()}/js/build/gtag-events.asset.php", new BuiltScriptDependencyArray( [ 'dependencies' => [], 'version' => $this->get_version(), ] ), function () { return is_page() || is_woocommerce() || is_cart(); } ); $this->assets_handler->register( $gtag_events ); $wp_consent_api = new ScriptWithBuiltDependenciesAsset( 'gla-wp-consent-api', 'js/build/wp-consent-api', "{$this->get_root_dir()}/js/build/wp-consent-api.asset.php", new BuiltScriptDependencyArray( [ 'dependencies' => [ 'wp-consent-api' ], 'version' => $this->get_version(), ] ) ); $this->assets_handler->register( $wp_consent_api ); add_action( 'wp_footer', function () use ( $gtag_events, $wp_consent_api ) { $gtag_events->add_localization( 'glaGtagData', [ 'currency_minor_unit' => wc_get_price_decimals(), 'products' => $this->products, ] ); $this->register_js_for_fast_refresh_dev(); $this->assets_handler->enqueue( $gtag_events ); if ( ! class_exists( '\WC_Google_Gtag_JS' ) && function_exists( 'wp_has_consent' ) ) { $this->assets_handler->enqueue( $wp_consent_api ); } } ); } /** * Activate the Global Site Tag framework: * - Insert GST code, or * - Include the Google Ads conversion ID in WooCommerce Google Analytics for WooCommerce output, if available * * @param string $ads_conversion_id Google Ads account conversion ID. */ public function activate_global_site_tag( string $ads_conversion_id ) { if ( $this->gtag_js->is_adding_framework() ) { if ( $this->gtag_js->ga4w_v2 ) { $this->wp->wp_add_inline_script( 'woocommerce-google-analytics-integration', $this->get_gtag_config( $ads_conversion_id ) ); } else { // Legacy code to support Google Analytics for WooCommerce version < 2.0.0. add_filter( 'woocommerce_gtag_snippet', function ( $gtag_snippet ) use ( $ads_conversion_id ) { return preg_replace( '~(\s)~', "\tgtag('config', '" . $ads_conversion_id . "', { 'groups': 'GLA', 'send_page_view': false });\n$1", $gtag_snippet ); } ); } } else { $this->display_global_site_tag( $ads_conversion_id ); } } /** * Display the JavaScript code to load the Global Site Tag framework. * * @param string $ads_conversion_id Google Ads account conversion ID. */ protected function display_global_site_tag( string $ads_conversion_id ) { // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript ?> wp->wp_add_inline_script( 'woocommerce-google-analytics-integration', $inline_script ); } else { $this->wp->wp_print_inline_script_tag( $inline_script ); } } /** * Display the JavaScript code to track conversions on the order confirmation page. * * @param string $ads_conversion_id Google Ads account conversion ID. * @param string $ads_conversion_label Google Ads conversion label. * @param int $order_id The order id. */ public function maybe_display_conversion_and_purchase_event_snippets( string $ads_conversion_id, string $ads_conversion_label, int $order_id ): void { // Only display on the order confirmation page. if ( ! is_order_received_page() ) { return; } $order = wc_get_order( $order_id ); // Make sure there is a valid order object and it is not already marked as tracked if ( ! $order || 1 === (int) $order->get_meta( self::ORDER_CONVERSION_META_KEY, true ) ) { return; } // Mark the order as tracked, to avoid double-reporting if the confirmation page is reloaded. $order->update_meta_data( self::ORDER_CONVERSION_META_KEY, 1 ); $order->save_meta_data(); $conversion_gtag_info = sprintf( 'gtag("event", "conversion", { send_to: "%s", value: %f, currency: "%s", transaction_id: "%s"});', esc_js( "{$ads_conversion_id}/{$ads_conversion_label}" ), $order->get_total(), esc_js( $order->get_currency() ), esc_js( $order->get_id() ), ); $this->add_inline_event_script( $conversion_gtag_info ); // Get the item info in the order $item_info = []; foreach ( $order->get_items() as $item_id => $item ) { $product_id = $item->get_product_id(); $product_name = $item->get_name(); $quantity = $item->get_quantity(); $price = $order->get_item_total( $item ); $item_info [] = sprintf( '{ id: "gla_%s", price: %f, google_business_vertical: "retail", name: "%s", quantity: %d, }', esc_js( $product_id ), $price, esc_js( $product_name ), $quantity, ); } // Check if this is the first time customer $is_new_customer = $this->is_first_time_customer( $order->get_billing_email() ); // Track the purchase page $language = $this->wp->get_locale(); if ( 'en_US' === $language ) { $language = 'English'; } $purchase_page_gtag = sprintf( 'gtag("event", "purchase", { ecomm_pagetype: "purchase", send_to: "%s", transaction_id: "%s", currency: "%s", country: "%s", value: %f, new_customer: %s, tax: %f, shipping: %f, delivery_postal_code: "%s", aw_feed_country: "%s", aw_feed_language: "%s", items: [%s]});', esc_js( "{$ads_conversion_id}/{$ads_conversion_label}" ), esc_js( $order->get_id() ), esc_js( $order->get_currency() ), esc_js( $this->wc->get_base_country() ), $order->get_total(), $is_new_customer ? 'true' : 'false', esc_js( $order->get_cart_tax() ), $order->get_total_shipping(), esc_js( $order->get_billing_postcode() ), esc_js( $this->wc->get_base_country() ), esc_js( $language ), join( ',', $item_info ), ); $this->add_inline_event_script( $purchase_page_gtag ); } /** * Display the JavaScript code to track the product view page. */ private function display_view_item_event_snippet(): void { $product = wc_get_product( get_the_ID() ); if ( ! $product instanceof WC_Product ) { return; } $this->add_product_data( $product ); $view_item_gtag = sprintf( 'gtag("event", "view_item", { send_to: "GLA", ecomm_pagetype: "product", value: %f, items:[{ id: "gla_%s", price: %f, google_business_vertical: "retail", name: "%s", category: "%s", }]});', wc_get_price_to_display( $product ), esc_js( $product->get_id() ), wc_get_price_to_display( $product ), esc_js( $product->get_name() ), esc_js( join( ' & ', $this->product_helper->get_categories( $product ) ) ), ); $this->add_inline_event_script( $view_item_gtag ); } /** * Display the JavaScript code to track all pages. */ private function display_page_view_event_snippet(): void { if ( ! is_cart() ) { $this->add_inline_event_script( 'gtag("event", "page_view", {send_to: "GLA"});' ); return; } // display the JavaScript code to track the cart page $item_info = []; foreach ( WC()->cart->get_cart() as $cart_item ) { // gets the product id $id = $cart_item['product_id']; // gets the product object $product = $cart_item['data']; $name = $product->get_name(); $price = WC()->cart->display_prices_including_tax() ? wc_get_price_including_tax( $product ) : wc_get_price_excluding_tax( $product ); // gets the cart item quantity $quantity = $cart_item['quantity']; $item_info[] = sprintf( '{ id: "gla_%s", price: %f, google_business_vertical: "retail", name:"%s", quantity: %d, }', esc_js( $id ), $price, esc_js( $name ), $quantity, ); } $value = WC()->cart->total; $page_view_gtag = sprintf( 'gtag("event", "page_view", { send_to: "GLA", ecomm_pagetype: "cart", value: %f, items: [%s]});', $value, join( ',', $item_info ), ); $this->add_inline_event_script( $page_view_gtag ); } /** * Add product data to include in JS data. * * @since 2.0.3 * * @param WC_Product $product */ protected function add_product_data( $product ) { $this->products[ $product->get_id() ] = [ 'name' => $product->get_name(), 'price' => wc_get_price_to_display( $product ), ]; } /** * TODO: Should the Global Site Tag framework be used if there are no paid Ads campaigns? * * @return bool True if the Global Site Tag framework should be included. */ public static function is_needed(): bool { if ( apply_filters( 'woocommerce_gla_disable_gtag_tracking', false ) ) { return false; } return true; } /** * Check if the customer has previous orders. * Called after order creation (check for older orders including the order which was just created). * * @param string $customer_email Customer email address. * @return bool True if this customer has previous orders. */ private static function is_first_time_customer( $customer_email ): bool { $query = new \WC_Order_Query( [ 'limit' => 2, 'return' => 'ids', ] ); $query->set( 'customer', $customer_email ); $orders = $query->get_orders(); return count( $orders ) === 1 ? true : false; } /** * This method ONLY works during development in the Fast Refresh mode. * * The runtime.js and react-refresh-runtime.js files are created when the front-end development is * running `npm run start:hot`, and they need to be loaded to make the gtag-events scrips work. */ private function register_js_for_fast_refresh_dev() { // This file exists only when running `npm run start:hot` $runtime_path = "{$this->get_root_dir()}/js/build/runtime.js"; if ( ! file_exists( $runtime_path ) ) { return; } $plugin_url = $this->get_plugin_url(); wp_enqueue_script( 'gla-webpack-runtime', "{$plugin_url}/js/build/runtime.js", [], (string) filemtime( $runtime_path ), false ); // This script is one of the gtag-events dependencies, and its handle is wp-react-refresh-runtime. // Ref: js/build/gtag-events.asset.php wp_register_script( 'wp-react-refresh-runtime', "{$plugin_url}/js/build-dev/react-refresh-runtime.js", [ 'gla-webpack-runtime' ], $this->get_version(), false ); } } PK!Dy)src/Google/GoogleHelperAwareInterface.phpnu[google_helper = $google_helper; } } PK!tllsrc/Google/GoogleHelper.phpnu[ [ 'code' => 'DZ', 'currency' => 'DZD', 'id' => 2012, ], // Angola 'AO' => [ 'code' => 'AO', 'currency' => 'AOA', 'id' => 2024, ], // Argentina 'AR' => [ 'code' => 'AR', 'currency' => 'ARS', 'id' => 2032, ], // Australia 'AU' => [ 'code' => 'AU', 'currency' => 'AUD', 'id' => 2036, ], // Austria 'AT' => [ 'code' => 'AT', 'currency' => 'EUR', 'id' => 2040, ], // Bahrain 'BH' => [ 'code' => 'BH', 'currency' => 'BHD', 'id' => 2048, ], // Bangladesh 'BD' => [ 'code' => 'BD', 'currency' => 'BDT', 'id' => 2050, ], // Belarus 'BY' => [ 'code' => 'BY', 'currency' => 'BYN', 'id' => 2112, ], // Belgium 'BE' => [ 'code' => 'BE', 'currency' => 'EUR', 'id' => 2056, ], // Brazil 'BR' => [ 'code' => 'BR', 'currency' => 'BRL', 'id' => 2076, ], // Cambodia 'KH' => [ 'code' => 'KH', 'currency' => 'KHR', 'id' => 2116, ], // Cameroon 'CM' => [ 'code' => 'CM', 'currency' => 'XAF', 'id' => 2120, ], // Canada 'CA' => [ 'code' => 'CA', 'currency' => 'CAD', 'id' => 2124, ], // Chile 'CL' => [ 'code' => 'CL', 'currency' => 'CLP', 'id' => 2152, ], // Colombia 'CO' => [ 'code' => 'CO', 'currency' => 'COP', 'id' => 2170, ], // Costa Rica 'CR' => [ 'code' => 'CR', 'currency' => 'CRC', 'id' => 2188, ], // Cote d'Ivoire 'CI' => [ 'code' => 'CI', 'currency' => 'XOF', 'id' => 2384, ], // Czechia 'CZ' => [ 'code' => 'CZ', 'currency' => 'CZK', 'id' => 2203, ], // Denmark 'DK' => [ 'code' => 'DK', 'currency' => 'DKK', 'id' => 2208, ], // Dominican Republic 'DO' => [ 'code' => 'DO', 'currency' => 'DOP', 'id' => 2214, ], // Ecuador 'EC' => [ 'code' => 'EC', 'currency' => 'USD', 'id' => 2218, ], // Egypt 'EG' => [ 'code' => 'EG', 'currency' => 'EGP', 'id' => 2818, ], // El Salvador 'SV' => [ 'code' => 'SV', 'currency' => 'USD', 'id' => 2222, ], // Ethiopia 'ET' => [ 'code' => 'ET', 'currency' => 'ETB', 'id' => 2231, ], // Finland 'FI' => [ 'code' => 'FI', 'currency' => 'EUR', 'id' => 2246, ], // France 'FR' => [ 'code' => 'FR', 'currency' => 'EUR', 'id' => 2250, ], // Georgia 'GE' => [ 'code' => 'GE', 'currency' => 'GEL', 'id' => 2268, ], // Germany 'DE' => [ 'code' => 'DE', 'currency' => 'EUR', 'id' => 2276, ], // Ghana 'GH' => [ 'code' => 'GH', 'currency' => 'GHS', 'id' => 2288, ], // Greece 'GR' => [ 'code' => 'GR', 'currency' => 'EUR', 'id' => 2300, ], // Guatemala 'GT' => [ 'code' => 'GT', 'currency' => 'GTQ', 'id' => 2320, ], // Hong Kong 'HK' => [ 'code' => 'HK', 'currency' => 'HKD', 'id' => 2344, ], // Hungary 'HU' => [ 'code' => 'HU', 'currency' => 'HUF', 'id' => 2348, ], // India 'IN' => [ 'code' => 'IN', 'currency' => 'INR', 'id' => 2356, ], // Indonesia 'ID' => [ 'code' => 'ID', 'currency' => 'IDR', 'id' => 2360, ], // Ireland 'IE' => [ 'code' => 'IE', 'currency' => 'EUR', 'id' => 2372, ], // Israel 'IL' => [ 'code' => 'IL', 'currency' => 'ILS', 'id' => 2376, ], // Italy 'IT' => [ 'code' => 'IT', 'currency' => 'EUR', 'id' => 2380, ], // Japan 'JP' => [ 'code' => 'JP', 'currency' => 'JPY', 'id' => 2392, ], // Jordan 'JO' => [ 'code' => 'JO', 'currency' => 'JOD', 'id' => 2400, ], // Kazakhstan 'KZ' => [ 'code' => 'KZ', 'currency' => 'KZT', 'id' => 2398, ], // Kenya 'KE' => [ 'code' => 'KE', 'currency' => 'KES', 'id' => 2404, ], // Kuwait 'KW' => [ 'code' => 'KW', 'currency' => 'KWD', 'id' => 2414, ], // Lebanon 'LB' => [ 'code' => 'LB', 'currency' => 'LBP', 'id' => 2422, ], // Madagascar 'MG' => [ 'code' => 'MG', 'currency' => 'MGA', 'id' => 2450, ], // Malaysia 'MY' => [ 'code' => 'MY', 'currency' => 'MYR', 'id' => 2458, ], // Mauritius 'MU' => [ 'code' => 'MU', 'currency' => 'MUR', 'id' => 2480, ], // Mexico 'MX' => [ 'code' => 'MX', 'currency' => 'MXN', 'id' => 2484, ], // Morocco 'MA' => [ 'code' => 'MA', 'currency' => 'MAD', 'id' => 2504, ], // Mozambique 'MZ' => [ 'code' => 'MZ', 'currency' => 'MZN', 'id' => 2508, ], // Myanmar 'Burma' 'MM' => [ 'code' => 'MM', 'currency' => 'MMK', 'id' => 2104, ], // Nepal 'NP' => [ 'code' => 'NP', 'currency' => 'NPR', 'id' => 2524, ], // Netherlands 'NL' => [ 'code' => 'NL', 'currency' => 'EUR', 'id' => 2528, ], // New Zealand 'NZ' => [ 'code' => 'NZ', 'currency' => 'NZD', 'id' => 2554, ], // Nicaragua 'NI' => [ 'code' => 'NI', 'currency' => 'NIO', 'id' => 2558, ], // Nigeria 'NG' => [ 'code' => 'NG', 'currency' => 'NGN', 'id' => 2566, ], // Norway 'NO' => [ 'code' => 'NO', 'currency' => 'NOK', 'id' => 2578, ], // Oman 'OM' => [ 'code' => 'OM', 'currency' => 'OMR', 'id' => 2512, ], // Pakistan 'PK' => [ 'code' => 'PK', 'currency' => 'PKR', 'id' => 2586, ], // Panama 'PA' => [ 'code' => 'PA', 'currency' => 'PAB', 'id' => 2591, ], // Paraguay 'PY' => [ 'code' => 'PY', 'currency' => 'PYG', 'id' => 2600, ], // Peru 'PE' => [ 'code' => 'PE', 'currency' => 'PEN', 'id' => 2604, ], // Philippines 'PH' => [ 'code' => 'PH', 'currency' => 'PHP', 'id' => 2608, ], // Poland 'PL' => [ 'code' => 'PL', 'currency' => 'PLN', 'id' => 2616, ], // Portugal 'PT' => [ 'code' => 'PT', 'currency' => 'EUR', 'id' => 2620, ], // Puerto Rico 'PR' => [ 'code' => 'PR', 'currency' => 'USD', 'id' => 2630, ], // Romania 'RO' => [ 'code' => 'RO', 'currency' => 'RON', 'id' => 2642, ], // Russia 'RU' => [ 'code' => 'RU', 'currency' => 'RUB', 'id' => 2643, ], // Saudi Arabia 'SA' => [ 'code' => 'SA', 'currency' => 'SAR', 'id' => 2682, ], // Senegal 'SN' => [ 'code' => 'SN', 'currency' => 'XOF', 'id' => 2686, ], // Singapore 'SG' => [ 'code' => 'SG', 'currency' => 'SGD', 'id' => 2702, ], // Slovakia 'SK' => [ 'code' => 'SK', 'currency' => 'EUR', 'id' => 2703, ], // South Africa 'ZA' => [ 'code' => 'ZA', 'currency' => 'ZAR', 'id' => 2710, ], // Spain 'ES' => [ 'code' => 'ES', 'currency' => 'EUR', 'id' => 2724, ], // Sri Lanka 'LK' => [ 'code' => 'LK', 'currency' => 'LKR', 'id' => 2144, ], // Sweden 'SE' => [ 'code' => 'SE', 'currency' => 'SEK', 'id' => 2752, ], // Switzerland 'CH' => [ 'code' => 'CH', 'currency' => 'CHF', 'id' => 2756, ], // Taiwan 'TW' => [ 'code' => 'TW', 'currency' => 'TWD', 'id' => 2158, ], // Tanzania 'TZ' => [ 'code' => 'TZ', 'currency' => 'TZS', 'id' => 2834, ], // Thailand 'TH' => [ 'code' => 'TH', 'currency' => 'THB', 'id' => 2764, ], // Tunisia 'TN' => [ 'code' => 'TN', 'currency' => 'TND', 'id' => 2788, ], // Turkey 'TR' => [ 'code' => 'TR', 'currency' => 'TRY', 'id' => 2792, ], // United Arab Emirates 'AE' => [ 'code' => 'AE', 'currency' => 'AED', 'id' => 2784, ], // Uganda 'UG' => [ 'code' => 'UG', 'currency' => 'UGX', 'id' => 2800, ], // Ukraine 'UA' => [ 'code' => 'UA', 'currency' => 'UAH', 'id' => 2804, ], // United Kingdom 'GB' => [ 'code' => 'GB', 'currency' => 'GBP', 'id' => 2826, ], // United States 'US' => [ 'code' => 'US', 'currency' => 'USD', 'id' => 2840, ], // Uruguay 'UY' => [ 'code' => 'UY', 'currency' => 'UYU', 'id' => 2858, ], // Uzbekistan 'UZ' => [ 'code' => 'UZ', 'currency' => 'UZS', 'id' => 2860, ], // Venezuela 'VE' => [ 'code' => 'VE', 'currency' => 'VEF', 'id' => 2862, ], // Vietnam 'VN' => [ 'code' => 'VN', 'currency' => 'VND', 'id' => 2704, ], // Zambia 'ZM' => [ 'code' => 'ZM', 'currency' => 'ZMW', 'id' => 2894, ], // Zimbabwe 'ZW' => [ 'code' => 'ZW', 'currency' => 'USD', 'id' => 2716, ], ]; protected const COUNTRY_SUBDIVISIONS = [ // Australia 'AU' => [ 'ACT' => [ 'id' => 20034, 'code' => 'ACT', 'name' => 'Australian Capital Territory', ], 'NSW' => [ 'id' => 20035, 'code' => 'NSW', 'name' => 'New South Wales', ], 'NT' => [ 'id' => 20036, 'code' => 'NT', 'name' => 'Northern Territory', ], 'QLD' => [ 'id' => 20037, 'code' => 'QLD', 'name' => 'Queensland', ], 'SA' => [ 'id' => 20038, 'code' => 'SA', 'name' => 'South Australia', ], 'TAS' => [ 'id' => 20039, 'code' => 'TAS', 'name' => 'Tasmania', ], 'VIC' => [ 'id' => 20040, 'code' => 'VIC', 'name' => 'Victoria', ], 'WA' => [ 'id' => 20041, 'code' => 'WA', 'name' => 'Western Australia', ], ], // Japan 'JP' => [ 'JP01' => [ 'id' => 20624, 'code' => 'JP01', 'name' => 'Hokkaido', ], 'JP02' => [ 'id' => 20625, 'code' => 'JP02', 'name' => 'Aomori', ], 'JP03' => [ 'id' => 20626, 'code' => 'JP03', 'name' => 'Iwate', ], 'JP04' => [ 'id' => 20627, 'code' => 'JP04', 'name' => 'Miyagi', ], 'JP05' => [ 'id' => 20628, 'code' => 'JP05', 'name' => 'Akita', ], 'JP06' => [ 'id' => 20629, 'code' => 'JP06', 'name' => 'Yamagata', ], 'JP07' => [ 'id' => 20630, 'code' => 'JP07', 'name' => 'Fukushima', ], 'JP08' => [ 'id' => 20631, 'code' => 'JP08', 'name' => 'Ibaraki', ], 'JP09' => [ 'id' => 20632, 'code' => 'JP09', 'name' => 'Tochigi', ], 'JP10' => [ 'id' => 20633, 'code' => 'JP10', 'name' => 'Gunma', ], 'JP11' => [ 'id' => 20634, 'code' => 'JP11', 'name' => 'Saitama', ], 'JP12' => [ 'id' => 20635, 'code' => 'JP12', 'name' => 'Chiba', ], 'JP13' => [ 'id' => 20636, 'code' => 'JP13', 'name' => 'Tokyo', ], 'JP14' => [ 'id' => 20637, 'code' => 'JP14', 'name' => 'Kanagawa', ], 'JP15' => [ 'id' => 20638, 'code' => 'JP15', 'name' => 'Niigata', ], 'JP16' => [ 'id' => 20639, 'code' => 'JP16', 'name' => 'Toyama', ], 'JP17' => [ 'id' => 20640, 'code' => 'JP17', 'name' => 'Ishikawa', ], 'JP18' => [ 'id' => 20641, 'code' => 'JP18', 'name' => 'Fukui', ], 'JP19' => [ 'id' => 20642, 'code' => 'JP19', 'name' => 'Yamanashi', ], 'JP20' => [ 'id' => 20643, 'code' => 'JP20', 'name' => 'Nagano', ], 'JP21' => [ 'id' => 20644, 'code' => 'JP21', 'name' => 'Gifu', ], 'JP22' => [ 'id' => 20645, 'code' => 'JP22', 'name' => 'Shizuoka', ], 'JP23' => [ 'id' => 20646, 'code' => 'JP23', 'name' => 'Aichi', ], 'JP24' => [ 'id' => 20647, 'code' => 'JP24', 'name' => 'Mie', ], 'JP25' => [ 'id' => 20648, 'code' => 'JP25', 'name' => 'Shiga', ], 'JP26' => [ 'id' => 20649, 'code' => 'JP26', 'name' => 'Kyoto', ], 'JP27' => [ 'id' => 20650, 'code' => 'JP27', 'name' => 'Osaka', ], 'JP28' => [ 'id' => 20651, 'code' => 'JP28', 'name' => 'Hyogo', ], 'JP29' => [ 'id' => 20652, 'code' => 'JP29', 'name' => 'Nara', ], 'JP30' => [ 'id' => 20653, 'code' => 'JP30', 'name' => 'Wakayama', ], 'JP31' => [ 'id' => 20654, 'code' => 'JP31', 'name' => 'Tottori', ], 'JP32' => [ 'id' => 20655, 'code' => 'JP32', 'name' => 'Shimane', ], 'JP33' => [ 'id' => 20656, 'code' => 'JP33', 'name' => 'Okayama', ], 'JP34' => [ 'id' => 20657, 'code' => 'JP34', 'name' => 'Hiroshima', ], 'JP35' => [ 'id' => 20658, 'code' => 'JP35', 'name' => 'Yamaguchi', ], 'JP36' => [ 'id' => 20659, 'code' => 'JP36', 'name' => 'Tokushima', ], 'JP37' => [ 'id' => 20660, 'code' => 'JP37', 'name' => 'Kagawa', ], 'JP38' => [ 'id' => 20661, 'code' => 'JP38', 'name' => 'Ehime', ], 'JP39' => [ 'id' => 20662, 'code' => 'JP39', 'name' => 'Kochi', ], 'JP40' => [ 'id' => 20663, 'code' => 'JP40', 'name' => 'Fukuoka', ], 'JP41' => [ 'id' => 20664, 'code' => 'JP41', 'name' => 'Saga', ], 'JP42' => [ 'id' => 20665, 'code' => 'JP42', 'name' => 'Nagasaki', ], 'JP43' => [ 'id' => 20666, 'code' => 'JP43', 'name' => 'Kumamoto', ], 'JP44' => [ 'id' => 20667, 'code' => 'JP44', 'name' => 'Oita', ], 'JP45' => [ 'id' => 20668, 'code' => 'JP45', 'name' => 'Miyazaki', ], 'JP46' => [ 'id' => 20669, 'code' => 'JP46', 'name' => 'Kagoshima', ], 'JP47' => [ 'id' => 20670, 'code' => 'JP47', 'name' => 'Okinawa', ], ], // United States 'US' => [ 'AK' => [ 'id' => 21132, 'code' => 'AK', 'name' => 'Alaska', ], 'AL' => [ 'id' => 21133, 'code' => 'AL', 'name' => 'Alabama', ], 'AR' => [ 'id' => 21135, 'code' => 'AR', 'name' => 'Arkansas', ], 'AZ' => [ 'id' => 21136, 'code' => 'AZ', 'name' => 'Arizona', ], 'CA' => [ 'id' => 21137, 'code' => 'CA', 'name' => 'California', ], 'CO' => [ 'id' => 21138, 'code' => 'CO', 'name' => 'Colorado', ], 'CT' => [ 'id' => 21139, 'code' => 'CT', 'name' => 'Connecticut', ], 'DC' => [ 'id' => 21140, 'code' => 'DC', 'name' => 'District of Columbia', ], 'DE' => [ 'id' => 21141, 'code' => 'DE', 'name' => 'Delaware', ], 'FL' => [ 'id' => 21142, 'code' => 'FL', 'name' => 'Florida', ], 'GA' => [ 'id' => 21143, 'code' => 'GA', 'name' => 'Georgia', ], 'HI' => [ 'id' => 21144, 'code' => 'HI', 'name' => 'Hawaii', ], 'IA' => [ 'id' => 21145, 'code' => 'IA', 'name' => 'Iowa', ], 'ID' => [ 'id' => 21146, 'code' => 'ID', 'name' => 'Idaho', ], 'IL' => [ 'id' => 21147, 'code' => 'IL', 'name' => 'Illinois', ], 'IN' => [ 'id' => 21148, 'code' => 'IN', 'name' => 'Indiana', ], 'KS' => [ 'id' => 21149, 'code' => 'KS', 'name' => 'Kansas', ], 'KY' => [ 'id' => 21150, 'code' => 'KY', 'name' => 'Kentucky', ], 'LA' => [ 'id' => 21151, 'code' => 'LA', 'name' => 'Louisiana', ], 'MA' => [ 'id' => 21152, 'code' => 'MA', 'name' => 'Massachusetts', ], 'MD' => [ 'id' => 21153, 'code' => 'MD', 'name' => 'Maryland', ], 'ME' => [ 'id' => 21154, 'code' => 'ME', 'name' => 'Maine', ], 'MI' => [ 'id' => 21155, 'code' => 'MI', 'name' => 'Michigan', ], 'MN' => [ 'id' => 21156, 'code' => 'MN', 'name' => 'Minnesota', ], 'MO' => [ 'id' => 21157, 'code' => 'MO', 'name' => 'Missouri', ], 'MS' => [ 'id' => 21158, 'code' => 'MS', 'name' => 'Mississippi', ], 'MT' => [ 'id' => 21159, 'code' => 'MT', 'name' => 'Montana', ], 'NC' => [ 'id' => 21160, 'code' => 'NC', 'name' => 'North Carolina', ], 'ND' => [ 'id' => 21161, 'code' => 'ND', 'name' => 'North Dakota', ], 'NE' => [ 'id' => 21162, 'code' => 'NE', 'name' => 'Nebraska', ], 'NH' => [ 'id' => 21163, 'code' => 'NH', 'name' => 'New Hampshire', ], 'NJ' => [ 'id' => 21164, 'code' => 'NJ', 'name' => 'New Jersey', ], 'NM' => [ 'id' => 21165, 'code' => 'NM', 'name' => 'New Mexico', ], 'NV' => [ 'id' => 21166, 'code' => 'NV', 'name' => 'Nevada', ], 'NY' => [ 'id' => 21167, 'code' => 'NY', 'name' => 'New York', ], 'OH' => [ 'id' => 21168, 'code' => 'OH', 'name' => 'Ohio', ], 'OK' => [ 'id' => 21169, 'code' => 'OK', 'name' => 'Oklahoma', ], 'OR' => [ 'id' => 21170, 'code' => 'OR', 'name' => 'Oregon', ], 'PA' => [ 'id' => 21171, 'code' => 'PA', 'name' => 'Pennsylvania', ], 'RI' => [ 'id' => 21172, 'code' => 'RI', 'name' => 'Rhode Island', ], 'SC' => [ 'id' => 21173, 'code' => 'SC', 'name' => 'South Carolina', ], 'SD' => [ 'id' => 21174, 'code' => 'SD', 'name' => 'South Dakota', ], 'TN' => [ 'id' => 21175, 'code' => 'TN', 'name' => 'Tennessee', ], 'TX' => [ 'id' => 21176, 'code' => 'TX', 'name' => 'Texas', ], 'UT' => [ 'id' => 21177, 'code' => 'UT', 'name' => 'Utah', ], 'VA' => [ 'id' => 21178, 'code' => 'VA', 'name' => 'Virginia', ], 'VT' => [ 'id' => 21179, 'code' => 'VT', 'name' => 'Vermont', ], 'WA' => [ 'id' => 21180, 'code' => 'WA', 'name' => 'Washington', ], 'WI' => [ 'id' => 21182, 'code' => 'WI', 'name' => 'Wisconsin', ], 'WV' => [ 'id' => 21183, 'code' => 'WV', 'name' => 'West Virginia', ], 'WY' => [ 'id' => 21184, 'code' => 'WY', 'name' => 'Wyoming', ], ], ]; /** * @var WC */ protected $wc; /** * @var array Map of location ids to country codes. */ private $country_id_code_map; /** * @var array Map of location ids to subdivision codes. */ private $subdivision_id_code_map; /** * GoogleHelper constructor. * * @param WC $wc */ public function __construct( WC $wc ) { $this->wc = $wc; } /** * Get the data for countries supported by Google. * * @return array[] */ protected function get_mc_supported_countries_data(): array { $supported = self::SUPPORTED_COUNTRIES; // Currency conversion is unavailable in South Korea: https://support.google.com/merchants/answer/7055540 if ( 'KRW' === $this->wc->get_woocommerce_currency() ) { // South Korea $supported['KR'] = [ 'code' => 'KR', 'currency' => 'KRW', 'id' => 2410, ]; } return $supported; } /** * Get an array of Google Merchant Center supported countries and currencies. * * Note - Other currencies may be supported using currency conversion. * * WooCommerce Countries -> https://github.com/woocommerce/woocommerce/blob/master/i18n/countries.php * Google Supported Countries -> https://support.google.com/merchants/answer/160637?hl=en * * @return array */ public function get_mc_supported_countries_currencies(): array { return array_column( $this->get_mc_supported_countries_data(), 'currency', 'code' ); } /** * Get an array of Google Merchant Center supported countries. * * WooCommerce Countries -> https://github.com/woocommerce/woocommerce/blob/master/i18n/countries.php * Google Supported Countries -> https://support.google.com/merchants/answer/160637?hl=en * * @return string[] Array of country codes. */ public function get_mc_supported_countries(): array { return array_keys( $this->get_mc_supported_countries_data() ); } /** * Get an array of Google Merchant Center supported countries and currencies for promotions. * * Google Promotion Supported Countries -> https://developers.google.com/shopping-content/reference/rest/v2.1/promotions * * @return array */ protected function get_mc_promotion_supported_countries_currencies(): array { return [ 'AU' => 'AUD', // Australia 'BR' => 'BRL', // Brazil 'CA' => 'CAD', // Canada 'DE' => 'EUR', // Germany 'ES' => 'EUR', // Spain 'FR' => 'EUR', // France 'GB' => 'GBP', // United Kingdom 'IN' => 'INR', // India 'IT' => 'EUR', // Italy 'JP' => 'JPY', // Japan 'NL' => 'EUR', // The Netherlands 'KR' => 'KRW', // South Korea 'US' => 'USD', // United States ]; } /** * Get an array of Google Merchant Center supported countries for promotions. * * @return string[] */ public function get_mc_promotion_supported_countries(): array { return array_keys( $this->get_mc_promotion_supported_countries_currencies() ); } /** * Get an array of Google Merchant Center supported languages (ISO 639-1). * * WooCommerce Languages -> https://translate.wordpress.org/projects/wp-plugins/woocommerce/ * Google Supported Languages -> https://support.google.com/merchants/answer/160637?hl=en * ISO 639-1 -> https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes * * @return array */ public function get_mc_supported_languages(): array { // Repeated values removed: // 'pt', // Brazilian Portuguese // 'zh', // Simplified Chinese* return [ 'ar' => 'ar', // Arabic 'cs' => 'cs', // Czech 'da' => 'da', // Danish 'nl' => 'nl', // Dutch 'en' => 'en', // English 'fi' => 'fi', // Finnish 'fr' => 'fr', // French 'de' => 'de', // German 'he' => 'he', // Hebrew 'hu' => 'hu', // Hungarian 'id' => 'id', // Indonesian 'it' => 'it', // Italian 'ja' => 'ja', // Japanese 'ko' => 'ko', // Korean 'el' => 'el', // Modern Greek 'nb' => 'nb', // Norwegian (Norsk Bokmål) 'nn' => 'nn', // Norwegian (Norsk Nynorsk) 'no' => 'no', // Norwegian 'pl' => 'pl', // Polish 'pt' => 'pt', // Portuguese 'ro' => 'ro', // Romanian 'ru' => 'ru', // Russian 'sk' => 'sk', // Slovak 'es' => 'es', // Spanish 'sv' => 'sv', // Swedish 'th' => 'th', // Thai 'zh' => 'zh', // Traditional Chinese 'tr' => 'tr', // Turkish 'uk' => 'uk', // Ukrainian 'vi' => 'vi', // Vietnamese ]; } /** * Get whether the country is supported by the Merchant Center. * * @param string $country Country code. * * @return bool True if the country is in the list of MC-supported countries. */ public function is_country_supported( string $country ): bool { return array_key_exists( strtoupper( $country ), $this->get_mc_supported_countries_data() ); } /** * Find the ISO 3166-1 code of the Merchant Center supported country by its location ID. * * @param int $id * * @return string|null ISO 3166-1 representation of the country code. */ public function find_country_code_by_id( int $id ): ?string { return $this->get_country_id_code_map()[ $id ] ?? null; } /** * Find the code of the Merchant Center supported subdivision by its location ID. * * @param int $id * * @return string|null */ public function find_subdivision_code_by_id( int $id ): ?string { return $this->get_subdivision_id_code_map()[ $id ] ?? null; } /** * Find and return the location id for the given country code. * * @param string $code * * @return int|null */ public function find_country_id_by_code( string $code ): ?int { $countries = $this->get_mc_supported_countries_data(); if ( isset( $countries[ $code ] ) ) { return $countries[ $code ]['id']; } return null; } /** * Find and return the location id for the given subdivision (state, province, etc.) code. * * @param string $code * @param string $country_code * * @return int|null */ public function find_subdivision_id_by_code( string $code, string $country_code ): ?int { return self::COUNTRY_SUBDIVISIONS[ $country_code ][ $code ]['id'] ?? null; } /** * Gets the list of supported Merchant Center countries from a continent. * * @param string $continent_code * * @return string[] Returns an array of country codes with each country code used both as the key and value. * For example: [ 'US' => 'US', 'DE' => 'DE' ]. * * @since 1.13.0 */ public function get_supported_countries_from_continent( string $continent_code ): array { $countries = []; $continents = $this->wc->get_continents(); if ( isset( $continents[ $continent_code ] ) ) { $countries = $continents[ $continent_code ]['countries']; // Match the list of countries with the list of Merchant Center supported countries. $countries = array_intersect( $countries, $this->get_mc_supported_countries() ); // Use the country code as array keys. $countries = array_combine( $countries, $countries ); } return $countries; } /** * Check whether the given country code supports regional shipping (i.e. setting up rates for states/provinces and postal codes). * * @param string $country_code * * @return bool * * @since 2.1.0 */ public function does_country_support_regional_shipping( string $country_code ): bool { return in_array( $country_code, [ 'AU', 'JP', 'US' ], true ); } /** * Returns an array mapping the ID of the Merchant Center supported countries to their respective codes. * * @return string[] Array of country codes with location IDs as keys. e.g. [ 2840 => 'US' ] */ protected function get_country_id_code_map(): array { if ( isset( $this->country_id_code_map ) ) { return $this->country_id_code_map; } $this->country_id_code_map = []; $countries = $this->get_mc_supported_countries_data(); foreach ( $countries as $country ) { $this->country_id_code_map[ $country['id'] ] = $country['code']; } return $this->country_id_code_map; } /** * Returns an array mapping the ID of the Merchant Center supported subdivisions to their respective codes. * * @return string[] Array of subdivision codes with location IDs as keys. e.g. [ 20035 => 'NSW' ] */ protected function get_subdivision_id_code_map(): array { if ( isset( $this->subdivision_id_code_map ) ) { return $this->subdivision_id_code_map; } $this->subdivision_id_code_map = []; foreach ( self::COUNTRY_SUBDIVISIONS as $subdivisions ) { foreach ( $subdivisions as $item ) { $this->subdivision_id_code_map[ $item['id'] ] = $item['code']; } } return $this->subdivision_id_code_map; } } PK! #src/Google/GoogleProductService.phpnu[shopping_service = $shopping_service; } /** * @param string $product_id Google product ID. * * @return GoogleProduct * * @throws GoogleException If there are any Google API errors. */ public function get( string $product_id ): GoogleProduct { $merchant_id = $this->options->get_merchant_id(); return $this->shopping_service->products->get( $merchant_id, $product_id ); } /** * @param GoogleProduct $product * * @return GoogleProduct * * @throws GoogleException If there are any Google API errors. */ public function insert( GoogleProduct $product ): GoogleProduct { $merchant_id = $this->options->get_merchant_id(); return $this->shopping_service->products->insert( $merchant_id, $product ); } /** * @param string $product_id Google product ID. * * @throws GoogleException If there are any Google API errors. */ public function delete( string $product_id ) { $merchant_id = $this->options->get_merchant_id(); $this->shopping_service->products->delete( $merchant_id, $product_id ); } /** * @param BatchProductIDRequestEntry[] $products * * @return BatchProductResponse * * @throws InvalidValue If any of the provided products are invalid. * @throws GoogleException If there are any Google API errors. */ public function get_batch( array $products ): BatchProductResponse { return $this->custom_batch( $products, self::METHOD_GET ); } /** * @param BatchProductRequestEntry[] $products * * @return BatchProductResponse * * @throws InvalidValue If any of the provided products are invalid. * @throws GoogleException If there are any Google API errors. */ public function insert_batch( array $products ): BatchProductResponse { return $this->custom_batch( $products, self::METHOD_INSERT ); } /** * @param BatchProductIDRequestEntry[] $products * * @return BatchProductResponse * * @throws InvalidValue If any of the provided products are invalid. * @throws GoogleException If there are any Google API errors. */ public function delete_batch( array $products ): BatchProductResponse { return $this->custom_batch( $products, self::METHOD_DELETE ); } /** * @param BatchProductRequestEntry[]|BatchProductIDRequestEntry[] $products * @param string $method * * @return BatchProductResponse * * @throws InvalidValue If any of the products' type is invalid for the batch method. * @throws GoogleException If there are any Google API errors. */ protected function custom_batch( array $products, string $method ): BatchProductResponse { if ( empty( $products ) ) { return new BatchProductResponse( [], [] ); } $merchant_id = $this->options->get_merchant_id(); $request_entries = []; // An array of product entries mapped to each batch ID. Used to parse Google's batch response. $batch_id_product_map = []; $batch_id = 0; foreach ( $products as $product_entry ) { $this->validate_batch_request_entry( $product_entry, $method ); $request_entry = new GoogleBatchRequestEntry( [ 'batchId' => $batch_id, 'merchantId' => $merchant_id, 'method' => $method, ] ); if ( $product_entry instanceof BatchProductRequestEntry ) { $request_entry['product'] = $product_entry->get_product(); } else { $request_entry['product_id'] = $product_entry->get_product_id(); } $request_entries[] = $request_entry; $batch_id_product_map[ $batch_id ] = $product_entry; ++$batch_id; } $responses = $this->shopping_service->products->custombatch( new GoogleBatchRequest( [ 'entries' => $request_entries ] ) ); return $this->parse_batch_responses( $responses, $batch_id_product_map ); } /** * @param GoogleBatchResponse $responses * @param BatchProductRequestEntry[]|BatchProductIDRequestEntry[] $batch_id_product_map An array of product entries mapped to each batch ID. Used to parse Google's batch response. * * @return BatchProductResponse */ protected function parse_batch_responses( GoogleBatchResponse $responses, array $batch_id_product_map ): BatchProductResponse { $result_products = []; $errors = []; /** * @var GoogleBatchResponseEntry $response */ foreach ( $responses as $response ) { // Product entry is mapped to batchId when sending the request $product_entry = $batch_id_product_map[ $response->batchId ]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $wc_product_id = $product_entry->get_wc_product_id(); if ( $product_entry instanceof BatchProductRequestEntry ) { $google_product_id = $product_entry->get_product()->getId(); } else { $google_product_id = $product_entry->get_product_id(); } if ( empty( $response->getErrors() ) ) { $result_products[] = new BatchProductEntry( $wc_product_id, $response->getProduct() ); } else { $errors[] = new BatchInvalidProductEntry( $wc_product_id, $google_product_id, self::get_batch_response_error_messages( $response ) ); } } return new BatchProductResponse( $result_products, $errors ); } /** * @param BatchProductRequestEntry|BatchProductIDRequestEntry $request_entry * @param string $method * * @throws InvalidValue If the product type is invalid for the batch method. */ protected function validate_batch_request_entry( $request_entry, string $method ) { if ( self::METHOD_INSERT === $method ) { $this->validate_instanceof( $request_entry, BatchProductRequestEntry::class ); } else { $this->validate_instanceof( $request_entry, BatchProductIDRequestEntry::class ); } } /** * @param GoogleBatchResponseEntry $batch_response_entry * * @return string[] */ protected static function get_batch_response_error_messages( GoogleBatchResponseEntry $batch_response_entry ): array { $errors = []; foreach ( $batch_response_entry->getErrors()->getErrors() as $error ) { $errors[ $error->getReason() ] = $error->getMessage(); } return $errors; } } PK!LL%src/Google/GooglePromotionService.phpnu[shopping_service = $shopping_service; } /** * * @param GooglePromotion $promotion * * @return GooglePromotion * * @throws GoogleException If there are any Google API errors. */ public function create( GooglePromotion $promotion ): GooglePromotion { $merchant_id = $this->options->get_merchant_id(); return $this->shopping_service->promotions->create( $merchant_id, $promotion ); } } PK!8 !src/Google/InvalidCouponEntry.phpnu[wc_coupon_id = $wc_coupon_id; $this->target_country = $target_country; $this->google_promotion_id = $google_promotion_id; $this->errors = $errors; } /** * * @return int */ public function get_wc_coupon_id(): int { return $this->wc_coupon_id; } /** * * @return string|null */ public function get_google_promotion_id(): ?string { return $this->google_promotion_id; } /** * * @return string|null */ public function get_target_country(): ?string { return $this->target_country; } /** * * @return string[] */ public function get_errors(): array { return $this->errors; } /** * * @param int $error_code * * @return bool */ public function has_error( int $error_code ): bool { return ! empty( $this->errors[ $error_code ] ); } /** * * @param ConstraintViolationListInterface $violations * * @return InvalidCouponEntry */ public function map_validation_violations( ConstraintViolationListInterface $violations ): InvalidCouponEntry { $validation_errors = []; foreach ( $violations as $violation ) { array_push( $validation_errors, sprintf( '[%s] %s', $violation->getPropertyPath(), $violation->getMessage() ) ); } $this->errors = $validation_errors; return $this; } /** * * @return array */ public function jsonSerialize(): array { $data = [ 'woocommerce_id' => $this->get_wc_coupon_id(), 'errors' => $this->get_errors(), ]; if ( null !== $this->get_google_promotion_id() ) { $data['google_id'] = $this->get_google_promotion_id(); } if ( null !== $this->get_target_country() ) { $data['google_target_country'] = $this->get_target_country(); } return $data; } } PK! $src/Google/RequestReviewStatuses.phpnu[ $program_type ) { // In case any Program is with no offers we consider it Onboarding if ( $program_type['globalState'] === self::NO_OFFERS ) { $status = self::ONBOARDING; break; } // In case any Program is not enabled or there are no regionStatuses we return null status if ( ! isset( $program_type['regionStatuses'] ) || ! in_array( $program_type['globalState'], $valid_program_states, true ) ) { continue; } // Otherwise, we compute the new status, issues and cooldown period foreach ( $program_type['regionStatuses'] as $region_status ) { $issues = array_merge( $issues, $region_status['reviewIssues'] ?? [] ); $cooldown = $this->maybe_update_cooldown_period( $region_status, $cooldown ); $status = $this->maybe_update_status( $region_status['eligibilityStatus'], $status ); $review_eligible_regions = $this->maybe_load_eligible_region( $region_status, $review_eligible_regions, $program_type_name ); } } return [ 'issues' => array_map( 'strtolower', array_values( array_unique( $issues ) ) ), 'cooldown' => $this->get_cooldown( $cooldown ), // add lifetime cache to cooldown time 'status' => $status, 'reviewEligibleRegions' => array_unique( $review_eligible_regions ), ]; } /** * Updates the cooldown period in case the new cooldown period date is available and later than the current cooldown period. * * @param array $region_status Associative array containing (maybe) a cooldown date property. * @param int $cooldown Referenced current cooldown to compare with * * @return int The cooldown */ private function maybe_update_cooldown_period( $region_status, $cooldown ) { if ( isset( $region_status['reviewIneligibilityReasonDetails'] ) && isset( $region_status['reviewIneligibilityReasonDetails']['cooldownTime'] ) ) { $region_cooldown = intval( strtotime( $region_status['reviewIneligibilityReasonDetails']['cooldownTime'] ) ); if ( ! $cooldown || $region_cooldown > $cooldown ) { $cooldown = $region_cooldown; } } return $cooldown; } /** * Updates the status reference in case the new status has more priority. * * @param String $new_status New status to check has more priority than the current one * @param String $status Referenced current status * * @return String The status */ private function maybe_update_status( $new_status, $status ) { $status_priority_list = [ self::ONBOARDING, // highest priority self::DISAPPROVED, self::WARNING, self::UNDER_REVIEW, self::PENDING_REVIEW, self::APPROVED, ]; $current_status_priority = array_search( $status, $status_priority_list, true ); $new_status_priority = array_search( $new_status, $status_priority_list, true ); if ( $new_status_priority !== false && ( is_null( $status ) || $current_status_priority > $new_status_priority ) ) { return $new_status; } return $status; } /** * Updates the regions where a request review is allowed. * * @param array $region_status Associative array containing the region eligibility. * @param array $review_eligible_regions Indexed array with the current eligible regions. * @param "freeListingsProgram"|"shoppingAdsProgram" $type The program type. * * @return array The (maybe) modified $review_eligible_regions array */ private function maybe_load_eligible_region( $region_status, $review_eligible_regions, $type = 'freeListingsProgram' ) { if ( ! empty( $region_status['regionCodes'] ) && isset( $region_status['reviewEligibilityStatus'] ) && $region_status['reviewEligibilityStatus'] === self::ELIGIBLE ) { $region_codes = $region_status['regionCodes']; sort( $region_codes ); // sometimes the regions come unsorted between the different programs $region_id = $region_codes[0]; if ( ! isset( $review_eligible_regions[ $region_id ] ) ) { $review_eligible_regions[ $region_id ] = []; } $review_eligible_regions[ $region_id ][] = strtolower( $type ); // lowercase as is how we expect it in WCS } return $review_eligible_regions; } /** * Allows a hook to modify the lifetime of the Account review data. * * @return int */ public function get_account_review_lifetime(): int { return apply_filters( 'woocommerce_gla_mc_account_review_lifetime', self::MC_ACCOUNT_REVIEW_LIFETIME ); } /** * @param int $cooldown The cooldown in PHP format (seconds) * * @return int The cooldown in milliseconds and adding the lifetime cache */ private function get_cooldown( int $cooldown ) { if ( $cooldown ) { $cooldown = ( $cooldown + $this->get_account_review_lifetime() ) * 1000; } return $cooldown; } } PK!*PP#src/Google/SiteVerificationMeta.phpnu[display_meta_token(); } ); } /** * Display the meta tag with the site verification token. */ protected function display_meta_token() { $settings = $this->options->get( OptionsInterface::SITE_VERIFICATION, [] ); if ( empty( $settings['meta_tag'] ) ) { return; } echo '' . PHP_EOL; echo wp_kses( $settings['meta_tag'], [ 'meta' => [ 'name' => true, 'content' => true, ], ] ) . PHP_EOL; } } PK!3+src/HelperTraits/GTINMigrationUtilities.phpnu[=' ) && method_exists( WC_Product::class, 'get_global_unique_id' ); } /** * If GTIN field should be hidden, this is when initial installed version is after the GTIN migration logic. * * @return bool */ protected function should_hide_gtin(): bool { // Don't hide in case GTIN is not available in core. if ( ! $this->is_gtin_available_in_core() ) { return false; } $first_install_version = $this->options()->get( OptionsInterface::INSTALL_VERSION, false ); return $first_install_version && version_compare( $first_install_version, $this->get_gtin_hidden_version(), '>' ); } /** * Get the status for the migration of GTIN. * * GTIN_MIGRATION_COMPLETED: GTIN is not available on that WC version or the initial version installed after GTIN migration. * GTIN_MIGRATION_READY: GTIN is available in core and on read-only mode in the extension. It's ready for migration. * GTIN_MIGRATION_STARTED: GTIN migration is started * GTIN_MIGRATION_COMPLETED: GTIN Migration is completed * * @return string */ protected function get_gtin_migration_status(): string { // If the current version doesn't show GTIN field or the GTIN field is not available in core. if ( ! $this->is_gtin_available_in_core() || $this->should_hide_gtin() ) { return MigrateGTIN::GTIN_MIGRATION_UNAVAILABLE; } return $this->options()->get( OptionsInterface::GTIN_MIGRATION_STATUS, MigrateGTIN::GTIN_MIGRATION_READY ); } /** * * Get the options object. * Notice classes with OptionsAwareTrait only get the options object auto-loaded if * they are registered in the Container class. * If they are instantiated on the fly (like the input fields), then this won't get done. * That's why we need to fetch it from the container in case options field is null. * * @return OptionsInterface */ protected function options(): OptionsInterface { return $this->options ?? woogle_get_container()->get( OptionsInterface::class ); } /** * Prepares the GTIN to be saved. * * @param string $gtin * @return string */ protected function prepare_gtin( string $gtin ): string { return str_replace( '-', '', $gtin ); } /** * Gets the message when the GTIN is invalid. * * @param WC_Product $product * @param string $gtin * @return string */ protected function error_gtin_invalid( WC_Product $product, string $gtin ): string { return sprintf( 'GTIN [ %s ] has been skipped for Product ID: %s - %s. Invalid GTIN was found.', $gtin, $product->get_id(), $product->get_name() ); } /** * Gets the message when the GTIN is already in the Product Inventory * * @param WC_Product $product * @return string */ protected function error_gtin_already_set( WC_Product $product ): string { return sprintf( 'GTIN has been skipped for Product ID: %s - %s. GTIN was found in Product Inventory tab.', $product->get_id(), $product->get_name() ); } /** * Gets the message when the GTIN is not found. * * @param WC_Product $product * @return string */ protected function error_gtin_not_found( WC_Product $product ): string { return sprintf( 'GTIN has been skipped for Product ID: %s - %s. No GTIN was found', $product->get_id(), $product->get_name() ); } /** * Gets the message when the GTIN had an error when saving. * * @param WC_Product $product * @param string $gtin * @param Exception $e * * @return string */ protected function error_gtin_not_saved( WC_Product $product, string $gtin, Exception $e ): string { return sprintf( 'GTIN [ %s ] for Product ID: %s - %s has an error - %s', $gtin, $product->get_id(), $product->get_name(), $e->getMessage() ); } /** * Gets the message when the GTIN is successfully migrated. * * @param WC_Product $product * @param string $gtin * * @return string */ protected function successful_migrated_gtin( WC_Product $product, string $gtin ): string { return sprintf( 'GTIN [ %s ] has been migrated for Product ID: %s - %s', $gtin, $product->get_id(), $product->get_name() ); } /** * Gets the GTIN value * * @param WC_Product $product The product * @return string|null */ protected function get_gtin( WC_Product $product ): ?string { /** * Filters the value of the GTIN before performing the migration. * This value will be he one that we copy inside the Product Inventory GTIN. */ return apply_filters( 'woocommerce_gla_gtin_migration_value', $this->attribute_manager->get_value( $product, 'gtin' ), $product ); } } PK!ir %src/HelperTraits/ISO3166Awareness.phpnu[iso3166_data_provider = $provider; } } PK!O5E E src/HelperTraits/Utilities.phpnu[ $status, 'limit' => $count, 'return' => 'ids', 'orderby' => 'date', 'order' => 'ASC', ]; return $count === count( wc_get_orders( $args ) ); } /** * Test how long GLA has been active. * * @param int $seconds Time in seconds to check. * @return bool Whether or not GLA has been active for $seconds. */ protected function gla_active_for( $seconds ): bool { $gla_installed = $this->options->get( OptionsInterface::INSTALL_TIMESTAMP, false ); if ( false === $gla_installed ) { return false; } return ( ( time() - $gla_installed ) >= $seconds ); } /** * Test how long GLA has been setup for. * * @param int $seconds Time in seconds to check. * @return bool Whether or not GLA has been active for $seconds. */ protected function gla_setup_for( $seconds ): bool { $gla_completed_setup = $this->options->get( OptionsInterface::MC_SETUP_COMPLETED_AT, false ); if ( false === $gla_completed_setup ) { return false; } return ( ( time() - $gla_completed_setup ) >= $seconds ); } /** * Is Jetpack connected? * * @since 1.12.5 * * @return boolean */ protected function is_jetpack_connected(): bool { return boolval( $this->options->get( OptionsInterface::JETPACK_CONNECTED, false ) ); } /** * Encode data to Base64URL * * @since 2.8.0 * * @param string $data The string that will be base64 URL encoded. * * @return string */ protected function base64url_encode( $data ): string { $b64 = base64_encode( $data ); // Convert Base64 to Base64URL by replacing "+" with "-" and "/" with "_" $url = strtr( $b64, '+/', '-_' ); // Remove padding character from the end of line and return the Base64URL result return rtrim( $url, '=' ); } /** * Decode Base64URL string * * @since 2.8.0 * * @param string $data The data that will be base64 URL encoded. * * @return boolean|string */ protected function base64url_decode( $data ): string { // Convert Base64URL to Base64 by replacing "-" with "+" and "_" with "/" $b64 = strtr( $data, '-_', '+/' ); // Decode Base64 string and return the original data return base64_decode( $b64 ); } } PK!Rp$src/HelperTraits/ViewHelperTrait.phpnu[ true, 'aria-details' => true, 'aria-label' => true, 'aria-labelledby' => true, 'aria-hidden' => true, 'class' => true, 'id' => true, 'style' => true, 'title' => true, 'role' => true, 'data-*' => true, 'action' => true, 'value' => true, 'name' => true, 'selected' => true, 'type' => true, 'disabled' => true, ]; return array_merge( wp_kses_allowed_html( 'post' ), [ 'form' => $allowed_attributes, 'input' => $allowed_attributes, 'select' => $allowed_attributes, 'option' => $allowed_attributes, ] ); } /** * Appends a prefix to the given ID and returns it. * * @param string $id * * @return string * * @since 1.1.0 */ protected function prefix_id( string $id ): string { return "{$this->get_slug()}_$id"; } } PK!X.#src/Infrastructure/Activateable.phpnu[container = $container; } /** * Activate the plugin. * * @return void */ public function activate(): void { // Delay activation if a required plugin is missing or an incompatible plugin is active. if ( ! PluginValidator::validate() ) { // Using update_option because we cannot access the option service // when the services have not been registered. update_option( 'gla_' . OptionsInterface::DELAYED_ACTIVATE, true ); return; } $this->maybe_register_services(); foreach ( $this->registered_services as $service ) { if ( $service instanceof Activateable ) { $service->activate(); } } } /** * Deactivate the plugin. * * @return void */ public function deactivate(): void { $this->maybe_register_services(); foreach ( $this->registered_services as $service ) { if ( $service instanceof Deactivateable ) { $service->deactivate(); } } } /** * Register the plugin with the WordPress system. * * @return void */ public function register(): void { add_action( self::SERVICE_REGISTRATION_HOOK, function () { $this->maybe_register_services(); }, 20 ); add_action( 'init', function () { // Register the job initializer only if it is available, see JobInitializer::is_needed. // Note: ActionScheduler must be loaded after the init hook, so we can't load JobInitializer like a regular Service. if ( $this->container->has( JobInitializer::class ) ) { $this->container->get( JobInitializer::class )->register(); } // Check if activation is still pending. if ( $this->container->get( OptionsInterface::class )->get( OptionsInterface::DELAYED_ACTIVATE ) ) { $this->activate(); // Remove the DELAYED_ACTIVATE flag. $this->container->get( OptionsInterface::class )->delete( OptionsInterface::DELAYED_ACTIVATE ); } } ); } /** * Register our services if dependency validation passes. */ protected function maybe_register_services(): void { // Don't register anything if a required plugin is missing or an incompatible plugin is active. if ( ! PluginValidator::validate() ) { $this->registered_services = []; return; } static $registered = false; if ( $registered ) { return; } /** @var Service[] $services */ $services = $this->container->get( Service::class ); foreach ( $services as $service ) { if ( $service instanceof Registerable ) { $service->register(); } $this->registered_services[ get_class( $service ) ] = $service; } $registered = true; } } PK!src/Infrastructure/Plugin.phpnu[validate_instanceof( $integration, IntegrationInterface::class ); $this->integrations[] = $integration; } } /** * Initialize all active integrations. */ public function register(): void { foreach ( $this->integrations as $integration ) { if ( $integration->is_active() ) { $integration->init(); } } } } PK!~A**(src/Integration/IntegrationInterface.phpnu[verify_json_api_authorization_request(); add_action( 'wp_login', [ $this, 'store_json_api_authorization_token' ], 10, 2 ); add_action( 'login_message', [ $this, 'login_message_json_api_authorization' ] ); add_action( 'login_form', [ $this, 'preserve_action_in_login_form_for_json_api_authorization' ] ); add_filter( 'site_url', [ $this, 'post_login_form_to_signed_url' ], 10, 3 ); } /** * If someone logs in to approve API access, store the Access Code in usermeta. * * @param string $user_login Unused. * @param WP_User $user User logged in. * * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5349 */ public function store_json_api_authorization_token( $user_login, $user ) { add_filter( 'login_redirect', [ $this, 'add_token_to_login_redirect_json_api_authorization' ], 10, 3 ); add_filter( 'allowed_redirect_hosts', [ $this, 'allow_wpcom_public_api_domain' ] ); $token = wp_generate_password( 32, false ); update_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], $token ); } /** * Make sure the POSTed request is handled by the same action. * * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5336 */ public function preserve_action_in_login_form_for_json_api_authorization() { $http_host = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- escaped with esc_url below. $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- escaped with esc_url below. echo "\n"; echo "\n"; } /** * Make sure the login form is POSTed to the signed URL so we can reverify the request. * * @param string $url Redirect URL. * @param string $path Path. * @param string $scheme URL Scheme. * * @see https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/class.jetpack.php#L5318 */ public function post_login_form_to_signed_url( $url, $path, $scheme ) { if ( 'wp-login.php' !== $path || ( 'login_post' !== $scheme && 'login' !== $scheme ) ) { return $url; } $query_string = isset( $_SERVER['QUERY_STRING'] ) ? wp_unslash( $_SERVER['QUERY_STRING'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $parsed_url = wp_parse_url( $url ); $url = strtok( $url, '?' ); $url = "$url?{$query_string}"; if ( ! empty( $parsed_url['query'] ) ) { $url .= "&{$parsed_url['query']}"; } return $url; } /** * Add the Access Code details to the public-api.wordpress.com redirect. * * @param string $redirect_to URL. * @param string $original_redirect_to URL. * @param WP_User $user WP_User for the redirect. * * @return string * * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5401 */ public function add_token_to_login_redirect_json_api_authorization( $redirect_to, $original_redirect_to, $user ) { return add_query_arg( urlencode_deep( [ 'jetpack-code' => get_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], true ), 'jetpack-user-id' => (int) $user->ID, 'jetpack-state' => $this->json_api_authorization_request['state'], ] ), $redirect_to ); } /** * Add public-api.wordpress.com to the safe redirect allowed list - only added when someone allows API access. * To be used with a filter of allowed domains for a redirect. * * @param array $domains Allowed WP.com Environments. * * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5363 */ public function allow_wpcom_public_api_domain( $domains ) { $domains[] = 'public-api.wordpress.com'; return $domains; } /** * Check if the redirect is encoded. * * @param string $redirect_url Redirect URL. * * @return bool If redirect has been encoded. * * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5375 */ public static function is_redirect_encoded( $redirect_url ) { return preg_match( '/https?%3A%2F%2F/i', $redirect_url ) > 0; } /** * HTML for the JSON API authorization notice. * * @return string * * @see https://github.com/Automattic/jetpack/blob/6066d7181f78bdec7c355d8b2152733f4691e8a9/projects/plugins/jetpack/class.jetpack.php#L5603 */ public function login_message_json_api_authorization() { return '

' . sprintf( /* translators: Name/image of the client requesting authorization */ esc_html__( '%s wants to access your site’s data. Log in to authorize that access.', 'google-listings-and-ads' ), '' . esc_html( $this->json_api_authorization_request['client_title'] ) . '' ) . '

'; } /** * Verifies the request by checking the signature * * @param null|array $environment Value to override $_REQUEST. * * @see https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/class.jetpack.php#L5422 */ public function verify_json_api_authorization_request( $environment = null ) { $environment = $environment === null ? $_REQUEST // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verification handled later in function. : $environment; list( $env_token,, $env_user_id ) = explode( ':', $environment['token'] ); $token = ( new Tokens() )->get_access_token( $env_user_id, $env_token ); if ( ! $token || empty( $token->secret ) ) { wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'google-listings-and-ads' ) ); } $die_error = __( 'Someone may be trying to trick you into giving them access to your site. Or it could be you just encountered a bug :). Either way, please close this window.', 'google-listings-and-ads' ); // Host has encoded the request URL, probably as a result of a bad http => https redirect. if ( self::is_redirect_encoded( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no site changes, we're erroring out. /** * Jetpack authorisation request Error. */ do_action( 'jetpack_verify_api_authorization_request_error_double_encode' ); $die_error = sprintf( /* translators: %s is a URL */ __( 'Your site is incorrectly double-encoding redirects from http to https. This is preventing Jetpack from authenticating your connection. Please visit our support page for details about how to resolve this.', 'google-listings-and-ads' ), esc_url( 'https://jetpack.com/support/double-encoding/' ) ); } $jetpack_signature = new Jetpack_Signature( $token->secret, (int) Jetpack_Options::get_option( 'time_diff' ) ); if ( isset( $environment['jetpack_json_api_original_query'] ) ) { $signature = $jetpack_signature->sign_request( $environment['token'], $environment['timestamp'], $environment['nonce'], '', 'GET', $environment['jetpack_json_api_original_query'], null, true ); } else { $signature = $jetpack_signature->sign_current_request( [ 'body' => null, 'method' => 'GET', ] ); } if ( ! $signature ) { wp_die( wp_kses( $die_error, [ 'a' => [ 'href' => [], ], ] ) ); } elseif ( is_wp_error( $signature ) ) { wp_die( wp_kses( $die_error, [ 'a' => [ 'href' => [], ], ] ) ); } elseif ( ! hash_equals( $signature, $environment['signature'] ) ) { if ( is_ssl() ) { // If we signed an HTTP request on the Jetpack Servers, but got redirected to HTTPS by the local blog, check the HTTP signature as well. $signature = $jetpack_signature->sign_current_request( [ 'scheme' => 'http', 'body' => null, 'method' => 'GET', ] ); if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $environment['signature'] ) ) { wp_die( wp_kses( $die_error, [ 'a' => [ 'href' => [], ], ] ) ); } } else { wp_die( wp_kses( $die_error, [ 'a' => [ 'href' => [], ], ] ) ); } } $timestamp = (int) $environment['timestamp']; $nonce = stripslashes( (string) $environment['nonce'] ); if ( ! $this->connection_manager ) { $this->connection_manager = new Connection_Manager(); } if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) { // De-nonce the nonce, at least for 5 minutes. // We have to reuse this nonce at least once (used the first time when the initial request is made, used a second time when the login form is POSTed). $old_nonce_time = get_option( "jetpack_nonce_{$timestamp}_{$nonce}" ); if ( $old_nonce_time < time() - 300 ) { wp_die( esc_html__( 'The authorization process expired. Please go back and try again.', 'google-listings-and-ads' ) ); } } $data = json_decode( base64_decode( stripslashes( $environment['data'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode $data_filters = [ 'state' => 'opaque', 'client_id' => 'int', 'client_title' => 'string', 'client_image' => 'url', ]; foreach ( $data_filters as $key => $sanitation ) { if ( ! isset( $data->$key ) ) { wp_die( wp_kses( $die_error, [ 'a' => [ 'href' => [], ], ] ) ); } switch ( $sanitation ) { case 'int': $this->json_api_authorization_request[ $key ] = (int) $data->$key; break; case 'opaque': $this->json_api_authorization_request[ $key ] = (string) $data->$key; break; case 'string': $this->json_api_authorization_request[ $key ] = wp_kses( (string) $data->$key, [] ); break; case 'url': $this->json_api_authorization_request[ $key ] = esc_url_raw( (string) $data->$key ); break; } } if ( empty( $this->json_api_authorization_request['client_id'] ) ) { wp_die( wp_kses( $die_error, [ 'a' => [ 'href' => [], ], ] ) ); } } } PK!Bz %src/Integration/WooCommerceBrands.phpnu[wp = $wp; } /** * Returns whether the integration is active or not. * * @return bool */ public function is_active(): bool { return defined( 'WC_BRANDS_VERSION' ); } /** * Initializes the integration (e.g. by registering the required hooks, filters, etc.). * * @return void */ public function init(): void { add_filter( 'woocommerce_gla_product_attribute_value_options_brand', function ( array $value_options ) { return $this->add_value_option( $value_options ); } ); add_filter( 'woocommerce_gla_product_attribute_value_brand', function ( $value, WC_Product $product ) { return $this->get_brand( $value, $product ); }, 10, 2 ); } /** * @param array $value_options * * @return array */ protected function add_value_option( array $value_options ): array { $value_options[ self::VALUE_KEY ] = 'From WooCommerce Brands'; return $value_options; } /** * @param mixed $value * @param WC_Product $product * * @return mixed */ protected function get_brand( $value, WC_Product $product ) { if ( self::VALUE_KEY === $value ) { $product_id = $product instanceof WC_Product_Variation ? $product->get_parent_id() : $product->get_id(); $terms = $this->wp->get_the_terms( $product_id, 'product_brand' ); if ( is_array( $terms ) ) { return $this->get_brand_from_terms( $terms ); } } return self::VALUE_KEY === $value ? null : $value; } /** * Returns the brand from the given taxonomy terms. * * If multiple, it returns the first selected brand as primary brand * * @param WP_Term[] $terms * * @return string */ protected function get_brand_from_terms( array $terms ): string { $brands = []; foreach ( $terms as $term ) { $brands[] = $term->name; if ( empty( $term->parent ) ) { return $term->name; } } return $brands[0]; } } PK!uė(src/Integration/WooCommercePreOrders.phpnu[product_helper = $product_helper; } /** * Returns whether the integration is active or not. * * @return bool */ public function is_active(): bool { return defined( 'WC_PRE_ORDERS_VERSION' ); } /** * Initializes the integration (e.g. by registering the required hooks, filters, etc.). * * @return void */ public function init(): void { add_filter( 'woocommerce_gla_product_attribute_values', function ( array $attributes, WC_Product $product ) { return $this->maybe_set_preorder_availability( $attributes, $product ); }, 2, 10 ); add_action( 'wc_pre_orders_pre_orders_disabled_for_product', function ( $product_id ) { $this->trigger_sync( $product_id ); }, ); } /** * Sets the product's availability to "preorder" if it's in-stock and can be pre-ordered. * * @param array $attributes * @param WC_Product $product * * @return array */ protected function maybe_set_preorder_availability( array $attributes, WC_Product $product ): array { if ( $product->is_in_stock() && WC_Pre_Orders_Product::product_can_be_pre_ordered( $product ) ) { $attributes['availability'] = WCProductAdapter::AVAILABILITY_PREORDER; $availability_date = $this->get_availability_datetime( $product ); if ( ! empty( $availability_date ) ) { $attributes['availabilityDate'] = (string) $availability_date; } } return $attributes; } /** * @param WC_Product $product * * @return WC_DateTime|null */ protected function get_availability_datetime( WC_Product $product ): ?WC_DateTime { $product = $this->product_helper->maybe_swap_for_parent( $product ); $timestamp = $product->get_meta( '_wc_pre_orders_availability_datetime', true ); if ( empty( $timestamp ) ) { return null; } try { return new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); return null; } } /** * Triggers an update job for the product to be synced with Merchant Center. * * This is required because WooCommerce Pre-orders updates the product's metadata via `update_post_meta`, which * does not automatically trigger a sync. * * @hooked wc_pre_orders_pre_orders_disabled_for_product * * @param mixed $product_id */ protected function trigger_sync( $product_id ): void { try { $product = $this->product_helper->get_wc_product( (int) $product_id ); } catch ( InvalidValue $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); return; } // Manually trigger an update job by saving the product object. $product->save(); } } PK!8d-src/Integration/WooCommerceProductBundles.phpnu[attribute_manager = $attribute_manager; } /** * Returns whether the integration is active or not. * * @return bool */ public function is_active(): bool { return class_exists( 'WC_Bundles' ) && is_callable( 'WC_Bundles::instance' ); } /** * Initializes the integration (e.g. by registering the required hooks, filters, etc.). * * @return void */ public function init(): void { $this->init_product_types(); // update the isBundle attribute for bundle products add_action( 'woocommerce_new_product', function ( int $product_id, WC_Product $product ) { $this->handle_update_product( $product ); }, 10, 2 ); add_action( 'woocommerce_update_product', function ( int $product_id, WC_Product $product ) { $this->handle_update_product( $product ); }, 10, 2 ); // recalculate the product price for bundles add_filter( 'woocommerce_gla_product_attribute_value_price', function ( float $price, WC_Product $product, bool $tax_excluded ) { return $this->calculate_price( $price, $product, $tax_excluded ); }, 10, 3 ); add_filter( 'woocommerce_gla_product_attribute_value_sale_price', function ( float $sale_price, WC_Product $product, bool $tax_excluded ) { return $this->calculate_sale_price( $sale_price, $product, $tax_excluded ); }, 10, 3 ); // adapt the `is_virtual` property for bundle products add_filter( 'woocommerce_gla_product_property_value_is_virtual', function ( bool $is_virtual, WC_Product $product ) { return $this->is_virtual_bundle( $is_virtual, $product ); }, 10, 2 ); // filter unsupported bundle products add_filter( 'woocommerce_gla_get_sync_ready_products_pre_filter', function ( array $products ) { return $this->get_sync_ready_bundle_products( $products ); } ); } /** * Adds the "bundle" product type to the list of applicable types * for every attribute that can be applied to "simple" products. * * @return void */ protected function init_product_types(): void { // every attribute that applies to simple products also applies to bundle products. foreach ( AttributeManager::get_available_attribute_types() as $attribute_type ) { $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); $applicable_types = call_user_func( [ $attribute_type, 'get_applicable_product_types' ] ); if ( ! in_array( 'simple', $applicable_types, true ) ) { continue; } add_filter( "woocommerce_gla_attribute_applicable_product_types_{$attribute_id}", function ( array $applicable_types ) { return $this->add_bundle_type( $applicable_types ); } ); } // hide the isBundle attribute on 'bundle' products (we set it automatically to true) add_filter( 'woocommerce_gla_attribute_hidden_product_types_isBundle', function ( array $applicable_types ) { return $this->add_bundle_type( $applicable_types ); } ); // add the 'bundle' type to list of supported product types add_filter( 'woocommerce_gla_supported_product_types', function ( array $product_types ) { return $this->add_bundle_type( $product_types ); } ); } /** * @param array $types * * @return array */ private function add_bundle_type( array $types ): array { $types[] = 'bundle'; return $types; } /** * Set the isBundle product attribute to 'true' if product is a bundle. * * @param WC_Product $product */ private function handle_update_product( WC_Product $product ) { if ( $product->is_type( 'bundle' ) ) { $this->attribute_manager->update( $product, new IsBundle( true ) ); } } /** * @param float $price Calculated price of the product * @param WC_Product $product WooCommerce product * @param bool $tax_excluded Whether tax is excluded from product price */ private function calculate_price( float $price, WC_Product $product, bool $tax_excluded ): float { if ( ! $product instanceof WC_Product_Bundle ) { return $price; } return $tax_excluded ? $product->get_bundle_regular_price_excluding_tax() : $product->get_bundle_regular_price_including_tax(); } /** * @param float $sale_price Calculated sale price of the product * @param WC_Product $product WooCommerce product * @param bool $tax_excluded Whether tax is excluded from product price */ private function calculate_sale_price( float $sale_price, WC_Product $product, bool $tax_excluded ): float { if ( ! $product instanceof WC_Product_Bundle ) { return $sale_price; } $regular_price = $tax_excluded ? $product->get_bundle_regular_price_excluding_tax() : $product->get_bundle_regular_price_including_tax(); $price = $tax_excluded ? $product->get_bundle_price_excluding_tax() : $product->get_bundle_price_including_tax(); // return current price as the sale price if it's lower than the regular price. if ( $price < $regular_price ) { return $price; } return $sale_price; } /** * @param bool $is_virtual Whether a product is virtual * @param WC_Product $product WooCommerce product */ private function is_virtual_bundle( bool $is_virtual, WC_Product $product ): bool { if ( $product instanceof WC_Product_Bundle && is_callable( [ $product, 'is_virtual_bundle' ] ) ) { return $product->is_virtual_bundle(); } return $is_virtual; } /** * Skip unsupported bundle products. * * @param WC_Product[] $products WooCommerce products */ private function get_sync_ready_bundle_products( array $products ): array { return array_filter( $products, function ( WC_Product $product ) { if ( $product instanceof WC_Product_Bundle && $product->requires_input() ) { return false; } return true; } ); } } PK!S1S(S(src/Integration/WPCOMProxy.phpnu[shipping_time_query = $shipping_time_query; $this->attribute_manager = $attribute_manager; } /** * The meta key used to filter the items. * * @var string */ public const KEY_VISIBILITY = '_wc_gla_visibility'; /** * The Post types to be filtered. * * @var array */ public static $post_types_to_filter = [ 'product' => [ 'meta_query' => [ [ 'key' => self::KEY_VISIBILITY, 'value' => ChannelVisibility::SYNC_AND_SHOW, 'compare' => '=', ], ], ], 'shop_coupon' => [ 'meta_query' => [ [ 'key' => self::KEY_VISIBILITY, 'value' => ChannelVisibility::SYNC_AND_SHOW, 'compare' => '=', ], [ 'key' => 'customer_email', 'compare' => 'NOT EXISTS', ], ], ], 'product_variation' => [ 'meta_query' => null, ], ]; /** * Register all filters. */ public function register(): void { // Allow to filter by gla_syncable. add_filter( 'woocommerce_rest_query_vars', function ( $valid_vars ) { $valid_vars[] = 'gla_syncable'; return $valid_vars; } ); $this->register_callbacks(); foreach ( array_keys( self::$post_types_to_filter ) as $object_type ) { $this->register_object_types_filter( $object_type ); } } /** * Register the filters for a specific object type. * * @param string $object_type The object type. */ protected function register_object_types_filter( string $object_type ): void { add_filter( 'woocommerce_rest_prepare_' . $object_type . '_object', [ $this, 'filter_response_by_syncable_item' ], PHP_INT_MAX, // Run this filter last to override any other response. 3 ); add_filter( 'woocommerce_rest_prepare_' . $object_type . '_object', [ $this, 'prepare_response' ], PHP_INT_MAX - 1, 3 ); add_filter( 'woocommerce_rest_' . $object_type . '_object_query', [ $this, 'filter_by_metaquery' ], 10, 2 ); } /** * Register the callbacks. */ protected function register_callbacks() { add_filter( 'rest_request_after_callbacks', /** * Add the Google for WooCommerce and Ads settings to the settings/general response. * * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response The response object. * @param mixed $handler The handler. * @param WP_REST_Request $request The request object. */ function ( $response, $handler, $request ) { if ( ! $this->is_gla_request( $request ) || ! $response instanceof WP_REST_Response ) { return $response; } $data = $response->get_data(); if ( $request->get_route() === '/wc/v3/settings/general' ) { $data[] = [ 'id' => 'gla_target_audience', 'label' => 'Google for WooCommerce: Target Audience', 'value' => $this->options->get( OptionsInterface::TARGET_AUDIENCE, [] ), ]; $data[] = [ 'id' => 'gla_shipping_times', 'label' => 'Google for WooCommerce: Shipping Times', 'value' => $this->shipping_time_query->get_all_shipping_times(), ]; $data[] = [ 'id' => 'gla_language', 'label' => 'Google for WooCommerce: Store language', 'value' => get_locale(), ]; $response->set_data( array_values( $data ) ); } $response->set_data( $this->prepare_data( $response->get_data(), $request ) ); return $response; }, 10, 3 ); } /** * Prepares the data converting the empty arrays in objects for consistency. * * @param array $data The response data to parse * @param WP_REST_Request $request The request object. * @return mixed */ public function prepare_data( $data, $request ) { if ( ! is_array( $data ) ) { return $data; } foreach ( $data as $key => $value ) { if ( preg_match( '/^\/wc\/v3\/shipping\/zones\/\d+\/methods/', $request->get_route() ) && isset( $value['settings'] ) && empty( $value['settings'] ) ) { $data[ $key ]['settings'] = (object) $value['settings']; } } return $data; } /** * Whether the request is coming from the WPCOM proxy. * * @param WP_REST_Request $request The request object. * * @return bool */ protected function is_gla_request( WP_REST_Request $request ): bool { // WPCOM proxy will set the gla_syncable to 1 if the request is coming from the proxy and it is the Google App. return $request->get_param( 'gla_syncable' ) === '1'; } /** * Get route pieces: resource and id, if present. * * @param WP_REST_Request $request The request object. * * @return array The route pieces. */ protected function get_route_pieces( WP_REST_Request $request ): array { $route = $request->get_route(); $pattern = '/(?P[\w]+)(?:\/(?P[\d]+))?$/'; preg_match( $pattern, $route, $matches ); return $matches; } /** * Filter response by syncable item. * * @param WP_REST_Response $response The response object. * @param mixed $item The item. * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response object updated. */ public function filter_response_by_syncable_item( $response, $item, WP_REST_Request $request ): WP_REST_Response { if ( ! $this->is_gla_request( $request ) ) { return $response; } $pieces = $this->get_route_pieces( $request ); if ( ! isset( $pieces['id'] ) || ! isset( $pieces['resource'] ) || ! in_array( $pieces['resource'], self::PROTECTED_RESOURCES, true ) ) { return $response; } $meta_data = $response->get_data()['meta_data'] ?? []; foreach ( $meta_data as $meta ) { if ( $meta->key === self::KEY_VISIBILITY && $meta->value === ChannelVisibility::SYNC_AND_SHOW ) { return $response; } } return new WP_REST_Response( [ 'code' => 'gla_rest_item_no_syncable', 'message' => 'Item not syncable', 'data' => [ 'status' => '403', ], ], 403 ); } /** * Query items with specific args for example where _wc_gla_visibility is set to sync-and-show. * * @param array $args The query args. * @param WP_REST_Request $request The request object. * * @return array The query args updated. * */ public function filter_by_metaquery( array $args, WP_REST_Request $request ): array { if ( ! $this->is_gla_request( $request ) ) { return $args; } $post_type = $args['post_type']; $post_type_filters = self::$post_types_to_filter[ $post_type ]; if ( ! isset( $post_type_filters['meta_query'] ) || ! is_array( $post_type_filters['meta_query'] ) ) { return $args; } $args['meta_query'] = [ ...$args['meta_query'] ?? [], ...$post_type_filters['meta_query'] ]; return $args; } /** * Prepares the response when the request is coming from the WPCOM proxy: * * Filter all the private metadata and returns only the public metadata and those prefixed with _wc_gla * For WooCommerce products, it will add the attribute mapping values. * * @param WP_REST_Response $response The response object. * @param mixed $item The item. * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response object updated. */ public function prepare_response( WP_REST_Response $response, $item, WP_REST_Request $request ): WP_REST_Response { if ( ! $this->is_gla_request( $request ) ) { return $response; } $data = $response->get_data(); $resource = $this->get_route_pieces( $request )['resource'] ?? null; if ( $item instanceof WC_Product && ( $resource === 'products' || $resource === 'variations' ) ) { $attr = $this->attribute_manager->get_all_aggregated_values( $item ); // In case of empty array, convert to object to keep the response consistent. $data['gla_attributes'] = (object) $attr; // Force types and prevent user type change for fields as Google has strict type requirements. $data['price'] = strval( $data['price'] ?? null ); $data['regular_price'] = strval( $data['regular_price'] ?? null ); $data['sale_price'] = strval( $data['sale_price'] ?? null ); } foreach ( $data['meta_data'] ?? [] as $key => $meta ) { if ( str_starts_with( $meta->key, '_' ) && ! str_starts_with( $meta->key, '_wc_gla' ) ) { unset( $data['meta_data'][ $key ] ); } } $data['meta_data'] = array_values( $data['meta_data'] ); $response->set_data( $data ); return $response; } } PK!>Y'src/Integration/YoastWooCommerceSeo.phpnu[add_value_option( $value_options ); } ); add_filter( 'woocommerce_gla_product_attribute_value_options_gtin', function ( array $value_options ) { return $this->add_value_option( $value_options ); } ); add_filter( 'woocommerce_gla_product_attribute_value_mpn', function ( $value, WC_Product $product ) { return $this->get_mpn( $value, $product ); }, 10, 2 ); add_filter( 'woocommerce_gla_product_attribute_value_gtin', function ( $value, WC_Product $product ) { return $this->get_gtin( $value, $product ); }, 10, 2 ); add_filter( 'woocommerce_gla_attribute_mapping_sources', function ( $sources, $attribute_id ) { return $this->load_yoast_seo_attribute_mapping_sources( $sources, $attribute_id ); }, 10, 2 ); add_filter( 'woocommerce_gla_gtin_migration_value', function ( $gtin, $product ) { if ( ! $gtin || self::VALUE_KEY === $gtin ) { return $this->get_gtin( self::VALUE_KEY, $product ); } return $gtin; }, 10, 2 ); } /** * @param array $value_options * * @return array */ protected function add_value_option( array $value_options ): array { $value_options[ self::VALUE_KEY ] = 'From Yoast WooCommerce SEO'; return $value_options; } /** * @param mixed $value * @param WC_Product $product * * @return mixed */ protected function get_mpn( $value, WC_Product $product ) { if ( strpos( $value, self::VALUE_KEY ) === 0 ) { $value = $this->get_identifier_value( 'mpn', $product ); } return ! empty( $value ) ? $value : null; } /** * @param mixed $value * @param WC_Product $product * * @return mixed */ protected function get_gtin( $value, WC_Product $product ) { if ( strpos( $value, self::VALUE_KEY ) === 0 ) { $gtin_values = [ $this->get_identifier_value( 'isbn', $product ), $this->get_identifier_value( 'gtin8', $product ), $this->get_identifier_value( 'gtin12', $product ), $this->get_identifier_value( 'gtin13', $product ), $this->get_identifier_value( 'gtin14', $product ), ]; $gtin_values = array_values( array_filter( $gtin_values ) ); $value = $gtin_values[0] ?? null; } return $value; } /** * Get the identifier value from cache or product meta. * * @param string $key * @param WC_Product $product * * @return mixed|null */ protected function get_identifier_value( string $key, WC_Product $product ) { $product_id = $product->get_id(); if ( ! isset( $this->yoast_global_identifiers[ $product_id ] ) ) { $this->yoast_global_identifiers[ $product_id ] = $this->get_identifier_meta( $product ); } return ! empty( $this->yoast_global_identifiers[ $product_id ][ $key ] ) ? $this->yoast_global_identifiers[ $product_id ][ $key ] : null; } /** * Get identifier meta from product. * For variations fallback to parent product if meta is empty. * * @since 2.3.1 * * @param WC_Product $product * * @return mixed|null */ protected function get_identifier_meta( WC_Product $product ) { if ( ! $product ) { return null; } if ( $product instanceof WC_Product_Variation ) { $identifiers = $product->get_meta( 'wpseo_variation_global_identifiers_values', true ); if ( ! is_array( $identifiers ) || empty( array_filter( $identifiers ) ) ) { $parent_product = wc_get_product( $product->get_parent_id() ); $identifiers = $this->get_identifier_meta( $parent_product ); } return $identifiers; } return $product->get_meta( 'wpseo_global_identifier_values', true ); } /** * * Merge the YOAST Fields with the Attribute Mapping available sources * * @param array $sources The current sources * @param string $attribute_id The Attribute ID * @return array The merged sources */ protected function load_yoast_seo_attribute_mapping_sources( array $sources, string $attribute_id ): array { if ( $attribute_id === GTIN::get_id() ) { return array_merge( self::get_yoast_seo_attribute_mapping_gtin_sources(), $sources ); } if ( $attribute_id === MPN::get_id() ) { return array_merge( self::get_yoast_seo_attribute_mapping_mpn_sources(), $sources ); } return $sources; } /** * Load the group disabled option for Attribute mapping YOAST SEO * * @return array The disabled group option */ protected function get_yoast_seo_attribute_mapping_group_source(): array { return [ 'disabled:' . self::VALUE_KEY => __( '- Yoast SEO -', 'google-listings-and-ads' ) ]; } /** * Load the GTIN Fields for Attribute mapping YOAST SEO * * @return array The GTIN sources */ protected function get_yoast_seo_attribute_mapping_gtin_sources(): array { return array_merge( self::get_yoast_seo_attribute_mapping_group_source(), [ self::VALUE_KEY . ':gtin' => __( 'GTIN Field', 'google-listings-and-ads' ) ] ); } /** * Load the MPN Fields for Attribute mapping YOAST SEO * * @return array The MPN sources */ protected function get_yoast_seo_attribute_mapping_mpn_sources(): array { return array_merge( self::get_yoast_seo_attribute_mapping_group_source(), [ self::VALUE_KEY . ':mpn' => __( 'MPN Field', 'google-listings-and-ads' ) ] ); } } PK!Q=src/Internal/DependencyManagement/AbstractServiceProvider.phpnu[provides ); } /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { foreach ( $this->provides as $class => $provided ) { $this->share( $class ); } } /** * Add an interface to the container. * * @param string $interface_name The interface to add. * @param string|null $concrete (Optional) The concrete class. * * @return DefinitionInterface */ protected function share_concrete( string $interface_name, $concrete = null ): DefinitionInterface { return $this->getContainer()->addShared( $interface_name, $concrete ); } /** * Share a class and add interfaces as tags. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @return DefinitionInterface */ protected function share_with_tags( string $class_name, ...$arguments ): DefinitionInterface { $definition = $this->share( $class_name, ...$arguments ); foreach ( class_implements( $class_name ) as $interface_name ) { $definition->addTag( $interface_name ); } return $definition; } /** * Share a class. * * Shared classes will always return the same instance of the class when the class is requested * from the container. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @return DefinitionInterface */ protected function share( string $class_name, ...$arguments ): DefinitionInterface { return $this->getContainer()->addShared( $class_name )->addArguments( $arguments ); } /** * Add a class. * * Classes will return a new instance of the class when the class is requested from the container. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @return DefinitionInterface */ protected function add( string $class_name, ...$arguments ): DefinitionInterface { return $this->getContainer()->add( $class_name )->addArguments( $arguments ); } /** * Maybe share a class and add interfaces as tags. * * This will also check any classes that implement the Conditional interface and only add them if * they are needed. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. */ protected function conditionally_share_with_tags( string $class_name, ...$arguments ) { $implements = class_implements( $class_name ); if ( array_key_exists( Conditional::class, $implements ) ) { /** @var Conditional $class */ if ( ! $class_name::is_needed() ) { return; } } $this->provides[ $class_name ] = true; $this->share_with_tags( $class_name, ...$arguments ); } } PK!N:src/Internal/DependencyManagement/AdminServiceProvider.phpnu[ true, AttributeMapping::class => true, BulkEditInitializer::class => true, ConnectionTest::class => true, CouponBulkEdit::class => true, Dashboard::class => true, GetStarted::class => true, MetaBoxInterface::class => true, MetaBoxInitializer::class => true, ProductFeed::class => true, Redirect::class => true, Reports::class => true, Settings::class => true, SetupAds::class => true, SetupMerchantCenter::class => true, Shipping::class => true, Service::class => true, ]; /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->share_with_tags( Admin::class, AssetsHandlerInterface::class, PHPViewFactory::class, MerchantCenterService::class, AdsService::class ); $this->share_with_tags( PHPViewFactory::class ); $this->share_with_tags( Redirect::class, WP::class ); // Share bulk edit views $this->share_with_tags( CouponBulkEdit::class, CouponMetaHandler::class, MerchantCenterService::class, TargetAudience::class ); $this->share_with_tags( BulkEditInitializer::class ); // Share admin meta boxes $this->share_with_tags( ChannelVisibilityMetaBox::class, Admin::class, ProductMetaHandler::class, ProductHelper::class, MerchantCenterService::class ); $this->share_with_tags( CouponChannelVisibilityMetaBox::class, Admin::class, CouponMetaHandler::class, CouponHelper::class, MerchantCenterService::class, TargetAudience::class ); $this->share_with_tags( MetaBoxInitializer::class, Admin::class, MetaBoxInterface::class ); $this->share_with_tags( ConnectionTest::class ); $this->share_with_tags( AttributeMapping::class ); $this->share_with_tags( Dashboard::class ); $this->share_with_tags( GetStarted::class ); $this->share_with_tags( ProductFeed::class ); $this->share_with_tags( Reports::class ); $this->share_with_tags( Settings::class ); $this->share_with_tags( SetupAds::class ); $this->share_with_tags( SetupMerchantCenter::class ); $this->share_with_tags( Shipping::class ); } } PK!e N N9src/Internal/DependencyManagement/CoreServiceProvider.phpnu[ true, AddressUtility::class => true, AssetsHandlerInterface::class => true, ContactInformationNote::class => true, CompleteSetupTask::class => true, CompleteSetupNote::class => true, CouponHelper::class => true, CouponMetaHandler::class => true, CouponSyncer::class => true, DateTimeUtility::class => true, EventTracking::class => true, GlobalSiteTag::class => true, ISOUtility::class => true, SiteVerificationEvents::class => true, OptionsInterface::class => true, TransientsInterface::class => true, ReconnectWordPressNote::class => true, ReviewAfterClicksNote::class => true, RESTControllers::class => true, Service::class => true, SetupCampaignNote::class => true, SetupCampaign2Note::class => true, SetupCouponSharingNote::class => true, TableManager::class => true, TrackerSnapshot::class => true, Tracks::class => true, TracksInterface::class => true, ProductSyncer::class => true, ProductHelper::class => true, ProductMetaHandler::class => true, SiteVerificationMeta::class => true, BatchProductHelper::class => true, ProductFilter::class => true, ProductRepository::class => true, ViewFactory::class => true, DebugLogger::class => true, MerchantStatuses::class => true, PhoneVerification::class => true, PolicyComplianceCheck::class => true, ContactInformation::class => true, MerchantCenterService::class => true, NotificationsService::class => true, TargetAudience::class => true, MerchantAccountState::class => true, AdsAccountState::class => true, DBInstaller::class => true, AttributeManager::class => true, ProductFactory::class => true, AttributesTab::class => true, VariationsAttributes::class => true, DeprecatedFilters::class => true, ZoneLocationsParser::class => true, ZoneMethodsParser::class => true, LocationRatesProcessor::class => true, ShippingZone::class => true, AdsAccountService::class => true, MerchantAccountService::class => true, MarketingChannelRegistrar::class => true, OAuthService::class => true, WPCLIMigrationGTIN::class => true, ]; /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->conditionally_share_with_tags( DebugLogger::class ); // Share our interfaces, possibly with concrete objects. $this->share_concrete( AssetsHandlerInterface::class, AssetsHandler::class ); $this->share_concrete( TransientsInterface::class, Transients::class ); $this->share_concrete( TracksInterface::class, $this->share_with_tags( Tracks::class, TracksProxy::class ) ); // Set up Options, and inflect classes that need options. $this->share_concrete( OptionsInterface::class, Options::class ); $this->getContainer() ->inflector( OptionsAwareInterface::class ) ->invokeMethod( 'set_options_object', [ OptionsInterface::class ] ); // Share helper classes, and inflect classes that need it. $this->share_with_tags( GoogleHelper::class, WC::class ); $this->getContainer() ->inflector( GoogleHelperAwareInterface::class ) ->invokeMethod( 'set_google_helper_object', [ GoogleHelper::class ] ); // Set up the TargetAudience service. $this->share_with_tags( TargetAudience::class, WC::class, OptionsInterface::class, GoogleHelper::class ); // Set up MerchantCenter service, and inflect classes that need it. $this->share_with_tags( MerchantCenterService::class ); // Set up Notifications service. $this->share_with_tags( NotificationsService::class, MerchantCenterService::class, AccountService::class ); // Set up OAuthService service. $this->share_with_tags( OAuthService::class ); $this->getContainer() ->inflector( MerchantCenterAwareInterface::class ) ->invokeMethod( 'set_merchant_center_object', [ MerchantCenterService::class ] ); // Set up Ads service, and inflect classes that need it. $this->share_with_tags( AdsAccountState::class ); $this->share_with_tags( AdsService::class, AdsAccountState::class ); $this->getContainer() ->inflector( AdsAwareInterface::class ) ->invokeMethod( 'set_ads_object', [ AdsService::class ] ); $this->share_with_tags( AssetSuggestionsService::class, WP::class, WC::class, ImageUtility::class, wpdb::class, AdsAssetGroupAsset::class ); // Set up the installer. $this->share_with_tags( Installer::class, WP::class ); // Share utility classes $this->share_with_tags( AddressUtility::class ); $this->share_with_tags( DateTimeUtility::class ); $this->share_with_tags( ImageUtility::class, WP::class ); $this->share_with_tags( ISOUtility::class, ISO3166DataProvider::class ); // Share our regular service classes. $this->share_with_tags( TrackerSnapshot::class ); $this->share_with_tags( EventTracking::class ); $this->share_with_tags( RESTControllers::class ); $this->share_with_tags( CompleteSetupTask::class ); $this->conditionally_share_with_tags( GlobalSiteTag::class, AssetsHandlerInterface::class, GoogleGtagJs::class, ProductHelper::class, WC::class, WP::class ); $this->share_with_tags( SiteVerificationMeta::class ); $this->conditionally_share_with_tags( MerchantSetupCompleted::class ); $this->conditionally_share_with_tags( AdsSetupCompleted::class ); $this->share_with_tags( AdsAccountService::class, AdsAccountState::class ); $this->share_with_tags( MerchantAccountService::class, MerchantAccountState::class ); // Inbox Notes $this->share_with_tags( ContactInformationNote::class ); $this->share_with_tags( CompleteSetupNote::class ); $this->share_with_tags( ReconnectWordPressNote::class, GoogleConnection::class ); $this->share_with_tags( ReviewAfterClicksNote::class, MerchantMetrics::class, WP::class ); $this->share_with_tags( ReviewAfterConversionsNote::class, MerchantMetrics::class, WP::class ); $this->share_with_tags( SetupCampaignNote::class, MerchantCenterService::class ); $this->share_with_tags( SetupCampaign2Note::class, MerchantCenterService::class ); $this->share_with_tags( SetupCouponSharingNote::class, MerchantStatuses::class ); $this->share_with_tags( NoteInitializer::class, ActionScheduler::class ); // Product attributes $this->conditionally_share_with_tags( AttributeManager::class, AttributeMappingRulesQuery::class, WC::class ); $this->conditionally_share_with_tags( AttributesTab::class, Admin::class, AttributeManager::class, MerchantCenterService::class ); $this->conditionally_share_with_tags( VariationsAttributes::class, Admin::class, AttributeManager::class, MerchantCenterService::class ); // Product Block Editor $this->share_with_tags( ChannelVisibilityBlock::class, ProductHelper::class, MerchantCenterService::class ); $this->conditionally_share_with_tags( ProductBlocksService::class, AssetsHandlerInterface::class, ChannelVisibilityBlock::class, AttributeManager::class, MerchantCenterService::class ); $this->share_with_tags( MerchantAccountState::class ); $this->share_with_tags( MerchantStatuses::class ); $this->share_with_tags( PhoneVerification::class, Merchant::class, WP::class, ISOUtility::class ); $this->share_with_tags( PolicyComplianceCheck::class, WC::class, GoogleHelper::class, TargetAudience::class ); $this->share_with_tags( ContactInformation::class, Merchant::class, GoogleSettings::class ); $this->share_with_tags( ProductMetaHandler::class ); $this->share( ProductHelper::class, ProductMetaHandler::class, WC::class, TargetAudience::class ); $this->share_with_tags( ProductFilter::class, ProductHelper::class ); $this->share_with_tags( ProductRepository::class, ProductMetaHandler::class, ProductFilter::class ); $this->share_with_tags( ProductFactory::class, AttributeManager::class, WC::class ); $this->share_with_tags( BatchProductHelper::class, ProductMetaHandler::class, ProductHelper::class, ValidatorInterface::class, ProductFactory::class, TargetAudience::class, AttributeMappingRulesQuery::class ); $this->share_with_tags( ProductSyncer::class, GoogleProductService::class, BatchProductHelper::class, ProductHelper::class, MerchantCenterService::class, WC::class, ProductRepository::class ); // Coupon management classes $this->share_with_tags( CouponMetaHandler::class ); $this->share_with_tags( CouponHelper::class, CouponMetaHandler::class, WC::class, MerchantCenterService::class ); $this->share_with_tags( CouponSyncer::class, GooglePromotionService::class, CouponHelper::class, ValidatorInterface::class, MerchantCenterService::class, TargetAudience::class, WC::class ); // Set up inflector for tracks classes. $this->getContainer() ->inflector( TracksAwareInterface::class ) ->invokeMethod( 'set_tracks', [ TracksInterface::class ] ); // Share other classes. $this->share_with_tags( ActivatedEvents::class, $_SERVER ); $this->share_with_tags( GenericEvents::class ); $this->share_with_tags( SiteClaimEvents::class ); $this->share_with_tags( SiteVerificationEvents::class ); $this->conditionally_share_with_tags( InstallTimestamp::class ); $this->conditionally_share_with_tags( ClearProductStatsCache::class, MerchantStatuses::class ); $this->share_with_tags( TableManager::class, 'db_table' ); $this->share_with_tags( DBInstaller::class, TableManager::class, Migrator::class ); $this->share_with_tags( DeprecatedFilters::class ); $this->share_with_tags( LocationRatesProcessor::class ); $this->share_with_tags( ZoneLocationsParser::class, GoogleHelper::class ); $this->share_with_tags( ZoneMethodsParser::class, WC::class ); $this->share_with_tags( ShippingZone::class, WC::class, ZoneLocationsParser::class, ZoneMethodsParser::class, LocationRatesProcessor::class ); $this->share_with_tags( ShippingSuggestionService::class, ShippingZone::class, WC::class ); $this->share_with_tags( RequestReviewStatuses::class ); // Share Attribute Mapping related classes $this->share_with_tags( AttributeMappingHelper::class ); if ( class_exists( MarketingChannels::class ) ) { $this->share_with_tags( GLAChannel::class, MerchantCenterService::class, AdsCampaign::class, Ads::class, MerchantStatuses::class, ProductSyncStats::class ); $this->share_with_tags( MarketingChannelRegistrar::class, GLAChannel::class, WC::class ); } // ClI Classes $this->conditionally_share_with_tags( WPCLIMigrationGTIN::class, ProductRepository::class, AttributeManager::class ); } } PK!7src/Internal/DependencyManagement/DBServiceProvider.phpnu[ true, AttributeMappingRulesQuery::class => true, ShippingRateTable::class => true, ShippingRateQuery::class => true, ShippingTimeTable::class => true, ShippingTimeQuery::class => true, BudgetRecommendationTable::class => true, BudgetRecommendationQuery::class => true, MerchantIssueTable::class => true, MerchantIssueQuery::class => true, ProductFeedQueryHelper::class => true, ProductMetaQueryHelper::class => true, MigrationInterface::class => true, Migrator::class => true, ]; /** * Returns a boolean if checking whether this provider provides a specific * service or returns an array of provided services if no argument passed. * * @param string $service * * @return boolean */ public function provides( string $service ): bool { return 'db_table' === $service || 'db_query' === $service || parent::provides( $service ); } /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->share_table_class( AttributeMappingRulesTable::class ); $this->add_query_class( AttributeMappingRulesQuery::class, AttributeMappingRulesTable::class ); $this->share_table_class( BudgetRecommendationTable::class ); $this->add_query_class( BudgetRecommendationQuery::class, BudgetRecommendationTable::class ); $this->share_table_class( ShippingRateTable::class ); $this->add_query_class( ShippingRateQuery::class, ShippingRateTable::class ); $this->share_table_class( ShippingTimeTable::class ); $this->add_query_class( ShippingTimeQuery::class, ShippingTimeTable::class ); $this->share_table_class( MerchantIssueTable::class ); $this->add_query_class( MerchantIssueQuery::class, MerchantIssueTable::class ); $this->share_with_tags( ProductFeedQueryHelper::class, wpdb::class, ProductRepository::class ); $this->share_with_tags( ProductMetaQueryHelper::class, wpdb::class ); // Share DB migrations $this->share_migration( MigrationVersion141::class, MerchantIssueTable::class ); $this->share_migration( Migration20211228T1640692399::class, ShippingRateTable::class, OptionsInterface::class ); $this->share_with_tags( Migration20220524T1653383133::class, BudgetRecommendationTable::class ); $this->share_migration( Migration20231109T1653383133::class, BudgetRecommendationTable::class ); $this->share_migration( Migration20240813T1653383133::class, ShippingTimeTable::class ); $this->share_with_tags( Migrator::class, MigrationInterface::class ); } /** * Add a query class. * * @param string $class_name * @param mixed ...$arguments * * @return DefinitionInterface */ protected function add_query_class( string $class_name, ...$arguments ): DefinitionInterface { return $this->add( $class_name, wpdb::class, ...$arguments )->addTag( 'db_query' ); } /** * Share a table class. * * Shared classes will always return the same instance of the class when the class is requested * from the container. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @return DefinitionInterface */ protected function share_table_class( string $class_name, ...$arguments ): DefinitionInterface { return parent::share( $class_name, WP::class, wpdb::class, ...$arguments )->addTag( 'db_table' ); } /** * Share a migration class. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @throws InvalidClass When the given class does not implement the MigrationInterface. * * @since 1.4.1 */ protected function share_migration( string $class_name, ...$arguments ) { $this->validate_interface( $class_name, MigrationInterface::class ); $this->share_with_tags( $class_name, wpdb::class, ...$arguments ); } } PK!a;;;src/Internal/DependencyManagement/GoogleServiceProvider.phpnu[ true, ShoppingContent::class => true, GoogleAdsClient::class => true, GuzzleClient::class => true, Middleware::class => true, Merchant::class => true, MerchantMetrics::class => true, Ads::class => true, AdsAssetGroup::class => true, AdsCampaign::class => true, AdsCampaignBudget::class => true, AdsCampaignLabel::class => true, AdsConversionAction::class => true, AdsReport::class => true, AdsAssetGroupAsset::class => true, AdsAsset::class => true, 'connect_server_root' => true, Connection::class => true, GoogleProductService::class => true, GooglePromotionService::class => true, SiteVerification::class => true, Settings::class => true, ]; /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->register_guzzle(); $this->register_ads_client(); $this->register_google_classes(); $this->share( Middleware::class ); $this->add( Connection::class ); $this->add( Settings::class ); $this->share( Ads::class, GoogleAdsClient::class ); $this->share( AdsAssetGroup::class, GoogleAdsClient::class, AdsAssetGroupAsset::class ); $this->share( AdsCampaign::class, GoogleAdsClient::class, AdsCampaignBudget::class, AdsCampaignCriterion::class, GoogleHelper::class, AdsCampaignLabel::class ); $this->share( AdsCampaignBudget::class, GoogleAdsClient::class ); $this->share( AdsAssetGroupAsset::class, GoogleAdsClient::class, AdsAsset::class ); $this->share( AdsAsset::class, GoogleAdsClient::class, WP::class ); $this->share( AdsCampaignCriterion::class ); $this->share( AdsCampaignLabel::class, GoogleAdsClient::class ); $this->share( AdsConversionAction::class, GoogleAdsClient::class ); $this->share( AdsReport::class, GoogleAdsClient::class ); $this->share( Merchant::class, ShoppingContent::class ); $this->share( MerchantMetrics::class, ShoppingContent::class, GoogleAdsClient::class, WP::class, TransientsInterface::class ); $this->share( MerchantReport::class, ShoppingContent::class, ProductHelper::class ); $this->share( SiteVerification::class ); $this->getContainer()->add( 'connect_server_root', $this->get_connect_server_url_root() ); } /** * Register guzzle with authorization middleware added. */ protected function register_guzzle() { $callback = function () { $handler_stack = HandlerStack::create(); $handler_stack->remove( 'http_errors' ); $handler_stack->push( $this->error_handler(), 'http_errors' ); $handler_stack->push( $this->add_auth_header(), 'auth_header' ); $handler_stack->push( $this->add_plugin_version_header(), 'plugin_version_header' ); // Override endpoint URL if we are using http locally. if ( 0 === strpos( $this->get_connect_server_url_root(), 'http://' ) ) { $handler_stack->push( $this->override_http_url(), 'override_http_url' ); } return new GuzzleClient( [ 'handler' => $handler_stack ] ); }; $this->share_concrete( GuzzleClient::class, new Definition( GuzzleClient::class, $callback ) ); $this->share_concrete( ClientInterface::class, new Definition( GuzzleClient::class, $callback ) ); } /** * Register ads client. */ protected function register_ads_client() { $callback = function () { return new GoogleAdsClient( $this->get_connect_server_endpoint() ); }; $this->share_concrete( GoogleAdsClient::class, new Definition( GoogleAdsClient::class, $callback ) )->addMethodCall( 'setHttpClient', [ ClientInterface::class ] ); } /** * Register the various Google classes we use. */ protected function register_google_classes() { $this->add( Client::class )->addMethodCall( 'setHttpClient', [ ClientInterface::class ] ); $this->add( ShoppingContent::class, Client::class, $this->get_connect_server_url_root( 'google/google-mc' ) ); $this->add( SiteVerificationService::class, Client::class, $this->get_connect_server_url_root( 'google/google-sv' ) ); $this->share( GoogleProductService::class, ShoppingContent::class ); $this->share( GooglePromotionService::class, ShoppingContent::class ); } /** * Custom error handler to detect and handle a disconnected status. * * @return callable */ protected function error_handler(): callable { return function ( callable $handler ) { return function ( RequestInterface $request, array $options ) use ( $handler ) { return $handler( $request, $options )->then( function ( ResponseInterface $response ) use ( $request ) { $code = $response->getStatusCode(); $path = $request->getUri()->getPath(); // Partial Failures come back with a status code of 200, so it's necessary to call GoogleAdsFailures:init every time. if ( strpos( $path, 'google-ads' ) !== false ) { GoogleAdsFailures::init(); } if ( $code < 400 ) { return $response; } if ( 401 === $code ) { $this->handle_unauthorized_error( $request, $response ); } throw RequestException::create( $request, $response ); } ); }; }; } /** * Handle a 401 unauthorized error. * Marks either the Jetpack or the Google account as disconnected. * * @since 1.12.5 * * @param RequestInterface $request * @param ResponseInterface $response * * @throws AccountReconnect When an account must be reconnected. */ protected function handle_unauthorized_error( RequestInterface $request, ResponseInterface $response ) { $auth_header = $response->getHeader( 'www-authenticate' )[0] ?? ''; if ( 0 === strpos( $auth_header, 'X_JP_Auth' ) ) { // Log original exception before throwing reconnect exception. do_action( 'woocommerce_gla_exception', RequestException::create( $request, $response ), __METHOD__ ); $this->set_jetpack_connected( false ); throw AccountReconnect::jetpack_disconnected(); } // Exclude listing customers as it will handle it's own unauthorized errors. $path = $request->getUri()->getPath(); if ( false === strpos( $path, 'customers:listAccessibleCustomers' ) ) { // Log original exception before throwing reconnect exception. do_action( 'woocommerce_gla_exception', RequestException::create( $request, $response ), __METHOD__ ); $this->set_google_disconnected(); throw AccountReconnect::google_disconnected(); } } /** * @return callable */ protected function add_auth_header(): callable { return function ( callable $handler ) { return function ( RequestInterface $request, array $options ) use ( $handler ) { try { $request = $request->withHeader( 'Authorization', $this->generate_auth_header() ); // Getting a valid authorization token, indicates Jetpack is connected. $this->set_jetpack_connected( true ); } catch ( WPError $error ) { do_action( 'woocommerce_gla_guzzle_client_exception', $error, __METHOD__ . ' in add_auth_header()' ); $this->set_jetpack_connected( false ); throw AccountReconnect::jetpack_disconnected(); } return $handler( $request, $options ); }; }; } /** * Add client name and version headers to request * * @since 2.4.11 * * @return callable */ protected function add_plugin_version_header(): callable { return function ( callable $handler ) { return function ( RequestInterface $request, array $options ) use ( $handler ) { $request = $request->withHeader( 'x-client-name', $this->get_client_name() ) ->withHeader( 'x-client-version', $this->get_version() ); return $handler( $request, $options ); }; }; } /** * @return callable */ protected function override_http_url(): callable { return function ( callable $handler ) { return function ( RequestInterface $request, array $options ) use ( $handler ) { $request = $request->withUri( $request->getUri()->withScheme( 'http' ) ); return $handler( $request, $options ); }; }; } /** * Generate the authorization header for the GuzzleClient and GoogleAdsClient. * * @return string Empty if no access token is available. * * @throws WPError If the authorization token isn't found. */ protected function generate_auth_header(): string { /** @var Manager $manager */ $manager = $this->getContainer()->get( Manager::class ); $token = $manager->get_tokens()->get_access_token( false, false, false ); $this->check_for_wp_error( $token ); [ $key, $secret ] = explode( '.', $token->secret ); $key = sprintf( '%s:%d:%d', $key, defined( 'JETPACK__API_VERSION' ) ? JETPACK__API_VERSION : 1, $token->external_user_id ); $timestamp = time() + (int) Jetpack_Options::get_option( 'time_diff' ); $nonce = wp_generate_password( 10, false ); $request = join( "\n", [ $key, $timestamp, $nonce, '' ] ); $signature = base64_encode( hash_hmac( 'sha1', $request, $secret, true ) ); $auth = [ 'token' => $key, 'timestamp' => $timestamp, 'nonce' => $nonce, 'signature' => $signature, ]; $pieces = [ 'X_JP_Auth' ]; foreach ( $auth as $key => $value ) { $pieces[] = sprintf( '%s="%s"', $key, $value ); } return join( ' ', $pieces ); } /** * Get the root Url for the connect server. * * @param string $path (Optional) A path relative to the root to include. * * @return string */ protected function get_connect_server_url_root( string $path = '' ): string { $url = trailingslashit( $this->get_connect_server_url() ); $path = trim( $path, '/' ); return "{$url}{$path}"; } /** * Get the connect server endpoint in the format `domain:port/path` * * @return string */ protected function get_connect_server_endpoint(): string { $parts = wp_parse_url( $this->get_connect_server_url_root( 'google/google-ads' ) ); $port = empty( $parts['port'] ) ? 443 : $parts['port']; return sprintf( '%s:%d%s', $parts['host'], $port, $parts['path'] ); } /** * Set the Google account connection as disconnected. */ protected function set_google_disconnected() { /** @var Options $options */ $options = $this->getContainer()->get( OptionsInterface::class ); $options->update( OptionsInterface::GOOGLE_CONNECTED, false ); } /** * Set the Jetpack account connection. * * @since 1.12.5 * * @param bool $connected Is connected or disconnected? */ protected function set_jetpack_connected( bool $connected ) { /** @var Options $options */ $options = $this->getContainer()->get( OptionsInterface::class ); // Save previous connected status before updating. $previous_connected = boolval( $options->get( OptionsInterface::JETPACK_CONNECTED ) ); $options->update( OptionsInterface::JETPACK_CONNECTED, $connected ); if ( $previous_connected !== $connected ) { $this->jetpack_connected_change( $connected ); } } /** * Handle the reconnect notification when the Jetpack connection status changes. * * @since 1.12.5 * * @param boolean $connected */ protected function jetpack_connected_change( bool $connected ) { /** @var ReconnectWordPress $note */ $note = $this->getContainer()->get( ReconnectWordPress::class ); if ( $connected ) { $note->delete(); } else { $note->get_entry()->save(); } } } PK!B>  @src/Internal/DependencyManagement/IntegrationServiceProvider.phpnu[ true, IntegrationInterface::class => true, IntegrationInitializer::class => true, ]; /** * @return void */ public function register(): void { $this->share_with_tags( YoastWooCommerceSeo::class ); $this->share_with_tags( WooCommerceBrands::class, WP::class ); $this->share_with_tags( WooCommerceProductBundles::class, AttributeManager::class ); $this->share_with_tags( WooCommercePreOrders::class, ProductHelper::class ); $this->conditionally_share_with_tags( JetpackWPCOM::class ); $this->share_with_tags( WPCOMProxy::class, ShippingTimeQuery::class, AttributeManager::class ); $this->share_with_tags( IntegrationInitializer::class, IntegrationInterface::class ); } } PK!y-(-(8src/Internal/DependencyManagement/JobServiceProvider.phpnu[ true, ActionSchedulerInterface::class => true, AsyncActionRunner::class => true, ActionSchedulerJobMonitor::class => true, Coupon\SyncerHooks::class => true, PluginUpdate::class => true, Product\SyncerHooks::class => true, ProductSyncStats::class => true, Service::class => true, JobRepository::class => true, ]; /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->share_with_tags( AsyncActionRunner::class, new QueueRunnerAsyncRequest( ActionSchedulerCore::store() ), ActionSchedulerCore::lock() ); $this->share_with_tags( ActionScheduler::class, AsyncActionRunner::class ); $this->share_with_tags( ActionSchedulerJobMonitor::class, ActionScheduler::class ); $this->share_with_tags( ProductSyncStats::class, ActionScheduler::class ); // share product syncer jobs $this->share_product_syncer_job( UpdateAllProducts::class ); $this->share_product_syncer_job( DeleteAllProducts::class ); $this->share_product_syncer_job( UpdateProducts::class ); $this->share_product_syncer_job( DeleteProducts::class ); $this->share_product_syncer_job( ResubmitExpiringProducts::class ); $this->share_product_syncer_job( CleanupProductsJob::class ); $this->share_product_syncer_job( CleanupSyncedProducts::class ); // share coupon syncer jobs. $this->share_coupon_syncer_job( UpdateCoupon::class ); $this->share_coupon_syncer_job( DeleteCoupon::class ); // share product notifications job $this->share_action_scheduler_job( ProductNotificationJob::class, NotificationsService::class, ProductHelper::class ); // share coupon notifications job $this->share_action_scheduler_job( CouponNotificationJob::class, NotificationsService::class, CouponHelper::class ); // share GTIN migration job $this->share_action_scheduler_job( MigrateGTIN::class, ProductRepository::class, Product\Attributes\AttributeManager::class ); $this->share_with_tags( JobRepository::class ); $this->conditionally_share_with_tags( JobInitializer::class, JobRepository::class, ActionScheduler::class ); $this->share_with_tags( Product\SyncerHooks::class, BatchProductHelper::class, ProductHelper::class, JobRepository::class, MerchantCenterService::class, NotificationsService::class, WC::class ); $this->share_with_tags( Coupon\SyncerHooks::class, CouponHelper::class, JobRepository::class, MerchantCenterService::class, NotificationsService::class, WC::class, WP::class ); $this->share_with_tags( StartProductSync::class, JobRepository::class ); $this->share_with_tags( PluginUpdate::class, JobRepository::class ); // Share shipping notifications job $this->share_action_scheduler_job( ShippingNotificationJob::class, NotificationsService::class ); // Share settings notifications job $this->share_action_scheduler_job( SettingsNotificationJob::class, NotificationsService::class ); // Share settings syncer hooks $this->share_with_tags( Settings\SyncerHooks::class, JobRepository::class, NotificationsService::class ); // Share shipping settings syncer job and hooks. $this->share_action_scheduler_job( UpdateShippingSettings::class, MerchantCenterService::class, GoogleSettings::class ); $this->share_with_tags( Shipping\SyncerHooks::class, MerchantCenterService::class, GoogleSettings::class, JobRepository::class, NotificationsService::class ); // Share plugin update jobs $this->share_product_syncer_job( CleanupProductTargetCountriesJob::class ); // Share update syncable products count job $this->share_action_scheduler_job( UpdateSyncableProductsCount::class, ProductRepository::class, ProductHelper::class ); $this->share_action_scheduler_job( UpdateMerchantProductStatuses::class, MerchantCenterService::class, MerchantReport::class, MerchantStatuses::class ); } /** * Share an ActionScheduler job class * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @throws InvalidClass When the given class does not implement the ActionSchedulerJobInterface. */ protected function share_action_scheduler_job( string $class_name, ...$arguments ) { $this->validate_interface( $class_name, ActionSchedulerJobInterface::class ); $this->share_with_tags( $class_name, ActionScheduler::class, ActionSchedulerJobMonitor::class, ...$arguments ); } /** * Share a product syncer job class * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. */ protected function share_product_syncer_job( string $class_name, ...$arguments ) { if ( is_subclass_of( $class_name, AbstractProductSyncerBatchedJob::class ) ) { $this->share_action_scheduler_job( $class_name, ProductSyncer::class, ProductRepository::class, BatchProductHelper::class, MerchantCenterService::class, MerchantStatuses::class, ...$arguments ); } else { $this->share_action_scheduler_job( $class_name, ProductSyncer::class, ProductRepository::class, MerchantCenterService::class, ...$arguments ); } } /** * Share a coupon syncer job class * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. */ protected function share_coupon_syncer_job( string $class_name, ...$arguments ) { $this->share_action_scheduler_job( $class_name, CouponHelper::class, CouponSyncer::class, WC::class, MerchantCenterService::class, ...$arguments ); } } PK!l:src/Internal/DependencyManagement/ProxyServiceProvider.phpnu[ true, Tracks::class => true, GoogleGtagJs::class => true, WP::class => true, Jetpack::class => true, WCProxy::class => true, ]; /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->share( RESTServer::class ); $this->share( Tracks::class ); $this->share( GoogleGtagJs::class ); $this->share( WP::class ); $this->share( Jetpack::class ); $this->share( WCProxy::class, WC()->countries ); // Use a wrapper function to get the wpdb object. $this->share_concrete( wpdb::class, new Definition( wpdb::class, function () { global $wpdb; return $wpdb; } ) ); } } PK!HIR . .9src/Internal/DependencyManagement/RESTServiceProvider.phpnu[container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $this->share( SettingsController::class, ShippingZone::class ); $this->share( ConnectionController::class ); $this->share( AdsAccountController::class, AdsAccountService::class ); $this->share( AdsCampaignController::class, AdsCampaign::class ); $this->share( AdsAssetGroupController::class, AdsAssetGroup::class ); $this->share( AdsReportsController::class ); $this->share( GoogleAccountController::class, Connection::class ); $this->share( JetpackAccountController::class, Manager::class, Middleware::class ); $this->share( MerchantCenterProductStatsController::class, MerchantStatuses::class, ProductSyncStats::class ); $this->share( MerchantCenterIssuesController::class, MerchantStatuses::class, ProductHelper::class ); $this->share( MerchantCenterProductFeedController::class, ProductFeedQueryHelper::class ); $this->share( MerchantCenterProductVisibilityController::class, ProductHelper::class, MerchantIssueQuery::class ); $this->share( MerchantCenterContactInformationController::class, ContactInformation::class, Settings::class, AddressUtility::class ); $this->share( AdsBudgetRecommendationController::class, BudgetRecommendationQuery::class, Ads::class ); $this->share( PhoneVerificationController::class, PhoneVerification::class ); $this->share( MerchantCenterAccountController::class, MerchantAccountService::class ); $this->share( MerchantCenterRequestReviewController::class, Middleware::class, Merchant::class, RequestReviewStatuses::class, TransientsInterface::class ); $this->share( MerchantCenterReportsController::class ); $this->share( ShippingRateBatchController::class, ShippingRateQuery::class ); $this->share( ShippingRateController::class, ShippingRateQuery::class ); $this->share( ShippingRateSuggestionsController::class, ShippingSuggestionService::class ); $this->share( ShippingTimeBatchController::class ); $this->share( ShippingTimeController::class ); $this->share( TargetAudienceController::class, WP::class, WC::class, ShippingZone::class, GoogleHelper::class ); $this->share( SupportedCountriesController::class, WC::class, GoogleHelper::class ); $this->share( SettingsSyncController::class, Settings::class ); $this->share( DisconnectController::class ); $this->share( SetupCompleteController::class, MerchantMetrics::class ); $this->share( AssetSuggestionsController::class, AdsAssetSuggestionsService::class ); $this->share( SyncableProductsCountController::class, JobRepository::class ); $this->share( PolicyComplianceCheckController::class, PolicyComplianceCheck::class ); $this->share( AttributeMappingDataController::class, AttributeMappingHelper::class ); $this->share( AttributeMappingRulesController::class, AttributeMappingHelper::class, AttributeMappingRulesQuery::class ); $this->share( AttributeMappingCategoriesController::class ); $this->share( AttributeMappingSyncerController::class, ProductSyncStats::class ); $this->share( TourController::class ); $this->share( RestAPIAuthController::class, OAuthService::class, MerchantAccountService::class ); $this->share( GTINMigrationController::class, JobRepository::class ); } /** * Share a class. * * Overridden to include the RESTServer proxy with all classes. * * @param string $class_name The class name to add. * @param mixed ...$arguments Constructor arguments for the class. * * @return DefinitionInterface */ protected function share( string $class_name, ...$arguments ): DefinitionInterface { return parent::share( $class_name, RESTServer::class, ...$arguments )->addTag( 'rest_controller' ); } } PK!s ?src/Internal/DependencyManagement/ThirdPartyServiceProvider.phpnu[ true, Manager::class => true, ISO3166DataProvider::class => true, ValidatorInterface::class => true, ]; /** * Use the register method to register items with the container via the * protected $this->container property or the `getContainer` method * from the ContainerAwareTrait. * * @return void */ public function register(): void { $jetpack_id = 'google-listings-and-ads'; $this->share( Manager::class )->addArgument( $jetpack_id ); $this->share( Config::class )->addMethodCall( 'ensure', [ 'connection', [ 'slug' => $jetpack_id, 'name' => 'Google for WooCommerce', // Use hardcoded name for initial registration. ], ] ); $this->share_concrete( ISO3166DataProvider::class, ISO3166::class ); $this->getContainer() ->inflector( ISO3166AwareInterface::class ) ->invokeMethod( 'set_iso3166_provider', [ ISO3166DataProvider::class ] ); $this->share_concrete( ValidatorInterface::class, function () { return Validation::createValidatorBuilder() ->addMethodMapping( 'load_validator_metadata' ) ->getValidator(); } ); // Update Jetpack connection with a translatable name, after init is called. add_action( 'init', function () { $manager = $this->getContainer()->get( Manager::class ); $manager->get_plugin()->add( __( 'Google for WooCommerce', 'google-listings-and-ads' ) ); } ); } } PK!u3src/Internal/Interfaces/ContainerAwareInterface.phpnu[validate_google_product_feed_inactive(); } catch ( ExtensionRequirementException $e ) { add_filter( 'woocommerce_gla_custom_merchant_issues', function ( $issues, $current_time ) { return $this->add_conflict_issue( $issues, $current_time ); }, 10, 2 ); add_action( 'deactivated_plugin', function ( $plugin ) { if ( 'woocommerce-product-feeds/woocommerce-gpf.php' === $plugin ) { /** @var MerchantStatuses $merchant_statuses */ $merchant_statuses = woogle_get_container()->get( MerchantStatuses::class ); if ( $merchant_statuses instanceof MerchantStatuses ) { $merchant_statuses->clear_cache(); } } } ); } return true; } /** * Validate that Google Product Feed isn't enabled. * * @throws ExtensionRequirementException When Google Product Feed is active. */ protected function validate_google_product_feed_inactive() { if ( defined( 'WOOCOMMERCE_GPF_VERSION' ) ) { throw ExtensionRequirementException::incompatible_plugin( 'Google Product Feed' ); } } /** * Add an account-level issue regarding the plugin conflict * to the array of issues to be saved in the database. * * @param array $issues The current array of account-level issues * @param DateTime $cache_created_time The time of the cache/issues generation. * * @return array The issues with the new conflict issue included */ protected function add_conflict_issue( array $issues, DateTime $cache_created_time ): array { $issues[] = [ 'product_id' => 0, 'product' => 'All products', 'code' => 'incompatible_google_product_feed', 'issue' => __( 'The Google Product Feed plugin may cause conflicts or unexpected results.', 'google-listings-and-ads' ), 'action' => __( 'Deactivate the Google Product Feed plugin from your store', 'google-listings-and-ads' ), 'action_url' => 'https://developers.google.com/shopping-content/guides/best-practices#do-not-use-api-and-feeds', 'created_at' => $cache_created_time->format( 'Y-m-d H:i:s' ), 'type' => MerchantStatuses::TYPE_ACCOUNT, 'severity' => 'error', 'source' => 'filter', ]; return $issues; } } PK! F-src/Internal/Requirements/PluginValidator.phpnu[validate() ) { self::$is_validated = false; } } return self::$is_validated; } } PK!_z1;src/Internal/Requirements/RequirementValidatorInterface.phpnu[' . PHP_EOL; echo '

' . esc_html( $e->get_formatted_message() ) . '

' . PHP_EOL; echo '' . PHP_EOL; } ); } } PK!]22.src/Internal/Requirements/VersionValidator.phpnu[validate_php_version(); $this->validate_php_architecture(); return true; } catch ( InvalidVersion $e ) { $this->add_admin_notice( $e ); return false; } } /** * Validate the PHP version being used. * * @throws InvalidVersion When the PHP version does not meet the minimum version. */ protected function validate_php_version() { if ( ! version_compare( PHP_VERSION, WC_GLA_MIN_PHP_VER, '>=' ) ) { throw InvalidVersion::from_requirement( 'PHP', PHP_VERSION, WC_GLA_MIN_PHP_VER ); } } /** * Validate the PHP Architecture being 64 Bits. * This is done by checking PHP_INT_SIZE. In 32 bits this will be 4 Bytes. In 64 Bits this will be 8 Bytes * * @see https://www.php.net/manual/en/language.types.integer.php * @since 2.3.9 * * @throws InvalidVersion When the PHP Architecture is not 64 Bits. */ protected function validate_php_architecture() { if ( PHP_INT_SIZE !== 8 ) { throw InvalidVersion::invalid_architecture(); } } } PK![)).src/Internal/Requirements/WCAdminValidator.phpnu[validate_wc_admin_active(); return true; } catch ( ExtensionRequirementException $e ) { $this->add_admin_notice( $e ); return false; } } /** * Validate that WooCommerce Admin is enabled. * * @throws ExtensionRequirementException When the WooCommerce Admin is disabled by hook. */ protected function validate_wc_admin_active() { if ( apply_filters( 'woocommerce_admin_disabled', false ) ) { throw ExtensionRequirementException::missing_required_plugin( 'WooCommerce Admin' ); } } } PK!1f<)src/Internal/Requirements/WCValidator.phpnu[validate_wc_version(); return true; } catch ( InvalidVersion $e ) { $this->add_admin_notice( $e ); return false; } } /** * Validate the minimum required WooCommerce version (after plugins are fully loaded). * * @throws InvalidVersion When the WooCommerce version does not meet the minimum version. */ protected function validate_wc_version() { if ( ! defined( 'WC_VERSION' ) ) { throw InvalidVersion::requirement_missing( 'WooCommerce', WC_GLA_MIN_WC_VER ); } if ( ! version_compare( WC_VERSION, WC_GLA_MIN_WC_VER, '>=' ) ) { throw InvalidVersion::from_requirement( 'WooCommerce', WC_VERSION, WC_GLA_MIN_WC_VER ); } } } PK!YPP$src/Internal/ContainerAwareTrait.phpnu[container = $container; } } PK!an"src/Internal/DeprecatedFilters.phpnu[ 'old'. * * @var array */ protected $deprecated_hooks = [ 'woocommerce_gla_enable_connection_test' => 'gla_enable_connection_test', 'woocommerce_gla_enable_debug_logging' => 'gla_enable_debug_logging', 'woocommerce_gla_enable_reports' => 'gla_enable_reports', ]; /** * Array of versions when each hook has been deprecated. * * @var array */ protected $deprecated_version = [ 'gla_enable_connection_test' => '1.0.1', 'gla_enable_debug_logging' => '1.0.1', 'gla_enable_reports' => '1.0.1', ]; /** * Hook into the new hook so we can handle deprecated hooks once fired. * * @param string $hook_name Hook name. */ public function hook_in( $hook_name ) { add_filter( $hook_name, [ $this, 'maybe_handle_deprecated_hook' ], -1000, 8 ); } /** * If the old hook is in-use, trigger it. * * @param string $new_hook New hook name. * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @param mixed $return_value Returned value. * @return mixed */ public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) { if ( has_filter( $old_hook ) ) { $this->display_notice( $old_hook, $new_hook ); $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); } return $return_value; } /** * Fire off a legacy hook with it's args. * * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @return mixed */ protected function trigger_hook( $old_hook, $new_callback_args ) { return apply_filters_ref_array( $old_hook, $new_callback_args ); } } PK!!src/Internal/InstallTimestamp.phpnu[options->add( OptionsInterface::INSTALL_TIMESTAMP, time() ); $this->options->add( OptionsInterface::INSTALL_VERSION, $this->get_version() ); } } PK!yVsrc/Internal/StatusMapping.phpnu[can_process( $item, $topic ) && $this->notifications_service->notify( $topic, $item, $data ) ) { $this->set_status( $item, $this->get_after_notification_status( $topic ) ); $this->handle_notified( $topic, $item ); } } catch ( InvalidValue $e ) { do_action( 'woocommerce_gla_error', sprintf( 'Error sending Notification for - Item ID: %s - Topic: %s - Data %s. Product was deleted from the database before the notification was sent.', $item, $topic, wp_json_encode( $data ) ), __METHOD__ ); } } /** * Set the notification status for the item. * * @param int $item_id * @param string $status * @throws InvalidValue If the given ID doesn't reference a valid product. */ protected function set_status( int $item_id, string $status ): void { $item = $this->get_item( $item_id ); $this->get_helper()->set_notification_status( $item, $status ); } /** * Get the Notification Status after the notification happens * * @param string $topic * @return string */ protected function get_after_notification_status( string $topic ): string { if ( $this->is_create_topic( $topic ) ) { return NotificationStatus::NOTIFICATION_CREATED; } elseif ( $this->is_delete_topic( $topic ) ) { return NotificationStatus::NOTIFICATION_DELETED; } else { return NotificationStatus::NOTIFICATION_UPDATED; } } /** * Checks if the item can be processed based on the topic. * This is needed because the item can change the Notification Status before * the Job process the item. * * @param int $item_id * @param string $topic * @throws InvalidValue If the given ID doesn't reference a valid product. * @return bool */ protected function can_process( int $item_id, string $topic ): bool { $item = $this->get_item( $item_id ); if ( $this->is_create_topic( $topic ) ) { return $this->get_helper()->should_trigger_create_notification( $item ); } elseif ( $this->is_delete_topic( $topic ) ) { return $this->get_helper()->should_trigger_delete_notification( $item ); } else { return $this->get_helper()->should_trigger_update_notification( $item ); } } /** * Handle the item after the notification. * * @param string $topic * @param int $item * @throws InvalidValue If the given ID doesn't reference a valid product. */ protected function handle_notified( string $topic, int $item ): void { if ( $this->is_delete_topic( $topic ) ) { $this->get_helper()->mark_as_unsynced( $this->get_item( $item ) ); } if ( $this->is_create_topic( $topic ) ) { $this->get_helper()->mark_as_notified( $this->get_item( $item ) ); } } /** * If a topic is a delete topic * * @param string $topic The topic to check * * @return bool */ protected function is_delete_topic( $topic ): bool { return str_contains( $topic, '.delete' ); } /** * If a topic is a create topic * * @param string $topic The topic to check * * @return bool */ protected function is_create_topic( $topic ): bool { return str_contains( $topic, '.create' ); } /** * Get the item * * @param int $item_id * @return \WC_Product|\WC_Coupon */ abstract protected function get_item( int $item_id ); /** * Get the helper * * @return HelperNotificationInterface */ abstract public function get_helper(): HelperNotificationInterface; } PK!  2src/Jobs/Notifications/AbstractNotificationJob.phpnu[notifications_service = $notifications_service; parent::__construct( $action_scheduler, $monitor ); } /** * Get the parent job name * * @return string */ public function get_name(): string { return 'notifications/' . $this->get_job_name(); } /** * Schedule the Job * * @param array $args */ public function schedule( array $args = [] ): void { if ( $this->can_schedule( [ $args ] ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $args ] ); } } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { /** * Allow users to disable the notification job schedule. * * @since 2.8.0 * * @param bool $value The current filter value. By default, it is the result of `$this->can_schedule` function. * @param string $job_name The current Job name. * @param array $args The arguments for the schedule function with the item id and the topic. */ return apply_filters( 'woocommerce_gla_notification_job_can_schedule', $this->notifications_service->is_ready() && parent::can_schedule( $args ), $this->get_job_name(), $args ); } /** * Get the child job name * * @return string */ abstract public function get_job_name(): string; /** * Logic when processing the items * * @param array $args */ abstract protected function process_items( array $args ): void; } PK!Lb0src/Jobs/Notifications/CouponNotificationJob.phpnu[helper = $coupon_helper; parent::__construct( $action_scheduler, $monitor, $notifications_service ); } /** * Get the coupon * * @param int $item_id * @return \WC_Coupon */ protected function get_item( int $item_id ) { return $this->helper->get_wc_coupon( $item_id ); } /** * Get the Coupon Helper * * @return HelperNotificationInterface */ public function get_helper(): HelperNotificationInterface { return $this->helper; } /** * Get the job name * * @return string */ public function get_job_name(): string { return 'coupons'; } } PK!hB& 6src/Jobs/Notifications/HelperNotificationInterface.phpnu[helper = $helper; parent::__construct( $action_scheduler, $monitor, $notifications_service ); } /** * Override Product Notification adding Offer ID for deletions. * The offer_id might match the real offer ID or not, depending on whether the product has been synced by us or not. * Google should check on their side if the product actually exists. * * @param array $args Arguments with the item id and the topic. */ protected function process_items( $args ): void { if ( isset( $args['topic'] ) && isset( $args['item_id'] ) && $this->is_delete_topic( $args['topic'] ) ) { $args['data'] = [ 'offer_id' => $this->helper->get_offer_id( $args['item_id'] ) ]; } parent::process_items( $args ); } /** * Get the product * * @param int $item_id * @throws InvalidValue If the given ID doesn't reference a valid product. * * @return \WC_Product */ protected function get_item( int $item_id ) { return $this->helper->get_wc_product( $item_id ); } /** * Get the Product Helper * * @return ProductHelper */ public function get_helper(): HelperNotificationInterface { return $this->helper; } /** * Get the job name * * @return string */ public function get_job_name(): string { return 'products'; } } PK!:(2src/Jobs/Notifications/SettingsNotificationJob.phpnu[notifications_service->notify( $this->notifications_service::TOPIC_SETTINGS_UPDATED ); } /** * Get the job name * * @return string */ public function get_job_name(): string { return 'settings'; } } PK!J2src/Jobs/Notifications/ShippingNotificationJob.phpnu[notifications_service->notify( $this->notifications_service::TOPIC_SHIPPING_UPDATED, null, $args ); } } PK!i+4src/Jobs/Update/CleanupProductTargetCountriesJob.phpnu[product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Process batch items. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. * * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler. */ protected function process_items( array $items ) { $products = $this->product_repository->find_by_ids( $items ); $stale_entries = $this->batch_product_helper->generate_stale_countries_request_entries( $products ); $this->product_syncer->delete_by_batch_requests( $stale_entries ); } } PK!o  src/Jobs/Update/PluginUpdate.phpnu[job_repository = $job_repository; } /** * Update Jobs that need to be run per version. * * @var array */ private $updates = [ '1.0.1' => [ CleanupProductTargetCountriesJob::class, UpdateAllProducts::class, ], '1.12.6' => [ UpdateAllProducts::class, ], ]; /** * Run installation logic for this class. * * @param string $old_version Previous version before updating. * @param string $new_version Current version after updating. */ public function install( string $old_version, string $new_version ): void { foreach ( $this->updates as $version => $jobs ) { if ( version_compare( $old_version, $version, '<' ) ) { $this->schedule_jobs( $jobs ); } } } /** * Schedules a list of jobs. * * @param array $jobs List of jobs */ protected function schedule_jobs( array $jobs ): void { foreach ( $jobs as $job ) { try { $this->job_repository->get( $job )->schedule(); } catch ( JobException $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); } } } } PK!QDD'src/Jobs/AbstractActionSchedulerJob.phpnu[action_scheduler = $action_scheduler; $this->monitor = $monitor; } /** * Init the batch schedule for the job. * * The job name is used to generate the schedule event name. */ public function init(): void { add_action( $this->get_process_item_hook(), [ $this, 'handle_process_items_action' ] ); } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return ! $this->is_running( $args ); } /** * Handles processing single item action hook. * * @hooked gla/jobs/{$job_name}/process_item * * @param array $items The job items from the current batch. * * @throws Exception If an error occurs. */ public function handle_process_items_action( array $items = [] ) { $process_hook = $this->get_process_item_hook(); $process_args = [ $items ]; $this->monitor->validate_failure_rate( $this, $process_hook, $process_args ); if ( $this->retry_on_timeout ) { $this->monitor->attach_timeout_monitor( $process_hook, $process_args ); } try { $this->process_items( $items ); } catch ( Exception $exception ) { // reschedule on failure $this->action_scheduler->schedule_immediate( $process_hook, $process_args ); // throw the exception again so that it can be logged throw $exception; } $this->monitor->detach_timeout_monitor( $process_hook, $process_args ); } /** * Check if this job is running. * * The job is considered to be running if the "process_item" action is currently pending or in-progress. * * @param array|null $args * * @return bool */ protected function is_running( ?array $args = [] ): bool { return $this->action_scheduler->has_scheduled_action( $this->get_process_item_hook(), $args ); } /** * Get the base name for the job's scheduled actions. * * @return string */ protected function get_hook_base_name(): string { return "{$this->get_slug()}/jobs/{$this->get_name()}/"; } /** * Get the hook name for the "process item" action. * * This method is required by the job monitor. * * @return string */ public function get_process_item_hook(): string { return "{$this->get_hook_base_name()}process_item"; } /** * Process batch items. * * @param array $items A single batch from the get_batch() method. * * @throws Exception If an error occurs. The exception will be logged by ActionScheduler. */ abstract protected function process_items( array $items ); } PK!uf.src/Jobs/AbstractBatchedActionSchedulerJob.phpnu[get_create_batch_hook(), [ $this, 'handle_create_batch_action' ] ); parent::init(); } /** * Get the hook name for the "create batch" action. * * @return string */ protected function get_create_batch_hook(): string { return "{$this->get_hook_base_name()}create_batch"; } /** * Enqueue the "create_batch" action provided it doesn't already exist. * * To minimize the resource use of starting the job the batch creation is handled async. * * @param array $args */ public function schedule( array $args = [] ) { $this->schedule_create_batch_action( 1 ); } /** * Handles batch creation action hook. * * @hooked gla/jobs/{$job_name}/create_batch * * Schedules an action to run immediately for the items in the batch. * * @param int $batch_number The batch number increments for each new batch in the job cycle. * * @throws Exception If an error occurs. * @throws JobException If the job failure rate is too high. */ public function handle_create_batch_action( int $batch_number ) { $create_batch_hook = $this->get_create_batch_hook(); $create_batch_args = [ $batch_number ]; $this->monitor->validate_failure_rate( $this, $create_batch_hook, $create_batch_args ); if ( $this->retry_on_timeout ) { $this->monitor->attach_timeout_monitor( $create_batch_hook, $create_batch_args ); } $items = $this->get_batch( $batch_number ); if ( empty( $items ) ) { // if no more items the job is complete $this->handle_complete( $batch_number ); } else { // if items, schedule the process action $this->schedule_process_action( $items ); // Add another "create_batch" action to handle unfiltered items. // The last batch created here will be an empty batch, it // will call "handle_complete" to finish the job. $this->schedule_create_batch_action( $batch_number + 1 ); } $this->monitor->detach_timeout_monitor( $create_batch_hook, $create_batch_args ); } /** * Get job batch size. * * @return int */ protected function get_batch_size(): int { /** * Filters the batch size for the job. * * @param string Job's name */ return apply_filters( 'woocommerce_gla_batched_job_size', 100, $this->get_name() ); } /** * Get the query offset based on a given batch number and the specified batch size. * * @param int $batch_number * * @return int */ protected function get_query_offset( int $batch_number ): int { return $this->get_batch_size() * ( $batch_number - 1 ); } /** * Schedule a new "create batch" action to run immediately. * * @param int $batch_number The batch number for the new batch. */ protected function schedule_create_batch_action( int $batch_number ) { if ( $this->can_schedule( [ $batch_number ] ) ) { $this->action_scheduler->schedule_immediate( $this->get_create_batch_hook(), [ $batch_number ] ); } } /** * Schedule a new "process" action to run immediately. * * @param int[] $items Array of item ids. */ protected function schedule_process_action( array $items ) { if ( ! $this->is_processing( $items ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $items ] ); } } /** * Check if this job is running. * * The job is considered to be running if a "create_batch" action is currently pending or in-progress. * * @param array|null $args * * @return bool */ protected function is_running( ?array $args = [] ): bool { return $this->action_scheduler->has_scheduled_action( $this->get_create_batch_hook(), $args ); } /** * Check if this job is processing the given items. * * The job is considered to be processing if a "process_item" action is currently pending or in-progress. * * @param array $items * * @return bool */ protected function is_processing( array $items = [] ): bool { return $this->action_scheduler->has_scheduled_action( $this->get_process_item_hook(), [ $items ] ); } /** * Called when the job is completed. * * @param int $final_batch_number The final batch number when the job was completed. * If equal to 1 then no items were processed by the job. */ protected function handle_complete( int $final_batch_number ) { // Optionally over-ride this method in child class. } /** * Get a single batch of items. * * If no items are returned the job will stop. * * @param int $batch_number The batch number increments for each new batch in the job cycle. * * @return array * * @throws Exception If an error occurs. The exception will be logged by ActionScheduler. */ abstract protected function get_batch( int $batch_number ): array; } PK!^ 8$src/Jobs/AbstractCouponSyncerJob.phpnu[coupon_helper = $coupon_helper; $this->coupon_syncer = $coupon_syncer; $this->wc = $wc; $this->merchant_center = $merchant_center; parent::__construct( $action_scheduler, $monitor ); } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return ! $this->is_running( $args ) && $this->merchant_center->should_push(); } } PK!K ,src/Jobs/AbstractProductSyncerBatchedJob.phpnu[batch_product_helper = $batch_product_helper; $this->product_syncer = $product_syncer; $this->product_repository = $product_repository; $this->merchant_center = $merchant_center; $this->merchant_statuses = $merchant_statuses; parent::__construct( $action_scheduler, $monitor ); } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return ! $this->is_running( $args ) && $this->merchant_center->should_push(); } } PK!KK%src/Jobs/AbstractProductSyncerJob.phpnu[product_syncer = $product_syncer; $this->product_repository = $product_repository; $this->merchant_center = $merchant_center; parent::__construct( $action_scheduler, $monitor ); } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return ! $this->is_running( $args ) && $this->merchant_center->should_push(); } } PK!TG(src/Jobs/ActionSchedulerJobInterface.phpnu[action_scheduler = $action_scheduler; } /** * Check whether the failure rate is above the specified threshold within the timeframe. * * To protect against failing jobs running forever the job's failure rate is checked before creating a new batch. * By default, a job is stopped if it has 5 failures in the last hour. * * @param ActionSchedulerJobInterface $job * @param string $hook The job action hook. * @param array|null $args The job arguments. * * @throws JobException If the job's error rate is above the threshold. */ public function validate_failure_rate( ActionSchedulerJobInterface $job, string $hook, ?array $args = null ) { if ( $this->is_failure_rate_above_threshold( $hook, $args ) ) { throw JobException::stopped_due_to_high_failure_rate( $job->get_name() ); } } /** * Reschedules the job if it has failed due to timeout. * * @param string $hook The job action hook. * @param array|null $args The job arguments. * * @since 1.7.0 */ public function attach_timeout_monitor( string $hook, ?array $args = null ) { $this->monitored_hooks[ self::get_job_hash( $hook, $args ) ] = true; add_action( 'action_scheduler_unexpected_shutdown', [ $this, 'reschedule_if_timeout' ], 10, 2 ); } /** * Detaches the timeout monitor that handles rescheduling jobs on timeout. * * @param string $hook The job action hook. * @param array|null $args The job arguments. * * @since 1.7.0 */ public function detach_timeout_monitor( string $hook, ?array $args = null ) { unset( $this->monitored_hooks[ self::get_job_hash( $hook, $args ) ] ); remove_action( 'action_scheduler_unexpected_shutdown', [ $this, 'reschedule_if_timeout' ] ); } /** * Reschedules an action if it has failed due to a timeout error. * * The number of previous failures will be checked before rescheduling the action, and it must be below the * specified threshold in `self::get_failure_rate_threshold` within the timeframe specified in * `self::get_failure_timeframe` for the action to be rescheduled. * * @param int $action_id * @param array $error * * @since 1.7.0 */ public function reschedule_if_timeout( $action_id, $error ) { if ( ! empty( $error ) && $this->is_timeout_error( $error ) ) { $action = $this->action_scheduler->fetch_action( $action_id ); $hook = $action->get_hook(); $args = $action->get_args(); // Confirm that the job is initiated by GLA and monitored by this instance. // The `self::attach_timeout_monitor` method will register the job's hook and arguments hash into the $monitored_hooks variable. if ( $this->get_slug() !== $action->get_group() || ! $this->is_monitored_for_timeout( $hook, $args ) ) { return; } // Check if the job has not failed more than the allowed threshold. if ( $this->is_failure_rate_above_threshold( $hook, $args ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'The %s job failed too many times, not rescheduling.', $hook ), __METHOD__ ); return; } do_action( 'woocommerce_gla_debug_message', sprintf( 'The %s job has failed due to a timeout error, rescheduling...', $hook ), __METHOD__ ); $this->action_scheduler->schedule_immediate( $hook, $args ); } } /** * Determines whether the given error is an execution "timeout" error. * * @param array $error An associative array describing the error with keys "type", "message", "file" and "line". * * @return bool * * @link https://www.php.net/manual/en/function.error-get-last.php * * @since 1.7.0 */ protected function is_timeout_error( array $error ): bool { return isset( $error['type'] ) && $error['type'] === E_ERROR && isset( $error['message'] ) && strpos( $error ['message'], 'Maximum execution time' ) !== false; } /** * Check whether the job's failure rate is above the specified threshold within the timeframe. * * @param string $hook The job action hook. * @param array|null $args The job arguments. * * @return bool True if the job's error rate is above the threshold, and false otherwise. * * @see ActionSchedulerJobMonitor::get_failure_rate_threshold() * @see ActionSchedulerJobMonitor::get_failure_timeframe() * * @since 1.7.0 */ protected function is_failure_rate_above_threshold( string $hook, ?array $args = null ): bool { $failed_actions = $this->action_scheduler->search( [ 'hook' => $hook, 'args' => $args, 'status' => $this->action_scheduler::STATUS_FAILED, 'per_page' => $this->get_failure_rate_threshold(), 'date' => gmdate( 'U' ) - $this->get_failure_timeframe(), 'date_compare' => '>', ], 'ids' ); return count( $failed_actions ) >= $this->get_failure_rate_threshold(); } /** * Get the job failure rate threshold (per timeframe). * * @return int */ protected function get_failure_rate_threshold(): int { return absint( apply_filters( 'woocommerce_gla_job_failure_rate_threshold', 3 ) ); } /** * Get the job failure timeframe (in seconds). * * @return int */ protected function get_failure_timeframe(): int { return absint( apply_filters( 'woocommerce_gla_job_failure_timeframe', 2 * HOUR_IN_SECONDS ) ); } /** * Generates a unique hash (checksum) for each job using its hook name and arguments. * * @param string $hook * @param array|null $args * * @return string * * @since 1.7.0 */ protected static function get_job_hash( string $hook, ?array $args = null ): string { return hash( 'crc32b', $hook . wp_json_encode( $args ) ); } /** * Determines whether the given set of job hook and arguments is monitored for timeout. * * @param string $hook * @param array|null $args * * @return bool * * @since 1.7.0 */ protected function is_monitored_for_timeout( string $hook, ?array $args = null ): bool { return isset( $this->monitored_hooks[ self::get_job_hash( $hook, $args ) ] ); } } PK!n/src/Jobs/BatchedActionSchedulerJobInterface.phpnu[product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Process batch items. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. * * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler. */ protected function process_items( array $items ) { $products = $this->product_repository->find_by_ids( $items ); $stale_entries = $this->batch_product_helper->generate_stale_products_request_entries( $products ); $this->product_syncer->delete_by_batch_requests( $stale_entries ); } } PK!ND"src/Jobs/CleanupSyncedProducts.phpnu[merchant_center->is_connected(); } /** * Get the name of the job. * * @return string */ public function get_name(): string { return 'cleanup_synced_products'; } /** * Can the job be scheduled. * Only cleanup when the Merchant Center is disconnected. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return ! $this->is_running( $args ) && ! $this->is_mc_connected(); } /** * Get a single batch of items. * * If no items are returned the job will stop. * * @param int $batch_number The batch number increments for each new batch in the job cycle. * * @return int[] */ public function get_batch( int $batch_number ): array { return $this->product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Process batch items. * Skips processing if the Merchant Center has been connected again. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. */ protected function process_items( array $items ) { if ( $this->is_mc_connected() ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Skipping cleanup of unsynced products because Merchant Center is connected: %s', implode( ',', $items ) ), __METHOD__ ); return; } $this->batch_product_helper->mark_batch_as_unsynced( $items ); } } PK!Lrttsrc/Jobs/DeleteAllProducts.phpnu[product_repository->find_synced_product_ids( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Process batch items. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. * * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler. */ protected function process_items( array $items ) { $products = $this->product_repository->find_by_ids( $items ); $product_entries = $this->batch_product_helper->generate_delete_request_entries( $products ); $this->product_syncer->delete_by_batch_requests( $product_entries ); } /** * Called when the job is completed. * * @since 2.6.4 * * @param int $final_batch_number The final batch number when the job was completed. * If equal to 1 then no items were processed by the job. */ protected function handle_complete( int $final_batch_number ) { $this->merchant_statuses->maybe_refresh_status_data( true ); } } PK!i )U U src/Jobs/DeleteCoupon.phpnu[coupon_syncer->delete( new DeleteCouponEntry( $wc_coupon_id, new GooglePromotion( $google_promotion ), $google_ids ) ); } /** * Schedule the job. * * @param array[] $args * * @throws JobException If no coupon is provided as argument. The exception will be logged by ActionScheduler. */ public function schedule( array $args = [] ) { $coupon_entry = $args[0] ?? null; if ( ! $coupon_entry instanceof DeleteCouponEntry ) { throw JobException::item_not_provided( 'DeleteCouponEntry for the coupon to delete' ); } if ( $this->can_schedule( [ $coupon_entry ] ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ [ $coupon_entry->get_wc_coupon_id(), $coupon_entry->get_google_promotion(), $coupon_entry->get_synced_google_ids(), ], ] ); } } /** * Get the name of an action hook to attach the job's start method to. * * @return StartHook */ public function get_start_hook(): StartHook { return new StartHook( "{$this->get_hook_base_name()}start" ); } } PK!  src/Jobs/DeleteProducts.phpnu[product_syncer->delete_by_batch_requests( $product_entries ); } /** * Schedule the job. * * @param array $args * * @throws JobException If no product is provided as argument. The exception will be logged by ActionScheduler. */ public function schedule( array $args = [] ) { $args = $args[0] ?? []; $id_map = ( new ProductIDMap( $args ) )->get(); if ( empty( $id_map ) ) { throw JobException::item_not_provided( 'Array of WooCommerce product IDs' ); } if ( did_action( 'woocommerce_gla_batch_retry_delete_products' ) ) { // Retry after one minute. $this->action_scheduler->schedule_single( gmdate( 'U' ) + 60, $this->get_process_item_hook(), [ $id_map ] ); } elseif ( $this->can_schedule( [ $id_map ] ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $id_map ] ); } } /** * Get an action hook to attach the job's start method to. * * @return StartHook */ public function get_start_hook(): StartHook { return new StartHook( 'woocommerce_gla_batch_retry_delete_products', 1 ); } } PK!3Jsrc/Jobs/JobException.phpnu[job_repository = $job_repository; $this->action_scheduler = $action_scheduler; } /** * Initialize all jobs. */ public function register(): void { foreach ( $this->job_repository->list() as $job ) { $job->init(); if ( $job instanceof StartOnHookInterface ) { add_action( $job->get_start_hook()->get_hook(), function ( ...$args ) use ( $job ) { $job->schedule( $args ); }, 10, $job->get_start_hook()->get_argument_count() ); } if ( $job instanceof RecurringJobInterface && ! $this->action_scheduler->has_scheduled_action( $job->get_start_hook()->get_hook() ) && $job->can_schedule() ) { $recurring_date_time = new DateTime( 'tomorrow 3am', wp_timezone() ); $schedule = '0 3 * * *'; // 3 am every day $this->action_scheduler->schedule_cron( $recurring_date_time->getTimestamp(), $schedule, $job->get_start_hook()->get_hook() ); } } } /** * Check whether this object is currently needed. * * @return bool Whether the object is needed. */ public static function is_needed(): bool { return ( defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) || ( defined( 'WP_CLI' ) && WP_CLI ) || is_admin() ); } } PK!msrc/Jobs/JobInterface.phpnu[container->get( JobInterface::class ) as $job ) { $this->jobs[ get_class( $job ) ] = $job; } return $this->jobs; } /** * Fetch job from Container (or cache if previously fetched). * * @param string $classname Job class name. * * @return JobInterface * * @throws JobException If the job is not found. */ public function get( string $classname ): JobInterface { if ( ! isset( $this->jobs[ $classname ] ) ) { try { $job = $this->container->get( $classname ); } catch ( Exception $e ) { throw JobException::job_does_not_exist( $classname ); } $classname = get_class( $job ); $this->jobs[ $classname ] = $job; } return $this->jobs[ $classname ]; } } PK!Ksrc/Jobs/MigrateGTIN.phpnu[product_repository = $product_repository; $this->attribute_manager = $attribute_manager; } /** * Get the name of the job. * * @return string */ public function get_name(): string { return 'migrate_gtin'; } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return ! parent::is_running( $args ) && $this->is_gtin_available_in_core(); } /** * Process batch items. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. */ protected function process_items( array $items ) { // update the product core GTIN using G4W GTIN $products = $this->product_repository->find_by_ids( $items ); foreach ( $products as $product ) { // process variations if ( $product instanceof \WC_Product_Variable ) { $variations = $product->get_children(); $this->process_items( $variations ); continue; } if ( $product->get_global_unique_id() ) { $this->debug( $this->error_gtin_already_set( $product ) ); continue; } $gtin = $this->get_gtin( $product ); if ( ! $gtin ) { $this->debug( $this->error_gtin_not_found( $product ) ); continue; } $gtin = $this->prepare_gtin( $gtin ); if ( ! is_numeric( $gtin ) ) { $this->debug( $this->error_gtin_invalid( $product, $gtin ) ); continue; } try { $product->set_global_unique_id( $gtin ); $product->save(); $this->debug( $this->successful_migrated_gtin( $product, $gtin ) ); } catch ( Exception $e ) { $this->debug( $this->error_gtin_not_saved( $product, $gtin, $e ) ); } } } /** * Tweak schedule function for adding a start flag. * * @param array $args */ public function schedule( array $args = [] ) { $this->options->update( OptionsInterface::GTIN_MIGRATION_STATUS, self::GTIN_MIGRATION_STARTED ); parent::schedule( $args ); } /** * * To run when the job is completed. * * @param int $final_batch_number */ public function handle_complete( int $final_batch_number ) { $this->options->update( OptionsInterface::GTIN_MIGRATION_STATUS, self::GTIN_MIGRATION_COMPLETED ); } /** * Get a single batch of items. * * If no items are returned the job will stop. * * @param int $batch_number The batch number increments for each new batch in the job cycle. * * @return array * * @throws Exception If an error occurs. The exception will be logged by ActionScheduler. */ protected function get_batch( int $batch_number ): array { return $this->product_repository->find_all_product_ids( $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Debug info in the logs. * * @param string $message * * @return void */ protected function debug( string $message ): void { do_action( 'woocommerce_gla_debug_message', $message, __METHOD__ ); } } PK!/OOsrc/Jobs/ProductSyncStats.phpnu[scheduler = $scheduler; } /** * Check if a job name is used for product syncing. * * @param string $hook * * @return bool */ protected function job_matches( string $hook ): bool { foreach ( self::MATCHES as $match ) { if ( false !== stripos( $hook, $match ) ) { return true; } } return false; } /** * Return the amount of product sync jobs which are pending. * * @return int */ public function get_count(): int { $count = 0; $scheduled = $this->scheduler->search( [ 'status' => 'pending', 'per_page' => -1, ] ); foreach ( $scheduled as $action ) { if ( $this->job_matches( $action->get_hook() ) ) { ++$count; } } return $count; } } PK!wޣ"src/Jobs/RecurringJobInterface.phpnu[product_repository->find_expiring_product_ids( $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Process batch items. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. * * @throws ProductSyncerException If an error occurs. The exception will be logged by ActionScheduler. */ protected function process_items( array $items ) { $products = $this->product_repository->find_by_ids( $items ); $this->product_syncer->update( $products ); } /** * Return the recurring job's interval in seconds. * * @return int */ public function get_interval(): int { return 24 * 60 * 60; // 24 hours } /** * Get the name of an action hook to attach the job's start method to. * * @return StartHook */ public function get_start_hook(): StartHook { return new StartHook( "{$this->get_hook_base_name()}start" ); } } PK!|src/Jobs/StartHook.phpnu[hook = $hook; $this->argument_count = $argument_count; } /** * @return string */ public function get_hook(): string { return $this->hook; } /** * @return int */ public function get_argument_count(): int { return $this->argument_count; } } PK!O!src/Jobs/StartOnHookInterface.phpnu[get_filtered_batch( $batch_number )->get(); } /** * Get a single filtered batch of items. * * If no items are returned the job will stop. * * @since 1.4.0 * * @param int $batch_number The batch number increments for each new batch in the job cycle. * * @return FilteredProductList */ protected function get_filtered_batch( int $batch_number ): FilteredProductList { return $this->product_repository->find_sync_ready_products( [], $this->get_batch_size(), $this->get_query_offset( $batch_number ) ); } /** * Handles batch creation action hook. * * @hooked gla/jobs/{$job_name}/create_batch * * Schedules an action to run immediately for the items in the batch. * Uses the unfiltered count to check if there are additional batches. * * @since 1.4.0 * * @param int $batch_number The batch number increments for each new batch in the job cycle. * * @throws Exception If an error occurs. * @throws JobException If the job failure rate is too high. */ public function handle_create_batch_action( int $batch_number ) { $create_batch_hook = $this->get_create_batch_hook(); $create_batch_args = [ $batch_number ]; $this->monitor->validate_failure_rate( $this, $create_batch_hook, $create_batch_args ); if ( $this->retry_on_timeout ) { $this->monitor->attach_timeout_monitor( $create_batch_hook, $create_batch_args ); } $items = $this->get_filtered_batch( $batch_number ); if ( 0 === $items->get_unfiltered_count() ) { // if no more items the job is complete $this->handle_complete( $batch_number ); } else { // if items, schedule the process action if ( count( $items ) ) { $this->schedule_process_action( $items->get_product_ids() ); } // Add another "create_batch" action to handle unfiltered items. // The last batch created here will be an empty batch, it // will call "handle_complete" to finish the job. $this->schedule_create_batch_action( $batch_number + 1 ); } $this->monitor->detach_timeout_monitor( $create_batch_hook, $create_batch_args ); } } PK!)FFsrc/Jobs/UpdateAllProducts.phpnu[product_repository->find_by_ids( $items ); $this->product_syncer->update( $products ); } /** * Schedules a delayed batched job * * @param int $delay The delay time in seconds */ public function schedule_delayed( int $delay ) { if ( $this->can_schedule( [ 1 ] ) ) { $this->action_scheduler->schedule_single( gmdate( 'U' ) + $delay, $this->get_create_batch_hook(), [ 1 ] ); } } /** * Called when the job is completed. * * @param int $final_batch_number The final batch number when the job was completed. * If equal to 1 then no items were processed by the job. */ protected function handle_complete( int $final_batch_number ) { $this->options->update( OptionsInterface::UPDATE_ALL_PRODUCTS_LAST_SYNC, strtotime( 'now' ) ); $this->merchant_statuses->maybe_refresh_status_data( true ); } } PK!dsrc/Jobs/UpdateCoupon.phpnu[wc->maybe_get_coupon( $coupon_id ); if ( $coupon instanceof WC_Coupon && $this->coupon_helper->is_sync_ready( $coupon ) ) { $this->coupon_syncer->update( $coupon ); } } } /** * Schedule the job. * * @param array[] $args * * @throws JobException If no coupon is provided as argument. The exception will be logged by ActionScheduler. */ public function schedule( array $args = [] ) { $args = $args[0] ?? null; $coupon_ids = array_filter( $args, 'is_integer' ); if ( empty( $coupon_ids ) ) { throw JobException::item_not_provided( 'WooCommerce Coupon IDs' ); } if ( $this->can_schedule( [ $coupon_ids ] ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $coupon_ids ] ); } } /** * Get the name of an action hook to attach the job's start method to. * * @return StartHook */ public function get_start_hook(): StartHook { return new StartHook( "{$this->get_hook_base_name()}start" ); } } PK!C*src/Jobs/UpdateMerchantProductStatuses.phpnu[merchant_center = $merchant_center; $this->merchant_report = $merchant_report; $this->merchant_statuses = $merchant_statuses; } /** * Get the name of the job. * * @return string */ public function get_name(): string { return 'update_merchant_product_statuses'; } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return parent::can_schedule( $args ) && $this->merchant_center->is_connected(); } /** * Process the job. * * @param int[] $items An array of job arguments. * * @throws JobException If the merchant product statuses cannot be retrieved.. */ public function process_items( array $items ) { try { $next_page_token = $items['next_page_token'] ?? null; // Clear the cache if we're starting from the beginning. if ( ! $next_page_token ) { $this->merchant_statuses->clear_product_statuses_cache_and_issues(); $this->merchant_statuses->refresh_account_and_presync_issues(); } $results = $this->merchant_report->get_product_view_report( $next_page_token ); $next_page_token = $results['next_page_token']; $this->merchant_statuses->process_product_statuses( $results['statuses'] ); if ( $next_page_token ) { $this->schedule( [ [ 'next_page_token' => $next_page_token ] ] ); } else { $this->merchant_statuses->handle_complete_mc_statuses_fetching(); } } catch ( Throwable $e ) { $this->merchant_statuses->handle_failed_mc_statuses_fetching( $e->getMessage() ); throw new JobException( 'Error updating merchant product statuses: ' . $e->getMessage() ); } } /** * Schedule the job. * * @param array $args - arguments. */ public function schedule( array $args = [] ) { if ( $this->can_schedule( $args ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), $args ); } } /** * The job is considered to be scheduled if the "process_item" action is currently pending or in-progress regardless of the arguments. * * @return bool */ public function is_scheduled(): bool { // We set 'args' to null so it matches any arguments. This is because it's possible to have multiple instances of the job running with different page tokens return $this->is_running( null ); } /** * Validate the failure rate of the job. * * @return string|void Returns an error message if the failure rate is too high, otherwise null. */ public function get_failure_rate_message() { try { $this->monitor->validate_failure_rate( $this, $this->get_process_item_hook() ); } catch ( JobException $e ) { return $e->getMessage(); } } } PK!4ͬsrc/Jobs/UpdateProducts.phpnu[ $product_ids ]; $products = $this->product_repository->find_sync_ready_products( $args )->get(); if ( empty( $products ) ) { throw JobException::item_not_found(); } $this->product_syncer->update( $products ); } /** * Schedule the job. * * @param array $args - arguments. * * @throws JobException If no product is provided as argument. The exception will be logged by ActionScheduler. */ public function schedule( array $args = [] ) { $args = $args[0] ?? []; $ids = array_filter( $args, 'is_integer' ); if ( empty( $ids ) ) { throw JobException::item_not_provided( 'Array of WooCommerce Product IDs' ); } if ( did_action( 'woocommerce_gla_batch_retry_update_products' ) ) { $this->action_scheduler->schedule_single( gmdate( 'U' ) + 60, $this->get_process_item_hook(), [ $ids ] ); } elseif ( $this->can_schedule( [ $ids ] ) ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook(), [ $ids ] ); } } /** * Get an action hook to attach the job's start method to. * * @return StartHook */ public function get_start_hook(): StartHook { return new StartHook( 'woocommerce_gla_batch_retry_update_products', 1 ); } } PK!f! ! #src/Jobs/UpdateShippingSettings.phpnu[merchant_center = $merchant_center; $this->google_settings = $google_settings; } /** * Get the name of the job. * * @return string */ public function get_name(): string { return 'update_shipping_settings'; } /** * Can the job be scheduled. * * @param array|null $args * * @return bool Returns true if the job can be scheduled. */ public function can_schedule( $args = [] ): bool { return parent::can_schedule( $args ) && $this->can_sync_shipping(); } /** * Process the job. * * @param int[] $items An array of job arguments. * * @throws JobException If the shipping settings cannot be synced. */ public function process_items( array $items ) { if ( ! $this->can_sync_shipping() ) { throw new JobException( 'Cannot sync shipping settings. Confirm that the merchant center account is connected and the option to automatically sync the shipping settings is selected.' ); } $this->google_settings->sync_shipping(); } /** * Schedule the job. * * @param array $args - arguments. */ public function schedule( array $args = [] ) { if ( $this->can_schedule() ) { $this->action_scheduler->schedule_immediate( $this->get_process_item_hook() ); } } /** * Can the WooCommerce shipping settings be synced to Google Merchant Center. * * @return bool */ protected function can_sync_shipping(): bool { // Confirm that the Merchant Center account is connected and the user has chosen for the shipping rates to be synced from WooCommerce settings. return $this->merchant_center->is_connected() && $this->google_settings->should_get_shipping_rates_from_woocommerce(); } } PK!q-ee(src/Jobs/UpdateSyncableProductsCount.phpnu[product_repository = $product_repository; $this->product_helper = $product_helper; } /** * Get the name of the job. * * @return string */ public function get_name(): string { return 'update_syncable_products_count'; } /** * Get job batch size. * * @return int */ protected function get_batch_size(): int { /** * Filters the batch size for the job. * * @param string Job's name */ return apply_filters( 'woocommerce_gla_batched_job_size', 500, $this->get_name() ); } /** * Process batch items. * * @param int[] $items A single batch of WooCommerce product IDs from the get_batch() method. */ protected function process_items( array $items ) { $product_ids = $this->options->get( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA ); if ( ! is_array( $product_ids ) ) { $product_ids = []; } $grouped_items = $this->product_helper->maybe_swap_for_parent_ids( $items ); $this->options->update( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA, array_unique( [ ...$product_ids, ...$grouped_items ] ) ); } /** * Called when the job is completed. * * @param int $final_batch_number The final batch number when the job was completed. * If equal to 1 then no items were processed by the job. */ protected function handle_complete( int $final_batch_number ) { $product_ids = $this->options->get( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA ); $count = is_array( $product_ids ) ? count( $product_ids ) : 0; $this->options->update( OptionsInterface::SYNCABLE_PRODUCTS_COUNT, $count ); $this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA ); } } PK!8 src/Logging/DebugLogger.phpnu[logger = wc_get_logger(); add_action( 'woocommerce_gla_debug_message', [ $this, 'log_message' ], 10, 2 ); add_action( 'woocommerce_gla_exception', [ $this, 'log_exception' ], 10, 2 ); add_action( 'woocommerce_gla_error', [ $this, 'log_error' ], 10, 2 ); add_action( 'woocommerce_gla_mc_client_exception', [ $this, 'log_exception' ], 10, 2 ); add_action( 'woocommerce_gla_ads_client_exception', [ $this, 'log_exception' ], 10, 2 ); add_action( 'woocommerce_gla_sv_client_exception', [ $this, 'log_exception' ], 10, 2 ); add_action( 'woocommerce_gla_guzzle_client_exception', [ $this, 'log_exception' ], 10, 2 ); add_action( 'woocommerce_gla_guzzle_invalid_response', [ $this, 'log_response' ], 10, 2 ); } } /** * Log an exception. * * @param Exception $exception * @param string $method */ public function log_exception( $exception, string $method ): void { $this->log( $exception->getMessage(), $method, WC_Log_Levels::ERROR ); } /** * Log an exception. * * @param string $message * @param string $method */ public function log_error( string $message, string $method ): void { $this->log( $message, $method, WC_Log_Levels::ERROR ); } /** * Log a JSON response. * * @param mixed $response * @param string $method */ public function log_response( $response, string $method ): void { $message = wp_json_encode( $response, JSON_PRETTY_PRINT ); $this->log( $message, $method ); } /** * Log a generic note. * * @param string $message * @param string $method */ public function log_message( string $message, string $method ): void { $this->log( $message, $method ); } /** * Log a message as a debug log entry. * * @param string $message * @param string $method * @param string $level */ protected function log( string $message, string $method, string $level = WC_Log_Levels::DEBUG ) { $this->logger->log( $level, sprintf( '%s %s', $method, $message ), [ 'source' => 'google-listings-and-ads', ] ); } } PK!dd##src/Menu/AttributeMapping.phpnu[ __( 'Attribute Mapping', 'google-listings-and-ads' ), 'parent' => 'google-listings-and-ads-category', 'path' => '/google/attribute-mapping', 'id' => 'google-attribute-mapping', ] ); } ); } } PK!/src/Menu/Dashboard.phpnu[merchant_center->is_setup_complete() ) { return; } add_action( 'admin_menu', function () { $this->register_classic_submenu_page( [ 'id' => 'google-listings-and-ads', 'title' => __( 'Google for WooCommerce', 'google-listings-and-ads' ), 'parent' => 'woocommerce-marketing', 'path' => self::PATH, ] ); } ); } } PK!:src/Menu/GetStarted.phpnu[merchant_center->is_setup_complete() ) { return; } add_action( 'admin_menu', function () { $this->register_classic_submenu_page( [ 'id' => 'google-listings-and-ads', 'title' => __( 'Google for WooCommerce', 'google-listings-and-ads' ), 'parent' => 'woocommerce-marketing', 'path' => self::PATH, ] ); } ); } } PK!  src/Menu/MenuFixesTrait.phpnu[register_page()` called by `wc_admin_register_page` replaces the * menu/submenu slug internally. * - Coupons submenu is added by `register_post_type` that calls `add_submenu_page` * directly in WP core and is moved to Marketing dynamically. * * Details: * * There is a guide with a few examples showing how to add a page to WooCommerce Admin. * * @link https://developer.woocommerce.com/extension-developer-guide/working-with-woocommerce-admin-pages/ * * Originally, a React-powered page is expected to be registered by WC core function * `wc_admin_register_page`, and the function also handles the registration of wp-admin * menu and submenu via PageController. In addition, the function will concatenate * 'wc-admin&path=' with the page path as the menu/submenu slug when calling `add_menu_page` * or `add_submenu_page`. * * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Admin/PageController.php#L449-L451 * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Admin/PageController.php#L458 * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Admin/PageController.php#L467 * * However, the main menu of Marketing is not registered in that way but calls WP core * function `add_menu_page` directly with 'woocommerce-marketing' as its menu slug, * so the menu slug of Marketing won't have the above replacement processing. * This causes other pages that need to be submenus under Marketing won't appear * if they were added by `wc_admin_register_page` due to mismatched slugs. * Instead, they have to be added via "woocommerce_marketing_menu_items" filter. * * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Internal/Admin/Marketing.php#L71-L92 * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Internal/Admin/Marketing.php#L106-L119 * * Unfortunately, `wc_admin_register_page` doesn't pass the `position` option to * `add_submenu_page`. So the order of submenus is determined with the calling order * of `wc_admin_register_page`, which usually is decided by the `priority` parameter * of the corresponding "admin_menu" action. Even though raising the priority of * "admin_menu" action to 6 or a smaller number could make submenu appear but it will * still be above the Coupons or even the Overview submenu. As mentioned at the beginning, * specifying the `position` won't work either because it won't be passed to `add_submenu_page`. * * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Admin/PageController.php#L466-L473 * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Internal/Admin/Marketing.php#L60 * @link https://github.com/WordPress/wordpress-develop/blob/6.0.3/src/wp-admin/includes/plugin.php#L1445-L1449 * * About the Coupons submenu, it's added by `register_post_type` in an "init" action, * and its appearing menu might be modified to Marketing dynamically, then WP core calls * `add_submenu_page` directly via "admin_menu" action with the default priority 10. * * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/includes/class-wc-post-types.php#L451-L491 * @link https://github.com/woocommerce/woocommerce/blob/7.0.0/plugins/woocommerce/src/Internal/Admin/Coupons.php#L95-L98 * @link https://github.com/WordPress/wordpress-develop/blob/6.0.3/src/wp-includes/post.php#L2067-L2076 * @link https://github.com/WordPress/wordpress-develop/blob/6.0.3/src/wp-includes/default-filters.php#L537 * * Taken together, when using the suggested `wc_admin_register_page` to add a submenu * under Marketing menu, if the priority of "admin_menu" action is > 6, it won't appear. * If the priority is <= 6, the order of added submenu will be above the Coupons. * When using the dedicated "woocommerce_marketing_menu_items" filter, the order of added * submenu will still be above the Coupons. * * In summary, the order in which submenus call `add_submenu_page` determines the order * in which they appear in the Marketing menu, and the way in which submenus call * `add_submenu_page` and whether they are called before the Marketing menu calls * `add_menu_page` determines whether the submenus can match the parent slug to appear * under Marketing menu. * * The method and order of calling is as follows: * 1. Overview submenu: PageController->register_page() with priority 5. * 2. Marketing menu: add_menu_page() with priority 6. * 3. Coupons submenu: add_submenu_page() with the default priority 10. * 4. This workaround will be this order if we add a submenu by "admin_menu" * action with a priority >= 10. Moreover, the `position` will be effective * to change the final ordering. * * @param array $options { * Array describing the submenu page. * * @type string id ID to reference the page. * @type string title Page title. Used in menus and breadcrumbs. * @type string parent Parent ID. * @type string path Path for this page. * @type string capability Capability needed to access the page. * @type int position|null Menu item position. * } */ protected function register_classic_submenu_page( $options ) { $defaults = [ 'capability' => 'manage_woocommerce', 'position' => null, ]; $options = wp_parse_args( $options, $defaults ); $options['js_page'] = true; if ( 0 !== strpos( $options['path'], PageController::PAGE_ROOT ) ) { $options['path'] = PageController::PAGE_ROOT . '&path=' . $options['path']; } add_submenu_page( $options['parent'], $options['title'], $options['title'], $options['capability'], $options['path'], [ PageController::class, 'page_wrapper' ], $options['position'], ); PageController::get_instance()->connect_page( $options ); } } PK!R  src/Menu/ProductFeed.phpnu[ __( 'Product Feed', 'google-listings-and-ads' ), 'parent' => 'google-listings-and-ads-category', 'path' => '/google/product-feed', 'id' => 'google-product-feed', ] ); } ); } } PK! Isrc/Menu/Reports.phpnu[ __( 'Reports', 'google-listings-and-ads' ), 'parent' => 'google-listings-and-ads-category', 'path' => '/google/reports', 'id' => 'google-reports', ] ); } ); } } PK!M src/Menu/Settings.phpnu[ __( 'Settings', 'google-listings-and-ads' ), 'parent' => 'google-listings-and-ads-category', 'path' => '/google/settings', 'id' => 'google-settings', ] ); } ); } } PK!src/Menu/SetupAds.phpnu[ __( 'Ads Setup Wizard', 'google-listings-and-ads' ), 'parent' => '', 'path' => '/google/setup-ads', 'id' => 'google-setup-ads', ] ); } ); } } PK!\xz src/Menu/SetupMerchantCenter.phpnu[ __( 'MC Setup Wizard', 'google-listings-and-ads' ), 'parent' => '', 'path' => '/google/setup-mc', 'id' => 'google-setup-mc', ] ); } ); } } PK!1=src/Menu/Shipping.phpnu[ 'google-shipping', 'parent' => 'google-listings-and-ads-category', 'title' => __( 'Shipping', 'google-listings-and-ads' ), 'path' => '/google/shipping', ] ); } ); } } PK!jW[[%src/MerchantCenter/AccountService.phpnu[state = $state; } /** * Get all Merchant Accounts associated with the connected account. * * @return array * @throws Exception When an API error occurs. */ public function get_accounts(): array { return $this->container->get( Middleware::class )->get_merchant_accounts(); } /** * Use an existing MC account. Mark the 'set_id' step as done, update the MC account's website URL, * and sets the Merchant ID. * * @param int $account_id The merchant ID to use. * * @throws ExceptionWithResponseData If there's a website URL conflict, or account data can't be retrieved. */ public function use_existing_account_id( int $account_id ): void { // Reset the process if the provided ID isn't the same as the one stored in options. $merchant_id = $this->options->get_merchant_id(); if ( $merchant_id && $merchant_id !== $account_id ) { $this->reset_account_setup(); } $state = $this->state->get(); // Don't do anything if this step was already finished. if ( MerchantAccountState::STEP_DONE === $state['set_id']['status'] ) { return; } try { // Make sure the existing account has the correct website URL (or fail). $this->maybe_add_merchant_center_url( $account_id ); // Re-fetch state as it might have changed. $state = $this->state->get(); $middleware = $this->container->get( Middleware::class ); // Maybe the existing account is a sub-account! $state['set_id']['data']['from_mca'] = false; foreach ( $middleware->get_merchant_accounts() as $existing_account ) { if ( $existing_account['id'] === $account_id ) { $state['set_id']['data']['from_mca'] = $existing_account['subaccount']; break; } } $middleware->link_merchant_account( $account_id ); $state['set_id']['status'] = MerchantAccountState::STEP_DONE; $this->state->update( $state ); } catch ( ExceptionWithResponseData $e ) { throw $e; } catch ( Exception $e ) { throw $this->prepare_exception( $e->getMessage(), [], $e->getCode() ); } } /** * Run the process for setting up a Merchant Center account (sub-account or standalone). * * @param int $account_id * * @return array The account ID if setup has completed. * @throws ExceptionWithResponseData When the account is already connected or a setup error occurs. */ public function setup_account( int $account_id ) { // Reset the process if the provided ID isn't the same as the one stored in options. $merchant_id = $this->options->get_merchant_id(); if ( $merchant_id && $merchant_id !== $account_id ) { $this->reset_account_setup(); } try { return $this->setup_account_steps(); } catch ( ExceptionWithResponseData | ApiNotReady $e ) { throw $e; } catch ( Exception $e ) { throw $this->prepare_exception( $e->getMessage(), [], $e->getCode() ); } } /** * Create or link an account, switching the URL during the set_id step. * * @param int $account_id * * @return array * @throws ExceptionWithResponseData When a setup error occurs. */ public function switch_url( int $account_id ): array { $state = $this->state->get(); $switch_necessary = ! empty( $state['set_id']['data']['old_url'] ); $set_id_status = $state['set_id']['status'] ?? MerchantAccountState::STEP_PENDING; if ( ! $account_id || MerchantAccountState::STEP_DONE === $set_id_status || ! $switch_necessary ) { throw $this->prepare_exception( __( 'Attempting invalid URL switch.', 'google-listings-and-ads' ) ); } $this->allow_switch_url = true; $this->use_existing_account_id( $account_id ); return $this->setup_account( $account_id ); } /** * Create or link an account, overwriting the website claim during the claim step. * * @param int $account_id * * @return array * @throws ExceptionWithResponseData When a setup error occurs. */ public function overwrite_claim( int $account_id ): array { $state = $this->state->get( false ); $overwrite_necessary = ! empty( $state['claim']['data']['overwrite_required'] ); $claim_status = $state['claim']['status'] ?? MerchantAccountState::STEP_PENDING; if ( MerchantAccountState::STEP_DONE === $claim_status || ! $overwrite_necessary ) { throw $this->prepare_exception( __( 'Attempting invalid claim overwrite.', 'google-listings-and-ads' ) ); } $this->overwrite_claim = true; return $this->setup_account( $account_id ); } /** * Get the connected merchant account. * * @return array */ public function get_connected_status(): array { /** @var NotificationsService $notifications_service */ $notifications_service = $this->container->get( NotificationsService::class ); $id = $this->options->get_merchant_id(); $wpcom_rest_api_status = $this->options->get( OptionsInterface::WPCOM_REST_API_STATUS ); // If token is revoked outside the extension. Set the status as error to force the merchant to grant access again. if ( $wpcom_rest_api_status === 'approved' && ! $this->is_wpcom_api_status_healthy() ) { $wpcom_rest_api_status = OAuthService::STATUS_ERROR; $this->options->update( OptionsInterface::WPCOM_REST_API_STATUS, $wpcom_rest_api_status ); } $status = [ 'id' => $id, 'status' => $id ? 'connected' : 'disconnected', 'notification_service_enabled' => $notifications_service->is_enabled(), 'wpcom_rest_api_status' => $wpcom_rest_api_status, ]; $incomplete = $this->state->last_incomplete_step(); if ( ! empty( $incomplete ) ) { $status['status'] = 'incomplete'; $status['step'] = $incomplete; } return $status; } /** * Return the setup status to determine what step to continue at. * * @return array */ public function get_setup_status(): array { return $this->container->get( MerchantCenterService::class )->get_setup_status(); } /** * Disconnect Merchant Center account */ public function disconnect() { $this->options->delete( OptionsInterface::CONTACT_INFO_SETUP ); $this->options->delete( OptionsInterface::MC_SETUP_COMPLETED_AT ); $this->options->delete( OptionsInterface::MERCHANT_ACCOUNT_STATE ); $this->options->delete( OptionsInterface::MERCHANT_CENTER ); $this->options->delete( OptionsInterface::SITE_VERIFICATION ); $this->options->delete( OptionsInterface::TARGET_AUDIENCE ); $this->options->delete( OptionsInterface::MERCHANT_ID ); $this->options->delete( OptionsInterface::CLAIMED_URL_HASH ); $this->container->get( MerchantStatuses::class )->delete(); $this->container->get( MerchantIssueTable::class )->truncate(); $this->container->get( ShippingRateTable::class )->truncate(); $this->container->get( ShippingTimeTable::class )->truncate(); $this->container->get( JobRepository::class )->get( CleanupSyncedProducts::class )->schedule(); $this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_ACCOUNT_REVIEW ); $this->container->get( TransientsInterface::class )->delete( TransientsInterface::URL_MATCHES ); $this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_IS_SUBACCOUNT ); } /** * Performs the steps necessary to initialize a Merchant Center account. * Should always resume up at the last pending or unfinished step. If the Merchant Center account * has already been created, the ID is simply returned. * * @return array The newly created (or pre-existing) Merchant account data. * @throws ExceptionWithResponseData If an error occurs during any step. * @throws Exception If the step is unknown. * @throws ApiNotReady If we should wait to complete the next step. */ private function setup_account_steps() { $state = $this->state->get(); $merchant_id = $this->options->get_merchant_id(); $merchant = $this->container->get( Merchant::class ); $middleware = $this->container->get( Middleware::class ); foreach ( $state as $name => &$step ) { if ( MerchantAccountState::STEP_DONE === $step['status'] ) { continue; } if ( 'link' === $name ) { $time_to_wait = $this->state->get_seconds_to_wait_after_created(); if ( $time_to_wait ) { sleep( $time_to_wait ); } } try { switch ( $name ) { case 'set_id': // Just in case, don't create another merchant ID. if ( ! empty( $merchant_id ) ) { break; } $merchant_id = $middleware->create_merchant_account(); $step['data']['from_mca'] = true; $step['data']['created_timestamp'] = time(); break; case 'verify': // Skip if previously verified. if ( $this->state->is_site_verified() ) { break; } $site_url = esc_url_raw( $this->get_site_url() ); $this->container->get( SiteVerification::class )->verify_site( $site_url ); break; case 'link': $middleware->link_merchant_to_mca(); break; case 'claim': // At this step, the website URL is assumed to be correct. // If the URL is already claimed, no claim should be attempted. if ( $merchant->get_accountstatus( $merchant_id )->getWebsiteClaimed() ) { break; } if ( $this->overwrite_claim ) { $middleware->claim_merchant_website( true ); } else { $merchant->claimwebsite(); } break; case 'link_ads': // Continue to next step if Ads account is not connected yet. if ( ! $this->options->get_ads_id() ) { // Save step as pending and continue the foreach loop with `continue 2`. $state[ $name ]['status'] = MerchantAccountState::STEP_PENDING; $this->state->update( $state ); continue 2; } $this->link_ads_account(); break; default: throw new Exception( sprintf( /* translators: 1: is a string representing an unknown step name */ __( 'Unknown merchant account creation step %1$s', 'google-listings-and-ads' ), $name ) ); } $step['status'] = MerchantAccountState::STEP_DONE; $step['message'] = ''; $this->state->update( $state ); } catch ( Exception $e ) { $step['status'] = MerchantAccountState::STEP_ERROR; $step['message'] = $e->getMessage(); // URL already claimed. if ( 'claim' === $name && 403 === $e->getCode() ) { $data = [ 'id' => $merchant_id, 'website_url' => $this->strip_url_protocol( esc_url_raw( $this->get_site_url() ) ), ]; // Sub-account: request overwrite confirmation. if ( $state['set_id']['data']['from_mca'] ?? true ) { do_action( 'woocommerce_gla_site_claim_overwrite_required', [] ); $step['data']['overwrite_required'] = true; $e = $this->prepare_exception( $e->getMessage(), $data, $e->getCode() ); } else { do_action( 'woocommerce_gla_site_claim_failure', [ 'details' => 'independent_account' ] ); // Independent account: overwrite not possible. $e = $this->prepare_exception( __( 'Unable to claim website URL with this Merchant Center Account.', 'google-listings-and-ads' ), $data, 406 ); } } elseif ( 'link' === $name && 401 === $e->getCode() ) { // New sub-account not yet manipulable. $state['set_id']['data']['created_timestamp'] = time(); $e = ApiNotReady::retry_after( MerchantAccountState::MC_DELAY_AFTER_CREATE ); } $this->state->update( $state ); throw $e; } } return [ 'id' => $merchant_id ]; } /** * Restart the account setup when we are connecting with a different account ID. * Do not allow reset when the full setup process has completed. * * @throws ExceptionWithResponseData When the full setup process is completed. */ private function reset_account_setup() { // Can't reset if the MC connection process has been completed previously. if ( $this->container->get( MerchantCenterService::class )->is_setup_complete() ) { throw $this->prepare_exception( sprintf( /* translators: 1: is a numeric account ID */ __( 'Merchant Center account already connected: %d', 'google-listings-and-ads' ), $this->options->get_merchant_id() ) ); } $this->disconnect(); } /** * Ensure the Merchant Center account's Website URL matches the site URL. Update an empty value or * a different, unclaimed URL value. Throw a 409 exception if a different, claimed URL is found. * * @param int $merchant_id The Merchant Center account to update. * * @throws ExceptionWithResponseData If the account URL doesn't match the site URL or the URL is invalid. */ private function maybe_add_merchant_center_url( int $merchant_id ) { $site_url = esc_url_raw( $this->get_site_url() ); if ( ! wc_is_valid_url( $site_url ) ) { throw $this->prepare_exception( __( 'Invalid site URL.', 'google-listings-and-ads' ) ); } /** @var Merchant $merchant */ $merchant = $this->container->get( Merchant::class ); /** @var Account $account */ $account = $merchant->get_account( $merchant_id ); $account_url = $account->getWebsiteUrl() ?: ''; if ( untrailingslashit( $site_url ) !== untrailingslashit( $account_url ) ) { $is_website_claimed = $merchant->get_accountstatus( $merchant_id )->getWebsiteClaimed(); if ( ! empty( $account_url ) && $is_website_claimed && ! $this->allow_switch_url ) { $state = $this->state->get(); $state['set_id']['data']['old_url'] = $account_url; $state['set_id']['status'] = MerchantAccountState::STEP_ERROR; $this->state->update( $state ); $clean_account_url = $this->strip_url_protocol( $account_url ); $clean_site_url = $this->strip_url_protocol( $site_url ); do_action( 'woocommerce_gla_url_switch_required', [] ); throw $this->prepare_exception( sprintf( /* translators: 1: is a website URL (without the protocol) */ __( 'This Merchant Center account already has a verified and claimed URL, %1$s', 'google-listings-and-ads' ), $clean_account_url ), [ 'id' => $merchant_id, 'claimed_url' => $clean_account_url, 'new_url' => $clean_site_url, ], 409 ); } $account->setWebsiteUrl( $site_url ); $merchant->update_account( $account ); // Clear previous hashed URL. $this->options->delete( OptionsInterface::CLAIMED_URL_HASH ); do_action( 'woocommerce_gla_url_switch_success', [] ); } } /** * Get the callback function for linking an Ads account. * * @throws Exception When the merchant account hasn't been set yet. */ private function link_ads_account() { if ( ! $this->options->get_merchant_id() ) { throw new Exception( 'A Merchant Center account must be connected' ); } $ads_state = $this->container->get( AdsAccountState::class ); // Create link for Merchant and accept it in Ads. $this->container->get( Merchant::class )->link_ads_id( $this->options->get_ads_id() ); $this->container->get( Ads::class )->accept_merchant_link( $this->options->get_merchant_id() ); $ads_state->complete_step( 'link_merchant' ); } /** * Prepares an Exception to be thrown with Merchant data: * - Ensure it has the merchant_id value * - Default to a 400 error code * * @param string $message * @param array $data * @param int|null $code * * @return ExceptionWithResponseData */ private function prepare_exception( string $message, array $data = [], ?int $code = null ): ExceptionWithResponseData { $merchant_id = $this->options->get_merchant_id(); if ( $merchant_id && ! isset( $data['id'] ) ) { $data['id'] = $merchant_id; } return new ExceptionWithResponseData( $message, $code ?: 400, null, $data ); } /** * Delete the option regarding WPCOM authorization * * @return bool */ public function reset_wpcom_api_authorization_data(): bool { $this->delete_wpcom_api_auth_nonce(); $this->delete_wpcom_api_status_transient(); return $this->options->delete( OptionsInterface::WPCOM_REST_API_STATUS ); } /** * Update the status of the merchant granting access to Google's WPCOM app in the database. * Before updating the status in the DB it will compare the nonce stored in the DB with the nonce passed to the API. * * @param string $status The status of the merchant granting access to Google's WPCOM app. * @param string $nonce The nonce provided by Google in the URL query parameter when Google redirects back to merchant's site. * * @return bool * @throws ExceptionWithResponseData If the stored nonce / nonce from query param is not provided, or the nonces mismatch. */ public function update_wpcom_api_authorization( string $status, string $nonce ): bool { try { $stored_nonce = $this->options->get( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE ); if ( empty( $stored_nonce ) ) { throw $this->prepare_exception( __( 'No stored nonce found in the database, skip updating auth status.', 'google-listings-and-ads' ) ); } if ( empty( $nonce ) ) { throw $this->prepare_exception( __( 'Nonce is not provided, skip updating auth status.', 'google-listings-and-ads' ) ); } if ( $stored_nonce !== $nonce ) { $this->delete_wpcom_api_auth_nonce(); throw $this->prepare_exception( __( 'Nonces mismatch, skip updating auth status.', 'google-listings-and-ads' ) ); } $this->delete_wpcom_api_auth_nonce(); /** * When the WPCOM Authorization status has been updated. * * @event update_wpcom_api_authorization * @property string status The status of the request. * @property int|null blog_id The blog ID. */ do_action( 'woocommerce_gla_track_event', 'update_wpcom_api_authorization', [ 'status' => $status, 'blog_id' => Jetpack_Options::get_option( 'id' ), ] ); $this->delete_wpcom_api_status_transient(); return $this->options->update( OptionsInterface::WPCOM_REST_API_STATUS, $status ); } catch ( ExceptionWithResponseData $e ) { /** * When the WPCOM Authorization status has been updated with errors. * * @event update_wpcom_api_authorization * @property string status The status of the request. * @property int|null blog_id The blog ID. */ do_action( 'woocommerce_gla_track_event', 'update_wpcom_api_authorization', [ 'status' => $e->getMessage(), 'blog_id' => Jetpack_Options::get_option( 'id' ), ] ); throw $e; } } /** * Delete the nonce of "verifying Google is the one redirect back to merchant site and set the auth status" in the database. * * @return bool */ public function delete_wpcom_api_auth_nonce(): bool { return $this->options->delete( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE ); } /** * Deletes the transient storing the WPCOM Status data. */ public function delete_wpcom_api_status_transient(): void { $transients = $this->container->get( TransientsInterface::class ); $transients->delete( TransientsInterface::WPCOM_API_STATUS ); } /** * Check if the WPCOM API Status is healthy by doing a request to /wc/partners/google/remote-site-status endpoint in WPCOM. * * @return bool True when the status is healthy, false otherwise. */ public function is_wpcom_api_status_healthy() { /** @var TransientsInterface $transients */ $transients = $this->container->get( TransientsInterface::class ); $status = $transients->get( TransientsInterface::WPCOM_API_STATUS ); if ( ! $status ) { $integration_status_args = [ 'method' => 'GET', 'timeout' => 30, 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/remote-site-status', 'user_id' => get_current_user_id(), ]; $integration_remote_request_response = Client::remote_request( $integration_status_args, null ); if ( is_wp_error( $integration_remote_request_response ) ) { $status = [ 'is_healthy' => false ]; } else { $status = json_decode( wp_remote_retrieve_body( $integration_remote_request_response ), true ) ?? [ 'is_healthy' => false ]; } $transients->set( TransientsInterface::WPCOM_API_STATUS, $status, MINUTE_IN_SECONDS * 30 ); } return isset( $status['is_healthy'] ) && $status['is_healthy'] && $status['is_wc_rest_api_healthy'] && $status['is_partner_token_healthy']; } } PK!b/  )src/MerchantCenter/ContactInformation.phpnu[merchant = $merchant; $this->settings = $settings; } /** * Get the contact information for the connected Merchant Center account. * * @return AccountBusinessInformation|null The contact information associated with the Merchant Center account or * null. * * @throws ExceptionWithResponseData If the Merchant Center account can't be retrieved. */ public function get_contact_information(): ?AccountBusinessInformation { $business_information = $this->merchant->get_account()->getBusinessInformation(); return $business_information ?: null; } /** * Update the address for the connected Merchant Center account to the store address set in WooCommerce * settings. * * @return AccountBusinessInformation The contact information associated with the Merchant Center account. * * @throws ExceptionWithResponseData If the Merchant Center account can't be retrieved or updated. */ public function update_address_based_on_store_settings(): AccountBusinessInformation { $business_information = $this->get_contact_information() ?: new AccountBusinessInformation(); $store_address = $this->settings->get_store_address(); $business_information->setAddress( $store_address ); $this->update_contact_information( $business_information ); return $business_information; } /** * Update the contact information for the connected Merchant Center account. * * @param AccountBusinessInformation $business_information * * @throws ExceptionWithResponseData If the Merchant Center account can't be retrieved or updated. */ protected function update_contact_information( AccountBusinessInformation $business_information ): void { $account = $this->merchant->get_account(); $account->setBusinessInformation( $business_information ); $this->merchant->update_account( $account ); } } PK!B3src/MerchantCenter/MerchantCenterAwareInterface.phpnu[merchant_center = $merchant_center; } } PK!~F44,src/MerchantCenter/MerchantCenterService.phpnu[maybe_add_contact_info_issue( $issues, $cache_created_time ); }, 10, 2 ); } /** * Get whether Merchant Center setup is completed. * * @return bool */ public function is_setup_complete(): bool { return boolval( $this->options->get( OptionsInterface::MC_SETUP_COMPLETED_AT, false ) ); } /** * Get whether Merchant Center is connected. * * @return bool */ public function is_connected(): bool { return $this->is_google_connected() && $this->is_setup_complete(); } /** * Get whether the dependent Google account is connected. * * @return bool */ public function is_google_connected(): bool { return boolval( $this->options->get( OptionsInterface::GOOGLE_CONNECTED, false ) ); } /** * Whether we are able to sync data to the Merchant Center account. * Account must be connected and the URL we claimed with must match the site URL. * URL matches is stored in a transient to prevent it from being refetched in cases * where the site is unable to access account data. * * @since 1.13.0 * @return boolean */ public function is_ready_for_syncing(): bool { if ( ! $this->is_connected() ) { return false; } /** @var TransientsInterface $transients */ $transients = $this->container->get( TransientsInterface::class ); $url_matches = $transients->get( TransientsInterface::URL_MATCHES ); if ( null === $url_matches ) { $claimed_url_hash = $this->container->get( Merchant::class )->get_claimed_url_hash(); $site_url_hash = md5( untrailingslashit( $this->get_site_url() ) ); $url_matches = apply_filters( 'woocommerce_gla_ready_for_syncing', $claimed_url_hash === $site_url_hash ) ? 'yes' : 'no'; $transients->set( TransientsInterface::URL_MATCHES, $url_matches, HOUR_IN_SECONDS * 12 ); } return 'yes' === $url_matches; } /** * Whether we should push data into MC. Only if: * - MC is ready for syncing {@see is_ready_for_syncing} * - Notifications Service is not enabled * * @return bool * @since 2.8.0 */ public function should_push(): bool { return $this->is_ready_for_syncing(); } /** * Get whether the country is supported by the Merchant Center. * * @return bool True if the country is in the list of MC-supported countries. * * @since 1.9.0 */ public function is_store_country_supported(): bool { $country = $this->container->get( WC::class )->get_base_country(); /** @var GoogleHelper $google_helper */ $google_helper = $this->container->get( GoogleHelper::class ); return $google_helper->is_country_supported( $country ); } /** * Get whether the language is supported by the Merchant Center. * * @param string $language Optional - to check a language other than the site language. * @return bool True if the language is in the list of MC-supported languages. */ public function is_language_supported( string $language = '' ): bool { // Default to base site language if ( empty( $language ) ) { $language = substr( $this->container->get( WP::class )->get_locale(), 0, 2 ); } /** @var GoogleHelper $google_helper */ $google_helper = $this->container->get( GoogleHelper::class ); return array_key_exists( strtolower( $language ), $google_helper->get_mc_supported_languages() ); } /** * Get whether the contact information has been setup. * * @since 1.4.0 * * @return bool */ public function is_contact_information_setup(): bool { if ( true === boolval( $this->options->get( OptionsInterface::CONTACT_INFO_SETUP, false ) ) ) { return true; } // Additional check for users that have already gone through on-boarding. if ( $this->is_setup_complete() ) { $is_mc_setup = $this->is_mc_contact_information_setup(); $this->options->update( OptionsInterface::CONTACT_INFO_SETUP, $is_mc_setup ); return $is_mc_setup; } return false; } /** * Return if the given country is supported to have promotions on Google. * * @param string $country * * @return bool */ public function is_promotion_supported_country( string $country = '' ): bool { // Default to WooCommerce store country if ( empty( $country ) ) { $country = $this->container->get( WC::class )->get_base_country(); } /** @var GoogleHelper $google_helper */ $google_helper = $this->container->get( GoogleHelper::class ); return in_array( $country, $google_helper->get_mc_promotion_supported_countries(), true ); } /** * Return the setup status to determine what step to continue at. * * @return array */ public function get_setup_status(): array { if ( $this->is_setup_complete() ) { return [ 'status' => 'complete' ]; } $step = 'accounts'; if ( $this->connected_account() && $this->container->get( AdsService::class )->connected_account() && $this->is_mc_contact_information_setup() ) { $step = 'product_listings'; if ( $this->saved_target_audience() && $this->saved_shipping_and_tax_options() ) { $step = 'paid_ads'; } } return [ 'status' => 'incomplete', 'step' => $step, ]; } /** * Check if account has been connected. * * @return bool */ protected function connected_account(): bool { $id = $this->options->get_merchant_id(); return $id && ! $this->container->get( MerchantAccountState::class )->last_incomplete_step(); } /** * Check if target audience has been saved (with a valid selection of countries). * * @return bool */ protected function saved_target_audience(): bool { $audience = $this->options->get( OptionsInterface::TARGET_AUDIENCE ); if ( empty( $audience ) || ! isset( $audience['location'] ) ) { return false; } $empty_selection = 'selected' === $audience['location'] && empty( $audience['countries'] ); return ! $empty_selection; } /** * Checks if we should add an issue when the contact information is not setup. * * @since 1.4.0 * * @param array $issues The current array of custom issues * @param DateTime $cache_created_time The time of the cache/issues generation. * * @return array */ protected function maybe_add_contact_info_issue( array $issues, DateTime $cache_created_time ): array { if ( $this->is_setup_complete() && ! $this->is_contact_information_setup() ) { $issues[] = [ 'product_id' => 0, 'product' => 'All products', 'code' => 'missing_contact_information', 'issue' => __( 'No contact information.', 'google-listings-and-ads' ), 'action' => __( 'Add store contact information', 'google-listings-and-ads' ), 'action_url' => $this->get_settings_url(), 'created_at' => $cache_created_time->format( 'Y-m-d H:i:s' ), 'type' => MerchantStatuses::TYPE_ACCOUNT, 'severity' => 'error', 'source' => 'filter', ]; } return $issues; } /** * Check if the Merchant Center contact information has been setup already. * * @since 1.4.0 * * @return boolean */ protected function is_mc_contact_information_setup(): bool { $is_setup = [ 'address' => false, ]; try { $contact_info = $this->container->get( ContactInformation::class )->get_contact_information(); } catch ( ExceptionWithResponseData $exception ) { do_action( 'woocommerce_gla_debug_message', 'Error retrieving Merchant Center account\'s business information.', __METHOD__ ); return false; } if ( $contact_info instanceof AccountBusinessInformation ) { /** @var Settings $settings */ $settings = $this->container->get( Settings::class ); if ( $contact_info->getAddress() instanceof AccountAddress && $settings->get_store_address() instanceof AccountAddress ) { $is_setup['address'] = $this->container->get( AddressUtility::class )->compare_addresses( $contact_info->getAddress(), $settings->get_store_address() ); } } return $is_setup['address']; } /** * Check if the taxes + shipping rate and time + free shipping settings have been saved. * * @return bool If all required settings have been provided. * * @since 1.4.0 */ protected function saved_shipping_and_tax_options(): bool { $merchant_center_settings = $this->options->get( OptionsInterface::MERCHANT_CENTER, [] ); $target_countries = $this->container->get( TargetAudience::class )->get_target_countries(); // Tax options saved if: not US (no taxes) or tax_rate has been set if ( in_array( 'US', $target_countries, true ) && empty( $merchant_center_settings['tax_rate'] ) ) { return false; } // Shipping time saved if: 'manual' OR records for all countries if ( isset( $merchant_center_settings['shipping_time'] ) && 'manual' === $merchant_center_settings['shipping_time'] ) { $saved_shipping_time = true; } else { $shipping_time_rows = $this->container->get( ShippingTimeQuery::class )->get_results(); // Get the name of countries that have saved shipping times. $saved_time_countries = array_column( $shipping_time_rows, 'country' ); // Check if all target countries have a shipping time. $saved_shipping_time = count( $shipping_time_rows ) === count( $target_countries ) && empty( array_diff( $target_countries, $saved_time_countries ) ); } // Shipping rates saved if: 'manual', 'automatic', OR there are records for all countries if ( isset( $merchant_center_settings['shipping_rate'] ) && in_array( $merchant_center_settings['shipping_rate'], [ 'manual', 'automatic' ], true ) ) { $saved_shipping_rate = true; } else { // Get the list of saved shipping rates grouped by country. /** * @var ShippingRateQuery $shipping_rate_query */ $shipping_rate_query = $this->container->get( ShippingRateQuery::class ); $shipping_rate_query->group_by( 'country' ); $shipping_rate_rows = $shipping_rate_query->get_results(); // Get the name of countries that have saved shipping rates. $saved_rates_countries = array_column( $shipping_rate_rows, 'country' ); // Check if all target countries have a shipping rate. $saved_shipping_rate = count( $shipping_rate_rows ) === count( $target_countries ) && empty( array_diff( $target_countries, $saved_rates_countries ) ); } return $saved_shipping_rate && $saved_shipping_time; } /** * Determine whether there are any account-level issues. * * @since 1.11.0 * @return bool */ public function has_account_issues(): bool { $issues = $this->container->get( MerchantStatuses::class )->get_issues( MerchantStatuses::TYPE_ACCOUNT ); return isset( $issues['issues'] ) && count( $issues['issues'] ) >= 1; } /** * Determine whether there is at least one synced product. * * @since 1.11.0 * @return bool */ public function has_at_least_one_synced_product(): bool { $statuses = $this->container->get( MerchantStatuses::class )->get_product_statistics(); return isset( $statuses['statistics']['active'] ) && $statuses['statistics']['active'] >= 1; } } PK!*i'src/MerchantCenter/MerchantStatuses.phpnu[ [], 'parents' => [], ]; /** * @var array Default product stats. */ public const DEFAULT_PRODUCT_STATS = [ MCStatus::APPROVED => 0, MCStatus::PARTIALLY_APPROVED => 0, MCStatus::EXPIRING => 0, MCStatus::PENDING => 0, MCStatus::DISAPPROVED => 0, MCStatus::NOT_SYNCED => 0, 'parents' => [], ]; /** * @var array Initial intermediate data for product status counts. */ protected $initial_intermediate_data = self::DEFAULT_PRODUCT_STATS; /** * @var WC_Product[] Lookup of WooCommerce Product Objects. */ protected $product_data_lookup = []; /** * MerchantStatuses constructor. */ public function __construct() { $this->cache_created_time = new DateTime(); } /** * Get the Product Statistics (updating caches if necessary). This is the * number of product IDs with each status (approved and partially approved are combined as active). * * @param bool $force_refresh Force refresh of all product status data. * * @return array The product status statistics. * @throws Exception If no Merchant Center account is connected, or account status is not retrievable. */ public function get_product_statistics( bool $force_refresh = false ): array { $job = $this->maybe_refresh_status_data( $force_refresh ); $failure_rate_msg = $job->get_failure_rate_message(); $this->mc_statuses = $this->container->get( TransientsInterface::class )->get( Transients::MC_STATUSES ); // If the failure rate is too high, return an error message so the UI can stop polling. if ( $failure_rate_msg && null === $this->mc_statuses ) { return [ 'timestamp' => $this->cache_created_time->getTimestamp(), 'statistics' => null, 'loading' => false, 'error' => __( 'The scheduled job has been paused due to a high failure rate.', 'google-listings-and-ads' ), ]; } if ( $job->is_scheduled() || null === $this->mc_statuses ) { return [ 'timestamp' => $this->cache_created_time->getTimestamp(), 'statistics' => null, 'loading' => true, 'error' => null, ]; } if ( ! empty( $this->mc_statuses['error'] ) ) { return $this->mc_statuses; } $counting_stats = $this->mc_statuses['statistics']; $counting_stats = array_merge( [ 'active' => $counting_stats[ MCStatus::PARTIALLY_APPROVED ] + $counting_stats[ MCStatus::APPROVED ] ], $counting_stats ); unset( $counting_stats[ MCStatus::PARTIALLY_APPROVED ], $counting_stats[ MCStatus::APPROVED ] ); return array_merge( $this->mc_statuses, [ 'statistics' => $counting_stats ] ); } /** * Retrieve the Merchant Center issues and total count. Refresh if the cache issues have gone stale. * Issue details are reduced, and for products, grouped by type. * Issues can be filtered by type, severity and searched by name or ID (if product type) and paginated. * Count takes into account the type filter, but not the pagination. * * In case there are issues with severity Error we hide the other issues with lower severity. * * @param string|null $type To filter by issue type if desired. * @param int $per_page The number of issues to return (0 for no limit). * @param int $page The page to start on (1-indexed). * @param bool $force_refresh Force refresh of all product status data. * * @return array With two indices, results (may be paged), count (considers type) and loading (indicating whether the data is loading). * @throws Exception If the account state can't be retrieved from Google. */ public function get_issues( ?string $type = null, int $per_page = 0, int $page = 1, bool $force_refresh = false ): array { $job = $this->maybe_refresh_status_data( $force_refresh ); // Get only error issues $severity_error_issues = $this->fetch_issues( $type, $per_page, $page, true ); // In case there are error issues we show only those, otherwise we show all the issues. $issues = $severity_error_issues['total'] > 0 ? $severity_error_issues : $this->fetch_issues( $type, $per_page, $page ); $issues['loading'] = $job->is_scheduled(); return $issues; } /** * Clears the status cache data. * * @since 1.1.0 */ public function clear_cache(): void { $job_repository = $this->container->get( JobRepository::class ); $update_all_products_job = $job_repository->get( UpdateAllProducts::class ); $delete_all_products_job = $job_repository->get( DeleteAllProducts::class ); // Clear the cache if we are not in the middle of updating/deleting all products. Otherwise, we might update the product stats for each individual batch. // See: ClearProductStatsCache::register if ( $update_all_products_job->can_schedule( null ) && $delete_all_products_job->can_schedule( null ) ) { $this->container->get( TransientsInterface::class )->delete( TransientsInterface::MC_STATUSES ); } } /** * Delete the intermediate product status count data. * * @since 2.6.4 */ protected function delete_product_statuses_count_intermediate_data(): void { $this->options->delete( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA ); } /** * Delete the stale issues from the database. * * @since 2.6.4 */ protected function delete_stale_issues(): void { $this->container->get( MerchantIssueTable::class )->delete_stale( $this->cache_created_time ); } /** * Delete the stale mc statuses from the database. * * @since 2.6.4 */ protected function delete_stale_mc_statuses(): void { $product_meta_query_helper = $this->container->get( ProductMetaQueryHelper::class ); $product_meta_query_helper->delete_all_values( ProductMetaHandler::KEY_MC_STATUS ); } /** * Clear the product statuses cache and delete stale issues. * * @since 2.6.4 */ public function clear_product_statuses_cache_and_issues(): void { $this->delete_stale_issues(); $this->delete_stale_mc_statuses(); $this->delete_product_statuses_count_intermediate_data(); } /** * Check if the Merchant Center account is connected and throw an exception if it's not. * * @since 2.6.4 * * @throws Exception If the Merchant Center account is not connected. */ protected function check_mc_is_connected() { $mc_service = $this->container->get( MerchantCenterService::class ); if ( ! $mc_service->is_connected() ) { // Return a 401 to redirect to reconnect flow if the Google account is not connected. if ( ! $mc_service->is_google_connected() ) { throw new Exception( __( 'Google account is not connected.', 'google-listings-and-ads' ), 401 ); } throw new Exception( __( 'Merchant Center account is not set up.', 'google-listings-and-ads' ) ); } } /** * Maybe start the job to refresh the status and issues data. * * @param bool $force_refresh Force refresh of all status-related data. * * @return UpdateMerchantProductStatuses The job to update the statuses. * * @throws Exception If no Merchant Center account is connected, or account status is not retrievable. * @throws NotFoundExceptionInterface If the class is not found in the container. * @throws ContainerExceptionInterface If the container throws an exception. */ public function maybe_refresh_status_data( bool $force_refresh = false ): UpdateMerchantProductStatuses { $this->check_mc_is_connected(); // Only refresh if the current data has expired. $this->mc_statuses = $this->container->get( TransientsInterface::class )->get( Transients::MC_STATUSES ); $job = $this->container->get( JobRepository::class )->get( UpdateMerchantProductStatuses::class ); // If force_refresh is true or if not transient, return empty array and scheduled the job to update the statuses. if ( ! $job->is_scheduled() && ( $force_refresh || ( ! $force_refresh && null === $this->mc_statuses ) ) ) { // Delete the transient before scheduling the job because some errors, like the failure rate message, can occur before the job is executed. $this->clear_cache(); // Schedule job to update the statuses. If the failure rate is too high, the job will not be scheduled. $job->schedule(); } return $job; } /** * Delete the cached statistics and issues. */ public function delete(): void { $this->container->get( TransientsInterface::class )->delete( Transients::MC_STATUSES ); $this->container->get( MerchantIssueTable::class )->truncate(); } /** * Fetch the cached issues from the database. * * @param string|null $type To filter by issue type if desired. * @param int $per_page The number of issues to return (0 for no limit). * @param int $page The page to start on (1-indexed). * @param bool $only_errors Filters only the issues with error and critical severity. * * @return array The requested issues and the total count of issues. * @throws InvalidValue If the type filter is invalid. */ protected function fetch_issues( ?string $type = null, int $per_page = 0, int $page = 1, bool $only_errors = false ): array { /** @var MerchantIssueQuery $issue_query */ $issue_query = $this->container->get( MerchantIssueQuery::class ); // Ensure account issues are shown first. $issue_query->set_order( 'type' ); $issue_query->set_order( 'product' ); $issue_query->set_order( 'issue' ); // Filter by type if valid. if ( in_array( $type, $this->get_valid_issue_types(), true ) ) { $issue_query->where( 'type', $type ); } elseif ( null !== $type ) { throw InvalidValue::not_in_allowed_list( 'type filter', $this->get_valid_issue_types() ); } // Result pagination. if ( $per_page > 0 ) { $issue_query->set_limit( $per_page ); $issue_query->set_offset( $per_page * ( $page - 1 ) ); } if ( $only_errors ) { $issue_query->where( 'severity', [ 'error', 'critical' ], 'IN' ); } $issues = []; foreach ( $issue_query->get_results() as $row ) { $issue = [ 'type' => $row['type'], 'product_id' => intval( $row['product_id'] ), 'product' => $row['product'], 'issue' => $row['issue'], 'code' => $row['code'], 'action' => $row['action'], 'action_url' => $row['action_url'], 'severity' => $this->get_issue_severity( $row ), ]; if ( $issue['product_id'] ) { $issue['applicable_countries'] = json_decode( $row['applicable_countries'], true ); } else { unset( $issue['product_id'] ); } $issues[] = $issue; } return [ 'issues' => $issues, 'total' => $issue_query->get_count(), ]; } /** * Get MC product issues from a list of Product View statuses. * * @param array $statuses The list of Product View statuses. * @throws NotFoundExceptionInterface If the class is not found in the container. * @throws ContainerExceptionInterface If the container throws an exception. * * @return array The list of product issues. */ protected function get_product_issues( array $statuses ): array { /** @var Merchant $merchant */ $merchant = $this->container->get( Merchant::class ); /** @var ProductHelper $product_helper */ $product_helper = $this->container->get( ProductHelper::class ); $visibility_meta_key = $this->prefix_meta_key( ProductMetaHandler::KEY_VISIBILITY ); $google_ids = array_column( $statuses, 'mc_id' ); $product_issues = []; $created_at = $this->cache_created_time->format( 'Y-m-d H:i:s' ); $entries = $merchant->get_productstatuses_batch( $google_ids )->getEntries() ?? []; foreach ( $entries as $response_entry ) { /** @var GoogleProductStatus $mc_product_status */ $mc_product_status = $response_entry->getProductStatus(); $mc_product_id = $mc_product_status->getProductId(); $wc_product_id = $product_helper->get_wc_product_id( $mc_product_id ); $wc_product = $this->product_data_lookup[ $wc_product_id ] ?? null; // Skip products not synced by this extension. if ( ! $wc_product ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Merchant Center product %s not found in this WooCommerce store.', $mc_product_id ), __METHOD__ . ' in remove_invalid_statuses()', ); continue; } // Unsynced issues shouldn't be shown. if ( ChannelVisibility::DONT_SYNC_AND_SHOW === $wc_product->get_meta( $visibility_meta_key ) ) { continue; } // Confirm there are issues for this product. if ( empty( $mc_product_status->getItemLevelIssues() ) ) { continue; } $product_issue_template = [ 'product' => html_entity_decode( $wc_product->get_name(), ENT_QUOTES ), 'product_id' => $wc_product_id, 'created_at' => $created_at, 'applicable_countries' => [], 'source' => 'mc', ]; foreach ( $mc_product_status->getItemLevelIssues() as $item_level_issue ) { if ( 'merchant_action' !== $item_level_issue->getResolution() ) { continue; } $hash_key = $wc_product_id . '__' . md5( $item_level_issue->getDescription() ); $this->product_issue_countries[ $hash_key ] = array_merge( $this->product_issue_countries[ $hash_key ] ?? [], $item_level_issue->getApplicableCountries() ); $product_issues[ $hash_key ] = $product_issue_template + [ 'code' => $item_level_issue->getCode(), 'issue' => $item_level_issue->getDescription(), 'action' => $item_level_issue->getDetail(), 'action_url' => $item_level_issue->getDocumentation(), 'severity' => $item_level_issue->getServability(), ]; } } return $product_issues; } /** * Refresh the account , pre-sync product validation and custom merchant issues. * * @since 2.6.4 * * @throws Exception If the account state can't be retrieved from Google. */ public function refresh_account_and_presync_issues(): void { // Update account-level issues. $this->refresh_account_issues(); // Update pre-sync product validation issues. $this->refresh_presync_product_issues(); // Include any custom merchant issues. $this->refresh_custom_merchant_issues(); } /** * Retrieve all account-level issues and store them in the database. * * @throws Exception If the account state can't be retrieved from Google. */ protected function refresh_account_issues(): void { /** @var Merchant $merchant */ $merchant = $this->container->get( Merchant::class ); $account_issues = []; $created_at = $this->cache_created_time->format( 'Y-m-d H:i:s' ); $issues = $merchant->get_accountstatus()->getAccountLevelIssues() ?? []; foreach ( $issues as $issue ) { $key = md5( $issue->getTitle() ); if ( isset( $account_issues[ $key ] ) ) { $account_issues[ $key ]['applicable_countries'][] = $issue->getCountry(); } else { $account_issues[ $key ] = [ 'product_id' => 0, 'product' => __( 'All products', 'google-listings-and-ads' ), 'code' => $issue->getId(), 'issue' => $issue->getTitle(), 'action' => $issue->getDetail(), 'action_url' => $issue->getDocumentation(), 'created_at' => $created_at, 'type' => self::TYPE_ACCOUNT, 'severity' => $issue->getSeverity(), 'source' => 'mc', 'applicable_countries' => [ $issue->getCountry() ], ]; $account_issues[ $key ] = $this->maybe_override_issue_values( $account_issues[ $key ] ); } } // Sort and encode countries $account_issues = array_map( function ( $issue ) { sort( $issue['applicable_countries'] ); $issue['applicable_countries'] = wp_json_encode( array_unique( $issue['applicable_countries'] ) ); return $issue; }, $account_issues ); /** @var MerchantIssueQuery $issue_query */ $issue_query = $this->container->get( MerchantIssueQuery::class ); $issue_query->update_or_insert( $account_issues ); } /** * Custom issues can be added to the merchant issues table. * * @since 1.2.0 */ protected function refresh_custom_merchant_issues() { $custom_issues = apply_filters( 'woocommerce_gla_custom_merchant_issues', [], $this->cache_created_time ); if ( empty( $custom_issues ) ) { return; } /** @var MerchantIssueQuery $issue_query */ $issue_query = $this->container->get( MerchantIssueQuery::class ); $issue_query->update_or_insert( $custom_issues ); } /** * Refresh product issues in the merchant issues table. * * @param array $product_issues Array of product issues. * @throws InvalidQuery If an invalid column name is provided. * @throws NotFoundExceptionInterface If the class is not found in the container. * @throws ContainerExceptionInterface If the container throws an exception. */ protected function refresh_product_issues( array $product_issues ): void { // Alphabetize all product/issue country lists. array_walk( $this->product_issue_countries, function ( &$countries ) { sort( $countries ); } ); // Product issue cleanup: sorting (by product ID) and encode applicable countries. ksort( $product_issues ); $product_issues = array_map( function ( $unique_key, $issue ) { $issue['applicable_countries'] = wp_json_encode( $this->product_issue_countries[ $unique_key ] ); return $issue; }, array_keys( $product_issues ), $product_issues ); /** @var MerchantIssueQuery $issue_query */ $issue_query = $this->container->get( MerchantIssueQuery::class ); $issue_query->update_or_insert( array_values( $product_issues ) ); } /** * Include local presync product validation issues in the merchant issues table. */ protected function refresh_presync_product_issues(): void { /** @var MerchantIssueQuery $issue_query */ $issue_query = $this->container->get( MerchantIssueQuery::class ); $created_at = $this->cache_created_time->format( 'Y-m-d H:i:s' ); $issue_action = __( 'Update this attribute in your product data', 'google-listings-and-ads' ); /** @var ProductMetaQueryHelper $product_meta_query_helper */ $product_meta_query_helper = $this->container->get( ProductMetaQueryHelper::class ); // Get all MC statuses. $all_errors = $product_meta_query_helper->get_all_values( ProductMetaHandler::KEY_ERRORS ); $chunk_size = apply_filters( 'woocommerce_gla_merchant_status_presync_issues_chunk', 500 ); $product_issues = []; foreach ( $all_errors as $product_id => $presync_errors ) { // Don't create issues with empty descriptions // or for variable parents (they contain issues of all children). $error = $presync_errors[ array_key_first( $presync_errors ) ]; if ( empty( $error ) || ! is_string( $error ) ) { continue; } $product = get_post( $product_id ); // Don't store pre-sync errors for unpublished (draft, trashed) products. if ( 'publish' !== get_post_status( $product ) ) { continue; } foreach ( $presync_errors as $text ) { $issue_parts = $this->parse_presync_issue_text( $text ); $product_issues[] = [ 'product' => $product->post_title, 'product_id' => $product_id, 'code' => $issue_parts['code'], 'severity' => self::SEVERITY_ERROR, 'issue' => $issue_parts['issue'], 'action' => $issue_action, 'action_url' => 'https://support.google.com/merchants/answer/10538362?hl=en&ref_topic=6098333', 'applicable_countries' => '["all"]', 'source' => 'pre-sync', 'created_at' => $created_at, ]; } // Do update-or-insert in chunks. if ( count( $product_issues ) >= $chunk_size ) { $issue_query->update_or_insert( $product_issues ); $product_issues = []; } } // Handle any leftover issues. $issue_query->update_or_insert( $product_issues ); } /** * Process product status statistics. * * @param array $product_view_statuses Product View statuses. * @see MerchantReport::get_product_view_report * * @throws NotFoundExceptionInterface If the class is not found in the container. * @throws ContainerExceptionInterface If the container throws an exception. */ public function process_product_statuses( array $product_view_statuses ): void { $this->mc_statuses = []; $product_repository = $this->container->get( ProductRepository::class ); $this->product_data_lookup = $product_repository->find_by_ids_as_associative_array( array_column( $product_view_statuses, 'product_id' ) ); $this->product_statuses = [ 'products' => [], 'parents' => [], ]; foreach ( $product_view_statuses as $product_status ) { $wc_product_id = $product_status['product_id']; $mc_product_status = $product_status['status']; $wc_product = $this->product_data_lookup[ $wc_product_id ] ?? null; if ( ! $wc_product || ! $wc_product_id ) { // Skip if the product does not exist in WooCommerce. do_action( 'woocommerce_gla_debug_message', sprintf( 'Merchant Center product %s not found in this WooCommerce store.', $wc_product_id ), __METHOD__, ); continue; } if ( $this->product_is_expiring( $product_status['expiration_date'] ) ) { $mc_product_status = MCStatus::EXPIRING; } // Products is used later for global product status statistics. $this->product_statuses['products'][ $wc_product_id ][ $mc_product_status ] = 1 + ( $this->product_statuses['products'][ $wc_product_id ][ $mc_product_status ] ?? 0 ); // Aggregate parent statuses for mc_status postmeta. $wc_parent_id = $wc_product->get_parent_id(); if ( ! $wc_parent_id ) { continue; } $this->product_statuses['parents'][ $wc_parent_id ][ $mc_product_status ] = 1 + ( $this->product_statuses['parents'][ $wc_parent_id ][ $mc_product_status ] ?? 0 ); } $parent_keys = array_values( array_keys( $this->product_statuses['parents'] ) ); $parent_products = $product_repository->find_by_ids_as_associative_array( $parent_keys ); $this->product_data_lookup = $this->product_data_lookup + $parent_products; // Update each product's mc_status and then update the global statistics. $this->update_products_meta_with_mc_status(); $this->update_intermediate_product_statistics(); $product_issues = $this->get_product_issues( $product_view_statuses ); $this->refresh_product_issues( $product_issues ); } /** * Whether a product is expiring. * * @param DateTime $expiration_date * * @return bool Whether the product is expiring. */ protected function product_is_expiring( DateTime $expiration_date ): bool { if ( ! $expiration_date ) { return false; } // Products are considered expiring if they will expire within 3 days. return time() + 3 * DAY_IN_SECONDS > $expiration_date->getTimestamp(); } /** * Sum and update the intermediate product status statistics. It will group * the variations for the same parent. * * For the case that one variation is approved and the other disapproved: * 1. Give each status a priority. * 2. Store the last highest priority status in `$parent_statuses`. * 3. Compare if a higher priority status is found for that variable product. * 4. Loop through the `$parent_statuses` array at the end to add the final status counts. * * @return array Product status statistics. */ protected function update_intermediate_product_statistics(): array { $product_statistics = self::DEFAULT_PRODUCT_STATS; // If the option is set, use it to sum the total quantity. $product_statistics_intermediate_data = $this->options->get( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA ); if ( $product_statistics_intermediate_data ) { $product_statistics = $product_statistics_intermediate_data; $this->initial_intermediate_data = $product_statistics; } $product_statistics_priority = [ MCStatus::APPROVED => 6, MCStatus::PARTIALLY_APPROVED => 5, MCStatus::EXPIRING => 4, MCStatus::PENDING => 3, MCStatus::DISAPPROVED => 2, MCStatus::NOT_SYNCED => 1, ]; $parent_statuses = []; foreach ( $this->product_statuses['products'] as $product_id => $statuses ) { foreach ( $statuses as $status => $num_products ) { $product = $this->product_data_lookup[ $product_id ] ?? null; if ( ! $product ) { continue; } $parent_id = $product->get_parent_id(); if ( ! $parent_id ) { $product_statistics[ $status ] += $num_products; } elseif ( ! isset( $parent_statuses[ $parent_id ] ) ) { $parent_statuses[ $parent_id ] = $status; } else { $current_parent_status = $parent_statuses[ $parent_id ]; if ( $product_statistics_priority[ $status ] < $product_statistics_priority[ $current_parent_status ] ) { $parent_statuses[ $parent_id ] = $status; } } } } foreach ( $parent_statuses as $parent_id => $new_parent_status ) { $current_parent_intermediate_data_status = $product_statistics_intermediate_data['parents'][ $parent_id ] ?? null; if ( $current_parent_intermediate_data_status === $new_parent_status ) { continue; } if ( ! $current_parent_intermediate_data_status ) { $product_statistics[ $new_parent_status ] += 1; $product_statistics['parents'][ $parent_id ] = $new_parent_status; continue; } // Check if the new parent status has higher priority than the previous one. if ( $product_statistics_priority[ $new_parent_status ] < $product_statistics_priority[ $current_parent_intermediate_data_status ] ) { $product_statistics[ $current_parent_intermediate_data_status ] -= 1; $product_statistics[ $new_parent_status ] += 1; $product_statistics['parents'][ $parent_id ] = $new_parent_status; } else { $product_statistics['parents'][ $parent_id ] = $current_parent_intermediate_data_status; } } $this->options->update( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA, $product_statistics ); return $product_statistics; } /** * Calculate the total count of products in the MC using the statistics. * * @since 2.6.4 * * @param array $statistics * * @return int */ protected function calculate_total_synced_product_statistics( array $statistics ): int { if ( ! count( $statistics ) ) { return 0; } $synced_status_values = array_values( array_diff( $statistics, [ $statistics[ MCStatus::NOT_SYNCED ] ] ) ); return array_sum( $synced_status_values ); } /** * Handle the failure of the Merchant Center statuses fetching. * * @since 2.6.4 * * @param string $error_message The error message. * * @throws NotFoundExceptionInterface If the class is not found in the container. * @throws ContainerExceptionInterface If the container throws an exception. */ public function handle_failed_mc_statuses_fetching( string $error_message = '' ): void { // Reset the intermediate data to the initial state when starting the job. $this->options->update( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA, $this->initial_intermediate_data ); // Let's remove any issue created during the failed fetch. $this->container->get( MerchantIssueTable::class )->delete_specific_product_issues( array_keys( $this->product_data_lookup ) ); $mc_statuses = [ 'timestamp' => $this->cache_created_time->getTimestamp(), 'statistics' => null, 'loading' => false, 'error' => $error_message, ]; $this->container->get( TransientsInterface::class )->set( Transients::MC_STATUSES, $mc_statuses, $this->get_status_lifetime() ); } /** * Handle the completion of the Merchant Center statuses fetching. * * @since 2.6.4 */ public function handle_complete_mc_statuses_fetching() { $intermediate_data = $this->options->get( OptionsInterface::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA, self::DEFAULT_PRODUCT_STATS ); unset( $intermediate_data['parents'] ); $total_synced_products = $this->calculate_total_synced_product_statistics( $intermediate_data ); /** @var ProductRepository $product_repository */ $product_repository = $this->container->get( ProductRepository::class ); $intermediate_data[ MCStatus::NOT_SYNCED ] = count( $product_repository->find_all_product_ids() ) - $total_synced_products; $mc_statuses = [ 'timestamp' => $this->cache_created_time->getTimestamp(), 'statistics' => $intermediate_data, 'loading' => false, 'error' => null, ]; $this->container->get( TransientsInterface::class )->set( Transients::MC_STATUSES, $mc_statuses, $this->get_status_lifetime() ); $this->delete_product_statuses_count_intermediate_data(); } /** * Update the Merchant Center status for each product. */ protected function update_products_meta_with_mc_status() { // Generate a product_id=>mc_status array. $new_product_statuses = []; foreach ( $this->product_statuses as $types ) { foreach ( $types as $product_id => $statuses ) { if ( isset( $statuses[ MCStatus::PENDING ] ) ) { $new_product_statuses[ $product_id ] = MCStatus::PENDING; } elseif ( isset( $statuses[ MCStatus::EXPIRING ] ) ) { $new_product_statuses[ $product_id ] = MCStatus::EXPIRING; } elseif ( isset( $statuses[ MCStatus::APPROVED ] ) ) { if ( count( $statuses ) > 1 ) { $new_product_statuses[ $product_id ] = MCStatus::PARTIALLY_APPROVED; } else { $new_product_statuses[ $product_id ] = MCStatus::APPROVED; } } else { $new_product_statuses[ $product_id ] = array_key_first( $statuses ); } } } foreach ( $new_product_statuses as $product_id => $new_status ) { $product = $this->product_data_lookup[ $product_id ] ?? null; // At this point, the product should exist in WooCommerce but in the case that product is not found, log it. if ( ! $product ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Merchant Center product with WooCommerce ID %d is not found in this store.', $product_id ), __METHOD__, ); continue; } $product->add_meta_data( $this->prefix_meta_key( ProductMetaHandler::KEY_MC_STATUS ), $new_status, true ); // We use save_meta_data so we don't trigger the woocommerce_update_product hook and the Syncer Hooks. $product->save_meta_data(); } } /** * Allows a hook to modify the lifetime of the statuses data. * * @return int */ protected function get_status_lifetime(): int { return apply_filters( 'woocommerce_gla_mc_status_lifetime', self::STATUS_LIFETIME ); } /** * Valid issues types for issue type filter. * * @return string[] */ protected function get_valid_issue_types(): array { return [ self::TYPE_ACCOUNT, self::TYPE_PRODUCT, ]; } /** * Parse the code and formatted issue text out of the presync validation error text. * * Converts the error strings: * "[attribute] Error message." > "Error message [attribute]" * * Note: * If attribute is an array the name can be "[attribute[0]]". * So we need to match the additional set of square brackets. * * @param string $text * * @return string[] With indexes `code` and `issue` */ protected function parse_presync_issue_text( string $text ): array { $matches = []; preg_match( '/^\[([^\]]+\]?)\]\s*(.+)$/', $text, $matches ); if ( count( $matches ) !== 3 ) { return [ 'code' => 'presync_error_attrib_' . md5( $text ), 'issue' => $text, ]; } // Convert attribute name "imageLink" to "image". if ( 'imageLink' === $matches[1] ) { $matches[1] = 'image'; } // Convert attribute name "additionalImageLinks[]" to "galleryImage". if ( str_starts_with( $matches[1], 'additionalImageLinks' ) ) { $matches[1] = 'galleryImage'; } $matches[2] = trim( $matches[2], ' .' ); return [ 'code' => 'presync_error_' . $matches[1], 'issue' => "{$matches[2]} [{$matches[1]}]", ]; } /** * Return a standardized Merchant Issue severity value. * * @param array $row * * @return string */ protected function get_issue_severity( array $row ): string { $is_warning = in_array( $row['severity'], [ 'warning', 'suggestion', 'demoted', 'unaffected', ], true ); return $is_warning ? self::SEVERITY_WARNING : self::SEVERITY_ERROR; } /** * In very rare instances, issue values need to be overridden manually. * * @param array $issue * * @return array The original issue with any possibly overridden values. */ private function maybe_override_issue_values( array $issue ): array { /** * Code 'merchant_quality_low' for matching the original issue. * Ref: https://developers.google.com/shopping-content/guides/account-issues#merchant_quality_low * * Issue string "Account isn't eligible for free listings" for matching * the updated copy after Free and Enhanced Listings merge. * * TODO: Remove the condition of matching the $issue['issue'] * if its issue code is the same as 'merchant_quality_low' * after Google replaces the issue title on their side. */ if ( 'merchant_quality_low' === $issue['code'] || "Account isn't eligible for free listings" === $issue['issue'] ) { $issue['issue'] = 'Show products on additional surfaces across Google through free listings'; $issue['severity'] = self::SEVERITY_WARNING; $issue['action_url'] = 'https://support.google.com/merchants/answer/9199328?hl=en'; } /** * Reference: https://github.com/woocommerce/google-listings-and-ads/issues/1688 */ if ( 'home_page_issue' === $issue['code'] ) { $issue['issue'] = 'Website claim is lost, need to re verify and claim your website. Please reference the support link'; $issue['action_url'] = 'https://woocommerce.com/document/google-for-woocommerce/faq/#reverify-website'; } return $issue; } /** * Getter for get_cache_created_time * * @return DateTime The DateTime stored in cache_created_time */ public function get_cache_created_time(): DateTime { return $this->cache_created_time; } } PK!1src/MerchantCenter/PhoneVerificationException.phpnu[merchant = $merchant; $this->wp = $wp; $this->iso_utility = $iso_utility; } /** * Request verification code to start phone verification. * * @param string $region_code Two-letter country code (ISO 3166-1 alpha-2) for the phone number, for * example CA for Canadian numbers. * @param PhoneNumber $phone_number Phone number to be verified. * @param string $verification_method Verification method to receive verification code. * * @return string The verification ID to use in subsequent calls to * `PhoneVerification::verify_phone_number`. * * @throws PhoneVerificationException If there are any errors requesting verification. * @throws InvalidValue If an invalid input provided. */ public function request_phone_verification( string $region_code, PhoneNumber $phone_number, string $verification_method ): string { $this->validate_verification_method( $verification_method ); $this->validate_phone_region( $region_code ); try { return $this->merchant->request_phone_verification( $region_code, $phone_number->get(), $verification_method, $this->get_language_code() ); } catch ( GoogleServiceException $e ) { throw $this->map_google_exception( $e ); } } /** * Validates verification code to verify phone number for the account. * * @param string $verification_id The verification ID returned by * `PhoneVerification::request_phone_verification`. * @param string $verification_code The verification code that was sent to the phone number for validation. * @param string $verification_method Verification method used to receive verification code. * * @return void * * @throws PhoneVerificationException If there are any errors verifying the phone number. * @throws InvalidValue If an invalid input provided. */ public function verify_phone_number( string $verification_id, string $verification_code, string $verification_method ): void { $this->validate_verification_method( $verification_method ); try { $this->merchant->verify_phone_number( $verification_id, $verification_code, $verification_method ); } catch ( GoogleServiceException $e ) { throw $this->map_google_exception( $e ); } } /** * @param string $method * * @throws InvalidValue If the verification method is invalid. */ protected function validate_verification_method( string $method ) { $allowed = [ self::VERIFICATION_METHOD_SMS, self::VERIFICATION_METHOD_PHONE_CALL ]; if ( ! in_array( $method, $allowed, true ) ) { throw InvalidValue::not_in_allowed_list( $method, $allowed ); } } /** * @param string $region_code * * @throws InvalidValue If the phone region code is not a valid ISO 3166-1 alpha-2 country code. */ protected function validate_phone_region( string $region_code ) { if ( ! $this->iso_utility->is_iso3166_alpha2_country_code( $region_code ) ) { throw new InvalidValue( 'Invalid phone region! Phone region must be a two letter ISO 3166-1 alpha-2 country code.' ); } } /** * @return string */ protected function get_language_code(): string { return $this->iso_utility->wp_locale_to_bcp47( $this->wp->get_user_locale() ); } /** * @param GoogleServiceException $exception * * @return PhoneVerificationException */ protected function map_google_exception( GoogleServiceException $exception ): PhoneVerificationException { $code = $exception->getCode(); $message = $exception->getMessage(); $reason = ''; $errors = $exception->getErrors(); if ( ! empty( $errors ) ) { $error = $errors[ array_key_first( $errors ) ]; $message = $error['message'] ?? ''; $reason = $error['reason'] ?? ''; } return new PhoneVerificationException( $message, $code, $exception, [ 'reason' => $reason ] ); } } PK!EX$$,src/MerchantCenter/PolicyComplianceCheck.phpnu[wc = $wc; $this->google_helper = $google_helper; $this->target_audience = $target_audience; } /** * Check if the store website is accessed by all users for the controller. * * @return bool */ public function is_accessible(): bool { $all_allowed_countries = $this->wc->get_allowed_countries(); $target_countries = $this->target_audience->get_target_countries(); foreach ( $target_countries as $country ) { if ( ! array_key_exists( $country, $all_allowed_countries ) ) { return false; } } return true; } /** * Check if the store sample product landing pages lead to a 404 error. * * @return bool */ public function has_page_not_found_error(): bool { $url = $this->get_landing_page_url(); $response = wp_remote_get( $url ); if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { return true; } return false; } /** * Check if the store sample product landing pages has redirects through 3P domains. * * @return bool */ public function has_redirects(): bool { $url = $this->get_landing_page_url(); $response = wp_remote_get( $url, [ 'redirection' => 0 ] ); $code = wp_remote_retrieve_response_code( $response ); if ( $code >= 300 && $code <= 399 ) { return true; } return false; } /** * Returns a product page URL, uses homepage as a fallback. * * @return string Landing page URL. */ private function get_landing_page_url(): string { $products = wc_get_products( [ 'limit' => 1, 'status' => 'publish', ] ); if ( ! empty( $products ) ) { return $products[0]->get_permalink(); } return $this->get_site_url(); } /** * Check if the merchant set the restrictions in robots.txt or not in the store. * * @return bool */ public function has_restriction(): bool { return ! $this->robots_allowed( $this->get_site_url() ); } /** * Check if the robots.txt has restrictions or not in the store. * * @param string $url * @return bool */ private function robots_allowed( $url ) { $agents = [ preg_quote( '*', '/' ) ]; $agents = implode( '|', $agents ); // location of robots.txt file $response = wp_remote_get( trailingslashit( $url ) . 'robots.txt' ); if ( is_wp_error( $response ) ) { return true; } $body = wp_remote_retrieve_body( $response ); $robotstxt = preg_split( "/\r\n|\n|\r/", $body ); if ( empty( $robotstxt ) ) { return true; } $rule_applies = false; foreach ( $robotstxt as $line ) { $line = trim( $line ); if ( ! $line ) { continue; } // following rules only apply if User-agent matches '*' if ( preg_match( '/^\s*User-agent:\s*(.*)/i', $line, $match ) ) { $rule_applies = '*' === $match[1]; } if ( $rule_applies && preg_match( '/^\s*Disallow:\s*(.*)/i', $line, $regs ) ) { if ( ! $regs[1] ) { return true; } if ( '/' === trim( $regs[1] ) ) { return false; } } } return true; } /** * Check if the payment gateways is empty or not for the controller. * * @return bool */ public function has_payment_gateways(): bool { $gateways = $this->wc->get_available_payment_gateways(); if ( empty( $gateways ) ) { return false; } return true; } /** * Check if the store is using SSL for the controller. * * @return bool */ public function get_is_store_ssl(): bool { return 'https' === wp_parse_url( $this->get_site_url(), PHP_URL_SCHEME ); } /** * Check if the store has refund return policy page for the controller. * * @return bool */ public function has_refund_return_policy_page(): bool { // Check the slug as it's translated by the "woocommerce" text domain name. // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch if ( $this->the_slug_exists( _x( 'refund_returns', 'Page slug', 'woocommerce' ) ) ) { return true; } return false; } /** * Check if the slug exists or not. * * @param string $post_name * @return bool */ protected function the_slug_exists( string $post_name ): bool { $args = [ 'name' => $post_name, 'post_type' => 'page', 'post_status' => 'publish', 'numberposts' => 1, ]; if ( get_posts( $args ) ) { return true; } return false; } } PK!}6%src/MerchantCenter/TargetAudience.phpnu[wc = $wc; $this->options = $options; $this->google_helper = $google_helper; } /** * @return string[] List of target countries specified in options. Defaults to WooCommerce store base country. */ public function get_target_countries(): array { $target_countries = [ $this->wc->get_base_country() ]; $target_audience = $this->options->get( OptionsInterface::TARGET_AUDIENCE ); if ( empty( $target_audience['location'] ) && empty( $target_audience['countries'] ) ) { return $target_countries; } $location = strtolower( $target_audience['location'] ); if ( 'all' === $location ) { $target_countries = $this->google_helper->get_mc_supported_countries(); } elseif ( 'selected' === $location && ! empty( $target_audience['countries'] ) ) { $target_countries = $target_audience['countries']; } return $target_countries; } /** * Return the main target country (default Store country). * If the store country is not included then use the first target country. * * @return string */ public function get_main_target_country(): string { $target_countries = $this->get_target_countries(); $shop_country = $this->wc->get_base_country(); return in_array( $shop_country, $target_countries, true ) ? $shop_country : $target_countries[0]; } } PK! WHH(src/MultichannelMarketing/GLAChannel.phpnu[merchant_center = $merchant_center; $this->ads_campaign = $ads_campaign; $this->ads = $ads; $this->merchant_statuses = $merchant_statuses; $this->product_sync_stats = $product_sync_stats; $this->campaign_types = []; if ( $this->is_mcm_enabled() ) { $this->campaign_types = $this->generate_campaign_types(); } } /** * Determines if the multichannel marketing is enabled. * * @return bool */ protected function is_mcm_enabled(): bool { return apply_filters( 'woocommerce_gla_enable_mcm', false ) === true; } /** * Returns the unique identifier string for the marketing channel extension, also known as the plugin slug. * * @return string */ public function get_slug(): string { return 'google-listings-and-ads'; } /** * Returns the name of the marketing channel. * * @return string */ public function get_name(): string { return __( 'Google for WooCommerce', 'google-listings-and-ads' ); } /** * Returns the description of the marketing channel. * * @return string */ public function get_description(): string { return __( 'Native integration with Google that allows merchants to easily display their products across Google’s network.', 'google-listings-and-ads' ); } /** * Returns the path to the channel icon. * * @return string */ public function get_icon_url(): string { return 'https://woocommerce.com/wp-content/uploads/2021/06/woo-GoogleListingsAds-jworee.png'; } /** * Returns the setup status of the marketing channel. * * @return bool */ public function is_setup_completed(): bool { return $this->merchant_center->is_setup_complete(); } /** * Returns the URL to the settings page, or the link to complete the setup/onboarding if the channel has not been set up yet. * * @return string */ public function get_setup_url(): string { if ( ! $this->is_setup_completed() ) { return admin_url( 'admin.php?page=wc-admin&path=/google/start' ); } return admin_url( 'admin.php?page=wc-admin&path=/google/settings' ); } /** * Returns the status of the marketing channel's product listings. * * @return string */ public function get_product_listings_status(): string { if ( ! $this->merchant_center->is_ready_for_syncing() ) { return self::PRODUCT_LISTINGS_NOT_APPLICABLE; } return $this->product_sync_stats->get_count() > 0 ? self::PRODUCT_LISTINGS_SYNC_IN_PROGRESS : self::PRODUCT_LISTINGS_SYNCED; } /** * Returns the number of channel issues/errors (e.g. account-related errors, product synchronization issues, etc.). * * @return int The number of issues to resolve, or 0 if there are no issues with the channel. */ public function get_errors_count(): int { try { return $this->merchant_statuses->get_issues()['total']; } catch ( Exception $e ) { return 0; } } /** * Returns an array of marketing campaign types that the channel supports. * * @return MarketingCampaignType[] Array of marketing campaign type objects. */ public function get_supported_campaign_types(): array { return $this->campaign_types; } /** * Returns an array of the channel's marketing campaigns. * * @return MarketingCampaign[] */ public function get_campaigns(): array { if ( ! $this->ads->ads_id_exists() || ! $this->is_mcm_enabled() ) { return []; } try { $currency = $this->ads->get_ads_currency(); return array_map( function ( array $campaign_data ) use ( $currency ) { $cost = null; if ( isset( $campaign_data['amount'] ) ) { $cost = new Price( (string) $campaign_data['amount'], $currency ); } return new MarketingCampaign( (string) $campaign_data['id'], $this->campaign_types['google-ads'], $campaign_data['name'], admin_url( 'admin.php?page=wc-admin&path=/google/dashboard&subpath=/campaigns/edit&programId=' . $campaign_data['id'] ), $cost, ); }, $this->ads_campaign->get_campaigns() ); } catch ( ExceptionWithResponseData $e ) { return []; } } /** * Generate an array of supported marketing campaign types. * * @return MarketingCampaignType[] */ protected function generate_campaign_types(): array { return [ 'google-ads' => new MarketingCampaignType( 'google-ads', $this, 'Google Ads', 'Boost your product listings with a campaign that is automatically optimized to meet your goals.', admin_url( 'admin.php?page=wc-admin&path=/google/dashboard&subpath=/campaigns/create' ), $this->get_icon_url() ), ]; } } PK!07src/MultichannelMarketing/MarketingChannelRegistrar.phpnu[marketing_channels = $wc->wc_get_container()->get( MarketingChannels::class ); $this->channel = $channel; } /** * Register as a WooCommerce marketing channel. */ public function register(): void { $this->marketing_channels->register( $this->channel ); } } PK!$src/Notes/AbstractNote.phpnu[get_name() ); } } /** * Get note data store. * * @see \Automattic\WooCommerce\Admin\Notes\DataStore for relavent data store. * * @return WC_Data_Store */ protected function get_data_store(): WC_Data_Store { return WC_Data_Store::load( 'admin-note' ); } /** * Check if the note has already been added. * * @return bool */ protected function has_been_added(): bool { $note_ids = $this->get_data_store()->get_notes_with_name( $this->get_name() ); return ! empty( $note_ids ); } } PK!Yp #src/Notes/AbstractSetupCampaign.phpnu[merchant_center_service = $merchant_center_service; } /** * Get the note entry. */ public function get_entry(): NoteEntry { $note = new NoteEntry(); $this->set_title_and_content( $note ); $this->add_common_note_settings( $note ); return $note; } /** * @param NoteEntry $note * * @return void */ protected function add_common_note_settings( NoteEntry $note ): void { $note->set_content_data( new stdClass() ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_layout( 'plain' ); $note->set_image( '' ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); } /** * Checks if a note can and should be added. * * Check if ads setup IS NOT complete * Check if it is > $this->get_gla_setup_days() days ago from DATE OF SETUP COMPLETION * Send notification * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() ) { return false; } if ( $this->ads_service->is_setup_complete() ) { return false; } if ( ! $this->gla_setup_for( $this->get_gla_setup_days() * DAY_IN_SECONDS ) ) { return false; } // We don't need to process exceptions here, as we're just determining whether to add a note. try { if ( $this->merchant_center_service->has_account_issues() ) { return false; } if ( ! $this->merchant_center_service->has_at_least_one_synced_product() ) { return false; } } catch ( Exception $e ) { return false; } return true; } /** * Get the number of days after which to add the note. * * @since 1.11.0 * * @return int */ abstract protected function get_gla_setup_days(): int; /** * Set the title and content of the Note. * * @since 1.11.0 * * @param NoteEntry $note * * @return void */ abstract protected function set_title_and_content( NoteEntry $note ): void; } PK!\Y66src/Notes/CompleteSetup.phpnu[set_title( __( 'Reach more shoppers with free listings on Google', 'google-listings-and-ads' ) ); $note->set_content( __( 'Finish setting up Google for WooCommerce to list your products on Google for free and promote them with ads.', 'google-listings-and-ads' ) ); $note->set_content_data( new stdClass() ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_layout( 'plain' ); $note->set_image( '' ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); $note->add_action( 'complete-setup', __( 'Finish setup', 'google-listings-and-ads' ), $this->get_start_url() ); return $note; } /** * Checks if a note can and should be added. * * Check if setup IS NOT complete * Check if a stores done 5 sales * Send notification * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() ) { return false; } if ( $this->merchant_center->is_setup_complete() ) { return false; } if ( ! $this->has_orders( 5 ) ) { return false; } return true; } } PK!D`  src/Notes/ContactInformation.phpnu[set_title( __( 'Please add your contact information', 'google-listings-and-ads' ) ); $note->set_content( __( 'Google requires the phone number and store address for all stores using Google Merchant Center. This is required to verify your store, and it will not be shown to customers. If you do not add your contact information, your listings may not appear on Google.', 'google-listings-and-ads' ) ); $note->set_content_data( (object) [] ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_layout( 'plain' ); $note->set_image( '' ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); $note->add_action( 'contact-information', __( 'Add contact information', 'google-listings-and-ads' ), $this->get_settings_url() ); return $note; } /** * Checks if a note can and should be added. * * Checks if merchant center has been setup and contact information is valid. * Send notification * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() ) { return false; } if ( ! $this->merchant_center->is_connected() ) { return false; } if ( $this->merchant_center->is_contact_information_setup() ) { return false; } return true; } } PK!iM\\$src/Notes/LeaveReviewActionTrait.phpnu[add_action( 'leave-review', __( 'Leave a review', 'google-listings-and-ads' ), wp_rand( 0, 1 ) ? $wp_link : $wc_link ); } } PK!D.8 src/Notes/NoteInitializer.phpnu[action_scheduler = $action_scheduler; } /** * Register the service. */ public function register(): void { add_action( self::CRON_HOOK, [ $this, 'add_notes' ] ); } /** * Loop through all notes to add any that should be added. */ public function add_notes(): void { $notes = $this->container->get( Note::class ); foreach ( $notes as $note ) { try { if ( $note->should_be_added() ) { $note->get_entry()->save(); } } catch ( Exception $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); } } } /** * Activate the service. * * @return void */ public function activate(): void { $this->maybe_add_cron_job(); } /** * Run's when plugin is installed or updated. * * @param string $old_version Previous version before updating. * @param string $new_version Current version after updating. */ public function install( string $old_version, string $new_version ): void { $this->maybe_add_cron_job(); } /** * Add notes cron job if it doesn't already exist. */ protected function maybe_add_cron_job(): void { if ( ! $this->action_scheduler->has_scheduled_action( self::CRON_HOOK ) ) { $this->action_scheduler->schedule_recurring( time(), DAY_IN_SECONDS, self::CRON_HOOK ); } } /** * Deactivate the service. * * Delete the notes cron job and all notes. */ public function deactivate(): void { try { $this->action_scheduler->cancel( self::CRON_HOOK ); } catch ( ActionSchedulerException $e ) { do_action( 'woocommerce_gla_exception', $e, __METHOD__ ); } // Ensure all note names are deleted if ( class_exists( Notes::class ) ) { $note_names = []; $notes = $this->container->get( Note::class ); foreach ( $notes as $note ) { $note_names[] = $note->get_name(); } Notes::delete_notes_with_name( $note_names ); } } } PK!UIPPsrc/Notes/Note.phpnu[connection = $connection; } /** * Get the note's unique name. * * @return string */ public function get_name(): string { return 'gla-reconnect-wordpress'; } /** * Get the note entry. */ public function get_entry(): NoteEntry { $note = new NoteEntry(); $note->set_title( __( 'Re-connect your store to Google for WooCommerce', 'google-listings-and-ads' ) ); $note->set_content( __( 'Your WordPress.com account has been disconnected from Google for WooCommerce. Connect your WordPress.com account again to ensure your products stay listed on Google through the Google for WooCommerce extension.

If you do not re-connect, any existing listings may be removed from Google.', 'google-listings-and-ads' ) ); $note->set_content_data( (object) [] ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); $note->add_action( 'reconnect-wordpress', __( 'Go to Google for WooCommerce', 'google-listings-and-ads' ), add_query_arg( 'subpath', '/reconnect-wpcom-account', $this->get_settings_url() ) ); return $note; } /** * Checks if a note can and should be added. * * - Triggers a status check if not already disconnected. * - Checks if Jetpack is disconnected. * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() || ! $this->merchant_center->is_setup_complete() ) { return false; } $this->maybe_check_status(); return ! $this->is_jetpack_connected(); } /** * Trigger a status check if we are not already disconnected. * A request to the server must be sent to detect a disconnect. */ protected function maybe_check_status() { if ( ! $this->is_jetpack_connected() ) { return; } try { $this->connection->get_status(); } catch ( Exception $e ) { return; } } } PK!#G~  src/Notes/ReviewAfterClicks.phpnu[merchant_metrics = $merchant_metrics; $this->wp = $wp; } /** * Get the note's unique name. * * @return string */ public function get_name(): string { return 'gla-review-after-clicks'; } /** * Get the note entry. * * @throws Exception When unable to get clicks data. */ public function get_entry(): NoteEntry { $clicks_count = $this->get_free_listing_clicks_count(); // Round to nearest 10 $clicks_count_rounded = floor( $clicks_count / 10 ) * 10; $note = new NoteEntry(); $note->set_title( sprintf( /* translators: %s number of clicks */ __( 'You’ve gotten %s+ clicks on your free listings! 🎉', 'google-listings-and-ads' ), $this->wp->number_format_i18n( $clicks_count_rounded ) ) ); $note->set_content( __( 'Congratulations! Tell us what you think about Google for WooCommerce by leaving a review. Your feedback will help us make WooCommerce even better for you.', 'google-listings-and-ads' ) ); $note->set_content_data( (object) [] ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); $this->add_leave_review_note_action( $note ); return $note; } /** * Checks if a note can and should be added. * * - checks there are more than 10 clicks * * @throws Exception When unable to get clicks data. * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() ) { return false; } $clicks_count = $this->get_free_listing_clicks_count(); return $clicks_count > 10; } /** * Get free listing clicks count. * * Will return 0 if account is not connected. * * @return int * * @throws Exception When unable to get data. */ protected function get_free_listing_clicks_count(): int { if ( ! $this->merchant_center->is_connected() ) { return 0; } $metrics = $this->merchant_metrics->get_cached_free_listing_metrics(); return empty( $metrics ) ? 0 : $metrics['clicks']; } } PK! $src/Notes/ReviewAfterConversions.phpnu[merchant_metrics = $merchant_metrics; $this->wp = $wp; } /** * Get the note's unique name. * * @return string */ public function get_name(): string { return 'gla-review-after-conversions'; } /** * Get ads conversions count. * * @return int * * @throws Exception When unable to get data. */ protected function get_ads_conversions_count(): int { $metrics = $this->merchant_metrics->get_cached_ads_metrics(); return empty( $metrics ) ? 0 : (int) $metrics['conversions']; } /** * Get the note entry. * * @throws Exception When unable to get data. */ public function get_entry(): NoteEntry { $note = new NoteEntry(); $note->set_title( __( 'You got your first conversion on Google Ads! 🎉', 'google-listings-and-ads' ), ); $note->set_content( __( 'Congratulations! Tell us what you think about Google for WooCommerce by leaving a review. Your feedback will help us make WooCommerce even better for you.', 'google-listings-and-ads' ) ); $note->set_content_data( (object) [] ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); $this->add_leave_review_note_action( $note ); return $note; } /** * Checks if a note can and should be added. * * - checks there is at least one ad conversion * * @throws Exception When unable to get data. * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() ) { return false; } if ( ! $this->ads_service->is_connected() ) { return false; } if ( $this->get_ads_conversions_count() < 1 ) { return false; } return true; } } PK!%F F src/Notes/SetupCampaign.phpnu[ads_service->is_setup_started() ) { $note->set_title( __( 'Launch ads to drive traffic and grow sales', 'google-listings-and-ads' ) ); $note->set_content( __( 'Your products are ready for Google Ads! Get your products shown on Google exactly when shoppers are searching for the products you offer. For new Google Ads accounts, get $500 in ad credit when you spend $500 within your first 60 days. T&Cs apply.', 'google-listings-and-ads' ) ); $note->add_action( 'setup-campaign', __( 'Set up Google Ads', 'google-listings-and-ads' ), $this->get_setup_ads_url(), NoteEntry::E_WC_ADMIN_NOTE_ACTIONED, true ); } else { $note->set_title( __( 'Finish connecting your Google Ads account', 'google-listings-and-ads' ) ); $note->set_content( __( 'Your products are ready for Google Ads! Finish connecting your account, create your campaign, pick your budget, and easily measure the impact of your ads. Plus, Google will give you $500 USD in ad credit when you spend $500 for new accounts. T&Cs apply.', 'google-listings-and-ads' ) ); $note->add_action( 'setup-campaign', __( 'Complete Setup', 'google-listings-and-ads' ), $this->get_setup_ads_url(), NoteEntry::E_WC_ADMIN_NOTE_ACTIONED, true ); } $note->add_action( 'setup-campaign-learn-more', __( 'Learn more', 'google-listings-and-ads' ), 'https://woocommerce.com/document/google-for-woocommerce/get-started/google-performance-max-campaigns' ); } } PK!,**#src/Notes/SetupCampaignTwoWeeks.phpnu[ads_service->is_setup_started() ) { $note->set_title( __( 'Reach more shoppers with Google Ads', 'google-listings-and-ads' ) ); $note->set_content( __( 'Your products are ready for Google Ads! Connect with the right shoppers at the right moment when they’re searching for products like yours. Connect your Google Ads account to create your first campaign.', 'google-listings-and-ads' ) ); $note->add_action( 'setup-campaign', __( 'Set up Google Ads', 'google-listings-and-ads' ), $this->get_setup_ads_url(), NoteEntry::E_WC_ADMIN_NOTE_ACTIONED, true ); } else { $note->set_title( __( 'Finish setting up your ads campaign and boost your sales', 'google-listings-and-ads' ) ); $note->set_content( __( "You're just a few steps away from reaching new shoppers across Google. Finish connecting your account, create your campaign, pick your budget, and easily measure the impact of your ads.", 'google-listings-and-ads' ) ); $note->add_action( 'setup-campaign', __( 'Complete Setup', 'google-listings-and-ads' ), $this->get_setup_ads_url(), NoteEntry::E_WC_ADMIN_NOTE_ACTIONED, true ); } } } PK! } src/Notes/SetupCouponSharing.phpnu[merchant_statuses = $merchant_statuses; } /** * Get the note's unique name. * * @return string */ public function get_name(): string { return 'gla-coupon-optin'; } /** * Get the note entry. */ public function get_entry(): NoteEntry { $note = new NoteEntry(); $note->set_title( __( 'Show your store coupons on your Google listings', 'google-listings-and-ads' ) ); $note->set_content( __( 'Sync your store promotions and coupons directly with Google to showcase on your product listings across the Google Shopping tab.

When creating a coupon, you’ll see a Channel Visibility settings box on the right; select "Show coupon on Google" to enable.', 'google-listings-and-ads' ) ); $note->set_content_data( (object) [] ); $note->set_type( NoteEntry::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_layout( 'plain' ); $note->set_image( '' ); $note->set_name( $this->get_name() ); $note->set_source( $this->get_slug() ); $note->add_action( 'coupon-views', __( 'Go to coupons', 'google-listings-and-ads' ), $this->get_coupons_url() ); return $note; } /** * Checks if a note can and should be added. We insert the notes only when all the conditions are satisfied: * 1. Store is in promotion supported country * 2. Store has at least one active product in Merchant Center * 3. Store has coupons created, but no coupons synced with Merchant Center * 4. Store has Ads account connected and has been setup for >3 days OR no Ads account and >17 days * * @return bool */ public function should_be_added(): bool { if ( $this->has_been_added() ) { return false; } if ( ! $this->merchant_center->is_promotion_supported_country() ) { return false; } // Check if there are synced products. try { $statuses = $this->merchant_statuses->get_product_statistics(); if ( isset( $statuses['statistics']['active'] ) && $statuses['statistics']['active'] <= 0 ) { return false; } } catch ( Exception $e ) { return false; } // Check if merchants have created coupons. $coupons = get_posts( [ 'post_type' => 'shop_coupon' ] ); $shared_coupons = get_posts( [ 'post_type' => 'shop_coupon', 'meta_key' => CouponMetaHandler::KEY_VISIBILITY, 'meta_value' => ChannelVisibility::SYNC_AND_SHOW, ] ); if ( empty( $coupons ) || ! empty( $shared_coupons ) ) { return false; } if ( $this->ads_service->is_setup_complete() ) { if ( ! $this->gla_setup_for( 3 * DAY_IN_SECONDS ) ) { return false; } } elseif ( ! $this->gla_setup_for( 17 * DAY_IN_SECONDS ) ) { return false; } return true; } } PK!c\ \ src/Options/AccountState.phpnu[options->get( $this->option_name(), [] ); if ( empty( $state ) && $initialize_if_not_found ) { $state = []; foreach ( $this->account_creation_steps() as $step ) { $state[ $step ] = [ 'status' => self::STEP_PENDING, 'message' => '', 'data' => [], ]; } $this->update( $state ); } return $state; } /** * Update the account state option. * * @param array $state */ public function update( array $state ) { $this->options->update( $this->option_name(), $state ); } /** * Mark a step as completed. * * @param string $step Name of the completed step. */ public function complete_step( string $step ) { $state = $this->get( false ); if ( isset( $state[ $step ] ) ) { $state[ $step ]['status'] = self::STEP_DONE; $this->update( $state ); } } /** * Returns the name of the last incompleted step. * * @return string */ public function last_incomplete_step(): string { $incomplete = ''; foreach ( $this->get( false ) as $name => $step ) { if ( ! isset( $step['status'] ) || self::STEP_DONE !== $step['status'] ) { $incomplete = $name; break; } } return $incomplete; } /** * Returns any data from a specific step. * * @param string $step Step name. * @return array */ public function get_step_data( string $step ): array { return $this->get( false )[ $step ]['data'] ?? []; } } PK!kttsrc/Options/AdsAccountState.phpnu[set_completed_timestamp(); } ); } /** * Set the timestamp when setup was completed. */ protected function set_completed_timestamp() { $this->options->update( self::OPTION, time() ); } } PK!gtĥ$src/Options/MerchantAccountState.phpnu[options->get( OptionsInterface::SITE_VERIFICATION ); return ! empty( $current_options['verified'] ) && SiteVerification::VERIFICATION_STATUS_VERIFIED === $current_options['verified']; } /** * Calculate the number of seconds to wait after creating a sub-account and * before operating on the new sub-account (MCA link and website claim). * * @return int */ public function get_seconds_to_wait_after_created(): int { $state = $this->get( false ); $created_timestamp = $state['set_id']['data']['created_timestamp'] ?? 0; $seconds_elapsed = time() - $created_timestamp; return max( 0, self::MC_DELAY_AFTER_CREATE - $seconds_elapsed ); } } PK!"\z&src/Options/MerchantSetupCompleted.phpnu[set_contact_information_setup(); $this->set_completed_timestamp(); } ); } /** * Mark the contact information as setup. * * @since 1.4.0 */ protected function set_contact_information_setup() { $this->options->update( OptionsInterface::CONTACT_INFO_SETUP, true ); } /** * Set the timestamp when setup was completed. */ protected function set_completed_timestamp() { $this->options->update( self::OPTION, time() ); } } PK!gZ%src/Options/OptionsAwareInterface.phpnu[options = $options; } } PK!A src/Options/OptionsInterface.phpnu[ true, self::ADS_ACCOUNT_OCID => true, self::ADS_ACCOUNT_STATE => true, self::ADS_BILLING_URL => true, self::ADS_ID => true, self::ADS_CONVERSION_ACTION => true, self::ADS_SETUP_COMPLETED_AT => true, self::CAMPAIGN_CONVERT_STATUS => true, self::CLAIMED_URL_HASH => true, self::CONTACT_INFO_SETUP => true, self::DB_VERSION => true, self::FILE_VERSION => true, self::GOOGLE_CONNECTED => true, self::INSTALL_TIMESTAMP => true, self::INSTALL_VERSION => true, self::JETPACK_CONNECTED => true, self::MC_SETUP_COMPLETED_AT => true, self::MERCHANT_ACCOUNT_STATE => true, self::MERCHANT_CENTER => true, self::MERCHANT_ID => true, self::DELAYED_ACTIVATE => true, self::SHIPPING_RATES => true, self::SHIPPING_TIMES => true, self::REDIRECT_TO_ONBOARDING => true, self::SITE_VERIFICATION => true, self::SYNCABLE_PRODUCTS_COUNT => true, self::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA => true, self::PRODUCT_STATUSES_COUNT_INTERMEDIATE_DATA => true, self::TARGET_AUDIENCE => true, self::TOURS => true, self::UPDATE_ALL_PRODUCTS_LAST_SYNC => true, self::WP_TOS_ACCEPTED => true, self::WPCOM_REST_API_STATUS => true, self::GOOGLE_WPCOM_AUTH_NONCE => true, self::GTIN_MIGRATION_STATUS => true, ]; public const OPTION_TYPES = [ self::ADS_ID => PositiveInteger::class, self::MERCHANT_ID => PositiveInteger::class, ]; /** * Get an option. * * @param string $name The option name. * @param mixed $default_value A default value for the option. * * @return mixed */ public function get( string $name, $default_value = null ); /** * Add an option. * * @param string $name The option name. * @param mixed $value The option value. * * @return bool */ public function add( string $name, $value ): bool; /** * Update an option. * * @param string $name The option name. * @param mixed $value The option value. * * @return bool */ public function update( string $name, $value ): bool; /** * Delete an option. * * @param string $name The option name. * * @return bool */ public function delete( string $name ): bool; /** * Helper function to retrieve the Merchant Account ID. * * @return int */ public function get_merchant_id(): int; /** * Returns all available option keys. * * @return array */ public static function get_all_option_keys(): array; /** * Helper function to retrieve the Ads Account ID. * * @return int */ public function get_ads_id(): int; /** * If the WPCOM API is authorized * * @return bool */ public function is_wpcom_api_authorized(): bool; } PK!ْC<<src/Options/Options.phpnu[validate_option_key( $name ); if ( ! array_key_exists( $name, $this->options ) ) { $value = get_option( $this->prefix_name( $name ), $default_value ); $this->options[ $name ] = $this->maybe_cast_value( $name, $value ); } return $this->raw_value( $this->options[ $name ] ); } /** * Add an option. * * @param string $name The option name. * @param mixed $value The option value. * * @return bool */ public function add( string $name, $value ): bool { $this->validate_option_key( $name ); $value = $this->maybe_convert_value( $name, $value ); $this->options[ $name ] = $value; $result = add_option( $this->prefix_name( $name ), $this->raw_value( $value ) ); do_action( "woocommerce_gla_options_updated_{$name}", $value ); return $result; } /** * Update an option. * * @param string $name The option name. * @param mixed $value The option value. * * @return bool */ public function update( string $name, $value ): bool { $this->validate_option_key( $name ); $value = $this->maybe_convert_value( $name, $value ); $this->options[ $name ] = $value; $result = update_option( $this->prefix_name( $name ), $this->raw_value( $value ) ); do_action( "woocommerce_gla_options_updated_{$name}", $value ); return $result; } /** * Delete an option. * * @param string $name The option name. * * @return bool */ public function delete( string $name ): bool { $this->validate_option_key( $name ); unset( $this->options[ $name ] ); $result = delete_option( $this->prefix_name( $name ) ); do_action( "woocommerce_gla_options_deleted_{$name}" ); return $result; } /** * Helper function to retrieve the Ads Account ID. * * @return int */ public function get_ads_id(): int { // TODO: Remove overriding with default once ConnectionTest is removed. $default = intval( $_GET['customer_id'] ?? 0 ); // phpcs:ignore WordPress.Security return $default ?: $this->get( self::ADS_ID ); } /** * Helper function to retrieve the Merchant Account ID. * * @return int */ public function get_merchant_id(): int { // TODO: Remove overriding with default once ConnectionTest is removed. $default = intval( $_GET['merchant_id'] ?? 0 ); // phpcs:ignore WordPress.Security return $default ?: $this->get( self::MERCHANT_ID ); } /** * Returns all available option keys. * * @return array */ public static function get_all_option_keys(): array { return array_keys( self::VALID_OPTIONS ); } /** * Ensure that a given option key is valid. * * @param string $name The option name. * * @throws InvalidOption When the option key is not valid. */ protected function validate_option_key( string $name ) { if ( ! array_key_exists( $name, self::VALID_OPTIONS ) ) { throw InvalidOption::invalid_name( $name ); } } /** * Cast to a specific value type. * * @param string $name The option name. * @param mixed $value The option value. * * @return mixed */ protected function maybe_cast_value( string $name, $value ) { if ( isset( self::OPTION_TYPES[ $name ] ) ) { /** @var CastableValueInterface $class */ $class = self::OPTION_TYPES[ $name ]; $value = $class::cast( $value ); } return $value; } /** * Convert to a specific value type. * * @param string $name The option name. * @param mixed $value The option value. * * @return mixed * @throws InvalidValue When the value is invalid. */ protected function maybe_convert_value( string $name, $value ) { if ( isset( self::OPTION_TYPES[ $name ] ) ) { $class = self::OPTION_TYPES[ $name ]; $value = new $class( $value ); } return $value; } /** * Return raw value. * * @param mixed $value Possible object value. * * @return mixed */ protected function raw_value( $value ) { return $value instanceof ValueInterface ? $value->get() : $value; } /** * Prefix an option name with the plugin prefix. * * @param string $name * * @return string */ protected function prefix_name( string $name ): string { return "{$this->get_slug()}_{$name}"; } /** * Checks if WPCOM API is Authorized. * * @return bool */ public function is_wpcom_api_authorized(): bool { return $this->get( self::WPCOM_REST_API_STATUS ) === 'approved'; } } PK!ZEMM#src/Options/TransientsInterface.phpnu[ true, self::ADS_METRICS => true, self::FREE_LISTING_METRICS => true, self::MC_ACCOUNT_REVIEW => true, self::MC_IS_SUBACCOUNT => true, self::MC_STATUSES => true, self::URL_MATCHES => true, self::WPCOM_API_STATUS => true, ]; /** * Get a transient. * * @param string $name The transient name. * @param mixed $default_value A default value for the transient. * * @return mixed */ public function get( string $name, $default_value = null ); /** * Add or update a transient. * * @param string $name The transient name. * @param mixed $value The transient value. * @param int $expiration Time until expiration in seconds. * * @return bool */ public function set( string $name, $value, int $expiration = 0 ): bool; /** * Delete a transient. * * @param string $name The transient name. * * @return bool */ public function delete( string $name ): bool; /** * Returns all available transient keys. * * @return array * * @since 1.3.0 */ public static function get_all_transient_keys(): array; } PK! ܍ src/Options/Transients.phpnu[validate_transient_key( $name ); if ( ! array_key_exists( $name, $this->transients ) ) { $value = get_transient( $this->prefix_name( $name ) ); if ( false === $value ) { $value = $default_value; } $this->transients[ $name ] = $value; } return $this->transients[ $name ]; } /** * Add or update a transient. * * @param string $name The transient name. * @param mixed $value The transient value. * @param int $expiration Time until expiration in seconds. * * @return bool * * @throws InvalidValue If a boolean $value is provided. */ public function set( string $name, $value, int $expiration = 0 ): bool { if ( is_bool( $value ) ) { throw new InvalidValue( 'Transients cannot have boolean values.' ); } $this->validate_transient_key( $name ); $this->transients[ $name ] = $value; return boolval( set_transient( $this->prefix_name( $name ), $value, $expiration ) ); } /** * Delete a transient. * * @param string $name The transient name. * * @return bool */ public function delete( string $name ): bool { $this->validate_transient_key( $name ); unset( $this->transients[ $name ] ); return boolval( delete_transient( $this->prefix_name( $name ) ) ); } /** * Returns all available transient keys. * * @return array * * @since 1.3.0 */ public static function get_all_transient_keys(): array { return array_keys( self::VALID_OPTIONS ); } /** * Ensure that a given transient key is valid. * * @param string $name The transient name. * * @throws InvalidOption When the transient key is not valid. */ protected function validate_transient_key( string $name ) { if ( ! array_key_exists( $name, self::VALID_OPTIONS ) ) { throw InvalidOption::invalid_name( $name ); } } /** * Prefix a transient name with the plugin prefix. * * @param string $name * * @return string */ protected function prefix_name( string $name ): string { return "{$this->get_slug()}_{$name}"; } } PK!]? DD3src/Product/AttributeMapping/Traits/IsEnumTrait.phpnu[zz4src/Product/AttributeMapping/Traits/IsFieldTrait.phpnu[name ) ) { $attributes[ 'taxonomy:' . $taxonomy->name ] = $taxonomy->label; continue; } $taxonomies[ 'taxonomy:' . $taxonomy->name ] = $taxonomy->label; } asort( $taxonomies ); asort( $attributes ); $attributes = apply_filters( 'woocommerce_gla_attribute_mapping_sources_global_attributes', $attributes ); $taxonomies = apply_filters( 'woocommerce_gla_attribute_mapping_sources_taxonomies', $taxonomies ); if ( ! empty( $attributes ) ) { $sources = array_merge( [ 'disabled:attributes' => __( '- Global attributes -', 'google-listings-and-ads' ), ], $attributes ); } if ( ! empty( $taxonomies ) ) { $sources = array_merge( $sources, [ 'disabled:taxonomies' => __( '- Taxonomies -', 'google-listings-and-ads' ), ], $taxonomies ); } return $sources; } /** * Get a list of the available product sources. * * @return array An array with the available product sources. */ public static function get_source_product_fields(): array { $fields = [ 'product:backorders' => __( 'Allow backorders setting', 'google-listings-and-ads' ), 'product:title' => __( 'Product title', 'google-listings-and-ads' ), 'product:sku' => __( 'SKU', 'google-listings-and-ads' ), 'product:stock_quantity' => __( 'Stock Qty', 'google-listings-and-ads' ), 'product:stock_status' => __( 'Stock Status', 'google-listings-and-ads' ), 'product:tax_class' => __( 'Tax class', 'google-listings-and-ads' ), 'product:name' => __( 'Variation title', 'google-listings-and-ads' ), 'product:weight' => __( 'Weight (raw value, no units)', 'google-listings-and-ads' ), 'product:weight_with_unit' => __( 'Weight (with units)', 'google-listings-and-ads' ), ]; asort( $fields ); $fields = array_merge( [ 'disabled:product' => __( '- Product fields -', 'google-listings-and-ads' ), ], $fields ); return apply_filters( 'woocommerce_gla_attribute_mapping_sources_product_fields', $fields ); } /** * Allowing to register custom attributes by using a filter. * * @return array The custom attributes */ public static function get_source_custom_attributes(): array { $attributes = []; $attribute_keys = apply_filters( 'woocommerce_gla_attribute_mapping_sources_custom_attributes', [] ); foreach ( $attribute_keys as $key ) { $attributes[ 'attribute:' . $key ] = $key; } if ( ! empty( $attributes ) ) { $attributes = array_merge( [ 'disabled:attribute' => __( '- Custom Attributes -', 'google-listings-and-ads' ), ], $attributes ); } return $attributes; } } PK!i7  7src/Product/AttributeMapping/AttributeMappingHelper.phpnu[ $attribute::get_id(), 'label' => $attribute::get_name(), 'enum' => $attribute::is_enum(), ] ); } return $destinations; } /** * Get the attribute class based on attribute ID. * * @param string $attribute_id The attribute ID to get the class * @return string|null The attribute class path or null if it's not found */ public static function get_attribute_by_id( string $attribute_id ): ?string { foreach ( self::ATTRIBUTES_AVAILABLE_FOR_MAPPING as $class ) { if ( $class::get_id() === $attribute_id ) { return $class; } } return null; } /** * Get the sources for an attribute * * @param string $attribute_id The attribute ID to get the sources from. * @return array The sources for the attribute */ public function get_sources_for_attribute( string $attribute_id ): array { /** * @var AttributeInterface $attribute */ $attribute = self::get_attribute_by_id( $attribute_id ); $attribute_sources = []; if ( is_null( $attribute ) ) { return $attribute_sources; } foreach ( $attribute::get_sources() as $key => $value ) { array_push( $attribute_sources, [ 'id' => $key, 'label' => $value, ] ); } return $attribute_sources; } /** * Get the available conditions for the category. * * @return string[] The list of available category conditions */ public function get_category_condition_types(): array { return [ self::CATEGORY_CONDITION_TYPE_ALL, self::CATEGORY_CONDITION_TYPE_EXCEPT, self::CATEGORY_CONDITION_TYPE_ONLY, ]; } } PK!j9rr,src/Product/Attributes/AbstractAttribute.phpnu[set_value( $value ); } /** * Return the attribute type. Must be a valid PHP type. * * @return string * * @link https://www.php.net/manual/en/function.settype.php */ public static function get_value_type(): string { return 'string'; } /** * Returns the attribute value. * * @return mixed */ public function get_value() { return $this->value; } /** * @param mixed $value * * @return $this */ public function set_value( $value ): AbstractAttribute { $this->value = $this->cast_value( $value ); return $this; } /** * Casts the value to the attribute value type and returns the result. * * @param mixed $value * * @return mixed */ protected function cast_value( $value ) { if ( is_string( $value ) ) { $value = trim( $value ); if ( '' === $value ) { return null; } } $value_type = static::get_value_type(); if ( in_array( $value_type, [ 'bool', 'boolean' ], true ) ) { $value = wc_string_to_bool( $value ); } else { settype( $value, $value_type ); } return $value; } /** * Return an array of WooCommerce product types that this attribute can be applied to. * * @return array */ public static function get_applicable_product_types(): array { return [ 'simple', 'variable', 'variation' ]; } /** * @return string */ public function __toString() { return (string) $this->get_value(); } } PK!C src/Product/Attributes/Adult.phpnu[ __( 'Yes', 'google-listings-and-ads' ), 'no' => __( 'No', 'google-listings-and-ads' ), ]; } } PK!@U#src/Product/Attributes/AgeGroup.phpnu[ __( 'Newborn', 'google-listings-and-ads' ), 'infant' => __( 'Infant', 'google-listings-and-ads' ), 'toddler' => __( 'Toddler', 'google-listings-and-ads' ), 'kids' => __( 'Kids', 'google-listings-and-ads' ), 'adult' => __( 'Adult', 'google-listings-and-ads' ), ]; } /** * Return an array of WooCommerce product types that this attribute can be applied to. * * @return array */ public static function get_applicable_product_types(): array { return [ 'simple', 'variation' ]; } /** * Return the attribute's input class. Must be an instance of `AttributeInputInterface`. * * @return string * * @see AttributeInputInterface * * @since 1.5.0 */ public static function get_input_type(): string { return AgeGroupInput::class; } /** * Returns the attribute name * * @return string */ public static function get_name(): string { return __( 'Age group', 'google-listings-and-ads' ); } } PK!vss-src/Product/Attributes/AttributeInterface.phpnu[attribute_mapping_rules_query = $attribute_mapping_rules_query; $this->wc = $wc; } /** * @param WC_Product $product * @param AttributeInterface $attribute * * @throws InvalidValue If the attribute is invalid for the given product. */ public function update( WC_Product $product, AttributeInterface $attribute ) { $this->validate( $product, $attribute::get_id() ); if ( null === $attribute->get_value() || '' === $attribute->get_value() ) { $this->delete( $product, $attribute::get_id() ); return; } $value = $attribute->get_value(); if ( in_array( $attribute::get_value_type(), [ 'bool', 'boolean' ], true ) ) { $value = wc_bool_to_string( $value ); } $product->update_meta_data( $this->prefix_meta_key( $attribute::get_id() ), $value ); $product->save_meta_data(); } /** * @param WC_Product $product * @param string $attribute_id * * @return AttributeInterface|null * * @throws InvalidValue If the attribute ID is invalid for the given product. */ public function get( WC_Product $product, string $attribute_id ): ?AttributeInterface { $this->validate( $product, $attribute_id ); $value = null; if ( $this->exists( $product, $attribute_id ) ) { $value = $product->get_meta( $this->prefix_meta_key( $attribute_id ), true ); } if ( null === $value || '' === $value ) { return null; } $attribute_class = $this->get_attribute_types_for_product( $product )[ $attribute_id ]; return new $attribute_class( $value ); } /** * Return all attribute values for the given product, after the mapping rules, GLA attributes, and filters have been applied. * GLA Attributes has priority over the product attributes. * * @since 2.8.0 * * @param WC_Product $product * * @return array of attribute values * @throws InvalidValue When the product does not exist. */ public function get_all_aggregated_values( WC_Product $product ) { $attributes = $this->get_all_values( $product ); $parent_product = null; // merge with parent's attributes if it's a variation product if ( $product instanceof WC_Product_Variation ) { $parent_product = $this->wc->get_product( $product->get_parent_id() ); $parent_attributes = $this->get_all_values( $parent_product ); $attributes = array_merge( $parent_attributes, $attributes ); } $mapping_rules = $this->attribute_mapping_rules_query->get_results(); $adapted_product = new WCProductAdapter( [ 'wc_product' => $product, 'parent_wc_product' => $parent_product, 'targetCountry' => 'US', // targetCountry is required to create a new WCProductAdapter instance, but it's not used in the attributes context. 'gla_attributes' => $attributes, 'mapping_rules' => $mapping_rules, ] ); foreach ( self::ATTRIBUTES as $attribute_class ) { $attribute_id = $attribute_class::get_id(); if ( $attribute_id === 'size' ) { $attribute_id = 'sizes'; } if ( isset( $adapted_product->$attribute_id ) ) { $attributes[ $attribute_id ] = $adapted_product->$attribute_id; } } return $attributes; } /** * Return attribute value. * * @param WC_Product $product * @param string $attribute_id * * @return mixed|null */ public function get_value( WC_Product $product, string $attribute_id ) { $attribute = $this->get( $product, $attribute_id ); return $attribute instanceof AttributeInterface ? $attribute->get_value() : null; } /** * Return all attributes for the given product * * @param WC_Product $product * * @return AttributeInterface[] */ public function get_all( WC_Product $product ): array { $all_attributes = []; foreach ( array_keys( $this->get_attribute_types_for_product( $product ) ) as $attribute_id ) { $attribute = $this->get( $product, $attribute_id ); if ( null !== $attribute ) { $all_attributes[ $attribute_id ] = $attribute; } } return $all_attributes; } /** * Return all attribute values for the given product * * @param WC_Product $product * * @return array of attribute values */ public function get_all_values( WC_Product $product ): array { $all_attributes = []; foreach ( array_keys( $this->get_attribute_types_for_product( $product ) ) as $attribute_id ) { $attribute = $this->get_value( $product, $attribute_id ); if ( null !== $attribute ) { $all_attributes[ $attribute_id ] = $attribute; } } return $all_attributes; } /** * @param WC_Product $product * @param string $attribute_id * * @throws InvalidValue If the attribute ID is invalid for the given product. */ public function delete( WC_Product $product, string $attribute_id ) { $this->validate( $product, $attribute_id ); $product->delete_meta_data( $this->prefix_meta_key( $attribute_id ) ); $product->save_meta_data(); } /** * Whether the attribute exists and has been set for the product. * * @param WC_Product $product * @param string $attribute_id * * @return bool * * @since 1.2.0 */ public function exists( WC_Product $product, string $attribute_id ): bool { return $product->meta_exists( $this->prefix_meta_key( $attribute_id ) ); } /** * Returns an array of attribute types for the given product * * @param WC_Product $product * * @return string[] of attribute classes mapped to attribute IDs */ public function get_attribute_types_for_product( WC_Product $product ): array { return $this->get_attribute_types_for_product_types( [ $product->get_type() ] ); } /** * Returns an array of attribute types for the given product types * * @param string[] $product_types array of WooCommerce product types * * @return string[] of attribute classes mapped to attribute IDs */ public function get_attribute_types_for_product_types( array $product_types ): array { // flip the product types array to have them as array keys $product_types_keys = array_flip( $product_types ); // intersect the product types with our stored attributes map to get arrays of attributes matching the given product types $match_attributes = array_intersect_key( $this->get_attribute_types_map(), $product_types_keys ); // re-index the attributes map array to avoid string ($product_type) array keys $match_attributes = array_values( $match_attributes ); if ( empty( $match_attributes ) ) { return []; } // merge all of the attribute arrays from the map (there might be duplicates) and return the results return array_merge( ...$match_attributes ); } /** * Returns all available attribute IDs. * * @return array * * @since 1.3.0 */ public static function get_available_attribute_ids(): array { $attributes = []; foreach ( self::get_available_attribute_types() as $attribute_type ) { if ( method_exists( $attribute_type, 'get_id' ) ) { $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); $attributes[ $attribute_id ] = $attribute_id; } } return $attributes; } /** * Return an array of all available attribute class names. * * @return string[] Attribute class names * * @since 1.3.0 */ public static function get_available_attribute_types(): array { /** * Filters the list of available product attributes. * * @param string[] $attributes Array of attribute class names (FQN) */ return apply_filters( 'woocommerce_gla_product_attribute_types', self::ATTRIBUTES ); } /** * Returns an array of attribute types for all product types * * @return string[][] of attribute classes mapped to product types */ protected function get_attribute_types_map(): array { if ( ! isset( $this->attribute_types_map ) ) { $this->map_attribute_types(); } return $this->attribute_types_map; } /** * @param WC_Product $product * @param string $attribute_id * * @throws InvalidValue If the attribute type is invalid for the given product. */ protected function validate( WC_Product $product, string $attribute_id ) { $attribute_types = $this->get_attribute_types_for_product( $product ); if ( ! isset( $attribute_types[ $attribute_id ] ) ) { do_action( 'woocommerce_gla_error', sprintf( 'Attribute "%s" is not supported for a "%s" product (ID: %s).', $attribute_id, $product->get_type(), $product->get_id() ), __METHOD__ ); throw InvalidValue::not_in_allowed_list( 'attribute_id', array_keys( $attribute_types ) ); } } /** * @throws InvalidClass If any of the given attribute classes do not implement the AttributeInterface. */ protected function map_attribute_types(): void { $this->attribute_types_map = []; foreach ( self::get_available_attribute_types() as $attribute_type ) { $this->validate_interface( $attribute_type, AttributeInterface::class ); $attribute_id = call_user_func( [ $attribute_type, 'get_id' ] ); $applicable_types = call_user_func( [ $attribute_type, 'get_applicable_product_types' ] ); /** * Filters the list of applicable product types for each attribute. * * @param string[] $applicable_types Array of WooCommerce product types * @param string $attribute_type Attribute class name (FQN) */ $applicable_types = apply_filters( "woocommerce_gla_attribute_applicable_product_types_{$attribute_id}", $applicable_types, $attribute_type ); foreach ( $applicable_types as $product_type ) { $this->attribute_types_map[ $product_type ] = $this->attribute_types_map[ $product_type ] ?? []; $this->attribute_types_map[ $product_type ][ $attribute_id ] = $attribute_type; } } } } PK!c&?f+src/Product/Attributes/AvailabilityDate.phpnu[ __( 'New', 'google-listings-and-ads' ), 'refurbished' => __( 'Refurbished', 'google-listings-and-ads' ), 'used' => __( 'Used', 'google-listings-and-ads' ), ]; } /** * Return the attribute's input class. Must be an instance of `AttributeInputInterface`. * * @return string * * @see AttributeInputInterface * * @since 1.5.0 */ public static function get_input_type(): string { return ConditionInput::class; } /** * Returns the attribute name * * @return string */ public static function get_name(): string { return __( 'Condition', 'google-listings-and-ads' ); } } PK!?Xhh!src/Product/Attributes/Gender.phpnu[ __( 'Male', 'google-listings-and-ads' ), 'female' => __( 'Female', 'google-listings-and-ads' ), 'unisex' => __( 'Unisex', 'google-listings-and-ads' ), ]; } /** * Return an array of WooCommerce product types that this attribute can be applied to. * * @return array */ public static function get_applicable_product_types(): array { return [ 'simple', 'variation' ]; } /** * Return the attribute's input class. Must be an instance of `AttributeInputInterface`. * * @return string * * @see AttributeInputInterface * * @since 1.5.0 */ public static function get_input_type(): string { return GenderInput::class; } /** * Returns the attribute name * * @return string */ public static function get_name(): string { return __( 'Gender', 'google-listings-and-ads' ); } } PK!Nsrc/Product/Attributes/GTIN.phpnu[ __( 'Yes', 'google-listings-and-ads' ), 'no' => __( 'No', 'google-listings-and-ads' ), ]; } } PK!$r#src/Product/Attributes/Material.phpnu[ __( 'US', 'google-listings-and-ads' ), 'EU' => __( 'EU', 'google-listings-and-ads' ), 'UK' => __( 'UK', 'google-listings-and-ads' ), 'DE' => __( 'DE', 'google-listings-and-ads' ), 'FR' => __( 'FR', 'google-listings-and-ads' ), 'IT' => __( 'IT', 'google-listings-and-ads' ), 'AU' => __( 'AU', 'google-listings-and-ads' ), 'BR' => __( 'BR', 'google-listings-and-ads' ), 'CN' => __( 'CN', 'google-listings-and-ads' ), 'JP' => __( 'JP', 'google-listings-and-ads' ), 'MEX' => __( 'MEX', 'google-listings-and-ads' ), ]; } /** * Return the attribute's input class. Must be an instance of `AttributeInputInterface`. * * @return string * * @see AttributeInputInterface * * @since 1.5.0 */ public static function get_input_type(): string { return SizeSystemInput::class; } /** * Returns the attribute name * * @return string */ public static function get_name(): string { return __( 'Size System', 'google-listings-and-ads' ); } } PK!Kpp#src/Product/Attributes/SizeType.phpnu[ __( 'Regular', 'google-listings-and-ads' ), 'petite' => __( 'Petite', 'google-listings-and-ads' ), 'plus' => __( 'Plus', 'google-listings-and-ads' ), 'tall' => __( 'Tall', 'google-listings-and-ads' ), 'big' => __( 'Big', 'google-listings-and-ads' ), 'maternity' => __( 'Maternity', 'google-listings-and-ads' ), ]; } /** * Return the attribute's input class. Must be an instance of `AttributeInputInterface`. * * @return string * * @see AttributeInputInterface * * @since 1.5.0 */ public static function get_input_type(): string { return SizeTypeInput::class; } /** * Returns the attribute name * * @return string */ public static function get_name(): string { return __( 'Size Type', 'google-listings-and-ads' ); } } PK!o /src/Product/Attributes/WithMappingInterface.phpnu[meta_handler = $meta_handler; $this->product_helper = $product_helper; $this->validator = $validator; $this->product_factory = $product_factory; $this->target_audience = $target_audience; $this->attribute_mapping_rules_query = $attribute_mapping_rules_query; } /** * Filters and returns only the products already synced with Google Merchant Center. * * @param WC_Product[] $products * * @return WC_Product[] The synced products. */ public function filter_synced_products( array $products ): array { return array_filter( $products, [ $this->product_helper, 'is_product_synced' ] ); } /** * @param BatchProductEntry $product_entry */ public function mark_as_synced( BatchProductEntry $product_entry ) { $wc_product = $this->product_helper->get_wc_product( $product_entry->get_wc_product_id() ); $google_product = $product_entry->get_google_product(); $this->validate_instanceof( $google_product, GoogleProduct::class ); $this->product_helper->mark_as_synced( $wc_product, $google_product ); } /** * @param BatchProductEntry $product_entry */ public function mark_as_unsynced( BatchProductEntry $product_entry ) { try { $wc_product = $this->product_helper->get_wc_product( $product_entry->get_wc_product_id() ); } catch ( InvalidValue $exception ) { return; } $this->product_helper->mark_as_unsynced( $wc_product ); } /** * Mark a batch of WooCommerce product IDs as unsynced. * Invalid products will be skipped. * * @since 1.12.0 * * @param array $product_ids */ public function mark_batch_as_unsynced( array $product_ids ) { foreach ( $product_ids as $product_id ) { try { $product = $this->product_helper->get_wc_product( $product_id ); } catch ( InvalidValue $exception ) { continue; } $this->product_helper->mark_as_unsynced( $product ); } } /** * Marks a WooCommerce product as invalid and stores the errors in a meta data key. * * Note: If a product variation is invalid then the parent product is also marked as invalid. * * @param BatchInvalidProductEntry $product_entry */ public function mark_as_invalid( BatchInvalidProductEntry $product_entry ) { $wc_product = $this->product_helper->get_wc_product( $product_entry->get_wc_product_id() ); $errors = $product_entry->get_errors(); $this->product_helper->mark_as_invalid( $wc_product, $errors ); } /** * Generates an array map containing the Google product IDs as key and the WooCommerce product IDs as values. * * @param WC_Product[] $products * * @return BatchProductIDRequestEntry[] */ public function generate_delete_request_entries( array $products ): array { $request_entries = []; foreach ( $products as $product ) { $this->validate_instanceof( $product, WC_Product::class ); if ( $product instanceof WC_Product_Variable ) { $request_entries = array_merge( $request_entries, $this->generate_delete_request_entries( $product->get_available_variations( 'objects' ) ) ); continue; } $google_ids = $this->product_helper->get_synced_google_product_ids( $product ); if ( empty( $google_ids ) ) { continue; } foreach ( $google_ids as $google_id ) { $request_entries[ $google_id ] = new BatchProductIDRequestEntry( $product->get_id(), $google_id ); } } return $request_entries; } /** * @param WC_Product[] $products * * @return BatchProductRequestEntry[] */ public function validate_and_generate_update_request_entries( array $products ): array { $request_entries = []; $mapping_rules = $this->attribute_mapping_rules_query->get_results(); foreach ( $products as $product ) { $this->validate_instanceof( $product, WC_Product::class ); try { if ( ! $this->product_helper->is_sync_ready( $product ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Skipping product (ID: %s) because it is not ready to be synced.', $product->get_id() ), __METHOD__ ); continue; } if ( $product instanceof WC_Product_Variable ) { $request_entries = array_merge( $request_entries, $this->validate_and_generate_update_request_entries( $product->get_available_variations( 'objects' ) ) ); continue; } $target_countries = $this->target_audience->get_target_countries(); $main_target_country = $this->target_audience->get_main_target_country(); // validate the product $adapted_product = $this->product_factory->create( $product, $main_target_country, $mapping_rules ); $validation_result = $this->validate_product( $adapted_product ); if ( $validation_result instanceof BatchInvalidProductEntry ) { $this->mark_as_invalid( $validation_result ); do_action( 'woocommerce_gla_debug_message', sprintf( 'Skipping product (ID: %s) because it does not pass validation: %s', $product->get_id(), wp_json_encode( $validation_result ) ), __METHOD__ ); continue; } // add shipping for all selected target countries array_walk( $target_countries, [ $adapted_product, 'add_shipping_country' ] ); $request_entries[] = new BatchProductRequestEntry( $product->get_id(), $adapted_product ); } catch ( GoogleListingsAndAdsException $exception ) { do_action( 'woocommerce_gla_error', sprintf( 'Skipping product (ID: %s) due to exception: %s', $product->get_id(), $exception->getMessage() ), __METHOD__ ); continue; } } return $request_entries; } /** * @param WCProductAdapter $product * * @return BatchInvalidProductEntry|true */ protected function validate_product( WCProductAdapter $product ) { $violations = $this->validator->validate( $product ); if ( 0 !== count( $violations ) ) { $invalid_product = new BatchInvalidProductEntry( $product->get_wc_product()->get_id() ); $invalid_product->map_validation_violations( $violations ); return $invalid_product; } return true; } /** * Filters and returns an array of request entries for Google products that should no longer be submitted for the selected target audience. * * @param WC_Product[] $products * * @return BatchProductIDRequestEntry[] */ public function generate_stale_products_request_entries( array $products ): array { $target_audience = $this->target_audience->get_target_countries(); $request_entries = []; foreach ( $products as $product ) { $google_ids = $this->meta_handler->get_google_ids( $product ) ?: []; $stale_ids = array_diff_key( $google_ids, array_flip( $target_audience ) ); foreach ( $stale_ids as $stale_id ) { $request_entries[ $stale_id ] = new BatchProductIDRequestEntry( $product->get_id(), $stale_id ); } } return $request_entries; } /** * Returns an array of request entries for Google products that should no * longer be submitted for every target country. * * @since 1.1.0 * * @param WC_Product[] $products * * @return BatchProductIDRequestEntry[] */ public function generate_stale_countries_request_entries( array $products ): array { $main_target_country = $this->target_audience->get_main_target_country(); $request_entries = []; foreach ( $products as $product ) { $google_ids = $this->meta_handler->get_google_ids( $product ) ?: []; $stale_ids = array_diff_key( $google_ids, array_flip( [ $main_target_country ] ) ); foreach ( $stale_ids as $stale_id ) { $request_entries[ $stale_id ] = new BatchProductIDRequestEntry( $product->get_id(), $stale_id ); } } return $request_entries; } } PK!GFxx#src/Product/FilteredProductList.phpnu[products = $products; $this->unfiltered_count = $unfiltered_count; } /** * Get the list of products. * * @return WC_Product[] */ public function get(): array { return $this->products; } /** * Get product IDs. * * @return int[] */ public function get_product_ids(): array { return array_map( function ( $product ) { if ( $product instanceof WC_Product ) { return $product->get_id(); } return $product; }, $this->products ); } /** * Get the unfiltered amount of results. * * @return int */ public function get_unfiltered_count(): int { return $this->unfiltered_count; } /** * Count products for Countable. * * @return int */ public function count(): int { return count( $this->products ); } } PK!{  src/Product/ProductFactory.phpnu[attribute_manager = $attribute_manager; $this->wc = $wc; } /** * @param WC_Product $product * @param string $target_country * @param array $mapping_rules The mapping rules setup by the user * * @return WCProductAdapter * * @throws InvalidValue When the product is a variation and its parent does not exist. */ public function create( WC_Product $product, string $target_country, array $mapping_rules ): WCProductAdapter { // We do not support syncing the parent variable product. Each variation is synced individually instead. $this->validate_not_instanceof( $product, WC_Product_Variable::class ); $attributes = $this->attribute_manager->get_all_values( $product ); $parent_product = null; // merge with parent's attributes if it's a variation product if ( $product instanceof WC_Product_Variation ) { $parent_product = $this->wc->get_product( $product->get_parent_id() ); $parent_attributes = $this->attribute_manager->get_all_values( $parent_product ); $attributes = array_merge( $parent_attributes, $attributes ); } return new WCProductAdapter( [ 'wc_product' => $product, 'parent_wc_product' => $parent_product, 'targetCountry' => $target_country, 'gla_attributes' => $attributes, 'mapping_rules' => $mapping_rules, ] ); } } PK!F9 9 src/Product/ProductFilter.phpnu[product_helper = $product_helper; } /** * Filters and returns a list of products that are ready to be submitted to Google Merchant Center. * * @param WC_Product[] $products * * @return FilteredProductList */ public function filter_sync_ready_products( array $products ): FilteredProductList { $unfiltered_count = count( $products ); /** * Filters the list of products ready to be synced (before applying filters to check failures and sync-ready status). * * @param WC_Product[] $products Sync-ready WooCommerce products */ $products = apply_filters( 'woocommerce_gla_get_sync_ready_products_pre_filter', $products ); $results = array_values( array_filter( $products, function ( $product ) { return $this->product_helper->is_sync_ready( $product ) && ! $this->product_helper->is_sync_failed_recently( $product ); } ) ); /** * Filters the list of products ready to be synced (after applying filters to check failures and sync-ready status). * * @param WC_Product[] $results Sync-ready WooCommerce products */ $results = apply_filters( 'woocommerce_gla_get_sync_ready_products_filter', $results ); return new FilteredProductList( $results, $unfiltered_count ); } /** * Filter and return a list of products that can be deleted. * * @since 1.12.0 * * @param WC_Product[] $products * * @return FilteredProductList */ public function filter_products_for_delete( array $products ): FilteredProductList { $results = array_values( array_filter( $products, function ( $product ) { return ! $this->product_helper->is_delete_failed_threshold_reached( $product ); } ) ); return new FilteredProductList( $results, count( $products ) ); } } PK!N >meta_handler = $meta_handler; $this->wc = $wc; $this->target_audience = $target_audience; } /** * Mark the item as notified. * * @param WC_Product $product * * @return void */ public function mark_as_notified( $product ): void { $this->meta_handler->delete_failed_delete_attempts( $product ); $this->meta_handler->update_synced_at( $product, time() ); $this->meta_handler->update_sync_status( $product, SyncStatus::SYNCED ); $this->update_empty_visibility( $product ); // mark the parent product as synced if it's a variation if ( $product instanceof WC_Product_Variation ) { try { $parent_product = $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { return; } $this->mark_as_notified( $parent_product ); } } /** * Mark a product as synced in the local database. * This function also handles the following cleanup tasks: * - Remove any failed delete attempts * - Update the visibility (if it was previously empty) * - Remove any previous product errors (if it was synced for all target countries) * * @param WC_Product $product * @param GoogleProduct $google_product */ public function mark_as_synced( WC_Product $product, GoogleProduct $google_product ) { $this->meta_handler->delete_failed_delete_attempts( $product ); $this->meta_handler->update_synced_at( $product, time() ); $this->meta_handler->update_sync_status( $product, SyncStatus::SYNCED ); $this->update_empty_visibility( $product ); // merge and update all google product ids $current_google_ids = $this->meta_handler->get_google_ids( $product ); $current_google_ids = ! empty( $current_google_ids ) ? $current_google_ids : []; $google_ids = array_unique( array_merge( $current_google_ids, [ $google_product->getTargetCountry() => $google_product->getId() ] ) ); $this->meta_handler->update_google_ids( $product, $google_ids ); // check if product is synced for main target country and remove any previous errors if it is $synced_countries = array_keys( $google_ids ); $target_countries = $this->target_audience->get_target_countries(); if ( empty( array_diff( $synced_countries, $target_countries ) ) ) { $this->meta_handler->delete_errors( $product ); $this->meta_handler->delete_failed_sync_attempts( $product ); $this->meta_handler->delete_sync_failed_at( $product ); } // mark the parent product as synced if it's a variation if ( $product instanceof WC_Product_Variation ) { try { $parent_product = $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { return; } $this->mark_as_synced( $parent_product, $google_product ); } } /** * @param WC_Product $product */ public function mark_as_unsynced( $product ): void { $this->meta_handler->delete_synced_at( $product ); if ( ! $this->is_sync_ready( $product ) ) { $this->meta_handler->delete_sync_status( $product ); } else { $this->meta_handler->update_sync_status( $product, SyncStatus::NOT_SYNCED ); } $this->meta_handler->delete_google_ids( $product ); $this->meta_handler->delete_errors( $product ); $this->meta_handler->delete_failed_sync_attempts( $product ); $this->meta_handler->delete_sync_failed_at( $product ); // mark the parent product as un-synced if it's a variation if ( $product instanceof WC_Product_Variation ) { try { $parent_product = $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { return; } $this->mark_as_unsynced( $parent_product ); } } /** * @param WC_Product $product * @param string $google_id */ public function remove_google_id( WC_Product $product, string $google_id ) { $google_ids = $this->meta_handler->get_google_ids( $product ); if ( empty( $google_ids ) ) { return; } $idx = array_search( $google_id, $google_ids, true ); if ( false === $idx ) { return; } unset( $google_ids[ $idx ] ); if ( ! empty( $google_ids ) ) { $this->meta_handler->update_google_ids( $product, $google_ids ); } else { // if there are no Google IDs left then this product is no longer considered "synced" $this->mark_as_unsynced( $product ); } } /** * Marks a WooCommerce product as invalid and stores the errors in a meta data key. * * Note: If a product variation is invalid then the parent product is also marked as invalid. * * @param WC_Product $product * @param string[] $errors */ public function mark_as_invalid( WC_Product $product, array $errors ) { // bail if no errors exist if ( empty( $errors ) ) { return; } $this->meta_handler->update_errors( $product, $errors ); $this->meta_handler->update_sync_status( $product, SyncStatus::HAS_ERRORS ); $this->update_empty_visibility( $product ); if ( ! empty( $errors[ GoogleProductService::INTERNAL_ERROR_REASON ] ) ) { // update failed sync attempts count in case of internal errors $failed_attempts = ! empty( $this->meta_handler->get_failed_sync_attempts( $product ) ) ? $this->meta_handler->get_failed_sync_attempts( $product ) : 0; $this->meta_handler->update_failed_sync_attempts( $product, $failed_attempts + 1 ); $this->meta_handler->update_sync_failed_at( $product, time() ); } // mark the parent product as invalid if it's a variation if ( $product instanceof WC_Product_Variation ) { try { $parent_product = $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { return; } $parent_errors = ! empty( $this->meta_handler->get_errors( $parent_product ) ) ? $this->meta_handler->get_errors( $parent_product ) : []; $parent_errors[ $product->get_id() ] = $errors; $this->mark_as_invalid( $parent_product, $parent_errors ); } } /** * Marks a WooCommerce product as pending synchronization. * * Note: If a product variation is pending then the parent product is also marked as pending. * * @param WC_Product $product */ public function mark_as_pending( WC_Product $product ) { $this->meta_handler->update_sync_status( $product, SyncStatus::PENDING ); // mark the parent product as pending if it's a variation if ( $product instanceof WC_Product_Variation ) { try { $parent_product = $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { return; } $this->mark_as_pending( $parent_product ); } } /** * Update empty (NOT EXIST) visibility meta values to SYNC_AND_SHOW. * * @param WC_Product $product */ protected function update_empty_visibility( WC_Product $product ): void { try { $product = $this->maybe_swap_for_parent( $product ); } catch ( InvalidValue $exception ) { return; } $visibility = $this->meta_handler->get_visibility( $product ); if ( empty( $visibility ) ) { $this->meta_handler->update_visibility( $product, ChannelVisibility::SYNC_AND_SHOW ); } } /** * Update a product's channel visibility. * * @param WC_Product $product * @param string $visibility */ public function update_channel_visibility( WC_Product $product, string $visibility ): void { try { $product = $this->maybe_swap_for_parent( $product ); } catch ( InvalidValue $exception ) { // The error has been logged within the call of maybe_swap_for_parent return; } try { $visibility = ChannelVisibility::cast( $visibility )->get(); } catch ( InvalidValue $exception ) { do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); return; } $this->meta_handler->update_visibility( $product, $visibility ); } /** * @param WC_Product $product * * @return string[]|null An array of Google product IDs stored for each WooCommerce product */ public function get_synced_google_product_ids( WC_Product $product ): ?array { return $this->meta_handler->get_google_ids( $product ); } /** * See: WCProductAdapter::map_wc_product_id() * * @param string $mc_product_id Simple product ID (`merchant_center_id`) or * namespaced product ID (`online:en:GB:merchant_center_id`) * * @return int the ID for the WC product linked to the provided Google product ID (0 if not found) */ public function get_wc_product_id( string $mc_product_id ): int { // Maybe remove everything before the last colon ':' $mc_product_id_tokens = explode( ':', $mc_product_id ); $mc_product_id = end( $mc_product_id_tokens ); // Support a fully numeric ID both with and without the `gla_` prefix. $wc_product_id = 0; $pattern = '/^(' . preg_quote( $this->get_slug(), '/' ) . '_)?(\d+)$/'; if ( preg_match( $pattern, $mc_product_id, $matches ) ) { $wc_product_id = (int) $matches[2]; } /** * Filters the WooCommerce product ID that was determined to be associated with the * given Merchant Center product ID. * * @param string $wc_product_id The WooCommerce product ID as determined by default. * @param string $mc_product_id Simple Merchant Center product ID (without any prefixes). * @since 2.4.6 * * @return string Merchant Center product ID as normally generated by the plugin (e.g., gla_1234). */ return (int) apply_filters( 'woocommerce_gla_get_wc_product_id', $wc_product_id, $mc_product_id ); } /** * Attempt to get the WooCommerce product title. * The MC ID is converted to a WC ID before retrieving the product. * If we can't retrieve the title we fallback to the original MC ID. * * @param string $mc_product_id Merchant Center product ID. * * @return string */ public function get_wc_product_title( string $mc_product_id ): string { try { $product = $this->get_wc_product( $this->get_wc_product_id( $mc_product_id ) ); } catch ( InvalidValue $e ) { return $mc_product_id; } return $product->get_title(); } /** * Get WooCommerce product * * @param int $product_id * * @return WC_Product * * @throws InvalidValue If the given ID doesn't reference a valid product. */ public function get_wc_product( int $product_id ): WC_Product { return $this->wc->get_product( $product_id ); } /** * Get WooCommerce product by WP get_post * * @param int $product_id * * @return WP_Post|null */ public function get_wc_product_by_wp_post( int $product_id ): ?WP_Post { return get_post( $product_id ); } /** * @param WC_Product $product * * @return bool */ public function is_product_synced( WC_Product $product ): bool { $synced_at = $this->meta_handler->get_synced_at( $product ); $google_ids = $this->meta_handler->get_google_ids( $product ); return ! empty( $synced_at ) && ! empty( $google_ids ); } /** * Indicates if a product is ready for sending Notifications. * A product is ready to send notifications if DONT_SYNC_AND_SHOW is not enabled and the post status is publish. * * @param WC_Product $product * * @return bool */ public function is_ready_to_notify( WC_Product $product ): bool { $is_ready = ChannelVisibility::DONT_SYNC_AND_SHOW !== $this->get_channel_visibility( $product ) && $product->get_status() === 'publish' && in_array( $product->get_type(), ProductSyncer::get_supported_product_types(), true ); if ( $is_ready && $product instanceof WC_Product_Variation ) { $parent = $this->maybe_swap_for_parent( $product ); $is_ready = $this->is_ready_to_notify( $parent ); } /** * Allow users to filter if a product is ready to notify. * * @since 2.8.0 * * @param bool $value The current filter value. * @param WC_Product $product The product for the notification. */ return apply_filters( 'woocommerce_gla_product_is_ready_to_notify', $is_ready, $product ); } /** * Indicates if a product is ready for sending a create Notification. * A product is ready to send create notifications if is ready to notify and has not sent create notification yet. * * @param WC_Product $product * * @return bool */ public function should_trigger_create_notification( $product ): bool { return ! $product instanceof WC_Product_Variation && $this->is_ready_to_notify( $product ) && ! $this->has_notified_creation( $product ); } /** * Indicates if a product is ready for sending an update Notification. * A product is ready to send update notifications if is ready to notify and has sent create notification already. * * @param WC_Product $product * * @return bool */ public function should_trigger_update_notification( $product ): bool { return ! $product instanceof WC_Product_Variation && $this->is_ready_to_notify( $product ) && $this->has_notified_creation( $product ); } /** * Indicates if a product is ready for sending a delete Notification. * A product is ready to send delete notifications if it is not ready to notify and has sent create notification already. * * @param WC_Product $product * * @return bool */ public function should_trigger_delete_notification( $product ): bool { return ! $this->is_ready_to_notify( $product ) && $this->has_notified_creation( $product ); } /** * Indicates if a product was already notified about its creation. * Notice we consider synced products in MC as notified for creation. * * @param WC_Product $product * * @return bool */ public function has_notified_creation( WC_Product $product ): bool { if ( $product instanceof WC_Product_Variation ) { return $this->has_notified_creation( $this->maybe_swap_for_parent( $product ) ); } $valid_has_notified_creation_statuses = [ NotificationStatus::NOTIFICATION_CREATED, NotificationStatus::NOTIFICATION_UPDATED, NotificationStatus::NOTIFICATION_PENDING_UPDATE, NotificationStatus::NOTIFICATION_PENDING_DELETE, ]; return in_array( $this->meta_handler->get_notification_status( $product ), $valid_has_notified_creation_statuses, true ) || $this->is_product_synced( $product ); } /** * Set the notification status for a WooCommerce product. * * @param WC_Product $product * @param string $status */ public function set_notification_status( $product, $status ): void { $this->meta_handler->update_notification_status( $product, $status ); } /** * @param WC_Product $product * * @return bool */ public function is_sync_ready( WC_Product $product ): bool { $product_visibility = $product->is_visible(); $product_status = $product->get_status(); if ( $product instanceof WC_Product_Variation ) { // Check the post status of the parent product if it's a variation try { $parent = $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { do_action( 'woocommerce_gla_error', sprintf( 'Cannot sync an orphaned variation (ID: %s).', $product->get_id() ), __METHOD__ ); return false; } $product_status = $parent->get_status(); /** * Optionally hide invisible variations (disabled variations and variations with empty price). * * @see WC_Product_Variable::get_available_variations for filter documentation */ if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $parent->get_id(), $product ) && ! $product->variation_is_visible() ) { $product_visibility = false; } } return ( ChannelVisibility::DONT_SYNC_AND_SHOW !== $this->get_channel_visibility( $product ) ) && ( in_array( $product->get_type(), ProductSyncer::get_supported_product_types(), true ) ) && ( 'publish' === $product_status ) && $product_visibility; } /** * Whether the sync has failed repeatedly for the product within the given timeframe. * * @param WC_Product $product * * @return bool * * @see ProductSyncer::FAILURE_THRESHOLD The number of failed attempts allowed per timeframe * @see ProductSyncer::FAILURE_THRESHOLD_WINDOW The specified timeframe */ public function is_sync_failed_recently( WC_Product $product ): bool { $failed_attempts = $this->meta_handler->get_failed_sync_attempts( $product ); $failed_at = $this->meta_handler->get_sync_failed_at( $product ); // if it has failed more times than the specified threshold AND if syncing it has failed within the specified window return $failed_attempts > ProductSyncer::FAILURE_THRESHOLD && $failed_at > strtotime( sprintf( '-%s', ProductSyncer::FAILURE_THRESHOLD_WINDOW ) ); } /** * Increment failed delete attempts. * * @since 1.12.0 * * @param WC_Product $product */ public function increment_failed_delete_attempt( WC_Product $product ) { $failed_attempts = $this->meta_handler->get_failed_delete_attempts( $product ) ?? 0; $this->meta_handler->update_failed_delete_attempts( $product, $failed_attempts + 1 ); } /** * Whether deleting has failed more times than the specified threshold. * * @since 1.12.0 * * @param WC_Product $product * * @return boolean */ public function is_delete_failed_threshold_reached( WC_Product $product ): bool { $failed_attempts = $this->meta_handler->get_failed_delete_attempts( $product ) ?? 0; return $failed_attempts >= ProductSyncer::FAILURE_THRESHOLD; } /** * Increment failed delete attempts. * * @since 1.12.2 * * @param WC_Product $product */ public function increment_failed_update_attempt( WC_Product $product ) { $failed_attempts = $this->meta_handler->get_failed_sync_attempts( $product ) ?? 0; $this->meta_handler->update_failed_sync_attempts( $product, $failed_attempts + 1 ); } /** * Whether deleting has failed more times than the specified threshold. * * @since 1.12.2 * * @param WC_Product $product * * @return boolean */ public function is_update_failed_threshold_reached( WC_Product $product ): bool { $failed_attempts = $this->meta_handler->get_failed_sync_attempts( $product ) ?? 0; return $failed_attempts >= ProductSyncer::FAILURE_THRESHOLD; } /** * @param WC_Product $wc_product * * @return string|null */ public function get_channel_visibility( WC_Product $wc_product ): ?string { try { // todo: we might need to define visibility per variation later. return $this->meta_handler->get_visibility( $this->maybe_swap_for_parent( $wc_product ) ); } catch ( InvalidValue $exception ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Channel visibility forced to "%s" for invalid product (ID: %s).', ChannelVisibility::DONT_SYNC_AND_SHOW, $wc_product->get_id() ), __METHOD__ ); return ChannelVisibility::DONT_SYNC_AND_SHOW; } } /** * Return a string indicating sync status based on several factors. * * @param WC_Product $wc_product * * @return string|null */ public function get_sync_status( WC_Product $wc_product ): ?string { return $this->meta_handler->get_sync_status( $wc_product ); } /** * Return the string indicating the product status as reported by the Merchant Center. * * @param WC_Product $wc_product * * @return string|null */ public function get_mc_status( WC_Product $wc_product ): ?string { try { // If the mc_status is not set, return NOT_SYNCED. return $this->meta_handler->get_mc_status( $this->maybe_swap_for_parent( $wc_product ) ) ?: MCStatus::NOT_SYNCED; } catch ( InvalidValue $exception ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Product status returned null for invalid product (ID: %s).', $wc_product->get_id() ), __METHOD__ ); return null; } } /** * If an item from the provided list of products has a parent, replace it with the parent ID. * * @param int[] $product_ids A list of WooCommerce product ID. * @param bool $check_product_status (Optional) Check if the product status is publish. * @param bool $ignore_product_on_error (Optional) Ignore the product when invalid value error occurs. * * @return int[] A list of parent ID or product ID if it doesn't have a parent. * * @throws InvalidValue If the given param ignore_product_on_error is false and any of a given ID doesn't reference a valid product. * Or if a variation product does not have a valid parent ID (i.e. it's an orphan). * * @since 2.2.0 */ public function maybe_swap_for_parent_ids( array $product_ids, bool $check_product_status = true, bool $ignore_product_on_error = true ) { $new_product_ids = []; foreach ( $product_ids as $index => $product_id ) { try { $product = $this->get_wc_product( $product_id ); $new_product = $this->maybe_swap_for_parent( $product ); if ( ! $check_product_status || 'publish' === $new_product->get_status() ) { $new_product_ids[ $index ] = $new_product->get_id(); } } catch ( InvalidValue $exception ) { if ( ! $ignore_product_on_error ) { throw $exception; } } } return array_unique( $new_product_ids ); } /** * If the provided product has a parent, return its ID. Otherwise, return the given (valid product) ID. * * @param int $product_id WooCommerce product ID. * * @return int The parent ID or product ID if it doesn't have a parent. * * @throws InvalidValue If a given ID doesn't reference a valid product. Or if a variation product does not have a * valid parent ID (i.e. it's an orphan). */ public function maybe_swap_for_parent_id( int $product_id ): int { $product = $this->get_wc_product( $product_id ); return $this->maybe_swap_for_parent( $product )->get_id(); } /** * If the provided product has a parent, return its parent object. Otherwise, return the given product. * * @param WC_Product $product WooCommerce product object. * * @return WC_Product The parent product object or the given product object if it doesn't have a parent. * * @throws InvalidValue If a variation product does not have a valid parent ID (i.e. it's an orphan). * * @since 1.3.0 */ public function maybe_swap_for_parent( WC_Product $product ): WC_Product { if ( $product instanceof WC_Product_Variation ) { try { return $this->get_wc_product( $product->get_parent_id() ); } catch ( InvalidValue $exception ) { do_action( 'woocommerce_gla_error', sprintf( 'An orphaned variation found (ID: %s). Please delete it via "WooCommerce > Status > Tools > Delete orphaned variations".', $product->get_id() ), __METHOD__ ); throw $exception; } } return $product; } /** * Get validation errors for a specific product. * Combines errors for variable products, which have a variation-indexed array of errors. * * @param WC_Product $product * * @return array */ public function get_validation_errors( WC_Product $product ): array { $errors = $this->meta_handler->get_errors( $product ) ?: []; $first_key = array_key_first( $errors ); if ( ! empty( $errors ) && is_numeric( $first_key ) && 0 !== $first_key ) { $errors = array_unique( array_merge( ...$errors ) ); } return $errors; } /** * Get categories list for a specific product. * * @param WC_Product $product * * @return array */ public function get_categories( WC_Product $product ): array { $terms = get_the_terms( $product->get_id(), 'product_cat' ); return ( empty( $terms ) || is_wp_error( $terms ) ) ? [] : wp_list_pluck( $terms, 'name' ); } /** * Get the offer id for a product * * @since 2.8.0 * @param int $product_id The product id to get the offer id. * * @return string The offer id */ public function get_offer_id( int $product_id ) { return WCProductAdapter::get_google_product_offer_id( $this->get_slug(), $product_id ); } } PK!8&&"src/Product/ProductMetaHandler.phpnu[ 'int', self::KEY_GOOGLE_IDS => 'array', self::KEY_VISIBILITY => 'string', self::KEY_ERRORS => 'array', self::KEY_FAILED_DELETE_ATTEMPTS => 'int', self::KEY_FAILED_SYNC_ATTEMPTS => 'int', self::KEY_SYNC_FAILED_AT => 'int', self::KEY_SYNC_STATUS => 'string', self::KEY_MC_STATUS => 'string', self::KEY_NOTIFICATION_STATUS => 'string', ]; /** * @param string $name * @param mixed $arguments * * @return mixed * * @throws BadMethodCallException If the method that's called doesn't exist. * @throws InvalidMeta If the meta key is invalid. */ public function __call( string $name, $arguments ) { $found_matches = preg_match( '/^([a-z]+)_([\w\d]+)$/i', $name, $matches ); if ( ! $found_matches ) { throw new BadMethodCallException( sprintf( 'The method %s does not exist in class ProductMetaHandler', $name ) ); } [ $function_name, $method, $key ] = $matches; // validate the method if ( ! in_array( $method, [ 'update', 'delete', 'get' ], true ) ) { throw new BadMethodCallException( sprintf( 'The method %s does not exist in class ProductMetaHandler', $function_name ) ); } // set the value as the third argument if method is `update` if ( 'update' === $method ) { $arguments[2] = $arguments[1]; } // set the key as the second argument $arguments[1] = $key; return call_user_func_array( [ $this, $method ], $arguments ); } /** * @param WC_Product $product * @param string $key * @param mixed $value * * @throws InvalidMeta If the meta key is invalid. */ public function update( WC_Product $product, string $key, $value ) { self::validate_meta_key( $key ); if ( isset( self::TYPES[ $key ] ) ) { if ( in_array( self::TYPES[ $key ], [ 'bool', 'boolean' ], true ) ) { $value = wc_bool_to_string( $value ); } else { settype( $value, self::TYPES[ $key ] ); } } $product->update_meta_data( $this->prefix_meta_key( $key ), $value ); $product->save_meta_data(); } /** * @param WC_Product $product * @param string $key * * @throws InvalidMeta If the meta key is invalid. */ public function delete( WC_Product $product, string $key ) { self::validate_meta_key( $key ); $product->delete_meta_data( $this->prefix_meta_key( $key ) ); $product->save_meta_data(); } /** * @param WC_Product $product * @param string $key * * @return mixed The value, or null if the meta key doesn't exist. * * @throws InvalidMeta If the meta key is invalid. */ public function get( WC_Product $product, string $key ) { self::validate_meta_key( $key ); $value = null; if ( $product->meta_exists( $this->prefix_meta_key( $key ) ) ) { $value = $product->get_meta( $this->prefix_meta_key( $key ), true ); if ( isset( self::TYPES[ $key ] ) && in_array( self::TYPES[ $key ], [ 'bool', 'boolean' ], true ) ) { $value = wc_string_to_bool( $value ); } } return $value; } /** * @param string $key * * @throws InvalidMeta If the meta key is invalid. */ protected static function validate_meta_key( string $key ) { if ( ! self::is_meta_key_valid( $key ) ) { do_action( 'woocommerce_gla_error', sprintf( 'Product meta key is invalid: %s', $key ), __METHOD__ ); throw InvalidMeta::invalid_key( $key ); } } /** * @param string $key * * @return bool Whether the meta key is valid. */ public static function is_meta_key_valid( string $key ): bool { return isset( self::TYPES[ $key ] ); } /** * Register a service. */ public function register(): void { add_filter( 'woocommerce_product_data_store_cpt_get_products_query', function ( array $query, array $query_vars ) { return $this->handle_query_vars( $query, $query_vars ); }, 10, 2 ); } /** * Handle the WooCommerce product's meta data query vars. * * @hooked handle_query_vars * * @param array $query Args for WP_Query. * @param array $query_vars Query vars from WC_Product_Query. * * @return array modified $query */ protected function handle_query_vars( array $query, array $query_vars ): array { if ( ! empty( $query_vars['meta_query'] ) ) { $meta_query = $this->sanitize_meta_query( $query_vars['meta_query'] ); if ( ! empty( $meta_query ) ) { $query['meta_query'] = array_merge( $query['meta_query'], $meta_query ); } } return $query; } /** * Ensure the 'meta_query' argument passed to self::handle_query_vars is well-formed. * * @param array $queries Array of meta query clauses. * * @return array Sanitized array of meta query clauses. */ protected function sanitize_meta_query( $queries ): array { $prefixed_valid_keys = array_map( [ $this, 'prefix_meta_key' ], array_keys( self::TYPES ) ); $clean_queries = []; if ( ! is_array( $queries ) ) { return $clean_queries; } foreach ( $queries as $key => $meta_query ) { if ( 'relation' !== $key && ! is_array( $meta_query ) ) { continue; } if ( 'relation' === $key && is_string( $meta_query ) ) { $clean_queries[ $key ] = $meta_query; // First-order clause. } elseif ( isset( $meta_query['key'] ) || isset( $meta_query['value'] ) ) { if ( in_array( $meta_query['key'], $prefixed_valid_keys, true ) ) { $clean_queries[ $key ] = $meta_query; } // Otherwise, it's a nested meta_query, so we recurse. } else { $cleaned_query = $this->sanitize_meta_query( $meta_query ); if ( ! empty( $cleaned_query ) ) { $clean_queries[ $key ] = $cleaned_query; } } } return $clean_queries; } /** * @param array $meta_queries * * @return array */ public function prefix_meta_query_keys( $meta_queries ): array { $updated_queries = []; if ( ! is_array( $meta_queries ) ) { return $updated_queries; } foreach ( $meta_queries as $key => $meta_query ) { // First-order clause. if ( 'relation' === $key && is_string( $meta_query ) ) { $updated_queries[ $key ] = $meta_query; // First-order clause. } elseif ( isset( $meta_query['key'] ) || isset( $meta_query['value'] ) ) { if ( self::is_meta_key_valid( $meta_query['key'] ) ) { $meta_query['key'] = $this->prefix_meta_key( $meta_query['key'] ); } } else { // Otherwise, it's a nested meta_query, so we recurse. $meta_query = $this->prefix_meta_query_keys( $meta_query ); } $updated_queries[ $key ] = $meta_query; } return $updated_queries; } /** * Returns all available meta keys. * * @return array */ public static function get_all_meta_keys(): array { return array_keys( self::TYPES ); } } PK!pR1R1!src/Product/ProductRepository.phpnu[meta_handler = $meta_handler; $this->product_filter = $product_filter; } /** * Find and return an array of WooCommerce product objects based on the provided arguments. * * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @see execute_woocommerce_query For more information about the arguments. * * @return WC_Product[] Array of WooCommerce product objects */ public function find( array $args = [], int $limit = -1, int $offset = 0 ): array { $args['return'] = 'objects'; return $this->execute_woocommerce_query( $args, $limit, $offset ); } /** * Find and return an array of WooCommerce product IDs based on the provided arguments. * * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @see execute_woocommerce_query For more information about the arguments. * * @return int[] Array of WooCommerce product IDs */ public function find_ids( array $args = [], int $limit = -1, int $offset = 0 ): array { $args['return'] = 'ids'; return $this->execute_woocommerce_query( $args, $limit, $offset ); } /** * Find and return an array of WooCommerce product objects based on the provided product IDs. * * @param int[] $ids Array of WooCommerce product IDs * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return WC_Product[] Array of WooCommerce product objects */ public function find_by_ids( array $ids, array $args = [], int $limit = -1, int $offset = 0 ): array { // If no product IDs are supplied then return early to avoid querying and loading every product. if ( empty( $ids ) ) { return []; } $args['include'] = $ids; return $this->find( $args, $limit, $offset ); } /** * Find and return an associative array of products with the product ID as the key. * * @param int[] $ids Array of WooCommerce product IDs * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return WC_Product[] Array of WooCommerce product objects */ public function find_by_ids_as_associative_array( array $ids, array $args = [], int $limit = -1, int $offset = 0 ): array { $products = $this->find_by_ids( $ids, $args, $limit, $offset ); $map = []; foreach ( $products as $product ) { $map[ $product->get_id() ] = $product; } return $map; } /** * Find and return an array of WooCommerce product objects already submitted to Google Merchant Center. * * @param array $args Array of WooCommerce args (except 'return' and 'meta_query'). * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return WC_Product[] Array of WooCommerce product objects */ public function find_synced_products( array $args = [], int $limit = -1, int $offset = 0 ): array { $args['meta_query'] = $this->get_synced_products_meta_query(); return $this->find( $args, $limit, $offset ); } /** * Find and return an array of WooCommerce product IDs already submitted to Google Merchant Center. * * Note: Includes product variations. * * @param array $args Array of WooCommerce args (except 'return' and 'meta_query'). * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return int[] Array of WooCommerce product IDs */ public function find_synced_product_ids( array $args = [], int $limit = -1, int $offset = 0 ): array { $args['meta_query'] = $this->get_synced_products_meta_query(); return $this->find_ids( $args, $limit, $offset ); } /** * @return array */ protected function get_synced_products_meta_query(): array { return [ [ 'key' => ProductMetaHandler::KEY_GOOGLE_IDS, 'compare' => 'EXISTS', ], ]; } /** * Find and return an array of WooCommerce product objects ready to be submitted to Google Merchant Center. * * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return FilteredProductList List of WooCommerce product objects after filtering. */ public function find_sync_ready_products( array $args = [], int $limit = - 1, int $offset = 0 ): FilteredProductList { $results = $this->find( $this->get_sync_ready_products_query_args( $args ), $limit, $offset ); return $this->product_filter->filter_sync_ready_products( $results ); } /** * Find and return an array of WooCommerce product ID's ready to be deleted from the Google Merchant Center. * * @since 1.12.0 * * @param int[] $ids Array of WooCommerce product IDs * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return array */ public function find_delete_product_ids( array $ids, int $limit = - 1, int $offset = 0 ): array { // Default status query args in WC_Product_Query plus status trash. $args = [ 'status' => [ 'draft', 'pending', 'private', 'publish', 'trash' ] ]; $results = $this->find_by_ids( $ids, $args, $limit, $offset ); return $this->product_filter->filter_products_for_delete( $results )->get_product_ids(); } /** * @return array */ protected function get_sync_ready_products_meta_query(): array { return [ 'relation' => 'OR', [ 'key' => ProductMetaHandler::KEY_VISIBILITY, 'compare' => 'NOT EXISTS', ], [ 'key' => ProductMetaHandler::KEY_VISIBILITY, 'compare' => '!=', 'value' => ChannelVisibility::DONT_SYNC_AND_SHOW, ], ]; } /** * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * * @return array */ protected function get_sync_ready_products_query_args( array $args = [] ): array { $args['meta_query'] = $this->get_sync_ready_products_meta_query(); // don't include variable products in query $args['type'] = array_diff( ProductSyncer::get_supported_product_types(), [ 'variable' ] ); // only include published products if ( empty( $args['status'] ) ) { $args['status'] = [ 'publish' ]; } return $args; } /** * @return array */ protected function get_valid_products_meta_query(): array { return [ 'relation' => 'OR', [ 'key' => ProductMetaHandler::KEY_ERRORS, 'compare' => 'NOT EXISTS', ], [ 'key' => ProductMetaHandler::KEY_ERRORS, 'compare' => '=', 'value' => '', ], ]; } /** * Find and return an array of WooCommerce product IDs nearly expired and ready to be re-submitted to Google Merchant Center. * * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return int[] Array of WooCommerce product IDs */ public function find_expiring_product_ids( int $limit = - 1, int $offset = 0 ): array { $args['meta_query'] = [ 'relation' => 'AND', $this->get_sync_ready_products_meta_query(), $this->get_valid_products_meta_query(), [ [ 'key' => ProductMetaHandler::KEY_SYNCED_AT, 'compare' => '<', 'value' => strtotime( '-25 days' ), ], ], ]; return $this->find_ids( $args, $limit, $offset ); } /** * Find all simple and variable product IDs regardless of MC status or visibility. * * @since 2.6.4 * * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @return int[] Array of WooCommerce product IDs */ public function find_all_product_ids( int $limit = -1, int $offset = 0 ): array { $args = [ 'status' => 'publish', 'return' => 'ids', 'type' => 'any', ]; return $this->find_ids( $args, $limit, $offset ); } /** * Returns an array of Google Product IDs associated with all synced WooCommerce products. * Note: excludes variable parent products as only the child variation products are actually synced * to Merchant Center * * @since 1.1.0 * * @return array Google Product IDS */ public function find_all_synced_google_ids(): array { // Don't include variable parent products as they aren't actually synced to Merchant Center. $args['type'] = array_diff( ProductSyncer::get_supported_product_types(), [ 'variable' ] ); $synced_product_ids = $this->find_synced_product_ids( $args ); $google_ids_meta_key = $this->prefix_meta_key( ProductMetaHandler::KEY_GOOGLE_IDS ); $synced_google_ids = []; foreach ( $synced_product_ids as $product_id ) { $meta_google_ids = get_post_meta( $product_id, $google_ids_meta_key, true ); if ( ! is_array( $meta_google_ids ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( 'Invalid Google IDs retrieve for product %d', $product_id ), __METHOD__ ); continue; } $synced_google_ids = array_merge( $synced_google_ids, array_values( $meta_google_ids ) ); } return $synced_google_ids; } /** * Find and return an array of WooCommerce products based on the provided arguments. * * @param array $args Array of WooCommerce args (see below), and product metadata. * @param int $limit Maximum number of results to retrieve or -1 for unlimited. * @param int $offset Amount to offset product results. * * @link https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query * @see ProductMetaHandler::TYPES For the list of meta data that can be used as query arguments. * * @return WC_Product[]|int[] Array of WooCommerce product objects or IDs, depending on the 'return' argument. */ protected function execute_woocommerce_query( array $args = [], int $limit = -1, int $offset = 0 ): array { $args['limit'] = $limit; $args['offset'] = $offset; return wc_get_products( $this->prepare_query_args( $args ) ); } /** * @param array $args Array of WooCommerce args (except 'return'), and product metadata. * * @see execute_woocommerce_query For more information about the arguments. * * @return array */ protected function prepare_query_args( array $args = [] ): array { if ( empty( $args ) ) { return []; } if ( ! empty( $args['meta_query'] ) ) { $args['meta_query'] = $this->meta_handler->prefix_meta_query_keys( $args['meta_query'] ); } // only include supported product types if ( empty( $args['type'] ) ) { $args['type'] = ProductSyncer::get_supported_product_types(); } // It'll fetch all products with the post_type of 'product', excluding variations. if ( $args['type'] === 'any' ) { unset( $args['type'] ); } // use no ordering unless specified in arguments. overrides the default WooCommerce query args if ( empty( $args['orderby'] ) ) { $args['orderby'] = 'none'; } $args = apply_filters( 'woocommerce_gla_product_query_args', $args ); return $args; } } PK!GiW'&src/Product/ProductSyncerException.phpnu[google_service = $google_service; $this->batch_helper = $batch_helper; $this->product_helper = $product_helper; $this->merchant_center = $merchant_center; $this->wc = $wc; $this->product_repository = $product_repository; } /** * Submits an array of WooCommerce products to Google Merchant Center. * * @param WC_Product[] $products * * @return BatchProductResponse Containing both the synced and invalid products. * * @throws ProductSyncerException If there are any errors while syncing products with Google Merchant Center. */ public function update( array $products ): BatchProductResponse { $this->validate_merchant_center_setup(); // prepare and validate products $product_entries = $this->batch_helper->validate_and_generate_update_request_entries( $products ); return $this->update_by_batch_requests( $product_entries ); } /** * Submits an array of WooCommerce products to Google Merchant Center. * * @param BatchProductRequestEntry[] $product_entries * * @return BatchProductResponse Containing both the synced and invalid products. * * @throws ProductSyncerException If there are any errors while syncing products with Google Merchant Center. */ public function update_by_batch_requests( array $product_entries ): BatchProductResponse { $this->validate_merchant_center_setup(); // bail if no valid products provided if ( empty( $product_entries ) ) { return new BatchProductResponse( [], [] ); } $updated_products = []; $invalid_products = []; foreach ( array_chunk( $product_entries, GoogleProductService::BATCH_SIZE ) as $batch_entries ) { try { $response = $this->google_service->insert_batch( $batch_entries ); $updated_products = array_merge( $updated_products, $response->get_products() ); $invalid_products = array_merge( $invalid_products, $response->get_errors() ); // update the meta data for the synced and invalid products array_walk( $updated_products, [ $this->batch_helper, 'mark_as_synced' ] ); array_walk( $invalid_products, [ $this->batch_helper, 'mark_as_invalid' ] ); } catch ( Exception $exception ) { do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); throw new ProductSyncerException( sprintf( 'Error updating Google products: %s', $exception->getMessage() ), 0, $exception ); } } $this->handle_update_errors( $invalid_products ); do_action( 'woocommerce_gla_batch_updated_products', $updated_products, $invalid_products ); do_action( 'woocommerce_gla_debug_message', sprintf( "Submitted %s products:\n%s", count( $updated_products ), wp_json_encode( $updated_products ) ), __METHOD__ ); if ( ! empty( $invalid_products ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( "%s products failed to sync with Merchant Center:\n%s", count( $invalid_products ), wp_json_encode( $invalid_products ) ), __METHOD__ ); } return new BatchProductResponse( $updated_products, $invalid_products ); } /** * Deletes an array of WooCommerce products from Google Merchant Center. * * @param WC_Product[] $products * * @return BatchProductResponse Containing both the deleted and invalid products. * * @throws ProductSyncerException If there are any errors while deleting products from Google Merchant Center. */ public function delete( array $products ): BatchProductResponse { $this->validate_merchant_center_setup(); $synced_products = $this->batch_helper->filter_synced_products( $products ); $product_entries = $this->batch_helper->generate_delete_request_entries( $synced_products ); return $this->delete_by_batch_requests( $product_entries ); } /** * Deletes an array of WooCommerce products from Google Merchant Center. * * Note: This method does not automatically delete variations of a parent product. They each must be provided via the $product_entries argument. * * @param BatchProductIDRequestEntry[] $product_entries * * @return BatchProductResponse Containing both the deleted and invalid products (including their variation). * * @throws ProductSyncerException If there are any errors while deleting products from Google Merchant Center. */ public function delete_by_batch_requests( array $product_entries ): BatchProductResponse { $this->validate_merchant_center_setup(); // return empty response if no synced product found if ( empty( $product_entries ) ) { return new BatchProductResponse( [], [] ); } $deleted_products = []; $invalid_products = []; foreach ( array_chunk( $product_entries, GoogleProductService::BATCH_SIZE ) as $batch_entries ) { try { $response = $this->google_service->delete_batch( $batch_entries ); $deleted_products = array_merge( $deleted_products, $response->get_products() ); $invalid_products = array_merge( $invalid_products, $response->get_errors() ); array_walk( $deleted_products, [ $this->batch_helper, 'mark_as_unsynced' ] ); } catch ( Exception $exception ) { do_action( 'woocommerce_gla_exception', $exception, __METHOD__ ); throw new ProductSyncerException( sprintf( 'Error deleting Google products: %s', $exception->getMessage() ), 0, $exception ); } } $this->handle_delete_errors( $invalid_products ); do_action( 'woocommerce_gla_batch_deleted_products', $deleted_products, $invalid_products ); do_action( 'woocommerce_gla_debug_message', sprintf( "Deleted %s products:\n%s", count( $deleted_products ), wp_json_encode( $deleted_products ), ), __METHOD__ ); if ( ! empty( $invalid_products ) ) { do_action( 'woocommerce_gla_debug_message', sprintf( "Failed to delete %s products from Merchant Center:\n%s", count( $invalid_products ), wp_json_encode( $invalid_products ) ), __METHOD__ ); } return new BatchProductResponse( $deleted_products, $invalid_products ); } /** * Return the list of supported product types. * * @return array */ public static function get_supported_product_types(): array { return (array) apply_filters( 'woocommerce_gla_supported_product_types', [ 'simple', 'variable', 'variation' ] ); } /** * @param BatchInvalidProductEntry[] $invalid_products */ protected function handle_update_errors( array $invalid_products ) { $error_products = []; foreach ( $invalid_products as $invalid_product ) { if ( $invalid_product->has_error( GoogleProductService::INTERNAL_ERROR_REASON ) ) { $wc_product_id = $invalid_product->get_wc_product_id(); $wc_product = $this->wc->maybe_get_product( $wc_product_id ); // Only schedule for retry if the failure threshold has not been reached. if ( $wc_product instanceof WC_Product && ! $this->product_helper->is_update_failed_threshold_reached( $wc_product ) ) { $error_products[ $wc_product_id ] = $wc_product_id; } } } if ( ! empty( $error_products ) && apply_filters( 'woocommerce_gla_products_update_retry_on_failure', true, $invalid_products ) ) { do_action( 'woocommerce_gla_batch_retry_update_products', $error_products ); do_action( 'woocommerce_gla_error', sprintf( 'Internal API errors while submitting the following products: %s', join( ', ', $error_products ) ), __METHOD__ ); } } /** * @param BatchInvalidProductEntry[] $invalid_products */ protected function handle_delete_errors( array $invalid_products ) { $internal_error_ids = []; foreach ( $invalid_products as $invalid_product ) { $google_product_id = $invalid_product->get_google_product_id(); $wc_product_id = $invalid_product->get_wc_product_id(); $wc_product = $this->wc->maybe_get_product( $wc_product_id ); if ( ! $wc_product instanceof WC_Product || empty( $google_product_id ) ) { continue; } // not found if ( $invalid_product->has_error( GoogleProductService::NOT_FOUND_ERROR_REASON ) ) { do_action( 'woocommerce_gla_error', sprintf( 'Attempted to delete product "%s" (WooCommerce Product ID: %s) but it did not exist in Google Merchant Center, removing the synced product ID from database.', $google_product_id, $wc_product_id ), __METHOD__ ); $this->product_helper->remove_google_id( $wc_product, $google_product_id ); } // internal error if ( $invalid_product->has_error( GoogleProductService::INTERNAL_ERROR_REASON ) ) { $this->product_helper->increment_failed_delete_attempt( $wc_product ); // Only schedule for retry if the failure threshold has not been reached. if ( ! $this->product_helper->is_delete_failed_threshold_reached( $wc_product ) ) { $internal_error_ids[ $google_product_id ] = $wc_product_id; } } } // Exclude any ID's which are not ready to delete or are not available in the DB. $product_ids = array_values( $internal_error_ids ); $ready_ids = $this->product_repository->find_delete_product_ids( $product_ids ); $internal_error_ids = array_intersect( $internal_error_ids, $ready_ids ); // call an action to retry if any products with internal errors exist if ( ! empty( $internal_error_ids ) && apply_filters( 'woocommerce_gla_products_delete_retry_on_failure', true, $invalid_products ) ) { do_action( 'woocommerce_gla_batch_retry_delete_products', $internal_error_ids ); do_action( 'woocommerce_gla_error', // phpcs:ignore WordPress.PHP.DevelopmentFunctions sprintf( 'Internal API errors while deleting the following products: %s', print_r( $internal_error_ids, true ) ), __METHOD__ ); } } /** * Validates whether Merchant Center is connected and ready for pushing data. * * @throws ProductSyncerException If the Google Merchant Center connection is not ready or cannot push data. */ protected function validate_merchant_center_setup(): void { if ( ! $this->merchant_center->is_ready_for_syncing() ) { do_action( 'woocommerce_gla_error', 'Cannot sync any products before setting up Google Merchant Center.', __METHOD__ ); throw new ProductSyncerException( __( 'Google Merchant Center has not been set up correctly. Please review your configuration.', 'google-listings-and-ads' ) ); } if ( ! $this->merchant_center->should_push() ) { do_action( 'woocommerce_gla_error', 'Cannot push any products because they are being fetched automatically.', __METHOD__ ); throw new ProductSyncerException( __( 'Pushing products will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings.', 'google-listings-and-ads' ) ); } } } PK!іM 9999src/Product/SyncerHooks.phpnu[batch_helper = $batch_helper; $this->product_helper = $product_helper; $this->job_repository = $job_repository; $this->merchant_center = $merchant_center; $this->notifications_service = $notifications_service; $this->wc = $wc; } /** * Register a service. */ public function register(): void { // only register the hooks if Merchant Center is connected correctly. if ( ! $this->merchant_center->is_ready_for_syncing() ) { return; } // when a product is added / updated, schedule an "update" job. add_action( 'woocommerce_new_product', [ $this, 'update_by_id' ], 90 ); add_action( 'woocommerce_new_product_variation', [ $this, 'update_by_id' ], 90 ); add_action( 'woocommerce_update_product', [ $this, 'update_by_object' ], 90, 2 ); add_action( 'woocommerce_update_product_variation', [ $this, 'update_by_object' ], 90, 2 ); // if we don't attach to these we miss product gallery updates. add_action( 'woocommerce_process_product_meta', [ $this, 'update_by_id' ], 90 ); // when a product is trashed or removed, schedule a "delete" job. add_action( 'wp_trash_post', [ $this, 'pre_delete' ], 90 ); add_action( 'before_delete_post', [ $this, 'pre_delete' ], 90 ); add_action( 'woocommerce_before_delete_product_variation', [ $this, 'pre_delete' ], 90 ); add_action( 'trashed_post', [ $this, 'delete' ], 90 ); add_action( 'deleted_post', [ $this, 'delete' ], 90 ); // when a product is restored from the trash, schedule an "update" job. add_action( 'untrashed_post', [ $this, 'update_by_id' ], 90 ); // exclude the sync metadata when duplicating the product add_filter( 'woocommerce_duplicate_product_exclude_meta', [ $this, 'duplicate_product_exclude_meta' ], 90 ); } /** * Update a Product by WC_Product * * @param int $product_id * @param WC_Product $product */ public function update_by_object( int $product_id, WC_Product $product ) { $this->handle_update_products( [ $product ] ); } /** * Update a Product by the ID * * @param int $product_id */ public function update_by_id( int $product_id ) { $product = $this->wc->maybe_get_product( $product_id ); $this->handle_update_products( [ $product ] ); } /** * Pre delete a Product by the ID * * @param int $product_id */ public function pre_delete( int $product_id ) { $this->handle_pre_delete_product( $product_id ); } /** * Delete a Product by the ID * * @param int $product_id */ public function delete( int $product_id ) { $this->handle_delete_product( $product_id ); } /** * Filters woocommerce_duplicate_product_exclude_meta adding some custom prefix * * @param array $exclude_meta * @return array */ public function duplicate_product_exclude_meta( array $exclude_meta ): array { return $this->get_duplicated_product_excluded_meta( $exclude_meta ); } /** * Handle updating of a product. * * @param WC_Product[] $products The products being saved. * @param bool $notify If true. It will try to handle notifications. * * @return void */ protected function handle_update_products( array $products, $notify = true ) { $products_to_update = []; $products_to_delete = []; foreach ( $products as $product ) { if ( ! $product instanceof WC_Product ) { continue; } $product_id = $product->get_id(); // Avoid to handle variations directly. We handle them from the parent. if ( $this->notifications_service->is_ready() && $notify ) { $this->handle_update_product_notification( $product ); } // Bail if an event is already scheduled for this product in the current request if ( $this->is_already_scheduled_to_update( $product_id ) ) { continue; } // If it's a variable product we handle each variation separately if ( $product instanceof WC_Product_Variable ) { // This is only for MC Push mechanism. We don't handle notifications here. $this->handle_update_products( $product->get_available_variations( 'objects' ), false ); continue; } // Schedule an update job if product sync is enabled. if ( $this->product_helper->is_sync_ready( $product ) ) { $this->product_helper->mark_as_pending( $product ); $products_to_update[] = $product->get_id(); $this->set_already_scheduled_to_update( $product_id ); } elseif ( $this->product_helper->is_product_synced( $product ) ) { // Delete the product from Google Merchant Center if it's already synced BUT it is not sync ready after the edit. $products_to_delete[] = $product; $this->set_already_scheduled_to_delete( $product_id ); do_action( 'woocommerce_gla_debug_message', sprintf( 'Deleting product (ID: %s) from Google Merchant Center because it is not ready to be synced.', $product->get_id() ), __METHOD__ ); } else { $this->product_helper->mark_as_unsynced( $product ); } } if ( ! empty( $products_to_update ) ) { $this->job_repository->get( UpdateProducts::class )->schedule( [ $products_to_update ] ); } if ( ! empty( $products_to_delete ) ) { $request_entries = $this->batch_helper->generate_delete_request_entries( $products_to_delete ); $this->job_repository->get( DeleteProducts::class )->schedule( [ BatchProductIDRequestEntry::convert_to_id_map( $request_entries )->get() ] ); } } /** * Schedules notifications for an updated product * * @param WC_Product $product */ protected function handle_update_product_notification( WC_Product $product ) { if ( $this->product_helper->should_trigger_create_notification( $product ) ) { $this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_PENDING_CREATE ); $this->job_repository->get( ProductNotificationJob::class )->schedule( [ 'item_id' => $product->get_id(), 'topic' => NotificationsService::TOPIC_PRODUCT_CREATED, ] ); } elseif ( $this->product_helper->should_trigger_update_notification( $product ) ) { $this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_PENDING_UPDATE ); $this->job_repository->get( ProductNotificationJob::class )->schedule( [ 'item_id' => $product->get_id(), 'topic' => NotificationsService::TOPIC_PRODUCT_UPDATED, ] ); } elseif ( $this->product_helper->should_trigger_delete_notification( $product ) ) { $this->schedule_delete_notification( $product ); // Schedule variation deletion when the parent is deleted. if ( $product instanceof WC_Product_Variable ) { foreach ( $product->get_available_variations( 'objects' ) as $variation ) { $this->handle_update_product_notification( $variation ); } } } } /** * Handle deleting of a product. * * @param int $product_id */ protected function handle_delete_product( int $product_id ) { if ( isset( $this->delete_requests_map[ $product_id ] ) ) { $product_id_map = BatchProductIDRequestEntry::convert_to_id_map( $this->delete_requests_map[ $product_id ] )->get(); if ( ! empty( $product_id_map ) && ! $this->is_already_scheduled_to_delete( $product_id ) ) { $this->job_repository->get( DeleteProducts::class )->schedule( [ $product_id_map ] ); $this->set_already_scheduled_to_delete( $product_id ); } } } /** * Maybe send the product deletion notification * and mark the product as un-synced after. * * @since 2.8.0 * @param int $product_id */ protected function maybe_send_delete_notification( int $product_id ) { $product = $this->wc->maybe_get_product( $product_id ); if ( $product instanceof WC_Product && $this->product_helper->has_notified_creation( $product ) ) { $result = $this->notifications_service->notify( NotificationsService::TOPIC_PRODUCT_DELETED, $product_id, [ 'offer_id' => $this->product_helper->get_offer_id( $product_id ) ] ); if ( $result ) { $this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_DELETED ); $this->product_helper->mark_as_unsynced( $product ); } } } /** * Schedules a job to send the product deletion notification * * @since 2.8.0 * @param WC_Product $product */ protected function schedule_delete_notification( $product ) { $this->product_helper->set_notification_status( $product, NotificationStatus::NOTIFICATION_PENDING_DELETE ); $this->job_repository->get( ProductNotificationJob::class )->schedule( [ 'item_id' => $product->get_id(), 'topic' => NotificationsService::TOPIC_PRODUCT_DELETED, ] ); } /** * Create request entries for the product (containing its Google ID) so that we can schedule a delete job when the * product is actually trashed / deleted. * * @param int $product_id */ protected function handle_pre_delete_product( int $product_id ) { if ( $this->notifications_service->is_ready() ) { /** * For deletions, we do send directly the notification instead of scheduling it. * This is because we want to avoid that the product is not in the database anymore when the scheduled action runs. */ $this->maybe_send_delete_notification( $product_id ); } $product = $this->wc->maybe_get_product( $product_id ); // each variation is passed to this method separately so we don't need to delete the variable product if ( $product instanceof WC_Product && ! $product instanceof WC_Product_Variable && $this->product_helper->is_product_synced( $product ) ) { $this->delete_requests_map[ $product_id ] = $this->batch_helper->generate_delete_request_entries( [ $product ] ); } } /** * Return the list of metadata keys to be excluded when duplicating a product. * * @param array $exclude_meta The keys to exclude from the duplicate. * * @return array */ protected function get_duplicated_product_excluded_meta( array $exclude_meta ): array { $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_SYNCED_AT ); $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_GOOGLE_IDS ); $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_ERRORS ); $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_FAILED_SYNC_ATTEMPTS ); $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_SYNC_FAILED_AT ); $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_SYNC_STATUS ); $exclude_meta[] = $this->prefix_meta_key( ProductMetaHandler::KEY_MC_STATUS ); return $exclude_meta; } /** * @param int $product_id * @param string $schedule_type * * @return bool */ protected function is_already_scheduled( int $product_id, string $schedule_type ): bool { return isset( $this->already_scheduled[ $product_id ] ) && $this->already_scheduled[ $product_id ] === $schedule_type; } /** * @param int $product_id * * @return bool */ protected function is_already_scheduled_to_update( int $product_id ): bool { return $this->is_already_scheduled( $product_id, self::SCHEDULE_TYPE_UPDATE ); } /** * @param int $product_id * * @return bool */ protected function is_already_scheduled_to_delete( int $product_id ): bool { return $this->is_already_scheduled( $product_id, self::SCHEDULE_TYPE_DELETE ); } /** * @param int $product_id * @param string $schedule_type * * @return void */ protected function set_already_scheduled( int $product_id, string $schedule_type ): void { $this->already_scheduled[ $product_id ] = $schedule_type; } /** * @param int $product_id * * @return void */ protected function set_already_scheduled_to_update( int $product_id ): void { $this->set_already_scheduled( $product_id, self::SCHEDULE_TYPE_UPDATE ); } /** * @param int $product_id * * @return void */ protected function set_already_scheduled_to_delete( int $product_id ): void { $this->set_already_scheduled( $product_id, self::SCHEDULE_TYPE_DELETE ); } } PK!/ src/Product/WCProductAdapter.phpnu[wc_product = $properties['wc_product']; $this->parent_wc_product = $properties['parent_wc_product'] ?? null; $mapping_rules = $properties['mapping_rules'] ?? []; $gla_attributes = $properties['gla_attributes'] ?? []; // Google doesn't expect extra fields, so it's best to remove them unset( $properties['wc_product'] ); unset( $properties['parent_wc_product'] ); unset( $properties['gla_attributes'] ); unset( $properties['mapping_rules'] ); parent::mapTypes( $properties ); $this->map_woocommerce_product(); $this->map_attribute_mapping_rules( $mapping_rules ); $this->map_gla_attributes( $gla_attributes ); $this->map_gtin(); // Allow users to override the product's attributes using a WordPress filter. $this->override_attributes(); } /** * Map the WooCommerce product attributes to the current class. * * @return void */ protected function map_woocommerce_product() { $this->setChannel( self::CHANNEL_ONLINE ); $content_language = empty( get_locale() ) ? 'en' : strtolower( substr( get_locale(), 0, 2 ) ); // ISO 639-1. $this->setContentLanguage( $content_language ); $this->map_wc_product_id() ->map_wc_general_attributes() ->map_product_categories() ->map_wc_product_image( self::IMAGE_SIZE_FULL ) ->map_wc_availability() ->map_wc_product_shipping() ->map_wc_prices(); } /** * Overrides the product attributes by applying a filter and setting the provided values. * * @since 1.4.0 */ protected function override_attributes() { /** * Filters the list of overridden attributes to set for this product. * * Note: This filter takes precedence over any other filter that modify products attributes. Including * `woocommerce_gla_product_attribute_value_{$attribute_id}` defined in self::map_gla_attributes. * * @param array $attributes An array of values for the product properties. All properties of the * `\Google\Service\ShoppingContent\Product` class can be set by providing * the property name as key and its value as array item. * For example: * [ 'imageLink' => 'https://example.com/image.jpg' ] overrides the product's * main image. * * @param WC_Product $wc_product The WooCommerce product object. * @param WCProductAdapter $this The Adapted Google product object. All WooCommerce product properties * are already mapped to this object. * * @see \Google\Service\ShoppingContent\Product for the list of product properties that can be overriden. * @see WCProductAdapter::map_gla_attributes for the docuementation of `woocommerce_gla_product_attribute_value_{$attribute_id}` * filter, which allows modifying some attributes such as GTIN, MPN, etc. * * @since 1.4.0 */ $attributes = apply_filters( 'woocommerce_gla_product_attribute_values', [], $this->wc_product, $this ); if ( ! empty( $attributes ) ) { parent::mapTypes( $attributes ); } } /** * Map the general WooCommerce product attributes. * * @return $this */ protected function map_wc_general_attributes() { $this->setTitle( $this->wc_product->get_title() ); $this->setDescription( $this->get_wc_product_description() ); $this->setLink( $this->wc_product->get_permalink() ); // set item group id for variations if ( $this->is_variation() ) { $this->setItemGroupId( $this->parent_wc_product->get_id() ); } return $this; } /** * Map WooCommerce product categories to Google product types. * * @return $this */ protected function map_product_categories() { // set product type using merchants defined product categories $base_product_id = $this->is_variation() ? $this->parent_wc_product->get_id() : $this->wc_product->get_id(); // Fetch only selected term ids without parents. $this->product_category_ids = wc_get_product_term_ids( $base_product_id, 'product_cat' ); if ( ! empty( $this->product_category_ids ) ) { $google_product_types = self::convert_product_types( $this->product_category_ids ); do_action( 'woocommerce_gla_debug_message', sprintf( 'Product category (ID: %s): %s.', $base_product_id, wp_json_encode( $google_product_types ) ), __METHOD__ ); $google_product_types = array_slice( $google_product_types, 0, 10 ); $this->setProductTypes( $google_product_types ); } return $this; } /** * Covert WooCommerce product categories to product_type, which follows Google requirements: * https://support.google.com/merchants/answer/6324406?hl=en# * * @param int[] $category_ids * * @return array */ public static function convert_product_types( $category_ids ): array { $product_types = []; foreach ( array_unique( $category_ids ) as $category_id ) { if ( ! is_int( $category_id ) ) { continue; } $product_type = self::get_product_type_by_id( $category_id ); array_push( $product_types, $product_type ); } return $product_types; } /** * Return category names including ancestors, separated by ">" * * @param int $category_id * * @return string */ protected static function get_product_type_by_id( int $category_id ): string { $category_names = []; do { $term = get_term_by( 'id', $category_id, 'product_cat', 'ARRAY_A' ); array_push( $category_names, $term['name'] ); $category_id = $term['parent']; } while ( ! empty( $term['parent'] ) ); return implode( ' > ', array_reverse( $category_names ) ); } /** * Map the WooCommerce product ID. * * @return $this */ protected function map_wc_product_id(): WCProductAdapter { $this->setOfferId( self::get_google_product_offer_id( $this->get_slug(), $this->wc_product->get_id() ) ); return $this; } /** * * @param string $slug * @param int $product_id * @return string */ public static function get_google_product_offer_id( string $slug, int $product_id ): string { /** * Filters a WooCommerce product ID to be used as the Merchant Center product ID. * * @param string $mc_product_id Default generated Merchant Center product ID. * @param int $product_id WooCommerce product ID. * @since 2.4.6 * * @return string Merchant Center product ID corresponding to the given WooCommerce product ID. */ return apply_filters( 'woocommerce_gla_get_google_product_offer_id', "{$slug}_{$product_id}", $product_id ); } /** * Get the description for the WooCommerce product. * * @return string */ protected function get_wc_product_description(): string { /** * Filters whether the short product description should be used for the synced product. * * @param bool $use_short_description */ $use_short_description = apply_filters( 'woocommerce_gla_use_short_description', false ); $description = ! empty( $this->wc_product->get_description() ) && ! $use_short_description ? $this->wc_product->get_description() : $this->wc_product->get_short_description(); // prepend the parent product description to the variation product if ( $this->is_variation() ) { $parent_description = ! empty( $this->parent_wc_product->get_description() ) && ! $use_short_description ? $this->parent_wc_product->get_description() : $this->parent_wc_product->get_short_description(); $new_line = ! empty( $description ) && ! empty( $parent_description ) ? PHP_EOL : ''; $description = $parent_description . $new_line . $description; } /** * Filters whether the shortcodes should be applied for product descriptions when syncing a product or be stripped out. * * @since 1.4.0 * * @param bool $apply_shortcodes Shortcodes are applied if set to `true` and stripped out if set to `false`. * @param WC_Product $wc_product WooCommerce product object. */ $apply_shortcodes = apply_filters( 'woocommerce_gla_product_description_apply_shortcodes', false, $this->wc_product ); if ( $apply_shortcodes ) { // Apply active shortcodes $description = do_shortcode( $description ); } else { // Strip out active shortcodes $description = strip_shortcodes( $description ); } // Strip out invalid unicode. $description = mb_convert_encoding( $description, 'UTF-8', 'UTF-8' ); $description = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', '', $description ); // Strip out invalid HTML tags (e.g. script, style, canvas, etc.) along with attributes of all tags. $valid_html_tags = array_keys( wp_kses_allowed_html( 'post' ) ); $kses_allowed_tags = array_fill_keys( $valid_html_tags, [] ); $description = wp_kses( $description, $kses_allowed_tags ); // Trim the description if it's more than 5000 characters. $description = mb_substr( $description, 0, 5000, 'utf-8' ); /** * Filters the product's description. * * @param string $description Product description. * @param WC_Product $wc_product WooCommerce product object. */ return apply_filters( 'woocommerce_gla_product_attribute_value_description', $description, $this->wc_product ); } /** * Map the WooCommerce product images. * * @param string $image_size * * @return $this */ protected function map_wc_product_image( string $image_size ) { $image_id = $this->wc_product->get_image_id(); $gallery_image_ids = $this->wc_product->get_gallery_image_ids() ?: []; // check if we can use the parent product image if it's a variation if ( $this->is_variation() ) { $image_id = $image_id ?? $this->parent_wc_product->get_image_id(); $parent_gallery_images = $this->parent_wc_product->get_gallery_image_ids() ?: []; $gallery_image_ids = ! empty( $gallery_image_ids ) ? $gallery_image_ids : $parent_gallery_images; } // use a gallery image as the main product image if no main image is available if ( empty( $image_id ) && ! empty( $gallery_image_ids[0] ) ) { $image_id = $gallery_image_ids[0]; // remove the recently set main image from the list of gallery images unset( $gallery_image_ids[0] ); } // set main image $image_link = wp_get_attachment_image_url( $image_id, $image_size, false ); $this->setImageLink( $image_link ); // set additional images $gallery_image_links = array_map( function ( $gallery_image_id ) use ( $image_size ) { return wp_get_attachment_image_url( $gallery_image_id, $image_size, false ); }, $gallery_image_ids ); // Uniquify the set of additional images $gallery_image_links = array_unique( $gallery_image_links, SORT_REGULAR ); // Limit additional image links up to 10 $gallery_image_links = array_slice( $gallery_image_links, 0, 10 ); $this->setAdditionalImageLinks( $gallery_image_links ); return $this; } /** * Map the general WooCommerce product attributes. * * @return $this */ protected function map_wc_availability() { if ( ! $this->wc_product->is_in_stock() ) { $availability = self::AVAILABILITY_OUT_OF_STOCK; } elseif ( $this->wc_product->is_on_backorder( 1 ) ) { $availability = self::AVAILABILITY_BACKORDER; } else { $availability = self::AVAILABILITY_IN_STOCK; } $this->setAvailability( $availability ); return $this; } /** * Map the shipping information for WooCommerce product. * * @return $this */ protected function map_wc_product_shipping(): WCProductAdapter { $this->add_shipping_country( $this->getTargetCountry() ); if ( ! $this->is_virtual() ) { $dimension_unit = apply_filters( 'woocommerce_gla_dimension_unit', get_option( 'woocommerce_dimension_unit' ) ); $weight_unit = apply_filters( 'woocommerce_gla_weight_unit', get_option( 'woocommerce_weight_unit' ) ); $this->map_wc_shipping_dimensions( $dimension_unit ) ->map_wc_shipping_weight( $weight_unit ); } // Set the product's shipping class slug as the shipping label. $shipping_class = $this->wc_product->get_shipping_class(); if ( ! empty( $shipping_class ) ) { $this->setShippingLabel( $shipping_class ); } return $this; } /** * Add a shipping country for the product. * * @param string $country */ public function add_shipping_country( string $country ): void { $product_shipping = [ 'country' => $country, ]; // Virtual products should override any country shipping cost. if ( $this->is_virtual() ) { $product_shipping['price'] = [ 'currency' => get_woocommerce_currency(), 'value' => 0, ]; } $new_shipping = [ new GoogleProductShipping( $product_shipping ), ]; if ( ! $this->shipping_country_exists( $country ) ) { $current_shipping = $this->getShipping() ?? []; $this->setShipping( array_merge( $current_shipping, $new_shipping ) ); } } /** * Remove a shipping country from the product. * * @param string $country * * @since 1.2.0 */ public function remove_shipping_country( string $country ): void { $product_shippings = $this->getShipping() ?? []; foreach ( $product_shippings as $index => $shipping ) { if ( $country === $shipping->getCountry() ) { unset( $product_shippings[ $index ] ); } } $this->setShipping( $product_shippings ); } /** * @param string $country * * @return bool */ protected function shipping_country_exists( string $country ): bool { $current_shipping = $this->getShipping() ?? []; foreach ( $current_shipping as $shipping ) { if ( $country === $shipping->getCountry() ) { return true; } } return false; } /** * Map the measurements for the WooCommerce product. * * @param string $unit * * @return $this */ protected function map_wc_shipping_dimensions( string $unit = 'cm' ): WCProductAdapter { $length = $this->wc_product->get_length(); $width = $this->wc_product->get_width(); $height = $this->wc_product->get_height(); // Use cm if the unit isn't supported. if ( ! in_array( $unit, [ 'in', 'cm' ], true ) ) { $unit = 'cm'; } $length = wc_get_dimension( (float) $length, $unit ); $width = wc_get_dimension( (float) $width, $unit ); $height = wc_get_dimension( (float) $height, $unit ); if ( $length > 0 && $width > 0 && $height > 0 ) { $this->setShippingLength( new GoogleProductShippingDimension( [ 'unit' => $unit, 'value' => $length, ] ) ); $this->setShippingWidth( new GoogleProductShippingDimension( [ 'unit' => $unit, 'value' => $width, ] ) ); $this->setShippingHeight( new GoogleProductShippingDimension( [ 'unit' => $unit, 'value' => $height, ] ) ); } return $this; } /** * Map the weight for the WooCommerce product. * * @param string $unit * * @return $this */ protected function map_wc_shipping_weight( string $unit = 'g' ): WCProductAdapter { // Use g if the unit isn't supported. if ( ! in_array( $unit, [ 'g', 'lbs', 'oz' ], true ) ) { $unit = 'g'; } $weight = wc_get_weight( $this->wc_product->get_weight(), $unit ); // Use lb if the unit is lbs, since GMC uses lb. if ( 'lbs' === $unit ) { $unit = 'lb'; } $this->setShippingWeight( new GoogleProductShippingWeight( [ 'unit' => $unit, 'value' => $weight, ] ) ); return $this; } /** * Sets whether tax is excluded from product price. * * @return $this */ protected function map_tax_excluded(): WCProductAdapter { // tax is excluded from price in US and CA $this->tax_excluded = in_array( $this->getTargetCountry(), [ 'US', 'CA' ], true ); $this->tax_excluded = boolval( apply_filters( 'woocommerce_gla_tax_excluded', $this->tax_excluded ) ); return $this; } /** * Map the prices (base and sale price) for the product. * * @return $this */ protected function map_wc_prices(): WCProductAdapter { $this->map_tax_excluded(); $this->map_wc_product_price( $this->wc_product ); return $this; } /** * Map the prices (base and sale price) for a given WooCommerce product. * * @param WC_Product $product * * @return $this */ protected function map_wc_product_price( WC_Product $product ): WCProductAdapter { // set regular price $regular_price = $product->get_regular_price(); if ( '' !== $regular_price ) { $price = $this->tax_excluded ? wc_get_price_excluding_tax( $product, [ 'price' => $regular_price ] ) : wc_get_price_including_tax( $product, [ 'price' => $regular_price ] ); /** * Filters the calculated product price. * * @param float $price Calculated price of the product * @param WC_Product $product WooCommerce product * @param bool $tax_excluded Whether tax is excluded from product price */ $price = apply_filters( 'woocommerce_gla_product_attribute_value_price', $price, $product, $this->tax_excluded ); $this->setPrice( new GooglePrice( [ 'currency' => get_woocommerce_currency(), 'value' => $price, ] ) ); } // set sale price $this->map_wc_product_sale_price( $product ); return $this; } /** * Map the sale price and sale effective date for a given WooCommerce product. * * @param WC_Product $product * * @return $this */ protected function map_wc_product_sale_price( WC_Product $product ): WCProductAdapter { // Grab the sale price of the base product. Some plugins (Dynamic // pricing as an example) filter the active price, but not the sale // price. If the active price < the regular price treat it as a sale // price. $regular_price = $product->get_regular_price(); $sale_price = $product->get_sale_price(); $active_price = $product->get_price(); if ( ( empty( $sale_price ) && $active_price < $regular_price ) || ( ! empty( $sale_price ) && $active_price < $sale_price ) ) { $sale_price = $active_price; } // set sale price and sale effective date if any if ( '' !== $sale_price ) { $sale_price = $this->tax_excluded ? wc_get_price_excluding_tax( $product, [ 'price' => $sale_price ] ) : wc_get_price_including_tax( $product, [ 'price' => $sale_price ] ); /** * Filters the calculated product sale price. * * @param float $sale_price Calculated sale price of the product * @param WC_Product $product WooCommerce product * @param bool $tax_excluded Whether tax is excluded from product price */ $sale_price = apply_filters( 'woocommerce_gla_product_attribute_value_sale_price', $sale_price, $product, $this->tax_excluded ); // If the sale price dates no longer apply, make sure we don't include a sale price. $now = new WC_DateTime(); $sale_price_end_date = $product->get_date_on_sale_to(); if ( empty( $sale_price_end_date ) || $sale_price_end_date >= $now ) { $this->setSalePrice( new GooglePrice( [ 'currency' => get_woocommerce_currency(), 'value' => $sale_price, ] ) ); $this->setSalePriceEffectiveDate( $this->get_wc_product_sale_price_effective_date( $product ) ); } } return $this; } /** * Return the sale effective dates for the WooCommerce product. * * @param WC_Product $product * * @return string|null */ protected function get_wc_product_sale_price_effective_date( WC_Product $product ): ?string { $start_date = $product->get_date_on_sale_from(); $end_date = $product->get_date_on_sale_to(); $now = new WC_DateTime(); // if we have a sale end date in the future, but no start date, set the start date to now() if ( ! empty( $end_date ) && $end_date > $now && empty( $start_date ) ) { $start_date = $now; } // if we have a sale start date in the past, but no end date, do not include the start date. if ( ! empty( $start_date ) && $start_date < $now && empty( $end_date ) ) { $start_date = null; } // if we have a start date in the future, but no end date, assume a one-day sale. if ( ! empty( $start_date ) && $start_date > $now && empty( $end_date ) ) { $end_date = clone $start_date; $end_date->add( new DateInterval( 'P1D' ) ); } if ( empty( $start_date ) && empty( $end_date ) ) { return null; } return sprintf( '%s/%s', (string) $start_date, (string) $end_date ); } /** * Return whether the WooCommerce product is a variation. * * @return bool */ public function is_variation(): bool { return $this->wc_product instanceof WC_Product_Variation; } /** * Return whether the WooCommerce product is virtual. * * @return bool */ public function is_virtual(): bool { $is_virtual = $this->wc_product->is_virtual(); /** * Filters the virtual property value of a product. * * @param bool $is_virtual Whether a product is virtual * @param WC_Product $product WooCommerce product */ $is_virtual = apply_filters( 'woocommerce_gla_product_property_value_is_virtual', $is_virtual, $this->wc_product ); return false !== $is_virtual; } /** * @param ClassMetadata $metadata */ public static function load_validator_metadata( ClassMetadata $metadata ) { $metadata->addPropertyConstraint( 'offerId', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'title', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'description', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'link', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'link', new Assert\Url() ); $metadata->addPropertyConstraint( 'imageLink', new Assert\NotBlank() ); $metadata->addPropertyConstraint( 'imageLink', new ImageUrlConstraint() ); $metadata->addPropertyConstraint( 'additionalImageLinks', new Assert\All( [ 'constraints' => [ new ImageUrlConstraint() ], ] ) ); $metadata->addGetterConstraint( 'price', new Assert\NotNull() ); $metadata->addGetterConstraint( 'price', new GooglePriceConstraint() ); $metadata->addGetterConstraint( 'salePrice', new GooglePriceConstraint() ); $metadata->addConstraint( new Assert\Callback( 'validate_item_group_id' ) ); $metadata->addConstraint( new Assert\Callback( 'validate_availability' ) ); $metadata->addPropertyConstraint( 'gtin', new Assert\Regex( '/^\d{8}(?:\d{4,6})?$/' ) ); $metadata->addPropertyConstraint( 'mpn', new Assert\Type( 'string' ) ); $metadata->addPropertyConstraint( 'mpn', new Assert\Length( null, 0, 70 ) ); // maximum 70 characters $metadata->addPropertyConstraint( 'sizes', new Assert\All( [ 'constraints' => [ new Assert\Type( 'string' ), new Assert\Length( null, 0, 100 ), // maximum 100 characters ], ] ) ); $metadata->addPropertyConstraint( 'sizeSystem', new Assert\Choice( array_keys( SizeSystem::get_value_options() ) ) ); $metadata->addPropertyConstraint( 'sizeType', new Assert\Choice( array_keys( SizeType::get_value_options() ) ) ); $metadata->addPropertyConstraint( 'color', new Assert\Length( null, 0, 100 ) ); // maximum 100 characters $metadata->addPropertyConstraint( 'material', new Assert\Length( null, 0, 200 ) ); // maximum 200 characters $metadata->addPropertyConstraint( 'pattern', new Assert\Length( null, 0, 100 ) ); // maximum 200 characters $metadata->addPropertyConstraint( 'ageGroup', new Assert\Choice( array_keys( AgeGroup::get_value_options() ) ) ); $metadata->addPropertyConstraint( 'adult', new Assert\Type( 'boolean' ) ); $metadata->addPropertyConstraint( 'condition', new Assert\Choice( array_keys( Condition::get_value_options() ) ) ); $metadata->addPropertyConstraint( 'multipack', new Assert\Type( 'integer' ) ); $metadata->addPropertyConstraint( 'multipack', new Assert\PositiveOrZero() ); $metadata->addPropertyConstraint( 'isBundle', new Assert\Type( 'boolean' ) ); } /** * Used by the validator to check if the variation product has an itemGroupId * * @param ExecutionContextInterface $context */ public function validate_item_group_id( ExecutionContextInterface $context ) { if ( $this->is_variation() && empty( $this->getItemGroupId() ) ) { $context->buildViolation( 'ItemGroupId needs to be set for variable products.' ) ->atPath( 'itemGroupId' ) ->addViolation(); } } /** * Used by the validator to check if the availability date is set for product available as `backorder` or * `preorder`. * * @param ExecutionContextInterface $context */ public function validate_availability( ExecutionContextInterface $context ) { if ( ( self::AVAILABILITY_BACKORDER === $this->getAvailability() || self::AVAILABILITY_PREORDER === $this->getAvailability() ) && empty( $this->getAvailabilityDate() ) ) { $context->buildViolation( 'Availability date is required if you set the product\'s availability to backorder or pre-order.' ) ->atPath( 'availabilityDate' ) ->addViolation(); } } /** * @return WC_Product */ public function get_wc_product(): WC_Product { return $this->wc_product; } /** * @param array $attributes Attribute values * * @return $this */ protected function map_gla_attributes( array $attributes ): WCProductAdapter { $gla_attributes = []; foreach ( $attributes as $attribute_id => $attribute_value ) { if ( property_exists( $this, $attribute_id ) ) { /** * Filters a product attribute's value. * * This only applies to the extra attributes defined in `AttributeManager::ATTRIBUTES` * like GTIN, MPN, Brand, Size, etc. and it cannot modify other product attributes. * * This filter also cannot add or set a new attribute or modify one that isn't currently * set for the product through WooCommerce's edit product page * * In order to override all product attributes and/or set new ones for the product use the * `woocommerce_gla_product_attribute_values` filter. * * Note that the `woocommerce_gla_product_attribute_values` filter takes precedence over * this filter, and it can be used to override any values defined here. * * @param mixed $attribute_value The attribute's current value * @param WC_Product $wc_product The WooCommerce product object. * * @see AttributeManager::ATTRIBUTES for the list of attributes that their values can be modified using this filter. * @see WCProductAdapter::override_attributes for the documentation of the `woocommerce_gla_product_attribute_values` filter. */ $gla_attributes[ $attribute_id ] = apply_filters( "woocommerce_gla_product_attribute_value_{$attribute_id}", $attribute_value, $this->get_wc_product() ); } } parent::mapTypes( $gla_attributes ); // Size if ( ! empty( $attributes['size'] ) ) { $this->setSizes( [ $attributes['size'] ] ); } return $this; } /** * Map the WooCommerce core global unique ID (GTIN) value if it's available. * * @since 2.9.0 * * @return $this */ protected function map_gtin(): WCProductAdapter { // compatibility-code "WC < 9.2" -- Core global unique ID field was added in 9.2 if ( ! method_exists( $this->wc_product, 'get_global_unique_id' ) ) { return $this; } // avoid dashes and other unsupported format $global_unique_id = preg_replace( '/[^0-9]/', '', $this->wc_product->get_global_unique_id() ); if ( ! empty( $global_unique_id ) ) { $this->setGtin( $global_unique_id ); } return $this; } /** * @param string $targetCountry * * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase */ public function setTargetCountry( $targetCountry ) { // remove shipping for current target country $this->remove_shipping_country( $this->getTargetCountry() ); // set the new target country parent::setTargetCountry( $targetCountry ); // we need to reset the prices because tax is based on the country $this->map_wc_prices(); // product shipping information is also country based $this->map_wc_product_shipping(); } /** * Performs the attribute mapping. * This function applies rules setting values for the different attributes in the product. * * @param array $mapping_rules The set of rules to apply */ protected function map_attribute_mapping_rules( array $mapping_rules ) { $attributes = []; if ( empty( $mapping_rules ) ) { return $this; } foreach ( $mapping_rules as $mapping_rule ) { if ( $this->rule_match_conditions( $mapping_rule ) ) { $attribute_id = $mapping_rule['attribute']; $attributes[ $attribute_id ] = $this->format_attribute( apply_filters( "woocommerce_gla_product_attribute_value_{$attribute_id}", $this->get_source( $mapping_rule['source'] ), $this->get_wc_product() ), $attribute_id ); } } parent::mapTypes( $attributes ); // Size if ( ! empty( $attributes['size'] ) ) { $this->setSizes( [ $attributes['size'] ] ); } return $this; } /** * Get a source value for attribute mapping * * @param string $source The source to get the value * @return string The source value for this product */ protected function get_source( string $source ) { $source_type = null; $type_separator = strpos( $source, ':' ); if ( $type_separator ) { $source_type = substr( $source, 0, $type_separator ); $source_value = substr( $source, $type_separator + 1 ); } // Detect if the source_type is kind of product, taxonomy or attribute. Otherwise, we take it the full source as a static value. switch ( $source_type ) { case 'product': return $this->get_product_field( $source_value ); case 'taxonomy': return $this->get_product_taxonomy( $source_value ); case 'attribute': return $this->get_custom_attribute( $source_value ); default: return $source; } } /** * Check if the current product match the conditions for applying the Attribute mapping rule. * For now the conditions are just matching with the product category conditions. * * @param array $rule The attribute mapping rule * @return bool True if the rule is applicable */ protected function rule_match_conditions( array $rule ): bool { $attribute = $rule['attribute']; $category_condition_type = $rule['category_condition_type']; if ( $category_condition_type === AttributeMappingHelper::CATEGORY_CONDITION_TYPE_ALL ) { return true; } // size is not the real attribute, the real attribute is sizes if ( ! property_exists( $this, $attribute ) && $attribute !== 'size' ) { return false; } $categories = explode( ',', $rule['categories'] ); $contains_rules_categories = ! empty( array_intersect( $categories, $this->product_category_ids ) ); if ( $category_condition_type === AttributeMappingHelper::CATEGORY_CONDITION_TYPE_ONLY ) { return $contains_rules_categories; } return ! $contains_rules_categories; } /** * Get taxonomy source type for attribute mapping * * @param string $taxonomy The taxonomy to get * @return string The taxonomy value */ protected function get_product_taxonomy( $taxonomy ) { $product = $this->get_wc_product(); if ( $product->is_type( 'variation' ) ) { $values = $product->get_attribute( $taxonomy ); if ( ! $values ) { // if taxonomy is not a global attribute (ie product_tag), attempt to get is with wc_get_product_terms $values = $this->get_taxonomy_term_names( $product->get_id(), $taxonomy ); } if ( ! $values ) { // if the value is still not available at this point, we try to get it from the parent $parent = wc_get_product( $product->get_parent_id() ); $values = $parent->get_attribute( $taxonomy ); if ( ! $values ) { $values = $this->get_taxonomy_term_names( $parent->get_id(), $taxonomy ); } } if ( is_string( $values ) ) { $values = explode( ', ', $values ); } } else { $values = $this->get_taxonomy_term_names( $product->get_id(), $taxonomy ); } if ( empty( $values ) || is_wp_error( $values ) ) { return ''; } return $values[0]; } /** * Get product source type for attribute mapping. * Those are fields belonging to the product core data. Like title, weight, SKU... * * @param string $field The field to get * @return string|null The field value (null if data is not available) */ protected function get_product_field( $field ) { $product = $this->get_wc_product(); if ( 'weight_with_unit' === $field ) { $weight = $product->get_weight(); return $weight ? $weight . ' ' . get_option( 'woocommerce_weight_unit' ) : null; } if ( is_callable( [ $product, 'get_' . $field ] ) ) { $getter = 'get_' . $field; return $product->$getter(); } return ''; } /** * * Formats the attribute for sending it via Google API * * @param string $value The value to format * @param string $attribute_id The attribute ID for which this value belongs * @return string|bool|int The attribute formatted based on theit attribute type */ protected function format_attribute( $value, $attribute_id ) { $attribute = AttributeMappingHelper::get_attribute_by_id( $attribute_id ); if ( in_array( $attribute::get_value_type(), [ 'bool', 'boolean' ], true ) ) { return wc_string_to_bool( $value ); } if ( in_array( $attribute::get_value_type(), [ 'int', 'integer' ], true ) ) { return (int) $value; } return $value; } /** * Gets a custom attribute from a product * * @param string $attribute_name - The attribute name to get. * @return string|null The attribute value or null if no value is found */ protected function get_custom_attribute( $attribute_name ) { $product = $this->get_wc_product(); $attribute_value = $product->get_attribute( $attribute_name ); if ( ! $attribute_value ) { $attribute_value = $product->get_meta( $attribute_name ); } // We only support scalar values. if ( ! is_scalar( $attribute_value ) ) { return ''; } $values = explode( WC_DELIMITER, (string) $attribute_value ); $values = array_filter( array_map( 'trim', $values ) ); return empty( $values ) ? '' : $values[0]; } /** * Get a taxonomy term names from a product using * * @param int $product_id - The product ID to get the taxonomy term * @param string $taxonomy - The taxonomy to get. * @return string[] An array of term names. */ protected function get_taxonomy_term_names( $product_id, $taxonomy ) { $values = wc_get_product_terms( $product_id, $taxonomy ); return wp_list_pluck( $values, 'name' ); } } PK!_ _ src/Proxies/GoogleGtagJs.phpnu[wcga_settings = get_option( 'woocommerce_google_analytics_settings', [] ); $this->ga4w_v2 = defined( '\WC_GOOGLE_ANALYTICS_INTEGRATION_VERSION' ) && version_compare( \WC_GOOGLE_ANALYTICS_INTEGRATION_VERSION, '2.0.0', '>=' ); // Prime some values. if ( $this->ga4w_v2 ) { $this->wcga_settings['ga_gtag_enabled'] = 'yes'; } elseif ( empty( $this->wcga_settings['ga_gtag_enabled'] ) ) { $this->wcga_settings['ga_gtag_enabled'] = 'no'; } if ( empty( $this->wcga_settings['ga_standard_tracking_enabled'] ) ) { $this->wcga_settings['ga_standard_tracking_enabled'] = 'no'; } if ( empty( $this->wcga_settings['ga_id'] ) ) { $this->wcga_settings['ga_id'] = null; } } /** * Determine whether WooCommerce Google Analytics for WooCommerce is already * injecting the gtag '); } elseif ($format === self::FORMAT_JS) { static::writeOutput(static::generateScript()); } static::resetStatic(); } } public function close(): void { self::resetStatic(); } public function reset() { parent::reset(); self::resetStatic(); } /** * Forget all logged records */ public static function resetStatic(): void { static::$records = []; } /** * Wrapper for register_shutdown_function to allow overriding */ protected function registerShutdownFunction(): void { if (PHP_SAPI !== 'cli') { register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']); } } /** * Wrapper for echo to allow overriding */ protected static function writeOutput(string $str): void { echo $str; } /** * Checks the format of the response * * If Content-Type is set to application/javascript or text/javascript -> js * If Content-Type is set to text/html, or is unset -> html * If Content-Type is anything else -> unknown * * @return string One of 'js', 'html' or 'unknown' * @phpstan-return self::FORMAT_* */ protected static function getResponseFormat(): string { // Check content type foreach (headers_list() as $header) { if (stripos($header, 'content-type:') === 0) { return static::getResponseFormatFromContentType($header); } } return self::FORMAT_HTML; } /** * @return string One of 'js', 'html' or 'unknown' * @phpstan-return self::FORMAT_* */ protected static function getResponseFormatFromContentType(string $contentType): string { // This handler only works with HTML and javascript outputs // text/javascript is obsolete in favour of application/javascript, but still used if (stripos($contentType, 'application/javascript') !== false || stripos($contentType, 'text/javascript') !== false) { return self::FORMAT_JS; } if (stripos($contentType, 'text/html') !== false) { return self::FORMAT_HTML; } return self::FORMAT_UNKNOWN; } private static function generateScript(): string { $script = []; foreach (static::$records as $record) { $context = static::dump('Context', $record['context']); $extra = static::dump('Extra', $record['extra']); if (empty($context) && empty($extra)) { $script[] = static::call_array(static::getConsoleMethodForLevel($record['level']), static::handleStyles($record['formatted'])); } else { $script = array_merge( $script, [static::call_array('groupCollapsed', static::handleStyles($record['formatted']))], $context, $extra, [static::call('groupEnd')] ); } } return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);"; } private static function getConsoleMethodForLevel(int $level): string { return [ Logger::DEBUG => 'debug', Logger::INFO => 'info', Logger::NOTICE => 'info', Logger::WARNING => 'warn', Logger::ERROR => 'error', Logger::CRITICAL => 'error', Logger::ALERT => 'error', Logger::EMERGENCY => 'error', ][$level] ?? 'log'; } /** * @return string[] */ private static function handleStyles(string $formatted): array { $args = []; $format = '%c' . $formatted; preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); foreach (array_reverse($matches) as $match) { $args[] = '"font-weight: normal"'; $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0])); $pos = $match[0][1]; $format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0])); } $args[] = static::quote('font-weight: normal'); $args[] = static::quote($format); return array_reverse($args); } private static function handleCustomStyles(string $style, string $string): string { static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey']; static $labels = []; $style = preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $m) use ($string, &$colors, &$labels) { if (trim($m[1]) === 'autolabel') { // Format the string as a label with consistent auto assigned background color if (!isset($labels[$string])) { $labels[$string] = $colors[count($labels) % count($colors)]; } $color = $labels[$string]; return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px"; } return $m[1]; }, $style); if (null === $style) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Failed to run preg_replace_callback: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); } return $style; } /** * @param mixed[] $dict * @return mixed[] */ private static function dump(string $title, array $dict): array { $script = []; $dict = array_filter($dict); if (empty($dict)) { return $script; } $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title)); foreach ($dict as $key => $value) { $value = json_encode($value); if (empty($value)) { $value = static::quote(''); } $script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value); } return $script; } private static function quote(string $arg): string { return '"' . addcslashes($arg, "\"\n\\") . '"'; } /** * @param mixed $args */ private static function call(...$args): string { $method = array_shift($args); if (!is_string($method)) { throw new \UnexpectedValueException('Expected the first arg to be a string, got: '.var_export($method, true)); } return static::call_array($method, $args); } /** * @param mixed[] $args */ private static function call_array(string $method, array $args): string { return 'c.' . $method . '(' . implode(', ', $args) . ');'; } } PK!>$$<vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; /** * Buffers all records until closing the handler and then pass them as batch. * * This is useful for a MailHandler to send only one mail per request instead of * sending one per log message. * * @author Christophe Coevoet * * @phpstan-import-type Record from \Monolog\Logger */ class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** @var HandlerInterface */ protected $handler; /** @var int */ protected $bufferSize = 0; /** @var int */ protected $bufferLimit; /** @var bool */ protected $flushOnOverflow; /** @var Record[] */ protected $buffer = []; /** @var bool */ protected $initialized = false; /** * @param HandlerInterface $handler Handler. * @param int $bufferLimit How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. * @param bool $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded */ public function __construct(HandlerInterface $handler, int $bufferLimit = 0, $level = Logger::DEBUG, bool $bubble = true, bool $flushOnOverflow = false) { parent::__construct($level, $bubble); $this->handler = $handler; $this->bufferLimit = $bufferLimit; $this->flushOnOverflow = $flushOnOverflow; } /** * {@inheritDoc} */ public function handle(array $record): bool { if ($record['level'] < $this->level) { return false; } if (!$this->initialized) { // __destructor() doesn't get called on Fatal errors register_shutdown_function([$this, 'close']); $this->initialized = true; } if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) { if ($this->flushOnOverflow) { $this->flush(); } else { array_shift($this->buffer); $this->bufferSize--; } } if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } $this->buffer[] = $record; $this->bufferSize++; return false === $this->bubble; } public function flush(): void { if ($this->bufferSize === 0) { return; } $this->handler->handleBatch($this->buffer); $this->clear(); } public function __destruct() { // suppress the parent behavior since we already have register_shutdown_function() // to call close(), and the reference contained there will prevent this from being // GC'd until the end of the request } /** * {@inheritDoc} */ public function close(): void { $this->flush(); $this->handler->close(); } /** * Clears the buffer without flushing any messages down to the wrapped handler. */ public function clear(): void { $this->bufferSize = 0; $this->buffer = []; } public function reset() { $this->flush(); parent::reset(); $this->resetProcessors(); if ($this->handler instanceof ResettableInterface) { $this->handler->reset(); } } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { if ($this->handler instanceof FormattableHandlerInterface) { $this->handler->setFormatter($formatter); return $this; } throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { if ($this->handler instanceof FormattableHandlerInterface) { return $this->handler->getFormatter(); } throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); } } PK!aPނ?vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\ChromePHPFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; use Monolog\Utils; /** * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) * * This also works out of the box with Firefox 43+ * * @author Christophe Coevoet * * @phpstan-import-type Record from \Monolog\Logger */ class ChromePHPHandler extends AbstractProcessingHandler { use WebRequestRecognizerTrait; /** * Version of the extension */ protected const VERSION = '4.0'; /** * Header name */ protected const HEADER_NAME = 'X-ChromeLogger-Data'; /** * Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+) */ protected const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}'; /** @var bool */ protected static $initialized = false; /** * Tracks whether we sent too much data * * Chrome limits the headers to 4KB, so when we sent 3KB we stop sending * * @var bool */ protected static $overflowed = false; /** @var mixed[] */ protected static $json = [ 'version' => self::VERSION, 'columns' => ['label', 'log', 'backtrace', 'type'], 'rows' => [], ]; /** @var bool */ protected static $sendHeaders = true; public function __construct($level = Logger::DEBUG, bool $bubble = true) { parent::__construct($level, $bubble); if (!function_exists('json_encode')) { throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler'); } } /** * {@inheritDoc} */ public function handleBatch(array $records): void { if (!$this->isWebRequest()) { return; } $messages = []; foreach ($records as $record) { if ($record['level'] < $this->level) { continue; } /** @var Record $message */ $message = $this->processRecord($record); $messages[] = $message; } if (!empty($messages)) { $messages = $this->getFormatter()->formatBatch($messages); self::$json['rows'] = array_merge(self::$json['rows'], $messages); $this->send(); } } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new ChromePHPFormatter(); } /** * Creates & sends header for a record * * @see sendHeader() * @see send() */ protected function write(array $record): void { if (!$this->isWebRequest()) { return; } self::$json['rows'][] = $record['formatted']; $this->send(); } /** * Sends the log header * * @see sendHeader() */ protected function send(): void { if (self::$overflowed || !self::$sendHeaders) { return; } if (!self::$initialized) { self::$initialized = true; self::$sendHeaders = $this->headersAccepted(); if (!self::$sendHeaders) { return; } self::$json['request_uri'] = $_SERVER['REQUEST_URI'] ?? ''; } $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true); $data = base64_encode($json); if (strlen($data) > 3 * 1024) { self::$overflowed = true; $record = [ 'message' => 'Incomplete logs, chrome header size limit reached', 'context' => [], 'level' => Logger::WARNING, 'level_name' => Logger::getLevelName(Logger::WARNING), 'channel' => 'monolog', 'datetime' => new \DateTimeImmutable(), 'extra' => [], ]; self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record); $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true); $data = base64_encode($json); } if (trim($data) !== '') { $this->sendHeader(static::HEADER_NAME, $data); } } /** * Send header string to the client */ protected function sendHeader(string $header, string $content): void { if (!headers_sent() && self::$sendHeaders) { header(sprintf('%s: %s', $header, $content)); } } /** * Verifies if the headers are accepted by the current user agent */ protected function headersAccepted(): bool { if (empty($_SERVER['HTTP_USER_AGENT'])) { return false; } return preg_match(static::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']) === 1; } } PK!M77=vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\JsonFormatter; use Monolog\Logger; /** * CouchDB handler * * @author Markus Bachmann */ class CouchDBHandler extends AbstractProcessingHandler { /** @var mixed[] */ private $options; /** * @param mixed[] $options */ public function __construct(array $options = [], $level = Logger::DEBUG, bool $bubble = true) { $this->options = array_merge([ 'host' => 'localhost', 'port' => 5984, 'dbname' => 'logger', 'username' => null, 'password' => null, ], $options); parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { $basicAuth = null; if ($this->options['username']) { $basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']); } $url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname']; $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'content' => $record['formatted'], 'ignore_errors' => true, 'max_redirects' => 0, 'header' => 'Content-type: application/json', ], ]); if (false === @file_get_contents($url, false, $context)) { throw new \RuntimeException(sprintf('Could not connect to %s', $url)); } } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false); } } PK!ex:vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; /** * Logs to Cube. * * @link https://github.com/square/cube/wiki * @author Wan Chen * @deprecated Since 2.8.0 and 3.2.0, Cube appears abandoned and thus we will drop this handler in Monolog 4 */ class CubeHandler extends AbstractProcessingHandler { /** @var resource|\Socket|null */ private $udpConnection = null; /** @var resource|\CurlHandle|null */ private $httpConnection = null; /** @var string */ private $scheme; /** @var string */ private $host; /** @var int */ private $port; /** @var string[] */ private $acceptedSchemes = ['http', 'udp']; /** * Create a Cube handler * * @throws \UnexpectedValueException when given url is not a valid url. * A valid url must consist of three parts : protocol://host:port * Only valid protocols used by Cube are http and udp */ public function __construct(string $url, $level = Logger::DEBUG, bool $bubble = true) { $urlInfo = parse_url($url); if ($urlInfo === false || !isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) { throw new \UnexpectedValueException('URL "'.$url.'" is not valid'); } if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) { throw new \UnexpectedValueException( 'Invalid protocol (' . $urlInfo['scheme'] . ').' . ' Valid options are ' . implode(', ', $this->acceptedSchemes) ); } $this->scheme = $urlInfo['scheme']; $this->host = $urlInfo['host']; $this->port = (int) $urlInfo['port']; parent::__construct($level, $bubble); } /** * Establish a connection to an UDP socket * * @throws \LogicException when unable to connect to the socket * @throws MissingExtensionException when there is no socket extension */ protected function connectUdp(): void { if (!extension_loaded('sockets')) { throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler'); } $udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0); if (false === $udpConnection) { throw new \LogicException('Unable to create a socket'); } $this->udpConnection = $udpConnection; if (!socket_connect($this->udpConnection, $this->host, $this->port)) { throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port); } } /** * Establish a connection to an http server * * @throws \LogicException when unable to connect to the socket * @throws MissingExtensionException when no curl extension */ protected function connectHttp(): void { if (!extension_loaded('curl')) { throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler'); } $httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put'); if (false === $httpConnection) { throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port); } $this->httpConnection = $httpConnection; curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true); } /** * {@inheritDoc} */ protected function write(array $record): void { $date = $record['datetime']; $data = ['time' => $date->format('Y-m-d\TH:i:s.uO')]; unset($record['datetime']); if (isset($record['context']['type'])) { $data['type'] = $record['context']['type']; unset($record['context']['type']); } else { $data['type'] = $record['channel']; } $data['data'] = $record['context']; $data['data']['level'] = $record['level']; if ($this->scheme === 'http') { $this->writeHttp(Utils::jsonEncode($data)); } else { $this->writeUdp(Utils::jsonEncode($data)); } } private function writeUdp(string $data): void { if (!$this->udpConnection) { $this->connectUdp(); } socket_send($this->udpConnection, $data, strlen($data), 0); } private function writeHttp(string $data): void { if (!$this->httpConnection) { $this->connectHttp(); } if (null === $this->httpConnection) { throw new \LogicException('No connection could be established'); } curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']'); curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Content-Length: ' . strlen('['.$data.']'), ]); Curl\Util::execute($this->httpConnection, 5, false); } } PK!Di$hCvendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Psr\Log\LogLevel; /** * Simple handler wrapper that deduplicates log records across multiple requests * * It also includes the BufferHandler functionality and will buffer * all messages until the end of the request or flush() is called. * * This works by storing all log records' messages above $deduplicationLevel * to the file specified by $deduplicationStore. When further logs come in at the end of the * request (or when flush() is called), all those above $deduplicationLevel are checked * against the existing stored logs. If they match and the timestamps in the stored log is * not older than $time seconds, the new log record is discarded. If no log record is new, the * whole data set is discarded. * * This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers * that send messages to people, to avoid spamming with the same message over and over in case of * a major component failure like a database server being down which makes all requests fail in the * same way. * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger */ class DeduplicationHandler extends BufferHandler { /** * @var string */ protected $deduplicationStore; /** * @var Level */ protected $deduplicationLevel; /** * @var int */ protected $time; /** * @var bool */ private $gc = false; /** * @param HandlerInterface $handler Handler. * @param string $deduplicationStore The file/path where the deduplication log should be kept * @param string|int $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes * @param int $time The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * * @phpstan-param Level|LevelName|LogLevel::* $deduplicationLevel */ public function __construct(HandlerInterface $handler, ?string $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, int $time = 60, bool $bubble = true) { parent::__construct($handler, 0, Logger::DEBUG, $bubble, false); $this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore; $this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel); $this->time = $time; } public function flush(): void { if ($this->bufferSize === 0) { return; } $passthru = null; foreach ($this->buffer as $record) { if ($record['level'] >= $this->deduplicationLevel) { $passthru = $passthru || !$this->isDuplicate($record); if ($passthru) { $this->appendRecord($record); } } } // default of null is valid as well as if no record matches duplicationLevel we just pass through if ($passthru === true || $passthru === null) { $this->handler->handleBatch($this->buffer); } $this->clear(); if ($this->gc) { $this->collectLogs(); } } /** * @phpstan-param Record $record */ private function isDuplicate(array $record): bool { if (!file_exists($this->deduplicationStore)) { return false; } $store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!is_array($store)) { return false; } $yesterday = time() - 86400; $timestampValidity = $record['datetime']->getTimestamp() - $this->time; $expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']); for ($i = count($store) - 1; $i >= 0; $i--) { list($timestamp, $level, $message) = explode(':', $store[$i], 3); if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) { return true; } if ($timestamp < $yesterday) { $this->gc = true; } } return false; } private function collectLogs(): void { if (!file_exists($this->deduplicationStore)) { return; } $handle = fopen($this->deduplicationStore, 'rw+'); if (!$handle) { throw new \RuntimeException('Failed to open file for reading and writing: ' . $this->deduplicationStore); } flock($handle, LOCK_EX); $validLogs = []; $timestampValidity = time() - $this->time; while (!feof($handle)) { $log = fgets($handle); if ($log && substr($log, 0, 10) >= $timestampValidity) { $validLogs[] = $log; } } ftruncate($handle, 0); rewind($handle); foreach ($validLogs as $log) { fwrite($handle, $log); } flock($handle, LOCK_UN); fclose($handle); $this->gc = false; } /** * @phpstan-param Record $record */ private function appendRecord(array $record): void { file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND); } } PK!=hhEvendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; use Doctrine\CouchDB\CouchDBClient; /** * CouchDB handler for Doctrine CouchDB ODM * * @author Markus Bachmann */ class DoctrineCouchDBHandler extends AbstractProcessingHandler { /** @var CouchDBClient */ private $client; public function __construct(CouchDBClient $client, $level = Logger::DEBUG, bool $bubble = true) { $this->client = $client; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { $this->client->postDocument($record['formatted']); } protected function getDefaultFormatter(): FormatterInterface { return new NormalizerFormatter; } } PK!@_ >vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Aws\Sdk; use Aws\DynamoDb\DynamoDbClient; use Monolog\Formatter\FormatterInterface; use Aws\DynamoDb\Marshaler; use Monolog\Formatter\ScalarFormatter; use Monolog\Logger; /** * Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/) * * @link https://github.com/aws/aws-sdk-php/ * @author Andrew Lawson */ class DynamoDbHandler extends AbstractProcessingHandler { public const DATE_FORMAT = 'Y-m-d\TH:i:s.uO'; /** * @var DynamoDbClient */ protected $client; /** * @var string */ protected $table; /** * @var int */ protected $version; /** * @var Marshaler */ protected $marshaler; public function __construct(DynamoDbClient $client, string $table, $level = Logger::DEBUG, bool $bubble = true) { /** @phpstan-ignore-next-line */ if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) { $this->version = 3; $this->marshaler = new Marshaler; } else { $this->version = 2; } $this->client = $client; $this->table = $table; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { $filtered = $this->filterEmptyFields($record['formatted']); if ($this->version === 3) { $formatted = $this->marshaler->marshalItem($filtered); } else { /** @phpstan-ignore-next-line */ $formatted = $this->client->formatAttributes($filtered); } $this->client->putItem([ 'TableName' => $this->table, 'Item' => $formatted, ]); } /** * @param mixed[] $record * @return mixed[] */ protected function filterEmptyFields(array $record): array { return array_filter($record, function ($value) { return !empty($value) || false === $value || 0 === $value; }); } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new ScalarFormatter(self::DATE_FORMAT); } } PK!+ >vendor/monolog/monolog/src/Monolog/Handler/ElasticaHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Elastica\Document; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\ElasticaFormatter; use Monolog\Logger; use Elastica\Client; use Elastica\Exception\ExceptionInterface; /** * Elastic Search handler * * Usage example: * * $client = new \Elastica\Client(); * $options = array( * 'index' => 'elastic_index_name', * 'type' => 'elastic_doc_type', Types have been removed in Elastica 7 * ); * $handler = new ElasticaHandler($client, $options); * $log = new Logger('application'); * $log->pushHandler($handler); * * @author Jelle Vink */ class ElasticaHandler extends AbstractProcessingHandler { /** * @var Client */ protected $client; /** * @var mixed[] Handler config options */ protected $options = []; /** * @param Client $client Elastica Client object * @param mixed[] $options Handler configuration */ public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) { parent::__construct($level, $bubble); $this->client = $client; $this->options = array_merge( [ 'index' => 'monolog', // Elastic index name 'type' => 'record', // Elastic document type 'ignore_error' => false, // Suppress Elastica exceptions ], $options ); } /** * {@inheritDoc} */ protected function write(array $record): void { $this->bulkSend([$record['formatted']]); } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { if ($formatter instanceof ElasticaFormatter) { return parent::setFormatter($formatter); } throw new \InvalidArgumentException('ElasticaHandler is only compatible with ElasticaFormatter'); } /** * @return mixed[] */ public function getOptions(): array { return $this->options; } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new ElasticaFormatter($this->options['index'], $this->options['type']); } /** * {@inheritDoc} */ public function handleBatch(array $records): void { $documents = $this->getFormatter()->formatBatch($records); $this->bulkSend($documents); } /** * Use Elasticsearch bulk API to send list of documents * * @param Document[] $documents * * @throws \RuntimeException */ protected function bulkSend(array $documents): void { try { $this->client->addDocuments($documents); } catch (ExceptionInterface $e) { if (!$this->options['ignore_error']) { throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e); } } } } PK!*=Cvendor/monolog/monolog/src/Monolog/Handler/ElasticsearchHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Elastic\Elasticsearch\Response\Elasticsearch; use Throwable; use RuntimeException; use Monolog\Logger; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\ElasticsearchFormatter; use InvalidArgumentException; use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException; use Elasticsearch\Client; use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException; use Elastic\Elasticsearch\Client as Client8; /** * Elasticsearch handler * * @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html * * Simple usage example: * * $client = \Elasticsearch\ClientBuilder::create() * ->setHosts($hosts) * ->build(); * * $options = array( * 'index' => 'elastic_index_name', * 'type' => 'elastic_doc_type', * ); * $handler = new ElasticsearchHandler($client, $options); * $log = new Logger('application'); * $log->pushHandler($handler); * * @author Avtandil Kikabidze */ class ElasticsearchHandler extends AbstractProcessingHandler { /** * @var Client|Client8 */ protected $client; /** * @var mixed[] Handler config options */ protected $options = []; /** * @var bool */ private $needsType; /** * @param Client|Client8 $client Elasticsearch Client object * @param mixed[] $options Handler configuration */ public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) { if (!$client instanceof Client && !$client instanceof Client8) { throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required'); } parent::__construct($level, $bubble); $this->client = $client; $this->options = array_merge( [ 'index' => 'monolog', // Elastic index name 'type' => '_doc', // Elastic document type 'ignore_error' => false, // Suppress Elasticsearch exceptions ], $options ); if ($client instanceof Client8 || $client::VERSION[0] === '7') { $this->needsType = false; // force the type to _doc for ES8/ES7 $this->options['type'] = '_doc'; } else { $this->needsType = true; } } /** * {@inheritDoc} */ protected function write(array $record): void { $this->bulkSend([$record['formatted']]); } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { if ($formatter instanceof ElasticsearchFormatter) { return parent::setFormatter($formatter); } throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter'); } /** * Getter options * * @return mixed[] */ public function getOptions(): array { return $this->options; } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new ElasticsearchFormatter($this->options['index'], $this->options['type']); } /** * {@inheritDoc} */ public function handleBatch(array $records): void { $documents = $this->getFormatter()->formatBatch($records); $this->bulkSend($documents); } /** * Use Elasticsearch bulk API to send list of documents * * @param array[] $records Records + _index/_type keys * @throws \RuntimeException */ protected function bulkSend(array $records): void { try { $params = [ 'body' => [], ]; foreach ($records as $record) { $params['body'][] = [ 'index' => $this->needsType ? [ '_index' => $record['_index'], '_type' => $record['_type'], ] : [ '_index' => $record['_index'], ], ]; unset($record['_index'], $record['_type']); $params['body'][] = $record; } /** @var Elasticsearch */ $responses = $this->client->bulk($params); if ($responses['errors'] === true) { throw $this->createExceptionFromResponses($responses); } } catch (Throwable $e) { if (! $this->options['ignore_error']) { throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e); } } } /** * Creates elasticsearch exception from responses array * * Only the first error is converted into an exception. * * @param mixed[]|Elasticsearch $responses returned by $this->client->bulk() */ protected function createExceptionFromResponses($responses): Throwable { // @phpstan-ignore offsetAccess.nonOffsetAccessible foreach ($responses['items'] ?? [] as $item) { if (isset($item['index']['error'])) { return $this->createExceptionFromError($item['index']['error']); } } if (class_exists(ElasticInvalidArgumentException::class)) { return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.'); } return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.'); } /** * Creates elasticsearch exception from error array * * @param mixed[] $error */ protected function createExceptionFromError(array $error): Throwable { $previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null; if (class_exists(ElasticInvalidArgumentException::class)) { return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous); } return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous); } } PK!  >vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\LineFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; use Monolog\Utils; /** * Stores to PHP error_log() handler. * * @author Elan Ruusamäe */ class ErrorLogHandler extends AbstractProcessingHandler { public const OPERATING_SYSTEM = 0; public const SAPI = 4; /** @var int */ protected $messageType; /** @var bool */ protected $expandNewlines; /** * @param int $messageType Says where the error should go. * @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries */ public function __construct(int $messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, bool $bubble = true, bool $expandNewlines = false) { parent::__construct($level, $bubble); if (false === in_array($messageType, self::getAvailableTypes(), true)) { $message = sprintf('The given message type "%s" is not supported', print_r($messageType, true)); throw new \InvalidArgumentException($message); } $this->messageType = $messageType; $this->expandNewlines = $expandNewlines; } /** * @return int[] With all available types */ public static function getAvailableTypes(): array { return [ self::OPERATING_SYSTEM, self::SAPI, ]; } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%'); } /** * {@inheritDoc} */ protected function write(array $record): void { if (!$this->expandNewlines) { error_log((string) $record['formatted'], $this->messageType); return; } $lines = preg_split('{[\r\n]+}', (string) $record['formatted']); if ($lines === false) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Failed to preg_split formatted string: ' . $pcreErrorCode . ' / '. Utils::pcreLastErrorMessage($pcreErrorCode)); } foreach ($lines as $line) { error_log($line, $this->messageType); } } } PK!VCvendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Throwable; /** * Forwards records to at most one handler * * If a handler fails, the exception is suppressed and the record is forwarded to the next handler. * * As soon as one handler handles a record successfully, the handling stops there. * * @phpstan-import-type Record from \Monolog\Logger */ class FallbackGroupHandler extends GroupHandler { /** * {@inheritDoc} */ public function handle(array $record): bool { if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } foreach ($this->handlers as $handler) { try { $handler->handle($record); break; } catch (Throwable $e) { // What throwable? } } return false === $this->bubble; } /** * {@inheritDoc} */ public function handleBatch(array $records): void { if ($this->processors) { $processed = []; foreach ($records as $record) { $processed[] = $this->processRecord($record); } /** @var Record[] $records */ $records = $processed; } foreach ($this->handlers as $handler) { try { $handler->handleBatch($records); break; } catch (Throwable $e) { // What throwable? } } } } PK!S)<vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; use Psr\Log\LogLevel; /** * Simple handler wrapper that filters records based on a list of levels * * It can be configured with an exact list of levels to allow, or a min/max level. * * @author Hennadiy Verkh * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class FilterHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** * Handler or factory callable($record, $this) * * @var callable|HandlerInterface * @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface */ protected $handler; /** * Minimum level for logs that are passed to handler * * @var int[] * @phpstan-var array */ protected $acceptedLevels; /** * Whether the messages that are handled can bubble up the stack or not * * @var bool */ protected $bubble; /** * @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler * * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $filterHandler). * @param int|array $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided * @param int|string $maxLevel Maximum level to accept, only used if $minLevelOrList is not an array * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * * @phpstan-param Level|LevelName|LogLevel::*|array $minLevelOrList * @phpstan-param Level|LevelName|LogLevel::* $maxLevel */ public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, bool $bubble = true) { $this->handler = $handler; $this->bubble = $bubble; $this->setAcceptedLevels($minLevelOrList, $maxLevel); if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); } } /** * @phpstan-return array */ public function getAcceptedLevels(): array { return array_flip($this->acceptedLevels); } /** * @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided * @param int|string $maxLevel Maximum level or level name to accept, only used if $minLevelOrList is not an array * * @phpstan-param Level|LevelName|LogLevel::*|array $minLevelOrList * @phpstan-param Level|LevelName|LogLevel::* $maxLevel */ public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY): self { if (is_array($minLevelOrList)) { $acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList); } else { $minLevelOrList = Logger::toMonologLevel($minLevelOrList); $maxLevel = Logger::toMonologLevel($maxLevel); $acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) { return $level >= $minLevelOrList && $level <= $maxLevel; })); } $this->acceptedLevels = array_flip($acceptedLevels); return $this; } /** * {@inheritDoc} */ public function isHandling(array $record): bool { return isset($this->acceptedLevels[$record['level']]); } /** * {@inheritDoc} */ public function handle(array $record): bool { if (!$this->isHandling($record)) { return false; } if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } $this->getHandler($record)->handle($record); return false === $this->bubble; } /** * {@inheritDoc} */ public function handleBatch(array $records): void { $filtered = []; foreach ($records as $record) { if ($this->isHandling($record)) { $filtered[] = $record; } } if (count($filtered) > 0) { $this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered); } } /** * Return the nested handler * * If the handler was provided as a factory callable, this will trigger the handler's instantiation. * * @return HandlerInterface * * @phpstan-param Record $record */ public function getHandler(?array $record = null) { if (!$this->handler instanceof HandlerInterface) { $this->handler = ($this->handler)($record, $this); if (!$this->handler instanceof HandlerInterface) { throw new \RuntimeException("The factory callable should return a HandlerInterface"); } } return $this->handler; } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $handler = $this->getHandler(); if ($handler instanceof FormattableHandlerInterface) { $handler->setFormatter($formatter); return $this; } throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { $handler = $this->getHandler(); if ($handler instanceof FormattableHandlerInterface) { return $handler->getFormatter(); } throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } public function reset() { $this->resetProcessors(); if ($this->getHandler() instanceof ResettableInterface) { $this->getHandler()->reset(); } } } PK!{!!Dvendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Monolog\Logger; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; use Psr\Log\LogLevel; /** * Buffers all records until a certain level is reached * * The advantage of this approach is that you don't get any clutter in your log files. * Only requests which actually trigger an error (or whatever your actionLevel is) will be * in the logs, but they will contain all records, not only those above the level threshold. * * You can then have a passthruLevel as well which means that at the end of the request, * even if it did not get activated, it will still send through log records of e.g. at least a * warning level. * * You can find the various activation strategies in the * Monolog\Handler\FingersCrossed\ namespace. * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class FingersCrossedHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** * @var callable|HandlerInterface * @phpstan-var callable(?Record, HandlerInterface): HandlerInterface|HandlerInterface */ protected $handler; /** @var ActivationStrategyInterface */ protected $activationStrategy; /** @var bool */ protected $buffering = true; /** @var int */ protected $bufferSize; /** @var Record[] */ protected $buffer = []; /** @var bool */ protected $stopBuffering; /** * @var ?int * @phpstan-var ?Level */ protected $passthruLevel; /** @var bool */ protected $bubble; /** * @psalm-param HandlerInterface|callable(?Record, HandlerInterface): HandlerInterface $handler * * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $fingersCrossedHandler). * @param int|string|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action, or a level name/value at which the handler is activated * @param int $bufferSize How many entries should be buffered at most, beyond that the oldest items are removed from the buffer. * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * @param bool $stopBuffering Whether the handler should stop buffering after being triggered (default true) * @param int|string $passthruLevel Minimum level to always flush to handler on close, even if strategy not triggered * * @phpstan-param Level|LevelName|LogLevel::* $passthruLevel * @phpstan-param Level|LevelName|LogLevel::*|ActivationStrategyInterface $activationStrategy */ public function __construct($handler, $activationStrategy = null, int $bufferSize = 0, bool $bubble = true, bool $stopBuffering = true, $passthruLevel = null) { if (null === $activationStrategy) { $activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING); } // convert simple int activationStrategy to an object if (!$activationStrategy instanceof ActivationStrategyInterface) { $activationStrategy = new ErrorLevelActivationStrategy($activationStrategy); } $this->handler = $handler; $this->activationStrategy = $activationStrategy; $this->bufferSize = $bufferSize; $this->bubble = $bubble; $this->stopBuffering = $stopBuffering; if ($passthruLevel !== null) { $this->passthruLevel = Logger::toMonologLevel($passthruLevel); } if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); } } /** * {@inheritDoc} */ public function isHandling(array $record): bool { return true; } /** * Manually activate this logger regardless of the activation strategy */ public function activate(): void { if ($this->stopBuffering) { $this->buffering = false; } $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer); $this->buffer = []; } /** * {@inheritDoc} */ public function handle(array $record): bool { if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } if ($this->buffering) { $this->buffer[] = $record; if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) { array_shift($this->buffer); } if ($this->activationStrategy->isHandlerActivated($record)) { $this->activate(); } } else { $this->getHandler($record)->handle($record); } return false === $this->bubble; } /** * {@inheritDoc} */ public function close(): void { $this->flushBuffer(); $this->getHandler()->close(); } public function reset() { $this->flushBuffer(); $this->resetProcessors(); if ($this->getHandler() instanceof ResettableInterface) { $this->getHandler()->reset(); } } /** * Clears the buffer without flushing any messages down to the wrapped handler. * * It also resets the handler to its initial buffering state. */ public function clear(): void { $this->buffer = []; $this->reset(); } /** * Resets the state of the handler. Stops forwarding records to the wrapped handler. */ private function flushBuffer(): void { if (null !== $this->passthruLevel) { $level = $this->passthruLevel; $this->buffer = array_filter($this->buffer, function ($record) use ($level) { return $record['level'] >= $level; }); if (count($this->buffer) > 0) { $this->getHandler(end($this->buffer))->handleBatch($this->buffer); } } $this->buffer = []; $this->buffering = true; } /** * Return the nested handler * * If the handler was provided as a factory callable, this will trigger the handler's instantiation. * * @return HandlerInterface * * @phpstan-param Record $record */ public function getHandler(?array $record = null) { if (!$this->handler instanceof HandlerInterface) { $this->handler = ($this->handler)($record, $this); if (!$this->handler instanceof HandlerInterface) { throw new \RuntimeException("The factory callable should return a HandlerInterface"); } } return $this->handler; } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $handler = $this->getHandler(); if ($handler instanceof FormattableHandlerInterface) { $handler->setFormatter($formatter); return $this; } throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { $handler = $this->getHandler(); if ($handler instanceof FormattableHandlerInterface) { return $handler->getFormatter(); } throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } } PK!=vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\WildfireFormatter; use Monolog\Formatter\FormatterInterface; /** * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol. * * @author Eric Clemmons (@ericclemmons) * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class FirePHPHandler extends AbstractProcessingHandler { use WebRequestRecognizerTrait; /** * WildFire JSON header message format */ protected const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'; /** * FirePHP structure for parsing messages & their presentation */ protected const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'; /** * Must reference a "known" plugin, otherwise headers won't display in FirePHP */ protected const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3'; /** * Header prefix for Wildfire to recognize & parse headers */ protected const HEADER_PREFIX = 'X-Wf'; /** * Whether or not Wildfire vendor-specific headers have been generated & sent yet * @var bool */ protected static $initialized = false; /** * Shared static message index between potentially multiple handlers * @var int */ protected static $messageIndex = 1; /** @var bool */ protected static $sendHeaders = true; /** * Base header creation function used by init headers & record headers * * @param array $meta Wildfire Plugin, Protocol & Structure Indexes * @param string $message Log message * * @return array Complete header string ready for the client as key and message as value * * @phpstan-return non-empty-array */ protected function createHeader(array $meta, string $message): array { $header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta)); return [$header => $message]; } /** * Creates message header from record * * @return array * * @phpstan-return non-empty-array * * @see createHeader() * * @phpstan-param FormattedRecord $record */ protected function createRecordHeader(array $record): array { // Wildfire is extensible to support multiple protocols & plugins in a single request, // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake. return $this->createHeader( [1, 1, 1, self::$messageIndex++], $record['formatted'] ); } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new WildfireFormatter(); } /** * Wildfire initialization headers to enable message parsing * * @see createHeader() * @see sendHeader() * * @return array */ protected function getInitHeaders(): array { // Initial payload consists of required headers for Wildfire return array_merge( $this->createHeader(['Protocol', 1], static::PROTOCOL_URI), $this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI), $this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI) ); } /** * Send header string to the client */ protected function sendHeader(string $header, string $content): void { if (!headers_sent() && self::$sendHeaders) { header(sprintf('%s: %s', $header, $content)); } } /** * Creates & sends header for a record, ensuring init headers have been sent prior * * @see sendHeader() * @see sendInitHeaders() */ protected function write(array $record): void { if (!self::$sendHeaders || !$this->isWebRequest()) { return; } // WildFire-specific headers must be sent prior to any messages if (!self::$initialized) { self::$initialized = true; self::$sendHeaders = $this->headersAccepted(); if (!self::$sendHeaders) { return; } foreach ($this->getInitHeaders() as $header => $content) { $this->sendHeader($header, $content); } } $header = $this->createRecordHeader($record); if (trim(current($header)) !== '') { $this->sendHeader(key($header), current($header)); } } /** * Verifies if the headers are accepted by the current user agent */ protected function headersAccepted(): bool { if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) { return true; } return isset($_SERVER['HTTP_X_FIREPHP_VERSION']); } } PK!_[  ?vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Monolog\Logger; /** * Sends logs to Fleep.io using Webhook integrations * * You'll need a Fleep.io account to use this handler. * * @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation * @author Ando Roots * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class FleepHookHandler extends SocketHandler { protected const FLEEP_HOST = 'fleep.io'; protected const FLEEP_HOOK_URI = '/hook/'; /** * @var string Webhook token (specifies the conversation where logs are sent) */ protected $token; /** * Construct a new Fleep.io Handler. * * For instructions on how to create a new web hook in your conversations * see https://fleep.io/integrations/webhooks/ * * @param string $token Webhook token * @throws MissingExtensionException */ public function __construct( string $token, $level = Logger::DEBUG, bool $bubble = true, bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { if (!extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler'); } $this->token = $token; $connectionString = 'ssl://' . static::FLEEP_HOST . ':443'; parent::__construct( $connectionString, $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); } /** * Returns the default formatter to use with this handler * * Overloaded to remove empty context and extra arrays from the end of the log message. * * @return LineFormatter */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter(null, null, true, true); } /** * Handles a log record */ public function write(array $record): void { parent::write($record); $this->closeSocket(); } /** * {@inheritDoc} */ protected function generateDataStream(array $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } /** * Builds the header of the API Call */ private function buildHeader(string $content): string { $header = "POST " . static::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n"; $header .= "Host: " . static::FLEEP_HOST . "\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($content) . "\r\n"; $header .= "\r\n"; return $header; } /** * Builds the body of API call * * @phpstan-param FormattedRecord $record */ private function buildContent(array $record): string { $dataArray = [ 'message' => $record['formatted'], ]; return http_build_query($dataArray); } } PK!{=g&&>vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; use Monolog\Formatter\FlowdockFormatter; use Monolog\Formatter\FormatterInterface; /** * Sends notifications through the Flowdock push API * * This must be configured with a FlowdockFormatter instance via setFormatter() * * Notes: * API token - Flowdock API token * * @author Dominik Liebler * @see https://www.flowdock.com/api/push * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler * @deprecated Since 2.9.0 and 3.3.0, Flowdock was shutdown we will thus drop this handler in Monolog 4 */ class FlowdockHandler extends SocketHandler { /** * @var string */ protected $apiToken; /** * @throws MissingExtensionException if OpenSSL is missing */ public function __construct( string $apiToken, $level = Logger::DEBUG, bool $bubble = true, bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { if (!extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler'); } parent::__construct( 'ssl://api.flowdock.com:443', $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); $this->apiToken = $apiToken; } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { if (!$formatter instanceof FlowdockFormatter) { throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); } return parent::setFormatter($formatter); } /** * Gets the default formatter. */ protected function getDefaultFormatter(): FormatterInterface { throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly'); } /** * {@inheritDoc} */ protected function write(array $record): void { parent::write($record); $this->closeSocket(); } /** * {@inheritDoc} */ protected function generateDataStream(array $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } /** * Builds the body of API call * * @phpstan-param FormattedRecord $record */ private function buildContent(array $record): string { return Utils::jsonEncode($record['formatted']['flowdock']); } /** * Builds the header of the API Call */ private function buildHeader(string $content): string { $header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n"; $header .= "Host: api.flowdock.com\r\n"; $header .= "Content-Type: application/json\r\n"; $header .= "Content-Length: " . strlen($content) . "\r\n"; $header .= "\r\n"; return $header; } } PK!BMMJvendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; /** * Interface to describe loggers that have a formatter * * @author Jordi Boggiano */ interface FormattableHandlerInterface { /** * Sets the formatter. * * @param FormatterInterface $formatter * @return HandlerInterface self */ public function setFormatter(FormatterInterface $formatter): HandlerInterface; /** * Gets the formatter. * * @return FormatterInterface */ public function getFormatter(): FormatterInterface; } PK!y`Fvendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; /** * Helper trait for implementing FormattableInterface * * @author Jordi Boggiano */ trait FormattableHandlerTrait { /** * @var ?FormatterInterface */ protected $formatter; /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $this->formatter = $formatter; return $this; } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { if (!$this->formatter) { $this->formatter = $this->getDefaultFormatter(); } return $this->formatter; } /** * Gets the default formatter. * * Overwrite this if the LineFormatter is not a good default for your handler. */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter(); } } PK!_9ww:vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Gelf\PublisherInterface; use Monolog\Logger; use Monolog\Formatter\GelfMessageFormatter; use Monolog\Formatter\FormatterInterface; /** * Handler to send messages to a Graylog2 (http://www.graylog2.org) server * * @author Matt Lehner * @author Benjamin Zikarsky */ class GelfHandler extends AbstractProcessingHandler { /** * @var PublisherInterface the publisher object that sends the message to the server */ protected $publisher; /** * @param PublisherInterface $publisher a gelf publisher object */ public function __construct(PublisherInterface $publisher, $level = Logger::DEBUG, bool $bubble = true) { parent::__construct($level, $bubble); $this->publisher = $publisher; } /** * {@inheritDoc} */ protected function write(array $record): void { $this->publisher->publish($record['formatted']); } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new GelfMessageFormatter(); } } PK!> ;vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\ResettableInterface; /** * Forwards records to multiple handlers * * @author Lenar Lõhmus * * @phpstan-import-type Record from \Monolog\Logger */ class GroupHandler extends Handler implements ProcessableHandlerInterface, ResettableInterface { use ProcessableHandlerTrait; /** @var HandlerInterface[] */ protected $handlers; /** @var bool */ protected $bubble; /** * @param HandlerInterface[] $handlers Array of Handlers. * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct(array $handlers, bool $bubble = true) { foreach ($handlers as $handler) { if (!$handler instanceof HandlerInterface) { throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.'); } } $this->handlers = $handlers; $this->bubble = $bubble; } /** * {@inheritDoc} */ public function isHandling(array $record): bool { foreach ($this->handlers as $handler) { if ($handler->isHandling($record)) { return true; } } return false; } /** * {@inheritDoc} */ public function handle(array $record): bool { if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } foreach ($this->handlers as $handler) { $handler->handle($record); } return false === $this->bubble; } /** * {@inheritDoc} */ public function handleBatch(array $records): void { if ($this->processors) { $processed = []; foreach ($records as $record) { $processed[] = $this->processRecord($record); } /** @var Record[] $records */ $records = $processed; } foreach ($this->handlers as $handler) { $handler->handleBatch($records); } } public function reset() { $this->resetProcessors(); foreach ($this->handlers as $handler) { if ($handler instanceof ResettableInterface) { $handler->reset(); } } } public function close(): void { parent::close(); foreach ($this->handlers as $handler) { $handler->close(); } } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { foreach ($this->handlers as $handler) { if ($handler instanceof FormattableHandlerInterface) { $handler->setFormatter($formatter); } } return $this; } } PK!6 ?vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; /** * Interface that all Monolog Handlers must implement * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger */ interface HandlerInterface { /** * Checks whether the given record will be handled by this handler. * * This is mostly done for performance reasons, to avoid calling processors for nothing. * * Handlers should still check the record levels within handle(), returning false in isHandling() * is no guarantee that handle() will not be called, and isHandling() might not be called * for a given record. * * @param array $record Partial log record containing only a level key * * @return bool * * @phpstan-param array{level: Level} $record */ public function isHandling(array $record): bool; /** * Handles a record. * * All records may be passed to this method, and the handler should discard * those that it does not want to handle. * * The return value of this function controls the bubbling process of the handler stack. * Unless the bubbling is interrupted (by returning true), the Logger class will keep on * calling further handlers in the stack with a given log record. * * @param array $record The record to handle * @return bool true means that this handler handled the record, and that bubbling is not permitted. * false means the record was either not processed or that this handler allows bubbling. * * @phpstan-param Record $record */ public function handle(array $record): bool; /** * Handles a set of records at once. * * @param array $records The records to handle (an array of record arrays) * * @phpstan-param Record[] $records */ public function handleBatch(array $records): void; /** * Closes the handler. * * Ends a log cycle and frees all resources used by the handler. * * Closing a Handler means flushing all buffers and freeing any open resources/handles. * * Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage) * and ideally handlers should be able to reopen themselves on handle() after they have been closed. * * This is useful at the end of a request and will be called automatically when the object * is destroyed if you extend Monolog\Handler\Handler. * * If you are thinking of calling this method yourself, most likely you should be * calling ResettableInterface::reset instead. Have a look. */ public function close(): void; } PK!46vendor/monolog/monolog/src/Monolog/Handler/Handler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; /** * Base Handler class providing basic close() support as well as handleBatch * * @author Jordi Boggiano */ abstract class Handler implements HandlerInterface { /** * {@inheritDoc} */ public function handleBatch(array $records): void { foreach ($records as $record) { $this->handle($record); } } /** * {@inheritDoc} */ public function close(): void { } public function __destruct() { try { $this->close(); } catch (\Throwable $e) { // do nothing } } public function __sleep() { $this->close(); $reflClass = new \ReflectionClass($this); $keys = []; foreach ($reflClass->getProperties() as $reflProp) { if (!$reflProp->isStatic()) { $keys[] = $reflProp->getName(); } } return $keys; } } PK!C  =vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; /** * This simple wrapper class can be used to extend handlers functionality. * * Example: A custom filtering that can be applied to any handler. * * Inherit from this class and override handle() like this: * * public function handle(array $record) * { * if ($record meets certain conditions) { * return false; * } * return $this->handler->handle($record); * } * * @author Alexey Karapetov */ class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface { /** * @var HandlerInterface */ protected $handler; public function __construct(HandlerInterface $handler) { $this->handler = $handler; } /** * {@inheritDoc} */ public function isHandling(array $record): bool { return $this->handler->isHandling($record); } /** * {@inheritDoc} */ public function handle(array $record): bool { return $this->handler->handle($record); } /** * {@inheritDoc} */ public function handleBatch(array $records): void { $this->handler->handleBatch($records); } /** * {@inheritDoc} */ public function close(): void { $this->handler->close(); } /** * {@inheritDoc} */ public function pushProcessor(callable $callback): HandlerInterface { if ($this->handler instanceof ProcessableHandlerInterface) { $this->handler->pushProcessor($callback); return $this; } throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class); } /** * {@inheritDoc} */ public function popProcessor(): callable { if ($this->handler instanceof ProcessableHandlerInterface) { return $this->handler->popProcessor(); } throw new \LogicException('The wrapped handler does not implement ' . ProcessableHandlerInterface::class); } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { if ($this->handler instanceof FormattableHandlerInterface) { $this->handler->setFormatter($formatter); return $this; } throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { if ($this->handler instanceof FormattableHandlerInterface) { return $this->handler->getFormatter(); } throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); } public function reset() { if ($this->handler instanceof ResettableInterface) { $this->handler->reset(); } } } PK!wӇ;vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; /** * IFTTTHandler uses cURL to trigger IFTTT Maker actions * * Register a secret key and trigger/event name at https://ifttt.com/maker * * value1 will be the channel from monolog's Logger constructor, * value2 will be the level name (ERROR, WARNING, ..) * value3 will be the log record's message * * @author Nehal Patel */ class IFTTTHandler extends AbstractProcessingHandler { /** @var string */ private $eventName; /** @var string */ private $secretKey; /** * @param string $eventName The name of the IFTTT Maker event that should be triggered * @param string $secretKey A valid IFTTT secret key */ public function __construct(string $eventName, string $secretKey, $level = Logger::ERROR, bool $bubble = true) { if (!extension_loaded('curl')) { throw new MissingExtensionException('The curl extension is needed to use the IFTTTHandler'); } $this->eventName = $eventName; $this->secretKey = $secretKey; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ public function write(array $record): void { $postData = [ "value1" => $record["channel"], "value2" => $record["level_name"], "value3" => $record["message"], ]; $postString = Utils::jsonEncode($postData); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $postString); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Content-Type: application/json", ]); Curl\Util::execute($ch); } } PK!::@vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; /** * Inspired on LogEntriesHandler. * * @author Robert Kaufmann III * @author Gabriel Machado */ class InsightOpsHandler extends SocketHandler { /** * @var string */ protected $logToken; /** * @param string $token Log token supplied by InsightOps * @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'. * @param bool $useSSL Whether or not SSL encryption should be used * * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing */ public function __construct( string $token, string $region = 'us', bool $useSSL = true, $level = Logger::DEBUG, bool $bubble = true, bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { if ($useSSL && !extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for InsightOpsHandler'); } $endpoint = $useSSL ? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443' : $region . '.data.logs.insight.rapid7.com:80'; parent::__construct( $endpoint, $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); $this->logToken = $token; } /** * {@inheritDoc} */ protected function generateDataStream(array $record): string { return $this->logToken . ' ' . $record['formatted']; } } PK! g@vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; /** * @author Robert Kaufmann III */ class LogEntriesHandler extends SocketHandler { /** * @var string */ protected $logToken; /** * @param string $token Log token supplied by LogEntries * @param bool $useSSL Whether or not SSL encryption should be used. * @param string $host Custom hostname to send the data to if needed * * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing */ public function __construct( string $token, bool $useSSL = true, $level = Logger::DEBUG, bool $bubble = true, string $host = 'data.logentries.com', bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { if ($useSSL && !extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler'); } $endpoint = $useSSL ? 'ssl://' . $host . ':443' : $host . ':80'; parent::__construct( $endpoint, $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); $this->logToken = $token; } /** * {@inheritDoc} */ protected function generateDataStream(array $record): string { return $this->logToken . ' ' . $record['formatted']; } } PK!F<vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LogglyFormatter; use function array_key_exists; use CurlHandle; /** * Sends errors to Loggly. * * @author Przemek Sobstel * @author Adam Pancutt * @author Gregory Barchard */ class LogglyHandler extends AbstractProcessingHandler { protected const HOST = 'logs-01.loggly.com'; protected const ENDPOINT_SINGLE = 'inputs'; protected const ENDPOINT_BATCH = 'bulk'; /** * Caches the curl handlers for every given endpoint. * * @var resource[]|CurlHandle[] */ protected $curlHandlers = []; /** @var string */ protected $token; /** @var string[] */ protected $tag = []; /** * @param string $token API token supplied by Loggly * * @throws MissingExtensionException If the curl extension is missing */ public function __construct(string $token, $level = Logger::DEBUG, bool $bubble = true) { if (!extension_loaded('curl')) { throw new MissingExtensionException('The curl extension is needed to use the LogglyHandler'); } $this->token = $token; parent::__construct($level, $bubble); } /** * Loads and returns the shared curl handler for the given endpoint. * * @param string $endpoint * * @return resource|CurlHandle */ protected function getCurlHandler(string $endpoint) { if (!array_key_exists($endpoint, $this->curlHandlers)) { $this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint); } return $this->curlHandlers[$endpoint]; } /** * Starts a fresh curl session for the given endpoint and returns its handler. * * @param string $endpoint * * @return resource|CurlHandle */ private function loadCurlHandle(string $endpoint) { $url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); return $ch; } /** * @param string[]|string $tag */ public function setTag($tag): self { $tag = !empty($tag) ? $tag : []; $this->tag = is_array($tag) ? $tag : [$tag]; return $this; } /** * @param string[]|string $tag */ public function addTag($tag): self { if (!empty($tag)) { $tag = is_array($tag) ? $tag : [$tag]; $this->tag = array_unique(array_merge($this->tag, $tag)); } return $this; } protected function write(array $record): void { $this->send($record["formatted"], static::ENDPOINT_SINGLE); } public function handleBatch(array $records): void { $level = $this->level; $records = array_filter($records, function ($record) use ($level) { return ($record['level'] >= $level); }); if ($records) { $this->send($this->getFormatter()->formatBatch($records), static::ENDPOINT_BATCH); } } protected function send(string $data, string $endpoint): void { $ch = $this->getCurlHandler($endpoint); $headers = ['Content-Type: application/json']; if (!empty($this->tag)) { $headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag); } curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); Curl\Util::execute($ch, 5, false); } protected function getDefaultFormatter(): FormatterInterface { return new LogglyFormatter(); } } PK!ܩ >vendor/monolog/monolog/src/Monolog/Handler/LogmaticHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LogmaticFormatter; /** * @author Julien Breux */ class LogmaticHandler extends SocketHandler { /** * @var string */ private $logToken; /** * @var string */ private $hostname; /** * @var string */ private $appname; /** * @param string $token Log token supplied by Logmatic. * @param string $hostname Host name supplied by Logmatic. * @param string $appname Application name supplied by Logmatic. * @param bool $useSSL Whether or not SSL encryption should be used. * * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing */ public function __construct( string $token, string $hostname = '', string $appname = '', bool $useSSL = true, $level = Logger::DEBUG, bool $bubble = true, bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { if ($useSSL && !extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use SSL encrypted connection for LogmaticHandler'); } $endpoint = $useSSL ? 'ssl://api.logmatic.io:10515' : 'api.logmatic.io:10514'; $endpoint .= '/v1/'; parent::__construct( $endpoint, $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); $this->logToken = $token; $this->hostname = $hostname; $this->appname = $appname; } /** * {@inheritDoc} */ protected function generateDataStream(array $record): string { return $this->logToken . ' ' . $record['formatted']; } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { $formatter = new LogmaticFormatter(); if (!empty($this->hostname)) { $formatter->setHostname($this->hostname); } if (!empty($this->appname)) { $formatter->setAppname($this->appname); } return $formatter; } } PK!ww% % :vendor/monolog/monolog/src/Monolog/Handler/MailHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\HtmlFormatter; /** * Base class for all mail handlers * * @author Gyula Sallai * * @phpstan-import-type Record from \Monolog\Logger */ abstract class MailHandler extends AbstractProcessingHandler { /** * {@inheritDoc} */ public function handleBatch(array $records): void { $messages = []; foreach ($records as $record) { if ($record['level'] < $this->level) { continue; } /** @var Record $message */ $message = $this->processRecord($record); $messages[] = $message; } if (!empty($messages)) { $this->send((string) $this->getFormatter()->formatBatch($messages), $messages); } } /** * Send a mail with the given content * * @param string $content formatted email body to be sent * @param array $records the array of log records that formed this content * * @phpstan-param Record[] $records */ abstract protected function send(string $content, array $records): void; /** * {@inheritDoc} */ protected function write(array $record): void { $this->send((string) $record['formatted'], [$record]); } /** * @phpstan-param non-empty-array $records * @phpstan-return Record */ protected function getHighestRecord(array $records): array { $highestRecord = null; foreach ($records as $record) { if ($highestRecord === null || $highestRecord['level'] < $record['level']) { $highestRecord = $record; } } return $highestRecord; } protected function isHtmlBody(string $body): bool { return ($body[0] ?? null) === '<'; } /** * Gets the default formatter. * * @return FormatterInterface */ protected function getDefaultFormatter(): FormatterInterface { return new HtmlFormatter(); } } PK!H7 >vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Swift; use Swift_Message; /** * MandrillHandler uses cURL to send the emails to the Mandrill API * * @author Adam Nicholson */ class MandrillHandler extends MailHandler { /** @var Swift_Message */ protected $message; /** @var string */ protected $apiKey; /** * @psalm-param Swift_Message|callable(): Swift_Message $message * * @param string $apiKey A valid Mandrill API key * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced */ public function __construct(string $apiKey, $message, $level = Logger::ERROR, bool $bubble = true) { parent::__construct($level, $bubble); if (!$message instanceof Swift_Message && is_callable($message)) { $message = $message(); } if (!$message instanceof Swift_Message) { throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it'); } $this->message = $message; $this->apiKey = $apiKey; } /** * {@inheritDoc} */ protected function send(string $content, array $records): void { $mime = 'text/plain'; if ($this->isHtmlBody($content)) { $mime = 'text/html'; } $message = clone $this->message; $message->setBody($content, $mime); /** @phpstan-ignore-next-line */ if (version_compare(Swift::VERSION, '6.0.0', '>=')) { $message->setDate(new \DateTimeImmutable()); } else { /** @phpstan-ignore-next-line */ $message->setDate(time()); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://mandrillapp.com/api/1.0/messages/send-raw.json'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 'key' => $this->apiKey, 'raw_message' => (string) $message, 'async' => false, ])); Curl\Util::execute($ch); } } PK!Hvendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; /** * Exception can be thrown if an extension for a handler is missing * * @author Christian Bergau */ class MissingExtensionException extends \Exception { } PK!C =vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use MongoDB\Driver\BulkWrite; use MongoDB\Driver\Manager; use MongoDB\Client; use Monolog\Logger; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\MongoDBFormatter; /** * Logs to a MongoDB database. * * Usage example: * * $log = new \Monolog\Logger('application'); * $client = new \MongoDB\Client('mongodb://localhost:27017'); * $mongodb = new \Monolog\Handler\MongoDBHandler($client, 'logs', 'prod'); * $log->pushHandler($mongodb); * * The above examples uses the MongoDB PHP library's client class; however, the * MongoDB\Driver\Manager class from ext-mongodb is also supported. */ class MongoDBHandler extends AbstractProcessingHandler { /** @var \MongoDB\Collection */ private $collection; /** @var Client|Manager */ private $manager; /** @var string */ private $namespace; /** * Constructor. * * @param Client|Manager $mongodb MongoDB library or driver client * @param string $database Database name * @param string $collection Collection name */ public function __construct($mongodb, string $database, string $collection, $level = Logger::DEBUG, bool $bubble = true) { if (!($mongodb instanceof Client || $mongodb instanceof Manager)) { throw new \InvalidArgumentException('MongoDB\Client or MongoDB\Driver\Manager instance required'); } if ($mongodb instanceof Client) { $this->collection = $mongodb->selectCollection($database, $collection); } else { $this->manager = $mongodb; $this->namespace = $database . '.' . $collection; } parent::__construct($level, $bubble); } protected function write(array $record): void { if (isset($this->collection)) { $this->collection->insertOne($record['formatted']); } if (isset($this->manager, $this->namespace)) { $bulk = new BulkWrite; $bulk->insert($record["formatted"]); $this->manager->executeBulkWrite($this->namespace, $bulk); } } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new MongoDBFormatter; } } PK!^wn#kkBvendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Formatter\LineFormatter; /** * NativeMailerHandler uses the mail() function to send the emails * * @author Christophe Coevoet * @author Mark Garrett */ class NativeMailerHandler extends MailHandler { /** * The email addresses to which the message will be sent * @var string[] */ protected $to; /** * The subject of the email * @var string */ protected $subject; /** * Optional headers for the message * @var string[] */ protected $headers = []; /** * Optional parameters for the message * @var string[] */ protected $parameters = []; /** * The wordwrap length for the message * @var int */ protected $maxColumnWidth; /** * The Content-type for the message * @var string|null */ protected $contentType; /** * The encoding for the message * @var string */ protected $encoding = 'utf-8'; /** * @param string|string[] $to The receiver of the mail * @param string $subject The subject of the mail * @param string $from The sender of the mail * @param int $maxColumnWidth The maximum column width that the message lines will have */ public function __construct($to, string $subject, string $from, $level = Logger::ERROR, bool $bubble = true, int $maxColumnWidth = 70) { parent::__construct($level, $bubble); $this->to = (array) $to; $this->subject = $subject; $this->addHeader(sprintf('From: %s', $from)); $this->maxColumnWidth = $maxColumnWidth; } /** * Add headers to the message * * @param string|string[] $headers Custom added headers */ public function addHeader($headers): self { foreach ((array) $headers as $header) { if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) { throw new \InvalidArgumentException('Headers can not contain newline characters for security reasons'); } $this->headers[] = $header; } return $this; } /** * Add parameters to the message * * @param string|string[] $parameters Custom added parameters */ public function addParameter($parameters): self { $this->parameters = array_merge($this->parameters, (array) $parameters); return $this; } /** * {@inheritDoc} */ protected function send(string $content, array $records): void { $contentType = $this->getContentType() ?: ($this->isHtmlBody($content) ? 'text/html' : 'text/plain'); if ($contentType !== 'text/html') { $content = wordwrap($content, $this->maxColumnWidth); } $headers = ltrim(implode("\r\n", $this->headers) . "\r\n", "\r\n"); $headers .= 'Content-type: ' . $contentType . '; charset=' . $this->getEncoding() . "\r\n"; if ($contentType === 'text/html' && false === strpos($headers, 'MIME-Version:')) { $headers .= 'MIME-Version: 1.0' . "\r\n"; } $subject = $this->subject; if ($records) { $subjectFormatter = new LineFormatter($this->subject); $subject = $subjectFormatter->format($this->getHighestRecord($records)); } $parameters = implode(' ', $this->parameters); foreach ($this->to as $to) { mail($to, $subject, $content, $headers, $parameters); } } public function getContentType(): ?string { return $this->contentType; } public function getEncoding(): string { return $this->encoding; } /** * @param string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML messages. */ public function setContentType(string $contentType): self { if (strpos($contentType, "\n") !== false || strpos($contentType, "\r") !== false) { throw new \InvalidArgumentException('The content type can not contain newline characters to prevent email header injection'); } $this->contentType = $contentType; return $this; } public function setEncoding(string $encoding): self { if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) { throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection'); } $this->encoding = $encoding; return $this; } } PK!RCC>vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; use Monolog\Formatter\NormalizerFormatter; use Monolog\Formatter\FormatterInterface; /** * Class to record a log on a NewRelic application. * Enabling New Relic High Security mode may prevent capture of useful information. * * This handler requires a NormalizerFormatter to function and expects an array in $record['formatted'] * * @see https://docs.newrelic.com/docs/agents/php-agent * @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security */ class NewRelicHandler extends AbstractProcessingHandler { /** * Name of the New Relic application that will receive logs from this handler. * * @var ?string */ protected $appName; /** * Name of the current transaction * * @var ?string */ protected $transactionName; /** * Some context and extra data is passed into the handler as arrays of values. Do we send them as is * (useful if we are using the API), or explode them for display on the NewRelic RPM website? * * @var bool */ protected $explodeArrays; /** * {@inheritDoc} * * @param string|null $appName * @param bool $explodeArrays * @param string|null $transactionName */ public function __construct( $level = Logger::ERROR, bool $bubble = true, ?string $appName = null, bool $explodeArrays = false, ?string $transactionName = null ) { parent::__construct($level, $bubble); $this->appName = $appName; $this->explodeArrays = $explodeArrays; $this->transactionName = $transactionName; } /** * {@inheritDoc} */ protected function write(array $record): void { if (!$this->isNewRelicEnabled()) { throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler'); } if ($appName = $this->getAppName($record['context'])) { $this->setNewRelicAppName($appName); } if ($transactionName = $this->getTransactionName($record['context'])) { $this->setNewRelicTransactionName($transactionName); unset($record['formatted']['context']['transaction_name']); } if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { newrelic_notice_error($record['message'], $record['context']['exception']); unset($record['formatted']['context']['exception']); } else { newrelic_notice_error($record['message']); } if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) { foreach ($record['formatted']['context'] as $key => $parameter) { if (is_array($parameter) && $this->explodeArrays) { foreach ($parameter as $paramKey => $paramValue) { $this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue); } } else { $this->setNewRelicParameter('context_' . $key, $parameter); } } } if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) { foreach ($record['formatted']['extra'] as $key => $parameter) { if (is_array($parameter) && $this->explodeArrays) { foreach ($parameter as $paramKey => $paramValue) { $this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue); } } else { $this->setNewRelicParameter('extra_' . $key, $parameter); } } } } /** * Checks whether the NewRelic extension is enabled in the system. * * @return bool */ protected function isNewRelicEnabled(): bool { return extension_loaded('newrelic'); } /** * Returns the appname where this log should be sent. Each log can override the default appname, set in this * handler's constructor, by providing the appname in it's context. * * @param mixed[] $context */ protected function getAppName(array $context): ?string { if (isset($context['appname'])) { return $context['appname']; } return $this->appName; } /** * Returns the name of the current transaction. Each log can override the default transaction name, set in this * handler's constructor, by providing the transaction_name in it's context * * @param mixed[] $context */ protected function getTransactionName(array $context): ?string { if (isset($context['transaction_name'])) { return $context['transaction_name']; } return $this->transactionName; } /** * Sets the NewRelic application that should receive this log. */ protected function setNewRelicAppName(string $appName): void { newrelic_set_appname($appName); } /** * Overwrites the name of the current transaction */ protected function setNewRelicTransactionName(string $transactionName): void { newrelic_name_transaction($transactionName); } /** * @param string $key * @param mixed $value */ protected function setNewRelicParameter(string $key, $value): void { if (null === $value || is_scalar($value)) { newrelic_add_custom_parameter($key, $value); } else { newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true)); } } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new NormalizerFormatter(); } } PK!hpp:vendor/monolog/monolog/src/Monolog/Handler/NoopHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; /** * No-op * * This handler handles anything, but does nothing, and does not stop bubbling to the rest of the stack. * This can be used for testing, or to disable a handler when overriding a configuration without * influencing the rest of the stack. * * @author Roel Harbers */ class NoopHandler extends Handler { /** * {@inheritDoc} */ public function isHandling(array $record): bool { return true; } /** * {@inheritDoc} */ public function handle(array $record): bool { return false; } } PK!Ik99:vendor/monolog/monolog/src/Monolog/Handler/NullHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Psr\Log\LogLevel; /** * Blackhole * * Any record it can handle will be thrown away. This can be used * to put on top of an existing stack to override it temporarily. * * @author Jordi Boggiano * * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class NullHandler extends Handler { /** * @var int */ private $level; /** * @param string|int $level The minimum logging level at which this handler will be triggered * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function __construct($level = Logger::DEBUG) { $this->level = Logger::toMonologLevel($level); } /** * {@inheritDoc} */ public function isHandling(array $record): bool { return $record['level'] >= $this->level; } /** * {@inheritDoc} */ public function handle(array $record): bool { return $record['level'] >= $this->level; } } PK!cR>vendor/monolog/monolog/src/Monolog/Handler/OverflowHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Formatter\FormatterInterface; /** * Handler to only pass log messages when a certain threshold of number of messages is reached. * * This can be useful in cases of processing a batch of data, but you're for example only interested * in case it fails catastrophically instead of a warning for 1 or 2 events. Worse things can happen, right? * * Usage example: * * ``` * $log = new Logger('application'); * $handler = new SomeHandler(...) * * // Pass all warnings to the handler when more than 10 & all error messages when more then 5 * $overflow = new OverflowHandler($handler, [Logger::WARNING => 10, Logger::ERROR => 5]); * * $log->pushHandler($overflow); *``` * * @author Kris Buist */ class OverflowHandler extends AbstractHandler implements FormattableHandlerInterface { /** @var HandlerInterface */ private $handler; /** @var int[] */ private $thresholdMap = [ Logger::DEBUG => 0, Logger::INFO => 0, Logger::NOTICE => 0, Logger::WARNING => 0, Logger::ERROR => 0, Logger::CRITICAL => 0, Logger::ALERT => 0, Logger::EMERGENCY => 0, ]; /** * Buffer of all messages passed to the handler before the threshold was reached * * @var mixed[][] */ private $buffer = []; /** * @param HandlerInterface $handler * @param int[] $thresholdMap Dictionary of logger level => threshold */ public function __construct( HandlerInterface $handler, array $thresholdMap = [], $level = Logger::DEBUG, bool $bubble = true ) { $this->handler = $handler; foreach ($thresholdMap as $thresholdLevel => $threshold) { $this->thresholdMap[$thresholdLevel] = $threshold; } parent::__construct($level, $bubble); } /** * Handles a record. * * All records may be passed to this method, and the handler should discard * those that it does not want to handle. * * The return value of this function controls the bubbling process of the handler stack. * Unless the bubbling is interrupted (by returning true), the Logger class will keep on * calling further handlers in the stack with a given log record. * * {@inheritDoc} */ public function handle(array $record): bool { if ($record['level'] < $this->level) { return false; } $level = $record['level']; if (!isset($this->thresholdMap[$level])) { $this->thresholdMap[$level] = 0; } if ($this->thresholdMap[$level] > 0) { // The overflow threshold is not yet reached, so we're buffering the record and lowering the threshold by 1 $this->thresholdMap[$level]--; $this->buffer[$level][] = $record; return false === $this->bubble; } if ($this->thresholdMap[$level] == 0) { // This current message is breaking the threshold. Flush the buffer and continue handling the current record foreach ($this->buffer[$level] ?? [] as $buffered) { $this->handler->handle($buffered); } $this->thresholdMap[$level]--; unset($this->buffer[$level]); } $this->handler->handle($record); return false === $this->bubble; } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { if ($this->handler instanceof FormattableHandlerInterface) { $this->handler->setFormatter($formatter); return $this; } throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { if ($this->handler instanceof FormattableHandlerInterface) { return $this->handler->getFormatter(); } throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); } } PK!vY,),)@vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\LineFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; use Monolog\Utils; use PhpConsole\Connector; use PhpConsole\Handler as VendorPhpConsoleHandler; use PhpConsole\Helper; /** * Monolog handler for Google Chrome extension "PHP Console" * * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely * * Usage: * 1. Install Google Chrome extension [now dead and removed from the chrome store] * 2. See overview https://github.com/barbushin/php-console#overview * 3. Install PHP Console library https://github.com/barbushin/php-console#installation * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png) * * $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler())); * \Monolog\ErrorHandler::register($logger); * echo $undefinedVar; * $logger->debug('SELECT * FROM users', array('db', 'time' => 0.012)); * PC::debug($_SERVER); // PHP Console debugger for any type of vars * * @author Sergey Barbushin https://www.linkedin.com/in/barbushin * * @phpstan-import-type Record from \Monolog\Logger * @deprecated Since 2.8.0 and 3.2.0, PHPConsole is abandoned and thus we will drop this handler in Monolog 4 */ class PHPConsoleHandler extends AbstractProcessingHandler { /** @var array */ private $options = [ 'enabled' => true, // bool Is PHP Console server enabled 'classesPartialsTraceIgnore' => ['Monolog\\'], // array Hide calls of classes started with... 'debugTagsKeysInContext' => [0, 'tag'], // bool Is PHP Console server enabled 'useOwnErrorsHandler' => false, // bool Enable errors handling 'useOwnExceptionsHandler' => false, // bool Enable exceptions handling 'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths 'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s') 'serverEncoding' => null, // string|null Server internal encoding 'headersLimit' => null, // int|null Set headers size limit for your web-server 'password' => null, // string|null Protect PHP Console connection by password 'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed 'ipMasks' => [], // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1') 'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required) 'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings 'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level 'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number 'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item 'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON 'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug 'dataStorage' => null, // \PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ) ]; /** @var Connector */ private $connector; /** * @param array $options See \Monolog\Handler\PHPConsoleHandler::$options for more details * @param Connector|null $connector Instance of \PhpConsole\Connector class (optional) * @throws \RuntimeException */ public function __construct(array $options = [], ?Connector $connector = null, $level = Logger::DEBUG, bool $bubble = true) { if (!class_exists('PhpConsole\Connector')) { throw new \RuntimeException('PHP Console library not found. See https://github.com/barbushin/php-console#installation'); } parent::__construct($level, $bubble); $this->options = $this->initOptions($options); $this->connector = $this->initConnector($connector); } /** * @param array $options * * @return array */ private function initOptions(array $options): array { $wrongOptions = array_diff(array_keys($options), array_keys($this->options)); if ($wrongOptions) { throw new \RuntimeException('Unknown options: ' . implode(', ', $wrongOptions)); } return array_replace($this->options, $options); } private function initConnector(?Connector $connector = null): Connector { if (!$connector) { if ($this->options['dataStorage']) { Connector::setPostponeStorage($this->options['dataStorage']); } $connector = Connector::getInstance(); } if ($this->options['registerHelper'] && !Helper::isRegistered()) { Helper::register(); } if ($this->options['enabled'] && $connector->isActiveClient()) { if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) { $handler = VendorPhpConsoleHandler::getInstance(); $handler->setHandleErrors($this->options['useOwnErrorsHandler']); $handler->setHandleExceptions($this->options['useOwnExceptionsHandler']); $handler->start(); } if ($this->options['sourcesBasePath']) { $connector->setSourcesBasePath($this->options['sourcesBasePath']); } if ($this->options['serverEncoding']) { $connector->setServerEncoding($this->options['serverEncoding']); } if ($this->options['password']) { $connector->setPassword($this->options['password']); } if ($this->options['enableSslOnlyMode']) { $connector->enableSslOnlyMode(); } if ($this->options['ipMasks']) { $connector->setAllowedIpMasks($this->options['ipMasks']); } if ($this->options['headersLimit']) { $connector->setHeadersLimit($this->options['headersLimit']); } if ($this->options['detectDumpTraceAndSource']) { $connector->getDebugDispatcher()->detectTraceAndSource = true; } $dumper = $connector->getDumper(); $dumper->levelLimit = $this->options['dumperLevelLimit']; $dumper->itemsCountLimit = $this->options['dumperItemsCountLimit']; $dumper->itemSizeLimit = $this->options['dumperItemSizeLimit']; $dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit']; $dumper->detectCallbacks = $this->options['dumperDetectCallbacks']; if ($this->options['enableEvalListener']) { $connector->startEvalRequestsListener(); } } return $connector; } public function getConnector(): Connector { return $this->connector; } /** * @return array */ public function getOptions(): array { return $this->options; } public function handle(array $record): bool { if ($this->options['enabled'] && $this->connector->isActiveClient()) { return parent::handle($record); } return !$this->bubble; } /** * Writes the record down to the log of the implementing handler */ protected function write(array $record): void { if ($record['level'] < Logger::NOTICE) { $this->handleDebugRecord($record); } elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { $this->handleExceptionRecord($record); } else { $this->handleErrorRecord($record); } } /** * @phpstan-param Record $record */ private function handleDebugRecord(array $record): void { $tags = $this->getRecordTags($record); $message = $record['message']; if ($record['context']) { $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true); } $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']); } /** * @phpstan-param Record $record */ private function handleExceptionRecord(array $record): void { $this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']); } /** * @phpstan-param Record $record */ private function handleErrorRecord(array $record): void { $context = $record['context']; $this->connector->getErrorsDispatcher()->dispatchError( $context['code'] ?? null, $context['message'] ?? $record['message'], $context['file'] ?? null, $context['line'] ?? null, $this->options['classesPartialsTraceIgnore'] ); } /** * @phpstan-param Record $record * @return string */ private function getRecordTags(array &$record) { $tags = null; if (!empty($record['context'])) { $context = & $record['context']; foreach ($this->options['debugTagsKeysInContext'] as $key) { if (!empty($context[$key])) { $tags = $context[$key]; if ($key === 0) { array_shift($context); } else { unset($context[$key]); } break; } } } return $tags ?: strtolower($record['level_name']); } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter('%message%'); } } PK!y:0Jvendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Processor\ProcessorInterface; /** * Interface to describe loggers that have processors * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger */ interface ProcessableHandlerInterface { /** * Adds a processor in the stack. * * @psalm-param ProcessorInterface|callable(Record): Record $callback * * @param ProcessorInterface|callable $callback * @return HandlerInterface self */ public function pushProcessor(callable $callback): HandlerInterface; /** * Removes the processor on top of the stack and returns it. * * @psalm-return ProcessorInterface|callable(Record): Record $callback * * @throws \LogicException In case the processor stack is empty * @return callable|ProcessorInterface */ public function popProcessor(): callable; } PK!ZFvendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\ResettableInterface; use Monolog\Processor\ProcessorInterface; /** * Helper trait for implementing ProcessableInterface * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger */ trait ProcessableHandlerTrait { /** * @var callable[] * @phpstan-var array */ protected $processors = []; /** * {@inheritDoc} */ public function pushProcessor(callable $callback): HandlerInterface { array_unshift($this->processors, $callback); return $this; } /** * {@inheritDoc} */ public function popProcessor(): callable { if (!$this->processors) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } /** * Processes a record. * * @phpstan-param Record $record * @phpstan-return Record */ protected function processRecord(array $record): array { foreach ($this->processors as $processor) { $record = $processor($record); } return $record; } protected function resetProcessors(): void { foreach ($this->processors as $processor) { if ($processor instanceof ResettableInterface) { $processor->reset(); } } } } PK!Vb__=vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; /** * Stores to STDIN of any process, specified by a command. * * Usage example: *
 * $log = new Logger('myLogger');
 * $log->pushHandler(new ProcessHandler('/usr/bin/php /var/www/monolog/someScript.php'));
 * 
* * @author Kolja Zuelsdorf */ class ProcessHandler extends AbstractProcessingHandler { /** * Holds the process to receive data on its STDIN. * * @var resource|bool|null */ private $process; /** * @var string */ private $command; /** * @var string|null */ private $cwd; /** * @var resource[] */ private $pipes = []; /** * @var array */ protected const DESCRIPTOR_SPEC = [ 0 => ['pipe', 'r'], // STDIN is a pipe that the child will read from 1 => ['pipe', 'w'], // STDOUT is a pipe that the child will write to 2 => ['pipe', 'w'], // STDERR is a pipe to catch the any errors ]; /** * @param string $command Command for the process to start. Absolute paths are recommended, * especially if you do not use the $cwd parameter. * @param string|null $cwd "Current working directory" (CWD) for the process to be executed in. * @throws \InvalidArgumentException */ public function __construct(string $command, $level = Logger::DEBUG, bool $bubble = true, ?string $cwd = null) { if ($command === '') { throw new \InvalidArgumentException('The command argument must be a non-empty string.'); } if ($cwd === '') { throw new \InvalidArgumentException('The optional CWD argument must be a non-empty string or null.'); } parent::__construct($level, $bubble); $this->command = $command; $this->cwd = $cwd; } /** * Writes the record down to the log of the implementing handler * * @throws \UnexpectedValueException */ protected function write(array $record): void { $this->ensureProcessIsStarted(); $this->writeProcessInput($record['formatted']); $errors = $this->readProcessErrors(); if (empty($errors) === false) { throw new \UnexpectedValueException(sprintf('Errors while writing to process: %s', $errors)); } } /** * Makes sure that the process is actually started, and if not, starts it, * assigns the stream pipes, and handles startup errors, if any. */ private function ensureProcessIsStarted(): void { if (is_resource($this->process) === false) { $this->startProcess(); $this->handleStartupErrors(); } } /** * Starts the actual process and sets all streams to non-blocking. */ private function startProcess(): void { $this->process = proc_open($this->command, static::DESCRIPTOR_SPEC, $this->pipes, $this->cwd); foreach ($this->pipes as $pipe) { stream_set_blocking($pipe, false); } } /** * Selects the STDERR stream, handles upcoming startup errors, and throws an exception, if any. * * @throws \UnexpectedValueException */ private function handleStartupErrors(): void { $selected = $this->selectErrorStream(); if (false === $selected) { throw new \UnexpectedValueException('Something went wrong while selecting a stream.'); } $errors = $this->readProcessErrors(); if (is_resource($this->process) === false || empty($errors) === false) { throw new \UnexpectedValueException( sprintf('The process "%s" could not be opened: ' . $errors, $this->command) ); } } /** * Selects the STDERR stream. * * @return int|bool */ protected function selectErrorStream() { $empty = []; $errorPipes = [$this->pipes[2]]; return stream_select($errorPipes, $empty, $empty, 1); } /** * Reads the errors of the process, if there are any. * * @codeCoverageIgnore * @return string Empty string if there are no errors. */ protected function readProcessErrors(): string { return (string) stream_get_contents($this->pipes[2]); } /** * Writes to the input stream of the opened process. * * @codeCoverageIgnore */ protected function writeProcessInput(string $string): void { fwrite($this->pipes[0], $string); } /** * {@inheritDoc} */ public function close(): void { if (is_resource($this->process)) { foreach ($this->pipes as $pipe) { fclose($pipe); } proc_close($this->process); $this->process = null; } } } PK!r!l 9vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Psr\Log\LoggerInterface; use Monolog\Formatter\FormatterInterface; /** * Proxies log messages to an existing PSR-3 compliant logger. * * If a formatter is configured, the formatter's output MUST be a string and the * formatted message will be fed to the wrapped PSR logger instead of the original * log record's message. * * @author Michael Moussa */ class PsrHandler extends AbstractHandler implements FormattableHandlerInterface { /** * PSR-3 compliant logger * * @var LoggerInterface */ protected $logger; /** * @var FormatterInterface|null */ protected $formatter; /** * @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied */ public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, bool $bubble = true) { parent::__construct($level, $bubble); $this->logger = $logger; } /** * {@inheritDoc} */ public function handle(array $record): bool { if (!$this->isHandling($record)) { return false; } if ($this->formatter) { $formatted = $this->formatter->format($record); $this->logger->log(strtolower($record['level_name']), (string) $formatted, $record['context']); } else { $this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']); } return false === $this->bubble; } /** * Sets the formatter. * * @param FormatterInterface $formatter */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $this->formatter = $formatter; return $this; } /** * Gets the formatter. * * @return FormatterInterface */ public function getFormatter(): FormatterInterface { if (!$this->formatter) { throw new \LogicException('No formatter has been set and this handler does not have a default formatter'); } return $this->formatter; } } PK!{”>vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; use Psr\Log\LogLevel; /** * Sends notifications through the pushover api to mobile phones * * @author Sebastian Göttschkes * @see https://www.pushover.net/api * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class PushoverHandler extends SocketHandler { /** @var string */ private $token; /** @var array */ private $users; /** @var string */ private $title; /** @var string|int|null */ private $user = null; /** @var int */ private $retry; /** @var int */ private $expire; /** @var int */ private $highPriorityLevel; /** @var int */ private $emergencyLevel; /** @var bool */ private $useFormattedMessage = false; /** * All parameters that can be sent to Pushover * @see https://pushover.net/api * @var array */ private $parameterNames = [ 'token' => true, 'user' => true, 'message' => true, 'device' => true, 'title' => true, 'url' => true, 'url_title' => true, 'priority' => true, 'timestamp' => true, 'sound' => true, 'retry' => true, 'expire' => true, 'callback' => true, ]; /** * Sounds the api supports by default * @see https://pushover.net/api#sounds * @var string[] */ private $sounds = [ 'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming', 'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb', 'persistent', 'echo', 'updown', 'none', ]; /** * @param string $token Pushover api token * @param string|array $users Pushover user id or array of ids the message will be sent to * @param string|null $title Title sent to the Pushover API * @param bool $useSSL Whether to connect via SSL. Required when pushing messages to users that are not * the pushover.net app owner. OpenSSL is required for this option. * @param string|int $highPriorityLevel The minimum logging level at which this handler will start * sending "high priority" requests to the Pushover API * @param string|int $emergencyLevel The minimum logging level at which this handler will start * sending "emergency" requests to the Pushover API * @param int $retry The retry parameter specifies how often (in seconds) the Pushover servers will * send the same notification to the user. * @param int $expire The expire parameter specifies how many seconds your notification will continue * to be retried for (every retry seconds). * * @phpstan-param string|array $users * @phpstan-param Level|LevelName|LogLevel::* $highPriorityLevel * @phpstan-param Level|LevelName|LogLevel::* $emergencyLevel */ public function __construct( string $token, $users, ?string $title = null, $level = Logger::CRITICAL, bool $bubble = true, bool $useSSL = true, $highPriorityLevel = Logger::CRITICAL, $emergencyLevel = Logger::EMERGENCY, int $retry = 30, int $expire = 25200, bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { $connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80'; parent::__construct( $connectionString, $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); $this->token = $token; $this->users = (array) $users; $this->title = $title ?: (string) gethostname(); $this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel); $this->emergencyLevel = Logger::toMonologLevel($emergencyLevel); $this->retry = $retry; $this->expire = $expire; } protected function generateDataStream(array $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } /** * @phpstan-param FormattedRecord $record */ private function buildContent(array $record): string { // Pushover has a limit of 512 characters on title and message combined. $maxMessageLength = 512 - strlen($this->title); $message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message']; $message = Utils::substr($message, 0, $maxMessageLength); $timestamp = $record['datetime']->getTimestamp(); $dataArray = [ 'token' => $this->token, 'user' => $this->user, 'message' => $message, 'title' => $this->title, 'timestamp' => $timestamp, ]; if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) { $dataArray['priority'] = 2; $dataArray['retry'] = $this->retry; $dataArray['expire'] = $this->expire; } elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) { $dataArray['priority'] = 1; } // First determine the available parameters $context = array_intersect_key($record['context'], $this->parameterNames); $extra = array_intersect_key($record['extra'], $this->parameterNames); // Least important info should be merged with subsequent info $dataArray = array_merge($extra, $context, $dataArray); // Only pass sounds that are supported by the API if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) { unset($dataArray['sound']); } return http_build_query($dataArray); } private function buildHeader(string $content): string { $header = "POST /1/messages.json HTTP/1.1\r\n"; $header .= "Host: api.pushover.net\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($content) . "\r\n"; $header .= "\r\n"; return $header; } protected function write(array $record): void { foreach ($this->users as $user) { $this->user = $user; parent::write($record); $this->closeSocket(); } $this->user = null; } /** * @param int|string $value * * @phpstan-param Level|LevelName|LogLevel::* $value */ public function setHighPriorityLevel($value): self { $this->highPriorityLevel = Logger::toMonologLevel($value); return $this; } /** * @param int|string $value * * @phpstan-param Level|LevelName|LogLevel::* $value */ public function setEmergencyLevel($value): self { $this->emergencyLevel = Logger::toMonologLevel($value); return $this; } /** * Use the formatted message? */ public function useFormattedMessage(bool $value): self { $this->useFormattedMessage = $value; return $this; } } PK!G ;vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\LineFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; /** * Logs to a Redis key using rpush * * usage example: * * $log = new Logger('application'); * $redis = new RedisHandler(new Predis\Client("tcp://localhost:6379"), "logs", "prod"); * $log->pushHandler($redis); * * @author Thomas Tourlourat * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class RedisHandler extends AbstractProcessingHandler { /** @var \Predis\Client<\Predis\Client>|\Redis */ private $redisClient; /** @var string */ private $redisKey; /** @var int */ protected $capSize; /** * @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance * @param string $key The key name to push records to * @param int $capSize Number of entries to limit list size to, 0 = unlimited */ public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true, int $capSize = 0) { if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { throw new \InvalidArgumentException('Predis\Client or Redis instance required'); } $this->redisClient = $redis; $this->redisKey = $key; $this->capSize = $capSize; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { if ($this->capSize) { $this->writeCapped($record); } else { $this->redisClient->rpush($this->redisKey, $record["formatted"]); } } /** * Write and cap the collection * Writes the record to the redis list and caps its * * @phpstan-param FormattedRecord $record */ protected function writeCapped(array $record): void { if ($this->redisClient instanceof \Redis) { $mode = defined('\Redis::MULTI') ? \Redis::MULTI : 1; $this->redisClient->multi($mode) ->rpush($this->redisKey, $record["formatted"]) ->ltrim($this->redisKey, -$this->capSize, -1) ->exec(); } else { $redisKey = $this->redisKey; $capSize = $this->capSize; $this->redisClient->transaction(function ($tx) use ($record, $redisKey, $capSize) { $tx->rpush($redisKey, $record["formatted"]); $tx->ltrim($redisKey, -$capSize, -1); }); } } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter(); } } PK!ZAvendor/monolog/monolog/src/Monolog/Handler/RedisPubSubHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\LineFormatter; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; /** * Sends the message to a Redis Pub/Sub channel using PUBLISH * * usage example: * * $log = new Logger('application'); * $redis = new RedisPubSubHandler(new Predis\Client("tcp://localhost:6379"), "logs", Logger::WARNING); * $log->pushHandler($redis); * * @author Gaëtan Faugère */ class RedisPubSubHandler extends AbstractProcessingHandler { /** @var \Predis\Client<\Predis\Client>|\Redis */ private $redisClient; /** @var string */ private $channelKey; /** * @param \Predis\Client<\Predis\Client>|\Redis $redis The redis instance * @param string $key The channel key to publish records to */ public function __construct($redis, string $key, $level = Logger::DEBUG, bool $bubble = true) { if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) { throw new \InvalidArgumentException('Predis\Client or Redis instance required'); } $this->redisClient = $redis; $this->channelKey = $key; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { $this->redisClient->publish($this->channelKey, $record["formatted"]); } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter(); } } PK!Y,T T =vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Rollbar\RollbarLogger; use Throwable; use Monolog\Logger; /** * Sends errors to Rollbar * * If the context data contains a `payload` key, that is used as an array * of payload options to RollbarLogger's log method. * * Rollbar's context info will contain the context + extra keys from the log record * merged, and then on top of that a few keys: * * - level (rollbar level name) * - monolog_level (monolog level name, raw level, as rollbar only has 5 but monolog 8) * - channel * - datetime (unix timestamp) * * @author Paul Statezny */ class RollbarHandler extends AbstractProcessingHandler { /** * @var RollbarLogger */ protected $rollbarLogger; /** @var string[] */ protected $levelMap = [ Logger::DEBUG => 'debug', Logger::INFO => 'info', Logger::NOTICE => 'info', Logger::WARNING => 'warning', Logger::ERROR => 'error', Logger::CRITICAL => 'critical', Logger::ALERT => 'critical', Logger::EMERGENCY => 'critical', ]; /** * Records whether any log records have been added since the last flush of the rollbar notifier * * @var bool */ private $hasRecords = false; /** @var bool */ protected $initialized = false; /** * @param RollbarLogger $rollbarLogger RollbarLogger object constructed with valid token */ public function __construct(RollbarLogger $rollbarLogger, $level = Logger::ERROR, bool $bubble = true) { $this->rollbarLogger = $rollbarLogger; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { if (!$this->initialized) { // __destructor() doesn't get called on Fatal errors register_shutdown_function(array($this, 'close')); $this->initialized = true; } $context = $record['context']; $context = array_merge($context, $record['extra'], [ 'level' => $this->levelMap[$record['level']], 'monolog_level' => $record['level_name'], 'channel' => $record['channel'], 'datetime' => $record['datetime']->format('U'), ]); if (isset($context['exception']) && $context['exception'] instanceof Throwable) { $exception = $context['exception']; unset($context['exception']); $toLog = $exception; } else { $toLog = $record['message']; } // @phpstan-ignore-next-line $this->rollbarLogger->log($context['level'], $toLog, $context); $this->hasRecords = true; } public function flush(): void { if ($this->hasRecords) { $this->rollbarLogger->flush(); $this->hasRecords = false; } } /** * {@inheritDoc} */ public function close(): void { $this->flush(); } /** * {@inheritDoc} */ public function reset() { $this->flush(); parent::reset(); } } PK!aBvendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use InvalidArgumentException; use Monolog\Logger; use Monolog\Utils; /** * Stores logs to files that are rotated every day and a limited number of files are kept. * * This rotation is only intended to be used as a workaround. Using logrotate to * handle the rotation is strongly encouraged when you can use it. * * @author Christophe Coevoet * @author Jordi Boggiano */ class RotatingFileHandler extends StreamHandler { public const FILE_PER_DAY = 'Y-m-d'; public const FILE_PER_MONTH = 'Y-m'; public const FILE_PER_YEAR = 'Y'; /** @var string */ protected $filename; /** @var int */ protected $maxFiles; /** @var bool */ protected $mustRotate; /** @var \DateTimeImmutable */ protected $nextRotation; /** @var string */ protected $filenameFormat; /** @var string */ protected $dateFormat; /** * @param string $filename * @param int $maxFiles The maximal amount of files to keep (0 means unlimited) * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) * @param bool $useLocking Try to lock log file before doing any writes */ public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false) { $this->filename = Utils::canonicalizePath($filename); $this->maxFiles = $maxFiles; $this->nextRotation = new \DateTimeImmutable('tomorrow'); $this->filenameFormat = '{filename}-{date}'; $this->dateFormat = static::FILE_PER_DAY; parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking); } /** * {@inheritDoc} */ public function close(): void { parent::close(); if (true === $this->mustRotate) { $this->rotate(); } } /** * {@inheritDoc} */ public function reset() { parent::reset(); if (true === $this->mustRotate) { $this->rotate(); } } public function setFilenameFormat(string $filenameFormat, string $dateFormat): self { if (!preg_match('{^[Yy](([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) { throw new InvalidArgumentException( 'Invalid date format - format must be one of '. 'RotatingFileHandler::FILE_PER_DAY ("Y-m-d"), RotatingFileHandler::FILE_PER_MONTH ("Y-m") '. 'or RotatingFileHandler::FILE_PER_YEAR ("Y"), or you can set one of the '. 'date formats using slashes, underscores and/or dots instead of dashes.' ); } if (substr_count($filenameFormat, '{date}') === 0) { throw new InvalidArgumentException( 'Invalid filename format - format must contain at least `{date}`, because otherwise rotating is impossible.' ); } $this->filenameFormat = $filenameFormat; $this->dateFormat = $dateFormat; $this->url = $this->getTimedFilename(); $this->close(); return $this; } /** * {@inheritDoc} */ protected function write(array $record): void { // on the first record written, if the log is new, we rotate (once per day) after the log has been written so that the new file exists if (null === $this->mustRotate) { $this->mustRotate = null === $this->url || !file_exists($this->url); } // if the next rotation is expired, then we rotate immediately if ($this->nextRotation <= $record['datetime']) { $this->mustRotate = true; $this->close(); // triggers rotation } parent::write($record); if ($this->mustRotate) { $this->close(); // triggers rotation } } /** * Rotates the files. */ protected function rotate(): void { // update filename $this->url = $this->getTimedFilename(); $this->nextRotation = new \DateTimeImmutable('tomorrow'); $this->mustRotate = false; // skip GC of old logs if files are unlimited if (0 === $this->maxFiles) { return; } $logFiles = glob($this->getGlobPattern()); if (false === $logFiles) { // failed to glob return; } if ($this->maxFiles >= count($logFiles)) { // no files to remove return; } // Sorting the files by name to remove the older ones usort($logFiles, function ($a, $b) { return strcmp($b, $a); }); foreach (array_slice($logFiles, $this->maxFiles) as $file) { if (is_writable($file)) { // suppress errors here as unlink() might fail if two processes // are cleaning up/rotating at the same time set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool { return false; }); unlink($file); restore_error_handler(); } } } protected function getTimedFilename(): string { $fileInfo = pathinfo($this->filename); $timedFilename = str_replace( ['{filename}', '{date}'], [$fileInfo['filename'], date($this->dateFormat)], $fileInfo['dirname'] . '/' . $this->filenameFormat ); if (isset($fileInfo['extension'])) { $timedFilename .= '.'.$fileInfo['extension']; } return $timedFilename; } protected function getGlobPattern(): string { $fileInfo = pathinfo($this->filename); $glob = str_replace( ['{filename}', '{date}'], [$fileInfo['filename'], str_replace( ['Y', 'y', 'm', 'd'], ['[0-9][0-9][0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]'], $this->dateFormat) ], $fileInfo['dirname'] . '/' . $this->filenameFormat ); if (isset($fileInfo['extension'])) { $glob .= '.'.$fileInfo['extension']; } return $glob; } } PK!1>vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; /** * Sampling handler * * A sampled event stream can be useful for logging high frequency events in * a production environment where you only need an idea of what is happening * and are not concerned with capturing every occurrence. Since the decision to * handle or not handle a particular event is determined randomly, the * resulting sampled log is not guaranteed to contain 1/N of the events that * occurred in the application, but based on the Law of large numbers, it will * tend to be close to this ratio with a large number of attempts. * * @author Bryan Davis * @author Kunal Mehta * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger */ class SamplingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface { use ProcessableHandlerTrait; /** * @var HandlerInterface|callable * @phpstan-var HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface */ protected $handler; /** * @var int $factor */ protected $factor; /** * @psalm-param HandlerInterface|callable(Record|array{level: Level}|null, HandlerInterface): HandlerInterface $handler * * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $samplingHandler). * @param int $factor Sample factor (e.g. 10 means every ~10th record is sampled) */ public function __construct($handler, int $factor) { parent::__construct(); $this->handler = $handler; $this->factor = $factor; if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) { throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object"); } } public function isHandling(array $record): bool { return $this->getHandler($record)->isHandling($record); } public function handle(array $record): bool { if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) { if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } $this->getHandler($record)->handle($record); } return false === $this->bubble; } /** * Return the nested handler * * If the handler was provided as a factory callable, this will trigger the handler's instantiation. * * @phpstan-param Record|array{level: Level}|null $record * * @return HandlerInterface */ public function getHandler(?array $record = null) { if (!$this->handler instanceof HandlerInterface) { $this->handler = ($this->handler)($record, $this); if (!$this->handler instanceof HandlerInterface) { throw new \RuntimeException("The factory callable should return a HandlerInterface"); } } return $this->handler; } /** * {@inheritDoc} */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { $handler = $this->getHandler(); if ($handler instanceof FormattableHandlerInterface) { $handler->setFormatter($formatter); return $this; } throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } /** * {@inheritDoc} */ public function getFormatter(): FormatterInterface { $handler = $this->getHandler(); if ($handler instanceof FormattableHandlerInterface) { return $handler->getFormatter(); } throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } } PK!$sM7 7 >vendor/monolog/monolog/src/Monolog/Handler/SendGridHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; /** * SendGridrHandler uses the SendGrid API v2 function to send Log emails, more information in https://sendgrid.com/docs/API_Reference/Web_API/mail.html * * @author Ricardo Fontanelli */ class SendGridHandler extends MailHandler { /** * The SendGrid API User * @var string */ protected $apiUser; /** * The SendGrid API Key * @var string */ protected $apiKey; /** * The email addresses to which the message will be sent * @var string */ protected $from; /** * The email addresses to which the message will be sent * @var string[] */ protected $to; /** * The subject of the email * @var string */ protected $subject; /** * @param string $apiUser The SendGrid API User * @param string $apiKey The SendGrid API Key * @param string $from The sender of the email * @param string|string[] $to The recipients of the email * @param string $subject The subject of the mail */ public function __construct(string $apiUser, string $apiKey, string $from, $to, string $subject, $level = Logger::ERROR, bool $bubble = true) { if (!extension_loaded('curl')) { throw new MissingExtensionException('The curl extension is needed to use the SendGridHandler'); } parent::__construct($level, $bubble); $this->apiUser = $apiUser; $this->apiKey = $apiKey; $this->from = $from; $this->to = (array) $to; $this->subject = $subject; } /** * {@inheritDoc} */ protected function send(string $content, array $records): void { $message = []; $message['api_user'] = $this->apiUser; $message['api_key'] = $this->apiKey; $message['from'] = $this->from; foreach ($this->to as $recipient) { $message['to[]'] = $recipient; } $message['subject'] = $this->subject; $message['date'] = date('r'); if ($this->isHtmlBody($content)) { $message['html'] = $content; } else { $message['text'] = $content; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.sendgrid.com/api/mail.send.json'); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($message)); Curl\Util::execute($ch, 2); } } PK!ll;vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; use Monolog\Utils; use Monolog\Handler\Slack\SlackRecord; /** * Sends notifications through Slack API * * @author Greg Kedzierski * @see https://api.slack.com/ * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class SlackHandler extends SocketHandler { /** * Slack API token * @var string */ private $token; /** * Instance of the SlackRecord util class preparing data for Slack API. * @var SlackRecord */ private $slackRecord; /** * @param string $token Slack API token * @param string $channel Slack channel (encoded ID or name) * @param string|null $username Name of a bot * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) * @param string|null $iconEmoji The emoji name to use (or null) * @param bool $useShortAttachment Whether the context/extra messages added to Slack as attachments are in a short style * @param bool $includeContextAndExtra Whether the attachment should include context and extra data * @param string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] * @throws MissingExtensionException If no OpenSSL PHP extension configured */ public function __construct( string $token, string $channel, ?string $username = null, bool $useAttachment = true, ?string $iconEmoji = null, $level = Logger::CRITICAL, bool $bubble = true, bool $useShortAttachment = false, bool $includeContextAndExtra = false, array $excludeFields = array(), bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { if (!extension_loaded('openssl')) { throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler'); } parent::__construct( 'ssl://slack.com:443', $level, $bubble, $persistent, $timeout, $writingTimeout, $connectionTimeout, $chunkSize ); $this->slackRecord = new SlackRecord( $channel, $username, $useAttachment, $iconEmoji, $useShortAttachment, $includeContextAndExtra, $excludeFields ); $this->token = $token; } public function getSlackRecord(): SlackRecord { return $this->slackRecord; } public function getToken(): string { return $this->token; } /** * {@inheritDoc} */ protected function generateDataStream(array $record): string { $content = $this->buildContent($record); return $this->buildHeader($content) . $content; } /** * Builds the body of API call * * @phpstan-param FormattedRecord $record */ private function buildContent(array $record): string { $dataArray = $this->prepareContentData($record); return http_build_query($dataArray); } /** * @phpstan-param FormattedRecord $record * @return string[] */ protected function prepareContentData(array $record): array { $dataArray = $this->slackRecord->getSlackData($record); $dataArray['token'] = $this->token; if (!empty($dataArray['attachments'])) { $dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']); } return $dataArray; } /** * Builds the header of the API Call */ private function buildHeader(string $content): string { $header = "POST /api/chat.postMessage HTTP/1.1\r\n"; $header .= "Host: slack.com\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($content) . "\r\n"; $header .= "\r\n"; return $header; } /** * {@inheritDoc} */ protected function write(array $record): void { parent::write($record); $this->finalizeWrite(); } /** * Finalizes the request by reading some bytes and then closing the socket * * If we do not read some but close the socket too early, slack sometimes * drops the request entirely. */ protected function finalizeWrite(): void { $res = $this->getResource(); if (is_resource($res)) { @fread($res, 2048); } $this->closeSocket(); } public function setFormatter(FormatterInterface $formatter): HandlerInterface { parent::setFormatter($formatter); $this->slackRecord->setFormatter($formatter); return $this; } public function getFormatter(): FormatterInterface { $formatter = parent::getFormatter(); $this->slackRecord->setFormatter($formatter); return $formatter; } /** * Channel used by the bot when posting */ public function setChannel(string $channel): self { $this->slackRecord->setChannel($channel); return $this; } /** * Username used by the bot when posting */ public function setUsername(string $username): self { $this->slackRecord->setUsername($username); return $this; } public function useAttachment(bool $useAttachment): self { $this->slackRecord->useAttachment($useAttachment); return $this; } public function setIconEmoji(string $iconEmoji): self { $this->slackRecord->setUserIcon($iconEmoji); return $this; } public function useShortAttachment(bool $useShortAttachment): self { $this->slackRecord->useShortAttachment($useShortAttachment); return $this; } public function includeContextAndExtra(bool $includeContextAndExtra): self { $this->slackRecord->includeContextAndExtra($includeContextAndExtra); return $this; } /** * @param string[] $excludeFields */ public function excludeFields(array $excludeFields): self { $this->slackRecord->excludeFields($excludeFields); return $this; } } PK!` * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Logger; use Monolog\Utils; use Monolog\Handler\Slack\SlackRecord; /** * Sends notifications through Slack Webhooks * * @author Haralan Dobrev * @see https://api.slack.com/incoming-webhooks */ class SlackWebhookHandler extends AbstractProcessingHandler { /** * Slack Webhook token * @var string */ private $webhookUrl; /** * Instance of the SlackRecord util class preparing data for Slack API. * @var SlackRecord */ private $slackRecord; /** * @param string $webhookUrl Slack Webhook URL * @param string|null $channel Slack channel (encoded ID or name) * @param string|null $username Name of a bot * @param bool $useAttachment Whether the message should be added to Slack as attachment (plain text otherwise) * @param string|null $iconEmoji The emoji name to use (or null) * @param bool $useShortAttachment Whether the the context/extra messages added to Slack as attachments are in a short style * @param bool $includeContextAndExtra Whether the attachment should include context and extra data * @param string[] $excludeFields Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2'] */ public function __construct( string $webhookUrl, ?string $channel = null, ?string $username = null, bool $useAttachment = true, ?string $iconEmoji = null, bool $useShortAttachment = false, bool $includeContextAndExtra = false, $level = Logger::CRITICAL, bool $bubble = true, array $excludeFields = array() ) { if (!extension_loaded('curl')) { throw new MissingExtensionException('The curl extension is needed to use the SlackWebhookHandler'); } parent::__construct($level, $bubble); $this->webhookUrl = $webhookUrl; $this->slackRecord = new SlackRecord( $channel, $username, $useAttachment, $iconEmoji, $useShortAttachment, $includeContextAndExtra, $excludeFields ); } public function getSlackRecord(): SlackRecord { return $this->slackRecord; } public function getWebhookUrl(): string { return $this->webhookUrl; } /** * {@inheritDoc} */ protected function write(array $record): void { $postData = $this->slackRecord->getSlackData($record); $postString = Utils::jsonEncode($postData); $ch = curl_init(); $options = array( CURLOPT_URL => $this->webhookUrl, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array('Content-type: application/json'), CURLOPT_POSTFIELDS => $postString, ); if (defined('CURLOPT_SAFE_UPLOAD')) { $options[CURLOPT_SAFE_UPLOAD] = true; } curl_setopt_array($ch, $options); Curl\Util::execute($ch); } public function setFormatter(FormatterInterface $formatter): HandlerInterface { parent::setFormatter($formatter); $this->slackRecord->setFormatter($formatter); return $this; } public function getFormatter(): FormatterInterface { $formatter = parent::getFormatter(); $this->slackRecord->setFormatter($formatter); return $formatter; } } PK!?p#//<vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; /** * Stores to any socket - uses fsockopen() or pfsockopen(). * * @author Pablo de Leon Belloc * @see http://php.net/manual/en/function.fsockopen.php * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class SocketHandler extends AbstractProcessingHandler { /** @var string */ private $connectionString; /** @var float */ private $connectionTimeout; /** @var resource|null */ private $resource; /** @var float */ private $timeout; /** @var float */ private $writingTimeout; /** @var ?int */ private $lastSentBytes = null; /** @var ?int */ private $chunkSize; /** @var bool */ private $persistent; /** @var ?int */ private $errno = null; /** @var ?string */ private $errstr = null; /** @var ?float */ private $lastWritingAt = null; /** * @param string $connectionString Socket connection string * @param bool $persistent Flag to enable/disable persistent connections * @param float $timeout Socket timeout to wait until the request is being aborted * @param float $writingTimeout Socket timeout to wait until the request should've been sent/written * @param float|null $connectionTimeout Socket connect timeout to wait until the connection should've been * established * @param int|null $chunkSize Sets the chunk size. Only has effect during connection in the writing cycle * * @throws \InvalidArgumentException If an invalid timeout value (less than 0) is passed. */ public function __construct( string $connectionString, $level = Logger::DEBUG, bool $bubble = true, bool $persistent = false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null ) { parent::__construct($level, $bubble); $this->connectionString = $connectionString; if ($connectionTimeout !== null) { $this->validateTimeout($connectionTimeout); } $this->connectionTimeout = $connectionTimeout ?? (float) ini_get('default_socket_timeout'); $this->persistent = $persistent; $this->validateTimeout($timeout); $this->timeout = $timeout; $this->validateTimeout($writingTimeout); $this->writingTimeout = $writingTimeout; $this->chunkSize = $chunkSize; } /** * Connect (if necessary) and write to the socket * * {@inheritDoc} * * @throws \UnexpectedValueException * @throws \RuntimeException */ protected function write(array $record): void { $this->connectIfNotConnected(); $data = $this->generateDataStream($record); $this->writeToSocket($data); } /** * We will not close a PersistentSocket instance so it can be reused in other requests. */ public function close(): void { if (!$this->isPersistent()) { $this->closeSocket(); } } /** * Close socket, if open */ public function closeSocket(): void { if (is_resource($this->resource)) { fclose($this->resource); $this->resource = null; } } /** * Set socket connection to be persistent. It only has effect before the connection is initiated. */ public function setPersistent(bool $persistent): self { $this->persistent = $persistent; return $this; } /** * Set connection timeout. Only has effect before we connect. * * @see http://php.net/manual/en/function.fsockopen.php */ public function setConnectionTimeout(float $seconds): self { $this->validateTimeout($seconds); $this->connectionTimeout = $seconds; return $this; } /** * Set write timeout. Only has effect before we connect. * * @see http://php.net/manual/en/function.stream-set-timeout.php */ public function setTimeout(float $seconds): self { $this->validateTimeout($seconds); $this->timeout = $seconds; return $this; } /** * Set writing timeout. Only has effect during connection in the writing cycle. * * @param float $seconds 0 for no timeout */ public function setWritingTimeout(float $seconds): self { $this->validateTimeout($seconds); $this->writingTimeout = $seconds; return $this; } /** * Set chunk size. Only has effect during connection in the writing cycle. */ public function setChunkSize(int $bytes): self { $this->chunkSize = $bytes; return $this; } /** * Get current connection string */ public function getConnectionString(): string { return $this->connectionString; } /** * Get persistent setting */ public function isPersistent(): bool { return $this->persistent; } /** * Get current connection timeout setting */ public function getConnectionTimeout(): float { return $this->connectionTimeout; } /** * Get current in-transfer timeout */ public function getTimeout(): float { return $this->timeout; } /** * Get current local writing timeout * * @return float */ public function getWritingTimeout(): float { return $this->writingTimeout; } /** * Get current chunk size */ public function getChunkSize(): ?int { return $this->chunkSize; } /** * Check to see if the socket is currently available. * * UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details. */ public function isConnected(): bool { return is_resource($this->resource) && !feof($this->resource); // on TCP - other party can close connection. } /** * Wrapper to allow mocking * * @return resource|false */ protected function pfsockopen() { return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); } /** * Wrapper to allow mocking * * @return resource|false */ protected function fsockopen() { return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); } /** * Wrapper to allow mocking * * @see http://php.net/manual/en/function.stream-set-timeout.php * * @return bool */ protected function streamSetTimeout() { $seconds = floor($this->timeout); $microseconds = round(($this->timeout - $seconds) * 1e6); if (!is_resource($this->resource)) { throw new \LogicException('streamSetTimeout called but $this->resource is not a resource'); } return stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds); } /** * Wrapper to allow mocking * * @see http://php.net/manual/en/function.stream-set-chunk-size.php * * @return int|bool */ protected function streamSetChunkSize() { if (!is_resource($this->resource)) { throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource'); } if (null === $this->chunkSize) { throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set'); } return stream_set_chunk_size($this->resource, $this->chunkSize); } /** * Wrapper to allow mocking * * @return int|bool */ protected function fwrite(string $data) { if (!is_resource($this->resource)) { throw new \LogicException('fwrite called but $this->resource is not a resource'); } return @fwrite($this->resource, $data); } /** * Wrapper to allow mocking * * @return mixed[]|bool */ protected function streamGetMetadata() { if (!is_resource($this->resource)) { throw new \LogicException('streamGetMetadata called but $this->resource is not a resource'); } return stream_get_meta_data($this->resource); } private function validateTimeout(float $value): void { if ($value < 0) { throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)"); } } private function connectIfNotConnected(): void { if ($this->isConnected()) { return; } $this->connect(); } /** * @phpstan-param FormattedRecord $record */ protected function generateDataStream(array $record): string { return (string) $record['formatted']; } /** * @return resource|null */ protected function getResource() { return $this->resource; } private function connect(): void { $this->createSocketResource(); $this->setSocketTimeout(); $this->setStreamChunkSize(); } private function createSocketResource(): void { if ($this->isPersistent()) { $resource = $this->pfsockopen(); } else { $resource = $this->fsockopen(); } if (is_bool($resource)) { throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)"); } $this->resource = $resource; } private function setSocketTimeout(): void { if (!$this->streamSetTimeout()) { throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()"); } } private function setStreamChunkSize(): void { if ($this->chunkSize && !$this->streamSetChunkSize()) { throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()"); } } private function writeToSocket(string $data): void { $length = strlen($data); $sent = 0; $this->lastSentBytes = $sent; while ($this->isConnected() && $sent < $length) { if (0 == $sent) { $chunk = $this->fwrite($data); } else { $chunk = $this->fwrite(substr($data, $sent)); } if ($chunk === false) { throw new \RuntimeException("Could not write to socket"); } $sent += $chunk; $socketInfo = $this->streamGetMetadata(); if (is_array($socketInfo) && $socketInfo['timed_out']) { throw new \RuntimeException("Write timed-out"); } if ($this->writingIsTimedOut($sent)) { throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)"); } } if (!$this->isConnected() && $sent < $length) { throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)"); } } private function writingIsTimedOut(int $sent): bool { // convert to ms if (0.0 == $this->writingTimeout) { return false; } if ($sent !== $this->lastSentBytes) { $this->lastWritingAt = microtime(true); $this->lastSentBytes = $sent; return false; } else { usleep(100); } if ((microtime(true) - $this->lastWritingAt) >= $this->writingTimeout) { $this->closeSocket(); return true; } return false; } } PK!!9vendor/monolog/monolog/src/Monolog/Handler/SqsHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Aws\Sqs\SqsClient; use Monolog\Logger; use Monolog\Utils; /** * Writes to any sqs queue. * * @author Martijn van Calker */ class SqsHandler extends AbstractProcessingHandler { /** 256 KB in bytes - maximum message size in SQS */ protected const MAX_MESSAGE_SIZE = 262144; /** 100 KB in bytes - head message size for new error log */ protected const HEAD_MESSAGE_SIZE = 102400; /** @var SqsClient */ private $client; /** @var string */ private $queueUrl; public function __construct(SqsClient $sqsClient, string $queueUrl, $level = Logger::DEBUG, bool $bubble = true) { parent::__construct($level, $bubble); $this->client = $sqsClient; $this->queueUrl = $queueUrl; } /** * {@inheritDoc} */ protected function write(array $record): void { if (!isset($record['formatted']) || 'string' !== gettype($record['formatted'])) { throw new \InvalidArgumentException('SqsHandler accepts only formatted records as a string' . Utils::getRecordMessageForException($record)); } $messageBody = $record['formatted']; if (strlen($messageBody) >= static::MAX_MESSAGE_SIZE) { $messageBody = Utils::substr($messageBody, 0, static::HEAD_MESSAGE_SIZE); } $this->client->sendMessage([ 'QueueUrl' => $this->queueUrl, 'MessageBody' => $messageBody, ]); } } PK! <vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; /** * Stores to any stream resource * * Can be used to store into php://stderr, remote and local files, etc. * * @author Jordi Boggiano * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class StreamHandler extends AbstractProcessingHandler { /** @const int */ protected const MAX_CHUNK_SIZE = 2147483647; /** @const int 10MB */ protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024; /** @var int */ protected $streamChunkSize; /** @var resource|null */ protected $stream; /** @var ?string */ protected $url = null; /** @var ?string */ private $errorMessage = null; /** @var ?int */ protected $filePermission; /** @var bool */ protected $useLocking; /** @var string */ protected $fileOpenMode; /** @var true|null */ private $dirCreated = null; /** @var bool */ private $retrying = false; /** * @param resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write) * @param bool $useLocking Try to lock log file before doing any writes * @param string $fileOpenMode The fopen() mode used when opening a file, if $stream is a file path * * @throws \InvalidArgumentException If stream is not a resource or string */ public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false, $fileOpenMode = 'a') { parent::__construct($level, $bubble); if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) { if ($phpMemoryLimit > 0) { // use max 10% of allowed memory for the chunk size, and at least 100KB $this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024)); } else { // memory is unlimited, set to the default 10MB $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE; } } else { // no memory limit information, set to the default 10MB $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE; } if (is_resource($stream)) { $this->stream = $stream; stream_set_chunk_size($this->stream, $this->streamChunkSize); } elseif (is_string($stream)) { $this->url = Utils::canonicalizePath($stream); } else { throw new \InvalidArgumentException('A stream must either be a resource or a string.'); } $this->fileOpenMode = $fileOpenMode; $this->filePermission = $filePermission; $this->useLocking = $useLocking; } /** * {@inheritDoc} */ public function close(): void { if ($this->url && is_resource($this->stream)) { fclose($this->stream); } $this->stream = null; $this->dirCreated = null; } /** * Return the currently active stream if it is open * * @return resource|null */ public function getStream() { return $this->stream; } /** * Return the stream URL if it was configured with a URL and not an active resource * * @return string|null */ public function getUrl(): ?string { return $this->url; } /** * @return int */ public function getStreamChunkSize(): int { return $this->streamChunkSize; } /** * {@inheritDoc} */ protected function write(array $record): void { if (!is_resource($this->stream)) { $url = $this->url; if (null === $url || '' === $url) { throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' . Utils::getRecordMessageForException($record)); } $this->createDir($url); $this->errorMessage = null; set_error_handler(function (...$args) { return $this->customErrorHandler(...$args); }); try { $stream = fopen($url, $this->fileOpenMode); if ($this->filePermission !== null) { @chmod($url, $this->filePermission); } } finally { restore_error_handler(); } if (!is_resource($stream)) { $this->stream = null; throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url) . Utils::getRecordMessageForException($record)); } stream_set_chunk_size($stream, $this->streamChunkSize); $this->stream = $stream; } $stream = $this->stream; if (!is_resource($stream)) { throw new \LogicException('No stream was opened yet' . Utils::getRecordMessageForException($record)); } if ($this->useLocking) { // ignoring errors here, there's not much we can do about them flock($stream, LOCK_EX); } $this->errorMessage = null; set_error_handler(function (...$args) { return $this->customErrorHandler(...$args); }); try { $this->streamWrite($stream, $record); } finally { restore_error_handler(); } if ($this->errorMessage !== null) { $error = $this->errorMessage; // close the resource if possible to reopen it, and retry the failed write if (!$this->retrying && $this->url !== null && $this->url !== 'php://memory') { $this->retrying = true; $this->close(); $this->write($record); return; } throw new \UnexpectedValueException('Writing to the log file failed: '.$error . Utils::getRecordMessageForException($record)); } $this->retrying = false; if ($this->useLocking) { flock($stream, LOCK_UN); } } /** * Write to stream * @param resource $stream * @param array $record * * @phpstan-param FormattedRecord $record */ protected function streamWrite($stream, array $record): void { fwrite($stream, (string) $record['formatted']); } private function customErrorHandler(int $code, string $msg): bool { $this->errorMessage = preg_replace('{^(fopen|mkdir|fwrite)\(.*?\): }', '', $msg); return true; } private function getDirFromStream(string $stream): ?string { $pos = strpos($stream, '://'); if ($pos === false) { return dirname($stream); } if ('file://' === substr($stream, 0, 7)) { return dirname(substr($stream, 7)); } return null; } private function createDir(string $url): void { // Do not try to create dir if it has already been tried. if ($this->dirCreated) { return; } $dir = $this->getDirFromStream($url); if (null !== $dir && !is_dir($dir)) { $this->errorMessage = null; set_error_handler(function (...$args) { return $this->customErrorHandler(...$args); }); $status = mkdir($dir, 0777, true); restore_error_handler(); if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) { throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir)); } } $this->dirCreated = true; } } PK!8 rUUAvendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Swift_Message; use Swift; /** * SwiftMailerHandler uses Swift_Mailer to send the emails * * @author Gyula Sallai * * @phpstan-import-type Record from \Monolog\Logger * @deprecated Since Monolog 2.6. Use SymfonyMailerHandler instead. */ class SwiftMailerHandler extends MailHandler { /** @var \Swift_Mailer */ protected $mailer; /** @var Swift_Message|callable(string, Record[]): Swift_Message */ private $messageTemplate; /** * @psalm-param Swift_Message|callable(string, Record[]): Swift_Message $message * * @param \Swift_Mailer $mailer The mailer to use * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced */ public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, bool $bubble = true) { parent::__construct($level, $bubble); @trigger_error('The SwiftMailerHandler is deprecated since Monolog 2.6. Use SymfonyMailerHandler instead.', E_USER_DEPRECATED); $this->mailer = $mailer; $this->messageTemplate = $message; } /** * {@inheritDoc} */ protected function send(string $content, array $records): void { $this->mailer->send($this->buildMessage($content, $records)); } /** * Gets the formatter for the Swift_Message subject. * * @param string|null $format The format of the subject */ protected function getSubjectFormatter(?string $format): FormatterInterface { return new LineFormatter($format); } /** * Creates instance of Swift_Message to be sent * * @param string $content formatted email body to be sent * @param array $records Log records that formed the content * @return Swift_Message * * @phpstan-param Record[] $records */ protected function buildMessage(string $content, array $records): Swift_Message { $message = null; if ($this->messageTemplate instanceof Swift_Message) { $message = clone $this->messageTemplate; $message->generateId(); } elseif (is_callable($this->messageTemplate)) { $message = ($this->messageTemplate)($content, $records); } if (!$message instanceof Swift_Message) { $record = reset($records); throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : '')); } if ($records) { $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); $message->setSubject($subjectFormatter->format($this->getHighestRecord($records))); } $mime = 'text/plain'; if ($this->isHtmlBody($content)) { $mime = 'text/html'; } $message->setBody($content, $mime); /** @phpstan-ignore-next-line */ if (version_compare(Swift::VERSION, '6.0.0', '>=')) { $message->setDate(new \DateTimeImmutable()); } else { /** @phpstan-ignore-next-line */ $message->setDate(time()); } return $message; } } PK!蕮 Cvendor/monolog/monolog/src/Monolog/Handler/SymfonyMailerHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mime\Email; /** * SymfonyMailerHandler uses Symfony's Mailer component to send the emails * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger */ class SymfonyMailerHandler extends MailHandler { /** @var MailerInterface|TransportInterface */ protected $mailer; /** @var Email|callable(string, Record[]): Email */ private $emailTemplate; /** * @psalm-param Email|callable(string, Record[]): Email $email * * @param MailerInterface|TransportInterface $mailer The mailer to use * @param callable|Email $email An email template, the subject/body will be replaced */ public function __construct($mailer, $email, $level = Logger::ERROR, bool $bubble = true) { parent::__construct($level, $bubble); $this->mailer = $mailer; $this->emailTemplate = $email; } /** * {@inheritDoc} */ protected function send(string $content, array $records): void { $this->mailer->send($this->buildMessage($content, $records)); } /** * Gets the formatter for the Swift_Message subject. * * @param string|null $format The format of the subject */ protected function getSubjectFormatter(?string $format): FormatterInterface { return new LineFormatter($format); } /** * Creates instance of Email to be sent * * @param string $content formatted email body to be sent * @param array $records Log records that formed the content * * @phpstan-param Record[] $records */ protected function buildMessage(string $content, array $records): Email { $message = null; if ($this->emailTemplate instanceof Email) { $message = clone $this->emailTemplate; } elseif (is_callable($this->emailTemplate)) { $message = ($this->emailTemplate)($content, $records); } if (!$message instanceof Email) { $record = reset($records); throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it' . ($record ? Utils::getRecordMessageForException($record) : '')); } if ($records) { $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); $message->subject($subjectFormatter->format($this->getHighestRecord($records))); } if ($this->isHtmlBody($content)) { if (null !== ($charset = $message->getHtmlCharset())) { $message->html($content, $charset); } else { $message->html($content); } } else { if (null !== ($charset = $message->getTextCharset())) { $message->text($content, $charset); } else { $message->text($content); } } return $message->date(new \DateTimeImmutable()); } } PK!rEss<vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Monolog\Utils; /** * Logs to syslog service. * * usage example: * * $log = new Logger('application'); * $syslog = new SyslogHandler('myfacility', 'local6'); * $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); * $syslog->setFormatter($formatter); * $log->pushHandler($syslog); * * @author Sven Paulus */ class SyslogHandler extends AbstractSyslogHandler { /** @var string */ protected $ident; /** @var int */ protected $logopts; /** * @param string $ident * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant * @param int $logopts Option flags for the openlog() call, defaults to LOG_PID */ public function __construct(string $ident, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, int $logopts = LOG_PID) { parent::__construct($facility, $level, $bubble); $this->ident = $ident; $this->logopts = $logopts; } /** * {@inheritDoc} */ public function close(): void { closelog(); } /** * {@inheritDoc} */ protected function write(array $record): void { if (!openlog($this->ident, $this->logopts, $this->facility)) { throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"' . Utils::getRecordMessageForException($record)); } syslog($this->logLevels[$record['level']], (string) $record['formatted']); } } PK!H?vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use DateTimeInterface; use Monolog\Logger; use Monolog\Handler\SyslogUdp\UdpSocket; use Monolog\Utils; /** * A Handler for logging to a remote syslogd server. * * @author Jesper Skovgaard Nielsen * @author Dominik Kukacka */ class SyslogUdpHandler extends AbstractSyslogHandler { const RFC3164 = 0; const RFC5424 = 1; const RFC5424e = 2; /** @var array */ private $dateFormats = array( self::RFC3164 => 'M d H:i:s', self::RFC5424 => \DateTime::RFC3339, self::RFC5424e => \DateTime::RFC3339_EXTENDED, ); /** @var UdpSocket */ protected $socket; /** @var string */ protected $ident; /** @var self::RFC* */ protected $rfc; /** * @param string $host Either IP/hostname or a path to a unix socket (port must be 0 then) * @param int $port Port number, or 0 if $host is a unix socket * @param string|int $facility Either one of the names of the keys in $this->facilities, or a LOG_* facility constant * @param bool $bubble Whether the messages that are handled can bubble up the stack or not * @param string $ident Program name or tag for each log message. * @param int $rfc RFC to format the message for. * @throws MissingExtensionException * * @phpstan-param self::RFC* $rfc */ public function __construct(string $host, int $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, bool $bubble = true, string $ident = 'php', int $rfc = self::RFC5424) { if (!extension_loaded('sockets')) { throw new MissingExtensionException('The sockets extension is required to use the SyslogUdpHandler'); } parent::__construct($facility, $level, $bubble); $this->ident = $ident; $this->rfc = $rfc; $this->socket = new UdpSocket($host, $port); } protected function write(array $record): void { $lines = $this->splitMessageIntoLines($record['formatted']); $header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']], $record['datetime']); foreach ($lines as $line) { $this->socket->write($line, $header); } } public function close(): void { $this->socket->close(); } /** * @param string|string[] $message * @return string[] */ private function splitMessageIntoLines($message): array { if (is_array($message)) { $message = implode("\n", $message); } $lines = preg_split('/$\R?^/m', (string) $message, -1, PREG_SPLIT_NO_EMPTY); if (false === $lines) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Could not preg_split: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode)); } return $lines; } /** * Make common syslog header (see rfc5424 or rfc3164) */ protected function makeCommonSyslogHeader(int $severity, DateTimeInterface $datetime): string { $priority = $severity + $this->facility; if (!$pid = getmypid()) { $pid = '-'; } if (!$hostname = gethostname()) { $hostname = '-'; } if ($this->rfc === self::RFC3164) { // see https://github.com/phpstan/phpstan/issues/5348 // @phpstan-ignore-next-line $dateNew = $datetime->setTimezone(new \DateTimeZone('UTC')); $date = $dateNew->format($this->dateFormats[$this->rfc]); return "<$priority>" . $date . " " . $hostname . " " . $this->ident . "[" . $pid . "]: "; } $date = $datetime->format($this->dateFormats[$this->rfc]); return "<$priority>1 " . $date . " " . $hostname . " " . $this->ident . " " . $pid . " - - "; } /** * Inject your own socket, mainly used for testing */ public function setSocket(UdpSocket $socket): self { $this->socket = $socket; return $this; } } PK!'[ Avendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use RuntimeException; use Monolog\Logger; use Monolog\Utils; /** * Handler send logs to Telegram using Telegram Bot API. * * How to use: * 1) Create telegram bot with https://telegram.me/BotFather * 2) Create a telegram channel where logs will be recorded. * 3) Add created bot from step 1 to the created channel from step 2. * * Use telegram bot API key from step 1 and channel name with '@' prefix from step 2 to create instance of TelegramBotHandler * * @link https://core.telegram.org/bots/api * * @author Mazur Alexandr * * @phpstan-import-type Record from \Monolog\Logger */ class TelegramBotHandler extends AbstractProcessingHandler { private const BOT_API = 'https://api.telegram.org/bot'; /** * The available values of parseMode according to the Telegram api documentation */ private const AVAILABLE_PARSE_MODES = [ 'HTML', 'MarkdownV2', 'Markdown', // legacy mode without underline and strikethrough, use MarkdownV2 instead ]; /** * The maximum number of characters allowed in a message according to the Telegram api documentation */ private const MAX_MESSAGE_LENGTH = 4096; /** * Telegram bot access token provided by BotFather. * Create telegram bot with https://telegram.me/BotFather and use access token from it. * @var string */ private $apiKey; /** * Telegram channel name. * Since to start with '@' symbol as prefix. * @var string */ private $channel; /** * The kind of formatting that is used for the message. * See available options at https://core.telegram.org/bots/api#formatting-options * or in AVAILABLE_PARSE_MODES * @var ?string */ private $parseMode; /** * Disables link previews for links in the message. * @var ?bool */ private $disableWebPagePreview; /** * Sends the message silently. Users will receive a notification with no sound. * @var ?bool */ private $disableNotification; /** * True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages. * False - truncates a message that is too long. * @var bool */ private $splitLongMessages; /** * Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests). * @var bool */ private $delayBetweenMessages; /** * @param string $apiKey Telegram bot access token provided by BotFather * @param string $channel Telegram channel name * @param bool $splitLongMessages Split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages * @param bool $delayBetweenMessages Adds delay between sending a split message according to Telegram API * @throws MissingExtensionException */ public function __construct( string $apiKey, string $channel, $level = Logger::DEBUG, bool $bubble = true, ?string $parseMode = null, ?bool $disableWebPagePreview = null, ?bool $disableNotification = null, bool $splitLongMessages = false, bool $delayBetweenMessages = false ) { if (!extension_loaded('curl')) { throw new MissingExtensionException('The curl extension is needed to use the TelegramBotHandler'); } parent::__construct($level, $bubble); $this->apiKey = $apiKey; $this->channel = $channel; $this->setParseMode($parseMode); $this->disableWebPagePreview($disableWebPagePreview); $this->disableNotification($disableNotification); $this->splitLongMessages($splitLongMessages); $this->delayBetweenMessages($delayBetweenMessages); } public function setParseMode(?string $parseMode = null): self { if ($parseMode !== null && !in_array($parseMode, self::AVAILABLE_PARSE_MODES)) { throw new \InvalidArgumentException('Unknown parseMode, use one of these: ' . implode(', ', self::AVAILABLE_PARSE_MODES) . '.'); } $this->parseMode = $parseMode; return $this; } public function disableWebPagePreview(?bool $disableWebPagePreview = null): self { $this->disableWebPagePreview = $disableWebPagePreview; return $this; } public function disableNotification(?bool $disableNotification = null): self { $this->disableNotification = $disableNotification; return $this; } /** * True - split a message longer than MAX_MESSAGE_LENGTH into parts and send in multiple messages. * False - truncates a message that is too long. * @param bool $splitLongMessages * @return $this */ public function splitLongMessages(bool $splitLongMessages = false): self { $this->splitLongMessages = $splitLongMessages; return $this; } /** * Adds 1-second delay between sending a split message (according to Telegram API to avoid 429 Too Many Requests). * @param bool $delayBetweenMessages * @return $this */ public function delayBetweenMessages(bool $delayBetweenMessages = false): self { $this->delayBetweenMessages = $delayBetweenMessages; return $this; } /** * {@inheritDoc} */ public function handleBatch(array $records): void { /** @var Record[] $messages */ $messages = []; foreach ($records as $record) { if (!$this->isHandling($record)) { continue; } if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } $messages[] = $record; } if (!empty($messages)) { $this->send((string)$this->getFormatter()->formatBatch($messages)); } } /** * @inheritDoc */ protected function write(array $record): void { $this->send($record['formatted']); } /** * Send request to @link https://api.telegram.org/bot on SendMessage action. * @param string $message */ protected function send(string $message): void { $messages = $this->handleMessageLength($message); foreach ($messages as $key => $msg) { if ($this->delayBetweenMessages && $key > 0) { sleep(1); } $this->sendCurl($msg); } } protected function sendCurl(string $message): void { $ch = curl_init(); $url = self::BOT_API . $this->apiKey . '/SendMessage'; curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 'text' => $message, 'chat_id' => $this->channel, 'parse_mode' => $this->parseMode, 'disable_web_page_preview' => $this->disableWebPagePreview, 'disable_notification' => $this->disableNotification, ])); $result = Curl\Util::execute($ch); if (!is_string($result)) { throw new RuntimeException('Telegram API error. Description: No response'); } $result = json_decode($result, true); if ($result['ok'] === false) { throw new RuntimeException('Telegram API error. Description: ' . $result['description']); } } /** * Handle a message that is too long: truncates or splits into several * @param string $message * @return string[] */ private function handleMessageLength(string $message): array { $truncatedMarker = ' (...truncated)'; if (!$this->splitLongMessages && strlen($message) > self::MAX_MESSAGE_LENGTH) { return [Utils::substr($message, 0, self::MAX_MESSAGE_LENGTH - strlen($truncatedMarker)) . $truncatedMarker]; } return str_split($message, self::MAX_MESSAGE_LENGTH); } } PK!b^^:vendor/monolog/monolog/src/Monolog/Handler/TestHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Logger; use Psr\Log\LogLevel; /** * Used for testing purposes. * * It records all records and gives you access to them for verification. * * @author Jordi Boggiano * * @method bool hasEmergency($record) * @method bool hasAlert($record) * @method bool hasCritical($record) * @method bool hasError($record) * @method bool hasWarning($record) * @method bool hasNotice($record) * @method bool hasInfo($record) * @method bool hasDebug($record) * * @method bool hasEmergencyRecords() * @method bool hasAlertRecords() * @method bool hasCriticalRecords() * @method bool hasErrorRecords() * @method bool hasWarningRecords() * @method bool hasNoticeRecords() * @method bool hasInfoRecords() * @method bool hasDebugRecords() * * @method bool hasEmergencyThatContains($message) * @method bool hasAlertThatContains($message) * @method bool hasCriticalThatContains($message) * @method bool hasErrorThatContains($message) * @method bool hasWarningThatContains($message) * @method bool hasNoticeThatContains($message) * @method bool hasInfoThatContains($message) * @method bool hasDebugThatContains($message) * * @method bool hasEmergencyThatMatches($message) * @method bool hasAlertThatMatches($message) * @method bool hasCriticalThatMatches($message) * @method bool hasErrorThatMatches($message) * @method bool hasWarningThatMatches($message) * @method bool hasNoticeThatMatches($message) * @method bool hasInfoThatMatches($message) * @method bool hasDebugThatMatches($message) * * @method bool hasEmergencyThatPasses($message) * @method bool hasAlertThatPasses($message) * @method bool hasCriticalThatPasses($message) * @method bool hasErrorThatPasses($message) * @method bool hasWarningThatPasses($message) * @method bool hasNoticeThatPasses($message) * @method bool hasInfoThatPasses($message) * @method bool hasDebugThatPasses($message) * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class TestHandler extends AbstractProcessingHandler { /** @var Record[] */ protected $records = []; /** @var array */ protected $recordsByLevel = []; /** @var bool */ private $skipReset = false; /** * @return array * * @phpstan-return Record[] */ public function getRecords() { return $this->records; } /** * @return void */ public function clear() { $this->records = []; $this->recordsByLevel = []; } /** * @return void */ public function reset() { if (!$this->skipReset) { $this->clear(); } } /** * @return void */ public function setSkipReset(bool $skipReset) { $this->skipReset = $skipReset; } /** * @param string|int $level Logging level value or name * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function hasRecords($level): bool { return isset($this->recordsByLevel[Logger::toMonologLevel($level)]); } /** * @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records * @param string|int $level Logging level value or name * * @phpstan-param array{message: string, context?: mixed[]}|string $record * @phpstan-param Level|LevelName|LogLevel::* $level */ public function hasRecord($record, $level): bool { if (is_string($record)) { $record = array('message' => $record); } return $this->hasRecordThatPasses(function ($rec) use ($record) { if ($rec['message'] !== $record['message']) { return false; } if (isset($record['context']) && $rec['context'] !== $record['context']) { return false; } return true; }, $level); } /** * @param string|int $level Logging level value or name * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function hasRecordThatContains(string $message, $level): bool { return $this->hasRecordThatPasses(function ($rec) use ($message) { return strpos($rec['message'], $message) !== false; }, $level); } /** * @param string|int $level Logging level value or name * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function hasRecordThatMatches(string $regex, $level): bool { return $this->hasRecordThatPasses(function (array $rec) use ($regex): bool { return preg_match($regex, $rec['message']) > 0; }, $level); } /** * @param string|int $level Logging level value or name * @return bool * * @psalm-param callable(Record, int): mixed $predicate * @phpstan-param Level|LevelName|LogLevel::* $level */ public function hasRecordThatPasses(callable $predicate, $level) { $level = Logger::toMonologLevel($level); if (!isset($this->recordsByLevel[$level])) { return false; } foreach ($this->recordsByLevel[$level] as $i => $rec) { if ($predicate($rec, $i)) { return true; } } return false; } /** * {@inheritDoc} */ protected function write(array $record): void { $this->recordsByLevel[$record['level']][] = $record; $this->records[] = $record; } /** * @param string $method * @param mixed[] $args * @return bool */ public function __call($method, $args) { if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; $level = constant('Monolog\Logger::' . strtoupper($matches[2])); $callback = [$this, $genericMethod]; if (is_callable($callback)) { $args[] = $level; return call_user_func_array($callback, $args); } } throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); } } PK!y  Hvendor/monolog/monolog/src/Monolog/Handler/WebRequestRecognizerTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; trait WebRequestRecognizerTrait { /** * Checks if PHP's serving a web request * @return bool */ protected function isWebRequest(): bool { return 'cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI; } } PK!&CFvendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; /** * Forwards records to multiple handlers suppressing failures of each handler * and continuing through to give every handler a chance to succeed. * * @author Craig D'Amelio * * @phpstan-import-type Record from \Monolog\Logger */ class WhatFailureGroupHandler extends GroupHandler { /** * {@inheritDoc} */ public function handle(array $record): bool { if ($this->processors) { /** @var Record $record */ $record = $this->processRecord($record); } foreach ($this->handlers as $handler) { try { $handler->handle($record); } catch (\Throwable $e) { // What failure? } } return false === $this->bubble; } /** * {@inheritDoc} */ public function handleBatch(array $records): void { if ($this->processors) { $processed = array(); foreach ($records as $record) { $processed[] = $this->processRecord($record); } /** @var Record[] $records */ $records = $processed; } foreach ($this->handlers as $handler) { try { $handler->handleBatch($records); } catch (\Throwable $e) { // What failure? } } } /** * {@inheritDoc} */ public function close(): void { foreach ($this->handlers as $handler) { try { $handler->close(); } catch (\Throwable $e) { // What failure? } } } } PK!=A A Avendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\NormalizerFormatter; use Monolog\Logger; /** * Handler sending logs to Zend Monitor * * @author Christian Bergau * @author Jason Davis * * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class ZendMonitorHandler extends AbstractProcessingHandler { /** * Monolog level / ZendMonitor Custom Event priority map * * @var array */ protected $levelMap = []; /** * @throws MissingExtensionException */ public function __construct($level = Logger::DEBUG, bool $bubble = true) { if (!function_exists('zend_monitor_custom_event')) { throw new MissingExtensionException( 'You must have Zend Server installed with Zend Monitor enabled in order to use this handler' ); } //zend monitor constants are not defined if zend monitor is not enabled. $this->levelMap = [ Logger::DEBUG => \ZEND_MONITOR_EVENT_SEVERITY_INFO, Logger::INFO => \ZEND_MONITOR_EVENT_SEVERITY_INFO, Logger::NOTICE => \ZEND_MONITOR_EVENT_SEVERITY_INFO, Logger::WARNING => \ZEND_MONITOR_EVENT_SEVERITY_WARNING, Logger::ERROR => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, Logger::CRITICAL => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, Logger::ALERT => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, Logger::EMERGENCY => \ZEND_MONITOR_EVENT_SEVERITY_ERROR, ]; parent::__construct($level, $bubble); } /** * {@inheritDoc} */ protected function write(array $record): void { $this->writeZendMonitorCustomEvent( Logger::getLevelName($record['level']), $record['message'], $record['formatted'], $this->levelMap[$record['level']] ); } /** * Write to Zend Monitor Events * @param string $type Text displayed in "Class Name (custom)" field * @param string $message Text displayed in "Error String" * @param array $formatted Displayed in Custom Variables tab * @param int $severity Set the event severity level (-1,0,1) * * @phpstan-param FormattedRecord $formatted */ protected function writeZendMonitorCustomEvent(string $type, string $message, array $formatted, int $severity): void { zend_monitor_custom_event($type, $message, $formatted, $severity); } /** * {@inheritDoc} */ public function getDefaultFormatter(): FormatterInterface { return new NormalizerFormatter(); } /** * @return array */ public function getLevelMap(): array { return $this->levelMap; } } PK!x\˕=vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; use Monolog\Logger; use Psr\Log\LogLevel; /** * Injects Git branch and Git commit SHA in all records * * @author Nick Otter * @author Jordi Boggiano * * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class GitProcessor implements ProcessorInterface { /** @var int */ private $level; /** @var array{branch: string, commit: string}|array|null */ private static $cache = null; /** * @param string|int $level The minimum logging level at which this Processor will be triggered * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function __construct($level = Logger::DEBUG) { $this->level = Logger::toMonologLevel($level); } /** * {@inheritDoc} */ public function __invoke(array $record): array { // return if the level is not high enough if ($record['level'] < $this->level) { return $record; } $record['extra']['git'] = self::getGitInfo(); return $record; } /** * @return array{branch: string, commit: string}|array */ private static function getGitInfo(): array { if (self::$cache) { return self::$cache; } $branches = `git branch -v --no-abbrev`; if ($branches && preg_match('{^\* (.+?)\s+([a-f0-9]{40})(?:\s|$)}m', $branches, $matches)) { return self::$cache = [ 'branch' => $matches[1], 'commit' => $matches[2], ]; } return self::$cache = []; } } PK!aBvendor/monolog/monolog/src/Monolog/Processor/HostnameProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Injects value of gethostname in all records */ class HostnameProcessor implements ProcessorInterface { /** @var string */ private static $host; public function __construct() { self::$host = (string) gethostname(); } /** * {@inheritDoc} */ public function __invoke(array $record): array { $record['extra']['hostname'] = self::$host; return $record; } } PK! .fGvendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; use Monolog\Logger; use Psr\Log\LogLevel; /** * Injects line/file:class/function where the log message came from * * Warning: This only works if the handler processes the logs directly. * If you put the processor on a handler that is behind a FingersCrossedHandler * for example, the processor will only be called once the trigger level is reached, * and all the log records will have the same file/line/.. data from the call that * triggered the FingersCrossedHandler. * * @author Jordi Boggiano * * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class IntrospectionProcessor implements ProcessorInterface { /** @var int */ private $level; /** @var string[] */ private $skipClassesPartials; /** @var int */ private $skipStackFramesCount; /** @var string[] */ private $skipFunctions = [ 'call_user_func', 'call_user_func_array', ]; /** * @param string|int $level The minimum logging level at which this Processor will be triggered * @param string[] $skipClassesPartials * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function __construct($level = Logger::DEBUG, array $skipClassesPartials = [], int $skipStackFramesCount = 0) { $this->level = Logger::toMonologLevel($level); $this->skipClassesPartials = array_merge(['Monolog\\'], $skipClassesPartials); $this->skipStackFramesCount = $skipStackFramesCount; } /** * {@inheritDoc} */ public function __invoke(array $record): array { // return if the level is not high enough if ($record['level'] < $this->level) { return $record; } $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // skip first since it's always the current method array_shift($trace); // the call_user_func call is also skipped array_shift($trace); $i = 0; while ($this->isTraceClassOrSkippedFunction($trace, $i)) { if (isset($trace[$i]['class'])) { foreach ($this->skipClassesPartials as $part) { if (strpos($trace[$i]['class'], $part) !== false) { $i++; continue 2; } } } elseif (in_array($trace[$i]['function'], $this->skipFunctions)) { $i++; continue; } break; } $i += $this->skipStackFramesCount; // we should have the call source now $record['extra'] = array_merge( $record['extra'], [ 'file' => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null, 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, 'class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : null, 'callType' => isset($trace[$i]['type']) ? $trace[$i]['type'] : null, 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, ] ); return $record; } /** * @param array[] $trace */ private function isTraceClassOrSkippedFunction(array $trace, int $index): bool { if (!isset($trace[$index])) { return false; } return isset($trace[$index]['class']) || in_array($trace[$index]['function'], $this->skipFunctions); } } PK!DDIvendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Injects memory_get_peak_usage in all records * * @see Monolog\Processor\MemoryProcessor::__construct() for options * @author Rob Jensen */ class MemoryPeakUsageProcessor extends MemoryProcessor { /** * {@inheritDoc} */ public function __invoke(array $record): array { $usage = memory_get_peak_usage($this->realUsage); if ($this->useFormatting) { $usage = $this->formatBytes($usage); } $record['extra']['memory_peak_usage'] = $usage; return $record; } } PK!a ##@vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Some methods that are common for all memory processors * * @author Rob Jensen */ abstract class MemoryProcessor implements ProcessorInterface { /** * @var bool If true, get the real size of memory allocated from system. Else, only the memory used by emalloc() is reported. */ protected $realUsage; /** * @var bool If true, then format memory size to human readable string (MB, KB, B depending on size) */ protected $useFormatting; /** * @param bool $realUsage Set this to true to get the real size of memory allocated from system. * @param bool $useFormatting If true, then format memory size to human readable string (MB, KB, B depending on size) */ public function __construct(bool $realUsage = true, bool $useFormatting = true) { $this->realUsage = $realUsage; $this->useFormatting = $useFormatting; } /** * Formats bytes into a human readable string if $this->useFormatting is true, otherwise return $bytes as is * * @param int $bytes * @return string|int Formatted string if $this->useFormatting is true, otherwise return $bytes as int */ protected function formatBytes(int $bytes) { if (!$this->useFormatting) { return $bytes; } if ($bytes > 1024 * 1024) { return round($bytes / 1024 / 1024, 2).' MB'; } elseif ($bytes > 1024) { return round($bytes / 1024, 2).' KB'; } return $bytes . ' B'; } } PK!)11Evendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Injects memory_get_usage in all records * * @see Monolog\Processor\MemoryProcessor::__construct() for options * @author Rob Jensen */ class MemoryUsageProcessor extends MemoryProcessor { /** * {@inheritDoc} */ public function __invoke(array $record): array { $usage = memory_get_usage($this->realUsage); if ($this->useFormatting) { $usage = $this->formatBytes($usage); } $record['extra']['memory_usage'] = $usage; return $record; } } PK!:DChhCvendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; use Monolog\Logger; use Psr\Log\LogLevel; /** * Injects Hg branch and Hg revision number in all records * * @author Jonathan A. Schweder * * @phpstan-import-type LevelName from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger */ class MercurialProcessor implements ProcessorInterface { /** @var Level */ private $level; /** @var array{branch: string, revision: string}|array|null */ private static $cache = null; /** * @param int|string $level The minimum logging level at which this Processor will be triggered * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function __construct($level = Logger::DEBUG) { $this->level = Logger::toMonologLevel($level); } /** * {@inheritDoc} */ public function __invoke(array $record): array { // return if the level is not high enough if ($record['level'] < $this->level) { return $record; } $record['extra']['hg'] = self::getMercurialInfo(); return $record; } /** * @return array{branch: string, revision: string}|array */ private static function getMercurialInfo(): array { if (self::$cache) { return self::$cache; } $result = explode(' ', trim(`hg id -nb`)); if (count($result) >= 3) { return self::$cache = [ 'branch' => $result[1], 'revision' => $result[2], ]; } return self::$cache = []; } } PK!__Cvendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Adds value of getmypid into records * * @author Andreas Hörnicke */ class ProcessIdProcessor implements ProcessorInterface { /** * {@inheritDoc} */ public function __invoke(array $record): array { $record['extra']['process_id'] = getmypid(); return $record; } } PK!Y^Cvendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * An optional interface to allow labelling Monolog processors. * * @author Nicolas Grekas * * @phpstan-import-type Record from \Monolog\Logger */ interface ProcessorInterface { /** * @return array The processed record * * @phpstan-param Record $record * @phpstan-return Record */ public function __invoke(array $record); } PK!#G Gvendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; use Monolog\Utils; /** * Processes a record's message according to PSR-3 rules * * It replaces {foo} with the value from $context['foo'] * * @author Jordi Boggiano */ class PsrLogMessageProcessor implements ProcessorInterface { public const SIMPLE_DATE = "Y-m-d\TH:i:s.uP"; /** @var string|null */ private $dateFormat; /** @var bool */ private $removeUsedContextFields; /** * @param string|null $dateFormat The format of the timestamp: one supported by DateTime::format * @param bool $removeUsedContextFields If set to true the fields interpolated into message gets unset */ public function __construct(?string $dateFormat = null, bool $removeUsedContextFields = false) { $this->dateFormat = $dateFormat; $this->removeUsedContextFields = $removeUsedContextFields; } /** * {@inheritDoc} */ public function __invoke(array $record): array { if (false === strpos($record['message'], '{')) { return $record; } $replacements = []; foreach ($record['context'] as $key => $val) { $placeholder = '{' . $key . '}'; if (strpos($record['message'], $placeholder) === false) { continue; } if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { $replacements[$placeholder] = $val; } elseif ($val instanceof \DateTimeInterface) { if (!$this->dateFormat && $val instanceof \Monolog\DateTimeImmutable) { // handle monolog dates using __toString if no specific dateFormat was asked for // so that it follows the useMicroseconds flag $replacements[$placeholder] = (string) $val; } else { $replacements[$placeholder] = $val->format($this->dateFormat ?: static::SIMPLE_DATE); } } elseif ($val instanceof \UnitEnum) { $replacements[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name; } elseif (is_object($val)) { $replacements[$placeholder] = '[object '.Utils::getClass($val).']'; } elseif (is_array($val)) { $replacements[$placeholder] = 'array'.Utils::jsonEncode($val, null, true); } else { $replacements[$placeholder] = '['.gettype($val).']'; } if ($this->removeUsedContextFields) { unset($record['context'][$key]); } } $record['message'] = strtr($record['message'], $replacements); return $record; } } PK!{bb=vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Adds a tags array into record * * @author Martijn Riemers */ class TagProcessor implements ProcessorInterface { /** @var string[] */ private $tags; /** * @param string[] $tags */ public function __construct(array $tags = []) { $this->setTags($tags); } /** * @param string[] $tags */ public function addTags(array $tags = []): self { $this->tags = array_merge($this->tags, $tags); return $this; } /** * @param string[] $tags */ public function setTags(array $tags = []): self { $this->tags = $tags; return $this; } /** * {@inheritDoc} */ public function __invoke(array $record): array { $record['extra']['tags'] = $this->tags; return $record; } } PK!x=vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; use Monolog\ResettableInterface; /** * Adds a unique identifier into records * * @author Simon Mönch */ class UidProcessor implements ProcessorInterface, ResettableInterface { /** @var string */ private $uid; public function __construct(int $length = 7) { if ($length > 32 || $length < 1) { throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); } $this->uid = $this->generateUid($length); } /** * {@inheritDoc} */ public function __invoke(array $record): array { $record['extra']['uid'] = $this->uid; return $record; } public function getUid(): string { return $this->uid; } public function reset() { $this->uid = $this->generateUid(strlen($this->uid)); } private function generateUid(int $length): string { return substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length); } } PK!E =vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Processor; /** * Injects url/method and remote IP of the current web request in all records * * @author Jordi Boggiano */ class WebProcessor implements ProcessorInterface { /** * @var array|\ArrayAccess */ protected $serverData; /** * Default fields * * Array is structured as [key in record.extra => key in $serverData] * * @var array */ protected $extraFields = [ 'url' => 'REQUEST_URI', 'ip' => 'REMOTE_ADDR', 'http_method' => 'REQUEST_METHOD', 'server' => 'SERVER_NAME', 'referrer' => 'HTTP_REFERER', 'user_agent' => 'HTTP_USER_AGENT', ]; /** * @param array|\ArrayAccess|null $serverData Array or object w/ ArrayAccess that provides access to the $_SERVER data * @param array|array|null $extraFields Field names and the related key inside $serverData to be added (or just a list of field names to use the default configured $serverData mapping). If not provided it defaults to: [url, ip, http_method, server, referrer] + unique_id if present in server data */ public function __construct($serverData = null, ?array $extraFields = null) { if (null === $serverData) { $this->serverData = &$_SERVER; } elseif (is_array($serverData) || $serverData instanceof \ArrayAccess) { $this->serverData = $serverData; } else { throw new \UnexpectedValueException('$serverData must be an array or object implementing ArrayAccess.'); } $defaultEnabled = ['url', 'ip', 'http_method', 'server', 'referrer']; if (isset($this->serverData['UNIQUE_ID'])) { $this->extraFields['unique_id'] = 'UNIQUE_ID'; $defaultEnabled[] = 'unique_id'; } if (null === $extraFields) { $extraFields = $defaultEnabled; } if (isset($extraFields[0])) { foreach (array_keys($this->extraFields) as $fieldName) { if (!in_array($fieldName, $extraFields)) { unset($this->extraFields[$fieldName]); } } } else { $this->extraFields = $extraFields; } } /** * {@inheritDoc} */ public function __invoke(array $record): array { // skip processing if for some reason request data // is not present (CLI or wonky SAPIs) if (!isset($this->serverData['REQUEST_URI'])) { return $record; } $record['extra'] = $this->appendExtraFields($record['extra']); return $record; } public function addExtraField(string $extraName, string $serverName): self { $this->extraFields[$extraName] = $serverName; return $this; } /** * @param mixed[] $extra * @return mixed[] */ private function appendExtraFields(array $extra): array { foreach ($this->extraFields as $extraName => $serverName) { $extra[$extraName] = $this->serverData[$serverName] ?? null; } return $extra; } } PK!Mnn n 4vendor/monolog/monolog/src/Monolog/Test/TestCase.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Test; use Monolog\Logger; use Monolog\DateTimeImmutable; use Monolog\Formatter\FormatterInterface; /** * Lets you easily generate log records and a dummy formatter for testing purposes * * @author Jordi Boggiano * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type Level from \Monolog\Logger * * @internal feel free to reuse this to test your own handlers, this is marked internal to avoid issues with PHPStorm https://github.com/Seldaek/monolog/issues/1677 */ class TestCase extends \PHPUnit\Framework\TestCase { public function tearDown(): void { parent::tearDown(); if (isset($this->handler)) { unset($this->handler); } } /** * @param mixed[] $context * * @return array Record * * @phpstan-param Level $level * @phpstan-return Record */ protected function getRecord(int $level = Logger::WARNING, string $message = 'test', array $context = []): array { return [ 'message' => (string) $message, 'context' => $context, 'level' => $level, 'level_name' => Logger::getLevelName($level), 'channel' => 'test', 'datetime' => new DateTimeImmutable(true), 'extra' => [], ]; } /** * @phpstan-return Record[] */ protected function getMultipleRecords(): array { return [ $this->getRecord(Logger::DEBUG, 'debug message 1'), $this->getRecord(Logger::DEBUG, 'debug message 2'), $this->getRecord(Logger::INFO, 'information'), $this->getRecord(Logger::WARNING, 'warning'), $this->getRecord(Logger::ERROR, 'error'), ]; } protected function getIdentityFormatter(): FormatterInterface { $formatter = $this->createMock(FormatterInterface::class); $formatter->expects($this->any()) ->method('format') ->will($this->returnCallback(function ($record) { return $record['message']; })); return $formatter; } } PK![8vendor/monolog/monolog/src/Monolog/DateTimeImmutable.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; use DateTimeZone; /** * Overrides default json encoding of date time objects * * @author Menno Holtkamp * @author Jordi Boggiano */ class DateTimeImmutable extends \DateTimeImmutable implements \JsonSerializable { /** * @var bool */ private $useMicroseconds; public function __construct(bool $useMicroseconds, ?DateTimeZone $timezone = null) { $this->useMicroseconds = $useMicroseconds; // if you like to use a custom time to pass to Logger::addRecord directly, // call modify() or setTimestamp() on this instance to change the date after creating it parent::__construct('now', $timezone); } public function jsonSerialize(): string { if ($this->useMicroseconds) { return $this->format('Y-m-d\TH:i:s.uP'); } return $this->format('Y-m-d\TH:i:sP'); } public function __toString(): string { return $this->jsonSerialize(); } } PK!sTw*w*3vendor/monolog/monolog/src/Monolog/ErrorHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * Monolog error handler * * A facility to enable logging of runtime errors, exceptions and fatal errors. * * Quick setup: ErrorHandler::register($logger); * * @author Jordi Boggiano */ class ErrorHandler { /** @var LoggerInterface */ private $logger; /** @var ?callable */ private $previousExceptionHandler = null; /** @var array an array of class name to LogLevel::* constant mapping */ private $uncaughtExceptionLevelMap = []; /** @var callable|true|null */ private $previousErrorHandler = null; /** @var array an array of E_* constant to LogLevel::* constant mapping */ private $errorLevelMap = []; /** @var bool */ private $handleOnlyReportedErrors = true; /** @var bool */ private $hasFatalErrorHandler = false; /** @var LogLevel::* */ private $fatalLevel = LogLevel::ALERT; /** @var ?string */ private $reservedMemory = null; /** @var ?array{type: int, message: string, file: string, line: int, trace: mixed} */ private $lastFatalData = null; /** @var int[] */ private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } /** * Registers a new ErrorHandler for a given Logger * * By default it will handle errors, exceptions and fatal errors * * @param LoggerInterface $logger * @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling * @param array|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling * @return ErrorHandler */ public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self { /** @phpstan-ignore-next-line */ $handler = new static($logger); if ($errorLevelMap !== false) { $handler->registerErrorHandler($errorLevelMap); } if ($exceptionLevelMap !== false) { $handler->registerExceptionHandler($exceptionLevelMap); } if ($fatalLevel !== false) { $handler->registerFatalHandler($fatalLevel); } return $handler; } /** * @param array $levelMap an array of class name to LogLevel::* constant mapping * @return $this */ public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self { $prev = set_exception_handler(function (\Throwable $e): void { $this->handleException($e); }); $this->uncaughtExceptionLevelMap = $levelMap; foreach ($this->defaultExceptionLevelMap() as $class => $level) { if (!isset($this->uncaughtExceptionLevelMap[$class])) { $this->uncaughtExceptionLevelMap[$class] = $level; } } if ($callPrevious && $prev) { $this->previousExceptionHandler = $prev; } return $this; } /** * @param array $levelMap an array of E_* constant to LogLevel::* constant mapping * @return $this */ public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self { $prev = set_error_handler([$this, 'handleError'], $errorTypes); $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap); if ($callPrevious) { $this->previousErrorHandler = $prev ?: true; } else { $this->previousErrorHandler = null; } $this->handleOnlyReportedErrors = $handleOnlyReportedErrors; return $this; } /** * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT * @param int $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done */ public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self { register_shutdown_function([$this, 'handleFatalError']); $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); $this->fatalLevel = null === $level ? LogLevel::ALERT : $level; $this->hasFatalErrorHandler = true; return $this; } /** * @return array */ protected function defaultExceptionLevelMap(): array { return [ 'ParseError' => LogLevel::CRITICAL, 'Throwable' => LogLevel::ERROR, ]; } /** * @return array */ protected function defaultErrorLevelMap(): array { return [ E_ERROR => LogLevel::CRITICAL, E_WARNING => LogLevel::WARNING, E_PARSE => LogLevel::ALERT, E_NOTICE => LogLevel::NOTICE, E_CORE_ERROR => LogLevel::CRITICAL, E_CORE_WARNING => LogLevel::WARNING, E_COMPILE_ERROR => LogLevel::ALERT, E_COMPILE_WARNING => LogLevel::WARNING, E_USER_ERROR => LogLevel::ERROR, E_USER_WARNING => LogLevel::WARNING, E_USER_NOTICE => LogLevel::NOTICE, E_STRICT => LogLevel::NOTICE, E_RECOVERABLE_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::NOTICE, E_USER_DEPRECATED => LogLevel::NOTICE, ]; } /** * @phpstan-return never */ private function handleException(\Throwable $e): void { $level = LogLevel::ERROR; foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) { if ($e instanceof $class) { $level = $candidate; break; } } $this->logger->log( $level, sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e] ); if ($this->previousExceptionHandler) { ($this->previousExceptionHandler)($e); } if (!headers_sent() && in_array(strtolower((string) ini_get('display_errors')), ['0', '', 'false', 'off', 'none', 'no'], true)) { http_response_code(500); } exit(255); } /** * @private * * @param mixed[] $context */ public function handleError(int $code, string $message, string $file = '', int $line = 0, ?array $context = []): bool { if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) { return false; } // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) { $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL; $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]); } else { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); array_shift($trace); // Exclude handleError from trace $this->lastFatalData = ['type' => $code, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $trace]; } if ($this->previousErrorHandler === true) { return false; } elseif ($this->previousErrorHandler) { return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context); } return true; } /** * @private */ public function handleFatalError(): void { $this->reservedMemory = ''; if (is_array($this->lastFatalData)) { $lastError = $this->lastFatalData; } else { $lastError = error_get_last(); } if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) { $trace = $lastError['trace'] ?? null; $this->logger->log( $this->fatalLevel, 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $trace] ); if ($this->logger instanceof Logger) { foreach ($this->logger->getHandlers() as $handler) { $handler->close(); } } } } /** * @param int $code */ private static function codeToString($code): string { switch ($code) { case E_ERROR: return 'E_ERROR'; case E_WARNING: return 'E_WARNING'; case E_PARSE: return 'E_PARSE'; case E_NOTICE: return 'E_NOTICE'; case E_CORE_ERROR: return 'E_CORE_ERROR'; case E_CORE_WARNING: return 'E_CORE_WARNING'; case E_COMPILE_ERROR: return 'E_COMPILE_ERROR'; case E_COMPILE_WARNING: return 'E_COMPILE_WARNING'; case E_USER_ERROR: return 'E_USER_ERROR'; case E_USER_WARNING: return 'E_USER_WARNING'; case E_USER_NOTICE: return 'E_USER_NOTICE'; case E_STRICT: return 'E_STRICT'; case E_RECOVERABLE_ERROR: return 'E_RECOVERABLE_ERROR'; case E_DEPRECATED: return 'E_DEPRECATED'; case E_USER_DEPRECATED: return 'E_USER_DEPRECATED'; } return 'Unknown PHP error'; } } PK!UZUZ-vendor/monolog/monolog/src/Monolog/Logger.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; use DateTimeZone; use Monolog\Handler\HandlerInterface; use Psr\Log\LoggerInterface; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Throwable; use Stringable; /** * Monolog log channel * * It contains a stack of Handlers and a stack of Processors, * and uses them to store records that are added to it. * * @author Jordi Boggiano * * @phpstan-type Level Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY * @phpstan-type LevelName 'DEBUG'|'INFO'|'NOTICE'|'WARNING'|'ERROR'|'CRITICAL'|'ALERT'|'EMERGENCY' * @phpstan-type Record array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[]} */ class Logger implements LoggerInterface, ResettableInterface { /** * Detailed debug information */ public const DEBUG = 100; /** * Interesting events * * Examples: User logs in, SQL logs. */ public const INFO = 200; /** * Uncommon events */ public const NOTICE = 250; /** * Exceptional occurrences that are not errors * * Examples: Use of deprecated APIs, poor use of an API, * undesirable things that are not necessarily wrong. */ public const WARNING = 300; /** * Runtime errors */ public const ERROR = 400; /** * Critical conditions * * Example: Application component unavailable, unexpected exception. */ public const CRITICAL = 500; /** * Action must be taken immediately * * Example: Entire website down, database unavailable, etc. * This should trigger the SMS alerts and wake you up. */ public const ALERT = 550; /** * Urgent alert. */ public const EMERGENCY = 600; /** * Monolog API version * * This is only bumped when API breaks are done and should * follow the major version of the library * * @var int */ public const API = 2; /** * This is a static variable and not a constant to serve as an extension point for custom levels * * @var array $levels Logging levels with the levels as key * * @phpstan-var array $levels Logging levels with the levels as key */ protected static $levels = [ self::DEBUG => 'DEBUG', self::INFO => 'INFO', self::NOTICE => 'NOTICE', self::WARNING => 'WARNING', self::ERROR => 'ERROR', self::CRITICAL => 'CRITICAL', self::ALERT => 'ALERT', self::EMERGENCY => 'EMERGENCY', ]; /** * Mapping between levels numbers defined in RFC 5424 and Monolog ones * * @phpstan-var array $rfc_5424_levels */ private const RFC_5424_LEVELS = [ 7 => self::DEBUG, 6 => self::INFO, 5 => self::NOTICE, 4 => self::WARNING, 3 => self::ERROR, 2 => self::CRITICAL, 1 => self::ALERT, 0 => self::EMERGENCY, ]; /** * @var string */ protected $name; /** * The handler stack * * @var HandlerInterface[] */ protected $handlers; /** * Processors that will process all log records * * To process records of a single handler instead, add the processor on that specific handler * * @var callable[] */ protected $processors; /** * @var bool */ protected $microsecondTimestamps = true; /** * @var DateTimeZone */ protected $timezone; /** * @var callable|null */ protected $exceptionHandler; /** * @var int Keeps track of depth to prevent infinite logging loops */ private $logDepth = 0; /** * @var \WeakMap<\Fiber, int> Keeps track of depth inside fibers to prevent infinite logging loops */ private $fiberLogDepth; /** * @var bool Whether to detect infinite logging loops * * This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this */ private $detectCycles = true; /** * @psalm-param array $processors * * @param string $name The logging channel, a simple descriptive name that is attached to all log records * @param HandlerInterface[] $handlers Optional stack of handlers, the first one in the array is called first, etc. * @param callable[] $processors Optional array of processors * @param DateTimeZone|null $timezone Optional timezone, if not provided date_default_timezone_get() will be used */ public function __construct(string $name, array $handlers = [], array $processors = [], ?DateTimeZone $timezone = null) { $this->name = $name; $this->setHandlers($handlers); $this->processors = $processors; $this->timezone = $timezone ?: new DateTimeZone(date_default_timezone_get() ?: 'UTC'); if (\PHP_VERSION_ID >= 80100) { // Local variable for phpstan, see https://github.com/phpstan/phpstan/issues/6732#issuecomment-1111118412 /** @var \WeakMap<\Fiber, int> $fiberLogDepth */ $fiberLogDepth = new \WeakMap(); $this->fiberLogDepth = $fiberLogDepth; } } public function getName(): string { return $this->name; } /** * Return a new cloned instance with the name changed */ public function withName(string $name): self { $new = clone $this; $new->name = $name; return $new; } /** * Pushes a handler on to the stack. */ public function pushHandler(HandlerInterface $handler): self { array_unshift($this->handlers, $handler); return $this; } /** * Pops a handler from the stack * * @throws \LogicException If empty handler stack */ public function popHandler(): HandlerInterface { if (!$this->handlers) { throw new \LogicException('You tried to pop from an empty handler stack.'); } return array_shift($this->handlers); } /** * Set handlers, replacing all existing ones. * * If a map is passed, keys will be ignored. * * @param HandlerInterface[] $handlers */ public function setHandlers(array $handlers): self { $this->handlers = []; foreach (array_reverse($handlers) as $handler) { $this->pushHandler($handler); } return $this; } /** * @return HandlerInterface[] */ public function getHandlers(): array { return $this->handlers; } /** * Adds a processor on to the stack. */ public function pushProcessor(callable $callback): self { array_unshift($this->processors, $callback); return $this; } /** * Removes the processor on top of the stack and returns it. * * @throws \LogicException If empty processor stack * @return callable */ public function popProcessor(): callable { if (!$this->processors) { throw new \LogicException('You tried to pop from an empty processor stack.'); } return array_shift($this->processors); } /** * @return callable[] */ public function getProcessors(): array { return $this->processors; } /** * Control the use of microsecond resolution timestamps in the 'datetime' * member of new records. * * As of PHP7.1 microseconds are always included by the engine, so * there is no performance penalty and Monolog 2 enabled microseconds * by default. This function lets you disable them though in case you want * to suppress microseconds from the output. * * @param bool $micro True to use microtime() to create timestamps */ public function useMicrosecondTimestamps(bool $micro): self { $this->microsecondTimestamps = $micro; return $this; } public function useLoggingLoopDetection(bool $detectCycles): self { $this->detectCycles = $detectCycles; return $this; } /** * Adds a log record. * * @param int $level The logging level (a Monolog or RFC 5424 level) * @param string $message The log message * @param mixed[] $context The log context * @param DateTimeImmutable $datetime Optional log date to log into the past or future * @return bool Whether the record has been processed * * @phpstan-param Level $level */ public function addRecord(int $level, string $message, array $context = [], ?DateTimeImmutable $datetime = null): bool { if (isset(self::RFC_5424_LEVELS[$level])) { $level = self::RFC_5424_LEVELS[$level]; } if ($this->detectCycles) { if (\PHP_VERSION_ID >= 80100 && $fiber = \Fiber::getCurrent()) { // @phpstan-ignore offsetAssign.dimType $this->fiberLogDepth[$fiber] = $this->fiberLogDepth[$fiber] ?? 0; $logDepth = ++$this->fiberLogDepth[$fiber]; } else { $logDepth = ++$this->logDepth; } } else { $logDepth = 0; } if ($logDepth === 3) { $this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.'); return false; } elseif ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above return false; } try { $record = null; foreach ($this->handlers as $handler) { if (null === $record) { // skip creating the record as long as no handler is going to handle it if (!$handler->isHandling(['level' => $level])) { continue; } $levelName = static::getLevelName($level); $record = [ 'message' => $message, 'context' => $context, 'level' => $level, 'level_name' => $levelName, 'channel' => $this->name, 'datetime' => $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone), 'extra' => [], ]; try { foreach ($this->processors as $processor) { $record = $processor($record); } } catch (Throwable $e) { $this->handleException($e, $record); return true; } } // once the record exists, send it to all handlers as long as the bubbling chain is not interrupted try { if (true === $handler->handle($record)) { break; } } catch (Throwable $e) { $this->handleException($e, $record); return true; } } } finally { if ($this->detectCycles) { if (isset($fiber)) { $this->fiberLogDepth[$fiber]--; } else { $this->logDepth--; } } } return null !== $record; } /** * Ends a log cycle and frees all resources used by handlers. * * Closing a Handler means flushing all buffers and freeing any open resources/handles. * Handlers that have been closed should be able to accept log records again and re-open * themselves on demand, but this may not always be possible depending on implementation. * * This is useful at the end of a request and will be called automatically on every handler * when they get destructed. */ public function close(): void { foreach ($this->handlers as $handler) { $handler->close(); } } /** * Ends a log cycle and resets all handlers and processors to their initial state. * * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal * state, and getting it back to a state in which it can receive log records again. * * This is useful in case you want to avoid logs leaking between two requests or jobs when you * have a long running process like a worker or an application server serving multiple requests * in one process. */ public function reset(): void { foreach ($this->handlers as $handler) { if ($handler instanceof ResettableInterface) { $handler->reset(); } } foreach ($this->processors as $processor) { if ($processor instanceof ResettableInterface) { $processor->reset(); } } } /** * Gets all supported logging levels. * * @return array Assoc array with human-readable level names => level codes. * @phpstan-return array */ public static function getLevels(): array { return array_flip(static::$levels); } /** * Gets the name of the logging level. * * @throws \Psr\Log\InvalidArgumentException If level is not defined * * @phpstan-param Level $level * @phpstan-return LevelName */ public static function getLevelName(int $level): string { if (!isset(static::$levels[$level])) { throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels))); } return static::$levels[$level]; } /** * Converts PSR-3 levels to Monolog ones if necessary * * @param string|int $level Level number (monolog) or name (PSR-3) * @throws \Psr\Log\InvalidArgumentException If level is not defined * * @phpstan-param Level|LevelName|LogLevel::* $level * @phpstan-return Level */ public static function toMonologLevel($level): int { if (is_string($level)) { if (is_numeric($level)) { /** @phpstan-ignore-next-line */ return intval($level); } // Contains chars of all log levels and avoids using strtoupper() which may have // strange results depending on locale (for example, "i" will become "İ" in Turkish locale) $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY'); if (defined(__CLASS__.'::'.$upper)) { return constant(__CLASS__ . '::' . $upper); } throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels)); } if (!is_int($level)) { throw new InvalidArgumentException('Level "'.var_export($level, true).'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels)); } return $level; } /** * Checks whether the Logger has a handler that listens on the given level * * @phpstan-param Level $level */ public function isHandling(int $level): bool { $record = [ 'level' => $level, ]; foreach ($this->handlers as $handler) { if ($handler->isHandling($record)) { return true; } } return false; } /** * Set a custom exception handler that will be called if adding a new record fails * * The callable will receive an exception object and the record that failed to be logged */ public function setExceptionHandler(?callable $callback): self { $this->exceptionHandler = $callback; return $this; } public function getExceptionHandler(): ?callable { return $this->exceptionHandler; } /** * Adds a log record at an arbitrary level. * * This method allows for compatibility with common interfaces. * * @param mixed $level The log level (a Monolog, PSR-3 or RFC 5424 level) * @param string|Stringable $message The log message * @param mixed[] $context The log context * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function log($level, $message, array $context = []): void { if (!is_int($level) && !is_string($level)) { throw new \InvalidArgumentException('$level is expected to be a string or int'); } if (isset(self::RFC_5424_LEVELS[$level])) { $level = self::RFC_5424_LEVELS[$level]; } $level = static::toMonologLevel($level); $this->addRecord($level, (string) $message, $context); } /** * Adds a log record at the DEBUG level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function debug($message, array $context = []): void { $this->addRecord(static::DEBUG, (string) $message, $context); } /** * Adds a log record at the INFO level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function info($message, array $context = []): void { $this->addRecord(static::INFO, (string) $message, $context); } /** * Adds a log record at the NOTICE level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function notice($message, array $context = []): void { $this->addRecord(static::NOTICE, (string) $message, $context); } /** * Adds a log record at the WARNING level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function warning($message, array $context = []): void { $this->addRecord(static::WARNING, (string) $message, $context); } /** * Adds a log record at the ERROR level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function error($message, array $context = []): void { $this->addRecord(static::ERROR, (string) $message, $context); } /** * Adds a log record at the CRITICAL level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function critical($message, array $context = []): void { $this->addRecord(static::CRITICAL, (string) $message, $context); } /** * Adds a log record at the ALERT level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function alert($message, array $context = []): void { $this->addRecord(static::ALERT, (string) $message, $context); } /** * Adds a log record at the EMERGENCY level. * * This method allows for compatibility with common interfaces. * * @param string|Stringable $message The log message * @param mixed[] $context The log context */ public function emergency($message, array $context = []): void { $this->addRecord(static::EMERGENCY, (string) $message, $context); } /** * Sets the timezone to be used for the timestamp of log records. */ public function setTimezone(DateTimeZone $tz): self { $this->timezone = $tz; return $this; } /** * Returns the timezone to be used for the timestamp of log records. */ public function getTimezone(): DateTimeZone { return $this->timezone; } /** * Delegates exception management to the custom exception handler, * or throws the exception if no custom handler is set. * * @param array $record * @phpstan-param Record $record */ protected function handleException(Throwable $e, array $record): void { if (!$this->exceptionHandler) { throw $e; } ($this->exceptionHandler)($e, $record); } /** * @return array */ public function __serialize(): array { return [ 'name' => $this->name, 'handlers' => $this->handlers, 'processors' => $this->processors, 'microsecondTimestamps' => $this->microsecondTimestamps, 'timezone' => $this->timezone, 'exceptionHandler' => $this->exceptionHandler, 'logDepth' => $this->logDepth, 'detectCycles' => $this->detectCycles, ]; } /** * @param array $data */ public function __unserialize(array $data): void { foreach (['name', 'handlers', 'processors', 'microsecondTimestamps', 'timezone', 'exceptionHandler', 'logDepth', 'detectCycles'] as $property) { if (isset($data[$property])) { $this->$property = $data[$property]; } } if (\PHP_VERSION_ID >= 80100) { // Local variable for phpstan, see https://github.com/phpstan/phpstan/issues/6732#issuecomment-1111118412 /** @var \WeakMap<\Fiber, int> $fiberLogDepth */ $fiberLogDepth = new \WeakMap(); $this->fiberLogDepth = $fiberLogDepth; } } } PK!T0vendor/monolog/monolog/src/Monolog/LogRecord.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; use ArrayAccess; /** * Monolog log record interface for forward compatibility with Monolog 3.0 * * This is just present in Monolog 2.4+ to allow interoperable code to be written against * both versions by type-hinting arguments as `array|\Monolog\LogRecord $record` * * Do not rely on this interface for other purposes, and do not implement it. * * @author Jordi Boggiano * @template-extends \ArrayAccess<'message'|'level'|'context'|'level_name'|'channel'|'datetime'|'extra'|'formatted', mixed> * @phpstan-import-type Record from Logger */ interface LogRecord extends \ArrayAccess { /** * @phpstan-return Record */ public function toArray(): array; } PK! bt/vendor/monolog/monolog/src/Monolog/Registry.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; use InvalidArgumentException; /** * Monolog log registry * * Allows to get `Logger` instances in the global scope * via static method calls on this class. * * * $application = new Monolog\Logger('application'); * $api = new Monolog\Logger('api'); * * Monolog\Registry::addLogger($application); * Monolog\Registry::addLogger($api); * * function testLogger() * { * Monolog\Registry::api()->error('Sent to $api Logger instance'); * Monolog\Registry::application()->error('Sent to $application Logger instance'); * } * * * @author Tomas Tatarko */ class Registry { /** * List of all loggers in the registry (by named indexes) * * @var Logger[] */ private static $loggers = []; /** * Adds new logging channel to the registry * * @param Logger $logger Instance of the logging channel * @param string|null $name Name of the logging channel ($logger->getName() by default) * @param bool $overwrite Overwrite instance in the registry if the given name already exists? * @throws \InvalidArgumentException If $overwrite set to false and named Logger instance already exists * @return void */ public static function addLogger(Logger $logger, ?string $name = null, bool $overwrite = false) { $name = $name ?: $logger->getName(); if (isset(self::$loggers[$name]) && !$overwrite) { throw new InvalidArgumentException('Logger with the given name already exists'); } self::$loggers[$name] = $logger; } /** * Checks if such logging channel exists by name or instance * * @param string|Logger $logger Name or logger instance */ public static function hasLogger($logger): bool { if ($logger instanceof Logger) { $index = array_search($logger, self::$loggers, true); return false !== $index; } return isset(self::$loggers[$logger]); } /** * Removes instance from registry by name or instance * * @param string|Logger $logger Name or logger instance */ public static function removeLogger($logger): void { if ($logger instanceof Logger) { if (false !== ($idx = array_search($logger, self::$loggers, true))) { unset(self::$loggers[$idx]); } } else { unset(self::$loggers[$logger]); } } /** * Clears the registry */ public static function clear(): void { self::$loggers = []; } /** * Gets Logger instance from the registry * * @param string $name Name of the requested Logger instance * @throws \InvalidArgumentException If named Logger instance is not in the registry */ public static function getInstance($name): Logger { if (!isset(self::$loggers[$name])) { throw new InvalidArgumentException(sprintf('Requested "%s" logger instance is not in the registry', $name)); } return self::$loggers[$name]; } /** * Gets Logger instance from the registry via static method call * * @param string $name Name of the requested Logger instance * @param mixed[] $arguments Arguments passed to static method call * @throws \InvalidArgumentException If named Logger instance is not in the registry * @return Logger Requested instance of Logger */ public static function __callStatic($name, $arguments) { return self::getInstance($name); } } PK!G:vendor/monolog/monolog/src/Monolog/ResettableInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; /** * Handler or Processor implementing this interface will be reset when Logger::reset() is called. * * Resetting ends a log cycle gets them back to their initial state. * * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal * state, and getting it back to a state in which it can receive log records again. * * This is useful in case you want to avoid logs leaking between two requests or jobs when you * have a long running process like a worker or an application server serving multiple requests * in one process. * * @author Grégoire Pineau */ interface ResettableInterface { /** * @return void */ public function reset(); } PK!4vendor/monolog/monolog/src/Monolog/SignalHandler.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use ReflectionExtension; /** * Monolog POSIX signal handler * * @author Robert Gust-Bardon * * @phpstan-import-type Level from \Monolog\Logger * @phpstan-import-type LevelName from \Monolog\Logger */ class SignalHandler { /** @var LoggerInterface */ private $logger; /** @var array SIG_DFL, SIG_IGN or previous callable */ private $previousSignalHandler = []; /** @var array */ private $signalLevelMap = []; /** @var array */ private $signalRestartSyscalls = []; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } /** * @param int|string $level Level or level name * @param bool $callPrevious * @param bool $restartSyscalls * @param bool|null $async * @return $this * * @phpstan-param Level|LevelName|LogLevel::* $level */ public function registerSignalHandler(int $signo, $level = LogLevel::CRITICAL, bool $callPrevious = true, bool $restartSyscalls = true, ?bool $async = true): self { if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { return $this; } $level = Logger::toMonologLevel($level); if ($callPrevious) { $handler = pcntl_signal_get_handler($signo); $this->previousSignalHandler[$signo] = $handler; } else { unset($this->previousSignalHandler[$signo]); } $this->signalLevelMap[$signo] = $level; $this->signalRestartSyscalls[$signo] = $restartSyscalls; if ($async !== null) { pcntl_async_signals($async); } pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); return $this; } /** * @param mixed $siginfo */ public function handleSignal(int $signo, $siginfo = null): void { static $signals = []; if (!$signals && extension_loaded('pcntl')) { $pcntl = new ReflectionExtension('pcntl'); // HHVM 3.24.2 returns an empty array. foreach ($pcntl->getConstants() ?: get_defined_constants(true)['Core'] as $name => $value) { if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { $signals[$value] = $name; } } } $level = $this->signalLevelMap[$signo] ?? LogLevel::CRITICAL; $signal = $signals[$signo] ?? $signo; $context = $siginfo ?? []; $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); if (!isset($this->previousSignalHandler[$signo])) { return; } if ($this->previousSignalHandler[$signo] === SIG_DFL) { if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill') ) { $restartSyscalls = $this->signalRestartSyscalls[$signo] ?? true; pcntl_signal($signo, SIG_DFL, $restartSyscalls); pcntl_sigprocmask(SIG_UNBLOCK, [$signo], $oldset); posix_kill(posix_getpid(), $signo); pcntl_signal_dispatch(); pcntl_sigprocmask(SIG_SETMASK, $oldset); pcntl_signal($signo, [$this, 'handleSignal'], $restartSyscalls); } } elseif (is_callable($this->previousSignalHandler[$signo])) { $this->previousSignalHandler[$signo]($signo, $siginfo); } } } PK!֙;%;%,vendor/monolog/monolog/src/Monolog/Utils.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog; final class Utils { const DEFAULT_JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR; public static function getClass(object $object): string { $class = \get_class($object); if (false === ($pos = \strpos($class, "@anonymous\0"))) { return $class; } if (false === ($parent = \get_parent_class($class))) { return \substr($class, 0, $pos + 10); } return $parent . '@anonymous'; } public static function substr(string $string, int $start, ?int $length = null): string { if (extension_loaded('mbstring')) { return mb_strcut($string, $start, $length); } return substr($string, $start, (null === $length) ? strlen($string) : $length); } /** * Makes sure if a relative path is passed in it is turned into an absolute path * * @param string $streamUrl stream URL or path without protocol */ public static function canonicalizePath(string $streamUrl): string { $prefix = ''; if ('file://' === substr($streamUrl, 0, 7)) { $streamUrl = substr($streamUrl, 7); $prefix = 'file://'; } // other type of stream, not supported if (false !== strpos($streamUrl, '://')) { return $streamUrl; } // already absolute if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') { return $prefix.$streamUrl; } $streamUrl = getcwd() . '/' . $streamUrl; return $prefix.$streamUrl; } /** * Return the JSON representation of a value * * @param mixed $data * @param int $encodeFlags flags to pass to json encode, defaults to DEFAULT_JSON_FLAGS * @param bool $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null * @throws \RuntimeException if encoding fails and errors are not ignored * @return string when errors are ignored and the encoding fails, "null" is returned which is valid json for null */ public static function jsonEncode($data, ?int $encodeFlags = null, bool $ignoreErrors = false): string { if (null === $encodeFlags) { $encodeFlags = self::DEFAULT_JSON_FLAGS; } if ($ignoreErrors) { $json = @json_encode($data, $encodeFlags); if (false === $json) { return 'null'; } return $json; } $json = json_encode($data, $encodeFlags); if (false === $json) { $json = self::handleJsonError(json_last_error(), $data); } return $json; } /** * Handle a json_encode failure. * * If the failure is due to invalid string encoding, try to clean the * input and encode again. If the second encoding attempt fails, the * initial error is not encoding related or the input can't be cleaned then * raise a descriptive exception. * * @param int $code return code of json_last_error function * @param mixed $data data that was meant to be encoded * @param int $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION * @throws \RuntimeException if failure can't be corrected * @return string JSON encoded data after error correction */ public static function handleJsonError(int $code, $data, ?int $encodeFlags = null): string { if ($code !== JSON_ERROR_UTF8) { self::throwEncodeError($code, $data); } if (is_string($data)) { self::detectAndCleanUtf8($data); } elseif (is_array($data)) { array_walk_recursive($data, array('Monolog\Utils', 'detectAndCleanUtf8')); } else { self::throwEncodeError($code, $data); } if (null === $encodeFlags) { $encodeFlags = self::DEFAULT_JSON_FLAGS; } $json = json_encode($data, $encodeFlags); if ($json === false) { self::throwEncodeError(json_last_error(), $data); } return $json; } /** * @internal */ public static function pcreLastErrorMessage(int $code): string { if (PHP_VERSION_ID >= 80000) { return preg_last_error_msg(); } $constants = (get_defined_constants(true))['pcre']; $constants = array_filter($constants, function ($key) { return substr($key, -6) == '_ERROR'; }, ARRAY_FILTER_USE_KEY); $constants = array_flip($constants); return $constants[$code] ?? 'UNDEFINED_ERROR'; } /** * Throws an exception according to a given code with a customized message * * @param int $code return code of json_last_error function * @param mixed $data data that was meant to be encoded * @throws \RuntimeException * * @return never */ private static function throwEncodeError(int $code, $data): void { switch ($code) { case JSON_ERROR_DEPTH: $msg = 'Maximum stack depth exceeded'; break; case JSON_ERROR_STATE_MISMATCH: $msg = 'Underflow or the modes mismatch'; break; case JSON_ERROR_CTRL_CHAR: $msg = 'Unexpected control character found'; break; case JSON_ERROR_UTF8: $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; break; default: $msg = 'Unknown error'; } throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true)); } /** * Detect invalid UTF-8 string characters and convert to valid UTF-8. * * Valid UTF-8 input will be left unmodified, but strings containing * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed * original encoding of ISO-8859-15. This conversion may result in * incorrect output if the actual encoding was not ISO-8859-15, but it * will be clean UTF-8 output and will not rely on expensive and fragile * detection algorithms. * * Function converts the input in place in the passed variable so that it * can be used as a callback for array_walk_recursive. * * @param mixed $data Input to check and convert if needed, passed by ref */ private static function detectAndCleanUtf8(&$data): void { if (is_string($data) && !preg_match('//u', $data)) { $data = preg_replace_callback( '/[\x80-\xFF]+/', function ($m) { return function_exists('mb_convert_encoding') ? mb_convert_encoding($m[0], 'UTF-8', 'ISO-8859-1') : utf8_encode($m[0]); }, $data ); if (!is_string($data)) { $pcreErrorCode = preg_last_error(); throw new \RuntimeException('Failed to preg_replace_callback: ' . $pcreErrorCode . ' / ' . self::pcreLastErrorMessage($pcreErrorCode)); } $data = str_replace( ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'], ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'], $data ); } } /** * Converts a string with a valid 'memory_limit' format, to bytes. * * @param string|false $val * @return int|false Returns an integer representing bytes. Returns FALSE in case of error. */ public static function expandIniShorthandBytes($val) { if (!is_string($val)) { return false; } // support -1 if ((int) $val < 0) { return (int) $val; } if (!preg_match('/^\s*(?\d+)(?:\.\d+)?\s*(?[gmk]?)\s*$/i', $val, $match)) { return false; } $val = (int) $match['val']; switch (strtolower($match['unit'])) { case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } /** * @param array $record */ public static function getRecordMessageForException(array $record): string { $context = ''; $extra = ''; try { if ($record['context']) { $context = "\nContext: " . json_encode($record['context']); } if ($record['extra']) { $extra = "\nExtra: " . json_encode($record['extra']); } } catch (\Throwable $e) { // noop } return "\nThe exception occurred while attempting to log: " . $record['message'] . $context . $extra; } } PK!*"Ҭ44#vendor/monolog/monolog/CHANGELOG.mdnu[### 2.10.0 (2024-11-12) * Added `$fileOpenMode` to `StreamHandler` to define a custom fopen mode to open the log file (#1913) * Fixed `StreamHandler` handling of write failures so that it now closes/reopens the stream and retries the write once before failing (#1882) * Fixed `StreamHandler` error handler causing issues if a stream handler triggers an error (#1866) * Fixed `JsonFormatter` handling of incomplete classes (#1834) * Fixed `RotatingFileHandler` bug where rotation could sometimes not happen correctly (#1905) ### 2.9.3 (2024-04-12) * Fixed PHP 8.4 deprecation warnings (#1874) ### 2.9.2 (2023-10-27) * Fixed display_errors parsing in ErrorHandler which did not support string values (#1804) * Fixed bug where the previous error handler would not be restored in some cases where StreamHandler fails (#1815) * Fixed normalization error when normalizing incomplete classes (#1833) ### 2.9.1 (2023-02-06) * Fixed Logger not being serializable anymore (#1792) ### 2.9.0 (2023-02-05) * Deprecated FlowdockHandler & Formatter as the flowdock service was shutdown (#1748) * Added support for enum context values in PsrLogMessageProcessor (#1773) * Added graylog2/gelf-php 2.x support (#1747) * Improved `BrowserConsoleHandler` logging to use more appropriate methods than just console.log in the browser (#1739) * Fixed `WhatFailureGroupHandler` not catching errors happening inside `close()` (#1791) * Fixed datetime field in `GoogleCloudLoggingFormatter` (#1758) * Fixed infinite loop detection within Fibers (#1753) * Fixed `AmqpHandler->setExtraAttributes` not working with buffering handler wrappers (#1781) ### 2.8.0 (2022-07-24) * Deprecated `CubeHandler` and `PHPConsoleHandler` as both projects are abandoned and those should not be used anymore (#1734) * Added RFC 5424 level (`7` to `0`) support to `Logger::log` and `Logger::addRecord` to increase interoperability (#1723) * Added support for `__toString` for objects which are not json serializable in `JsonFormatter` (#1733) * Added `GoogleCloudLoggingFormatter` (#1719) * Added support for Predis 2.x (#1732) * Added `AmqpHandler->setExtraAttributes` to allow configuring attributes when using an AMQPExchange (#1724) * Fixed serialization/unserialization of handlers to make sure private properties are included (#1727) * Fixed allowInlineLineBreaks in LineFormatter causing issues with windows paths containing `\n` or `\r` sequences (#1720) * Fixed max normalization depth not being taken into account when formatting exceptions with a deep chain of previous exceptions (#1726) * Fixed PHP 8.2 deprecation warnings (#1722) * Fixed rare race condition or filesystem issue where StreamHandler is unable to create the directory the log should go into yet it exists already (#1678) ### 2.7.0 (2022-06-09) * Added `$datetime` parameter to `Logger::addRecord` as low level API to allow logging into the past or future (#1682) * Added `Logger::useLoggingLoopDetection` to allow disabling cyclic logging detection in concurrent frameworks (#1681) * Fixed handling of fatal errors if callPrevious is disabled in ErrorHandler (#1670) * Marked the reusable `Monolog\Test\TestCase` class as `@internal` to make sure PHPStorm does not show it above PHPUnit, you may still use it to test your own handlers/etc though (#1677) * Fixed RotatingFileHandler issue when the date format contained slashes (#1671) ### 2.6.0 (2022-05-10) * Deprecated `SwiftMailerHandler`, use `SymfonyMailerHandler` instead * Added `SymfonyMailerHandler` (#1663) * Added ElasticSearch 8.x support to the ElasticsearchHandler (#1662) * Added a way to filter/modify stack traces in LineFormatter (#1665) * Fixed UdpSocket not being able to reopen/reconnect after close() * Fixed infinite loops if a Handler is triggering logging while handling log records ### 2.5.0 (2022-04-08) * Added `callType` to IntrospectionProcessor (#1612) * Fixed AsMonologProcessor syntax to be compatible with PHP 7.2 (#1651) ### 2.4.0 (2022-03-14) * Added [`Monolog\LogRecord`](src/Monolog/LogRecord.php) interface that can be used to type-hint records like `array|\Monolog\LogRecord $record` to be forward compatible with the upcoming Monolog 3 changes * Added `includeStacktraces` constructor params to LineFormatter & JsonFormatter (#1603) * Added `persistent`, `timeout`, `writingTimeout`, `connectionTimeout`, `chunkSize` constructor params to SocketHandler and derivatives (#1600) * Added `AsMonologProcessor` PHP attribute which can help autowiring / autoconfiguration of processors if frameworks / integrations decide to make use of it. This is useless when used purely with Monolog (#1637) * Added support for keeping native BSON types as is in MongoDBFormatter (#1620) * Added support for a `user_agent` key in WebProcessor, disabled by default but you can use it by configuring the $extraFields you want (#1613) * Added support for username/userIcon in SlackWebhookHandler (#1617) * Added extension points to BrowserConsoleHandler (#1593) * Added record message/context/extra info to exceptions thrown when a StreamHandler cannot open its stream to avoid completely losing the data logged (#1630) * Fixed error handler signature to accept a null $context which happens with internal PHP errors (#1614) * Fixed a few setter methods not returning `self` (#1609) * Fixed handling of records going over the max Telegram message length (#1616) ### 2.3.5 (2021-10-01) * Fixed regression in StreamHandler since 2.3.3 on systems with the memory_limit set to >=20GB (#1592) ### 2.3.4 (2021-09-15) * Fixed support for psr/log 3.x (#1589) ### 2.3.3 (2021-09-14) * Fixed memory usage when using StreamHandler and calling stream_get_contents on the resource you passed to it (#1578, #1577) * Fixed support for psr/log 2.x (#1587) * Fixed some type annotations ### 2.3.2 (2021-07-23) * Fixed compatibility with PHP 7.2 - 7.4 when experiencing PCRE errors (#1568) ### 2.3.1 (2021-07-14) * Fixed Utils::getClass handling of anonymous classes not being fully compatible with PHP 8 (#1563) * Fixed some `@inheritDoc` annotations having the wrong case ### 2.3.0 (2021-07-05) * Added a ton of PHPStan type annotations as well as type aliases on Monolog\Logger for Record, Level and LevelName that you can import (#1557) * Added ability to customize date format when using JsonFormatter (#1561) * Fixed FilterHandler not calling reset on its internal handler when reset() is called on it (#1531) * Fixed SyslogUdpHandler not setting the timezone correctly on DateTimeImmutable instances (#1540) * Fixed StreamHandler thread safety - chunk size set to 2GB now to avoid interlacing when doing concurrent writes (#1553) ### 2.2.0 (2020-12-14) * Added JSON_PARTIAL_OUTPUT_ON_ERROR to default json encoding flags, to avoid dropping entire context data or even records due to an invalid subset of it somewhere * Added setDateFormat to NormalizerFormatter (and Line/Json formatters by extension) to allow changing this after object creation * Added RedisPubSubHandler to log records to a Redis channel using PUBLISH * Added support for Elastica 7, and deprecated the $type argument of ElasticaFormatter which is not in use anymore as of Elastica 7 * Added support for millisecond write timeouts in SocketHandler, you can now pass floats to setWritingTimeout, e.g. 0.2 is 200ms * Added support for unix sockets in SyslogUdpHandler (set $port to 0 to make the $host a unix socket) * Added handleBatch support for TelegramBotHandler * Added RFC5424e extended date format including milliseconds to SyslogUdpHandler * Added support for configuring handlers with numeric level values in strings (coming from e.g. env vars) * Fixed Wildfire/FirePHP/ChromePHP handling of unicode characters * Fixed PHP 8 issues in SyslogUdpHandler * Fixed internal type error when mbstring is missing ### 2.1.1 (2020-07-23) * Fixed removing of json encoding options * Fixed type hint of $level not accepting strings in SendGridHandler and OverflowHandler * Fixed SwiftMailerHandler not accepting email templates with an empty subject * Fixed array access on null in RavenHandler * Fixed unique_id in WebProcessor not being disableable ### 2.1.0 (2020-05-22) * Added `JSON_INVALID_UTF8_SUBSTITUTE` to default json flags, so that invalid UTF8 characters now get converted to [�](https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character) instead of being converted from ISO-8859-15 to UTF8 as it was before, which was hardly a comprehensive solution * Added `$ignoreEmptyContextAndExtra` option to JsonFormatter to skip empty context/extra entirely from the output * Added `$parseMode`, `$disableWebPagePreview` and `$disableNotification` options to TelegramBotHandler * Added tentative support for PHP 8 * NormalizerFormatter::addJsonEncodeOption and removeJsonEncodeOption are now public to allow modifying default json flags * Fixed GitProcessor type error when there is no git repo present * Fixed normalization of SoapFault objects containing deeply nested objects as "detail" * Fixed support for relative paths in RotatingFileHandler ### 2.0.2 (2019-12-20) * Fixed ElasticsearchHandler swallowing exceptions details when failing to index log records * Fixed normalization of SoapFault objects containing non-strings as "detail" in LineFormatter * Fixed formatting of resources in JsonFormatter * Fixed RedisHandler failing to use MULTI properly when passed a proxied Redis instance (e.g. in Symfony with lazy services) * Fixed FilterHandler triggering a notice when handleBatch was filtering all records passed to it * Fixed Turkish locale messing up the conversion of level names to their constant values ### 2.0.1 (2019-11-13) * Fixed normalization of Traversables to avoid traversing them as not all of them are rewindable * Fixed setFormatter/getFormatter to forward to the nested handler in FilterHandler, FingersCrossedHandler, BufferHandler, OverflowHandler and SamplingHandler * Fixed BrowserConsoleHandler formatting when using multiple styles * Fixed normalization of exception codes to be always integers even for PDOException which have them as numeric strings * Fixed normalization of SoapFault objects containing non-strings as "detail" * Fixed json encoding across all handlers to always attempt recovery of non-UTF-8 strings instead of failing the whole encoding * Fixed ChromePHPHandler to avoid sending more data than latest Chrome versions allow in headers (4KB down from 256KB). * Fixed type error in BrowserConsoleHandler when the context array of log records was not associative. ### 2.0.0 (2019-08-30) * BC Break: This is a major release, see [UPGRADE.md](UPGRADE.md) for details if you are coming from a 1.x release * BC Break: Logger methods log/debug/info/notice/warning/error/critical/alert/emergency now have explicit void return types * Added FallbackGroupHandler which works like the WhatFailureGroupHandler but stops dispatching log records as soon as one handler accepted it * Fixed support for UTF-8 when cutting strings to avoid cutting a multibyte-character in half * Fixed normalizers handling of exception backtraces to avoid serializing arguments in some cases * Fixed date timezone handling in SyslogUdpHandler ### 2.0.0-beta2 (2019-07-06) * BC Break: This is a major release, see [UPGRADE.md](UPGRADE.md) for details if you are coming from a 1.x release * BC Break: PHP 7.2 is now the minimum required PHP version. * BC Break: Removed SlackbotHandler, RavenHandler and HipChatHandler, see [UPGRADE.md](UPGRADE.md) for details * Added OverflowHandler which will only flush log records to its nested handler when reaching a certain amount of logs (i.e. only pass through when things go really bad) * Added TelegramBotHandler to log records to a [Telegram](https://core.telegram.org/bots/api) bot account * Added support for JsonSerializable when normalizing exceptions * Added support for RFC3164 (outdated BSD syslog protocol) to SyslogUdpHandler * Added SoapFault details to formatted exceptions * Fixed DeduplicationHandler silently failing to start when file could not be opened * Fixed issue in GroupHandler and WhatFailureGroupHandler where setting multiple processors would duplicate records * Fixed GelfFormatter losing some data when one attachment was too long * Fixed issue in SignalHandler restarting syscalls functionality * Improved performance of LogglyHandler when sending multiple logs in a single request ### 2.0.0-beta1 (2018-12-08) * BC Break: This is a major release, see [UPGRADE.md](UPGRADE.md) for details if you are coming from a 1.x release * BC Break: PHP 7.1 is now the minimum required PHP version. * BC Break: Quite a few interface changes, only relevant if you implemented your own handlers/processors/formatters * BC Break: Removed non-PSR-3 methods to add records, all the `add*` (e.g. `addWarning`) methods as well as `emerg`, `crit`, `err` and `warn` * BC Break: The record timezone is now set per Logger instance and not statically anymore * BC Break: There is no more default handler configured on empty Logger instances * BC Break: ElasticSearchHandler renamed to ElasticaHandler * BC Break: Various handler-specific breaks, see [UPGRADE.md](UPGRADE.md) for details * Added scalar type hints and return hints in all the places it was possible. Switched strict_types on for more reliability. * Added DateTimeImmutable support, all record datetime are now immutable, and will toString/json serialize with the correct date format, including microseconds (unless disabled) * Added timezone and microseconds to the default date format * Added SendGridHandler to use the SendGrid API to send emails * Added LogmaticHandler to use the Logmatic.io API to store log records * Added SqsHandler to send log records to an AWS SQS queue * Added ElasticsearchHandler to send records via the official ES library. Elastica users should now use ElasticaHandler instead of ElasticSearchHandler * Added NoopHandler which is similar to the NullHandle but does not prevent the bubbling of log records to handlers further down the configuration, useful for temporarily disabling a handler in configuration files * Added ProcessHandler to write log output to the STDIN of a given process * Added HostnameProcessor that adds the machine's hostname to log records * Added a `$dateFormat` option to the PsrLogMessageProcessor which lets you format DateTime instances nicely * Added support for the PHP 7.x `mongodb` extension in the MongoDBHandler * Fixed many minor issues in various handlers, and probably added a few regressions too ### 1.26.1 (2021-05-28) * Fixed PHP 8.1 deprecation warning ### 1.26.0 (2020-12-14) * Added $dateFormat and $removeUsedContextFields arguments to PsrLogMessageProcessor (backport from 2.x) ### 1.25.5 (2020-07-23) * Fixed array access on null in RavenHandler * Fixed unique_id in WebProcessor not being disableable ### 1.25.4 (2020-05-22) * Fixed GitProcessor type error when there is no git repo present * Fixed normalization of SoapFault objects containing deeply nested objects as "detail" * Fixed support for relative paths in RotatingFileHandler ### 1.25.3 (2019-12-20) * Fixed formatting of resources in JsonFormatter * Fixed RedisHandler failing to use MULTI properly when passed a proxied Redis instance (e.g. in Symfony with lazy services) * Fixed FilterHandler triggering a notice when handleBatch was filtering all records passed to it * Fixed Turkish locale messing up the conversion of level names to their constant values ### 1.25.2 (2019-11-13) * Fixed normalization of Traversables to avoid traversing them as not all of them are rewindable * Fixed setFormatter/getFormatter to forward to the nested handler in FilterHandler, FingersCrossedHandler, BufferHandler and SamplingHandler * Fixed BrowserConsoleHandler formatting when using multiple styles * Fixed normalization of exception codes to be always integers even for PDOException which have them as numeric strings * Fixed normalization of SoapFault objects containing non-strings as "detail" * Fixed json encoding across all handlers to always attempt recovery of non-UTF-8 strings instead of failing the whole encoding ### 1.25.1 (2019-09-06) * Fixed forward-compatible interfaces to be compatible with Monolog 1.x too. ### 1.25.0 (2019-09-06) * Deprecated SlackbotHandler, use SlackWebhookHandler or SlackHandler instead * Deprecated RavenHandler, use sentry/sentry 2.x and their Sentry\Monolog\Handler instead * Deprecated HipChatHandler, migrate to Slack and use SlackWebhookHandler or SlackHandler instead * Added forward-compatible interfaces and traits FormattableHandlerInterface, FormattableHandlerTrait, ProcessableHandlerInterface, ProcessableHandlerTrait. If you use modern PHP and want to make code compatible with Monolog 1 and 2 this can help. You will have to require at least Monolog 1.25 though. * Added support for RFC3164 (outdated BSD syslog protocol) to SyslogUdpHandler * Fixed issue in GroupHandler and WhatFailureGroupHandler where setting multiple processors would duplicate records * Fixed issue in SignalHandler restarting syscalls functionality * Fixed normalizers handling of exception backtraces to avoid serializing arguments in some cases * Fixed ZendMonitorHandler to work with the latest Zend Server versions * Fixed ChromePHPHandler to avoid sending more data than latest Chrome versions allow in headers (4KB down from 256KB). ### 1.24.0 (2018-11-05) * BC Notice: If you are extending any of the Monolog's Formatters' `normalize` method, make sure you add the new `$depth = 0` argument to your function signature to avoid strict PHP warnings. * Added a `ResettableInterface` in order to reset/reset/clear/flush handlers and processors * Added a `ProcessorInterface` as an optional way to label a class as being a processor (mostly useful for autowiring dependency containers) * Added a way to log signals being received using Monolog\SignalHandler * Added ability to customize error handling at the Logger level using Logger::setExceptionHandler * Added InsightOpsHandler to migrate users of the LogEntriesHandler * Added protection to NormalizerFormatter against circular and very deep structures, it now stops normalizing at a depth of 9 * Added capture of stack traces to ErrorHandler when logging PHP errors * Added RavenHandler support for a `contexts` context or extra key to forward that to Sentry's contexts * Added forwarding of context info to FluentdFormatter * Added SocketHandler::setChunkSize to override the default chunk size in case you must send large log lines to rsyslog for example * Added ability to extend/override BrowserConsoleHandler * Added SlackWebhookHandler::getWebhookUrl and SlackHandler::getToken to enable class extensibility * Added SwiftMailerHandler::getSubjectFormatter to enable class extensibility * Dropped official support for HHVM in test builds * Fixed normalization of exception traces when call_user_func is used to avoid serializing objects and the data they contain * Fixed naming of fields in Slack handler, all field names are now capitalized in all cases * Fixed HipChatHandler bug where slack dropped messages randomly * Fixed normalization of objects in Slack handlers * Fixed support for PHP7's Throwable in NewRelicHandler * Fixed race bug when StreamHandler sometimes incorrectly reported it failed to create a directory * Fixed table row styling issues in HtmlFormatter * Fixed RavenHandler dropping the message when logging exception * Fixed WhatFailureGroupHandler skipping processors when using handleBatch and implement it where possible * Fixed display of anonymous class names ### 1.23.0 (2017-06-19) * Improved SyslogUdpHandler's support for RFC5424 and added optional `$ident` argument * Fixed GelfHandler truncation to be per field and not per message * Fixed compatibility issue with PHP <5.3.6 * Fixed support for headless Chrome in ChromePHPHandler * Fixed support for latest Aws SDK in DynamoDbHandler * Fixed support for SwiftMailer 6.0+ in SwiftMailerHandler ### 1.22.1 (2017-03-13) * Fixed lots of minor issues in the new Slack integrations * Fixed support for allowInlineLineBreaks in LineFormatter when formatting exception backtraces ### 1.22.0 (2016-11-26) * Added SlackbotHandler and SlackWebhookHandler to set up Slack integration more easily * Added MercurialProcessor to add mercurial revision and branch names to log records * Added support for AWS SDK v3 in DynamoDbHandler * Fixed fatal errors occurring when normalizing generators that have been fully consumed * Fixed RollbarHandler to include a level (rollbar level), monolog_level (original name), channel and datetime (unix) * Fixed RollbarHandler not flushing records automatically, calling close() explicitly is not necessary anymore * Fixed SyslogUdpHandler to avoid sending empty frames * Fixed a few PHP 7.0 and 7.1 compatibility issues ### 1.21.0 (2016-07-29) * Break: Reverted the addition of $context when the ErrorHandler handles regular php errors from 1.20.0 as it was causing issues * Added support for more formats in RotatingFileHandler::setFilenameFormat as long as they have Y, m and d in order * Added ability to format the main line of text the SlackHandler sends by explicitly setting a formatter on the handler * Added information about SoapFault instances in NormalizerFormatter * Added $handleOnlyReportedErrors option on ErrorHandler::registerErrorHandler (default true) to allow logging of all errors no matter the error_reporting level ### 1.20.0 (2016-07-02) * Added FingersCrossedHandler::activate() to manually trigger the handler regardless of the activation policy * Added StreamHandler::getUrl to retrieve the stream's URL * Added ability to override addRow/addTitle in HtmlFormatter * Added the $context to context information when the ErrorHandler handles a regular php error * Deprecated RotatingFileHandler::setFilenameFormat to only support 3 formats: Y, Y-m and Y-m-d * Fixed WhatFailureGroupHandler to work with PHP7 throwables * Fixed a few minor bugs ### 1.19.0 (2016-04-12) * Break: StreamHandler will not close streams automatically that it does not own. If you pass in a stream (not a path/url), then it will not close it for you. You can retrieve those using getStream() if needed * Added DeduplicationHandler to remove duplicate records from notifications across multiple requests, useful for email or other notifications on errors * Added ability to use `%message%` and other LineFormatter replacements in the subject line of emails sent with NativeMailHandler and SwiftMailerHandler * Fixed HipChatHandler handling of long messages ### 1.18.2 (2016-04-02) * Fixed ElasticaFormatter to use more precise dates * Fixed GelfMessageFormatter sending too long messages ### 1.18.1 (2016-03-13) * Fixed SlackHandler bug where slack dropped messages randomly * Fixed RedisHandler issue when using with the PHPRedis extension * Fixed AmqpHandler content-type being incorrectly set when using with the AMQP extension * Fixed BrowserConsoleHandler regression ### 1.18.0 (2016-03-01) * Added optional reduction of timestamp precision via `Logger->useMicrosecondTimestamps(false)`, disabling it gets you a bit of performance boost but reduces the precision to the second instead of microsecond * Added possibility to skip some extra stack frames in IntrospectionProcessor if you have some library wrapping Monolog that is always adding frames * Added `Logger->withName` to clone a logger (keeping all handlers) with a new name * Added FluentdFormatter for the Fluentd unix socket protocol * Added HandlerWrapper base class to ease the creation of handler wrappers, just extend it and override as needed * Added support for replacing context sub-keys using `%context.*%` in LineFormatter * Added support for `payload` context value in RollbarHandler * Added setRelease to RavenHandler to describe the application version, sent with every log * Added support for `fingerprint` context value in RavenHandler * Fixed JSON encoding errors that would gobble up the whole log record, we now handle those more gracefully by dropping chars as needed * Fixed write timeouts in SocketHandler and derivatives, set to 10sec by default, lower it with `setWritingTimeout()` * Fixed PHP7 compatibility with regard to Exception/Throwable handling in a few places ### 1.17.2 (2015-10-14) * Fixed ErrorHandler compatibility with non-Monolog PSR-3 loggers * Fixed SlackHandler handling to use slack functionalities better * Fixed SwiftMailerHandler bug when sending multiple emails they all had the same id * Fixed 5.3 compatibility regression ### 1.17.1 (2015-08-31) * Fixed RollbarHandler triggering PHP notices ### 1.17.0 (2015-08-30) * Added support for `checksum` and `release` context/extra values in RavenHandler * Added better support for exceptions in RollbarHandler * Added UidProcessor::getUid * Added support for showing the resource type in NormalizedFormatter * Fixed IntrospectionProcessor triggering PHP notices ### 1.16.0 (2015-08-09) * Added IFTTTHandler to notify ifttt.com triggers * Added Logger::setHandlers() to allow setting/replacing all handlers * Added $capSize in RedisHandler to cap the log size * Fixed StreamHandler creation of directory to only trigger when the first log write happens * Fixed bug in the handling of curl failures * Fixed duplicate logging of fatal errors when both error and fatal error handlers are registered in monolog's ErrorHandler * Fixed missing fatal errors records with handlers that need to be closed to flush log records * Fixed TagProcessor::addTags support for associative arrays ### 1.15.0 (2015-07-12) * Added addTags and setTags methods to change a TagProcessor * Added automatic creation of directories if they are missing for a StreamHandler to open a log file * Added retry functionality to Loggly, Cube and Mandrill handlers so they retry up to 5 times in case of network failure * Fixed process exit code being incorrectly reset to 0 if ErrorHandler::registerExceptionHandler was used * Fixed HTML/JS escaping in BrowserConsoleHandler * Fixed JSON encoding errors being silently suppressed (PHP 5.5+ only) ### 1.14.0 (2015-06-19) * Added PHPConsoleHandler to send record to Chrome's PHP Console extension and library * Added support for objects implementing __toString in the NormalizerFormatter * Added support for HipChat's v2 API in HipChatHandler * Added Logger::setTimezone() to initialize the timezone monolog should use in case date.timezone isn't correct for your app * Added an option to send formatted message instead of the raw record on PushoverHandler via ->useFormattedMessage(true) * Fixed curl errors being silently suppressed ### 1.13.1 (2015-03-09) * Fixed regression in HipChat requiring a new token to be created ### 1.13.0 (2015-03-05) * Added Registry::hasLogger to check for the presence of a logger instance * Added context.user support to RavenHandler * Added HipChat API v2 support in the HipChatHandler * Added NativeMailerHandler::addParameter to pass params to the mail() process * Added context data to SlackHandler when $includeContextAndExtra is true * Added ability to customize the Swift_Message per-email in SwiftMailerHandler * Fixed SwiftMailerHandler to lazily create message instances if a callback is provided * Fixed serialization of INF and NaN values in Normalizer and LineFormatter ### 1.12.0 (2014-12-29) * Break: HandlerInterface::isHandling now receives a partial record containing only a level key. This was always the intent and does not break any Monolog handler but is strictly speaking a BC break and you should check if you relied on any other field in your own handlers. * Added PsrHandler to forward records to another PSR-3 logger * Added SamplingHandler to wrap around a handler and include only every Nth record * Added MongoDBFormatter to support better storage with MongoDBHandler (it must be enabled manually for now) * Added exception codes in the output of most formatters * Added LineFormatter::includeStacktraces to enable exception stack traces in logs (uses more than one line) * Added $useShortAttachment to SlackHandler to minify attachment size and $includeExtra to append extra data * Added $host to HipChatHandler for users of private instances * Added $transactionName to NewRelicHandler and support for a transaction_name context value * Fixed MandrillHandler to avoid outputting API call responses * Fixed some non-standard behaviors in SyslogUdpHandler ### 1.11.0 (2014-09-30) * Break: The NewRelicHandler extra and context data are now prefixed with extra_ and context_ to avoid clashes. Watch out if you have scripts reading those from the API and rely on names * Added WhatFailureGroupHandler to suppress any exception coming from the wrapped handlers and avoid chain failures if a logging service fails * Added MandrillHandler to send emails via the Mandrillapp.com API * Added SlackHandler to log records to a Slack.com account * Added FleepHookHandler to log records to a Fleep.io account * Added LogglyHandler::addTag to allow adding tags to an existing handler * Added $ignoreEmptyContextAndExtra to LineFormatter to avoid empty [] at the end * Added $useLocking to StreamHandler and RotatingFileHandler to enable flock() while writing * Added support for PhpAmqpLib in the AmqpHandler * Added FingersCrossedHandler::clear and BufferHandler::clear to reset them between batches in long running jobs * Added support for adding extra fields from $_SERVER in the WebProcessor * Fixed support for non-string values in PrsLogMessageProcessor * Fixed SwiftMailer messages being sent with the wrong date in long running scripts * Fixed minor PHP 5.6 compatibility issues * Fixed BufferHandler::close being called twice ### 1.10.0 (2014-06-04) * Added Logger::getHandlers() and Logger::getProcessors() methods * Added $passthruLevel argument to FingersCrossedHandler to let it always pass some records through even if the trigger level is not reached * Added support for extra data in NewRelicHandler * Added $expandNewlines flag to the ErrorLogHandler to create multiple log entries when a message has multiple lines ### 1.9.1 (2014-04-24) * Fixed regression in RotatingFileHandler file permissions * Fixed initialization of the BufferHandler to make sure it gets flushed after receiving records * Fixed ChromePHPHandler and FirePHPHandler's activation strategies to be more conservative ### 1.9.0 (2014-04-20) * Added LogEntriesHandler to send logs to a LogEntries account * Added $filePermissions to tweak file mode on StreamHandler and RotatingFileHandler * Added $useFormatting flag to MemoryProcessor to make it send raw data in bytes * Added support for table formatting in FirePHPHandler via the table context key * Added a TagProcessor to add tags to records, and support for tags in RavenHandler * Added $appendNewline flag to the JsonFormatter to enable using it when logging to files * Added sound support to the PushoverHandler * Fixed multi-threading support in StreamHandler * Fixed empty headers issue when ChromePHPHandler received no records * Fixed default format of the ErrorLogHandler ### 1.8.0 (2014-03-23) * Break: the LineFormatter now strips newlines by default because this was a bug, set $allowInlineLineBreaks to true if you need them * Added BrowserConsoleHandler to send logs to any browser's console via console.log() injection in the output * Added FilterHandler to filter records and only allow those of a given list of levels through to the wrapped handler * Added FlowdockHandler to send logs to a Flowdock account * Added RollbarHandler to send logs to a Rollbar account * Added HtmlFormatter to send prettier log emails with colors for each log level * Added GitProcessor to add the current branch/commit to extra record data * Added a Monolog\Registry class to allow easier global access to pre-configured loggers * Added support for the new official graylog2/gelf-php lib for GelfHandler, upgrade if you can by replacing the mlehner/gelf-php requirement * Added support for HHVM * Added support for Loggly batch uploads * Added support for tweaking the content type and encoding in NativeMailerHandler * Added $skipClassesPartials to tweak the ignored classes in the IntrospectionProcessor * Fixed batch request support in GelfHandler ### 1.7.0 (2013-11-14) * Added ElasticSearchHandler to send logs to an Elastic Search server * Added DynamoDbHandler and ScalarFormatter to send logs to Amazon's Dynamo DB * Added SyslogUdpHandler to send logs to a remote syslogd server * Added LogglyHandler to send logs to a Loggly account * Added $level to IntrospectionProcessor so it only adds backtraces when needed * Added $version to LogstashFormatter to allow using the new v1 Logstash format * Added $appName to NewRelicHandler * Added configuration of Pushover notification retries/expiry * Added $maxColumnWidth to NativeMailerHandler to change the 70 chars default * Added chainability to most setters for all handlers * Fixed RavenHandler batch processing so it takes the message from the record with highest priority * Fixed HipChatHandler batch processing so it sends all messages at once * Fixed issues with eAccelerator * Fixed and improved many small things ### 1.6.0 (2013-07-29) * Added HipChatHandler to send logs to a HipChat chat room * Added ErrorLogHandler to send logs to PHP's error_log function * Added NewRelicHandler to send logs to NewRelic's service * Added Monolog\ErrorHandler helper class to register a Logger as exception/error/fatal handler * Added ChannelLevelActivationStrategy for the FingersCrossedHandler to customize levels by channel * Added stack traces output when normalizing exceptions (json output & co) * Added Monolog\Logger::API constant (currently 1) * Added support for ChromePHP's v4.0 extension * Added support for message priorities in PushoverHandler, see $highPriorityLevel and $emergencyLevel * Added support for sending messages to multiple users at once with the PushoverHandler * Fixed RavenHandler's support for batch sending of messages (when behind a Buffer or FingersCrossedHandler) * Fixed normalization of Traversables with very large data sets, only the first 1000 items are shown now * Fixed issue in RotatingFileHandler when an open_basedir restriction is active * Fixed minor issues in RavenHandler and bumped the API to Raven 0.5.0 * Fixed SyslogHandler issue when many were used concurrently with different facilities ### 1.5.0 (2013-04-23) * Added ProcessIdProcessor to inject the PID in log records * Added UidProcessor to inject a unique identifier to all log records of one request/run * Added support for previous exceptions in the LineFormatter exception serialization * Added Monolog\Logger::getLevels() to get all available levels * Fixed ChromePHPHandler so it avoids sending headers larger than Chrome can handle ### 1.4.1 (2013-04-01) * Fixed exception formatting in the LineFormatter to be more minimalistic * Fixed RavenHandler's handling of context/extra data, requires Raven client >0.1.0 * Fixed log rotation in RotatingFileHandler to work with long running scripts spanning multiple days * Fixed WebProcessor array access so it checks for data presence * Fixed Buffer, Group and FingersCrossed handlers to make use of their processors ### 1.4.0 (2013-02-13) * Added RedisHandler to log to Redis via the Predis library or the phpredis extension * Added ZendMonitorHandler to log to the Zend Server monitor * Added the possibility to pass arrays of handlers and processors directly in the Logger constructor * Added `$useSSL` option to the PushoverHandler which is enabled by default * Fixed ChromePHPHandler and FirePHPHandler issue when multiple instances are used simultaneously * Fixed header injection capability in the NativeMailHandler ### 1.3.1 (2013-01-11) * Fixed LogstashFormatter to be usable with stream handlers * Fixed GelfMessageFormatter levels on Windows ### 1.3.0 (2013-01-08) * Added PSR-3 compliance, the `Monolog\Logger` class is now an instance of `Psr\Log\LoggerInterface` * Added PsrLogMessageProcessor that you can selectively enable for full PSR-3 compliance * Added LogstashFormatter (combine with SocketHandler or StreamHandler to send logs to Logstash) * Added PushoverHandler to send mobile notifications * Added CouchDBHandler and DoctrineCouchDBHandler * Added RavenHandler to send data to Sentry servers * Added support for the new MongoClient class in MongoDBHandler * Added microsecond precision to log records' timestamps * Added `$flushOnOverflow` param to BufferHandler to flush by batches instead of losing the oldest entries * Fixed normalization of objects with cyclic references ### 1.2.1 (2012-08-29) * Added new $logopts arg to SyslogHandler to provide custom openlog options * Fixed fatal error in SyslogHandler ### 1.2.0 (2012-08-18) * Added AmqpHandler (for use with AMQP servers) * Added CubeHandler * Added NativeMailerHandler::addHeader() to send custom headers in mails * Added the possibility to specify more than one recipient in NativeMailerHandler * Added the possibility to specify float timeouts in SocketHandler * Added NOTICE and EMERGENCY levels to conform with RFC 5424 * Fixed the log records to use the php default timezone instead of UTC * Fixed BufferHandler not being flushed properly on PHP fatal errors * Fixed normalization of exotic resource types * Fixed the default format of the SyslogHandler to avoid duplicating datetimes in syslog ### 1.1.0 (2012-04-23) * Added Monolog\Logger::isHandling() to check if a handler will handle the given log level * Added ChromePHPHandler * Added MongoDBHandler * Added GelfHandler (for use with Graylog2 servers) * Added SocketHandler (for use with syslog-ng for example) * Added NormalizerFormatter * Added the possibility to change the activation strategy of the FingersCrossedHandler * Added possibility to show microseconds in logs * Added `server` and `referer` to WebProcessor output ### 1.0.2 (2011-10-24) * Fixed bug in IE with large response headers and FirePHPHandler ### 1.0.1 (2011-08-25) * Added MemoryPeakUsageProcessor and MemoryUsageProcessor * Added Monolog\Logger::getName() to get a logger's channel name ### 1.0.0 (2011-07-06) * Added IntrospectionProcessor to get info from where the logger was called * Fixed WebProcessor in CLI ### 1.0.0-RC1 (2011-07-01) * Initial release PK!+ $vendor/monolog/monolog/composer.jsonnu[{ "name": "monolog/monolog", "description": "Sends your logs to files, sockets, inboxes, databases and various web services", "keywords": ["log", "logging", "psr-3"], "homepage": "https://github.com/Seldaek/monolog", "type": "library", "license": "MIT", "authors": [ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", "homepage": "https://seld.be" } ], "require": { "php": ">=7.2", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { "ext-json": "*", "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", "elasticsearch/elasticsearch": "^7 || ^8", "graylog2/gelf-php": "^1.4.2 || ^2@dev", "guzzlehttp/guzzle": "^7.4", "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", "phpspec/prophecy": "^1.15", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^8.5.38 || ^9.6.19", "predis/predis": "^1.1 || ^2.0", "rollbar/rollbar": "^1.3 || ^2 || ^3", "ruflin/elastica": "^7", "swiftmailer/swiftmailer": "^5.3|^6.0", "symfony/mailer": "^5.4 || ^6", "symfony/mime": "^5.4 || ^6" }, "suggest": { "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", "ruflin/elastica": "Allow sending log messages to an Elastic Search server", "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "rollbar/rollbar": "Allow sending log messages to Rollbar", "ext-mbstring": "Allow to work properly with unicode symbols", "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", "ext-openssl": "Required to send log messages using SSL" }, "autoload": { "psr-4": {"Monolog\\": "src/Monolog"} }, "autoload-dev": { "psr-4": {"Monolog\\": "tests/Monolog"} }, "provide": { "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" }, "extra": { "branch-alias": { "dev-main": "2.x-dev" } }, "scripts": { "test": "@php vendor/bin/phpunit", "phpstan": "@php vendor/bin/phpstan analyse" }, "config": { "lock": false, "sort-packages": true, "platform-check": false, "allow-plugins": { "composer/package-versions-deprecated": true } } } PK!''vendor/monolog/monolog/LICENSEnu[Copyright (c) 2011-2020 Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!\HH vendor/monolog/monolog/README.mdnu[# Monolog - Logging for PHP [![Continuous Integration](https://github.com/Seldaek/monolog/workflows/Continuous%20Integration/badge.svg?branch=main)](https://github.com/Seldaek/monolog/actions) [![Total Downloads](https://img.shields.io/packagist/dt/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog) [![Latest Stable Version](https://img.shields.io/packagist/v/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog) Monolog sends your logs to files, sockets, inboxes, databases and various web services. See the complete list of handlers below. Special handlers allow you to build advanced logging strategies. This library implements the [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) interface that you can type-hint against in your own libraries to keep a maximum of interoperability. You can also use it in your applications to make sure you can always use another compatible logger at a later time. As of 1.11.0 Monolog public APIs will also accept PSR-3 log levels. Internally Monolog still uses its own level scheme since it predates PSR-3. ## Installation Install the latest version with ```bash $ composer require monolog/monolog ``` ## Basic Usage ```php pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING)); // add records to the log $log->warning('Foo'); $log->error('Bar'); ``` ## Documentation - [Usage Instructions](doc/01-usage.md) - [Handlers, Formatters and Processors](doc/02-handlers-formatters-processors.md) - [Utility Classes](doc/03-utilities.md) - [Extending Monolog](doc/04-extending.md) - [Log Record Structure](doc/message-structure.md) ## Support Monolog Financially Get supported Monolog and help fund the project with the [Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-monolog-monolog?utm_source=packagist-monolog-monolog&utm_medium=referral&utm_campaign=enterprise) or via [GitHub sponsorship](https://github.com/sponsors/Seldaek). Tidelift delivers commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. ## Third Party Packages Third party handlers, formatters and processors are [listed in the wiki](https://github.com/Seldaek/monolog/wiki/Third-Party-Packages). You can also add your own there if you publish one. ## About ### Requirements - Monolog `^2.0` works with PHP 7.2 or above, use Monolog `^1.25` for PHP 5.3+ support. ### Support Monolog 1.x support is somewhat limited at this point and only important fixes will be done. You should migrate to Monolog 2 where possible to benefit from all the latest features and fixes. ### Submitting bugs and feature requests Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/monolog/issues) ### Framework Integrations - Frameworks and libraries using [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) can be used very easily with Monolog since it implements the interface. - [Symfony](http://symfony.com) comes out of the box with Monolog. - [Laravel](http://laravel.com/) comes out of the box with Monolog. - [Lumen](http://lumen.laravel.com/) comes out of the box with Monolog. - [PPI](https://github.com/ppi/framework) comes out of the box with Monolog. - [CakePHP](http://cakephp.org/) is usable with Monolog via the [cakephp-monolog](https://github.com/jadb/cakephp-monolog) plugin. - [Slim](http://www.slimframework.com/) is usable with Monolog via the [Slim-Monolog](https://github.com/Flynsarmy/Slim-Monolog) log writer. - [XOOPS 2.6](http://xoops.org/) comes out of the box with Monolog. - [Aura.Web_Project](https://github.com/auraphp/Aura.Web_Project) comes out of the box with Monolog. - [Nette Framework](http://nette.org/en/) is usable with Monolog via the [contributte/monolog](https://github.com/contributte/monolog) or [orisai/nette-monolog](https://github.com/orisai/nette-monolog) extensions. - [Proton Micro Framework](https://github.com/alexbilbie/Proton) comes out of the box with Monolog. - [FuelPHP](http://fuelphp.com/) comes out of the box with Monolog. - [Equip Framework](https://github.com/equip/framework) comes out of the box with Monolog. - [Yii 2](http://www.yiiframework.com/) is usable with Monolog via the [yii2-monolog](https://github.com/merorafael/yii2-monolog) or [yii2-psr-log-target](https://github.com/samdark/yii2-psr-log-target) plugins. - [Hawkbit Micro Framework](https://github.com/HawkBitPhp/hawkbit) comes out of the box with Monolog. - [SilverStripe 4](https://www.silverstripe.org/) comes out of the box with Monolog. - [Drupal](https://www.drupal.org/) is usable with Monolog via the [monolog](https://www.drupal.org/project/monolog) module. - [Aimeos ecommerce framework](https://aimeos.org/) is usable with Monolog via the [ai-monolog](https://github.com/aimeos/ai-monolog) extension. - [Magento](https://magento.com/) comes out of the box with Monolog. ### Author Jordi Boggiano - -
See also the list of [contributors](https://github.com/Seldaek/monolog/contributors) who participated in this project. ### License Monolog is licensed under the MIT License - see the [LICENSE](LICENSE) file for details ### Acknowledgements This library is heavily inspired by Python's [Logbook](https://logbook.readthedocs.io/en/stable/) library, although most concepts have been adjusted to fit to the PHP world. PK!ƻ,_ _ !vendor/monolog/monolog/UPGRADE.mdnu[### 2.0.0 - `Monolog\Logger::API` can be used to distinguish between a Monolog `1` and `2` install of Monolog when writing integration code. - Removed non-PSR-3 methods to add records, all the `add*` (e.g. `addWarning`) methods as well as `emerg`, `crit`, `err` and `warn`. - DateTime are now formatted with a timezone and microseconds (unless disabled). Various formatters and log output might be affected, which may mess with log parsing in some cases. - The `datetime` in every record array is now a DateTimeImmutable, not that you should have been modifying these anyway. - The timezone is now set per Logger instance and not statically, either via ->setTimezone or passed in the constructor. Calls to Logger::setTimezone should be converted. - `HandlerInterface` has been split off and two new interfaces now exist for more granular controls: `ProcessableHandlerInterface` and `FormattableHandlerInterface`. Handlers not extending `AbstractHandler` should make sure to implement the relevant interfaces. - `HandlerInterface` now requires the `close` method to be implemented. This only impacts you if you implement the interface yourself, but you can extend the new `Monolog\Handler\Handler` base class too. - There is no more default handler configured on empty Logger instances, if you were relying on that you will not get any output anymore, make sure to configure the handler you need. #### LogglyFormatter - The records' `datetime` is not sent anymore. Only `timestamp` is sent to Loggly. #### AmqpHandler - Log levels are not shortened to 4 characters anymore. e.g. a warning record will be sent using the `warning.channel` routing key instead of `warn.channel` as in 1.x. - The exchange name does not default to 'log' anymore, and it is completely ignored now for the AMQP extension users. Only PHPAmqpLib uses it if provided. #### RotatingFileHandler - The file name format must now contain `{date}` and the date format must be set to one of the predefined FILE_PER_* constants to avoid issues with file rotation. See `setFilenameFormat`. #### LogstashFormatter - Removed Logstash V0 support - Context/extra prefix has been removed in favor of letting users configure the exact key being sent - Context/extra data are now sent as an object instead of single keys #### HipChatHandler - Removed deprecated HipChat handler, migrate to Slack and use SlackWebhookHandler or SlackHandler instead #### SlackbotHandler - Removed deprecated SlackbotHandler handler, use SlackWebhookHandler or SlackHandler instead #### RavenHandler - Removed deprecated RavenHandler handler, use sentry/sentry 2.x and their Sentry\Monolog\Handler instead #### ElasticSearchHandler - As support for the official Elasticsearch library was added, the former ElasticSearchHandler has been renamed to ElasticaHandler and the new one added as ElasticsearchHandler. PK!LY Y 9vendor/paragonie/constant_time_encoding/src/Base32Hex.phpnu[ 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86 $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86); return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 5-bit integers * into 8-bit integers. * * @param int $src * @return int */ protected static function decode5BitsUpper(int $src): int { $ret = -1; // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54 $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54); return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 5-bit integers. * * @param int $src * @return string */ protected static function encode5Bits(int $src): string { $src += 0x30; // if ($src > 0x39) $src += 0x61 - 0x3a; // 39 $src += ((0x39 - $src) >> 8) & 39; return \pack('C', $src); } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 5-bit integers. * * Uppercase variant. * * @param int $src * @return string */ protected static function encode5BitsUpper(int $src): string { $src += 0x30; // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 $src += ((0x39 - $src) >> 8) & 7; return \pack('C', $src); } }PK!ExEE6vendor/paragonie/constant_time_encoding/src/Base32.phpnu[ 96 && $src < 123) $ret += $src - 97 + 1; // -64 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96); // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 5-bit integers * into 8-bit integers. * * Uppercase variant. * * @param int $src * @return int */ protected static function decode5BitsUpper(int $src): int { $ret = -1; // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 5-bit integers. * * @param int $src * @return string */ protected static function encode5Bits(int $src): string { $diff = 0x61; // if ($src > 25) $ret -= 72; $diff -= ((25 - $src) >> 8) & 73; return \pack('C', $src + $diff); } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 5-bit integers. * * Uppercase variant. * * @param int $src * @return string */ protected static function encode5BitsUpper(int $src): string { $diff = 0x41; // if ($src > 25) $ret -= 40; $diff -= ((25 - $src) >> 8) & 41; return \pack('C', $src + $diff); } /** * @param string $encodedString * @param bool $upper * @return string */ public static function decodeNoPadding( #[\SensitiveParameter] string $encodedString, bool $upper = false ): string { $srcLen = Binary::safeStrlen($encodedString); if ($srcLen === 0) { return ''; } if (($srcLen & 7) === 0) { for ($j = 0; $j < 7 && $j < $srcLen; ++$j) { if ($encodedString[$srcLen - $j - 1] === '=') { throw new InvalidArgumentException( "decodeNoPadding() doesn't tolerate padding" ); } } } return static::doDecode( $encodedString, $upper, true ); } /** * Base32 decoding * * @param string $src * @param bool $upper * @param bool $strictPadding * @return string * * @throws TypeError */ protected static function doDecode( #[\SensitiveParameter] string $src, bool $upper = false, bool $strictPadding = false ): string { // We do this to reduce code duplication: $method = $upper ? 'decode5BitsUpper' : 'decode5Bits'; // Remove padding $srcLen = Binary::safeStrlen($src); if ($srcLen === 0) { return ''; } if ($strictPadding) { if (($srcLen & 7) === 0) { for ($j = 0; $j < 7; ++$j) { if ($src[$srcLen - 1] === '=') { $srcLen--; } else { break; } } } if (($srcLen & 7) === 1) { throw new RangeException( 'Incorrect padding' ); } } else { $src = \rtrim($src, '='); $srcLen = Binary::safeStrlen($src); } $err = 0; $dest = ''; // Main loop (no padding): for ($i = 0; $i + 8 <= $srcLen; $i += 8) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8)); /** @var int $c0 */ $c0 = static::$method($chunk[1]); /** @var int $c1 */ $c1 = static::$method($chunk[2]); /** @var int $c2 */ $c2 = static::$method($chunk[3]); /** @var int $c3 */ $c3 = static::$method($chunk[4]); /** @var int $c4 */ $c4 = static::$method($chunk[5]); /** @var int $c5 */ $c5 = static::$method($chunk[6]); /** @var int $c6 */ $c6 = static::$method($chunk[7]); /** @var int $c7 */ $c7 = static::$method($chunk[8]); $dest .= \pack( 'CCCCC', (($c0 << 3) | ($c1 >> 2) ) & 0xff, (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, (($c3 << 4) | ($c4 >> 1) ) & 0xff, (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff, (($c6 << 5) | ($c7 ) ) & 0xff ); $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8; } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); /** @var int $c0 */ $c0 = static::$method($chunk[1]); if ($i + 6 < $srcLen) { /** @var int $c1 */ $c1 = static::$method($chunk[2]); /** @var int $c2 */ $c2 = static::$method($chunk[3]); /** @var int $c3 */ $c3 = static::$method($chunk[4]); /** @var int $c4 */ $c4 = static::$method($chunk[5]); /** @var int $c5 */ $c5 = static::$method($chunk[6]); /** @var int $c6 */ $c6 = static::$method($chunk[7]); $dest .= \pack( 'CCCC', (($c0 << 3) | ($c1 >> 2) ) & 0xff, (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, (($c3 << 4) | ($c4 >> 1) ) & 0xff, (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff ); $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8; if ($strictPadding) { $err |= ($c6 << 5) & 0xff; } } elseif ($i + 5 < $srcLen) { /** @var int $c1 */ $c1 = static::$method($chunk[2]); /** @var int $c2 */ $c2 = static::$method($chunk[3]); /** @var int $c3 */ $c3 = static::$method($chunk[4]); /** @var int $c4 */ $c4 = static::$method($chunk[5]); /** @var int $c5 */ $c5 = static::$method($chunk[6]); $dest .= \pack( 'CCCC', (($c0 << 3) | ($c1 >> 2) ) & 0xff, (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, (($c3 << 4) | ($c4 >> 1) ) & 0xff, (($c4 << 7) | ($c5 << 2) ) & 0xff ); $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8; } elseif ($i + 4 < $srcLen) { /** @var int $c1 */ $c1 = static::$method($chunk[2]); /** @var int $c2 */ $c2 = static::$method($chunk[3]); /** @var int $c3 */ $c3 = static::$method($chunk[4]); /** @var int $c4 */ $c4 = static::$method($chunk[5]); $dest .= \pack( 'CCC', (($c0 << 3) | ($c1 >> 2) ) & 0xff, (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, (($c3 << 4) | ($c4 >> 1) ) & 0xff ); $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8; if ($strictPadding) { $err |= ($c4 << 7) & 0xff; } } elseif ($i + 3 < $srcLen) { /** @var int $c1 */ $c1 = static::$method($chunk[2]); /** @var int $c2 */ $c2 = static::$method($chunk[3]); /** @var int $c3 */ $c3 = static::$method($chunk[4]); $dest .= \pack( 'CC', (($c0 << 3) | ($c1 >> 2) ) & 0xff, (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff ); $err |= ($c0 | $c1 | $c2 | $c3) >> 8; if ($strictPadding) { $err |= ($c3 << 4) & 0xff; } } elseif ($i + 2 < $srcLen) { /** @var int $c1 */ $c1 = static::$method($chunk[2]); /** @var int $c2 */ $c2 = static::$method($chunk[3]); $dest .= \pack( 'CC', (($c0 << 3) | ($c1 >> 2) ) & 0xff, (($c1 << 6) | ($c2 << 1) ) & 0xff ); $err |= ($c0 | $c1 | $c2) >> 8; if ($strictPadding) { $err |= ($c2 << 6) & 0xff; } } elseif ($i + 1 < $srcLen) { /** @var int $c1 */ $c1 = static::$method($chunk[2]); $dest .= \pack( 'C', (($c0 << 3) | ($c1 >> 2) ) & 0xff ); $err |= ($c0 | $c1) >> 8; if ($strictPadding) { $err |= ($c1 << 6) & 0xff; } } else { $dest .= \pack( 'C', (($c0 << 3) ) & 0xff ); $err |= ($c0) >> 8; } } $check = ($err === 0); if (!$check) { throw new RangeException( 'Base32::doDecode() only expects characters in the correct base32 alphabet' ); } return $dest; } /** * Base32 Encoding * * @param string $src * @param bool $upper * @param bool $pad * @return string * @throws TypeError */ protected static function doEncode( #[\SensitiveParameter] string $src, bool $upper = false, $pad = true ): string { // We do this to reduce code duplication: $method = $upper ? 'encode5BitsUpper' : 'encode5Bits'; $dest = ''; $srcLen = Binary::safeStrlen($src); // Main loop (no padding): for ($i = 0; $i + 5 <= $srcLen; $i += 5) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5)); $b0 = $chunk[1]; $b1 = $chunk[2]; $b2 = $chunk[3]; $b3 = $chunk[4]; $b4 = $chunk[5]; $dest .= static::$method( ($b0 >> 3) & 31) . static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . static::$method((($b1 >> 1) ) & 31) . static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . static::$method((($b3 >> 2) ) & 31) . static::$method((($b3 << 3) | ($b4 >> 5)) & 31) . static::$method( $b4 & 31); } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); $b0 = $chunk[1]; if ($i + 3 < $srcLen) { $b1 = $chunk[2]; $b2 = $chunk[3]; $b3 = $chunk[4]; $dest .= static::$method( ($b0 >> 3) & 31) . static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . static::$method((($b1 >> 1) ) & 31) . static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . static::$method((($b3 >> 2) ) & 31) . static::$method((($b3 << 3) ) & 31); if ($pad) { $dest .= '='; } } elseif ($i + 2 < $srcLen) { $b1 = $chunk[2]; $b2 = $chunk[3]; $dest .= static::$method( ($b0 >> 3) & 31) . static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . static::$method((($b1 >> 1) ) & 31) . static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . static::$method((($b2 << 1) ) & 31); if ($pad) { $dest .= '==='; } } elseif ($i + 1 < $srcLen) { $b1 = $chunk[2]; $dest .= static::$method( ($b0 >> 3) & 31) . static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . static::$method((($b1 >> 1) ) & 31) . static::$method((($b1 << 4) ) & 31); if ($pad) { $dest .= '===='; } } else { $dest .= static::$method( ($b0 >> 3) & 31) . static::$method( ($b0 << 2) & 31); if ($pad) { $dest .= '======'; } } } return $dest; } } PK!EZ Evendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.phpnu[ 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45 $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45); // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52); // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58); return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 6-bit integers. * * @param int $src * @return string */ protected static function encode6Bits(int $src): string { $src += 0x2e; // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 $src += ((0x39 - $src) >> 8) & 7; // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 $src += ((0x5a - $src) >> 8) & 6; return \pack('C', $src); } } PK!o~{ >vendor/paragonie/constant_time_encoding/src/Base64DotSlash.phpnu[ 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45 $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45); // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62); // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68); // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7); return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 6-bit integers. * * @param int $src * @return string */ protected static function encode6Bits(int $src): string { $src += 0x2e; // if ($src > 0x2f) $src += 0x41 - 0x30; // 17 $src += ((0x2f - $src) >> 8) & 17; // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 $src += ((0x5a - $src) >> 8) & 6; // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75 $src -= ((0x7a - $src) >> 8) & 75; return \pack('C', $src); } } PK!28D''6vendor/paragonie/constant_time_encoding/src/Base64.phpnu[ $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3)); $b0 = $chunk[1]; $b1 = $chunk[2]; $b2 = $chunk[3]; $dest .= static::encode6Bits( $b0 >> 2 ) . static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . static::encode6Bits( $b2 & 63); } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); $b0 = $chunk[1]; if ($i + 1 < $srcLen) { $b1 = $chunk[2]; $dest .= static::encode6Bits($b0 >> 2) . static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . static::encode6Bits(($b1 << 2) & 63); if ($pad) { $dest .= '='; } } else { $dest .= static::encode6Bits( $b0 >> 2) . static::encode6Bits(($b0 << 4) & 63); if ($pad) { $dest .= '=='; } } } return $dest; } /** * decode from base64 into binary * * Base64 character set "./[A-Z][a-z][0-9]" * * @param string $encodedString * @param bool $strictPadding * @return string * * @throws RangeException * @throws TypeError */ public static function decode( #[\SensitiveParameter] string $encodedString, bool $strictPadding = false ): string { // Remove padding $srcLen = Binary::safeStrlen($encodedString); if ($srcLen === 0) { return ''; } if ($strictPadding) { if (($srcLen & 3) === 0) { if ($encodedString[$srcLen - 1] === '=') { $srcLen--; if ($encodedString[$srcLen - 1] === '=') { $srcLen--; } } } if (($srcLen & 3) === 1) { throw new RangeException( 'Incorrect padding' ); } if ($encodedString[$srcLen - 1] === '=') { throw new RangeException( 'Incorrect padding' ); } } else { $encodedString = \rtrim($encodedString, '='); $srcLen = Binary::safeStrlen($encodedString); } $err = 0; $dest = ''; // Main loop (no padding): for ($i = 0; $i + 4 <= $srcLen; $i += 4) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4)); $c0 = static::decode6Bits($chunk[1]); $c1 = static::decode6Bits($chunk[2]); $c2 = static::decode6Bits($chunk[3]); $c3 = static::decode6Bits($chunk[4]); $dest .= \pack( 'CCC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff), ((($c2 << 6) | $c3 ) & 0xff) ); $err |= ($c0 | $c1 | $c2 | $c3) >> 8; } // The last chunk, which may have padding: if ($i < $srcLen) { /** @var array $chunk */ $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i)); $c0 = static::decode6Bits($chunk[1]); if ($i + 2 < $srcLen) { $c1 = static::decode6Bits($chunk[2]); $c2 = static::decode6Bits($chunk[3]); $dest .= \pack( 'CC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff) ); $err |= ($c0 | $c1 | $c2) >> 8; if ($strictPadding) { $err |= ($c2 << 6) & 0xff; } } elseif ($i + 1 < $srcLen) { $c1 = static::decode6Bits($chunk[2]); $dest .= \pack( 'C', ((($c0 << 2) | ($c1 >> 4)) & 0xff) ); $err |= ($c0 | $c1) >> 8; if ($strictPadding) { $err |= ($c1 << 4) & 0xff; } } elseif ($strictPadding) { $err |= 1; } } $check = ($err === 0); if (!$check) { throw new RangeException( 'Base64::decode() only expects characters in the correct base64 alphabet' ); } return $dest; } /** * @param string $encodedString * @return string */ public static function decodeNoPadding( #[\SensitiveParameter] string $encodedString ): string { $srcLen = Binary::safeStrlen($encodedString); if ($srcLen === 0) { return ''; } if (($srcLen & 3) === 0) { // If $strLen is not zero, and it is divisible by 4, then it's at least 4. if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') { throw new InvalidArgumentException( "decodeNoPadding() doesn't tolerate padding" ); } } return static::decode( $encodedString, true ); } /** * Uses bitwise operators instead of table-lookups to turn 6-bit integers * into 8-bit integers. * * Base64 character set: * [A-Z] [a-z] [0-9] + / * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f * * @param int $src * @return int */ protected static function decode6Bits(int $src): int { $ret = -1; // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); // if ($src == 0x2b) $ret += 62 + 1; $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; // if ($src == 0x2f) ret += 63 + 1; $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 6-bit integers. * * @param int $src * @return string */ protected static function encode6Bits(int $src): string { $diff = 0x41; // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 $diff += ((25 - $src) >> 8) & 6; // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 $diff -= ((51 - $src) >> 8) & 75; // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 $diff -= ((61 - $src) >> 8) & 15; // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 $diff += ((62 - $src) >> 8) & 3; return \pack('C', $src + $diff); } } PK!ϧ =vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.phpnu[ 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); // if ($src == 0x2c) $ret += 62 + 1; $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63; // if ($src == 0x5f) ret += 63 + 1; $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64; return $ret; } /** * Uses bitwise operators instead of table-lookups to turn 8-bit integers * into 6-bit integers. * * @param int $src * @return string */ protected static function encode6Bits(int $src): string { $diff = 0x41; // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 $diff += ((25 - $src) >> 8) & 6; // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 $diff -= ((51 - $src) >> 8) & 75; // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13 $diff -= ((61 - $src) >> 8) & 13; // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3 $diff += ((62 - $src) >> 8) & 49; return \pack('C', $src + $diff); } } PK!s 6vendor/paragonie/constant_time_encoding/src/Binary.phpnu[ $chunk */ $chunk = \unpack('C', $binString[$i]); $c = $chunk[1] & 0xf; $b = $chunk[1] >> 4; $hex .= \pack( 'CC', (87 + $b + ((($b - 10) >> 8) & ~38)), (87 + $c + ((($c - 10) >> 8) & ~38)) ); } return $hex; } /** * Convert a binary string into a hexadecimal string without cache-timing * leaks, returning uppercase letters (as per RFC 4648) * * @param string $binString (raw binary) * @return string * @throws TypeError */ public static function encodeUpper( #[\SensitiveParameter] string $binString ): string { $hex = ''; $len = Binary::safeStrlen($binString); for ($i = 0; $i < $len; ++$i) { /** @var array $chunk */ $chunk = \unpack('C', $binString[$i]); $c = $chunk[1] & 0xf; $b = $chunk[1] >> 4; $hex .= \pack( 'CC', (55 + $b + ((($b - 10) >> 8) & ~6)), (55 + $c + ((($c - 10) >> 8) & ~6)) ); } return $hex; } /** * Convert a hexadecimal string into a binary string without cache-timing * leaks * * @param string $encodedString * @param bool $strictPadding * @return string (raw binary) * @throws RangeException */ public static function decode( #[\SensitiveParameter] string $encodedString, bool $strictPadding = false ): string { $hex_pos = 0; $bin = ''; $c_acc = 0; $hex_len = Binary::safeStrlen($encodedString); $state = 0; if (($hex_len & 1) !== 0) { if ($strictPadding) { throw new RangeException( 'Expected an even number of hexadecimal characters' ); } else { $encodedString = '0' . $encodedString; ++$hex_len; } } /** @var array $chunk */ $chunk = \unpack('C*', $encodedString); while ($hex_pos < $hex_len) { ++$hex_pos; $c = $chunk[$hex_pos]; $c_num = $c ^ 48; $c_num0 = ($c_num - 10) >> 8; $c_alpha = ($c & ~32) - 55; $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; if (($c_num0 | $c_alpha0) === 0) { throw new RangeException( 'Expected hexadecimal character' ); } $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); if ($state === 0) { $c_acc = $c_val * 16; } else { $bin .= \pack('C', $c_acc | $c_val); } $state ^= 1; } return $bin; } } PK!,\oo7vendor/paragonie/constant_time_encoding/src/RFC4648.phpnu[ "Zm9v" * * @param string $str * @return string * * @throws TypeError */ public static function base64Encode( #[\SensitiveParameter] string $str ): string { return Base64::encode($str); } /** * RFC 4648 Base64 decoding * * "Zm9v" -> "foo" * * @param string $str * @return string * * @throws TypeError */ public static function base64Decode( #[\SensitiveParameter] string $str ): string { return Base64::decode($str, true); } /** * RFC 4648 Base64 (URL Safe) encoding * * "foo" -> "Zm9v" * * @param string $str * @return string * * @throws TypeError */ public static function base64UrlSafeEncode( #[\SensitiveParameter] string $str ): string { return Base64UrlSafe::encode($str); } /** * RFC 4648 Base64 (URL Safe) decoding * * "Zm9v" -> "foo" * * @param string $str * @return string * * @throws TypeError */ public static function base64UrlSafeDecode( #[\SensitiveParameter] string $str ): string { return Base64UrlSafe::decode($str, true); } /** * RFC 4648 Base32 encoding * * "foo" -> "MZXW6===" * * @param string $str * @return string * * @throws TypeError */ public static function base32Encode( #[\SensitiveParameter] string $str ): string { return Base32::encodeUpper($str); } /** * RFC 4648 Base32 encoding * * "MZXW6===" -> "foo" * * @param string $str * @return string * * @throws TypeError */ public static function base32Decode( #[\SensitiveParameter] string $str ): string { return Base32::decodeUpper($str, true); } /** * RFC 4648 Base32-Hex encoding * * "foo" -> "CPNMU===" * * @param string $str * @return string * * @throws TypeError */ public static function base32HexEncode( #[\SensitiveParameter] string $str ): string { return Base32::encodeUpper($str); } /** * RFC 4648 Base32-Hex decoding * * "CPNMU===" -> "foo" * * @param string $str * @return string * * @throws TypeError */ public static function base32HexDecode( #[\SensitiveParameter] string $str ): string { return Base32::decodeUpper($str, true); } /** * RFC 4648 Base16 decoding * * "foo" -> "666F6F" * * @param string $str * @return string * * @throws TypeError */ public static function base16Encode( #[\SensitiveParameter] string $str ): string { return Hex::encodeUpper($str); } /** * RFC 4648 Base16 decoding * * "666F6F" -> "foo" * * @param string $str * @return string */ public static function base16Decode( #[\SensitiveParameter] string $str ): string { return Hex::decode($str, true); } } PK!9f115vendor/paragonie/constant_time_encoding/composer.jsonnu[{ "name": "paragonie/constant_time_encoding", "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", "keywords": [ "base64", "encoding", "rfc4648", "base32", "base16", "hex", "bin2hex", "hex2bin", "base64_encode", "base64_decode", "base32_encode", "base32_decode" ], "license": "MIT", "type": "library", "authors": [ { "name": "Paragon Initiative Enterprises", "email": "security@paragonie.com", "homepage": "https://paragonie.com", "role": "Maintainer" }, { "name": "Steve 'Sc00bz' Thomas", "email": "steve@tobtu.com", "homepage": "https://www.tobtu.com", "role": "Original Developer" } ], "support": { "issues": "https://github.com/paragonie/constant_time_encoding/issues", "email": "info@paragonie.com", "source": "https://github.com/paragonie/constant_time_encoding" }, "require": { "php": "^7|^8" }, "require-dev": { "phpunit/phpunit": "^6|^7|^8|^9", "vimeo/psalm": "^1|^2|^3|^4" }, "autoload": { "psr-4": { "ParagonIE\\ConstantTime\\": "src/" } }, "autoload-dev": { "psr-4": { "ParagonIE\\ConstantTime\\Tests\\": "tests/" } } } PK!|[e e 3vendor/paragonie/constant_time_encoding/LICENSE.txtnu[The MIT License (MIT) Copyright (c) 2016 - 2022 Paragon Initiative Enterprises Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ This library was based on the work of Steve "Sc00bz" Thomas. ------------------------------------------------------------------------------ The MIT License (MIT) Copyright (c) 2014 Steve Thomas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!ДsY Y 1vendor/paragonie/constant_time_encoding/README.mdnu[# Constant-Time Encoding [![Build Status](https://github.com/paragonie/constant_time_encoding/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions) [![Latest Stable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/stable)](https://packagist.org/packages/paragonie/constant_time_encoding) [![Latest Unstable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/unstable)](https://packagist.org/packages/paragonie/constant_time_encoding) [![License](https://poser.pugx.org/paragonie/constant_time_encoding/license)](https://packagist.org/packages/paragonie/constant_time_encoding) [![Downloads](https://img.shields.io/packagist/dt/paragonie/constant_time_encoding.svg)](https://packagist.org/packages/paragonie/constant_time_encoding) Based on the [constant-time base64 implementation made by Steve "Sc00bz" Thomas](https://github.com/Sc00bz/ConstTimeEncoding), this library aims to offer character encoding functions that do not leak information about what you are encoding/decoding via processor cache misses. Further reading on [cache-timing attacks](http://blog.ircmaxell.com/2014/11/its-all-about-time.html). Our fork offers the following enhancements: * `mbstring.func_overload` resistance * Unit tests * Composer- and Packagist-ready * Base16 encoding * Base32 encoding * Uses `pack()` and `unpack()` instead of `chr()` and `ord()` ## PHP Version Requirements Version 2 of this library should work on **PHP 7** or newer. For PHP 5 support, see [the v1.x branch](https://github.com/paragonie/constant_time_encoding/tree/v1.x). If you are adding this as a dependency to a project intended to work on both PHP 5 and PHP 7, please set the required version to `^1|^2` instead of just `^1` or `^2`. ## How to Install ```sh composer require paragonie/constant_time_encoding ``` ## How to Use ```php use ParagonIE\ConstantTime\Encoding; // possibly (if applicable): // require 'vendor/autoload.php'; $data = random_bytes(32); echo Encoding::base64Encode($data), "\n"; echo Encoding::base32EncodeUpper($data), "\n"; echo Encoding::base32Encode($data), "\n"; echo Encoding::hexEncode($data), "\n"; echo Encoding::hexEncodeUpper($data), "\n"; ``` Example output: ``` 1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE= 2VMKKPSHSWVCVZJ6E7SONRY3ZXCNG3GE6ZZFU7TGJSX7KUKFNLAQ==== 2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq==== d558a53e4795aa2ae53e27e4e6c71bcdc4d36cc4f6725a7e664caff551456ac1 D558A53E4795AA2AE53E27E4E6C71BDCC4D36CC4F6725A7E664CAFF551456AC1 ``` If you only need a particular variant, you can just reference the required class like so: ```php use ParagonIE\ConstantTime\Base64; use ParagonIE\ConstantTime\Base32; $data = random_bytes(32); echo Base64::encode($data), "\n"; echo Base32::encode($data), "\n"; ``` Example output: ``` 1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE= 2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq==== ``` ## Support Contracts If your company uses this library in their products or services, you may be interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise). PK!*A|=vendor/paragonie/random_compat/dist/random_compat.phar.pubkeynu[-----BEGIN PUBLIC KEY----- MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEEd+wCqJDrx5B4OldM0dQE0ZMX+lx1ZWm pui0SUqD4G29L3NGsz9UhJ/0HjBdbnkhIK5xviT0X5vtjacF6ajgcCArbTB+ds+p +h7Q084NuSuIpNb6YPfoUFgC/CL9kAoc -----END PUBLIC KEY----- PK!١iAvendor/paragonie/random_compat/dist/random_compat.phar.pubkey.ascnu[-----BEGIN PGP SIGNATURE----- Version: GnuPG v2.0.22 (MingW32) iQEcBAABAgAGBQJWtW1hAAoJEGuXocKCZATaJf0H+wbZGgskK1dcRTsuVJl9IWip QwGw/qIKI280SD6/ckoUMxKDCJiFuPR14zmqnS36k7N5UNPnpdTJTS8T11jttSpg 1LCmgpbEIpgaTah+cELDqFCav99fS+bEiAL5lWDAHBTE/XPjGVCqeehyPYref4IW NDBIEsvnHPHPLsn6X5jq4+Yj5oUixgxaMPiR+bcO4Sh+RzOVB6i2D0upWfRXBFXA NNnsg9/zjvoC7ZW73y9uSH+dPJTt/Vgfeiv52/v41XliyzbUyLalf02GNPY+9goV JHG1ulEEBJOCiUD9cE1PUIJwHA/HqyhHIvV350YoEFiHl8iSwm7SiZu5kPjaq74= =B6+8 -----END PGP SIGNATURE----- PK!&//-vendor/paragonie/random_compat/lib/random.phpnu[buildFromDirectory(dirname(__DIR__).'/lib'); rename( dirname(__DIR__).'/lib/index.php', dirname(__DIR__).'/lib/random.php' ); /** * If we pass an (optional) path to a private key as a second argument, we will * sign the Phar with OpenSSL. * * If you leave this out, it will produce an unsigned .phar! */ if ($argc > 1) { if (!@is_readable($argv[1])) { echo 'Could not read the private key file:', $argv[1], "\n"; exit(255); } $pkeyFile = file_get_contents($argv[1]); $private = openssl_get_privatekey($pkeyFile); if ($private !== false) { $pkey = ''; openssl_pkey_export($private, $pkey); $phar->setSignatureAlgorithm(Phar::OPENSSL, $pkey); /** * Save the corresponding public key to the file */ if (!@is_readable($dist.'/random_compat.phar.pubkey')) { $details = openssl_pkey_get_details($private); file_put_contents( $dist.'/random_compat.phar.pubkey', $details['key'] ); } } else { echo 'An error occurred reading the private key from OpenSSL.', "\n"; exit(255); } } PK!t8Q,vendor/paragonie/random_compat/build-phar.shnu[#!/usr/bin/env bash basedir=$( dirname $( readlink -f ${BASH_SOURCE[0]} ) ) php -dphar.readonly=0 "$basedir/other/build_phar.php" $*PK!P|*ff,vendor/paragonie/random_compat/composer.jsonnu[{ "name": "paragonie/random_compat", "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", "random", "polyfill", "pseudorandom" ], "license": "MIT", "type": "library", "authors": [ { "name": "Paragon Initiative Enterprises", "email": "security@paragonie.com", "homepage": "https://paragonie.com" } ], "support": { "issues": "https://github.com/paragonie/random_compat/issues", "email": "info@paragonie.com", "source": "https://github.com/paragonie/random_compat" }, "require": { "php": ">= 7" }, "require-dev": { "vimeo/psalm": "^1", "phpunit/phpunit": "4.*|5.*" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." } } PK!>JJ&vendor/paragonie/random_compat/LICENSEnu[The MIT License (MIT) Copyright (c) 2015 Paragon Initiative Enterprises Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!1vendor/paragonie/random_compat/psalm-autoload.phpnu[ PK!"#bYY7vendor/phpseclib/bcmath_compat/.github/workflows/ci.ymlnu[name: CI on: [push, pull_request] permissions: contents: read # to fetch code (actions/checkout) jobs: tests: name: Tests timeout-minutes: 10 runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - name: Composer Install run: composer install --no-interaction --no-cache - name: PHPUnit run: vendor/bin/phpunit strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']PK!U -vendor/phpseclib/bcmath_compat/lib/bcmath.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ use bcmath_compat\BCMath; if (!function_exists('bcadd')) { /** * Add two arbitrary precision numbers * * @var string $left_operand * @var string $right_operand * @var int $scale optional */ function bcadd($left_operand, $right_operand, $scale = 0) { return BCMath::add($left_operand, $right_operand, $scale); } /** * Compare two arbitrary precision numbers * * @var string $left_operand * @var string $right_operand * @var int $scale optional */ function bccomp($left_operand, $right_operand, $scale = 0) { return BCMath::comp($left_operand, $right_operand, $scale); } /** * Divide two arbitrary precision numbers * * @var string $dividend * @var string $divisor * @var int $scale optional */ function bcdiv($dividend, $divisor, $scale = 0) { return BCMath::div($dividend, $divisor, $scale); } /** * Get modulus of an arbitrary precision number * * @var string $dividend * @var string $divisor * @var int $scale optional */ function bcmod($dividend, $divisor, $scale = 0) { return BCMath::mod($dividend, $divisor, $scale); } /** * Multiply two arbitrary precision numbers * * @var string $left_operand * @var string $right_operand * @var int $scale optional */ function bcmul($dividend, $divisor, $scale = 0) { return BCMath::mul($dividend, $divisor, $scale); } /** * Raise an arbitrary precision number to another * * @var string $base * @var string $exponent * @var int $scale optional */ function bcpow($base, $exponent, $scale = 0) { return BCMath::pow($base, $exponent, $scale); } /** * Raise an arbitrary precision number to another, reduced by a specified modulus * * @var string $base * @var string $exponent * @var string $modulus * @var int $scale optional */ function bcpowmod($base, $exponent, $modulus, $scale = 0) { return BCMath::powmod($base, $exponent, $modulus, $scale); } /** * Set or get default scale parameter for all bc math functions * * @var int $scale */ function bcscale($scale = null) { return BCMath::scale($scale); } /** * Get the square root of an arbitrary precision number * * @var string $operand * @var int $scale optional */ function bcsqrt($operand, $scale = 0) { return BCMath::sqrt($operand, $scale); } /** * Subtract one arbitrary precision number from another * * @var string $left_operand * @var string $right_operand * @var int $scale optional */ function bcsub($left_operand, $right_operand, $scale = 0) { return BCMath::sub($left_operand, $right_operand, $scale); } } // the following were introduced in PHP 7.0.0 if (!class_exists('Error')) { class Error extends Exception { } class ArithmeticError extends Error { } class DivisionByZeroError extends ArithmeticError { } class TypeError extends Error { } } // the following was introduced in PHP 7.1.0 if (!class_exists('ArgumentCountError')) { class ArgumentCountError extends TypeError { } } // the following was introduced in PHP 8.0.0 if (!class_exists('ValueError')) { class ValueError extends Error { } }PK!uub66-vendor/phpseclib/bcmath_compat/src/BCMath.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace bcmath_compat; use phpseclib3\Math\BigInteger; /** * BCMath Emulation Class * * @author Jim Wigginton * @access public */ abstract class BCMath { /** * Default scale parameter for all bc math functions */ private static $scale; /** * Set or get default scale parameter for all bc math functions * * Uses the PHP 7.3+ behavior * * @var int $scale optional */ private static function scale($scale = null) { if (isset($scale)) { self::$scale = (int) $scale; } return self::$scale; } /** * Formats numbers * * Places the decimal place at the appropriate place, adds trailing 0's as appropriate, etc * * @var string $x * @var int $scale * @var int $pad * @var boolean $trim */ private static function format($x, $scale, $pad) { $sign = self::isNegative($x) ? '-' : ''; $x = str_replace('-', '', $x); if (strlen($x) != $pad) { $x = str_pad($x, $pad, '0', STR_PAD_LEFT); } $temp = $pad ? substr_replace($x, '.', -$pad, 0) : $x; $temp = explode('.', $temp); if ($temp[0] == '') { $temp[0] = '0'; } if (isset($temp[1])) { $temp[1] = substr($temp[1], 0, $scale); $temp[1] = str_pad($temp[1], $scale, '0'); } elseif ($scale) { $temp[1] = str_repeat('0', $scale); } return $sign . rtrim(implode('.', $temp), '.'); } /** * Negativity Test * * @var BigInteger $x */ private static function isNegative($x) { return $x->compare(new BigInteger()) < 0; } /** * Add two arbitrary precision numbers * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function add($x, $y, $scale, $pad) { $z = $x->add($y); return self::format($z, $scale, $pad); } /** * Subtract one arbitrary precision number from another * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function sub($x, $y, $scale, $pad) { $z = $x->subtract($y); return self::format($z, $scale, $pad); } /** * Multiply two arbitrary precision numbers * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function mul($x, $y, $scale, $pad) { if ($x == '0' || $y == '0') { $r = '0'; if ($scale) { $r.= '.' . str_repeat('0', $scale); } return $r; } $z = $x->abs()->multiply($y->abs()); $sign = (self::isNegative($x) ^ self::isNegative($y)) ? '-' : ''; return $sign . self::format($z, $scale, 2 * $pad); } /** * Divide two arbitrary precision numbers * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function div($x, $y, $scale, $pad) { if ($y == '0') { // < PHP 8.0 triggered a warning // >= PHP 8.0 throws an exception throw new \DivisionByZeroError('Division by zero'); } $temp = '1' . str_repeat('0', $scale); $temp = new BigInteger($temp); list($q) = $x->multiply($temp)->divide($y); return self::format($q, $scale, $scale); } /** * Get modulus of an arbitrary precision number * * Uses the PHP 7.2+ behavior * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function mod($x, $y, $scale, $pad) { if ($y == '0') { // < PHP 8.0 triggered a warning // >= PHP 8.0 throws an exception throw new \DivisionByZeroError('Division by zero'); } list($q) = $x->divide($y); $z = $y->multiply($q); $z = $x->subtract($z); return self::format($z, $scale, $pad); } /** * Compare two arbitrary precision numbers * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function comp($x, $y, $scale, $pad) { $x = new BigInteger($x[0] . substr($x[1], 0, $scale)); $y = new BigInteger($y[0] . substr($y[1], 0, $scale)); return $x->compare($y); } /** * Raise an arbitrary precision number to another * * Uses the PHP 7.2+ behavior * * @var string $x * @var string $y * @var int $scale * @var int $pad */ private static function pow($x, $y, $scale, $pad) { if ($y == '0') { $r = '1'; if ($scale) { $r.= '.' . str_repeat('0', $scale); } return $r; } $min = defined('PHP_INT_MIN') ? PHP_INT_MIN : ~PHP_INT_MAX; if (bccomp($y, PHP_INT_MAX) > 0 || bccomp($y, $min) <= 0) { throw new \ValueError('bcpow(): Argument #2 ($exponent) is too large'); } $sign = self::isNegative($x) ? '-' : ''; $x = $x->abs(); $r = new BigInteger(1); for ($i = 0; $i < abs($y); $i++) { $r = $r->multiply($x); } if ($y < 0) { $temp = '1' . str_repeat('0', $scale + $pad * abs($y)); $temp = new BigInteger($temp); list($r) = $temp->divide($r); $pad = $scale; } else { $pad*= abs($y); } return $sign . self::format($r, $scale, $pad); } /** * Raise an arbitrary precision number to another, reduced by a specified modulus * * @var string $x * @var string $e * @var string $n * @var int $scale * @var int $pad */ private static function powmod($x, $e, $n, $scale, $pad) { if ($e[0] == '-' || $n == '0') { // < PHP 8.0 returned false // >= PHP 8.0 throws an exception throw new \ValueError('bcpowmod(): Argument #2 ($exponent) must be greater than or equal to 0'); } if ($n[0] == '-') { $n = substr($n, 1); } if ($e == '0') { return $scale ? '1.' . str_repeat('0', $scale) : '1'; } $x = new BigInteger($x); $e = new BigInteger($e); $n = new BigInteger($n); $z = $x->powMod($e, $n); return $scale ? "$z." . str_repeat('0', $scale) : "$z"; } /** * Get the square root of an arbitrary precision number * * @var string $n * @var int $scale * @var int $pad */ private static function sqrt($n, $scale, $pad) { // the following is based off of the following URL: // https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Decimal_(base_10) if (!is_numeric($n)) { return '0'; } $temp = explode('.', $n); $decStart = ceil(strlen($temp[0]) / 2); $n = implode('', $temp); if (strlen($n) % 2) { $n = "0$n"; } $parts = str_split($n, 2); $parts = array_map('intval', $parts); $i = 0; $p = 0; // for the first step, p = 0 $c = $parts[$i]; $result = ''; while (true) { // determine the greatest digit x such that x(20p+x) <= c for ($x = 1; $x <= 10; $x++) { if ($x * (20 * $p + $x) > $c) { $x--; break; } } $result.= $x; $y = $x * (20 * $p + $x); $p = 10 * $p + $x; $c = 100 * ($c - $y); if (isset($parts[++$i])) { $c+= $parts[$i]; } if ((!$c && $i >= $decStart) || $i - $decStart == $scale) { break; } if ($decStart == $i) { $result.= '.'; } } $result = explode('.', $result); if (isset($result[1])) { $result[1] = str_pad($result[1], $scale, '0'); } elseif ($scale) { $result[1] = str_repeat('0', $scale); } return implode('.', $result); } /** * __callStatic Magic Method * * @var string $name * @var array $arguments */ public static function __callStatic($name, $arguments) { static $params = [ 'add' => 3, 'comp' => 3, 'div' => 3, 'mod' => 3, 'mul' => 3, 'pow' => 3, 'powmod' => 4, 'scale' => 1, 'sqrt' => 2, 'sub' => 3 ]; if (count($arguments) < $params[$name] - 1) { $min = $params[$name] - 1; throw new \ArgumentCountError("bc$name() expects at least $min parameters, " . func_num_args() . " given"); } if (count($arguments) > $params[$name]) { $str = "bc$name() expects at most {$params[$name]} parameters, " . func_num_args() . " given"; throw new \ArgumentCountError($str); } $numbers = array_slice($arguments, 0, $params[$name] - 1); $ints = []; switch ($name) { case 'pow': $ints = array_slice($numbers, count($numbers) - 1); $numbers = array_slice($numbers, 0, count($numbers) - 1); $names = ['exponent']; break; case 'powmod': $ints = $numbers; $numbers = []; $names = ['base', 'exponent', 'modulus']; break; case 'sqrt': $names = ['num']; break; default: $names = ['num1', 'num2']; } foreach ($ints as $i => &$int) { if (!is_numeric($int)) { $int = '0'; } $pos = strpos($int, '.'); if ($pos !== false) { $int = substr($int, 0, $pos); throw new \ValueError("bc$name(): Argument #2 (\$$names[$i]) cannot have a fractional part"); } } foreach ($numbers as $i => $arg) { $num = $i + 1; switch (true) { case is_bool($arg): case is_numeric($arg): case is_string($arg): case is_object($arg) && method_exists($arg, '__toString'): if (!is_bool($arg) && !is_numeric("$arg")) { throw new \ValueError("bc$name: bcmath function argument is not well-formed"); } break; // PHP >= 8.1 has deprecated the passing of nulls to string parameters case is_null($arg): $error = "bc$name(): Passing null to parameter #$num (\$$names[$i]) of type string is deprecated"; trigger_error($error, E_USER_DEPRECATED); break; default: $type = is_object($arg) ? get_class($arg) : gettype($arg); $error = "bc$name(): Argument #$num (\$$names[$i]) must be of type string, $type given"; throw new \TypeError($error); } } if (!isset(self::$scale)) { $scale = ini_get('bcmath.scale'); self::$scale = $scale !== false ? max(intval($scale), 0) : 0; } $scale = isset($arguments[$params[$name] - 1]) ? $arguments[$params[$name] - 1] : self::$scale; switch (true) { case is_bool($scale): case is_numeric($scale): case is_string($scale) && preg_match('#0-9\.#', $scale[0]): break; default: $type = is_object($arg) ? get_class($arg) : gettype($arg); $str = "bc$name(): Argument #$params[$name] (\$scale) must be of type ?int, string given"; throw new \TypeError($str); } $scale = (int) $scale; if ($scale < 0) { throw new \ValueError("bc$name(): Argument #$params[$name] (\$scale) must be between 0 and 2147483647"); } $pad = 0; foreach ($numbers as &$num) { if (is_bool($num)) { $num = $num ? '1' : '0'; } elseif (!is_numeric($num)) { $num = '0'; } $num = explode('.', $num); if (isset($num[1])) { $pad = max($pad, strlen($num[1])); } } switch ($name) { case 'add': case 'sub': case 'mul': case 'div': case 'mod': case 'pow': foreach ($numbers as &$num) { if (!isset($num[1])) { $num[1] = ''; } $num[1] = str_pad($num[1], $pad, '0'); $num = new BigInteger($num[0] . $num[1]); } break; case 'comp': foreach ($numbers as &$num) { if (!isset($num[1])) { $num[1] = ''; } $num[1] = str_pad($num[1], $pad, '0'); } break; case 'sqrt': $numbers = [$arguments[0]]; } $arguments = array_merge($numbers, $ints, [$scale, $pad]); $result = call_user_func_array(self::class . "::$name", $arguments); return preg_match('#^-0\.?0*$#', $result) ? substr($result, 1) : $result; } } PK!hN-%%,vendor/phpseclib/bcmath_compat/composer.jsonnu[{ "name": "phpseclib/bcmath_compat", "description": "PHP 5.x-8.x polyfill for bcmath extension", "keywords": [ "bcmath", "math", "biginteger", "bigdecimal", "polyfill" ], "license": "MIT", "type": "library", "authors": [ { "name": "Jim Wigginton", "email": "terrafrost@php.net", "homepage": "http://phpseclib.sourceforge.net" } ], "support": { "issues": "https://github.com/phpseclib/bcmath_compat/issues", "email": "terrafrost@php.net", "source": "https://github.com/phpseclib/bcmath_compat" }, "require": { "phpseclib/phpseclib": "^3.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", "squizlabs/php_codesniffer": "^3.0" }, "suggest": { "ext-gmp": "Will enable faster math operations" }, "autoload": { "files": ["lib/bcmath.php"], "psr-4": { "bcmath_compat\\": "src" } }, "scripts": { "test": "phpunit", "check-style": "phpcs src tests", "fix-style": "phpcbf src tests" }, "provide": { "ext-bcmath": "8.1.0" } } PK!Skk)vendor/phpseclib/bcmath_compat/LICENSE.mdnu[# The MIT License (MIT) Copyright (c) 2019 terrafrost > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: > > The above copyright notice and this permission notice shall be included in > all copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN > THE SOFTWARE.PK!>Ǜ(vendor/phpseclib/bcmath_compat/README.mdnu[# bcmath_compat [![Software License][ico-license]](LICENSE.md) [![CI Status](https://github.com/phpseclib/bcmath_compat/actions/workflows/ci.yml/badge.svg?branch=2.0&event=push "CI Status")](https://github.com/phpseclib/bcmath_compat/actions/workflows/ci.yml?query=branch%3A2.0) PHP 5.x-8.x polyfill for bcmath extension ## Installation With [Composer](https://getcomposer.org/): ```bash $ composer require phpseclib/bcmath_compat ``` ## Limitations - `extension_loaded('bcmath')` bcmath_compat cannot make this return true. The recommended remediation is to not do this. - `ini_set('bcmath.scale', ...)` You cannot set configuration options for extensions that are not installed. If you do `ini_set('bcmath.scale', 5)` on a system without bcmath installed then `ini_get('bcmath.scale')` will return `false`. It's similar to what happens when you do `ini_set('zzz', 5)` and then `ini_get('zzz')`. You'll get `false` back. The recommended remediation to doing `ini_set('bcmath.scale', ...)` is to do `bcscale(...)`. The recommended remediation for doing `ini_get` is (if you're using PHP >= 7.3.0) to do `bcscale()` or (if you're using PHP < 7.3.0) to do `max(0, strlen(bcadd('0', '0')) - 2)`. Note that `ini_get` always returns a string whereas the recommended remediations return integers. [ico-version]: https://img.shields.io/packagist/v/phpseclib/bcmath_compat.svg?style=flat-square [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square [ico-travis]: https://img.shields.io/travis/phpseclib/bcmath_compat/master.svg?style=flat-square [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/phpseclib/bcmath_compat.svg?style=flat-square [ico-code-quality]: https://img.shields.io/scrutinizer/g/phpseclib/bcmath_compat.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/phpseclib/bcmath_compat.svg?style=flat-square [link-packagist]: https://packagist.org/packages/phpseclib/bcmath_compat [link-travis]: https://travis-ci.org/phpseclib/bcmath_compat [link-scrutinizer]: https://scrutinizer-ci.com/g/phpseclib/bcmath_compat/code-structure [link-code-quality]: https://scrutinizer-ci.com/g/phpseclib/bcmath_compat [link-downloads]: https://packagist.org/packages/phpseclib/bcmath_compat PK! T}?}?Avendor/phpseclib/phpseclib/phpseclib/Common/Functions/Strings.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Common\Functions; use ParagonIE\ConstantTime\Base64; use ParagonIE\ConstantTime\Base64UrlSafe; use ParagonIE\ConstantTime\Hex; use phpseclib3\Math\BigInteger; use phpseclib3\Math\Common\FiniteField; /** * Common String Functions * * @author Jim Wigginton */ abstract class Strings { /** * String Shift * * Inspired by array_shift * * @param string $string * @param int $index * @return string */ public static function shift(&$string, $index = 1) { $substr = substr($string, 0, $index); $string = substr($string, $index); return $substr; } /** * String Pop * * Inspired by array_pop * * @param string $string * @param int $index * @return string */ public static function pop(&$string, $index = 1) { $substr = substr($string, -$index); $string = substr($string, 0, -$index); return $substr; } /** * Parse SSH2-style string * * Returns either an array or a boolean if $data is malformed. * * Valid characters for $format are as follows: * * C = byte * b = boolean (true/false) * N = uint32 * Q = uint64 * s = string * i = mpint * L = name-list * * uint64 is not supported. * * @param string $format * @param string $data * @return mixed */ public static function unpackSSH2($format, &$data) { $format = self::formatPack($format); $result = []; for ($i = 0; $i < strlen($format); $i++) { switch ($format[$i]) { case 'C': case 'b': if (!strlen($data)) { throw new \LengthException('At least one byte needs to be present for successful C / b decodes'); } break; case 'N': case 'i': case 's': case 'L': if (strlen($data) < 4) { throw new \LengthException('At least four byte needs to be present for successful N / i / s / L decodes'); } break; case 'Q': if (strlen($data) < 8) { throw new \LengthException('At least eight byte needs to be present for successful N / i / s / L decodes'); } break; default: throw new \InvalidArgumentException('$format contains an invalid character'); } switch ($format[$i]) { case 'C': $result[] = ord(self::shift($data)); continue 2; case 'b': $result[] = ord(self::shift($data)) != 0; continue 2; case 'N': list(, $temp) = unpack('N', self::shift($data, 4)); $result[] = $temp; continue 2; case 'Q': // pack() added support for Q in PHP 5.6.3 and PHP 5.6 is phpseclib 3's minimum version // so in theory we could support this BUT, "64-bit format codes are not available for // 32-bit versions" and phpseclib works on 32-bit installs. on 32-bit installs // 64-bit floats can be used to get larger numbers then 32-bit signed ints would allow // for. sure, you're not gonna get the full precision of 64-bit numbers but just because // you need > 32-bit precision doesn't mean you need the full 64-bit precision extract(unpack('Nupper/Nlower', self::shift($data, 8))); $temp = $upper ? 4294967296 * $upper : 0; $temp += $lower < 0 ? ($lower & 0x7FFFFFFFF) + 0x80000000 : $lower; // $temp = hexdec(bin2hex(self::shift($data, 8))); $result[] = $temp; continue 2; } list(, $length) = unpack('N', self::shift($data, 4)); if (strlen($data) < $length) { throw new \LengthException("$length bytes needed; " . strlen($data) . ' bytes available'); } $temp = self::shift($data, $length); switch ($format[$i]) { case 'i': $result[] = new BigInteger($temp, -256); break; case 's': $result[] = $temp; break; case 'L': $result[] = explode(',', $temp); } } return $result; } /** * Create SSH2-style string * * @param string $format * @param string|int|float|array|bool ...$elements * @return string */ public static function packSSH2($format, ...$elements) { $format = self::formatPack($format); if (strlen($format) != count($elements)) { throw new \InvalidArgumentException('There must be as many arguments as there are characters in the $format string'); } $result = ''; for ($i = 0; $i < strlen($format); $i++) { $element = $elements[$i]; switch ($format[$i]) { case 'C': if (!is_int($element)) { throw new \InvalidArgumentException('Bytes must be represented as an integer between 0 and 255, inclusive.'); } $result .= pack('C', $element); break; case 'b': if (!is_bool($element)) { throw new \InvalidArgumentException('A boolean parameter was expected.'); } $result .= $element ? "\1" : "\0"; break; case 'Q': if (!is_int($element) && !is_float($element)) { throw new \InvalidArgumentException('An integer was expected.'); } // 4294967296 == 1 << 32 $result .= pack('NN', $element / 4294967296, $element); break; case 'N': if (is_float($element)) { $element = (int) $element; } if (!is_int($element)) { throw new \InvalidArgumentException('An integer was expected.'); } $result .= pack('N', $element); break; case 's': if (!self::is_stringable($element)) { throw new \InvalidArgumentException('A string was expected.'); } $result .= pack('Na*', strlen($element), $element); break; case 'i': if (!$element instanceof BigInteger && !$element instanceof FiniteField\Integer) { throw new \InvalidArgumentException('A phpseclib3\Math\BigInteger or phpseclib3\Math\Common\FiniteField\Integer object was expected.'); } $element = $element->toBytes(true); $result .= pack('Na*', strlen($element), $element); break; case 'L': if (!is_array($element)) { throw new \InvalidArgumentException('An array was expected.'); } $element = implode(',', $element); $result .= pack('Na*', strlen($element), $element); break; default: throw new \InvalidArgumentException('$format contains an invalid character'); } } return $result; } /** * Expand a pack string * * Converts C5 to CCCCC, for example. * * @param string $format * @return string */ private static function formatPack($format) { $parts = preg_split('#(\d+)#', $format, -1, PREG_SPLIT_DELIM_CAPTURE); $format = ''; for ($i = 1; $i < count($parts); $i += 2) { $format .= substr($parts[$i - 1], 0, -1) . str_repeat(substr($parts[$i - 1], -1), $parts[$i]); } $format .= $parts[$i - 1]; return $format; } /** * Convert binary data into bits * * bin2hex / hex2bin refer to base-256 encoded data as binary, whilst * decbin / bindec refer to base-2 encoded data as binary. For the purposes * of this function, bin refers to base-256 encoded data whilst bits refers * to base-2 encoded data * * @param string $x * @return string */ public static function bits2bin($x) { /* // the pure-PHP approach is faster than the GMP approach if (function_exists('gmp_export')) { return strlen($x) ? gmp_export(gmp_init($x, 2)) : gmp_init(0); } */ if (preg_match('#[^01]#', $x)) { throw new \RuntimeException('The only valid characters are 0 and 1'); } if (!defined('PHP_INT_MIN')) { define('PHP_INT_MIN', ~PHP_INT_MAX); } $length = strlen($x); if (!$length) { return ''; } $block_size = PHP_INT_SIZE << 3; $pad = $block_size - ($length % $block_size); if ($pad != $block_size) { $x = str_repeat('0', $pad) . $x; } $parts = str_split($x, $block_size); $str = ''; foreach ($parts as $part) { $xor = $part[0] == '1' ? PHP_INT_MIN : 0; $part[0] = '0'; $str .= pack( PHP_INT_SIZE == 4 ? 'N' : 'J', $xor ^ eval('return 0b' . $part . ';') ); } return ltrim($str, "\0"); } /** * Convert bits to binary data * * @param string $x * @return string */ public static function bin2bits($x, $trim = true) { /* // the pure-PHP approach is slower than the GMP approach BUT // i want to the pure-PHP version to be easily unit tested as well if (function_exists('gmp_import')) { return gmp_strval(gmp_import($x), 2); } */ $len = strlen($x); $mod = $len % PHP_INT_SIZE; if ($mod) { $x = str_pad($x, $len + PHP_INT_SIZE - $mod, "\0", STR_PAD_LEFT); } $bits = ''; if (PHP_INT_SIZE == 4) { $digits = unpack('N*', $x); foreach ($digits as $digit) { $bits .= sprintf('%032b', $digit); } } else { $digits = unpack('J*', $x); foreach ($digits as $digit) { $bits .= sprintf('%064b', $digit); } } return $trim ? ltrim($bits, '0') : $bits; } /** * Switch Endianness Bit Order * * @param string $x * @return string */ public static function switchEndianness($x) { $r = ''; for ($i = strlen($x) - 1; $i >= 0; $i--) { $b = ord($x[$i]); if (PHP_INT_SIZE === 8) { // 3 operations // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64BitsDiv $r .= chr((($b * 0x0202020202) & 0x010884422010) % 1023); } else { // 7 operations // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits $p1 = ($b * 0x0802) & 0x22110; $p2 = ($b * 0x8020) & 0x88440; $r .= chr( (($p1 | $p2) * 0x10101) >> 16 ); } } return $r; } /** * Increment the current string * * @param string $var * @return string */ public static function increment_str(&$var) { if (function_exists('sodium_increment')) { $var = strrev($var); sodium_increment($var); $var = strrev($var); return $var; } for ($i = 4; $i <= strlen($var); $i += 4) { $temp = substr($var, -$i, 4); switch ($temp) { case "\xFF\xFF\xFF\xFF": $var = substr_replace($var, "\x00\x00\x00\x00", -$i, 4); break; case "\x7F\xFF\xFF\xFF": $var = substr_replace($var, "\x80\x00\x00\x00", -$i, 4); return $var; default: $temp = unpack('Nnum', $temp); $var = substr_replace($var, pack('N', $temp['num'] + 1), -$i, 4); return $var; } } $remainder = strlen($var) % 4; if ($remainder == 0) { return $var; } $temp = unpack('Nnum', str_pad(substr($var, 0, $remainder), 4, "\0", STR_PAD_LEFT)); $temp = substr(pack('N', $temp['num'] + 1), -$remainder); $var = substr_replace($var, $temp, 0, $remainder); return $var; } /** * Find whether the type of a variable is string (or could be converted to one) * * @param mixed $var * @return bool * @psalm-assert-if-true string|\Stringable $var */ public static function is_stringable($var) { return is_string($var) || (is_object($var) && method_exists($var, '__toString')); } /** * Constant Time Base64-decoding * * ParagoneIE\ConstantTime doesn't use libsodium if it's available so we'll do so * ourselves. see https://github.com/paragonie/constant_time_encoding/issues/39 * * @param string $data * @return string */ public static function base64_decode($data) { return function_exists('sodium_base642bin') ? sodium_base642bin($data, SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, '=') : Base64::decode($data); } /** * Constant Time Base64-decoding (URL safe) * * @param string $data * @return string */ public static function base64url_decode($data) { // return self::base64_decode(str_replace(['-', '_'], ['+', '/'], $data)); return function_exists('sodium_base642bin') ? sodium_base642bin($data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, '=') : Base64UrlSafe::decode($data); } /** * Constant Time Base64-encoding * * @param string $data * @return string */ public static function base64_encode($data) { return function_exists('sodium_bin2base64') ? sodium_bin2base64($data, SODIUM_BASE64_VARIANT_ORIGINAL) : Base64::encode($data); } /** * Constant Time Base64-encoding (URL safe) * * @param string $data * @return string */ public static function base64url_encode($data) { // return str_replace(['+', '/'], ['-', '_'], self::base64_encode($data)); return function_exists('sodium_bin2base64') ? sodium_bin2base64($data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING) : Base64UrlSafe::encode($data); } /** * Constant Time Hex Decoder * * @param string $data * @return string */ public static function hex2bin($data) { return function_exists('sodium_hex2bin') ? sodium_hex2bin($data) : Hex::decode($data); } /** * Constant Time Hex Encoder * * @param string $data * @return string */ public static function bin2hex($data) { return function_exists('sodium_bin2hex') ? sodium_bin2hex($data) : Hex::encode($data); } } PK!5SFvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/JWK.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Keys; use phpseclib3\Common\Functions\Strings; /** * JSON Web Key Formatted Key Handler * * @author Jim Wigginton */ abstract class JWK { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password * @return array */ public static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } $key = preg_replace('#\s#', '', $key); // remove whitespace if (PHP_VERSION_ID >= 73000) { $key = json_decode($key, null, 512, JSON_THROW_ON_ERROR); } else { $key = json_decode($key); if (!$key) { throw new \RuntimeException('Unable to decode JSON'); } } if (isset($key->kty)) { return $key; } if (count($key->keys) != 1) { throw new \RuntimeException('Although the JWK key format supports multiple keys phpseclib does not'); } return $key->keys[0]; } /** * Wrap a key appropriately * * @return string */ protected static function wrapKey(array $key, array $options) { return json_encode(['keys' => [$key + $options]]); } } PK! TvvJvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\AES; use phpseclib3\Crypt\Random; use phpseclib3\Exception\BadDecryptionException; /** * OpenSSH Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class OpenSSH { /** * Default comment * * @var string */ protected static $comment = 'phpseclib-generated-key'; /** * Binary key flag * * @var bool */ protected static $binary = false; /** * Sets the default comment * * @param string $comment */ public static function setComment($comment) { self::$comment = str_replace(["\r", "\n"], '', $comment); } /** * Break a public or private key down into its constituent components * * $type can be either ssh-dss or ssh-rsa * * @param string $key * @param string $password * @return array */ public static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } // key format is described here: // https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) { $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key); $key = Strings::base64_decode($key); $magic = Strings::shift($key, 15); if ($magic != "openssh-key-v1\0") { throw new \RuntimeException('Expected openssh-key-v1'); } list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key); if ($numKeys != 1) { // if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys // would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass // that to the appropriate key loading parser $numKey times or something throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not'); } switch ($ciphername) { case 'none': break; case 'aes256-ctr': if ($kdfname != 'bcrypt') { throw new \RuntimeException('Only the bcrypt kdf is supported (' . $kdfname . ' encountered)'); } list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); $crypto = new AES('ctr'); //$crypto->setKeyLength(256); //$crypto->disablePadding(); $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); break; default: throw new \RuntimeException('The only supported ciphers are: none, aes256-ctr (' . $ciphername . ' is being used)'); } list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key); list($type) = Strings::unpackSSH2('s', $publicKey); if (isset($crypto)) { $paddedKey = $crypto->decrypt($paddedKey); } list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey); // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc. if ($checkint1 != $checkint2) { if (isset($crypto)) { throw new BadDecryptionException('Unable to decrypt key - please verify the password you are using'); } throw new \RuntimeException("The two checkints do not match ($checkint1 vs. $checkint2)"); } self::checkType($type); return compact('type', 'publicKey', 'paddedKey'); } $parts = explode(' ', $key, 3); if (!isset($parts[1])) { $key = base64_decode($parts[0]); $comment = false; } else { $asciiType = $parts[0]; self::checkType($parts[0]); $key = base64_decode($parts[1]); $comment = isset($parts[2]) ? $parts[2] : false; } if ($key === false) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } list($type) = Strings::unpackSSH2('s', $key); self::checkType($type); if (isset($asciiType) && $asciiType != $type) { throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type); } if (strlen($key) <= 4) { throw new \UnexpectedValueException('Key appears to be malformed'); } $publicKey = $key; return compact('type', 'publicKey', 'comment'); } /** * Toggle between binary and printable keys * * Printable keys are what are generated by default. These are the ones that go in * $HOME/.ssh/authorized_key. * * @param bool $enabled */ public static function setBinaryOutput($enabled) { self::$binary = $enabled; } /** * Checks to see if the type is valid * * @param string $candidate */ private static function checkType($candidate) { if (!in_array($candidate, static::$types)) { throw new \RuntimeException("The key type ($candidate) is not equal to: " . implode(',', static::$types)); } } /** * Wrap a private key appropriately * * @param string $publicKey * @param string $privateKey * @param string $password * @param array $options * @return string */ protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options) { list(, $checkint) = unpack('N', Random::string(4)); $comment = isset($options['comment']) ? $options['comment'] : self::$comment; $paddedKey = Strings::packSSH2('NN', $checkint, $checkint) . $privateKey . Strings::packSSH2('s', $comment); $usesEncryption = !empty($password) && is_string($password); /* from http://tools.ietf.org/html/rfc4253#section-6 : Note that the length of the concatenation of 'packet_length', 'padding_length', 'payload', and 'random padding' MUST be a multiple of the cipher block size or 8, whichever is larger. */ $blockSize = $usesEncryption ? 16 : 8; $paddingLength = (($blockSize - 1) * strlen($paddedKey)) % $blockSize; for ($i = 1; $i <= $paddingLength; $i++) { $paddedKey .= chr($i); } if (!$usesEncryption) { $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); } else { $rounds = isset($options['rounds']) ? $options['rounds'] : 16; $salt = Random::string(16); $kdfoptions = Strings::packSSH2('sN', $salt, $rounds); $crypto = new AES('ctr'); $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); $paddedKey = $crypto->encrypt($paddedKey); $key = Strings::packSSH2('sssNss', 'aes256-ctr', 'bcrypt', $kdfoptions, 1, $publicKey, $paddedKey); } $key = "openssh-key-v1\0$key"; return "-----BEGIN OPENSSH PRIVATE KEY-----\n" . chunk_split(Strings::base64_encode($key), 70, "\n") . "-----END OPENSSH PRIVATE KEY-----\n"; } } PK!IȥHvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS1.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\AES; use phpseclib3\Crypt\DES; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\TripleDES; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\File\ASN1; /** * PKCS1 Formatted Key Handler * * @author Jim Wigginton */ abstract class PKCS1 extends PKCS { /** * Default encryption algorithm * * @var string */ private static $defaultEncryptionAlgorithm = 'AES-128-CBC'; /** * Sets the default encryption algorithm * * @param string $algo */ public static function setEncryptionAlgorithm($algo) { self::$defaultEncryptionAlgorithm = $algo; } /** * Returns the mode constant corresponding to the mode string * * @param string $mode * @return int * @throws \UnexpectedValueException if the block cipher mode is unsupported */ private static function getEncryptionMode($mode) { switch ($mode) { case 'CBC': case 'ECB': case 'CFB': case 'OFB': case 'CTR': return $mode; } throw new \UnexpectedValueException('Unsupported block cipher mode of operation'); } /** * Returns a cipher object corresponding to a string * * @param string $algo * @return string * @throws \UnexpectedValueException if the encryption algorithm is unsupported */ private static function getEncryptionObject($algo) { $modes = '(CBC|ECB|CFB|OFB|CTR)'; switch (true) { case preg_match("#^AES-(128|192|256)-$modes$#", $algo, $matches): $cipher = new AES(self::getEncryptionMode($matches[2])); $cipher->setKeyLength($matches[1]); return $cipher; case preg_match("#^DES-EDE3-$modes$#", $algo, $matches): return new TripleDES(self::getEncryptionMode($matches[1])); case preg_match("#^DES-$modes$#", $algo, $matches): return new DES(self::getEncryptionMode($matches[1])); default: throw new UnsupportedAlgorithmException($algo . ' is not a supported algorithm'); } } /** * Generate a symmetric key for PKCS#1 keys * * @param string $password * @param string $iv * @param int $length * @return string */ private static function generateSymmetricKey($password, $iv, $length) { $symkey = ''; $iv = substr($iv, 0, 8); while (strlen($symkey) < $length) { $symkey .= md5($symkey . $password . $iv, true); } return substr($symkey, 0, $length); } /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ protected static function load($key, $password) { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } /* Although PKCS#1 proposes a format that public and private keys can use, encrypting them is "outside the scope" of PKCS#1. PKCS#1 then refers you to PKCS#12 and PKCS#15 if you're wanting to protect private keys, however, that's not what OpenSSL* does. OpenSSL protects private keys by adding two new "fields" to the key - DEK-Info and Proc-Type. These fields are discussed here: http://tools.ietf.org/html/rfc1421#section-4.6.1.1 http://tools.ietf.org/html/rfc1421#section-4.6.1.3 DES-EDE3-CBC as an algorithm, however, is not discussed anywhere, near as I can tell. DES-CBC and DES-EDE are discussed in RFC1423, however, DES-EDE3-CBC isn't, nor is its key derivation function. As is, the definitive authority on this encoding scheme isn't the IETF but rather OpenSSL's own implementation. ie. the implementation *is* the standard and any bugs that may exist in that implementation are part of the standard, as well. * OpenSSL is the de facto standard. It's utilized by OpenSSH and other projects */ if (preg_match('#DEK-Info: (.+),(.+)#', $key, $matches)) { $iv = Strings::hex2bin(trim($matches[2])); // remove the Proc-Type / DEK-Info sections as they're no longer needed $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $key); $ciphertext = ASN1::extractBER($key); if ($ciphertext === false) { $ciphertext = $key; } $crypto = self::getEncryptionObject($matches[1]); $crypto->setKey(self::generateSymmetricKey($password, $iv, $crypto->getKeyLength() >> 3)); $crypto->setIV($iv); $key = $crypto->decrypt($ciphertext); } else { if (self::$format != self::MODE_DER) { $decoded = ASN1::extractBER($key); if ($decoded !== false) { $key = $decoded; } elseif (self::$format == self::MODE_PEM) { throw new \UnexpectedValueException('Expected base64-encoded PEM format but was unable to decode base64 text'); } } } return $key; } /** * Wrap a private key appropriately * * @param string $key * @param string $type * @param string $password * @param array $options optional * @return string */ protected static function wrapPrivateKey($key, $type, $password, array $options = []) { if (empty($password) || !is_string($password)) { return "-----BEGIN $type PRIVATE KEY-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END $type PRIVATE KEY-----"; } $encryptionAlgorithm = isset($options['encryptionAlgorithm']) ? $options['encryptionAlgorithm'] : self::$defaultEncryptionAlgorithm; $cipher = self::getEncryptionObject($encryptionAlgorithm); $iv = Random::string($cipher->getBlockLength() >> 3); $cipher->setKey(self::generateSymmetricKey($password, $iv, $cipher->getKeyLength() >> 3)); $cipher->setIV($iv); $iv = strtoupper(Strings::bin2hex($iv)); return "-----BEGIN $type PRIVATE KEY-----\r\n" . "Proc-Type: 4,ENCRYPTED\r\n" . "DEK-Info: " . $encryptionAlgorithm . ",$iv\r\n" . "\r\n" . chunk_split(Strings::base64_encode($cipher->encrypt($key)), 64) . "-----END $type PRIVATE KEY-----"; } /** * Wrap a public key appropriately * * @param string $key * @param string $type * @return string */ protected static function wrapPublicKey($key, $type) { return "-----BEGIN $type PUBLIC KEY-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END $type PUBLIC KEY-----"; } } PK!)ppHvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS8.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\AES; use phpseclib3\Crypt\DES; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\RC2; use phpseclib3\Crypt\RC4; use phpseclib3\Crypt\TripleDES; use phpseclib3\Exception\InsufficientSetupException; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; /** * PKCS#8 Formatted Key Handler * * @author Jim Wigginton */ abstract class PKCS8 extends PKCS { /** * Default encryption algorithm * * @var string */ private static $defaultEncryptionAlgorithm = 'id-PBES2'; /** * Default encryption scheme * * Only used when defaultEncryptionAlgorithm is id-PBES2 * * @var string */ private static $defaultEncryptionScheme = 'aes128-CBC-PAD'; /** * Default PRF * * Only used when defaultEncryptionAlgorithm is id-PBES2 * * @var string */ private static $defaultPRF = 'id-hmacWithSHA256'; /** * Default Iteration Count * * @var int */ private static $defaultIterationCount = 2048; /** * OIDs loaded * * @var bool */ private static $oidsLoaded = false; /** * Binary key flag * * @var bool */ private static $binary = false; /** * Sets the default encryption algorithm * * @param string $algo */ public static function setEncryptionAlgorithm($algo) { self::$defaultEncryptionAlgorithm = $algo; } /** * Sets the default encryption algorithm for PBES2 * * @param string $algo */ public static function setEncryptionScheme($algo) { self::$defaultEncryptionScheme = $algo; } /** * Sets the iteration count * * @param int $count */ public static function setIterationCount($count) { self::$defaultIterationCount = $count; } /** * Sets the PRF for PBES2 * * @param string $algo */ public static function setPRF($algo) { self::$defaultPRF = $algo; } /** * Returns a SymmetricKey object based on a PBES1 $algo * * @return \phpseclib3\Crypt\Common\SymmetricKey * @param string $algo */ private static function getPBES1EncryptionObject($algo) { $algo = preg_match('#^pbeWith(?:MD2|MD5|SHA1|SHA)And(.*?)-CBC$#', $algo, $matches) ? $matches[1] : substr($algo, 13); // strlen('pbeWithSHAAnd') == 13 switch ($algo) { case 'DES': $cipher = new DES('cbc'); break; case 'RC2': $cipher = new RC2('cbc'); $cipher->setKeyLength(64); break; case '3-KeyTripleDES': $cipher = new TripleDES('cbc'); break; case '2-KeyTripleDES': $cipher = new TripleDES('cbc'); $cipher->setKeyLength(128); break; case '128BitRC2': $cipher = new RC2('cbc'); $cipher->setKeyLength(128); break; case '40BitRC2': $cipher = new RC2('cbc'); $cipher->setKeyLength(40); break; case '128BitRC4': $cipher = new RC4(); $cipher->setKeyLength(128); break; case '40BitRC4': $cipher = new RC4(); $cipher->setKeyLength(40); break; default: throw new UnsupportedAlgorithmException("$algo is not a supported algorithm"); } return $cipher; } /** * Returns a hash based on a PBES1 $algo * * @return string * @param string $algo */ private static function getPBES1Hash($algo) { if (preg_match('#^pbeWith(MD2|MD5|SHA1|SHA)And.*?-CBC$#', $algo, $matches)) { return $matches[1] == 'SHA' ? 'sha1' : $matches[1]; } return 'sha1'; } /** * Returns a KDF baesd on a PBES1 $algo * * @return string * @param string $algo */ private static function getPBES1KDF($algo) { switch ($algo) { case 'pbeWithMD2AndDES-CBC': case 'pbeWithMD2AndRC2-CBC': case 'pbeWithMD5AndDES-CBC': case 'pbeWithMD5AndRC2-CBC': case 'pbeWithSHA1AndDES-CBC': case 'pbeWithSHA1AndRC2-CBC': return 'pbkdf1'; } return 'pkcs12'; } /** * Returns a SymmetricKey object baesd on a PBES2 $algo * * @return SymmetricKey * @param string $algo */ private static function getPBES2EncryptionObject($algo) { switch ($algo) { case 'desCBC': $cipher = new DES('cbc'); break; case 'des-EDE3-CBC': $cipher = new TripleDES('cbc'); break; case 'rc2CBC': $cipher = new RC2('cbc'); // in theory this can be changed $cipher->setKeyLength(128); break; case 'rc5-CBC-PAD': throw new UnsupportedAlgorithmException('rc5-CBC-PAD is not supported for PBES2 PKCS#8 keys'); case 'aes128-CBC-PAD': case 'aes192-CBC-PAD': case 'aes256-CBC-PAD': $cipher = new AES('cbc'); $cipher->setKeyLength(substr($algo, 3, 3)); break; default: throw new UnsupportedAlgorithmException("$algo is not supported"); } return $cipher; } /** * Initialize static variables * */ private static function initialize_static_variables() { if (!isset(static::$childOIDsLoaded)) { throw new InsufficientSetupException('This class should not be called directly'); } if (!static::$childOIDsLoaded) { ASN1::loadOIDs(is_array(static::OID_NAME) ? array_combine(static::OID_NAME, static::OID_VALUE) : [static::OID_NAME => static::OID_VALUE]); static::$childOIDsLoaded = true; } if (!self::$oidsLoaded) { // from https://tools.ietf.org/html/rfc2898 ASN1::loadOIDs([ // PBES1 encryption schemes 'pbeWithMD2AndDES-CBC' => '1.2.840.113549.1.5.1', 'pbeWithMD2AndRC2-CBC' => '1.2.840.113549.1.5.4', 'pbeWithMD5AndDES-CBC' => '1.2.840.113549.1.5.3', 'pbeWithMD5AndRC2-CBC' => '1.2.840.113549.1.5.6', 'pbeWithSHA1AndDES-CBC' => '1.2.840.113549.1.5.10', 'pbeWithSHA1AndRC2-CBC' => '1.2.840.113549.1.5.11', // from PKCS#12: // https://tools.ietf.org/html/rfc7292 'pbeWithSHAAnd128BitRC4' => '1.2.840.113549.1.12.1.1', 'pbeWithSHAAnd40BitRC4' => '1.2.840.113549.1.12.1.2', 'pbeWithSHAAnd3-KeyTripleDES-CBC' => '1.2.840.113549.1.12.1.3', 'pbeWithSHAAnd2-KeyTripleDES-CBC' => '1.2.840.113549.1.12.1.4', 'pbeWithSHAAnd128BitRC2-CBC' => '1.2.840.113549.1.12.1.5', 'pbeWithSHAAnd40BitRC2-CBC' => '1.2.840.113549.1.12.1.6', 'id-PBKDF2' => '1.2.840.113549.1.5.12', 'id-PBES2' => '1.2.840.113549.1.5.13', 'id-PBMAC1' => '1.2.840.113549.1.5.14', // from PKCS#5 v2.1: // http://www.rsa.com/rsalabs/pkcs/files/h11302-wp-pkcs5v2-1-password-based-cryptography-standard.pdf 'id-hmacWithSHA1' => '1.2.840.113549.2.7', 'id-hmacWithSHA224' => '1.2.840.113549.2.8', 'id-hmacWithSHA256' => '1.2.840.113549.2.9', 'id-hmacWithSHA384' => '1.2.840.113549.2.10', 'id-hmacWithSHA512' => '1.2.840.113549.2.11', 'id-hmacWithSHA512-224' => '1.2.840.113549.2.12', 'id-hmacWithSHA512-256' => '1.2.840.113549.2.13', 'desCBC' => '1.3.14.3.2.7', 'des-EDE3-CBC' => '1.2.840.113549.3.7', 'rc2CBC' => '1.2.840.113549.3.2', 'rc5-CBC-PAD' => '1.2.840.113549.3.9', 'aes128-CBC-PAD' => '2.16.840.1.101.3.4.1.2', 'aes192-CBC-PAD' => '2.16.840.1.101.3.4.1.22', 'aes256-CBC-PAD' => '2.16.840.1.101.3.4.1.42' ]); self::$oidsLoaded = true; } } /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ protected static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } $isPublic = strpos($key, 'PUBLIC') !== false; $isPrivate = strpos($key, 'PRIVATE') !== false; $decoded = self::preParse($key); $meta = []; $decrypted = ASN1::asn1map($decoded[0], Maps\EncryptedPrivateKeyInfo::MAP); if (strlen($password) && is_array($decrypted)) { $algorithm = $decrypted['encryptionAlgorithm']['algorithm']; switch ($algorithm) { // PBES1 case 'pbeWithMD2AndDES-CBC': case 'pbeWithMD2AndRC2-CBC': case 'pbeWithMD5AndDES-CBC': case 'pbeWithMD5AndRC2-CBC': case 'pbeWithSHA1AndDES-CBC': case 'pbeWithSHA1AndRC2-CBC': case 'pbeWithSHAAnd3-KeyTripleDES-CBC': case 'pbeWithSHAAnd2-KeyTripleDES-CBC': case 'pbeWithSHAAnd128BitRC2-CBC': case 'pbeWithSHAAnd40BitRC2-CBC': case 'pbeWithSHAAnd128BitRC4': case 'pbeWithSHAAnd40BitRC4': $cipher = self::getPBES1EncryptionObject($algorithm); $hash = self::getPBES1Hash($algorithm); $kdf = self::getPBES1KDF($algorithm); $meta['meta']['algorithm'] = $algorithm; $temp = ASN1::decodeBER($decrypted['encryptionAlgorithm']['parameters']); if (!$temp) { throw new \RuntimeException('Unable to decode BER'); } extract(ASN1::asn1map($temp[0], Maps\PBEParameter::MAP)); $iterationCount = (int) $iterationCount->toString(); $cipher->setPassword($password, $kdf, $hash, $salt, $iterationCount); $key = $cipher->decrypt($decrypted['encryptedData']); $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER 2'); } break; case 'id-PBES2': $meta['meta']['algorithm'] = $algorithm; $temp = ASN1::decodeBER($decrypted['encryptionAlgorithm']['parameters']); if (!$temp) { throw new \RuntimeException('Unable to decode BER'); } $temp = ASN1::asn1map($temp[0], Maps\PBES2params::MAP); extract($temp); $cipher = self::getPBES2EncryptionObject($encryptionScheme['algorithm']); $meta['meta']['cipher'] = $encryptionScheme['algorithm']; $temp = ASN1::decodeBER($decrypted['encryptionAlgorithm']['parameters']); if (!$temp) { throw new \RuntimeException('Unable to decode BER'); } $temp = ASN1::asn1map($temp[0], Maps\PBES2params::MAP); extract($temp); if (!$cipher instanceof RC2) { $cipher->setIV($encryptionScheme['parameters']['octetString']); } else { $temp = ASN1::decodeBER($encryptionScheme['parameters']); if (!$temp) { throw new \RuntimeException('Unable to decode BER'); } extract(ASN1::asn1map($temp[0], Maps\RC2CBCParameter::MAP)); $effectiveKeyLength = (int) $rc2ParametersVersion->toString(); switch ($effectiveKeyLength) { case 160: $effectiveKeyLength = 40; break; case 120: $effectiveKeyLength = 64; break; case 58: $effectiveKeyLength = 128; break; //default: // should be >= 256 } $cipher->setIV($iv); $cipher->setKeyLength($effectiveKeyLength); } $meta['meta']['keyDerivationFunc'] = $keyDerivationFunc['algorithm']; switch ($keyDerivationFunc['algorithm']) { case 'id-PBKDF2': $temp = ASN1::decodeBER($keyDerivationFunc['parameters']); if (!$temp) { throw new \RuntimeException('Unable to decode BER'); } $prf = ['algorithm' => 'id-hmacWithSHA1']; $params = ASN1::asn1map($temp[0], Maps\PBKDF2params::MAP); extract($params); $meta['meta']['prf'] = $prf['algorithm']; $hash = str_replace('-', '/', substr($prf['algorithm'], 11)); $params = [ $password, 'pbkdf2', $hash, $salt, (int) $iterationCount->toString() ]; if (isset($keyLength)) { $params[] = (int) $keyLength->toString(); } $cipher->setPassword(...$params); $key = $cipher->decrypt($decrypted['encryptedData']); $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER 3'); } break; default: throw new UnsupportedAlgorithmException('Only PBKDF2 is supported for PBES2 PKCS#8 keys'); } break; case 'id-PBMAC1': //$temp = ASN1::decodeBER($decrypted['encryptionAlgorithm']['parameters']); //$value = ASN1::asn1map($temp[0], Maps\PBMAC1params::MAP); // since i can't find any implementation that does PBMAC1 it is unsupported throw new UnsupportedAlgorithmException('Only PBES1 and PBES2 PKCS#8 keys are supported.'); // at this point we'll assume that the key conforms to PublicKeyInfo } } $private = ASN1::asn1map($decoded[0], Maps\OneAsymmetricKey::MAP); if (is_array($private)) { if ($isPublic) { throw new \UnexpectedValueException('Human readable string claims public key but DER encoded string claims private key'); } if (isset($private['privateKeyAlgorithm']['parameters']) && !$private['privateKeyAlgorithm']['parameters'] instanceof ASN1\Element && isset($decoded[0]['content'][1]['content'][1])) { $temp = $decoded[0]['content'][1]['content'][1]; $private['privateKeyAlgorithm']['parameters'] = new ASN1\Element(substr($key, $temp['start'], $temp['length'])); } if (is_array(static::OID_NAME)) { if (!in_array($private['privateKeyAlgorithm']['algorithm'], static::OID_NAME)) { throw new UnsupportedAlgorithmException($private['privateKeyAlgorithm']['algorithm'] . ' is not a supported key type'); } } else { if ($private['privateKeyAlgorithm']['algorithm'] != static::OID_NAME) { throw new UnsupportedAlgorithmException('Only ' . static::OID_NAME . ' keys are supported; this is a ' . $private['privateKeyAlgorithm']['algorithm'] . ' key'); } } if (isset($private['publicKey'])) { if ($private['publicKey'][0] != "\0") { throw new \UnexpectedValueException('The first byte of the public key should be null - not ' . bin2hex($private['publicKey'][0])); } $private['publicKey'] = substr($private['publicKey'], 1); } return $private + $meta; } // EncryptedPrivateKeyInfo and PublicKeyInfo have largely identical "signatures". the only difference // is that the former has an octet string and the later has a bit string. the first byte of a bit // string represents the number of bits in the last byte that are to be ignored but, currently, // bit strings wanting a non-zero amount of bits trimmed are not supported $public = ASN1::asn1map($decoded[0], Maps\PublicKeyInfo::MAP); if (is_array($public)) { if ($isPrivate) { throw new \UnexpectedValueException('Human readable string claims private key but DER encoded string claims public key'); } if ($public['publicKey'][0] != "\0") { throw new \UnexpectedValueException('The first byte of the public key should be null - not ' . bin2hex($public['publicKey'][0])); } if (is_array(static::OID_NAME)) { if (!in_array($public['publicKeyAlgorithm']['algorithm'], static::OID_NAME)) { throw new UnsupportedAlgorithmException($public['publicKeyAlgorithm']['algorithm'] . ' is not a supported key type'); } } else { if ($public['publicKeyAlgorithm']['algorithm'] != static::OID_NAME) { throw new UnsupportedAlgorithmException('Only ' . static::OID_NAME . ' keys are supported; this is a ' . $public['publicKeyAlgorithm']['algorithm'] . ' key'); } } if (isset($public['publicKeyAlgorithm']['parameters']) && !$public['publicKeyAlgorithm']['parameters'] instanceof ASN1\Element && isset($decoded[0]['content'][0]['content'][1])) { $temp = $decoded[0]['content'][0]['content'][1]; $public['publicKeyAlgorithm']['parameters'] = new ASN1\Element(substr($key, $temp['start'], $temp['length'])); } $public['publicKey'] = substr($public['publicKey'], 1); return $public; } throw new \RuntimeException('Unable to parse using either OneAsymmetricKey or PublicKeyInfo ASN1 maps'); } /** * Toggle between binary (DER) and printable (PEM) keys * * Printable keys are what are generated by default. * * @param bool $enabled */ public static function setBinaryOutput($enabled) { self::$binary = $enabled; } /** * Wrap a private key appropriately * * @param string $key * @param string $attr * @param mixed $params * @param string $password * @param string $oid optional * @param string $publicKey optional * @param array $options optional * @return string */ protected static function wrapPrivateKey($key, $attr, $params, $password, $oid = null, $publicKey = '', array $options = []) { self::initialize_static_variables(); $key = [ 'version' => 'v1', 'privateKeyAlgorithm' => [ 'algorithm' => is_string(static::OID_NAME) ? static::OID_NAME : $oid ], 'privateKey' => $key ]; if ($oid != 'id-Ed25519' && $oid != 'id-Ed448') { $key['privateKeyAlgorithm']['parameters'] = $params; } if (!empty($attr)) { $key['attributes'] = $attr; } if (!empty($publicKey)) { $key['version'] = 'v2'; $key['publicKey'] = $publicKey; } $key = ASN1::encodeDER($key, Maps\OneAsymmetricKey::MAP); if (!empty($password) && is_string($password)) { $salt = Random::string(8); $iterationCount = isset($options['iterationCount']) ? $options['iterationCount'] : self::$defaultIterationCount; $encryptionAlgorithm = isset($options['encryptionAlgorithm']) ? $options['encryptionAlgorithm'] : self::$defaultEncryptionAlgorithm; $encryptionScheme = isset($options['encryptionScheme']) ? $options['encryptionScheme'] : self::$defaultEncryptionScheme; $prf = isset($options['PRF']) ? $options['PRF'] : self::$defaultPRF; if ($encryptionAlgorithm == 'id-PBES2') { $crypto = self::getPBES2EncryptionObject($encryptionScheme); $hash = str_replace('-', '/', substr($prf, 11)); $kdf = 'pbkdf2'; $iv = Random::string($crypto->getBlockLength() >> 3); $PBKDF2params = [ 'salt' => $salt, 'iterationCount' => $iterationCount, 'prf' => ['algorithm' => $prf, 'parameters' => null] ]; $PBKDF2params = ASN1::encodeDER($PBKDF2params, Maps\PBKDF2params::MAP); if (!$crypto instanceof RC2) { $params = ['octetString' => $iv]; } else { $params = [ 'rc2ParametersVersion' => 58, 'iv' => $iv ]; $params = ASN1::encodeDER($params, Maps\RC2CBCParameter::MAP); $params = new ASN1\Element($params); } $params = [ 'keyDerivationFunc' => [ 'algorithm' => 'id-PBKDF2', 'parameters' => new ASN1\Element($PBKDF2params) ], 'encryptionScheme' => [ 'algorithm' => $encryptionScheme, 'parameters' => $params ] ]; $params = ASN1::encodeDER($params, Maps\PBES2params::MAP); $crypto->setIV($iv); } else { $crypto = self::getPBES1EncryptionObject($encryptionAlgorithm); $hash = self::getPBES1Hash($encryptionAlgorithm); $kdf = self::getPBES1KDF($encryptionAlgorithm); $params = [ 'salt' => $salt, 'iterationCount' => $iterationCount ]; $params = ASN1::encodeDER($params, Maps\PBEParameter::MAP); } $crypto->setPassword($password, $kdf, $hash, $salt, $iterationCount); $key = $crypto->encrypt($key); $key = [ 'encryptionAlgorithm' => [ 'algorithm' => $encryptionAlgorithm, 'parameters' => new ASN1\Element($params) ], 'encryptedData' => $key ]; $key = ASN1::encodeDER($key, Maps\EncryptedPrivateKeyInfo::MAP); if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $key; } return "-----BEGIN ENCRYPTED PRIVATE KEY-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END ENCRYPTED PRIVATE KEY-----"; } if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $key; } return "-----BEGIN PRIVATE KEY-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END PRIVATE KEY-----"; } /** * Wrap a public key appropriately * * @param string $key * @param mixed $params * @param string $oid * @return string */ protected static function wrapPublicKey($key, $params, $oid = null, array $options = []) { self::initialize_static_variables(); $key = [ 'publicKeyAlgorithm' => [ 'algorithm' => is_string(static::OID_NAME) ? static::OID_NAME : $oid ], 'publicKey' => "\0" . $key ]; if ($oid != 'id-Ed25519' && $oid != 'id-Ed448') { $key['publicKeyAlgorithm']['parameters'] = $params; } $key = ASN1::encodeDER($key, Maps\PublicKeyInfo::MAP); if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $key; } return "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END PUBLIC KEY-----"; } /** * Perform some preliminary parsing of the key * * @param string $key * @return array */ private static function preParse(&$key) { self::initialize_static_variables(); if (self::$format != self::MODE_DER) { $decoded = ASN1::extractBER($key); if ($decoded !== false) { $key = $decoded; } elseif (self::$format == self::MODE_PEM) { throw new \UnexpectedValueException('Expected base64-encoded PEM format but was unable to decode base64 text'); } } $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } return $decoded; } /** * Returns the encryption parameters used by the key * * @param string $key * @return array */ public static function extractEncryptionAlgorithm($key) { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } $decoded = self::preParse($key); $r = ASN1::asn1map($decoded[0], Maps\EncryptedPrivateKeyInfo::MAP); if (!is_array($r)) { throw new \RuntimeException('Unable to parse using EncryptedPrivateKeyInfo map'); } if ($r['encryptionAlgorithm']['algorithm'] == 'id-PBES2') { $decoded = ASN1::decodeBER($r['encryptionAlgorithm']['parameters']->element); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $r['encryptionAlgorithm']['parameters'] = ASN1::asn1map($decoded[0], Maps\PBES2params::MAP); $kdf = &$r['encryptionAlgorithm']['parameters']['keyDerivationFunc']; switch ($kdf['algorithm']) { case 'id-PBKDF2': $decoded = ASN1::decodeBER($kdf['parameters']->element); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $kdf['parameters'] = ASN1::asn1map($decoded[0], Maps\PBKDF2params::MAP); } } return $r['encryptionAlgorithm']; } } PK!aaGvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Keys; /** * PKCS1 Formatted Key Handler * * @author Jim Wigginton */ abstract class PKCS { /** * Auto-detect the format */ const MODE_ANY = 0; /** * Require base64-encoded PEM's be supplied */ const MODE_PEM = 1; /** * Require raw DER's be supplied */ const MODE_DER = 2; /**#@-*/ /** * Is the key a base-64 encoded PEM, DER or should it be auto-detected? * * @var int */ protected static $format = self::MODE_ANY; /** * Require base64-encoded PEM's be supplied * */ public static function requirePEM() { self::$format = self::MODE_PEM; } /** * Require raw DER's be supplied * */ public static function requireDER() { self::$format = self::MODE_DER; } /** * Accept any format and auto detect the format * * This is the default setting * */ public static function requireAny() { self::$format = self::MODE_ANY; } } PK!UV(H4H4Hvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PuTTY.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\AES; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\Random; use phpseclib3\Exception\UnsupportedAlgorithmException; /** * PuTTY Formatted Key Handler * * @author Jim Wigginton */ abstract class PuTTY { /** * Default comment * * @var string */ private static $comment = 'phpseclib-generated-key'; /** * Default version * * @var int */ private static $version = 2; /** * Sets the default comment * * @param string $comment */ public static function setComment($comment) { self::$comment = str_replace(["\r", "\n"], '', $comment); } /** * Sets the default version * * @param int $version */ public static function setVersion($version) { if ($version != 2 && $version != 3) { throw new \RuntimeException('Only supported versions are 2 and 3'); } self::$version = $version; } /** * Generate a symmetric key for PuTTY v2 keys * * @param string $password * @param int $length * @return string */ private static function generateV2Key($password, $length) { $symkey = ''; $sequence = 0; while (strlen($symkey) < $length) { $temp = pack('Na*', $sequence++, $password); $symkey .= Strings::hex2bin(sha1($temp)); } return substr($symkey, 0, $length); } /** * Generate a symmetric key for PuTTY v3 keys * * @param string $password * @param string $flavour * @param int $memory * @param int $passes * @param string $salt * @return array */ private static function generateV3Key($password, $flavour, $memory, $passes, $salt) { if (!function_exists('sodium_crypto_pwhash')) { throw new \RuntimeException('sodium_crypto_pwhash needs to exist for Argon2 password hasing'); } switch ($flavour) { case 'Argon2i': $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13; break; case 'Argon2id': $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13; break; default: throw new UnsupportedAlgorithmException('Only Argon2i and Argon2id are supported'); } $length = 80; // keylen + ivlen + mac_keylen $temp = sodium_crypto_pwhash($length, $password, $salt, $passes, $memory << 10, $flavour); $symkey = substr($temp, 0, 32); $symiv = substr($temp, 32, 16); $hashkey = substr($temp, -32); return compact('symkey', 'symiv', 'hashkey'); } /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password * @return array */ public static function load($key, $password) { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } if (strpos($key, 'BEGIN SSH2 PUBLIC KEY') !== false) { $lines = preg_split('#[\r\n]+#', $key); switch (true) { case $lines[0] != '---- BEGIN SSH2 PUBLIC KEY ----': throw new \UnexpectedValueException('Key doesn\'t start with ---- BEGIN SSH2 PUBLIC KEY ----'); case $lines[count($lines) - 1] != '---- END SSH2 PUBLIC KEY ----': throw new \UnexpectedValueException('Key doesn\'t end with ---- END SSH2 PUBLIC KEY ----'); } $lines = array_splice($lines, 1, -1); $lines = array_map(function ($line) { return rtrim($line, "\r\n"); }, $lines); $data = $current = ''; $values = []; $in_value = false; foreach ($lines as $line) { switch (true) { case preg_match('#^(.*?): (.*)#', $line, $match): $in_value = $line[strlen($line) - 1] == '\\'; $current = strtolower($match[1]); $values[$current] = $in_value ? substr($match[2], 0, -1) : $match[2]; break; case $in_value: $in_value = $line[strlen($line) - 1] == '\\'; $values[$current] .= $in_value ? substr($line, 0, -1) : $line; break; default: $data .= $line; } } $components = call_user_func([static::PUBLIC_HANDLER, 'load'], $data); if ($components === false) { throw new \UnexpectedValueException('Unable to decode public key'); } $components += $values; $components['comment'] = str_replace(['\\\\', '\"'], ['\\', '"'], $values['comment']); return $components; } $components = []; $key = preg_split('#\r\n|\r|\n#', trim($key)); if (Strings::shift($key[0], strlen('PuTTY-User-Key-File-')) != 'PuTTY-User-Key-File-') { return false; } $version = (int) Strings::shift($key[0], 3); // should be either "2: " or "3: 0" prior to int casting if ($version != 2 && $version != 3) { throw new \RuntimeException('Only v2 and v3 PuTTY private keys are supported'); } $components['type'] = $type = rtrim($key[0]); if (!in_array($type, static::$types)) { $error = count(static::$types) == 1 ? 'Only ' . static::$types[0] . ' keys are supported. ' : ''; throw new UnsupportedAlgorithmException($error . 'This is an unsupported ' . $type . ' key'); } $encryption = trim(preg_replace('#Encryption: (.+)#', '$1', $key[1])); $components['comment'] = trim(preg_replace('#Comment: (.+)#', '$1', $key[2])); $publicLength = trim(preg_replace('#Public-Lines: (\d+)#', '$1', $key[3])); $public = Strings::base64_decode(implode('', array_map('trim', array_slice($key, 4, $publicLength)))); $source = Strings::packSSH2('ssss', $type, $encryption, $components['comment'], $public); extract(unpack('Nlength', Strings::shift($public, 4))); $newtype = Strings::shift($public, $length); if ($newtype != $type) { throw new \RuntimeException('The binary type does not match the human readable type field'); } $components['public'] = $public; switch ($version) { case 3: $hashkey = ''; break; case 2: $hashkey = 'putty-private-key-file-mac-key'; } $offset = $publicLength + 4; switch ($encryption) { case 'aes256-cbc': $crypto = new AES('cbc'); switch ($version) { case 3: $flavour = trim(preg_replace('#Key-Derivation: (.*)#', '$1', $key[$offset++])); $memory = trim(preg_replace('#Argon2-Memory: (\d+)#', '$1', $key[$offset++])); $passes = trim(preg_replace('#Argon2-Passes: (\d+)#', '$1', $key[$offset++])); $parallelism = trim(preg_replace('#Argon2-Parallelism: (\d+)#', '$1', $key[$offset++])); $salt = Strings::hex2bin(trim(preg_replace('#Argon2-Salt: ([0-9a-f]+)#', '$1', $key[$offset++]))); extract(self::generateV3Key($password, $flavour, $memory, $passes, $salt)); break; case 2: $symkey = self::generateV2Key($password, 32); $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3); $hashkey .= $password; } } switch ($version) { case 3: $hash = new Hash('sha256'); $hash->setKey($hashkey); break; case 2: $hash = new Hash('sha1'); $hash->setKey(sha1($hashkey, true)); } $privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$offset++])); $private = Strings::base64_decode(implode('', array_map('trim', array_slice($key, $offset, $privateLength)))); if ($encryption != 'none') { $crypto->setKey($symkey); $crypto->setIV($symiv); $crypto->disablePadding(); $private = $crypto->decrypt($private); } $source .= Strings::packSSH2('s', $private); $hmac = trim(preg_replace('#Private-MAC: (.+)#', '$1', $key[$offset + $privateLength])); $hmac = Strings::hex2bin($hmac); if (!hash_equals($hash->hash($source), $hmac)) { throw new \UnexpectedValueException('MAC validation error'); } $components['private'] = $private; return $components; } /** * Wrap a private key appropriately * * @param string $public * @param string $private * @param string $type * @param string $password * @param array $options optional * @return string */ protected static function wrapPrivateKey($public, $private, $type, $password, array $options = []) { $encryption = (!empty($password) || is_string($password)) ? 'aes256-cbc' : 'none'; $comment = isset($options['comment']) ? $options['comment'] : self::$comment; $version = isset($options['version']) ? $options['version'] : self::$version; $key = "PuTTY-User-Key-File-$version: $type\r\n"; $key .= "Encryption: $encryption\r\n"; $key .= "Comment: $comment\r\n"; $public = Strings::packSSH2('s', $type) . $public; $source = Strings::packSSH2('ssss', $type, $encryption, $comment, $public); $public = Strings::base64_encode($public); $key .= "Public-Lines: " . ((strlen($public) + 63) >> 6) . "\r\n"; $key .= chunk_split($public, 64); if (empty($password) && !is_string($password)) { $source .= Strings::packSSH2('s', $private); switch ($version) { case 3: $hash = new Hash('sha256'); $hash->setKey(''); break; case 2: $hash = new Hash('sha1'); $hash->setKey(sha1('putty-private-key-file-mac-key', true)); } } else { $private .= Random::string(16 - (strlen($private) & 15)); $source .= Strings::packSSH2('s', $private); $crypto = new AES('cbc'); switch ($version) { case 3: $salt = Random::string(16); $key .= "Key-Derivation: Argon2id\r\n"; $key .= "Argon2-Memory: 8192\r\n"; $key .= "Argon2-Passes: 13\r\n"; $key .= "Argon2-Parallelism: 1\r\n"; $key .= "Argon2-Salt: " . Strings::bin2hex($salt) . "\r\n"; extract(self::generateV3Key($password, 'Argon2id', 8192, 13, $salt)); $hash = new Hash('sha256'); $hash->setKey($hashkey); break; case 2: $symkey = self::generateV2Key($password, 32); $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3); $hashkey = 'putty-private-key-file-mac-key' . $password; $hash = new Hash('sha1'); $hash->setKey(sha1($hashkey, true)); } $crypto->setKey($symkey); $crypto->setIV($symiv); $crypto->disablePadding(); $private = $crypto->encrypt($private); $mac = $hash->hash($source); } $private = Strings::base64_encode($private); $key .= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n"; $key .= chunk_split($private, 64); $key .= 'Private-MAC: ' . Strings::bin2hex($hash->hash($source)) . "\r\n"; return $key; } /** * Wrap a public key appropriately * * This is basically the format described in RFC 4716 (https://tools.ietf.org/html/rfc4716) * * @param string $key * @param string $type * @return string */ protected static function wrapPublicKey($key, $type) { $key = pack('Na*a*', strlen($type), $type, $key); $key = "---- BEGIN SSH2 PUBLIC KEY ----\r\n" . 'Comment: "' . str_replace(['\\', '"'], ['\\\\', '\"'], self::$comment) . "\"\r\n" . chunk_split(Strings::base64_encode($key), 64) . '---- END SSH2 PUBLIC KEY ----'; return $key; } } PK!aEKvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Signature/Raw.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Formats\Signature; use phpseclib3\Math\BigInteger; /** * Raw Signature Handler * * @author Jim Wigginton */ abstract class Raw { /** * Loads a signature * * @param array $sig * @return array|bool */ public static function load($sig) { switch (true) { case !is_array($sig): case !isset($sig['r']) || !isset($sig['s']): case !$sig['r'] instanceof BigInteger: case !$sig['s'] instanceof BigInteger: return false; } return [ 'r' => $sig['r'], 's' => $sig['s'] ]; } /** * Returns a signature in the appropriate format * * @param BigInteger $r * @param BigInteger $s * @return string */ public static function save(BigInteger $r, BigInteger $s) { return compact('r', 's'); } } PK!rNHvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Traits/Fingerprint.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Traits; use phpseclib3\Crypt\Hash; /** * Fingerprint Trait for Private Keys * * @author Jim Wigginton */ trait Fingerprint { /** * Returns the public key's fingerprint * * The public key's fingerprint is returned, which is equivalent to running `ssh-keygen -lf rsa.pub`. If there is * no public key currently loaded, false is returned. * Example output (md5): "c1:b1:30:29:d7:b8:de:6c:97:77:10:d7:46:41:63:87" (as specified by RFC 4716) * * @param string $algorithm The hashing algorithm to be used. Valid options are 'md5' and 'sha256'. False is returned * for invalid values. * @return mixed */ public function getFingerprint($algorithm = 'md5') { $type = self::validatePlugin('Keys', 'OpenSSH', 'savePublicKey'); if ($type === false) { return false; } $key = $this->toString('OpenSSH', ['binary' => true]); if ($key === false) { return false; } switch ($algorithm) { case 'sha256': $hash = new Hash('sha256'); $base = base64_encode($hash->hash($key)); return substr($base, 0, strlen($base) - 1); case 'md5': return substr(chunk_split(md5($key), 2, ':'), 0, -1); default: return false; } } } PK!ɛ**Nvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Traits/PasswordProtected.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common\Traits; /** * Password Protected Trait for Private Keys * * @author Jim Wigginton */ trait PasswordProtected { /** * Password * * @var string|bool */ private $password = false; /** * Sets the password * * Private keys can be encrypted with a password. To unset the password, pass in the empty string or false. * Or rather, pass in $password such that empty($password) && !is_string($password) is true. * * @see self::createKey() * @see self::load() * @param string|bool $password */ public function withPassword($password = false) { $new = clone $this; $new->password = $password; return $new; } } PK!`v 4;4;Cvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common; use phpseclib3\Crypt\DSA; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\RSA; use phpseclib3\Exception\NoKeyLoadedException; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\Math\BigInteger; /** * Base Class for all asymmetric cipher classes * * @author Jim Wigginton */ abstract class AsymmetricKey { /** * Precomputed Zero * * @var BigInteger */ protected static $zero; /** * Precomputed One * * @var BigInteger */ protected static $one; /** * Format of the loaded key * * @var string */ protected $format; /** * Hash function * * @var Hash */ protected $hash; /** * HMAC function * * @var Hash */ private $hmac; /** * Supported plugins (lower case) * * @see self::initialize_static_variables() * @var array */ private static $plugins = []; /** * Invisible plugins * * @see self::initialize_static_variables() * @var array */ private static $invisiblePlugins = []; /** * Available Engines * * @var boolean[] */ protected static $engines = []; /** * Key Comment * * @var null|string */ private $comment; /** * @param string $type * @return array|string */ abstract public function toString($type, array $options = []); /** * The constructor */ protected function __construct() { self::initialize_static_variables(); $this->hash = new Hash('sha256'); $this->hmac = new Hash('sha256'); } /** * Initialize static variables */ protected static function initialize_static_variables() { if (!isset(self::$zero)) { self::$zero = new BigInteger(0); self::$one = new BigInteger(1); } self::loadPlugins('Keys'); if (static::ALGORITHM != 'RSA' && static::ALGORITHM != 'DH') { self::loadPlugins('Signature'); } } /** * Load the key * * @param string $key * @param string $password optional * @return PublicKey|PrivateKey */ public static function load($key, $password = false) { self::initialize_static_variables(); $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('load() should not be called from final classes (' . static::class . ')'); } $components = false; foreach (self::$plugins[static::ALGORITHM]['Keys'] as $format) { if (isset(self::$invisiblePlugins[static::ALGORITHM]) && in_array($format, self::$invisiblePlugins[static::ALGORITHM])) { continue; } try { $components = $format::load($key, $password); } catch (\Exception $e) { $components = false; } if ($components !== false) { break; } } if ($components === false) { throw new NoKeyLoadedException('Unable to read key'); } $components['format'] = $format; $components['secret'] = isset($components['secret']) ? $components['secret'] : ''; $comment = isset($components['comment']) ? $components['comment'] : null; $new = static::onLoad($components); $new->format = $format; $new->comment = $comment; return $new instanceof PrivateKey ? $new->withPassword($password) : $new; } /** * Loads a private key * * @return PrivateKey * @param string|array $key * @param string $password optional */ public static function loadPrivateKey($key, $password = '') { $key = self::load($key, $password); if (!$key instanceof PrivateKey) { throw new NoKeyLoadedException('The key that was loaded was not a private key'); } return $key; } /** * Loads a public key * * @return PublicKey * @param string|array $key */ public static function loadPublicKey($key) { $key = self::load($key); if (!$key instanceof PublicKey) { throw new NoKeyLoadedException('The key that was loaded was not a public key'); } return $key; } /** * Loads parameters * * @return AsymmetricKey * @param string|array $key */ public static function loadParameters($key) { $key = self::load($key); if (!$key instanceof PrivateKey && !$key instanceof PublicKey) { throw new NoKeyLoadedException('The key that was loaded was not a parameter'); } return $key; } /** * Load the key, assuming a specific format * * @param string $type * @param string $key * @param string $password optional * @return static */ public static function loadFormat($type, $key, $password = false) { self::initialize_static_variables(); $components = false; $format = strtolower($type); if (isset(self::$plugins[static::ALGORITHM]['Keys'][$format])) { $format = self::$plugins[static::ALGORITHM]['Keys'][$format]; $components = $format::load($key, $password); } if ($components === false) { throw new NoKeyLoadedException('Unable to read key'); } $components['format'] = $format; $components['secret'] = isset($components['secret']) ? $components['secret'] : ''; $new = static::onLoad($components); $new->format = $format; return $new instanceof PrivateKey ? $new->withPassword($password) : $new; } /** * Loads a private key * * @return PrivateKey * @param string $type * @param string $key * @param string $password optional */ public static function loadPrivateKeyFormat($type, $key, $password = false) { $key = self::loadFormat($type, $key, $password); if (!$key instanceof PrivateKey) { throw new NoKeyLoadedException('The key that was loaded was not a private key'); } return $key; } /** * Loads a public key * * @return PublicKey * @param string $type * @param string $key */ public static function loadPublicKeyFormat($type, $key) { $key = self::loadFormat($type, $key); if (!$key instanceof PublicKey) { throw new NoKeyLoadedException('The key that was loaded was not a public key'); } return $key; } /** * Loads parameters * * @return AsymmetricKey * @param string $type * @param string|array $key */ public static function loadParametersFormat($type, $key) { $key = self::loadFormat($type, $key); if (!$key instanceof PrivateKey && !$key instanceof PublicKey) { throw new NoKeyLoadedException('The key that was loaded was not a parameter'); } return $key; } /** * Validate Plugin * * @param string $format * @param string $type * @param string $method optional * @return mixed */ protected static function validatePlugin($format, $type, $method = null) { $type = strtolower($type); if (!isset(self::$plugins[static::ALGORITHM][$format][$type])) { throw new UnsupportedFormatException("$type is not a supported format"); } $type = self::$plugins[static::ALGORITHM][$format][$type]; if (isset($method) && !method_exists($type, $method)) { throw new UnsupportedFormatException("$type does not implement $method"); } return $type; } /** * Load Plugins * * @param string $format */ private static function loadPlugins($format) { if (!isset(self::$plugins[static::ALGORITHM][$format])) { self::$plugins[static::ALGORITHM][$format] = []; foreach (new \DirectoryIterator(__DIR__ . '/../' . static::ALGORITHM . '/Formats/' . $format . '/') as $file) { if ($file->getExtension() != 'php') { continue; } $name = $file->getBasename('.php'); if ($name[0] == '.') { continue; } $type = 'phpseclib3\Crypt\\' . static::ALGORITHM . '\\Formats\\' . $format . '\\' . $name; $reflect = new \ReflectionClass($type); if ($reflect->isTrait()) { continue; } self::$plugins[static::ALGORITHM][$format][strtolower($name)] = $type; if ($reflect->hasConstant('IS_INVISIBLE')) { self::$invisiblePlugins[static::ALGORITHM][] = $type; } } } } /** * Returns a list of supported formats. * * @return array */ public static function getSupportedKeyFormats() { self::initialize_static_variables(); return self::$plugins[static::ALGORITHM]['Keys']; } /** * Add a fileformat plugin * * The plugin needs to either already be loaded or be auto-loadable. * Loading a plugin whose shortname overwrite an existing shortname will overwrite the old plugin. * * @see self::load() * @param string $fullname * @return bool */ public static function addFileFormat($fullname) { self::initialize_static_variables(); if (class_exists($fullname)) { $meta = new \ReflectionClass($fullname); $shortname = $meta->getShortName(); self::$plugins[static::ALGORITHM]['Keys'][strtolower($shortname)] = $fullname; if ($meta->hasConstant('IS_INVISIBLE')) { self::$invisiblePlugins[static::ALGORITHM][] = strtolower($shortname); } } } /** * Returns the format of the loaded key. * * If the key that was loaded wasn't in a valid or if the key was auto-generated * with RSA::createKey() then this will throw an exception. * * @see self::load() * @return mixed */ public function getLoadedFormat() { if (empty($this->format)) { throw new NoKeyLoadedException('This key was created with createKey - it was not loaded with load. Therefore there is no "loaded format"'); } $meta = new \ReflectionClass($this->format); return $meta->getShortName(); } /** * Returns the key's comment * * Not all key formats support comments. If you want to set a comment use toString() * * @return null|string */ public function getComment() { return $this->comment; } /** * Tests engine validity * */ public static function useBestEngine() { static::$engines = [ 'PHP' => true, 'OpenSSL' => extension_loaded('openssl'), // this test can be satisfied by either of the following: // http://php.net/manual/en/book.sodium.php // https://github.com/paragonie/sodium_compat 'libsodium' => function_exists('sodium_crypto_sign_keypair') ]; return static::$engines; } /** * Flag to use internal engine only (useful for unit testing) * */ public static function useInternalEngine() { static::$engines = [ 'PHP' => true, 'OpenSSL' => false, 'libsodium' => false ]; } /** * __toString() magic method * * @return string */ public function __toString() { return $this->toString('PKCS8'); } /** * Determines which hashing function should be used * * @param string $hash */ public function withHash($hash) { $new = clone $this; $new->hash = new Hash($hash); $new->hmac = new Hash($hash); return $new; } /** * Returns the hash algorithm currently being used * */ public function getHash() { return clone $this->hash; } /** * Compute the pseudorandom k for signature generation, * using the process specified for deterministic DSA. * * @param string $h1 * @return string */ protected function computek($h1) { $v = str_repeat("\1", strlen($h1)); $k = str_repeat("\0", strlen($h1)); $x = $this->int2octets($this->x); $h1 = $this->bits2octets($h1); $this->hmac->setKey($k); $k = $this->hmac->hash($v . "\0" . $x . $h1); $this->hmac->setKey($k); $v = $this->hmac->hash($v); $k = $this->hmac->hash($v . "\1" . $x . $h1); $this->hmac->setKey($k); $v = $this->hmac->hash($v); $qlen = $this->q->getLengthInBytes(); while (true) { $t = ''; while (strlen($t) < $qlen) { $v = $this->hmac->hash($v); $t = $t . $v; } $k = $this->bits2int($t); if (!$k->equals(self::$zero) && $k->compare($this->q) < 0) { break; } $k = $this->hmac->hash($v . "\0"); $this->hmac->setKey($k); $v = $this->hmac->hash($v); } return $k; } /** * Integer to Octet String * * @param BigInteger $v * @return string */ private function int2octets($v) { $out = $v->toBytes(); $rolen = $this->q->getLengthInBytes(); if (strlen($out) < $rolen) { return str_pad($out, $rolen, "\0", STR_PAD_LEFT); } elseif (strlen($out) > $rolen) { return substr($out, -$rolen); } else { return $out; } } /** * Bit String to Integer * * @param string $in * @return BigInteger */ protected function bits2int($in) { $v = new BigInteger($in, 256); $vlen = strlen($in) << 3; $qlen = $this->q->getLength(); if ($vlen > $qlen) { return $v->bitwise_rightShift($vlen - $qlen); } return $v; } /** * Bit String to Octet String * * @param string $in * @return string */ private function bits2octets($in) { $z1 = $this->bits2int($in); $z2 = $z1->subtract($this->q); return $z2->compare(self::$zero) < 0 ? $this->int2octets($z1) : $this->int2octets($z2); } } PK! Avendor/phpseclib/phpseclib/phpseclib/Crypt/Common/BlockCipher.phpnu[ * @author Hans-Juergen Petrich * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common; /** * Base Class for all block cipher classes * * @author Jim Wigginton */ abstract class BlockCipher extends SymmetricKey { } PK!b2@vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/PrivateKey.phpnu[ * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common; /** * PrivateKey interface * * @author Jim Wigginton */ interface PrivateKey { public function sign($message); //public function decrypt($ciphertext); public function getPublicKey(); public function toString($type, array $options = []); /** * @param string|false $password * @return mixed */ public function withPassword($password = false); } PK!.[-NN?vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/PublicKey.phpnu[ * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common; /** * PublicKey interface * * @author Jim Wigginton */ interface PublicKey { public function verify($message, $signature); //public function encrypt($plaintext); public function toString($type, array $options = []); public function getFingerprint($algorithm); } PK!]2jjBvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/StreamCipher.phpnu[ * @author Hans-Juergen Petrich * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common; /** * Base Class for all stream cipher classes * * @author Jim Wigginton */ abstract class StreamCipher extends SymmetricKey { /** * Block Length of the cipher * * Stream ciphers do not have a block size * * @see \phpseclib3\Crypt\Common\SymmetricKey::block_size * @var int */ protected $block_size = 0; /** * Default Constructor. * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @return StreamCipher */ public function __construct() { parent::__construct('stream'); } /** * Stream ciphers not use an IV * * @return bool */ public function usesIV() { return false; } } PK!ڤBvendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.phpnu[ * @author Hans-Juergen Petrich * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\Common; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Blowfish; use phpseclib3\Crypt\Hash; use phpseclib3\Exception\BadDecryptionException; use phpseclib3\Exception\BadModeException; use phpseclib3\Exception\InconsistentSetupException; use phpseclib3\Exception\InsufficientSetupException; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Math\BigInteger; use phpseclib3\Math\BinaryField; use phpseclib3\Math\PrimeField; /** * Base Class for all \phpseclib3\Crypt\* cipher classes * * @author Jim Wigginton * @author Hans-Juergen Petrich */ abstract class SymmetricKey { /** * Encrypt / decrypt using the Counter mode. * * Set to -1 since that's what Crypt/Random.php uses to index the CTR mode. * * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Counter_.28CTR.29 * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_CTR = -1; /** * Encrypt / decrypt using the Electronic Code Book mode. * * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Electronic_codebook_.28ECB.29 * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_ECB = 1; /** * Encrypt / decrypt using the Code Book Chaining mode. * * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29 * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_CBC = 2; /** * Encrypt / decrypt using the Cipher Feedback mode. * * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher_feedback_.28CFB.29 * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_CFB = 3; /** * Encrypt / decrypt using the Cipher Feedback mode (8bit) * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_CFB8 = 7; /** * Encrypt / decrypt using the Output Feedback mode (8bit) * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_OFB8 = 8; /** * Encrypt / decrypt using the Output Feedback mode. * * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Output_feedback_.28OFB.29 * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_OFB = 4; /** * Encrypt / decrypt using Galois/Counter mode. * * @link https://en.wikipedia.org/wiki/Galois/Counter_Mode * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_GCM = 5; /** * Encrypt / decrypt using streaming mode. * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() */ const MODE_STREAM = 6; /** * Mode Map * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const MODE_MAP = [ 'ctr' => self::MODE_CTR, 'ecb' => self::MODE_ECB, 'cbc' => self::MODE_CBC, 'cfb' => self::MODE_CFB, 'cfb8' => self::MODE_CFB8, 'ofb' => self::MODE_OFB, 'ofb8' => self::MODE_OFB8, 'gcm' => self::MODE_GCM, 'stream' => self::MODE_STREAM ]; /** * Base value for the internal implementation $engine switch * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const ENGINE_INTERNAL = 1; /** * Base value for the eval() implementation $engine switch * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const ENGINE_EVAL = 2; /** * Base value for the mcrypt implementation $engine switch * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const ENGINE_MCRYPT = 3; /** * Base value for the openssl implementation $engine switch * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const ENGINE_OPENSSL = 4; /** * Base value for the libsodium implementation $engine switch * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const ENGINE_LIBSODIUM = 5; /** * Base value for the openssl / gcm implementation $engine switch * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() */ const ENGINE_OPENSSL_GCM = 6; /** * Engine Reverse Map * * @see \phpseclib3\Crypt\Common\SymmetricKey::getEngine() */ const ENGINE_MAP = [ self::ENGINE_INTERNAL => 'PHP', self::ENGINE_EVAL => 'Eval', self::ENGINE_MCRYPT => 'mcrypt', self::ENGINE_OPENSSL => 'OpenSSL', self::ENGINE_LIBSODIUM => 'libsodium', self::ENGINE_OPENSSL_GCM => 'OpenSSL (GCM)' ]; /** * The Encryption Mode * * @see self::__construct() * @var int */ protected $mode; /** * The Block Length of the block cipher * * @var int */ protected $block_size = 16; /** * The Key * * @see self::setKey() * @var string */ protected $key = false; /** * HMAC Key * * @see self::setupGCM() * @var ?string */ protected $hKey = false; /** * The Initialization Vector * * @see self::setIV() * @var string */ protected $iv = false; /** * A "sliding" Initialization Vector * * @see self::enableContinuousBuffer() * @see self::clearBuffers() * @var string */ protected $encryptIV; /** * A "sliding" Initialization Vector * * @see self::enableContinuousBuffer() * @see self::clearBuffers() * @var string */ protected $decryptIV; /** * Continuous Buffer status * * @see self::enableContinuousBuffer() * @var bool */ protected $continuousBuffer = false; /** * Encryption buffer for CTR, OFB and CFB modes * * @see self::encrypt() * @see self::clearBuffers() * @var array */ protected $enbuffer; /** * Decryption buffer for CTR, OFB and CFB modes * * @see self::decrypt() * @see self::clearBuffers() * @var array */ protected $debuffer; /** * mcrypt resource for encryption * * The mcrypt resource can be recreated every time something needs to be created or it can be created just once. * Since mcrypt operates in continuous mode, by default, it'll need to be recreated when in non-continuous mode. * * @see self::encrypt() * @var resource */ private $enmcrypt; /** * mcrypt resource for decryption * * The mcrypt resource can be recreated every time something needs to be created or it can be created just once. * Since mcrypt operates in continuous mode, by default, it'll need to be recreated when in non-continuous mode. * * @see self::decrypt() * @var resource */ private $demcrypt; /** * Does the enmcrypt resource need to be (re)initialized? * * @see \phpseclib3\Crypt\Twofish::setKey() * @see \phpseclib3\Crypt\Twofish::setIV() * @var bool */ private $enchanged = true; /** * Does the demcrypt resource need to be (re)initialized? * * @see \phpseclib3\Crypt\Twofish::setKey() * @see \phpseclib3\Crypt\Twofish::setIV() * @var bool */ private $dechanged = true; /** * mcrypt resource for CFB mode * * mcrypt's CFB mode, in (and only in) buffered context, * is broken, so phpseclib implements the CFB mode by it self, * even when the mcrypt php extension is available. * * In order to do the CFB-mode work (fast) phpseclib * use a separate ECB-mode mcrypt resource. * * @link http://phpseclib.sourceforge.net/cfb-demo.phps * @see self::encrypt() * @see self::decrypt() * @see self::setupMcrypt() * @var resource */ private $ecb; /** * Optimizing value while CFB-encrypting * * Only relevant if $continuousBuffer enabled * and $engine == self::ENGINE_MCRYPT * * It's faster to re-init $enmcrypt if * $buffer bytes > $cfb_init_len than * using the $ecb resource furthermore. * * This value depends of the chosen cipher * and the time it would be needed for it's * initialization [by mcrypt_generic_init()] * which, typically, depends on the complexity * on its internaly Key-expanding algorithm. * * @see self::encrypt() * @var int */ protected $cfb_init_len = 600; /** * Does internal cipher state need to be (re)initialized? * * @see self::setKey() * @see self::setIV() * @see self::disableContinuousBuffer() * @var bool */ protected $changed = true; /** * Does Eval engie need to be (re)initialized? * * @see self::setup() * @var bool */ protected $nonIVChanged = true; /** * Padding status * * @see self::enablePadding() * @var bool */ private $padding = true; /** * Is the mode one that is paddable? * * @see self::__construct() * @var bool */ private $paddable = false; /** * Holds which crypt engine internaly should be use, * which will be determined automatically on __construct() * * Currently available $engines are: * - self::ENGINE_LIBSODIUM (very fast, php-extension: libsodium, extension_loaded('libsodium') required) * - self::ENGINE_OPENSSL_GCM (very fast, php-extension: openssl, extension_loaded('openssl') required) * - self::ENGINE_OPENSSL (very fast, php-extension: openssl, extension_loaded('openssl') required) * - self::ENGINE_MCRYPT (fast, php-extension: mcrypt, extension_loaded('mcrypt') required) * - self::ENGINE_EVAL (medium, pure php-engine, no php-extension required) * - self::ENGINE_INTERNAL (slower, pure php-engine, no php-extension required) * * @see self::setEngine() * @see self::encrypt() * @see self::decrypt() * @var int */ protected $engine; /** * Holds the preferred crypt engine * * @see self::setEngine() * @see self::setPreferredEngine() * @var int */ private $preferredEngine; /** * The mcrypt specific name of the cipher * * Only used if $engine == self::ENGINE_MCRYPT * * @link http://www.php.net/mcrypt_module_open * @link http://www.php.net/mcrypt_list_algorithms * @see self::setupMcrypt() * @var string */ protected $cipher_name_mcrypt; /** * The openssl specific name of the cipher * * Only used if $engine == self::ENGINE_OPENSSL * * @link http://www.php.net/openssl-get-cipher-methods * @var string */ protected $cipher_name_openssl; /** * The openssl specific name of the cipher in ECB mode * * If OpenSSL does not support the mode we're trying to use (CTR) * it can still be emulated with ECB mode. * * @link http://www.php.net/openssl-get-cipher-methods * @var string */ protected $cipher_name_openssl_ecb; /** * The default salt used by setPassword() * * @see self::setPassword() * @var string */ private $password_default_salt = 'phpseclib/salt'; /** * The name of the performance-optimized callback function * * Used by encrypt() / decrypt() * only if $engine == self::ENGINE_INTERNAL * * @see self::encrypt() * @see self::decrypt() * @see self::setupInlineCrypt() * @var Callback */ protected $inline_crypt; /** * If OpenSSL can be used in ECB but not in CTR we can emulate CTR * * @see self::openssl_ctr_process() * @var bool */ private $openssl_emulate_ctr = false; /** * Don't truncate / null pad key * * @see self::clearBuffers() * @var bool */ private $skip_key_adjustment = false; /** * Has the key length explicitly been set or should it be derived from the key, itself? * * @see self::setKeyLength() * @var bool */ protected $explicit_key_length = false; /** * Hash subkey for GHASH * * @see self::setupGCM() * @see self::ghash() * @var BinaryField\Integer */ private $h; /** * Additional authenticated data * * @var string */ protected $aad = ''; /** * Authentication Tag produced after a round of encryption * * @var string */ protected $newtag = false; /** * Authentication Tag to be verified during decryption * * @var string */ protected $oldtag = false; /** * GCM Binary Field * * @see self::__construct() * @see self::ghash() * @var BinaryField */ private static $gcmField; /** * Poly1305 Prime Field * * @see self::enablePoly1305() * @see self::poly1305() * @var PrimeField */ private static $poly1305Field; /** * Flag for using regular vs "safe" intval * * @see self::initialize_static_variables() * @var boolean */ protected static $use_reg_intval; /** * Poly1305 Key * * @see self::setPoly1305Key() * @see self::poly1305() * @var string */ protected $poly1305Key; /** * Poly1305 Flag * * @see self::setPoly1305Key() * @see self::enablePoly1305() * @var boolean */ protected $usePoly1305 = false; /** * The Original Initialization Vector * * GCM uses the nonce to build the IV but we want to be able to distinguish between nonce-derived * IV's and user-set IV's * * @see self::setIV() * @var string */ private $origIV = false; /** * Nonce * * Only used with GCM. We could re-use setIV() but nonce's can be of a different length and * toggling between GCM and other modes could be more complicated if we re-used setIV() * * @see self::setNonce() * @var string */ protected $nonce = false; /** * Default Constructor. * * $mode could be: * * - ecb * * - cbc * * - ctr * * - cfb * * - cfb8 * * - ofb * * - ofb8 * * - gcm * * @param string $mode * @throws BadModeException if an invalid / unsupported mode is provided */ public function __construct($mode) { $mode = strtolower($mode); // necessary because of 5.6 compatibility; we can't do isset(self::MODE_MAP[$mode]) in 5.6 $map = self::MODE_MAP; if (!isset($map[$mode])) { throw new BadModeException('No valid mode has been specified'); } $mode = self::MODE_MAP[$mode]; // $mode dependent settings switch ($mode) { case self::MODE_ECB: case self::MODE_CBC: $this->paddable = true; break; case self::MODE_CTR: case self::MODE_CFB: case self::MODE_CFB8: case self::MODE_OFB: case self::MODE_OFB8: case self::MODE_STREAM: $this->paddable = false; break; case self::MODE_GCM: if ($this->block_size != 16) { throw new BadModeException('GCM is only valid for block ciphers with a block size of 128 bits'); } if (!isset(self::$gcmField)) { self::$gcmField = new BinaryField(128, 7, 2, 1, 0); } $this->paddable = false; break; default: throw new BadModeException('No valid mode has been specified'); } $this->mode = $mode; static::initialize_static_variables(); } /** * Initialize static variables */ protected static function initialize_static_variables() { if (!isset(self::$use_reg_intval)) { switch (true) { // PHP_OS & "\xDF\xDF\xDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster case (PHP_OS & "\xDF\xDF\xDF") === 'WIN': case !function_exists('php_uname'): case !is_string(php_uname('m')): case (php_uname('m') & "\xDF\xDF\xDF") != 'ARM': case defined('PHP_INT_SIZE') && PHP_INT_SIZE == 8: self::$use_reg_intval = true; break; case (php_uname('m') & "\xDF\xDF\xDF") == 'ARM': switch (true) { /* PHP 7.0.0 introduced a bug that affected 32-bit ARM processors: https://github.com/php/php-src/commit/716da71446ebbd40fa6cf2cea8a4b70f504cc3cd altho the changelogs make no mention of it, this bug was fixed with this commit: https://github.com/php/php-src/commit/c1729272b17a1fe893d1a54e423d3b71470f3ee8 affected versions of PHP are: 7.0.x, 7.1.0 - 7.1.23 and 7.2.0 - 7.2.11 */ case PHP_VERSION_ID >= 70000 && PHP_VERSION_ID <= 70123: case PHP_VERSION_ID >= 70200 && PHP_VERSION_ID <= 70211: self::$use_reg_intval = false; break; default: self::$use_reg_intval = true; } } } } /** * Sets the initialization vector. * * setIV() is not required when ecb or gcm modes are being used. * * {@internal Can be overwritten by a sub class, but does not have to be} * * @param string $iv * @throws \LengthException if the IV length isn't equal to the block size * @throws \BadMethodCallException if an IV is provided when one shouldn't be */ public function setIV($iv) { if ($this->mode == self::MODE_ECB) { throw new \BadMethodCallException('This mode does not require an IV.'); } if ($this->mode == self::MODE_GCM) { throw new \BadMethodCallException('Use setNonce instead'); } if (!$this->usesIV()) { throw new \BadMethodCallException('This algorithm does not use an IV.'); } if (strlen($iv) != $this->block_size) { throw new \LengthException('Received initialization vector of size ' . strlen($iv) . ', but size ' . $this->block_size . ' is required'); } $this->iv = $this->origIV = $iv; $this->changed = true; } /** * Enables Poly1305 mode. * * Once enabled Poly1305 cannot be disabled. * * @throws \BadMethodCallException if Poly1305 is enabled whilst in GCM mode */ public function enablePoly1305() { if ($this->mode == self::MODE_GCM) { throw new \BadMethodCallException('Poly1305 cannot be used in GCM mode'); } $this->usePoly1305 = true; } /** * Enables Poly1305 mode. * * Once enabled Poly1305 cannot be disabled. If $key is not passed then an attempt to call createPoly1305Key * will be made. * * @param string $key optional * @throws \LengthException if the key isn't long enough * @throws \BadMethodCallException if Poly1305 is enabled whilst in GCM mode */ public function setPoly1305Key($key = null) { if ($this->mode == self::MODE_GCM) { throw new \BadMethodCallException('Poly1305 cannot be used in GCM mode'); } if (!is_string($key) || strlen($key) != 32) { throw new \LengthException('The Poly1305 key must be 32 bytes long (256 bits)'); } if (!isset(self::$poly1305Field)) { // 2^130-5 self::$poly1305Field = new PrimeField(new BigInteger('3fffffffffffffffffffffffffffffffb', 16)); } $this->poly1305Key = $key; $this->usePoly1305 = true; } /** * Sets the nonce. * * setNonce() is only required when gcm is used * * @param string $nonce * @throws \BadMethodCallException if an nonce is provided when one shouldn't be */ public function setNonce($nonce) { if ($this->mode != self::MODE_GCM) { throw new \BadMethodCallException('Nonces are only used in GCM mode.'); } $this->nonce = $nonce; $this->setEngine(); } /** * Sets additional authenticated data * * setAAD() is only used by gcm or in poly1305 mode * * @param string $aad * @throws \BadMethodCallException if mode isn't GCM or if poly1305 isn't being utilized */ public function setAAD($aad) { if ($this->mode != self::MODE_GCM && !$this->usePoly1305) { throw new \BadMethodCallException('Additional authenticated data is only utilized in GCM mode or with Poly1305'); } $this->aad = $aad; } /** * Returns whether or not the algorithm uses an IV * * @return bool */ public function usesIV() { return $this->mode != self::MODE_GCM && $this->mode != self::MODE_ECB; } /** * Returns whether or not the algorithm uses a nonce * * @return bool */ public function usesNonce() { return $this->mode == self::MODE_GCM; } /** * Returns the current key length in bits * * @return int */ public function getKeyLength() { return $this->key_length << 3; } /** * Returns the current block length in bits * * @return int */ public function getBlockLength() { return $this->block_size << 3; } /** * Returns the current block length in bytes * * @return int */ public function getBlockLengthInBytes() { return $this->block_size; } /** * Sets the key length. * * Keys with explicitly set lengths need to be treated accordingly * * @param int $length */ public function setKeyLength($length) { $this->explicit_key_length = $length >> 3; if (is_string($this->key) && strlen($this->key) != $this->explicit_key_length) { $this->key = false; throw new InconsistentSetupException('Key has already been set and is not ' . $this->explicit_key_length . ' bytes long'); } } /** * Sets the key. * * The min/max length(s) of the key depends on the cipher which is used. * If the key not fits the length(s) of the cipher it will paded with null bytes * up to the closest valid key length. If the key is more than max length, * we trim the excess bits. * * If the key is not explicitly set, it'll be assumed to be all null bytes. * * {@internal Could, but not must, extend by the child Crypt_* class} * * @param string $key */ public function setKey($key) { if ($this->explicit_key_length !== false && strlen($key) != $this->explicit_key_length) { throw new InconsistentSetupException('Key length has already been set to ' . $this->explicit_key_length . ' bytes and this key is ' . strlen($key) . ' bytes'); } $this->key = $key; $this->key_length = strlen($key); $this->setEngine(); } /** * Sets the password. * * Depending on what $method is set to, setPassword()'s (optional) parameters are as follows: * {@link http://en.wikipedia.org/wiki/PBKDF2 pbkdf2} or pbkdf1: * $hash, $salt, $count, $dkLen * * Where $hash (default = sha1) currently supports the following hashes: see: Crypt/Hash.php * {@link https://en.wikipedia.org/wiki/Bcrypt bcypt}: * $salt, $rounds, $keylen * * This is a modified version of bcrypt used by OpenSSH. * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see Crypt/Hash.php * @param string $password * @param string $method * @param int|string ...$func_args * @throws \LengthException if pbkdf1 is being used and the derived key length exceeds the hash length * @throws \RuntimeException if bcrypt is being used and a salt isn't provided * @return bool */ public function setPassword($password, $method = 'pbkdf2', ...$func_args) { $key = ''; $method = strtolower($method); switch ($method) { case 'bcrypt': if (!isset($func_args[2])) { throw new \RuntimeException('A salt must be provided for bcrypt to work'); } $salt = $func_args[0]; $rounds = isset($func_args[1]) ? $func_args[1] : 16; $keylen = isset($func_args[2]) ? $func_args[2] : $this->key_length; $key = Blowfish::bcrypt_pbkdf($password, $salt, $keylen + $this->block_size, $rounds); $this->setKey(substr($key, 0, $keylen)); $this->setIV(substr($key, $keylen)); return true; case 'pkcs12': // from https://tools.ietf.org/html/rfc7292#appendix-B.2 case 'pbkdf1': case 'pbkdf2': // Hash function $hash = isset($func_args[0]) ? strtolower($func_args[0]) : 'sha1'; $hashObj = new Hash(); $hashObj->setHash($hash); // WPA and WPA2 use the SSID as the salt $salt = isset($func_args[1]) ? $func_args[1] : $this->password_default_salt; // RFC2898#section-4.2 uses 1,000 iterations by default // WPA and WPA2 use 4,096. $count = isset($func_args[2]) ? $func_args[2] : 1000; // Keylength if (isset($func_args[3])) { if ($func_args[3] <= 0) { throw new \LengthException('Derived key length cannot be longer 0 or less'); } $dkLen = $func_args[3]; } else { $key_length = $this->explicit_key_length !== false ? $this->explicit_key_length : $this->key_length; $dkLen = $method == 'pbkdf1' ? 2 * $key_length : $key_length; } switch (true) { case $method == 'pkcs12': /* In this specification, however, all passwords are created from BMPStrings with a NULL terminator. This means that each character in the original BMPString is encoded in 2 bytes in big-endian format (most-significant byte first). There are no Unicode byte order marks. The 2 bytes produced from the last character in the BMPString are followed by 2 additional bytes with the value 0x00. -- https://tools.ietf.org/html/rfc7292#appendix-B.1 */ $password = "\0" . chunk_split($password, 1, "\0") . "\0"; /* This standard specifies 3 different values for the ID byte mentioned above: 1. If ID=1, then the pseudorandom bits being produced are to be used as key material for performing encryption or decryption. 2. If ID=2, then the pseudorandom bits being produced are to be used as an IV (Initial Value) for encryption or decryption. 3. If ID=3, then the pseudorandom bits being produced are to be used as an integrity key for MACing. */ // Construct a string, D (the "diversifier"), by concatenating v/8 // copies of ID. $blockLength = $hashObj->getBlockLengthInBytes(); $d1 = str_repeat(chr(1), $blockLength); $d2 = str_repeat(chr(2), $blockLength); $s = ''; if (strlen($salt)) { while (strlen($s) < $blockLength) { $s .= $salt; } } $s = substr($s, 0, $blockLength); $p = ''; if (strlen($password)) { while (strlen($p) < $blockLength) { $p .= $password; } } $p = substr($p, 0, $blockLength); $i = $s . $p; $this->setKey(self::pkcs12helper($dkLen, $hashObj, $i, $d1, $count)); if ($this->usesIV()) { $this->setIV(self::pkcs12helper($this->block_size, $hashObj, $i, $d2, $count)); } return true; case $method == 'pbkdf1': if ($dkLen > $hashObj->getLengthInBytes()) { throw new \LengthException('Derived key length cannot be longer than the hash length'); } $t = $password . $salt; for ($i = 0; $i < $count; ++$i) { $t = $hashObj->hash($t); } $key = substr($t, 0, $dkLen); $this->setKey(substr($key, 0, $dkLen >> 1)); if ($this->usesIV()) { $this->setIV(substr($key, $dkLen >> 1)); } return true; case !in_array($hash, hash_algos()): $i = 1; $hashObj->setKey($password); while (strlen($key) < $dkLen) { $f = $u = $hashObj->hash($salt . pack('N', $i++)); for ($j = 2; $j <= $count; ++$j) { $u = $hashObj->hash($u); $f ^= $u; } $key .= $f; } $key = substr($key, 0, $dkLen); break; default: $key = hash_pbkdf2($hash, $password, $salt, $count, $dkLen, true); } break; default: throw new UnsupportedAlgorithmException($method . ' is not a supported password hashing method'); } $this->setKey($key); return true; } /** * PKCS#12 KDF Helper Function * * As discussed here: * * {@link https://tools.ietf.org/html/rfc7292#appendix-B} * * @see self::setPassword() * @param int $n * @param Hash $hashObj * @param string $i * @param string $d * @param int $count * @return string $a */ private static function pkcs12helper($n, $hashObj, $i, $d, $count) { static $one; if (!isset($one)) { $one = new BigInteger(1); } $blockLength = $hashObj->getBlockLength() >> 3; $c = ceil($n / $hashObj->getLengthInBytes()); $a = ''; for ($j = 1; $j <= $c; $j++) { $ai = $d . $i; for ($k = 0; $k < $count; $k++) { $ai = $hashObj->hash($ai); } $b = ''; while (strlen($b) < $blockLength) { $b .= $ai; } $b = substr($b, 0, $blockLength); $b = new BigInteger($b, 256); $newi = ''; for ($k = 0; $k < strlen($i); $k += $blockLength) { $temp = substr($i, $k, $blockLength); $temp = new BigInteger($temp, 256); $temp->setPrecision($blockLength << 3); $temp = $temp->add($b); $temp = $temp->add($one); $newi .= $temp->toBytes(false); } $i = $newi; $a .= $ai; } return substr($a, 0, $n); } /** * Encrypts a message. * * $plaintext will be padded with additional bytes such that it's length is a multiple of the block size. Other cipher * implementations may or may not pad in the same manner. Other common approaches to padding and the reasons why it's * necessary are discussed in the following * URL: * * {@link http://www.di-mgt.com.au/cryptopad.html http://www.di-mgt.com.au/cryptopad.html} * * An alternative to padding is to, separately, send the length of the file. This is what SSH, in fact, does. * strlen($plaintext) will still need to be a multiple of the block size, however, arbitrary values can be added to make it that * length. * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see self::decrypt() * @param string $plaintext * @return string $ciphertext */ public function encrypt($plaintext) { if ($this->paddable) { $plaintext = $this->pad($plaintext); } $this->setup(); if ($this->mode == self::MODE_GCM) { $oldIV = $this->iv; Strings::increment_str($this->iv); $cipher = new static('ctr'); $cipher->setKey($this->key); $cipher->setIV($this->iv); $ciphertext = $cipher->encrypt($plaintext); $s = $this->ghash( self::nullPad128($this->aad) . self::nullPad128($ciphertext) . self::len64($this->aad) . self::len64($ciphertext) ); $cipher->encryptIV = $this->iv = $this->encryptIV = $this->decryptIV = $oldIV; $this->newtag = $cipher->encrypt($s); return $ciphertext; } if (isset($this->poly1305Key)) { $cipher = clone $this; unset($cipher->poly1305Key); $this->usePoly1305 = false; $ciphertext = $cipher->encrypt($plaintext); $this->newtag = $this->poly1305($ciphertext); return $ciphertext; } if ($this->engine === self::ENGINE_OPENSSL) { switch ($this->mode) { case self::MODE_STREAM: return openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); case self::MODE_ECB: return openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); case self::MODE_CBC: $result = openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->encryptIV); if ($this->continuousBuffer) { $this->encryptIV = substr($result, -$this->block_size); } return $result; case self::MODE_CTR: return $this->openssl_ctr_process($plaintext, $this->encryptIV, $this->enbuffer); case self::MODE_CFB: // cfb loosely routines inspired by openssl's: // {@link http://cvs.openssl.org/fileview?f=openssl/crypto/modes/cfb128.c&v=1.3.2.2.2.1} $ciphertext = ''; if ($this->continuousBuffer) { $iv = &$this->encryptIV; $pos = &$this->enbuffer['pos']; } else { $iv = $this->encryptIV; $pos = 0; } $len = strlen($plaintext); $i = 0; if ($pos) { $orig_pos = $pos; $max = $this->block_size - $pos; if ($len >= $max) { $i = $max; $len -= $max; $pos = 0; } else { $i = $len; $pos += $len; $len = 0; } // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize $ciphertext = substr($iv, $orig_pos) ^ $plaintext; $iv = substr_replace($iv, $ciphertext, $orig_pos, $i); $plaintext = substr($plaintext, $i); } $overflow = $len % $this->block_size; if ($overflow) { $ciphertext .= openssl_encrypt(substr($plaintext, 0, -$overflow) . str_repeat("\0", $this->block_size), $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); $iv = Strings::pop($ciphertext, $this->block_size); $size = $len - $overflow; $block = $iv ^ substr($plaintext, -$overflow); $iv = substr_replace($iv, $block, 0, $overflow); $ciphertext .= $block; $pos = $overflow; } elseif ($len) { $ciphertext = openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); $iv = substr($ciphertext, -$this->block_size); } return $ciphertext; case self::MODE_CFB8: $ciphertext = openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->encryptIV); if ($this->continuousBuffer) { if (($len = strlen($ciphertext)) >= $this->block_size) { $this->encryptIV = substr($ciphertext, -$this->block_size); } else { $this->encryptIV = substr($this->encryptIV, $len - $this->block_size) . substr($ciphertext, -$len); } } return $ciphertext; case self::MODE_OFB8: $ciphertext = ''; $len = strlen($plaintext); $iv = $this->encryptIV; for ($i = 0; $i < $len; ++$i) { $xor = openssl_encrypt($iv, $this->cipher_name_openssl_ecb, $this->key, $this->openssl_options, $this->decryptIV); $ciphertext .= $plaintext[$i] ^ $xor; $iv = substr($iv, 1) . $xor[0]; } if ($this->continuousBuffer) { $this->encryptIV = $iv; } break; case self::MODE_OFB: return $this->openssl_ofb_process($plaintext, $this->encryptIV, $this->enbuffer); } } if ($this->engine === self::ENGINE_MCRYPT) { set_error_handler(function () { }); if ($this->enchanged) { mcrypt_generic_init($this->enmcrypt, $this->key, $this->getIV($this->encryptIV)); $this->enchanged = false; } // re: {@link http://phpseclib.sourceforge.net/cfb-demo.phps} // using mcrypt's default handing of CFB the above would output two different things. using phpseclib's // rewritten CFB implementation the above outputs the same thing twice. if ($this->mode == self::MODE_CFB && $this->continuousBuffer) { $block_size = $this->block_size; $iv = &$this->encryptIV; $pos = &$this->enbuffer['pos']; $len = strlen($plaintext); $ciphertext = ''; $i = 0; if ($pos) { $orig_pos = $pos; $max = $block_size - $pos; if ($len >= $max) { $i = $max; $len -= $max; $pos = 0; } else { $i = $len; $pos += $len; $len = 0; } $ciphertext = substr($iv, $orig_pos) ^ $plaintext; $iv = substr_replace($iv, $ciphertext, $orig_pos, $i); $this->enbuffer['enmcrypt_init'] = true; } if ($len >= $block_size) { if ($this->enbuffer['enmcrypt_init'] === false || $len > $this->cfb_init_len) { if ($this->enbuffer['enmcrypt_init'] === true) { mcrypt_generic_init($this->enmcrypt, $this->key, $iv); $this->enbuffer['enmcrypt_init'] = false; } $ciphertext .= mcrypt_generic($this->enmcrypt, substr($plaintext, $i, $len - $len % $block_size)); $iv = substr($ciphertext, -$block_size); $len %= $block_size; } else { while ($len >= $block_size) { $iv = mcrypt_generic($this->ecb, $iv) ^ substr($plaintext, $i, $block_size); $ciphertext .= $iv; $len -= $block_size; $i += $block_size; } } } if ($len) { $iv = mcrypt_generic($this->ecb, $iv); $block = $iv ^ substr($plaintext, -$len); $iv = substr_replace($iv, $block, 0, $len); $ciphertext .= $block; $pos = $len; } restore_error_handler(); return $ciphertext; } $ciphertext = mcrypt_generic($this->enmcrypt, $plaintext); if (!$this->continuousBuffer) { mcrypt_generic_init($this->enmcrypt, $this->key, $this->getIV($this->encryptIV)); } restore_error_handler(); return $ciphertext; } if ($this->engine === self::ENGINE_EVAL) { $inline = $this->inline_crypt; return $inline('encrypt', $plaintext); } $buffer = &$this->enbuffer; $block_size = $this->block_size; $ciphertext = ''; switch ($this->mode) { case self::MODE_ECB: for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $ciphertext .= $this->encryptBlock(substr($plaintext, $i, $block_size)); } break; case self::MODE_CBC: $xor = $this->encryptIV; for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $block = substr($plaintext, $i, $block_size); $block = $this->encryptBlock($block ^ $xor); $xor = $block; $ciphertext .= $block; } if ($this->continuousBuffer) { $this->encryptIV = $xor; } break; case self::MODE_CTR: $xor = $this->encryptIV; if (strlen($buffer['ciphertext'])) { for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $block = substr($plaintext, $i, $block_size); if (strlen($block) > strlen($buffer['ciphertext'])) { $buffer['ciphertext'] .= $this->encryptBlock($xor); Strings::increment_str($xor); } $key = Strings::shift($buffer['ciphertext'], $block_size); $ciphertext .= $block ^ $key; } } else { for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $block = substr($plaintext, $i, $block_size); $key = $this->encryptBlock($xor); Strings::increment_str($xor); $ciphertext .= $block ^ $key; } } if ($this->continuousBuffer) { $this->encryptIV = $xor; if ($start = strlen($plaintext) % $block_size) { $buffer['ciphertext'] = substr($key, $start) . $buffer['ciphertext']; } } break; case self::MODE_CFB: // cfb loosely routines inspired by openssl's: // {@link http://cvs.openssl.org/fileview?f=openssl/crypto/modes/cfb128.c&v=1.3.2.2.2.1} if ($this->continuousBuffer) { $iv = &$this->encryptIV; $pos = &$buffer['pos']; } else { $iv = $this->encryptIV; $pos = 0; } $len = strlen($plaintext); $i = 0; if ($pos) { $orig_pos = $pos; $max = $block_size - $pos; if ($len >= $max) { $i = $max; $len -= $max; $pos = 0; } else { $i = $len; $pos += $len; $len = 0; } // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize $ciphertext = substr($iv, $orig_pos) ^ $plaintext; $iv = substr_replace($iv, $ciphertext, $orig_pos, $i); } while ($len >= $block_size) { $iv = $this->encryptBlock($iv) ^ substr($plaintext, $i, $block_size); $ciphertext .= $iv; $len -= $block_size; $i += $block_size; } if ($len) { $iv = $this->encryptBlock($iv); $block = $iv ^ substr($plaintext, $i); $iv = substr_replace($iv, $block, 0, $len); $ciphertext .= $block; $pos = $len; } break; case self::MODE_CFB8: $ciphertext = ''; $len = strlen($plaintext); $iv = $this->encryptIV; for ($i = 0; $i < $len; ++$i) { $ciphertext .= ($c = $plaintext[$i] ^ $this->encryptBlock($iv)); $iv = substr($iv, 1) . $c; } if ($this->continuousBuffer) { if ($len >= $block_size) { $this->encryptIV = substr($ciphertext, -$block_size); } else { $this->encryptIV = substr($this->encryptIV, $len - $block_size) . substr($ciphertext, -$len); } } break; case self::MODE_OFB8: $ciphertext = ''; $len = strlen($plaintext); $iv = $this->encryptIV; for ($i = 0; $i < $len; ++$i) { $xor = $this->encryptBlock($iv); $ciphertext .= $plaintext[$i] ^ $xor; $iv = substr($iv, 1) . $xor[0]; } if ($this->continuousBuffer) { $this->encryptIV = $iv; } break; case self::MODE_OFB: $xor = $this->encryptIV; if (strlen($buffer['xor'])) { for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $block = substr($plaintext, $i, $block_size); if (strlen($block) > strlen($buffer['xor'])) { $xor = $this->encryptBlock($xor); $buffer['xor'] .= $xor; } $key = Strings::shift($buffer['xor'], $block_size); $ciphertext .= $block ^ $key; } } else { for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $xor = $this->encryptBlock($xor); $ciphertext .= substr($plaintext, $i, $block_size) ^ $xor; } $key = $xor; } if ($this->continuousBuffer) { $this->encryptIV = $xor; if ($start = strlen($plaintext) % $block_size) { $buffer['xor'] = substr($key, $start) . $buffer['xor']; } } break; case self::MODE_STREAM: $ciphertext = $this->encryptBlock($plaintext); break; } return $ciphertext; } /** * Decrypts a message. * * If strlen($ciphertext) is not a multiple of the block size, null bytes will be added to the end of the string until * it is. * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see self::encrypt() * @param string $ciphertext * @return string $plaintext * @throws \LengthException if we're inside a block cipher and the ciphertext length is not a multiple of the block size */ public function decrypt($ciphertext) { if ($this->paddable && strlen($ciphertext) % $this->block_size) { throw new \LengthException('The ciphertext length (' . strlen($ciphertext) . ') needs to be a multiple of the block size (' . $this->block_size . ')'); } $this->setup(); if ($this->mode == self::MODE_GCM || isset($this->poly1305Key)) { if ($this->oldtag === false) { throw new InsufficientSetupException('Authentication Tag has not been set'); } if (isset($this->poly1305Key)) { $newtag = $this->poly1305($ciphertext); } else { $oldIV = $this->iv; Strings::increment_str($this->iv); $cipher = new static('ctr'); $cipher->setKey($this->key); $cipher->setIV($this->iv); $plaintext = $cipher->decrypt($ciphertext); $s = $this->ghash( self::nullPad128($this->aad) . self::nullPad128($ciphertext) . self::len64($this->aad) . self::len64($ciphertext) ); $cipher->encryptIV = $this->iv = $this->encryptIV = $this->decryptIV = $oldIV; $newtag = $cipher->encrypt($s); } if ($this->oldtag != substr($newtag, 0, strlen($newtag))) { $cipher = clone $this; unset($cipher->poly1305Key); $this->usePoly1305 = false; $plaintext = $cipher->decrypt($ciphertext); $this->oldtag = false; throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); } $this->oldtag = false; return $plaintext; } if ($this->engine === self::ENGINE_OPENSSL) { switch ($this->mode) { case self::MODE_STREAM: $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); break; case self::MODE_ECB: $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); break; case self::MODE_CBC: $offset = $this->block_size; $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->decryptIV); if ($this->continuousBuffer) { $this->decryptIV = substr($ciphertext, -$offset, $this->block_size); } break; case self::MODE_CTR: $plaintext = $this->openssl_ctr_process($ciphertext, $this->decryptIV, $this->debuffer); break; case self::MODE_CFB: // cfb loosely routines inspired by openssl's: // {@link http://cvs.openssl.org/fileview?f=openssl/crypto/modes/cfb128.c&v=1.3.2.2.2.1} $plaintext = ''; if ($this->continuousBuffer) { $iv = &$this->decryptIV; $pos = &$this->debuffer['pos']; } else { $iv = $this->decryptIV; $pos = 0; } $len = strlen($ciphertext); $i = 0; if ($pos) { $orig_pos = $pos; $max = $this->block_size - $pos; if ($len >= $max) { $i = $max; $len -= $max; $pos = 0; } else { $i = $len; $pos += $len; $len = 0; } // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $this->blocksize $plaintext = substr($iv, $orig_pos) ^ $ciphertext; $iv = substr_replace($iv, substr($ciphertext, 0, $i), $orig_pos, $i); $ciphertext = substr($ciphertext, $i); } $overflow = $len % $this->block_size; if ($overflow) { $plaintext .= openssl_decrypt(substr($ciphertext, 0, -$overflow), $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); if ($len - $overflow) { $iv = substr($ciphertext, -$overflow - $this->block_size, -$overflow); } $iv = openssl_encrypt(str_repeat("\0", $this->block_size), $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); $plaintext .= $iv ^ substr($ciphertext, -$overflow); $iv = substr_replace($iv, substr($ciphertext, -$overflow), 0, $overflow); $pos = $overflow; } elseif ($len) { $plaintext .= openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); $iv = substr($ciphertext, -$this->block_size); } break; case self::MODE_CFB8: $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->decryptIV); if ($this->continuousBuffer) { if (($len = strlen($ciphertext)) >= $this->block_size) { $this->decryptIV = substr($ciphertext, -$this->block_size); } else { $this->decryptIV = substr($this->decryptIV, $len - $this->block_size) . substr($ciphertext, -$len); } } break; case self::MODE_OFB8: $plaintext = ''; $len = strlen($ciphertext); $iv = $this->decryptIV; for ($i = 0; $i < $len; ++$i) { $xor = openssl_encrypt($iv, $this->cipher_name_openssl_ecb, $this->key, $this->openssl_options, $this->decryptIV); $plaintext .= $ciphertext[$i] ^ $xor; $iv = substr($iv, 1) . $xor[0]; } if ($this->continuousBuffer) { $this->decryptIV = $iv; } break; case self::MODE_OFB: $plaintext = $this->openssl_ofb_process($ciphertext, $this->decryptIV, $this->debuffer); } return $this->paddable ? $this->unpad($plaintext) : $plaintext; } if ($this->engine === self::ENGINE_MCRYPT) { set_error_handler(function () { }); $block_size = $this->block_size; if ($this->dechanged) { mcrypt_generic_init($this->demcrypt, $this->key, $this->getIV($this->decryptIV)); $this->dechanged = false; } if ($this->mode == self::MODE_CFB && $this->continuousBuffer) { $iv = &$this->decryptIV; $pos = &$this->debuffer['pos']; $len = strlen($ciphertext); $plaintext = ''; $i = 0; if ($pos) { $orig_pos = $pos; $max = $block_size - $pos; if ($len >= $max) { $i = $max; $len -= $max; $pos = 0; } else { $i = $len; $pos += $len; $len = 0; } // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize $plaintext = substr($iv, $orig_pos) ^ $ciphertext; $iv = substr_replace($iv, substr($ciphertext, 0, $i), $orig_pos, $i); } if ($len >= $block_size) { $cb = substr($ciphertext, $i, $len - $len % $block_size); $plaintext .= mcrypt_generic($this->ecb, $iv . $cb) ^ $cb; $iv = substr($cb, -$block_size); $len %= $block_size; } if ($len) { $iv = mcrypt_generic($this->ecb, $iv); $plaintext .= $iv ^ substr($ciphertext, -$len); $iv = substr_replace($iv, substr($ciphertext, -$len), 0, $len); $pos = $len; } restore_error_handler(); return $plaintext; } $plaintext = mdecrypt_generic($this->demcrypt, $ciphertext); if (!$this->continuousBuffer) { mcrypt_generic_init($this->demcrypt, $this->key, $this->getIV($this->decryptIV)); } restore_error_handler(); return $this->paddable ? $this->unpad($plaintext) : $plaintext; } if ($this->engine === self::ENGINE_EVAL) { $inline = $this->inline_crypt; return $inline('decrypt', $ciphertext); } $block_size = $this->block_size; $buffer = &$this->debuffer; $plaintext = ''; switch ($this->mode) { case self::MODE_ECB: for ($i = 0; $i < strlen($ciphertext); $i += $block_size) { $plaintext .= $this->decryptBlock(substr($ciphertext, $i, $block_size)); } break; case self::MODE_CBC: $xor = $this->decryptIV; for ($i = 0; $i < strlen($ciphertext); $i += $block_size) { $block = substr($ciphertext, $i, $block_size); $plaintext .= $this->decryptBlock($block) ^ $xor; $xor = $block; } if ($this->continuousBuffer) { $this->decryptIV = $xor; } break; case self::MODE_CTR: $xor = $this->decryptIV; if (strlen($buffer['ciphertext'])) { for ($i = 0; $i < strlen($ciphertext); $i += $block_size) { $block = substr($ciphertext, $i, $block_size); if (strlen($block) > strlen($buffer['ciphertext'])) { $buffer['ciphertext'] .= $this->encryptBlock($xor); Strings::increment_str($xor); } $key = Strings::shift($buffer['ciphertext'], $block_size); $plaintext .= $block ^ $key; } } else { for ($i = 0; $i < strlen($ciphertext); $i += $block_size) { $block = substr($ciphertext, $i, $block_size); $key = $this->encryptBlock($xor); Strings::increment_str($xor); $plaintext .= $block ^ $key; } } if ($this->continuousBuffer) { $this->decryptIV = $xor; if ($start = strlen($ciphertext) % $block_size) { $buffer['ciphertext'] = substr($key, $start) . $buffer['ciphertext']; } } break; case self::MODE_CFB: if ($this->continuousBuffer) { $iv = &$this->decryptIV; $pos = &$buffer['pos']; } else { $iv = $this->decryptIV; $pos = 0; } $len = strlen($ciphertext); $i = 0; if ($pos) { $orig_pos = $pos; $max = $block_size - $pos; if ($len >= $max) { $i = $max; $len -= $max; $pos = 0; } else { $i = $len; $pos += $len; $len = 0; } // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize $plaintext = substr($iv, $orig_pos) ^ $ciphertext; $iv = substr_replace($iv, substr($ciphertext, 0, $i), $orig_pos, $i); } while ($len >= $block_size) { $iv = $this->encryptBlock($iv); $cb = substr($ciphertext, $i, $block_size); $plaintext .= $iv ^ $cb; $iv = $cb; $len -= $block_size; $i += $block_size; } if ($len) { $iv = $this->encryptBlock($iv); $plaintext .= $iv ^ substr($ciphertext, $i); $iv = substr_replace($iv, substr($ciphertext, $i), 0, $len); $pos = $len; } break; case self::MODE_CFB8: $plaintext = ''; $len = strlen($ciphertext); $iv = $this->decryptIV; for ($i = 0; $i < $len; ++$i) { $plaintext .= $ciphertext[$i] ^ $this->encryptBlock($iv); $iv = substr($iv, 1) . $ciphertext[$i]; } if ($this->continuousBuffer) { if ($len >= $block_size) { $this->decryptIV = substr($ciphertext, -$block_size); } else { $this->decryptIV = substr($this->decryptIV, $len - $block_size) . substr($ciphertext, -$len); } } break; case self::MODE_OFB8: $plaintext = ''; $len = strlen($ciphertext); $iv = $this->decryptIV; for ($i = 0; $i < $len; ++$i) { $xor = $this->encryptBlock($iv); $plaintext .= $ciphertext[$i] ^ $xor; $iv = substr($iv, 1) . $xor[0]; } if ($this->continuousBuffer) { $this->decryptIV = $iv; } break; case self::MODE_OFB: $xor = $this->decryptIV; if (strlen($buffer['xor'])) { for ($i = 0; $i < strlen($ciphertext); $i += $block_size) { $block = substr($ciphertext, $i, $block_size); if (strlen($block) > strlen($buffer['xor'])) { $xor = $this->encryptBlock($xor); $buffer['xor'] .= $xor; } $key = Strings::shift($buffer['xor'], $block_size); $plaintext .= $block ^ $key; } } else { for ($i = 0; $i < strlen($ciphertext); $i += $block_size) { $xor = $this->encryptBlock($xor); $plaintext .= substr($ciphertext, $i, $block_size) ^ $xor; } $key = $xor; } if ($this->continuousBuffer) { $this->decryptIV = $xor; if ($start = strlen($ciphertext) % $block_size) { $buffer['xor'] = substr($key, $start) . $buffer['xor']; } } break; case self::MODE_STREAM: $plaintext = $this->decryptBlock($ciphertext); break; } return $this->paddable ? $this->unpad($plaintext) : $plaintext; } /** * Get the authentication tag * * Only used in GCM or Poly1305 mode * * @see self::encrypt() * @param int $length optional * @return string * @throws \LengthException if $length isn't of a sufficient length * @throws \RuntimeException if GCM mode isn't being used */ public function getTag($length = 16) { if ($this->mode != self::MODE_GCM && !$this->usePoly1305) { throw new \BadMethodCallException('Authentication tags are only utilized in GCM mode or with Poly1305'); } if ($this->newtag === false) { throw new \BadMethodCallException('A tag can only be returned after a round of encryption has been performed'); } // the tag is 128-bits. it can't be greater than 16 bytes because that's bigger than the tag is. if it // were 0 you might as well be doing CTR and less than 4 provides minimal security that could be trivially // easily brute forced. // see https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf#page=36 // for more info if ($length < 4 || $length > 16) { throw new \LengthException('The authentication tag must be between 4 and 16 bytes long'); } return $length == 16 ? $this->newtag : substr($this->newtag, 0, $length); } /** * Sets the authentication tag * * Only used in GCM mode * * @see self::decrypt() * @param string $tag * @throws \LengthException if $length isn't of a sufficient length * @throws \RuntimeException if GCM mode isn't being used */ public function setTag($tag) { if ($this->usePoly1305 && !isset($this->poly1305Key) && method_exists($this, 'createPoly1305Key')) { $this->createPoly1305Key(); } if ($this->mode != self::MODE_GCM && !$this->usePoly1305) { throw new \BadMethodCallException('Authentication tags are only utilized in GCM mode or with Poly1305'); } $length = strlen($tag); if ($length < 4 || $length > 16) { throw new \LengthException('The authentication tag must be between 4 and 16 bytes long'); } $this->oldtag = $tag; } /** * Get the IV * * mcrypt requires an IV even if ECB is used * * @see self::encrypt() * @see self::decrypt() * @param string $iv * @return string */ protected function getIV($iv) { return $this->mode == self::MODE_ECB ? str_repeat("\0", $this->block_size) : $iv; } /** * OpenSSL CTR Processor * * PHP's OpenSSL bindings do not operate in continuous mode so we'll wrap around it. Since the keystream * for CTR is the same for both encrypting and decrypting this function is re-used by both SymmetricKey::encrypt() * and SymmetricKey::decrypt(). Also, OpenSSL doesn't implement CTR for all of it's symmetric ciphers so this * function will emulate CTR with ECB when necessary. * * @see self::encrypt() * @see self::decrypt() * @param string $plaintext * @param string $encryptIV * @param array $buffer * @return string */ private function openssl_ctr_process($plaintext, &$encryptIV, &$buffer) { $ciphertext = ''; $block_size = $this->block_size; $key = $this->key; if ($this->openssl_emulate_ctr) { $xor = $encryptIV; if (strlen($buffer['ciphertext'])) { for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $block = substr($plaintext, $i, $block_size); if (strlen($block) > strlen($buffer['ciphertext'])) { $buffer['ciphertext'] .= openssl_encrypt($xor, $this->cipher_name_openssl_ecb, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); } Strings::increment_str($xor); $otp = Strings::shift($buffer['ciphertext'], $block_size); $ciphertext .= $block ^ $otp; } } else { for ($i = 0; $i < strlen($plaintext); $i += $block_size) { $block = substr($plaintext, $i, $block_size); $otp = openssl_encrypt($xor, $this->cipher_name_openssl_ecb, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); Strings::increment_str($xor); $ciphertext .= $block ^ $otp; } } if ($this->continuousBuffer) { $encryptIV = $xor; if ($start = strlen($plaintext) % $block_size) { $buffer['ciphertext'] = substr($key, $start) . $buffer['ciphertext']; } } return $ciphertext; } if (strlen($buffer['ciphertext'])) { $ciphertext = $plaintext ^ Strings::shift($buffer['ciphertext'], strlen($plaintext)); $plaintext = substr($plaintext, strlen($ciphertext)); if (!strlen($plaintext)) { return $ciphertext; } } $overflow = strlen($plaintext) % $block_size; if ($overflow) { $plaintext2 = Strings::pop($plaintext, $overflow); // ie. trim $plaintext to a multiple of $block_size and put rest of $plaintext in $plaintext2 $encrypted = openssl_encrypt($plaintext . str_repeat("\0", $block_size), $this->cipher_name_openssl, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $encryptIV); $temp = Strings::pop($encrypted, $block_size); $ciphertext .= $encrypted . ($plaintext2 ^ $temp); if ($this->continuousBuffer) { $buffer['ciphertext'] = substr($temp, $overflow); $encryptIV = $temp; } } elseif (!strlen($buffer['ciphertext'])) { $ciphertext .= openssl_encrypt($plaintext . str_repeat("\0", $block_size), $this->cipher_name_openssl, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $encryptIV); $temp = Strings::pop($ciphertext, $block_size); if ($this->continuousBuffer) { $encryptIV = $temp; } } if ($this->continuousBuffer) { $encryptIV = openssl_decrypt($encryptIV, $this->cipher_name_openssl_ecb, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); if ($overflow) { Strings::increment_str($encryptIV); } } return $ciphertext; } /** * OpenSSL OFB Processor * * PHP's OpenSSL bindings do not operate in continuous mode so we'll wrap around it. Since the keystream * for OFB is the same for both encrypting and decrypting this function is re-used by both SymmetricKey::encrypt() * and SymmetricKey::decrypt(). * * @see self::encrypt() * @see self::decrypt() * @param string $plaintext * @param string $encryptIV * @param array $buffer * @return string */ private function openssl_ofb_process($plaintext, &$encryptIV, &$buffer) { if (strlen($buffer['xor'])) { $ciphertext = $plaintext ^ $buffer['xor']; $buffer['xor'] = substr($buffer['xor'], strlen($ciphertext)); $plaintext = substr($plaintext, strlen($ciphertext)); } else { $ciphertext = ''; } $block_size = $this->block_size; $len = strlen($plaintext); $key = $this->key; $overflow = $len % $block_size; if (strlen($plaintext)) { if ($overflow) { $ciphertext .= openssl_encrypt(substr($plaintext, 0, -$overflow) . str_repeat("\0", $block_size), $this->cipher_name_openssl, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $encryptIV); $xor = Strings::pop($ciphertext, $block_size); if ($this->continuousBuffer) { $encryptIV = $xor; } $ciphertext .= Strings::shift($xor, $overflow) ^ substr($plaintext, -$overflow); if ($this->continuousBuffer) { $buffer['xor'] = $xor; } } else { $ciphertext = openssl_encrypt($plaintext, $this->cipher_name_openssl, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $encryptIV); if ($this->continuousBuffer) { $encryptIV = substr($ciphertext, -$block_size) ^ substr($plaintext, -$block_size); } } } return $ciphertext; } /** * phpseclib <-> OpenSSL Mode Mapper * * May need to be overwritten by classes extending this one in some cases * * @return string */ protected function openssl_translate_mode() { switch ($this->mode) { case self::MODE_ECB: return 'ecb'; case self::MODE_CBC: return 'cbc'; case self::MODE_CTR: case self::MODE_GCM: return 'ctr'; case self::MODE_CFB: return 'cfb'; case self::MODE_CFB8: return 'cfb8'; case self::MODE_OFB: return 'ofb'; } } /** * Pad "packets". * * Block ciphers working by encrypting between their specified [$this->]block_size at a time * If you ever need to encrypt or decrypt something that isn't of the proper length, it becomes necessary to * pad the input so that it is of the proper length. * * Padding is enabled by default. Sometimes, however, it is undesirable to pad strings. Such is the case in SSH, * where "packets" are padded with random bytes before being encrypted. Unpad these packets and you risk stripping * away characters that shouldn't be stripped away. (SSH knows how many bytes are added because the length is * transmitted separately) * * @see self::disablePadding() */ public function enablePadding() { $this->padding = true; } /** * Do not pad packets. * * @see self::enablePadding() */ public function disablePadding() { $this->padding = false; } /** * Treat consecutive "packets" as if they are a continuous buffer. * * Say you have a 32-byte plaintext $plaintext. Using the default behavior, the two following code snippets * will yield different outputs: * * * echo $rijndael->encrypt(substr($plaintext, 0, 16)); * echo $rijndael->encrypt(substr($plaintext, 16, 16)); * * * echo $rijndael->encrypt($plaintext); * * * The solution is to enable the continuous buffer. Although this will resolve the above discrepancy, it creates * another, as demonstrated with the following: * * * $rijndael->encrypt(substr($plaintext, 0, 16)); * echo $rijndael->decrypt($rijndael->encrypt(substr($plaintext, 16, 16))); * * * echo $rijndael->decrypt($rijndael->encrypt(substr($plaintext, 16, 16))); * * * With the continuous buffer disabled, these would yield the same output. With it enabled, they yield different * outputs. The reason is due to the fact that the initialization vector's change after every encryption / * decryption round when the continuous buffer is enabled. When it's disabled, they remain constant. * * Put another way, when the continuous buffer is enabled, the state of the \phpseclib3\Crypt\*() object changes after each * encryption / decryption round, whereas otherwise, it'd remain constant. For this reason, it's recommended that * continuous buffers not be used. They do offer better security and are, in fact, sometimes required (SSH uses them), * however, they are also less intuitive and more likely to cause you problems. * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see self::disableContinuousBuffer() */ public function enableContinuousBuffer() { if ($this->mode == self::MODE_ECB) { return; } if ($this->mode == self::MODE_GCM) { throw new \BadMethodCallException('This mode does not run in continuous mode'); } $this->continuousBuffer = true; $this->setEngine(); } /** * Treat consecutive packets as if they are a discontinuous buffer. * * The default behavior. * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see self::enableContinuousBuffer() */ public function disableContinuousBuffer() { if ($this->mode == self::MODE_ECB) { return; } if (!$this->continuousBuffer) { return; } $this->continuousBuffer = false; $this->setEngine(); } /** * Test for engine validity * * @see self::__construct() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { switch ($engine) { case self::ENGINE_OPENSSL: $this->openssl_emulate_ctr = false; $result = $this->cipher_name_openssl && extension_loaded('openssl'); if (!$result) { return false; } $methods = openssl_get_cipher_methods(); if (in_array($this->cipher_name_openssl, $methods)) { return true; } // not all of openssl's symmetric cipher's support ctr. for those // that don't we'll emulate it switch ($this->mode) { case self::MODE_CTR: if (in_array($this->cipher_name_openssl_ecb, $methods)) { $this->openssl_emulate_ctr = true; return true; } } return false; case self::ENGINE_MCRYPT: set_error_handler(function () { }); $result = $this->cipher_name_mcrypt && extension_loaded('mcrypt') && in_array($this->cipher_name_mcrypt, mcrypt_list_algorithms()); restore_error_handler(); return $result; case self::ENGINE_EVAL: return method_exists($this, 'setupInlineCrypt'); case self::ENGINE_INTERNAL: return true; } return false; } /** * Test for engine validity * * @see self::__construct() * @param string $engine * @return bool */ public function isValidEngine($engine) { static $reverseMap; if (!isset($reverseMap)) { $reverseMap = array_map('strtolower', self::ENGINE_MAP); $reverseMap = array_flip($reverseMap); } $engine = strtolower($engine); if (!isset($reverseMap[$engine])) { return false; } return $this->isValidEngineHelper($reverseMap[$engine]); } /** * Sets the preferred crypt engine * * Currently, $engine could be: * * - libsodium[very fast] * * - OpenSSL [very fast] * * - mcrypt [fast] * * - Eval [slow] * * - PHP [slowest] * * If the preferred crypt engine is not available the fastest available one will be used * * @see self::__construct() * @param string $engine */ public function setPreferredEngine($engine) { static $reverseMap; if (!isset($reverseMap)) { $reverseMap = array_map('strtolower', self::ENGINE_MAP); $reverseMap = array_flip($reverseMap); } $engine = is_string($engine) ? strtolower($engine) : ''; $this->preferredEngine = isset($reverseMap[$engine]) ? $reverseMap[$engine] : self::ENGINE_LIBSODIUM; $this->setEngine(); } /** * Returns the engine currently being utilized * * @see self::setEngine() */ public function getEngine() { return self::ENGINE_MAP[$this->engine]; } /** * Sets the engine as appropriate * * @see self::__construct() */ protected function setEngine() { $this->engine = null; $candidateEngines = [ self::ENGINE_LIBSODIUM, self::ENGINE_OPENSSL_GCM, self::ENGINE_OPENSSL, self::ENGINE_MCRYPT, self::ENGINE_EVAL ]; if (isset($this->preferredEngine)) { $temp = [$this->preferredEngine]; $candidateEngines = array_merge( $temp, array_diff($candidateEngines, $temp) ); } foreach ($candidateEngines as $engine) { if ($this->isValidEngineHelper($engine)) { $this->engine = $engine; break; } } if (!$this->engine) { $this->engine = self::ENGINE_INTERNAL; } if ($this->engine != self::ENGINE_MCRYPT && $this->enmcrypt) { set_error_handler(function () { }); // Closing the current mcrypt resource(s). _mcryptSetup() will, if needed, // (re)open them with the module named in $this->cipher_name_mcrypt mcrypt_module_close($this->enmcrypt); mcrypt_module_close($this->demcrypt); $this->enmcrypt = null; $this->demcrypt = null; if ($this->ecb) { mcrypt_module_close($this->ecb); $this->ecb = null; } restore_error_handler(); } $this->changed = $this->nonIVChanged = true; } /** * Encrypts a block * * Note: Must be extended by the child \phpseclib3\Crypt\* class * * @param string $in * @return string */ abstract protected function encryptBlock($in); /** * Decrypts a block * * Note: Must be extended by the child \phpseclib3\Crypt\* class * * @param string $in * @return string */ abstract protected function decryptBlock($in); /** * Setup the key (expansion) * * Only used if $engine == self::ENGINE_INTERNAL * * Note: Must extend by the child \phpseclib3\Crypt\* class * * @see self::setup() */ abstract protected function setupKey(); /** * Setup the self::ENGINE_INTERNAL $engine * * (re)init, if necessary, the internal cipher $engine and flush all $buffers * Used (only) if $engine == self::ENGINE_INTERNAL * * _setup() will be called each time if $changed === true * typically this happens when using one or more of following public methods: * * - setKey() * * - setIV() * * - disableContinuousBuffer() * * - First run of encrypt() / decrypt() with no init-settings * * {@internal setup() is always called before en/decryption.} * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see self::setKey() * @see self::setIV() * @see self::disableContinuousBuffer() */ protected function setup() { if (!$this->changed) { return; } $this->changed = false; if ($this->usePoly1305 && !isset($this->poly1305Key) && method_exists($this, 'createPoly1305Key')) { $this->createPoly1305Key(); } $this->enbuffer = $this->debuffer = ['ciphertext' => '', 'xor' => '', 'pos' => 0, 'enmcrypt_init' => true]; //$this->newtag = $this->oldtag = false; if ($this->usesNonce()) { if ($this->nonce === false) { throw new InsufficientSetupException('No nonce has been defined'); } if ($this->mode == self::MODE_GCM && !in_array($this->engine, [self::ENGINE_LIBSODIUM, self::ENGINE_OPENSSL_GCM])) { $this->setupGCM(); } } else { $this->iv = $this->origIV; } if ($this->iv === false && !in_array($this->mode, [self::MODE_STREAM, self::MODE_ECB])) { if ($this->mode != self::MODE_GCM || !in_array($this->engine, [self::ENGINE_LIBSODIUM, self::ENGINE_OPENSSL_GCM])) { throw new InsufficientSetupException('No IV has been defined'); } } if ($this->key === false) { throw new InsufficientSetupException('No key has been defined'); } $this->encryptIV = $this->decryptIV = $this->iv; switch ($this->engine) { case self::ENGINE_MCRYPT: $this->enchanged = $this->dechanged = true; set_error_handler(function () { }); if (!isset($this->enmcrypt)) { static $mcrypt_modes = [ self::MODE_CTR => 'ctr', self::MODE_ECB => MCRYPT_MODE_ECB, self::MODE_CBC => MCRYPT_MODE_CBC, self::MODE_CFB => 'ncfb', self::MODE_CFB8 => MCRYPT_MODE_CFB, self::MODE_OFB => MCRYPT_MODE_NOFB, self::MODE_OFB8 => MCRYPT_MODE_OFB, self::MODE_STREAM => MCRYPT_MODE_STREAM, ]; $this->demcrypt = mcrypt_module_open($this->cipher_name_mcrypt, '', $mcrypt_modes[$this->mode], ''); $this->enmcrypt = mcrypt_module_open($this->cipher_name_mcrypt, '', $mcrypt_modes[$this->mode], ''); // we need the $ecb mcrypt resource (only) in MODE_CFB with enableContinuousBuffer() // to workaround mcrypt's broken ncfb implementation in buffered mode // see: {@link http://phpseclib.sourceforge.net/cfb-demo.phps} if ($this->mode == self::MODE_CFB) { $this->ecb = mcrypt_module_open($this->cipher_name_mcrypt, '', MCRYPT_MODE_ECB, ''); } } // else should mcrypt_generic_deinit be called? if ($this->mode == self::MODE_CFB) { mcrypt_generic_init($this->ecb, $this->key, str_repeat("\0", $this->block_size)); } restore_error_handler(); break; case self::ENGINE_INTERNAL: $this->setupKey(); break; case self::ENGINE_EVAL: if ($this->nonIVChanged) { $this->setupKey(); $this->setupInlineCrypt(); } } $this->nonIVChanged = false; } /** * Pads a string * * Pads a string using the RSA PKCS padding standards so that its length is a multiple of the blocksize. * $this->block_size - (strlen($text) % $this->block_size) bytes are added, each of which is equal to * chr($this->block_size - (strlen($text) % $this->block_size) * * If padding is disabled and $text is not a multiple of the blocksize, the string will be padded regardless * and padding will, hence forth, be enabled. * * @see self::unpad() * @param string $text * @throws \LengthException if padding is disabled and the plaintext's length is not a multiple of the block size * @return string */ protected function pad($text) { $length = strlen($text); if (!$this->padding) { if ($length % $this->block_size == 0) { return $text; } else { throw new \LengthException("The plaintext's length ($length) is not a multiple of the block size ({$this->block_size}). Try enabling padding."); } } $pad = $this->block_size - ($length % $this->block_size); return str_pad($text, $length + $pad, chr($pad)); } /** * Unpads a string. * * If padding is enabled and the reported padding length is invalid the encryption key will be assumed to be wrong * and false will be returned. * * @see self::pad() * @param string $text * @throws \LengthException if the ciphertext's length is not a multiple of the block size * @return string */ protected function unpad($text) { if (!$this->padding) { return $text; } $length = ord($text[strlen($text) - 1]); if (!$length || $length > $this->block_size) { throw new BadDecryptionException("The ciphertext has an invalid padding length ($length) compared to the block size ({$this->block_size})"); } return substr($text, 0, -$length); } /** * Setup the performance-optimized function for de/encrypt() * * Stores the created (or existing) callback function-name * in $this->inline_crypt * * Internally for phpseclib developers: * * _setupInlineCrypt() would be called only if: * * - $this->engine === self::ENGINE_EVAL * * - each time on _setup(), after(!) _setupKey() * * * This ensures that _setupInlineCrypt() has always a * full ready2go initializated internal cipher $engine state * where, for example, the keys already expanded, * keys/block_size calculated and such. * * It is, each time if called, the responsibility of _setupInlineCrypt(): * * - to set $this->inline_crypt to a valid and fully working callback function * as a (faster) replacement for encrypt() / decrypt() * * - NOT to create unlimited callback functions (for memory reasons!) * no matter how often _setupInlineCrypt() would be called. At some * point of amount they must be generic re-useable. * * - the code of _setupInlineCrypt() it self, * and the generated callback code, * must be, in following order: * - 100% safe * - 100% compatible to encrypt()/decrypt() * - using only php5+ features/lang-constructs/php-extensions if * compatibility (down to php4) or fallback is provided * - readable/maintainable/understandable/commented and... not-cryptic-styled-code :-) * - >= 10% faster than encrypt()/decrypt() [which is, by the way, * the reason for the existence of _setupInlineCrypt() :-)] * - memory-nice * - short (as good as possible) * * Note: - _setupInlineCrypt() is using _createInlineCryptFunction() to create the full callback function code. * - In case of using inline crypting, _setupInlineCrypt() must extend by the child \phpseclib3\Crypt\* class. * - The following variable names are reserved: * - $_* (all variable names prefixed with an underscore) * - $self (object reference to it self. Do not use $this, but $self instead) * - $in (the content of $in has to en/decrypt by the generated code) * - The callback function should not use the 'return' statement, but en/decrypt'ing the content of $in only * * {@internal If a Crypt_* class providing inline crypting it must extend _setupInlineCrypt()} * * @see self::setup() * @see self::createInlineCryptFunction() * @see self::encrypt() * @see self::decrypt() */ //protected function setupInlineCrypt(); /** * Creates the performance-optimized function for en/decrypt() * * Internally for phpseclib developers: * * _createInlineCryptFunction(): * * - merge the $cipher_code [setup'ed by _setupInlineCrypt()] * with the current [$this->]mode of operation code * * - create the $inline function, which called by encrypt() / decrypt() * as its replacement to speed up the en/decryption operations. * * - return the name of the created $inline callback function * * - used to speed up en/decryption * * * * The main reason why can speed up things [up to 50%] this way are: * * - using variables more effective then regular. * (ie no use of expensive arrays but integers $k_0, $k_1 ... * or even, for example, the pure $key[] values hardcoded) * * - avoiding 1000's of function calls of ie _encryptBlock() * but inlining the crypt operations. * in the mode of operation for() loop. * * - full loop unroll the (sometimes key-dependent) rounds * avoiding this way ++$i counters and runtime-if's etc... * * The basic code architectur of the generated $inline en/decrypt() * lambda function, in pseudo php, is: * * * +----------------------------------------------------------------------------------------------+ * | callback $inline = create_function: | * | lambda_function_0001_crypt_ECB($action, $text) | * | { | * | INSERT PHP CODE OF: | * | $cipher_code['init_crypt']; // general init code. | * | // ie: $sbox'es declarations used for | * | // encrypt and decrypt'ing. | * | | * | switch ($action) { | * | case 'encrypt': | * | INSERT PHP CODE OF: | * | $cipher_code['init_encrypt']; // encrypt sepcific init code. | * | ie: specified $key or $box | * | declarations for encrypt'ing. | * | | * | foreach ($ciphertext) { | * | $in = $block_size of $ciphertext; | * | | * | INSERT PHP CODE OF: | * | $cipher_code['encrypt_block']; // encrypt's (string) $in, which is always: | * | // strlen($in) == $this->block_size | * | // here comes the cipher algorithm in action | * | // for encryption. | * | // $cipher_code['encrypt_block'] has to | * | // encrypt the content of the $in variable | * | | * | $plaintext .= $in; | * | } | * | return $plaintext; | * | | * | case 'decrypt': | * | INSERT PHP CODE OF: | * | $cipher_code['init_decrypt']; // decrypt sepcific init code | * | ie: specified $key or $box | * | declarations for decrypt'ing. | * | foreach ($plaintext) { | * | $in = $block_size of $plaintext; | * | | * | INSERT PHP CODE OF: | * | $cipher_code['decrypt_block']; // decrypt's (string) $in, which is always | * | // strlen($in) == $this->block_size | * | // here comes the cipher algorithm in action | * | // for decryption. | * | // $cipher_code['decrypt_block'] has to | * | // decrypt the content of the $in variable | * | $ciphertext .= $in; | * | } | * | return $ciphertext; | * | } | * | } | * +----------------------------------------------------------------------------------------------+ * * * See also the \phpseclib3\Crypt\*::_setupInlineCrypt()'s for * productive inline $cipher_code's how they works. * * Structure of: * * $cipher_code = [ * 'init_crypt' => (string) '', // optional * 'init_encrypt' => (string) '', // optional * 'init_decrypt' => (string) '', // optional * 'encrypt_block' => (string) '', // required * 'decrypt_block' => (string) '' // required * ]; * * * @see self::setupInlineCrypt() * @see self::encrypt() * @see self::decrypt() * @param array $cipher_code * @return string (the name of the created callback function) */ protected function createInlineCryptFunction($cipher_code) { $block_size = $this->block_size; // optional $init_crypt = isset($cipher_code['init_crypt']) ? $cipher_code['init_crypt'] : ''; $init_encrypt = isset($cipher_code['init_encrypt']) ? $cipher_code['init_encrypt'] : ''; $init_decrypt = isset($cipher_code['init_decrypt']) ? $cipher_code['init_decrypt'] : ''; // required $encrypt_block = $cipher_code['encrypt_block']; $decrypt_block = $cipher_code['decrypt_block']; // Generating mode of operation inline code, // merged with the $cipher_code algorithm // for encrypt- and decryption. switch ($this->mode) { case self::MODE_ECB: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_plaintext_len = strlen($_text); for ($_i = 0; $_i < $_plaintext_len; $_i+= ' . $block_size . ') { $in = substr($_text, $_i, ' . $block_size . '); ' . $encrypt_block . ' $_ciphertext.= $in; } return $_ciphertext; '; $decrypt = $init_decrypt . ' $_plaintext = ""; $_text = str_pad($_text, strlen($_text) + (' . $block_size . ' - strlen($_text) % ' . $block_size . ') % ' . $block_size . ', chr(0)); $_ciphertext_len = strlen($_text); for ($_i = 0; $_i < $_ciphertext_len; $_i+= ' . $block_size . ') { $in = substr($_text, $_i, ' . $block_size . '); ' . $decrypt_block . ' $_plaintext.= $in; } return $this->unpad($_plaintext); '; break; case self::MODE_CTR: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_plaintext_len = strlen($_text); $_xor = $this->encryptIV; $_buffer = &$this->enbuffer; if (strlen($_buffer["ciphertext"])) { for ($_i = 0; $_i < $_plaintext_len; $_i+= ' . $block_size . ') { $_block = substr($_text, $_i, ' . $block_size . '); if (strlen($_block) > strlen($_buffer["ciphertext"])) { $in = $_xor; ' . $encrypt_block . ' \phpseclib3\Common\Functions\Strings::increment_str($_xor); $_buffer["ciphertext"].= $in; } $_key = \phpseclib3\Common\Functions\Strings::shift($_buffer["ciphertext"], ' . $block_size . '); $_ciphertext.= $_block ^ $_key; } } else { for ($_i = 0; $_i < $_plaintext_len; $_i+= ' . $block_size . ') { $_block = substr($_text, $_i, ' . $block_size . '); $in = $_xor; ' . $encrypt_block . ' \phpseclib3\Common\Functions\Strings::increment_str($_xor); $_key = $in; $_ciphertext.= $_block ^ $_key; } } if ($this->continuousBuffer) { $this->encryptIV = $_xor; if ($_start = $_plaintext_len % ' . $block_size . ') { $_buffer["ciphertext"] = substr($_key, $_start) . $_buffer["ciphertext"]; } } return $_ciphertext; '; $decrypt = $init_encrypt . ' $_plaintext = ""; $_ciphertext_len = strlen($_text); $_xor = $this->decryptIV; $_buffer = &$this->debuffer; if (strlen($_buffer["ciphertext"])) { for ($_i = 0; $_i < $_ciphertext_len; $_i+= ' . $block_size . ') { $_block = substr($_text, $_i, ' . $block_size . '); if (strlen($_block) > strlen($_buffer["ciphertext"])) { $in = $_xor; ' . $encrypt_block . ' \phpseclib3\Common\Functions\Strings::increment_str($_xor); $_buffer["ciphertext"].= $in; } $_key = \phpseclib3\Common\Functions\Strings::shift($_buffer["ciphertext"], ' . $block_size . '); $_plaintext.= $_block ^ $_key; } } else { for ($_i = 0; $_i < $_ciphertext_len; $_i+= ' . $block_size . ') { $_block = substr($_text, $_i, ' . $block_size . '); $in = $_xor; ' . $encrypt_block . ' \phpseclib3\Common\Functions\Strings::increment_str($_xor); $_key = $in; $_plaintext.= $_block ^ $_key; } } if ($this->continuousBuffer) { $this->decryptIV = $_xor; if ($_start = $_ciphertext_len % ' . $block_size . ') { $_buffer["ciphertext"] = substr($_key, $_start) . $_buffer["ciphertext"]; } } return $_plaintext; '; break; case self::MODE_CFB: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_buffer = &$this->enbuffer; if ($this->continuousBuffer) { $_iv = &$this->encryptIV; $_pos = &$_buffer["pos"]; } else { $_iv = $this->encryptIV; $_pos = 0; } $_len = strlen($_text); $_i = 0; if ($_pos) { $_orig_pos = $_pos; $_max = ' . $block_size . ' - $_pos; if ($_len >= $_max) { $_i = $_max; $_len-= $_max; $_pos = 0; } else { $_i = $_len; $_pos+= $_len; $_len = 0; } $_ciphertext = substr($_iv, $_orig_pos) ^ $_text; $_iv = substr_replace($_iv, $_ciphertext, $_orig_pos, $_i); } while ($_len >= ' . $block_size . ') { $in = $_iv; ' . $encrypt_block . '; $_iv = $in ^ substr($_text, $_i, ' . $block_size . '); $_ciphertext.= $_iv; $_len-= ' . $block_size . '; $_i+= ' . $block_size . '; } if ($_len) { $in = $_iv; ' . $encrypt_block . ' $_iv = $in; $_block = $_iv ^ substr($_text, $_i); $_iv = substr_replace($_iv, $_block, 0, $_len); $_ciphertext.= $_block; $_pos = $_len; } return $_ciphertext; '; $decrypt = $init_encrypt . ' $_plaintext = ""; $_buffer = &$this->debuffer; if ($this->continuousBuffer) { $_iv = &$this->decryptIV; $_pos = &$_buffer["pos"]; } else { $_iv = $this->decryptIV; $_pos = 0; } $_len = strlen($_text); $_i = 0; if ($_pos) { $_orig_pos = $_pos; $_max = ' . $block_size . ' - $_pos; if ($_len >= $_max) { $_i = $_max; $_len-= $_max; $_pos = 0; } else { $_i = $_len; $_pos+= $_len; $_len = 0; } $_plaintext = substr($_iv, $_orig_pos) ^ $_text; $_iv = substr_replace($_iv, substr($_text, 0, $_i), $_orig_pos, $_i); } while ($_len >= ' . $block_size . ') { $in = $_iv; ' . $encrypt_block . ' $_iv = $in; $cb = substr($_text, $_i, ' . $block_size . '); $_plaintext.= $_iv ^ $cb; $_iv = $cb; $_len-= ' . $block_size . '; $_i+= ' . $block_size . '; } if ($_len) { $in = $_iv; ' . $encrypt_block . ' $_iv = $in; $_plaintext.= $_iv ^ substr($_text, $_i); $_iv = substr_replace($_iv, substr($_text, $_i), 0, $_len); $_pos = $_len; } return $_plaintext; '; break; case self::MODE_CFB8: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_len = strlen($_text); $_iv = $this->encryptIV; for ($_i = 0; $_i < $_len; ++$_i) { $in = $_iv; ' . $encrypt_block . ' $_ciphertext .= ($_c = $_text[$_i] ^ $in); $_iv = substr($_iv, 1) . $_c; } if ($this->continuousBuffer) { if ($_len >= ' . $block_size . ') { $this->encryptIV = substr($_ciphertext, -' . $block_size . '); } else { $this->encryptIV = substr($this->encryptIV, $_len - ' . $block_size . ') . substr($_ciphertext, -$_len); } } return $_ciphertext; '; $decrypt = $init_encrypt . ' $_plaintext = ""; $_len = strlen($_text); $_iv = $this->decryptIV; for ($_i = 0; $_i < $_len; ++$_i) { $in = $_iv; ' . $encrypt_block . ' $_plaintext .= $_text[$_i] ^ $in; $_iv = substr($_iv, 1) . $_text[$_i]; } if ($this->continuousBuffer) { if ($_len >= ' . $block_size . ') { $this->decryptIV = substr($_text, -' . $block_size . '); } else { $this->decryptIV = substr($this->decryptIV, $_len - ' . $block_size . ') . substr($_text, -$_len); } } return $_plaintext; '; break; case self::MODE_OFB8: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_len = strlen($_text); $_iv = $this->encryptIV; for ($_i = 0; $_i < $_len; ++$_i) { $in = $_iv; ' . $encrypt_block . ' $_ciphertext.= $_text[$_i] ^ $in; $_iv = substr($_iv, 1) . $in[0]; } if ($this->continuousBuffer) { $this->encryptIV = $_iv; } return $_ciphertext; '; $decrypt = $init_encrypt . ' $_plaintext = ""; $_len = strlen($_text); $_iv = $this->decryptIV; for ($_i = 0; $_i < $_len; ++$_i) { $in = $_iv; ' . $encrypt_block . ' $_plaintext.= $_text[$_i] ^ $in; $_iv = substr($_iv, 1) . $in[0]; } if ($this->continuousBuffer) { $this->decryptIV = $_iv; } return $_plaintext; '; break; case self::MODE_OFB: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_plaintext_len = strlen($_text); $_xor = $this->encryptIV; $_buffer = &$this->enbuffer; if (strlen($_buffer["xor"])) { for ($_i = 0; $_i < $_plaintext_len; $_i+= ' . $block_size . ') { $_block = substr($_text, $_i, ' . $block_size . '); if (strlen($_block) > strlen($_buffer["xor"])) { $in = $_xor; ' . $encrypt_block . ' $_xor = $in; $_buffer["xor"].= $_xor; } $_key = \phpseclib3\Common\Functions\Strings::shift($_buffer["xor"], ' . $block_size . '); $_ciphertext.= $_block ^ $_key; } } else { for ($_i = 0; $_i < $_plaintext_len; $_i+= ' . $block_size . ') { $in = $_xor; ' . $encrypt_block . ' $_xor = $in; $_ciphertext.= substr($_text, $_i, ' . $block_size . ') ^ $_xor; } $_key = $_xor; } if ($this->continuousBuffer) { $this->encryptIV = $_xor; if ($_start = $_plaintext_len % ' . $block_size . ') { $_buffer["xor"] = substr($_key, $_start) . $_buffer["xor"]; } } return $_ciphertext; '; $decrypt = $init_encrypt . ' $_plaintext = ""; $_ciphertext_len = strlen($_text); $_xor = $this->decryptIV; $_buffer = &$this->debuffer; if (strlen($_buffer["xor"])) { for ($_i = 0; $_i < $_ciphertext_len; $_i+= ' . $block_size . ') { $_block = substr($_text, $_i, ' . $block_size . '); if (strlen($_block) > strlen($_buffer["xor"])) { $in = $_xor; ' . $encrypt_block . ' $_xor = $in; $_buffer["xor"].= $_xor; } $_key = \phpseclib3\Common\Functions\Strings::shift($_buffer["xor"], ' . $block_size . '); $_plaintext.= $_block ^ $_key; } } else { for ($_i = 0; $_i < $_ciphertext_len; $_i+= ' . $block_size . ') { $in = $_xor; ' . $encrypt_block . ' $_xor = $in; $_plaintext.= substr($_text, $_i, ' . $block_size . ') ^ $_xor; } $_key = $_xor; } if ($this->continuousBuffer) { $this->decryptIV = $_xor; if ($_start = $_ciphertext_len % ' . $block_size . ') { $_buffer["xor"] = substr($_key, $_start) . $_buffer["xor"]; } } return $_plaintext; '; break; case self::MODE_STREAM: $encrypt = $init_encrypt . ' $_ciphertext = ""; ' . $encrypt_block . ' return $_ciphertext; '; $decrypt = $init_decrypt . ' $_plaintext = ""; ' . $decrypt_block . ' return $_plaintext; '; break; // case self::MODE_CBC: default: $encrypt = $init_encrypt . ' $_ciphertext = ""; $_plaintext_len = strlen($_text); $in = $this->encryptIV; for ($_i = 0; $_i < $_plaintext_len; $_i+= ' . $block_size . ') { $in = substr($_text, $_i, ' . $block_size . ') ^ $in; ' . $encrypt_block . ' $_ciphertext.= $in; } if ($this->continuousBuffer) { $this->encryptIV = $in; } return $_ciphertext; '; $decrypt = $init_decrypt . ' $_plaintext = ""; $_text = str_pad($_text, strlen($_text) + (' . $block_size . ' - strlen($_text) % ' . $block_size . ') % ' . $block_size . ', chr(0)); $_ciphertext_len = strlen($_text); $_iv = $this->decryptIV; for ($_i = 0; $_i < $_ciphertext_len; $_i+= ' . $block_size . ') { $in = $_block = substr($_text, $_i, ' . $block_size . '); ' . $decrypt_block . ' $_plaintext.= $in ^ $_iv; $_iv = $_block; } if ($this->continuousBuffer) { $this->decryptIV = $_iv; } return $this->unpad($_plaintext); '; break; } // Before discrediting this, please read the following: // @see https://github.com/phpseclib/phpseclib/issues/1293 // @see https://github.com/phpseclib/phpseclib/pull/1143 eval('$func = function ($_action, $_text) { ' . $init_crypt . 'if ($_action == "encrypt") { ' . $encrypt . ' } else { ' . $decrypt . ' }};'); return \Closure::bind($func, $this, static::class); } /** * Convert float to int * * On ARM CPUs converting floats to ints doesn't always work * * @param string $x * @return int */ protected static function safe_intval($x) { if (is_int($x)) { return $x; } if (self::$use_reg_intval) { return PHP_INT_SIZE == 4 && PHP_VERSION_ID >= 80100 ? intval($x) : $x; } return (fmod($x, 0x80000000) & 0x7FFFFFFF) | ((fmod(floor($x / 0x80000000), 2) & 1) << 31); } /** * eval()'able string for in-line float to int * * @return string */ protected static function safe_intval_inline() { if (self::$use_reg_intval) { return PHP_INT_SIZE == 4 && PHP_VERSION_ID >= 80100 ? 'intval(%s)' : '%s'; } $safeint = '(is_int($temp = %s) ? $temp : (fmod($temp, 0x80000000) & 0x7FFFFFFF) | '; return $safeint . '((fmod(floor($temp / 0x80000000), 2) & 1) << 31))'; } /** * Sets up GCM parameters * * See steps 1-2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf#page=23 * for more info * */ private function setupGCM() { // don't keep on re-calculating $this->h if (!$this->h || $this->hKey != $this->key) { $cipher = new static('ecb'); $cipher->setKey($this->key); $cipher->disablePadding(); $this->h = self::$gcmField->newInteger( Strings::switchEndianness($cipher->encrypt("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")) ); $this->hKey = $this->key; } if (strlen($this->nonce) == 12) { $this->iv = $this->nonce . "\0\0\0\1"; } else { $this->iv = $this->ghash( self::nullPad128($this->nonce) . str_repeat("\0", 8) . self::len64($this->nonce) ); } } /** * Performs GHASH operation * * See https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf#page=20 * for more info * * @see self::decrypt() * @see self::encrypt() * @param string $x * @return string */ private function ghash($x) { $h = $this->h; $y = ["\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"]; $x = str_split($x, 16); $n = 0; // the switchEndianness calls are necessary because the multiplication algorithm in BinaryField/Integer // interprets strings as polynomials in big endian order whereas in GCM they're interpreted in little // endian order per https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf#page=19. // big endian order is what binary field elliptic curves use per http://www.secg.org/sec1-v2.pdf#page=18. // we could switchEndianness here instead of in the while loop but doing so in the while loop seems like it // might be slightly more performant //$x = Strings::switchEndianness($x); foreach ($x as $xn) { $xn = Strings::switchEndianness($xn); $t = $y[$n] ^ $xn; $temp = self::$gcmField->newInteger($t); $y[++$n] = $temp->multiply($h)->toBytes(); $y[$n] = substr($y[$n], 1); } $y[$n] = Strings::switchEndianness($y[$n]); return $y[$n]; } /** * Returns the bit length of a string in a packed format * * @see self::decrypt() * @see self::encrypt() * @see self::setupGCM() * @param string $str * @return string */ private static function len64($str) { return "\0\0\0\0" . pack('N', 8 * strlen($str)); } /** * NULL pads a string to be a multiple of 128 * * @see self::decrypt() * @see self::encrypt() * @see self::setupGCM() * @param string $str * @return string */ protected static function nullPad128($str) { $len = strlen($str); return $str . str_repeat("\0", 16 * ceil($len / 16) - $len); } /** * Calculates Poly1305 MAC * * On my system ChaCha20, with libsodium, takes 0.5s. With this custom Poly1305 implementation * it takes 1.2s. * * @see self::decrypt() * @see self::encrypt() * @param string $text * @return string */ protected function poly1305($text) { $s = $this->poly1305Key; // strlen($this->poly1305Key) == 32 $r = Strings::shift($s, 16); $r = strrev($r); $r &= "\x0f\xff\xff\xfc\x0f\xff\xff\xfc\x0f\xff\xff\xfc\x0f\xff\xff\xff"; $s = strrev($s); $r = self::$poly1305Field->newInteger(new BigInteger($r, 256)); $s = self::$poly1305Field->newInteger(new BigInteger($s, 256)); $a = self::$poly1305Field->newInteger(new BigInteger()); $blocks = str_split($text, 16); foreach ($blocks as $block) { $n = strrev($block . chr(1)); $n = self::$poly1305Field->newInteger(new BigInteger($n, 256)); $a = $a->add($n); $a = $a->multiply($r); } $r = $a->toBigInteger()->add($s->toBigInteger()); $mask = "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"; return strrev($r->toBytes()) & $mask; } /** * Return the mode * * You can do $obj instanceof AES or whatever to get the cipher but you can't do that to get the mode * * @return string */ public function getMode() { return array_flip(self::MODE_MAP)[$this->mode]; } /** * Is the continuous buffer enabled? * * @return boolean */ public function continuousBufferEnabled() { return $this->continuousBuffer; } } PK!Q{*<<Dvendor/phpseclib/phpseclib/phpseclib/Crypt/DH/Formats/Keys/PKCS1.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DH\Formats\Keys; use phpseclib3\Crypt\Common\Formats\Keys\PKCS1 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * "PKCS1" Formatted DH Key Handler * * @author Jim Wigginton */ abstract class PKCS1 extends Progenitor { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $components = ASN1::asn1map($decoded[0], Maps\DHParameter::MAP); if (!is_array($components)) { throw new \RuntimeException('Unable to perform ASN1 mapping on parameters'); } return $components; } /** * Convert EC parameters to the appropriate format * * @return string */ public static function saveParameters(BigInteger $prime, BigInteger $base, array $options = []) { $params = [ 'prime' => $prime, 'base' => $base ]; $params = ASN1::encodeDER($params, Maps\DHParameter::MAP); return "-----BEGIN DH PARAMETERS-----\r\n" . chunk_split(base64_encode($params), 64) . "-----END DH PARAMETERS-----\r\n"; } } PK!o`%Dvendor/phpseclib/phpseclib/phpseclib/Crypt/DH/Formats/Keys/PKCS8.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DH\Formats\Keys; use phpseclib3\Crypt\Common\Formats\Keys\PKCS8 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * PKCS#8 Formatted DH Key Handler * * @author Jim Wigginton */ abstract class PKCS8 extends Progenitor { /** * OID Name * * @var string */ const OID_NAME = 'dhKeyAgreement'; /** * OID Value * * @var string */ const OID_VALUE = '1.2.840.113549.1.3.1'; /** * Child OIDs loaded * * @var bool */ protected static $childOIDsLoaded = false; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); $type = isset($key['privateKey']) ? 'privateKey' : 'publicKey'; $decoded = ASN1::decodeBER($key[$type . 'Algorithm']['parameters']->element); if (empty($decoded)) { throw new \RuntimeException('Unable to decode BER of parameters'); } $components = ASN1::asn1map($decoded[0], Maps\DHParameter::MAP); if (!is_array($components)) { throw new \RuntimeException('Unable to perform ASN1 mapping on parameters'); } $decoded = ASN1::decodeBER($key[$type]); switch (true) { case !isset($decoded): case !isset($decoded[0]['content']): case !$decoded[0]['content'] instanceof BigInteger: throw new \RuntimeException('Unable to decode BER of parameters'); } $components[$type] = $decoded[0]['content']; return $components; } /** * Convert a private key to the appropriate format. * * @param BigInteger $prime * @param BigInteger $base * @param BigInteger $privateKey * @param BigInteger $publicKey * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $prime, BigInteger $base, BigInteger $privateKey, BigInteger $publicKey, $password = '', array $options = []) { $params = [ 'prime' => $prime, 'base' => $base ]; $params = ASN1::encodeDER($params, Maps\DHParameter::MAP); $params = new ASN1\Element($params); $key = ASN1::encodeDER($privateKey, ['type' => ASN1::TYPE_INTEGER]); return self::wrapPrivateKey($key, [], $params, $password, null, '', $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $prime * @param BigInteger $base * @param BigInteger $publicKey * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $prime, BigInteger $base, BigInteger $publicKey, array $options = []) { $params = [ 'prime' => $prime, 'base' => $base ]; $params = ASN1::encodeDER($params, Maps\DHParameter::MAP); $params = new ASN1\Element($params); $key = ASN1::encodeDER($publicKey, ['type' => ASN1::TYPE_INTEGER]); return self::wrapPublicKey($key, $params, null, $options); } } PK!.<vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/Parameters.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DH; use phpseclib3\Crypt\DH; /** * DH Parameters * * @author Jim Wigginton */ final class Parameters extends DH { /** * Returns the parameters * * @param string $type * @param array $options optional * @return string */ public function toString($type = 'PKCS1', array $options = []) { $type = self::validatePlugin('Keys', 'PKCS1', 'saveParameters'); return $type::saveParameters($this->prime, $this->base, $options); } } PK!t޵<vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/PrivateKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DH; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\DH; /** * DH Private Key * * @author Jim Wigginton */ final class PrivateKey extends DH { use Common\Traits\PasswordProtected; /** * Private Key * * @var \phpseclib3\Math\BigInteger */ protected $privateKey; /** * Public Key * * @var \phpseclib3\Math\BigInteger */ protected $publicKey; /** * Returns the public key * * @return PublicKey */ public function getPublicKey() { $type = self::validatePlugin('Keys', 'PKCS8', 'savePublicKey'); if (!isset($this->publicKey)) { $this->publicKey = $this->base->powMod($this->privateKey, $this->prime); } $key = $type::savePublicKey($this->prime, $this->base, $this->publicKey); return DH::loadFormat('PKCS8', $key); } /** * Returns the private key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePrivateKey'); if (!isset($this->publicKey)) { $this->publicKey = $this->base->powMod($this->privateKey, $this->prime); } return $type::savePrivateKey($this->prime, $this->base, $this->privateKey, $this->publicKey, $this->password, $options); } } PK!VM;vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/PublicKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DH; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\DH; /** * DH Public Key * * @author Jim Wigginton */ final class PublicKey extends DH { use Common\Traits\Fingerprint; /** * Returns the public key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePublicKey'); return $type::savePublicKey($this->prime, $this->base, $this->publicKey, $options); } /** * Returns the public key as a BigInteger * * @return \phpseclib3\Math\BigInteger */ public function toBigInteger() { return $this->publicKey; } } PK!4b:Gvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/OpenSSH.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\OpenSSH as Progenitor; use phpseclib3\Math\BigInteger; /** * OpenSSH Formatted DSA Key Handler * * @author Jim Wigginton */ abstract class OpenSSH extends Progenitor { /** * Supported Key Types * * @var array */ protected static $types = ['ssh-dss']; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $parsed = parent::load($key, $password); if (isset($parsed['paddedKey'])) { list($type) = Strings::unpackSSH2('s', $parsed['paddedKey']); if ($type != $parsed['type']) { throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])"); } list($p, $q, $g, $y, $x, $comment) = Strings::unpackSSH2('i5s', $parsed['paddedKey']); return compact('p', 'q', 'g', 'y', 'x', 'comment'); } list($p, $q, $g, $y) = Strings::unpackSSH2('iiii', $parsed['publicKey']); $comment = $parsed['comment']; return compact('p', 'q', 'g', 'y', 'comment'); } /** * Convert a public key to the appropriate format * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, array $options = []) { if ($q->getLength() != 160) { throw new \InvalidArgumentException('SSH only supports keys with an N (length of Group Order q) of 160'); } // from : // string "ssh-dss" // mpint p // mpint q // mpint g // mpint y $DSAPublicKey = Strings::packSSH2('siiii', 'ssh-dss', $p, $q, $g, $y); if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $DSAPublicKey; } $comment = isset($options['comment']) ? $options['comment'] : self::$comment; $DSAPublicKey = 'ssh-dss ' . base64_encode($DSAPublicKey) . ' ' . $comment; return $DSAPublicKey; } /** * Convert a private key to the appropriate format. * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param BigInteger $x * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, BigInteger $x, $password = '', array $options = []) { $publicKey = self::savePublicKey($p, $q, $g, $y, ['binary' => true]); $privateKey = Strings::packSSH2('si5', 'ssh-dss', $p, $q, $g, $y, $x); return self::wrapPrivateKey($publicKey, $privateKey, $password, $options); } } PK!-R++Evendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS1.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PKCS1 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * PKCS#1 Formatted DSA Key Handler * * @author Jim Wigginton */ abstract class PKCS1 extends Progenitor { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $key = ASN1::asn1map($decoded[0], Maps\DSAParams::MAP); if (is_array($key)) { return $key; } $key = ASN1::asn1map($decoded[0], Maps\DSAPrivateKey::MAP); if (is_array($key)) { return $key; } $key = ASN1::asn1map($decoded[0], Maps\DSAPublicKey::MAP); if (is_array($key)) { return $key; } throw new \RuntimeException('Unable to perform ASN1 mapping'); } /** * Convert DSA parameters to the appropriate format * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @return string */ public static function saveParameters(BigInteger $p, BigInteger $q, BigInteger $g) { $key = [ 'p' => $p, 'q' => $q, 'g' => $g ]; $key = ASN1::encodeDER($key, Maps\DSAParams::MAP); return "-----BEGIN DSA PARAMETERS-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END DSA PARAMETERS-----\r\n"; } /** * Convert a private key to the appropriate format. * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param BigInteger $x * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, BigInteger $x, $password = '', array $options = []) { $key = [ 'version' => 0, 'p' => $p, 'q' => $q, 'g' => $g, 'y' => $y, 'x' => $x ]; $key = ASN1::encodeDER($key, Maps\DSAPrivateKey::MAP); return self::wrapPrivateKey($key, 'DSA', $password, $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @return string */ public static function savePublicKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y) { $key = ASN1::encodeDER($y, Maps\DSAPublicKey::MAP); return self::wrapPublicKey($key, 'DSA'); } } PK!aQxxEvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS8.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Keys; use phpseclib3\Crypt\Common\Formats\Keys\PKCS8 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * PKCS#8 Formatted DSA Key Handler * * @author Jim Wigginton */ abstract class PKCS8 extends Progenitor { /** * OID Name * * @var string */ const OID_NAME = 'id-dsa'; /** * OID Value * * @var string */ const OID_VALUE = '1.2.840.10040.4.1'; /** * Child OIDs loaded * * @var bool */ protected static $childOIDsLoaded = false; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); $type = isset($key['privateKey']) ? 'privateKey' : 'publicKey'; $decoded = ASN1::decodeBER($key[$type . 'Algorithm']['parameters']->element); if (!$decoded) { throw new \RuntimeException('Unable to decode BER of parameters'); } $components = ASN1::asn1map($decoded[0], Maps\DSAParams::MAP); if (!is_array($components)) { throw new \RuntimeException('Unable to perform ASN1 mapping on parameters'); } $decoded = ASN1::decodeBER($key[$type]); if (empty($decoded)) { throw new \RuntimeException('Unable to decode BER'); } $var = $type == 'privateKey' ? 'x' : 'y'; $components[$var] = ASN1::asn1map($decoded[0], Maps\DSAPublicKey::MAP); if (!$components[$var] instanceof BigInteger) { throw new \RuntimeException('Unable to perform ASN1 mapping'); } if (isset($key['meta'])) { $components['meta'] = $key['meta']; } return $components; } /** * Convert a private key to the appropriate format. * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param BigInteger $x * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, BigInteger $x, $password = '', array $options = []) { $params = [ 'p' => $p, 'q' => $q, 'g' => $g ]; $params = ASN1::encodeDER($params, Maps\DSAParams::MAP); $params = new ASN1\Element($params); $key = ASN1::encodeDER($x, Maps\DSAPublicKey::MAP); return self::wrapPrivateKey($key, [], $params, $password, null, '', $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, array $options = []) { $params = [ 'p' => $p, 'q' => $q, 'g' => $g ]; $params = ASN1::encodeDER($params, Maps\DSAParams::MAP); $params = new ASN1\Element($params); $key = ASN1::encodeDER($y, Maps\DSAPublicKey::MAP); return self::wrapPublicKey($key, $params, null, $options); } } PK! 160 kinda useless, hence this handlers not supporting such keys. * * PHP version 5 * * @author Jim Wigginton * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PuTTY as Progenitor; use phpseclib3\Math\BigInteger; /** * PuTTY Formatted DSA Key Handler * * @author Jim Wigginton */ abstract class PuTTY extends Progenitor { /** * Public Handler * * @var string */ const PUBLIC_HANDLER = 'phpseclib3\Crypt\DSA\Formats\Keys\OpenSSH'; /** * Algorithm Identifier * * @var array */ protected static $types = ['ssh-dss']; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $components = parent::load($key, $password); if (!isset($components['private'])) { return $components; } extract($components); unset($components['public'], $components['private']); list($p, $q, $g, $y) = Strings::unpackSSH2('iiii', $public); list($x) = Strings::unpackSSH2('i', $private); return compact('p', 'q', 'g', 'y', 'x', 'comment'); } /** * Convert a private key to the appropriate format. * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param BigInteger $x * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, BigInteger $x, $password = false, array $options = []) { if ($q->getLength() != 160) { throw new \InvalidArgumentException('SSH only supports keys with an N (length of Group Order q) of 160'); } $public = Strings::packSSH2('iiii', $p, $q, $g, $y); $private = Strings::packSSH2('i', $x); return self::wrapPrivateKey($public, $private, 'ssh-dss', $password, $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @return string */ public static function savePublicKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y) { if ($q->getLength() != 160) { throw new \InvalidArgumentException('SSH only supports keys with an N (length of Group Order q) of 160'); } return self::wrapPublicKey(Strings::packSSH2('iiii', $p, $q, $g, $y), 'ssh-dss'); } } PK!#N~d d Cvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/Raw.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Keys; use phpseclib3\Math\BigInteger; /** * Raw DSA Key Handler * * @author Jim Wigginton */ abstract class Raw { /** * Break a public or private key down into its constituent components * * @param array $key * @param string $password optional * @return array */ public static function load($key, $password = '') { if (!is_array($key)) { throw new \UnexpectedValueException('Key should be a array - not a ' . gettype($key)); } switch (true) { case !isset($key['p']) || !isset($key['q']) || !isset($key['g']): case !$key['p'] instanceof BigInteger: case !$key['q'] instanceof BigInteger: case !$key['g'] instanceof BigInteger: case !isset($key['x']) && !isset($key['y']): case isset($key['x']) && !$key['x'] instanceof BigInteger: case isset($key['y']) && !$key['y'] instanceof BigInteger: throw new \UnexpectedValueException('Key appears to be malformed'); } $options = ['p' => 1, 'q' => 1, 'g' => 1, 'x' => 1, 'y' => 1]; return array_intersect_key($key, $options); } /** * Convert a private key to the appropriate format. * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @param BigInteger $x * @param string $password optional * @return string */ public static function savePrivateKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, BigInteger $x, $password = '') { return compact('p', 'q', 'g', 'y', 'x'); } /** * Convert a public key to the appropriate format * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @return string */ public static function savePublicKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y) { return compact('p', 'q', 'g', 'y'); } } PK!u!{  Cvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/XML.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\BadConfigurationException; use phpseclib3\Math\BigInteger; /** * XML Formatted DSA Key Handler * * @author Jim Wigginton */ abstract class XML { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } if (!class_exists('DOMDocument')) { throw new BadConfigurationException('The dom extension is not setup correctly on this system'); } $use_errors = libxml_use_internal_errors(true); $dom = new \DOMDocument(); if (substr($key, 0, 5) != '' . $key . ''; } if (!$dom->loadXML($key)) { libxml_use_internal_errors($use_errors); throw new \UnexpectedValueException('Key does not appear to contain XML'); } $xpath = new \DOMXPath($dom); $keys = ['p', 'q', 'g', 'y', 'j', 'seed', 'pgencounter']; foreach ($keys as $key) { // $dom->getElementsByTagName($key) is case-sensitive $temp = $xpath->query("//*[translate(local-name(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$key']"); if (!$temp->length) { continue; } $value = new BigInteger(Strings::base64_decode($temp->item(0)->nodeValue), 256); switch ($key) { case 'p': // a prime modulus meeting the [DSS] requirements // Parameters P, Q, and G can be public and common to a group of users. They might be known // from application context. As such, they are optional but P and Q must either both appear // or both be absent $components['p'] = $value; break; case 'q': // an integer in the range 2**159 < Q < 2**160 which is a prime divisor of P-1 $components['q'] = $value; break; case 'g': // an integer with certain properties with respect to P and Q $components['g'] = $value; break; case 'y': // G**X mod P (where X is part of the private key and not made public) $components['y'] = $value; // the remaining options do not do anything case 'j': // (P - 1) / Q // Parameter J is available for inclusion solely for efficiency as it is calculatable from // P and Q case 'seed': // a DSA prime generation seed // Parameters seed and pgenCounter are used in the DSA prime number generation algorithm // specified in [DSS]. As such, they are optional but must either both be present or both // be absent case 'pgencounter': // a DSA prime generation counter } } libxml_use_internal_errors($use_errors); if (!isset($components['y'])) { throw new \UnexpectedValueException('Key is missing y component'); } switch (true) { case !isset($components['p']): case !isset($components['q']): case !isset($components['g']): return ['y' => $components['y']]; } return $components; } /** * Convert a public key to the appropriate format * * See https://www.w3.org/TR/xmldsig-core/#sec-DSAKeyValue * * @param BigInteger $p * @param BigInteger $q * @param BigInteger $g * @param BigInteger $y * @return string */ public static function savePublicKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y) { return "\r\n" . '

' . Strings::base64_encode($p->toBytes()) . "

\r\n" . ' ' . Strings::base64_encode($q->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($g->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($y->toBytes()) . "\r\n" . '
'; } } PK!,ddIvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/ASN1.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Signature; use phpseclib3\File\ASN1 as Encoder; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * ASN1 Signature Handler * * @author Jim Wigginton */ abstract class ASN1 { /** * Loads a signature * * @param string $sig * @return array|bool */ public static function load($sig) { if (!is_string($sig)) { return false; } $decoded = Encoder::decodeBER($sig); if (empty($decoded)) { return false; } $components = Encoder::asn1map($decoded[0], Maps\DssSigValue::MAP); return $components; } /** * Returns a signature in the appropriate format * * @param BigInteger $r * @param BigInteger $s * @return string */ public static function save(BigInteger $r, BigInteger $s) { return Encoder::encodeDER(compact('r', 's'), Maps\DssSigValue::MAP); } } PK!ʻ  Hvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/Raw.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Signature; use phpseclib3\Crypt\Common\Formats\Signature\Raw as Progenitor; /** * Raw DSA Signature Handler * * @author Jim Wigginton */ abstract class Raw extends Progenitor { } PK!JeIvendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/SSH2.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA\Formats\Signature; use phpseclib3\Common\Functions\Strings; use phpseclib3\Math\BigInteger; /** * SSH2 Signature Handler * * @author Jim Wigginton */ abstract class SSH2 { /** * Loads a signature * * @param string $sig * @return mixed */ public static function load($sig) { if (!is_string($sig)) { return false; } $result = Strings::unpackSSH2('ss', $sig); if ($result === false) { return false; } list($type, $blob) = $result; if ($type != 'ssh-dss' || strlen($blob) != 40) { return false; } return [ 'r' => new BigInteger(substr($blob, 0, 20), 256), 's' => new BigInteger(substr($blob, 20), 256) ]; } /** * Returns a signature in the appropriate format * * @param BigInteger $r * @param BigInteger $s * @return string */ public static function save(BigInteger $r, BigInteger $s) { if ($r->getLength() > 160 || $s->getLength() > 160) { return false; } return Strings::packSSH2( 'ss', 'ssh-dss', str_pad($r->toBytes(), 20, "\0", STR_PAD_LEFT) . str_pad($s->toBytes(), 20, "\0", STR_PAD_LEFT) ); } } PK!h=vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Parameters.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA; use phpseclib3\Crypt\DSA; /** * DSA Parameters * * @author Jim Wigginton */ final class Parameters extends DSA { /** * Returns the parameters * * @param string $type * @param array $options optional * @return string */ public function toString($type = 'PKCS1', array $options = []) { $type = self::validatePlugin('Keys', 'PKCS1', 'saveParameters'); return $type::saveParameters($this->p, $this->q, $this->g, $options); } } PK!|=vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/PrivateKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\DSA; use phpseclib3\Crypt\DSA\Formats\Signature\ASN1 as ASN1Signature; use phpseclib3\Math\BigInteger; /** * DSA Private Key * * @author Jim Wigginton */ final class PrivateKey extends DSA implements Common\PrivateKey { use Common\Traits\PasswordProtected; /** * DSA secret exponent x * * @var BigInteger */ protected $x; /** * Returns the public key * * If you do "openssl rsa -in private.rsa -pubout -outform PEM" you get a PKCS8 formatted key * that contains a publicKeyAlgorithm AlgorithmIdentifier and a publicKey BIT STRING. * An AlgorithmIdentifier contains an OID and a parameters field. With RSA public keys this * parameters field is NULL. With DSA PKCS8 public keys it is not - it contains the p, q and g * variables. The publicKey BIT STRING contains, simply, the y variable. This can be verified * by getting a DSA PKCS8 public key: * * "openssl dsa -in private.dsa -pubout -outform PEM" * * ie. just swap out rsa with dsa in the rsa command above. * * A PKCS1 public key corresponds to the publicKey portion of the PKCS8 key. In the case of RSA * the publicKey portion /is/ the key. In the case of DSA it is not. You cannot verify a signature * without the parameters and the PKCS1 DSA public key format does not include the parameters. * * @see self::getPrivateKey() * @return mixed */ public function getPublicKey() { $type = self::validatePlugin('Keys', 'PKCS8', 'savePublicKey'); if (!isset($this->y)) { $this->y = $this->g->powMod($this->x, $this->p); } $key = $type::savePublicKey($this->p, $this->q, $this->g, $this->y); return DSA::loadFormat('PKCS8', $key) ->withHash($this->hash->getHash()) ->withSignatureFormat($this->shortFormat); } /** * Create a signature * * @see self::verify() * @param string $message * @return mixed */ public function sign($message) { $format = $this->sigFormat; if (self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods())) { $signature = ''; $result = openssl_sign($message, $signature, $this->toString('PKCS8'), $this->hash->getHash()); if ($result) { if ($this->shortFormat == 'ASN1') { return $signature; } extract(ASN1Signature::load($signature)); return $format::save($r, $s); } } $h = $this->hash->hash($message); $h = $this->bits2int($h); while (true) { $k = BigInteger::randomRange(self::$one, $this->q->subtract(self::$one)); $r = $this->g->powMod($k, $this->p); list(, $r) = $r->divide($this->q); if ($r->equals(self::$zero)) { continue; } $kinv = $k->modInverse($this->q); $temp = $h->add($this->x->multiply($r)); $temp = $kinv->multiply($temp); list(, $s) = $temp->divide($this->q); if (!$s->equals(self::$zero)) { break; } } // the following is an RFC6979 compliant implementation of deterministic DSA // it's unused because it's mainly intended for use when a good CSPRNG isn't // available. if phpseclib's CSPRNG isn't good then even key generation is // suspect /* $h1 = $this->hash->hash($message); $k = $this->computek($h1); $r = $this->g->powMod($k, $this->p); list(, $r) = $r->divide($this->q); $kinv = $k->modInverse($this->q); $h1 = $this->bits2int($h1); $temp = $h1->add($this->x->multiply($r)); $temp = $kinv->multiply($temp); list(, $s) = $temp->divide($this->q); */ return $format::save($r, $s); } /** * Returns the private key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePrivateKey'); if (!isset($this->y)) { $this->y = $this->g->powMod($this->x, $this->p); } return $type::savePrivateKey($this->p, $this->q, $this->g, $this->y, $this->x, $this->password, $options); } } PK!$I I <vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/PublicKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\DSA; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\DSA; use phpseclib3\Crypt\DSA\Formats\Signature\ASN1 as ASN1Signature; /** * DSA Public Key * * @author Jim Wigginton */ final class PublicKey extends DSA implements Common\PublicKey { use Common\Traits\Fingerprint; /** * Verify a signature * * @see self::verify() * @param string $message * @param string $signature * @return mixed */ public function verify($message, $signature) { $format = $this->sigFormat; $params = $format::load($signature); if ($params === false || count($params) != 2) { return false; } extract($params); if (self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods())) { $sig = $format != 'ASN1' ? ASN1Signature::save($r, $s) : $signature; $result = openssl_verify($message, $sig, $this->toString('PKCS8'), $this->hash->getHash()); if ($result != -1) { return (bool) $result; } } $q_1 = $this->q->subtract(self::$one); if (!$r->between(self::$one, $q_1) || !$s->between(self::$one, $q_1)) { return false; } $w = $s->modInverse($this->q); $h = $this->hash->hash($message); $h = $this->bits2int($h); list(, $u1) = $h->multiply($w)->divide($this->q); list(, $u2) = $r->multiply($w)->divide($this->q); $v1 = $this->g->powMod($u1, $this->p); $v2 = $this->y->powMod($u2, $this->p); list(, $v) = $v1->multiply($v2)->divide($this->p); list(, $v) = $v->divide($this->q); return $v->equals($r); } /** * Returns the public key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePublicKey'); return $type::savePublicKey($this->p, $this->q, $this->g, $this->y, $options); } } PK!BYYAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Base.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\BaseCurves; use phpseclib3\Math\BigInteger; /** * Base * * @author Jim Wigginton */ abstract class Base { /** * The Order * * @var BigInteger */ protected $order; /** * Finite Field Integer factory * * @var FiniteField\Integer */ protected $factory; /** * Returns a random integer * * @return object */ public function randomInteger() { return $this->factory->randomInteger(); } /** * Converts a BigInteger to a FiniteField\Integer integer * * @return object */ public function convertInteger(BigInteger $x) { return $this->factory->newInteger($x); } /** * Returns the length, in bytes, of the modulo * * @return integer */ public function getLengthInBytes() { return $this->factory->getLengthInBytes(); } /** * Returns the length, in bits, of the modulo * * @return integer */ public function getLength() { return $this->factory->getLength(); } /** * Multiply a point on the curve by a scalar * * Uses the montgomery ladder technique as described here: * * https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder * https://github.com/phpecc/phpecc/issues/16#issuecomment-59176772 * * @return array */ public function multiplyPoint(array $p, BigInteger $d) { $alreadyInternal = isset($p[2]); $r = $alreadyInternal ? [[], $p] : [[], $this->convertToInternal($p)]; $d = $d->toBits(); for ($i = 0; $i < strlen($d); $i++) { $d_i = (int) $d[$i]; $r[1 - $d_i] = $this->addPoint($r[0], $r[1]); $r[$d_i] = $this->doublePoint($r[$d_i]); } return $alreadyInternal ? $r[0] : $this->convertToAffine($r[0]); } /** * Creates a random scalar multiplier * * @return BigInteger */ public function createRandomMultiplier() { static $one; if (!isset($one)) { $one = new BigInteger(1); } return BigInteger::randomRange($one, $this->order->subtract($one)); } /** * Performs range check */ public function rangeCheck(BigInteger $x) { static $zero; if (!isset($zero)) { $zero = new BigInteger(); } if (!isset($this->order)) { throw new \RuntimeException('setOrder needs to be called before this method'); } if ($x->compare($this->order) > 0 || $x->compare($zero) <= 0) { throw new \RangeException('x must be between 1 and the order of the curve'); } } /** * Sets the Order */ public function setOrder(BigInteger $order) { $this->order = $order; } /** * Returns the Order * * @return BigInteger */ public function getOrder() { return $this->order; } /** * Use a custom defined modular reduction function * * @return object */ public function setReduction(callable $func) { $this->factory->setReduction($func); } /** * Returns the affine point * * @return object[] */ public function convertToAffine(array $p) { return $p; } /** * Converts an affine point to a jacobian coordinate * * @return object[] */ public function convertToInternal(array $p) { return $p; } /** * Negates a point * * @return object[] */ public function negatePoint(array $p) { $temp = [ $p[0], $p[1]->negate() ]; if (isset($p[2])) { $temp[] = $p[2]; } return $temp; } /** * Multiply and Add Points * * @return int[] */ public function multiplyAddPoints(array $points, array $scalars) { $p1 = $this->convertToInternal($points[0]); $p2 = $this->convertToInternal($points[1]); $p1 = $this->multiplyPoint($p1, $scalars[0]); $p2 = $this->multiplyPoint($p2, $scalars[1]); $r = $this->addPoint($p1, $p2); return $this->convertToAffine($r); } } PK!*&*&Cvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Binary.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\BaseCurves; use phpseclib3\Math\BigInteger; use phpseclib3\Math\BinaryField; use phpseclib3\Math\BinaryField\Integer as BinaryInteger; /** * Curves over y^2 + x*y = x^3 + a*x^2 + b * * @author Jim Wigginton */ class Binary extends Base { /** * Binary Field Integer factory * * @var BinaryField */ protected $factory; /** * Cofficient for x^1 * * @var object */ protected $a; /** * Cofficient for x^0 * * @var object */ protected $b; /** * Base Point * * @var object */ protected $p; /** * The number one over the specified finite field * * @var object */ protected $one; /** * The modulo * * @var BigInteger */ protected $modulo; /** * The Order * * @var BigInteger */ protected $order; /** * Sets the modulo */ public function setModulo(...$modulo) { $this->modulo = $modulo; $this->factory = new BinaryField(...$modulo); $this->one = $this->factory->newInteger("\1"); } /** * Set coefficients a and b * * @param string $a * @param string $b */ public function setCoefficients($a, $b) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->a = $this->factory->newInteger(pack('H*', $a)); $this->b = $this->factory->newInteger(pack('H*', $b)); } /** * Set x and y coordinates for the base point * * @param string|BinaryInteger $x * @param string|BinaryInteger $y */ public function setBasePoint($x, $y) { switch (true) { case !is_string($x) && !$x instanceof BinaryInteger: throw new \UnexpectedValueException('Argument 1 passed to Binary::setBasePoint() must be a string or an instance of BinaryField\Integer'); case !is_string($y) && !$y instanceof BinaryInteger: throw new \UnexpectedValueException('Argument 2 passed to Binary::setBasePoint() must be a string or an instance of BinaryField\Integer'); } if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->p = [ is_string($x) ? $this->factory->newInteger(pack('H*', $x)) : $x, is_string($y) ? $this->factory->newInteger(pack('H*', $y)) : $y ]; } /** * Retrieve the base point as an array * * @return array */ public function getBasePoint() { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } /* if (!isset($this->p)) { throw new \RuntimeException('setBasePoint needs to be called before this method'); } */ return $this->p; } /** * Adds two points on the curve * * @return FiniteField[] */ public function addPoint(array $p, array $q) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p) || !count($q)) { if (count($q)) { return $q; } if (count($p)) { return $p; } return []; } if (!isset($p[2]) || !isset($q[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to "Jacobi" coordinates or vice versa'); } if ($p[0]->equals($q[0])) { return !$p[1]->equals($q[1]) ? [] : $this->doublePoint($p); } // formulas from http://hyperelliptic.org/EFD/g12o/auto-shortw-jacobian.html list($x1, $y1, $z1) = $p; list($x2, $y2, $z2) = $q; $o1 = $z1->multiply($z1); $b = $x2->multiply($o1); if ($z2->equals($this->one)) { $d = $y2->multiply($o1)->multiply($z1); $e = $x1->add($b); $f = $y1->add($d); $z3 = $e->multiply($z1); $h = $f->multiply($x2)->add($z3->multiply($y2)); $i = $f->add($z3); $g = $z3->multiply($z3); $p1 = $this->a->multiply($g); $p2 = $f->multiply($i); $p3 = $e->multiply($e)->multiply($e); $x3 = $p1->add($p2)->add($p3); $y3 = $i->multiply($x3)->add($g->multiply($h)); return [$x3, $y3, $z3]; } $o2 = $z2->multiply($z2); $a = $x1->multiply($o2); $c = $y1->multiply($o2)->multiply($z2); $d = $y2->multiply($o1)->multiply($z1); $e = $a->add($b); $f = $c->add($d); $g = $e->multiply($z1); $h = $f->multiply($x2)->add($g->multiply($y2)); $z3 = $g->multiply($z2); $i = $f->add($z3); $p1 = $this->a->multiply($z3->multiply($z3)); $p2 = $f->multiply($i); $p3 = $e->multiply($e)->multiply($e); $x3 = $p1->add($p2)->add($p3); $y3 = $i->multiply($x3)->add($g->multiply($g)->multiply($h)); return [$x3, $y3, $z3]; } /** * Doubles a point on a curve * * @return FiniteField[] */ public function doublePoint(array $p) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p)) { return []; } if (!isset($p[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to "Jacobi" coordinates or vice versa'); } // formulas from http://hyperelliptic.org/EFD/g12o/auto-shortw-jacobian.html list($x1, $y1, $z1) = $p; $a = $x1->multiply($x1); $b = $a->multiply($a); if ($z1->equals($this->one)) { $x3 = $b->add($this->b); $z3 = clone $x1; $p1 = $a->add($y1)->add($z3)->multiply($this->b); $p2 = $a->add($y1)->multiply($b); $y3 = $p1->add($p2); return [$x3, $y3, $z3]; } $c = $z1->multiply($z1); $d = $c->multiply($c); $x3 = $b->add($this->b->multiply($d->multiply($d))); $z3 = $x1->multiply($c); $p1 = $b->multiply($z3); $p2 = $a->add($y1->multiply($z1))->add($z3)->multiply($x3); $y3 = $p1->add($p2); return [$x3, $y3, $z3]; } /** * Returns the X coordinate and the derived Y coordinate * * Not supported because it is covered by patents. * Quoting https://www.openssl.org/docs/man1.1.0/apps/ecparam.html , * * "Due to patent issues the compressed option is disabled by default for binary curves * and can be enabled by defining the preprocessor macro OPENSSL_EC_BIN_PT_COMP at * compile time." * * @return array */ public function derivePoint($m) { throw new \RuntimeException('Point compression on binary finite field elliptic curves is not supported'); } /** * Tests whether or not the x / y values satisfy the equation * * @return boolean */ public function verifyPoint(array $p) { list($x, $y) = $p; $lhs = $y->multiply($y); $lhs = $lhs->add($x->multiply($y)); $x2 = $x->multiply($x); $x3 = $x2->multiply($x); $rhs = $x3->add($this->a->multiply($x2))->add($this->b); return $lhs->equals($rhs); } /** * Returns the modulo * * @return BigInteger */ public function getModulo() { return $this->modulo; } /** * Returns the a coefficient * * @return \phpseclib3\Math\PrimeField\Integer */ public function getA() { return $this->a; } /** * Returns the a coefficient * * @return \phpseclib3\Math\PrimeField\Integer */ public function getB() { return $this->b; } /** * Returns the affine point * * A Jacobian Coordinate is of the form (x, y, z). * To convert a Jacobian Coordinate to an Affine Point * you do (x / z^2, y / z^3) * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToAffine(array $p) { if (!isset($p[2])) { return $p; } list($x, $y, $z) = $p; $z = $this->one->divide($z); $z2 = $z->multiply($z); return [ $x->multiply($z2), $y->multiply($z2)->multiply($z) ]; } /** * Converts an affine point to a jacobian coordinate * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToInternal(array $p) { if (isset($p[2])) { return $p; } $p[2] = clone $this->one; $p['fresh'] = true; return $p; } } PK!E''Ivendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/KoblitzPrime.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\BaseCurves; use phpseclib3\Math\BigInteger; use phpseclib3\Math\PrimeField; /** * Curves over y^2 = x^3 + b * * @author Jim Wigginton */ class KoblitzPrime extends Prime { /** * Basis * * @var list */ protected $basis; /** * Beta * * @var PrimeField\Integer */ protected $beta; // don't overwrite setCoefficients() with one that only accepts one parameter so that // one might be able to switch between KoblitzPrime and Prime more easily (for benchmarking // purposes). /** * Multiply and Add Points * * Uses a efficiently computable endomorphism to achieve a slight speedup * * Adapted from: * https://github.com/indutny/elliptic/blob/725bd91/lib/elliptic/curve/short.js#L219 * * @return int[] */ public function multiplyAddPoints(array $points, array $scalars) { static $zero, $one, $two; if (!isset($two)) { $two = new BigInteger(2); $one = new BigInteger(1); } if (!isset($this->beta)) { // get roots $inv = $this->one->divide($this->two)->negate(); $s = $this->three->negate()->squareRoot()->multiply($inv); $betas = [ $inv->add($s), $inv->subtract($s) ]; $this->beta = $betas[0]->compare($betas[1]) < 0 ? $betas[0] : $betas[1]; //echo strtoupper($this->beta->toHex(true)) . "\n"; exit; } if (!isset($this->basis)) { $factory = new PrimeField($this->order); $tempOne = $factory->newInteger($one); $tempTwo = $factory->newInteger($two); $tempThree = $factory->newInteger(new BigInteger(3)); $inv = $tempOne->divide($tempTwo)->negate(); $s = $tempThree->negate()->squareRoot()->multiply($inv); $lambdas = [ $inv->add($s), $inv->subtract($s) ]; $lhs = $this->multiplyPoint($this->p, $lambdas[0])[0]; $rhs = $this->p[0]->multiply($this->beta); $lambda = $lhs->equals($rhs) ? $lambdas[0] : $lambdas[1]; $this->basis = static::extendedGCD($lambda->toBigInteger(), $this->order); ///* foreach ($this->basis as $basis) { echo strtoupper($basis['a']->toHex(true)) . "\n"; echo strtoupper($basis['b']->toHex(true)) . "\n\n"; } exit; //*/ } $npoints = $nscalars = []; for ($i = 0; $i < count($points); $i++) { $p = $points[$i]; $k = $scalars[$i]->toBigInteger(); // begin split list($v1, $v2) = $this->basis; $c1 = $v2['b']->multiply($k); list($c1, $r) = $c1->divide($this->order); if ($this->order->compare($r->multiply($two)) <= 0) { $c1 = $c1->add($one); } $c2 = $v1['b']->negate()->multiply($k); list($c2, $r) = $c2->divide($this->order); if ($this->order->compare($r->multiply($two)) <= 0) { $c2 = $c2->add($one); } $p1 = $c1->multiply($v1['a']); $p2 = $c2->multiply($v2['a']); $q1 = $c1->multiply($v1['b']); $q2 = $c2->multiply($v2['b']); $k1 = $k->subtract($p1)->subtract($p2); $k2 = $q1->add($q2)->negate(); // end split $beta = [ $p[0]->multiply($this->beta), $p[1], clone $this->one ]; if (isset($p['naf'])) { $beta['naf'] = array_map(function ($p) { return [ $p[0]->multiply($this->beta), $p[1], clone $this->one ]; }, $p['naf']); $beta['nafwidth'] = $p['nafwidth']; } if ($k1->isNegative()) { $k1 = $k1->negate(); $p = $this->negatePoint($p); } if ($k2->isNegative()) { $k2 = $k2->negate(); $beta = $this->negatePoint($beta); } $pos = 2 * $i; $npoints[$pos] = $p; $nscalars[$pos] = $this->factory->newInteger($k1); $pos++; $npoints[$pos] = $beta; $nscalars[$pos] = $this->factory->newInteger($k2); } return parent::multiplyAddPoints($npoints, $nscalars); } /** * Returns the numerator and denominator of the slope * * @return FiniteField[] */ protected function doublePointHelper(array $p) { $numerator = $this->three->multiply($p[0])->multiply($p[0]); $denominator = $this->two->multiply($p[1]); return [$numerator, $denominator]; } /** * Doubles a jacobian coordinate on the curve * * See http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l * * @return FiniteField[] */ protected function jacobianDoublePoint(array $p) { list($x1, $y1, $z1) = $p; $a = $x1->multiply($x1); $b = $y1->multiply($y1); $c = $b->multiply($b); $d = $x1->add($b); $d = $d->multiply($d)->subtract($a)->subtract($c)->multiply($this->two); $e = $this->three->multiply($a); $f = $e->multiply($e); $x3 = $f->subtract($this->two->multiply($d)); $y3 = $e->multiply($d->subtract($x3))->subtract( $this->eight->multiply($c) ); $z3 = $this->two->multiply($y1)->multiply($z1); return [$x3, $y3, $z3]; } /** * Doubles a "fresh" jacobian coordinate on the curve * * See http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-mdbl-2007-bl * * @return FiniteField[] */ protected function jacobianDoublePointMixed(array $p) { list($x1, $y1) = $p; $xx = $x1->multiply($x1); $yy = $y1->multiply($y1); $yyyy = $yy->multiply($yy); $s = $x1->add($yy); $s = $s->multiply($s)->subtract($xx)->subtract($yyyy)->multiply($this->two); $m = $this->three->multiply($xx); $t = $m->multiply($m)->subtract($this->two->multiply($s)); $x3 = $t; $y3 = $s->subtract($t); $y3 = $m->multiply($y3)->subtract($this->eight->multiply($yyyy)); $z3 = $this->two->multiply($y1); return [$x3, $y3, $z3]; } /** * Tests whether or not the x / y values satisfy the equation * * @return boolean */ public function verifyPoint(array $p) { list($x, $y) = $p; $lhs = $y->multiply($y); $temp = $x->multiply($x)->multiply($x); $rhs = $temp->add($this->b); return $lhs->equals($rhs); } /** * Calculates the parameters needed from the Euclidean algorithm as discussed at * http://diamond.boisestate.edu/~liljanab/MATH308/GuideToECC.pdf#page=148 * * @param BigInteger $u * @param BigInteger $v * @return BigInteger[] */ protected static function extendedGCD(BigInteger $u, BigInteger $v) { $one = new BigInteger(1); $zero = new BigInteger(); $a = clone $one; $b = clone $zero; $c = clone $zero; $d = clone $one; $stop = $v->bitwise_rightShift($v->getLength() >> 1); $a1 = clone $zero; $b1 = clone $zero; $a2 = clone $zero; $b2 = clone $zero; $postGreatestIndex = 0; while (!$v->equals($zero)) { list($q) = $u->divide($v); $temp = $u; $u = $v; $v = $temp->subtract($v->multiply($q)); $temp = $a; $a = $c; $c = $temp->subtract($a->multiply($q)); $temp = $b; $b = $d; $d = $temp->subtract($b->multiply($q)); if ($v->compare($stop) > 0) { $a0 = $v; $b0 = $c; } else { $postGreatestIndex++; } if ($postGreatestIndex == 1) { $a1 = $v; $b1 = $c->negate(); } if ($postGreatestIndex == 2) { $rhs = $a0->multiply($a0)->add($b0->multiply($b0)); $lhs = $v->multiply($v)->add($b->multiply($b)); if ($lhs->compare($rhs) <= 0) { $a2 = $a0; $b2 = $b0->negate(); } else { $a2 = $v; $b2 = $c->negate(); } break; } } return [ ['a' => $a1, 'b' => $b1], ['a' => $a2, 'b' => $b2] ]; } } PK!XGvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Montgomery.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\BaseCurves; use phpseclib3\Crypt\EC\Curves\Curve25519; use phpseclib3\Math\BigInteger; use phpseclib3\Math\PrimeField; use phpseclib3\Math\PrimeField\Integer as PrimeInteger; /** * Curves over y^2 = x^3 + a*x + x * * @author Jim Wigginton */ class Montgomery extends Base { /** * Prime Field Integer factory * * @var PrimeField */ protected $factory; /** * Cofficient for x * * @var object */ protected $a; /** * Constant used for point doubling * * @var object */ protected $a24; /** * The Number Zero * * @var object */ protected $zero; /** * The Number One * * @var object */ protected $one; /** * Base Point * * @var object */ protected $p; /** * The modulo * * @var BigInteger */ protected $modulo; /** * The Order * * @var BigInteger */ protected $order; /** * Sets the modulo */ public function setModulo(BigInteger $modulo) { $this->modulo = $modulo; $this->factory = new PrimeField($modulo); $this->zero = $this->factory->newInteger(new BigInteger()); $this->one = $this->factory->newInteger(new BigInteger(1)); } /** * Set coefficients a */ public function setCoefficients(BigInteger $a) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->a = $this->factory->newInteger($a); $two = $this->factory->newInteger(new BigInteger(2)); $four = $this->factory->newInteger(new BigInteger(4)); $this->a24 = $this->a->subtract($two)->divide($four); } /** * Set x and y coordinates for the base point * * @param BigInteger|PrimeInteger $x * @param BigInteger|PrimeInteger $y * @return PrimeInteger[] */ public function setBasePoint($x, $y) { switch (true) { case !$x instanceof BigInteger && !$x instanceof PrimeInteger: throw new \UnexpectedValueException('Argument 1 passed to Prime::setBasePoint() must be an instance of either BigInteger or PrimeField\Integer'); case !$y instanceof BigInteger && !$y instanceof PrimeInteger: throw new \UnexpectedValueException('Argument 2 passed to Prime::setBasePoint() must be an instance of either BigInteger or PrimeField\Integer'); } if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->p = [ $x instanceof BigInteger ? $this->factory->newInteger($x) : $x, $y instanceof BigInteger ? $this->factory->newInteger($y) : $y ]; } /** * Retrieve the base point as an array * * @return array */ public function getBasePoint() { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } /* if (!isset($this->p)) { throw new \RuntimeException('setBasePoint needs to be called before this method'); } */ return $this->p; } /** * Doubles and adds a point on a curve * * See https://tools.ietf.org/html/draft-ietf-tls-curve25519-01#appendix-A.1.3 * * @return FiniteField[][] */ private function doubleAndAddPoint(array $p, array $q, PrimeInteger $x1) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p) || !count($q)) { return []; } if (!isset($p[1])) { throw new \RuntimeException('Affine coordinates need to be manually converted to XZ coordinates'); } list($x2, $z2) = $p; list($x3, $z3) = $q; $a = $x2->add($z2); $aa = $a->multiply($a); $b = $x2->subtract($z2); $bb = $b->multiply($b); $e = $aa->subtract($bb); $c = $x3->add($z3); $d = $x3->subtract($z3); $da = $d->multiply($a); $cb = $c->multiply($b); $temp = $da->add($cb); $x5 = $temp->multiply($temp); $temp = $da->subtract($cb); $z5 = $x1->multiply($temp->multiply($temp)); $x4 = $aa->multiply($bb); $temp = static::class == Curve25519::class ? $bb : $aa; $z4 = $e->multiply($temp->add($this->a24->multiply($e))); return [ [$x4, $z4], [$x5, $z5] ]; } /** * Multiply a point on the curve by a scalar * * Uses the montgomery ladder technique as described here: * * https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#Montgomery_ladder * https://github.com/phpecc/phpecc/issues/16#issuecomment-59176772 * * @return array */ public function multiplyPoint(array $p, BigInteger $d) { $p1 = [$this->one, $this->zero]; $alreadyInternal = isset($p[1]); $p2 = $this->convertToInternal($p); $x = $p[0]; $b = $d->toBits(); $b = str_pad($b, 256, '0', STR_PAD_LEFT); for ($i = 0; $i < strlen($b); $i++) { $b_i = (int) $b[$i]; if ($b_i) { list($p2, $p1) = $this->doubleAndAddPoint($p2, $p1, $x); } else { list($p1, $p2) = $this->doubleAndAddPoint($p1, $p2, $x); } } return $alreadyInternal ? $p1 : $this->convertToAffine($p1); } /** * Converts an affine point to an XZ coordinate * * From https://hyperelliptic.org/EFD/g1p/auto-montgom-xz.html * * XZ coordinates represent x y as X Z satsfying the following equations: * * x=X/Z * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToInternal(array $p) { if (empty($p)) { return [clone $this->zero, clone $this->one]; } if (isset($p[1])) { return $p; } $p[1] = clone $this->one; return $p; } /** * Returns the affine point * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToAffine(array $p) { if (!isset($p[1])) { return $p; } list($x, $z) = $p; return [$x->divide($z)]; } } PK!^mSSBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Prime.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\BaseCurves; use phpseclib3\Common\Functions\Strings; use phpseclib3\Math\BigInteger; use phpseclib3\Math\Common\FiniteField\Integer; use phpseclib3\Math\PrimeField; use phpseclib3\Math\PrimeField\Integer as PrimeInteger; /** * Curves over y^2 = x^3 + a*x + b * * @author Jim Wigginton */ class Prime extends Base { /** * Prime Field Integer factory * * @var \phpseclib3\Math\PrimeFields */ protected $factory; /** * Cofficient for x^1 * * @var object */ protected $a; /** * Cofficient for x^0 * * @var object */ protected $b; /** * Base Point * * @var object */ protected $p; /** * The number one over the specified finite field * * @var object */ protected $one; /** * The number two over the specified finite field * * @var object */ protected $two; /** * The number three over the specified finite field * * @var object */ protected $three; /** * The number four over the specified finite field * * @var object */ protected $four; /** * The number eight over the specified finite field * * @var object */ protected $eight; /** * The modulo * * @var BigInteger */ protected $modulo; /** * The Order * * @var BigInteger */ protected $order; /** * Sets the modulo */ public function setModulo(BigInteger $modulo) { $this->modulo = $modulo; $this->factory = new PrimeField($modulo); $this->two = $this->factory->newInteger(new BigInteger(2)); $this->three = $this->factory->newInteger(new BigInteger(3)); // used by jacobian coordinates $this->one = $this->factory->newInteger(new BigInteger(1)); $this->four = $this->factory->newInteger(new BigInteger(4)); $this->eight = $this->factory->newInteger(new BigInteger(8)); } /** * Set coefficients a and b */ public function setCoefficients(BigInteger $a, BigInteger $b) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->a = $this->factory->newInteger($a); $this->b = $this->factory->newInteger($b); } /** * Set x and y coordinates for the base point * * @param BigInteger|PrimeInteger $x * @param BigInteger|PrimeInteger $y * @return PrimeInteger[] */ public function setBasePoint($x, $y) { switch (true) { case !$x instanceof BigInteger && !$x instanceof PrimeInteger: throw new \UnexpectedValueException('Argument 1 passed to Prime::setBasePoint() must be an instance of either BigInteger or PrimeField\Integer'); case !$y instanceof BigInteger && !$y instanceof PrimeInteger: throw new \UnexpectedValueException('Argument 2 passed to Prime::setBasePoint() must be an instance of either BigInteger or PrimeField\Integer'); } if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->p = [ $x instanceof BigInteger ? $this->factory->newInteger($x) : $x, $y instanceof BigInteger ? $this->factory->newInteger($y) : $y ]; } /** * Retrieve the base point as an array * * @return array */ public function getBasePoint() { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } /* if (!isset($this->p)) { throw new \RuntimeException('setBasePoint needs to be called before this method'); } */ return $this->p; } /** * Adds two "fresh" jacobian form on the curve * * @return FiniteField[] */ protected function jacobianAddPointMixedXY(array $p, array $q) { list($u1, $s1) = $p; list($u2, $s2) = $q; if ($u1->equals($u2)) { if (!$s1->equals($s2)) { return []; } else { return $this->doublePoint($p); } } $h = $u2->subtract($u1); $r = $s2->subtract($s1); $h2 = $h->multiply($h); $h3 = $h2->multiply($h); $v = $u1->multiply($h2); $x3 = $r->multiply($r)->subtract($h3)->subtract($v->multiply($this->two)); $y3 = $r->multiply( $v->subtract($x3) )->subtract( $s1->multiply($h3) ); return [$x3, $y3, $h]; } /** * Adds one "fresh" jacobian form on the curve * * The second parameter should be the "fresh" one * * @return FiniteField[] */ protected function jacobianAddPointMixedX(array $p, array $q) { list($u1, $s1, $z1) = $p; list($x2, $y2) = $q; $z12 = $z1->multiply($z1); $u2 = $x2->multiply($z12); $s2 = $y2->multiply($z12->multiply($z1)); if ($u1->equals($u2)) { if (!$s1->equals($s2)) { return []; } else { return $this->doublePoint($p); } } $h = $u2->subtract($u1); $r = $s2->subtract($s1); $h2 = $h->multiply($h); $h3 = $h2->multiply($h); $v = $u1->multiply($h2); $x3 = $r->multiply($r)->subtract($h3)->subtract($v->multiply($this->two)); $y3 = $r->multiply( $v->subtract($x3) )->subtract( $s1->multiply($h3) ); $z3 = $h->multiply($z1); return [$x3, $y3, $z3]; } /** * Adds two jacobian coordinates on the curve * * @return FiniteField[] */ protected function jacobianAddPoint(array $p, array $q) { list($x1, $y1, $z1) = $p; list($x2, $y2, $z2) = $q; $z12 = $z1->multiply($z1); $z22 = $z2->multiply($z2); $u1 = $x1->multiply($z22); $u2 = $x2->multiply($z12); $s1 = $y1->multiply($z22->multiply($z2)); $s2 = $y2->multiply($z12->multiply($z1)); if ($u1->equals($u2)) { if (!$s1->equals($s2)) { return []; } else { return $this->doublePoint($p); } } $h = $u2->subtract($u1); $r = $s2->subtract($s1); $h2 = $h->multiply($h); $h3 = $h2->multiply($h); $v = $u1->multiply($h2); $x3 = $r->multiply($r)->subtract($h3)->subtract($v->multiply($this->two)); $y3 = $r->multiply( $v->subtract($x3) )->subtract( $s1->multiply($h3) ); $z3 = $h->multiply($z1)->multiply($z2); return [$x3, $y3, $z3]; } /** * Adds two points on the curve * * @return FiniteField[] */ public function addPoint(array $p, array $q) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p) || !count($q)) { if (count($q)) { return $q; } if (count($p)) { return $p; } return []; } // use jacobian coordinates if (isset($p[2]) && isset($q[2])) { if (isset($p['fresh']) && isset($q['fresh'])) { return $this->jacobianAddPointMixedXY($p, $q); } if (isset($p['fresh'])) { return $this->jacobianAddPointMixedX($q, $p); } if (isset($q['fresh'])) { return $this->jacobianAddPointMixedX($p, $q); } return $this->jacobianAddPoint($p, $q); } if (isset($p[2]) || isset($q[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to Jacobi coordinates or vice versa'); } if ($p[0]->equals($q[0])) { if (!$p[1]->equals($q[1])) { return []; } else { // eg. doublePoint list($numerator, $denominator) = $this->doublePointHelper($p); } } else { $numerator = $q[1]->subtract($p[1]); $denominator = $q[0]->subtract($p[0]); } $slope = $numerator->divide($denominator); $x = $slope->multiply($slope)->subtract($p[0])->subtract($q[0]); $y = $slope->multiply($p[0]->subtract($x))->subtract($p[1]); return [$x, $y]; } /** * Returns the numerator and denominator of the slope * * @return FiniteField[] */ protected function doublePointHelper(array $p) { $numerator = $this->three->multiply($p[0])->multiply($p[0])->add($this->a); $denominator = $this->two->multiply($p[1]); return [$numerator, $denominator]; } /** * Doubles a jacobian coordinate on the curve * * @return FiniteField[] */ protected function jacobianDoublePoint(array $p) { list($x, $y, $z) = $p; $x2 = $x->multiply($x); $y2 = $y->multiply($y); $z2 = $z->multiply($z); $s = $this->four->multiply($x)->multiply($y2); $m1 = $this->three->multiply($x2); $m2 = $this->a->multiply($z2->multiply($z2)); $m = $m1->add($m2); $x1 = $m->multiply($m)->subtract($this->two->multiply($s)); $y1 = $m->multiply($s->subtract($x1))->subtract( $this->eight->multiply($y2->multiply($y2)) ); $z1 = $this->two->multiply($y)->multiply($z); return [$x1, $y1, $z1]; } /** * Doubles a "fresh" jacobian coordinate on the curve * * @return FiniteField[] */ protected function jacobianDoublePointMixed(array $p) { list($x, $y) = $p; $x2 = $x->multiply($x); $y2 = $y->multiply($y); $s = $this->four->multiply($x)->multiply($y2); $m1 = $this->three->multiply($x2); $m = $m1->add($this->a); $x1 = $m->multiply($m)->subtract($this->two->multiply($s)); $y1 = $m->multiply($s->subtract($x1))->subtract( $this->eight->multiply($y2->multiply($y2)) ); $z1 = $this->two->multiply($y); return [$x1, $y1, $z1]; } /** * Doubles a point on a curve * * @return FiniteField[] */ public function doublePoint(array $p) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p)) { return []; } // use jacobian coordinates if (isset($p[2])) { if (isset($p['fresh'])) { return $this->jacobianDoublePointMixed($p); } return $this->jacobianDoublePoint($p); } list($numerator, $denominator) = $this->doublePointHelper($p); $slope = $numerator->divide($denominator); $x = $slope->multiply($slope)->subtract($p[0])->subtract($p[0]); $y = $slope->multiply($p[0]->subtract($x))->subtract($p[1]); return [$x, $y]; } /** * Returns the X coordinate and the derived Y coordinate * * @return array */ public function derivePoint($m) { $y = ord(Strings::shift($m)); $x = new BigInteger($m, 256); $xp = $this->convertInteger($x); switch ($y) { case 2: $ypn = false; break; case 3: $ypn = true; break; default: throw new \RuntimeException('Coordinate not in recognized format'); } $temp = $xp->multiply($this->a); $temp = $xp->multiply($xp)->multiply($xp)->add($temp); $temp = $temp->add($this->b); $b = $temp->squareRoot(); if (!$b) { throw new \RuntimeException('Unable to derive Y coordinate'); } $bn = $b->isOdd(); $yp = $ypn == $bn ? $b : $b->negate(); return [$xp, $yp]; } /** * Tests whether or not the x / y values satisfy the equation * * @return boolean */ public function verifyPoint(array $p) { list($x, $y) = $p; $lhs = $y->multiply($y); $temp = $x->multiply($this->a); $temp = $x->multiply($x)->multiply($x)->add($temp); $rhs = $temp->add($this->b); return $lhs->equals($rhs); } /** * Returns the modulo * * @return BigInteger */ public function getModulo() { return $this->modulo; } /** * Returns the a coefficient * * @return PrimeInteger */ public function getA() { return $this->a; } /** * Returns the a coefficient * * @return PrimeInteger */ public function getB() { return $this->b; } /** * Multiply and Add Points * * Adapted from: * https://github.com/indutny/elliptic/blob/725bd91/lib/elliptic/curve/base.js#L125 * * @return int[] */ public function multiplyAddPoints(array $points, array $scalars) { $length = count($points); foreach ($points as &$point) { $point = $this->convertToInternal($point); } $wnd = [$this->getNAFPoints($points[0], 7)]; $wndWidth = [isset($points[0]['nafwidth']) ? $points[0]['nafwidth'] : 7]; for ($i = 1; $i < $length; $i++) { $wnd[] = $this->getNAFPoints($points[$i], 1); $wndWidth[] = isset($points[$i]['nafwidth']) ? $points[$i]['nafwidth'] : 1; } $naf = []; // comb all window NAFs $max = 0; for ($i = $length - 1; $i >= 1; $i -= 2) { $a = $i - 1; $b = $i; if ($wndWidth[$a] != 1 || $wndWidth[$b] != 1) { $naf[$a] = $scalars[$a]->getNAF($wndWidth[$a]); $naf[$b] = $scalars[$b]->getNAF($wndWidth[$b]); $max = max(count($naf[$a]), count($naf[$b]), $max); continue; } $comb = [ $points[$a], // 1 null, // 3 null, // 5 $points[$b] // 7 ]; $comb[1] = $this->addPoint($points[$a], $points[$b]); $comb[2] = $this->addPoint($points[$a], $this->negatePoint($points[$b])); $index = [ -3, /* -1 -1 */ -1, /* -1 0 */ -5, /* -1 1 */ -7, /* 0 -1 */ 0, /* 0 -1 */ 7, /* 0 1 */ 5, /* 1 -1 */ 1, /* 1 0 */ 3 /* 1 1 */ ]; $jsf = self::getJSFPoints($scalars[$a], $scalars[$b]); $max = max(count($jsf[0]), $max); if ($max > 0) { $naf[$a] = array_fill(0, $max, 0); $naf[$b] = array_fill(0, $max, 0); } else { $naf[$a] = []; $naf[$b] = []; } for ($j = 0; $j < $max; $j++) { $ja = isset($jsf[0][$j]) ? $jsf[0][$j] : 0; $jb = isset($jsf[1][$j]) ? $jsf[1][$j] : 0; $naf[$a][$j] = $index[3 * ($ja + 1) + $jb + 1]; $naf[$b][$j] = 0; $wnd[$a] = $comb; } } $acc = []; $temp = [0, 0, 0, 0]; for ($i = $max; $i >= 0; $i--) { $k = 0; while ($i >= 0) { $zero = true; for ($j = 0; $j < $length; $j++) { $temp[$j] = isset($naf[$j][$i]) ? $naf[$j][$i] : 0; if ($temp[$j] != 0) { $zero = false; } } if (!$zero) { break; } $k++; $i--; } if ($i >= 0) { $k++; } while ($k--) { $acc = $this->doublePoint($acc); } if ($i < 0) { break; } for ($j = 0; $j < $length; $j++) { $z = $temp[$j]; $p = null; if ($z == 0) { continue; } $p = $z > 0 ? $wnd[$j][($z - 1) >> 1] : $this->negatePoint($wnd[$j][(-$z - 1) >> 1]); $acc = $this->addPoint($acc, $p); } } return $this->convertToAffine($acc); } /** * Precomputes NAF points * * Adapted from: * https://github.com/indutny/elliptic/blob/725bd91/lib/elliptic/curve/base.js#L351 * * @return int[] */ private function getNAFPoints(array $point, $wnd) { if (isset($point['naf'])) { return $point['naf']; } $res = [$point]; $max = (1 << $wnd) - 1; $dbl = $max == 1 ? null : $this->doublePoint($point); for ($i = 1; $i < $max; $i++) { $res[] = $this->addPoint($res[$i - 1], $dbl); } $point['naf'] = $res; /* $str = ''; foreach ($res as $re) { $re[0] = bin2hex($re[0]->toBytes()); $re[1] = bin2hex($re[1]->toBytes()); $str.= " ['$re[0]', '$re[1]'],\r\n"; } file_put_contents('temp.txt', $str); exit; */ return $res; } /** * Precomputes points in Joint Sparse Form * * Adapted from: * https://github.com/indutny/elliptic/blob/725bd91/lib/elliptic/utils.js#L96 * * @return int[] */ private static function getJSFPoints(Integer $k1, Integer $k2) { static $three; if (!isset($three)) { $three = new BigInteger(3); } $jsf = [[], []]; $k1 = $k1->toBigInteger(); $k2 = $k2->toBigInteger(); $d1 = 0; $d2 = 0; while ($k1->compare(new BigInteger(-$d1)) > 0 || $k2->compare(new BigInteger(-$d2)) > 0) { // first phase $m14 = $k1->testBit(0) + 2 * $k1->testBit(1); $m14 += $d1; $m14 &= 3; $m24 = $k2->testBit(0) + 2 * $k2->testBit(1); $m24 += $d2; $m24 &= 3; if ($m14 == 3) { $m14 = -1; } if ($m24 == 3) { $m24 = -1; } $u1 = 0; if ($m14 & 1) { // if $m14 is odd $m8 = $k1->testBit(0) + 2 * $k1->testBit(1) + 4 * $k1->testBit(2); $m8 += $d1; $m8 &= 7; $u1 = ($m8 == 3 || $m8 == 5) && $m24 == 2 ? -$m14 : $m14; } $jsf[0][] = $u1; $u2 = 0; if ($m24 & 1) { // if $m24 is odd $m8 = $k2->testBit(0) + 2 * $k2->testBit(1) + 4 * $k2->testBit(2); $m8 += $d2; $m8 &= 7; $u2 = ($m8 == 3 || $m8 == 5) && $m14 == 2 ? -$m24 : $m24; } $jsf[1][] = $u2; // second phase if (2 * $d1 == $u1 + 1) { $d1 = 1 - $d1; } if (2 * $d2 == $u2 + 1) { $d2 = 1 - $d2; } $k1 = $k1->bitwise_rightShift(1); $k2 = $k2->bitwise_rightShift(1); } return $jsf; } /** * Returns the affine point * * A Jacobian Coordinate is of the form (x, y, z). * To convert a Jacobian Coordinate to an Affine Point * you do (x / z^2, y / z^3) * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToAffine(array $p) { if (!isset($p[2])) { return $p; } list($x, $y, $z) = $p; $z = $this->one->divide($z); $z2 = $z->multiply($z); return [ $x->multiply($z2), $y->multiply($z2)->multiply($z) ]; } /** * Converts an affine point to a jacobian coordinate * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToInternal(array $p) { if (isset($p[2])) { return $p; } $p[2] = clone $this->one; $p['fresh'] = true; return $p; } } PK!e?$$Kvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/TwistedEdwards.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\BaseCurves; use phpseclib3\Math\BigInteger; use phpseclib3\Math\PrimeField; use phpseclib3\Math\PrimeField\Integer as PrimeInteger; /** * Curves over a*x^2 + y^2 = 1 + d*x^2*y^2 * * @author Jim Wigginton */ class TwistedEdwards extends Base { /** * The modulo * * @var BigInteger */ protected $modulo; /** * Cofficient for x^2 * * @var object */ protected $a; /** * Cofficient for x^2*y^2 * * @var object */ protected $d; /** * Base Point * * @var object[] */ protected $p; /** * The number zero over the specified finite field * * @var object */ protected $zero; /** * The number one over the specified finite field * * @var object */ protected $one; /** * The number two over the specified finite field * * @var object */ protected $two; /** * Sets the modulo */ public function setModulo(BigInteger $modulo) { $this->modulo = $modulo; $this->factory = new PrimeField($modulo); $this->zero = $this->factory->newInteger(new BigInteger(0)); $this->one = $this->factory->newInteger(new BigInteger(1)); $this->two = $this->factory->newInteger(new BigInteger(2)); } /** * Set coefficients a and b */ public function setCoefficients(BigInteger $a, BigInteger $d) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->a = $this->factory->newInteger($a); $this->d = $this->factory->newInteger($d); } /** * Set x and y coordinates for the base point */ public function setBasePoint($x, $y) { switch (true) { case !$x instanceof BigInteger && !$x instanceof PrimeInteger: throw new \UnexpectedValueException('Argument 1 passed to Prime::setBasePoint() must be an instance of either BigInteger or PrimeField\Integer'); case !$y instanceof BigInteger && !$y instanceof PrimeInteger: throw new \UnexpectedValueException('Argument 2 passed to Prime::setBasePoint() must be an instance of either BigInteger or PrimeField\Integer'); } if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } $this->p = [ $x instanceof BigInteger ? $this->factory->newInteger($x) : $x, $y instanceof BigInteger ? $this->factory->newInteger($y) : $y ]; } /** * Returns the a coefficient * * @return PrimeInteger */ public function getA() { return $this->a; } /** * Returns the a coefficient * * @return PrimeInteger */ public function getD() { return $this->d; } /** * Retrieve the base point as an array * * @return array */ public function getBasePoint() { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } /* if (!isset($this->p)) { throw new \RuntimeException('setBasePoint needs to be called before this method'); } */ return $this->p; } /** * Returns the affine point * * @return PrimeField\Integer[] */ public function convertToAffine(array $p) { if (!isset($p[2])) { return $p; } list($x, $y, $z) = $p; $z = $this->one->divide($z); return [ $x->multiply($z), $y->multiply($z) ]; } /** * Returns the modulo * * @return BigInteger */ public function getModulo() { return $this->modulo; } /** * Tests whether or not the x / y values satisfy the equation * * @return boolean */ public function verifyPoint(array $p) { list($x, $y) = $p; $x2 = $x->multiply($x); $y2 = $y->multiply($y); $lhs = $this->a->multiply($x2)->add($y2); $rhs = $this->d->multiply($x2)->multiply($y2)->add($this->one); return $lhs->equals($rhs); } } PK!JHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP160r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP160r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('E95E4A5F737059DC60DFC7AD95B3D8139515620F', 16)); $this->setCoefficients( new BigInteger('340E7BE2A280EB74E2BE61BADA745D97E8F7C300', 16), new BigInteger('1E589A8595423412134FAA2DBDEC95C8D8675E58', 16) ); $this->setBasePoint( new BigInteger('BED5AF16EA3F6A4F62938C4631EB5AF7BDBCDBC3', 16), new BigInteger('1667CB477A1A8EC338F94741669C976316DA6321', 16) ); $this->setOrder(new BigInteger('E95E4A5F737059DC60DF5991D45029409E60FC09', 16)); } } PK!)Hvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP160t1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP160t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('E95E4A5F737059DC60DFC7AD95B3D8139515620F', 16)); $this->setCoefficients( new BigInteger('E95E4A5F737059DC60DFC7AD95B3D8139515620C', 16), // eg. -3 new BigInteger('7A556B6DAE535B7B51ED2C4D7DAA7A0B5C55F380', 16) ); $this->setBasePoint( new BigInteger('B199B13B9B34EFC1397E64BAEB05ACC265FF2378', 16), new BigInteger('ADD6718B7C7C1961F0991B842443772152C9E0AD', 16) ); $this->setOrder(new BigInteger('E95E4A5F737059DC60DF5991D45029409E60FC09', 16)); } } PK!GGGHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP192r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP192r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('C302F41D932A36CDA7A3463093D18DB78FCE476DE1A86297', 16)); $this->setCoefficients( new BigInteger('6A91174076B1E0E19C39C031FE8685C1CAE040E5C69A28EF', 16), new BigInteger('469A28EF7C28CCA3DC721D044F4496BCCA7EF4146FBF25C9', 16) ); $this->setBasePoint( new BigInteger('C0A0647EAAB6A48753B033C56CB0F0900A2F5C4853375FD6', 16), new BigInteger('14B690866ABD5BB88B5F4828C1490002E6773FA2FA299B8F', 16) ); $this->setOrder(new BigInteger('C302F41D932A36CDA7A3462F9E9E916B5BE8F1029AC4ACC1', 16)); } } PK!sQQHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP192t1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP192t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('C302F41D932A36CDA7A3463093D18DB78FCE476DE1A86297', 16)); $this->setCoefficients( new BigInteger('C302F41D932A36CDA7A3463093D18DB78FCE476DE1A86294', 16), // eg. -3 new BigInteger('13D56FFAEC78681E68F9DEB43B35BEC2FB68542E27897B79', 16) ); $this->setBasePoint( new BigInteger('3AE9E58C82F63C30282E1FE7BBF43FA72C446AF6F4618129', 16), new BigInteger('097E2C5667C2223A902AB5CA449D0084B7E5B3DE7CCC01C9', 16) ); $this->setOrder(new BigInteger('C302F41D932A36CDA7A3462F9E9E916B5BE8F1029AC4ACC1', 16)); } } PK!<wwHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP224r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP224r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('D7C134AA264366862A18302575D1D787B09F075797DA89F57EC8C0FF', 16)); $this->setCoefficients( new BigInteger('68A5E62CA9CE6C1C299803A6C1530B514E182AD8B0042A59CAD29F43', 16), new BigInteger('2580F63CCFE44138870713B1A92369E33E2135D266DBB372386C400B', 16) ); $this->setBasePoint( new BigInteger('0D9029AD2C7E5CF4340823B2A87DC68C9E4CE3174C1E6EFDEE12C07D', 16), new BigInteger('58AA56F772C0726F24C6B89E4ECDAC24354B9E99CAA3F6D3761402CD', 16) ); $this->setOrder(new BigInteger('D7C134AA264366862A18302575D0FB98D116BC4B6DDEBCA3A5A7939F', 16)); } } PK!ZǁHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP224t1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP224t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('D7C134AA264366862A18302575D1D787B09F075797DA89F57EC8C0FF', 16)); $this->setCoefficients( new BigInteger('D7C134AA264366862A18302575D1D787B09F075797DA89F57EC8C0FC', 16), // eg. -3 new BigInteger('4B337D934104CD7BEF271BF60CED1ED20DA14C08B3BB64F18A60888D', 16) ); $this->setBasePoint( new BigInteger('6AB1E344CE25FF3896424E7FFE14762ECB49F8928AC0C76029B4D580', 16), new BigInteger('0374E9F5143E568CD23F3F4D7C0D4B1E41C8CC0D1C6ABD5F1A46DB4C', 16) ); $this->setOrder(new BigInteger('D7C134AA264366862A18302575D0FB98D116BC4B6DDEBCA3A5A7939F', 16)); } } PK!oHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP256r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP256r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377', 16)); $this->setCoefficients( new BigInteger('7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9', 16), new BigInteger('26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6', 16) ); $this->setBasePoint( new BigInteger('8BD2AEB9CB7E57CB2C4B482FFC81B7AFB9DE27E1E3BD23C23A4453BD9ACE3262', 16), new BigInteger('547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997', 16) ); $this->setOrder(new BigInteger('A9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7', 16)); } } PK!6Hvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP256t1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP256t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377', 16)); $this->setCoefficients( new BigInteger('A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5374', 16), // eg. -3 new BigInteger('662C61C430D84EA4FE66A7733D0B76B7BF93EBC4AF2F49256AE58101FEE92B04', 16) ); $this->setBasePoint( new BigInteger('A3E8EB3CC1CFE7B7732213B23A656149AFA142C47AAFBC2B79A191562E1305F4', 16), new BigInteger('2D996C823439C56D7F7B22E14644417E69BCB6DE39D027001DABE8F35B25C9BE', 16) ); $this->setOrder(new BigInteger('A9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7', 16)); } } PK!]Hvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP320r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP320r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('D35E472036BC4FB7E13C785ED201E065F98FCFA6F6F40DEF4F9' . '2B9EC7893EC28FCD412B1F1B32E27', 16)); $this->setCoefficients( new BigInteger('3EE30B568FBAB0F883CCEBD46D3F3BB8A2A73513F5EB79DA66190EB085FFA9F4' . '92F375A97D860EB4', 16), new BigInteger('520883949DFDBC42D3AD198640688A6FE13F41349554B49ACC31DCCD88453981' . '6F5EB4AC8FB1F1A6', 16) ); $this->setBasePoint( new BigInteger('43BD7E9AFB53D8B85289BCC48EE5BFE6F20137D10A087EB6E7871E2A10A599C7' . '10AF8D0D39E20611', 16), new BigInteger('14FDD05545EC1CC8AB4093247F77275E0743FFED117182EAA9C77877AAAC6AC7' . 'D35245D1692E8EE1', 16) ); $this->setOrder(new BigInteger('D35E472036BC4FB7E13C785ED201E065F98FCFA5B68F12A32D4' . '82EC7EE8658E98691555B44C59311', 16)); } } PK!&Hvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP320t1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP320t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('D35E472036BC4FB7E13C785ED201E065F98FCFA6F6F40DEF4F9' . '2B9EC7893EC28FCD412B1F1B32E27', 16)); $this->setCoefficients( new BigInteger('D35E472036BC4FB7E13C785ED201E065F98FCFA6F6F40DEF4F92B9EC7893EC28' . 'FCD412B1F1B32E24', 16), // eg. -3 new BigInteger('A7F561E038EB1ED560B3D147DB782013064C19F27ED27C6780AAF77FB8A547CE' . 'B5B4FEF422340353', 16) ); $this->setBasePoint( new BigInteger('925BE9FB01AFC6FB4D3E7D4990010F813408AB106C4F09CB7EE07868CC136FFF' . '3357F624A21BED52', 16), new BigInteger('63BA3A7A27483EBF6671DBEF7ABB30EBEE084E58A0B077AD42A5A0989D1EE71B' . '1B9BC0455FB0D2C3', 16) ); $this->setOrder(new BigInteger('D35E472036BC4FB7E13C785ED201E065F98FCFA5B68F12A32D4' . '82EC7EE8658E98691555B44C59311', 16)); } } PK!HHHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP384r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP384r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger( '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A7' . '1874700133107EC53', 16 )); $this->setCoefficients( new BigInteger( '7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503' . 'AD4EB04A8C7DD22CE2826', 16 ), new BigInteger( '4A8C7DD22CE28268B39B55416F0447C2FB77DE107DCD2A62E880EA53EEB62D57CB4390295DB' . 'C9943AB78696FA504C11', 16 ) ); $this->setBasePoint( new BigInteger( '1D1C64F068CF45FFA2A63A81B7C13F6B8847A3E77EF14FE3DB7FCAFE0CBD10E8E826E03436D' . '646AAEF87B2E247D4AF1E', 16 ), new BigInteger( '8ABE1D7520F9C2A45CB1EB8E95CFD55262B70B29FEEC5864E19C054FF99129280E464621779' . '1811142820341263C5315', 16 ) ); $this->setOrder(new BigInteger( '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC31' . '03B883202E9046565', 16 )); } } PK! * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP384t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger( '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A7' . '1874700133107EC53', 16 )); $this->setCoefficients( new BigInteger( '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901' . 'D1A71874700133107EC50', 16 ), // eg. -3 new BigInteger( '7F519EADA7BDA81BD826DBA647910F8C4B9346ED8CCDC64E4B1ABD11756DCE1D2074AA263B8' . '8805CED70355A33B471EE', 16 ) ); $this->setBasePoint( new BigInteger( '18DE98B02DB9A306F2AFCD7235F72A819B80AB12EBD653172476FECD462AABFFC4FF191B946' . 'A5F54D8D0AA2F418808CC', 16 ), new BigInteger( '25AB056962D30651A114AFD2755AD336747F93475B7A1FCA3B88F2B6A208CCFE469408584DC' . '2B2912675BF5B9E582928', 16 ) ); $this->setOrder(new BigInteger( '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC31' . '03B883202E9046565', 16 )); } } PK!ęHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP512r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP512r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger( 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC' . '66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F3', 16 )); $this->setCoefficients( new BigInteger( '7830A3318B603B89E2327145AC234CC594CBDD8D3DF91610A83441CAEA9863BC2DED5D5AA82' . '53AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CA', 16 ), new BigInteger( '3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C' . '1AC4D77FC94CADC083E67984050B75EBAE5DD2809BD638016F723', 16 ) ); $this->setBasePoint( new BigInteger( '81AEE4BDD82ED9645A21322E9C4C6A9385ED9F70B5D916C1B43B62EEF4D0098EFF3B1F78E2D' . '0D48D50D1687B93B97D5F7C6D5047406A5E688B352209BCB9F822', 16 ), new BigInteger( '7DDE385D566332ECC0EABFA9CF7822FDF209F70024A57B1AA000C55B881F8111B2DCDE494A5' . 'F485E5BCA4BD88A2763AED1CA2B2FA8F0540678CD1E0F3AD80892', 16 ) ); $this->setOrder(new BigInteger( 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA' . '92619418661197FAC10471DB1D381085DDADDB58796829CA90069', 16 )); } } PK!h:OuHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP512t1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class brainpoolP512t1 extends Prime { public function __construct() { $this->setModulo(new BigInteger( 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC' . '66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F3', 16 )); $this->setCoefficients( new BigInteger( 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC' . '66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F0', 16 ), // eg. -3 new BigInteger( '7CBBBCF9441CFAB76E1890E46884EAE321F70C0BCB4981527897504BEC3E36A62BCDFA23049' . '76540F6450085F2DAE145C22553B465763689180EA2571867423E', 16 ) ); $this->setBasePoint( new BigInteger( '640ECE5C12788717B9C1BA06CBC2A6FEBA85842458C56DDE9DB1758D39C0313D82BA51735CD' . 'B3EA499AA77A7D6943A64F7A3F25FE26F06B51BAA2696FA9035DA', 16 ), new BigInteger( '5B534BD595F5AF0FA2C892376C84ACE1BB4E3019B71634C01131159CAE03CEE9D9932184BEE' . 'F216BD71DF2DADF86A627306ECFF96DBB8BACE198B61E00F8B332', 16 ) ); $this->setOrder(new BigInteger( 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA' . '92619418661197FAC10471DB1D381085DDADDB58796829CA90069', 16 )); } } PK!0 Cvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Curve25519.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Montgomery; use phpseclib3\Math\BigInteger; class Curve25519 extends Montgomery { public function __construct() { // 2^255 - 19 $this->setModulo(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED', 16)); $this->a24 = $this->factory->newInteger(new BigInteger('121666')); $this->p = [$this->factory->newInteger(new BigInteger(9))]; // 2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed $this->setOrder(new BigInteger('1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED', 16)); /* $this->setCoefficients( new BigInteger('486662'), // a ); $this->setBasePoint( new BigInteger(9), new BigInteger('14781619447589544791020593568409986887264606134616475288964881837755586237401') ); */ } /** * Multiply a point on the curve by a scalar * * Modifies the scalar as described at https://tools.ietf.org/html/rfc7748#page-8 * * @return array */ public function multiplyPoint(array $p, BigInteger $d) { //$r = strrev(sodium_crypto_scalarmult($d->toBytes(), strrev($p[0]->toBytes()))); //return [$this->factory->newInteger(new BigInteger($r, 256))]; $d = $d->toBytes(); $d &= "\xF8" . str_repeat("\xFF", 30) . "\x7F"; $d = strrev($d); $d |= "\x40"; $d = new BigInteger($d, -256); return parent::multiplyPoint($p, $d); } /** * Creates a random scalar multiplier * * @return BigInteger */ public function createRandomMultiplier() { return BigInteger::random(256); } /** * Performs range check */ public function rangeCheck(BigInteger $x) { if ($x->getLength() > 256 || $x->isNegative()) { throw new \RangeException('x must be a positive integer less than 256 bytes in length'); } } } PK!vG G Avendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Curve448.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Montgomery; use phpseclib3\Math\BigInteger; class Curve448 extends Montgomery { public function __construct() { // 2^448 - 2^224 - 1 $this->setModulo(new BigInteger( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE' . 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16 )); $this->a24 = $this->factory->newInteger(new BigInteger('39081')); $this->p = [$this->factory->newInteger(new BigInteger(5))]; // 2^446 - 0x8335dc163bb124b65129c96fde933d8d723a70aadc873d6d54a7bb0d $this->setOrder(new BigInteger( '3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . '7CCA23E9C44EDB49AED63690216CC2728DC58F552378C292AB5844F3', 16 )); /* $this->setCoefficients( new BigInteger('156326'), // a ); $this->setBasePoint( new BigInteger(5), new BigInteger( '355293926785568175264127502063783334808976399387714271831880898' . '435169088786967410002932673765864550910142774147268105838985595290' . '606362') ); */ } /** * Multiply a point on the curve by a scalar * * Modifies the scalar as described at https://tools.ietf.org/html/rfc7748#page-8 * * @return array */ public function multiplyPoint(array $p, BigInteger $d) { //$r = strrev(sodium_crypto_scalarmult($d->toBytes(), strrev($p[0]->toBytes()))); //return [$this->factory->newInteger(new BigInteger($r, 256))]; $d = $d->toBytes(); $d[0] = $d[0] & "\xFC"; $d = strrev($d); $d |= "\x80"; $d = new BigInteger($d, 256); return parent::multiplyPoint($p, $d); } /** * Creates a random scalar multiplier * * @return BigInteger */ public function createRandomMultiplier() { return BigInteger::random(446); } /** * Performs range check */ public function rangeCheck(BigInteger $x) { if ($x->getLength() > 448 || $x->isNegative()) { throw new \RangeException('x must be a positive integer less than 446 bytes in length'); } } } PK!δ''@vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Ed25519.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\Random; use phpseclib3\Math\BigInteger; class Ed25519 extends TwistedEdwards { const HASH = 'sha512'; /* Per https://tools.ietf.org/html/rfc8032#page-6 EdDSA has several parameters, one of which is b: 2. An integer b with 2^(b-1) > p. EdDSA public keys have exactly b bits, and EdDSA signatures have exactly 2*b bits. b is recommended to be a multiple of 8, so public key and signature lengths are an integral number of octets. SIZE corresponds to b */ const SIZE = 32; public function __construct() { // 2^255 - 19 $this->setModulo(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED', 16)); $this->setCoefficients( // -1 new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC', 16), // a // -121665/121666 new BigInteger('52036CEE2B6FFE738CC740797779E89800700A4D4141D8AB75EB4DCA135978A3', 16) // d ); $this->setBasePoint( new BigInteger('216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A', 16), new BigInteger('6666666666666666666666666666666666666666666666666666666666666658', 16) ); $this->setOrder(new BigInteger('1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED', 16)); // algorithm 14.47 from http://cacr.uwaterloo.ca/hac/about/chap14.pdf#page=16 /* $this->setReduction(function($x) { $parts = $x->bitwise_split(255); $className = $this->className; if (count($parts) > 2) { list(, $r) = $x->divide($className::$modulo); return $r; } $zero = new BigInteger(); $c = new BigInteger(19); switch (count($parts)) { case 2: list($qi, $ri) = $parts; break; case 1: $qi = $zero; list($ri) = $parts; break; case 0: return $zero; } $r = $ri; while ($qi->compare($zero) > 0) { $temp = $qi->multiply($c)->bitwise_split(255); if (count($temp) == 2) { list($qi, $ri) = $temp; } else { $qi = $zero; list($ri) = $temp; } $r = $r->add($ri); } while ($r->compare($className::$modulo) > 0) { $r = $r->subtract($className::$modulo); } return $r; }); */ } /** * Recover X from Y * * Implements steps 2-4 at https://tools.ietf.org/html/rfc8032#section-5.1.3 * * Used by EC\Keys\Common.php * * @param BigInteger $y * @param boolean $sign * @return object[] */ public function recoverX(BigInteger $y, $sign) { $y = $this->factory->newInteger($y); $y2 = $y->multiply($y); $u = $y2->subtract($this->one); $v = $this->d->multiply($y2)->add($this->one); $x2 = $u->divide($v); if ($x2->equals($this->zero)) { if ($sign) { throw new \RuntimeException('Unable to recover X coordinate (x2 = 0)'); } return clone $this->zero; } // find the square root /* we don't do $x2->squareRoot() because, quoting from https://tools.ietf.org/html/rfc8032#section-5.1.1: "For point decoding or "decompression", square roots modulo p are needed. They can be computed using the Tonelli-Shanks algorithm or the special case for p = 5 (mod 8). To find a square root of a, first compute the candidate root x = a^((p+3)/8) (mod p)." */ $exp = $this->getModulo()->add(new BigInteger(3)); $exp = $exp->bitwise_rightShift(3); $x = $x2->pow($exp); // If v x^2 = -u (mod p), set x <-- x * 2^((p-1)/4), which is a square root. if (!$x->multiply($x)->subtract($x2)->equals($this->zero)) { $temp = $this->getModulo()->subtract(new BigInteger(1)); $temp = $temp->bitwise_rightShift(2); $temp = $this->two->pow($temp); $x = $x->multiply($temp); if (!$x->multiply($x)->subtract($x2)->equals($this->zero)) { throw new \RuntimeException('Unable to recover X coordinate'); } } if ($x->isOdd() != $sign) { $x = $x->negate(); } return [$x, $y]; } /** * Extract Secret Scalar * * Implements steps 1-3 at https://tools.ietf.org/html/rfc8032#section-5.1.5 * * Used by the various key handlers * * @param string $str * @return array */ public function extractSecret($str) { if (strlen($str) != 32) { throw new \LengthException('Private Key should be 32-bytes long'); } // 1. Hash the 32-byte private key using SHA-512, storing the digest in // a 64-octet large buffer, denoted h. Only the lower 32 bytes are // used for generating the public key. $hash = new Hash('sha512'); $h = $hash->hash($str); $h = substr($h, 0, 32); // 2. Prune the buffer: The lowest three bits of the first octet are // cleared, the highest bit of the last octet is cleared, and the // second highest bit of the last octet is set. $h[0] = $h[0] & chr(0xF8); $h = strrev($h); $h[0] = ($h[0] & chr(0x3F)) | chr(0x40); // 3. Interpret the buffer as the little-endian integer, forming a // secret scalar s. $dA = new BigInteger($h, 256); return [ 'dA' => $dA, 'secret' => $str ]; } /** * Encode a point as a string * * @param array $point * @return string */ public function encodePoint($point) { list($x, $y) = $point; $y = $y->toBytes(); $y[0] = $y[0] & chr(0x7F); if ($x->isOdd()) { $y[0] = $y[0] | chr(0x80); } $y = strrev($y); return $y; } /** * Creates a random scalar multiplier * * @return \phpseclib3\Math\PrimeField\Integer */ public function createRandomMultiplier() { return $this->extractSecret(Random::string(32))['dA']; } /** * Converts an affine point to an extended homogeneous coordinate * * From https://tools.ietf.org/html/rfc8032#section-5.1.4 : * * A point (x,y) is represented in extended homogeneous coordinates (X, Y, Z, T), * with x = X/Z, y = Y/Z, x * y = T/Z. * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToInternal(array $p) { if (empty($p)) { return [clone $this->zero, clone $this->one, clone $this->one, clone $this->zero]; } if (isset($p[2])) { return $p; } $p[2] = clone $this->one; $p[3] = $p[0]->multiply($p[1]); return $p; } /** * Doubles a point on a curve * * @return FiniteField[] */ public function doublePoint(array $p) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p)) { return []; } if (!isset($p[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to "Jacobi" coordinates or vice versa'); } // from https://tools.ietf.org/html/rfc8032#page-12 list($x1, $y1, $z1, $t1) = $p; $a = $x1->multiply($x1); $b = $y1->multiply($y1); $c = $this->two->multiply($z1)->multiply($z1); $h = $a->add($b); $temp = $x1->add($y1); $e = $h->subtract($temp->multiply($temp)); $g = $a->subtract($b); $f = $c->add($g); $x3 = $e->multiply($f); $y3 = $g->multiply($h); $t3 = $e->multiply($h); $z3 = $f->multiply($g); return [$x3, $y3, $z3, $t3]; } /** * Adds two points on the curve * * @return FiniteField[] */ public function addPoint(array $p, array $q) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p) || !count($q)) { if (count($q)) { return $q; } if (count($p)) { return $p; } return []; } if (!isset($p[2]) || !isset($q[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to "Jacobi" coordinates or vice versa'); } if ($p[0]->equals($q[0])) { return !$p[1]->equals($q[1]) ? [] : $this->doublePoint($p); } // from https://tools.ietf.org/html/rfc8032#page-12 list($x1, $y1, $z1, $t1) = $p; list($x2, $y2, $z2, $t2) = $q; $a = $y1->subtract($x1)->multiply($y2->subtract($x2)); $b = $y1->add($x1)->multiply($y2->add($x2)); $c = $t1->multiply($this->two)->multiply($this->d)->multiply($t2); $d = $z1->multiply($this->two)->multiply($z2); $e = $b->subtract($a); $f = $d->subtract($c); $g = $d->add($c); $h = $b->add($a); $x3 = $e->multiply($f); $y3 = $g->multiply($h); $t3 = $e->multiply($h); $z3 = $f->multiply($g); return [$x3, $y3, $z3, $t3]; } } PK!*>vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Ed448.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\Random; use phpseclib3\Math\BigInteger; class Ed448 extends TwistedEdwards { const HASH = 'shake256-912'; const SIZE = 57; public function __construct() { // 2^448 - 2^224 - 1 $this->setModulo(new BigInteger( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE' . 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16 )); $this->setCoefficients( new BigInteger(1), // -39081 new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE' . 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6756', 16) ); $this->setBasePoint( new BigInteger('4F1970C66BED0DED221D15A622BF36DA9E146570470F1767EA6DE324' . 'A3D3A46412AE1AF72AB66511433B80E18B00938E2626A82BC70CC05E', 16), new BigInteger('693F46716EB6BC248876203756C9C7624BEA73736CA3984087789C1E' . '05A0C2D73AD3FF1CE67C39C4FDBD132C4ED7C8AD9808795BF230FA14', 16) ); $this->setOrder(new BigInteger( '3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . '7CCA23E9C44EDB49AED63690216CC2728DC58F552378C292AB5844F3', 16 )); } /** * Recover X from Y * * Implements steps 2-4 at https://tools.ietf.org/html/rfc8032#section-5.2.3 * * Used by EC\Keys\Common.php * * @param BigInteger $y * @param boolean $sign * @return object[] */ public function recoverX(BigInteger $y, $sign) { $y = $this->factory->newInteger($y); $y2 = $y->multiply($y); $u = $y2->subtract($this->one); $v = $this->d->multiply($y2)->subtract($this->one); $x2 = $u->divide($v); if ($x2->equals($this->zero)) { if ($sign) { throw new \RuntimeException('Unable to recover X coordinate (x2 = 0)'); } return clone $this->zero; } // find the square root $exp = $this->getModulo()->add(new BigInteger(1)); $exp = $exp->bitwise_rightShift(2); $x = $x2->pow($exp); if (!$x->multiply($x)->subtract($x2)->equals($this->zero)) { throw new \RuntimeException('Unable to recover X coordinate'); } if ($x->isOdd() != $sign) { $x = $x->negate(); } return [$x, $y]; } /** * Extract Secret Scalar * * Implements steps 1-3 at https://tools.ietf.org/html/rfc8032#section-5.2.5 * * Used by the various key handlers * * @param string $str * @return array */ public function extractSecret($str) { if (strlen($str) != 57) { throw new \LengthException('Private Key should be 57-bytes long'); } // 1. Hash the 57-byte private key using SHAKE256(x, 114), storing the // digest in a 114-octet large buffer, denoted h. Only the lower 57 // bytes are used for generating the public key. $hash = new Hash('shake256-912'); $h = $hash->hash($str); $h = substr($h, 0, 57); // 2. Prune the buffer: The two least significant bits of the first // octet are cleared, all eight bits the last octet are cleared, and // the highest bit of the second to last octet is set. $h[0] = $h[0] & chr(0xFC); $h = strrev($h); $h[0] = "\0"; $h[1] = $h[1] | chr(0x80); // 3. Interpret the buffer as the little-endian integer, forming a // secret scalar s. $dA = new BigInteger($h, 256); return [ 'dA' => $dA, 'secret' => $str ]; $dA->secret = $str; return $dA; } /** * Encode a point as a string * * @param array $point * @return string */ public function encodePoint($point) { list($x, $y) = $point; $y = "\0" . $y->toBytes(); if ($x->isOdd()) { $y[0] = $y[0] | chr(0x80); } $y = strrev($y); return $y; } /** * Creates a random scalar multiplier * * @return \phpseclib3\Math\PrimeField\Integer */ public function createRandomMultiplier() { return $this->extractSecret(Random::string(57))['dA']; } /** * Converts an affine point to an extended homogeneous coordinate * * From https://tools.ietf.org/html/rfc8032#section-5.2.4 : * * A point (x,y) is represented in extended homogeneous coordinates (X, Y, Z, T), * with x = X/Z, y = Y/Z, x * y = T/Z. * * @return \phpseclib3\Math\PrimeField\Integer[] */ public function convertToInternal(array $p) { if (empty($p)) { return [clone $this->zero, clone $this->one, clone $this->one]; } if (isset($p[2])) { return $p; } $p[2] = clone $this->one; return $p; } /** * Doubles a point on a curve * * @return FiniteField[] */ public function doublePoint(array $p) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p)) { return []; } if (!isset($p[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to "Jacobi" coordinates or vice versa'); } // from https://tools.ietf.org/html/rfc8032#page-18 list($x1, $y1, $z1) = $p; $b = $x1->add($y1); $b = $b->multiply($b); $c = $x1->multiply($x1); $d = $y1->multiply($y1); $e = $c->add($d); $h = $z1->multiply($z1); $j = $e->subtract($this->two->multiply($h)); $x3 = $b->subtract($e)->multiply($j); $y3 = $c->subtract($d)->multiply($e); $z3 = $e->multiply($j); return [$x3, $y3, $z3]; } /** * Adds two points on the curve * * @return FiniteField[] */ public function addPoint(array $p, array $q) { if (!isset($this->factory)) { throw new \RuntimeException('setModulo needs to be called before this method'); } if (!count($p) || !count($q)) { if (count($q)) { return $q; } if (count($p)) { return $p; } return []; } if (!isset($p[2]) || !isset($q[2])) { throw new \RuntimeException('Affine coordinates need to be manually converted to "Jacobi" coordinates or vice versa'); } if ($p[0]->equals($q[0])) { return !$p[1]->equals($q[1]) ? [] : $this->doublePoint($p); } // from https://tools.ietf.org/html/rfc8032#page-17 list($x1, $y1, $z1) = $p; list($x2, $y2, $z2) = $q; $a = $z1->multiply($z2); $b = $a->multiply($a); $c = $x1->multiply($x2); $d = $y1->multiply($y2); $e = $this->d->multiply($c)->multiply($d); $f = $b->subtract($e); $g = $b->add($e); $h = $x1->add($y1)->multiply($x2->add($y2)); $x3 = $a->multiply($f)->multiply($h->subtract($c)->subtract($d)); $y3 = $a->multiply($g)->multiply($d->subtract($c)); $z3 = $f->multiply($g); return [$x3, $y3, $z3]; } } PK!9ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistb233.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistb233 extends sect233r1 { } PK!( ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistb409.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistb409 extends sect409r1 { } PK!MIffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk163.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistk163 extends sect163k1 { } PK! ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk233.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistk233 extends sect233k1 { } PK!TyggAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk283.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistk283 extends sect283k1 { } PK!ٍ*ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk409.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistk409 extends sect409k1 { } PK!)ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp192.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistp192 extends secp192r1 { } PK!l)OffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp224.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistp224 extends secp224r1 { } PK!v[ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp256.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistp256 extends secp256r1 { } PK!JxffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp384.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistp384 extends secp384r1 { } PK!'olffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp521.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistp521 extends secp521r1 { } PK!ffAvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistt571.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class nistt571 extends sect571k1 { } PK!KjjCvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class prime192v1 extends secp192r1 { } PK!yT==Cvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class prime192v2 extends Prime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC', 16), new BigInteger('CC22D6DFB95C6B25E49C0D6364A4E5980C393AA21668D953', 16) ); $this->setBasePoint( new BigInteger('EEA2BAE7E1497842F2DE7769CFE9C989C072AD696F48034A', 16), new BigInteger('6574D11D69B6EC7A672BB82A083DF2F2B0847DE970B2DE15', 16) ); $this->setOrder(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFE5FB1A724DC80418648D8DD31', 16)); } } PK!K{==Cvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v3.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class prime192v3 extends Prime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC', 16), new BigInteger('22123DC2395A05CAA7423DAECCC94760A7D462256BD56916', 16) ); $this->setBasePoint( new BigInteger('7D29778100C65A1DA1783716588DCE2B8B4AEE8E228F1896', 16), new BigInteger('38A90F22637337334B49DCB66A6DC8F9978ACA7648A943B0', 16) ); $this->setOrder(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFF7A62D031C83F4294F640EC13', 16)); } } PK!X uCvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class prime239v1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFF8000000000007FFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFF8000000000007FFFFFFFFFFC', 16), new BigInteger('6B016C3BDCF18941D0D654921475CA71A9DB2FB27D1D37796185C2942C0A', 16) ); $this->setBasePoint( new BigInteger('0FFA963CDCA8816CCC33B8642BEDF905C3D358573D3F27FBBD3B3CB9AAAF', 16), new BigInteger('7DEBE8E4E90A5DAE6E4054CA530BA04654B36818CE226B39FCCB7B02F1AE', 16) ); $this->setOrder(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFF9E5E9A9F5D9071FBD1522688909D0B', 16)); } } PK!嬅Cvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class prime239v2 extends Prime { public function __construct() { $this->setModulo(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFF8000000000007FFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFF8000000000007FFFFFFFFFFC', 16), new BigInteger('617FAB6832576CBBFED50D99F0249C3FEE58B94BA0038C7AE84C8C832F2C', 16) ); $this->setBasePoint( new BigInteger('38AF09D98727705120C921BB5E9E26296A3CDCF2F35757A0EAFD87B830E7', 16), new BigInteger('5B0125E4DBEA0EC7206DA0FC01D9B081329FB555DE6EF460237DFF8BE4BA', 16) ); $this->setOrder(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF800000CFA7E8594377D414C03821BC582063', 16)); } } PK!VhCvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v3.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class prime239v3 extends Prime { public function __construct() { $this->setModulo(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFF8000000000007FFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFF8000000000007FFFFFFFFFFC', 16), new BigInteger('255705FA2A306654B1F4CB03D6A750A30C250102D4988717D9BA15AB6D3E', 16) ); $this->setBasePoint( new BigInteger('6768AE8E18BB92CFCF005C949AA2C6D94853D0E660BBF854B1C9505FE95A', 16), new BigInteger('1607E6898F390C06BC1D552BAD226F3B6FCFE48B6E818499AF18E3ED6CF3', 16) ); $this->setOrder(new BigInteger('7FFFFFFFFFFFFFFFFFFFFFFF7FFFFF975DEB41B3A6057C3C432146526551', 16)); } } PK!gjjCvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime256v1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; final class prime256v1 extends secp256r1 { } PK!XBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp112r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp112r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('DB7C2ABF62E35E668076BEAD208B', 16)); $this->setCoefficients( new BigInteger('DB7C2ABF62E35E668076BEAD2088', 16), new BigInteger('659EF8BA043916EEDE8911702B22', 16) ); $this->setBasePoint( new BigInteger('09487239995A5EE76B55F9C2F098', 16), new BigInteger('A89CE5AF8724C0A23E0E0FF77500', 16) ); $this->setOrder(new BigInteger('DB7C2ABF62E35E7628DFAC6561C5', 16)); } } PK!i.Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp112r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp112r2 extends Prime { public function __construct() { // same modulo as secp112r1 $this->setModulo(new BigInteger('DB7C2ABF62E35E668076BEAD208B', 16)); $this->setCoefficients( new BigInteger('6127C24C05F38A0AAAF65C0EF02C', 16), new BigInteger('51DEF1815DB5ED74FCC34C85D709', 16) ); $this->setBasePoint( new BigInteger('4BA30AB5E892B4E1649DD0928643', 16), new BigInteger('ADCD46F5882E3747DEF36E956E97', 16) ); $this->setOrder(new BigInteger('36DF0AAFD8B8D7597CA10520D04B', 16)); } } PK!6DBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp128r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp128r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC', 16), new BigInteger('E87579C11079F43DD824993C2CEE5ED3', 16) ); $this->setBasePoint( new BigInteger('161FF7528B899B2D0C28607CA52C5B86', 16), new BigInteger('CF5AC8395BAFEB13C02DA292DDED7A83', 16) ); $this->setOrder(new BigInteger('FFFFFFFE0000000075A30D1B9038A115', 16)); } } PK!U\Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp128r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp128r2 extends Prime { public function __construct() { // same as secp128r1 $this->setModulo(new BigInteger('FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('D6031998D1B3BBFEBF59CC9BBFF9AEE1', 16), new BigInteger('5EEEFCA380D02919DC2C6558BB6D8A5D', 16) ); $this->setBasePoint( new BigInteger('7B6AA5D85E572983E6FB32A7CDEBC140', 16), new BigInteger('27B6916A894D3AEE7106FE805FC34B44', 16) ); $this->setOrder(new BigInteger('3FFFFFFF7FFFFFFFBE0024720613B5A3', 16)); } } PK!qBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\KoblitzPrime; use phpseclib3\Math\BigInteger; class secp160k1 extends KoblitzPrime { public function __construct() { // same as secp160r2 $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73', 16)); $this->setCoefficients( new BigInteger('0000000000000000000000000000000000000000', 16), new BigInteger('0000000000000000000000000000000000000007', 16) ); $this->setBasePoint( new BigInteger('3B4C382CE37AA192A4019E763036F4F5DD4D7EBB', 16), new BigInteger('938CF935318FDCED6BC28286531733C3F03C4FEE', 16) ); $this->setOrder(new BigInteger('0100000000000000000001B8FA16DFAB9ACA16B6B3', 16)); $this->basis = []; $this->basis[] = [ 'a' => new BigInteger('0096341F1138933BC2F505', -16), 'b' => new BigInteger('FF6E9D0418C67BB8D5F562', -16) ]; $this->basis[] = [ 'a' => new BigInteger('01BDCB3A09AAAABEAFF4A8', -16), 'b' => new BigInteger('04D12329FF0EF498EA67', -16) ]; $this->beta = $this->factory->newInteger(new BigInteger('645B7345A143464942CC46D7CF4D5D1E1E6CBB68', -16)); } } PK!*  Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp160r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF', 16)); $this->setCoefficients( new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC', 16), new BigInteger('1C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45', 16) ); $this->setBasePoint( new BigInteger('4A96B5688EF573284664698968C38BB913CBFC82', 16), new BigInteger('23A628553168947D59DCC912042351377AC5FB32', 16) ); $this->setOrder(new BigInteger('0100000000000000000001F4C8F927AED3CA752257', 16)); } } PK!c**Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp160r2 extends Prime { public function __construct() { // same as secp160k1 $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73', 16)); $this->setCoefficients( new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70', 16), new BigInteger('B4E134D3FB59EB8BAB57274904664D5AF50388BA', 16) ); $this->setBasePoint( new BigInteger('52DCB034293A117E1F4FF11B30F7199D3144CE6D', 16), new BigInteger('FEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E', 16) ); $this->setOrder(new BigInteger('0100000000000000000000351EE786A818F3A1A16B', 16)); } } PK!e>>Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp192k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\KoblitzPrime; use phpseclib3\Math\BigInteger; class secp192k1 extends KoblitzPrime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37', 16)); $this->setCoefficients( new BigInteger('000000000000000000000000000000000000000000000000', 16), new BigInteger('000000000000000000000000000000000000000000000003', 16) ); $this->setBasePoint( new BigInteger('DB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D', 16), new BigInteger('9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D', 16) ); $this->setOrder(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D', 16)); $this->basis = []; $this->basis[] = [ 'a' => new BigInteger('00B3FB3400DEC5C4ADCEB8655C', -16), 'b' => new BigInteger('8EE96418CCF4CFC7124FDA0F', -16) ]; $this->basis[] = [ 'a' => new BigInteger('01D90D03E8F096B9948B20F0A9', -16), 'b' => new BigInteger('42E49819ABBA9474E1083F6B', -16) ]; $this->beta = $this->factory->newInteger(new BigInteger('447A96E6C647963E2F7809FEAAB46947F34B0AA3CA0BBA74', -16)); } } PK!32w Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp192r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp192r1 extends Prime { public function __construct() { $modulo = new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF', 16); $this->setModulo($modulo); // algorithm 2.27 from http://diamond.boisestate.edu/~liljanab/MATH308/GuideToECC.pdf#page=66 /* in theory this should be faster than regular modular reductions save for one small issue. to convert to / from base-2**8 with BCMath you have to call bcmul() and bcdiv() a lot. to convert to / from base-2**8 with PHP64 you have to call base256_rshift() a lot. in short, converting to / from base-2**8 is pretty expensive and that expense is enough to offset whatever else might be gained by a simplified reduction algorithm. now, if PHP supported unsigned integers things might be different. no bit-shifting would be required for the PHP engine and it'd be a lot faster. but as is, BigInteger uses base-2**31 or base-2**26 depending on whether or not the system is has a 32-bit or a 64-bit OS. */ /* $m_length = $this->getLengthInBytes(); $this->setReduction(function($c) use ($m_length) { $cBytes = $c->toBytes(); $className = $this->className; if (strlen($cBytes) > 2 * $m_length) { list(, $r) = $c->divide($className::$modulo); return $r; } $c = str_pad($cBytes, 48, "\0", STR_PAD_LEFT); $c = array_reverse(str_split($c, 8)); $null = "\0\0\0\0\0\0\0\0"; $s1 = new BigInteger($c[2] . $c[1] . $c[0], 256); $s2 = new BigInteger($null . $c[3] . $c[3], 256); $s3 = new BigInteger($c[4] . $c[4] . $null, 256); $s4 = new BigInteger($c[5] . $c[5] . $c[5], 256); $r = $s1->add($s2)->add($s3)->add($s4); while ($r->compare($className::$modulo) >= 0) { $r = $r->subtract($className::$modulo); } return $r; }); */ $this->setCoefficients( new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC', 16), new BigInteger('64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1', 16) ); $this->setBasePoint( new BigInteger('188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012', 16), new BigInteger('07192B95FFC8DA78631011ED6B24CDD573F977A11E794811', 16) ); $this->setOrder(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831', 16)); } } PK!rBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp224k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\KoblitzPrime; use phpseclib3\Math\BigInteger; class secp224k1 extends KoblitzPrime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D', 16)); $this->setCoefficients( new BigInteger('00000000000000000000000000000000000000000000000000000000', 16), new BigInteger('00000000000000000000000000000000000000000000000000000005', 16) ); $this->setBasePoint( new BigInteger('A1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C', 16), new BigInteger('7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5', 16) ); $this->setOrder(new BigInteger('010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7', 16)); $this->basis = []; $this->basis[] = [ 'a' => new BigInteger('00B8ADF1378A6EB73409FA6C9C637D', -16), 'b' => new BigInteger('94730F82B358A3776A826298FA6F', -16) ]; $this->basis[] = [ 'a' => new BigInteger('01DCE8D2EC6184CAF0A972769FCC8B', -16), 'b' => new BigInteger('4D2100BA3DC75AAB747CCF355DEC', -16) ]; $this->beta = $this->factory->newInteger(new BigInteger('01F178FFA4B17C89E6F73AECE2AAD57AF4C0A748B63C830947B27E04', -16)); } } PK!N*kkBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp224r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp224r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001', 16)); $this->setCoefficients( new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE', 16), new BigInteger('B4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4', 16) ); $this->setBasePoint( new BigInteger('B70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21', 16), new BigInteger('BD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34', 16) ); $this->setOrder(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D', 16)); } } PK!L-AABvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp256k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; //use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Crypt\EC\BaseCurves\KoblitzPrime; use phpseclib3\Math\BigInteger; //class secp256k1 extends Prime class secp256k1 extends KoblitzPrime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16)); $this->setCoefficients( new BigInteger('0000000000000000000000000000000000000000000000000000000000000000', 16), new BigInteger('0000000000000000000000000000000000000000000000000000000000000007', 16) ); $this->setOrder(new BigInteger('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16)); $this->setBasePoint( new BigInteger('79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798', 16), new BigInteger('483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8', 16) ); $this->basis = []; $this->basis[] = [ 'a' => new BigInteger('3086D221A7D46BCDE86C90E49284EB15', -16), 'b' => new BigInteger('FF1BBC8129FEF177D790AB8056F5401B3D', -16) ]; $this->basis[] = [ 'a' => new BigInteger('114CA50F7A8E2F3F657C1108D9D44CFD8', -16), 'b' => new BigInteger('3086D221A7D46BCDE86C90E49284EB15', -16) ]; $this->beta = $this->factory->newInteger(new BigInteger('7AE96A2B657C07106E64479EAC3434E99CF0497512F58995C1396C28719501EE', -16)); } } PK!(ڛBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp256r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp256r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF', 16)); $this->setCoefficients( new BigInteger('FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC', 16), new BigInteger('5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B', 16) ); $this->setBasePoint( new BigInteger('6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296', 16), new BigInteger('4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5', 16) ); $this->setOrder(new BigInteger('FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', 16)); } } PK!WWBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp384r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp384r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF', 16 )); $this->setCoefficients( new BigInteger( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC', 16 ), new BigInteger( 'B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF', 16 ) ); $this->setBasePoint( new BigInteger( 'AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7', 16 ), new BigInteger( '3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F', 16 ) ); $this->setOrder(new BigInteger( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973', 16 )); } } PK!& GBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp521r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Prime; use phpseclib3\Math\BigInteger; class secp521r1 extends Prime { public function __construct() { $this->setModulo(new BigInteger('01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . 'FFFF', 16)); $this->setCoefficients( new BigInteger('01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . 'FFFC', 16), new BigInteger('0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF1' . '09E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B50' . '3F00', 16) ); $this->setBasePoint( new BigInteger('00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D' . '3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5' . 'BD66', 16), new BigInteger('011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E' . '662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD1' . '6650', 16) ); $this->setOrder(new BigInteger('01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . 'FFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E9138' . '6409', 16)); } } PK!GVVBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect113r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect113r1 extends Binary { public function __construct() { $this->setModulo(113, 9, 0); $this->setCoefficients( '003088250CA6E7C7FE649CE85820F7', '00E8BEE4D3E2260744188BE0E9C723' ); $this->setBasePoint( '009D73616F35F4AB1407D73562C10F', '00A52830277958EE84D1315ED31886' ); $this->setOrder(new BigInteger('0100000000000000D9CCEC8A39E56F', 16)); } } PK!T"VVBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect113r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect113r2 extends Binary { public function __construct() { $this->setModulo(113, 9, 0); $this->setCoefficients( '00689918DBEC7E5A0DD6DFC0AA55C7', '0095E9A9EC9B297BD4BF36E059184F' ); $this->setBasePoint( '01A57A6A7B26CA5EF52FCDB8164797', '00B3ADC94ED1FE674C06E695BABA1D' ); $this->setOrder(new BigInteger('010000000000000108789B2496AF93', 16)); } } PK!5MppBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect131r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect131r1 extends Binary { public function __construct() { $this->setModulo(131, 8, 3, 2, 0); $this->setCoefficients( '07A11B09A76B562144418FF3FF8C2570B8', '0217C05610884B63B9C6C7291678F9D341' ); $this->setBasePoint( '0081BAF91FDF9833C40F9C181343638399', '078C6E7EA38C001F73C8134B1B4EF9E150' ); $this->setOrder(new BigInteger('0400000000000000023123953A9464B54D', 16)); } } PK! ppBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect131r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect131r2 extends Binary { public function __construct() { $this->setModulo(131, 8, 3, 2, 0); $this->setCoefficients( '03E5A88919D7CAFCBF415F07C2176573B2', '04B8266A46C55657AC734CE38F018F2192' ); $this->setBasePoint( '0356DCD8F2F95031AD652D23951BB366A8', '0648F06D867940A5366D9E265DE9EB240F' ); $this->setOrder(new BigInteger('0400000000000000016954A233049BA98F', 16)); } } PK!1E Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect163k1 extends Binary { public function __construct() { $this->setModulo(163, 7, 6, 3, 0); $this->setCoefficients( '000000000000000000000000000000000000000001', '000000000000000000000000000000000000000001' ); $this->setBasePoint( '02FE13C0537BBC11ACAA07D793DE4E6D5E5C94EEE8', '0289070FB05D38FF58321F2E800536D538CCDAA3D9' ); $this->setOrder(new BigInteger('04000000000000000000020108A2E0CC0D99F8A5EF', 16)); } } PK!^KBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect163r1 extends Binary { public function __construct() { $this->setModulo(163, 7, 6, 3, 0); $this->setCoefficients( '07B6882CAAEFA84F9554FF8428BD88E246D2782AE2', '0713612DCDDCB40AAB946BDA29CA91F73AF958AFD9' ); $this->setBasePoint( '0369979697AB43897789566789567F787A7876A654', '00435EDB42EFAFB2989D51FEFCE3C80988F41FF883' ); $this->setOrder(new BigInteger('03FFFFFFFFFFFFFFFFFFFF48AAB689C29CA710279B', 16)); } } PK! lBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect163r2 extends Binary { public function __construct() { $this->setModulo(163, 7, 6, 3, 0); $this->setCoefficients( '000000000000000000000000000000000000000001', '020A601907B8C953CA1481EB10512F78744A3205FD' ); $this->setBasePoint( '03F0EBA16286A2D57EA0991168D4994637E8343E36', '00D51FBC6C71A0094FA2CDD545B11C5C0C797324F1' ); $this->setOrder(new BigInteger('040000000000000000000292FE77E70C12A4234C33', 16)); } } PK!oۆBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect193r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect193r1 extends Binary { public function __construct() { $this->setModulo(193, 15, 0); $this->setCoefficients( '0017858FEB7A98975169E171F77B4087DE098AC8A911DF7B01', '00FDFB49BFE6C3A89FACADAA7A1E5BBC7CC1C2E5D831478814' ); $this->setBasePoint( '01F481BC5F0FF84A74AD6CDF6FDEF4BF6179625372D8C0C5E1', '0025E399F2903712CCF3EA9E3A1AD17FB0B3201B6AF7CE1B05' ); $this->setOrder(new BigInteger('01000000000000000000000000C7F34A778F443ACC920EBA49', 16)); } } PK!gBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect193r2.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect193r2 extends Binary { public function __construct() { $this->setModulo(193, 15, 0); $this->setCoefficients( '0163F35A5137C2CE3EA6ED8667190B0BC43ECD69977702709B', '00C9BB9E8927D4D64C377E2AB2856A5B16E3EFB7F61D4316AE' ); $this->setBasePoint( '00D9B67D192E0367C803F39E1A7E82CA14A651350AAE617E8F', '01CE94335607C304AC29E7DEFBD9CA01F596F927224CDECF6C' ); $this->setOrder(new BigInteger('010000000000000000000000015AAB561B005413CCD4EE99D5', 16)); } } PK!"Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect233k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect233k1 extends Binary { public function __construct() { $this->setModulo(233, 74, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000000', '000000000000000000000000000000000000000000000000000000000001' ); $this->setBasePoint( '017232BA853A7E731AF129F22FF4149563A419C26BF50A4C9D6EEFAD6126', '01DB537DECE819B7F70F555A67C427A8CD9BF18AEB9B56E0C11056FAE6A3' ); $this->setOrder(new BigInteger('8000000000000000000000000000069D5BB915BCD46EFB1AD5F173ABDF', 16)); } } PK!شXBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect233r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect233r1 extends Binary { public function __construct() { $this->setModulo(233, 74, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000001', '0066647EDE6C332C7F8C0923BB58213B333B20E9CE4281FE115F7D8F90AD' ); $this->setBasePoint( '00FAC9DFCBAC8313BB2139F1BB755FEF65BC391F8B36F8F8EB7371FD558B', '01006A08A41903350678E58528BEBF8A0BEFF867A7CA36716F7E01F81052' ); $this->setOrder(new BigInteger('01000000000000000000000000000013E974E72F8A6922031D2603CFE0D7', 16)); } } PK!E3Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect239k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect239k1 extends Binary { public function __construct() { $this->setModulo(239, 158, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000000', '000000000000000000000000000000000000000000000000000000000001' ); $this->setBasePoint( '29A0B6A887A983E9730988A68727A8B2D126C44CC2CC7B2A6555193035DC', '76310804F12E549BDB011C103089E73510ACB275FC312A5DC6B76553F0CA' ); $this->setOrder(new BigInteger('2000000000000000000000000000005A79FEC67CB6E91F1C1DA800E478A5', 16)); } } PK!~11Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect283k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect283k1 extends Binary { public function __construct() { $this->setModulo(283, 12, 7, 5, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000000000000000000', '000000000000000000000000000000000000000000000000000000000000000000000001' ); $this->setBasePoint( '0503213F78CA44883F1A3B8162F188E553CD265F23C1567A16876913B0C2AC2458492836', '01CCDA380F1C9E318D90F95D07E5426FE87E45C0E8184698E45962364E34116177DD2259' ); $this->setOrder(new BigInteger('01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9AE2ED07577265DFF7F94451E061E163C61', 16)); } } PK!E11Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect283r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect283r1 extends Binary { public function __construct() { $this->setModulo(283, 12, 7, 5, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000000000000000001', '027B680AC8B8596DA5A4AF8A19A0303FCA97FD7645309FA2A581485AF6263E313B79A2F5' ); $this->setBasePoint( '05F939258DB7DD90E1934F8C70B0DFEC2EED25B8557EAC9C80E2E198F8CDBECD86B12053', '03676854FE24141CB98FE6D4B20D02B4516FF702350EDDB0826779C813F0DF45BE8112F4' ); $this->setOrder(new BigInteger('03FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF90399660FC938A90165B042A7CEFADB307', 16)); } } PK!ԋBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect409k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect409k1 extends Binary { public function __construct() { $this->setModulo(409, 87, 0); $this->setCoefficients( '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001' ); $this->setBasePoint( '0060F05F658F49C1AD3AB1890F7184210EFD0987E307C84C27ACCFB8F9F67CC2C460189EB5AAAA62EE222EB1B35540CFE9023746', '01E369050B7C4E42ACBA1DACBF04299C3460782F918EA427E6325165E9EA10E3DA5F6C42E9C55215AA9CA27A5863EC48D8E0286B' ); $this->setOrder(new BigInteger( '7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5F' . '83B2D4EA20400EC4557D5ED3E3E7CA5B4B5C83B8E01E5FCF', 16 )); } } PK!ؐBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect409r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect409r1 extends Binary { public function __construct() { $this->setModulo(409, 87, 0); $this->setCoefficients( '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', '0021A5C2C8EE9FEB5C4B9A753B7B476B7FD6422EF1F3DD674761FA99D6AC27C8A9A197B272822F6CD57A55AA4F50AE317B13545F' ); $this->setBasePoint( '015D4860D088DDB3496B0C6064756260441CDE4AF1771D4DB01FFE5B34E59703DC255A868A1180515603AEAB60794E54BB7996A7', '0061B1CFAB6BE5F32BBFA78324ED106A7636B9C5A7BD198D0158AA4F5488D08F38514F1FDF4B4F40D2181B3681C364BA0273C706' ); $this->setOrder(new BigInteger( '010000000000000000000000000000000000000000000000000001E2' . 'AAD6A612F33307BE5FA47C3C9E052F838164CD37D9A21173', 16 )); } } PK!I&Bvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect571k1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect571k1 extends Binary { public function __construct() { $this->setModulo(571, 10, 5, 2, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000000000000000000' . '000000000000000000000000000000000000000000000000000000000000000000000000', '000000000000000000000000000000000000000000000000000000000000000000000000' . '000000000000000000000000000000000000000000000000000000000000000000000001' ); $this->setBasePoint( '026EB7A859923FBC82189631F8103FE4AC9CA2970012D5D46024804801841CA443709584' . '93B205E647DA304DB4CEB08CBBD1BA39494776FB988B47174DCA88C7E2945283A01C8972', '0349DC807F4FBF374F4AEADE3BCA95314DD58CEC9F307A54FFC61EFC006D8A2C9D4979C0' . 'AC44AEA74FBEBBB9F772AEDCB620B01A7BA7AF1B320430C8591984F601CD4C143EF1C7A3' ); $this->setOrder(new BigInteger( '020000000000000000000000000000000000000000000000000000000000000000000000' . '131850E1F19A63E4B391A8DB917F4138B630D84BE5D639381E91DEB45CFE778F637C1001', 16 )); } } PK!%^XBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect571r1.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Crypt\EC\Curves; use phpseclib3\Crypt\EC\BaseCurves\Binary; use phpseclib3\Math\BigInteger; class sect571r1 extends Binary { public function __construct() { $this->setModulo(571, 10, 5, 2, 0); $this->setCoefficients( '000000000000000000000000000000000000000000000000000000000000000000000000' . '000000000000000000000000000000000000000000000000000000000000000000000001', '02F40E7E2221F295DE297117B7F3D62F5C6A97FFCB8CEFF1CD6BA8CE4A9A18AD84FFABBD' . '8EFA59332BE7AD6756A66E294AFD185A78FF12AA520E4DE739BACA0C7FFEFF7F2955727A' ); $this->setBasePoint( '0303001D34B856296C16C0D40D3CD7750A93D1D2955FA80AA5F40FC8DB7B2ABDBDE53950' . 'F4C0D293CDD711A35B67FB1499AE60038614F1394ABFA3B4C850D927E1E7769C8EEC2D19', '037BF27342DA639B6DCCFFFEB73D69D78C6C27A6009CBBCA1980F8533921E8A684423E43' . 'BAB08A576291AF8F461BB2A8B3531D2F0485C19B16E2F1516E23DD3C1A4827AF1B8AC15B' ); $this->setOrder(new BigInteger( '03FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' . 'E661CE18FF55987308059B186823851EC7DD9CA1161DE93D5174D66E8382E9BB2FE84E47', 16 )); } } PK!F\X[X[Evendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/Common.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\BaseCurves\Binary as BinaryCurve; use phpseclib3\Crypt\EC\BaseCurves\Prime as PrimeCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * Generic EC Key Parsing Helper functions * * @author Jim Wigginton */ trait Common { /** * Curve OIDs * * @var array */ private static $curveOIDs = []; /** * Child OIDs loaded * * @var bool */ protected static $childOIDsLoaded = false; /** * Use Named Curves * * @var bool */ private static $useNamedCurves = true; /** * Initialize static variables */ private static function initialize_static_variables() { if (empty(self::$curveOIDs)) { // the sec* curves are from the standards for efficient cryptography group // sect* curves are curves over binary finite fields // secp* curves are curves over prime finite fields // sec*r* curves are regular curves; sec*k* curves are koblitz curves // brainpool*r* curves are regular prime finite field curves // brainpool*t* curves are twisted versions of the brainpool*r* curves self::$curveOIDs = [ 'prime192v1' => '1.2.840.10045.3.1.1', // J.5.1, example 1 (aka secp192r1) 'prime192v2' => '1.2.840.10045.3.1.2', // J.5.1, example 2 'prime192v3' => '1.2.840.10045.3.1.3', // J.5.1, example 3 'prime239v1' => '1.2.840.10045.3.1.4', // J.5.2, example 1 'prime239v2' => '1.2.840.10045.3.1.5', // J.5.2, example 2 'prime239v3' => '1.2.840.10045.3.1.6', // J.5.2, example 3 'prime256v1' => '1.2.840.10045.3.1.7', // J.5.3, example 1 (aka secp256r1) // https://tools.ietf.org/html/rfc5656#section-10 'nistp256' => '1.2.840.10045.3.1.7', // aka secp256r1 'nistp384' => '1.3.132.0.34', // aka secp384r1 'nistp521' => '1.3.132.0.35', // aka secp521r1 'nistk163' => '1.3.132.0.1', // aka sect163k1 'nistp192' => '1.2.840.10045.3.1.1', // aka secp192r1 'nistp224' => '1.3.132.0.33', // aka secp224r1 'nistk233' => '1.3.132.0.26', // aka sect233k1 'nistb233' => '1.3.132.0.27', // aka sect233r1 'nistk283' => '1.3.132.0.16', // aka sect283k1 'nistk409' => '1.3.132.0.36', // aka sect409k1 'nistb409' => '1.3.132.0.37', // aka sect409r1 'nistt571' => '1.3.132.0.38', // aka sect571k1 // from https://tools.ietf.org/html/rfc5915 'secp192r1' => '1.2.840.10045.3.1.1', // aka prime192v1 'sect163k1' => '1.3.132.0.1', 'sect163r2' => '1.3.132.0.15', 'secp224r1' => '1.3.132.0.33', 'sect233k1' => '1.3.132.0.26', 'sect233r1' => '1.3.132.0.27', 'secp256r1' => '1.2.840.10045.3.1.7', // aka prime256v1 'sect283k1' => '1.3.132.0.16', 'sect283r1' => '1.3.132.0.17', 'secp384r1' => '1.3.132.0.34', 'sect409k1' => '1.3.132.0.36', 'sect409r1' => '1.3.132.0.37', 'secp521r1' => '1.3.132.0.35', 'sect571k1' => '1.3.132.0.38', 'sect571r1' => '1.3.132.0.39', // from http://www.secg.org/SEC2-Ver-1.0.pdf 'secp112r1' => '1.3.132.0.6', 'secp112r2' => '1.3.132.0.7', 'secp128r1' => '1.3.132.0.28', 'secp128r2' => '1.3.132.0.29', 'secp160k1' => '1.3.132.0.9', 'secp160r1' => '1.3.132.0.8', 'secp160r2' => '1.3.132.0.30', 'secp192k1' => '1.3.132.0.31', 'secp224k1' => '1.3.132.0.32', 'secp256k1' => '1.3.132.0.10', 'sect113r1' => '1.3.132.0.4', 'sect113r2' => '1.3.132.0.5', 'sect131r1' => '1.3.132.0.22', 'sect131r2' => '1.3.132.0.23', 'sect163r1' => '1.3.132.0.2', 'sect193r1' => '1.3.132.0.24', 'sect193r2' => '1.3.132.0.25', 'sect239k1' => '1.3.132.0.3', // from http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.202.2977&rep=rep1&type=pdf#page=36 /* 'c2pnb163v1' => '1.2.840.10045.3.0.1', // J.4.1, example 1 'c2pnb163v2' => '1.2.840.10045.3.0.2', // J.4.1, example 2 'c2pnb163v3' => '1.2.840.10045.3.0.3', // J.4.1, example 3 'c2pnb172w1' => '1.2.840.10045.3.0.4', // J.4.2, example 1 'c2tnb191v1' => '1.2.840.10045.3.0.5', // J.4.3, example 1 'c2tnb191v2' => '1.2.840.10045.3.0.6', // J.4.3, example 2 'c2tnb191v3' => '1.2.840.10045.3.0.7', // J.4.3, example 3 'c2onb191v4' => '1.2.840.10045.3.0.8', // J.4.3, example 4 'c2onb191v5' => '1.2.840.10045.3.0.9', // J.4.3, example 5 'c2pnb208w1' => '1.2.840.10045.3.0.10', // J.4.4, example 1 'c2tnb239v1' => '1.2.840.10045.3.0.11', // J.4.5, example 1 'c2tnb239v2' => '1.2.840.10045.3.0.12', // J.4.5, example 2 'c2tnb239v3' => '1.2.840.10045.3.0.13', // J.4.5, example 3 'c2onb239v4' => '1.2.840.10045.3.0.14', // J.4.5, example 4 'c2onb239v5' => '1.2.840.10045.3.0.15', // J.4.5, example 5 'c2pnb272w1' => '1.2.840.10045.3.0.16', // J.4.6, example 1 'c2pnb304w1' => '1.2.840.10045.3.0.17', // J.4.7, example 1 'c2tnb359v1' => '1.2.840.10045.3.0.18', // J.4.8, example 1 'c2pnb368w1' => '1.2.840.10045.3.0.19', // J.4.9, example 1 'c2tnb431r1' => '1.2.840.10045.3.0.20', // J.4.10, example 1 */ // http://www.ecc-brainpool.org/download/Domain-parameters.pdf // https://tools.ietf.org/html/rfc5639 'brainpoolP160r1' => '1.3.36.3.3.2.8.1.1.1', 'brainpoolP160t1' => '1.3.36.3.3.2.8.1.1.2', 'brainpoolP192r1' => '1.3.36.3.3.2.8.1.1.3', 'brainpoolP192t1' => '1.3.36.3.3.2.8.1.1.4', 'brainpoolP224r1' => '1.3.36.3.3.2.8.1.1.5', 'brainpoolP224t1' => '1.3.36.3.3.2.8.1.1.6', 'brainpoolP256r1' => '1.3.36.3.3.2.8.1.1.7', 'brainpoolP256t1' => '1.3.36.3.3.2.8.1.1.8', 'brainpoolP320r1' => '1.3.36.3.3.2.8.1.1.9', 'brainpoolP320t1' => '1.3.36.3.3.2.8.1.1.10', 'brainpoolP384r1' => '1.3.36.3.3.2.8.1.1.11', 'brainpoolP384t1' => '1.3.36.3.3.2.8.1.1.12', 'brainpoolP512r1' => '1.3.36.3.3.2.8.1.1.13', 'brainpoolP512t1' => '1.3.36.3.3.2.8.1.1.14' ]; ASN1::loadOIDs([ 'prime-field' => '1.2.840.10045.1.1', 'characteristic-two-field' => '1.2.840.10045.1.2', 'characteristic-two-basis' => '1.2.840.10045.1.2.3', // per http://www.secg.org/SEC1-Ver-1.0.pdf#page=84, gnBasis "not used here" 'gnBasis' => '1.2.840.10045.1.2.3.1', // NULL 'tpBasis' => '1.2.840.10045.1.2.3.2', // Trinomial 'ppBasis' => '1.2.840.10045.1.2.3.3' // Pentanomial ] + self::$curveOIDs); } } /** * Explicitly set the curve * * If the key contains an implicit curve phpseclib needs the curve * to be explicitly provided * * @param BaseCurve $curve */ public static function setImplicitCurve(BaseCurve $curve) { self::$implicitCurve = $curve; } /** * Returns an instance of \phpseclib3\Crypt\EC\BaseCurves\Base based * on the curve parameters * * @param array $params * @return BaseCurve|false */ protected static function loadCurveByParam(array $params) { if (count($params) > 1) { throw new \RuntimeException('No parameters are present'); } if (isset($params['namedCurve'])) { $curve = '\phpseclib3\Crypt\EC\Curves\\' . $params['namedCurve']; if (!class_exists($curve)) { throw new UnsupportedCurveException('Named Curve of ' . $params['namedCurve'] . ' is not supported'); } return new $curve(); } if (isset($params['implicitCurve'])) { if (!isset(self::$implicitCurve)) { throw new \RuntimeException('Implicit curves can be provided by calling setImplicitCurve'); } return self::$implicitCurve; } if (isset($params['specifiedCurve'])) { $data = $params['specifiedCurve']; switch ($data['fieldID']['fieldType']) { case 'prime-field': $curve = new PrimeCurve(); $curve->setModulo($data['fieldID']['parameters']); $curve->setCoefficients( new BigInteger($data['curve']['a'], 256), new BigInteger($data['curve']['b'], 256) ); $point = self::extractPoint("\0" . $data['base'], $curve); $curve->setBasePoint(...$point); $curve->setOrder($data['order']); return $curve; case 'characteristic-two-field': $curve = new BinaryCurve(); $params = ASN1::decodeBER($data['fieldID']['parameters']); $params = ASN1::asn1map($params[0], Maps\Characteristic_two::MAP); $modulo = [(int) $params['m']->toString()]; switch ($params['basis']) { case 'tpBasis': $modulo[] = (int) $params['parameters']->toString(); break; case 'ppBasis': $temp = ASN1::decodeBER($params['parameters']); $temp = ASN1::asn1map($temp[0], Maps\Pentanomial::MAP); $modulo[] = (int) $temp['k3']->toString(); $modulo[] = (int) $temp['k2']->toString(); $modulo[] = (int) $temp['k1']->toString(); } $modulo[] = 0; $curve->setModulo(...$modulo); $len = ceil($modulo[0] / 8); $curve->setCoefficients( Strings::bin2hex($data['curve']['a']), Strings::bin2hex($data['curve']['b']) ); $point = self::extractPoint("\0" . $data['base'], $curve); $curve->setBasePoint(...$point); $curve->setOrder($data['order']); return $curve; default: throw new UnsupportedCurveException('Field Type of ' . $data['fieldID']['fieldType'] . ' is not supported'); } } throw new \RuntimeException('No valid parameters are present'); } /** * Extract points from a string * * Supports both compressed and uncompressed points * * @param string $str * @param BaseCurve $curve * @return object[] */ public static function extractPoint($str, BaseCurve $curve) { if ($curve instanceof TwistedEdwardsCurve) { // first step of point deciding as discussed at the following URL's: // https://tools.ietf.org/html/rfc8032#section-5.1.3 // https://tools.ietf.org/html/rfc8032#section-5.2.3 $y = $str; $y = strrev($y); $sign = (bool) (ord($y[0]) & 0x80); $y[0] = $y[0] & chr(0x7F); $y = new BigInteger($y, 256); if ($y->compare($curve->getModulo()) >= 0) { throw new \RuntimeException('The Y coordinate should not be >= the modulo'); } $point = $curve->recoverX($y, $sign); if (!$curve->verifyPoint($point)) { throw new \RuntimeException('Unable to verify that point exists on curve'); } return $point; } // the first byte of a bit string represents the number of bits in the last byte that are to be ignored but, // currently, bit strings wanting a non-zero amount of bits trimmed are not supported if (($val = Strings::shift($str)) != "\0") { throw new \UnexpectedValueException('extractPoint expects the first byte to be null - not ' . Strings::bin2hex($val)); } if ($str == "\0") { return []; } $keylen = strlen($str); $order = $curve->getLengthInBytes(); // point compression is being used if ($keylen == $order + 1) { return $curve->derivePoint($str); } // point compression is not being used if ($keylen == 2 * $order + 1) { preg_match("#(.)(.{{$order}})(.{{$order}})#s", $str, $matches); list(, $w, $x, $y) = $matches; if ($w != "\4") { throw new \UnexpectedValueException('The first byte of an uncompressed point should be 04 - not ' . Strings::bin2hex($val)); } $point = [ $curve->convertInteger(new BigInteger($x, 256)), $curve->convertInteger(new BigInteger($y, 256)) ]; if (!$curve->verifyPoint($point)) { throw new \RuntimeException('Unable to verify that point exists on curve'); } return $point; } throw new \UnexpectedValueException('The string representation of the points is not of an appropriate length'); } /** * Encode Parameters * * @todo Maybe at some point this could be moved to __toString() for each of the curves? * @param BaseCurve $curve * @param bool $returnArray optional * @param array $options optional * @return string|false */ private static function encodeParameters(BaseCurve $curve, $returnArray = false, array $options = []) { $useNamedCurves = isset($options['namedCurve']) ? $options['namedCurve'] : self::$useNamedCurves; $reflect = new \ReflectionClass($curve); $name = $reflect->getShortName(); if ($useNamedCurves) { if (isset(self::$curveOIDs[$name])) { if ($reflect->isFinal()) { $reflect = $reflect->getParentClass(); $name = $reflect->getShortName(); } return $returnArray ? ['namedCurve' => $name] : ASN1::encodeDER(['namedCurve' => $name], Maps\ECParameters::MAP); } foreach (new \DirectoryIterator(__DIR__ . '/../../Curves/') as $file) { if ($file->getExtension() != 'php') { continue; } $testName = $file->getBasename('.php'); $class = 'phpseclib3\Crypt\EC\Curves\\' . $testName; $reflect = new \ReflectionClass($class); if ($reflect->isFinal()) { continue; } $candidate = new $class(); switch ($name) { case 'Prime': if (!$candidate instanceof PrimeCurve) { break; } if (!$candidate->getModulo()->equals($curve->getModulo())) { break; } if ($candidate->getA()->toBytes() != $curve->getA()->toBytes()) { break; } if ($candidate->getB()->toBytes() != $curve->getB()->toBytes()) { break; } list($candidateX, $candidateY) = $candidate->getBasePoint(); list($curveX, $curveY) = $curve->getBasePoint(); if ($candidateX->toBytes() != $curveX->toBytes()) { break; } if ($candidateY->toBytes() != $curveY->toBytes()) { break; } return $returnArray ? ['namedCurve' => $testName] : ASN1::encodeDER(['namedCurve' => $testName], Maps\ECParameters::MAP); case 'Binary': if (!$candidate instanceof BinaryCurve) { break; } if ($candidate->getModulo() != $curve->getModulo()) { break; } if ($candidate->getA()->toBytes() != $curve->getA()->toBytes()) { break; } if ($candidate->getB()->toBytes() != $curve->getB()->toBytes()) { break; } list($candidateX, $candidateY) = $candidate->getBasePoint(); list($curveX, $curveY) = $curve->getBasePoint(); if ($candidateX->toBytes() != $curveX->toBytes()) { break; } if ($candidateY->toBytes() != $curveY->toBytes()) { break; } return $returnArray ? ['namedCurve' => $testName] : ASN1::encodeDER(['namedCurve' => $testName], Maps\ECParameters::MAP); } } } $order = $curve->getOrder(); // we could try to calculate the order thusly: // https://crypto.stackexchange.com/a/27914/4520 // https://en.wikipedia.org/wiki/Schoof%E2%80%93Elkies%E2%80%93Atkin_algorithm if (!$order) { throw new \RuntimeException('Specified Curves need the order to be specified'); } $point = $curve->getBasePoint(); $x = $point[0]->toBytes(); $y = $point[1]->toBytes(); if ($curve instanceof PrimeCurve) { /* * valid versions are: * * ecdpVer1: * - neither the curve or the base point are generated verifiably randomly. * ecdpVer2: * - curve and base point are generated verifiably at random and curve.seed is present * ecdpVer3: * - base point is generated verifiably at random but curve is not. curve.seed is present */ // other (optional) parameters can be calculated using the methods discused at // https://crypto.stackexchange.com/q/28947/4520 $data = [ 'version' => 'ecdpVer1', 'fieldID' => [ 'fieldType' => 'prime-field', 'parameters' => $curve->getModulo() ], 'curve' => [ 'a' => $curve->getA()->toBytes(), 'b' => $curve->getB()->toBytes() ], 'base' => "\4" . $x . $y, 'order' => $order ]; return $returnArray ? ['specifiedCurve' => $data] : ASN1::encodeDER(['specifiedCurve' => $data], Maps\ECParameters::MAP); } if ($curve instanceof BinaryCurve) { $modulo = $curve->getModulo(); $basis = count($modulo); $m = array_shift($modulo); array_pop($modulo); // the last parameter should always be 0 //rsort($modulo); switch ($basis) { case 3: $basis = 'tpBasis'; $modulo = new BigInteger($modulo[0]); break; case 5: $basis = 'ppBasis'; // these should be in strictly ascending order (hence the commented out rsort above) $modulo = [ 'k1' => new BigInteger($modulo[2]), 'k2' => new BigInteger($modulo[1]), 'k3' => new BigInteger($modulo[0]) ]; $modulo = ASN1::encodeDER($modulo, Maps\Pentanomial::MAP); $modulo = new ASN1\Element($modulo); } $params = ASN1::encodeDER([ 'm' => new BigInteger($m), 'basis' => $basis, 'parameters' => $modulo ], Maps\Characteristic_two::MAP); $params = new ASN1\Element($params); $a = ltrim($curve->getA()->toBytes(), "\0"); if (!strlen($a)) { $a = "\0"; } $b = ltrim($curve->getB()->toBytes(), "\0"); if (!strlen($b)) { $b = "\0"; } $data = [ 'version' => 'ecdpVer1', 'fieldID' => [ 'fieldType' => 'characteristic-two-field', 'parameters' => $params ], 'curve' => [ 'a' => $a, 'b' => $b ], 'base' => "\4" . $x . $y, 'order' => $order ]; return $returnArray ? ['specifiedCurve' => $data] : ASN1::encodeDER(['specifiedCurve' => $data], Maps\ECParameters::MAP); } throw new UnsupportedCurveException('Curve cannot be serialized'); } /** * Use Specified Curve * * A specified curve has all the coefficients, the base points, etc, explicitely included. * A specified curve is a more verbose way of representing a curve */ public static function useSpecifiedCurve() { self::$useNamedCurves = false; } /** * Use Named Curve * * A named curve does not include any parameters. It is up to the EC parameters to * know what the coefficients, the base points, etc, are from the name of the curve. * A named curve is a more concise way of representing a curve */ public static function useNamedCurve() { self::$useNamedCurves = true; } } PK!kPPBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/JWK.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\JWK as Progenitor; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Crypt\EC\Curves\secp256k1; use phpseclib3\Crypt\EC\Curves\secp256r1; use phpseclib3\Crypt\EC\Curves\secp384r1; use phpseclib3\Crypt\EC\Curves\secp521r1; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\Math\BigInteger; /** * JWK Formatted EC Handler * * @author Jim Wigginton */ abstract class JWK extends Progenitor { use Common; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); switch ($key->kty) { case 'EC': switch ($key->crv) { case 'P-256': case 'P-384': case 'P-521': case 'secp256k1': break; default: throw new UnsupportedCurveException('Only P-256, P-384, P-521 and secp256k1 curves are accepted (' . $key->crv . ' provided)'); } break; case 'OKP': switch ($key->crv) { case 'Ed25519': case 'Ed448': break; default: throw new UnsupportedCurveException('Only Ed25519 and Ed448 curves are accepted (' . $key->crv . ' provided)'); } break; default: throw new \Exception('Only EC and OKP JWK keys are supported'); } $curve = '\phpseclib3\Crypt\EC\Curves\\' . str_replace('P-', 'nistp', $key->crv); $curve = new $curve(); if ($curve instanceof TwistedEdwardsCurve) { $QA = self::extractPoint(Strings::base64url_decode($key->x), $curve); if (!isset($key->d)) { return compact('curve', 'QA'); } $arr = $curve->extractSecret(Strings::base64url_decode($key->d)); return compact('curve', 'QA') + $arr; } $QA = [ $curve->convertInteger(new BigInteger(Strings::base64url_decode($key->x), 256)), $curve->convertInteger(new BigInteger(Strings::base64url_decode($key->y), 256)) ]; if (!$curve->verifyPoint($QA)) { throw new \RuntimeException('Unable to verify that point exists on curve'); } if (!isset($key->d)) { return compact('curve', 'QA'); } $dA = new BigInteger(Strings::base64url_decode($key->d), 256); $curve->rangeCheck($dA); return compact('curve', 'dA', 'QA'); } /** * Returns the alias that corresponds to a curve * * @return string */ private static function getAlias(BaseCurve $curve) { switch (true) { case $curve instanceof secp256r1: return 'P-256'; case $curve instanceof secp384r1: return 'P-384'; case $curve instanceof secp521r1: return 'P-521'; case $curve instanceof secp256k1: return 'secp256k1'; } $reflect = new \ReflectionClass($curve); $curveName = $reflect->isFinal() ? $reflect->getParentClass()->getShortName() : $reflect->getShortName(); throw new UnsupportedCurveException("$curveName is not a supported curve"); } /** * Return the array superstructure for an EC public key * * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @return array */ private static function savePublicKeyHelper(BaseCurve $curve, array $publicKey) { if ($curve instanceof TwistedEdwardsCurve) { return [ 'kty' => 'OKP', 'crv' => $curve instanceof Ed25519 ? 'Ed25519' : 'Ed448', 'x' => Strings::base64url_encode($curve->encodePoint($publicKey)) ]; } return [ 'kty' => 'EC', 'crv' => self::getAlias($curve), 'x' => Strings::base64url_encode($publicKey[0]->toBytes()), 'y' => Strings::base64url_encode($publicKey[1]->toBytes()) ]; } /** * Convert an EC public key to the appropriate format * * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param array $options optional * @return string */ public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = []) { $key = self::savePublicKeyHelper($curve, $publicKey); return self::wrapKey($key, $options); } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param Ed25519 $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $secret = null, $password = '', array $options = []) { $key = self::savePublicKeyHelper($curve, $publicKey); $key['d'] = $curve instanceof TwistedEdwardsCurve ? $secret : $privateKey->toBytes(); $key['d'] = Strings::base64url_encode($key['d']); return self::wrapKey($key, $options); } } PK!VHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/libsodium.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\Math\BigInteger; /** * libsodium Key Handler * * @author Jim Wigginton */ abstract class libsodium { use Common; /** * Is invisible flag * */ const IS_INVISIBLE = true; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { switch (strlen($key)) { case 32: $public = $key; break; case 64: $private = substr($key, 0, 32); $public = substr($key, -32); break; case 96: $public = substr($key, -32); if (substr($key, 32, 32) != $public) { throw new \RuntimeException('Keys with 96 bytes should have the 2nd and 3rd set of 32 bytes match'); } $private = substr($key, 0, 32); break; default: throw new \RuntimeException('libsodium keys need to either be 32 bytes long, 64 bytes long or 96 bytes long'); } $curve = new Ed25519(); $components = ['curve' => $curve]; if (isset($private)) { $arr = $curve->extractSecret($private); $components['dA'] = $arr['dA']; $components['secret'] = $arr['secret']; } $components['QA'] = isset($public) ? self::extractPoint($public, $curve) : $curve->multiplyPoint($curve->getBasePoint(), $components['dA']); return $components; } /** * Convert an EC public key to the appropriate format * * @param Ed25519 $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @return string */ public static function savePublicKey(Ed25519 $curve, array $publicKey) { return $curve->encodePoint($publicKey); } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param Ed25519 $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, Ed25519 $curve, array $publicKey, $secret = null, $password = '') { if (!isset($secret)) { throw new \RuntimeException('Private Key does not have a secret set'); } if (strlen($secret) != 32) { throw new \RuntimeException('Private Key secret is not of the correct length'); } if (!empty($password) && is_string($password)) { throw new UnsupportedFormatException('libsodium private keys do not support encryption'); } return $secret . $curve->encodePoint($publicKey); } } PK!W3j Pvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/MontgomeryPrivate.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\Curves\Curve25519; use phpseclib3\Crypt\EC\Curves\Curve448; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\Math\BigInteger; /** * Montgomery Curve Private Key Handler * * @author Jim Wigginton */ abstract class MontgomeryPrivate { /** * Is invisible flag * */ const IS_INVISIBLE = true; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { switch (strlen($key)) { case 32: $curve = new Curve25519(); break; case 56: $curve = new Curve448(); break; default: throw new \LengthException('The only supported lengths are 32 and 56'); } $components = ['curve' => $curve]; $components['dA'] = new BigInteger($key, 256); $curve->rangeCheck($components['dA']); // note that EC::getEncodedCoordinates does some additional "magic" (it does strrev on the result) $components['QA'] = $components['curve']->multiplyPoint($components['curve']->getBasePoint(), $components['dA']); return $components; } /** * Convert an EC public key to the appropriate format * * @param MontgomeryCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @return string */ public static function savePublicKey(MontgomeryCurve $curve, array $publicKey) { return strrev($publicKey[0]->toBytes()); } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param MontgomeryCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, MontgomeryCurve $curve, array $publicKey, $secret = null, $password = '') { if (!empty($password) && is_string($password)) { throw new UnsupportedFormatException('MontgomeryPrivate private keys do not support encryption'); } return $privateKey->toBytes(); } } PK!uOvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/MontgomeryPublic.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\Curves\Curve25519; use phpseclib3\Crypt\EC\Curves\Curve448; use phpseclib3\Math\BigInteger; /** * Montgomery Public Key Handler * * @author Jim Wigginton */ abstract class MontgomeryPublic { /** * Is invisible flag * */ const IS_INVISIBLE = true; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { switch (strlen($key)) { case 32: $curve = new Curve25519(); break; case 56: $curve = new Curve448(); break; default: throw new \LengthException('The only supported lengths are 32 and 56'); } $components = ['curve' => $curve]; $components['QA'] = [$components['curve']->convertInteger(new BigInteger(strrev($key), 256))]; return $components; } /** * Convert an EC public key to the appropriate format * * @param MontgomeryCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @return string */ public static function savePublicKey(MontgomeryCurve $curve, array $publicKey) { return strrev($publicKey[0]->toBytes()); } } PK!7MFvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/OpenSSH.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\OpenSSH as Progenitor; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\Math\BigInteger; /** * OpenSSH Formatted EC Key Handler * * @author Jim Wigginton */ abstract class OpenSSH extends Progenitor { use Common; /** * Supported Key Types * * @var array */ protected static $types = [ 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519' ]; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $parsed = parent::load($key, $password); if (isset($parsed['paddedKey'])) { $paddedKey = $parsed['paddedKey']; list($type) = Strings::unpackSSH2('s', $paddedKey); if ($type != $parsed['type']) { throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])"); } if ($type == 'ssh-ed25519') { list(, $key, $comment) = Strings::unpackSSH2('sss', $paddedKey); $key = libsodium::load($key); $key['comment'] = $comment; return $key; } list($curveName, $publicKey, $privateKey, $comment) = Strings::unpackSSH2('ssis', $paddedKey); $curve = self::loadCurveByParam(['namedCurve' => $curveName]); $curve->rangeCheck($privateKey); return [ 'curve' => $curve, 'dA' => $privateKey, 'QA' => self::extractPoint("\0$publicKey", $curve), 'comment' => $comment ]; } if ($parsed['type'] == 'ssh-ed25519') { if (Strings::shift($parsed['publicKey'], 4) != "\0\0\0\x20") { throw new \RuntimeException('Length of ssh-ed25519 key should be 32'); } $curve = new Ed25519(); $qa = self::extractPoint($parsed['publicKey'], $curve); } else { list($curveName, $publicKey) = Strings::unpackSSH2('ss', $parsed['publicKey']); $curveName = '\phpseclib3\Crypt\EC\Curves\\' . $curveName; $curve = new $curveName(); $qa = self::extractPoint("\0" . $publicKey, $curve); } return [ 'curve' => $curve, 'QA' => $qa, 'comment' => $parsed['comment'] ]; } /** * Returns the alias that corresponds to a curve * * @return string */ private static function getAlias(BaseCurve $curve) { self::initialize_static_variables(); $reflect = new \ReflectionClass($curve); $name = $reflect->getShortName(); $oid = self::$curveOIDs[$name]; $aliases = array_filter(self::$curveOIDs, function ($v) use ($oid) { return $v == $oid; }); $aliases = array_keys($aliases); for ($i = 0; $i < count($aliases); $i++) { if (in_array('ecdsa-sha2-' . $aliases[$i], self::$types)) { $alias = $aliases[$i]; break; } } if (!isset($alias)) { throw new UnsupportedCurveException($name . ' is not a curve that the OpenSSH plugin supports'); } return $alias; } /** * Convert an EC public key to the appropriate format * * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param array $options optional * @return string */ public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = []) { $comment = isset($options['comment']) ? $options['comment'] : self::$comment; if ($curve instanceof Ed25519) { $key = Strings::packSSH2('ss', 'ssh-ed25519', $curve->encodePoint($publicKey)); if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $key; } $key = 'ssh-ed25519 ' . base64_encode($key) . ' ' . $comment; return $key; } $alias = self::getAlias($curve); $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); $key = Strings::packSSH2('sss', 'ecdsa-sha2-' . $alias, $alias, $points); if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $key; } $key = 'ecdsa-sha2-' . $alias . ' ' . base64_encode($key) . ' ' . $comment; return $key; } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param Ed25519 $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $secret = null, $password = '', array $options = []) { if ($curve instanceof Ed25519) { if (!isset($secret)) { throw new \RuntimeException('Private Key does not have a secret set'); } if (strlen($secret) != 32) { throw new \RuntimeException('Private Key secret is not of the correct length'); } $pubKey = $curve->encodePoint($publicKey); $publicKey = Strings::packSSH2('ss', 'ssh-ed25519', $pubKey); $privateKey = Strings::packSSH2('sss', 'ssh-ed25519', $pubKey, $secret . $pubKey); return self::wrapPrivateKey($publicKey, $privateKey, $password, $options); } $alias = self::getAlias($curve); $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); $publicKey = self::savePublicKey($curve, $publicKey, ['binary' => true]); $privateKey = Strings::packSSH2('sssi', 'ecdsa-sha2-' . $alias, $alias, $points, $privateKey); return self::wrapPrivateKey($publicKey, $privateKey, $password, $options); } } PK!ppDvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PKCS1.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PKCS1 as Progenitor; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * "PKCS1" (RFC5915) Formatted EC Key Handler * * @author Jim Wigginton */ abstract class PKCS1 extends Progenitor { use Common; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { self::initialize_static_variables(); if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } if (strpos($key, 'BEGIN EC PARAMETERS') && strpos($key, 'BEGIN EC PRIVATE KEY')) { $components = []; preg_match('#-*BEGIN EC PRIVATE KEY-*[^-]*-*END EC PRIVATE KEY-*#s', $key, $matches); $decoded = parent::load($matches[0], $password); $decoded = ASN1::decodeBER($decoded); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $ecPrivate = ASN1::asn1map($decoded[0], Maps\ECPrivateKey::MAP); if (!is_array($ecPrivate)) { throw new \RuntimeException('Unable to perform ASN1 mapping'); } if (isset($ecPrivate['parameters'])) { $components['curve'] = self::loadCurveByParam($ecPrivate['parameters']); } preg_match('#-*BEGIN EC PARAMETERS-*[^-]*-*END EC PARAMETERS-*#s', $key, $matches); $decoded = parent::load($matches[0], ''); $decoded = ASN1::decodeBER($decoded); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $ecParams = ASN1::asn1map($decoded[0], Maps\ECParameters::MAP); if (!is_array($ecParams)) { throw new \RuntimeException('Unable to perform ASN1 mapping'); } $ecParams = self::loadCurveByParam($ecParams); // comparing $ecParams and $components['curve'] directly won't work because they'll have different Math\Common\FiniteField classes // even if the modulo is the same if (isset($components['curve']) && self::encodeParameters($ecParams, false, []) != self::encodeParameters($components['curve'], false, [])) { throw new \RuntimeException('EC PARAMETERS does not correspond to EC PRIVATE KEY'); } if (!isset($components['curve'])) { $components['curve'] = $ecParams; } $components['dA'] = new BigInteger($ecPrivate['privateKey'], 256); $components['curve']->rangeCheck($components['dA']); $components['QA'] = isset($ecPrivate['publicKey']) ? self::extractPoint($ecPrivate['publicKey'], $components['curve']) : $components['curve']->multiplyPoint($components['curve']->getBasePoint(), $components['dA']); return $components; } $key = parent::load($key, $password); $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $key = ASN1::asn1map($decoded[0], Maps\ECParameters::MAP); if (is_array($key)) { return ['curve' => self::loadCurveByParam($key)]; } $key = ASN1::asn1map($decoded[0], Maps\ECPrivateKey::MAP); if (!is_array($key)) { throw new \RuntimeException('Unable to perform ASN1 mapping'); } if (!isset($key['parameters'])) { throw new \RuntimeException('Key cannot be loaded without parameters'); } $components = []; $components['curve'] = self::loadCurveByParam($key['parameters']); $components['dA'] = new BigInteger($key['privateKey'], 256); $components['QA'] = isset($ecPrivate['publicKey']) ? self::extractPoint($ecPrivate['publicKey'], $components['curve']) : $components['curve']->multiplyPoint($components['curve']->getBasePoint(), $components['dA']); return $components; } /** * Convert EC parameters to the appropriate format * * @return string */ public static function saveParameters(BaseCurve $curve, array $options = []) { self::initialize_static_variables(); if ($curve instanceof TwistedEdwardsCurve || $curve instanceof MontgomeryCurve) { throw new UnsupportedCurveException('TwistedEdwards and Montgomery Curves are not supported'); } $key = self::encodeParameters($curve, false, $options); return "-----BEGIN EC PARAMETERS-----\r\n" . chunk_split(Strings::base64_encode($key), 64) . "-----END EC PARAMETERS-----\r\n"; } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $secret = null, $password = '', array $options = []) { self::initialize_static_variables(); if ($curve instanceof TwistedEdwardsCurve || $curve instanceof MontgomeryCurve) { throw new UnsupportedCurveException('TwistedEdwards Curves are not supported'); } $publicKey = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); $key = [ 'version' => 'ecPrivkeyVer1', 'privateKey' => $privateKey->toBytes(), 'parameters' => new ASN1\Element(self::encodeParameters($curve)), 'publicKey' => "\0" . $publicKey ]; $key = ASN1::encodeDER($key, Maps\ECPrivateKey::MAP); return self::wrapPrivateKey($key, 'EC', $password, $options); } } PK!u Dvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PKCS8.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Crypt\Common\Formats\Keys\PKCS8 as Progenitor; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Crypt\EC\Curves\Ed448; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * PKCS#8 Formatted EC Key Handler * * @author Jim Wigginton */ abstract class PKCS8 extends Progenitor { use Common; /** * OID Name * * @var array */ const OID_NAME = ['id-ecPublicKey', 'id-Ed25519', 'id-Ed448']; /** * OID Value * * @var string */ const OID_VALUE = ['1.2.840.10045.2.1', '1.3.101.112', '1.3.101.113']; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { // initialize_static_variables() is defined in both the trait and the parent class // when it's defined in two places it's the traits one that's called // the parent one is needed, as well, but the parent one is called by other methods // in the parent class as needed and in the context of the parent it's the parent // one that's called self::initialize_static_variables(); $key = parent::load($key, $password); $type = isset($key['privateKey']) ? 'privateKey' : 'publicKey'; switch ($key[$type . 'Algorithm']['algorithm']) { case 'id-Ed25519': case 'id-Ed448': return self::loadEdDSA($key); } $decoded = ASN1::decodeBER($key[$type . 'Algorithm']['parameters']->element); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $params = ASN1::asn1map($decoded[0], Maps\ECParameters::MAP); if (!$params) { throw new \RuntimeException('Unable to decode the parameters using Maps\ECParameters'); } $components = []; $components['curve'] = self::loadCurveByParam($params); if ($type == 'publicKey') { $components['QA'] = self::extractPoint("\0" . $key['publicKey'], $components['curve']); return $components; } $decoded = ASN1::decodeBER($key['privateKey']); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $key = ASN1::asn1map($decoded[0], Maps\ECPrivateKey::MAP); if (isset($key['parameters']) && $params != $key['parameters']) { throw new \RuntimeException('The PKCS8 parameter field does not match the private key parameter field'); } $components['dA'] = new BigInteger($key['privateKey'], 256); $components['curve']->rangeCheck($components['dA']); $components['QA'] = isset($key['publicKey']) ? self::extractPoint($key['publicKey'], $components['curve']) : $components['curve']->multiplyPoint($components['curve']->getBasePoint(), $components['dA']); return $components; } /** * Break a public or private EdDSA key down into its constituent components * * @return array */ private static function loadEdDSA(array $key) { $components = []; if (isset($key['privateKey'])) { $components['curve'] = $key['privateKeyAlgorithm']['algorithm'] == 'id-Ed25519' ? new Ed25519() : new Ed448(); $expected = chr(ASN1::TYPE_OCTET_STRING) . ASN1::encodeLength($components['curve']::SIZE); if (substr($key['privateKey'], 0, 2) != $expected) { throw new \RuntimeException( 'The first two bytes of the ' . $key['privateKeyAlgorithm']['algorithm'] . ' private key field should be 0x' . bin2hex($expected) ); } $arr = $components['curve']->extractSecret(substr($key['privateKey'], 2)); $components['dA'] = $arr['dA']; $components['secret'] = $arr['secret']; } if (isset($key['publicKey'])) { if (!isset($components['curve'])) { $components['curve'] = $key['publicKeyAlgorithm']['algorithm'] == 'id-Ed25519' ? new Ed25519() : new Ed448(); } $components['QA'] = self::extractPoint($key['publicKey'], $components['curve']); } if (isset($key['privateKey']) && !isset($components['QA'])) { $components['QA'] = $components['curve']->multiplyPoint($components['curve']->getBasePoint(), $components['dA']); } return $components; } /** * Convert an EC public key to the appropriate format * * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param array $options optional * @return string */ public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = []) { self::initialize_static_variables(); if ($curve instanceof MontgomeryCurve) { throw new UnsupportedCurveException('Montgomery Curves are not supported'); } if ($curve instanceof TwistedEdwardsCurve) { return self::wrapPublicKey( $curve->encodePoint($publicKey), null, $curve instanceof Ed25519 ? 'id-Ed25519' : 'id-Ed448', $options ); } $params = new ASN1\Element(self::encodeParameters($curve, false, $options)); $key = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); return self::wrapPublicKey($key, $params, 'id-ecPublicKey', $options); } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $secret = null, $password = '', array $options = []) { self::initialize_static_variables(); if ($curve instanceof MontgomeryCurve) { throw new UnsupportedCurveException('Montgomery Curves are not supported'); } if ($curve instanceof TwistedEdwardsCurve) { return self::wrapPrivateKey( chr(ASN1::TYPE_OCTET_STRING) . ASN1::encodeLength($curve::SIZE) . $secret, [], null, $password, $curve instanceof Ed25519 ? 'id-Ed25519' : 'id-Ed448' ); } $publicKey = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); $params = new ASN1\Element(self::encodeParameters($curve, false, $options)); $key = [ 'version' => 'ecPrivkeyVer1', 'privateKey' => $privateKey->toBytes(), //'parameters' => $params, 'publicKey' => "\0" . $publicKey ]; $key = ASN1::encodeDER($key, Maps\ECPrivateKey::MAP); return self::wrapPrivateKey($key, [], $params, $password, 'id-ecPublicKey', '', $options); } } PK!#yEEDvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PuTTY.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PuTTY as Progenitor; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Math\BigInteger; /** * PuTTY Formatted EC Key Handler * * @author Jim Wigginton */ abstract class PuTTY extends Progenitor { use Common; /** * Public Handler * * @var string */ const PUBLIC_HANDLER = 'phpseclib3\Crypt\EC\Formats\Keys\OpenSSH'; /** * Supported Key Types * * @var array */ protected static $types = [ 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519' ]; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $components = parent::load($key, $password); if (!isset($components['private'])) { return $components; } $private = $components['private']; $temp = Strings::base64_encode(Strings::packSSH2('s', $components['type']) . $components['public']); $components = OpenSSH::load($components['type'] . ' ' . $temp . ' ' . $components['comment']); if ($components['curve'] instanceof TwistedEdwardsCurve) { if (Strings::shift($private, 4) != "\0\0\0\x20") { throw new \RuntimeException('Length of ssh-ed25519 key should be 32'); } $arr = $components['curve']->extractSecret($private); $components['dA'] = $arr['dA']; $components['secret'] = $arr['secret']; } else { list($components['dA']) = Strings::unpackSSH2('i', $private); $components['curve']->rangeCheck($components['dA']); } return $components; } /** * Convert a private key to the appropriate format. * * @param BigInteger $privateKey * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param string $secret optional * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $secret = null, $password = false, array $options = []) { self::initialize_static_variables(); $public = explode(' ', OpenSSH::savePublicKey($curve, $publicKey)); $name = $public[0]; $public = Strings::base64_decode($public[1]); list(, $length) = unpack('N', Strings::shift($public, 4)); Strings::shift($public, $length); // PuTTY pads private keys with a null byte per the following: // https://github.com/github/putty/blob/a3d14d77f566a41fc61dfdc5c2e0e384c9e6ae8b/sshecc.c#L1926 if (!$curve instanceof TwistedEdwardsCurve) { $private = $privateKey->toBytes(); if (!(strlen($privateKey->toBits()) & 7)) { $private = "\0$private"; } } $private = $curve instanceof TwistedEdwardsCurve ? Strings::packSSH2('s', $secret) : Strings::packSSH2('s', $private); return self::wrapPrivateKey($public, $private, $name, $password, $options); } /** * Convert an EC public key to the appropriate format * * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField[] $publicKey * @return string */ public static function savePublicKey(BaseCurve $curve, array $publicKey) { $public = explode(' ', OpenSSH::savePublicKey($curve, $publicKey)); $type = $public[0]; $public = Strings::base64_decode($public[1]); list(, $length) = unpack('N', Strings::shift($public, 4)); Strings::shift($public, $length); return self::wrapPublicKey($public, $type); } } PK!֟7XEXEBvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/XML.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\BaseCurves\Prime as PrimeCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Exception\BadConfigurationException; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\Math\BigInteger; /** * XML Formatted EC Key Handler * * @author Jim Wigginton */ abstract class XML { use Common; /** * Default namespace * * @var string */ private static $namespace; /** * Flag for using RFC4050 syntax * * @var bool */ private static $rfc4050 = false; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { self::initialize_static_variables(); if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } if (!class_exists('DOMDocument')) { throw new BadConfigurationException('The dom extension is not setup correctly on this system'); } $use_errors = libxml_use_internal_errors(true); $temp = self::isolateNamespace($key, 'http://www.w3.org/2009/xmldsig11#'); if ($temp) { $key = $temp; } $temp = self::isolateNamespace($key, 'http://www.w3.org/2001/04/xmldsig-more#'); if ($temp) { $key = $temp; } $dom = new \DOMDocument(); if (substr($key, 0, 5) != '' . $key . ''; } if (!$dom->loadXML($key)) { libxml_use_internal_errors($use_errors); throw new \UnexpectedValueException('Key does not appear to contain XML'); } $xpath = new \DOMXPath($dom); libxml_use_internal_errors($use_errors); $curve = self::loadCurveByParam($xpath); $pubkey = self::query($xpath, 'publickey', 'Public Key is not present'); $QA = self::query($xpath, 'ecdsakeyvalue')->length ? self::extractPointRFC4050($xpath, $curve) : self::extractPoint("\0" . $pubkey, $curve); libxml_use_internal_errors($use_errors); return compact('curve', 'QA'); } /** * Case-insensitive xpath query * * @param \DOMXPath $xpath * @param string $name * @param string $error optional * @param bool $decode optional * @return \DOMNodeList */ private static function query(\DOMXPath $xpath, $name, $error = null, $decode = true) { $query = '/'; $names = explode('/', $name); foreach ($names as $name) { $query .= "/*[translate(local-name(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$name']"; } $result = $xpath->query($query); if (!isset($error)) { return $result; } if (!$result->length) { throw new \RuntimeException($error); } return $decode ? self::decodeValue($result->item(0)->textContent) : $result->item(0)->textContent; } /** * Finds the first element in the relevant namespace, strips the namespacing and returns the XML for that element. * * @param string $xml * @param string $ns */ private static function isolateNamespace($xml, $ns) { $dom = new \DOMDocument(); if (!$dom->loadXML($xml)) { return false; } $xpath = new \DOMXPath($dom); $nodes = $xpath->query("//*[namespace::*[.='$ns'] and not(../namespace::*[.='$ns'])]"); if (!$nodes->length) { return false; } $node = $nodes->item(0); $ns_name = $node->lookupPrefix($ns); if ($ns_name) { $node->removeAttributeNS($ns, $ns_name); } return $dom->saveXML($node); } /** * Decodes the value * * @param string $value */ private static function decodeValue($value) { return Strings::base64_decode(str_replace(["\r", "\n", ' ', "\t"], '', $value)); } /** * Extract points from an XML document * * @param \DOMXPath $xpath * @param BaseCurve $curve * @return object[] */ private static function extractPointRFC4050(\DOMXPath $xpath, BaseCurve $curve) { $x = self::query($xpath, 'publickey/x'); $y = self::query($xpath, 'publickey/y'); if (!$x->length || !$x->item(0)->hasAttribute('Value')) { throw new \RuntimeException('Public Key / X coordinate not found'); } if (!$y->length || !$y->item(0)->hasAttribute('Value')) { throw new \RuntimeException('Public Key / Y coordinate not found'); } $point = [ $curve->convertInteger(new BigInteger($x->item(0)->getAttribute('Value'))), $curve->convertInteger(new BigInteger($y->item(0)->getAttribute('Value'))) ]; if (!$curve->verifyPoint($point)) { throw new \RuntimeException('Unable to verify that point exists on curve'); } return $point; } /** * Returns an instance of \phpseclib3\Crypt\EC\BaseCurves\Base based * on the curve parameters * * @param \DomXPath $xpath * @return BaseCurve|false */ private static function loadCurveByParam(\DOMXPath $xpath) { $namedCurve = self::query($xpath, 'namedcurve'); if ($namedCurve->length == 1) { $oid = $namedCurve->item(0)->getAttribute('URN'); $oid = preg_replace('#[^\d.]#', '', $oid); $name = array_search($oid, self::$curveOIDs); if ($name === false) { throw new UnsupportedCurveException('Curve with OID of ' . $oid . ' is not supported'); } $curve = '\phpseclib3\Crypt\EC\Curves\\' . $name; if (!class_exists($curve)) { throw new UnsupportedCurveException('Named Curve of ' . $name . ' is not supported'); } return new $curve(); } $params = self::query($xpath, 'explicitparams'); if ($params->length) { return self::loadCurveByParamRFC4050($xpath); } $params = self::query($xpath, 'ecparameters'); if (!$params->length) { throw new \RuntimeException('No parameters are present'); } $fieldTypes = [ 'prime-field' => ['fieldid/prime/p'], 'gnb' => ['fieldid/gnb/m'], 'tnb' => ['fieldid/tnb/k'], 'pnb' => ['fieldid/pnb/k1', 'fieldid/pnb/k2', 'fieldid/pnb/k3'], 'unknown' => [] ]; foreach ($fieldTypes as $type => $queries) { foreach ($queries as $query) { $result = self::query($xpath, $query); if (!$result->length) { continue 2; } $param = preg_replace('#.*/#', '', $query); $$param = self::decodeValue($result->item(0)->textContent); } break; } $a = self::query($xpath, 'curve/a', 'A coefficient is not present'); $b = self::query($xpath, 'curve/b', 'B coefficient is not present'); $base = self::query($xpath, 'base', 'Base point is not present'); $order = self::query($xpath, 'order', 'Order is not present'); switch ($type) { case 'prime-field': $curve = new PrimeCurve(); $curve->setModulo(new BigInteger($p, 256)); $curve->setCoefficients( new BigInteger($a, 256), new BigInteger($b, 256) ); $point = self::extractPoint("\0" . $base, $curve); $curve->setBasePoint(...$point); $curve->setOrder(new BigInteger($order, 256)); return $curve; case 'gnb': case 'tnb': case 'pnb': default: throw new UnsupportedCurveException('Field Type of ' . $type . ' is not supported'); } } /** * Returns an instance of \phpseclib3\Crypt\EC\BaseCurves\Base based * on the curve parameters * * @param \DomXPath $xpath * @return BaseCurve|false */ private static function loadCurveByParamRFC4050(\DOMXPath $xpath) { $fieldTypes = [ 'prime-field' => ['primefieldparamstype/p'], 'unknown' => [] ]; foreach ($fieldTypes as $type => $queries) { foreach ($queries as $query) { $result = self::query($xpath, $query); if (!$result->length) { continue 2; } $param = preg_replace('#.*/#', '', $query); $$param = $result->item(0)->textContent; } break; } $a = self::query($xpath, 'curveparamstype/a', 'A coefficient is not present', false); $b = self::query($xpath, 'curveparamstype/b', 'B coefficient is not present', false); $x = self::query($xpath, 'basepointparams/basepoint/ecpointtype/x', 'Base Point X is not present', false); $y = self::query($xpath, 'basepointparams/basepoint/ecpointtype/y', 'Base Point Y is not present', false); $order = self::query($xpath, 'order', 'Order is not present', false); switch ($type) { case 'prime-field': $curve = new PrimeCurve(); $p = str_replace(["\r", "\n", ' ', "\t"], '', $p); $curve->setModulo(new BigInteger($p)); $a = str_replace(["\r", "\n", ' ', "\t"], '', $a); $b = str_replace(["\r", "\n", ' ', "\t"], '', $b); $curve->setCoefficients( new BigInteger($a), new BigInteger($b) ); $x = str_replace(["\r", "\n", ' ', "\t"], '', $x); $y = str_replace(["\r", "\n", ' ', "\t"], '', $y); $curve->setBasePoint( new BigInteger($x), new BigInteger($y) ); $order = str_replace(["\r", "\n", ' ', "\t"], '', $order); $curve->setOrder(new BigInteger($order)); return $curve; default: throw new UnsupportedCurveException('Field Type of ' . $type . ' is not supported'); } } /** * Sets the namespace. dsig11 is the most common one. * * Set to null to unset. Used only for creating public keys. * * @param string $namespace */ public static function setNamespace($namespace) { self::$namespace = $namespace; } /** * Uses the XML syntax specified in https://tools.ietf.org/html/rfc4050 */ public static function enableRFC4050Syntax() { self::$rfc4050 = true; } /** * Uses the XML syntax specified in https://www.w3.org/TR/xmldsig-core/#sec-ECParameters */ public static function disableRFC4050Syntax() { self::$rfc4050 = false; } /** * Convert a public key to the appropriate format * * @param BaseCurve $curve * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey * @param array $options optional * @return string */ public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = []) { self::initialize_static_variables(); if ($curve instanceof TwistedEdwardsCurve || $curve instanceof MontgomeryCurve) { throw new UnsupportedCurveException('TwistedEdwards and Montgomery Curves are not supported'); } if (empty(static::$namespace)) { $pre = $post = ''; } else { $pre = static::$namespace . ':'; $post = ':' . static::$namespace; } if (self::$rfc4050) { return '<' . $pre . 'ECDSAKeyValue xmlns' . $post . '="http://www.w3.org/2001/04/xmldsig-more#">' . "\r\n" . self::encodeXMLParameters($curve, $pre, $options) . "\r\n" . '<' . $pre . 'PublicKey>' . "\r\n" . '<' . $pre . 'X Value="' . $publicKey[0] . '" />' . "\r\n" . '<' . $pre . 'Y Value="' . $publicKey[1] . '" />' . "\r\n" . '' . "\r\n" . ''; } $publicKey = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); return '<' . $pre . 'ECDSAKeyValue xmlns' . $post . '="http://www.w3.org/2009/xmldsig11#">' . "\r\n" . self::encodeXMLParameters($curve, $pre, $options) . "\r\n" . '<' . $pre . 'PublicKey>' . Strings::base64_encode($publicKey) . '' . "\r\n" . ''; } /** * Encode Parameters * * @param BaseCurve $curve * @param string $pre * @param array $options optional * @return string|false */ private static function encodeXMLParameters(BaseCurve $curve, $pre, array $options = []) { $result = self::encodeParameters($curve, true, $options); if (isset($result['namedCurve'])) { $namedCurve = '<' . $pre . 'NamedCurve URI="urn:oid:' . self::$curveOIDs[$result['namedCurve']] . '" />'; return self::$rfc4050 ? '' . str_replace('URI', 'URN', $namedCurve) . '' : $namedCurve; } if (self::$rfc4050) { $xml = '<' . $pre . 'ExplicitParams>' . "\r\n" . '<' . $pre . 'FieldParams>' . "\r\n"; $temp = $result['specifiedCurve']; switch ($temp['fieldID']['fieldType']) { case 'prime-field': $xml .= '<' . $pre . 'PrimeFieldParamsType>' . "\r\n" . '<' . $pre . 'P>' . $temp['fieldID']['parameters'] . '' . "\r\n" . '' . "\r\n"; $a = $curve->getA(); $b = $curve->getB(); list($x, $y) = $curve->getBasePoint(); break; default: throw new UnsupportedCurveException('Field Type of ' . $temp['fieldID']['fieldType'] . ' is not supported'); } $xml .= '' . "\r\n" . '<' . $pre . 'CurveParamsType>' . "\r\n" . '<' . $pre . 'A>' . $a . '' . "\r\n" . '<' . $pre . 'B>' . $b . '' . "\r\n" . '' . "\r\n" . '<' . $pre . 'BasePointParams>' . "\r\n" . '<' . $pre . 'BasePoint>' . "\r\n" . '<' . $pre . 'ECPointType>' . "\r\n" . '<' . $pre . 'X>' . $x . '' . "\r\n" . '<' . $pre . 'Y>' . $y . '' . "\r\n" . '' . "\r\n" . '' . "\r\n" . '<' . $pre . 'Order>' . $curve->getOrder() . '' . "\r\n" . '' . "\r\n" . '' . "\r\n"; return $xml; } if (isset($result['specifiedCurve'])) { $xml = '<' . $pre . 'ECParameters>' . "\r\n" . '<' . $pre . 'FieldID>' . "\r\n"; $temp = $result['specifiedCurve']; switch ($temp['fieldID']['fieldType']) { case 'prime-field': $xml .= '<' . $pre . 'Prime>' . "\r\n" . '<' . $pre . 'P>' . Strings::base64_encode($temp['fieldID']['parameters']->toBytes()) . '' . "\r\n" . '' . "\r\n" ; break; default: throw new UnsupportedCurveException('Field Type of ' . $temp['fieldID']['fieldType'] . ' is not supported'); } $xml .= '' . "\r\n" . '<' . $pre . 'Curve>' . "\r\n" . '<' . $pre . 'A>' . Strings::base64_encode($temp['curve']['a']) . '' . "\r\n" . '<' . $pre . 'B>' . Strings::base64_encode($temp['curve']['b']) . '' . "\r\n" . '' . "\r\n" . '<' . $pre . 'Base>' . Strings::base64_encode($temp['base']) . '' . "\r\n" . '<' . $pre . 'Order>' . Strings::base64_encode($temp['order']) . '' . "\r\n" . ''; return $xml; } } } PK!ƫ]ffHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/ASN1.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Signature; use phpseclib3\File\ASN1 as Encoder; use phpseclib3\File\ASN1\Maps\EcdsaSigValue; use phpseclib3\Math\BigInteger; /** * ASN1 Signature Handler * * @author Jim Wigginton */ abstract class ASN1 { /** * Loads a signature * * @param string $sig * @return array */ public static function load($sig) { if (!is_string($sig)) { return false; } $decoded = Encoder::decodeBER($sig); if (empty($decoded)) { return false; } $components = Encoder::asn1map($decoded[0], EcdsaSigValue::MAP); return $components; } /** * Returns a signature in the appropriate format * * @param BigInteger $r * @param BigInteger $s * @return string */ public static function save(BigInteger $r, BigInteger $s) { return Encoder::encodeDER(compact('r', 's'), EcdsaSigValue::MAP); } } PK!q5GGHvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/IEEE.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Signature; use phpseclib3\Math\BigInteger; /** * ASN1 Signature Handler * * @author Jim Wigginton */ abstract class IEEE { /** * Loads a signature * * @param string $sig * @return array */ public static function load($sig) { if (!is_string($sig)) { return false; } $len = strlen($sig); if ($len & 1) { return false; } $r = new BigInteger(substr($sig, 0, $len >> 1), 256); $s = new BigInteger(substr($sig, $len >> 1), 256); return compact('r', 's'); } /** * Returns a signature in the appropriate format * * @param BigInteger $r * @param BigInteger $s * @param string $curve * @param int $length * @return string */ public static function save(BigInteger $r, BigInteger $s, $curve, $length) { $r = $r->toBytes(); $s = $s->toBytes(); $length = (int) ceil($length / 8); return str_pad($r, $length, "\0", STR_PAD_LEFT) . str_pad($s, $length, "\0", STR_PAD_LEFT); } } PK!|j2Gvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/Raw.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Signature; use phpseclib3\Crypt\Common\Formats\Signature\Raw as Progenitor; /** * Raw DSA Signature Handler * * @author Jim Wigginton */ abstract class Raw extends Progenitor { } PK!辇Hvendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/SSH2.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC\Formats\Signature; use phpseclib3\Common\Functions\Strings; use phpseclib3\Math\BigInteger; /** * SSH2 Signature Handler * * @author Jim Wigginton */ abstract class SSH2 { /** * Loads a signature * * @param string $sig * @return mixed */ public static function load($sig) { if (!is_string($sig)) { return false; } $result = Strings::unpackSSH2('ss', $sig); if ($result === false) { return false; } list($type, $blob) = $result; switch ($type) { // see https://tools.ietf.org/html/rfc5656#section-3.1.2 case 'ecdsa-sha2-nistp256': case 'ecdsa-sha2-nistp384': case 'ecdsa-sha2-nistp521': break; default: return false; } $result = Strings::unpackSSH2('ii', $blob); if ($result === false) { return false; } return [ 'r' => $result[0], 's' => $result[1] ]; } /** * Returns a signature in the appropriate format * * @param BigInteger $r * @param BigInteger $s * @param string $curve * @return string */ public static function save(BigInteger $r, BigInteger $s, $curve) { switch ($curve) { case 'secp256r1': $curve = 'nistp256'; break; case 'secp384r1': $curve = 'nistp384'; break; case 'secp521r1': $curve = 'nistp521'; break; default: return false; } $blob = Strings::packSSH2('ii', $r, $s); return Strings::packSSH2('ss', 'ecdsa-sha2-' . $curve, $blob); } } PK!1PA<vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Parameters.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC; use phpseclib3\Crypt\EC; /** * EC Parameters * * @author Jim Wigginton */ final class Parameters extends EC { /** * Returns the parameters * * @param string $type * @param array $options optional * @return string */ public function toString($type = 'PKCS1', array $options = []) { $type = self::validatePlugin('Keys', 'PKCS1', 'saveParameters'); return $type::saveParameters($this->curve, $options); } } PK!L6''<vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\EC; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Crypt\EC\Curves\Curve25519; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Crypt\EC\Formats\Keys\PKCS1; use phpseclib3\Crypt\EC\Formats\Signature\ASN1 as ASN1Signature; use phpseclib3\Crypt\Hash; use phpseclib3\Exception\UnsupportedOperationException; use phpseclib3\Math\BigInteger; /** * EC Private Key * * @author Jim Wigginton */ final class PrivateKey extends EC implements Common\PrivateKey { use Common\Traits\PasswordProtected; /** * Private Key dA * * sign() converts this to a BigInteger so one might wonder why this is a FiniteFieldInteger instead of * a BigInteger. That's because a FiniteFieldInteger, when converted to a byte string, is null padded by * a certain amount whereas a BigInteger isn't. * * @var object */ protected $dA; /** * @var string */ protected $secret; /** * Multiplies an encoded point by the private key * * Used by ECDH * * @param string $coordinates * @return string */ public function multiply($coordinates) { if ($this->curve instanceof MontgomeryCurve) { if ($this->curve instanceof Curve25519 && self::$engines['libsodium']) { return sodium_crypto_scalarmult($this->dA->toBytes(), $coordinates); } $point = [$this->curve->convertInteger(new BigInteger(strrev($coordinates), 256))]; $point = $this->curve->multiplyPoint($point, $this->dA); return strrev($point[0]->toBytes(true)); } if (!$this->curve instanceof TwistedEdwardsCurve) { $coordinates = "\0$coordinates"; } $point = PKCS1::extractPoint($coordinates, $this->curve); $point = $this->curve->multiplyPoint($point, $this->dA); if ($this->curve instanceof TwistedEdwardsCurve) { return $this->curve->encodePoint($point); } if (empty($point)) { throw new \RuntimeException('The infinity point is invalid'); } return "\4" . $point[0]->toBytes(true) . $point[1]->toBytes(true); } /** * Create a signature * * @see self::verify() * @param string $message * @return mixed */ public function sign($message) { if ($this->curve instanceof MontgomeryCurve) { throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures'); } $dA = $this->dA; $order = $this->curve->getOrder(); $shortFormat = $this->shortFormat; $format = $this->sigFormat; if ($format === false) { return false; } if ($this->curve instanceof TwistedEdwardsCurve) { if ($this->curve instanceof Ed25519 && self::$engines['libsodium'] && !isset($this->context)) { $result = sodium_crypto_sign_detached($message, $this->withPassword()->toString('libsodium')); return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result) : $result; } // contexts (Ed25519ctx) are supported but prehashing (Ed25519ph) is not. // quoting https://tools.ietf.org/html/rfc8032#section-8.5 , // "The Ed25519ph and Ed448ph variants ... SHOULD NOT be used" $A = $this->curve->encodePoint($this->QA); $curve = $this->curve; $hash = new Hash($curve::HASH); $secret = substr($hash->hash($this->secret), $curve::SIZE); if ($curve instanceof Ed25519) { $dom = !isset($this->context) ? '' : 'SigEd25519 no Ed25519 collisions' . "\0" . chr(strlen($this->context)) . $this->context; } else { $context = isset($this->context) ? $this->context : ''; $dom = 'SigEd448' . "\0" . chr(strlen($context)) . $context; } // SHA-512(dom2(F, C) || prefix || PH(M)) $r = $hash->hash($dom . $secret . $message); $r = strrev($r); $r = new BigInteger($r, 256); list(, $r) = $r->divide($order); $R = $curve->multiplyPoint($curve->getBasePoint(), $r); $R = $curve->encodePoint($R); $k = $hash->hash($dom . $R . $A . $message); $k = strrev($k); $k = new BigInteger($k, 256); list(, $k) = $k->divide($order); $S = $k->multiply($dA)->add($r); list(, $S) = $S->divide($order); $S = str_pad(strrev($S->toBytes()), $curve::SIZE, "\0"); return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $R . $S) : $R . $S; } if (self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods())) { $signature = ''; // altho PHP's OpenSSL bindings only supported EC key creation in PHP 7.1 they've long // supported signing / verification // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve; // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even // has curve-specific optimizations $result = openssl_sign($message, $signature, $this->withPassword()->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash()); if ($result) { if ($shortFormat == 'ASN1') { return $signature; } extract(ASN1Signature::load($signature)); return $this->formatSignature($r, $s); } } $e = $this->hash->hash($message); $e = new BigInteger($e, 256); $Ln = $this->hash->getLength() - $order->getLength(); $z = $Ln > 0 ? $e->bitwise_rightShift($Ln) : $e; while (true) { $k = BigInteger::randomRange(self::$one, $order->subtract(self::$one)); list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k); $x = $x->toBigInteger(); list(, $r) = $x->divide($order); if ($r->equals(self::$zero)) { continue; } $kinv = $k->modInverse($order); $temp = $z->add($dA->multiply($r)); $temp = $kinv->multiply($temp); list(, $s) = $temp->divide($order); if (!$s->equals(self::$zero)) { break; } } // the following is an RFC6979 compliant implementation of deterministic ECDSA // it's unused because it's mainly intended for use when a good CSPRNG isn't // available. if phpseclib's CSPRNG isn't good then even key generation is // suspect /* // if this were actually being used it'd probably be better if this lived in load() and createKey() $this->q = $this->curve->getOrder(); $dA = $this->dA->toBigInteger(); $this->x = $dA; $h1 = $this->hash->hash($message); $k = $this->computek($h1); list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k); $x = $x->toBigInteger(); list(, $r) = $x->divide($this->q); $kinv = $k->modInverse($this->q); $h1 = $this->bits2int($h1); $temp = $h1->add($dA->multiply($r)); $temp = $kinv->multiply($temp); list(, $s) = $temp->divide($this->q); */ return $this->formatSignature($r, $s); } /** * Returns the private key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePrivateKey'); return $type::savePrivateKey($this->dA, $this->curve, $this->QA, $this->secret, $this->password, $options); } /** * Returns the public key * * @see self::getPrivateKey() * @return mixed */ public function getPublicKey() { $format = 'PKCS8'; if ($this->curve instanceof MontgomeryCurve) { $format = 'MontgomeryPublic'; } $type = self::validatePlugin('Keys', $format, 'savePublicKey'); $key = $type::savePublicKey($this->curve, $this->QA); $key = EC::loadFormat($format, $key); if ($this->curve instanceof MontgomeryCurve) { return $key; } $key = $key ->withHash($this->hash->getHash()) ->withSignatureFormat($this->shortFormat); if ($this->curve instanceof TwistedEdwardsCurve) { $key = $key->withContext($this->context); } return $key; } /** * Returns a signature in the appropriate format * * @return string */ private function formatSignature(BigInteger $r, BigInteger $s) { $format = $this->sigFormat; $temp = new \ReflectionMethod($format, 'save'); $paramCount = $temp->getNumberOfRequiredParameters(); // @codingStandardsIgnoreStart switch ($paramCount) { case 2: return $format::save($r, $s); case 3: return $format::save($r, $s, $this->getCurve()); case 4: return $format::save($r, $s, $this->getCurve(), $this->getLength()); } // @codingStandardsIgnoreEnd // presumably the only way you could get to this is if you were using a custom plugin throw new UnsupportedOperationException("$format::save() has $paramCount parameters - the only valid parameter counts are 2 or 3"); } } PK!L 24BB;vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PublicKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\EC; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\EC; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Crypt\EC\Formats\Keys\PKCS1; use phpseclib3\Crypt\EC\Formats\Signature\ASN1 as ASN1Signature; use phpseclib3\Crypt\Hash; use phpseclib3\Exception\UnsupportedOperationException; use phpseclib3\Math\BigInteger; /** * EC Public Key * * @author Jim Wigginton */ final class PublicKey extends EC implements Common\PublicKey { use Common\Traits\Fingerprint; /** * Verify a signature * * @see self::verify() * @param string $message * @param string $signature * @return mixed */ public function verify($message, $signature) { if ($this->curve instanceof MontgomeryCurve) { throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures'); } $shortFormat = $this->shortFormat; $format = $this->sigFormat; if ($format === false) { return false; } $order = $this->curve->getOrder(); if ($this->curve instanceof TwistedEdwardsCurve) { if ($shortFormat == 'SSH2') { list(, $signature) = Strings::unpackSSH2('ss', $signature); } if ($this->curve instanceof Ed25519 && self::$engines['libsodium'] && !isset($this->context)) { return sodium_crypto_sign_verify_detached($signature, $message, $this->toString('libsodium')); } $curve = $this->curve; if (strlen($signature) != 2 * $curve::SIZE) { return false; } $R = substr($signature, 0, $curve::SIZE); $S = substr($signature, $curve::SIZE); try { $R = PKCS1::extractPoint($R, $curve); $R = $this->curve->convertToInternal($R); } catch (\Exception $e) { return false; } $S = strrev($S); $S = new BigInteger($S, 256); if ($S->compare($order) >= 0) { return false; } $A = $curve->encodePoint($this->QA); if ($curve instanceof Ed25519) { $dom2 = !isset($this->context) ? '' : 'SigEd25519 no Ed25519 collisions' . "\0" . chr(strlen($this->context)) . $this->context; } else { $context = isset($this->context) ? $this->context : ''; $dom2 = 'SigEd448' . "\0" . chr(strlen($context)) . $context; } $hash = new Hash($curve::HASH); $k = $hash->hash($dom2 . substr($signature, 0, $curve::SIZE) . $A . $message); $k = strrev($k); $k = new BigInteger($k, 256); list(, $k) = $k->divide($order); $qa = $curve->convertToInternal($this->QA); $lhs = $curve->multiplyPoint($curve->getBasePoint(), $S); $rhs = $curve->multiplyPoint($qa, $k); $rhs = $curve->addPoint($rhs, $R); $rhs = $curve->convertToAffine($rhs); return $lhs[0]->equals($rhs[0]) && $lhs[1]->equals($rhs[1]); } $params = $format::load($signature); if ($params === false || count($params) != 2) { return false; } extract($params); if (self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods())) { $sig = $format != 'ASN1' ? ASN1Signature::save($r, $s) : $signature; $result = openssl_verify($message, $sig, $this->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash()); if ($result != -1) { return (bool) $result; } } $n_1 = $order->subtract(self::$one); if (!$r->between(self::$one, $n_1) || !$s->between(self::$one, $n_1)) { return false; } $e = $this->hash->hash($message); $e = new BigInteger($e, 256); $Ln = $this->hash->getLength() - $order->getLength(); $z = $Ln > 0 ? $e->bitwise_rightShift($Ln) : $e; $w = $s->modInverse($order); list(, $u1) = $z->multiply($w)->divide($order); list(, $u2) = $r->multiply($w)->divide($order); $u1 = $this->curve->convertInteger($u1); $u2 = $this->curve->convertInteger($u2); list($x1, $y1) = $this->curve->multiplyAddPoints( [$this->curve->getBasePoint(), $this->QA], [$u1, $u2] ); $x1 = $x1->toBigInteger(); list(, $x1) = $x1->divide($order); return $x1->equals($r); } /** * Returns the public key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePublicKey'); return $type::savePublicKey($this->curve, $this->QA, $options); } } PK!@Cvendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/JWK.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\JWK as Progenitor; use phpseclib3\Math\BigInteger; /** * JWK Formatted RSA Handler * * @author Jim Wigginton */ abstract class JWK extends Progenitor { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); if ($key->kty != 'RSA') { throw new \RuntimeException('Only RSA JWK keys are supported'); } $count = $publicCount = 0; $vars = ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi']; foreach ($vars as $var) { if (!isset($key->$var) || !is_string($key->$var)) { continue; } $count++; $value = new BigInteger(Strings::base64url_decode($key->$var), 256); switch ($var) { case 'n': $publicCount++; $components['modulus'] = $value; break; case 'e': $publicCount++; $components['publicExponent'] = $value; break; case 'd': $components['privateExponent'] = $value; break; case 'p': $components['primes'][1] = $value; break; case 'q': $components['primes'][2] = $value; break; case 'dp': $components['exponents'][1] = $value; break; case 'dq': $components['exponents'][2] = $value; break; case 'qi': $components['coefficients'][2] = $value; } } if ($count == count($vars)) { return $components + ['isPublicKey' => false]; } if ($count == 2 && $publicCount == 2) { return $components + ['isPublicKey' => true]; } throw new \UnexpectedValueException('Key does not have an appropriate number of RSA parameters'); } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { if (count($primes) != 2) { throw new \InvalidArgumentException('JWK does not support multi-prime RSA keys'); } $key = [ 'kty' => 'RSA', 'n' => Strings::base64url_encode($n->toBytes()), 'e' => Strings::base64url_encode($e->toBytes()), 'd' => Strings::base64url_encode($d->toBytes()), 'p' => Strings::base64url_encode($primes[1]->toBytes()), 'q' => Strings::base64url_encode($primes[2]->toBytes()), 'dp' => Strings::base64url_encode($exponents[1]->toBytes()), 'dq' => Strings::base64url_encode($exponents[2]->toBytes()), 'qi' => Strings::base64url_encode($coefficients[2]->toBytes()) ]; return self::wrapKey($key, $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []) { $key = [ 'kty' => 'RSA', 'n' => Strings::base64url_encode($n->toBytes()), 'e' => Strings::base64url_encode($e->toBytes()) ]; return self::wrapKey($key, $options); } } PK!HFvendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/MSBLOB.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\Math\BigInteger; /** * Microsoft BLOB Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class MSBLOB { /** * Public/Private Key Pair * */ const PRIVATEKEYBLOB = 0x7; /** * Public Key * */ const PUBLICKEYBLOB = 0x6; /** * Public Key * */ const PUBLICKEYBLOBEX = 0xA; /** * RSA public key exchange algorithm * */ const CALG_RSA_KEYX = 0x0000A400; /** * RSA public key exchange algorithm * */ const CALG_RSA_SIGN = 0x00002400; /** * Public Key * */ const RSA1 = 0x31415352; /** * Private Key * */ const RSA2 = 0x32415352; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } $key = Strings::base64_decode($key); if (!is_string($key)) { throw new \UnexpectedValueException('Base64 decoding produced an error'); } if (strlen($key) < 20) { throw new \UnexpectedValueException('Key appears to be malformed'); } // PUBLICKEYSTRUC publickeystruc // https://msdn.microsoft.com/en-us/library/windows/desktop/aa387453(v=vs.85).aspx extract(unpack('atype/aversion/vreserved/Valgo', Strings::shift($key, 8))); /** * @var string $type * @var string $version * @var integer $reserved * @var integer $algo */ switch (ord($type)) { case self::PUBLICKEYBLOB: case self::PUBLICKEYBLOBEX: $publickey = true; break; case self::PRIVATEKEYBLOB: $publickey = false; break; default: throw new \UnexpectedValueException('Key appears to be malformed'); } $components = ['isPublicKey' => $publickey]; // https://msdn.microsoft.com/en-us/library/windows/desktop/aa375549(v=vs.85).aspx switch ($algo) { case self::CALG_RSA_KEYX: case self::CALG_RSA_SIGN: break; default: throw new \UnexpectedValueException('Key appears to be malformed'); } // RSAPUBKEY rsapubkey // https://msdn.microsoft.com/en-us/library/windows/desktop/aa387685(v=vs.85).aspx // could do V for pubexp but that's unsigned 32-bit whereas some PHP installs only do signed 32-bit extract(unpack('Vmagic/Vbitlen/a4pubexp', Strings::shift($key, 12))); /** * @var integer $magic * @var integer $bitlen * @var string $pubexp */ switch ($magic) { case self::RSA2: $components['isPublicKey'] = false; // fall-through case self::RSA1: break; default: throw new \UnexpectedValueException('Key appears to be malformed'); } $baseLength = $bitlen / 16; if (strlen($key) != 2 * $baseLength && strlen($key) != 9 * $baseLength) { throw new \UnexpectedValueException('Key appears to be malformed'); } $components[$components['isPublicKey'] ? 'publicExponent' : 'privateExponent'] = new BigInteger(strrev($pubexp), 256); // BYTE modulus[rsapubkey.bitlen/8] $components['modulus'] = new BigInteger(strrev(Strings::shift($key, $bitlen / 8)), 256); if ($publickey) { return $components; } $components['isPublicKey'] = false; // BYTE prime1[rsapubkey.bitlen/16] $components['primes'] = [1 => new BigInteger(strrev(Strings::shift($key, $bitlen / 16)), 256)]; // BYTE prime2[rsapubkey.bitlen/16] $components['primes'][] = new BigInteger(strrev(Strings::shift($key, $bitlen / 16)), 256); // BYTE exponent1[rsapubkey.bitlen/16] $components['exponents'] = [1 => new BigInteger(strrev(Strings::shift($key, $bitlen / 16)), 256)]; // BYTE exponent2[rsapubkey.bitlen/16] $components['exponents'][] = new BigInteger(strrev(Strings::shift($key, $bitlen / 16)), 256); // BYTE coefficient[rsapubkey.bitlen/16] $components['coefficients'] = [2 => new BigInteger(strrev(Strings::shift($key, $bitlen / 16)), 256)]; if (isset($components['privateExponent'])) { $components['publicExponent'] = $components['privateExponent']; } // BYTE privateExponent[rsapubkey.bitlen/8] $components['privateExponent'] = new BigInteger(strrev(Strings::shift($key, $bitlen / 8)), 256); return $components; } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '') { if (count($primes) != 2) { throw new \InvalidArgumentException('MSBLOB does not support multi-prime RSA keys'); } if (!empty($password) && is_string($password)) { throw new UnsupportedFormatException('MSBLOB private keys do not support encryption'); } $n = strrev($n->toBytes()); $e = str_pad(strrev($e->toBytes()), 4, "\0"); $key = pack('aavV', chr(self::PRIVATEKEYBLOB), chr(2), 0, self::CALG_RSA_KEYX); $key .= pack('VVa*', self::RSA2, 8 * strlen($n), $e); $key .= $n; $key .= strrev($primes[1]->toBytes()); $key .= strrev($primes[2]->toBytes()); $key .= strrev($exponents[1]->toBytes()); $key .= strrev($exponents[2]->toBytes()); $key .= strrev($coefficients[2]->toBytes()); $key .= strrev($d->toBytes()); return Strings::base64_encode($key); } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e) { $n = strrev($n->toBytes()); $e = str_pad(strrev($e->toBytes()), 4, "\0"); $key = pack('aavV', chr(self::PUBLICKEYBLOB), chr(2), 0, self::CALG_RSA_KEYX); $key .= pack('VVa*', self::RSA1, 8 * strlen($n), $e); $key .= $n; return Strings::base64_encode($key); } } PK!&5Gvendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/OpenSSH.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\OpenSSH as Progenitor; use phpseclib3\Math\BigInteger; /** * OpenSSH Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class OpenSSH extends Progenitor { /** * Supported Key Types * * @var array */ protected static $types = ['ssh-rsa']; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { static $one; if (!isset($one)) { $one = new BigInteger(1); } $parsed = parent::load($key, $password); if (isset($parsed['paddedKey'])) { list($type) = Strings::unpackSSH2('s', $parsed['paddedKey']); if ($type != $parsed['type']) { throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])"); } $primes = $coefficients = []; list( $modulus, $publicExponent, $privateExponent, $coefficients[2], $primes[1], $primes[2], $comment, ) = Strings::unpackSSH2('i6s', $parsed['paddedKey']); $temp = $primes[1]->subtract($one); $exponents = [1 => $publicExponent->modInverse($temp)]; $temp = $primes[2]->subtract($one); $exponents[] = $publicExponent->modInverse($temp); $isPublicKey = false; return compact('publicExponent', 'modulus', 'privateExponent', 'primes', 'coefficients', 'exponents', 'comment', 'isPublicKey'); } list($publicExponent, $modulus) = Strings::unpackSSH2('ii', $parsed['publicKey']); return [ 'isPublicKey' => true, 'modulus' => $modulus, 'publicExponent' => $publicExponent, 'comment' => $parsed['comment'] ]; } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []) { $RSAPublicKey = Strings::packSSH2('sii', 'ssh-rsa', $e, $n); if (isset($options['binary']) ? $options['binary'] : self::$binary) { return $RSAPublicKey; } $comment = isset($options['comment']) ? $options['comment'] : self::$comment; $RSAPublicKey = 'ssh-rsa ' . base64_encode($RSAPublicKey) . ' ' . $comment; return $RSAPublicKey; } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { $publicKey = self::savePublicKey($n, $e, ['binary' => true]); $privateKey = Strings::packSSH2('si6', 'ssh-rsa', $n, $e, $d, $coefficients[2], $primes[1], $primes[2]); return self::wrapPrivateKey($publicKey, $privateKey, $password, $options); } } PK!,55Evendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS1.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PKCS1 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * PKCS#1 Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class PKCS1 extends Progenitor { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } if (strpos($key, 'PUBLIC') !== false) { $components = ['isPublicKey' => true]; } elseif (strpos($key, 'PRIVATE') !== false) { $components = ['isPublicKey' => false]; } else { $components = []; } $key = parent::load($key, $password); $decoded = ASN1::decodeBER($key); if (!$decoded) { throw new \RuntimeException('Unable to decode BER'); } $key = ASN1::asn1map($decoded[0], Maps\RSAPrivateKey::MAP); if (is_array($key)) { $components += [ 'modulus' => $key['modulus'], 'publicExponent' => $key['publicExponent'], 'privateExponent' => $key['privateExponent'], 'primes' => [1 => $key['prime1'], $key['prime2']], 'exponents' => [1 => $key['exponent1'], $key['exponent2']], 'coefficients' => [2 => $key['coefficient']] ]; if ($key['version'] == 'multi') { foreach ($key['otherPrimeInfos'] as $primeInfo) { $components['primes'][] = $primeInfo['prime']; $components['exponents'][] = $primeInfo['exponent']; $components['coefficients'][] = $primeInfo['coefficient']; } } if (!isset($components['isPublicKey'])) { $components['isPublicKey'] = false; } return $components; } $key = ASN1::asn1map($decoded[0], Maps\RSAPublicKey::MAP); if (!is_array($key)) { throw new \RuntimeException('Unable to perform ASN1 mapping'); } if (!isset($components['isPublicKey'])) { $components['isPublicKey'] = true; } return $components + $key; } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { $num_primes = count($primes); $key = [ 'version' => $num_primes == 2 ? 'two-prime' : 'multi', 'modulus' => $n, 'publicExponent' => $e, 'privateExponent' => $d, 'prime1' => $primes[1], 'prime2' => $primes[2], 'exponent1' => $exponents[1], 'exponent2' => $exponents[2], 'coefficient' => $coefficients[2] ]; for ($i = 3; $i <= $num_primes; $i++) { $key['otherPrimeInfos'][] = [ 'prime' => $primes[$i], 'exponent' => $exponents[$i], 'coefficient' => $coefficients[$i] ]; } $key = ASN1::encodeDER($key, Maps\RSAPrivateKey::MAP); return self::wrapPrivateKey($key, 'RSA', $password, $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e) { $key = [ 'modulus' => $n, 'publicExponent' => $e ]; $key = ASN1::encodeDER($key, Maps\RSAPublicKey::MAP); return self::wrapPublicKey($key, 'RSA'); } } PK!Lu\ Evendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS8.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Crypt\Common\Formats\Keys\PKCS8 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\Math\BigInteger; /** * PKCS#8 Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class PKCS8 extends Progenitor { /** * OID Name * * @var string */ const OID_NAME = 'rsaEncryption'; /** * OID Value * * @var string */ const OID_VALUE = '1.2.840.113549.1.1.1'; /** * Child OIDs loaded * * @var bool */ protected static $childOIDsLoaded = false; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { $key = parent::load($key, $password); if (isset($key['privateKey'])) { $components['isPublicKey'] = false; $type = 'private'; } else { $components['isPublicKey'] = true; $type = 'public'; } $result = $components + PKCS1::load($key[$type . 'Key']); if (isset($key['meta'])) { $result['meta'] = $key['meta']; } return $result; } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { $key = PKCS1::savePrivateKey($n, $e, $d, $primes, $exponents, $coefficients); $key = ASN1::extractBER($key); return self::wrapPrivateKey($key, [], null, $password, null, '', $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []) { $key = PKCS1::savePublicKey($n, $e); $key = ASN1::extractBER($key); return self::wrapPublicKey($key, null, null, $options); } } PK!r:nCvendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PSS.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PKCS8 as Progenitor; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * PKCS#8 Formatted RSA-PSS Key Handler * * @author Jim Wigginton */ abstract class PSS extends Progenitor { /** * OID Name * * @var string */ const OID_NAME = 'id-RSASSA-PSS'; /** * OID Value * * @var string */ const OID_VALUE = '1.2.840.113549.1.1.10'; /** * OIDs loaded * * @var bool */ private static $oidsLoaded = false; /** * Child OIDs loaded * * @var bool */ protected static $childOIDsLoaded = false; /** * Initialize static variables */ private static function initialize_static_variables() { if (!self::$oidsLoaded) { ASN1::loadOIDs([ 'md2' => '1.2.840.113549.2.2', 'md4' => '1.2.840.113549.2.4', 'md5' => '1.2.840.113549.2.5', 'id-sha1' => '1.3.14.3.2.26', 'id-sha256' => '2.16.840.1.101.3.4.2.1', 'id-sha384' => '2.16.840.1.101.3.4.2.2', 'id-sha512' => '2.16.840.1.101.3.4.2.3', 'id-sha224' => '2.16.840.1.101.3.4.2.4', 'id-sha512/224' => '2.16.840.1.101.3.4.2.5', 'id-sha512/256' => '2.16.840.1.101.3.4.2.6', 'id-mgf1' => '1.2.840.113549.1.1.8' ]); self::$oidsLoaded = true; } } /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { self::initialize_static_variables(); if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } $components = ['isPublicKey' => strpos($key, 'PUBLIC') !== false]; $key = parent::load($key, $password); $type = isset($key['privateKey']) ? 'private' : 'public'; $result = $components + PKCS1::load($key[$type . 'Key']); if (isset($key[$type . 'KeyAlgorithm']['parameters'])) { $decoded = ASN1::decodeBER($key[$type . 'KeyAlgorithm']['parameters']); if ($decoded === false) { throw new \UnexpectedValueException('Unable to decode parameters'); } $params = ASN1::asn1map($decoded[0], Maps\RSASSA_PSS_params::MAP); } else { $params = []; } if (isset($params['maskGenAlgorithm']['parameters'])) { $decoded = ASN1::decodeBER($params['maskGenAlgorithm']['parameters']); if ($decoded === false) { throw new \UnexpectedValueException('Unable to decode parameters'); } $params['maskGenAlgorithm']['parameters'] = ASN1::asn1map($decoded[0], Maps\HashAlgorithm::MAP); } else { $params['maskGenAlgorithm'] = [ 'algorithm' => 'id-mgf1', 'parameters' => ['algorithm' => 'id-sha1'] ]; } if (!isset($params['hashAlgorithm']['algorithm'])) { $params['hashAlgorithm']['algorithm'] = 'id-sha1'; } $result['hash'] = str_replace('id-', '', $params['hashAlgorithm']['algorithm']); $result['MGFHash'] = str_replace('id-', '', $params['maskGenAlgorithm']['parameters']['algorithm']); if (isset($params['saltLength'])) { $result['saltLength'] = (int) $params['saltLength']->toString(); } if (isset($key['meta'])) { $result['meta'] = $key['meta']; } return $result; } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { self::initialize_static_variables(); $key = PKCS1::savePrivateKey($n, $e, $d, $primes, $exponents, $coefficients); $key = ASN1::extractBER($key); $params = self::savePSSParams($options); return self::wrapPrivateKey($key, [], $params, $password, null, '', $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @param array $options optional * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = []) { self::initialize_static_variables(); $key = PKCS1::savePublicKey($n, $e); $key = ASN1::extractBER($key); $params = self::savePSSParams($options); return self::wrapPublicKey($key, $params); } /** * Encodes PSS parameters * * @param array $options * @return string */ public static function savePSSParams(array $options) { /* The trailerField field is an integer. It provides compatibility with IEEE Std 1363a-2004 [P1363A]. The value MUST be 1, which represents the trailer field with hexadecimal value 0xBC. Other trailer fields, including the trailer field composed of HashID concatenated with 0xCC that is specified in IEEE Std 1363a, are not supported. Implementations that perform signature generation MUST omit the trailerField field, indicating that the default trailer field value was used. Implementations that perform signature validation MUST recognize both a present trailerField field with value 1 and an absent trailerField field. source: https://tools.ietf.org/html/rfc4055#page-9 */ $params = [ 'trailerField' => new BigInteger(1) ]; if (isset($options['hash'])) { $params['hashAlgorithm']['algorithm'] = 'id-' . $options['hash']; } if (isset($options['MGFHash'])) { $temp = ['algorithm' => 'id-' . $options['MGFHash']]; $temp = ASN1::encodeDER($temp, Maps\HashAlgorithm::MAP); $params['maskGenAlgorithm'] = [ 'algorithm' => 'id-mgf1', 'parameters' => new ASN1\Element($temp) ]; } if (isset($options['saltLength'])) { $params['saltLength'] = new BigInteger($options['saltLength']); } return new ASN1\Element(ASN1::encodeDER($params, Maps\RSASSA_PSS_params::MAP)); } } PK!Evendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PuTTY.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\Formats\Keys\PuTTY as Progenitor; use phpseclib3\Math\BigInteger; /** * PuTTY Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class PuTTY extends Progenitor { /** * Public Handler * * @var string */ const PUBLIC_HANDLER = 'phpseclib3\Crypt\RSA\Formats\Keys\OpenSSH'; /** * Algorithm Identifier * * @var array */ protected static $types = ['ssh-rsa']; /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { static $one; if (!isset($one)) { $one = new BigInteger(1); } $components = parent::load($key, $password); if (!isset($components['private'])) { return $components; } extract($components); unset($components['public'], $components['private']); $isPublicKey = false; $result = Strings::unpackSSH2('ii', $public); if ($result === false) { throw new \UnexpectedValueException('Key appears to be malformed'); } list($publicExponent, $modulus) = $result; $result = Strings::unpackSSH2('iiii', $private); if ($result === false) { throw new \UnexpectedValueException('Key appears to be malformed'); } $primes = $coefficients = []; list($privateExponent, $primes[1], $primes[2], $coefficients[2]) = $result; $temp = $primes[1]->subtract($one); $exponents = [1 => $publicExponent->modInverse($temp)]; $temp = $primes[2]->subtract($one); $exponents[] = $publicExponent->modInverse($temp); return compact('publicExponent', 'modulus', 'privateExponent', 'primes', 'coefficients', 'exponents', 'comment', 'isPublicKey'); } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { if (count($primes) != 2) { throw new \InvalidArgumentException('PuTTY does not support multi-prime RSA keys'); } $public = Strings::packSSH2('ii', $e, $n); $private = Strings::packSSH2('iiii', $d, $primes[1], $primes[2], $coefficients[2]); return self::wrapPrivateKey($public, $private, 'ssh-rsa', $password, $options); } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e) { return self::wrapPublicKey(Strings::packSSH2('ii', $e, $n), 'ssh-rsa'); } } PK!YkCvendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/Raw.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Math\BigInteger; /** * Raw RSA Key Handler * * @author Jim Wigginton */ abstract class Raw { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { if (!is_array($key)) { throw new \UnexpectedValueException('Key should be a array - not a ' . gettype($key)); } $key = array_change_key_case($key, CASE_LOWER); $components = ['isPublicKey' => false]; foreach (['e', 'exponent', 'publicexponent', 0, 'privateexponent', 'd'] as $index) { if (isset($key[$index])) { $components['publicExponent'] = $key[$index]; break; } } foreach (['n', 'modulo', 'modulus', 1] as $index) { if (isset($key[$index])) { $components['modulus'] = $key[$index]; break; } } if (!isset($components['publicExponent']) || !isset($components['modulus'])) { throw new \UnexpectedValueException('Modulus / exponent not present'); } if (isset($key['primes'])) { $components['primes'] = $key['primes']; } elseif (isset($key['p']) && isset($key['q'])) { $indices = [ ['p', 'q'], ['prime1', 'prime2'] ]; foreach ($indices as $index) { list($i0, $i1) = $index; if (isset($key[$i0]) && isset($key[$i1])) { $components['primes'] = [1 => $key[$i0], $key[$i1]]; } } } if (isset($key['exponents'])) { $components['exponents'] = $key['exponents']; } else { $indices = [ ['dp', 'dq'], ['exponent1', 'exponent2'] ]; foreach ($indices as $index) { list($i0, $i1) = $index; if (isset($key[$i0]) && isset($key[$i1])) { $components['exponents'] = [1 => $key[$i0], $key[$i1]]; } } } if (isset($key['coefficients'])) { $components['coefficients'] = $key['coefficients']; } else { foreach (['inverseq', 'q\'', 'coefficient'] as $index) { if (isset($key[$index])) { $components['coefficients'] = [2 => $key[$index]]; } } } if (!isset($components['primes'])) { $components['isPublicKey'] = true; return $components; } if (!isset($components['exponents'])) { $one = new BigInteger(1); $temp = $components['primes'][1]->subtract($one); $exponents = [1 => $components['publicExponent']->modInverse($temp)]; $temp = $components['primes'][2]->subtract($one); $exponents[] = $components['publicExponent']->modInverse($temp); $components['exponents'] = $exponents; } if (!isset($components['coefficients'])) { $components['coefficients'] = [2 => $components['primes'][2]->modInverse($components['primes'][1])]; } foreach (['privateexponent', 'd'] as $index) { if (isset($key[$index])) { $components['privateExponent'] = $key[$index]; break; } } return $components; } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @param array $options optional * @return array */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) { if (!empty($password) && is_string($password)) { throw new UnsupportedFormatException('Raw private keys do not support encryption'); } return [ 'e' => clone $e, 'n' => clone $n, 'd' => clone $d, 'primes' => array_map(function ($var) { return clone $var; }, $primes), 'exponents' => array_map(function ($var) { return clone $var; }, $exponents), 'coefficients' => array_map(function ($var) { return clone $var; }, $coefficients) ]; } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @return array */ public static function savePublicKey(BigInteger $n, BigInteger $e) { return ['e' => clone $e, 'n' => clone $n]; } } PK!W'ppCvendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/XML.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA\Formats\Keys; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\BadConfigurationException; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\Math\BigInteger; /** * XML Formatted RSA Key Handler * * @author Jim Wigginton */ abstract class XML { /** * Break a public or private key down into its constituent components * * @param string $key * @param string $password optional * @return array */ public static function load($key, $password = '') { if (!Strings::is_stringable($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } if (!class_exists('DOMDocument')) { throw new BadConfigurationException('The dom extension is not setup correctly on this system'); } $components = [ 'isPublicKey' => false, 'primes' => [], 'exponents' => [], 'coefficients' => [] ]; $use_errors = libxml_use_internal_errors(true); $dom = new \DOMDocument(); if (substr($key, 0, 5) != '' . $key . ''; } if (!$dom->loadXML($key)) { libxml_use_internal_errors($use_errors); throw new \UnexpectedValueException('Key does not appear to contain XML'); } $xpath = new \DOMXPath($dom); $keys = ['modulus', 'exponent', 'p', 'q', 'dp', 'dq', 'inverseq', 'd']; foreach ($keys as $key) { // $dom->getElementsByTagName($key) is case-sensitive $temp = $xpath->query("//*[translate(local-name(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$key']"); if (!$temp->length) { continue; } $value = new BigInteger(Strings::base64_decode($temp->item(0)->nodeValue), 256); switch ($key) { case 'modulus': $components['modulus'] = $value; break; case 'exponent': $components['publicExponent'] = $value; break; case 'p': $components['primes'][1] = $value; break; case 'q': $components['primes'][2] = $value; break; case 'dp': $components['exponents'][1] = $value; break; case 'dq': $components['exponents'][2] = $value; break; case 'inverseq': $components['coefficients'][2] = $value; break; case 'd': $components['privateExponent'] = $value; } } libxml_use_internal_errors($use_errors); foreach ($components as $key => $value) { if (is_array($value) && !count($value)) { unset($components[$key]); } } if (isset($components['modulus']) && isset($components['publicExponent'])) { if (count($components) == 3) { $components['isPublicKey'] = true; } return $components; } throw new \UnexpectedValueException('Modulus / exponent not present'); } /** * Convert a private key to the appropriate format. * * @param BigInteger $n * @param BigInteger $e * @param BigInteger $d * @param array $primes * @param array $exponents * @param array $coefficients * @param string $password optional * @return string */ public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '') { if (count($primes) != 2) { throw new \InvalidArgumentException('XML does not support multi-prime RSA keys'); } if (!empty($password) && is_string($password)) { throw new UnsupportedFormatException('XML private keys do not support encryption'); } return "\r\n" . ' ' . Strings::base64_encode($n->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($e->toBytes()) . "\r\n" . '

' . Strings::base64_encode($primes[1]->toBytes()) . "

\r\n" . ' ' . Strings::base64_encode($primes[2]->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($exponents[1]->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($exponents[2]->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($coefficients[2]->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($d->toBytes()) . "\r\n" . '
'; } /** * Convert a public key to the appropriate format * * @param BigInteger $n * @param BigInteger $e * @return string */ public static function savePublicKey(BigInteger $n, BigInteger $e) { return "\r\n" . ' ' . Strings::base64_encode($n->toBytes()) . "\r\n" . ' ' . Strings::base64_encode($e->toBytes()) . "\r\n" . ''; } } PK!z.`>>=vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PrivateKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA\Formats\Keys\PSS; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\Math\BigInteger; /** * Raw RSA Key Handler * * @author Jim Wigginton */ final class PrivateKey extends RSA implements Common\PrivateKey { use Common\Traits\PasswordProtected; /** * Primes for Chinese Remainder Theorem (ie. p and q) * * @var array */ protected $primes; /** * Exponents for Chinese Remainder Theorem (ie. dP and dQ) * * @var array */ protected $exponents; /** * Coefficients for Chinese Remainder Theorem (ie. qInv) * * @var array */ protected $coefficients; /** * Private Exponent * * @var BigInteger */ protected $privateExponent; /** * RSADP * * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.2 RFC3447#section-5.1.2}. * * @return bool|BigInteger */ private function rsadp(BigInteger $c) { if ($c->compare(self::$zero) < 0 || $c->compare($this->modulus) > 0) { throw new \OutOfRangeException('Ciphertext representative out of range'); } return $this->exponentiate($c); } /** * RSASP1 * * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.1 RFC3447#section-5.2.1}. * * @return bool|BigInteger */ private function rsasp1(BigInteger $m) { if ($m->compare(self::$zero) < 0 || $m->compare($this->modulus) > 0) { throw new \OutOfRangeException('Signature representative out of range'); } return $this->exponentiate($m); } /** * Exponentiate * * @param BigInteger $x * @return BigInteger */ protected function exponentiate(BigInteger $x) { switch (true) { case empty($this->primes): case $this->primes[1]->equals(self::$zero): case empty($this->coefficients): case $this->coefficients[2]->equals(self::$zero): case empty($this->exponents): case $this->exponents[1]->equals(self::$zero): return $x->modPow($this->exponent, $this->modulus); } $num_primes = count($this->primes); if (!static::$enableBlinding) { $m_i = [ 1 => $x->modPow($this->exponents[1], $this->primes[1]), 2 => $x->modPow($this->exponents[2], $this->primes[2]) ]; $h = $m_i[1]->subtract($m_i[2]); $h = $h->multiply($this->coefficients[2]); list(, $h) = $h->divide($this->primes[1]); $m = $m_i[2]->add($h->multiply($this->primes[2])); $r = $this->primes[1]; for ($i = 3; $i <= $num_primes; $i++) { $m_i = $x->modPow($this->exponents[$i], $this->primes[$i]); $r = $r->multiply($this->primes[$i - 1]); $h = $m_i->subtract($m); $h = $h->multiply($this->coefficients[$i]); list(, $h) = $h->divide($this->primes[$i]); $m = $m->add($r->multiply($h)); } } else { $smallest = $this->primes[1]; for ($i = 2; $i <= $num_primes; $i++) { if ($smallest->compare($this->primes[$i]) > 0) { $smallest = $this->primes[$i]; } } $r = BigInteger::randomRange(self::$one, $smallest->subtract(self::$one)); $m_i = [ 1 => $this->blind($x, $r, 1), 2 => $this->blind($x, $r, 2) ]; $h = $m_i[1]->subtract($m_i[2]); $h = $h->multiply($this->coefficients[2]); list(, $h) = $h->divide($this->primes[1]); $m = $m_i[2]->add($h->multiply($this->primes[2])); $r = $this->primes[1]; for ($i = 3; $i <= $num_primes; $i++) { $m_i = $this->blind($x, $r, $i); $r = $r->multiply($this->primes[$i - 1]); $h = $m_i->subtract($m); $h = $h->multiply($this->coefficients[$i]); list(, $h) = $h->divide($this->primes[$i]); $m = $m->add($r->multiply($h)); } } return $m; } /** * Performs RSA Blinding * * Protects against timing attacks by employing RSA Blinding. * Returns $x->modPow($this->exponents[$i], $this->primes[$i]) * * @param BigInteger $x * @param BigInteger $r * @param int $i * @return BigInteger */ private function blind(BigInteger $x, BigInteger $r, $i) { $x = $x->multiply($r->modPow($this->publicExponent, $this->primes[$i])); $x = $x->modPow($this->exponents[$i], $this->primes[$i]); $r = $r->modInverse($this->primes[$i]); $x = $x->multiply($r); list(, $x) = $x->divide($this->primes[$i]); return $x; } /** * EMSA-PSS-ENCODE * * See {@link http://tools.ietf.org/html/rfc3447#section-9.1.1 RFC3447#section-9.1.1}. * * @return string * @param string $m * @throws \RuntimeException on encoding error * @param int $emBits */ private function emsa_pss_encode($m, $emBits) { // if $m is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error // be output. $emLen = ($emBits + 1) >> 3; // ie. ceil($emBits / 8) $sLen = $this->sLen !== null ? $this->sLen : $this->hLen; $mHash = $this->hash->hash($m); if ($emLen < $this->hLen + $sLen + 2) { throw new \LengthException('RSA modulus too short'); } $salt = Random::string($sLen); $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; $h = $this->hash->hash($m2); $ps = str_repeat(chr(0), $emLen - $sLen - $this->hLen - 2); $db = $ps . chr(1) . $salt; $dbMask = $this->mgf1($h, $emLen - $this->hLen - 1); // ie. stlren($db) $maskedDB = $db ^ $dbMask; $maskedDB[0] = ~chr(0xFF << ($emBits & 7)) & $maskedDB[0]; $em = $maskedDB . $h . chr(0xBC); return $em; } /** * RSASSA-PSS-SIGN * * See {@link http://tools.ietf.org/html/rfc3447#section-8.1.1 RFC3447#section-8.1.1}. * * @param string $m * @return bool|string */ private function rsassa_pss_sign($m) { // EMSA-PSS encoding $em = $this->emsa_pss_encode($m, 8 * $this->k - 1); // RSA signature $m = $this->os2ip($em); $s = $this->rsasp1($m); $s = $this->i2osp($s, $this->k); // Output the signature S return $s; } /** * RSASSA-PKCS1-V1_5-SIGN * * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.1 RFC3447#section-8.2.1}. * * @param string $m * @throws \LengthException if the RSA modulus is too short * @return bool|string */ private function rsassa_pkcs1_v1_5_sign($m) { // EMSA-PKCS1-v1_5 encoding // If the encoding operation outputs "intended encoded message length too short," output "RSA modulus // too short" and stop. try { $em = $this->emsa_pkcs1_v1_5_encode($m, $this->k); } catch (\LengthException $e) { throw new \LengthException('RSA modulus too short'); } // RSA signature $m = $this->os2ip($em); $s = $this->rsasp1($m); $s = $this->i2osp($s, $this->k); // Output the signature S return $s; } /** * Create a signature * * @see self::verify() * @param string $message * @return string */ public function sign($message) { switch ($this->signaturePadding) { case self::SIGNATURE_PKCS1: case self::SIGNATURE_RELAXED_PKCS1: return $this->rsassa_pkcs1_v1_5_sign($message); //case self::SIGNATURE_PSS: default: return $this->rsassa_pss_sign($message); } } /** * RSAES-PKCS1-V1_5-DECRYPT * * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.2 RFC3447#section-7.2.2}. * * @param string $c * @return bool|string */ private function rsaes_pkcs1_v1_5_decrypt($c) { // Length checking if (strlen($c) != $this->k) { // or if k < 11 throw new \LengthException('Ciphertext representative too long'); } // RSA decryption $c = $this->os2ip($c); $m = $this->rsadp($c); $em = $this->i2osp($m, $this->k); // EME-PKCS1-v1_5 decoding if (ord($em[0]) != 0 || ord($em[1]) > 2) { throw new \RuntimeException('Decryption error'); } $ps = substr($em, 2, strpos($em, chr(0), 2) - 2); $m = substr($em, strlen($ps) + 3); if (strlen($ps) < 8) { throw new \RuntimeException('Decryption error'); } // Output M return $m; } /** * RSAES-OAEP-DECRYPT * * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.2 RFC3447#section-7.1.2}. The fact that the error * messages aren't distinguishable from one another hinders debugging, but, to quote from RFC3447#section-7.1.2: * * Note. Care must be taken to ensure that an opponent cannot * distinguish the different error conditions in Step 3.g, whether by * error message or timing, or, more generally, learn partial * information about the encoded message EM. Otherwise an opponent may * be able to obtain useful information about the decryption of the * ciphertext C, leading to a chosen-ciphertext attack such as the one * observed by Manger [36]. * * @param string $c * @return bool|string */ private function rsaes_oaep_decrypt($c) { // Length checking // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error // be output. if (strlen($c) != $this->k || $this->k < 2 * $this->hLen + 2) { throw new \LengthException('Ciphertext representative too long'); } // RSA decryption $c = $this->os2ip($c); $m = $this->rsadp($c); $em = $this->i2osp($m, $this->k); // EME-OAEP decoding $lHash = $this->hash->hash($this->label); $y = ord($em[0]); $maskedSeed = substr($em, 1, $this->hLen); $maskedDB = substr($em, $this->hLen + 1); $seedMask = $this->mgf1($maskedDB, $this->hLen); $seed = $maskedSeed ^ $seedMask; $dbMask = $this->mgf1($seed, $this->k - $this->hLen - 1); $db = $maskedDB ^ $dbMask; $lHash2 = substr($db, 0, $this->hLen); $m = substr($db, $this->hLen); $hashesMatch = hash_equals($lHash, $lHash2); $leadingZeros = 1; $patternMatch = 0; $offset = 0; for ($i = 0; $i < strlen($m); $i++) { $patternMatch |= $leadingZeros & ($m[$i] === "\1"); $leadingZeros &= $m[$i] === "\0"; $offset += $patternMatch ? 0 : 1; } // we do | instead of || to avoid https://en.wikipedia.org/wiki/Short-circuit_evaluation // to protect against timing attacks if (!$hashesMatch | !$patternMatch) { throw new \RuntimeException('Decryption error'); } // Output the message M return substr($m, $offset + 1); } /** * Raw Encryption / Decryption * * Doesn't use padding and is not recommended. * * @param string $m * @return bool|string * @throws \LengthException if strlen($m) > $this->k */ private function raw_encrypt($m) { if (strlen($m) > $this->k) { throw new \LengthException('Ciphertext representative too long'); } $temp = $this->os2ip($m); $temp = $this->rsadp($temp); return $this->i2osp($temp, $this->k); } /** * Decryption * * @see self::encrypt() * @param string $ciphertext * @return bool|string */ public function decrypt($ciphertext) { switch ($this->encryptionPadding) { case self::ENCRYPTION_NONE: return $this->raw_encrypt($ciphertext); case self::ENCRYPTION_PKCS1: return $this->rsaes_pkcs1_v1_5_decrypt($ciphertext); //case self::ENCRYPTION_OAEP: default: return $this->rsaes_oaep_decrypt($ciphertext); } } /** * Returns the public key * * @return mixed */ public function getPublicKey() { $type = self::validatePlugin('Keys', 'PKCS8', 'savePublicKey'); if (empty($this->modulus) || empty($this->publicExponent)) { throw new \RuntimeException('Public key components not found'); } $key = $type::savePublicKey($this->modulus, $this->publicExponent); return RSA::loadFormat('PKCS8', $key) ->withHash($this->hash->getHash()) ->withMGFHash($this->mgfHash->getHash()) ->withSaltLength($this->sLen) ->withLabel($this->label) ->withPadding($this->signaturePadding | $this->encryptionPadding); } /** * Returns the private key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { $type = self::validatePlugin( 'Keys', $type, empty($this->primes) ? 'savePublicKey' : 'savePrivateKey' ); if ($type == PSS::class) { if ($this->signaturePadding == self::SIGNATURE_PSS) { $options += [ 'hash' => $this->hash->getHash(), 'MGFHash' => $this->mgfHash->getHash(), 'saltLength' => $this->getSaltLength() ]; } else { throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS'); } } if (empty($this->primes)) { return $type::savePublicKey($this->modulus, $this->exponent, $options); } return $type::savePrivateKey($this->modulus, $this->publicExponent, $this->exponent, $this->primes, $this->exponents, $this->coefficients, $this->password, $options); /* $key = $type::savePrivateKey($this->modulus, $this->publicExponent, $this->exponent, $this->primes, $this->exponents, $this->coefficients, $this->password, $options); if ($key !== false || count($this->primes) == 2) { return $key; } $nSize = $this->getSize() >> 1; $primes = [1 => clone self::$one, clone self::$one]; $i = 1; foreach ($this->primes as $prime) { $primes[$i] = $primes[$i]->multiply($prime); if ($primes[$i]->getLength() >= $nSize) { $i++; } } $exponents = []; $coefficients = [2 => $primes[2]->modInverse($primes[1])]; foreach ($primes as $i => $prime) { $temp = $prime->subtract(self::$one); $exponents[$i] = $this->modulus->modInverse($temp); } return $type::savePrivateKey($this->modulus, $this->publicExponent, $this->exponent, $primes, $exponents, $coefficients, $this->password, $options); */ } } PK!7PJ<<<vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PublicKey.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt\RSA; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA\Formats\Keys\PSS; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Exception\UnsupportedFormatException; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps\DigestInfo; use phpseclib3\Math\BigInteger; /** * Raw RSA Key Handler * * @author Jim Wigginton */ final class PublicKey extends RSA implements Common\PublicKey { use Common\Traits\Fingerprint; /** * Exponentiate * * @param BigInteger $x * @return BigInteger */ private function exponentiate(BigInteger $x) { return $x->modPow($this->exponent, $this->modulus); } /** * RSAVP1 * * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.2 RFC3447#section-5.2.2}. * * @param BigInteger $s * @return bool|BigInteger */ private function rsavp1($s) { if ($s->compare(self::$zero) < 0 || $s->compare($this->modulus) > 0) { return false; } return $this->exponentiate($s); } /** * RSASSA-PKCS1-V1_5-VERIFY * * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.2 RFC3447#section-8.2.2}. * * @param string $m * @param string $s * @throws \LengthException if the RSA modulus is too short * @return bool */ private function rsassa_pkcs1_v1_5_verify($m, $s) { // Length checking if (strlen($s) != $this->k) { return false; } // RSA verification $s = $this->os2ip($s); $m2 = $this->rsavp1($s); if ($m2 === false) { return false; } $em = $this->i2osp($m2, $this->k); if ($em === false) { return false; } // EMSA-PKCS1-v1_5 encoding $exception = false; // If the encoding operation outputs "intended encoded message length too short," output "RSA modulus // too short" and stop. try { $em2 = $this->emsa_pkcs1_v1_5_encode($m, $this->k); $r1 = hash_equals($em, $em2); } catch (\LengthException $e) { $exception = true; } try { $em3 = $this->emsa_pkcs1_v1_5_encode_without_null($m, $this->k); $r2 = hash_equals($em, $em3); } catch (\LengthException $e) { $exception = true; } catch (UnsupportedAlgorithmException $e) { $r2 = false; } if ($exception) { throw new \LengthException('RSA modulus too short'); } // Compare return $r1 || $r2; } /** * RSASSA-PKCS1-V1_5-VERIFY (relaxed matching) * * Per {@link http://tools.ietf.org/html/rfc3447#page-43 RFC3447#page-43} PKCS1 v1.5 * specified the use BER encoding rather than DER encoding that PKCS1 v2.0 specified. * This means that under rare conditions you can have a perfectly valid v1.5 signature * that fails to validate with _rsassa_pkcs1_v1_5_verify(). PKCS1 v2.1 also recommends * that if you're going to validate these types of signatures you "should indicate * whether the underlying BER encoding is a DER encoding and hence whether the signature * is valid with respect to the specification given in [PKCS1 v2.0+]". so if you do * $rsa->getLastPadding() and get RSA::PADDING_RELAXED_PKCS1 back instead of * RSA::PADDING_PKCS1... that means BER encoding was used. * * @param string $m * @param string $s * @return bool */ private function rsassa_pkcs1_v1_5_relaxed_verify($m, $s) { // Length checking if (strlen($s) != $this->k) { return false; } // RSA verification $s = $this->os2ip($s); $m2 = $this->rsavp1($s); if ($m2 === false) { return false; } $em = $this->i2osp($m2, $this->k); if ($em === false) { return false; } if (Strings::shift($em, 2) != "\0\1") { return false; } $em = ltrim($em, "\xFF"); if (Strings::shift($em) != "\0") { return false; } $decoded = ASN1::decodeBER($em); if (!is_array($decoded) || empty($decoded[0]) || strlen($em) > $decoded[0]['length']) { return false; } static $oids; if (!isset($oids)) { $oids = [ 'md2' => '1.2.840.113549.2.2', 'md4' => '1.2.840.113549.2.4', // from PKCS1 v1.5 'md5' => '1.2.840.113549.2.5', 'id-sha1' => '1.3.14.3.2.26', 'id-sha256' => '2.16.840.1.101.3.4.2.1', 'id-sha384' => '2.16.840.1.101.3.4.2.2', 'id-sha512' => '2.16.840.1.101.3.4.2.3', // from PKCS1 v2.2 'id-sha224' => '2.16.840.1.101.3.4.2.4', 'id-sha512/224' => '2.16.840.1.101.3.4.2.5', 'id-sha512/256' => '2.16.840.1.101.3.4.2.6', ]; ASN1::loadOIDs($oids); } $decoded = ASN1::asn1map($decoded[0], DigestInfo::MAP); if (!isset($decoded) || $decoded === false) { return false; } if (!isset($oids[$decoded['digestAlgorithm']['algorithm']])) { return false; } if (isset($decoded['digestAlgorithm']['parameters']) && $decoded['digestAlgorithm']['parameters'] !== ['null' => '']) { return false; } $hash = $decoded['digestAlgorithm']['algorithm']; $hash = substr($hash, 0, 3) == 'id-' ? substr($hash, 3) : $hash; $hash = new Hash($hash); $em = $hash->hash($m); $em2 = $decoded['digest']; return hash_equals($em, $em2); } /** * EMSA-PSS-VERIFY * * See {@link http://tools.ietf.org/html/rfc3447#section-9.1.2 RFC3447#section-9.1.2}. * * @param string $m * @param string $em * @param int $emBits * @return string */ private function emsa_pss_verify($m, $em, $emBits) { // if $m is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error // be output. $emLen = ($emBits + 7) >> 3; // ie. ceil($emBits / 8); $sLen = $this->sLen !== null ? $this->sLen : $this->hLen; $mHash = $this->hash->hash($m); if ($emLen < $this->hLen + $sLen + 2) { return false; } if ($em[strlen($em) - 1] != chr(0xBC)) { return false; } $maskedDB = substr($em, 0, -$this->hLen - 1); $h = substr($em, -$this->hLen - 1, $this->hLen); $temp = chr(0xFF << ($emBits & 7)); if ((~$maskedDB[0] & $temp) != $temp) { return false; } $dbMask = $this->mgf1($h, $emLen - $this->hLen - 1); $db = $maskedDB ^ $dbMask; $db[0] = ~chr(0xFF << ($emBits & 7)) & $db[0]; $temp = $emLen - $this->hLen - $sLen - 2; if (substr($db, 0, $temp) != str_repeat(chr(0), $temp) || ord($db[$temp]) != 1) { return false; } $salt = substr($db, $temp + 1); // should be $sLen long $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; $h2 = $this->hash->hash($m2); return hash_equals($h, $h2); } /** * RSASSA-PSS-VERIFY * * See {@link http://tools.ietf.org/html/rfc3447#section-8.1.2 RFC3447#section-8.1.2}. * * @param string $m * @param string $s * @return bool|string */ private function rsassa_pss_verify($m, $s) { // Length checking if (strlen($s) != $this->k) { return false; } // RSA verification $modBits = strlen($this->modulus->toBits()); $s2 = $this->os2ip($s); $m2 = $this->rsavp1($s2); $em = $this->i2osp($m2, $this->k); if ($em === false) { return false; } // EMSA-PSS verification return $this->emsa_pss_verify($m, $em, $modBits - 1); } /** * Verifies a signature * * @see self::sign() * @param string $message * @param string $signature * @return bool */ public function verify($message, $signature) { switch ($this->signaturePadding) { case self::SIGNATURE_RELAXED_PKCS1: return $this->rsassa_pkcs1_v1_5_relaxed_verify($message, $signature); case self::SIGNATURE_PKCS1: return $this->rsassa_pkcs1_v1_5_verify($message, $signature); //case self::SIGNATURE_PSS: default: return $this->rsassa_pss_verify($message, $signature); } } /** * RSAES-PKCS1-V1_5-ENCRYPT * * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.1 RFC3447#section-7.2.1}. * * @param string $m * @param bool $pkcs15_compat optional * @throws \LengthException if strlen($m) > $this->k - 11 * @return bool|string */ private function rsaes_pkcs1_v1_5_encrypt($m, $pkcs15_compat = false) { $mLen = strlen($m); // Length checking if ($mLen > $this->k - 11) { throw new \LengthException('Message too long'); } // EME-PKCS1-v1_5 encoding $psLen = $this->k - $mLen - 3; $ps = ''; while (strlen($ps) != $psLen) { $temp = Random::string($psLen - strlen($ps)); $temp = str_replace("\x00", '', $temp); $ps .= $temp; } $type = 2; $em = chr(0) . chr($type) . $ps . chr(0) . $m; // RSA encryption $m = $this->os2ip($em); $c = $this->rsaep($m); $c = $this->i2osp($c, $this->k); // Output the ciphertext C return $c; } /** * RSAES-OAEP-ENCRYPT * * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.1 RFC3447#section-7.1.1} and * {http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding OAES}. * * @param string $m * @throws \LengthException if strlen($m) > $this->k - 2 * $this->hLen - 2 * @return string */ private function rsaes_oaep_encrypt($m) { $mLen = strlen($m); // Length checking // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error // be output. if ($mLen > $this->k - 2 * $this->hLen - 2) { throw new \LengthException('Message too long'); } // EME-OAEP encoding $lHash = $this->hash->hash($this->label); $ps = str_repeat(chr(0), $this->k - $mLen - 2 * $this->hLen - 2); $db = $lHash . $ps . chr(1) . $m; $seed = Random::string($this->hLen); $dbMask = $this->mgf1($seed, $this->k - $this->hLen - 1); $maskedDB = $db ^ $dbMask; $seedMask = $this->mgf1($maskedDB, $this->hLen); $maskedSeed = $seed ^ $seedMask; $em = chr(0) . $maskedSeed . $maskedDB; // RSA encryption $m = $this->os2ip($em); $c = $this->rsaep($m); $c = $this->i2osp($c, $this->k); // Output the ciphertext C return $c; } /** * RSAEP * * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.1}. * * @param BigInteger $m * @return bool|BigInteger */ private function rsaep($m) { if ($m->compare(self::$zero) < 0 || $m->compare($this->modulus) > 0) { throw new \OutOfRangeException('Message representative out of range'); } return $this->exponentiate($m); } /** * Raw Encryption / Decryption * * Doesn't use padding and is not recommended. * * @param string $m * @return bool|string * @throws \LengthException if strlen($m) > $this->k */ private function raw_encrypt($m) { if (strlen($m) > $this->k) { throw new \LengthException('Message too long'); } $temp = $this->os2ip($m); $temp = $this->rsaep($temp); return $this->i2osp($temp, $this->k); } /** * Encryption * * Both self::PADDING_OAEP and self::PADDING_PKCS1 both place limits on how long $plaintext can be. * If $plaintext exceeds those limits it will be broken up so that it does and the resultant ciphertext's will * be concatenated together. * * @see self::decrypt() * @param string $plaintext * @return bool|string * @throws \LengthException if the RSA modulus is too short */ public function encrypt($plaintext) { switch ($this->encryptionPadding) { case self::ENCRYPTION_NONE: return $this->raw_encrypt($plaintext); case self::ENCRYPTION_PKCS1: return $this->rsaes_pkcs1_v1_5_encrypt($plaintext); //case self::ENCRYPTION_OAEP: default: return $this->rsaes_oaep_encrypt($plaintext); } } /** * Returns the public key * * The public key is only returned under two circumstances - if the private key had the public key embedded within it * or if the public key was set via setPublicKey(). If the currently loaded key is supposed to be the public key this * function won't return it since this library, for the most part, doesn't distinguish between public and private keys. * * @param string $type * @param array $options optional * @return mixed */ public function toString($type, array $options = []) { $type = self::validatePlugin('Keys', $type, 'savePublicKey'); if ($type == PSS::class) { if ($this->signaturePadding == self::SIGNATURE_PSS) { $options += [ 'hash' => $this->hash->getHash(), 'MGFHash' => $this->mgfHash->getHash(), 'saltLength' => $this->getSaltLength() ]; } else { throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS'); } } return $type::savePublicKey($this->modulus, $this->publicExponent, $options); } /** * Converts a public key to a private key * * @return RSA */ public function asPrivateKey() { $new = new PrivateKey(); $new->exponent = $this->exponent; $new->modulus = $this->modulus; $new->k = $this->k; $new->format = $this->format; return $new ->withHash($this->hash->getHash()) ->withMGFHash($this->mgfHash->getHash()) ->withSaltLength($this->sLen) ->withLabel($this->label) ->withPadding($this->signaturePadding | $this->encryptionPadding); } } PK! 2vendor/phpseclib/phpseclib/phpseclib/Crypt/AES.phpnu[ * setKey('abcdefghijklmnop'); * * $size = 10 * 1024; * $plaintext = ''; * for ($i = 0; $i < $size; $i++) { * $plaintext.= 'a'; * } * * echo $aes->decrypt($aes->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @copyright 2008 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; /** * Pure-PHP implementation of AES. * * @author Jim Wigginton */ class AES extends Rijndael { /** * Dummy function * * Since \phpseclib3\Crypt\AES extends \phpseclib3\Crypt\Rijndael, this function is, technically, available, but it doesn't do anything. * * @see \phpseclib3\Crypt\Rijndael::setBlockLength() * @param int $length * @throws \BadMethodCallException anytime it's called */ public function setBlockLength($length) { throw new \BadMethodCallException('The block length cannot be set for AES.'); } /** * Sets the key length * * Valid key lengths are 128, 192, and 256. Set the link to bool(false) to disable a fixed key length * * @see \phpseclib3\Crypt\Rijndael:setKeyLength() * @param int $length * @throws \LengthException if the key length isn't supported */ public function setKeyLength($length) { switch ($length) { case 128: case 192: case 256: break; default: throw new \LengthException('Key of size ' . $length . ' not supported by this algorithm. Only keys of sizes 128, 192 or 256 supported'); } parent::setKeyLength($length); } /** * Sets the key. * * Rijndael supports five different key lengths, AES only supports three. * * @see \phpseclib3\Crypt\Rijndael:setKey() * @see setKeyLength() * @param string $key * @throws \LengthException if the key length isn't supported */ public function setKey($key) { switch (strlen($key)) { case 16: case 24: case 32: break; default: throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes 16, 24 or 32 supported'); } parent::setKey($key); } } PK!%3TT7vendor/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.phpnu[ unpack('N*', $x), $blocks); it jumps up by an additional * ~90MB, yielding a 106x increase in memory usage. Consequently, it bcrypt calls a different * _encryptBlock() then the regular Blowfish does. That said, the Blowfish _encryptBlock() is * basically just a thin wrapper around the bcrypt _encryptBlock(), so there's that. * * This explains 3 of the 4 _encryptBlock() implementations. the last _encryptBlock() * implementation can best be understood by doing Ctrl + F and searching for where * self::$use_reg_intval is defined. * * # phpseclib's three different _setupKey() implementations * * Every bcrypt round is the equivalent of encrypting 512KB of data. Since OpenSSH uses 16 * rounds by default that's ~8MB of data that's essentially being encrypted whenever * you use bcrypt. That's a lot of data, however, bcrypt operates within tighter constraints * than regular Blowfish, so we can use that to our advantage. In particular, whereas Blowfish * supports variable length keys, in bcrypt, the initial "key" is the sha512 hash of the * password. sha512 hashes are 512 bits or 64 bytes long and thus the bcrypt keys are of a * fixed length whereas Blowfish keys are not of a fixed length. * * bcrypt actually has two different key expansion steps. The first one (expandstate) is * constantly XOR'ing every _encryptBlock() parameter against the salt prior _encryptBlock()'s * being called. The second one (expand0state) is more similar to Blowfish's _setupKey() * but it can still use the fixed length key optimization discussed above and can do away with * the pack() / unpack() calls. * * I suppose _setupKey() could be made to be a thin wrapper around expandstate() but idk it's * just a lot of work for very marginal benefits as _setupKey() is only called once for * regular Blowfish vs the 128 times it's called --per round-- with bcrypt. * * # blowfish + bcrypt in the same class * * Altho there's a lot of Blowfish code that bcrypt doesn't re-use, bcrypt does re-use the * initial S-boxes, the initial P-array and the int-only _encryptBlock() implementation. * * # Credit * * phpseclib's bcrypt implementation is based losely off of OpenSSH's implementation: * * https://github.com/openssh/openssh-portable/blob/master/openbsd-compat/bcrypt_pbkdf.c * * Here's a short example of how to use this library: * * setKey('12345678901234567890123456789012'); * * $plaintext = str_repeat('a', 1024); * * echo $blowfish->decrypt($blowfish->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @author Hans-Juergen Petrich * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\BlockCipher; /** * Pure-PHP implementation of Blowfish. * * @author Jim Wigginton * @author Hans-Juergen Petrich */ class Blowfish extends BlockCipher { /** * Block Length of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::block_size * @var int */ protected $block_size = 8; /** * The mcrypt specific name of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @var string */ protected $cipher_name_mcrypt = 'blowfish'; /** * Optimizing value while CFB-encrypting * * @see \phpseclib3\Crypt\Common\SymmetricKey::cfb_init_len * @var int */ protected $cfb_init_len = 500; /** * The fixed subkeys boxes * * S-Box * * @var array */ private static $sbox = [ 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 ]; /** * P-Array consists of 18 32-bit subkeys * * @var array */ private static $parray = [ 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b ]; /** * The BCTX-working Array * * Holds the expanded key [p] and the key-depended s-boxes [sb] * * @var array */ private $bctx; /** * Holds the last used key * * @var array */ private $kl; /** * The Key Length (in bytes) * {@internal The max value is 256 / 8 = 32, the min value is 128 / 8 = 16. Exists in conjunction with $Nk * because the encryption / decryption / key schedule creation requires this number and not $key_length. We could * derive this from $key_length or vice versa, but that'd mean we'd have to do multiple shift operations, so in lieu * of that, we'll just precompute it once.} * * @see \phpseclib3\Crypt\Common\SymmetricKey::setKeyLength() * @var int */ protected $key_length = 16; /** * Default Constructor. * * @param string $mode * @throws \InvalidArgumentException if an invalid / unsupported mode is provided */ public function __construct($mode) { parent::__construct($mode); if ($this->mode == self::MODE_STREAM) { throw new \InvalidArgumentException('Block ciphers cannot be ran in stream mode'); } } /** * Sets the key length. * * Key lengths can be between 32 and 448 bits. * * @param int $length */ public function setKeyLength($length) { if ($length < 32 || $length > 448) { throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys of sizes between 32 and 448 bits are supported'); } $this->key_length = $length >> 3; parent::setKeyLength($length); } /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { if ($engine == self::ENGINE_OPENSSL) { if ($this->key_length < 16) { return false; } // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { return false; } $this->cipher_name_openssl_ecb = 'bf-ecb'; $this->cipher_name_openssl = 'bf-' . $this->openssl_translate_mode(); } return parent::isValidEngineHelper($engine); } /** * Setup the key (expansion) * * @see \phpseclib3\Crypt\Common\SymmetricKey::_setupKey() */ protected function setupKey() { if (isset($this->kl['key']) && $this->key === $this->kl['key']) { // already expanded return; } $this->kl = ['key' => $this->key]; /* key-expanding p[] and S-Box building sb[] */ $this->bctx = [ 'p' => [], 'sb' => self::$sbox ]; // unpack binary string in unsigned chars $key = array_values(unpack('C*', $this->key)); $keyl = count($key); // with bcrypt $keyl will always be 16 (because the key is the sha512 of the key you provide) for ($j = 0, $i = 0; $i < 18; ++$i) { // xor P1 with the first 32-bits of the key, xor P2 with the second 32-bits ... for ($data = 0, $k = 0; $k < 4; ++$k) { $data = ($data << 8) | $key[$j]; if (++$j >= $keyl) { $j = 0; } } $this->bctx['p'][] = self::$parray[$i] ^ intval($data); } // encrypt the zero-string, replace P1 and P2 with the encrypted data, // encrypt P3 and P4 with the new P1 and P2, do it with all P-array and subkeys $data = "\0\0\0\0\0\0\0\0"; for ($i = 0; $i < 18; $i += 2) { list($l, $r) = array_values(unpack('N*', $data = $this->encryptBlock($data))); $this->bctx['p'][$i ] = $l; $this->bctx['p'][$i + 1] = $r; } for ($i = 0; $i < 0x400; $i += 0x100) { for ($j = 0; $j < 256; $j += 2) { list($l, $r) = array_values(unpack('N*', $data = $this->encryptBlock($data))); $this->bctx['sb'][$i | $j] = $l; $this->bctx['sb'][$i | ($j + 1)] = $r; } } } /** * Initialize Static Variables */ protected static function initialize_static_variables() { if (is_float(self::$sbox[0x200])) { self::$sbox = array_map('intval', self::$sbox); self::$parray = array_map('intval', self::$parray); } parent::initialize_static_variables(); } /** * bcrypt * * @param string $sha2pass * @param string $sha2salt * @access private * @return string */ private static function bcrypt_hash($sha2pass, $sha2salt) { $p = self::$parray; $sbox = self::$sbox; $cdata = array_values(unpack('N*', 'OxychromaticBlowfishSwatDynamite')); $sha2pass = array_values(unpack('N*', $sha2pass)); $sha2salt = array_values(unpack('N*', $sha2salt)); self::expandstate($sha2salt, $sha2pass, $sbox, $p); for ($i = 0; $i < 64; $i++) { self::expand0state($sha2salt, $sbox, $p); self::expand0state($sha2pass, $sbox, $p); } for ($i = 0; $i < 64; $i++) { for ($j = 0; $j < 8; $j += 2) { // count($cdata) == 8 list($cdata[$j], $cdata[$j + 1]) = self::encryptBlockHelperFast($cdata[$j], $cdata[$j + 1], $sbox, $p); } } return pack('V*', ...$cdata); } /** * Performs OpenSSH-style bcrypt * * @param string $pass * @param string $salt * @param int $keylen * @param int $rounds * @access public * @return string */ public static function bcrypt_pbkdf($pass, $salt, $keylen, $rounds) { self::initialize_static_variables(); if (PHP_INT_SIZE == 4) { throw new \RuntimeException('bcrypt is far too slow to be practical on 32-bit versions of PHP'); } $sha2pass = hash('sha512', $pass, true); $results = []; $count = 1; while (32 * count($results) < $keylen) { $countsalt = $salt . pack('N', $count++); $sha2salt = hash('sha512', $countsalt, true); $out = $tmpout = self::bcrypt_hash($sha2pass, $sha2salt); for ($i = 1; $i < $rounds; $i++) { $sha2salt = hash('sha512', $tmpout, true); $tmpout = self::bcrypt_hash($sha2pass, $sha2salt); $out ^= $tmpout; } $results[] = $out; } $output = ''; for ($i = 0; $i < 32; $i++) { foreach ($results as $result) { $output .= $result[$i]; } } return substr($output, 0, $keylen); } /** * Key expansion without salt * * @access private * @param int[] $key * @param int[] $sbox * @param int[] $p * @see self::_bcrypt_hash() */ private static function expand0state(array $key, array &$sbox, array &$p) { // expand0state is basically the same thing as this: //return self::expandstate(array_fill(0, 16, 0), $key); // but this separate function eliminates a bunch of XORs and array lookups $p = [ $p[0] ^ $key[0], $p[1] ^ $key[1], $p[2] ^ $key[2], $p[3] ^ $key[3], $p[4] ^ $key[4], $p[5] ^ $key[5], $p[6] ^ $key[6], $p[7] ^ $key[7], $p[8] ^ $key[8], $p[9] ^ $key[9], $p[10] ^ $key[10], $p[11] ^ $key[11], $p[12] ^ $key[12], $p[13] ^ $key[13], $p[14] ^ $key[14], $p[15] ^ $key[15], $p[16] ^ $key[0], $p[17] ^ $key[1] ]; // @codingStandardsIgnoreStart list( $p[0], $p[1]) = self::encryptBlockHelperFast( 0, 0, $sbox, $p); list( $p[2], $p[3]) = self::encryptBlockHelperFast($p[ 0], $p[ 1], $sbox, $p); list( $p[4], $p[5]) = self::encryptBlockHelperFast($p[ 2], $p[ 3], $sbox, $p); list( $p[6], $p[7]) = self::encryptBlockHelperFast($p[ 4], $p[ 5], $sbox, $p); list( $p[8], $p[9]) = self::encryptBlockHelperFast($p[ 6], $p[ 7], $sbox, $p); list($p[10], $p[11]) = self::encryptBlockHelperFast($p[ 8], $p[ 9], $sbox, $p); list($p[12], $p[13]) = self::encryptBlockHelperFast($p[10], $p[11], $sbox, $p); list($p[14], $p[15]) = self::encryptBlockHelperFast($p[12], $p[13], $sbox, $p); list($p[16], $p[17]) = self::encryptBlockHelperFast($p[14], $p[15], $sbox, $p); // @codingStandardsIgnoreEnd list($sbox[0], $sbox[1]) = self::encryptBlockHelperFast($p[16], $p[17], $sbox, $p); for ($i = 2; $i < 1024; $i += 2) { list($sbox[$i], $sbox[$i + 1]) = self::encryptBlockHelperFast($sbox[$i - 2], $sbox[$i - 1], $sbox, $p); } } /** * Key expansion with salt * * @access private * @param int[] $data * @param int[] $key * @param int[] $sbox * @param int[] $p * @see self::_bcrypt_hash() */ private static function expandstate(array $data, array $key, array &$sbox, array &$p) { $p = [ $p[0] ^ $key[0], $p[1] ^ $key[1], $p[2] ^ $key[2], $p[3] ^ $key[3], $p[4] ^ $key[4], $p[5] ^ $key[5], $p[6] ^ $key[6], $p[7] ^ $key[7], $p[8] ^ $key[8], $p[9] ^ $key[9], $p[10] ^ $key[10], $p[11] ^ $key[11], $p[12] ^ $key[12], $p[13] ^ $key[13], $p[14] ^ $key[14], $p[15] ^ $key[15], $p[16] ^ $key[0], $p[17] ^ $key[1] ]; // @codingStandardsIgnoreStart list( $p[0], $p[1]) = self::encryptBlockHelperFast($data[ 0] , $data[ 1] , $sbox, $p); list( $p[2], $p[3]) = self::encryptBlockHelperFast($data[ 2] ^ $p[ 0], $data[ 3] ^ $p[ 1], $sbox, $p); list( $p[4], $p[5]) = self::encryptBlockHelperFast($data[ 4] ^ $p[ 2], $data[ 5] ^ $p[ 3], $sbox, $p); list( $p[6], $p[7]) = self::encryptBlockHelperFast($data[ 6] ^ $p[ 4], $data[ 7] ^ $p[ 5], $sbox, $p); list( $p[8], $p[9]) = self::encryptBlockHelperFast($data[ 8] ^ $p[ 6], $data[ 9] ^ $p[ 7], $sbox, $p); list($p[10], $p[11]) = self::encryptBlockHelperFast($data[10] ^ $p[ 8], $data[11] ^ $p[ 9], $sbox, $p); list($p[12], $p[13]) = self::encryptBlockHelperFast($data[12] ^ $p[10], $data[13] ^ $p[11], $sbox, $p); list($p[14], $p[15]) = self::encryptBlockHelperFast($data[14] ^ $p[12], $data[15] ^ $p[13], $sbox, $p); list($p[16], $p[17]) = self::encryptBlockHelperFast($data[ 0] ^ $p[14], $data[ 1] ^ $p[15], $sbox, $p); // @codingStandardsIgnoreEnd list($sbox[0], $sbox[1]) = self::encryptBlockHelperFast($data[2] ^ $p[16], $data[3] ^ $p[17], $sbox, $p); for ($i = 2, $j = 4; $i < 1024; $i += 2, $j = ($j + 2) % 16) { // instead of 16 maybe count($data) would be better? list($sbox[$i], $sbox[$i + 1]) = self::encryptBlockHelperFast($data[$j] ^ $sbox[$i - 2], $data[$j + 1] ^ $sbox[$i - 1], $sbox, $p); } } /** * Encrypts a block * * @param string $in * @return string */ protected function encryptBlock($in) { $p = $this->bctx['p']; // extract($this->bctx['sb'], EXTR_PREFIX_ALL, 'sb'); // slower $sb = $this->bctx['sb']; $in = unpack('N*', $in); $l = $in[1]; $r = $in[2]; list($r, $l) = PHP_INT_SIZE == 4 ? self::encryptBlockHelperSlow($l, $r, $sb, $p) : self::encryptBlockHelperFast($l, $r, $sb, $p); return pack("N*", $r, $l); } /** * Fast helper function for block encryption * * @access private * @param int $x0 * @param int $x1 * @param int[] $sbox * @param int[] $p * @return int[] */ private static function encryptBlockHelperFast($x0, $x1, array $sbox, array $p) { $x0 ^= $p[0]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[1]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[2]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[3]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[4]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[5]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[6]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[7]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[8]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[9]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[10]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[11]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[12]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[13]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[14]; $x1 ^= ((($sbox[($x0 & 0xFF000000) >> 24] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[15]; $x0 ^= ((($sbox[($x1 & 0xFF000000) >> 24] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[16]; return [$x1 & 0xFFFFFFFF ^ $p[17], $x0 & 0xFFFFFFFF]; } /** * Slow helper function for block encryption * * @access private * @param int $x0 * @param int $x1 * @param int[] $sbox * @param int[] $p * @return int[] */ private static function encryptBlockHelperSlow($x0, $x1, array $sbox, array $p) { // -16777216 == intval(0xFF000000) on 32-bit PHP installs $x0 ^= $p[0]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[1]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[2]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[3]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[4]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[5]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[6]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[7]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[8]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[9]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[10]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[11]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[12]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[13]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[14]; $x1 ^= self::safe_intval((self::safe_intval($sbox[(($x0 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x0 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x0 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x0 & 0xFF)]) ^ $p[15]; $x0 ^= self::safe_intval((self::safe_intval($sbox[(($x1 & -16777216) >> 24) & 0xFF] + $sbox[0x100 | (($x1 & 0xFF0000) >> 16)]) ^ $sbox[0x200 | (($x1 & 0xFF00) >> 8)]) + $sbox[0x300 | ($x1 & 0xFF)]) ^ $p[16]; return [$x1 ^ $p[17], $x0]; } /** * Decrypts a block * * @param string $in * @return string */ protected function decryptBlock($in) { $p = $this->bctx['p']; $sb = $this->bctx['sb']; $in = unpack('N*', $in); $l = $in[1]; $r = $in[2]; for ($i = 17; $i > 2; $i -= 2) { $l ^= $p[$i]; $r ^= self::safe_intval((self::safe_intval($sb[$l >> 24 & 0xff] + $sb[0x100 + ($l >> 16 & 0xff)]) ^ $sb[0x200 + ($l >> 8 & 0xff)]) + $sb[0x300 + ($l & 0xff)]); $r ^= $p[$i - 1]; $l ^= self::safe_intval((self::safe_intval($sb[$r >> 24 & 0xff] + $sb[0x100 + ($r >> 16 & 0xff)]) ^ $sb[0x200 + ($r >> 8 & 0xff)]) + $sb[0x300 + ($r & 0xff)]); } return pack('N*', $r ^ $p[0], $l ^ $p[1]); } /** * Setup the performance-optimized function for de/encrypt() * * @see \phpseclib3\Crypt\Common\SymmetricKey::_setupInlineCrypt() */ protected function setupInlineCrypt() { $p = $this->bctx['p']; $init_crypt = ' static $sb; if (!$sb) { $sb = $this->bctx["sb"]; } '; $safeint = self::safe_intval_inline(); // Generating encrypt code: $encrypt_block = ' $in = unpack("N*", $in); $l = $in[1]; $r = $in[2]; '; for ($i = 0; $i < 16; $i += 2) { $encrypt_block .= ' $l^= ' . $p[$i] . '; $r^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb[$l >> 24 & 0xff] + $sb[0x100 + ($l >> 16 & 0xff)]') . ' ^ $sb[0x200 + ($l >> 8 & 0xff)]) + $sb[0x300 + ($l & 0xff)]') . '; $r^= ' . $p[$i + 1] . '; $l^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb[$r >> 24 & 0xff] + $sb[0x100 + ($r >> 16 & 0xff)]') . ' ^ $sb[0x200 + ($r >> 8 & 0xff)]) + $sb[0x300 + ($r & 0xff)]') . '; '; } $encrypt_block .= ' $in = pack("N*", $r ^ ' . $p[17] . ', $l ^ ' . $p[16] . ' ); '; // Generating decrypt code: $decrypt_block = ' $in = unpack("N*", $in); $l = $in[1]; $r = $in[2]; '; for ($i = 17; $i > 2; $i -= 2) { $decrypt_block .= ' $l^= ' . $p[$i] . '; $r^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb[$l >> 24 & 0xff] + $sb[0x100 + ($l >> 16 & 0xff)]') . ' ^ $sb[0x200 + ($l >> 8 & 0xff)]) + $sb[0x300 + ($l & 0xff)]') . '; $r^= ' . $p[$i - 1] . '; $l^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb[$r >> 24 & 0xff] + $sb[0x100 + ($r >> 16 & 0xff)]') . ' ^ $sb[0x200 + ($r >> 8 & 0xff)]) + $sb[0x300 + ($r & 0xff)]') . '; '; } $decrypt_block .= ' $in = pack("N*", $r ^ ' . $p[0] . ', $l ^ ' . $p[1] . ' ); '; $this->inline_crypt = $this->createInlineCryptFunction( [ 'init_crypt' => $init_crypt, 'init_encrypt' => '', 'init_decrypt' => '', 'encrypt_block' => $encrypt_block, 'decrypt_block' => $decrypt_block ] ); } } PK!0N7vendor/phpseclib/phpseclib/phpseclib/Crypt/ChaCha20.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Exception\BadDecryptionException; use phpseclib3\Exception\InsufficientSetupException; /** * Pure-PHP implementation of ChaCha20. * * @author Jim Wigginton */ class ChaCha20 extends Salsa20 { /** * The OpenSSL specific name of the cipher * * @var string */ protected $cipher_name_openssl = 'chacha20'; /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { switch ($engine) { case self::ENGINE_LIBSODIUM: // PHP 7.2.0 (30 Nov 2017) added support for libsodium // we could probably make it so that if $this->counter == 0 then the first block would be done with either OpenSSL // or PHP and then subsequent blocks would then be done with libsodium but idk - it's not a high priority atm // we could also make it so that if $this->counter == 0 and $this->continuousBuffer then do the first string // with libsodium and subsequent strings with openssl or pure-PHP but again not a high priority return function_exists('sodium_crypto_aead_chacha20poly1305_ietf_encrypt') && $this->key_length == 32 && (($this->usePoly1305 && !isset($this->poly1305Key) && $this->counter == 0) || $this->counter == 1) && !$this->continuousBuffer; case self::ENGINE_OPENSSL: // OpenSSL 1.1.0 (released 25 Aug 2016) added support for chacha20. // PHP didn't support OpenSSL 1.1.0 until 7.0.19 (11 May 2017) // if you attempt to provide openssl with a 128 bit key (as opposed to a 256 bit key) openssl will null // pad the key to 256 bits and still use the expansion constant for 256-bit keys. the fact that // openssl treats the IV as both the counter and nonce, however, let's us use openssl in continuous mode // whereas libsodium does not if ($this->key_length != 32) { return false; } } return parent::isValidEngineHelper($engine); } /** * Encrypts a message. * * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() * @see self::crypt() * @param string $plaintext * @return string $ciphertext */ public function encrypt($plaintext) { $this->setup(); if ($this->engine == self::ENGINE_LIBSODIUM) { return $this->encrypt_with_libsodium($plaintext); } return parent::encrypt($plaintext); } /** * Decrypts a message. * * $this->decrypt($this->encrypt($plaintext)) == $this->encrypt($this->encrypt($plaintext)). * At least if the continuous buffer is disabled. * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see self::crypt() * @param string $ciphertext * @return string $plaintext */ public function decrypt($ciphertext) { $this->setup(); if ($this->engine == self::ENGINE_LIBSODIUM) { return $this->decrypt_with_libsodium($ciphertext); } return parent::decrypt($ciphertext); } /** * Encrypts a message with libsodium * * @see self::encrypt() * @param string $plaintext * @return string $text */ private function encrypt_with_libsodium($plaintext) { $params = [$plaintext, $this->aad, $this->nonce, $this->key]; $ciphertext = strlen($this->nonce) == 8 ? sodium_crypto_aead_chacha20poly1305_encrypt(...$params) : sodium_crypto_aead_chacha20poly1305_ietf_encrypt(...$params); if (!$this->usePoly1305) { return substr($ciphertext, 0, strlen($plaintext)); } $newciphertext = substr($ciphertext, 0, strlen($plaintext)); $this->newtag = $this->usingGeneratedPoly1305Key && strlen($this->nonce) == 12 ? substr($ciphertext, strlen($plaintext)) : $this->poly1305($newciphertext); return $newciphertext; } /** * Decrypts a message with libsodium * * @see self::decrypt() * @param string $ciphertext * @return string $text */ private function decrypt_with_libsodium($ciphertext) { $params = [$ciphertext, $this->aad, $this->nonce, $this->key]; if (isset($this->poly1305Key)) { if ($this->oldtag === false) { throw new InsufficientSetupException('Authentication Tag has not been set'); } if ($this->usingGeneratedPoly1305Key && strlen($this->nonce) == 12) { $plaintext = sodium_crypto_aead_chacha20poly1305_ietf_decrypt(...$params); $this->oldtag = false; if ($plaintext === false) { throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); } return $plaintext; } $newtag = $this->poly1305($ciphertext); if ($this->oldtag != substr($newtag, 0, strlen($this->oldtag))) { $this->oldtag = false; throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); } $this->oldtag = false; } $plaintext = strlen($this->nonce) == 8 ? sodium_crypto_aead_chacha20poly1305_encrypt(...$params) : sodium_crypto_aead_chacha20poly1305_ietf_encrypt(...$params); return substr($plaintext, 0, strlen($ciphertext)); } /** * Sets the nonce. * * @param string $nonce */ public function setNonce($nonce) { if (!is_string($nonce)) { throw new \UnexpectedValueException('The nonce should be a string'); } /* from https://tools.ietf.org/html/rfc7539#page-7 "Note also that the original ChaCha had a 64-bit nonce and 64-bit block count. We have modified this here to be more consistent with recommendations in Section 3.2 of [RFC5116]." */ switch (strlen($nonce)) { case 8: // 64 bits case 12: // 96 bits break; default: throw new \LengthException('Nonce of size ' . strlen($nonce) . ' not supported by this algorithm. Only 64-bit nonces or 96-bit nonces are supported'); } $this->nonce = $nonce; $this->changed = true; $this->setEngine(); } /** * Setup the self::ENGINE_INTERNAL $engine * * (re)init, if necessary, the internal cipher $engine * * _setup() will be called each time if $changed === true * typically this happens when using one or more of following public methods: * * - setKey() * * - setNonce() * * - First run of encrypt() / decrypt() with no init-settings * * @see self::setKey() * @see self::setNonce() * @see self::disableContinuousBuffer() */ protected function setup() { if (!$this->changed) { return; } $this->enbuffer = $this->debuffer = ['ciphertext' => '', 'counter' => $this->counter]; $this->changed = $this->nonIVChanged = false; if ($this->nonce === false) { throw new InsufficientSetupException('No nonce has been defined'); } if ($this->key === false) { throw new InsufficientSetupException('No key has been defined'); } if ($this->usePoly1305 && !isset($this->poly1305Key)) { $this->usingGeneratedPoly1305Key = true; if ($this->engine == self::ENGINE_LIBSODIUM) { return; } $this->createPoly1305Key(); } $key = $this->key; if (strlen($key) == 16) { $constant = 'expand 16-byte k'; $key .= $key; } else { $constant = 'expand 32-byte k'; } $this->p1 = $constant . $key; $this->p2 = $this->nonce; if (strlen($this->nonce) == 8) { $this->p2 = "\0\0\0\0" . $this->p2; } } /** * The quarterround function * * @param int $a * @param int $b * @param int $c * @param int $d */ protected static function quarterRound(&$a, &$b, &$c, &$d) { // in https://datatracker.ietf.org/doc/html/rfc7539#section-2.1 the addition, // xor'ing and rotation are all on the same line so i'm keeping it on the same // line here as well // @codingStandardsIgnoreStart $a+= $b; $d = self::leftRotate(intval($d) ^ intval($a), 16); $c+= $d; $b = self::leftRotate(intval($b) ^ intval($c), 12); $a+= $b; $d = self::leftRotate(intval($d) ^ intval($a), 8); $c+= $d; $b = self::leftRotate(intval($b) ^ intval($c), 7); // @codingStandardsIgnoreEnd } /** * The doubleround function * * @param int $x0 (by reference) * @param int $x1 (by reference) * @param int $x2 (by reference) * @param int $x3 (by reference) * @param int $x4 (by reference) * @param int $x5 (by reference) * @param int $x6 (by reference) * @param int $x7 (by reference) * @param int $x8 (by reference) * @param int $x9 (by reference) * @param int $x10 (by reference) * @param int $x11 (by reference) * @param int $x12 (by reference) * @param int $x13 (by reference) * @param int $x14 (by reference) * @param int $x15 (by reference) */ protected static function doubleRound(&$x0, &$x1, &$x2, &$x3, &$x4, &$x5, &$x6, &$x7, &$x8, &$x9, &$x10, &$x11, &$x12, &$x13, &$x14, &$x15) { // columnRound static::quarterRound($x0, $x4, $x8, $x12); static::quarterRound($x1, $x5, $x9, $x13); static::quarterRound($x2, $x6, $x10, $x14); static::quarterRound($x3, $x7, $x11, $x15); // rowRound static::quarterRound($x0, $x5, $x10, $x15); static::quarterRound($x1, $x6, $x11, $x12); static::quarterRound($x2, $x7, $x8, $x13); static::quarterRound($x3, $x4, $x9, $x14); } /** * The Salsa20 hash function function * * On my laptop this loop unrolled / function dereferenced version of parent::salsa20 encrypts 1mb of text in * 0.65s vs the 0.85s that it takes with the parent method. * * If we were free to assume that the host OS would always be 64-bits then the if condition in leftRotate could * be eliminated and we could knock this done to 0.60s. * * For comparison purposes, RC4 takes 0.16s and AES in CTR mode with the Eval engine takes 0.48s. * AES in CTR mode with the PHP engine takes 1.19s. Salsa20 / ChaCha20 do not benefit as much from the Eval * approach due to the fact that there are a lot less variables to de-reference, fewer loops to unroll, etc * * @param string $x */ protected static function salsa20($x) { list(, $x0, $x1, $x2, $x3, $x4, $x5, $x6, $x7, $x8, $x9, $x10, $x11, $x12, $x13, $x14, $x15) = unpack('V*', $x); $z0 = $x0; $z1 = $x1; $z2 = $x2; $z3 = $x3; $z4 = $x4; $z5 = $x5; $z6 = $x6; $z7 = $x7; $z8 = $x8; $z9 = $x9; $z10 = $x10; $z11 = $x11; $z12 = $x12; $z13 = $x13; $z14 = $x14; $z15 = $x15; // @codingStandardsIgnoreStart // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // columnRound $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 16); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 12); $x0+= $x4; $x12 = self::leftRotate(intval($x12) ^ intval($x0), 8); $x8+= $x12; $x4 = self::leftRotate(intval($x4) ^ intval($x8), 7); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 16); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 12); $x1+= $x5; $x13 = self::leftRotate(intval($x13) ^ intval($x1), 8); $x9+= $x13; $x5 = self::leftRotate(intval($x5) ^ intval($x9), 7); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 16); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 12); $x2+= $x6; $x14 = self::leftRotate(intval($x14) ^ intval($x2), 8); $x10+= $x14; $x6 = self::leftRotate(intval($x6) ^ intval($x10), 7); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 16); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 12); $x3+= $x7; $x15 = self::leftRotate(intval($x15) ^ intval($x3), 8); $x11+= $x15; $x7 = self::leftRotate(intval($x7) ^ intval($x11), 7); // rowRound $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 16); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 12); $x0+= $x5; $x15 = self::leftRotate(intval($x15) ^ intval($x0), 8); $x10+= $x15; $x5 = self::leftRotate(intval($x5) ^ intval($x10), 7); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 16); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 12); $x1+= $x6; $x12 = self::leftRotate(intval($x12) ^ intval($x1), 8); $x11+= $x12; $x6 = self::leftRotate(intval($x6) ^ intval($x11), 7); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 16); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 12); $x2+= $x7; $x13 = self::leftRotate(intval($x13) ^ intval($x2), 8); $x8+= $x13; $x7 = self::leftRotate(intval($x7) ^ intval($x8), 7); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 16); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 12); $x3+= $x4; $x14 = self::leftRotate(intval($x14) ^ intval($x3), 8); $x9+= $x14; $x4 = self::leftRotate(intval($x4) ^ intval($x9), 7); // @codingStandardsIgnoreEnd $x0 += $z0; $x1 += $z1; $x2 += $z2; $x3 += $z3; $x4 += $z4; $x5 += $z5; $x6 += $z6; $x7 += $z7; $x8 += $z8; $x9 += $z9; $x10 += $z10; $x11 += $z11; $x12 += $z12; $x13 += $z13; $x14 += $z14; $x15 += $z15; return pack('V*', $x0, $x1, $x2, $x3, $x4, $x5, $x6, $x7, $x8, $x9, $x10, $x11, $x12, $x13, $x14, $x15); } } PK!cNu  2vendor/phpseclib/phpseclib/phpseclib/Crypt/DES.phpnu[ * setKey('abcdefgh'); * * $size = 10 * 1024; * $plaintext = ''; * for ($i = 0; $i < $size; $i++) { * $plaintext.= 'a'; * } * * echo $des->decrypt($des->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\BlockCipher; use phpseclib3\Exception\BadModeException; /** * Pure-PHP implementation of DES. * * @author Jim Wigginton */ class DES extends BlockCipher { /** * Contains $keys[self::ENCRYPT] * * @see \phpseclib3\Crypt\DES::setupKey() * @see \phpseclib3\Crypt\DES::processBlock() */ const ENCRYPT = 0; /** * Contains $keys[self::DECRYPT] * * @see \phpseclib3\Crypt\DES::setupKey() * @see \phpseclib3\Crypt\DES::processBlock() */ const DECRYPT = 1; /** * Block Length of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::block_size * @var int */ protected $block_size = 8; /** * Key Length (in bytes) * * @see \phpseclib3\Crypt\Common\SymmetricKey::setKeyLength() * @var int */ protected $key_length = 8; /** * The mcrypt specific name of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @var string */ protected $cipher_name_mcrypt = 'des'; /** * The OpenSSL names of the cipher / modes * * @see \phpseclib3\Crypt\Common\SymmetricKey::openssl_mode_names * @var array */ protected $openssl_mode_names = [ self::MODE_ECB => 'des-ecb', self::MODE_CBC => 'des-cbc', self::MODE_CFB => 'des-cfb', self::MODE_OFB => 'des-ofb' // self::MODE_CTR is undefined for DES ]; /** * Optimizing value while CFB-encrypting * * @see \phpseclib3\Crypt\Common\SymmetricKey::cfb_init_len * @var int */ protected $cfb_init_len = 500; /** * Switch for DES/3DES encryption * * Used only if $engine == self::ENGINE_INTERNAL * * @see self::setupKey() * @see self::processBlock() * @var int */ protected $des_rounds = 1; /** * max possible size of $key * * @see self::setKey() * @var string */ protected $key_length_max = 8; /** * The Key Schedule * * @see self::setupKey() * @var array */ private $keys; /** * Key Cache "key" * * @see self::setupKey() * @var array */ private $kl; /** * Shuffle table. * * For each byte value index, the entry holds an 8-byte string * with each byte containing all bits in the same state as the * corresponding bit in the index value. * * @see self::processBlock() * @see self::setupKey() * @var array */ protected static $shuffle = [ "\x00\x00\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00\x00\xFF", "\x00\x00\x00\x00\x00\x00\xFF\x00", "\x00\x00\x00\x00\x00\x00\xFF\xFF", "\x00\x00\x00\x00\x00\xFF\x00\x00", "\x00\x00\x00\x00\x00\xFF\x00\xFF", "\x00\x00\x00\x00\x00\xFF\xFF\x00", "\x00\x00\x00\x00\x00\xFF\xFF\xFF", "\x00\x00\x00\x00\xFF\x00\x00\x00", "\x00\x00\x00\x00\xFF\x00\x00\xFF", "\x00\x00\x00\x00\xFF\x00\xFF\x00", "\x00\x00\x00\x00\xFF\x00\xFF\xFF", "\x00\x00\x00\x00\xFF\xFF\x00\x00", "\x00\x00\x00\x00\xFF\xFF\x00\xFF", "\x00\x00\x00\x00\xFF\xFF\xFF\x00", "\x00\x00\x00\x00\xFF\xFF\xFF\xFF", "\x00\x00\x00\xFF\x00\x00\x00\x00", "\x00\x00\x00\xFF\x00\x00\x00\xFF", "\x00\x00\x00\xFF\x00\x00\xFF\x00", "\x00\x00\x00\xFF\x00\x00\xFF\xFF", "\x00\x00\x00\xFF\x00\xFF\x00\x00", "\x00\x00\x00\xFF\x00\xFF\x00\xFF", "\x00\x00\x00\xFF\x00\xFF\xFF\x00", "\x00\x00\x00\xFF\x00\xFF\xFF\xFF", "\x00\x00\x00\xFF\xFF\x00\x00\x00", "\x00\x00\x00\xFF\xFF\x00\x00\xFF", "\x00\x00\x00\xFF\xFF\x00\xFF\x00", "\x00\x00\x00\xFF\xFF\x00\xFF\xFF", "\x00\x00\x00\xFF\xFF\xFF\x00\x00", "\x00\x00\x00\xFF\xFF\xFF\x00\xFF", "\x00\x00\x00\xFF\xFF\xFF\xFF\x00", "\x00\x00\x00\xFF\xFF\xFF\xFF\xFF", "\x00\x00\xFF\x00\x00\x00\x00\x00", "\x00\x00\xFF\x00\x00\x00\x00\xFF", "\x00\x00\xFF\x00\x00\x00\xFF\x00", "\x00\x00\xFF\x00\x00\x00\xFF\xFF", "\x00\x00\xFF\x00\x00\xFF\x00\x00", "\x00\x00\xFF\x00\x00\xFF\x00\xFF", "\x00\x00\xFF\x00\x00\xFF\xFF\x00", "\x00\x00\xFF\x00\x00\xFF\xFF\xFF", "\x00\x00\xFF\x00\xFF\x00\x00\x00", "\x00\x00\xFF\x00\xFF\x00\x00\xFF", "\x00\x00\xFF\x00\xFF\x00\xFF\x00", "\x00\x00\xFF\x00\xFF\x00\xFF\xFF", "\x00\x00\xFF\x00\xFF\xFF\x00\x00", "\x00\x00\xFF\x00\xFF\xFF\x00\xFF", "\x00\x00\xFF\x00\xFF\xFF\xFF\x00", "\x00\x00\xFF\x00\xFF\xFF\xFF\xFF", "\x00\x00\xFF\xFF\x00\x00\x00\x00", "\x00\x00\xFF\xFF\x00\x00\x00\xFF", "\x00\x00\xFF\xFF\x00\x00\xFF\x00", "\x00\x00\xFF\xFF\x00\x00\xFF\xFF", "\x00\x00\xFF\xFF\x00\xFF\x00\x00", "\x00\x00\xFF\xFF\x00\xFF\x00\xFF", "\x00\x00\xFF\xFF\x00\xFF\xFF\x00", "\x00\x00\xFF\xFF\x00\xFF\xFF\xFF", "\x00\x00\xFF\xFF\xFF\x00\x00\x00", "\x00\x00\xFF\xFF\xFF\x00\x00\xFF", "\x00\x00\xFF\xFF\xFF\x00\xFF\x00", "\x00\x00\xFF\xFF\xFF\x00\xFF\xFF", "\x00\x00\xFF\xFF\xFF\xFF\x00\x00", "\x00\x00\xFF\xFF\xFF\xFF\x00\xFF", "\x00\x00\xFF\xFF\xFF\xFF\xFF\x00", "\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF", "\x00\xFF\x00\x00\x00\x00\x00\x00", "\x00\xFF\x00\x00\x00\x00\x00\xFF", "\x00\xFF\x00\x00\x00\x00\xFF\x00", "\x00\xFF\x00\x00\x00\x00\xFF\xFF", "\x00\xFF\x00\x00\x00\xFF\x00\x00", "\x00\xFF\x00\x00\x00\xFF\x00\xFF", "\x00\xFF\x00\x00\x00\xFF\xFF\x00", "\x00\xFF\x00\x00\x00\xFF\xFF\xFF", "\x00\xFF\x00\x00\xFF\x00\x00\x00", "\x00\xFF\x00\x00\xFF\x00\x00\xFF", "\x00\xFF\x00\x00\xFF\x00\xFF\x00", "\x00\xFF\x00\x00\xFF\x00\xFF\xFF", "\x00\xFF\x00\x00\xFF\xFF\x00\x00", "\x00\xFF\x00\x00\xFF\xFF\x00\xFF", "\x00\xFF\x00\x00\xFF\xFF\xFF\x00", "\x00\xFF\x00\x00\xFF\xFF\xFF\xFF", "\x00\xFF\x00\xFF\x00\x00\x00\x00", "\x00\xFF\x00\xFF\x00\x00\x00\xFF", "\x00\xFF\x00\xFF\x00\x00\xFF\x00", "\x00\xFF\x00\xFF\x00\x00\xFF\xFF", "\x00\xFF\x00\xFF\x00\xFF\x00\x00", "\x00\xFF\x00\xFF\x00\xFF\x00\xFF", "\x00\xFF\x00\xFF\x00\xFF\xFF\x00", "\x00\xFF\x00\xFF\x00\xFF\xFF\xFF", "\x00\xFF\x00\xFF\xFF\x00\x00\x00", "\x00\xFF\x00\xFF\xFF\x00\x00\xFF", "\x00\xFF\x00\xFF\xFF\x00\xFF\x00", "\x00\xFF\x00\xFF\xFF\x00\xFF\xFF", "\x00\xFF\x00\xFF\xFF\xFF\x00\x00", "\x00\xFF\x00\xFF\xFF\xFF\x00\xFF", "\x00\xFF\x00\xFF\xFF\xFF\xFF\x00", "\x00\xFF\x00\xFF\xFF\xFF\xFF\xFF", "\x00\xFF\xFF\x00\x00\x00\x00\x00", "\x00\xFF\xFF\x00\x00\x00\x00\xFF", "\x00\xFF\xFF\x00\x00\x00\xFF\x00", "\x00\xFF\xFF\x00\x00\x00\xFF\xFF", "\x00\xFF\xFF\x00\x00\xFF\x00\x00", "\x00\xFF\xFF\x00\x00\xFF\x00\xFF", "\x00\xFF\xFF\x00\x00\xFF\xFF\x00", "\x00\xFF\xFF\x00\x00\xFF\xFF\xFF", "\x00\xFF\xFF\x00\xFF\x00\x00\x00", "\x00\xFF\xFF\x00\xFF\x00\x00\xFF", "\x00\xFF\xFF\x00\xFF\x00\xFF\x00", "\x00\xFF\xFF\x00\xFF\x00\xFF\xFF", "\x00\xFF\xFF\x00\xFF\xFF\x00\x00", "\x00\xFF\xFF\x00\xFF\xFF\x00\xFF", "\x00\xFF\xFF\x00\xFF\xFF\xFF\x00", "\x00\xFF\xFF\x00\xFF\xFF\xFF\xFF", "\x00\xFF\xFF\xFF\x00\x00\x00\x00", "\x00\xFF\xFF\xFF\x00\x00\x00\xFF", "\x00\xFF\xFF\xFF\x00\x00\xFF\x00", "\x00\xFF\xFF\xFF\x00\x00\xFF\xFF", "\x00\xFF\xFF\xFF\x00\xFF\x00\x00", "\x00\xFF\xFF\xFF\x00\xFF\x00\xFF", "\x00\xFF\xFF\xFF\x00\xFF\xFF\x00", "\x00\xFF\xFF\xFF\x00\xFF\xFF\xFF", "\x00\xFF\xFF\xFF\xFF\x00\x00\x00", "\x00\xFF\xFF\xFF\xFF\x00\x00\xFF", "\x00\xFF\xFF\xFF\xFF\x00\xFF\x00", "\x00\xFF\xFF\xFF\xFF\x00\xFF\xFF", "\x00\xFF\xFF\xFF\xFF\xFF\x00\x00", "\x00\xFF\xFF\xFF\xFF\xFF\x00\xFF", "\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", "\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF", "\xFF\x00\x00\x00\x00\x00\x00\x00", "\xFF\x00\x00\x00\x00\x00\x00\xFF", "\xFF\x00\x00\x00\x00\x00\xFF\x00", "\xFF\x00\x00\x00\x00\x00\xFF\xFF", "\xFF\x00\x00\x00\x00\xFF\x00\x00", "\xFF\x00\x00\x00\x00\xFF\x00\xFF", "\xFF\x00\x00\x00\x00\xFF\xFF\x00", "\xFF\x00\x00\x00\x00\xFF\xFF\xFF", "\xFF\x00\x00\x00\xFF\x00\x00\x00", "\xFF\x00\x00\x00\xFF\x00\x00\xFF", "\xFF\x00\x00\x00\xFF\x00\xFF\x00", "\xFF\x00\x00\x00\xFF\x00\xFF\xFF", "\xFF\x00\x00\x00\xFF\xFF\x00\x00", "\xFF\x00\x00\x00\xFF\xFF\x00\xFF", "\xFF\x00\x00\x00\xFF\xFF\xFF\x00", "\xFF\x00\x00\x00\xFF\xFF\xFF\xFF", "\xFF\x00\x00\xFF\x00\x00\x00\x00", "\xFF\x00\x00\xFF\x00\x00\x00\xFF", "\xFF\x00\x00\xFF\x00\x00\xFF\x00", "\xFF\x00\x00\xFF\x00\x00\xFF\xFF", "\xFF\x00\x00\xFF\x00\xFF\x00\x00", "\xFF\x00\x00\xFF\x00\xFF\x00\xFF", "\xFF\x00\x00\xFF\x00\xFF\xFF\x00", "\xFF\x00\x00\xFF\x00\xFF\xFF\xFF", "\xFF\x00\x00\xFF\xFF\x00\x00\x00", "\xFF\x00\x00\xFF\xFF\x00\x00\xFF", "\xFF\x00\x00\xFF\xFF\x00\xFF\x00", "\xFF\x00\x00\xFF\xFF\x00\xFF\xFF", "\xFF\x00\x00\xFF\xFF\xFF\x00\x00", "\xFF\x00\x00\xFF\xFF\xFF\x00\xFF", "\xFF\x00\x00\xFF\xFF\xFF\xFF\x00", "\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF", "\xFF\x00\xFF\x00\x00\x00\x00\x00", "\xFF\x00\xFF\x00\x00\x00\x00\xFF", "\xFF\x00\xFF\x00\x00\x00\xFF\x00", "\xFF\x00\xFF\x00\x00\x00\xFF\xFF", "\xFF\x00\xFF\x00\x00\xFF\x00\x00", "\xFF\x00\xFF\x00\x00\xFF\x00\xFF", "\xFF\x00\xFF\x00\x00\xFF\xFF\x00", "\xFF\x00\xFF\x00\x00\xFF\xFF\xFF", "\xFF\x00\xFF\x00\xFF\x00\x00\x00", "\xFF\x00\xFF\x00\xFF\x00\x00\xFF", "\xFF\x00\xFF\x00\xFF\x00\xFF\x00", "\xFF\x00\xFF\x00\xFF\x00\xFF\xFF", "\xFF\x00\xFF\x00\xFF\xFF\x00\x00", "\xFF\x00\xFF\x00\xFF\xFF\x00\xFF", "\xFF\x00\xFF\x00\xFF\xFF\xFF\x00", "\xFF\x00\xFF\x00\xFF\xFF\xFF\xFF", "\xFF\x00\xFF\xFF\x00\x00\x00\x00", "\xFF\x00\xFF\xFF\x00\x00\x00\xFF", "\xFF\x00\xFF\xFF\x00\x00\xFF\x00", "\xFF\x00\xFF\xFF\x00\x00\xFF\xFF", "\xFF\x00\xFF\xFF\x00\xFF\x00\x00", "\xFF\x00\xFF\xFF\x00\xFF\x00\xFF", "\xFF\x00\xFF\xFF\x00\xFF\xFF\x00", "\xFF\x00\xFF\xFF\x00\xFF\xFF\xFF", "\xFF\x00\xFF\xFF\xFF\x00\x00\x00", "\xFF\x00\xFF\xFF\xFF\x00\x00\xFF", "\xFF\x00\xFF\xFF\xFF\x00\xFF\x00", "\xFF\x00\xFF\xFF\xFF\x00\xFF\xFF", "\xFF\x00\xFF\xFF\xFF\xFF\x00\x00", "\xFF\x00\xFF\xFF\xFF\xFF\x00\xFF", "\xFF\x00\xFF\xFF\xFF\xFF\xFF\x00", "\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF", "\xFF\xFF\x00\x00\x00\x00\x00\x00", "\xFF\xFF\x00\x00\x00\x00\x00\xFF", "\xFF\xFF\x00\x00\x00\x00\xFF\x00", "\xFF\xFF\x00\x00\x00\x00\xFF\xFF", "\xFF\xFF\x00\x00\x00\xFF\x00\x00", "\xFF\xFF\x00\x00\x00\xFF\x00\xFF", "\xFF\xFF\x00\x00\x00\xFF\xFF\x00", "\xFF\xFF\x00\x00\x00\xFF\xFF\xFF", "\xFF\xFF\x00\x00\xFF\x00\x00\x00", "\xFF\xFF\x00\x00\xFF\x00\x00\xFF", "\xFF\xFF\x00\x00\xFF\x00\xFF\x00", "\xFF\xFF\x00\x00\xFF\x00\xFF\xFF", "\xFF\xFF\x00\x00\xFF\xFF\x00\x00", "\xFF\xFF\x00\x00\xFF\xFF\x00\xFF", "\xFF\xFF\x00\x00\xFF\xFF\xFF\x00", "\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF", "\xFF\xFF\x00\xFF\x00\x00\x00\x00", "\xFF\xFF\x00\xFF\x00\x00\x00\xFF", "\xFF\xFF\x00\xFF\x00\x00\xFF\x00", "\xFF\xFF\x00\xFF\x00\x00\xFF\xFF", "\xFF\xFF\x00\xFF\x00\xFF\x00\x00", "\xFF\xFF\x00\xFF\x00\xFF\x00\xFF", "\xFF\xFF\x00\xFF\x00\xFF\xFF\x00", "\xFF\xFF\x00\xFF\x00\xFF\xFF\xFF", "\xFF\xFF\x00\xFF\xFF\x00\x00\x00", "\xFF\xFF\x00\xFF\xFF\x00\x00\xFF", "\xFF\xFF\x00\xFF\xFF\x00\xFF\x00", "\xFF\xFF\x00\xFF\xFF\x00\xFF\xFF", "\xFF\xFF\x00\xFF\xFF\xFF\x00\x00", "\xFF\xFF\x00\xFF\xFF\xFF\x00\xFF", "\xFF\xFF\x00\xFF\xFF\xFF\xFF\x00", "\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF", "\xFF\xFF\xFF\x00\x00\x00\x00\x00", "\xFF\xFF\xFF\x00\x00\x00\x00\xFF", "\xFF\xFF\xFF\x00\x00\x00\xFF\x00", "\xFF\xFF\xFF\x00\x00\x00\xFF\xFF", "\xFF\xFF\xFF\x00\x00\xFF\x00\x00", "\xFF\xFF\xFF\x00\x00\xFF\x00\xFF", "\xFF\xFF\xFF\x00\x00\xFF\xFF\x00", "\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF", "\xFF\xFF\xFF\x00\xFF\x00\x00\x00", "\xFF\xFF\xFF\x00\xFF\x00\x00\xFF", "\xFF\xFF\xFF\x00\xFF\x00\xFF\x00", "\xFF\xFF\xFF\x00\xFF\x00\xFF\xFF", "\xFF\xFF\xFF\x00\xFF\xFF\x00\x00", "\xFF\xFF\xFF\x00\xFF\xFF\x00\xFF", "\xFF\xFF\xFF\x00\xFF\xFF\xFF\x00", "\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF", "\xFF\xFF\xFF\xFF\x00\x00\x00\x00", "\xFF\xFF\xFF\xFF\x00\x00\x00\xFF", "\xFF\xFF\xFF\xFF\x00\x00\xFF\x00", "\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF", "\xFF\xFF\xFF\xFF\x00\xFF\x00\x00", "\xFF\xFF\xFF\xFF\x00\xFF\x00\xFF", "\xFF\xFF\xFF\xFF\x00\xFF\xFF\x00", "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF", "\xFF\xFF\xFF\xFF\xFF\x00\x00\x00", "\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF", "\xFF\xFF\xFF\xFF\xFF\x00\xFF\x00", "\xFF\xFF\xFF\xFF\xFF\x00\xFF\xFF", "\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00", "\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF", "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00", "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" ]; /** * IP mapping helper table. * * Indexing this table with each source byte performs the initial bit permutation. * * @var array */ protected static $ipmap = [ 0x00, 0x10, 0x01, 0x11, 0x20, 0x30, 0x21, 0x31, 0x02, 0x12, 0x03, 0x13, 0x22, 0x32, 0x23, 0x33, 0x40, 0x50, 0x41, 0x51, 0x60, 0x70, 0x61, 0x71, 0x42, 0x52, 0x43, 0x53, 0x62, 0x72, 0x63, 0x73, 0x04, 0x14, 0x05, 0x15, 0x24, 0x34, 0x25, 0x35, 0x06, 0x16, 0x07, 0x17, 0x26, 0x36, 0x27, 0x37, 0x44, 0x54, 0x45, 0x55, 0x64, 0x74, 0x65, 0x75, 0x46, 0x56, 0x47, 0x57, 0x66, 0x76, 0x67, 0x77, 0x80, 0x90, 0x81, 0x91, 0xA0, 0xB0, 0xA1, 0xB1, 0x82, 0x92, 0x83, 0x93, 0xA2, 0xB2, 0xA3, 0xB3, 0xC0, 0xD0, 0xC1, 0xD1, 0xE0, 0xF0, 0xE1, 0xF1, 0xC2, 0xD2, 0xC3, 0xD3, 0xE2, 0xF2, 0xE3, 0xF3, 0x84, 0x94, 0x85, 0x95, 0xA4, 0xB4, 0xA5, 0xB5, 0x86, 0x96, 0x87, 0x97, 0xA6, 0xB6, 0xA7, 0xB7, 0xC4, 0xD4, 0xC5, 0xD5, 0xE4, 0xF4, 0xE5, 0xF5, 0xC6, 0xD6, 0xC7, 0xD7, 0xE6, 0xF6, 0xE7, 0xF7, 0x08, 0x18, 0x09, 0x19, 0x28, 0x38, 0x29, 0x39, 0x0A, 0x1A, 0x0B, 0x1B, 0x2A, 0x3A, 0x2B, 0x3B, 0x48, 0x58, 0x49, 0x59, 0x68, 0x78, 0x69, 0x79, 0x4A, 0x5A, 0x4B, 0x5B, 0x6A, 0x7A, 0x6B, 0x7B, 0x0C, 0x1C, 0x0D, 0x1D, 0x2C, 0x3C, 0x2D, 0x3D, 0x0E, 0x1E, 0x0F, 0x1F, 0x2E, 0x3E, 0x2F, 0x3F, 0x4C, 0x5C, 0x4D, 0x5D, 0x6C, 0x7C, 0x6D, 0x7D, 0x4E, 0x5E, 0x4F, 0x5F, 0x6E, 0x7E, 0x6F, 0x7F, 0x88, 0x98, 0x89, 0x99, 0xA8, 0xB8, 0xA9, 0xB9, 0x8A, 0x9A, 0x8B, 0x9B, 0xAA, 0xBA, 0xAB, 0xBB, 0xC8, 0xD8, 0xC9, 0xD9, 0xE8, 0xF8, 0xE9, 0xF9, 0xCA, 0xDA, 0xCB, 0xDB, 0xEA, 0xFA, 0xEB, 0xFB, 0x8C, 0x9C, 0x8D, 0x9D, 0xAC, 0xBC, 0xAD, 0xBD, 0x8E, 0x9E, 0x8F, 0x9F, 0xAE, 0xBE, 0xAF, 0xBF, 0xCC, 0xDC, 0xCD, 0xDD, 0xEC, 0xFC, 0xED, 0xFD, 0xCE, 0xDE, 0xCF, 0xDF, 0xEE, 0xFE, 0xEF, 0xFF ]; /** * Inverse IP mapping helper table. * Indexing this table with a byte value reverses the bit order. * * @var array */ protected static $invipmap = [ 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF ]; /** * Pre-permuted S-box1 * * Each box ($sbox1-$sbox8) has been vectorized, then each value pre-permuted using the * P table: concatenation can then be replaced by exclusive ORs. * * @var array */ protected static $sbox1 = [ 0x00808200, 0x00000000, 0x00008000, 0x00808202, 0x00808002, 0x00008202, 0x00000002, 0x00008000, 0x00000200, 0x00808200, 0x00808202, 0x00000200, 0x00800202, 0x00808002, 0x00800000, 0x00000002, 0x00000202, 0x00800200, 0x00800200, 0x00008200, 0x00008200, 0x00808000, 0x00808000, 0x00800202, 0x00008002, 0x00800002, 0x00800002, 0x00008002, 0x00000000, 0x00000202, 0x00008202, 0x00800000, 0x00008000, 0x00808202, 0x00000002, 0x00808000, 0x00808200, 0x00800000, 0x00800000, 0x00000200, 0x00808002, 0x00008000, 0x00008200, 0x00800002, 0x00000200, 0x00000002, 0x00800202, 0x00008202, 0x00808202, 0x00008002, 0x00808000, 0x00800202, 0x00800002, 0x00000202, 0x00008202, 0x00808200, 0x00000202, 0x00800200, 0x00800200, 0x00000000, 0x00008002, 0x00008200, 0x00000000, 0x00808002 ]; /** * Pre-permuted S-box2 * * @var array */ protected static $sbox2 = [ 0x40084010, 0x40004000, 0x00004000, 0x00084010, 0x00080000, 0x00000010, 0x40080010, 0x40004010, 0x40000010, 0x40084010, 0x40084000, 0x40000000, 0x40004000, 0x00080000, 0x00000010, 0x40080010, 0x00084000, 0x00080010, 0x40004010, 0x00000000, 0x40000000, 0x00004000, 0x00084010, 0x40080000, 0x00080010, 0x40000010, 0x00000000, 0x00084000, 0x00004010, 0x40084000, 0x40080000, 0x00004010, 0x00000000, 0x00084010, 0x40080010, 0x00080000, 0x40004010, 0x40080000, 0x40084000, 0x00004000, 0x40080000, 0x40004000, 0x00000010, 0x40084010, 0x00084010, 0x00000010, 0x00004000, 0x40000000, 0x00004010, 0x40084000, 0x00080000, 0x40000010, 0x00080010, 0x40004010, 0x40000010, 0x00080010, 0x00084000, 0x00000000, 0x40004000, 0x00004010, 0x40000000, 0x40080010, 0x40084010, 0x00084000 ]; /** * Pre-permuted S-box3 * * @var array */ protected static $sbox3 = [ 0x00000104, 0x04010100, 0x00000000, 0x04010004, 0x04000100, 0x00000000, 0x00010104, 0x04000100, 0x00010004, 0x04000004, 0x04000004, 0x00010000, 0x04010104, 0x00010004, 0x04010000, 0x00000104, 0x04000000, 0x00000004, 0x04010100, 0x00000100, 0x00010100, 0x04010000, 0x04010004, 0x00010104, 0x04000104, 0x00010100, 0x00010000, 0x04000104, 0x00000004, 0x04010104, 0x00000100, 0x04000000, 0x04010100, 0x04000000, 0x00010004, 0x00000104, 0x00010000, 0x04010100, 0x04000100, 0x00000000, 0x00000100, 0x00010004, 0x04010104, 0x04000100, 0x04000004, 0x00000100, 0x00000000, 0x04010004, 0x04000104, 0x00010000, 0x04000000, 0x04010104, 0x00000004, 0x00010104, 0x00010100, 0x04000004, 0x04010000, 0x04000104, 0x00000104, 0x04010000, 0x00010104, 0x00000004, 0x04010004, 0x00010100 ]; /** * Pre-permuted S-box4 * * @var array */ protected static $sbox4 = [ 0x80401000, 0x80001040, 0x80001040, 0x00000040, 0x00401040, 0x80400040, 0x80400000, 0x80001000, 0x00000000, 0x00401000, 0x00401000, 0x80401040, 0x80000040, 0x00000000, 0x00400040, 0x80400000, 0x80000000, 0x00001000, 0x00400000, 0x80401000, 0x00000040, 0x00400000, 0x80001000, 0x00001040, 0x80400040, 0x80000000, 0x00001040, 0x00400040, 0x00001000, 0x00401040, 0x80401040, 0x80000040, 0x00400040, 0x80400000, 0x00401000, 0x80401040, 0x80000040, 0x00000000, 0x00000000, 0x00401000, 0x00001040, 0x00400040, 0x80400040, 0x80000000, 0x80401000, 0x80001040, 0x80001040, 0x00000040, 0x80401040, 0x80000040, 0x80000000, 0x00001000, 0x80400000, 0x80001000, 0x00401040, 0x80400040, 0x80001000, 0x00001040, 0x00400000, 0x80401000, 0x00000040, 0x00400000, 0x00001000, 0x00401040 ]; /** * Pre-permuted S-box5 * * @var array */ protected static $sbox5 = [ 0x00000080, 0x01040080, 0x01040000, 0x21000080, 0x00040000, 0x00000080, 0x20000000, 0x01040000, 0x20040080, 0x00040000, 0x01000080, 0x20040080, 0x21000080, 0x21040000, 0x00040080, 0x20000000, 0x01000000, 0x20040000, 0x20040000, 0x00000000, 0x20000080, 0x21040080, 0x21040080, 0x01000080, 0x21040000, 0x20000080, 0x00000000, 0x21000000, 0x01040080, 0x01000000, 0x21000000, 0x00040080, 0x00040000, 0x21000080, 0x00000080, 0x01000000, 0x20000000, 0x01040000, 0x21000080, 0x20040080, 0x01000080, 0x20000000, 0x21040000, 0x01040080, 0x20040080, 0x00000080, 0x01000000, 0x21040000, 0x21040080, 0x00040080, 0x21000000, 0x21040080, 0x01040000, 0x00000000, 0x20040000, 0x21000000, 0x00040080, 0x01000080, 0x20000080, 0x00040000, 0x00000000, 0x20040000, 0x01040080, 0x20000080 ]; /** * Pre-permuted S-box6 * * @var array */ protected static $sbox6 = [ 0x10000008, 0x10200000, 0x00002000, 0x10202008, 0x10200000, 0x00000008, 0x10202008, 0x00200000, 0x10002000, 0x00202008, 0x00200000, 0x10000008, 0x00200008, 0x10002000, 0x10000000, 0x00002008, 0x00000000, 0x00200008, 0x10002008, 0x00002000, 0x00202000, 0x10002008, 0x00000008, 0x10200008, 0x10200008, 0x00000000, 0x00202008, 0x10202000, 0x00002008, 0x00202000, 0x10202000, 0x10000000, 0x10002000, 0x00000008, 0x10200008, 0x00202000, 0x10202008, 0x00200000, 0x00002008, 0x10000008, 0x00200000, 0x10002000, 0x10000000, 0x00002008, 0x10000008, 0x10202008, 0x00202000, 0x10200000, 0x00202008, 0x10202000, 0x00000000, 0x10200008, 0x00000008, 0x00002000, 0x10200000, 0x00202008, 0x00002000, 0x00200008, 0x10002008, 0x00000000, 0x10202000, 0x10000000, 0x00200008, 0x10002008 ]; /** * Pre-permuted S-box7 * * @var array */ protected static $sbox7 = [ 0x00100000, 0x02100001, 0x02000401, 0x00000000, 0x00000400, 0x02000401, 0x00100401, 0x02100400, 0x02100401, 0x00100000, 0x00000000, 0x02000001, 0x00000001, 0x02000000, 0x02100001, 0x00000401, 0x02000400, 0x00100401, 0x00100001, 0x02000400, 0x02000001, 0x02100000, 0x02100400, 0x00100001, 0x02100000, 0x00000400, 0x00000401, 0x02100401, 0x00100400, 0x00000001, 0x02000000, 0x00100400, 0x02000000, 0x00100400, 0x00100000, 0x02000401, 0x02000401, 0x02100001, 0x02100001, 0x00000001, 0x00100001, 0x02000000, 0x02000400, 0x00100000, 0x02100400, 0x00000401, 0x00100401, 0x02100400, 0x00000401, 0x02000001, 0x02100401, 0x02100000, 0x00100400, 0x00000000, 0x00000001, 0x02100401, 0x00000000, 0x00100401, 0x02100000, 0x00000400, 0x02000001, 0x02000400, 0x00000400, 0x00100001 ]; /** * Pre-permuted S-box8 * * @var array */ protected static $sbox8 = [ 0x08000820, 0x00000800, 0x00020000, 0x08020820, 0x08000000, 0x08000820, 0x00000020, 0x08000000, 0x00020020, 0x08020000, 0x08020820, 0x00020800, 0x08020800, 0x00020820, 0x00000800, 0x00000020, 0x08020000, 0x08000020, 0x08000800, 0x00000820, 0x00020800, 0x00020020, 0x08020020, 0x08020800, 0x00000820, 0x00000000, 0x00000000, 0x08020020, 0x08000020, 0x08000800, 0x00020820, 0x00020000, 0x00020820, 0x00020000, 0x08020800, 0x00000800, 0x00000020, 0x08020020, 0x00000800, 0x00020820, 0x08000800, 0x00000020, 0x08000020, 0x08020000, 0x08020020, 0x08000000, 0x00020000, 0x08000820, 0x00000000, 0x08020820, 0x00020020, 0x08000020, 0x08020000, 0x08000800, 0x08000820, 0x00000000, 0x08020820, 0x00020800, 0x00020800, 0x00000820, 0x00000820, 0x00020020, 0x08000000, 0x08020800 ]; /** * Default Constructor. * * @param string $mode * @throws BadModeException if an invalid / unsupported mode is provided */ public function __construct($mode) { parent::__construct($mode); if ($this->mode == self::MODE_STREAM) { throw new BadModeException('Block ciphers cannot be ran in stream mode'); } } /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { if ($this->key_length_max == 8) { if ($engine == self::ENGINE_OPENSSL) { // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { return false; } $this->cipher_name_openssl_ecb = 'des-ecb'; $this->cipher_name_openssl = 'des-' . $this->openssl_translate_mode(); } } return parent::isValidEngineHelper($engine); } /** * Sets the key. * * Keys must be 64-bits long or 8 bytes long. * * DES also requires that every eighth bit be a parity bit, however, we'll ignore that. * * @see \phpseclib3\Crypt\Common\SymmetricKey::setKey() * @param string $key */ public function setKey($key) { if (!($this instanceof TripleDES) && strlen($key) != 8) { throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of size 8 are supported'); } // Sets the key parent::setKey($key); } /** * Encrypts a block * * @see \phpseclib3\Crypt\Common\SymmetricKey::encryptBlock() * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see self::encrypt() * @param string $in * @return string */ protected function encryptBlock($in) { return $this->processBlock($in, self::ENCRYPT); } /** * Decrypts a block * * @see \phpseclib3\Crypt\Common\SymmetricKey::decryptBlock() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() * @see self::decrypt() * @param string $in * @return string */ protected function decryptBlock($in) { return $this->processBlock($in, self::DECRYPT); } /** * Encrypts or decrypts a 64-bit block * * $mode should be either self::ENCRYPT or self::DECRYPT. See * {@link http://en.wikipedia.org/wiki/Image:Feistel.png Feistel.png} to get a general * idea of what this function does. * * @see self::encryptBlock() * @see self::decryptBlock() * @param string $block * @param int $mode * @return string */ private function processBlock($block, $mode) { static $sbox1, $sbox2, $sbox3, $sbox4, $sbox5, $sbox6, $sbox7, $sbox8, $shuffleip, $shuffleinvip; if (!$sbox1) { $sbox1 = array_map('intval', self::$sbox1); $sbox2 = array_map('intval', self::$sbox2); $sbox3 = array_map('intval', self::$sbox3); $sbox4 = array_map('intval', self::$sbox4); $sbox5 = array_map('intval', self::$sbox5); $sbox6 = array_map('intval', self::$sbox6); $sbox7 = array_map('intval', self::$sbox7); $sbox8 = array_map('intval', self::$sbox8); /* Merge $shuffle with $[inv]ipmap */ for ($i = 0; $i < 256; ++$i) { $shuffleip[] = self::$shuffle[self::$ipmap[$i]]; $shuffleinvip[] = self::$shuffle[self::$invipmap[$i]]; } } $keys = $this->keys[$mode]; $ki = -1; // Do the initial IP permutation. $t = unpack('Nl/Nr', $block); list($l, $r) = [$t['l'], $t['r']]; $block = ($shuffleip[ $r & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | ($shuffleip[($r >> 8) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | ($shuffleip[($r >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | ($shuffleip[($r >> 24) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | ($shuffleip[ $l & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | ($shuffleip[($l >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | ($shuffleip[($l >> 16) & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | ($shuffleip[($l >> 24) & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01"); // Extract L0 and R0. $t = unpack('Nl/Nr', $block); list($l, $r) = [$t['l'], $t['r']]; for ($des_round = 0; $des_round < $this->des_rounds; ++$des_round) { // Perform the 16 steps. for ($i = 0; $i < 16; $i++) { // start of "the Feistel (F) function" - see the following URL: // http://en.wikipedia.org/wiki/Image:Data_Encryption_Standard_InfoBox_Diagram.png // Merge key schedule. $b1 = (($r >> 3) & 0x1FFFFFFF) ^ ($r << 29) ^ $keys[++$ki]; $b2 = (($r >> 31) & 0x00000001) ^ ($r << 1) ^ $keys[++$ki]; // S-box indexing. $t = $sbox1[($b1 >> 24) & 0x3F] ^ $sbox2[($b2 >> 24) & 0x3F] ^ $sbox3[($b1 >> 16) & 0x3F] ^ $sbox4[($b2 >> 16) & 0x3F] ^ $sbox5[($b1 >> 8) & 0x3F] ^ $sbox6[($b2 >> 8) & 0x3F] ^ $sbox7[ $b1 & 0x3F] ^ $sbox8[ $b2 & 0x3F] ^ $l; // end of "the Feistel (F) function" $l = $r; $r = $t; } // Last step should not permute L & R. $t = $l; $l = $r; $r = $t; } // Perform the inverse IP permutation. return ($shuffleinvip[($r >> 24) & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | ($shuffleinvip[($l >> 24) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | ($shuffleinvip[($r >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | ($shuffleinvip[($l >> 16) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | ($shuffleinvip[($r >> 8) & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | ($shuffleinvip[($l >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | ($shuffleinvip[ $r & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | ($shuffleinvip[ $l & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01"); } /** * Creates the key schedule * * @see \phpseclib3\Crypt\Common\SymmetricKey::setupKey() */ protected function setupKey() { if (isset($this->kl['key']) && $this->key === $this->kl['key'] && $this->des_rounds === $this->kl['des_rounds']) { // already expanded return; } $this->kl = ['key' => $this->key, 'des_rounds' => $this->des_rounds]; static $shifts = [ // number of key bits shifted per round 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 ]; static $pc1map = [ 0x00, 0x00, 0x08, 0x08, 0x04, 0x04, 0x0C, 0x0C, 0x02, 0x02, 0x0A, 0x0A, 0x06, 0x06, 0x0E, 0x0E, 0x10, 0x10, 0x18, 0x18, 0x14, 0x14, 0x1C, 0x1C, 0x12, 0x12, 0x1A, 0x1A, 0x16, 0x16, 0x1E, 0x1E, 0x20, 0x20, 0x28, 0x28, 0x24, 0x24, 0x2C, 0x2C, 0x22, 0x22, 0x2A, 0x2A, 0x26, 0x26, 0x2E, 0x2E, 0x30, 0x30, 0x38, 0x38, 0x34, 0x34, 0x3C, 0x3C, 0x32, 0x32, 0x3A, 0x3A, 0x36, 0x36, 0x3E, 0x3E, 0x40, 0x40, 0x48, 0x48, 0x44, 0x44, 0x4C, 0x4C, 0x42, 0x42, 0x4A, 0x4A, 0x46, 0x46, 0x4E, 0x4E, 0x50, 0x50, 0x58, 0x58, 0x54, 0x54, 0x5C, 0x5C, 0x52, 0x52, 0x5A, 0x5A, 0x56, 0x56, 0x5E, 0x5E, 0x60, 0x60, 0x68, 0x68, 0x64, 0x64, 0x6C, 0x6C, 0x62, 0x62, 0x6A, 0x6A, 0x66, 0x66, 0x6E, 0x6E, 0x70, 0x70, 0x78, 0x78, 0x74, 0x74, 0x7C, 0x7C, 0x72, 0x72, 0x7A, 0x7A, 0x76, 0x76, 0x7E, 0x7E, 0x80, 0x80, 0x88, 0x88, 0x84, 0x84, 0x8C, 0x8C, 0x82, 0x82, 0x8A, 0x8A, 0x86, 0x86, 0x8E, 0x8E, 0x90, 0x90, 0x98, 0x98, 0x94, 0x94, 0x9C, 0x9C, 0x92, 0x92, 0x9A, 0x9A, 0x96, 0x96, 0x9E, 0x9E, 0xA0, 0xA0, 0xA8, 0xA8, 0xA4, 0xA4, 0xAC, 0xAC, 0xA2, 0xA2, 0xAA, 0xAA, 0xA6, 0xA6, 0xAE, 0xAE, 0xB0, 0xB0, 0xB8, 0xB8, 0xB4, 0xB4, 0xBC, 0xBC, 0xB2, 0xB2, 0xBA, 0xBA, 0xB6, 0xB6, 0xBE, 0xBE, 0xC0, 0xC0, 0xC8, 0xC8, 0xC4, 0xC4, 0xCC, 0xCC, 0xC2, 0xC2, 0xCA, 0xCA, 0xC6, 0xC6, 0xCE, 0xCE, 0xD0, 0xD0, 0xD8, 0xD8, 0xD4, 0xD4, 0xDC, 0xDC, 0xD2, 0xD2, 0xDA, 0xDA, 0xD6, 0xD6, 0xDE, 0xDE, 0xE0, 0xE0, 0xE8, 0xE8, 0xE4, 0xE4, 0xEC, 0xEC, 0xE2, 0xE2, 0xEA, 0xEA, 0xE6, 0xE6, 0xEE, 0xEE, 0xF0, 0xF0, 0xF8, 0xF8, 0xF4, 0xF4, 0xFC, 0xFC, 0xF2, 0xF2, 0xFA, 0xFA, 0xF6, 0xF6, 0xFE, 0xFE ]; // Mapping tables for the PC-2 transformation. static $pc2mapc1 = [ 0x00000000, 0x00000400, 0x00200000, 0x00200400, 0x00000001, 0x00000401, 0x00200001, 0x00200401, 0x02000000, 0x02000400, 0x02200000, 0x02200400, 0x02000001, 0x02000401, 0x02200001, 0x02200401 ]; static $pc2mapc2 = [ 0x00000000, 0x00000800, 0x08000000, 0x08000800, 0x00010000, 0x00010800, 0x08010000, 0x08010800, 0x00000000, 0x00000800, 0x08000000, 0x08000800, 0x00010000, 0x00010800, 0x08010000, 0x08010800, 0x00000100, 0x00000900, 0x08000100, 0x08000900, 0x00010100, 0x00010900, 0x08010100, 0x08010900, 0x00000100, 0x00000900, 0x08000100, 0x08000900, 0x00010100, 0x00010900, 0x08010100, 0x08010900, 0x00000010, 0x00000810, 0x08000010, 0x08000810, 0x00010010, 0x00010810, 0x08010010, 0x08010810, 0x00000010, 0x00000810, 0x08000010, 0x08000810, 0x00010010, 0x00010810, 0x08010010, 0x08010810, 0x00000110, 0x00000910, 0x08000110, 0x08000910, 0x00010110, 0x00010910, 0x08010110, 0x08010910, 0x00000110, 0x00000910, 0x08000110, 0x08000910, 0x00010110, 0x00010910, 0x08010110, 0x08010910, 0x00040000, 0x00040800, 0x08040000, 0x08040800, 0x00050000, 0x00050800, 0x08050000, 0x08050800, 0x00040000, 0x00040800, 0x08040000, 0x08040800, 0x00050000, 0x00050800, 0x08050000, 0x08050800, 0x00040100, 0x00040900, 0x08040100, 0x08040900, 0x00050100, 0x00050900, 0x08050100, 0x08050900, 0x00040100, 0x00040900, 0x08040100, 0x08040900, 0x00050100, 0x00050900, 0x08050100, 0x08050900, 0x00040010, 0x00040810, 0x08040010, 0x08040810, 0x00050010, 0x00050810, 0x08050010, 0x08050810, 0x00040010, 0x00040810, 0x08040010, 0x08040810, 0x00050010, 0x00050810, 0x08050010, 0x08050810, 0x00040110, 0x00040910, 0x08040110, 0x08040910, 0x00050110, 0x00050910, 0x08050110, 0x08050910, 0x00040110, 0x00040910, 0x08040110, 0x08040910, 0x00050110, 0x00050910, 0x08050110, 0x08050910, 0x01000000, 0x01000800, 0x09000000, 0x09000800, 0x01010000, 0x01010800, 0x09010000, 0x09010800, 0x01000000, 0x01000800, 0x09000000, 0x09000800, 0x01010000, 0x01010800, 0x09010000, 0x09010800, 0x01000100, 0x01000900, 0x09000100, 0x09000900, 0x01010100, 0x01010900, 0x09010100, 0x09010900, 0x01000100, 0x01000900, 0x09000100, 0x09000900, 0x01010100, 0x01010900, 0x09010100, 0x09010900, 0x01000010, 0x01000810, 0x09000010, 0x09000810, 0x01010010, 0x01010810, 0x09010010, 0x09010810, 0x01000010, 0x01000810, 0x09000010, 0x09000810, 0x01010010, 0x01010810, 0x09010010, 0x09010810, 0x01000110, 0x01000910, 0x09000110, 0x09000910, 0x01010110, 0x01010910, 0x09010110, 0x09010910, 0x01000110, 0x01000910, 0x09000110, 0x09000910, 0x01010110, 0x01010910, 0x09010110, 0x09010910, 0x01040000, 0x01040800, 0x09040000, 0x09040800, 0x01050000, 0x01050800, 0x09050000, 0x09050800, 0x01040000, 0x01040800, 0x09040000, 0x09040800, 0x01050000, 0x01050800, 0x09050000, 0x09050800, 0x01040100, 0x01040900, 0x09040100, 0x09040900, 0x01050100, 0x01050900, 0x09050100, 0x09050900, 0x01040100, 0x01040900, 0x09040100, 0x09040900, 0x01050100, 0x01050900, 0x09050100, 0x09050900, 0x01040010, 0x01040810, 0x09040010, 0x09040810, 0x01050010, 0x01050810, 0x09050010, 0x09050810, 0x01040010, 0x01040810, 0x09040010, 0x09040810, 0x01050010, 0x01050810, 0x09050010, 0x09050810, 0x01040110, 0x01040910, 0x09040110, 0x09040910, 0x01050110, 0x01050910, 0x09050110, 0x09050910, 0x01040110, 0x01040910, 0x09040110, 0x09040910, 0x01050110, 0x01050910, 0x09050110, 0x09050910 ]; static $pc2mapc3 = [ 0x00000000, 0x00000004, 0x00001000, 0x00001004, 0x00000000, 0x00000004, 0x00001000, 0x00001004, 0x10000000, 0x10000004, 0x10001000, 0x10001004, 0x10000000, 0x10000004, 0x10001000, 0x10001004, 0x00000020, 0x00000024, 0x00001020, 0x00001024, 0x00000020, 0x00000024, 0x00001020, 0x00001024, 0x10000020, 0x10000024, 0x10001020, 0x10001024, 0x10000020, 0x10000024, 0x10001020, 0x10001024, 0x00080000, 0x00080004, 0x00081000, 0x00081004, 0x00080000, 0x00080004, 0x00081000, 0x00081004, 0x10080000, 0x10080004, 0x10081000, 0x10081004, 0x10080000, 0x10080004, 0x10081000, 0x10081004, 0x00080020, 0x00080024, 0x00081020, 0x00081024, 0x00080020, 0x00080024, 0x00081020, 0x00081024, 0x10080020, 0x10080024, 0x10081020, 0x10081024, 0x10080020, 0x10080024, 0x10081020, 0x10081024, 0x20000000, 0x20000004, 0x20001000, 0x20001004, 0x20000000, 0x20000004, 0x20001000, 0x20001004, 0x30000000, 0x30000004, 0x30001000, 0x30001004, 0x30000000, 0x30000004, 0x30001000, 0x30001004, 0x20000020, 0x20000024, 0x20001020, 0x20001024, 0x20000020, 0x20000024, 0x20001020, 0x20001024, 0x30000020, 0x30000024, 0x30001020, 0x30001024, 0x30000020, 0x30000024, 0x30001020, 0x30001024, 0x20080000, 0x20080004, 0x20081000, 0x20081004, 0x20080000, 0x20080004, 0x20081000, 0x20081004, 0x30080000, 0x30080004, 0x30081000, 0x30081004, 0x30080000, 0x30080004, 0x30081000, 0x30081004, 0x20080020, 0x20080024, 0x20081020, 0x20081024, 0x20080020, 0x20080024, 0x20081020, 0x20081024, 0x30080020, 0x30080024, 0x30081020, 0x30081024, 0x30080020, 0x30080024, 0x30081020, 0x30081024, 0x00000002, 0x00000006, 0x00001002, 0x00001006, 0x00000002, 0x00000006, 0x00001002, 0x00001006, 0x10000002, 0x10000006, 0x10001002, 0x10001006, 0x10000002, 0x10000006, 0x10001002, 0x10001006, 0x00000022, 0x00000026, 0x00001022, 0x00001026, 0x00000022, 0x00000026, 0x00001022, 0x00001026, 0x10000022, 0x10000026, 0x10001022, 0x10001026, 0x10000022, 0x10000026, 0x10001022, 0x10001026, 0x00080002, 0x00080006, 0x00081002, 0x00081006, 0x00080002, 0x00080006, 0x00081002, 0x00081006, 0x10080002, 0x10080006, 0x10081002, 0x10081006, 0x10080002, 0x10080006, 0x10081002, 0x10081006, 0x00080022, 0x00080026, 0x00081022, 0x00081026, 0x00080022, 0x00080026, 0x00081022, 0x00081026, 0x10080022, 0x10080026, 0x10081022, 0x10081026, 0x10080022, 0x10080026, 0x10081022, 0x10081026, 0x20000002, 0x20000006, 0x20001002, 0x20001006, 0x20000002, 0x20000006, 0x20001002, 0x20001006, 0x30000002, 0x30000006, 0x30001002, 0x30001006, 0x30000002, 0x30000006, 0x30001002, 0x30001006, 0x20000022, 0x20000026, 0x20001022, 0x20001026, 0x20000022, 0x20000026, 0x20001022, 0x20001026, 0x30000022, 0x30000026, 0x30001022, 0x30001026, 0x30000022, 0x30000026, 0x30001022, 0x30001026, 0x20080002, 0x20080006, 0x20081002, 0x20081006, 0x20080002, 0x20080006, 0x20081002, 0x20081006, 0x30080002, 0x30080006, 0x30081002, 0x30081006, 0x30080002, 0x30080006, 0x30081002, 0x30081006, 0x20080022, 0x20080026, 0x20081022, 0x20081026, 0x20080022, 0x20080026, 0x20081022, 0x20081026, 0x30080022, 0x30080026, 0x30081022, 0x30081026, 0x30080022, 0x30080026, 0x30081022, 0x30081026 ]; static $pc2mapc4 = [ 0x00000000, 0x00100000, 0x00000008, 0x00100008, 0x00000200, 0x00100200, 0x00000208, 0x00100208, 0x00000000, 0x00100000, 0x00000008, 0x00100008, 0x00000200, 0x00100200, 0x00000208, 0x00100208, 0x04000000, 0x04100000, 0x04000008, 0x04100008, 0x04000200, 0x04100200, 0x04000208, 0x04100208, 0x04000000, 0x04100000, 0x04000008, 0x04100008, 0x04000200, 0x04100200, 0x04000208, 0x04100208, 0x00002000, 0x00102000, 0x00002008, 0x00102008, 0x00002200, 0x00102200, 0x00002208, 0x00102208, 0x00002000, 0x00102000, 0x00002008, 0x00102008, 0x00002200, 0x00102200, 0x00002208, 0x00102208, 0x04002000, 0x04102000, 0x04002008, 0x04102008, 0x04002200, 0x04102200, 0x04002208, 0x04102208, 0x04002000, 0x04102000, 0x04002008, 0x04102008, 0x04002200, 0x04102200, 0x04002208, 0x04102208, 0x00000000, 0x00100000, 0x00000008, 0x00100008, 0x00000200, 0x00100200, 0x00000208, 0x00100208, 0x00000000, 0x00100000, 0x00000008, 0x00100008, 0x00000200, 0x00100200, 0x00000208, 0x00100208, 0x04000000, 0x04100000, 0x04000008, 0x04100008, 0x04000200, 0x04100200, 0x04000208, 0x04100208, 0x04000000, 0x04100000, 0x04000008, 0x04100008, 0x04000200, 0x04100200, 0x04000208, 0x04100208, 0x00002000, 0x00102000, 0x00002008, 0x00102008, 0x00002200, 0x00102200, 0x00002208, 0x00102208, 0x00002000, 0x00102000, 0x00002008, 0x00102008, 0x00002200, 0x00102200, 0x00002208, 0x00102208, 0x04002000, 0x04102000, 0x04002008, 0x04102008, 0x04002200, 0x04102200, 0x04002208, 0x04102208, 0x04002000, 0x04102000, 0x04002008, 0x04102008, 0x04002200, 0x04102200, 0x04002208, 0x04102208, 0x00020000, 0x00120000, 0x00020008, 0x00120008, 0x00020200, 0x00120200, 0x00020208, 0x00120208, 0x00020000, 0x00120000, 0x00020008, 0x00120008, 0x00020200, 0x00120200, 0x00020208, 0x00120208, 0x04020000, 0x04120000, 0x04020008, 0x04120008, 0x04020200, 0x04120200, 0x04020208, 0x04120208, 0x04020000, 0x04120000, 0x04020008, 0x04120008, 0x04020200, 0x04120200, 0x04020208, 0x04120208, 0x00022000, 0x00122000, 0x00022008, 0x00122008, 0x00022200, 0x00122200, 0x00022208, 0x00122208, 0x00022000, 0x00122000, 0x00022008, 0x00122008, 0x00022200, 0x00122200, 0x00022208, 0x00122208, 0x04022000, 0x04122000, 0x04022008, 0x04122008, 0x04022200, 0x04122200, 0x04022208, 0x04122208, 0x04022000, 0x04122000, 0x04022008, 0x04122008, 0x04022200, 0x04122200, 0x04022208, 0x04122208, 0x00020000, 0x00120000, 0x00020008, 0x00120008, 0x00020200, 0x00120200, 0x00020208, 0x00120208, 0x00020000, 0x00120000, 0x00020008, 0x00120008, 0x00020200, 0x00120200, 0x00020208, 0x00120208, 0x04020000, 0x04120000, 0x04020008, 0x04120008, 0x04020200, 0x04120200, 0x04020208, 0x04120208, 0x04020000, 0x04120000, 0x04020008, 0x04120008, 0x04020200, 0x04120200, 0x04020208, 0x04120208, 0x00022000, 0x00122000, 0x00022008, 0x00122008, 0x00022200, 0x00122200, 0x00022208, 0x00122208, 0x00022000, 0x00122000, 0x00022008, 0x00122008, 0x00022200, 0x00122200, 0x00022208, 0x00122208, 0x04022000, 0x04122000, 0x04022008, 0x04122008, 0x04022200, 0x04122200, 0x04022208, 0x04122208, 0x04022000, 0x04122000, 0x04022008, 0x04122008, 0x04022200, 0x04122200, 0x04022208, 0x04122208 ]; static $pc2mapd1 = [ 0x00000000, 0x00000001, 0x08000000, 0x08000001, 0x00200000, 0x00200001, 0x08200000, 0x08200001, 0x00000002, 0x00000003, 0x08000002, 0x08000003, 0x00200002, 0x00200003, 0x08200002, 0x08200003 ]; static $pc2mapd2 = [ 0x00000000, 0x00100000, 0x00000800, 0x00100800, 0x00000000, 0x00100000, 0x00000800, 0x00100800, 0x04000000, 0x04100000, 0x04000800, 0x04100800, 0x04000000, 0x04100000, 0x04000800, 0x04100800, 0x00000004, 0x00100004, 0x00000804, 0x00100804, 0x00000004, 0x00100004, 0x00000804, 0x00100804, 0x04000004, 0x04100004, 0x04000804, 0x04100804, 0x04000004, 0x04100004, 0x04000804, 0x04100804, 0x00000000, 0x00100000, 0x00000800, 0x00100800, 0x00000000, 0x00100000, 0x00000800, 0x00100800, 0x04000000, 0x04100000, 0x04000800, 0x04100800, 0x04000000, 0x04100000, 0x04000800, 0x04100800, 0x00000004, 0x00100004, 0x00000804, 0x00100804, 0x00000004, 0x00100004, 0x00000804, 0x00100804, 0x04000004, 0x04100004, 0x04000804, 0x04100804, 0x04000004, 0x04100004, 0x04000804, 0x04100804, 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, 0x00020000, 0x00120000, 0x00020800, 0x00120800, 0x00020000, 0x00120000, 0x00020800, 0x00120800, 0x04020000, 0x04120000, 0x04020800, 0x04120800, 0x04020000, 0x04120000, 0x04020800, 0x04120800, 0x00020004, 0x00120004, 0x00020804, 0x00120804, 0x00020004, 0x00120004, 0x00020804, 0x00120804, 0x04020004, 0x04120004, 0x04020804, 0x04120804, 0x04020004, 0x04120004, 0x04020804, 0x04120804, 0x00020000, 0x00120000, 0x00020800, 0x00120800, 0x00020000, 0x00120000, 0x00020800, 0x00120800, 0x04020000, 0x04120000, 0x04020800, 0x04120800, 0x04020000, 0x04120000, 0x04020800, 0x04120800, 0x00020004, 0x00120004, 0x00020804, 0x00120804, 0x00020004, 0x00120004, 0x00020804, 0x00120804, 0x04020004, 0x04120004, 0x04020804, 0x04120804, 0x04020004, 0x04120004, 0x04020804, 0x04120804, 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, 0x04020204, 0x04120204, 0x04020A04, 0x04120A04, 0x04020204, 0x04120204, 0x04020A04, 0x04120A04, 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, 0x04020204, 0x04120204, 0x04020A04, 0x04120A04, 0x04020204, 0x04120204, 0x04020A04, 0x04120A04 ]; static $pc2mapd3 = [ 0x00000000, 0x00010000, 0x02000000, 0x02010000, 0x00000020, 0x00010020, 0x02000020, 0x02010020, 0x00040000, 0x00050000, 0x02040000, 0x02050000, 0x00040020, 0x00050020, 0x02040020, 0x02050020, 0x00002000, 0x00012000, 0x02002000, 0x02012000, 0x00002020, 0x00012020, 0x02002020, 0x02012020, 0x00042000, 0x00052000, 0x02042000, 0x02052000, 0x00042020, 0x00052020, 0x02042020, 0x02052020, 0x00000000, 0x00010000, 0x02000000, 0x02010000, 0x00000020, 0x00010020, 0x02000020, 0x02010020, 0x00040000, 0x00050000, 0x02040000, 0x02050000, 0x00040020, 0x00050020, 0x02040020, 0x02050020, 0x00002000, 0x00012000, 0x02002000, 0x02012000, 0x00002020, 0x00012020, 0x02002020, 0x02012020, 0x00042000, 0x00052000, 0x02042000, 0x02052000, 0x00042020, 0x00052020, 0x02042020, 0x02052020, 0x00000010, 0x00010010, 0x02000010, 0x02010010, 0x00000030, 0x00010030, 0x02000030, 0x02010030, 0x00040010, 0x00050010, 0x02040010, 0x02050010, 0x00040030, 0x00050030, 0x02040030, 0x02050030, 0x00002010, 0x00012010, 0x02002010, 0x02012010, 0x00002030, 0x00012030, 0x02002030, 0x02012030, 0x00042010, 0x00052010, 0x02042010, 0x02052010, 0x00042030, 0x00052030, 0x02042030, 0x02052030, 0x00000010, 0x00010010, 0x02000010, 0x02010010, 0x00000030, 0x00010030, 0x02000030, 0x02010030, 0x00040010, 0x00050010, 0x02040010, 0x02050010, 0x00040030, 0x00050030, 0x02040030, 0x02050030, 0x00002010, 0x00012010, 0x02002010, 0x02012010, 0x00002030, 0x00012030, 0x02002030, 0x02012030, 0x00042010, 0x00052010, 0x02042010, 0x02052010, 0x00042030, 0x00052030, 0x02042030, 0x02052030, 0x20000000, 0x20010000, 0x22000000, 0x22010000, 0x20000020, 0x20010020, 0x22000020, 0x22010020, 0x20040000, 0x20050000, 0x22040000, 0x22050000, 0x20040020, 0x20050020, 0x22040020, 0x22050020, 0x20002000, 0x20012000, 0x22002000, 0x22012000, 0x20002020, 0x20012020, 0x22002020, 0x22012020, 0x20042000, 0x20052000, 0x22042000, 0x22052000, 0x20042020, 0x20052020, 0x22042020, 0x22052020, 0x20000000, 0x20010000, 0x22000000, 0x22010000, 0x20000020, 0x20010020, 0x22000020, 0x22010020, 0x20040000, 0x20050000, 0x22040000, 0x22050000, 0x20040020, 0x20050020, 0x22040020, 0x22050020, 0x20002000, 0x20012000, 0x22002000, 0x22012000, 0x20002020, 0x20012020, 0x22002020, 0x22012020, 0x20042000, 0x20052000, 0x22042000, 0x22052000, 0x20042020, 0x20052020, 0x22042020, 0x22052020, 0x20000010, 0x20010010, 0x22000010, 0x22010010, 0x20000030, 0x20010030, 0x22000030, 0x22010030, 0x20040010, 0x20050010, 0x22040010, 0x22050010, 0x20040030, 0x20050030, 0x22040030, 0x22050030, 0x20002010, 0x20012010, 0x22002010, 0x22012010, 0x20002030, 0x20012030, 0x22002030, 0x22012030, 0x20042010, 0x20052010, 0x22042010, 0x22052010, 0x20042030, 0x20052030, 0x22042030, 0x22052030, 0x20000010, 0x20010010, 0x22000010, 0x22010010, 0x20000030, 0x20010030, 0x22000030, 0x22010030, 0x20040010, 0x20050010, 0x22040010, 0x22050010, 0x20040030, 0x20050030, 0x22040030, 0x22050030, 0x20002010, 0x20012010, 0x22002010, 0x22012010, 0x20002030, 0x20012030, 0x22002030, 0x22012030, 0x20042010, 0x20052010, 0x22042010, 0x22052010, 0x20042030, 0x20052030, 0x22042030, 0x22052030 ]; static $pc2mapd4 = [ 0x00000000, 0x00000400, 0x01000000, 0x01000400, 0x00000000, 0x00000400, 0x01000000, 0x01000400, 0x00000100, 0x00000500, 0x01000100, 0x01000500, 0x00000100, 0x00000500, 0x01000100, 0x01000500, 0x10000000, 0x10000400, 0x11000000, 0x11000400, 0x10000000, 0x10000400, 0x11000000, 0x11000400, 0x10000100, 0x10000500, 0x11000100, 0x11000500, 0x10000100, 0x10000500, 0x11000100, 0x11000500, 0x00080000, 0x00080400, 0x01080000, 0x01080400, 0x00080000, 0x00080400, 0x01080000, 0x01080400, 0x00080100, 0x00080500, 0x01080100, 0x01080500, 0x00080100, 0x00080500, 0x01080100, 0x01080500, 0x10080000, 0x10080400, 0x11080000, 0x11080400, 0x10080000, 0x10080400, 0x11080000, 0x11080400, 0x10080100, 0x10080500, 0x11080100, 0x11080500, 0x10080100, 0x10080500, 0x11080100, 0x11080500, 0x00000008, 0x00000408, 0x01000008, 0x01000408, 0x00000008, 0x00000408, 0x01000008, 0x01000408, 0x00000108, 0x00000508, 0x01000108, 0x01000508, 0x00000108, 0x00000508, 0x01000108, 0x01000508, 0x10000008, 0x10000408, 0x11000008, 0x11000408, 0x10000008, 0x10000408, 0x11000008, 0x11000408, 0x10000108, 0x10000508, 0x11000108, 0x11000508, 0x10000108, 0x10000508, 0x11000108, 0x11000508, 0x00080008, 0x00080408, 0x01080008, 0x01080408, 0x00080008, 0x00080408, 0x01080008, 0x01080408, 0x00080108, 0x00080508, 0x01080108, 0x01080508, 0x00080108, 0x00080508, 0x01080108, 0x01080508, 0x10080008, 0x10080408, 0x11080008, 0x11080408, 0x10080008, 0x10080408, 0x11080008, 0x11080408, 0x10080108, 0x10080508, 0x11080108, 0x11080508, 0x10080108, 0x10080508, 0x11080108, 0x11080508, 0x00001000, 0x00001400, 0x01001000, 0x01001400, 0x00001000, 0x00001400, 0x01001000, 0x01001400, 0x00001100, 0x00001500, 0x01001100, 0x01001500, 0x00001100, 0x00001500, 0x01001100, 0x01001500, 0x10001000, 0x10001400, 0x11001000, 0x11001400, 0x10001000, 0x10001400, 0x11001000, 0x11001400, 0x10001100, 0x10001500, 0x11001100, 0x11001500, 0x10001100, 0x10001500, 0x11001100, 0x11001500, 0x00081000, 0x00081400, 0x01081000, 0x01081400, 0x00081000, 0x00081400, 0x01081000, 0x01081400, 0x00081100, 0x00081500, 0x01081100, 0x01081500, 0x00081100, 0x00081500, 0x01081100, 0x01081500, 0x10081000, 0x10081400, 0x11081000, 0x11081400, 0x10081000, 0x10081400, 0x11081000, 0x11081400, 0x10081100, 0x10081500, 0x11081100, 0x11081500, 0x10081100, 0x10081500, 0x11081100, 0x11081500, 0x00001008, 0x00001408, 0x01001008, 0x01001408, 0x00001008, 0x00001408, 0x01001008, 0x01001408, 0x00001108, 0x00001508, 0x01001108, 0x01001508, 0x00001108, 0x00001508, 0x01001108, 0x01001508, 0x10001008, 0x10001408, 0x11001008, 0x11001408, 0x10001008, 0x10001408, 0x11001008, 0x11001408, 0x10001108, 0x10001508, 0x11001108, 0x11001508, 0x10001108, 0x10001508, 0x11001108, 0x11001508, 0x00081008, 0x00081408, 0x01081008, 0x01081408, 0x00081008, 0x00081408, 0x01081008, 0x01081408, 0x00081108, 0x00081508, 0x01081108, 0x01081508, 0x00081108, 0x00081508, 0x01081108, 0x01081508, 0x10081008, 0x10081408, 0x11081008, 0x11081408, 0x10081008, 0x10081408, 0x11081008, 0x11081408, 0x10081108, 0x10081508, 0x11081108, 0x11081508, 0x10081108, 0x10081508, 0x11081108, 0x11081508 ]; $keys = []; for ($des_round = 0; $des_round < $this->des_rounds; ++$des_round) { // pad the key and remove extra characters as appropriate. $key = str_pad(substr($this->key, $des_round * 8, 8), 8, "\0"); // Perform the PC/1 transformation and compute C and D. $t = unpack('Nl/Nr', $key); list($l, $r) = [$t['l'], $t['r']]; $key = (self::$shuffle[$pc1map[ $r & 0xFF]] & "\x80\x80\x80\x80\x80\x80\x80\x00") | (self::$shuffle[$pc1map[($r >> 8) & 0xFF]] & "\x40\x40\x40\x40\x40\x40\x40\x00") | (self::$shuffle[$pc1map[($r >> 16) & 0xFF]] & "\x20\x20\x20\x20\x20\x20\x20\x00") | (self::$shuffle[$pc1map[($r >> 24) & 0xFF]] & "\x10\x10\x10\x10\x10\x10\x10\x00") | (self::$shuffle[$pc1map[ $l & 0xFF]] & "\x08\x08\x08\x08\x08\x08\x08\x00") | (self::$shuffle[$pc1map[($l >> 8) & 0xFF]] & "\x04\x04\x04\x04\x04\x04\x04\x00") | (self::$shuffle[$pc1map[($l >> 16) & 0xFF]] & "\x02\x02\x02\x02\x02\x02\x02\x00") | (self::$shuffle[$pc1map[($l >> 24) & 0xFF]] & "\x01\x01\x01\x01\x01\x01\x01\x00"); $key = unpack('Nc/Nd', $key); $c = ( $key['c'] >> 4) & 0x0FFFFFFF; $d = (($key['d'] >> 4) & 0x0FFFFFF0) | ($key['c'] & 0x0F); $keys[$des_round] = [ self::ENCRYPT => [], self::DECRYPT => array_fill(0, 32, 0) ]; for ($i = 0, $ki = 31; $i < 16; ++$i, $ki -= 2) { $c <<= $shifts[$i]; $c = ($c | ($c >> 28)) & 0x0FFFFFFF; $d <<= $shifts[$i]; $d = ($d | ($d >> 28)) & 0x0FFFFFFF; // Perform the PC-2 transformation. $cp = $pc2mapc1[ $c >> 24 ] | $pc2mapc2[($c >> 16) & 0xFF] | $pc2mapc3[($c >> 8) & 0xFF] | $pc2mapc4[ $c & 0xFF]; $dp = $pc2mapd1[ $d >> 24 ] | $pc2mapd2[($d >> 16) & 0xFF] | $pc2mapd3[($d >> 8) & 0xFF] | $pc2mapd4[ $d & 0xFF]; // Reorder: odd bytes/even bytes. Push the result in key schedule. $val1 = ( $cp & intval(0xFF000000)) | (($cp << 8) & 0x00FF0000) | (($dp >> 16) & 0x0000FF00) | (($dp >> 8) & 0x000000FF); $val2 = (($cp << 8) & intval(0xFF000000)) | (($cp << 16) & 0x00FF0000) | (($dp >> 8) & 0x0000FF00) | ( $dp & 0x000000FF); $keys[$des_round][self::ENCRYPT][ ] = $val1; $keys[$des_round][self::DECRYPT][$ki - 1] = $val1; $keys[$des_round][self::ENCRYPT][ ] = $val2; $keys[$des_round][self::DECRYPT][$ki ] = $val2; } } switch ($this->des_rounds) { case 3: // 3DES keys $this->keys = [ self::ENCRYPT => array_merge( $keys[0][self::ENCRYPT], $keys[1][self::DECRYPT], $keys[2][self::ENCRYPT] ), self::DECRYPT => array_merge( $keys[2][self::DECRYPT], $keys[1][self::ENCRYPT], $keys[0][self::DECRYPT] ) ]; break; // case 1: // DES keys default: $this->keys = [ self::ENCRYPT => $keys[0][self::ENCRYPT], self::DECRYPT => $keys[0][self::DECRYPT] ]; } } /** * Setup the performance-optimized function for de/encrypt() * * @see \phpseclib3\Crypt\Common\SymmetricKey::setupInlineCrypt() */ protected function setupInlineCrypt() { // Engine configuration for: // - DES ($des_rounds == 1) or // - 3DES ($des_rounds == 3) $des_rounds = $this->des_rounds; $init_crypt = 'static $sbox1, $sbox2, $sbox3, $sbox4, $sbox5, $sbox6, $sbox7, $sbox8, $shuffleip, $shuffleinvip; if (!$sbox1) { $sbox1 = array_map("intval", self::$sbox1); $sbox2 = array_map("intval", self::$sbox2); $sbox3 = array_map("intval", self::$sbox3); $sbox4 = array_map("intval", self::$sbox4); $sbox5 = array_map("intval", self::$sbox5); $sbox6 = array_map("intval", self::$sbox6); $sbox7 = array_map("intval", self::$sbox7); $sbox8 = array_map("intval", self::$sbox8);' /* Merge $shuffle with $[inv]ipmap */ . ' for ($i = 0; $i < 256; ++$i) { $shuffleip[] = self::$shuffle[self::$ipmap[$i]]; $shuffleinvip[] = self::$shuffle[self::$invipmap[$i]]; } } '; $k = [ self::ENCRYPT => $this->keys[self::ENCRYPT], self::DECRYPT => $this->keys[self::DECRYPT] ]; $init_encrypt = ''; $init_decrypt = ''; // Creating code for en- and decryption. $crypt_block = []; foreach ([self::ENCRYPT, self::DECRYPT] as $c) { /* Do the initial IP permutation. */ $crypt_block[$c] = ' $in = unpack("N*", $in); $l = $in[1]; $r = $in[2]; $in = unpack("N*", ($shuffleip[ $r & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | ($shuffleip[($r >> 8) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | ($shuffleip[($r >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | ($shuffleip[($r >> 24) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | ($shuffleip[ $l & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | ($shuffleip[($l >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | ($shuffleip[($l >> 16) & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | ($shuffleip[($l >> 24) & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01") ); ' . /* Extract L0 and R0 */ ' $l = $in[1]; $r = $in[2]; '; $l = '$l'; $r = '$r'; // Perform DES or 3DES. for ($ki = -1, $des_round = 0; $des_round < $des_rounds; ++$des_round) { // Perform the 16 steps. for ($i = 0; $i < 16; ++$i) { // start of "the Feistel (F) function" - see the following URL: // http://en.wikipedia.org/wiki/Image:Data_Encryption_Standard_InfoBox_Diagram.png // Merge key schedule. $crypt_block[$c] .= ' $b1 = ((' . $r . ' >> 3) & 0x1FFFFFFF) ^ (' . $r . ' << 29) ^ ' . $k[$c][++$ki] . '; $b2 = ((' . $r . ' >> 31) & 0x00000001) ^ (' . $r . ' << 1) ^ ' . $k[$c][++$ki] . ';' . /* S-box indexing. */ $l . ' = $sbox1[($b1 >> 24) & 0x3F] ^ $sbox2[($b2 >> 24) & 0x3F] ^ $sbox3[($b1 >> 16) & 0x3F] ^ $sbox4[($b2 >> 16) & 0x3F] ^ $sbox5[($b1 >> 8) & 0x3F] ^ $sbox6[($b2 >> 8) & 0x3F] ^ $sbox7[ $b1 & 0x3F] ^ $sbox8[ $b2 & 0x3F] ^ ' . $l . '; '; // end of "the Feistel (F) function" // swap L & R list($l, $r) = [$r, $l]; } list($l, $r) = [$r, $l]; } // Perform the inverse IP permutation. $crypt_block[$c] .= '$in = ($shuffleinvip[($l >> 24) & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | ($shuffleinvip[($r >> 24) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | ($shuffleinvip[($l >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | ($shuffleinvip[($r >> 16) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | ($shuffleinvip[($l >> 8) & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | ($shuffleinvip[($r >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | ($shuffleinvip[ $l & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | ($shuffleinvip[ $r & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01"); '; } // Creates the inline-crypt function $this->inline_crypt = $this->createInlineCryptFunction( [ 'init_crypt' => $init_crypt, 'init_encrypt' => $init_encrypt, 'init_decrypt' => $init_decrypt, 'encrypt_block' => $crypt_block[self::ENCRYPT], 'decrypt_block' => $crypt_block[self::DECRYPT] ] ); } } PK!gU JJ1vendor/phpseclib/phpseclib/phpseclib/Crypt/DH.phpnu[ * * * * @author Jim Wigginton * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\DH\Parameters; use phpseclib3\Crypt\DH\PrivateKey; use phpseclib3\Crypt\DH\PublicKey; use phpseclib3\Exception\NoKeyLoadedException; use phpseclib3\Exception\UnsupportedOperationException; use phpseclib3\Math\BigInteger; /** * Pure-PHP (EC)DH implementation * * @author Jim Wigginton */ abstract class DH extends AsymmetricKey { /** * Algorithm Name * * @var string */ const ALGORITHM = 'DH'; /** * DH prime * * @var BigInteger */ protected $prime; /** * DH Base * * Prime divisor of p-1 * * @var BigInteger */ protected $base; /** * Public Key * * @var BigInteger */ protected $publicKey; /** * Create DH parameters * * This method is a bit polymorphic. It can take any of the following: * - two BigInteger's (prime and base) * - an integer representing the size of the prime in bits (the base is assumed to be 2) * - a string (eg. diffie-hellman-group14-sha1) * * @return Parameters */ public static function createParameters(...$args) { $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('createParameters() should not be called from final classes (' . static::class . ')'); } $params = new Parameters(); if (count($args) == 2 && $args[0] instanceof BigInteger && $args[1] instanceof BigInteger) { //if (!$args[0]->isPrime()) { // throw new \InvalidArgumentException('The first parameter should be a prime number'); //} $params->prime = $args[0]; $params->base = $args[1]; return $params; } elseif (count($args) == 1 && is_numeric($args[0])) { $params->prime = BigInteger::randomPrime($args[0]); $params->base = new BigInteger(2); return $params; } elseif (count($args) != 1 || !is_string($args[0])) { throw new \InvalidArgumentException('Valid parameters are either: two BigInteger\'s (prime and base), a single integer (the length of the prime; base is assumed to be 2) or a string'); } switch ($args[0]) { // see http://tools.ietf.org/html/rfc2409#section-6.2 and // http://tools.ietf.org/html/rfc2412, appendex E case 'diffie-hellman-group1-sha1': $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; break; // see http://tools.ietf.org/html/rfc3526#section-3 case 'diffie-hellman-group14-sha1': // 2048-bit MODP Group case 'diffie-hellman-group14-sha256': $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . '3995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF'; break; // see https://tools.ietf.org/html/rfc3526#section-4 case 'diffie-hellman-group15-sha512': // 3072-bit MODP Group $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . '3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33' . 'A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7' . 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864' . 'D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2' . '08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF'; break; // see https://tools.ietf.org/html/rfc3526#section-5 case 'diffie-hellman-group16-sha512': // 4096-bit MODP Group $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . '3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33' . 'A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7' . 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864' . 'D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2' . '08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7' . '88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8' . 'DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2' . '233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9' . '93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF'; break; // see https://tools.ietf.org/html/rfc3526#section-6 case 'diffie-hellman-group17-sha512': // 6144-bit MODP Group $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . '3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33' . 'A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7' . 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864' . 'D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2' . '08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7' . '88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8' . 'DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2' . '233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9' . '93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026' . 'C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AE' . 'B06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B' . 'DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92EC' . 'F032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E' . '59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA' . 'CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76' . 'F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468' . '043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF'; break; // see https://tools.ietf.org/html/rfc3526#section-7 case 'diffie-hellman-group18-sha512': // 8192-bit MODP Group $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . '3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33' . 'A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7' . 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864' . 'D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E2' . '08E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7' . '88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8' . 'DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2' . '233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9' . '93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026' . 'C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AE' . 'B06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B' . 'DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92EC' . 'F032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E' . '59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA' . 'CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76' . 'F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468' . '043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E4' . '38777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED' . '2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652D' . 'E3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B' . '4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6' . '6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851D' . 'F9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F92' . '4009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA' . '9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF'; break; default: throw new \InvalidArgumentException('Invalid named prime provided'); } $params->prime = new BigInteger($prime, 16); $params->base = new BigInteger(2); return $params; } /** * Create public / private key pair. * * The rationale for the second parameter is described in http://tools.ietf.org/html/rfc4419#section-6.2 : * * "To increase the speed of the key exchange, both client and server may * reduce the size of their private exponents. It should be at least * twice as long as the key material that is generated from the shared * secret. For more details, see the paper by van Oorschot and Wiener * [VAN-OORSCHOT]." * * $length is in bits * * @param Parameters $params * @param int $length optional * @return PrivateKey */ public static function createKey(Parameters $params, $length = 0) { $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('createKey() should not be called from final classes (' . static::class . ')'); } $one = new BigInteger(1); if ($length) { $max = $one->bitwise_leftShift($length); $max = $max->subtract($one); } else { $max = $params->prime->subtract($one); } $key = new PrivateKey(); $key->prime = $params->prime; $key->base = $params->base; $key->privateKey = BigInteger::randomRange($one, $max); $key->publicKey = $key->base->powMod($key->privateKey, $key->prime); return $key; } /** * Compute Shared Secret * * @param PrivateKey|EC $private * @param PublicKey|BigInteger|string $public * @return mixed */ public static function computeSecret($private, $public) { if ($private instanceof PrivateKey) { // DH\PrivateKey switch (true) { case $public instanceof PublicKey: if (!$private->prime->equals($public->prime) || !$private->base->equals($public->base)) { throw new \InvalidArgumentException('The public and private key do not share the same prime and / or base numbers'); } return $public->publicKey->powMod($private->privateKey, $private->prime)->toBytes(true); case is_string($public): $public = new BigInteger($public, -256); // fall-through case $public instanceof BigInteger: return $public->powMod($private->privateKey, $private->prime)->toBytes(true); default: throw new \InvalidArgumentException('$public needs to be an instance of DH\PublicKey, a BigInteger or a string'); } } if ($private instanceof EC\PrivateKey) { switch (true) { case $public instanceof EC\PublicKey: $public = $public->getEncodedCoordinates(); // fall-through case is_string($public): $point = $private->multiply($public); switch ($private->getCurve()) { case 'Curve25519': case 'Curve448': $secret = $point; break; default: // according to https://www.secg.org/sec1-v2.pdf#page=33 only X is returned $secret = substr($point, 1, (strlen($point) - 1) >> 1); } /* if (($secret[0] & "\x80") === "\x80") { $secret = "\0$secret"; } */ return $secret; default: throw new \InvalidArgumentException('$public needs to be an instance of EC\PublicKey or a string (an encoded coordinate)'); } } } /** * Load the key * * @param string $key * @param string $password optional * @return AsymmetricKey */ public static function load($key, $password = false) { try { return EC::load($key, $password); } catch (NoKeyLoadedException $e) { } return parent::load($key, $password); } /** * OnLoad Handler * * @return bool */ protected static function onLoad(array $components) { if (!isset($components['privateKey']) && !isset($components['publicKey'])) { $new = new Parameters(); } else { $new = isset($components['privateKey']) ? new PrivateKey() : new PublicKey(); } $new->prime = $components['prime']; $new->base = $components['base']; if (isset($components['privateKey'])) { $new->privateKey = $components['privateKey']; } if (isset($components['publicKey'])) { $new->publicKey = $components['publicKey']; } return $new; } /** * Determines which hashing function should be used * * @param string $hash */ public function withHash($hash) { throw new UnsupportedOperationException('DH does not use a hash algorithm'); } /** * Returns the hash algorithm currently being used * */ public function getHash() { throw new UnsupportedOperationException('DH does not use a hash algorithm'); } /** * Returns the parameters * * A public / private key is only returned if the currently loaded "key" contains an x or y * value. * * @see self::getPublicKey() * @return mixed */ public function getParameters() { $type = DH::validatePlugin('Keys', 'PKCS1', 'saveParameters'); $key = $type::saveParameters($this->prime, $this->base); return DH::load($key, 'PKCS1'); } } PK! Lg+$+$2vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA.phpnu[ * getPublicKey(); * * $plaintext = 'terrafrost'; * * $signature = $private->sign($plaintext); * * echo $public->verify($plaintext, $signature) ? 'verified' : 'unverified'; * ?> * * * @author Jim Wigginton * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\DSA\Parameters; use phpseclib3\Crypt\DSA\PrivateKey; use phpseclib3\Crypt\DSA\PublicKey; use phpseclib3\Exception\InsufficientSetupException; use phpseclib3\Math\BigInteger; /** * Pure-PHP FIPS 186-4 compliant implementation of DSA. * * @author Jim Wigginton */ abstract class DSA extends AsymmetricKey { /** * Algorithm Name * * @var string */ const ALGORITHM = 'DSA'; /** * DSA Prime P * * @var BigInteger */ protected $p; /** * DSA Group Order q * * Prime divisor of p-1 * * @var BigInteger */ protected $q; /** * DSA Group Generator G * * @var BigInteger */ protected $g; /** * DSA public key value y * * @var BigInteger */ protected $y; /** * Signature Format * * @var string */ protected $sigFormat; /** * Signature Format (Short) * * @var string */ protected $shortFormat; /** * Create DSA parameters * * @param int $L * @param int $N * @return DSA|bool */ public static function createParameters($L = 2048, $N = 224) { self::initialize_static_variables(); $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('createParameters() should not be called from final classes (' . static::class . ')'); } if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } switch (true) { case $N == 160: /* in FIPS 186-1 and 186-2 N was fixed at 160 whereas K had an upper bound of 1024. RFC 4253 (SSH Transport Layer Protocol) references FIPS 186-2 and as such most SSH DSA implementations only support keys with an N of 160. puttygen let's you set the size of L (but not the size of N) and uses 2048 as the default L value. that's not really compliant with any of the FIPS standards, however, for the purposes of maintaining compatibility with puttygen, we'll support it */ //case ($L >= 512 || $L <= 1024) && (($L & 0x3F) == 0) && $N == 160: // FIPS 186-3 changed this as follows: //case $L == 1024 && $N == 160: case $L == 2048 && $N == 224: case $L == 2048 && $N == 256: case $L == 3072 && $N == 256: break; default: throw new \InvalidArgumentException('Invalid values for N and L'); } $two = new BigInteger(2); $q = BigInteger::randomPrime($N); $divisor = $q->multiply($two); do { $x = BigInteger::random($L); list(, $c) = $x->divide($divisor); $p = $x->subtract($c->subtract(self::$one)); } while ($p->getLength() != $L || !$p->isPrime()); $p_1 = $p->subtract(self::$one); list($e) = $p_1->divide($q); // quoting http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf#page=50 , // "h could be obtained from a random number generator or from a counter that // changes after each use". PuTTY (sshdssg.c) starts h off at 1 and increments // it on each loop. wikipedia says "commonly h = 2 is used" so we'll just do that $h = clone $two; while (true) { $g = $h->powMod($e, $p); if (!$g->equals(self::$one)) { break; } $h = $h->add(self::$one); } $dsa = new Parameters(); $dsa->p = $p; $dsa->q = $q; $dsa->g = $g; return $dsa; } /** * Create public / private key pair. * * This method is a bit polymorphic. It can take a DSA/Parameters object, L / N as two distinct parameters or * no parameters (at which point L and N will be generated with this method) * * Returns the private key, from which the publickey can be extracted * * @param int[] ...$args * @return PrivateKey */ public static function createKey(...$args) { self::initialize_static_variables(); $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('createKey() should not be called from final classes (' . static::class . ')'); } if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } if (count($args) == 2 && is_int($args[0]) && is_int($args[1])) { $params = self::createParameters($args[0], $args[1]); } elseif (count($args) == 1 && $args[0] instanceof Parameters) { $params = $args[0]; } elseif (!count($args)) { $params = self::createParameters(); } else { throw new InsufficientSetupException('Valid parameters are either two integers (L and N), a single DSA object or no parameters at all.'); } $private = new PrivateKey(); $private->p = $params->p; $private->q = $params->q; $private->g = $params->g; $private->x = BigInteger::randomRange(self::$one, $private->q->subtract(self::$one)); $private->y = $private->g->powMod($private->x, $private->p); //$public = clone $private; //unset($public->x); return $private ->withHash($params->hash->getHash()) ->withSignatureFormat($params->shortFormat); } /** * OnLoad Handler * * @return bool */ protected static function onLoad(array $components) { if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } if (!isset($components['x']) && !isset($components['y'])) { $new = new Parameters(); } elseif (isset($components['x'])) { $new = new PrivateKey(); $new->x = $components['x']; } else { $new = new PublicKey(); } $new->p = $components['p']; $new->q = $components['q']; $new->g = $components['g']; if (isset($components['y'])) { $new->y = $components['y']; } return $new; } /** * Constructor * * PublicKey and PrivateKey objects can only be created from abstract RSA class */ protected function __construct() { $this->sigFormat = self::validatePlugin('Signature', 'ASN1'); $this->shortFormat = 'ASN1'; parent::__construct(); } /** * Returns the key size * * More specifically, this L (the length of DSA Prime P) and N (the length of DSA Group Order q) * * @return array */ public function getLength() { return ['L' => $this->p->getLength(), 'N' => $this->q->getLength()]; } /** * Returns the current engine being used * * @see self::useInternalEngine() * @see self::useBestEngine() * @return string */ public function getEngine() { if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } return self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods()) ? 'OpenSSL' : 'PHP'; } /** * Returns the parameters * * A public / private key is only returned if the currently loaded "key" contains an x or y * value. * * @see self::getPublicKey() * @return mixed */ public function getParameters() { $type = self::validatePlugin('Keys', 'PKCS1', 'saveParameters'); $key = $type::saveParameters($this->p, $this->q, $this->g); return DSA::load($key, 'PKCS1') ->withHash($this->hash->getHash()) ->withSignatureFormat($this->shortFormat); } /** * Determines the signature padding mode * * Valid values are: ASN1, SSH2, Raw * * @param string $format */ public function withSignatureFormat($format) { $new = clone $this; $new->shortFormat = $format; $new->sigFormat = self::validatePlugin('Signature', $format); return $new; } /** * Returns the signature format currently being used * */ public function getSignatureFormat() { return $this->shortFormat; } } PK!].4.41vendor/phpseclib/phpseclib/phpseclib/Crypt/EC.phpnu[ * getPublicKey(); * * $plaintext = 'terrafrost'; * * $signature = $private->sign($plaintext); * * echo $public->verify($plaintext, $signature) ? 'verified' : 'unverified'; * ?> * * * @author Jim Wigginton * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; use phpseclib3\Crypt\EC\Curves\Curve25519; use phpseclib3\Crypt\EC\Curves\Ed25519; use phpseclib3\Crypt\EC\Curves\Ed448; use phpseclib3\Crypt\EC\Formats\Keys\PKCS1; use phpseclib3\Crypt\EC\Parameters; use phpseclib3\Crypt\EC\PrivateKey; use phpseclib3\Crypt\EC\PublicKey; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\Exception\UnsupportedOperationException; use phpseclib3\File\ASN1; use phpseclib3\File\ASN1\Maps\ECParameters; use phpseclib3\Math\BigInteger; /** * Pure-PHP implementation of EC. * * @author Jim Wigginton */ abstract class EC extends AsymmetricKey { /** * Algorithm Name * * @var string */ const ALGORITHM = 'EC'; /** * Public Key QA * * @var object[] */ protected $QA; /** * Curve * * @var EC\BaseCurves\Base */ protected $curve; /** * Signature Format * * @var string */ protected $format; /** * Signature Format (Short) * * @var string */ protected $shortFormat; /** * Curve Name * * @var string */ private $curveName; /** * Curve Order * * Used for deterministic ECDSA * * @var BigInteger */ protected $q; /** * Alias for the private key * * Used for deterministic ECDSA. AsymmetricKey expects $x. I don't like x because * with x you have x * the base point yielding an (x, y)-coordinate that is the * public key. But the x is different depending on which side of the equal sign * you're on. It's less ambiguous if you do dA * base point = (x, y)-coordinate. * * @var BigInteger */ protected $x; /** * Context * * @var string */ protected $context; /** * Signature Format * * @var string */ protected $sigFormat; /** * Create public / private key pair. * * @param string $curve * @return PrivateKey */ public static function createKey($curve) { self::initialize_static_variables(); $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('createKey() should not be called from final classes (' . static::class . ')'); } if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } $curve = strtolower($curve); if (self::$engines['libsodium'] && $curve == 'ed25519' && function_exists('sodium_crypto_sign_keypair')) { $kp = sodium_crypto_sign_keypair(); $privatekey = EC::loadFormat('libsodium', sodium_crypto_sign_secretkey($kp)); //$publickey = EC::loadFormat('libsodium', sodium_crypto_sign_publickey($kp)); $privatekey->curveName = 'Ed25519'; //$publickey->curveName = $curve; return $privatekey; } $privatekey = new PrivateKey(); $curveName = $curve; if (preg_match('#(?:^curve|^ed)\d+$#', $curveName)) { $curveName = ucfirst($curveName); } elseif (substr($curveName, 0, 10) == 'brainpoolp') { $curveName = 'brainpoolP' . substr($curveName, 10); } $curve = '\phpseclib3\Crypt\EC\Curves\\' . $curveName; if (!class_exists($curve)) { throw new UnsupportedCurveException('Named Curve of ' . $curveName . ' is not supported'); } $reflect = new \ReflectionClass($curve); $curveName = $reflect->isFinal() ? $reflect->getParentClass()->getShortName() : $reflect->getShortName(); $curve = new $curve(); if ($curve instanceof TwistedEdwardsCurve) { $arr = $curve->extractSecret(Random::string($curve instanceof Ed448 ? 57 : 32)); $privatekey->dA = $dA = $arr['dA']; $privatekey->secret = $arr['secret']; } else { $privatekey->dA = $dA = $curve->createRandomMultiplier(); } if ($curve instanceof Curve25519 && self::$engines['libsodium']) { //$r = pack('H*', '0900000000000000000000000000000000000000000000000000000000000000'); //$QA = sodium_crypto_scalarmult($dA->toBytes(), $r); $QA = sodium_crypto_box_publickey_from_secretkey($dA->toBytes()); $privatekey->QA = [$curve->convertInteger(new BigInteger(strrev($QA), 256))]; } else { $privatekey->QA = $curve->multiplyPoint($curve->getBasePoint(), $dA); } $privatekey->curve = $curve; //$publickey = clone $privatekey; //unset($publickey->dA); //unset($publickey->x); $privatekey->curveName = $curveName; //$publickey->curveName = $curveName; if ($privatekey->curve instanceof TwistedEdwardsCurve) { return $privatekey->withHash($curve::HASH); } return $privatekey; } /** * OnLoad Handler * * @return bool */ protected static function onLoad(array $components) { if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } if (!isset($components['dA']) && !isset($components['QA'])) { $new = new Parameters(); $new->curve = $components['curve']; return $new; } $new = isset($components['dA']) ? new PrivateKey() : new PublicKey(); $new->curve = $components['curve']; $new->QA = $components['QA']; if (isset($components['dA'])) { $new->dA = $components['dA']; $new->secret = $components['secret']; } if ($new->curve instanceof TwistedEdwardsCurve) { return $new->withHash($components['curve']::HASH); } return $new; } /** * Constructor * * PublicKey and PrivateKey objects can only be created from abstract RSA class */ protected function __construct() { $this->sigFormat = self::validatePlugin('Signature', 'ASN1'); $this->shortFormat = 'ASN1'; parent::__construct(); } /** * Returns the curve * * Returns a string if it's a named curve, an array if not * * @return string|array */ public function getCurve() { if ($this->curveName) { return $this->curveName; } if ($this->curve instanceof MontgomeryCurve) { $this->curveName = $this->curve instanceof Curve25519 ? 'Curve25519' : 'Curve448'; return $this->curveName; } if ($this->curve instanceof TwistedEdwardsCurve) { $this->curveName = $this->curve instanceof Ed25519 ? 'Ed25519' : 'Ed448'; return $this->curveName; } $params = $this->getParameters()->toString('PKCS8', ['namedCurve' => true]); $decoded = ASN1::extractBER($params); $decoded = ASN1::decodeBER($decoded); $decoded = ASN1::asn1map($decoded[0], ECParameters::MAP); if (isset($decoded['namedCurve'])) { $this->curveName = $decoded['namedCurve']; return $decoded['namedCurve']; } if (!$namedCurves) { PKCS1::useSpecifiedCurve(); } return $decoded; } /** * Returns the key size * * Quoting https://tools.ietf.org/html/rfc5656#section-2, * * "The size of a set of elliptic curve domain parameters on a prime * curve is defined as the number of bits in the binary representation * of the field order, commonly denoted by p. Size on a * characteristic-2 curve is defined as the number of bits in the binary * representation of the field, commonly denoted by m. A set of * elliptic curve domain parameters defines a group of order n generated * by a base point P" * * @return int */ public function getLength() { return $this->curve->getLength(); } /** * Returns the current engine being used * * @see self::useInternalEngine() * @see self::useBestEngine() * @return string */ public function getEngine() { if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } if ($this->curve instanceof TwistedEdwardsCurve) { return $this->curve instanceof Ed25519 && self::$engines['libsodium'] && !isset($this->context) ? 'libsodium' : 'PHP'; } return self::$engines['OpenSSL'] && in_array($this->hash->getHash(), openssl_get_md_methods()) ? 'OpenSSL' : 'PHP'; } /** * Returns the public key coordinates as a string * * Used by ECDH * * @return string */ public function getEncodedCoordinates() { if ($this->curve instanceof MontgomeryCurve) { return strrev($this->QA[0]->toBytes(true)); } if ($this->curve instanceof TwistedEdwardsCurve) { return $this->curve->encodePoint($this->QA); } return "\4" . $this->QA[0]->toBytes(true) . $this->QA[1]->toBytes(true); } /** * Returns the parameters * * @see self::getPublicKey() * @param string $type optional * @return mixed */ public function getParameters($type = 'PKCS1') { $type = self::validatePlugin('Keys', $type, 'saveParameters'); $key = $type::saveParameters($this->curve); return EC::load($key, 'PKCS1') ->withHash($this->hash->getHash()) ->withSignatureFormat($this->shortFormat); } /** * Determines the signature padding mode * * Valid values are: ASN1, SSH2, Raw * * @param string $format */ public function withSignatureFormat($format) { if ($this->curve instanceof MontgomeryCurve) { throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures'); } $new = clone $this; $new->shortFormat = $format; $new->sigFormat = self::validatePlugin('Signature', $format); return $new; } /** * Returns the signature format currently being used * */ public function getSignatureFormat() { return $this->shortFormat; } /** * Sets the context * * Used by Ed25519 / Ed448. * * @see self::sign() * @see self::verify() * @param string $context optional */ public function withContext($context = null) { if (!$this->curve instanceof TwistedEdwardsCurve) { throw new UnsupportedCurveException('Only Ed25519 and Ed448 support contexts'); } $new = clone $this; if (!isset($context)) { $new->context = null; return $new; } if (!is_string($context)) { throw new \InvalidArgumentException('setContext expects a string'); } if (strlen($context) > 255) { throw new \LengthException('The context is supposed to be, at most, 255 bytes long'); } $new->context = $context; return $new; } /** * Returns the signature format currently being used * */ public function getContext() { return $this->context; } /** * Determines which hashing function should be used * * @param string $hash */ public function withHash($hash) { if ($this->curve instanceof MontgomeryCurve) { throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures'); } if ($this->curve instanceof Ed25519 && $hash != 'sha512') { throw new UnsupportedAlgorithmException('Ed25519 only supports sha512 as a hash'); } if ($this->curve instanceof Ed448 && $hash != 'shake256-912') { throw new UnsupportedAlgorithmException('Ed448 only supports shake256 with a length of 114 bytes'); } return parent::withHash($hash); } /** * __toString() magic method * * @return string */ public function __toString() { if ($this->curve instanceof MontgomeryCurve) { return ''; } return parent::__toString(); } } PK!٤3vendor/phpseclib/phpseclib/phpseclib/Crypt/Hash.phpnu[ * setKey('abcdefg'); * * echo base64_encode($hash->hash('abcdefg')); * ?> * * * @author Jim Wigginton * @copyright 2015 Jim Wigginton * @author Andreas Fischer * @copyright 2015 Andreas Fischer * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\InsufficientSetupException; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Math\BigInteger; use phpseclib3\Math\PrimeField; /** * @author Jim Wigginton * @author Andreas Fischer */ class Hash { /** * Padding Types * */ const PADDING_KECCAK = 1; /** * Padding Types * */ const PADDING_SHA3 = 2; /** * Padding Types * */ const PADDING_SHAKE = 3; /** * Padding Type * * Only used by SHA3 * * @var int */ private $paddingType = 0; /** * Hash Parameter * * @see self::setHash() * @var int */ private $hashParam; /** * Byte-length of hash output (Internal HMAC) * * @see self::setHash() * @var int */ private $length; /** * Hash Algorithm * * @see self::setHash() * @var string */ private $algo; /** * Key * * @see self::setKey() * @var string */ private $key = false; /** * Nonce * * @see self::setNonce() * @var string */ private $nonce = false; /** * Hash Parameters * * @var array */ private $parameters = []; /** * Computed Key * * @see self::_computeKey() * @var string */ private $computedKey = false; /** * Outer XOR (Internal HMAC) * * Used only for sha512 * * @see self::hash() * @var string */ private $opad; /** * Inner XOR (Internal HMAC) * * Used only for sha512 * * @see self::hash() * @var string */ private $ipad; /** * Recompute AES Key * * Used only for umac * * @see self::hash() * @var boolean */ private $recomputeAESKey; /** * umac cipher object * * @see self::hash() * @var AES */ private $c; /** * umac pad * * @see self::hash() * @var string */ private $pad; /** * Block Size * * @var int */ private $blockSize; /**#@+ * UMAC variables * * @var PrimeField */ private static $factory36; private static $factory64; private static $factory128; private static $offset64; private static $offset128; private static $marker64; private static $marker128; private static $maxwordrange64; private static $maxwordrange128; /**#@-*/ /** * Default Constructor. * * @param string $hash */ public function __construct($hash = 'sha256') { $this->setHash($hash); } /** * Sets the key for HMACs * * Keys can be of any length. * * @param string $key */ public function setKey($key = false) { $this->key = $key; $this->computeKey(); $this->recomputeAESKey = true; } /** * Sets the nonce for UMACs * * Keys can be of any length. * * @param string $nonce */ public function setNonce($nonce = false) { switch (true) { case !is_string($nonce): case strlen($nonce) > 0 && strlen($nonce) <= 16: $this->recomputeAESKey = true; $this->nonce = $nonce; return; } throw new \LengthException('The nonce length must be between 1 and 16 bytes, inclusive'); } /** * Pre-compute the key used by the HMAC * * Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes * will first hash the key using H and then use the resultant L byte string as the actual key to HMAC." * * As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/ * when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during * every call * */ private function computeKey() { if ($this->key === false) { $this->computedKey = false; return; } if (strlen($this->key) <= $this->getBlockLengthInBytes()) { $this->computedKey = $this->key; return; } $this->computedKey = is_array($this->algo) ? call_user_func($this->algo, $this->key) : hash($this->algo, $this->key, true); } /** * Gets the hash function. * * As set by the constructor or by the setHash() method. * * @return string */ public function getHash() { return $this->hashParam; } /** * Sets the hash function. * * @param string $hash */ public function setHash($hash) { $oldHash = $this->hashParam; $this->hashParam = $hash = strtolower($hash); switch ($hash) { case 'umac-32': case 'umac-64': case 'umac-96': case 'umac-128': if ($oldHash != $this->hashParam) { $this->recomputeAESKey = true; } $this->blockSize = 128; $this->length = abs(substr($hash, -3)) >> 3; $this->algo = 'umac'; return; case 'md2-96': case 'md5-96': case 'sha1-96': case 'sha224-96': case 'sha256-96': case 'sha384-96': case 'sha512-96': case 'sha512/224-96': case 'sha512/256-96': $hash = substr($hash, 0, -3); $this->length = 12; // 96 / 8 = 12 break; case 'md2': case 'md5': $this->length = 16; break; case 'sha1': $this->length = 20; break; case 'sha224': case 'sha512/224': case 'sha3-224': $this->length = 28; break; case 'keccak256': $this->paddingType = self::PADDING_KECCAK; // fall-through case 'sha256': case 'sha512/256': case 'sha3-256': $this->length = 32; break; case 'sha384': case 'sha3-384': $this->length = 48; break; case 'sha512': case 'sha3-512': $this->length = 64; break; default: if (preg_match('#^(shake(?:128|256))-(\d+)$#', $hash, $matches)) { $this->paddingType = self::PADDING_SHAKE; $hash = $matches[1]; $this->length = $matches[2] >> 3; } else { throw new UnsupportedAlgorithmException( "$hash is not a supported algorithm" ); } } switch ($hash) { case 'md2': case 'md2-96': $this->blockSize = 128; break; case 'md5-96': case 'sha1-96': case 'sha224-96': case 'sha256-96': case 'md5': case 'sha1': case 'sha224': case 'sha256': $this->blockSize = 512; break; case 'sha3-224': $this->blockSize = 1152; // 1600 - 2*224 break; case 'sha3-256': case 'shake256': case 'keccak256': $this->blockSize = 1088; // 1600 - 2*256 break; case 'sha3-384': $this->blockSize = 832; // 1600 - 2*384 break; case 'sha3-512': $this->blockSize = 576; // 1600 - 2*512 break; case 'shake128': $this->blockSize = 1344; // 1600 - 2*128 break; default: $this->blockSize = 1024; } if (in_array(substr($hash, 0, 5), ['sha3-', 'shake', 'kecca'])) { // PHP 7.1.0 introduced support for "SHA3 fixed mode algorithms": // http://php.net/ChangeLog-7.php#7.1.0 if (version_compare(PHP_VERSION, '7.1.0') < 0 || substr($hash, 0, 5) != 'sha3-') { //preg_match('#(\d+)$#', $hash, $matches); //$this->parameters['capacity'] = 2 * $matches[1]; // 1600 - $this->blockSize //$this->parameters['rate'] = 1600 - $this->parameters['capacity']; // == $this->blockSize if (!$this->paddingType) { $this->paddingType = self::PADDING_SHA3; } $this->parameters = [ 'capacity' => 1600 - $this->blockSize, 'rate' => $this->blockSize, 'length' => $this->length, 'padding' => $this->paddingType ]; $hash = ['phpseclib3\Crypt\Hash', PHP_INT_SIZE == 8 ? 'sha3_64' : 'sha3_32']; } } if ($hash == 'sha512/224' || $hash == 'sha512/256') { // PHP 7.1.0 introduced sha512/224 and sha512/256 support: // http://php.net/ChangeLog-7.php#7.1.0 if (version_compare(PHP_VERSION, '7.1.0') < 0) { // from http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf#page=24 $initial = $hash == 'sha512/256' ? [ '22312194FC2BF72C', '9F555FA3C84C64C2', '2393B86B6F53B151', '963877195940EABD', '96283EE2A88EFFE3', 'BE5E1E2553863992', '2B0199FC2C85B8AA', '0EB72DDC81C52CA2' ] : [ '8C3D37C819544DA2', '73E1996689DCD4D6', '1DFAB7AE32FF9C82', '679DD514582F9FCF', '0F6D2B697BD44DA8', '77E36F7304C48942', '3F9D85A86A1D36C8', '1112E6AD91D692A1' ]; for ($i = 0; $i < 8; $i++) { if (PHP_INT_SIZE == 8) { list(, $initial[$i]) = unpack('J', pack('H*', $initial[$i])); } else { $initial[$i] = new BigInteger($initial[$i], 16); $initial[$i]->setPrecision(64); } } $this->parameters = compact('initial'); $hash = ['phpseclib3\Crypt\Hash', PHP_INT_SIZE == 8 ? 'sha512_64' : 'sha512']; } } if (is_array($hash)) { $b = $this->blockSize >> 3; $this->ipad = str_repeat(chr(0x36), $b); $this->opad = str_repeat(chr(0x5C), $b); } $this->algo = $hash; $this->computeKey(); } /** * KDF: Key-Derivation Function * * The key-derivation function generates pseudorandom bits used to key the hash functions. * * @param int $index a non-negative integer less than 2^64 * @param int $numbytes a non-negative integer less than 2^64 * @return string string of length numbytes bytes */ private function kdf($index, $numbytes) { $this->c->setIV(pack('N4', 0, $index, 0, 1)); return $this->c->encrypt(str_repeat("\0", $numbytes)); } /** * PDF Algorithm * * @return string string of length taglen bytes. */ private function pdf() { $k = $this->key; $nonce = $this->nonce; $taglen = $this->length; // // Extract and zero low bit(s) of Nonce if needed // if ($taglen <= 8) { $last = strlen($nonce) - 1; $mask = $taglen == 4 ? "\3" : "\1"; $index = $nonce[$last] & $mask; $nonce[$last] = $nonce[$last] ^ $index; } // // Make Nonce BLOCKLEN bytes by appending zeroes if needed // $nonce = str_pad($nonce, 16, "\0"); // // Generate subkey, encipher and extract indexed substring // $kp = $this->kdf(0, 16); $c = new AES('ctr'); $c->disablePadding(); $c->setKey($kp); $c->setIV($nonce); $t = $c->encrypt("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); // we could use ord() but per https://paragonie.com/blog/2016/06/constant-time-encoding-boring-cryptography-rfc-4648-and-you // unpack() doesn't leak timing info return $taglen <= 8 ? substr($t, unpack('C', $index)[1] * $taglen, $taglen) : substr($t, 0, $taglen); } /** * UHASH Algorithm * * @param string $m string of length less than 2^67 bits. * @param int $taglen the integer 4, 8, 12 or 16. * @return string string of length taglen bytes. */ private function uhash($m, $taglen) { // // One internal iteration per 4 bytes of output // $iters = $taglen >> 2; // // Define total key needed for all iterations using KDF. // L1Key reuses most key material between iterations. // //$L1Key = $this->kdf(1, 1024 + ($iters - 1) * 16); $L1Key = $this->kdf(1, (1024 + ($iters - 1)) * 16); $L2Key = $this->kdf(2, $iters * 24); $L3Key1 = $this->kdf(3, $iters * 64); $L3Key2 = $this->kdf(4, $iters * 4); // // For each iteration, extract key and do three-layer hash. // If bytelength(M) <= 1024, then skip L2-HASH. // $y = ''; for ($i = 0; $i < $iters; $i++) { $L1Key_i = substr($L1Key, $i * 16, 1024); $L2Key_i = substr($L2Key, $i * 24, 24); $L3Key1_i = substr($L3Key1, $i * 64, 64); $L3Key2_i = substr($L3Key2, $i * 4, 4); $a = self::L1Hash($L1Key_i, $m); $b = strlen($m) <= 1024 ? "\0\0\0\0\0\0\0\0$a" : self::L2Hash($L2Key_i, $a); $c = self::L3Hash($L3Key1_i, $L3Key2_i, $b); $y .= $c; } return $y; } /** * L1-HASH Algorithm * * The first-layer hash breaks the message into 1024-byte chunks and * hashes each with a function called NH. Concatenating the results * forms a string, which is up to 128 times shorter than the original. * * @param string $k string of length 1024 bytes. * @param string $m string of length less than 2^67 bits. * @return string string of length (8 * ceil(bitlength(M)/8192)) bytes. */ private static function L1Hash($k, $m) { // // Break M into 1024 byte chunks (final chunk may be shorter) // $m = str_split($m, 1024); // // For each chunk, except the last: endian-adjust, NH hash // and add bit-length. Use results to build Y. // $length = 1024 * 8; $y = ''; for ($i = 0; $i < count($m) - 1; $i++) { $m[$i] = pack('N*', ...unpack('V*', $m[$i])); // ENDIAN-SWAP $y .= PHP_INT_SIZE == 8 ? static::nh64($k, $m[$i], $length) : static::nh32($k, $m[$i], $length); } // // For the last chunk: pad to 32-byte boundary, endian-adjust, // NH hash and add bit-length. Concatenate the result to Y. // $length = count($m) ? strlen($m[$i]) : 0; $pad = 32 - ($length % 32); $pad = max(32, $length + $pad % 32); $m[$i] = str_pad(isset($m[$i]) ? $m[$i] : '', $pad, "\0"); // zeropad $m[$i] = pack('N*', ...unpack('V*', $m[$i])); // ENDIAN-SWAP $y .= PHP_INT_SIZE == 8 ? static::nh64($k, $m[$i], $length * 8) : static::nh32($k, $m[$i], $length * 8); return $y; } /** * 32-bit safe 64-bit Multiply with 2x 32-bit ints * * @param int $x * @param int $y * @return string $x * $y */ private static function mul32_64($x, $y) { // see mul64() for a more detailed explanation of how this works $x1 = ($x >> 16) & 0xFFFF; $x0 = $x & 0xFFFF; $y1 = ($y >> 16) & 0xFFFF; $y0 = $y & 0xFFFF; // the following 3x lines will possibly yield floats $z2 = $x1 * $y1; $z0 = $x0 * $y0; $z1 = $x1 * $y0 + $x0 * $y1; $a = intval(fmod($z0, 65536)); $b = intval($z0 / 65536) + intval(fmod($z1, 65536)); $c = intval($z1 / 65536) + intval(fmod($z2, 65536)) + intval($b / 65536); $b = intval(fmod($b, 65536)); $d = intval($z2 / 65536) + intval($c / 65536); $c = intval(fmod($c, 65536)); $d = intval(fmod($d, 65536)); return pack('n4', $d, $c, $b, $a); } /** * 32-bit safe 64-bit Addition with 2x 64-bit strings * * @param int $x * @param int $y * @return int $x * $y */ private static function add32_64($x, $y) { list(, $x1, $x2, $x3, $x4) = unpack('n4', $x); list(, $y1, $y2, $y3, $y4) = unpack('n4', $y); $a = $x4 + $y4; $b = $x3 + $y3 + ($a >> 16); $c = $x2 + $y2 + ($b >> 16); $d = $x1 + $y1 + ($c >> 16); return pack('n4', $d, $c, $b, $a); } /** * 32-bit safe 32-bit Addition with 2x 32-bit strings * * @param int $x * @param int $y * @return int $x * $y */ private static function add32($x, $y) { // see add64() for a more detailed explanation of how this works $x1 = $x & 0xFFFF; $x2 = ($x >> 16) & 0xFFFF; $y1 = $y & 0xFFFF; $y2 = ($y >> 16) & 0xFFFF; $a = $x1 + $y1; $b = ($x2 + $y2 + ($a >> 16)) << 16; $a &= 0xFFFF; return $a | $b; } /** * NH Algorithm / 32-bit safe * * @param string $k string of length 1024 bytes. * @param string $m string with length divisible by 32 bytes. * @return string string of length 8 bytes. */ private static function nh32($k, $m, $length) { // // Break M and K into 4-byte chunks // $k = unpack('N*', $k); $m = unpack('N*', $m); $t = count($m); // // Perform NH hash on the chunks, pairing words for multiplication // which are 4 apart to accommodate vector-parallelism. // $i = 1; $y = "\0\0\0\0\0\0\0\0"; while ($i <= $t) { $temp = self::add32($m[$i], $k[$i]); $temp2 = self::add32($m[$i + 4], $k[$i + 4]); $y = self::add32_64($y, self::mul32_64($temp, $temp2)); $temp = self::add32($m[$i + 1], $k[$i + 1]); $temp2 = self::add32($m[$i + 5], $k[$i + 5]); $y = self::add32_64($y, self::mul32_64($temp, $temp2)); $temp = self::add32($m[$i + 2], $k[$i + 2]); $temp2 = self::add32($m[$i + 6], $k[$i + 6]); $y = self::add32_64($y, self::mul32_64($temp, $temp2)); $temp = self::add32($m[$i + 3], $k[$i + 3]); $temp2 = self::add32($m[$i + 7], $k[$i + 7]); $y = self::add32_64($y, self::mul32_64($temp, $temp2)); $i += 8; } return self::add32_64($y, pack('N2', 0, $length)); } /** * 64-bit Multiply with 2x 32-bit ints * * @param int $x * @param int $y * @return int $x * $y */ private static function mul64($x, $y) { // since PHP doesn't implement unsigned integers we'll implement them with signed integers // to do this we'll use karatsuba multiplication $x1 = $x >> 16; $x0 = $x & 0xFFFF; $y1 = $y >> 16; $y0 = $y & 0xFFFF; $z2 = $x1 * $y1; // up to 32 bits long $z0 = $x0 * $y0; // up to 32 bits long $z1 = $x1 * $y0 + $x0 * $y1; // up to 33 bit long // normally karatsuba multiplication calculates $z1 thusly: //$z1 = ($x1 + $x0) * ($y0 + $y1) - $z2 - $z0; // the idea being to eliminate one extra multiplication. for arbitrary precision math that makes sense // but not for this purpose // at this point karatsuba would normally return this: //return ($z2 << 64) + ($z1 << 32) + $z0; // the problem is that the output could be out of range for signed 64-bit ints, // which would cause PHP to switch to floats, which would risk losing the lower few bits // as such we'll OR 4x 16-bit blocks together like so: /* ........ | ........ | ........ | ........ upper $z2 | lower $z2 | lower $z1 | lower $z0 | +upper $z1 | +upper $z0 | + $carry | + $carry | | */ // technically upper $z1 is 17 bit - not 16 - but the most significant digit of that will // just get added to $carry $a = $z0 & 0xFFFF; $b = ($z0 >> 16) + ($z1 & 0xFFFF); $c = ($z1 >> 16) + ($z2 & 0xFFFF) + ($b >> 16); $b = ($b & 0xFFFF) << 16; $d = ($z2 >> 16) + ($c >> 16); $c = ($c & 0xFFFF) << 32; $d = ($d & 0xFFFF) << 48; return $a | $b | $c | $d; } /** * 64-bit Addition with 2x 64-bit ints * * @param int $x * @param int $y * @return int $x + $y */ private static function add64($x, $y) { // doing $x + $y risks returning a result that's out of range for signed 64-bit ints // in that event PHP would convert the result to a float and precision would be lost // so we'll just add 2x 32-bit ints together like so: /* ........ | ........ upper $x | lower $x +upper $y |+lower $y + $carry | */ $x1 = $x & 0xFFFFFFFF; $x2 = ($x >> 32) & 0xFFFFFFFF; $y1 = $y & 0xFFFFFFFF; $y2 = ($y >> 32) & 0xFFFFFFFF; $a = $x1 + $y1; $b = ($x2 + $y2 + ($a >> 32)) << 32; $a &= 0xFFFFFFFF; return $a | $b; } /** * NH Algorithm / 64-bit safe * * @param string $k string of length 1024 bytes. * @param string $m string with length divisible by 32 bytes. * @return string string of length 8 bytes. */ private static function nh64($k, $m, $length) { // // Break M and K into 4-byte chunks // $k = unpack('N*', $k); $m = unpack('N*', $m); $t = count($m); // // Perform NH hash on the chunks, pairing words for multiplication // which are 4 apart to accommodate vector-parallelism. // $i = 1; $y = 0; while ($i <= $t) { $temp = ($m[$i] + $k[$i]) & 0xFFFFFFFF; $temp2 = ($m[$i + 4] + $k[$i + 4]) & 0xFFFFFFFF; $y = self::add64($y, self::mul64($temp, $temp2)); $temp = ($m[$i + 1] + $k[$i + 1]) & 0xFFFFFFFF; $temp2 = ($m[$i + 5] + $k[$i + 5]) & 0xFFFFFFFF; $y = self::add64($y, self::mul64($temp, $temp2)); $temp = ($m[$i + 2] + $k[$i + 2]) & 0xFFFFFFFF; $temp2 = ($m[$i + 6] + $k[$i + 6]) & 0xFFFFFFFF; $y = self::add64($y, self::mul64($temp, $temp2)); $temp = ($m[$i + 3] + $k[$i + 3]) & 0xFFFFFFFF; $temp2 = ($m[$i + 7] + $k[$i + 7]) & 0xFFFFFFFF; $y = self::add64($y, self::mul64($temp, $temp2)); $i += 8; } return pack('J', self::add64($y, $length)); } /** * L2-HASH: Second-Layer Hash * * The second-layer rehashes the L1-HASH output using a polynomial hash * called POLY. If the L1-HASH output is long, then POLY is called once * on a prefix of the L1-HASH output and called using different settings * on the remainder. (This two-step hashing of the L1-HASH output is * needed only if the message length is greater than 16 megabytes.) * Careful implementation of POLY is necessary to avoid a possible * timing attack (see Section 6.6 for more information). * * @param string $k string of length 24 bytes. * @param string $m string of length less than 2^64 bytes. * @return string string of length 16 bytes. */ private static function L2Hash($k, $m) { // // Extract keys and restrict to special key-sets // $k64 = $k & "\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF"; $k64 = new BigInteger($k64, 256); $k128 = substr($k, 8) & "\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF"; $k128 = new BigInteger($k128, 256); // // If M is no more than 2^17 bytes, hash under 64-bit prime, // otherwise, hash first 2^17 bytes under 64-bit prime and // remainder under 128-bit prime. // if (strlen($m) <= 0x20000) { // 2^14 64-bit words $y = self::poly(64, self::$maxwordrange64, $k64, $m); } else { $m_1 = substr($m, 0, 0x20000); // 1 << 17 $m_2 = substr($m, 0x20000) . "\x80"; $length = strlen($m_2); $pad = 16 - ($length % 16); $pad %= 16; $m_2 = str_pad($m_2, $length + $pad, "\0"); // zeropad $y = self::poly(64, self::$maxwordrange64, $k64, $m_1); $y = str_pad($y, 16, "\0", STR_PAD_LEFT); $y = self::poly(128, self::$maxwordrange128, $k128, $y . $m_2); } return str_pad($y, 16, "\0", STR_PAD_LEFT); } /** * POLY Algorithm * * @param int $wordbits the integer 64 or 128. * @param BigInteger $maxwordrange positive integer less than 2^wordbits. * @param BigInteger $k integer in the range 0 ... prime(wordbits) - 1. * @param string $m string with length divisible by (wordbits / 8) bytes. * @return integer in the range 0 ... prime(wordbits) - 1. */ private static function poly($wordbits, $maxwordrange, $k, $m) { // // Define constants used for fixing out-of-range words // $wordbytes = $wordbits >> 3; if ($wordbits == 128) { $factory = self::$factory128; $offset = self::$offset128; $marker = self::$marker128; } else { $factory = self::$factory64; $offset = self::$offset64; $marker = self::$marker64; } $k = $factory->newInteger($k); // // Break M into chunks of length wordbytes bytes // $m_i = str_split($m, $wordbytes); // // Each input word m is compared with maxwordrange. If not smaller // then 'marker' and (m - offset), both in range, are hashed. // $y = $factory->newInteger(new BigInteger(1)); foreach ($m_i as $m) { $m = $factory->newInteger(new BigInteger($m, 256)); if ($m->compare($maxwordrange) >= 0) { $y = $k->multiply($y)->add($marker); $y = $k->multiply($y)->add($m->subtract($offset)); } else { $y = $k->multiply($y)->add($m); } } return $y->toBytes(); } /** * L3-HASH: Third-Layer Hash * * The output from L2-HASH is 16 bytes long. This final hash function * hashes the 16-byte string to a fixed length of 4 bytes. * * @param string $k1 string of length 64 bytes. * @param string $k2 string of length 4 bytes. * @param string $m string of length 16 bytes. * @return string string of length 4 bytes. */ private static function L3Hash($k1, $k2, $m) { $factory = self::$factory36; $y = $factory->newInteger(new BigInteger()); for ($i = 0; $i < 8; $i++) { $m_i = $factory->newInteger(new BigInteger(substr($m, 2 * $i, 2), 256)); $k_i = $factory->newInteger(new BigInteger(substr($k1, 8 * $i, 8), 256)); $y = $y->add($m_i->multiply($k_i)); } $y = str_pad(substr($y->toBytes(), -4), 4, "\0", STR_PAD_LEFT); $y = $y ^ $k2; return $y; } /** * Compute the Hash / HMAC / UMAC. * * @param string $text * @return string */ public function hash($text) { $algo = $this->algo; if ($algo == 'umac') { if ($this->recomputeAESKey) { if (!is_string($this->nonce)) { throw new InsufficientSetupException('No nonce has been set'); } if (!is_string($this->key)) { throw new InsufficientSetupException('No key has been set'); } if (strlen($this->key) != 16) { throw new \LengthException('Key must be 16 bytes long'); } if (!isset(self::$maxwordrange64)) { $one = new BigInteger(1); $prime36 = new BigInteger("\x00\x00\x00\x0F\xFF\xFF\xFF\xFB", 256); self::$factory36 = new PrimeField($prime36); $prime64 = new BigInteger("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC5", 256); self::$factory64 = new PrimeField($prime64); $prime128 = new BigInteger("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x61", 256); self::$factory128 = new PrimeField($prime128); self::$offset64 = new BigInteger("\1\0\0\0\0\0\0\0\0", 256); self::$offset64 = self::$factory64->newInteger(self::$offset64->subtract($prime64)); self::$offset128 = new BigInteger("\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 256); self::$offset128 = self::$factory128->newInteger(self::$offset128->subtract($prime128)); self::$marker64 = self::$factory64->newInteger($prime64->subtract($one)); self::$marker128 = self::$factory128->newInteger($prime128->subtract($one)); $maxwordrange64 = $one->bitwise_leftShift(64)->subtract($one->bitwise_leftShift(32)); self::$maxwordrange64 = self::$factory64->newInteger($maxwordrange64); $maxwordrange128 = $one->bitwise_leftShift(128)->subtract($one->bitwise_leftShift(96)); self::$maxwordrange128 = self::$factory128->newInteger($maxwordrange128); } $this->c = new AES('ctr'); $this->c->disablePadding(); $this->c->setKey($this->key); $this->pad = $this->pdf(); $this->recomputeAESKey = false; } $hashedmessage = $this->uhash($text, $this->length); return $hashedmessage ^ $this->pad; } if (is_array($algo)) { if (empty($this->key) || !is_string($this->key)) { return substr($algo($text, ...array_values($this->parameters)), 0, $this->length); } // SHA3 HMACs are discussed at https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf#page=30 $key = str_pad($this->computedKey, $b, chr(0)); $temp = $this->ipad ^ $key; $temp .= $text; $temp = substr($algo($temp, ...array_values($this->parameters)), 0, $this->length); $output = $this->opad ^ $key; $output .= $temp; $output = $algo($output, ...array_values($this->parameters)); return substr($output, 0, $this->length); } $output = !empty($this->key) || is_string($this->key) ? hash_hmac($algo, $text, $this->computedKey, true) : hash($algo, $text, true); return strlen($output) > $this->length ? substr($output, 0, $this->length) : $output; } /** * Returns the hash length (in bits) * * @return int */ public function getLength() { return $this->length << 3; } /** * Returns the hash length (in bytes) * * @return int */ public function getLengthInBytes() { return $this->length; } /** * Returns the block length (in bits) * * @return int */ public function getBlockLength() { return $this->blockSize; } /** * Returns the block length (in bytes) * * @return int */ public function getBlockLengthInBytes() { return $this->blockSize >> 3; } /** * Pads SHA3 based on the mode * * @param int $padLength * @param int $padType * @return string */ private static function sha3_pad($padLength, $padType) { switch ($padType) { case self::PADDING_KECCAK: $temp = chr(0x01) . str_repeat("\0", $padLength - 1); $temp[$padLength - 1] = $temp[$padLength - 1] | chr(0x80); return $temp; case self::PADDING_SHAKE: $temp = chr(0x1F) . str_repeat("\0", $padLength - 1); $temp[$padLength - 1] = $temp[$padLength - 1] | chr(0x80); return $temp; //case self::PADDING_SHA3: default: // from https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf#page=36 return $padLength == 1 ? chr(0x86) : chr(0x06) . str_repeat("\0", $padLength - 2) . chr(0x80); } } /** * Pure-PHP 32-bit implementation of SHA3 * * Whereas BigInteger.php's 32-bit engine works on PHP 64-bit this 32-bit implementation * of SHA3 will *not* work on PHP 64-bit. This is because this implementation * employees bitwise NOTs and bitwise left shifts. And the round constants only work * on 32-bit PHP. eg. dechex(-2147483648) returns 80000000 on 32-bit PHP and * FFFFFFFF80000000 on 64-bit PHP. Sure, we could do bitwise ANDs but that would slow * things down. * * SHA512 requires BigInteger to simulate 64-bit unsigned integers because SHA2 employees * addition whereas SHA3 just employees bitwise operators. PHP64 only supports signed * 64-bit integers, which complicates addition, whereas that limitation isn't an issue * for SHA3. * * In https://ws680.nist.gov/publication/get_pdf.cfm?pub_id=919061#page=16 KECCAK[C] is * defined as "the KECCAK instance with KECCAK-f[1600] as the underlying permutation and * capacity c". This is relevant because, altho the KECCAK standard defines a mode * (KECCAK-f[800]) designed for 32-bit machines that mode is incompatible with SHA3 * * @param string $p * @param int $c * @param int $r * @param int $d * @param int $padType */ private static function sha3_32($p, $c, $r, $d, $padType) { $block_size = $r >> 3; $padLength = $block_size - (strlen($p) % $block_size); $num_ints = $block_size >> 2; $p .= static::sha3_pad($padLength, $padType); $n = strlen($p) / $r; // number of blocks $s = [ [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] ]; $p = str_split($p, $block_size); foreach ($p as $pi) { $pi = unpack('V*', $pi); $x = $y = 0; for ($i = 1; $i <= $num_ints; $i += 2) { $s[$x][$y][0] ^= $pi[$i + 1]; $s[$x][$y][1] ^= $pi[$i]; if (++$y == 5) { $y = 0; $x++; } } static::processSHA3Block32($s); } $z = ''; $i = $j = 0; while (strlen($z) < $d) { $z .= pack('V2', $s[$i][$j][1], $s[$i][$j++][0]); if ($j == 5) { $j = 0; $i++; if ($i == 5) { $i = 0; static::processSHA3Block32($s); } } } return $z; } /** * 32-bit block processing method for SHA3 * * @param array $s */ private static function processSHA3Block32(&$s) { static $rotationOffsets = [ [ 0, 1, 62, 28, 27], [36, 44, 6, 55, 20], [ 3, 10, 43, 25, 39], [41, 45, 15, 21, 8], [18, 2, 61, 56, 14] ]; // the standards give these constants in hexadecimal notation. it's tempting to want to use // that same notation, here, however, we can't, because 0x80000000, on PHP32, is a positive // float - not the negative int that we need to be in PHP32. so we use -2147483648 instead static $roundConstants = [ [0, 1], [0, 32898], [-2147483648, 32906], [-2147483648, -2147450880], [0, 32907], [0, -2147483647], [-2147483648, -2147450751], [-2147483648, 32777], [0, 138], [0, 136], [0, -2147450871], [0, -2147483638], [0, -2147450741], [-2147483648, 139], [-2147483648, 32905], [-2147483648, 32771], [-2147483648, 32770], [-2147483648, 128], [0, 32778], [-2147483648, -2147483638], [-2147483648, -2147450751], [-2147483648, 32896], [0, -2147483647], [-2147483648, -2147450872] ]; for ($round = 0; $round < 24; $round++) { // theta step $parity = $rotated = []; for ($i = 0; $i < 5; $i++) { $parity[] = [ $s[0][$i][0] ^ $s[1][$i][0] ^ $s[2][$i][0] ^ $s[3][$i][0] ^ $s[4][$i][0], $s[0][$i][1] ^ $s[1][$i][1] ^ $s[2][$i][1] ^ $s[3][$i][1] ^ $s[4][$i][1] ]; $rotated[] = static::rotateLeft32($parity[$i], 1); } $temp = [ [$parity[4][0] ^ $rotated[1][0], $parity[4][1] ^ $rotated[1][1]], [$parity[0][0] ^ $rotated[2][0], $parity[0][1] ^ $rotated[2][1]], [$parity[1][0] ^ $rotated[3][0], $parity[1][1] ^ $rotated[3][1]], [$parity[2][0] ^ $rotated[4][0], $parity[2][1] ^ $rotated[4][1]], [$parity[3][0] ^ $rotated[0][0], $parity[3][1] ^ $rotated[0][1]] ]; for ($i = 0; $i < 5; $i++) { for ($j = 0; $j < 5; $j++) { $s[$i][$j][0] ^= $temp[$j][0]; $s[$i][$j][1] ^= $temp[$j][1]; } } $st = $s; // rho and pi steps for ($i = 0; $i < 5; $i++) { for ($j = 0; $j < 5; $j++) { $st[(2 * $i + 3 * $j) % 5][$j] = static::rotateLeft32($s[$j][$i], $rotationOffsets[$j][$i]); } } // chi step for ($i = 0; $i < 5; $i++) { $s[$i][0] = [ $st[$i][0][0] ^ (~$st[$i][1][0] & $st[$i][2][0]), $st[$i][0][1] ^ (~$st[$i][1][1] & $st[$i][2][1]) ]; $s[$i][1] = [ $st[$i][1][0] ^ (~$st[$i][2][0] & $st[$i][3][0]), $st[$i][1][1] ^ (~$st[$i][2][1] & $st[$i][3][1]) ]; $s[$i][2] = [ $st[$i][2][0] ^ (~$st[$i][3][0] & $st[$i][4][0]), $st[$i][2][1] ^ (~$st[$i][3][1] & $st[$i][4][1]) ]; $s[$i][3] = [ $st[$i][3][0] ^ (~$st[$i][4][0] & $st[$i][0][0]), $st[$i][3][1] ^ (~$st[$i][4][1] & $st[$i][0][1]) ]; $s[$i][4] = [ $st[$i][4][0] ^ (~$st[$i][0][0] & $st[$i][1][0]), $st[$i][4][1] ^ (~$st[$i][0][1] & $st[$i][1][1]) ]; } // iota step $s[0][0][0] ^= $roundConstants[$round][0]; $s[0][0][1] ^= $roundConstants[$round][1]; } } /** * Rotate 32-bit int * * @param array $x * @param int $shift */ private static function rotateLeft32($x, $shift) { if ($shift < 32) { list($hi, $lo) = $x; } else { $shift -= 32; list($lo, $hi) = $x; } $mask = -1 ^ (-1 << $shift); return [ ($hi << $shift) | (($lo >> (32 - $shift)) & $mask), ($lo << $shift) | (($hi >> (32 - $shift)) & $mask) ]; } /** * Pure-PHP 64-bit implementation of SHA3 * * @param string $p * @param int $c * @param int $r * @param int $d * @param int $padType */ private static function sha3_64($p, $c, $r, $d, $padType) { $block_size = $r >> 3; $padLength = $block_size - (strlen($p) % $block_size); $num_ints = $block_size >> 2; $p .= static::sha3_pad($padLength, $padType); $n = strlen($p) / $r; // number of blocks $s = [ [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0] ]; $p = str_split($p, $block_size); foreach ($p as $pi) { $pi = unpack('P*', $pi); $x = $y = 0; foreach ($pi as $subpi) { $s[$x][$y++] ^= $subpi; if ($y == 5) { $y = 0; $x++; } } static::processSHA3Block64($s); } $z = ''; $i = $j = 0; while (strlen($z) < $d) { $z .= pack('P', $s[$i][$j++]); if ($j == 5) { $j = 0; $i++; if ($i == 5) { $i = 0; static::processSHA3Block64($s); } } } return $z; } /** * 64-bit block processing method for SHA3 * * @param array $s */ private static function processSHA3Block64(&$s) { static $rotationOffsets = [ [ 0, 1, 62, 28, 27], [36, 44, 6, 55, 20], [ 3, 10, 43, 25, 39], [41, 45, 15, 21, 8], [18, 2, 61, 56, 14] ]; static $roundConstants = [ 1, 32898, -9223372036854742902, -9223372034707259392, 32907, 2147483649, -9223372034707259263, -9223372036854743031, 138, 136, 2147516425, 2147483658, 2147516555, -9223372036854775669, -9223372036854742903, -9223372036854743037, -9223372036854743038, -9223372036854775680, 32778, -9223372034707292150, -9223372034707259263, -9223372036854742912, 2147483649, -9223372034707259384 ]; for ($round = 0; $round < 24; $round++) { // theta step $parity = []; for ($i = 0; $i < 5; $i++) { $parity[] = $s[0][$i] ^ $s[1][$i] ^ $s[2][$i] ^ $s[3][$i] ^ $s[4][$i]; } $temp = [ $parity[4] ^ static::rotateLeft64($parity[1], 1), $parity[0] ^ static::rotateLeft64($parity[2], 1), $parity[1] ^ static::rotateLeft64($parity[3], 1), $parity[2] ^ static::rotateLeft64($parity[4], 1), $parity[3] ^ static::rotateLeft64($parity[0], 1) ]; for ($i = 0; $i < 5; $i++) { for ($j = 0; $j < 5; $j++) { $s[$i][$j] ^= $temp[$j]; } } $st = $s; // rho and pi steps for ($i = 0; $i < 5; $i++) { for ($j = 0; $j < 5; $j++) { $st[(2 * $i + 3 * $j) % 5][$j] = static::rotateLeft64($s[$j][$i], $rotationOffsets[$j][$i]); } } // chi step for ($i = 0; $i < 5; $i++) { $s[$i] = [ $st[$i][0] ^ (~$st[$i][1] & $st[$i][2]), $st[$i][1] ^ (~$st[$i][2] & $st[$i][3]), $st[$i][2] ^ (~$st[$i][3] & $st[$i][4]), $st[$i][3] ^ (~$st[$i][4] & $st[$i][0]), $st[$i][4] ^ (~$st[$i][0] & $st[$i][1]) ]; } // iota step $s[0][0] ^= $roundConstants[$round]; } } /** * Left rotate 64-bit int * * @param int $x * @param int $shift */ private static function rotateLeft64($x, $shift) { $mask = -1 ^ (-1 << $shift); return ($x << $shift) | (($x >> (64 - $shift)) & $mask); } /** * Right rotate 64-bit int * * @param int $x * @param int $shift */ private static function rotateRight64($x, $shift) { $mask = -1 ^ (-1 << (64 - $shift)); return (($x >> $shift) & $mask) | ($x << (64 - $shift)); } /** * Pure-PHP implementation of SHA512 * * @param string $m * @param array $hash * @return string */ private static function sha512($m, $hash) { static $k; if (!isset($k)) { // Initialize table of round constants // (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409) $k = [ '428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc', '3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118', 'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2', '72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694', 'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65', '2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5', '983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4', 'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70', '27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df', '650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b', 'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30', 'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8', '19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8', '391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3', '748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec', '90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b', 'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178', '06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b', '28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c', '4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817' ]; for ($i = 0; $i < 80; $i++) { $k[$i] = new BigInteger($k[$i], 16); } } // Pre-processing $length = strlen($m); // to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128 $m .= str_repeat(chr(0), 128 - (($length + 16) & 0x7F)); $m[$length] = chr(0x80); // we don't support hashing strings 512MB long $m .= pack('N4', 0, 0, 0, $length << 3); // Process the message in successive 1024-bit chunks $chunks = str_split($m, 128); foreach ($chunks as $chunk) { $w = []; for ($i = 0; $i < 16; $i++) { $temp = new BigInteger(Strings::shift($chunk, 8), 256); $temp->setPrecision(64); $w[] = $temp; } // Extend the sixteen 32-bit words into eighty 32-bit words for ($i = 16; $i < 80; $i++) { $temp = [ $w[$i - 15]->bitwise_rightRotate(1), $w[$i - 15]->bitwise_rightRotate(8), $w[$i - 15]->bitwise_rightShift(7) ]; $s0 = $temp[0]->bitwise_xor($temp[1]); $s0 = $s0->bitwise_xor($temp[2]); $temp = [ $w[$i - 2]->bitwise_rightRotate(19), $w[$i - 2]->bitwise_rightRotate(61), $w[$i - 2]->bitwise_rightShift(6) ]; $s1 = $temp[0]->bitwise_xor($temp[1]); $s1 = $s1->bitwise_xor($temp[2]); $w[$i] = clone $w[$i - 16]; $w[$i] = $w[$i]->add($s0); $w[$i] = $w[$i]->add($w[$i - 7]); $w[$i] = $w[$i]->add($s1); } // Initialize hash value for this chunk $a = clone $hash[0]; $b = clone $hash[1]; $c = clone $hash[2]; $d = clone $hash[3]; $e = clone $hash[4]; $f = clone $hash[5]; $g = clone $hash[6]; $h = clone $hash[7]; // Main loop for ($i = 0; $i < 80; $i++) { $temp = [ $a->bitwise_rightRotate(28), $a->bitwise_rightRotate(34), $a->bitwise_rightRotate(39) ]; $s0 = $temp[0]->bitwise_xor($temp[1]); $s0 = $s0->bitwise_xor($temp[2]); $temp = [ $a->bitwise_and($b), $a->bitwise_and($c), $b->bitwise_and($c) ]; $maj = $temp[0]->bitwise_xor($temp[1]); $maj = $maj->bitwise_xor($temp[2]); $t2 = $s0->add($maj); $temp = [ $e->bitwise_rightRotate(14), $e->bitwise_rightRotate(18), $e->bitwise_rightRotate(41) ]; $s1 = $temp[0]->bitwise_xor($temp[1]); $s1 = $s1->bitwise_xor($temp[2]); $temp = [ $e->bitwise_and($f), $g->bitwise_and($e->bitwise_not()) ]; $ch = $temp[0]->bitwise_xor($temp[1]); $t1 = $h->add($s1); $t1 = $t1->add($ch); $t1 = $t1->add($k[$i]); $t1 = $t1->add($w[$i]); $h = clone $g; $g = clone $f; $f = clone $e; $e = $d->add($t1); $d = clone $c; $c = clone $b; $b = clone $a; $a = $t1->add($t2); } // Add this chunk's hash to result so far $hash = [ $hash[0]->add($a), $hash[1]->add($b), $hash[2]->add($c), $hash[3]->add($d), $hash[4]->add($e), $hash[5]->add($f), $hash[6]->add($g), $hash[7]->add($h) ]; } // Produce the final hash value (big-endian) // (\phpseclib3\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here) $temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() . $hash[4]->toBytes() . $hash[5]->toBytes() . $hash[6]->toBytes() . $hash[7]->toBytes(); return $temp; } /** * Pure-PHP implementation of SHA512 * * @param string $m * @param array $hash * @return string */ private static function sha512_64($m, $hash) { static $k; if (!isset($k)) { // Initialize table of round constants // (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409) $k = [ '428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc', '3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118', 'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2', '72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694', 'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65', '2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5', '983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4', 'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70', '27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df', '650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b', 'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30', 'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8', '19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8', '391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3', '748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec', '90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b', 'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178', '06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b', '28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c', '4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817' ]; for ($i = 0; $i < 80; $i++) { list(, $k[$i]) = unpack('J', pack('H*', $k[$i])); } } // Pre-processing $length = strlen($m); // to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128 $m .= str_repeat(chr(0), 128 - (($length + 16) & 0x7F)); $m[$length] = chr(0x80); // we don't support hashing strings 512MB long $m .= pack('N4', 0, 0, 0, $length << 3); // Process the message in successive 1024-bit chunks $chunks = str_split($m, 128); foreach ($chunks as $chunk) { $w = []; for ($i = 0; $i < 16; $i++) { list(, $w[]) = unpack('J', Strings::shift($chunk, 8)); } // Extend the sixteen 32-bit words into eighty 32-bit words for ($i = 16; $i < 80; $i++) { $temp = [ self::rotateRight64($w[$i - 15], 1), self::rotateRight64($w[$i - 15], 8), ($w[$i - 15] >> 7) & 0x01FFFFFFFFFFFFFF, ]; $s0 = $temp[0] ^ $temp[1] ^ $temp[2]; $temp = [ self::rotateRight64($w[$i - 2], 19), self::rotateRight64($w[$i - 2], 61), ($w[$i - 2] >> 6) & 0x03FFFFFFFFFFFFFF, ]; $s1 = $temp[0] ^ $temp[1] ^ $temp[2]; $w[$i] = $w[$i - 16]; $w[$i] = self::add64($w[$i], $s0); $w[$i] = self::add64($w[$i], $w[$i - 7]); $w[$i] = self::add64($w[$i], $s1); } // Initialize hash value for this chunk list($a, $b, $c, $d, $e, $f, $g, $h) = $hash; // Main loop for ($i = 0; $i < 80; $i++) { $temp = [ self::rotateRight64($a, 28), self::rotateRight64($a, 34), self::rotateRight64($a, 39), ]; $s0 = $temp[0] ^ $temp[1] ^ $temp[2]; $temp = [$a & $b, $a & $c, $b & $c]; $maj = $temp[0] ^ $temp[1] ^ $temp[2]; $t2 = self::add64($s0, $maj); $temp = [ self::rotateRight64($e, 14), self::rotateRight64($e, 18), self::rotateRight64($e, 41), ]; $s1 = $temp[0] ^ $temp[1] ^ $temp[2]; $ch = ($e & $f) ^ ($g & ~$e); $t1 = self::add64($h, $s1); $t1 = self::add64($t1, $ch); $t1 = self::add64($t1, $k[$i]); $t1 = self::add64($t1, $w[$i]); $h = $g; $g = $f; $f = $e; $e = self::add64($d, $t1); $d = $c; $c = $b; $b = $a; $a = self::add64($t1, $t2); } // Add this chunk's hash to result so far $hash = [ self::add64($hash[0], $a), self::add64($hash[1], $b), self::add64($hash[2], $c), self::add64($hash[3], $d), self::add64($hash[4], $e), self::add64($hash[5], $f), self::add64($hash[6], $g), self::add64($hash[7], $h), ]; } // Produce the final hash value (big-endian) // (\phpseclib3\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here) return pack('J*', ...$hash); } /** * __toString() magic method */ public function __toString() { return $this->getHash(); } } PK!` >vendor/phpseclib/phpseclib/phpseclib/Crypt/PublicKeyLoader.phpnu[ * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\Common\PrivateKey; use phpseclib3\Crypt\Common\PublicKey; use phpseclib3\Exception\NoKeyLoadedException; use phpseclib3\File\X509; /** * PublicKeyLoader * * @author Jim Wigginton */ abstract class PublicKeyLoader { /** * Loads a public or private key * * @return AsymmetricKey * @param string|array $key * @param string $password optional */ public static function load($key, $password = false) { try { return EC::load($key, $password); } catch (NoKeyLoadedException $e) { } try { return RSA::load($key, $password); } catch (NoKeyLoadedException $e) { } try { return DSA::load($key, $password); } catch (NoKeyLoadedException $e) { } try { $x509 = new X509(); $x509->loadX509($key); $key = $x509->getPublicKey(); if ($key) { return $key; } } catch (\Exception $e) { } throw new NoKeyLoadedException('Unable to read key'); } /** * Loads a private key * * @return PrivateKey * @param string|array $key * @param string $password optional */ public static function loadPrivateKey($key, $password = false) { $key = self::load($key, $password); if (!$key instanceof PrivateKey) { throw new NoKeyLoadedException('The key that was loaded was not a private key'); } return $key; } /** * Loads a public key * * @return PublicKey * @param string|array $key */ public static function loadPublicKey($key) { $key = self::load($key); if (!$key instanceof PublicKey) { throw new NoKeyLoadedException('The key that was loaded was not a public key'); } return $key; } /** * Loads parameters * * @return AsymmetricKey * @param string|array $key */ public static function loadParameters($key) { $key = self::load($key); if (!$key instanceof PrivateKey && !$key instanceof PublicKey) { throw new NoKeyLoadedException('The key that was loaded was not a parameter'); } return $key; } } PK!:%1%1%5vendor/phpseclib/phpseclib/phpseclib/Crypt/Random.phpnu[ * * * * @author Jim Wigginton * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; /** * Pure-PHP Random Number Generator * * @author Jim Wigginton */ abstract class Random { /** * Generate a random string. * * Although microoptimizations are generally discouraged as they impair readability this function is ripe with * microoptimizations because this function has the potential of being called a huge number of times. * eg. for RSA key generation. * * @param int $length * @throws \RuntimeException if a symmetric cipher is needed but not loaded * @return string */ public static function string($length) { if (!$length) { return ''; } try { return random_bytes($length); } catch (\Exception $e) { // random_compat will throw an Exception, which in PHP 5 does not implement Throwable } catch (\Throwable $e) { // If a sufficient source of randomness is unavailable, random_bytes() will throw an // object that implements the Throwable interface (Exception, TypeError, Error). // We don't actually need to do anything here. The string() method should just continue // as normal. Note, however, that if we don't have a sufficient source of randomness for // random_bytes(), most of the other calls here will fail too, so we'll end up using // the PHP implementation. } // at this point we have no choice but to use a pure-PHP CSPRNG // cascade entropy across multiple PHP instances by fixing the session and collecting all // environmental variables, including the previous session data and the current session // data. // // mt_rand seeds itself by looking at the PID and the time, both of which are (relatively) // easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but // PHP isn't low level to be able to use those as sources and on a web server there's not likely // going to be a ton of keyboard or mouse action. web servers do have one thing that we can use // however, a ton of people visiting the website. obviously you don't want to base your seeding // solely on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled // by the user and (2) this isn't just looking at the data sent by the current user - it's based // on the data sent by all users. one user requests the page and a hash of their info is saved. // another user visits the page and the serialization of their data is utilized along with the // server environment stuff and a hash of the previous http request data (which itself utilizes // a hash of the session data before that). certainly an attacker should be assumed to have // full control over his own http requests. he, however, is not going to have control over // everyone's http requests. static $crypto = false, $v; if ($crypto === false) { // save old session data $old_session_id = session_id(); $old_use_cookies = ini_get('session.use_cookies'); $old_session_cache_limiter = session_cache_limiter(); $_OLD_SESSION = isset($_SESSION) ? $_SESSION : false; if ($old_session_id != '') { session_write_close(); } session_id(1); ini_set('session.use_cookies', 0); session_cache_limiter(''); session_start(); $v = (isset($_SERVER) ? self::safe_serialize($_SERVER) : '') . (isset($_POST) ? self::safe_serialize($_POST) : '') . (isset($_GET) ? self::safe_serialize($_GET) : '') . (isset($_COOKIE) ? self::safe_serialize($_COOKIE) : '') . // as of PHP 8.1 $GLOBALS can't be accessed by reference, which eliminates // the need for phpseclib_safe_serialize. see https://wiki.php.net/rfc/restrict_globals_usage // for more info (version_compare(PHP_VERSION, '8.1.0', '>=') ? serialize($GLOBALS) : self::safe_serialize($GLOBALS)) . self::safe_serialize($_SESSION) . self::safe_serialize($_OLD_SESSION); $v = $seed = $_SESSION['seed'] = sha1($v, true); if (!isset($_SESSION['count'])) { $_SESSION['count'] = 0; } $_SESSION['count']++; session_write_close(); // restore old session data if ($old_session_id != '') { session_id($old_session_id); session_start(); ini_set('session.use_cookies', $old_use_cookies); session_cache_limiter($old_session_cache_limiter); } else { if ($_OLD_SESSION !== false) { $_SESSION = $_OLD_SESSION; unset($_OLD_SESSION); } else { unset($_SESSION); } } // in SSH2 a shared secret and an exchange hash are generated through the key exchange process. // the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C. // if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the // original hash and the current hash. we'll be emulating that. for more info see the following URL: // // http://tools.ietf.org/html/rfc4253#section-7.2 // // see the is_string($crypto) part for an example of how to expand the keys $key = sha1($seed . 'A', true); $iv = sha1($seed . 'C', true); // ciphers are used as per the nist.gov link below. also, see this link: // // http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives switch (true) { case class_exists('\phpseclib3\Crypt\AES'): $crypto = new AES('ctr'); break; case class_exists('\phpseclib3\Crypt\Twofish'): $crypto = new Twofish('ctr'); break; case class_exists('\phpseclib3\Crypt\Blowfish'): $crypto = new Blowfish('ctr'); break; case class_exists('\phpseclib3\Crypt\TripleDES'): $crypto = new TripleDES('ctr'); break; case class_exists('\phpseclib3\Crypt\DES'): $crypto = new DES('ctr'); break; case class_exists('\phpseclib3\Crypt\RC4'): $crypto = new RC4(); break; default: throw new \RuntimeException(__CLASS__ . ' requires at least one symmetric cipher be loaded'); } $crypto->setKey(substr($key, 0, $crypto->getKeyLength() >> 3)); $crypto->setIV(substr($iv, 0, $crypto->getBlockLength() >> 3)); $crypto->enableContinuousBuffer(); } //return $crypto->encrypt(str_repeat("\0", $length)); // the following is based off of ANSI X9.31: // // http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf // // OpenSSL uses that same standard for it's random numbers: // // http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c // (do a search for "ANS X9.31 A.2.4") $result = ''; while (strlen($result) < $length) { $i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21 $r = $crypto->encrypt($i ^ $v); // strlen($v) == 20 $v = $crypto->encrypt($r ^ $i); // strlen($r) == 20 $result .= $r; } return substr($result, 0, $length); } /** * Safely serialize variables * * If a class has a private __sleep() it'll emit a warning * @return mixed * @param mixed $arr */ private static function safe_serialize(&$arr) { if (is_object($arr)) { return ''; } if (!is_array($arr)) { return serialize($arr); } // prevent circular array recursion if (isset($arr['__phpseclib_marker'])) { return ''; } $safearr = []; $arr['__phpseclib_marker'] = true; foreach (array_keys($arr) as $key) { // do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage if ($key !== '__phpseclib_marker') { $safearr[$key] = self::safe_serialize($arr[$key]); } } unset($arr['__phpseclib_marker']); return serialize($safearr); } } PK!,…TT2vendor/phpseclib/phpseclib/phpseclib/Crypt/RC2.phpnu[ * setKey('abcdefgh'); * * $plaintext = str_repeat('a', 1024); * * echo $rc2->decrypt($rc2->encrypt($plaintext)); * ?> * * * @author Patrick Monnerat * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\BlockCipher; use phpseclib3\Exception\BadModeException; /** * Pure-PHP implementation of RC2. * */ class RC2 extends BlockCipher { /** * Block Length of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::block_size * @var int */ protected $block_size = 8; /** * The Key * * @see \phpseclib3\Crypt\Common\SymmetricKey::key * @see self::setKey() * @var string */ protected $key; /** * The Original (unpadded) Key * * @see \phpseclib3\Crypt\Common\SymmetricKey::key * @see self::setKey() * @see self::encrypt() * @see self::decrypt() * @var string */ private $orig_key; /** * Key Length (in bytes) * * @see \phpseclib3\Crypt\RC2::setKeyLength() * @var int */ protected $key_length = 16; // = 128 bits /** * The mcrypt specific name of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @var string */ protected $cipher_name_mcrypt = 'rc2'; /** * Optimizing value while CFB-encrypting * * @see \phpseclib3\Crypt\Common\SymmetricKey::cfb_init_len * @var int */ protected $cfb_init_len = 500; /** * The key length in bits. * * {@internal Should be in range [1..1024].} * * {@internal Changing this value after setting the key has no effect.} * * @see self::setKeyLength() * @see self::setKey() * @var int */ private $default_key_length = 1024; /** * The key length in bits. * * {@internal Should be in range [1..1024].} * * @see self::isValidEnine() * @see self::setKey() * @var int */ private $current_key_length; /** * The Key Schedule * * @see self::setupKey() * @var array */ private $keys; /** * Key expansion randomization table. * Twice the same 256-value sequence to save a modulus in key expansion. * * @see self::setKey() * @var array */ private static $pitable = [ 0xD9, 0x78, 0xF9, 0xC4, 0x19, 0xDD, 0xB5, 0xED, 0x28, 0xE9, 0xFD, 0x79, 0x4A, 0xA0, 0xD8, 0x9D, 0xC6, 0x7E, 0x37, 0x83, 0x2B, 0x76, 0x53, 0x8E, 0x62, 0x4C, 0x64, 0x88, 0x44, 0x8B, 0xFB, 0xA2, 0x17, 0x9A, 0x59, 0xF5, 0x87, 0xB3, 0x4F, 0x13, 0x61, 0x45, 0x6D, 0x8D, 0x09, 0x81, 0x7D, 0x32, 0xBD, 0x8F, 0x40, 0xEB, 0x86, 0xB7, 0x7B, 0x0B, 0xF0, 0x95, 0x21, 0x22, 0x5C, 0x6B, 0x4E, 0x82, 0x54, 0xD6, 0x65, 0x93, 0xCE, 0x60, 0xB2, 0x1C, 0x73, 0x56, 0xC0, 0x14, 0xA7, 0x8C, 0xF1, 0xDC, 0x12, 0x75, 0xCA, 0x1F, 0x3B, 0xBE, 0xE4, 0xD1, 0x42, 0x3D, 0xD4, 0x30, 0xA3, 0x3C, 0xB6, 0x26, 0x6F, 0xBF, 0x0E, 0xDA, 0x46, 0x69, 0x07, 0x57, 0x27, 0xF2, 0x1D, 0x9B, 0xBC, 0x94, 0x43, 0x03, 0xF8, 0x11, 0xC7, 0xF6, 0x90, 0xEF, 0x3E, 0xE7, 0x06, 0xC3, 0xD5, 0x2F, 0xC8, 0x66, 0x1E, 0xD7, 0x08, 0xE8, 0xEA, 0xDE, 0x80, 0x52, 0xEE, 0xF7, 0x84, 0xAA, 0x72, 0xAC, 0x35, 0x4D, 0x6A, 0x2A, 0x96, 0x1A, 0xD2, 0x71, 0x5A, 0x15, 0x49, 0x74, 0x4B, 0x9F, 0xD0, 0x5E, 0x04, 0x18, 0xA4, 0xEC, 0xC2, 0xE0, 0x41, 0x6E, 0x0F, 0x51, 0xCB, 0xCC, 0x24, 0x91, 0xAF, 0x50, 0xA1, 0xF4, 0x70, 0x39, 0x99, 0x7C, 0x3A, 0x85, 0x23, 0xB8, 0xB4, 0x7A, 0xFC, 0x02, 0x36, 0x5B, 0x25, 0x55, 0x97, 0x31, 0x2D, 0x5D, 0xFA, 0x98, 0xE3, 0x8A, 0x92, 0xAE, 0x05, 0xDF, 0x29, 0x10, 0x67, 0x6C, 0xBA, 0xC9, 0xD3, 0x00, 0xE6, 0xCF, 0xE1, 0x9E, 0xA8, 0x2C, 0x63, 0x16, 0x01, 0x3F, 0x58, 0xE2, 0x89, 0xA9, 0x0D, 0x38, 0x34, 0x1B, 0xAB, 0x33, 0xFF, 0xB0, 0xBB, 0x48, 0x0C, 0x5F, 0xB9, 0xB1, 0xCD, 0x2E, 0xC5, 0xF3, 0xDB, 0x47, 0xE5, 0xA5, 0x9C, 0x77, 0x0A, 0xA6, 0x20, 0x68, 0xFE, 0x7F, 0xC1, 0xAD, 0xD9, 0x78, 0xF9, 0xC4, 0x19, 0xDD, 0xB5, 0xED, 0x28, 0xE9, 0xFD, 0x79, 0x4A, 0xA0, 0xD8, 0x9D, 0xC6, 0x7E, 0x37, 0x83, 0x2B, 0x76, 0x53, 0x8E, 0x62, 0x4C, 0x64, 0x88, 0x44, 0x8B, 0xFB, 0xA2, 0x17, 0x9A, 0x59, 0xF5, 0x87, 0xB3, 0x4F, 0x13, 0x61, 0x45, 0x6D, 0x8D, 0x09, 0x81, 0x7D, 0x32, 0xBD, 0x8F, 0x40, 0xEB, 0x86, 0xB7, 0x7B, 0x0B, 0xF0, 0x95, 0x21, 0x22, 0x5C, 0x6B, 0x4E, 0x82, 0x54, 0xD6, 0x65, 0x93, 0xCE, 0x60, 0xB2, 0x1C, 0x73, 0x56, 0xC0, 0x14, 0xA7, 0x8C, 0xF1, 0xDC, 0x12, 0x75, 0xCA, 0x1F, 0x3B, 0xBE, 0xE4, 0xD1, 0x42, 0x3D, 0xD4, 0x30, 0xA3, 0x3C, 0xB6, 0x26, 0x6F, 0xBF, 0x0E, 0xDA, 0x46, 0x69, 0x07, 0x57, 0x27, 0xF2, 0x1D, 0x9B, 0xBC, 0x94, 0x43, 0x03, 0xF8, 0x11, 0xC7, 0xF6, 0x90, 0xEF, 0x3E, 0xE7, 0x06, 0xC3, 0xD5, 0x2F, 0xC8, 0x66, 0x1E, 0xD7, 0x08, 0xE8, 0xEA, 0xDE, 0x80, 0x52, 0xEE, 0xF7, 0x84, 0xAA, 0x72, 0xAC, 0x35, 0x4D, 0x6A, 0x2A, 0x96, 0x1A, 0xD2, 0x71, 0x5A, 0x15, 0x49, 0x74, 0x4B, 0x9F, 0xD0, 0x5E, 0x04, 0x18, 0xA4, 0xEC, 0xC2, 0xE0, 0x41, 0x6E, 0x0F, 0x51, 0xCB, 0xCC, 0x24, 0x91, 0xAF, 0x50, 0xA1, 0xF4, 0x70, 0x39, 0x99, 0x7C, 0x3A, 0x85, 0x23, 0xB8, 0xB4, 0x7A, 0xFC, 0x02, 0x36, 0x5B, 0x25, 0x55, 0x97, 0x31, 0x2D, 0x5D, 0xFA, 0x98, 0xE3, 0x8A, 0x92, 0xAE, 0x05, 0xDF, 0x29, 0x10, 0x67, 0x6C, 0xBA, 0xC9, 0xD3, 0x00, 0xE6, 0xCF, 0xE1, 0x9E, 0xA8, 0x2C, 0x63, 0x16, 0x01, 0x3F, 0x58, 0xE2, 0x89, 0xA9, 0x0D, 0x38, 0x34, 0x1B, 0xAB, 0x33, 0xFF, 0xB0, 0xBB, 0x48, 0x0C, 0x5F, 0xB9, 0xB1, 0xCD, 0x2E, 0xC5, 0xF3, 0xDB, 0x47, 0xE5, 0xA5, 0x9C, 0x77, 0x0A, 0xA6, 0x20, 0x68, 0xFE, 0x7F, 0xC1, 0xAD ]; /** * Inverse key expansion randomization table. * * @see self::setKey() * @var array */ private static $invpitable = [ 0xD1, 0xDA, 0xB9, 0x6F, 0x9C, 0xC8, 0x78, 0x66, 0x80, 0x2C, 0xF8, 0x37, 0xEA, 0xE0, 0x62, 0xA4, 0xCB, 0x71, 0x50, 0x27, 0x4B, 0x95, 0xD9, 0x20, 0x9D, 0x04, 0x91, 0xE3, 0x47, 0x6A, 0x7E, 0x53, 0xFA, 0x3A, 0x3B, 0xB4, 0xA8, 0xBC, 0x5F, 0x68, 0x08, 0xCA, 0x8F, 0x14, 0xD7, 0xC0, 0xEF, 0x7B, 0x5B, 0xBF, 0x2F, 0xE5, 0xE2, 0x8C, 0xBA, 0x12, 0xE1, 0xAF, 0xB2, 0x54, 0x5D, 0x59, 0x76, 0xDB, 0x32, 0xA2, 0x58, 0x6E, 0x1C, 0x29, 0x64, 0xF3, 0xE9, 0x96, 0x0C, 0x98, 0x19, 0x8D, 0x3E, 0x26, 0xAB, 0xA5, 0x85, 0x16, 0x40, 0xBD, 0x49, 0x67, 0xDC, 0x22, 0x94, 0xBB, 0x3C, 0xC1, 0x9B, 0xEB, 0x45, 0x28, 0x18, 0xD8, 0x1A, 0x42, 0x7D, 0xCC, 0xFB, 0x65, 0x8E, 0x3D, 0xCD, 0x2A, 0xA3, 0x60, 0xAE, 0x93, 0x8A, 0x48, 0x97, 0x51, 0x15, 0xF7, 0x01, 0x0B, 0xB7, 0x36, 0xB1, 0x2E, 0x11, 0xFD, 0x84, 0x2D, 0x3F, 0x13, 0x88, 0xB3, 0x34, 0x24, 0x1B, 0xDE, 0xC5, 0x1D, 0x4D, 0x2B, 0x17, 0x31, 0x74, 0xA9, 0xC6, 0x43, 0x6D, 0x39, 0x90, 0xBE, 0xC3, 0xB0, 0x21, 0x6B, 0xF6, 0x0F, 0xD5, 0x99, 0x0D, 0xAC, 0x1F, 0x5C, 0x9E, 0xF5, 0xF9, 0x4C, 0xD6, 0xDF, 0x89, 0xE4, 0x8B, 0xFF, 0xC7, 0xAA, 0xE7, 0xED, 0x46, 0x25, 0xB6, 0x06, 0x5E, 0x35, 0xB5, 0xEC, 0xCE, 0xE8, 0x6C, 0x30, 0x55, 0x61, 0x4A, 0xFE, 0xA0, 0x79, 0x03, 0xF0, 0x10, 0x72, 0x7C, 0xCF, 0x52, 0xA6, 0xA7, 0xEE, 0x44, 0xD3, 0x9A, 0x57, 0x92, 0xD0, 0x5A, 0x7A, 0x41, 0x7F, 0x0E, 0x00, 0x63, 0xF2, 0x4F, 0x05, 0x83, 0xC9, 0xA1, 0xD4, 0xDD, 0xC4, 0x56, 0xF4, 0xD2, 0x77, 0x81, 0x09, 0x82, 0x33, 0x9F, 0x07, 0x86, 0x75, 0x38, 0x4E, 0x69, 0xF1, 0xAD, 0x23, 0x73, 0x87, 0x70, 0x02, 0xC2, 0x1E, 0xB8, 0x0A, 0xFC, 0xE6 ]; /** * Default Constructor. * * @param string $mode * @throws \InvalidArgumentException if an invalid / unsupported mode is provided */ public function __construct($mode) { parent::__construct($mode); if ($this->mode == self::MODE_STREAM) { throw new BadModeException('Block ciphers cannot be ran in stream mode'); } } /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { switch ($engine) { case self::ENGINE_OPENSSL: if ($this->current_key_length != 128 || strlen($this->orig_key) < 16) { return false; } // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { return false; } $this->cipher_name_openssl_ecb = 'rc2-ecb'; $this->cipher_name_openssl = 'rc2-' . $this->openssl_translate_mode(); } return parent::isValidEngineHelper($engine); } /** * Sets the key length. * * Valid key lengths are 8 to 1024. * Calling this function after setting the key has no effect until the next * \phpseclib3\Crypt\RC2::setKey() call. * * @param int $length in bits * @throws \LengthException if the key length isn't supported */ public function setKeyLength($length) { if ($length < 8 || $length > 1024) { throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys between 1 and 1024 bits, inclusive, are supported'); } $this->default_key_length = $this->current_key_length = $length; $this->explicit_key_length = $length >> 3; } /** * Returns the current key length * * @return int */ public function getKeyLength() { return $this->current_key_length; } /** * Sets the key. * * Keys can be of any length. RC2, itself, uses 8 to 1024 bit keys (eg. * strlen($key) <= 128), however, we only use the first 128 bytes if $key * has more then 128 bytes in it, and set $key to a single null byte if * it is empty. * * @see \phpseclib3\Crypt\Common\SymmetricKey::setKey() * @param string $key * @param int|boolean $t1 optional Effective key length in bits. * @throws \LengthException if the key length isn't supported */ public function setKey($key, $t1 = false) { $this->orig_key = $key; if ($t1 === false) { $t1 = $this->default_key_length; } if ($t1 < 1 || $t1 > 1024) { throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys between 1 and 1024 bits, inclusive, are supported'); } $this->current_key_length = $t1; if (strlen($key) < 1 || strlen($key) > 128) { throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes between 8 and 1024 bits, inclusive, are supported'); } $t = strlen($key); // The mcrypt RC2 implementation only supports effective key length // of 1024 bits. It is however possible to handle effective key // lengths in range 1..1024 by expanding the key and applying // inverse pitable mapping to the first byte before submitting it // to mcrypt. // Key expansion. $l = array_values(unpack('C*', $key)); $t8 = ($t1 + 7) >> 3; $tm = 0xFF >> (8 * $t8 - $t1); // Expand key. $pitable = self::$pitable; for ($i = $t; $i < 128; $i++) { $l[$i] = $pitable[$l[$i - 1] + $l[$i - $t]]; } $i = 128 - $t8; $l[$i] = $pitable[$l[$i] & $tm]; while ($i--) { $l[$i] = $pitable[$l[$i + 1] ^ $l[$i + $t8]]; } // Prepare the key for mcrypt. $l[0] = self::$invpitable[$l[0]]; array_unshift($l, 'C*'); $this->key = pack(...$l); $this->key_length = strlen($this->key); $this->changed = $this->nonIVChanged = true; $this->setEngine(); } /** * Encrypts a message. * * Mostly a wrapper for \phpseclib3\Crypt\Common\SymmetricKey::encrypt, with some additional OpenSSL handling code * * @see self::decrypt() * @param string $plaintext * @return string $ciphertext */ public function encrypt($plaintext) { if ($this->engine == self::ENGINE_OPENSSL) { $temp = $this->key; $this->key = $this->orig_key; $result = parent::encrypt($plaintext); $this->key = $temp; return $result; } return parent::encrypt($plaintext); } /** * Decrypts a message. * * Mostly a wrapper for \phpseclib3\Crypt\Common\SymmetricKey::decrypt, with some additional OpenSSL handling code * * @see self::encrypt() * @param string $ciphertext * @return string $plaintext */ public function decrypt($ciphertext) { if ($this->engine == self::ENGINE_OPENSSL) { $temp = $this->key; $this->key = $this->orig_key; $result = parent::decrypt($ciphertext); $this->key = $temp; return $result; } return parent::decrypt($ciphertext); } /** * Encrypts a block * * @see \phpseclib3\Crypt\Common\SymmetricKey::encryptBlock() * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @param string $in * @return string */ protected function encryptBlock($in) { list($r0, $r1, $r2, $r3) = array_values(unpack('v*', $in)); $keys = $this->keys; $limit = 20; $actions = [$limit => 44, 44 => 64]; $j = 0; for (;;) { // Mixing round. $r0 = (($r0 + $keys[$j++] + ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF) << 1; $r0 |= $r0 >> 16; $r1 = (($r1 + $keys[$j++] + ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF) << 2; $r1 |= $r1 >> 16; $r2 = (($r2 + $keys[$j++] + ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF) << 3; $r2 |= $r2 >> 16; $r3 = (($r3 + $keys[$j++] + ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF) << 5; $r3 |= $r3 >> 16; if ($j === $limit) { if ($limit === 64) { break; } // Mashing round. $r0 += $keys[$r3 & 0x3F]; $r1 += $keys[$r0 & 0x3F]; $r2 += $keys[$r1 & 0x3F]; $r3 += $keys[$r2 & 0x3F]; $limit = $actions[$limit]; } } return pack('vvvv', $r0, $r1, $r2, $r3); } /** * Decrypts a block * * @see \phpseclib3\Crypt\Common\SymmetricKey::decryptBlock() * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() * @param string $in * @return string */ protected function decryptBlock($in) { list($r0, $r1, $r2, $r3) = array_values(unpack('v*', $in)); $keys = $this->keys; $limit = 44; $actions = [$limit => 20, 20 => 0]; $j = 64; for (;;) { // R-mixing round. $r3 = ($r3 | ($r3 << 16)) >> 5; $r3 = ($r3 - $keys[--$j] - ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF; $r2 = ($r2 | ($r2 << 16)) >> 3; $r2 = ($r2 - $keys[--$j] - ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF; $r1 = ($r1 | ($r1 << 16)) >> 2; $r1 = ($r1 - $keys[--$j] - ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF; $r0 = ($r0 | ($r0 << 16)) >> 1; $r0 = ($r0 - $keys[--$j] - ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF; if ($j === $limit) { if ($limit === 0) { break; } // R-mashing round. $r3 = ($r3 - $keys[$r2 & 0x3F]) & 0xFFFF; $r2 = ($r2 - $keys[$r1 & 0x3F]) & 0xFFFF; $r1 = ($r1 - $keys[$r0 & 0x3F]) & 0xFFFF; $r0 = ($r0 - $keys[$r3 & 0x3F]) & 0xFFFF; $limit = $actions[$limit]; } } return pack('vvvv', $r0, $r1, $r2, $r3); } /** * Creates the key schedule * * @see \phpseclib3\Crypt\Common\SymmetricKey::setupKey() */ protected function setupKey() { if (!isset($this->key)) { $this->setKey(''); } // Key has already been expanded in \phpseclib3\Crypt\RC2::setKey(): // Only the first value must be altered. $l = unpack('Ca/Cb/v*', $this->key); array_unshift($l, self::$pitable[$l['a']] | ($l['b'] << 8)); unset($l['a']); unset($l['b']); $this->keys = $l; } /** * Setup the performance-optimized function for de/encrypt() * * @see \phpseclib3\Crypt\Common\SymmetricKey::setupInlineCrypt() */ protected function setupInlineCrypt() { // Init code for both, encrypt and decrypt. $init_crypt = '$keys = $this->keys;'; $keys = $this->keys; // $in is the current 8 bytes block which has to be en/decrypt $encrypt_block = $decrypt_block = ' $in = unpack("v4", $in); $r0 = $in[1]; $r1 = $in[2]; $r2 = $in[3]; $r3 = $in[4]; '; // Create code for encryption. $limit = 20; $actions = [$limit => 44, 44 => 64]; $j = 0; for (;;) { // Mixing round. $encrypt_block .= ' $r0 = (($r0 + ' . $keys[$j++] . ' + ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF) << 1; $r0 |= $r0 >> 16; $r1 = (($r1 + ' . $keys[$j++] . ' + ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF) << 2; $r1 |= $r1 >> 16; $r2 = (($r2 + ' . $keys[$j++] . ' + ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF) << 3; $r2 |= $r2 >> 16; $r3 = (($r3 + ' . $keys[$j++] . ' + ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF) << 5; $r3 |= $r3 >> 16;'; if ($j === $limit) { if ($limit === 64) { break; } // Mashing round. $encrypt_block .= ' $r0 += $keys[$r3 & 0x3F]; $r1 += $keys[$r0 & 0x3F]; $r2 += $keys[$r1 & 0x3F]; $r3 += $keys[$r2 & 0x3F];'; $limit = $actions[$limit]; } } $encrypt_block .= '$in = pack("v4", $r0, $r1, $r2, $r3);'; // Create code for decryption. $limit = 44; $actions = [$limit => 20, 20 => 0]; $j = 64; for (;;) { // R-mixing round. $decrypt_block .= ' $r3 = ($r3 | ($r3 << 16)) >> 5; $r3 = ($r3 - ' . $keys[--$j] . ' - ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF; $r2 = ($r2 | ($r2 << 16)) >> 3; $r2 = ($r2 - ' . $keys[--$j] . ' - ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF; $r1 = ($r1 | ($r1 << 16)) >> 2; $r1 = ($r1 - ' . $keys[--$j] . ' - ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF; $r0 = ($r0 | ($r0 << 16)) >> 1; $r0 = ($r0 - ' . $keys[--$j] . ' - ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF;'; if ($j === $limit) { if ($limit === 0) { break; } // R-mashing round. $decrypt_block .= ' $r3 = ($r3 - $keys[$r2 & 0x3F]) & 0xFFFF; $r2 = ($r2 - $keys[$r1 & 0x3F]) & 0xFFFF; $r1 = ($r1 - $keys[$r0 & 0x3F]) & 0xFFFF; $r0 = ($r0 - $keys[$r3 & 0x3F]) & 0xFFFF;'; $limit = $actions[$limit]; } } $decrypt_block .= '$in = pack("v4", $r0, $r1, $r2, $r3);'; // Creates the inline-crypt function $this->inline_crypt = $this->createInlineCryptFunction( [ 'init_crypt' => $init_crypt, 'encrypt_block' => $encrypt_block, 'decrypt_block' => $decrypt_block ] ); } } PK!ǃ2vendor/phpseclib/phpseclib/phpseclib/Crypt/RC4.phpnu[ * setKey('abcdefgh'); * * $size = 10 * 1024; * $plaintext = ''; * for ($i = 0; $i < $size; $i++) { * $plaintext.= 'a'; * } * * echo $rc4->decrypt($rc4->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\StreamCipher; /** * Pure-PHP implementation of RC4. * * @author Jim Wigginton */ class RC4 extends StreamCipher { /** * @see \phpseclib3\Crypt\RC4::_crypt() */ const ENCRYPT = 0; /** * @see \phpseclib3\Crypt\RC4::_crypt() */ const DECRYPT = 1; /** * Key Length (in bytes) * * @see \phpseclib3\Crypt\RC4::setKeyLength() * @var int */ protected $key_length = 128; // = 1024 bits /** * The mcrypt specific name of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @var string */ protected $cipher_name_mcrypt = 'arcfour'; /** * The Key * * @see self::setKey() * @var string */ protected $key; /** * The Key Stream for decryption and encryption * * @see self::setKey() * @var array */ private $stream; /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { if ($engine == self::ENGINE_OPENSSL) { if ($this->continuousBuffer) { return false; } // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { return false; } $this->cipher_name_openssl = 'rc4-40'; } return parent::isValidEngineHelper($engine); } /** * Sets the key length * * Keys can be between 1 and 256 bytes long. * * @param int $length * @throws \LengthException if the key length is invalid */ public function setKeyLength($length) { if ($length < 8 || $length > 2048) { throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys between 1 and 256 bytes are supported'); } $this->key_length = $length >> 3; parent::setKeyLength($length); } /** * Sets the key length * * Keys can be between 1 and 256 bytes long. * * @param string $key */ public function setKey($key) { $length = strlen($key); if ($length < 1 || $length > 256) { throw new \LengthException('Key size of ' . $length . ' bytes is not supported by RC4. Keys must be between 1 and 256 bytes long'); } parent::setKey($key); } /** * Encrypts a message. * * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() * @see self::crypt() * @param string $plaintext * @return string $ciphertext */ public function encrypt($plaintext) { if ($this->engine != self::ENGINE_INTERNAL) { return parent::encrypt($plaintext); } return $this->crypt($plaintext, self::ENCRYPT); } /** * Decrypts a message. * * $this->decrypt($this->encrypt($plaintext)) == $this->encrypt($this->encrypt($plaintext)). * At least if the continuous buffer is disabled. * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see self::crypt() * @param string $ciphertext * @return string $plaintext */ public function decrypt($ciphertext) { if ($this->engine != self::ENGINE_INTERNAL) { return parent::decrypt($ciphertext); } return $this->crypt($ciphertext, self::DECRYPT); } /** * Encrypts a block * * @param string $in */ protected function encryptBlock($in) { // RC4 does not utilize this method } /** * Decrypts a block * * @param string $in */ protected function decryptBlock($in) { // RC4 does not utilize this method } /** * Setup the key (expansion) * * @see \phpseclib3\Crypt\Common\SymmetricKey::_setupKey() */ protected function setupKey() { $key = $this->key; $keyLength = strlen($key); $keyStream = range(0, 255); $j = 0; for ($i = 0; $i < 256; $i++) { $j = ($j + $keyStream[$i] + ord($key[$i % $keyLength])) & 255; $temp = $keyStream[$i]; $keyStream[$i] = $keyStream[$j]; $keyStream[$j] = $temp; } $this->stream = []; $this->stream[self::DECRYPT] = $this->stream[self::ENCRYPT] = [ 0, // index $i 0, // index $j $keyStream ]; } /** * Encrypts or decrypts a message. * * @see self::encrypt() * @see self::decrypt() * @param string $text * @param int $mode * @return string $text */ private function crypt($text, $mode) { if ($this->changed) { $this->setup(); } $stream = &$this->stream[$mode]; if ($this->continuousBuffer) { $i = &$stream[0]; $j = &$stream[1]; $keyStream = &$stream[2]; } else { $i = $stream[0]; $j = $stream[1]; $keyStream = $stream[2]; } $len = strlen($text); for ($k = 0; $k < $len; ++$k) { $i = ($i + 1) & 255; $ksi = $keyStream[$i]; $j = ($j + $ksi) & 255; $ksj = $keyStream[$j]; $keyStream[$i] = $ksj; $keyStream[$j] = $ksi; $text[$k] = $text[$k] ^ chr($keyStream[($ksj + $ksi) & 255]); } return $text; } } PK!II7vendor/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.phpnu[ * setKey('abcdefghijklmnop'); * * $size = 10 * 1024; * $plaintext = ''; * for ($i = 0; $i < $size; $i++) { * $plaintext.= 'a'; * } * * echo $rijndael->decrypt($rijndael->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @copyright 2008 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\BlockCipher; use phpseclib3\Exception\BadDecryptionException; use phpseclib3\Exception\BadModeException; use phpseclib3\Exception\InconsistentSetupException; use phpseclib3\Exception\InsufficientSetupException; /** * Pure-PHP implementation of Rijndael. * * @author Jim Wigginton */ class Rijndael extends BlockCipher { /** * The mcrypt specific name of the cipher * * Mcrypt is useable for 128/192/256-bit $block_size/$key_length. For 160/224 not. * \phpseclib3\Crypt\Rijndael determines automatically whether mcrypt is useable * or not for the current $block_size/$key_length. * In case of, $cipher_name_mcrypt will be set dynamically at run time accordingly. * * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @see \phpseclib3\Crypt\Common\SymmetricKey::engine * @see self::isValidEngine() * @var string */ protected $cipher_name_mcrypt = 'rijndael-128'; /** * The Key Schedule * * @see self::setup() * @var array */ private $w; /** * The Inverse Key Schedule * * @see self::setup() * @var array */ private $dw; /** * The Block Length divided by 32 * * {@internal The max value is 256 / 32 = 8, the min value is 128 / 32 = 4. Exists in conjunction with $block_size * because the encryption / decryption / key schedule creation requires this number and not $block_size. We could * derive this from $block_size or vice versa, but that'd mean we'd have to do multiple shift operations, so in lieu * of that, we'll just precompute it once.} * * @see self::setBlockLength() * @var int */ private $Nb = 4; /** * The Key Length (in bytes) * * {@internal The max value is 256 / 8 = 32, the min value is 128 / 8 = 16. Exists in conjunction with $Nk * because the encryption / decryption / key schedule creation requires this number and not $key_length. We could * derive this from $key_length or vice versa, but that'd mean we'd have to do multiple shift operations, so in lieu * of that, we'll just precompute it once.} * * @see self::setKeyLength() * @var int */ protected $key_length = 16; /** * The Key Length divided by 32 * * @see self::setKeyLength() * @var int * @internal The max value is 256 / 32 = 8, the min value is 128 / 32 = 4 */ private $Nk = 4; /** * The Number of Rounds * * {@internal The max value is 14, the min value is 10.} * * @var int */ private $Nr; /** * Shift offsets * * @var array */ private $c; /** * Holds the last used key- and block_size information * * @var array */ private $kl; /** * Default Constructor. * * @param string $mode * @throws \InvalidArgumentException if an invalid / unsupported mode is provided */ public function __construct($mode) { parent::__construct($mode); if ($this->mode == self::MODE_STREAM) { throw new BadModeException('Block ciphers cannot be ran in stream mode'); } } /** * Sets the key length. * * Valid key lengths are 128, 160, 192, 224, and 256. * * Note: phpseclib extends Rijndael (and AES) for using 160- and 224-bit keys but they are officially not defined * and the most (if not all) implementations are not able using 160/224-bit keys but round/pad them up to * 192/256 bits as, for example, mcrypt will do. * * That said, if you want be compatible with other Rijndael and AES implementations, * you should not setKeyLength(160) or setKeyLength(224). * * Additional: In case of 160- and 224-bit keys, phpseclib will/can, for that reason, not use * the mcrypt php extension, even if available. * This results then in slower encryption. * * @throws \LengthException if the key length is invalid * @param int $length */ public function setKeyLength($length) { switch ($length) { case 128: case 160: case 192: case 224: case 256: $this->key_length = $length >> 3; break; default: throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys of sizes 128, 160, 192, 224 or 256 bits are supported'); } parent::setKeyLength($length); } /** * Sets the key. * * Rijndael supports five different key lengths * * @see setKeyLength() * @param string $key * @throws \LengthException if the key length isn't supported */ public function setKey($key) { switch (strlen($key)) { case 16: case 20: case 24: case 28: case 32: break; default: throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes 16, 20, 24, 28 or 32 are supported'); } parent::setKey($key); } /** * Sets the block length * * Valid block lengths are 128, 160, 192, 224, and 256. * * @param int $length */ public function setBlockLength($length) { switch ($length) { case 128: case 160: case 192: case 224: case 256: break; default: throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys of sizes 128, 160, 192, 224 or 256 bits are supported'); } $this->Nb = $length >> 5; $this->block_size = $length >> 3; $this->changed = $this->nonIVChanged = true; $this->setEngine(); } /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { switch ($engine) { case self::ENGINE_LIBSODIUM: return function_exists('sodium_crypto_aead_aes256gcm_is_available') && sodium_crypto_aead_aes256gcm_is_available() && $this->mode == self::MODE_GCM && $this->key_length == 32 && $this->nonce && strlen($this->nonce) == 12 && $this->block_size == 16; case self::ENGINE_OPENSSL_GCM: if (!extension_loaded('openssl')) { return false; } $methods = openssl_get_cipher_methods(); return $this->mode == self::MODE_GCM && version_compare(PHP_VERSION, '7.1.0', '>=') && in_array('aes-' . $this->getKeyLength() . '-gcm', $methods) && $this->block_size == 16; case self::ENGINE_OPENSSL: if ($this->block_size != 16) { return false; } $this->cipher_name_openssl_ecb = 'aes-' . ($this->key_length << 3) . '-ecb'; $this->cipher_name_openssl = 'aes-' . ($this->key_length << 3) . '-' . $this->openssl_translate_mode(); break; case self::ENGINE_MCRYPT: $this->cipher_name_mcrypt = 'rijndael-' . ($this->block_size << 3); if ($this->key_length % 8) { // is it a 160/224-bit key? // mcrypt is not usable for them, only for 128/192/256-bit keys return false; } } return parent::isValidEngineHelper($engine); } /** * Encrypts a block * * @param string $in * @return string */ protected function encryptBlock($in) { static $tables; if (empty($tables)) { $tables = &$this->getTables(); } $t0 = $tables[0]; $t1 = $tables[1]; $t2 = $tables[2]; $t3 = $tables[3]; $sbox = $tables[4]; $state = []; $words = unpack('N*', $in); $c = $this->c; $w = $this->w; $Nb = $this->Nb; $Nr = $this->Nr; // addRoundKey $wc = $Nb - 1; foreach ($words as $word) { $state[] = $word ^ $w[++$wc]; } // fips-197.pdf#page=19, "Figure 5. Pseudo Code for the Cipher", states that this loop has four components - // subBytes, shiftRows, mixColumns, and addRoundKey. fips-197.pdf#page=30, "Implementation Suggestions Regarding // Various Platforms" suggests that performs enhanced implementations are described in Rijndael-ammended.pdf. // Rijndael-ammended.pdf#page=20, "Implementation aspects / 32-bit processor", discusses such an optimization. // Unfortunately, the description given there is not quite correct. Per aes.spec.v316.pdf#page=19 [1], // equation (7.4.7) is supposed to use addition instead of subtraction, so we'll do that here, as well. // [1] http://fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.v316.pdf $temp = []; for ($round = 1; $round < $Nr; ++$round) { $i = 0; // $c[0] == 0 $j = $c[1]; $k = $c[2]; $l = $c[3]; while ($i < $Nb) { $temp[$i] = $t0[$state[$i] >> 24 & 0x000000FF] ^ $t1[$state[$j] >> 16 & 0x000000FF] ^ $t2[$state[$k] >> 8 & 0x000000FF] ^ $t3[$state[$l] & 0x000000FF] ^ $w[++$wc]; ++$i; $j = ($j + 1) % $Nb; $k = ($k + 1) % $Nb; $l = ($l + 1) % $Nb; } $state = $temp; } // subWord for ($i = 0; $i < $Nb; ++$i) { $state[$i] = $sbox[$state[$i] & 0x000000FF] | ($sbox[$state[$i] >> 8 & 0x000000FF] << 8) | ($sbox[$state[$i] >> 16 & 0x000000FF] << 16) | ($sbox[$state[$i] >> 24 & 0x000000FF] << 24); } // shiftRows + addRoundKey $i = 0; // $c[0] == 0 $j = $c[1]; $k = $c[2]; $l = $c[3]; while ($i < $Nb) { $temp[$i] = ($state[$i] & intval(0xFF000000)) ^ ($state[$j] & 0x00FF0000) ^ ($state[$k] & 0x0000FF00) ^ ($state[$l] & 0x000000FF) ^ $w[$i]; ++$i; $j = ($j + 1) % $Nb; $k = ($k + 1) % $Nb; $l = ($l + 1) % $Nb; } return pack('N*', ...$temp); } /** * Decrypts a block * * @param string $in * @return string */ protected function decryptBlock($in) { static $invtables; if (empty($invtables)) { $invtables = &$this->getInvTables(); } $dt0 = $invtables[0]; $dt1 = $invtables[1]; $dt2 = $invtables[2]; $dt3 = $invtables[3]; $isbox = $invtables[4]; $state = []; $words = unpack('N*', $in); $c = $this->c; $dw = $this->dw; $Nb = $this->Nb; $Nr = $this->Nr; // addRoundKey $wc = $Nb - 1; foreach ($words as $word) { $state[] = $word ^ $dw[++$wc]; } $temp = []; for ($round = $Nr - 1; $round > 0; --$round) { $i = 0; // $c[0] == 0 $j = $Nb - $c[1]; $k = $Nb - $c[2]; $l = $Nb - $c[3]; while ($i < $Nb) { $temp[$i] = $dt0[$state[$i] >> 24 & 0x000000FF] ^ $dt1[$state[$j] >> 16 & 0x000000FF] ^ $dt2[$state[$k] >> 8 & 0x000000FF] ^ $dt3[$state[$l] & 0x000000FF] ^ $dw[++$wc]; ++$i; $j = ($j + 1) % $Nb; $k = ($k + 1) % $Nb; $l = ($l + 1) % $Nb; } $state = $temp; } // invShiftRows + invSubWord + addRoundKey $i = 0; // $c[0] == 0 $j = $Nb - $c[1]; $k = $Nb - $c[2]; $l = $Nb - $c[3]; while ($i < $Nb) { $word = ($state[$i] & intval(0xFF000000)) | ($state[$j] & 0x00FF0000) | ($state[$k] & 0x0000FF00) | ($state[$l] & 0x000000FF); $temp[$i] = $dw[$i] ^ ($isbox[$word & 0x000000FF] | ($isbox[$word >> 8 & 0x000000FF] << 8) | ($isbox[$word >> 16 & 0x000000FF] << 16) | ($isbox[$word >> 24 & 0x000000FF] << 24)); ++$i; $j = ($j + 1) % $Nb; $k = ($k + 1) % $Nb; $l = ($l + 1) % $Nb; } return pack('N*', ...$temp); } /** * Setup the self::ENGINE_INTERNAL $engine * * (re)init, if necessary, the internal cipher $engine and flush all $buffers * Used (only) if $engine == self::ENGINE_INTERNAL * * _setup() will be called each time if $changed === true * typically this happens when using one or more of following public methods: * * - setKey() * * - setIV() * * - disableContinuousBuffer() * * - First run of encrypt() / decrypt() with no init-settings * * {@internal setup() is always called before en/decryption.} * * {@internal Could, but not must, extend by the child Crypt_* class} * * @see self::setKey() * @see self::setIV() * @see self::disableContinuousBuffer() */ protected function setup() { if (!$this->changed) { return; } parent::setup(); if (is_string($this->iv) && strlen($this->iv) != $this->block_size) { throw new InconsistentSetupException('The IV length (' . strlen($this->iv) . ') does not match the block size (' . $this->block_size . ')'); } } /** * Setup the key (expansion) * * @see \phpseclib3\Crypt\Common\SymmetricKey::setupKey() */ protected function setupKey() { // Each number in $rcon is equal to the previous number multiplied by two in Rijndael's finite field. // See http://en.wikipedia.org/wiki/Finite_field_arithmetic#Multiplicative_inverse static $rcon; if (!isset($rcon)) { $rcon = [0, 0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000, 0x80000000, 0x1B000000, 0x36000000, 0x6C000000, 0xD8000000, 0xAB000000, 0x4D000000, 0x9A000000, 0x2F000000, 0x5E000000, 0xBC000000, 0x63000000, 0xC6000000, 0x97000000, 0x35000000, 0x6A000000, 0xD4000000, 0xB3000000, 0x7D000000, 0xFA000000, 0xEF000000, 0xC5000000, 0x91000000 ]; $rcon = array_map('intval', $rcon); } if (isset($this->kl['key']) && $this->key === $this->kl['key'] && $this->key_length === $this->kl['key_length'] && $this->block_size === $this->kl['block_size']) { // already expanded return; } $this->kl = ['key' => $this->key, 'key_length' => $this->key_length, 'block_size' => $this->block_size]; $this->Nk = $this->key_length >> 2; // see Rijndael-ammended.pdf#page=44 $this->Nr = max($this->Nk, $this->Nb) + 6; // shift offsets for Nb = 5, 7 are defined in Rijndael-ammended.pdf#page=44, // "Table 8: Shift offsets in Shiftrow for the alternative block lengths" // shift offsets for Nb = 4, 6, 8 are defined in Rijndael-ammended.pdf#page=14, // "Table 2: Shift offsets for different block lengths" switch ($this->Nb) { case 4: case 5: case 6: $this->c = [0, 1, 2, 3]; break; case 7: $this->c = [0, 1, 2, 4]; break; case 8: $this->c = [0, 1, 3, 4]; } $w = array_values(unpack('N*words', $this->key)); $length = $this->Nb * ($this->Nr + 1); for ($i = $this->Nk; $i < $length; $i++) { $temp = $w[$i - 1]; if ($i % $this->Nk == 0) { // according to , "the size of an integer is platform-dependent". // on a 32-bit machine, it's 32-bits, and on a 64-bit machine, it's 64-bits. on a 32-bit machine, // 0xFFFFFFFF << 8 == 0xFFFFFF00, but on a 64-bit machine, it equals 0xFFFFFFFF00. as such, doing 'and' // with 0xFFFFFFFF (or 0xFFFFFF00) on a 32-bit machine is unnecessary, but on a 64-bit machine, it is. $temp = (($temp << 8) & intval(0xFFFFFF00)) | (($temp >> 24) & 0x000000FF); // rotWord $temp = $this->subWord($temp) ^ $rcon[$i / $this->Nk]; } elseif ($this->Nk > 6 && $i % $this->Nk == 4) { $temp = $this->subWord($temp); } $w[$i] = $w[$i - $this->Nk] ^ $temp; } // convert the key schedule from a vector of $Nb * ($Nr + 1) length to a matrix with $Nr + 1 rows and $Nb columns // and generate the inverse key schedule. more specifically, // according to (section 5.3.3), // "The key expansion for the Inverse Cipher is defined as follows: // 1. Apply the Key Expansion. // 2. Apply InvMixColumn to all Round Keys except the first and the last one." // also, see fips-197.pdf#page=27, "5.3.5 Equivalent Inverse Cipher" list($dt0, $dt1, $dt2, $dt3) = $this->getInvTables(); $temp = $this->w = $this->dw = []; for ($i = $row = $col = 0; $i < $length; $i++, $col++) { if ($col == $this->Nb) { if ($row == 0) { $this->dw[0] = $this->w[0]; } else { // subWord + invMixColumn + invSubWord = invMixColumn $j = 0; while ($j < $this->Nb) { $dw = $this->subWord($this->w[$row][$j]); $temp[$j] = $dt0[$dw >> 24 & 0x000000FF] ^ $dt1[$dw >> 16 & 0x000000FF] ^ $dt2[$dw >> 8 & 0x000000FF] ^ $dt3[$dw & 0x000000FF]; $j++; } $this->dw[$row] = $temp; } $col = 0; $row++; } $this->w[$row][$col] = $w[$i]; } $this->dw[$row] = $this->w[$row]; // Converting to 1-dim key arrays (both ascending) $this->dw = array_reverse($this->dw); $w = array_pop($this->w); $dw = array_pop($this->dw); foreach ($this->w as $r => $wr) { foreach ($wr as $c => $wc) { $w[] = $wc; $dw[] = $this->dw[$r][$c]; } } $this->w = $w; $this->dw = $dw; } /** * Performs S-Box substitutions * * @return array * @param int $word */ private function subWord($word) { static $sbox; if (empty($sbox)) { list(, , , , $sbox) = self::getTables(); } return $sbox[$word & 0x000000FF] | ($sbox[$word >> 8 & 0x000000FF] << 8) | ($sbox[$word >> 16 & 0x000000FF] << 16) | ($sbox[$word >> 24 & 0x000000FF] << 24); } /** * Provides the mixColumns and sboxes tables * * @see self::encryptBlock() * @see self::setupInlineCrypt() * @see self::subWord() * @return array &$tables */ protected function &getTables() { static $tables; if (empty($tables)) { // according to (section 5.2.1), // precomputed tables can be used in the mixColumns phase. in that example, they're assigned t0...t3, so // those are the names we'll use. $t3 = array_map('intval', [ // with array_map('intval', ...) we ensure we have only int's and not // some slower floats converted by php automatically on high values 0x6363A5C6, 0x7C7C84F8, 0x777799EE, 0x7B7B8DF6, 0xF2F20DFF, 0x6B6BBDD6, 0x6F6FB1DE, 0xC5C55491, 0x30305060, 0x01010302, 0x6767A9CE, 0x2B2B7D56, 0xFEFE19E7, 0xD7D762B5, 0xABABE64D, 0x76769AEC, 0xCACA458F, 0x82829D1F, 0xC9C94089, 0x7D7D87FA, 0xFAFA15EF, 0x5959EBB2, 0x4747C98E, 0xF0F00BFB, 0xADADEC41, 0xD4D467B3, 0xA2A2FD5F, 0xAFAFEA45, 0x9C9CBF23, 0xA4A4F753, 0x727296E4, 0xC0C05B9B, 0xB7B7C275, 0xFDFD1CE1, 0x9393AE3D, 0x26266A4C, 0x36365A6C, 0x3F3F417E, 0xF7F702F5, 0xCCCC4F83, 0x34345C68, 0xA5A5F451, 0xE5E534D1, 0xF1F108F9, 0x717193E2, 0xD8D873AB, 0x31315362, 0x15153F2A, 0x04040C08, 0xC7C75295, 0x23236546, 0xC3C35E9D, 0x18182830, 0x9696A137, 0x05050F0A, 0x9A9AB52F, 0x0707090E, 0x12123624, 0x80809B1B, 0xE2E23DDF, 0xEBEB26CD, 0x2727694E, 0xB2B2CD7F, 0x75759FEA, 0x09091B12, 0x83839E1D, 0x2C2C7458, 0x1A1A2E34, 0x1B1B2D36, 0x6E6EB2DC, 0x5A5AEEB4, 0xA0A0FB5B, 0x5252F6A4, 0x3B3B4D76, 0xD6D661B7, 0xB3B3CE7D, 0x29297B52, 0xE3E33EDD, 0x2F2F715E, 0x84849713, 0x5353F5A6, 0xD1D168B9, 0x00000000, 0xEDED2CC1, 0x20206040, 0xFCFC1FE3, 0xB1B1C879, 0x5B5BEDB6, 0x6A6ABED4, 0xCBCB468D, 0xBEBED967, 0x39394B72, 0x4A4ADE94, 0x4C4CD498, 0x5858E8B0, 0xCFCF4A85, 0xD0D06BBB, 0xEFEF2AC5, 0xAAAAE54F, 0xFBFB16ED, 0x4343C586, 0x4D4DD79A, 0x33335566, 0x85859411, 0x4545CF8A, 0xF9F910E9, 0x02020604, 0x7F7F81FE, 0x5050F0A0, 0x3C3C4478, 0x9F9FBA25, 0xA8A8E34B, 0x5151F3A2, 0xA3A3FE5D, 0x4040C080, 0x8F8F8A05, 0x9292AD3F, 0x9D9DBC21, 0x38384870, 0xF5F504F1, 0xBCBCDF63, 0xB6B6C177, 0xDADA75AF, 0x21216342, 0x10103020, 0xFFFF1AE5, 0xF3F30EFD, 0xD2D26DBF, 0xCDCD4C81, 0x0C0C1418, 0x13133526, 0xECEC2FC3, 0x5F5FE1BE, 0x9797A235, 0x4444CC88, 0x1717392E, 0xC4C45793, 0xA7A7F255, 0x7E7E82FC, 0x3D3D477A, 0x6464ACC8, 0x5D5DE7BA, 0x19192B32, 0x737395E6, 0x6060A0C0, 0x81819819, 0x4F4FD19E, 0xDCDC7FA3, 0x22226644, 0x2A2A7E54, 0x9090AB3B, 0x8888830B, 0x4646CA8C, 0xEEEE29C7, 0xB8B8D36B, 0x14143C28, 0xDEDE79A7, 0x5E5EE2BC, 0x0B0B1D16, 0xDBDB76AD, 0xE0E03BDB, 0x32325664, 0x3A3A4E74, 0x0A0A1E14, 0x4949DB92, 0x06060A0C, 0x24246C48, 0x5C5CE4B8, 0xC2C25D9F, 0xD3D36EBD, 0xACACEF43, 0x6262A6C4, 0x9191A839, 0x9595A431, 0xE4E437D3, 0x79798BF2, 0xE7E732D5, 0xC8C8438B, 0x3737596E, 0x6D6DB7DA, 0x8D8D8C01, 0xD5D564B1, 0x4E4ED29C, 0xA9A9E049, 0x6C6CB4D8, 0x5656FAAC, 0xF4F407F3, 0xEAEA25CF, 0x6565AFCA, 0x7A7A8EF4, 0xAEAEE947, 0x08081810, 0xBABAD56F, 0x787888F0, 0x25256F4A, 0x2E2E725C, 0x1C1C2438, 0xA6A6F157, 0xB4B4C773, 0xC6C65197, 0xE8E823CB, 0xDDDD7CA1, 0x74749CE8, 0x1F1F213E, 0x4B4BDD96, 0xBDBDDC61, 0x8B8B860D, 0x8A8A850F, 0x707090E0, 0x3E3E427C, 0xB5B5C471, 0x6666AACC, 0x4848D890, 0x03030506, 0xF6F601F7, 0x0E0E121C, 0x6161A3C2, 0x35355F6A, 0x5757F9AE, 0xB9B9D069, 0x86869117, 0xC1C15899, 0x1D1D273A, 0x9E9EB927, 0xE1E138D9, 0xF8F813EB, 0x9898B32B, 0x11113322, 0x6969BBD2, 0xD9D970A9, 0x8E8E8907, 0x9494A733, 0x9B9BB62D, 0x1E1E223C, 0x87879215, 0xE9E920C9, 0xCECE4987, 0x5555FFAA, 0x28287850, 0xDFDF7AA5, 0x8C8C8F03, 0xA1A1F859, 0x89898009, 0x0D0D171A, 0xBFBFDA65, 0xE6E631D7, 0x4242C684, 0x6868B8D0, 0x4141C382, 0x9999B029, 0x2D2D775A, 0x0F0F111E, 0xB0B0CB7B, 0x5454FCA8, 0xBBBBD66D, 0x16163A2C ]); foreach ($t3 as $t3i) { $t0[] = (($t3i << 24) & intval(0xFF000000)) | (($t3i >> 8) & 0x00FFFFFF); $t1[] = (($t3i << 16) & intval(0xFFFF0000)) | (($t3i >> 16) & 0x0000FFFF); $t2[] = (($t3i << 8) & intval(0xFFFFFF00)) | (($t3i >> 24) & 0x000000FF); } $tables = [ // The Precomputed mixColumns tables t0 - t3 $t0, $t1, $t2, $t3, // The SubByte S-Box [ 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 ] ]; } return $tables; } /** * Provides the inverse mixColumns and inverse sboxes tables * * @see self::decryptBlock() * @see self::setupInlineCrypt() * @see self::setupKey() * @return array &$tables */ protected function &getInvTables() { static $tables; if (empty($tables)) { $dt3 = array_map('intval', [ 0xF4A75051, 0x4165537E, 0x17A4C31A, 0x275E963A, 0xAB6BCB3B, 0x9D45F11F, 0xFA58ABAC, 0xE303934B, 0x30FA5520, 0x766DF6AD, 0xCC769188, 0x024C25F5, 0xE5D7FC4F, 0x2ACBD7C5, 0x35448026, 0x62A38FB5, 0xB15A49DE, 0xBA1B6725, 0xEA0E9845, 0xFEC0E15D, 0x2F7502C3, 0x4CF01281, 0x4697A38D, 0xD3F9C66B, 0x8F5FE703, 0x929C9515, 0x6D7AEBBF, 0x5259DA95, 0xBE832DD4, 0x7421D358, 0xE0692949, 0xC9C8448E, 0xC2896A75, 0x8E7978F4, 0x583E6B99, 0xB971DD27, 0xE14FB6BE, 0x88AD17F0, 0x20AC66C9, 0xCE3AB47D, 0xDF4A1863, 0x1A3182E5, 0x51336097, 0x537F4562, 0x6477E0B1, 0x6BAE84BB, 0x81A01CFE, 0x082B94F9, 0x48685870, 0x45FD198F, 0xDE6C8794, 0x7BF8B752, 0x73D323AB, 0x4B02E272, 0x1F8F57E3, 0x55AB2A66, 0xEB2807B2, 0xB5C2032F, 0xC57B9A86, 0x3708A5D3, 0x2887F230, 0xBFA5B223, 0x036ABA02, 0x16825CED, 0xCF1C2B8A, 0x79B492A7, 0x07F2F0F3, 0x69E2A14E, 0xDAF4CD65, 0x05BED506, 0x34621FD1, 0xA6FE8AC4, 0x2E539D34, 0xF355A0A2, 0x8AE13205, 0xF6EB75A4, 0x83EC390B, 0x60EFAA40, 0x719F065E, 0x6E1051BD, 0x218AF93E, 0xDD063D96, 0x3E05AEDD, 0xE6BD464D, 0x548DB591, 0xC45D0571, 0x06D46F04, 0x5015FF60, 0x98FB2419, 0xBDE997D6, 0x4043CC89, 0xD99E7767, 0xE842BDB0, 0x898B8807, 0x195B38E7, 0xC8EEDB79, 0x7C0A47A1, 0x420FE97C, 0x841EC9F8, 0x00000000, 0x80868309, 0x2BED4832, 0x1170AC1E, 0x5A724E6C, 0x0EFFFBFD, 0x8538560F, 0xAED51E3D, 0x2D392736, 0x0FD9640A, 0x5CA62168, 0x5B54D19B, 0x362E3A24, 0x0A67B10C, 0x57E70F93, 0xEE96D2B4, 0x9B919E1B, 0xC0C54F80, 0xDC20A261, 0x774B695A, 0x121A161C, 0x93BA0AE2, 0xA02AE5C0, 0x22E0433C, 0x1B171D12, 0x090D0B0E, 0x8BC7ADF2, 0xB6A8B92D, 0x1EA9C814, 0xF1198557, 0x75074CAF, 0x99DDBBEE, 0x7F60FDA3, 0x01269FF7, 0x72F5BC5C, 0x663BC544, 0xFB7E345B, 0x4329768B, 0x23C6DCCB, 0xEDFC68B6, 0xE4F163B8, 0x31DCCAD7, 0x63851042, 0x97224013, 0xC6112084, 0x4A247D85, 0xBB3DF8D2, 0xF93211AE, 0x29A16DC7, 0x9E2F4B1D, 0xB230F3DC, 0x8652EC0D, 0xC1E3D077, 0xB3166C2B, 0x70B999A9, 0x9448FA11, 0xE9642247, 0xFC8CC4A8, 0xF03F1AA0, 0x7D2CD856, 0x3390EF22, 0x494EC787, 0x38D1C1D9, 0xCAA2FE8C, 0xD40B3698, 0xF581CFA6, 0x7ADE28A5, 0xB78E26DA, 0xADBFA43F, 0x3A9DE42C, 0x78920D50, 0x5FCC9B6A, 0x7E466254, 0x8D13C2F6, 0xD8B8E890, 0x39F75E2E, 0xC3AFF582, 0x5D80BE9F, 0xD0937C69, 0xD52DA96F, 0x2512B3CF, 0xAC993BC8, 0x187DA710, 0x9C636EE8, 0x3BBB7BDB, 0x267809CD, 0x5918F46E, 0x9AB701EC, 0x4F9AA883, 0x956E65E6, 0xFFE67EAA, 0xBCCF0821, 0x15E8E6EF, 0xE79BD9BA, 0x6F36CE4A, 0x9F09D4EA, 0xB07CD629, 0xA4B2AF31, 0x3F23312A, 0xA59430C6, 0xA266C035, 0x4EBC3774, 0x82CAA6FC, 0x90D0B0E0, 0xA7D81533, 0x04984AF1, 0xECDAF741, 0xCD500E7F, 0x91F62F17, 0x4DD68D76, 0xEFB04D43, 0xAA4D54CC, 0x9604DFE4, 0xD1B5E39E, 0x6A881B4C, 0x2C1FB8C1, 0x65517F46, 0x5EEA049D, 0x8C355D01, 0x877473FA, 0x0B412EFB, 0x671D5AB3, 0xDBD25292, 0x105633E9, 0xD647136D, 0xD7618C9A, 0xA10C7A37, 0xF8148E59, 0x133C89EB, 0xA927EECE, 0x61C935B7, 0x1CE5EDE1, 0x47B13C7A, 0xD2DF599C, 0xF2733F55, 0x14CE7918, 0xC737BF73, 0xF7CDEA53, 0xFDAA5B5F, 0x3D6F14DF, 0x44DB8678, 0xAFF381CA, 0x68C43EB9, 0x24342C38, 0xA3405FC2, 0x1DC37216, 0xE2250CBC, 0x3C498B28, 0x0D9541FF, 0xA8017139, 0x0CB3DE08, 0xB4E49CD8, 0x56C19064, 0xCB84617B, 0x32B670D5, 0x6C5C7448, 0xB85742D0 ]); foreach ($dt3 as $dt3i) { $dt0[] = (($dt3i << 24) & intval(0xFF000000)) | (($dt3i >> 8) & 0x00FFFFFF); $dt1[] = (($dt3i << 16) & intval(0xFFFF0000)) | (($dt3i >> 16) & 0x0000FFFF); $dt2[] = (($dt3i << 8) & intval(0xFFFFFF00)) | (($dt3i >> 24) & 0x000000FF); }; $tables = [ // The Precomputed inverse mixColumns tables dt0 - dt3 $dt0, $dt1, $dt2, $dt3, // The inverse SubByte S-Box [ 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D ] ]; } return $tables; } /** * Setup the performance-optimized function for de/encrypt() * * @see \phpseclib3\Crypt\Common\SymmetricKey::setupInlineCrypt() */ protected function setupInlineCrypt() { $w = $this->w; $dw = $this->dw; $init_encrypt = ''; $init_decrypt = ''; $Nr = $this->Nr; $Nb = $this->Nb; $c = $this->c; // Generating encrypt code: $init_encrypt .= ' if (empty($tables)) { $tables = &$this->getTables(); } $t0 = $tables[0]; $t1 = $tables[1]; $t2 = $tables[2]; $t3 = $tables[3]; $sbox = $tables[4]; '; $s = 'e'; $e = 's'; $wc = $Nb - 1; // Preround: addRoundKey $encrypt_block = '$in = unpack("N*", $in);' . "\n"; for ($i = 0; $i < $Nb; ++$i) { $encrypt_block .= '$s' . $i . ' = $in[' . ($i + 1) . '] ^ ' . $w[++$wc] . ";\n"; } // Mainrounds: shiftRows + subWord + mixColumns + addRoundKey for ($round = 1; $round < $Nr; ++$round) { list($s, $e) = [$e, $s]; for ($i = 0; $i < $Nb; ++$i) { $encrypt_block .= '$' . $e . $i . ' = $t0[($' . $s . $i . ' >> 24) & 0xff] ^ $t1[($' . $s . (($i + $c[1]) % $Nb) . ' >> 16) & 0xff] ^ $t2[($' . $s . (($i + $c[2]) % $Nb) . ' >> 8) & 0xff] ^ $t3[ $' . $s . (($i + $c[3]) % $Nb) . ' & 0xff] ^ ' . $w[++$wc] . ";\n"; } } // Finalround: subWord + shiftRows + addRoundKey for ($i = 0; $i < $Nb; ++$i) { $encrypt_block .= '$' . $e . $i . ' = $sbox[ $' . $e . $i . ' & 0xff] | ($sbox[($' . $e . $i . ' >> 8) & 0xff] << 8) | ($sbox[($' . $e . $i . ' >> 16) & 0xff] << 16) | ($sbox[($' . $e . $i . ' >> 24) & 0xff] << 24);' . "\n"; } $encrypt_block .= '$in = pack("N*"' . "\n"; for ($i = 0; $i < $Nb; ++$i) { $encrypt_block .= ', ($' . $e . $i . ' & ' . ((int)0xFF000000) . ') ^ ($' . $e . (($i + $c[1]) % $Nb) . ' & 0x00FF0000 ) ^ ($' . $e . (($i + $c[2]) % $Nb) . ' & 0x0000FF00 ) ^ ($' . $e . (($i + $c[3]) % $Nb) . ' & 0x000000FF ) ^ ' . $w[$i] . "\n"; } $encrypt_block .= ');'; // Generating decrypt code: $init_decrypt .= ' if (empty($invtables)) { $invtables = &$this->getInvTables(); } $dt0 = $invtables[0]; $dt1 = $invtables[1]; $dt2 = $invtables[2]; $dt3 = $invtables[3]; $isbox = $invtables[4]; '; $s = 'e'; $e = 's'; $wc = $Nb - 1; // Preround: addRoundKey $decrypt_block = '$in = unpack("N*", $in);' . "\n"; for ($i = 0; $i < $Nb; ++$i) { $decrypt_block .= '$s' . $i . ' = $in[' . ($i + 1) . '] ^ ' . $dw[++$wc] . ';' . "\n"; } // Mainrounds: shiftRows + subWord + mixColumns + addRoundKey for ($round = 1; $round < $Nr; ++$round) { list($s, $e) = [$e, $s]; for ($i = 0; $i < $Nb; ++$i) { $decrypt_block .= '$' . $e . $i . ' = $dt0[($' . $s . $i . ' >> 24) & 0xff] ^ $dt1[($' . $s . (($Nb + $i - $c[1]) % $Nb) . ' >> 16) & 0xff] ^ $dt2[($' . $s . (($Nb + $i - $c[2]) % $Nb) . ' >> 8) & 0xff] ^ $dt3[ $' . $s . (($Nb + $i - $c[3]) % $Nb) . ' & 0xff] ^ ' . $dw[++$wc] . ";\n"; } } // Finalround: subWord + shiftRows + addRoundKey for ($i = 0; $i < $Nb; ++$i) { $decrypt_block .= '$' . $e . $i . ' = $isbox[ $' . $e . $i . ' & 0xff] | ($isbox[($' . $e . $i . ' >> 8) & 0xff] << 8) | ($isbox[($' . $e . $i . ' >> 16) & 0xff] << 16) | ($isbox[($' . $e . $i . ' >> 24) & 0xff] << 24);' . "\n"; } $decrypt_block .= '$in = pack("N*"' . "\n"; for ($i = 0; $i < $Nb; ++$i) { $decrypt_block .= ', ($' . $e . $i . ' & ' . ((int)0xFF000000) . ') ^ ($' . $e . (($Nb + $i - $c[1]) % $Nb) . ' & 0x00FF0000 ) ^ ($' . $e . (($Nb + $i - $c[2]) % $Nb) . ' & 0x0000FF00 ) ^ ($' . $e . (($Nb + $i - $c[3]) % $Nb) . ' & 0x000000FF ) ^ ' . $dw[$i] . "\n"; } $decrypt_block .= ');'; $this->inline_crypt = $this->createInlineCryptFunction( [ 'init_crypt' => 'static $tables; static $invtables;', 'init_encrypt' => $init_encrypt, 'init_decrypt' => $init_decrypt, 'encrypt_block' => $encrypt_block, 'decrypt_block' => $decrypt_block ] ); } /** * Encrypts a message. * * @see self::decrypt() * @see parent::encrypt() * @param string $plaintext * @return string */ public function encrypt($plaintext) { $this->setup(); switch ($this->engine) { case self::ENGINE_LIBSODIUM: $this->newtag = sodium_crypto_aead_aes256gcm_encrypt($plaintext, $this->aad, $this->nonce, $this->key); return Strings::shift($this->newtag, strlen($plaintext)); case self::ENGINE_OPENSSL_GCM: return openssl_encrypt( $plaintext, 'aes-' . $this->getKeyLength() . '-gcm', $this->key, OPENSSL_RAW_DATA, $this->nonce, $this->newtag, $this->aad ); } return parent::encrypt($plaintext); } /** * Decrypts a message. * * @see self::encrypt() * @see parent::decrypt() * @param string $ciphertext * @return string */ public function decrypt($ciphertext) { $this->setup(); switch ($this->engine) { case self::ENGINE_LIBSODIUM: if ($this->oldtag === false) { throw new InsufficientSetupException('Authentication Tag has not been set'); } if (strlen($this->oldtag) != 16) { break; } $plaintext = sodium_crypto_aead_aes256gcm_decrypt($ciphertext . $this->oldtag, $this->aad, $this->nonce, $this->key); if ($plaintext === false) { $this->oldtag = false; throw new BadDecryptionException('Error decrypting ciphertext with libsodium'); } return $plaintext; case self::ENGINE_OPENSSL_GCM: if ($this->oldtag === false) { throw new InsufficientSetupException('Authentication Tag has not been set'); } $plaintext = openssl_decrypt( $ciphertext, 'aes-' . $this->getKeyLength() . '-gcm', $this->key, OPENSSL_RAW_DATA, $this->nonce, $this->oldtag, $this->aad ); if ($plaintext === false) { $this->oldtag = false; throw new BadDecryptionException('Error decrypting ciphertext with OpenSSL'); } return $plaintext; } return parent::decrypt($ciphertext); } } PK!vll2vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.phpnu[ * getPublicKey(); * * $plaintext = 'terrafrost'; * * $ciphertext = $public->encrypt($plaintext); * * echo $private->decrypt($ciphertext); * ?> * * * Here's an example of how to create signatures and verify signatures with this library: * * getPublicKey(); * * $plaintext = 'terrafrost'; * * $signature = $private->sign($plaintext); * * echo $public->verify($plaintext, $signature) ? 'verified' : 'unverified'; * ?> * * * One thing to consider when using this: so phpseclib uses PSS mode by default. * Technically, id-RSASSA-PSS has a different key format than rsaEncryption. So * should phpseclib save to the id-RSASSA-PSS format by default or the * rsaEncryption format? For stand-alone keys I figure rsaEncryption is better * because SSH doesn't use PSS and idk how many SSH servers would be able to * decode an id-RSASSA-PSS key. For X.509 certificates the id-RSASSA-PSS * format is used by default (unless you change it up to use PKCS1 instead) * * @author Jim Wigginton * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\RSA\Formats\Keys\PSS; use phpseclib3\Crypt\RSA\PrivateKey; use phpseclib3\Crypt\RSA\PublicKey; use phpseclib3\Exception\InconsistentSetupException; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Math\BigInteger; /** * Pure-PHP PKCS#1 compliant implementation of RSA. * * @author Jim Wigginton */ abstract class RSA extends AsymmetricKey { /** * Algorithm Name * * @var string */ const ALGORITHM = 'RSA'; /** * Use {@link http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding Optimal Asymmetric Encryption Padding} * (OAEP) for encryption / decryption. * * Uses sha256 by default * * @see self::setHash() * @see self::setMGFHash() * @see self::encrypt() * @see self::decrypt() */ const ENCRYPTION_OAEP = 1; /** * Use PKCS#1 padding. * * Although self::PADDING_OAEP / self::PADDING_PSS offers more security, including PKCS#1 padding is necessary for purposes of backwards * compatibility with protocols (like SSH-1) written before OAEP's introduction. * * @see self::encrypt() * @see self::decrypt() */ const ENCRYPTION_PKCS1 = 2; /** * Do not use any padding * * Although this method is not recommended it can none-the-less sometimes be useful if you're trying to decrypt some legacy * stuff, if you're trying to diagnose why an encrypted message isn't decrypting, etc. * * @see self::encrypt() * @see self::decrypt() */ const ENCRYPTION_NONE = 4; /** * Use the Probabilistic Signature Scheme for signing * * Uses sha256 and 0 as the salt length * * @see self::setSaltLength() * @see self::setMGFHash() * @see self::setHash() * @see self::sign() * @see self::verify() * @see self::setHash() */ const SIGNATURE_PSS = 16; /** * Use a relaxed version of PKCS#1 padding for signature verification * * @see self::sign() * @see self::verify() * @see self::setHash() */ const SIGNATURE_RELAXED_PKCS1 = 32; /** * Use PKCS#1 padding for signature verification * * @see self::sign() * @see self::verify() * @see self::setHash() */ const SIGNATURE_PKCS1 = 64; /** * Encryption padding mode * * @var int */ protected $encryptionPadding = self::ENCRYPTION_OAEP; /** * Signature padding mode * * @var int */ protected $signaturePadding = self::SIGNATURE_PSS; /** * Length of hash function output * * @var int */ protected $hLen; /** * Length of salt * * @var int */ protected $sLen; /** * Label * * @var string */ protected $label = ''; /** * Hash function for the Mask Generation Function * * @var Hash */ protected $mgfHash; /** * Length of MGF hash function output * * @var int */ protected $mgfHLen; /** * Modulus (ie. n) * * @var Math\BigInteger */ protected $modulus; /** * Modulus length * * @var Math\BigInteger */ protected $k; /** * Exponent (ie. e or d) * * @var Math\BigInteger */ protected $exponent; /** * Default public exponent * * @var int * @link http://en.wikipedia.org/wiki/65537_%28number%29 */ private static $defaultExponent = 65537; /** * Enable Blinding? * * @var bool */ protected static $enableBlinding = true; /** * OpenSSL configuration file name. * * @see self::createKey() * @var ?string */ protected static $configFile; /** * Smallest Prime * * Per , this number ought not result in primes smaller * than 256 bits. As a consequence if the key you're trying to create is 1024 bits and you've set smallestPrime * to 384 bits then you're going to get a 384 bit prime and a 640 bit prime (384 + 1024 % 384). At least if * engine is set to self::ENGINE_INTERNAL. If Engine is set to self::ENGINE_OPENSSL then smallest Prime is * ignored (ie. multi-prime RSA support is more intended as a way to speed up RSA key generation when there's * a chance neither gmp nor OpenSSL are installed) * * @var int */ private static $smallestPrime = 4096; /** * Public Exponent * * @var Math\BigInteger */ protected $publicExponent; /** * Sets the public exponent for key generation * * This will be 65537 unless changed. * * @param int $val */ public static function setExponent($val) { self::$defaultExponent = $val; } /** * Sets the smallest prime number in bits. Used for key generation * * This will be 4096 unless changed. * * @param int $val */ public static function setSmallestPrime($val) { self::$smallestPrime = $val; } /** * Sets the OpenSSL config file path * * Set to the empty string to use the default config file * * @param string $val */ public static function setOpenSSLConfigPath($val) { self::$configFile = $val; } /** * Create a private key * * The public key can be extracted from the private key * * @return PrivateKey * @param int $bits */ public static function createKey($bits = 2048) { self::initialize_static_variables(); $class = new \ReflectionClass(static::class); if ($class->isFinal()) { throw new \RuntimeException('createKey() should not be called from final classes (' . static::class . ')'); } $regSize = $bits >> 1; // divide by two to see how many bits P and Q would be if ($regSize > self::$smallestPrime) { $num_primes = floor($bits / self::$smallestPrime); $regSize = self::$smallestPrime; } else { $num_primes = 2; } if ($num_primes == 2 && $bits >= 384 && self::$defaultExponent == 65537) { if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } // OpenSSL uses 65537 as the exponent and requires RSA keys be 384 bits minimum if (self::$engines['OpenSSL']) { $config = []; if (self::$configFile) { $config['config'] = self::$configFile; } $rsa = openssl_pkey_new(['private_key_bits' => $bits] + $config); openssl_pkey_export($rsa, $privatekeystr, null, $config); // clear the buffer of error strings stemming from a minimalistic openssl.cnf // https://github.com/php/php-src/issues/11054 talks about other errors this'll pick up while (openssl_error_string() !== false) { } return RSA::load($privatekeystr); } } static $e; if (!isset($e)) { $e = new BigInteger(self::$defaultExponent); } $n = clone self::$one; $exponents = $coefficients = $primes = []; $lcm = [ 'top' => clone self::$one, 'bottom' => false ]; do { for ($i = 1; $i <= $num_primes; $i++) { if ($i != $num_primes) { $primes[$i] = BigInteger::randomPrime($regSize); } else { extract(BigInteger::minMaxBits($bits)); /** @var BigInteger $min * @var BigInteger $max */ list($min) = $min->divide($n); $min = $min->add(self::$one); list($max) = $max->divide($n); $primes[$i] = BigInteger::randomRangePrime($min, $max); } // the first coefficient is calculated differently from the rest // ie. instead of being $primes[1]->modInverse($primes[2]), it's $primes[2]->modInverse($primes[1]) if ($i > 2) { $coefficients[$i] = $n->modInverse($primes[$i]); } $n = $n->multiply($primes[$i]); $temp = $primes[$i]->subtract(self::$one); // textbook RSA implementations use Euler's totient function instead of the least common multiple. // see http://en.wikipedia.org/wiki/Euler%27s_totient_function $lcm['top'] = $lcm['top']->multiply($temp); $lcm['bottom'] = $lcm['bottom'] === false ? $temp : $lcm['bottom']->gcd($temp); } list($temp) = $lcm['top']->divide($lcm['bottom']); $gcd = $temp->gcd($e); $i0 = 1; } while (!$gcd->equals(self::$one)); $coefficients[2] = $primes[2]->modInverse($primes[1]); $d = $e->modInverse($temp); foreach ($primes as $i => $prime) { $temp = $prime->subtract(self::$one); $exponents[$i] = $e->modInverse($temp); } // from : // RSAPrivateKey ::= SEQUENCE { // version Version, // modulus INTEGER, -- n // publicExponent INTEGER, -- e // privateExponent INTEGER, -- d // prime1 INTEGER, -- p // prime2 INTEGER, -- q // exponent1 INTEGER, -- d mod (p-1) // exponent2 INTEGER, -- d mod (q-1) // coefficient INTEGER, -- (inverse of q) mod p // otherPrimeInfos OtherPrimeInfos OPTIONAL // } $privatekey = new PrivateKey(); $privatekey->modulus = $n; $privatekey->k = $bits >> 3; $privatekey->publicExponent = $e; $privatekey->exponent = $d; $privatekey->primes = $primes; $privatekey->exponents = $exponents; $privatekey->coefficients = $coefficients; /* $publickey = new PublicKey; $publickey->modulus = $n; $publickey->k = $bits >> 3; $publickey->exponent = $e; $publickey->publicExponent = $e; $publickey->isPublic = true; */ return $privatekey; } /** * OnLoad Handler * * @return bool */ protected static function onLoad(array $components) { $key = $components['isPublicKey'] ? new PublicKey() : new PrivateKey(); $key->modulus = $components['modulus']; $key->publicExponent = $components['publicExponent']; $key->k = $key->modulus->getLengthInBytes(); if ($components['isPublicKey'] || !isset($components['privateExponent'])) { $key->exponent = $key->publicExponent; } else { $key->privateExponent = $components['privateExponent']; $key->exponent = $key->privateExponent; $key->primes = $components['primes']; $key->exponents = $components['exponents']; $key->coefficients = $components['coefficients']; } if ($components['format'] == PSS::class) { // in the X509 world RSA keys are assumed to use PKCS1 padding by default. only if the key is // explicitly a PSS key is the use of PSS assumed. phpseclib does not work like this. phpseclib // uses PSS padding by default. it assumes the more secure method by default and altho it provides // for the less secure PKCS1 method you have to go out of your way to use it. this is consistent // with the latest trends in crypto. libsodium (NaCl) is actually a little more extreme in that // not only does it defaults to the most secure methods - it doesn't even let you choose less // secure methods //$key = $key->withPadding(self::SIGNATURE_PSS); if (isset($components['hash'])) { $key = $key->withHash($components['hash']); } if (isset($components['MGFHash'])) { $key = $key->withMGFHash($components['MGFHash']); } if (isset($components['saltLength'])) { $key = $key->withSaltLength($components['saltLength']); } } return $key; } /** * Initialize static variables */ protected static function initialize_static_variables() { if (!isset(self::$configFile)) { self::$configFile = dirname(__FILE__) . '/../openssl.cnf'; } parent::initialize_static_variables(); } /** * Constructor * * PublicKey and PrivateKey objects can only be created from abstract RSA class */ protected function __construct() { parent::__construct(); $this->hLen = $this->hash->getLengthInBytes(); $this->mgfHash = new Hash('sha256'); $this->mgfHLen = $this->mgfHash->getLengthInBytes(); } /** * Integer-to-Octet-String primitive * * See {@link http://tools.ietf.org/html/rfc3447#section-4.1 RFC3447#section-4.1}. * * @param bool|Math\BigInteger $x * @param int $xLen * @return bool|string */ protected function i2osp($x, $xLen) { if ($x === false) { return false; } $x = $x->toBytes(); if (strlen($x) > $xLen) { throw new \OutOfRangeException('Resultant string length out of range'); } return str_pad($x, $xLen, chr(0), STR_PAD_LEFT); } /** * Octet-String-to-Integer primitive * * See {@link http://tools.ietf.org/html/rfc3447#section-4.2 RFC3447#section-4.2}. * * @param string $x * @return Math\BigInteger */ protected function os2ip($x) { return new BigInteger($x, 256); } /** * EMSA-PKCS1-V1_5-ENCODE * * See {@link http://tools.ietf.org/html/rfc3447#section-9.2 RFC3447#section-9.2}. * * @param string $m * @param int $emLen * @throws \LengthException if the intended encoded message length is too short * @return string */ protected function emsa_pkcs1_v1_5_encode($m, $emLen) { $h = $this->hash->hash($m); // see http://tools.ietf.org/html/rfc3447#page-43 switch ($this->hash->getHash()) { case 'md2': $t = "\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x02\x05\x00\x04\x10"; break; case 'md5': $t = "\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10"; break; case 'sha1': $t = "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14"; break; case 'sha256': $t = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20"; break; case 'sha384': $t = "\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30"; break; case 'sha512': $t = "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40"; break; // from https://www.emc.com/collateral/white-papers/h11300-pkcs-1v2-2-rsa-cryptography-standard-wp.pdf#page=40 case 'sha224': $t = "\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c"; break; case 'sha512/224': $t = "\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x05\x05\x00\x04\x1c"; break; case 'sha512/256': $t = "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x06\x05\x00\x04\x20"; } $t .= $h; $tLen = strlen($t); if ($emLen < $tLen + 11) { throw new \LengthException('Intended encoded message length too short'); } $ps = str_repeat(chr(0xFF), $emLen - $tLen - 3); $em = "\0\1$ps\0$t"; return $em; } /** * EMSA-PKCS1-V1_5-ENCODE (without NULL) * * Quoting https://tools.ietf.org/html/rfc8017#page-65, * * "The parameters field associated with id-sha1, id-sha224, id-sha256, * id-sha384, id-sha512, id-sha512/224, and id-sha512/256 should * generally be omitted, but if present, it shall have a value of type * NULL" * * @param string $m * @param int $emLen * @return string */ protected function emsa_pkcs1_v1_5_encode_without_null($m, $emLen) { $h = $this->hash->hash($m); // see http://tools.ietf.org/html/rfc3447#page-43 switch ($this->hash->getHash()) { case 'sha1': $t = "\x30\x1f\x30\x07\x06\x05\x2b\x0e\x03\x02\x1a\x04\x14"; break; case 'sha256': $t = "\x30\x2f\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x04\x20"; break; case 'sha384': $t = "\x30\x3f\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x04\x30"; break; case 'sha512': $t = "\x30\x4f\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x04\x40"; break; // from https://www.emc.com/collateral/white-papers/h11300-pkcs-1v2-2-rsa-cryptography-standard-wp.pdf#page=40 case 'sha224': $t = "\x30\x2b\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x04\x1c"; break; case 'sha512/224': $t = "\x30\x2b\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x05\x04\x1c"; break; case 'sha512/256': $t = "\x30\x2f\x30\x0b\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x06\x04\x20"; break; default: throw new UnsupportedAlgorithmException('md2 and md5 require NULLs'); } $t .= $h; $tLen = strlen($t); if ($emLen < $tLen + 11) { throw new \LengthException('Intended encoded message length too short'); } $ps = str_repeat(chr(0xFF), $emLen - $tLen - 3); $em = "\0\1$ps\0$t"; return $em; } /** * MGF1 * * See {@link http://tools.ietf.org/html/rfc3447#appendix-B.2.1 RFC3447#appendix-B.2.1}. * * @param string $mgfSeed * @param int $maskLen * @return string */ protected function mgf1($mgfSeed, $maskLen) { // if $maskLen would yield strings larger than 4GB, PKCS#1 suggests a "Mask too long" error be output. $t = ''; $count = ceil($maskLen / $this->mgfHLen); for ($i = 0; $i < $count; $i++) { $c = pack('N', $i); $t .= $this->mgfHash->hash($mgfSeed . $c); } return substr($t, 0, $maskLen); } /** * Returns the key size * * More specifically, this returns the size of the modulo in bits. * * @return int */ public function getLength() { return !isset($this->modulus) ? 0 : $this->modulus->getLength(); } /** * Determines which hashing function should be used * * Used with signature production / verification and (if the encryption mode is self::PADDING_OAEP) encryption and * decryption. * * @param string $hash */ public function withHash($hash) { $new = clone $this; // Crypt\Hash supports algorithms that PKCS#1 doesn't support. md5-96 and sha1-96, for example. switch (strtolower($hash)) { case 'md2': case 'md5': case 'sha1': case 'sha256': case 'sha384': case 'sha512': case 'sha224': case 'sha512/224': case 'sha512/256': $new->hash = new Hash($hash); break; default: throw new UnsupportedAlgorithmException( 'The only supported hash algorithms are: md2, md5, sha1, sha256, sha384, sha512, sha224, sha512/224, sha512/256' ); } $new->hLen = $new->hash->getLengthInBytes(); return $new; } /** * Determines which hashing function should be used for the mask generation function * * The mask generation function is used by self::PADDING_OAEP and self::PADDING_PSS and although it's * best if Hash and MGFHash are set to the same thing this is not a requirement. * * @param string $hash */ public function withMGFHash($hash) { $new = clone $this; // Crypt\Hash supports algorithms that PKCS#1 doesn't support. md5-96 and sha1-96, for example. switch (strtolower($hash)) { case 'md2': case 'md5': case 'sha1': case 'sha256': case 'sha384': case 'sha512': case 'sha224': case 'sha512/224': case 'sha512/256': $new->mgfHash = new Hash($hash); break; default: throw new UnsupportedAlgorithmException( 'The only supported hash algorithms are: md2, md5, sha1, sha256, sha384, sha512, sha224, sha512/224, sha512/256' ); } $new->mgfHLen = $new->mgfHash->getLengthInBytes(); return $new; } /** * Returns the MGF hash algorithm currently being used * */ public function getMGFHash() { return clone $this->mgfHash; } /** * Determines the salt length * * Used by RSA::PADDING_PSS * * To quote from {@link http://tools.ietf.org/html/rfc3447#page-38 RFC3447#page-38}: * * Typical salt lengths in octets are hLen (the length of the output * of the hash function Hash) and 0. * * @param int $sLen */ public function withSaltLength($sLen) { $new = clone $this; $new->sLen = $sLen; return $new; } /** * Returns the salt length currently being used * */ public function getSaltLength() { return $this->sLen !== null ? $this->sLen : $this->hLen; } /** * Determines the label * * Used by RSA::PADDING_OAEP * * To quote from {@link http://tools.ietf.org/html/rfc3447#page-17 RFC3447#page-17}: * * Both the encryption and the decryption operations of RSAES-OAEP take * the value of a label L as input. In this version of PKCS #1, L is * the empty string; other uses of the label are outside the scope of * this document. * * @param string $label */ public function withLabel($label) { $new = clone $this; $new->label = $label; return $new; } /** * Returns the label currently being used * */ public function getLabel() { return $this->label; } /** * Determines the padding modes * * Example: $key->withPadding(RSA::ENCRYPTION_PKCS1 | RSA::SIGNATURE_PKCS1); * * @param int $padding */ public function withPadding($padding) { $masks = [ self::ENCRYPTION_OAEP, self::ENCRYPTION_PKCS1, self::ENCRYPTION_NONE ]; $encryptedCount = 0; $selected = 0; foreach ($masks as $mask) { if ($padding & $mask) { $selected = $mask; $encryptedCount++; } } if ($encryptedCount > 1) { throw new InconsistentSetupException('Multiple encryption padding modes have been selected; at most only one should be selected'); } $encryptionPadding = $selected; $masks = [ self::SIGNATURE_PSS, self::SIGNATURE_RELAXED_PKCS1, self::SIGNATURE_PKCS1 ]; $signatureCount = 0; $selected = 0; foreach ($masks as $mask) { if ($padding & $mask) { $selected = $mask; $signatureCount++; } } if ($signatureCount > 1) { throw new InconsistentSetupException('Multiple signature padding modes have been selected; at most only one should be selected'); } $signaturePadding = $selected; $new = clone $this; if ($encryptedCount) { $new->encryptionPadding = $encryptionPadding; } if ($signatureCount) { $new->signaturePadding = $signaturePadding; } return $new; } /** * Returns the padding currently being used * */ public function getPadding() { return $this->signaturePadding | $this->encryptionPadding; } /** * Returns the current engine being used * * OpenSSL is only used in this class (and it's subclasses) for key generation * Even then it depends on the parameters you're using. It's not used for * multi-prime RSA nor is it used if the key length is outside of the range * supported by OpenSSL * * @see self::useInternalEngine() * @see self::useBestEngine() * @return string */ public function getEngine() { if (!isset(self::$engines['PHP'])) { self::useBestEngine(); } return self::$engines['OpenSSL'] && self::$defaultExponent == 65537 ? 'OpenSSL' : 'PHP'; } /** * Enable RSA Blinding * */ public static function enableBlinding() { static::$enableBlinding = true; } /** * Disable RSA Blinding * */ public static function disableBlinding() { static::$enableBlinding = false; } } PK!}996vendor/phpseclib/phpseclib/phpseclib/Crypt/Salsa20.phpnu[ * @copyright 2019 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\StreamCipher; use phpseclib3\Exception\BadDecryptionException; use phpseclib3\Exception\InsufficientSetupException; /** * Pure-PHP implementation of Salsa20. * * @author Jim Wigginton */ class Salsa20 extends StreamCipher { /** * Part 1 of the state * * @var string|false */ protected $p1 = false; /** * Part 2 of the state * * @var string|false */ protected $p2 = false; /** * Key Length (in bytes) * * @var int */ protected $key_length = 32; // = 256 bits /** * @see \phpseclib3\Crypt\Salsa20::crypt() */ const ENCRYPT = 0; /** * @see \phpseclib3\Crypt\Salsa20::crypt() */ const DECRYPT = 1; /** * Encryption buffer for continuous mode * * @var array */ protected $enbuffer; /** * Decryption buffer for continuous mode * * @var array */ protected $debuffer; /** * Counter * * @var int */ protected $counter = 0; /** * Using Generated Poly1305 Key * * @var boolean */ protected $usingGeneratedPoly1305Key = false; /** * Salsa20 uses a nonce * * @return bool */ public function usesNonce() { return true; } /** * Sets the key. * * @param string $key * @throws \LengthException if the key length isn't supported */ public function setKey($key) { switch (strlen($key)) { case 16: case 32: break; default: throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes 16 or 32 are supported'); } parent::setKey($key); } /** * Sets the nonce. * * @param string $nonce */ public function setNonce($nonce) { if (strlen($nonce) != 8) { throw new \LengthException('Nonce of size ' . strlen($key) . ' not supported by this algorithm. Only an 64-bit nonce is supported'); } $this->nonce = $nonce; $this->changed = true; $this->setEngine(); } /** * Sets the counter. * * @param int $counter */ public function setCounter($counter) { $this->counter = $counter; $this->setEngine(); } /** * Creates a Poly1305 key using the method discussed in RFC8439 * * See https://tools.ietf.org/html/rfc8439#section-2.6.1 */ protected function createPoly1305Key() { if ($this->nonce === false) { throw new InsufficientSetupException('No nonce has been defined'); } if ($this->key === false) { throw new InsufficientSetupException('No key has been defined'); } $c = clone $this; $c->setCounter(0); $c->usePoly1305 = false; $block = $c->encrypt(str_repeat("\0", 256)); $this->setPoly1305Key(substr($block, 0, 32)); if ($this->counter == 0) { $this->counter++; } } /** * Setup the self::ENGINE_INTERNAL $engine * * (re)init, if necessary, the internal cipher $engine * * _setup() will be called each time if $changed === true * typically this happens when using one or more of following public methods: * * - setKey() * * - setNonce() * * - First run of encrypt() / decrypt() with no init-settings * * @see self::setKey() * @see self::setNonce() * @see self::disableContinuousBuffer() */ protected function setup() { if (!$this->changed) { return; } $this->enbuffer = $this->debuffer = ['ciphertext' => '', 'counter' => $this->counter]; $this->changed = $this->nonIVChanged = false; if ($this->nonce === false) { throw new InsufficientSetupException('No nonce has been defined'); } if ($this->key === false) { throw new InsufficientSetupException('No key has been defined'); } if ($this->usePoly1305 && !isset($this->poly1305Key)) { $this->usingGeneratedPoly1305Key = true; $this->createPoly1305Key(); } $key = $this->key; if (strlen($key) == 16) { $constant = 'expand 16-byte k'; $key .= $key; } else { $constant = 'expand 32-byte k'; } $this->p1 = substr($constant, 0, 4) . substr($key, 0, 16) . substr($constant, 4, 4) . $this->nonce . "\0\0\0\0"; $this->p2 = substr($constant, 8, 4) . substr($key, 16, 16) . substr($constant, 12, 4); } /** * Setup the key (expansion) */ protected function setupKey() { // Salsa20 does not utilize this method } /** * Encrypts a message. * * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() * @see self::crypt() * @param string $plaintext * @return string $ciphertext */ public function encrypt($plaintext) { $ciphertext = $this->crypt($plaintext, self::ENCRYPT); if (isset($this->poly1305Key)) { $this->newtag = $this->poly1305($ciphertext); } return $ciphertext; } /** * Decrypts a message. * * $this->decrypt($this->encrypt($plaintext)) == $this->encrypt($this->encrypt($plaintext)). * At least if the continuous buffer is disabled. * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @see self::crypt() * @param string $ciphertext * @return string $plaintext */ public function decrypt($ciphertext) { if (isset($this->poly1305Key)) { if ($this->oldtag === false) { throw new InsufficientSetupException('Authentication Tag has not been set'); } $newtag = $this->poly1305($ciphertext); if ($this->oldtag != substr($newtag, 0, strlen($this->oldtag))) { $this->oldtag = false; throw new BadDecryptionException('Derived authentication tag and supplied authentication tag do not match'); } $this->oldtag = false; } return $this->crypt($ciphertext, self::DECRYPT); } /** * Encrypts a block * * @param string $in */ protected function encryptBlock($in) { // Salsa20 does not utilize this method } /** * Decrypts a block * * @param string $in */ protected function decryptBlock($in) { // Salsa20 does not utilize this method } /** * Encrypts or decrypts a message. * * @see self::encrypt() * @see self::decrypt() * @param string $text * @param int $mode * @return string $text */ private function crypt($text, $mode) { $this->setup(); if (!$this->continuousBuffer) { if ($this->engine == self::ENGINE_OPENSSL) { $iv = pack('V', $this->counter) . $this->p2; return openssl_encrypt( $text, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA, $iv ); } $i = $this->counter; $blocks = str_split($text, 64); foreach ($blocks as &$block) { $block ^= static::salsa20($this->p1 . pack('V', $i++) . $this->p2); } unset($block); return implode('', $blocks); } if ($mode == self::ENCRYPT) { $buffer = &$this->enbuffer; } else { $buffer = &$this->debuffer; } if (!strlen($buffer['ciphertext'])) { $ciphertext = ''; } else { $ciphertext = $text ^ Strings::shift($buffer['ciphertext'], strlen($text)); $text = substr($text, strlen($ciphertext)); if (!strlen($text)) { return $ciphertext; } } $overflow = strlen($text) % 64; // & 0x3F if ($overflow) { $text2 = Strings::pop($text, $overflow); if ($this->engine == self::ENGINE_OPENSSL) { $iv = pack('V', $buffer['counter']) . $this->p2; // at this point $text should be a multiple of 64 $buffer['counter'] += (strlen($text) >> 6) + 1; // ie. divide by 64 $encrypted = openssl_encrypt( $text . str_repeat("\0", 64), $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA, $iv ); $temp = Strings::pop($encrypted, 64); } else { $blocks = str_split($text, 64); if (strlen($text)) { foreach ($blocks as &$block) { $block ^= static::salsa20($this->p1 . pack('V', $buffer['counter']++) . $this->p2); } unset($block); } $encrypted = implode('', $blocks); $temp = static::salsa20($this->p1 . pack('V', $buffer['counter']++) . $this->p2); } $ciphertext .= $encrypted . ($text2 ^ $temp); $buffer['ciphertext'] = substr($temp, $overflow); } elseif (!strlen($buffer['ciphertext'])) { if ($this->engine == self::ENGINE_OPENSSL) { $iv = pack('V', $buffer['counter']) . $this->p2; $buffer['counter'] += (strlen($text) >> 6); $ciphertext .= openssl_encrypt( $text, $this->cipher_name_openssl, $this->key, OPENSSL_RAW_DATA, $iv ); } else { $blocks = str_split($text, 64); foreach ($blocks as &$block) { $block ^= static::salsa20($this->p1 . pack('V', $buffer['counter']++) . $this->p2); } unset($block); $ciphertext .= implode('', $blocks); } } return $ciphertext; } /** * Left Rotate * * @param int $x * @param int $n * @return int */ protected static function leftRotate($x, $n) { if (PHP_INT_SIZE == 8) { $r1 = $x << $n; $r1 &= 0xFFFFFFFF; $r2 = ($x & 0xFFFFFFFF) >> (32 - $n); } else { $x = (int) $x; $r1 = $x << $n; $r2 = $x >> (32 - $n); $r2 &= (1 << $n) - 1; } return $r1 | $r2; } /** * The quarterround function * * @param int $a * @param int $b * @param int $c * @param int $d */ protected static function quarterRound(&$a, &$b, &$c, &$d) { $b ^= self::leftRotate($a + $d, 7); $c ^= self::leftRotate($b + $a, 9); $d ^= self::leftRotate($c + $b, 13); $a ^= self::leftRotate($d + $c, 18); } /** * The doubleround function * * @param int $x0 (by reference) * @param int $x1 (by reference) * @param int $x2 (by reference) * @param int $x3 (by reference) * @param int $x4 (by reference) * @param int $x5 (by reference) * @param int $x6 (by reference) * @param int $x7 (by reference) * @param int $x8 (by reference) * @param int $x9 (by reference) * @param int $x10 (by reference) * @param int $x11 (by reference) * @param int $x12 (by reference) * @param int $x13 (by reference) * @param int $x14 (by reference) * @param int $x15 (by reference) */ protected static function doubleRound(&$x0, &$x1, &$x2, &$x3, &$x4, &$x5, &$x6, &$x7, &$x8, &$x9, &$x10, &$x11, &$x12, &$x13, &$x14, &$x15) { // columnRound static::quarterRound($x0, $x4, $x8, $x12); static::quarterRound($x5, $x9, $x13, $x1); static::quarterRound($x10, $x14, $x2, $x6); static::quarterRound($x15, $x3, $x7, $x11); // rowRound static::quarterRound($x0, $x1, $x2, $x3); static::quarterRound($x5, $x6, $x7, $x4); static::quarterRound($x10, $x11, $x8, $x9); static::quarterRound($x15, $x12, $x13, $x14); } /** * The Salsa20 hash function function * * @param string $x */ protected static function salsa20($x) { $z = $x = unpack('V*', $x); for ($i = 0; $i < 10; $i++) { static::doubleRound($z[1], $z[2], $z[3], $z[4], $z[5], $z[6], $z[7], $z[8], $z[9], $z[10], $z[11], $z[12], $z[13], $z[14], $z[15], $z[16]); } for ($i = 1; $i <= 16; $i++) { $x[$i] += $z[$i]; } return pack('V*', ...$x); } /** * Calculates Poly1305 MAC * * @see self::decrypt() * @see self::encrypt() * @param string $ciphertext * @return string */ protected function poly1305($ciphertext) { if (!$this->usingGeneratedPoly1305Key) { return parent::poly1305($this->aad . $ciphertext); } else { /* sodium_crypto_aead_chacha20poly1305_encrypt does not calculate the poly1305 tag the same way sodium_crypto_aead_chacha20poly1305_ietf_encrypt does. you can see how the latter encrypts it in Salsa20::encrypt(). here's how the former encrypts it: $this->newtag = $this->poly1305( $this->aad . pack('V', strlen($this->aad)) . "\0\0\0\0" . $ciphertext . pack('V', strlen($ciphertext)) . "\0\0\0\0" ); phpseclib opts to use the IETF construction, even when the nonce is 64-bits instead of 96-bits */ return parent::poly1305( self::nullPad128($this->aad) . self::nullPad128($ciphertext) . pack('V', strlen($this->aad)) . "\0\0\0\0" . pack('V', strlen($ciphertext)) . "\0\0\0\0" ); } } } PK!)448vendor/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.phpnu[ * setKey('abcdefghijklmnopqrstuvwx'); * * $size = 10 * 1024; * $plaintext = ''; * for ($i = 0; $i < $size; $i++) { * $plaintext.= 'a'; * } * * echo $des->decrypt($des->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; /** * Pure-PHP implementation of Triple DES. * * @author Jim Wigginton */ class TripleDES extends DES { /** * Encrypt / decrypt using inner chaining * * Inner chaining is used by SSH-1 and is generally considered to be less secure then outer chaining (self::MODE_CBC3). */ const MODE_3CBC = -2; /** * Encrypt / decrypt using outer chaining * * Outer chaining is used by SSH-2 and when the mode is set to \phpseclib3\Crypt\Common\BlockCipher::MODE_CBC. */ const MODE_CBC3 = self::MODE_CBC; /** * Key Length (in bytes) * * @see \phpseclib3\Crypt\TripleDES::setKeyLength() * @var int */ protected $key_length = 24; /** * The mcrypt specific name of the cipher * * @see \phpseclib3\Crypt\DES::cipher_name_mcrypt * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @var string */ protected $cipher_name_mcrypt = 'tripledes'; /** * Optimizing value while CFB-encrypting * * @see \phpseclib3\Crypt\Common\SymmetricKey::cfb_init_len * @var int */ protected $cfb_init_len = 750; /** * max possible size of $key * * @see self::setKey() * @see \phpseclib3\Crypt\DES::setKey() * @var string */ protected $key_length_max = 24; /** * Internal flag whether using self::MODE_3CBC or not * * @var bool */ private $mode_3cbc; /** * The \phpseclib3\Crypt\DES objects * * Used only if $mode_3cbc === true * * @var array */ private $des; /** * Default Constructor. * * Determines whether or not the mcrypt or OpenSSL extensions should be used. * * $mode could be: * * - ecb * * - cbc * * - ctr * * - cfb * * - ofb * * - 3cbc * * - cbc3 (same as cbc) * * @see \phpseclib3\Crypt\DES::__construct() * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @param string $mode */ public function __construct($mode) { switch (strtolower($mode)) { // In case of self::MODE_3CBC, we init as CRYPT_DES_MODE_CBC // and additional flag us internally as 3CBC case '3cbc': parent::__construct('cbc'); $this->mode_3cbc = true; // This three $des'es will do the 3CBC work (if $key > 64bits) $this->des = [ new DES('cbc'), new DES('cbc'), new DES('cbc'), ]; // we're going to be doing the padding, ourselves, so disable it in the \phpseclib3\Crypt\DES objects $this->des[0]->disablePadding(); $this->des[1]->disablePadding(); $this->des[2]->disablePadding(); break; case 'cbc3': $mode = 'cbc'; // fall-through // If not 3CBC, we init as usual default: parent::__construct($mode); if ($this->mode == self::MODE_STREAM) { throw new BadModeException('Block ciphers cannot be ran in stream mode'); } } } /** * Test for engine validity * * This is mainly just a wrapper to set things up for \phpseclib3\Crypt\Common\SymmetricKey::isValidEngine() * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @param int $engine * @return bool */ protected function isValidEngineHelper($engine) { if ($engine == self::ENGINE_OPENSSL) { $this->cipher_name_openssl_ecb = 'des-ede3'; $mode = $this->openssl_translate_mode(); $this->cipher_name_openssl = $mode == 'ecb' ? 'des-ede3' : 'des-ede3-' . $mode; } return parent::isValidEngineHelper($engine); } /** * Sets the initialization vector. * * SetIV is not required when \phpseclib3\Crypt\Common\SymmetricKey::MODE_ECB is being used. * * @see \phpseclib3\Crypt\Common\SymmetricKey::setIV() * @param string $iv */ public function setIV($iv) { parent::setIV($iv); if ($this->mode_3cbc) { $this->des[0]->setIV($iv); $this->des[1]->setIV($iv); $this->des[2]->setIV($iv); } } /** * Sets the key length. * * Valid key lengths are 128 and 192 bits. * * If you want to use a 64-bit key use DES.php * * @see \phpseclib3\Crypt\Common\SymmetricKey:setKeyLength() * @throws \LengthException if the key length is invalid * @param int $length */ public function setKeyLength($length) { switch ($length) { case 128: case 192: break; default: throw new \LengthException('Key size of ' . $length . ' bits is not supported by this algorithm. Only keys of sizes 128 or 192 bits are supported'); } parent::setKeyLength($length); } /** * Sets the key. * * Triple DES can use 128-bit (eg. strlen($key) == 16) or 192-bit (eg. strlen($key) == 24) keys. * * DES also requires that every eighth bit be a parity bit, however, we'll ignore that. * * @see \phpseclib3\Crypt\DES::setKey() * @see \phpseclib3\Crypt\Common\SymmetricKey::setKey() * @throws \LengthException if the key length is invalid * @param string $key */ public function setKey($key) { if ($this->explicit_key_length !== false && strlen($key) != $this->explicit_key_length) { throw new \LengthException('Key length has already been set to ' . $this->explicit_key_length . ' bytes and this key is ' . strlen($key) . ' bytes'); } switch (strlen($key)) { case 16: $key .= substr($key, 0, 8); break; case 24: break; default: throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes 16 or 24 are supported'); } // copied from self::setKey() $this->key = $key; $this->key_length = strlen($key); $this->changed = $this->nonIVChanged = true; $this->setEngine(); if ($this->mode_3cbc) { $this->des[0]->setKey(substr($key, 0, 8)); $this->des[1]->setKey(substr($key, 8, 8)); $this->des[2]->setKey(substr($key, 16, 8)); } } /** * Encrypts a message. * * @see \phpseclib3\Crypt\Common\SymmetricKey::encrypt() * @param string $plaintext * @return string $cipertext */ public function encrypt($plaintext) { // parent::en/decrypt() is able to do all the work for all modes and keylengths, // except for: self::MODE_3CBC (inner chaining CBC) with a key > 64bits // if the key is smaller then 8, do what we'd normally do if ($this->mode_3cbc && strlen($this->key) > 8) { return $this->des[2]->encrypt( $this->des[1]->decrypt( $this->des[0]->encrypt( $this->pad($plaintext) ) ) ); } return parent::encrypt($plaintext); } /** * Decrypts a message. * * @see \phpseclib3\Crypt\Common\SymmetricKey::decrypt() * @param string $ciphertext * @return string $plaintext */ public function decrypt($ciphertext) { if ($this->mode_3cbc && strlen($this->key) > 8) { return $this->unpad( $this->des[0]->decrypt( $this->des[1]->encrypt( $this->des[2]->decrypt( str_pad($ciphertext, (strlen($ciphertext) + 7) & 0xFFFFFFF8, "\0") ) ) ) ); } return parent::decrypt($ciphertext); } /** * Treat consecutive "packets" as if they are a continuous buffer. * * Say you have a 16-byte plaintext $plaintext. Using the default behavior, the two following code snippets * will yield different outputs: * * * echo $des->encrypt(substr($plaintext, 0, 8)); * echo $des->encrypt(substr($plaintext, 8, 8)); * * * echo $des->encrypt($plaintext); * * * The solution is to enable the continuous buffer. Although this will resolve the above discrepancy, it creates * another, as demonstrated with the following: * * * $des->encrypt(substr($plaintext, 0, 8)); * echo $des->decrypt($des->encrypt(substr($plaintext, 8, 8))); * * * echo $des->decrypt($des->encrypt(substr($plaintext, 8, 8))); * * * With the continuous buffer disabled, these would yield the same output. With it enabled, they yield different * outputs. The reason is due to the fact that the initialization vector's change after every encryption / * decryption round when the continuous buffer is enabled. When it's disabled, they remain constant. * * Put another way, when the continuous buffer is enabled, the state of the \phpseclib3\Crypt\DES() object changes after each * encryption / decryption round, whereas otherwise, it'd remain constant. For this reason, it's recommended that * continuous buffers not be used. They do offer better security and are, in fact, sometimes required (SSH uses them), * however, they are also less intuitive and more likely to cause you problems. * * @see \phpseclib3\Crypt\Common\SymmetricKey::enableContinuousBuffer() * @see self::disableContinuousBuffer() */ public function enableContinuousBuffer() { parent::enableContinuousBuffer(); if ($this->mode_3cbc) { $this->des[0]->enableContinuousBuffer(); $this->des[1]->enableContinuousBuffer(); $this->des[2]->enableContinuousBuffer(); } } /** * Treat consecutive packets as if they are a discontinuous buffer. * * The default behavior. * * @see \phpseclib3\Crypt\Common\SymmetricKey::disableContinuousBuffer() * @see self::enableContinuousBuffer() */ public function disableContinuousBuffer() { parent::disableContinuousBuffer(); if ($this->mode_3cbc) { $this->des[0]->disableContinuousBuffer(); $this->des[1]->disableContinuousBuffer(); $this->des[2]->disableContinuousBuffer(); } } /** * Creates the key schedule * * @see \phpseclib3\Crypt\DES::setupKey() * @see \phpseclib3\Crypt\Common\SymmetricKey::setupKey() */ protected function setupKey() { switch (true) { // if $key <= 64bits we configure our internal pure-php cipher engine // to act as regular [1]DES, not as 3DES. mcrypt.so::tripledes does the same. case strlen($this->key) <= 8: $this->des_rounds = 1; break; // otherwise, if $key > 64bits, we configure our engine to work as 3DES. default: $this->des_rounds = 3; // (only) if 3CBC is used we have, of course, to setup the $des[0-2] keys also separately. if ($this->mode_3cbc) { $this->des[0]->setupKey(); $this->des[1]->setupKey(); $this->des[2]->setupKey(); // because $des[0-2] will, now, do all the work we can return here // not need unnecessary stress parent::setupKey() with our, now unused, $key. return; } } // setup our key parent::setupKey(); } /** * Sets the internal crypt engine * * @see \phpseclib3\Crypt\Common\SymmetricKey::__construct() * @see \phpseclib3\Crypt\Common\SymmetricKey::setPreferredEngine() * @param int $engine */ public function setPreferredEngine($engine) { if ($this->mode_3cbc) { $this->des[0]->setPreferredEngine($engine); $this->des[1]->setPreferredEngine($engine); $this->des[2]->setPreferredEngine($engine); } parent::setPreferredEngine($engine); } } PK!TĴhh6vendor/phpseclib/phpseclib/phpseclib/Crypt/Twofish.phpnu[ * setKey('12345678901234567890123456789012'); * * $plaintext = str_repeat('a', 1024); * * echo $twofish->decrypt($twofish->encrypt($plaintext)); * ?> * * * @author Jim Wigginton * @author Hans-Juergen Petrich * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Crypt; use phpseclib3\Crypt\Common\BlockCipher; use phpseclib3\Exception\BadModeException; /** * Pure-PHP implementation of Twofish. * * @author Jim Wigginton * @author Hans-Juergen Petrich */ class Twofish extends BlockCipher { /** * The mcrypt specific name of the cipher * * @see \phpseclib3\Crypt\Common\SymmetricKey::cipher_name_mcrypt * @var string */ protected $cipher_name_mcrypt = 'twofish'; /** * Optimizing value while CFB-encrypting * * @see \phpseclib3\Crypt\Common\SymmetricKey::cfb_init_len * @var int */ protected $cfb_init_len = 800; /** * Q-Table * * @var array */ private static $q0 = [ 0xA9, 0x67, 0xB3, 0xE8, 0x04, 0xFD, 0xA3, 0x76, 0x9A, 0x92, 0x80, 0x78, 0xE4, 0xDD, 0xD1, 0x38, 0x0D, 0xC6, 0x35, 0x98, 0x18, 0xF7, 0xEC, 0x6C, 0x43, 0x75, 0x37, 0x26, 0xFA, 0x13, 0x94, 0x48, 0xF2, 0xD0, 0x8B, 0x30, 0x84, 0x54, 0xDF, 0x23, 0x19, 0x5B, 0x3D, 0x59, 0xF3, 0xAE, 0xA2, 0x82, 0x63, 0x01, 0x83, 0x2E, 0xD9, 0x51, 0x9B, 0x7C, 0xA6, 0xEB, 0xA5, 0xBE, 0x16, 0x0C, 0xE3, 0x61, 0xC0, 0x8C, 0x3A, 0xF5, 0x73, 0x2C, 0x25, 0x0B, 0xBB, 0x4E, 0x89, 0x6B, 0x53, 0x6A, 0xB4, 0xF1, 0xE1, 0xE6, 0xBD, 0x45, 0xE2, 0xF4, 0xB6, 0x66, 0xCC, 0x95, 0x03, 0x56, 0xD4, 0x1C, 0x1E, 0xD7, 0xFB, 0xC3, 0x8E, 0xB5, 0xE9, 0xCF, 0xBF, 0xBA, 0xEA, 0x77, 0x39, 0xAF, 0x33, 0xC9, 0x62, 0x71, 0x81, 0x79, 0x09, 0xAD, 0x24, 0xCD, 0xF9, 0xD8, 0xE5, 0xC5, 0xB9, 0x4D, 0x44, 0x08, 0x86, 0xE7, 0xA1, 0x1D, 0xAA, 0xED, 0x06, 0x70, 0xB2, 0xD2, 0x41, 0x7B, 0xA0, 0x11, 0x31, 0xC2, 0x27, 0x90, 0x20, 0xF6, 0x60, 0xFF, 0x96, 0x5C, 0xB1, 0xAB, 0x9E, 0x9C, 0x52, 0x1B, 0x5F, 0x93, 0x0A, 0xEF, 0x91, 0x85, 0x49, 0xEE, 0x2D, 0x4F, 0x8F, 0x3B, 0x47, 0x87, 0x6D, 0x46, 0xD6, 0x3E, 0x69, 0x64, 0x2A, 0xCE, 0xCB, 0x2F, 0xFC, 0x97, 0x05, 0x7A, 0xAC, 0x7F, 0xD5, 0x1A, 0x4B, 0x0E, 0xA7, 0x5A, 0x28, 0x14, 0x3F, 0x29, 0x88, 0x3C, 0x4C, 0x02, 0xB8, 0xDA, 0xB0, 0x17, 0x55, 0x1F, 0x8A, 0x7D, 0x57, 0xC7, 0x8D, 0x74, 0xB7, 0xC4, 0x9F, 0x72, 0x7E, 0x15, 0x22, 0x12, 0x58, 0x07, 0x99, 0x34, 0x6E, 0x50, 0xDE, 0x68, 0x65, 0xBC, 0xDB, 0xF8, 0xC8, 0xA8, 0x2B, 0x40, 0xDC, 0xFE, 0x32, 0xA4, 0xCA, 0x10, 0x21, 0xF0, 0xD3, 0x5D, 0x0F, 0x00, 0x6F, 0x9D, 0x36, 0x42, 0x4A, 0x5E, 0xC1, 0xE0 ]; /** * Q-Table * * @var array */ private static $q1 = [ 0x75, 0xF3, 0xC6, 0xF4, 0xDB, 0x7B, 0xFB, 0xC8, 0x4A, 0xD3, 0xE6, 0x6B, 0x45, 0x7D, 0xE8, 0x4B, 0xD6, 0x32, 0xD8, 0xFD, 0x37, 0x71, 0xF1, 0xE1, 0x30, 0x0F, 0xF8, 0x1B, 0x87, 0xFA, 0x06, 0x3F, 0x5E, 0xBA, 0xAE, 0x5B, 0x8A, 0x00, 0xBC, 0x9D, 0x6D, 0xC1, 0xB1, 0x0E, 0x80, 0x5D, 0xD2, 0xD5, 0xA0, 0x84, 0x07, 0x14, 0xB5, 0x90, 0x2C, 0xA3, 0xB2, 0x73, 0x4C, 0x54, 0x92, 0x74, 0x36, 0x51, 0x38, 0xB0, 0xBD, 0x5A, 0xFC, 0x60, 0x62, 0x96, 0x6C, 0x42, 0xF7, 0x10, 0x7C, 0x28, 0x27, 0x8C, 0x13, 0x95, 0x9C, 0xC7, 0x24, 0x46, 0x3B, 0x70, 0xCA, 0xE3, 0x85, 0xCB, 0x11, 0xD0, 0x93, 0xB8, 0xA6, 0x83, 0x20, 0xFF, 0x9F, 0x77, 0xC3, 0xCC, 0x03, 0x6F, 0x08, 0xBF, 0x40, 0xE7, 0x2B, 0xE2, 0x79, 0x0C, 0xAA, 0x82, 0x41, 0x3A, 0xEA, 0xB9, 0xE4, 0x9A, 0xA4, 0x97, 0x7E, 0xDA, 0x7A, 0x17, 0x66, 0x94, 0xA1, 0x1D, 0x3D, 0xF0, 0xDE, 0xB3, 0x0B, 0x72, 0xA7, 0x1C, 0xEF, 0xD1, 0x53, 0x3E, 0x8F, 0x33, 0x26, 0x5F, 0xEC, 0x76, 0x2A, 0x49, 0x81, 0x88, 0xEE, 0x21, 0xC4, 0x1A, 0xEB, 0xD9, 0xC5, 0x39, 0x99, 0xCD, 0xAD, 0x31, 0x8B, 0x01, 0x18, 0x23, 0xDD, 0x1F, 0x4E, 0x2D, 0xF9, 0x48, 0x4F, 0xF2, 0x65, 0x8E, 0x78, 0x5C, 0x58, 0x19, 0x8D, 0xE5, 0x98, 0x57, 0x67, 0x7F, 0x05, 0x64, 0xAF, 0x63, 0xB6, 0xFE, 0xF5, 0xB7, 0x3C, 0xA5, 0xCE, 0xE9, 0x68, 0x44, 0xE0, 0x4D, 0x43, 0x69, 0x29, 0x2E, 0xAC, 0x15, 0x59, 0xA8, 0x0A, 0x9E, 0x6E, 0x47, 0xDF, 0x34, 0x35, 0x6A, 0xCF, 0xDC, 0x22, 0xC9, 0xC0, 0x9B, 0x89, 0xD4, 0xED, 0xAB, 0x12, 0xA2, 0x0D, 0x52, 0xBB, 0x02, 0x2F, 0xA9, 0xD7, 0x61, 0x1E, 0xB4, 0x50, 0x04, 0xF6, 0xC2, 0x16, 0x25, 0x86, 0x56, 0x55, 0x09, 0xBE, 0x91 ]; /** * M-Table * * @var array */ private static $m0 = [ 0xBCBC3275, 0xECEC21F3, 0x202043C6, 0xB3B3C9F4, 0xDADA03DB, 0x02028B7B, 0xE2E22BFB, 0x9E9EFAC8, 0xC9C9EC4A, 0xD4D409D3, 0x18186BE6, 0x1E1E9F6B, 0x98980E45, 0xB2B2387D, 0xA6A6D2E8, 0x2626B74B, 0x3C3C57D6, 0x93938A32, 0x8282EED8, 0x525298FD, 0x7B7BD437, 0xBBBB3771, 0x5B5B97F1, 0x474783E1, 0x24243C30, 0x5151E20F, 0xBABAC6F8, 0x4A4AF31B, 0xBFBF4887, 0x0D0D70FA, 0xB0B0B306, 0x7575DE3F, 0xD2D2FD5E, 0x7D7D20BA, 0x666631AE, 0x3A3AA35B, 0x59591C8A, 0x00000000, 0xCDCD93BC, 0x1A1AE09D, 0xAEAE2C6D, 0x7F7FABC1, 0x2B2BC7B1, 0xBEBEB90E, 0xE0E0A080, 0x8A8A105D, 0x3B3B52D2, 0x6464BAD5, 0xD8D888A0, 0xE7E7A584, 0x5F5FE807, 0x1B1B1114, 0x2C2CC2B5, 0xFCFCB490, 0x3131272C, 0x808065A3, 0x73732AB2, 0x0C0C8173, 0x79795F4C, 0x6B6B4154, 0x4B4B0292, 0x53536974, 0x94948F36, 0x83831F51, 0x2A2A3638, 0xC4C49CB0, 0x2222C8BD, 0xD5D5F85A, 0xBDBDC3FC, 0x48487860, 0xFFFFCE62, 0x4C4C0796, 0x4141776C, 0xC7C7E642, 0xEBEB24F7, 0x1C1C1410, 0x5D5D637C, 0x36362228, 0x6767C027, 0xE9E9AF8C, 0x4444F913, 0x1414EA95, 0xF5F5BB9C, 0xCFCF18C7, 0x3F3F2D24, 0xC0C0E346, 0x7272DB3B, 0x54546C70, 0x29294CCA, 0xF0F035E3, 0x0808FE85, 0xC6C617CB, 0xF3F34F11, 0x8C8CE4D0, 0xA4A45993, 0xCACA96B8, 0x68683BA6, 0xB8B84D83, 0x38382820, 0xE5E52EFF, 0xADAD569F, 0x0B0B8477, 0xC8C81DC3, 0x9999FFCC, 0x5858ED03, 0x19199A6F, 0x0E0E0A08, 0x95957EBF, 0x70705040, 0xF7F730E7, 0x6E6ECF2B, 0x1F1F6EE2, 0xB5B53D79, 0x09090F0C, 0x616134AA, 0x57571682, 0x9F9F0B41, 0x9D9D803A, 0x111164EA, 0x2525CDB9, 0xAFAFDDE4, 0x4545089A, 0xDFDF8DA4, 0xA3A35C97, 0xEAEAD57E, 0x353558DA, 0xEDEDD07A, 0x4343FC17, 0xF8F8CB66, 0xFBFBB194, 0x3737D3A1, 0xFAFA401D, 0xC2C2683D, 0xB4B4CCF0, 0x32325DDE, 0x9C9C71B3, 0x5656E70B, 0xE3E3DA72, 0x878760A7, 0x15151B1C, 0xF9F93AEF, 0x6363BFD1, 0x3434A953, 0x9A9A853E, 0xB1B1428F, 0x7C7CD133, 0x88889B26, 0x3D3DA65F, 0xA1A1D7EC, 0xE4E4DF76, 0x8181942A, 0x91910149, 0x0F0FFB81, 0xEEEEAA88, 0x161661EE, 0xD7D77321, 0x9797F5C4, 0xA5A5A81A, 0xFEFE3FEB, 0x6D6DB5D9, 0x7878AEC5, 0xC5C56D39, 0x1D1DE599, 0x7676A4CD, 0x3E3EDCAD, 0xCBCB6731, 0xB6B6478B, 0xEFEF5B01, 0x12121E18, 0x6060C523, 0x6A6AB0DD, 0x4D4DF61F, 0xCECEE94E, 0xDEDE7C2D, 0x55559DF9, 0x7E7E5A48, 0x2121B24F, 0x03037AF2, 0xA0A02665, 0x5E5E198E, 0x5A5A6678, 0x65654B5C, 0x62624E58, 0xFDFD4519, 0x0606F48D, 0x404086E5, 0xF2F2BE98, 0x3333AC57, 0x17179067, 0x05058E7F, 0xE8E85E05, 0x4F4F7D64, 0x89896AAF, 0x10109563, 0x74742FB6, 0x0A0A75FE, 0x5C5C92F5, 0x9B9B74B7, 0x2D2D333C, 0x3030D6A5, 0x2E2E49CE, 0x494989E9, 0x46467268, 0x77775544, 0xA8A8D8E0, 0x9696044D, 0x2828BD43, 0xA9A92969, 0xD9D97929, 0x8686912E, 0xD1D187AC, 0xF4F44A15, 0x8D8D1559, 0xD6D682A8, 0xB9B9BC0A, 0x42420D9E, 0xF6F6C16E, 0x2F2FB847, 0xDDDD06DF, 0x23233934, 0xCCCC6235, 0xF1F1C46A, 0xC1C112CF, 0x8585EBDC, 0x8F8F9E22, 0x7171A1C9, 0x9090F0C0, 0xAAAA539B, 0x0101F189, 0x8B8BE1D4, 0x4E4E8CED, 0x8E8E6FAB, 0xABABA212, 0x6F6F3EA2, 0xE6E6540D, 0xDBDBF252, 0x92927BBB, 0xB7B7B602, 0x6969CA2F, 0x3939D9A9, 0xD3D30CD7, 0xA7A72361, 0xA2A2AD1E, 0xC3C399B4, 0x6C6C4450, 0x07070504, 0x04047FF6, 0x272746C2, 0xACACA716, 0xD0D07625, 0x50501386, 0xDCDCF756, 0x84841A55, 0xE1E15109, 0x7A7A25BE, 0x1313EF91 ]; /** * M-Table * * @var array */ private static $m1 = [ 0xA9D93939, 0x67901717, 0xB3719C9C, 0xE8D2A6A6, 0x04050707, 0xFD985252, 0xA3658080, 0x76DFE4E4, 0x9A084545, 0x92024B4B, 0x80A0E0E0, 0x78665A5A, 0xE4DDAFAF, 0xDDB06A6A, 0xD1BF6363, 0x38362A2A, 0x0D54E6E6, 0xC6432020, 0x3562CCCC, 0x98BEF2F2, 0x181E1212, 0xF724EBEB, 0xECD7A1A1, 0x6C774141, 0x43BD2828, 0x7532BCBC, 0x37D47B7B, 0x269B8888, 0xFA700D0D, 0x13F94444, 0x94B1FBFB, 0x485A7E7E, 0xF27A0303, 0xD0E48C8C, 0x8B47B6B6, 0x303C2424, 0x84A5E7E7, 0x54416B6B, 0xDF06DDDD, 0x23C56060, 0x1945FDFD, 0x5BA33A3A, 0x3D68C2C2, 0x59158D8D, 0xF321ECEC, 0xAE316666, 0xA23E6F6F, 0x82165757, 0x63951010, 0x015BEFEF, 0x834DB8B8, 0x2E918686, 0xD9B56D6D, 0x511F8383, 0x9B53AAAA, 0x7C635D5D, 0xA63B6868, 0xEB3FFEFE, 0xA5D63030, 0xBE257A7A, 0x16A7ACAC, 0x0C0F0909, 0xE335F0F0, 0x6123A7A7, 0xC0F09090, 0x8CAFE9E9, 0x3A809D9D, 0xF5925C5C, 0x73810C0C, 0x2C273131, 0x2576D0D0, 0x0BE75656, 0xBB7B9292, 0x4EE9CECE, 0x89F10101, 0x6B9F1E1E, 0x53A93434, 0x6AC4F1F1, 0xB499C3C3, 0xF1975B5B, 0xE1834747, 0xE66B1818, 0xBDC82222, 0x450E9898, 0xE26E1F1F, 0xF4C9B3B3, 0xB62F7474, 0x66CBF8F8, 0xCCFF9999, 0x95EA1414, 0x03ED5858, 0x56F7DCDC, 0xD4E18B8B, 0x1C1B1515, 0x1EADA2A2, 0xD70CD3D3, 0xFB2BE2E2, 0xC31DC8C8, 0x8E195E5E, 0xB5C22C2C, 0xE9894949, 0xCF12C1C1, 0xBF7E9595, 0xBA207D7D, 0xEA641111, 0x77840B0B, 0x396DC5C5, 0xAF6A8989, 0x33D17C7C, 0xC9A17171, 0x62CEFFFF, 0x7137BBBB, 0x81FB0F0F, 0x793DB5B5, 0x0951E1E1, 0xADDC3E3E, 0x242D3F3F, 0xCDA47676, 0xF99D5555, 0xD8EE8282, 0xE5864040, 0xC5AE7878, 0xB9CD2525, 0x4D049696, 0x44557777, 0x080A0E0E, 0x86135050, 0xE730F7F7, 0xA1D33737, 0x1D40FAFA, 0xAA346161, 0xED8C4E4E, 0x06B3B0B0, 0x706C5454, 0xB22A7373, 0xD2523B3B, 0x410B9F9F, 0x7B8B0202, 0xA088D8D8, 0x114FF3F3, 0x3167CBCB, 0xC2462727, 0x27C06767, 0x90B4FCFC, 0x20283838, 0xF67F0404, 0x60784848, 0xFF2EE5E5, 0x96074C4C, 0x5C4B6565, 0xB1C72B2B, 0xAB6F8E8E, 0x9E0D4242, 0x9CBBF5F5, 0x52F2DBDB, 0x1BF34A4A, 0x5FA63D3D, 0x9359A4A4, 0x0ABCB9B9, 0xEF3AF9F9, 0x91EF1313, 0x85FE0808, 0x49019191, 0xEE611616, 0x2D7CDEDE, 0x4FB22121, 0x8F42B1B1, 0x3BDB7272, 0x47B82F2F, 0x8748BFBF, 0x6D2CAEAE, 0x46E3C0C0, 0xD6573C3C, 0x3E859A9A, 0x6929A9A9, 0x647D4F4F, 0x2A948181, 0xCE492E2E, 0xCB17C6C6, 0x2FCA6969, 0xFCC3BDBD, 0x975CA3A3, 0x055EE8E8, 0x7AD0EDED, 0xAC87D1D1, 0x7F8E0505, 0xD5BA6464, 0x1AA8A5A5, 0x4BB72626, 0x0EB9BEBE, 0xA7608787, 0x5AF8D5D5, 0x28223636, 0x14111B1B, 0x3FDE7575, 0x2979D9D9, 0x88AAEEEE, 0x3C332D2D, 0x4C5F7979, 0x02B6B7B7, 0xB896CACA, 0xDA583535, 0xB09CC4C4, 0x17FC4343, 0x551A8484, 0x1FF64D4D, 0x8A1C5959, 0x7D38B2B2, 0x57AC3333, 0xC718CFCF, 0x8DF40606, 0x74695353, 0xB7749B9B, 0xC4F59797, 0x9F56ADAD, 0x72DAE3E3, 0x7ED5EAEA, 0x154AF4F4, 0x229E8F8F, 0x12A2ABAB, 0x584E6262, 0x07E85F5F, 0x99E51D1D, 0x34392323, 0x6EC1F6F6, 0x50446C6C, 0xDE5D3232, 0x68724646, 0x6526A0A0, 0xBC93CDCD, 0xDB03DADA, 0xF8C6BABA, 0xC8FA9E9E, 0xA882D6D6, 0x2BCF6E6E, 0x40507070, 0xDCEB8585, 0xFE750A0A, 0x328A9393, 0xA48DDFDF, 0xCA4C2929, 0x10141C1C, 0x2173D7D7, 0xF0CCB4B4, 0xD309D4D4, 0x5D108A8A, 0x0FE25151, 0x00000000, 0x6F9A1919, 0x9DE01A1A, 0x368F9494, 0x42E6C7C7, 0x4AECC9C9, 0x5EFDD2D2, 0xC1AB7F7F, 0xE0D8A8A8 ]; /** * M-Table * * @var array */ private static $m2 = [ 0xBC75BC32, 0xECF3EC21, 0x20C62043, 0xB3F4B3C9, 0xDADBDA03, 0x027B028B, 0xE2FBE22B, 0x9EC89EFA, 0xC94AC9EC, 0xD4D3D409, 0x18E6186B, 0x1E6B1E9F, 0x9845980E, 0xB27DB238, 0xA6E8A6D2, 0x264B26B7, 0x3CD63C57, 0x9332938A, 0x82D882EE, 0x52FD5298, 0x7B377BD4, 0xBB71BB37, 0x5BF15B97, 0x47E14783, 0x2430243C, 0x510F51E2, 0xBAF8BAC6, 0x4A1B4AF3, 0xBF87BF48, 0x0DFA0D70, 0xB006B0B3, 0x753F75DE, 0xD25ED2FD, 0x7DBA7D20, 0x66AE6631, 0x3A5B3AA3, 0x598A591C, 0x00000000, 0xCDBCCD93, 0x1A9D1AE0, 0xAE6DAE2C, 0x7FC17FAB, 0x2BB12BC7, 0xBE0EBEB9, 0xE080E0A0, 0x8A5D8A10, 0x3BD23B52, 0x64D564BA, 0xD8A0D888, 0xE784E7A5, 0x5F075FE8, 0x1B141B11, 0x2CB52CC2, 0xFC90FCB4, 0x312C3127, 0x80A38065, 0x73B2732A, 0x0C730C81, 0x794C795F, 0x6B546B41, 0x4B924B02, 0x53745369, 0x9436948F, 0x8351831F, 0x2A382A36, 0xC4B0C49C, 0x22BD22C8, 0xD55AD5F8, 0xBDFCBDC3, 0x48604878, 0xFF62FFCE, 0x4C964C07, 0x416C4177, 0xC742C7E6, 0xEBF7EB24, 0x1C101C14, 0x5D7C5D63, 0x36283622, 0x672767C0, 0xE98CE9AF, 0x441344F9, 0x149514EA, 0xF59CF5BB, 0xCFC7CF18, 0x3F243F2D, 0xC046C0E3, 0x723B72DB, 0x5470546C, 0x29CA294C, 0xF0E3F035, 0x088508FE, 0xC6CBC617, 0xF311F34F, 0x8CD08CE4, 0xA493A459, 0xCAB8CA96, 0x68A6683B, 0xB883B84D, 0x38203828, 0xE5FFE52E, 0xAD9FAD56, 0x0B770B84, 0xC8C3C81D, 0x99CC99FF, 0x580358ED, 0x196F199A, 0x0E080E0A, 0x95BF957E, 0x70407050, 0xF7E7F730, 0x6E2B6ECF, 0x1FE21F6E, 0xB579B53D, 0x090C090F, 0x61AA6134, 0x57825716, 0x9F419F0B, 0x9D3A9D80, 0x11EA1164, 0x25B925CD, 0xAFE4AFDD, 0x459A4508, 0xDFA4DF8D, 0xA397A35C, 0xEA7EEAD5, 0x35DA3558, 0xED7AEDD0, 0x431743FC, 0xF866F8CB, 0xFB94FBB1, 0x37A137D3, 0xFA1DFA40, 0xC23DC268, 0xB4F0B4CC, 0x32DE325D, 0x9CB39C71, 0x560B56E7, 0xE372E3DA, 0x87A78760, 0x151C151B, 0xF9EFF93A, 0x63D163BF, 0x345334A9, 0x9A3E9A85, 0xB18FB142, 0x7C337CD1, 0x8826889B, 0x3D5F3DA6, 0xA1ECA1D7, 0xE476E4DF, 0x812A8194, 0x91499101, 0x0F810FFB, 0xEE88EEAA, 0x16EE1661, 0xD721D773, 0x97C497F5, 0xA51AA5A8, 0xFEEBFE3F, 0x6DD96DB5, 0x78C578AE, 0xC539C56D, 0x1D991DE5, 0x76CD76A4, 0x3EAD3EDC, 0xCB31CB67, 0xB68BB647, 0xEF01EF5B, 0x1218121E, 0x602360C5, 0x6ADD6AB0, 0x4D1F4DF6, 0xCE4ECEE9, 0xDE2DDE7C, 0x55F9559D, 0x7E487E5A, 0x214F21B2, 0x03F2037A, 0xA065A026, 0x5E8E5E19, 0x5A785A66, 0x655C654B, 0x6258624E, 0xFD19FD45, 0x068D06F4, 0x40E54086, 0xF298F2BE, 0x335733AC, 0x17671790, 0x057F058E, 0xE805E85E, 0x4F644F7D, 0x89AF896A, 0x10631095, 0x74B6742F, 0x0AFE0A75, 0x5CF55C92, 0x9BB79B74, 0x2D3C2D33, 0x30A530D6, 0x2ECE2E49, 0x49E94989, 0x46684672, 0x77447755, 0xA8E0A8D8, 0x964D9604, 0x284328BD, 0xA969A929, 0xD929D979, 0x862E8691, 0xD1ACD187, 0xF415F44A, 0x8D598D15, 0xD6A8D682, 0xB90AB9BC, 0x429E420D, 0xF66EF6C1, 0x2F472FB8, 0xDDDFDD06, 0x23342339, 0xCC35CC62, 0xF16AF1C4, 0xC1CFC112, 0x85DC85EB, 0x8F228F9E, 0x71C971A1, 0x90C090F0, 0xAA9BAA53, 0x018901F1, 0x8BD48BE1, 0x4EED4E8C, 0x8EAB8E6F, 0xAB12ABA2, 0x6FA26F3E, 0xE60DE654, 0xDB52DBF2, 0x92BB927B, 0xB702B7B6, 0x692F69CA, 0x39A939D9, 0xD3D7D30C, 0xA761A723, 0xA21EA2AD, 0xC3B4C399, 0x6C506C44, 0x07040705, 0x04F6047F, 0x27C22746, 0xAC16ACA7, 0xD025D076, 0x50865013, 0xDC56DCF7, 0x8455841A, 0xE109E151, 0x7ABE7A25, 0x139113EF ]; /** * M-Table * * @var array */ private static $m3 = [ 0xD939A9D9, 0x90176790, 0x719CB371, 0xD2A6E8D2, 0x05070405, 0x9852FD98, 0x6580A365, 0xDFE476DF, 0x08459A08, 0x024B9202, 0xA0E080A0, 0x665A7866, 0xDDAFE4DD, 0xB06ADDB0, 0xBF63D1BF, 0x362A3836, 0x54E60D54, 0x4320C643, 0x62CC3562, 0xBEF298BE, 0x1E12181E, 0x24EBF724, 0xD7A1ECD7, 0x77416C77, 0xBD2843BD, 0x32BC7532, 0xD47B37D4, 0x9B88269B, 0x700DFA70, 0xF94413F9, 0xB1FB94B1, 0x5A7E485A, 0x7A03F27A, 0xE48CD0E4, 0x47B68B47, 0x3C24303C, 0xA5E784A5, 0x416B5441, 0x06DDDF06, 0xC56023C5, 0x45FD1945, 0xA33A5BA3, 0x68C23D68, 0x158D5915, 0x21ECF321, 0x3166AE31, 0x3E6FA23E, 0x16578216, 0x95106395, 0x5BEF015B, 0x4DB8834D, 0x91862E91, 0xB56DD9B5, 0x1F83511F, 0x53AA9B53, 0x635D7C63, 0x3B68A63B, 0x3FFEEB3F, 0xD630A5D6, 0x257ABE25, 0xA7AC16A7, 0x0F090C0F, 0x35F0E335, 0x23A76123, 0xF090C0F0, 0xAFE98CAF, 0x809D3A80, 0x925CF592, 0x810C7381, 0x27312C27, 0x76D02576, 0xE7560BE7, 0x7B92BB7B, 0xE9CE4EE9, 0xF10189F1, 0x9F1E6B9F, 0xA93453A9, 0xC4F16AC4, 0x99C3B499, 0x975BF197, 0x8347E183, 0x6B18E66B, 0xC822BDC8, 0x0E98450E, 0x6E1FE26E, 0xC9B3F4C9, 0x2F74B62F, 0xCBF866CB, 0xFF99CCFF, 0xEA1495EA, 0xED5803ED, 0xF7DC56F7, 0xE18BD4E1, 0x1B151C1B, 0xADA21EAD, 0x0CD3D70C, 0x2BE2FB2B, 0x1DC8C31D, 0x195E8E19, 0xC22CB5C2, 0x8949E989, 0x12C1CF12, 0x7E95BF7E, 0x207DBA20, 0x6411EA64, 0x840B7784, 0x6DC5396D, 0x6A89AF6A, 0xD17C33D1, 0xA171C9A1, 0xCEFF62CE, 0x37BB7137, 0xFB0F81FB, 0x3DB5793D, 0x51E10951, 0xDC3EADDC, 0x2D3F242D, 0xA476CDA4, 0x9D55F99D, 0xEE82D8EE, 0x8640E586, 0xAE78C5AE, 0xCD25B9CD, 0x04964D04, 0x55774455, 0x0A0E080A, 0x13508613, 0x30F7E730, 0xD337A1D3, 0x40FA1D40, 0x3461AA34, 0x8C4EED8C, 0xB3B006B3, 0x6C54706C, 0x2A73B22A, 0x523BD252, 0x0B9F410B, 0x8B027B8B, 0x88D8A088, 0x4FF3114F, 0x67CB3167, 0x4627C246, 0xC06727C0, 0xB4FC90B4, 0x28382028, 0x7F04F67F, 0x78486078, 0x2EE5FF2E, 0x074C9607, 0x4B655C4B, 0xC72BB1C7, 0x6F8EAB6F, 0x0D429E0D, 0xBBF59CBB, 0xF2DB52F2, 0xF34A1BF3, 0xA63D5FA6, 0x59A49359, 0xBCB90ABC, 0x3AF9EF3A, 0xEF1391EF, 0xFE0885FE, 0x01914901, 0x6116EE61, 0x7CDE2D7C, 0xB2214FB2, 0x42B18F42, 0xDB723BDB, 0xB82F47B8, 0x48BF8748, 0x2CAE6D2C, 0xE3C046E3, 0x573CD657, 0x859A3E85, 0x29A96929, 0x7D4F647D, 0x94812A94, 0x492ECE49, 0x17C6CB17, 0xCA692FCA, 0xC3BDFCC3, 0x5CA3975C, 0x5EE8055E, 0xD0ED7AD0, 0x87D1AC87, 0x8E057F8E, 0xBA64D5BA, 0xA8A51AA8, 0xB7264BB7, 0xB9BE0EB9, 0x6087A760, 0xF8D55AF8, 0x22362822, 0x111B1411, 0xDE753FDE, 0x79D92979, 0xAAEE88AA, 0x332D3C33, 0x5F794C5F, 0xB6B702B6, 0x96CAB896, 0x5835DA58, 0x9CC4B09C, 0xFC4317FC, 0x1A84551A, 0xF64D1FF6, 0x1C598A1C, 0x38B27D38, 0xAC3357AC, 0x18CFC718, 0xF4068DF4, 0x69537469, 0x749BB774, 0xF597C4F5, 0x56AD9F56, 0xDAE372DA, 0xD5EA7ED5, 0x4AF4154A, 0x9E8F229E, 0xA2AB12A2, 0x4E62584E, 0xE85F07E8, 0xE51D99E5, 0x39233439, 0xC1F66EC1, 0x446C5044, 0x5D32DE5D, 0x72466872, 0x26A06526, 0x93CDBC93, 0x03DADB03, 0xC6BAF8C6, 0xFA9EC8FA, 0x82D6A882, 0xCF6E2BCF, 0x50704050, 0xEB85DCEB, 0x750AFE75, 0x8A93328A, 0x8DDFA48D, 0x4C29CA4C, 0x141C1014, 0x73D72173, 0xCCB4F0CC, 0x09D4D309, 0x108A5D10, 0xE2510FE2, 0x00000000, 0x9A196F9A, 0xE01A9DE0, 0x8F94368F, 0xE6C742E6, 0xECC94AEC, 0xFDD25EFD, 0xAB7FC1AB, 0xD8A8E0D8 ]; /** * The Key Schedule Array * * @var array */ private $K = []; /** * The Key depended S-Table 0 * * @var array */ private $S0 = []; /** * The Key depended S-Table 1 * * @var array */ private $S1 = []; /** * The Key depended S-Table 2 * * @var array */ private $S2 = []; /** * The Key depended S-Table 3 * * @var array */ private $S3 = []; /** * Holds the last used key * * @var array */ private $kl; /** * The Key Length (in bytes) * * @see Crypt_Twofish::setKeyLength() * @var int */ protected $key_length = 16; /** * Default Constructor. * * @param string $mode * @throws BadModeException if an invalid / unsupported mode is provided */ public function __construct($mode) { parent::__construct($mode); if ($this->mode == self::MODE_STREAM) { throw new BadModeException('Block ciphers cannot be ran in stream mode'); } } /** * Initialize Static Variables */ protected static function initialize_static_variables() { if (is_float(self::$m3[0])) { self::$m0 = array_map('intval', self::$m0); self::$m1 = array_map('intval', self::$m1); self::$m2 = array_map('intval', self::$m2); self::$m3 = array_map('intval', self::$m3); self::$q0 = array_map('intval', self::$q0); self::$q1 = array_map('intval', self::$q1); } parent::initialize_static_variables(); } /** * Sets the key length. * * Valid key lengths are 128, 192 or 256 bits * * @param int $length */ public function setKeyLength($length) { switch ($length) { case 128: case 192: case 256: break; default: throw new \LengthException('Key of size ' . $length . ' not supported by this algorithm. Only keys of sizes 16, 24 or 32 supported'); } parent::setKeyLength($length); } /** * Sets the key. * * Rijndael supports five different key lengths * * @see setKeyLength() * @param string $key * @throws \LengthException if the key length isn't supported */ public function setKey($key) { switch (strlen($key)) { case 16: case 24: case 32: break; default: throw new \LengthException('Key of size ' . strlen($key) . ' not supported by this algorithm. Only keys of sizes 16, 24 or 32 supported'); } parent::setKey($key); } /** * Setup the key (expansion) * * @see \phpseclib3\Crypt\Common\SymmetricKey::_setupKey() */ protected function setupKey() { if (isset($this->kl['key']) && $this->key === $this->kl['key']) { // already expanded return; } $this->kl = ['key' => $this->key]; /* Key expanding and generating the key-depended s-boxes */ $le_longs = unpack('V*', $this->key); $key = unpack('C*', $this->key); $m0 = self::$m0; $m1 = self::$m1; $m2 = self::$m2; $m3 = self::$m3; $q0 = self::$q0; $q1 = self::$q1; $K = $S0 = $S1 = $S2 = $S3 = []; switch (strlen($this->key)) { case 16: list($s7, $s6, $s5, $s4) = $this->mdsrem($le_longs[1], $le_longs[2]); list($s3, $s2, $s1, $s0) = $this->mdsrem($le_longs[3], $le_longs[4]); for ($i = 0, $j = 1; $i < 40; $i += 2, $j += 2) { $A = $m0[$q0[$q0[$i] ^ $key[ 9]] ^ $key[1]] ^ $m1[$q0[$q1[$i] ^ $key[10]] ^ $key[2]] ^ $m2[$q1[$q0[$i] ^ $key[11]] ^ $key[3]] ^ $m3[$q1[$q1[$i] ^ $key[12]] ^ $key[4]]; $B = $m0[$q0[$q0[$j] ^ $key[13]] ^ $key[5]] ^ $m1[$q0[$q1[$j] ^ $key[14]] ^ $key[6]] ^ $m2[$q1[$q0[$j] ^ $key[15]] ^ $key[7]] ^ $m3[$q1[$q1[$j] ^ $key[16]] ^ $key[8]]; $B = ($B << 8) | ($B >> 24 & 0xff); $A = self::safe_intval($A + $B); $K[] = $A; $A = self::safe_intval($A + $B); $K[] = ($A << 9 | $A >> 23 & 0x1ff); } for ($i = 0; $i < 256; ++$i) { $S0[$i] = $m0[$q0[$q0[$i] ^ $s4] ^ $s0]; $S1[$i] = $m1[$q0[$q1[$i] ^ $s5] ^ $s1]; $S2[$i] = $m2[$q1[$q0[$i] ^ $s6] ^ $s2]; $S3[$i] = $m3[$q1[$q1[$i] ^ $s7] ^ $s3]; } break; case 24: list($sb, $sa, $s9, $s8) = $this->mdsrem($le_longs[1], $le_longs[2]); list($s7, $s6, $s5, $s4) = $this->mdsrem($le_longs[3], $le_longs[4]); list($s3, $s2, $s1, $s0) = $this->mdsrem($le_longs[5], $le_longs[6]); for ($i = 0, $j = 1; $i < 40; $i += 2, $j += 2) { $A = $m0[$q0[$q0[$q1[$i] ^ $key[17]] ^ $key[ 9]] ^ $key[1]] ^ $m1[$q0[$q1[$q1[$i] ^ $key[18]] ^ $key[10]] ^ $key[2]] ^ $m2[$q1[$q0[$q0[$i] ^ $key[19]] ^ $key[11]] ^ $key[3]] ^ $m3[$q1[$q1[$q0[$i] ^ $key[20]] ^ $key[12]] ^ $key[4]]; $B = $m0[$q0[$q0[$q1[$j] ^ $key[21]] ^ $key[13]] ^ $key[5]] ^ $m1[$q0[$q1[$q1[$j] ^ $key[22]] ^ $key[14]] ^ $key[6]] ^ $m2[$q1[$q0[$q0[$j] ^ $key[23]] ^ $key[15]] ^ $key[7]] ^ $m3[$q1[$q1[$q0[$j] ^ $key[24]] ^ $key[16]] ^ $key[8]]; $B = ($B << 8) | ($B >> 24 & 0xff); $A = self::safe_intval($A + $B); $K[] = $A; $A = self::safe_intval($A + $B); $K[] = ($A << 9 | $A >> 23 & 0x1ff); } for ($i = 0; $i < 256; ++$i) { $S0[$i] = $m0[$q0[$q0[$q1[$i] ^ $s8] ^ $s4] ^ $s0]; $S1[$i] = $m1[$q0[$q1[$q1[$i] ^ $s9] ^ $s5] ^ $s1]; $S2[$i] = $m2[$q1[$q0[$q0[$i] ^ $sa] ^ $s6] ^ $s2]; $S3[$i] = $m3[$q1[$q1[$q0[$i] ^ $sb] ^ $s7] ^ $s3]; } break; default: // 32 list($sf, $se, $sd, $sc) = $this->mdsrem($le_longs[1], $le_longs[2]); list($sb, $sa, $s9, $s8) = $this->mdsrem($le_longs[3], $le_longs[4]); list($s7, $s6, $s5, $s4) = $this->mdsrem($le_longs[5], $le_longs[6]); list($s3, $s2, $s1, $s0) = $this->mdsrem($le_longs[7], $le_longs[8]); for ($i = 0, $j = 1; $i < 40; $i += 2, $j += 2) { $A = $m0[$q0[$q0[$q1[$q1[$i] ^ $key[25]] ^ $key[17]] ^ $key[ 9]] ^ $key[1]] ^ $m1[$q0[$q1[$q1[$q0[$i] ^ $key[26]] ^ $key[18]] ^ $key[10]] ^ $key[2]] ^ $m2[$q1[$q0[$q0[$q0[$i] ^ $key[27]] ^ $key[19]] ^ $key[11]] ^ $key[3]] ^ $m3[$q1[$q1[$q0[$q1[$i] ^ $key[28]] ^ $key[20]] ^ $key[12]] ^ $key[4]]; $B = $m0[$q0[$q0[$q1[$q1[$j] ^ $key[29]] ^ $key[21]] ^ $key[13]] ^ $key[5]] ^ $m1[$q0[$q1[$q1[$q0[$j] ^ $key[30]] ^ $key[22]] ^ $key[14]] ^ $key[6]] ^ $m2[$q1[$q0[$q0[$q0[$j] ^ $key[31]] ^ $key[23]] ^ $key[15]] ^ $key[7]] ^ $m3[$q1[$q1[$q0[$q1[$j] ^ $key[32]] ^ $key[24]] ^ $key[16]] ^ $key[8]]; $B = ($B << 8) | ($B >> 24 & 0xff); $A = self::safe_intval($A + $B); $K[] = $A; $A = self::safe_intval($A + $B); $K[] = ($A << 9 | $A >> 23 & 0x1ff); } for ($i = 0; $i < 256; ++$i) { $S0[$i] = $m0[$q0[$q0[$q1[$q1[$i] ^ $sc] ^ $s8] ^ $s4] ^ $s0]; $S1[$i] = $m1[$q0[$q1[$q1[$q0[$i] ^ $sd] ^ $s9] ^ $s5] ^ $s1]; $S2[$i] = $m2[$q1[$q0[$q0[$q0[$i] ^ $se] ^ $sa] ^ $s6] ^ $s2]; $S3[$i] = $m3[$q1[$q1[$q0[$q1[$i] ^ $sf] ^ $sb] ^ $s7] ^ $s3]; } } $this->K = $K; $this->S0 = $S0; $this->S1 = $S1; $this->S2 = $S2; $this->S3 = $S3; } /** * _mdsrem function using by the twofish cipher algorithm * * @param string $A * @param string $B * @return array */ private function mdsrem($A, $B) { // No gain by unrolling this loop. for ($i = 0; $i < 8; ++$i) { // Get most significant coefficient. $t = 0xff & ($B >> 24); // Shift the others up. $B = ($B << 8) | (0xff & ($A >> 24)); $A <<= 8; $u = $t << 1; // Subtract the modular polynomial on overflow. if ($t & 0x80) { $u ^= 0x14d; } // Remove t * (a * x^2 + 1). $B ^= $t ^ ($u << 16); // Form u = a*t + t/a = t*(a + 1/a). $u ^= 0x7fffffff & ($t >> 1); // Add the modular polynomial on underflow. if ($t & 0x01) { $u ^= 0xa6 ; } // Remove t * (a + 1/a) * (x^3 + x). $B ^= ($u << 24) | ($u << 8); } return [ 0xff & $B >> 24, 0xff & $B >> 16, 0xff & $B >> 8, 0xff & $B]; } /** * Encrypts a block * * @param string $in * @return string */ protected function encryptBlock($in) { $S0 = $this->S0; $S1 = $this->S1; $S2 = $this->S2; $S3 = $this->S3; $K = $this->K; $in = unpack("V4", $in); $R0 = $K[0] ^ $in[1]; $R1 = $K[1] ^ $in[2]; $R2 = $K[2] ^ $in[3]; $R3 = $K[3] ^ $in[4]; $ki = 7; while ($ki < 39) { $t0 = $S0[ $R0 & 0xff] ^ $S1[($R0 >> 8) & 0xff] ^ $S2[($R0 >> 16) & 0xff] ^ $S3[($R0 >> 24) & 0xff]; $t1 = $S0[($R1 >> 24) & 0xff] ^ $S1[ $R1 & 0xff] ^ $S2[($R1 >> 8) & 0xff] ^ $S3[($R1 >> 16) & 0xff]; $R2 ^= self::safe_intval($t0 + $t1 + $K[++$ki]); $R2 = ($R2 >> 1 & 0x7fffffff) | ($R2 << 31); $R3 = ((($R3 >> 31) & 1) | ($R3 << 1)) ^ self::safe_intval($t0 + ($t1 << 1) + $K[++$ki]); $t0 = $S0[ $R2 & 0xff] ^ $S1[($R2 >> 8) & 0xff] ^ $S2[($R2 >> 16) & 0xff] ^ $S3[($R2 >> 24) & 0xff]; $t1 = $S0[($R3 >> 24) & 0xff] ^ $S1[ $R3 & 0xff] ^ $S2[($R3 >> 8) & 0xff] ^ $S3[($R3 >> 16) & 0xff]; $R0 ^= self::safe_intval($t0 + $t1 + $K[++$ki]); $R0 = ($R0 >> 1 & 0x7fffffff) | ($R0 << 31); $R1 = ((($R1 >> 31) & 1) | ($R1 << 1)) ^ self::safe_intval($t0 + ($t1 << 1) + $K[++$ki]); } // @codingStandardsIgnoreStart return pack("V4", $K[4] ^ $R2, $K[5] ^ $R3, $K[6] ^ $R0, $K[7] ^ $R1); // @codingStandardsIgnoreEnd } /** * Decrypts a block * * @param string $in * @return string */ protected function decryptBlock($in) { $S0 = $this->S0; $S1 = $this->S1; $S2 = $this->S2; $S3 = $this->S3; $K = $this->K; $in = unpack("V4", $in); $R0 = $K[4] ^ $in[1]; $R1 = $K[5] ^ $in[2]; $R2 = $K[6] ^ $in[3]; $R3 = $K[7] ^ $in[4]; $ki = 40; while ($ki > 8) { $t0 = $S0[$R0 & 0xff] ^ $S1[$R0 >> 8 & 0xff] ^ $S2[$R0 >> 16 & 0xff] ^ $S3[$R0 >> 24 & 0xff]; $t1 = $S0[$R1 >> 24 & 0xff] ^ $S1[$R1 & 0xff] ^ $S2[$R1 >> 8 & 0xff] ^ $S3[$R1 >> 16 & 0xff]; $R3 ^= self::safe_intval($t0 + ($t1 << 1) + $K[--$ki]); $R3 = $R3 >> 1 & 0x7fffffff | $R3 << 31; $R2 = ($R2 >> 31 & 0x1 | $R2 << 1) ^ self::safe_intval($t0 + $t1 + $K[--$ki]); $t0 = $S0[$R2 & 0xff] ^ $S1[$R2 >> 8 & 0xff] ^ $S2[$R2 >> 16 & 0xff] ^ $S3[$R2 >> 24 & 0xff]; $t1 = $S0[$R3 >> 24 & 0xff] ^ $S1[$R3 & 0xff] ^ $S2[$R3 >> 8 & 0xff] ^ $S3[$R3 >> 16 & 0xff]; $R1 ^= self::safe_intval($t0 + ($t1 << 1) + $K[--$ki]); $R1 = $R1 >> 1 & 0x7fffffff | $R1 << 31; $R0 = ($R0 >> 31 & 0x1 | $R0 << 1) ^ self::safe_intval($t0 + $t1 + $K[--$ki]); } // @codingStandardsIgnoreStart return pack("V4", $K[0] ^ $R2, $K[1] ^ $R3, $K[2] ^ $R0, $K[3] ^ $R1); // @codingStandardsIgnoreEnd } /** * Setup the performance-optimized function for de/encrypt() * * @see \phpseclib3\Crypt\Common\SymmetricKey::_setupInlineCrypt() */ protected function setupInlineCrypt() { $K = $this->K; $init_crypt = ' static $S0, $S1, $S2, $S3; if (!$S0) { for ($i = 0; $i < 256; ++$i) { $S0[] = (int)$this->S0[$i]; $S1[] = (int)$this->S1[$i]; $S2[] = (int)$this->S2[$i]; $S3[] = (int)$this->S3[$i]; } } '; $safeint = self::safe_intval_inline(); // Generating encrypt code: $encrypt_block = ' $in = unpack("V4", $in); $R0 = ' . $K[0] . ' ^ $in[1]; $R1 = ' . $K[1] . ' ^ $in[2]; $R2 = ' . $K[2] . ' ^ $in[3]; $R3 = ' . $K[3] . ' ^ $in[4]; '; for ($ki = 7, $i = 0; $i < 8; ++$i) { $encrypt_block .= ' $t0 = $S0[ $R0 & 0xff] ^ $S1[($R0 >> 8) & 0xff] ^ $S2[($R0 >> 16) & 0xff] ^ $S3[($R0 >> 24) & 0xff]; $t1 = $S0[($R1 >> 24) & 0xff] ^ $S1[ $R1 & 0xff] ^ $S2[($R1 >> 8) & 0xff] ^ $S3[($R1 >> 16) & 0xff]; $R2^= ' . sprintf($safeint, '$t0 + $t1 + ' . $K[++$ki]) . '; $R2 = ($R2 >> 1 & 0x7fffffff) | ($R2 << 31); $R3 = ((($R3 >> 31) & 1) | ($R3 << 1)) ^ ' . sprintf($safeint, '($t0 + ($t1 << 1) + ' . $K[++$ki] . ')') . '; $t0 = $S0[ $R2 & 0xff] ^ $S1[($R2 >> 8) & 0xff] ^ $S2[($R2 >> 16) & 0xff] ^ $S3[($R2 >> 24) & 0xff]; $t1 = $S0[($R3 >> 24) & 0xff] ^ $S1[ $R3 & 0xff] ^ $S2[($R3 >> 8) & 0xff] ^ $S3[($R3 >> 16) & 0xff]; $R0^= ' . sprintf($safeint, '($t0 + $t1 + ' . $K[++$ki] . ')') . '; $R0 = ($R0 >> 1 & 0x7fffffff) | ($R0 << 31); $R1 = ((($R1 >> 31) & 1) | ($R1 << 1)) ^ ' . sprintf($safeint, '($t0 + ($t1 << 1) + ' . $K[++$ki] . ')') . '; '; } $encrypt_block .= ' $in = pack("V4", ' . $K[4] . ' ^ $R2, ' . $K[5] . ' ^ $R3, ' . $K[6] . ' ^ $R0, ' . $K[7] . ' ^ $R1); '; // Generating decrypt code: $decrypt_block = ' $in = unpack("V4", $in); $R0 = ' . $K[4] . ' ^ $in[1]; $R1 = ' . $K[5] . ' ^ $in[2]; $R2 = ' . $K[6] . ' ^ $in[3]; $R3 = ' . $K[7] . ' ^ $in[4]; '; for ($ki = 40, $i = 0; $i < 8; ++$i) { $decrypt_block .= ' $t0 = $S0[$R0 & 0xff] ^ $S1[$R0 >> 8 & 0xff] ^ $S2[$R0 >> 16 & 0xff] ^ $S3[$R0 >> 24 & 0xff]; $t1 = $S0[$R1 >> 24 & 0xff] ^ $S1[$R1 & 0xff] ^ $S2[$R1 >> 8 & 0xff] ^ $S3[$R1 >> 16 & 0xff]; $R3^= ' . sprintf($safeint, '$t0 + ($t1 << 1) + ' . $K[--$ki]) . '; $R3 = $R3 >> 1 & 0x7fffffff | $R3 << 31; $R2 = ($R2 >> 31 & 0x1 | $R2 << 1) ^ ' . sprintf($safeint, '($t0 + $t1 + ' . $K[--$ki] . ')') . '; $t0 = $S0[$R2 & 0xff] ^ $S1[$R2 >> 8 & 0xff] ^ $S2[$R2 >> 16 & 0xff] ^ $S3[$R2 >> 24 & 0xff]; $t1 = $S0[$R3 >> 24 & 0xff] ^ $S1[$R3 & 0xff] ^ $S2[$R3 >> 8 & 0xff] ^ $S3[$R3 >> 16 & 0xff]; $R1^= ' . sprintf($safeint, '$t0 + ($t1 << 1) + ' . $K[--$ki]) . '; $R1 = $R1 >> 1 & 0x7fffffff | $R1 << 31; $R0 = ($R0 >> 31 & 0x1 | $R0 << 1) ^ ' . sprintf($safeint, '($t0 + $t1 + ' . $K[--$ki] . ')') . '; '; } $decrypt_block .= ' $in = pack("V4", ' . $K[0] . ' ^ $R2, ' . $K[1] . ' ^ $R3, ' . $K[2] . ' ^ $R0, ' . $K[3] . ' ^ $R1); '; $this->inline_crypt = $this->createInlineCryptFunction( [ 'init_crypt' => $init_crypt, 'init_encrypt' => '', 'init_decrypt' => '', 'encrypt_block' => $encrypt_block, 'decrypt_block' => $decrypt_block ] ); } } PK!Lvendor/phpseclib/phpseclib/phpseclib/Exception/BadConfigurationException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * BadConfigurationException * * @author Jim Wigginton */ class BadConfigurationException extends \RuntimeException { } PK!Ivendor/phpseclib/phpseclib/phpseclib/Exception/BadDecryptionException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * BadDecryptionException * * @author Jim Wigginton */ class BadDecryptionException extends \RuntimeException { } PK!ȍCvendor/phpseclib/phpseclib/phpseclib/Exception/BadModeException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * BadModeException * * @author Jim Wigginton */ class BadModeException extends \RuntimeException { } PK!Lvendor/phpseclib/phpseclib/phpseclib/Exception/ConnectionClosedException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * ConnectionClosedException * * @author Jim Wigginton */ class ConnectionClosedException extends \RuntimeException { } PK!YԘmHvendor/phpseclib/phpseclib/phpseclib/Exception/FileNotFoundException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * FileNotFoundException * * @author Jim Wigginton */ class FileNotFoundException extends \RuntimeException { } PK!/Mvendor/phpseclib/phpseclib/phpseclib/Exception/InconsistentSetupException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * InconsistentSetupException * * @author Jim Wigginton */ class InconsistentSetupException extends \RuntimeException { } PK!y=GMvendor/phpseclib/phpseclib/phpseclib/Exception/InsufficientSetupException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * InsufficientSetupException * * @author Jim Wigginton */ class InsufficientSetupException extends \RuntimeException { } PK!uOvendor/phpseclib/phpseclib/phpseclib/Exception/InvalidPacketLengthException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * NoKeyLoadedException * * @author Jim Wigginton */ class NoKeyLoadedException extends \RuntimeException { } PK!JQvendor/phpseclib/phpseclib/phpseclib/Exception/NoSupportedAlgorithmsException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * NoSupportedAlgorithmsException * * @author Jim Wigginton */ class NoSupportedAlgorithmsException extends \RuntimeException { } PK!LD:>Cvendor/phpseclib/phpseclib/phpseclib/Exception/TimeoutException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * UnableToConnectException * * @author Jim Wigginton */ class UnableToConnectException extends \RuntimeException { } PK!QZPvendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedAlgorithmException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * UnsupportedAlgorithmException * * @author Jim Wigginton */ class UnsupportedAlgorithmException extends \RuntimeException { } PK! Lvendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedCurveException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * UnsupportedCurveException * * @author Jim Wigginton */ class UnsupportedCurveException extends \RuntimeException { } PK!PfMvendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedFormatException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * UnsupportedFormatException * * @author Jim Wigginton */ class UnsupportedFormatException extends \RuntimeException { } PK!ZWPvendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedOperationException.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Exception; /** * UnsupportedOperationException * * @author Jim Wigginton */ class UnsupportedOperationException extends \RuntimeException { } PK!!AIvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AccessDescription.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AccessDescription * * @author Jim Wigginton */ abstract class AccessDescription { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'accessMethod' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'accessLocation' => GeneralName::MAP ] ]; } PK!|[Pvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AdministrationDomainName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AdministrationDomainName * * @author Jim Wigginton */ abstract class AdministrationDomainName { const MAP = [ 'type' => ASN1::TYPE_CHOICE, // if class isn't present it's assumed to be \phpseclib3\File\ASN1::CLASS_UNIVERSAL or // (if constant is present) \phpseclib3\File\ASN1::CLASS_CONTEXT_SPECIFIC 'class' => ASN1::CLASS_APPLICATION, 'cast' => 2, 'children' => [ 'numeric' => ['type' => ASN1::TYPE_NUMERIC_STRING], 'printable' => ['type' => ASN1::TYPE_PRINTABLE_STRING] ] ]; } PK!(Kvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AlgorithmIdentifier.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AlgorithmIdentifier * * @author Jim Wigginton */ abstract class AlgorithmIdentifier { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'algorithm' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'parameters' => [ 'type' => ASN1::TYPE_ANY, 'optional' => true ] ] ]; } PK!ߣA  Cvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AnotherName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AnotherName * * @author Jim Wigginton */ abstract class AnotherName { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'type-id' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'value' => [ 'type' => ASN1::TYPE_ANY, 'constant' => 0, 'optional' => true, 'explicit' => true ] ] ]; } PK!5~Avendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Attribute.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Attribute * * @author Jim Wigginton */ abstract class Attribute { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'type' => AttributeType::MAP, 'value' => [ 'type' => ASN1::TYPE_SET, 'min' => 1, 'max' => -1, 'children' => AttributeValue::MAP ] ] ]; } PK!S+Y55Bvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Attributes.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Attributes * * @author Jim Wigginton */ abstract class Attributes { const MAP = [ 'type' => ASN1::TYPE_SET, 'min' => 1, 'max' => -1, 'children' => Attribute::MAP ]; } PK!*<Mvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeTypeAndValue.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AttributeTypeAndValue * * @author Jim Wigginton */ abstract class AttributeTypeAndValue { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'type' => AttributeType::MAP, 'value' => AttributeValue::MAP ] ]; } PK!Evendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeType.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AttributeType * * @author Jim Wigginton */ abstract class AttributeType { const MAP = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; } PK!>aFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeValue.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AttributeValue * * @author Jim Wigginton */ abstract class AttributeValue { const MAP = ['type' => ASN1::TYPE_ANY]; } PK! ,ooQvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AuthorityInfoAccessSyntax.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AuthorityInfoAccessSyntax * * @author Jim Wigginton */ abstract class AuthorityInfoAccessSyntax { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => AccessDescription::MAP ]; } PK!8]SSNvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AuthorityKeyIdentifier.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * AuthorityKeyIdentifier * * @author Jim Wigginton */ abstract class AuthorityKeyIdentifier { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'keyIdentifier' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + KeyIdentifier::MAP, 'authorityCertIssuer' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + GeneralNames::MAP, 'authorityCertSerialNumber' => [ 'constant' => 2, 'optional' => true, 'implicit' => true ] + CertificateSerialNumber::MAP ] ]; } PK!nDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BaseDistance.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * BaseDistance * * @author Jim Wigginton */ abstract class BaseDistance { const MAP = ['type' => ASN1::TYPE_INTEGER]; } PK! $>>Hvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BasicConstraints.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * BasicConstraints * * @author Jim Wigginton */ abstract class BasicConstraints { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'cA' => [ 'type' => ASN1::TYPE_BOOLEAN, 'optional' => true, 'default' => false ], 'pathLenConstraint' => [ 'type' => ASN1::TYPE_INTEGER, 'optional' => true ] ] ]; } PK!Uvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInDomainDefinedAttribute.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * BuiltInDomainDefinedAttribute * * @author Jim Wigginton */ abstract class BuiltInDomainDefinedAttribute { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'type' => ['type' => ASN1::TYPE_PRINTABLE_STRING], 'value' => ['type' => ASN1::TYPE_PRINTABLE_STRING] ] ]; } PK!F]ũVvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInDomainDefinedAttributes.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * BuiltInDomainDefinedAttributes * * @author Jim Wigginton */ abstract class BuiltInDomainDefinedAttributes { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => 4, // ub-domain-defined-attributes 'children' => BuiltInDomainDefinedAttribute::MAP ]; } PK!/wQvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInStandardAttributes.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * BuiltInStandardAttributes * * @author Jim Wigginton */ abstract class BuiltInStandardAttributes { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'country-name' => ['optional' => true] + CountryName::MAP, 'administration-domain-name' => ['optional' => true] + AdministrationDomainName::MAP, 'network-address' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + NetworkAddress::MAP, 'terminal-identifier' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + TerminalIdentifier::MAP, 'private-domain-name' => [ 'constant' => 2, 'optional' => true, 'explicit' => true ] + PrivateDomainName::MAP, 'organization-name' => [ 'constant' => 3, 'optional' => true, 'implicit' => true ] + OrganizationName::MAP, 'numeric-user-identifier' => [ 'constant' => 4, 'optional' => true, 'implicit' => true ] + NumericUserIdentifier::MAP, 'personal-name' => [ 'constant' => 5, 'optional' => true, 'implicit' => true ] + PersonalName::MAP, 'organizational-unit-names' => [ 'constant' => 6, 'optional' => true, 'implicit' => true ] + OrganizationalUnitNames::MAP ] ]; } PK!5Ivendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateIssuer.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; /** * CertificateIssuer * * @author Jim Wigginton */ abstract class CertificateIssuer { const MAP = GeneralNames::MAP; } PK!6Gvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateList.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CertificateList * * @author Jim Wigginton */ abstract class CertificateList { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'tbsCertList' => TBSCertList::MAP, 'signatureAlgorithm' => AlgorithmIdentifier::MAP, 'signature' => ['type' => ASN1::TYPE_BIT_STRING] ] ]; } PK!贗Cvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Certificate.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Certificate * * @author Jim Wigginton */ abstract class Certificate { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'tbsCertificate' => TBSCertificate::MAP, 'signatureAlgorithm' => AlgorithmIdentifier::MAP, 'signature' => ['type' => ASN1::TYPE_BIT_STRING] ] ]; } PK!҅]]Kvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificatePolicies.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CertificatePolicies * * @author Jim Wigginton */ abstract class CertificatePolicies { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => PolicyInformation::MAP ]; } PK!Ovendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateSerialNumber.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CertificateSerialNumber * * @author Jim Wigginton */ abstract class CertificateSerialNumber { const MAP = ['type' => ASN1::TYPE_INTEGER]; } PK!AvPvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificationRequestInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CertificationRequestInfo * * @author Jim Wigginton */ abstract class CertificationRequestInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => ['v1'] ], 'subject' => Name::MAP, 'subjectPKInfo' => SubjectPublicKeyInfo::MAP, 'attributes' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + Attributes::MAP, ] ]; } PK!)Lvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificationRequest.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CertificationRequest * * @author Jim Wigginton */ abstract class CertificationRequest { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'certificationRequestInfo' => CertificationRequestInfo::MAP, 'signatureAlgorithm' => AlgorithmIdentifier::MAP, 'signature' => ['type' => ASN1::TYPE_BIT_STRING] ] ]; } PK!P*Dvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertPolicyId.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CertPolicyId * * @author Jim Wigginton */ abstract class CertPolicyId { const MAP = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; } PK!(&&Jvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Characteristic_two.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Characteristic_two * * @author Jim Wigginton */ abstract class Characteristic_two { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'm' => ['type' => ASN1::TYPE_INTEGER], // field size 2**m 'basis' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'parameters' => [ 'type' => ASN1::TYPE_ANY, 'optional' => true ] ] ]; } PK!զCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CountryName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CountryName * * @author Jim Wigginton */ abstract class CountryName { const MAP = [ 'type' => ASN1::TYPE_CHOICE, // if class isn't present it's assumed to be \phpseclib3\File\ASN1::CLASS_UNIVERSAL or // (if constant is present) \phpseclib3\File\ASN1::CLASS_CONTEXT_SPECIFIC 'class' => ASN1::CLASS_APPLICATION, 'cast' => 1, 'children' => [ 'x121-dcc-code' => ['type' => ASN1::TYPE_NUMERIC_STRING], 'iso-3166-alpha2-code' => ['type' => ASN1::TYPE_PRINTABLE_STRING] ] ]; } PK!f>vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CPSuri.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CPSuri * * @author Jim Wigginton */ abstract class CPSuri { const MAP = ['type' => ASN1::TYPE_IA5_STRING]; } PK!Ыy`ccMvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLDistributionPoints.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CRLDistributionPoints * * @author Jim Wigginton */ abstract class CRLDistributionPoints { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => DistributionPoint::MAP ]; } PK!Avendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLNumber.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CRLNumber * * @author Jim Wigginton */ abstract class CRLNumber { const MAP = ['type' => ASN1::TYPE_INTEGER]; } PK!z7bbAvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLReason.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * CRLReason * * @author Jim Wigginton */ abstract class CRLReason { const MAP = [ 'type' => ASN1::TYPE_ENUMERATED, 'mapping' => [ 'unspecified', 'keyCompromise', 'cACompromise', 'affiliationChanged', 'superseded', 'cessationOfOperation', 'certificateHold', // Value 7 is not used. 8 => 'removeFromCRL', 'privilegeWithdrawn', 'aACompromise' ] ]; } PK!U=vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Curve.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Curve * * @author Jim Wigginton */ abstract class Curve { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'a' => FieldElement::MAP, 'b' => FieldElement::MAP, 'seed' => [ 'type' => ASN1::TYPE_BIT_STRING, 'optional' => true ] ] ]; } PK!:VVCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DHParameter.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DHParameter * * @author Jim Wigginton */ abstract class DHParameter { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'prime' => ['type' => ASN1::TYPE_INTEGER], 'base' => ['type' => ASN1::TYPE_INTEGER], 'privateValueLength' => [ 'type' => ASN1::TYPE_INTEGER, 'optional' => true ] ] ]; } PK!jBvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DigestInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DigestInfo * * from https://tools.ietf.org/html/rfc2898#appendix-A.3 * * @author Jim Wigginton */ abstract class DigestInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'digestAlgorithm' => AlgorithmIdentifier::MAP, 'digest' => ['type' => ASN1::TYPE_OCTET_STRING] ] ]; } PK!7hܧrrGvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DirectoryString.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DirectoryString * * @author Jim Wigginton */ abstract class DirectoryString { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'teletexString' => ['type' => ASN1::TYPE_TELETEX_STRING], 'printableString' => ['type' => ASN1::TYPE_PRINTABLE_STRING], 'universalString' => ['type' => ASN1::TYPE_UNIVERSAL_STRING], 'utf8String' => ['type' => ASN1::TYPE_UTF8_STRING], 'bmpString' => ['type' => ASN1::TYPE_BMP_STRING] ] ]; } PK!rCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DisplayText.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DisplayText * * @author Jim Wigginton */ abstract class DisplayText { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'ia5String' => ['type' => ASN1::TYPE_IA5_STRING], 'visibleString' => ['type' => ASN1::TYPE_VISIBLE_STRING], 'bmpString' => ['type' => ASN1::TYPE_BMP_STRING], 'utf8String' => ['type' => ASN1::TYPE_UTF8_STRING] ] ]; } PK!aMvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DistributionPointName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DistributionPointName * * @author Jim Wigginton */ abstract class DistributionPointName { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'fullName' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + GeneralNames::MAP, 'nameRelativeToCRLIssuer' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + RelativeDistinguishedName::MAP ] ]; } PK!((Ivendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DistributionPoint.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DistributionPoint * * @author Jim Wigginton */ abstract class DistributionPoint { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'distributionPoint' => [ 'constant' => 0, 'optional' => true, 'explicit' => true ] + DistributionPointName::MAP, 'reasons' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + ReasonFlags::MAP, 'cRLIssuer' => [ 'constant' => 2, 'optional' => true, 'implicit' => true ] + GeneralNames::MAP ] ]; } PK!0Avendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAParams.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DSAParams * * @author Jim Wigginton */ abstract class DSAParams { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'p' => ['type' => ASN1::TYPE_INTEGER], 'q' => ['type' => ASN1::TYPE_INTEGER], 'g' => ['type' => ASN1::TYPE_INTEGER] ] ]; } PK!1NNEvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAPrivateKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DSAPrivateKey * * @author Jim Wigginton */ abstract class DSAPrivateKey { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => ['type' => ASN1::TYPE_INTEGER], 'p' => ['type' => ASN1::TYPE_INTEGER], 'q' => ['type' => ASN1::TYPE_INTEGER], 'g' => ['type' => ASN1::TYPE_INTEGER], 'y' => ['type' => ASN1::TYPE_INTEGER], 'x' => ['type' => ASN1::TYPE_INTEGER] ] ]; } PK!"5,Dvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAPublicKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DSAPublicKey * * @author Jim Wigginton */ abstract class DSAPublicKey { const MAP = ['type' => ASN1::TYPE_INTEGER]; } PK!5vvCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DssSigValue.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * DssSigValue * * @author Jim Wigginton */ abstract class DssSigValue { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'r' => ['type' => ASN1::TYPE_INTEGER], 's' => ['type' => ASN1::TYPE_INTEGER] ] ]; } PK!ߖg%||Evendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EcdsaSigValue.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * EcdsaSigValue * * @author Jim Wigginton */ abstract class EcdsaSigValue { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'r' => ['type' => ASN1::TYPE_INTEGER], 's' => ['type' => ASN1::TYPE_INTEGER] ] ]; } PK!_ttDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECParameters.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ECParameters * * ECParameters ::= CHOICE { * namedCurve OBJECT IDENTIFIER * -- implicitCurve NULL * -- specifiedCurve SpecifiedECDomain * } * -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. * -- Details for SpecifiedECDomain can be found in [X9.62]. * -- Any future additions to this CHOICE should be coordinated * -- with ANSI X9. * * @author Jim Wigginton */ abstract class ECParameters { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'namedCurve' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'implicitCurve' => ['type' => ASN1::TYPE_NULL], 'specifiedCurve' => SpecifiedECDomain::MAP ] ]; } PK!d?vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECPoint.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ECPoint * * @author Jim Wigginton */ abstract class ECPoint { const MAP = ['type' => ASN1::TYPE_OCTET_STRING]; } PK!Og~~Dvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECPrivateKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ECPrivateKey * * @author Jim Wigginton */ abstract class ECPrivateKey { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => [1 => 'ecPrivkeyVer1'] ], 'privateKey' => ['type' => ASN1::TYPE_OCTET_STRING], 'parameters' => [ 'constant' => 0, 'optional' => true, 'explicit' => true ] + ECParameters::MAP, 'publicKey' => [ 'type' => ASN1::TYPE_BIT_STRING, 'constant' => 1, 'optional' => true, 'explicit' => true ] ] ]; } PK!V:22Dvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EDIPartyName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * EDIPartyName * * @author Jim Wigginton */ abstract class EDIPartyName { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'nameAssigner' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + DirectoryString::MAP, // partyName is technically required but \phpseclib3\File\ASN1 doesn't currently support non-optional constants and // setting it to optional gets the job done in any event. 'partyName' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + DirectoryString::MAP ] ]; } PK!pEvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EncryptedData.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * EncryptedData * * @author Jim Wigginton */ abstract class EncryptedData { const MAP = ['type' => ASN1::TYPE_OCTET_STRING]; } PK!0צOvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EncryptedPrivateKeyInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * EncryptedPrivateKeyInfo * * @author Jim Wigginton */ abstract class EncryptedPrivateKeyInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'encryptionAlgorithm' => AlgorithmIdentifier::MAP, 'encryptedData' => EncryptedData::MAP ] ]; } PK!ܥX<Jvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtensionAttribute.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ExtensionAttribute * * @author Jim Wigginton */ abstract class ExtensionAttribute { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'extension-attribute-type' => [ 'type' => ASN1::TYPE_PRINTABLE_STRING, 'constant' => 0, 'optional' => true, 'implicit' => true ], 'extension-attribute-value' => [ 'type' => ASN1::TYPE_ANY, 'constant' => 1, 'optional' => true, 'explicit' => true ] ] ]; } PK! YuuKvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtensionAttributes.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ExtensionAttributes * * @author Jim Wigginton */ abstract class ExtensionAttributes { const MAP = [ 'type' => ASN1::TYPE_SET, 'min' => 1, 'max' => 256, // ub-extension-attributes 'children' => ExtensionAttribute::MAP ]; } PK!݈Y++Avendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Extension.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Extension * * A certificate using system MUST reject the certificate if it encounters * a critical extension it does not recognize; however, a non-critical * extension may be ignored if it is not recognized. * * http://tools.ietf.org/html/rfc5280#section-4.2 * * @author Jim Wigginton */ abstract class Extension { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'extnId' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'critical' => [ 'type' => ASN1::TYPE_BOOLEAN, 'optional' => true, 'default' => false ], 'extnValue' => ['type' => ASN1::TYPE_OCTET_STRING] ] ]; } PK!7Bvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Extensions.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Extensions * * @author Jim Wigginton */ abstract class Extensions { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, // technically, it's MAX, but we'll assume anything < 0 is MAX 'max' => -1, // if 'children' isn't an array then 'min' and 'max' must be defined 'children' => Extension::MAP ]; } PK!dRRIvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtKeyUsageSyntax.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ExtKeyUsageSyntax * * @author Jim Wigginton */ abstract class ExtKeyUsageSyntax { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => KeyPurposeId::MAP ]; } PK![:gDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/FieldElement.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * FieldElement * * @author Jim Wigginton */ abstract class FieldElement { const MAP = ['type' => ASN1::TYPE_OCTET_STRING]; } PK!qƃ?vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/FieldID.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * FieldID * * @author Jim Wigginton */ abstract class FieldID { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'fieldType' => ['type' => ASN1::TYPE_OBJECT_IDENTIFIER], 'parameters' => [ 'type' => ASN1::TYPE_ANY, 'optional' => true ] ] ]; } PK!Cvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * GeneralName * * @author Jim Wigginton */ abstract class GeneralName { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'otherName' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + AnotherName::MAP, 'rfc822Name' => [ 'type' => ASN1::TYPE_IA5_STRING, 'constant' => 1, 'optional' => true, 'implicit' => true ], 'dNSName' => [ 'type' => ASN1::TYPE_IA5_STRING, 'constant' => 2, 'optional' => true, 'implicit' => true ], 'x400Address' => [ 'constant' => 3, 'optional' => true, 'implicit' => true ] + ORAddress::MAP, 'directoryName' => [ 'constant' => 4, 'optional' => true, 'explicit' => true ] + Name::MAP, 'ediPartyName' => [ 'constant' => 5, 'optional' => true, 'implicit' => true ] + EDIPartyName::MAP, 'uniformResourceIdentifier' => [ 'type' => ASN1::TYPE_IA5_STRING, 'constant' => 6, 'optional' => true, 'implicit' => true ], 'iPAddress' => [ 'type' => ASN1::TYPE_OCTET_STRING, 'constant' => 7, 'optional' => true, 'implicit' => true ], 'registeredID' => [ 'type' => ASN1::TYPE_OBJECT_IDENTIFIER, 'constant' => 8, 'optional' => true, 'implicit' => true ] ] ]; } PK!CBBDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralNames.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * GeneralNames * * @author Jim Wigginton */ abstract class GeneralNames { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => GeneralName::MAP ]; } PK!ooFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralSubtree.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * GeneralSubtree * * @author Jim Wigginton */ abstract class GeneralSubtree { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'base' => GeneralName::MAP, 'minimum' => [ 'constant' => 0, 'optional' => true, 'implicit' => true, 'default' => '0' ] + BaseDistance::MAP, 'maximum' => [ 'constant' => 1, 'optional' => true, 'implicit' => true, ] + BaseDistance::MAP ] ]; } PK!ߠqNNGvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralSubtrees.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * GeneralSubtrees * * @author Jim Wigginton */ abstract class GeneralSubtrees { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => GeneralSubtree::MAP ]; } PK!$IEvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/HashAlgorithm.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; /** * HashAglorithm * * @author Jim Wigginton */ abstract class HashAlgorithm { const MAP = AlgorithmIdentifier::MAP; } PK!>uKvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/HoldInstructionCode.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * HoldInstructionCode * * @author Jim Wigginton */ abstract class HoldInstructionCode { const MAP = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; } PK!6njFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/InvalidityDate.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * InvalidityDate * * @author Jim Wigginton */ abstract class InvalidityDate { const MAP = ['type' => ASN1::TYPE_GENERALIZED_TIME]; } PK!O5Evendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/IssuerAltName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; /** * IssuerAltName * * @author Jim Wigginton */ abstract class IssuerAltName { const MAP = GeneralNames::MAP; } PK!S.y^^Pvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/IssuingDistributionPoint.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * IssuingDistributionPoint * * @author Jim Wigginton */ abstract class IssuingDistributionPoint { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'distributionPoint' => [ 'constant' => 0, 'optional' => true, 'explicit' => true ] + DistributionPointName::MAP, 'onlyContainsUserCerts' => [ 'type' => ASN1::TYPE_BOOLEAN, 'constant' => 1, 'optional' => true, 'default' => false, 'implicit' => true ], 'onlyContainsCACerts' => [ 'type' => ASN1::TYPE_BOOLEAN, 'constant' => 2, 'optional' => true, 'default' => false, 'implicit' => true ], 'onlySomeReasons' => [ 'constant' => 3, 'optional' => true, 'implicit' => true ] + ReasonFlags::MAP, 'indirectCRL' => [ 'type' => ASN1::TYPE_BOOLEAN, 'constant' => 4, 'optional' => true, 'default' => false, 'implicit' => true ], 'onlyContainsAttributeCerts' => [ 'type' => ASN1::TYPE_BOOLEAN, 'constant' => 5, 'optional' => true, 'default' => false, 'implicit' => true ] ] ]; } PK!;[Evendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyIdentifier.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * KeyIdentifier * * @author Jim Wigginton */ abstract class KeyIdentifier { const MAP = ['type' => ASN1::TYPE_OCTET_STRING]; } PK!npDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyPurposeId.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * KeyPurposeId * * @author Jim Wigginton */ abstract class KeyPurposeId { const MAP = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; } PK!$U  @vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyUsage.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * KeyUsage * * @author Jim Wigginton */ abstract class KeyUsage { const MAP = [ 'type' => ASN1::TYPE_BIT_STRING, 'mapping' => [ 'digitalSignature', 'nonRepudiation', 'keyEncipherment', 'dataEncipherment', 'keyAgreement', 'keyCertSign', 'cRLSign', 'encipherOnly', 'decipherOnly' ] ]; } PK!_Hvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/MaskGenAlgorithm.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; /** * MaskGenAglorithm * * @author Jim Wigginton */ abstract class MaskGenAlgorithm { const MAP = AlgorithmIdentifier::MAP; } PK!L Gvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NameConstraints.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * NameConstraints * * @author Jim Wigginton */ abstract class NameConstraints { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'permittedSubtrees' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + GeneralSubtrees::MAP, 'excludedSubtrees' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + GeneralSubtrees::MAP ] ]; } PK!In((<vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Name.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Name * * @author Jim Wigginton */ abstract class Name { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'rdnSequence' => RDNSequence::MAP ] ]; } PK!eNvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_ca_policy_url.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * netscape_ca_policy_url * * @author Jim Wigginton */ abstract class netscape_ca_policy_url { const MAP = ['type' => ASN1::TYPE_IA5_STRING]; } PK!{JJJvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_cert_type.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * netscape_cert_type * * mapping is from * * @author Jim Wigginton */ abstract class netscape_cert_type { const MAP = [ 'type' => ASN1::TYPE_BIT_STRING, 'mapping' => [ 'SSLClient', 'SSLServer', 'Email', 'ObjectSigning', 'Reserved', 'SSLCA', 'EmailCA', 'ObjectSigningCA' ] ]; } PK!Z?Hvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_comment.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * netscape_comment * * @author Jim Wigginton */ abstract class netscape_comment { const MAP = ['type' => ASN1::TYPE_IA5_STRING]; } PK!qt Fvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NetworkAddress.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * NetworkAddress * * @author Jim Wigginton */ abstract class NetworkAddress { const MAP = ['type' => ASN1::TYPE_NUMERIC_STRING]; } PK!͖""Gvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NoticeReference.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * NoticeReference * * @author Jim Wigginton */ abstract class NoticeReference { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'organization' => DisplayText::MAP, 'noticeNumbers' => [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => 200, 'children' => ['type' => ASN1::TYPE_INTEGER] ] ] ]; } PK!|ZkMvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NumericUserIdentifier.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * NumericUserIdentifier * * @author Jim Wigginton */ abstract class NumericUserIdentifier { const MAP = ['type' => ASN1::TYPE_NUMERIC_STRING]; } PK!WHvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OneAsymmetricKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * OneAsymmetricKey * * @author Jim Wigginton */ abstract class OneAsymmetricKey { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => ['v1', 'v2'] ], 'privateKeyAlgorithm' => AlgorithmIdentifier::MAP, 'privateKey' => PrivateKey::MAP, 'attributes' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + Attributes::MAP, 'publicKey' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + PublicKey::MAP ] ]; } PK!f{Avendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ORAddress.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ORAddress * * @author Jim Wigginton */ abstract class ORAddress { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'built-in-standard-attributes' => BuiltInStandardAttributes::MAP, 'built-in-domain-defined-attributes' => ['optional' => true] + BuiltInDomainDefinedAttributes::MAP, 'extension-attributes' => ['optional' => true] + ExtensionAttributes::MAP ] ]; } PK!w8uOvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OrganizationalUnitNames.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * OrganizationalUnitNames * * @author Jim Wigginton */ abstract class OrganizationalUnitNames { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => 4, // ub-organizational-units 'children' => ['type' => ASN1::TYPE_PRINTABLE_STRING] ]; } PK!ǓRHvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OrganizationName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * OrganizationName * * @author Jim Wigginton */ abstract class OrganizationName { const MAP = ['type' => ASN1::TYPE_PRINTABLE_STRING]; } PK!6RFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OtherPrimeInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * OtherPrimeInfo * * @author Jim Wigginton */ abstract class OtherPrimeInfo { // version must be multi if otherPrimeInfos present const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'prime' => ['type' => ASN1::TYPE_INTEGER], // ri 'exponent' => ['type' => ASN1::TYPE_INTEGER], // di 'coefficient' => ['type' => ASN1::TYPE_INTEGER] // ti ] ]; } PK!R|Gvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OtherPrimeInfos.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * OtherPrimeInfos * * @author Jim Wigginton */ abstract class OtherPrimeInfos { // version must be multi if otherPrimeInfos present const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => OtherPrimeInfo::MAP ]; } PK!%qDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBEParameter.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PBEParameter * * from https://tools.ietf.org/html/rfc2898#appendix-A.3 * * @author Jim Wigginton */ abstract class PBEParameter { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'salt' => ['type' => ASN1::TYPE_OCTET_STRING], 'iterationCount' => ['type' => ASN1::TYPE_INTEGER] ] ]; } PK![iDCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBES2params.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PBES2params * * from https://tools.ietf.org/html/rfc2898#appendix-A.3 * * @author Jim Wigginton */ abstract class PBES2params { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'keyDerivationFunc' => AlgorithmIdentifier::MAP, 'encryptionScheme' => AlgorithmIdentifier::MAP ] ]; } PK!ZDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBKDF2params.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PBKDF2params * * from https://tools.ietf.org/html/rfc2898#appendix-A.3 * * @author Jim Wigginton */ abstract class PBKDF2params { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ // technically, this is a CHOICE in RFC2898 but the other "choice" is, currently, more of a placeholder // in the RFC 'salt' => ['type' => ASN1::TYPE_OCTET_STRING], 'iterationCount' => ['type' => ASN1::TYPE_INTEGER], 'keyLength' => [ 'type' => ASN1::TYPE_INTEGER, 'optional' => true ], 'prf' => AlgorithmIdentifier::MAP + ['optional' => true] ] ]; } PK!SDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBMAC1params.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PBMAC1params * * from https://tools.ietf.org/html/rfc2898#appendix-A.3 * * @author Jim Wigginton */ abstract class PBMAC1params { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'keyDerivationFunc' => AlgorithmIdentifier::MAP, 'messageAuthScheme' => AlgorithmIdentifier::MAP ] ]; } PK!ǀCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Pentanomial.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Pentanomial * * @author Jim Wigginton */ abstract class Pentanomial { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'k1' => ['type' => ASN1::TYPE_INTEGER], // k1 > 0 'k2' => ['type' => ASN1::TYPE_INTEGER], // k2 > k1 'k3' => ['type' => ASN1::TYPE_INTEGER], // k3 > h2 ] ]; } PK!CCDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PersonalName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PersonalName * * @author Jim Wigginton */ abstract class PersonalName { const MAP = [ 'type' => ASN1::TYPE_SET, 'children' => [ 'surname' => [ 'type' => ASN1::TYPE_PRINTABLE_STRING, 'constant' => 0, 'optional' => true, 'implicit' => true ], 'given-name' => [ 'type' => ASN1::TYPE_PRINTABLE_STRING, 'constant' => 1, 'optional' => true, 'implicit' => true ], 'initials' => [ 'type' => ASN1::TYPE_PRINTABLE_STRING, 'constant' => 2, 'optional' => true, 'implicit' => true ], 'generation-qualifier' => [ 'type' => ASN1::TYPE_PRINTABLE_STRING, 'constant' => 3, 'optional' => true, 'implicit' => true ] ] ]; } PK!A2Z:Cvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PKCS9String.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PKCS9String * * @author Jim Wigginton */ abstract class PKCS9String { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'ia5String' => ['type' => ASN1::TYPE_IA5_STRING], 'directoryString' => DirectoryString::MAP ] ]; } PK! CMMIvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyInformation.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PolicyInformation * * @author Jim Wigginton */ abstract class PolicyInformation { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'policyIdentifier' => CertPolicyId::MAP, 'policyQualifiers' => [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 0, 'max' => -1, 'optional' => true, 'children' => PolicyQualifierInfo::MAP ] ] ]; } PK!^wFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyMappings.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PolicyMappings * * @author Jim Wigginton */ abstract class PolicyMappings { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'issuerDomainPolicy' => CertPolicyId::MAP, 'subjectDomainPolicy' => CertPolicyId::MAP ] ] ]; } PK!c5Ivendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyQualifierId.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PolicyQualifierId * * @author Jim Wigginton */ abstract class PolicyQualifierId { const MAP = ['type' => ASN1::TYPE_OBJECT_IDENTIFIER]; } PK!sމKvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyQualifierInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PolicyQualifierInfo * * @author Jim Wigginton */ abstract class PolicyQualifierInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'policyQualifierId' => PolicyQualifierId::MAP, 'qualifier' => ['type' => ASN1::TYPE_ANY] ] ]; } PK!M0eeEvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PostalAddress.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PostalAddress * * @author Jim Wigginton */ abstract class PostalAddress { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'optional' => true, 'min' => 1, 'max' => -1, 'children' => DirectoryString::MAP ]; } PK!^j?vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Prime_p.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Prime_p * * @author Jim Wigginton */ abstract class Prime_p { const MAP = ['type' => ASN1::TYPE_INTEGER]; } PK!DIvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateDomainName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PrivateDomainName * * @author Jim Wigginton */ abstract class PrivateDomainName { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'numeric' => ['type' => ASN1::TYPE_NUMERIC_STRING], 'printable' => ['type' => ASN1::TYPE_PRINTABLE_STRING] ] ]; } PK!ϒFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKeyInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PrivateKeyInfo * * @author Jim Wigginton */ abstract class PrivateKeyInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => ['v1'] ], 'privateKeyAlgorithm' => AlgorithmIdentifier::MAP, 'privateKey' => PrivateKey::MAP, 'attributes' => [ 'constant' => 0, 'optional' => true, 'implicit' => true ] + Attributes::MAP ] ]; } PK!'JBvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PrivateKey * * @author Jim Wigginton */ abstract class PrivateKey { const MAP = ['type' => ASN1::TYPE_OCTET_STRING]; } PK!$s}Mvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKeyUsagePeriod.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PrivateKeyUsagePeriod * * @author Jim Wigginton */ abstract class PrivateKeyUsagePeriod { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'notBefore' => [ 'constant' => 0, 'optional' => true, 'implicit' => true, 'type' => ASN1::TYPE_GENERALIZED_TIME], 'notAfter' => [ 'constant' => 1, 'optional' => true, 'implicit' => true, 'type' => ASN1::TYPE_GENERALIZED_TIME] ] ]; } PK!Mvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKeyAndChallenge.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PublicKeyAndChallenge * * @author Jim Wigginton */ abstract class PublicKeyAndChallenge { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'spki' => SubjectPublicKeyInfo::MAP, 'challenge' => ['type' => ASN1::TYPE_IA5_STRING] ] ]; } PK!ri--Evendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKeyInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PublicKeyInfo * * this format is not formally defined anywhere but is none-the-less the form you * get when you do "openssl rsa -in private.pem -outform PEM -pubout" * * @author Jim Wigginton */ abstract class PublicKeyInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'publicKeyAlgorithm' => AlgorithmIdentifier::MAP, 'publicKey' => ['type' => ASN1::TYPE_BIT_STRING] ] ]; } PK!nAvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * PublicKey * * @author Jim Wigginton */ abstract class PublicKey { const MAP = ['type' => ASN1::TYPE_BIT_STRING]; } PK!)Gvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RC2CBCParameter.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RC2CBCParameter * * from https://tools.ietf.org/html/rfc2898#appendix-A.3 * * @author Jim Wigginton */ abstract class RC2CBCParameter { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'rc2ParametersVersion' => [ 'type' => ASN1::TYPE_INTEGER, 'optional' => true ], 'iv' => ['type' => ASN1::TYPE_OCTET_STRING] ] ]; } PK!>paCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RDNSequence.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RDNSequence * * In practice, RDNs containing multiple name-value pairs (called "multivalued RDNs") are rare, * but they can be useful at times when either there is no unique attribute in the entry or you * want to ensure that the entry's DN contains some useful identifying information. * * - https://www.opends.org/wiki/page/DefinitionRelativeDistinguishedName * * @author Jim Wigginton */ abstract class RDNSequence { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, // RDNSequence does not define a min or a max, which means it doesn't have one 'min' => 0, 'max' => -1, 'children' => RelativeDistinguishedName::MAP ]; } PK!J*Cvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ReasonFlags.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * ReasonFlags * * @author Jim Wigginton */ abstract class ReasonFlags { const MAP = [ 'type' => ASN1::TYPE_BIT_STRING, 'mapping' => [ 'unused', 'keyCompromise', 'cACompromise', 'affiliationChanged', 'superseded', 'cessationOfOperation', 'certificateHold', 'privilegeWithdrawn', 'aACompromise' ] ]; } PK!hQvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RelativeDistinguishedName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RelativeDistinguishedName * * In practice, RDNs containing multiple name-value pairs (called "multivalued RDNs") are rare, * but they can be useful at times when either there is no unique attribute in the entry or you * want to ensure that the entry's DN contains some useful identifying information. * * - https://www.opends.org/wiki/page/DefinitionRelativeDistinguishedName * * @author Jim Wigginton */ abstract class RelativeDistinguishedName { const MAP = [ 'type' => ASN1::TYPE_SET, 'min' => 1, 'max' => -1, 'children' => AttributeTypeAndValue::MAP ]; } PK!b78Jvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RevokedCertificate.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RevokedCertificate * * @author Jim Wigginton */ abstract class RevokedCertificate { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'userCertificate' => CertificateSerialNumber::MAP, 'revocationDate' => Time::MAP, 'crlEntryExtensions' => [ 'optional' => true ] + Extensions::MAP ] ]; } PK!xRwEvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSAPrivateKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RSAPrivateKey * * @author Jim Wigginton */ abstract class RSAPrivateKey { // version must be multi if otherPrimeInfos present const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => ['two-prime', 'multi'] ], 'modulus' => ['type' => ASN1::TYPE_INTEGER], // n 'publicExponent' => ['type' => ASN1::TYPE_INTEGER], // e 'privateExponent' => ['type' => ASN1::TYPE_INTEGER], // d 'prime1' => ['type' => ASN1::TYPE_INTEGER], // p 'prime2' => ['type' => ASN1::TYPE_INTEGER], // q 'exponent1' => ['type' => ASN1::TYPE_INTEGER], // d mod (p-1) 'exponent2' => ['type' => ASN1::TYPE_INTEGER], // d mod (q-1) 'coefficient' => ['type' => ASN1::TYPE_INTEGER], // (inverse of q) mod p 'otherPrimeInfos' => OtherPrimeInfos::MAP + ['optional' => true] ] ]; } PK!"PDvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSAPublicKey.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RSAPublicKey * * @author Jim Wigginton */ abstract class RSAPublicKey { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'modulus' => ['type' => ASN1::TYPE_INTEGER], 'publicExponent' => ['type' => ASN1::TYPE_INTEGER] ] ]; } PK!XQIvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * RSASSA_PSS_params * * @author Jim Wigginton */ abstract class RSASSA_PSS_params { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'hashAlgorithm' => [ 'constant' => 0, 'optional' => true, 'explicit' => true, //'default' => 'sha1Identifier' ] + HashAlgorithm::MAP, 'maskGenAlgorithm' => [ 'constant' => 1, 'optional' => true, 'explicit' => true, //'default' => 'mgf1SHA1Identifier' ] + MaskGenAlgorithm::MAP, 'saltLength' => [ 'type' => ASN1::TYPE_INTEGER, 'constant' => 2, 'optional' => true, 'explicit' => true, 'default' => 20 ], 'trailerField' => [ 'type' => ASN1::TYPE_INTEGER, 'constant' => 3, 'optional' => true, 'explicit' => true, 'default' => 1 ] ] ]; } PK!ZPSvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SignedPublicKeyAndChallenge.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * SignedPublicKeyAndChallenge * * @author Jim Wigginton */ abstract class SignedPublicKeyAndChallenge { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'publicKeyAndChallenge' => PublicKeyAndChallenge::MAP, 'signatureAlgorithm' => AlgorithmIdentifier::MAP, 'signature' => ['type' => ASN1::TYPE_BIT_STRING] ] ]; } PK!RLOXXIvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SpecifiedECDomain.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * SpecifiedECDomain * * @author Jim Wigginton */ abstract class SpecifiedECDomain { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => [1 => 'ecdpVer1', 'ecdpVer2', 'ecdpVer3'] ], 'fieldID' => FieldID::MAP, 'curve' => Curve::MAP, 'base' => ECPoint::MAP, 'order' => ['type' => ASN1::TYPE_INTEGER], 'cofactor' => [ 'type' => ASN1::TYPE_INTEGER, 'optional' => true ], 'hash' => ['optional' => true] + HashAlgorithm::MAP ] ]; } PK!OFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectAltName.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; /** * SubjectAltName * * @author Jim Wigginton */ abstract class SubjectAltName { const MAP = GeneralNames::MAP; } PK![jjRvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectDirectoryAttributes.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * SubjectDirectoryAttributes * * @author Jim Wigginton */ abstract class SubjectDirectoryAttributes { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => Attribute::MAP ]; } PK!ZWWFvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TBSCertificate.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * TBSCertificate * * @author Jim Wigginton */ abstract class TBSCertificate { // assert($TBSCertificate['children']['signature'] == $Certificate['children']['signatureAlgorithm']) const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ // technically, default implies optional, but we'll define it as being optional, none-the-less, just to // reenforce that fact 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'constant' => 0, 'optional' => true, 'explicit' => true, 'mapping' => ['v1', 'v2', 'v3'], 'default' => 'v1' ], 'serialNumber' => CertificateSerialNumber::MAP, 'signature' => AlgorithmIdentifier::MAP, 'issuer' => Name::MAP, 'validity' => Validity::MAP, 'subject' => Name::MAP, 'subjectPublicKeyInfo' => SubjectPublicKeyInfo::MAP, // implicit means that the T in the TLV structure is to be rewritten, regardless of the type 'issuerUniqueID' => [ 'constant' => 1, 'optional' => true, 'implicit' => true ] + UniqueIdentifier::MAP, 'subjectUniqueID' => [ 'constant' => 2, 'optional' => true, 'implicit' => true ] + UniqueIdentifier::MAP, // doesn't use the EXPLICIT keyword but if // it's not IMPLICIT, it's EXPLICIT 'extensions' => [ 'constant' => 3, 'optional' => true, 'explicit' => true ] + Extensions::MAP ] ]; } PK!ǔ4iiOvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectInfoAccessSyntax.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * SubjectInfoAccessSyntax * * @author Jim Wigginton */ abstract class SubjectInfoAccessSyntax { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'min' => 1, 'max' => -1, 'children' => AccessDescription::MAP ]; } PK!]9Lvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectPublicKeyInfo.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * SubjectPublicKeyInfo * * @author Jim Wigginton */ abstract class SubjectPublicKeyInfo { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'algorithm' => AlgorithmIdentifier::MAP, 'subjectPublicKey' => ['type' => ASN1::TYPE_BIT_STRING] ] ]; } PK!~fVVCvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TBSCertList.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * TBSCertList * * @author Jim Wigginton */ abstract class TBSCertList { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'version' => [ 'type' => ASN1::TYPE_INTEGER, 'mapping' => ['v1', 'v2'], 'optional' => true, 'default' => 'v1' ], 'signature' => AlgorithmIdentifier::MAP, 'issuer' => Name::MAP, 'thisUpdate' => Time::MAP, 'nextUpdate' => [ 'optional' => true ] + Time::MAP, 'revokedCertificates' => [ 'type' => ASN1::TYPE_SEQUENCE, 'optional' => true, 'min' => 0, 'max' => -1, 'children' => RevokedCertificate::MAP ], 'crlExtensions' => [ 'constant' => 0, 'optional' => true, 'explicit' => true ] + Extensions::MAP ] ]; } PK!ȴJvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TerminalIdentifier.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * TerminalIdentifier * * @author Jim Wigginton */ abstract class TerminalIdentifier { const MAP = ['type' => ASN1::TYPE_PRINTABLE_STRING]; } PK!yy<vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Time.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Time * * @author Jim Wigginton */ abstract class Time { const MAP = [ 'type' => ASN1::TYPE_CHOICE, 'children' => [ 'utcTime' => ['type' => ASN1::TYPE_UTC_TIME], 'generalTime' => ['type' => ASN1::TYPE_GENERALIZED_TIME] ] ]; } PK!0Avendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Trinomial.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Trinomial * * @author Jim Wigginton */ abstract class Trinomial { const MAP = ['type' => ASN1::TYPE_INTEGER]; } PK!Bʑ7Hvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/UniqueIdentifier.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * UniqueIdentifier * * @author Jim Wigginton */ abstract class UniqueIdentifier { const MAP = ['type' => ASN1::TYPE_BIT_STRING]; } PK!mXA  Bvendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/UserNotice.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * UserNotice * * @author Jim Wigginton */ abstract class UserNotice { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'noticeRef' => [ 'optional' => true, 'implicit' => true ] + NoticeReference::MAP, 'explicitText' => [ 'optional' => true, 'implicit' => true ] + DisplayText::MAP ] ]; } PK!PnRR@vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Validity.phpnu[ * @copyright 2016 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1\Maps; use phpseclib3\File\ASN1; /** * Validity * * @author Jim Wigginton */ abstract class Validity { const MAP = [ 'type' => ASN1::TYPE_SEQUENCE, 'children' => [ 'notBefore' => Time::MAP, 'notAfter' => Time::MAP ] ]; } PK!I/44:vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Element.phpnu[ * @copyright 2012 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File\ASN1; /** * ASN.1 Raw Element * * An ASN.1 ANY mapping will return an ASN1\Element object. Use of this object * will also bypass the normal encoding rules in ASN1::encodeDER() * * @author Jim Wigginton */ class Element { /** * Raw element value * * @var string */ public $element; /** * Constructor * * @param string $encoded * @return Element */ public function __construct($encoded) { $this->element = $encoded; } } PK!O\ N N2vendor/phpseclib/phpseclib/phpseclib/File/ANSI.phpnu[ * @copyright 2012 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File; /** * Pure-PHP ANSI Decoder * * @author Jim Wigginton */ class ANSI { /** * Max Width * * @var int */ private $max_x; /** * Max Height * * @var int */ private $max_y; /** * Max History * * @var int */ private $max_history; /** * History * * @var array */ private $history; /** * History Attributes * * @var array */ private $history_attrs; /** * Current Column * * @var int */ private $x; /** * Current Row * * @var int */ private $y; /** * Old Column * * @var int */ private $old_x; /** * Old Row * * @var int */ private $old_y; /** * An empty attribute cell * * @var object */ private $base_attr_cell; /** * The current attribute cell * * @var object */ private $attr_cell; /** * An empty attribute row * * @var array */ private $attr_row; /** * The current screen text * * @var list */ private $screen; /** * The current screen attributes * * @var array */ private $attrs; /** * Current ANSI code * * @var string */ private $ansi; /** * Tokenization * * @var array */ private $tokenization; /** * Default Constructor. * * @return ANSI */ public function __construct() { $attr_cell = new \stdClass(); $attr_cell->bold = false; $attr_cell->underline = false; $attr_cell->blink = false; $attr_cell->background = 'black'; $attr_cell->foreground = 'white'; $attr_cell->reverse = false; $this->base_attr_cell = clone $attr_cell; $this->attr_cell = clone $attr_cell; $this->setHistory(200); $this->setDimensions(80, 24); } /** * Set terminal width and height * * Resets the screen as well * * @param int $x * @param int $y */ public function setDimensions($x, $y) { $this->max_x = $x - 1; $this->max_y = $y - 1; $this->x = $this->y = 0; $this->history = $this->history_attrs = []; $this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell); $this->screen = array_fill(0, $this->max_y + 1, ''); $this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row); $this->ansi = ''; } /** * Set the number of lines that should be logged past the terminal height * * @param int $history */ public function setHistory($history) { $this->max_history = $history; } /** * Load a string * * @param string $source */ public function loadString($source) { $this->setDimensions($this->max_x + 1, $this->max_y + 1); $this->appendString($source); } /** * Appdend a string * * @param string $source */ public function appendString($source) { $this->tokenization = ['']; for ($i = 0; $i < strlen($source); $i++) { if (strlen($this->ansi)) { $this->ansi .= $source[$i]; $chr = ord($source[$i]); // http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements // single character CSI's not currently supported switch (true) { case $this->ansi == "\x1B=": $this->ansi = ''; continue 2; case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['): case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126: break; default: continue 2; } $this->tokenization[] = $this->ansi; $this->tokenization[] = ''; // http://ascii-table.com/ansi-escape-sequences-vt-100.php switch ($this->ansi) { case "\x1B[H": // Move cursor to upper left corner $this->old_x = $this->x; $this->old_y = $this->y; $this->x = $this->y = 0; break; case "\x1B[J": // Clear screen from cursor down $this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y)); $this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, '')); $this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y)); $this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row)); if (count($this->history) == $this->max_history) { array_shift($this->history); array_shift($this->history_attrs); } // fall-through case "\x1B[K": // Clear screen from cursor right $this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x); array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell)); break; case "\x1B[2K": // Clear entire line $this->screen[$this->y] = str_repeat(' ', $this->x); $this->attrs[$this->y] = $this->attr_row; break; case "\x1B[?1h": // set cursor key to application case "\x1B[?25h": // show the cursor case "\x1B(B": // set united states g0 character set break; case "\x1BE": // Move to next line $this->newLine(); $this->x = 0; break; default: switch (true) { case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines $this->old_y = $this->y; $this->y += (int) $match[1]; break; case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h $this->old_x = $this->x; $this->old_y = $this->y; $this->x = $match[2] - 1; $this->y = (int) $match[1] - 1; break; case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines $this->old_x = $this->x; $this->x += $match[1]; break; case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines $this->old_x = $this->x; $this->x -= $match[1]; if ($this->x < 0) { $this->x = 0; } break; case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window break; case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes $attr_cell = &$this->attr_cell; $mods = explode(';', $match[1]); foreach ($mods as $mod) { switch ($mod) { case '': case '0': // Turn off character attributes $attr_cell = clone $this->base_attr_cell; break; case '1': // Turn bold mode on $attr_cell->bold = true; break; case '4': // Turn underline mode on $attr_cell->underline = true; break; case '5': // Turn blinking mode on $attr_cell->blink = true; break; case '7': // Turn reverse video on $attr_cell->reverse = !$attr_cell->reverse; $temp = $attr_cell->background; $attr_cell->background = $attr_cell->foreground; $attr_cell->foreground = $temp; break; default: // set colors //$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground; $front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' }; //$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background; $back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' }; switch ($mod) { // @codingStandardsIgnoreStart case '30': $front = 'black'; break; case '31': $front = 'red'; break; case '32': $front = 'green'; break; case '33': $front = 'yellow'; break; case '34': $front = 'blue'; break; case '35': $front = 'magenta'; break; case '36': $front = 'cyan'; break; case '37': $front = 'white'; break; case '40': $back = 'black'; break; case '41': $back = 'red'; break; case '42': $back = 'green'; break; case '43': $back = 'yellow'; break; case '44': $back = 'blue'; break; case '45': $back = 'magenta'; break; case '46': $back = 'cyan'; break; case '47': $back = 'white'; break; // @codingStandardsIgnoreEnd default: //user_error('Unsupported attribute: ' . $mod); $this->ansi = ''; break 2; } } } break; default: //user_error("{$this->ansi} is unsupported\r\n"); } } $this->ansi = ''; continue; } $this->tokenization[count($this->tokenization) - 1] .= $source[$i]; switch ($source[$i]) { case "\r": $this->x = 0; break; case "\n": $this->newLine(); break; case "\x08": // backspace if ($this->x) { $this->x--; $this->attrs[$this->y][$this->x] = clone $this->base_attr_cell; $this->screen[$this->y] = substr_replace( $this->screen[$this->y], $source[$i], $this->x, 1 ); } break; case "\x0F": // shift break; case "\x1B": // start ANSI escape code $this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1); //if (!strlen($this->tokenization[count($this->tokenization) - 1])) { // array_pop($this->tokenization); //} $this->ansi .= "\x1B"; break; default: $this->attrs[$this->y][$this->x] = clone $this->attr_cell; if ($this->x > strlen($this->screen[$this->y])) { $this->screen[$this->y] = str_repeat(' ', $this->x); } $this->screen[$this->y] = substr_replace( $this->screen[$this->y], $source[$i], $this->x, 1 ); if ($this->x > $this->max_x) { $this->x = 0; $this->newLine(); } else { $this->x++; } } } } /** * Add a new line * * Also update the $this->screen and $this->history buffers * */ private function newLine() { //if ($this->y < $this->max_y) { // $this->y++; //} while ($this->y >= $this->max_y) { $this->history = array_merge($this->history, [array_shift($this->screen)]); $this->screen[] = ''; $this->history_attrs = array_merge($this->history_attrs, [array_shift($this->attrs)]); $this->attrs[] = $this->attr_row; if (count($this->history) >= $this->max_history) { array_shift($this->history); array_shift($this->history_attrs); } $this->y--; } $this->y++; } /** * Returns the current coordinate without preformating * * @param \stdClass $last_attr * @param \stdClass $cur_attr * @param string $char * @return string */ private function processCoordinate(\stdClass $last_attr, \stdClass $cur_attr, $char) { $output = ''; if ($last_attr != $cur_attr) { $close = $open = ''; if ($last_attr->foreground != $cur_attr->foreground) { if ($cur_attr->foreground != 'white') { $open .= ''; } if ($last_attr->foreground != 'white') { $close = '' . $close; } } if ($last_attr->background != $cur_attr->background) { if ($cur_attr->background != 'black') { $open .= ''; } if ($last_attr->background != 'black') { $close = '' . $close; } } if ($last_attr->bold != $cur_attr->bold) { if ($cur_attr->bold) { $open .= ''; } else { $close = '' . $close; } } if ($last_attr->underline != $cur_attr->underline) { if ($cur_attr->underline) { $open .= ''; } else { $close = '' . $close; } } if ($last_attr->blink != $cur_attr->blink) { if ($cur_attr->blink) { $open .= ''; } else { $close = '' . $close; } } $output .= $close . $open; } $output .= htmlspecialchars($char); return $output; } /** * Returns the current screen without preformating * * @return string */ private function getScreenHelper() { $output = ''; $last_attr = $this->base_attr_cell; for ($i = 0; $i <= $this->max_y; $i++) { for ($j = 0; $j <= $this->max_x; $j++) { $cur_attr = $this->attrs[$i][$j]; $output .= $this->processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : ''); $last_attr = $this->attrs[$i][$j]; } $output .= "\r\n"; } $output = substr($output, 0, -2); // close any remaining open tags $output .= $this->processCoordinate($last_attr, $this->base_attr_cell, ''); return rtrim($output); } /** * Returns the current screen * * @return string */ public function getScreen() { return '
' . $this->getScreenHelper() . '
'; } /** * Returns the current screen and the x previous lines * * @return string */ public function getHistory() { $scrollback = ''; $last_attr = $this->base_attr_cell; for ($i = 0; $i < count($this->history); $i++) { for ($j = 0; $j <= $this->max_x + 1; $j++) { $cur_attr = $this->history_attrs[$i][$j]; $scrollback .= $this->processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : ''); $last_attr = $this->history_attrs[$i][$j]; } $scrollback .= "\r\n"; } $base_attr_cell = $this->base_attr_cell; $this->base_attr_cell = $last_attr; $scrollback .= $this->getScreen(); $this->base_attr_cell = $base_attr_cell; return '
' . $scrollback . '
'; } } PK!2vendor/phpseclib/phpseclib/phpseclib/File/ASN1.phpnu[ * @copyright 2012 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File; use phpseclib3\Common\Functions\Strings; use phpseclib3\File\ASN1\Element; use phpseclib3\Math\BigInteger; /** * Pure-PHP ASN.1 Parser * * @author Jim Wigginton */ abstract class ASN1 { // Tag Classes // http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=12 const CLASS_UNIVERSAL = 0; const CLASS_APPLICATION = 1; const CLASS_CONTEXT_SPECIFIC = 2; const CLASS_PRIVATE = 3; // Tag Classes // http://www.obj-sys.com/asn1tutorial/node124.html const TYPE_BOOLEAN = 1; const TYPE_INTEGER = 2; const TYPE_BIT_STRING = 3; const TYPE_OCTET_STRING = 4; const TYPE_NULL = 5; const TYPE_OBJECT_IDENTIFIER = 6; //const TYPE_OBJECT_DESCRIPTOR = 7; //const TYPE_INSTANCE_OF = 8; // EXTERNAL const TYPE_REAL = 9; const TYPE_ENUMERATED = 10; //const TYPE_EMBEDDED = 11; const TYPE_UTF8_STRING = 12; //const TYPE_RELATIVE_OID = 13; const TYPE_SEQUENCE = 16; // SEQUENCE OF const TYPE_SET = 17; // SET OF // More Tag Classes // http://www.obj-sys.com/asn1tutorial/node10.html const TYPE_NUMERIC_STRING = 18; const TYPE_PRINTABLE_STRING = 19; const TYPE_TELETEX_STRING = 20; // T61String const TYPE_VIDEOTEX_STRING = 21; const TYPE_IA5_STRING = 22; const TYPE_UTC_TIME = 23; const TYPE_GENERALIZED_TIME = 24; const TYPE_GRAPHIC_STRING = 25; const TYPE_VISIBLE_STRING = 26; // ISO646String const TYPE_GENERAL_STRING = 27; const TYPE_UNIVERSAL_STRING = 28; //const TYPE_CHARACTER_STRING = 29; const TYPE_BMP_STRING = 30; // Tag Aliases // These tags are kinda place holders for other tags. const TYPE_CHOICE = -1; const TYPE_ANY = -2; /** * ASN.1 object identifiers * * @var array * @link http://en.wikipedia.org/wiki/Object_identifier */ private static $oids = []; /** * ASN.1 object identifier reverse mapping * * @var array */ private static $reverseOIDs = []; /** * Default date format * * @var string * @link http://php.net/class.datetime */ private static $format = 'D, d M Y H:i:s O'; /** * Filters * * If the mapping type is self::TYPE_ANY what do we actually encode it as? * * @var array * @see self::encode_der() */ private static $filters; /** * Current Location of most recent ASN.1 encode process * * Useful for debug purposes * * @var array * @see self::encode_der() */ private static $location; /** * DER Encoded String * * In case we need to create ASN1\Element object's.. * * @var string * @see self::decodeDER() */ private static $encoded; /** * Type mapping table for the ANY type. * * Structured or unknown types are mapped to a \phpseclib3\File\ASN1\Element. * Unambiguous types get the direct mapping (int/real/bool). * Others are mapped as a choice, with an extra indexing level. * * @var array */ const ANY_MAP = [ self::TYPE_BOOLEAN => true, self::TYPE_INTEGER => true, self::TYPE_BIT_STRING => 'bitString', self::TYPE_OCTET_STRING => 'octetString', self::TYPE_NULL => 'null', self::TYPE_OBJECT_IDENTIFIER => 'objectIdentifier', self::TYPE_REAL => true, self::TYPE_ENUMERATED => 'enumerated', self::TYPE_UTF8_STRING => 'utf8String', self::TYPE_NUMERIC_STRING => 'numericString', self::TYPE_PRINTABLE_STRING => 'printableString', self::TYPE_TELETEX_STRING => 'teletexString', self::TYPE_VIDEOTEX_STRING => 'videotexString', self::TYPE_IA5_STRING => 'ia5String', self::TYPE_UTC_TIME => 'utcTime', self::TYPE_GENERALIZED_TIME => 'generalTime', self::TYPE_GRAPHIC_STRING => 'graphicString', self::TYPE_VISIBLE_STRING => 'visibleString', self::TYPE_GENERAL_STRING => 'generalString', self::TYPE_UNIVERSAL_STRING => 'universalString', //self::TYPE_CHARACTER_STRING => 'characterString', self::TYPE_BMP_STRING => 'bmpString' ]; /** * String type to character size mapping table. * * Non-convertable types are absent from this table. * size == 0 indicates variable length encoding. * * @var array */ const STRING_TYPE_SIZE = [ self::TYPE_UTF8_STRING => 0, self::TYPE_BMP_STRING => 2, self::TYPE_UNIVERSAL_STRING => 4, self::TYPE_PRINTABLE_STRING => 1, self::TYPE_TELETEX_STRING => 1, self::TYPE_IA5_STRING => 1, self::TYPE_VISIBLE_STRING => 1, ]; /** * Parse BER-encoding * * Serves a similar purpose to openssl's asn1parse * * @param Element|string $encoded * @return ?array */ public static function decodeBER($encoded) { if ($encoded instanceof Element) { $encoded = $encoded->element; } self::$encoded = $encoded; $decoded = self::decode_ber($encoded); if ($decoded === false) { return null; } return [$decoded]; } /** * Parse BER-encoding (Helper function) * * Sometimes we want to get the BER encoding of a particular tag. $start lets us do that without having to reencode. * $encoded is passed by reference for the recursive calls done for self::TYPE_BIT_STRING and * self::TYPE_OCTET_STRING. In those cases, the indefinite length is used. * * @param string $encoded * @param int $start * @param int $encoded_pos * @return array|bool */ private static function decode_ber($encoded, $start = 0, $encoded_pos = 0) { $current = ['start' => $start]; if (!isset($encoded[$encoded_pos])) { return false; } $type = ord($encoded[$encoded_pos++]); $startOffset = 1; $constructed = ($type >> 5) & 1; $tag = $type & 0x1F; if ($tag == 0x1F) { $tag = 0; // process septets (since the eighth bit is ignored, it's not an octet) do { if (!isset($encoded[$encoded_pos])) { return false; } $temp = ord($encoded[$encoded_pos++]); $startOffset++; $loop = $temp >> 7; $tag <<= 7; $temp &= 0x7F; // "bits 7 to 1 of the first subsequent octet shall not all be zero" if ($startOffset == 2 && $temp == 0) { return false; } $tag |= $temp; } while ($loop); } $start += $startOffset; // Length, as discussed in paragraph 8.1.3 of X.690-0207.pdf#page=13 if (!isset($encoded[$encoded_pos])) { return false; } $length = ord($encoded[$encoded_pos++]); $start++; if ($length == 0x80) { // indefinite length // "[A sender shall] use the indefinite form (see 8.1.3.6) if the encoding is constructed and is not all // immediately available." -- paragraph 8.1.3.2.c $length = strlen($encoded) - $encoded_pos; } elseif ($length & 0x80) { // definite length, long form // technically, the long form of the length can be represented by up to 126 octets (bytes), but we'll only // support it up to four. $length &= 0x7F; $temp = substr($encoded, $encoded_pos, $length); $encoded_pos += $length; // tags of indefinte length don't really have a header length; this length includes the tag $current += ['headerlength' => $length + 2]; $start += $length; extract(unpack('Nlength', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4))); /** @var integer $length */ } else { $current += ['headerlength' => 2]; } if ($length > (strlen($encoded) - $encoded_pos)) { return false; } $content = substr($encoded, $encoded_pos, $length); $content_pos = 0; // at this point $length can be overwritten. it's only accurate for definite length things as is /* Class is UNIVERSAL, APPLICATION, PRIVATE, or CONTEXT-SPECIFIC. The UNIVERSAL class is restricted to the ASN.1 built-in types. It defines an application-independent data type that must be distinguishable from all other data types. The other three classes are user defined. The APPLICATION class distinguishes data types that have a wide, scattered use within a particular presentation context. PRIVATE distinguishes data types within a particular organization or country. CONTEXT-SPECIFIC distinguishes members of a sequence or set, the alternatives of a CHOICE, or universally tagged set members. Only the class number appears in braces for this data type; the term CONTEXT-SPECIFIC does not appear. -- http://www.obj-sys.com/asn1tutorial/node12.html */ $class = ($type >> 6) & 3; switch ($class) { case self::CLASS_APPLICATION: case self::CLASS_PRIVATE: case self::CLASS_CONTEXT_SPECIFIC: if (!$constructed) { return [ 'type' => $class, 'constant' => $tag, 'content' => $content, 'length' => $length + $start - $current['start'] ] + $current; } $newcontent = []; $remainingLength = $length; while ($remainingLength > 0) { $temp = self::decode_ber($content, $start, $content_pos); if ($temp === false) { break; } $length = $temp['length']; // end-of-content octets - see paragraph 8.1.5 if (substr($content, $content_pos + $length, 2) == "\0\0") { $length += 2; $start += $length; $newcontent[] = $temp; break; } $start += $length; $remainingLength -= $length; $newcontent[] = $temp; $content_pos += $length; } return [ 'type' => $class, 'constant' => $tag, // the array encapsulation is for BC with the old format 'content' => $newcontent, // the only time when $content['headerlength'] isn't defined is when the length is indefinite. // the absence of $content['headerlength'] is how we know if something is indefinite or not. // technically, it could be defined to be 2 and then another indicator could be used but whatever. 'length' => $start - $current['start'] ] + $current; } $current += ['type' => $tag]; // decode UNIVERSAL tags switch ($tag) { case self::TYPE_BOOLEAN: // "The contents octets shall consist of a single octet." -- paragraph 8.2.1 if ($constructed || strlen($content) != 1) { return false; } $current['content'] = (bool) ord($content[$content_pos]); break; case self::TYPE_INTEGER: case self::TYPE_ENUMERATED: if ($constructed) { return false; } $current['content'] = new BigInteger(substr($content, $content_pos), -256); break; case self::TYPE_REAL: // not currently supported return false; case self::TYPE_BIT_STRING: // The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit, // the number of unused bits in the final subsequent octet. The number shall be in the range zero to // seven. if (!$constructed) { $current['content'] = substr($content, $content_pos); } else { $temp = self::decode_ber($content, $start, $content_pos); if ($temp === false) { return false; } $length -= (strlen($content) - $content_pos); $last = count($temp) - 1; for ($i = 0; $i < $last; $i++) { // all subtags should be bit strings if ($temp[$i]['type'] != self::TYPE_BIT_STRING) { return false; } $current['content'] .= substr($temp[$i]['content'], 1); } // all subtags should be bit strings if ($temp[$last]['type'] != self::TYPE_BIT_STRING) { return false; } $current['content'] = $temp[$last]['content'][0] . $current['content'] . substr($temp[$i]['content'], 1); } break; case self::TYPE_OCTET_STRING: if (!$constructed) { $current['content'] = substr($content, $content_pos); } else { $current['content'] = ''; $length = 0; while (substr($content, $content_pos, 2) != "\0\0") { $temp = self::decode_ber($content, $length + $start, $content_pos); if ($temp === false) { return false; } $content_pos += $temp['length']; // all subtags should be octet strings if ($temp['type'] != self::TYPE_OCTET_STRING) { return false; } $current['content'] .= $temp['content']; $length += $temp['length']; } if (substr($content, $content_pos, 2) == "\0\0") { $length += 2; // +2 for the EOC } } break; case self::TYPE_NULL: // "The contents octets shall not contain any octets." -- paragraph 8.8.2 if ($constructed || strlen($content)) { return false; } break; case self::TYPE_SEQUENCE: case self::TYPE_SET: if (!$constructed) { return false; } $offset = 0; $current['content'] = []; $content_len = strlen($content); while ($content_pos < $content_len) { // if indefinite length construction was used and we have an end-of-content string next // see paragraphs 8.1.1.3, 8.1.3.2, 8.1.3.6, 8.1.5, and (for an example) 8.6.4.2 if (!isset($current['headerlength']) && substr($content, $content_pos, 2) == "\0\0") { $length = $offset + 2; // +2 for the EOC break 2; } $temp = self::decode_ber($content, $start + $offset, $content_pos); if ($temp === false) { return false; } $content_pos += $temp['length']; $current['content'][] = $temp; $offset += $temp['length']; } break; case self::TYPE_OBJECT_IDENTIFIER: if ($constructed) { return false; } $current['content'] = self::decodeOID(substr($content, $content_pos)); if ($current['content'] === false) { return false; } break; /* Each character string type shall be encoded as if it had been declared: [UNIVERSAL x] IMPLICIT OCTET STRING -- X.690-0207.pdf#page=23 (paragraph 8.21.3) Per that, we're not going to do any validation. If there are any illegal characters in the string, we don't really care */ case self::TYPE_NUMERIC_STRING: // 0,1,2,3,4,5,6,7,8,9, and space case self::TYPE_PRINTABLE_STRING: // Upper and lower case letters, digits, space, apostrophe, left/right parenthesis, plus sign, comma, // hyphen, full stop, solidus, colon, equal sign, question mark case self::TYPE_TELETEX_STRING: // The Teletex character set in CCITT's T61, space, and delete // see http://en.wikipedia.org/wiki/Teletex#Character_sets case self::TYPE_VIDEOTEX_STRING: // The Videotex character set in CCITT's T.100 and T.101, space, and delete case self::TYPE_VISIBLE_STRING: // Printing character sets of international ASCII, and space case self::TYPE_IA5_STRING: // International Alphabet 5 (International ASCII) case self::TYPE_GRAPHIC_STRING: // All registered G sets, and space case self::TYPE_GENERAL_STRING: // All registered C and G sets, space and delete case self::TYPE_UTF8_STRING: // ???? case self::TYPE_BMP_STRING: if ($constructed) { return false; } $current['content'] = substr($content, $content_pos); break; case self::TYPE_UTC_TIME: case self::TYPE_GENERALIZED_TIME: if ($constructed) { return false; } $current['content'] = self::decodeTime(substr($content, $content_pos), $tag); break; default: return false; } $start += $length; // ie. length is the length of the full TLV encoding - it's not just the length of the value return $current + ['length' => $start - $current['start']]; } /** * ASN.1 Map * * Provides an ASN.1 semantic mapping ($mapping) from a parsed BER-encoding to a human readable format. * * "Special" mappings may be applied on a per tag-name basis via $special. * * @param array $decoded * @param array $mapping * @param array $special * @return array|bool|Element|string|null */ public static function asn1map(array $decoded, $mapping, $special = []) { if (isset($mapping['explicit']) && is_array($decoded['content'])) { $decoded = $decoded['content'][0]; } switch (true) { case $mapping['type'] == self::TYPE_ANY: $intype = $decoded['type']; // !isset(self::ANY_MAP[$intype]) produces a fatal error on PHP 5.6 if (isset($decoded['constant']) || !array_key_exists($intype, self::ANY_MAP) || (ord(self::$encoded[$decoded['start']]) & 0x20)) { return new Element(substr(self::$encoded, $decoded['start'], $decoded['length'])); } $inmap = self::ANY_MAP[$intype]; if (is_string($inmap)) { return [$inmap => self::asn1map($decoded, ['type' => $intype] + $mapping, $special)]; } break; case $mapping['type'] == self::TYPE_CHOICE: foreach ($mapping['children'] as $key => $option) { switch (true) { case isset($option['constant']) && $option['constant'] == $decoded['constant']: case !isset($option['constant']) && $option['type'] == $decoded['type']: $value = self::asn1map($decoded, $option, $special); break; case !isset($option['constant']) && $option['type'] == self::TYPE_CHOICE: $v = self::asn1map($decoded, $option, $special); if (isset($v)) { $value = $v; } } if (isset($value)) { if (isset($special[$key])) { $value = $special[$key]($value); } return [$key => $value]; } } return null; case isset($mapping['implicit']): case isset($mapping['explicit']): case $decoded['type'] == $mapping['type']: break; default: // if $decoded['type'] and $mapping['type'] are both strings, but different types of strings, // let it through switch (true) { case $decoded['type'] < 18: // self::TYPE_NUMERIC_STRING == 18 case $decoded['type'] > 30: // self::TYPE_BMP_STRING == 30 case $mapping['type'] < 18: case $mapping['type'] > 30: return null; } } if (isset($mapping['implicit'])) { $decoded['type'] = $mapping['type']; } switch ($decoded['type']) { case self::TYPE_SEQUENCE: $map = []; // ignore the min and max if (isset($mapping['min']) && isset($mapping['max'])) { $child = $mapping['children']; foreach ($decoded['content'] as $content) { if (($map[] = self::asn1map($content, $child, $special)) === null) { return null; } } return $map; } $n = count($decoded['content']); $i = 0; foreach ($mapping['children'] as $key => $child) { $maymatch = $i < $n; // Match only existing input. if ($maymatch) { $temp = $decoded['content'][$i]; if ($child['type'] != self::TYPE_CHOICE) { // Get the mapping and input class & constant. $childClass = $tempClass = self::CLASS_UNIVERSAL; $constant = null; if (isset($temp['constant'])) { $tempClass = $temp['type']; } if (isset($child['class'])) { $childClass = $child['class']; $constant = $child['cast']; } elseif (isset($child['constant'])) { $childClass = self::CLASS_CONTEXT_SPECIFIC; $constant = $child['constant']; } if (isset($constant) && isset($temp['constant'])) { // Can only match if constants and class match. $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; } else { // Can only match if no constant expected and type matches or is generic. $maymatch = !isset($child['constant']) && array_search($child['type'], [$temp['type'], self::TYPE_ANY, self::TYPE_CHOICE]) !== false; } } } if ($maymatch) { // Attempt submapping. $candidate = self::asn1map($temp, $child, $special); $maymatch = $candidate !== null; } if ($maymatch) { // Got the match: use it. if (isset($special[$key])) { $candidate = $special[$key]($candidate); } $map[$key] = $candidate; $i++; } elseif (isset($child['default'])) { $map[$key] = $child['default']; } elseif (!isset($child['optional'])) { return null; // Syntax error. } } // Fail mapping if all input items have not been consumed. return $i < $n ? null : $map; // the main diff between sets and sequences is the encapsulation of the foreach in another for loop case self::TYPE_SET: $map = []; // ignore the min and max if (isset($mapping['min']) && isset($mapping['max'])) { $child = $mapping['children']; foreach ($decoded['content'] as $content) { if (($map[] = self::asn1map($content, $child, $special)) === null) { return null; } } return $map; } for ($i = 0; $i < count($decoded['content']); $i++) { $temp = $decoded['content'][$i]; $tempClass = self::CLASS_UNIVERSAL; if (isset($temp['constant'])) { $tempClass = $temp['type']; } foreach ($mapping['children'] as $key => $child) { if (isset($map[$key])) { continue; } $maymatch = true; if ($child['type'] != self::TYPE_CHOICE) { $childClass = self::CLASS_UNIVERSAL; $constant = null; if (isset($child['class'])) { $childClass = $child['class']; $constant = $child['cast']; } elseif (isset($child['constant'])) { $childClass = self::CLASS_CONTEXT_SPECIFIC; $constant = $child['constant']; } if (isset($constant) && isset($temp['constant'])) { // Can only match if constants and class match. $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; } else { // Can only match if no constant expected and type matches or is generic. $maymatch = !isset($child['constant']) && array_search($child['type'], [$temp['type'], self::TYPE_ANY, self::TYPE_CHOICE]) !== false; } } if ($maymatch) { // Attempt submapping. $candidate = self::asn1map($temp, $child, $special); $maymatch = $candidate !== null; } if (!$maymatch) { break; } // Got the match: use it. if (isset($special[$key])) { $candidate = $special[$key]($candidate); } $map[$key] = $candidate; break; } } foreach ($mapping['children'] as $key => $child) { if (!isset($map[$key])) { if (isset($child['default'])) { $map[$key] = $child['default']; } elseif (!isset($child['optional'])) { return null; } } } return $map; case self::TYPE_OBJECT_IDENTIFIER: return isset(self::$oids[$decoded['content']]) ? self::$oids[$decoded['content']] : $decoded['content']; case self::TYPE_UTC_TIME: case self::TYPE_GENERALIZED_TIME: // for explicitly tagged optional stuff if (is_array($decoded['content'])) { $decoded['content'] = $decoded['content'][0]['content']; } // for implicitly tagged optional stuff // in theory, doing isset($mapping['implicit']) would work but malformed certs do exist // in the wild that OpenSSL decodes without issue so we'll support them as well if (!is_object($decoded['content'])) { $decoded['content'] = self::decodeTime($decoded['content'], $decoded['type']); } return $decoded['content'] ? $decoded['content']->format(self::$format) : false; case self::TYPE_BIT_STRING: if (isset($mapping['mapping'])) { $offset = ord($decoded['content'][0]); $size = (strlen($decoded['content']) - 1) * 8 - $offset; /* From X.680-0207.pdf#page=46 (21.7): "When a "NamedBitList" is used in defining a bitstring type ASN.1 encoding rules are free to add (or remove) arbitrarily any trailing 0 bits to (or from) values that are being encoded or decoded. Application designers should therefore ensure that different semantics are not associated with such values which differ only in the number of trailing 0 bits." */ $bits = count($mapping['mapping']) == $size ? [] : array_fill(0, count($mapping['mapping']) - $size, false); for ($i = strlen($decoded['content']) - 1; $i > 0; $i--) { $current = ord($decoded['content'][$i]); for ($j = $offset; $j < 8; $j++) { $bits[] = (bool) ($current & (1 << $j)); } $offset = 0; } $values = []; $map = array_reverse($mapping['mapping']); foreach ($map as $i => $value) { if ($bits[$i]) { $values[] = $value; } } return $values; } // fall-through case self::TYPE_OCTET_STRING: return $decoded['content']; case self::TYPE_NULL: return ''; case self::TYPE_BOOLEAN: case self::TYPE_NUMERIC_STRING: case self::TYPE_PRINTABLE_STRING: case self::TYPE_TELETEX_STRING: case self::TYPE_VIDEOTEX_STRING: case self::TYPE_IA5_STRING: case self::TYPE_GRAPHIC_STRING: case self::TYPE_VISIBLE_STRING: case self::TYPE_GENERAL_STRING: case self::TYPE_UNIVERSAL_STRING: case self::TYPE_UTF8_STRING: case self::TYPE_BMP_STRING: return $decoded['content']; case self::TYPE_INTEGER: case self::TYPE_ENUMERATED: $temp = $decoded['content']; if (isset($mapping['implicit'])) { $temp = new BigInteger($decoded['content'], -256); } if (isset($mapping['mapping'])) { $temp = (int) $temp->toString(); return isset($mapping['mapping'][$temp]) ? $mapping['mapping'][$temp] : false; } return $temp; } } /** * DER-decode the length * * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. * * @param string $string * @return int */ public static function decodeLength(&$string) { $length = ord(Strings::shift($string)); if ($length & 0x80) { // definite length, long form $length &= 0x7F; $temp = Strings::shift($string, $length); list(, $length) = unpack('N', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4)); } return $length; } /** * ASN.1 Encode * * DER-encodes an ASN.1 semantic mapping ($mapping). Some libraries would probably call this function * an ASN.1 compiler. * * "Special" mappings can be applied via $special. * * @param Element|string|array $source * @param array $mapping * @param array $special * @return string */ public static function encodeDER($source, $mapping, $special = []) { self::$location = []; return self::encode_der($source, $mapping, null, $special); } /** * ASN.1 Encode (Helper function) * * @param Element|string|array|null $source * @param array $mapping * @param int $idx * @param array $special * @return string */ private static function encode_der($source, array $mapping, $idx = null, array $special = []) { if ($source instanceof Element) { return $source->element; } // do not encode (implicitly optional) fields with value set to default if (isset($mapping['default']) && $source === $mapping['default']) { return ''; } if (isset($idx)) { if (isset($special[$idx])) { $source = $special[$idx]($source); } self::$location[] = $idx; } $tag = $mapping['type']; switch ($tag) { case self::TYPE_SET: // Children order is not important, thus process in sequence. case self::TYPE_SEQUENCE: $tag |= 0x20; // set the constructed bit // ignore the min and max if (isset($mapping['min']) && isset($mapping['max'])) { $value = []; $child = $mapping['children']; foreach ($source as $content) { $temp = self::encode_der($content, $child, null, $special); if ($temp === false) { return false; } $value[] = $temp; } /* "The encodings of the component values of a set-of value shall appear in ascending order, the encodings being compared as octet strings with the shorter components being padded at their trailing end with 0-octets. NOTE - The padding octets are for comparison purposes only and do not appear in the encodings." -- sec 11.6 of http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf */ if ($mapping['type'] == self::TYPE_SET) { sort($value); } $value = implode('', $value); break; } $value = ''; foreach ($mapping['children'] as $key => $child) { if (!array_key_exists($key, $source)) { if (!isset($child['optional'])) { return false; } continue; } $temp = self::encode_der($source[$key], $child, $key, $special); if ($temp === false) { return false; } // An empty child encoding means it has been optimized out. // Else we should have at least one tag byte. if ($temp === '') { continue; } // if isset($child['constant']) is true then isset($child['optional']) should be true as well if (isset($child['constant'])) { /* From X.680-0207.pdf#page=58 (30.6): "The tagging construction specifies explicit tagging if any of the following holds: ... c) the "Tag Type" alternative is used and the value of "TagDefault" for the module is IMPLICIT TAGS or AUTOMATIC TAGS, but the type defined by "Type" is an untagged choice type, an untagged open type, or an untagged "DummyReference" (see ITU-T Rec. X.683 | ISO/IEC 8824-4, 8.3)." */ if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); $temp = $subtag . self::encodeLength(strlen($temp)) . $temp; } else { $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']); $temp = $subtag . substr($temp, 1); } } $value .= $temp; } break; case self::TYPE_CHOICE: $temp = false; foreach ($mapping['children'] as $key => $child) { if (!isset($source[$key])) { continue; } $temp = self::encode_der($source[$key], $child, $key, $special); if ($temp === false) { return false; } // An empty child encoding means it has been optimized out. // Else we should have at least one tag byte. if ($temp === '') { continue; } $tag = ord($temp[0]); // if isset($child['constant']) is true then isset($child['optional']) should be true as well if (isset($child['constant'])) { if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); $temp = $subtag . self::encodeLength(strlen($temp)) . $temp; } else { $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']); $temp = $subtag . substr($temp, 1); } } } if (isset($idx)) { array_pop(self::$location); } if ($temp && isset($mapping['cast'])) { $temp[0] = chr(($mapping['class'] << 6) | ($tag & 0x20) | $mapping['cast']); } return $temp; case self::TYPE_INTEGER: case self::TYPE_ENUMERATED: if (!isset($mapping['mapping'])) { if (is_numeric($source)) { $source = new BigInteger($source); } $value = $source->toBytes(true); } else { $value = array_search($source, $mapping['mapping']); if ($value === false) { return false; } $value = new BigInteger($value); $value = $value->toBytes(true); } if (!strlen($value)) { $value = chr(0); } break; case self::TYPE_UTC_TIME: case self::TYPE_GENERALIZED_TIME: $format = $mapping['type'] == self::TYPE_UTC_TIME ? 'y' : 'Y'; $format .= 'mdHis'; // if $source does _not_ include timezone information within it then assume that the timezone is GMT $date = new \DateTime($source, new \DateTimeZone('GMT')); // if $source _does_ include timezone information within it then convert the time to GMT $date->setTimezone(new \DateTimeZone('GMT')); $value = $date->format($format) . 'Z'; break; case self::TYPE_BIT_STRING: if (isset($mapping['mapping'])) { $bits = array_fill(0, count($mapping['mapping']), 0); $size = 0; for ($i = 0; $i < count($mapping['mapping']); $i++) { if (in_array($mapping['mapping'][$i], $source)) { $bits[$i] = 1; $size = $i; } } if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) { $size = $mapping['min'] - 1; } $offset = 8 - (($size + 1) & 7); $offset = $offset !== 8 ? $offset : 0; $value = chr($offset); for ($i = $size + 1; $i < count($mapping['mapping']); $i++) { unset($bits[$i]); } $bits = implode('', array_pad($bits, $size + $offset + 1, 0)); $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' '))); foreach ($bytes as $byte) { $value .= chr(bindec($byte)); } break; } // fall-through case self::TYPE_OCTET_STRING: /* The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit, the number of unused bits in the final subsequent octet. The number shall be in the range zero to seven. -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=16 */ $value = $source; break; case self::TYPE_OBJECT_IDENTIFIER: $value = self::encodeOID($source); break; case self::TYPE_ANY: $loc = self::$location; if (isset($idx)) { array_pop(self::$location); } switch (true) { case !isset($source): return self::encode_der(null, ['type' => self::TYPE_NULL] + $mapping, null, $special); case is_int($source): case $source instanceof BigInteger: return self::encode_der($source, ['type' => self::TYPE_INTEGER] + $mapping, null, $special); case is_float($source): return self::encode_der($source, ['type' => self::TYPE_REAL] + $mapping, null, $special); case is_bool($source): return self::encode_der($source, ['type' => self::TYPE_BOOLEAN] + $mapping, null, $special); case is_array($source) && count($source) == 1: $typename = implode('', array_keys($source)); $outtype = array_search($typename, self::ANY_MAP, true); if ($outtype !== false) { return self::encode_der($source[$typename], ['type' => $outtype] + $mapping, null, $special); } } $filters = self::$filters; foreach ($loc as $part) { if (!isset($filters[$part])) { $filters = false; break; } $filters = $filters[$part]; } if ($filters === false) { throw new \RuntimeException('No filters defined for ' . implode('/', $loc)); } return self::encode_der($source, $filters + $mapping, null, $special); case self::TYPE_NULL: $value = ''; break; case self::TYPE_NUMERIC_STRING: case self::TYPE_TELETEX_STRING: case self::TYPE_PRINTABLE_STRING: case self::TYPE_UNIVERSAL_STRING: case self::TYPE_UTF8_STRING: case self::TYPE_BMP_STRING: case self::TYPE_IA5_STRING: case self::TYPE_VISIBLE_STRING: case self::TYPE_VIDEOTEX_STRING: case self::TYPE_GRAPHIC_STRING: case self::TYPE_GENERAL_STRING: $value = $source; break; case self::TYPE_BOOLEAN: $value = $source ? "\xFF" : "\x00"; break; default: throw new \RuntimeException('Mapping provides no type definition for ' . implode('/', self::$location)); } if (isset($idx)) { array_pop(self::$location); } if (isset($mapping['cast'])) { if (isset($mapping['explicit']) || $mapping['type'] == self::TYPE_CHOICE) { $value = chr($tag) . self::encodeLength(strlen($value)) . $value; $tag = ($mapping['class'] << 6) | 0x20 | $mapping['cast']; } else { $tag = ($mapping['class'] << 6) | (ord($temp[0]) & 0x20) | $mapping['cast']; } } return chr($tag) . self::encodeLength(strlen($value)) . $value; } /** * BER-decode the OID * * Called by _decode_ber() * * @param string $content * @return string */ public static function decodeOID($content) { static $eighty; if (!$eighty) { $eighty = new BigInteger(80); } $oid = []; $pos = 0; $len = strlen($content); // see https://github.com/openjdk/jdk/blob/2deb318c9f047ec5a4b160d66a4b52f93688ec42/src/java.base/share/classes/sun/security/util/ObjectIdentifier.java#L55 if ($len > 4096) { //throw new \RuntimeException("Object identifier size is limited to 4096 bytes ($len bytes present)"); return false; } if (ord($content[$len - 1]) & 0x80) { return false; } $n = new BigInteger(); while ($pos < $len) { $temp = ord($content[$pos++]); $n = $n->bitwise_leftShift(7); $n = $n->bitwise_or(new BigInteger($temp & 0x7F)); if (~$temp & 0x80) { $oid[] = $n; $n = new BigInteger(); } } $part1 = array_shift($oid); $first = floor(ord($content[0]) / 40); /* "This packing of the first two object identifier components recognizes that only three values are allocated from the root node, and at most 39 subsequent values from nodes reached by X = 0 and X = 1." -- https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=22 */ if ($first <= 2) { // ie. 0 <= ord($content[0]) < 120 (0x78) array_unshift($oid, ord($content[0]) % 40); array_unshift($oid, $first); } else { array_unshift($oid, $part1->subtract($eighty)); array_unshift($oid, 2); } return implode('.', $oid); } /** * DER-encode the OID * * Called by _encode_der() * * @param string $source * @return string */ public static function encodeOID($source) { static $mask, $zero, $forty; if (!$mask) { $mask = new BigInteger(0x7F); $zero = new BigInteger(); $forty = new BigInteger(40); } if (!preg_match('#(?:\d+\.)+#', $source)) { $oid = isset(self::$reverseOIDs[$source]) ? self::$reverseOIDs[$source] : false; } else { $oid = $source; } if ($oid === false) { throw new \RuntimeException('Invalid OID'); } $parts = explode('.', $oid); $part1 = array_shift($parts); $part2 = array_shift($parts); $first = new BigInteger($part1); $first = $first->multiply($forty); $first = $first->add(new BigInteger($part2)); array_unshift($parts, $first->toString()); $value = ''; foreach ($parts as $part) { if (!$part) { $temp = "\0"; } else { $temp = ''; $part = new BigInteger($part); while (!$part->equals($zero)) { $submask = $part->bitwise_and($mask); $submask->setPrecision(8); $temp = (chr(0x80) | $submask->toBytes()) . $temp; $part = $part->bitwise_rightShift(7); } $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7F); } $value .= $temp; } return $value; } /** * BER-decode the time * * Called by _decode_ber() and in the case of implicit tags asn1map(). * * @param string $content * @param int $tag * @return \DateTime|false */ private static function decodeTime($content, $tag) { /* UTCTime: http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 http://www.obj-sys.com/asn1tutorial/node15.html GeneralizedTime: http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 http://www.obj-sys.com/asn1tutorial/node14.html */ $format = 'YmdHis'; if ($tag == self::TYPE_UTC_TIME) { // https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=28 says "the seconds // element shall always be present" but none-the-less I've seen X509 certs where it isn't and if the // browsers parse it phpseclib ought to too if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $content, $matches)) { $content = $matches[1] . '00' . $matches[2]; } $prefix = substr($content, 0, 2) >= 50 ? '19' : '20'; $content = $prefix . $content; } elseif (strpos($content, '.') !== false) { $format .= '.u'; } if ($content[strlen($content) - 1] == 'Z') { $content = substr($content, 0, -1) . '+0000'; } if (strpos($content, '-') !== false || strpos($content, '+') !== false) { $format .= 'O'; } // error supression isn't necessary as of PHP 7.0: // http://php.net/manual/en/migration70.other-changes.php return @\DateTime::createFromFormat($format, $content); } /** * Set the time format * * Sets the time / date format for asn1map(). * * @param string $format */ public static function setTimeFormat($format) { self::$format = $format; } /** * Load OIDs * * Load the relevant OIDs for a particular ASN.1 semantic mapping. * Previously loaded OIDs are retained. * * @param array $oids */ public static function loadOIDs(array $oids) { self::$reverseOIDs += $oids; self::$oids = array_flip(self::$reverseOIDs); } /** * Set filters * * See \phpseclib3\File\X509, etc, for an example. * Previously loaded filters are not retained. * * @param array $filters */ public static function setFilters(array $filters) { self::$filters = $filters; } /** * String type conversion * * This is a lazy conversion, dealing only with character size. * No real conversion table is used. * * @param string $in * @param int $from * @param int $to * @return string */ public static function convert($in, $from = self::TYPE_UTF8_STRING, $to = self::TYPE_UTF8_STRING) { // isset(self::STRING_TYPE_SIZE[$from] returns a fatal error on PHP 5.6 if (!array_key_exists($from, self::STRING_TYPE_SIZE) || !array_key_exists($to, self::STRING_TYPE_SIZE)) { return false; } $insize = self::STRING_TYPE_SIZE[$from]; $outsize = self::STRING_TYPE_SIZE[$to]; $inlength = strlen($in); $out = ''; for ($i = 0; $i < $inlength;) { if ($inlength - $i < $insize) { return false; } // Get an input character as a 32-bit value. $c = ord($in[$i++]); switch (true) { case $insize == 4: $c = ($c << 8) | ord($in[$i++]); $c = ($c << 8) | ord($in[$i++]); // fall-through case $insize == 2: $c = ($c << 8) | ord($in[$i++]); // fall-through case $insize == 1: break; case ($c & 0x80) == 0x00: break; case ($c & 0x40) == 0x00: return false; default: $bit = 6; do { if ($bit > 25 || $i >= $inlength || (ord($in[$i]) & 0xC0) != 0x80) { return false; } $c = ($c << 6) | (ord($in[$i++]) & 0x3F); $bit += 5; $mask = 1 << $bit; } while ($c & $bit); $c &= $mask - 1; break; } // Convert and append the character to output string. $v = ''; switch (true) { case $outsize == 4: $v .= chr($c & 0xFF); $c >>= 8; $v .= chr($c & 0xFF); $c >>= 8; // fall-through case $outsize == 2: $v .= chr($c & 0xFF); $c >>= 8; // fall-through case $outsize == 1: $v .= chr($c & 0xFF); $c >>= 8; if ($c) { return false; } break; case ($c & (PHP_INT_SIZE == 8 ? 0x80000000 : (1 << 31))) != 0: return false; case $c >= 0x04000000: $v .= chr(0x80 | ($c & 0x3F)); $c = ($c >> 6) | 0x04000000; // fall-through case $c >= 0x00200000: $v .= chr(0x80 | ($c & 0x3F)); $c = ($c >> 6) | 0x00200000; // fall-through case $c >= 0x00010000: $v .= chr(0x80 | ($c & 0x3F)); $c = ($c >> 6) | 0x00010000; // fall-through case $c >= 0x00000800: $v .= chr(0x80 | ($c & 0x3F)); $c = ($c >> 6) | 0x00000800; // fall-through case $c >= 0x00000080: $v .= chr(0x80 | ($c & 0x3F)); $c = ($c >> 6) | 0x000000C0; // fall-through default: $v .= chr($c); break; } $out .= strrev($v); } return $out; } /** * Extract raw BER from Base64 encoding * * @param string $str * @return string */ public static function extractBER($str) { /* X.509 certs are assumed to be base64 encoded but sometimes they'll have additional things in them * above and beyond the ceritificate. * ie. some may have the following preceding the -----BEGIN CERTIFICATE----- line: * * Bag Attributes * localKeyID: 01 00 00 00 * subject=/O=organization/OU=org unit/CN=common name * issuer=/O=organization/CN=common name */ if (strlen($str) > ini_get('pcre.backtrack_limit')) { $temp = $str; } else { $temp = preg_replace('#.*?^-+[^-]+-+[\r\n ]*$#ms', '', $str, 1); $temp = preg_replace('#-+END.*[\r\n ]*.*#ms', '', $temp, 1); } // remove new lines $temp = str_replace(["\r", "\n", ' '], '', $temp); // remove the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- stuff $temp = preg_replace('#^-+[^-]+-+|-+[^-]+-+$#', '', $temp); $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? Strings::base64_decode($temp) : false; return $temp != false ? $temp : $str; } /** * DER-encode the length * * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. * * @param int $length * @return string */ public static function encodeLength($length) { if ($length <= 0x7F) { return chr($length); } $temp = ltrim(pack('N', $length), chr(0)); return pack('Ca*', 0x80 | strlen($temp), $temp); } /** * Returns the OID corresponding to a name * * What's returned in the associative array returned by loadX509() (or load*()) is either a name or an OID if * no OID to name mapping is available. The problem with this is that what may be an unmapped OID in one version * of phpseclib may not be unmapped in the next version, so apps that are looking at this OID may not be able * to work from version to version. * * This method will return the OID if a name is passed to it and if no mapping is avialable it'll assume that * what's being passed to it already is an OID and return that instead. A few examples. * * getOID('2.16.840.1.101.3.4.2.1') == '2.16.840.1.101.3.4.2.1' * getOID('id-sha256') == '2.16.840.1.101.3.4.2.1' * getOID('zzz') == 'zzz' * * @param string $name * @return string */ public static function getOID($name) { return isset(self::$reverseOIDs[$name]) ? self::$reverseOIDs[$name] : $name; } } PK!.֞A//2vendor/phpseclib/phpseclib/phpseclib/File/X509.phpnu[ * @copyright 2012 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\File; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\PrivateKey; use phpseclib3\Crypt\Common\PublicKey; use phpseclib3\Crypt\DSA; use phpseclib3\Crypt\EC; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\PublicKeyLoader; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA\Formats\Keys\PSS; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\File\ASN1\Element; use phpseclib3\File\ASN1\Maps; use phpseclib3\Math\BigInteger; /** * Pure-PHP X.509 Parser * * @author Jim Wigginton */ class X509 { /** * Flag to only accept signatures signed by certificate authorities * * Not really used anymore but retained all the same to suppress E_NOTICEs from old installs * */ const VALIDATE_SIGNATURE_BY_CA = 1; /** * Return internal array representation * * @see \phpseclib3\File\X509::getDN() */ const DN_ARRAY = 0; /** * Return string * * @see \phpseclib3\File\X509::getDN() */ const DN_STRING = 1; /** * Return ASN.1 name string * * @see \phpseclib3\File\X509::getDN() */ const DN_ASN1 = 2; /** * Return OpenSSL compatible array * * @see \phpseclib3\File\X509::getDN() */ const DN_OPENSSL = 3; /** * Return canonical ASN.1 RDNs string * * @see \phpseclib3\File\X509::getDN() */ const DN_CANON = 4; /** * Return name hash for file indexing * * @see \phpseclib3\File\X509::getDN() */ const DN_HASH = 5; /** * Save as PEM * * ie. a base64-encoded PEM with a header and a footer * * @see \phpseclib3\File\X509::saveX509() * @see \phpseclib3\File\X509::saveCSR() * @see \phpseclib3\File\X509::saveCRL() */ const FORMAT_PEM = 0; /** * Save as DER * * @see \phpseclib3\File\X509::saveX509() * @see \phpseclib3\File\X509::saveCSR() * @see \phpseclib3\File\X509::saveCRL() */ const FORMAT_DER = 1; /** * Save as a SPKAC * * @see \phpseclib3\File\X509::saveX509() * @see \phpseclib3\File\X509::saveCSR() * @see \phpseclib3\File\X509::saveCRL() * * Only works on CSRs. Not currently supported. */ const FORMAT_SPKAC = 2; /** * Auto-detect the format * * Used only by the load*() functions * * @see \phpseclib3\File\X509::saveX509() * @see \phpseclib3\File\X509::saveCSR() * @see \phpseclib3\File\X509::saveCRL() */ const FORMAT_AUTO_DETECT = 3; /** * Attribute value disposition. * If disposition is >= 0, this is the index of the target value. */ const ATTR_ALL = -1; // All attribute values (array). const ATTR_APPEND = -2; // Add a value. const ATTR_REPLACE = -3; // Clear first, then add a value. /** * Distinguished Name * * @var array */ private $dn; /** * Public key * * @var string|PublicKey */ private $publicKey; /** * Private key * * @var string|PrivateKey */ private $privateKey; /** * The certificate authorities * * @var array */ private $CAs = []; /** * The currently loaded certificate * * @var array */ private $currentCert; /** * The signature subject * * There's no guarantee \phpseclib3\File\X509 is going to re-encode an X.509 cert in the same way it was originally * encoded so we take save the portion of the original cert that the signature would have made for. * * @var string */ private $signatureSubject; /** * Certificate Start Date * * @var string */ private $startDate; /** * Certificate End Date * * @var string|Element */ private $endDate; /** * Serial Number * * @var string */ private $serialNumber; /** * Key Identifier * * See {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.1 RFC5280#section-4.2.1.1} and * {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.2 RFC5280#section-4.2.1.2}. * * @var string */ private $currentKeyIdentifier; /** * CA Flag * * @var bool */ private $caFlag = false; /** * SPKAC Challenge * * @var string */ private $challenge; /** * @var array */ private $extensionValues = []; /** * OIDs loaded * * @var bool */ private static $oidsLoaded = false; /** * Recursion Limit * * @var int */ private static $recur_limit = 5; /** * URL fetch flag * * @var bool */ private static $disable_url_fetch = false; /** * @var array */ private static $extensions = []; /** * @var ?array */ private $ipAddresses = null; /** * @var ?array */ private $domains = null; /** * Default Constructor. * * @return X509 */ public function __construct() { // Explicitly Tagged Module, 1988 Syntax // http://tools.ietf.org/html/rfc5280#appendix-A.1 if (!self::$oidsLoaded) { // OIDs from RFC5280 and those RFCs mentioned in RFC5280#section-4.1.1.2 ASN1::loadOIDs([ //'id-pkix' => '1.3.6.1.5.5.7', //'id-pe' => '1.3.6.1.5.5.7.1', //'id-qt' => '1.3.6.1.5.5.7.2', //'id-kp' => '1.3.6.1.5.5.7.3', //'id-ad' => '1.3.6.1.5.5.7.48', 'id-qt-cps' => '1.3.6.1.5.5.7.2.1', 'id-qt-unotice' => '1.3.6.1.5.5.7.2.2', 'id-ad-ocsp' => '1.3.6.1.5.5.7.48.1', 'id-ad-caIssuers' => '1.3.6.1.5.5.7.48.2', 'id-ad-timeStamping' => '1.3.6.1.5.5.7.48.3', 'id-ad-caRepository' => '1.3.6.1.5.5.7.48.5', //'id-at' => '2.5.4', 'id-at-name' => '2.5.4.41', 'id-at-surname' => '2.5.4.4', 'id-at-givenName' => '2.5.4.42', 'id-at-initials' => '2.5.4.43', 'id-at-generationQualifier' => '2.5.4.44', 'id-at-commonName' => '2.5.4.3', 'id-at-localityName' => '2.5.4.7', 'id-at-stateOrProvinceName' => '2.5.4.8', 'id-at-organizationName' => '2.5.4.10', 'id-at-organizationalUnitName' => '2.5.4.11', 'id-at-title' => '2.5.4.12', 'id-at-description' => '2.5.4.13', 'id-at-dnQualifier' => '2.5.4.46', 'id-at-countryName' => '2.5.4.6', 'id-at-serialNumber' => '2.5.4.5', 'id-at-pseudonym' => '2.5.4.65', 'id-at-postalCode' => '2.5.4.17', 'id-at-streetAddress' => '2.5.4.9', 'id-at-uniqueIdentifier' => '2.5.4.45', 'id-at-role' => '2.5.4.72', 'id-at-postalAddress' => '2.5.4.16', 'jurisdictionOfIncorporationCountryName' => '1.3.6.1.4.1.311.60.2.1.3', 'jurisdictionOfIncorporationStateOrProvinceName' => '1.3.6.1.4.1.311.60.2.1.2', 'jurisdictionLocalityName' => '1.3.6.1.4.1.311.60.2.1.1', 'id-at-businessCategory' => '2.5.4.15', //'id-domainComponent' => '0.9.2342.19200300.100.1.25', //'pkcs-9' => '1.2.840.113549.1.9', 'pkcs-9-at-emailAddress' => '1.2.840.113549.1.9.1', //'id-ce' => '2.5.29', 'id-ce-authorityKeyIdentifier' => '2.5.29.35', 'id-ce-subjectKeyIdentifier' => '2.5.29.14', 'id-ce-keyUsage' => '2.5.29.15', 'id-ce-privateKeyUsagePeriod' => '2.5.29.16', 'id-ce-certificatePolicies' => '2.5.29.32', //'anyPolicy' => '2.5.29.32.0', 'id-ce-policyMappings' => '2.5.29.33', 'id-ce-subjectAltName' => '2.5.29.17', 'id-ce-issuerAltName' => '2.5.29.18', 'id-ce-subjectDirectoryAttributes' => '2.5.29.9', 'id-ce-basicConstraints' => '2.5.29.19', 'id-ce-nameConstraints' => '2.5.29.30', 'id-ce-policyConstraints' => '2.5.29.36', 'id-ce-cRLDistributionPoints' => '2.5.29.31', 'id-ce-extKeyUsage' => '2.5.29.37', //'anyExtendedKeyUsage' => '2.5.29.37.0', 'id-kp-serverAuth' => '1.3.6.1.5.5.7.3.1', 'id-kp-clientAuth' => '1.3.6.1.5.5.7.3.2', 'id-kp-codeSigning' => '1.3.6.1.5.5.7.3.3', 'id-kp-emailProtection' => '1.3.6.1.5.5.7.3.4', 'id-kp-timeStamping' => '1.3.6.1.5.5.7.3.8', 'id-kp-OCSPSigning' => '1.3.6.1.5.5.7.3.9', 'id-ce-inhibitAnyPolicy' => '2.5.29.54', 'id-ce-freshestCRL' => '2.5.29.46', 'id-pe-authorityInfoAccess' => '1.3.6.1.5.5.7.1.1', 'id-pe-subjectInfoAccess' => '1.3.6.1.5.5.7.1.11', 'id-ce-cRLNumber' => '2.5.29.20', 'id-ce-issuingDistributionPoint' => '2.5.29.28', 'id-ce-deltaCRLIndicator' => '2.5.29.27', 'id-ce-cRLReasons' => '2.5.29.21', 'id-ce-certificateIssuer' => '2.5.29.29', 'id-ce-holdInstructionCode' => '2.5.29.23', //'holdInstruction' => '1.2.840.10040.2', 'id-holdinstruction-none' => '1.2.840.10040.2.1', 'id-holdinstruction-callissuer' => '1.2.840.10040.2.2', 'id-holdinstruction-reject' => '1.2.840.10040.2.3', 'id-ce-invalidityDate' => '2.5.29.24', 'rsaEncryption' => '1.2.840.113549.1.1.1', 'md2WithRSAEncryption' => '1.2.840.113549.1.1.2', 'md5WithRSAEncryption' => '1.2.840.113549.1.1.4', 'sha1WithRSAEncryption' => '1.2.840.113549.1.1.5', 'sha224WithRSAEncryption' => '1.2.840.113549.1.1.14', 'sha256WithRSAEncryption' => '1.2.840.113549.1.1.11', 'sha384WithRSAEncryption' => '1.2.840.113549.1.1.12', 'sha512WithRSAEncryption' => '1.2.840.113549.1.1.13', 'id-ecPublicKey' => '1.2.840.10045.2.1', 'ecdsa-with-SHA1' => '1.2.840.10045.4.1', // from https://tools.ietf.org/html/rfc5758#section-3.2 'ecdsa-with-SHA224' => '1.2.840.10045.4.3.1', 'ecdsa-with-SHA256' => '1.2.840.10045.4.3.2', 'ecdsa-with-SHA384' => '1.2.840.10045.4.3.3', 'ecdsa-with-SHA512' => '1.2.840.10045.4.3.4', 'id-dsa' => '1.2.840.10040.4.1', 'id-dsa-with-sha1' => '1.2.840.10040.4.3', // from https://tools.ietf.org/html/rfc5758#section-3.1 'id-dsa-with-sha224' => '2.16.840.1.101.3.4.3.1', 'id-dsa-with-sha256' => '2.16.840.1.101.3.4.3.2', // from https://tools.ietf.org/html/rfc8410: 'id-Ed25519' => '1.3.101.112', 'id-Ed448' => '1.3.101.113', 'id-RSASSA-PSS' => '1.2.840.113549.1.1.10', //'id-sha224' => '2.16.840.1.101.3.4.2.4', //'id-sha256' => '2.16.840.1.101.3.4.2.1', //'id-sha384' => '2.16.840.1.101.3.4.2.2', //'id-sha512' => '2.16.840.1.101.3.4.2.3', //'id-GostR3411-94-with-GostR3410-94' => '1.2.643.2.2.4', //'id-GostR3411-94-with-GostR3410-2001' => '1.2.643.2.2.3', //'id-GostR3410-2001' => '1.2.643.2.2.20', //'id-GostR3410-94' => '1.2.643.2.2.19', // Netscape Object Identifiers from "Netscape Certificate Extensions" 'netscape' => '2.16.840.1.113730', 'netscape-cert-extension' => '2.16.840.1.113730.1', 'netscape-cert-type' => '2.16.840.1.113730.1.1', 'netscape-comment' => '2.16.840.1.113730.1.13', 'netscape-ca-policy-url' => '2.16.840.1.113730.1.8', // the following are X.509 extensions not supported by phpseclib 'id-pe-logotype' => '1.3.6.1.5.5.7.1.12', 'entrustVersInfo' => '1.2.840.113533.7.65.0', 'verisignPrivate' => '2.16.840.1.113733.1.6.9', // for Certificate Signing Requests // see http://tools.ietf.org/html/rfc2985 'pkcs-9-at-unstructuredName' => '1.2.840.113549.1.9.2', // PKCS #9 unstructured name 'pkcs-9-at-challengePassword' => '1.2.840.113549.1.9.7', // Challenge password for certificate revocations 'pkcs-9-at-extensionRequest' => '1.2.840.113549.1.9.14' // Certificate extension request ]); } } /** * Load X.509 certificate * * Returns an associative array describing the X.509 cert or a false if the cert failed to load * * @param array|string $cert * @param int $mode * @return mixed */ public function loadX509($cert, $mode = self::FORMAT_AUTO_DETECT) { if (is_array($cert) && isset($cert['tbsCertificate'])) { unset($this->currentCert); unset($this->currentKeyIdentifier); $this->dn = $cert['tbsCertificate']['subject']; if (!isset($this->dn)) { return false; } $this->currentCert = $cert; $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; unset($this->signatureSubject); return $cert; } if ($mode != self::FORMAT_DER) { $newcert = ASN1::extractBER($cert); if ($mode == self::FORMAT_PEM && $cert == $newcert) { return false; } $cert = $newcert; } if ($cert === false) { $this->currentCert = false; return false; } $decoded = ASN1::decodeBER($cert); if ($decoded) { $x509 = ASN1::asn1map($decoded[0], Maps\Certificate::MAP); } if (!isset($x509) || $x509 === false) { $this->currentCert = false; return false; } $this->signatureSubject = substr($cert, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); if ($this->isSubArrayValid($x509, 'tbsCertificate/extensions')) { $this->mapInExtensions($x509, 'tbsCertificate/extensions'); } $this->mapInDNs($x509, 'tbsCertificate/issuer/rdnSequence'); $this->mapInDNs($x509, 'tbsCertificate/subject/rdnSequence'); $key = $x509['tbsCertificate']['subjectPublicKeyInfo']; $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP); $x509['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] = "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(base64_encode($key), 64) . "-----END PUBLIC KEY-----"; $this->currentCert = $x509; $this->dn = $x509['tbsCertificate']['subject']; $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; return $x509; } /** * Save X.509 certificate * * @param array $cert * @param int $format optional * @return string */ public function saveX509(array $cert, $format = self::FORMAT_PEM) { if (!is_array($cert) || !isset($cert['tbsCertificate'])) { return false; } switch (true) { // "case !$a: case !$b: break; default: whatever();" is the same thing as "if ($a && $b) whatever()" case !($algorithm = $this->subArray($cert, 'tbsCertificate/subjectPublicKeyInfo/algorithm/algorithm')): case is_object($cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): break; default: $cert['tbsCertificate']['subjectPublicKeyInfo'] = new Element( base64_decode(preg_replace('#-.+-|[\r\n]#', '', $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'])) ); } $filters = []; $type_utf8_string = ['type' => ASN1::TYPE_UTF8_STRING]; $filters['tbsCertificate']['signature']['parameters'] = $type_utf8_string; $filters['tbsCertificate']['signature']['issuer']['rdnSequence']['value'] = $type_utf8_string; $filters['tbsCertificate']['issuer']['rdnSequence']['value'] = $type_utf8_string; $filters['tbsCertificate']['subject']['rdnSequence']['value'] = $type_utf8_string; $filters['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = $type_utf8_string; $filters['signatureAlgorithm']['parameters'] = $type_utf8_string; $filters['authorityCertIssuer']['directoryName']['rdnSequence']['value'] = $type_utf8_string; //$filters['policyQualifiers']['qualifier'] = $type_utf8_string; $filters['distributionPoint']['fullName']['directoryName']['rdnSequence']['value'] = $type_utf8_string; $filters['directoryName']['rdnSequence']['value'] = $type_utf8_string; foreach (self::$extensions as $extension) { $filters['tbsCertificate']['extensions'][] = $extension; } /* in the case of policyQualifiers/qualifier, the type has to be \phpseclib3\File\ASN1::TYPE_IA5_STRING. \phpseclib3\File\ASN1::TYPE_PRINTABLE_STRING will cause OpenSSL's X.509 parser to spit out random characters. */ $filters['policyQualifiers']['qualifier'] = ['type' => ASN1::TYPE_IA5_STRING]; ASN1::setFilters($filters); $this->mapOutExtensions($cert, 'tbsCertificate/extensions'); $this->mapOutDNs($cert, 'tbsCertificate/issuer/rdnSequence'); $this->mapOutDNs($cert, 'tbsCertificate/subject/rdnSequence'); $cert = ASN1::encodeDER($cert, Maps\Certificate::MAP); switch ($format) { case self::FORMAT_DER: return $cert; // case self::FORMAT_PEM: default: return "-----BEGIN CERTIFICATE-----\r\n" . chunk_split(Strings::base64_encode($cert), 64) . '-----END CERTIFICATE-----'; } } /** * Map extension values from octet string to extension-specific internal * format. * * @param array $root (by reference) * @param string $path */ private function mapInExtensions(array &$root, $path) { $extensions = &$this->subArrayUnchecked($root, $path); if ($extensions) { for ($i = 0; $i < count($extensions); $i++) { $id = $extensions[$i]['extnId']; $value = &$extensions[$i]['extnValue']; /* [extnValue] contains the DER encoding of an ASN.1 value corresponding to the extension type identified by extnID */ $map = $this->getMapping($id); if (!is_bool($map)) { $decoder = $id == 'id-ce-nameConstraints' ? [static::class, 'decodeNameConstraintIP'] : [static::class, 'decodeIP']; $decoded = ASN1::decodeBER($value); if (!$decoded) { continue; } $mapped = ASN1::asn1map($decoded[0], $map, ['iPAddress' => $decoder]); $value = $mapped === false ? $decoded[0] : $mapped; if ($id == 'id-ce-certificatePolicies') { for ($j = 0; $j < count($value); $j++) { if (!isset($value[$j]['policyQualifiers'])) { continue; } for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) { $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId']; $map = $this->getMapping($subid); $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier']; if ($map !== false) { $decoded = ASN1::decodeBER($subvalue); if (!$decoded) { continue; } $mapped = ASN1::asn1map($decoded[0], $map); $subvalue = $mapped === false ? $decoded[0] : $mapped; } } } } } } } } /** * Map extension values from extension-specific internal format to * octet string. * * @param array $root (by reference) * @param string $path */ private function mapOutExtensions(array &$root, $path) { $extensions = &$this->subArray($root, $path, !empty($this->extensionValues)); foreach ($this->extensionValues as $id => $data) { extract($data); $newext = [ 'extnId' => $id, 'extnValue' => $value, 'critical' => $critical ]; if ($replace) { foreach ($extensions as $key => $value) { if ($value['extnId'] == $id) { $extensions[$key] = $newext; continue 2; } } } $extensions[] = $newext; } if (is_array($extensions)) { $size = count($extensions); for ($i = 0; $i < $size; $i++) { if ($extensions[$i] instanceof Element) { continue; } $id = $extensions[$i]['extnId']; $value = &$extensions[$i]['extnValue']; switch ($id) { case 'id-ce-certificatePolicies': for ($j = 0; $j < count($value); $j++) { if (!isset($value[$j]['policyQualifiers'])) { continue; } for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) { $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId']; $map = $this->getMapping($subid); $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier']; if ($map !== false) { // by default \phpseclib3\File\ASN1 will try to render qualifier as a \phpseclib3\File\ASN1::TYPE_IA5_STRING since it's // actual type is \phpseclib3\File\ASN1::TYPE_ANY $subvalue = new Element(ASN1::encodeDER($subvalue, $map)); } } } break; case 'id-ce-authorityKeyIdentifier': // use 00 as the serial number instead of an empty string if (isset($value['authorityCertSerialNumber'])) { if ($value['authorityCertSerialNumber']->toBytes() == '') { $temp = chr((ASN1::CLASS_CONTEXT_SPECIFIC << 6) | 2) . "\1\0"; $value['authorityCertSerialNumber'] = new Element($temp); } } } /* [extnValue] contains the DER encoding of an ASN.1 value corresponding to the extension type identified by extnID */ $map = $this->getMapping($id); if (is_bool($map)) { if (!$map) { //user_error($id . ' is not a currently supported extension'); unset($extensions[$i]); } } else { $value = ASN1::encodeDER($value, $map, ['iPAddress' => [static::class, 'encodeIP']]); } } } } /** * Map attribute values from ANY type to attribute-specific internal * format. * * @param array $root (by reference) * @param string $path */ private function mapInAttributes(&$root, $path) { $attributes = &$this->subArray($root, $path); if (is_array($attributes)) { for ($i = 0; $i < count($attributes); $i++) { $id = $attributes[$i]['type']; /* $value contains the DER encoding of an ASN.1 value corresponding to the attribute type identified by type */ $map = $this->getMapping($id); if (is_array($attributes[$i]['value'])) { $values = &$attributes[$i]['value']; for ($j = 0; $j < count($values); $j++) { $value = ASN1::encodeDER($values[$j], Maps\AttributeValue::MAP); $decoded = ASN1::decodeBER($value); if (!is_bool($map)) { if (!$decoded) { continue; } $mapped = ASN1::asn1map($decoded[0], $map); if ($mapped !== false) { $values[$j] = $mapped; } if ($id == 'pkcs-9-at-extensionRequest' && $this->isSubArrayValid($values, $j)) { $this->mapInExtensions($values, $j); } } elseif ($map) { $values[$j] = $value; } } } } } } /** * Map attribute values from attribute-specific internal format to * ANY type. * * @param array $root (by reference) * @param string $path */ private function mapOutAttributes(&$root, $path) { $attributes = &$this->subArray($root, $path); if (is_array($attributes)) { $size = count($attributes); for ($i = 0; $i < $size; $i++) { /* [value] contains the DER encoding of an ASN.1 value corresponding to the attribute type identified by type */ $id = $attributes[$i]['type']; $map = $this->getMapping($id); if ($map === false) { //user_error($id . ' is not a currently supported attribute', E_USER_NOTICE); unset($attributes[$i]); } elseif (is_array($attributes[$i]['value'])) { $values = &$attributes[$i]['value']; for ($j = 0; $j < count($values); $j++) { switch ($id) { case 'pkcs-9-at-extensionRequest': $this->mapOutExtensions($values, $j); break; } if (!is_bool($map)) { $temp = ASN1::encodeDER($values[$j], $map); $decoded = ASN1::decodeBER($temp); if (!$decoded) { continue; } $values[$j] = ASN1::asn1map($decoded[0], Maps\AttributeValue::MAP); } } } } } } /** * Map DN values from ANY type to DN-specific internal * format. * * @param array $root (by reference) * @param string $path */ private function mapInDNs(array &$root, $path) { $dns = &$this->subArray($root, $path); if (is_array($dns)) { for ($i = 0; $i < count($dns); $i++) { for ($j = 0; $j < count($dns[$i]); $j++) { $type = $dns[$i][$j]['type']; $value = &$dns[$i][$j]['value']; if (is_object($value) && $value instanceof Element) { $map = $this->getMapping($type); if (!is_bool($map)) { $decoded = ASN1::decodeBER($value); if (!$decoded) { continue; } $value = ASN1::asn1map($decoded[0], $map); } } } } } } /** * Map DN values from DN-specific internal format to * ANY type. * * @param array $root (by reference) * @param string $path */ private function mapOutDNs(array &$root, $path) { $dns = &$this->subArray($root, $path); if (is_array($dns)) { $size = count($dns); for ($i = 0; $i < $size; $i++) { for ($j = 0; $j < count($dns[$i]); $j++) { $type = $dns[$i][$j]['type']; $value = &$dns[$i][$j]['value']; if (is_object($value) && $value instanceof Element) { continue; } $map = $this->getMapping($type); if (!is_bool($map)) { $value = new Element(ASN1::encodeDER($value, $map)); } } } } } /** * Associate an extension ID to an extension mapping * * @param string $extnId * @return mixed */ private function getMapping($extnId) { if (!is_string($extnId)) { // eg. if it's a \phpseclib3\File\ASN1\Element object return true; } if (isset(self::$extensions[$extnId])) { return self::$extensions[$extnId]; } switch ($extnId) { case 'id-ce-keyUsage': return Maps\KeyUsage::MAP; case 'id-ce-basicConstraints': return Maps\BasicConstraints::MAP; case 'id-ce-subjectKeyIdentifier': return Maps\KeyIdentifier::MAP; case 'id-ce-cRLDistributionPoints': return Maps\CRLDistributionPoints::MAP; case 'id-ce-authorityKeyIdentifier': return Maps\AuthorityKeyIdentifier::MAP; case 'id-ce-certificatePolicies': return Maps\CertificatePolicies::MAP; case 'id-ce-extKeyUsage': return Maps\ExtKeyUsageSyntax::MAP; case 'id-pe-authorityInfoAccess': return Maps\AuthorityInfoAccessSyntax::MAP; case 'id-ce-subjectAltName': return Maps\SubjectAltName::MAP; case 'id-ce-subjectDirectoryAttributes': return Maps\SubjectDirectoryAttributes::MAP; case 'id-ce-privateKeyUsagePeriod': return Maps\PrivateKeyUsagePeriod::MAP; case 'id-ce-issuerAltName': return Maps\IssuerAltName::MAP; case 'id-ce-policyMappings': return Maps\PolicyMappings::MAP; case 'id-ce-nameConstraints': return Maps\NameConstraints::MAP; case 'netscape-cert-type': return Maps\netscape_cert_type::MAP; case 'netscape-comment': return Maps\netscape_comment::MAP; case 'netscape-ca-policy-url': return Maps\netscape_ca_policy_url::MAP; // since id-qt-cps isn't a constructed type it will have already been decoded as a string by the time it gets // back around to asn1map() and we don't want it decoded again. //case 'id-qt-cps': // return Maps\CPSuri::MAP; case 'id-qt-unotice': return Maps\UserNotice::MAP; // the following OIDs are unsupported but we don't want them to give notices when calling saveX509(). case 'id-pe-logotype': // http://www.ietf.org/rfc/rfc3709.txt case 'entrustVersInfo': // http://support.microsoft.com/kb/287547 case '1.3.6.1.4.1.311.20.2': // szOID_ENROLL_CERTTYPE_EXTENSION case '1.3.6.1.4.1.311.21.1': // szOID_CERTSRV_CA_VERSION // "SET Secure Electronic Transaction Specification" // http://www.maithean.com/docs/set_bk3.pdf case '2.23.42.7.0': // id-set-hashedRootKey // "Certificate Transparency" // https://tools.ietf.org/html/rfc6962 case '1.3.6.1.4.1.11129.2.4.2': // "Qualified Certificate statements" // https://tools.ietf.org/html/rfc3739#section-3.2.6 case '1.3.6.1.5.5.7.1.3': return true; // CSR attributes case 'pkcs-9-at-unstructuredName': return Maps\PKCS9String::MAP; case 'pkcs-9-at-challengePassword': return Maps\DirectoryString::MAP; case 'pkcs-9-at-extensionRequest': return Maps\Extensions::MAP; // CRL extensions. case 'id-ce-cRLNumber': return Maps\CRLNumber::MAP; case 'id-ce-deltaCRLIndicator': return Maps\CRLNumber::MAP; case 'id-ce-issuingDistributionPoint': return Maps\IssuingDistributionPoint::MAP; case 'id-ce-freshestCRL': return Maps\CRLDistributionPoints::MAP; case 'id-ce-cRLReasons': return Maps\CRLReason::MAP; case 'id-ce-invalidityDate': return Maps\InvalidityDate::MAP; case 'id-ce-certificateIssuer': return Maps\CertificateIssuer::MAP; case 'id-ce-holdInstructionCode': return Maps\HoldInstructionCode::MAP; case 'id-at-postalAddress': return Maps\PostalAddress::MAP; } return false; } /** * Load an X.509 certificate as a certificate authority * * @param string $cert * @return bool */ public function loadCA($cert) { $olddn = $this->dn; $oldcert = $this->currentCert; $oldsigsubj = $this->signatureSubject; $oldkeyid = $this->currentKeyIdentifier; $cert = $this->loadX509($cert); if (!$cert) { $this->dn = $olddn; $this->currentCert = $oldcert; $this->signatureSubject = $oldsigsubj; $this->currentKeyIdentifier = $oldkeyid; return false; } /* From RFC5280 "PKIX Certificate and CRL Profile": If the keyUsage extension is present, then the subject public key MUST NOT be used to verify signatures on certificates or CRLs unless the corresponding keyCertSign or cRLSign bit is set. */ //$keyUsage = $this->getExtension('id-ce-keyUsage'); //if ($keyUsage && !in_array('keyCertSign', $keyUsage)) { // return false; //} /* From RFC5280 "PKIX Certificate and CRL Profile": The cA boolean indicates whether the certified public key may be used to verify certificate signatures. If the cA boolean is not asserted, then the keyCertSign bit in the key usage extension MUST NOT be asserted. If the basic constraints extension is not present in a version 3 certificate, or the extension is present but the cA boolean is not asserted, then the certified public key MUST NOT be used to verify certificate signatures. */ //$basicConstraints = $this->getExtension('id-ce-basicConstraints'); //if (!$basicConstraints || !$basicConstraints['cA']) { // return false; //} $this->CAs[] = $cert; $this->dn = $olddn; $this->currentCert = $oldcert; $this->signatureSubject = $oldsigsubj; return true; } /** * Validate an X.509 certificate against a URL * * From RFC2818 "HTTP over TLS": * * Matching is performed using the matching rules specified by * [RFC2459]. If more than one identity of a given type is present in * the certificate (e.g., more than one dNSName name, a match in any one * of the set is considered acceptable.) Names may contain the wildcard * character * which is considered to match any single domain name * component or component fragment. E.g., *.a.com matches foo.a.com but * not bar.foo.a.com. f*.com matches foo.com but not bar.com. * * @param string $url * @return bool */ public function validateURL($url) { if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { return false; } $components = parse_url($url); if (!isset($components['host'])) { return false; } if ($names = $this->getExtension('id-ce-subjectAltName')) { foreach ($names as $name) { foreach ($name as $key => $value) { $value = preg_quote($value); $value = str_replace('\*', '[^.]*', $value); switch ($key) { case 'dNSName': /* From RFC2818 "HTTP over TLS": If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead. */ if (preg_match('#^' . $value . '$#', $components['host'])) { return true; } break; case 'iPAddress': /* From RFC2818 "HTTP over TLS": In some cases, the URI is specified as an IP address rather than a hostname. In this case, the iPAddress subjectAltName must be present in the certificate and must exactly match the IP in the URI. */ if (preg_match('#(?:\d{1-3}\.){4}#', $components['host'] . '.') && preg_match('#^' . $value . '$#', $components['host'])) { return true; } } } } return false; } if ($value = $this->getDNProp('id-at-commonName')) { $value = str_replace(['.', '*'], ['\.', '[^.]*'], $value[0]); return preg_match('#^' . $value . '$#', $components['host']) === 1; } return false; } /** * Validate a date * * If $date isn't defined it is assumed to be the current date. * * @param \DateTimeInterface|string $date optional * @return bool */ public function validateDate($date = null) { if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { return false; } if (!isset($date)) { $date = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); } $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore']; $notBefore = isset($notBefore['generalTime']) ? $notBefore['generalTime'] : $notBefore['utcTime']; $notAfter = $this->currentCert['tbsCertificate']['validity']['notAfter']; $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime']; if (is_string($date)) { $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get())); } $notBefore = new \DateTimeImmutable($notBefore, new \DateTimeZone(@date_default_timezone_get())); $notAfter = new \DateTimeImmutable($notAfter, new \DateTimeZone(@date_default_timezone_get())); return $date >= $notBefore && $date <= $notAfter; } /** * Fetches a URL * * @param string $url * @return bool|string */ private static function fetchURL($url) { if (self::$disable_url_fetch) { return false; } $parts = parse_url($url); $data = ''; switch ($parts['scheme']) { case 'http': $fsock = @fsockopen($parts['host'], isset($parts['port']) ? $parts['port'] : 80); if (!$fsock) { return false; } $path = $parts['path']; if (isset($parts['query'])) { $path .= '?' . $parts['query']; } fputs($fsock, "GET $path HTTP/1.0\r\n"); fputs($fsock, "Host: $parts[host]\r\n\r\n"); $line = fgets($fsock, 1024); if (strlen($line) < 3) { return false; } preg_match('#HTTP/1.\d (\d{3})#', $line, $temp); if ($temp[1] != '200') { return false; } // skip the rest of the headers in the http response while (!feof($fsock) && fgets($fsock, 1024) != "\r\n") { } while (!feof($fsock)) { $temp = fread($fsock, 1024); if ($temp === false) { return false; } $data .= $temp; } break; //case 'ftp': //case 'ldap': //default: } return $data; } /** * Validates an intermediate cert as identified via authority info access extension * * See https://tools.ietf.org/html/rfc4325 for more info * * @param bool $caonly * @param int $count * @return bool */ private function testForIntermediate($caonly, $count) { $opts = $this->getExtension('id-pe-authorityInfoAccess'); if (!is_array($opts)) { return false; } foreach ($opts as $opt) { if ($opt['accessMethod'] == 'id-ad-caIssuers') { // accessLocation is a GeneralName. GeneralName fields support stuff like email addresses, IP addresses, LDAP, // etc, but we're only supporting URI's. URI's and LDAP are the only thing https://tools.ietf.org/html/rfc4325 // discusses if (isset($opt['accessLocation']['uniformResourceIdentifier'])) { $url = $opt['accessLocation']['uniformResourceIdentifier']; break; } } } if (!isset($url)) { return false; } $cert = static::fetchURL($url); if (!is_string($cert)) { return false; } $parent = new static(); $parent->CAs = $this->CAs; /* "Conforming applications that support HTTP or FTP for accessing certificates MUST be able to accept .cer files and SHOULD be able to accept .p7c files." -- https://tools.ietf.org/html/rfc4325 A .p7c file is 'a "certs-only" CMS message as specified in RFC 2797" These are currently unsupported */ if (!is_array($parent->loadX509($cert))) { return false; } if (!$parent->validateSignatureCountable($caonly, ++$count)) { return false; } $this->CAs[] = $parent->currentCert; //$this->loadCA($cert); return true; } /** * Validate a signature * * Works on X.509 certs, CSR's and CRL's. * Returns true if the signature is verified, false if it is not correct or null on error * * By default returns false for self-signed certs. Call validateSignature(false) to make this support * self-signed. * * The behavior of this function is inspired by {@link http://php.net/openssl-verify openssl_verify}. * * @param bool $caonly optional * @return mixed */ public function validateSignature($caonly = true) { return $this->validateSignatureCountable($caonly, 0); } /** * Validate a signature * * Performs said validation whilst keeping track of how many times validation method is called * * @param bool $caonly * @param int $count * @return mixed */ private function validateSignatureCountable($caonly, $count) { if (!is_array($this->currentCert) || !isset($this->signatureSubject)) { return null; } if ($count == self::$recur_limit) { return false; } /* TODO: "emailAddress attribute values are not case-sensitive (e.g., "subscriber@example.com" is the same as "SUBSCRIBER@EXAMPLE.COM")." -- http://tools.ietf.org/html/rfc5280#section-4.1.2.6 implement pathLenConstraint in the id-ce-basicConstraints extension */ switch (true) { case isset($this->currentCert['tbsCertificate']): // self-signed cert switch (true) { case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $this->currentCert['tbsCertificate']['subject']: case defined('FILE_X509_IGNORE_TYPE') && $this->getIssuerDN(self::DN_STRING) === $this->getDN(self::DN_STRING): $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier'); switch (true) { case !is_array($authorityKey): case !$subjectKeyID: case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: $signingCert = $this->currentCert; // working cert } } if (!empty($this->CAs)) { for ($i = 0; $i < count($this->CAs); $i++) { // even if the cert is a self-signed one we still want to see if it's a CA; // if not, we'll conditionally return an error $ca = $this->CAs[$i]; switch (true) { case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']: case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertificate']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']): $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); switch (true) { case !is_array($authorityKey): case !$subjectKeyID: case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) { break 2; // serial mismatch - check other ca } $signingCert = $ca; // working cert break 3; } } } if (count($this->CAs) == $i && $caonly) { return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly); } } elseif (!isset($signingCert) || $caonly) { return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly); } return $this->validateSignatureHelper( $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $this->currentCert['signatureAlgorithm']['algorithm'], substr($this->currentCert['signature'], 1), $this->signatureSubject ); case isset($this->currentCert['certificationRequestInfo']): return $this->validateSignatureHelper( $this->currentCert['certificationRequestInfo']['subjectPKInfo']['algorithm']['algorithm'], $this->currentCert['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], $this->currentCert['signatureAlgorithm']['algorithm'], substr($this->currentCert['signature'], 1), $this->signatureSubject ); case isset($this->currentCert['publicKeyAndChallenge']): return $this->validateSignatureHelper( $this->currentCert['publicKeyAndChallenge']['spki']['algorithm']['algorithm'], $this->currentCert['publicKeyAndChallenge']['spki']['subjectPublicKey'], $this->currentCert['signatureAlgorithm']['algorithm'], substr($this->currentCert['signature'], 1), $this->signatureSubject ); case isset($this->currentCert['tbsCertList']): if (!empty($this->CAs)) { for ($i = 0; $i < count($this->CAs); $i++) { $ca = $this->CAs[$i]; switch (true) { case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertList']['issuer'] === $ca['tbsCertificate']['subject']: case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertList']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']): $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); switch (true) { case !is_array($authorityKey): case !$subjectKeyID: case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) { break 2; // serial mismatch - check other ca } $signingCert = $ca; // working cert break 3; } } } } if (!isset($signingCert)) { return false; } return $this->validateSignatureHelper( $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $this->currentCert['signatureAlgorithm']['algorithm'], substr($this->currentCert['signature'], 1), $this->signatureSubject ); default: return false; } } /** * Validates a signature * * Returns true if the signature is verified and false if it is not correct. * If the algorithms are unsupposed an exception is thrown. * * @param string $publicKeyAlgorithm * @param string $publicKey * @param string $signatureAlgorithm * @param string $signature * @param string $signatureSubject * @throws UnsupportedAlgorithmException if the algorithm is unsupported * @return bool */ private function validateSignatureHelper($publicKeyAlgorithm, $publicKey, $signatureAlgorithm, $signature, $signatureSubject) { switch ($publicKeyAlgorithm) { case 'id-RSASSA-PSS': $key = RSA::loadFormat('PSS', $publicKey); break; case 'rsaEncryption': $key = RSA::loadFormat('PKCS8', $publicKey); switch ($signatureAlgorithm) { case 'id-RSASSA-PSS': break; case 'md2WithRSAEncryption': case 'md5WithRSAEncryption': case 'sha1WithRSAEncryption': case 'sha224WithRSAEncryption': case 'sha256WithRSAEncryption': case 'sha384WithRSAEncryption': case 'sha512WithRSAEncryption': $key = $key ->withHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm)) ->withPadding(RSA::SIGNATURE_PKCS1); break; default: throw new UnsupportedAlgorithmException('Signature algorithm unsupported'); } break; case 'id-Ed25519': case 'id-Ed448': $key = EC::loadFormat('PKCS8', $publicKey); break; case 'id-ecPublicKey': $key = EC::loadFormat('PKCS8', $publicKey); switch ($signatureAlgorithm) { case 'ecdsa-with-SHA1': case 'ecdsa-with-SHA224': case 'ecdsa-with-SHA256': case 'ecdsa-with-SHA384': case 'ecdsa-with-SHA512': $key = $key ->withHash(preg_replace('#^ecdsa-with-#', '', strtolower($signatureAlgorithm))); break; default: throw new UnsupportedAlgorithmException('Signature algorithm unsupported'); } break; case 'id-dsa': $key = DSA::loadFormat('PKCS8', $publicKey); switch ($signatureAlgorithm) { case 'id-dsa-with-sha1': case 'id-dsa-with-sha224': case 'id-dsa-with-sha256': $key = $key ->withHash(preg_replace('#^id-dsa-with-#', '', strtolower($signatureAlgorithm))); break; default: throw new UnsupportedAlgorithmException('Signature algorithm unsupported'); } break; default: throw new UnsupportedAlgorithmException('Public key algorithm unsupported'); } return $key->verify($signatureSubject, $signature); } /** * Sets the recursion limit * * When validating a signature it may be necessary to download intermediate certs from URI's. * An intermediate cert that linked to itself would result in an infinite loop so to prevent * that we set a recursion limit. A negative number means that there is no recursion limit. * * @param int $count */ public static function setRecurLimit($count) { self::$recur_limit = $count; } /** * Prevents URIs from being automatically retrieved * */ public static function disableURLFetch() { self::$disable_url_fetch = true; } /** * Allows URIs to be automatically retrieved * */ public static function enableURLFetch() { self::$disable_url_fetch = false; } /** * Decodes an IP address * * Takes in a base64 encoded "blob" and returns a human readable IP address * * @param string $ip * @return string */ public static function decodeIP($ip) { return inet_ntop($ip); } /** * Decodes an IP address in a name constraints extension * * Takes in a base64 encoded "blob" and returns a human readable IP address / mask * * @param string $ip * @return array */ public static function decodeNameConstraintIP($ip) { $size = strlen($ip) >> 1; $mask = substr($ip, $size); $ip = substr($ip, 0, $size); return [inet_ntop($ip), inet_ntop($mask)]; } /** * Encodes an IP address * * Takes a human readable IP address into a base64-encoded "blob" * * @param string|array $ip * @return string */ public static function encodeIP($ip) { return is_string($ip) ? inet_pton($ip) : inet_pton($ip[0]) . inet_pton($ip[1]); } /** * "Normalizes" a Distinguished Name property * * @param string $propName * @return mixed */ private function translateDNProp($propName) { switch (strtolower($propName)) { case 'jurisdictionofincorporationcountryname': case 'jurisdictioncountryname': case 'jurisdictionc': return 'jurisdictionOfIncorporationCountryName'; case 'jurisdictionofincorporationstateorprovincename': case 'jurisdictionstateorprovincename': case 'jurisdictionst': return 'jurisdictionOfIncorporationStateOrProvinceName'; case 'jurisdictionlocalityname': case 'jurisdictionl': return 'jurisdictionLocalityName'; case 'id-at-businesscategory': case 'businesscategory': return 'id-at-businessCategory'; case 'id-at-countryname': case 'countryname': case 'c': return 'id-at-countryName'; case 'id-at-organizationname': case 'organizationname': case 'o': return 'id-at-organizationName'; case 'id-at-dnqualifier': case 'dnqualifier': return 'id-at-dnQualifier'; case 'id-at-commonname': case 'commonname': case 'cn': return 'id-at-commonName'; case 'id-at-stateorprovincename': case 'stateorprovincename': case 'state': case 'province': case 'provincename': case 'st': return 'id-at-stateOrProvinceName'; case 'id-at-localityname': case 'localityname': case 'l': return 'id-at-localityName'; case 'id-emailaddress': case 'emailaddress': return 'pkcs-9-at-emailAddress'; case 'id-at-serialnumber': case 'serialnumber': return 'id-at-serialNumber'; case 'id-at-postalcode': case 'postalcode': return 'id-at-postalCode'; case 'id-at-streetaddress': case 'streetaddress': return 'id-at-streetAddress'; case 'id-at-name': case 'name': return 'id-at-name'; case 'id-at-givenname': case 'givenname': return 'id-at-givenName'; case 'id-at-surname': case 'surname': case 'sn': return 'id-at-surname'; case 'id-at-initials': case 'initials': return 'id-at-initials'; case 'id-at-generationqualifier': case 'generationqualifier': return 'id-at-generationQualifier'; case 'id-at-organizationalunitname': case 'organizationalunitname': case 'ou': return 'id-at-organizationalUnitName'; case 'id-at-pseudonym': case 'pseudonym': return 'id-at-pseudonym'; case 'id-at-title': case 'title': return 'id-at-title'; case 'id-at-description': case 'description': return 'id-at-description'; case 'id-at-role': case 'role': return 'id-at-role'; case 'id-at-uniqueidentifier': case 'uniqueidentifier': case 'x500uniqueidentifier': return 'id-at-uniqueIdentifier'; case 'postaladdress': case 'id-at-postaladdress': return 'id-at-postalAddress'; default: return false; } } /** * Set a Distinguished Name property * * @param string $propName * @param mixed $propValue * @param string $type optional * @return bool */ public function setDNProp($propName, $propValue, $type = 'utf8String') { if (empty($this->dn)) { $this->dn = ['rdnSequence' => []]; } if (($propName = $this->translateDNProp($propName)) === false) { return false; } foreach ((array) $propValue as $v) { if (!is_array($v) && isset($type)) { $v = [$type => $v]; } $this->dn['rdnSequence'][] = [ [ 'type' => $propName, 'value' => $v ] ]; } return true; } /** * Remove Distinguished Name properties * * @param string $propName */ public function removeDNProp($propName) { if (empty($this->dn)) { return; } if (($propName = $this->translateDNProp($propName)) === false) { return; } $dn = &$this->dn['rdnSequence']; $size = count($dn); for ($i = 0; $i < $size; $i++) { if ($dn[$i][0]['type'] == $propName) { unset($dn[$i]); } } $dn = array_values($dn); // fix for https://bugs.php.net/75433 affecting PHP 7.2 if (!isset($dn[0])) { $dn = array_splice($dn, 0, 0); } } /** * Get Distinguished Name properties * * @param string $propName * @param array $dn optional * @param bool $withType optional * @return mixed */ public function getDNProp($propName, $dn = null, $withType = false) { if (!isset($dn)) { $dn = $this->dn; } if (empty($dn)) { return false; } if (($propName = $this->translateDNProp($propName)) === false) { return false; } $filters = []; $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; ASN1::setFilters($filters); $this->mapOutDNs($dn, 'rdnSequence'); $dn = $dn['rdnSequence']; $result = []; for ($i = 0; $i < count($dn); $i++) { if ($dn[$i][0]['type'] == $propName) { $v = $dn[$i][0]['value']; if (!$withType) { if (is_array($v)) { foreach ($v as $type => $s) { $type = array_search($type, ASN1::ANY_MAP); if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) { $s = ASN1::convert($s, $type); if ($s !== false) { $v = $s; break; } } } if (is_array($v)) { $v = array_pop($v); // Always strip data type. } } elseif (is_object($v) && $v instanceof Element) { $map = $this->getMapping($propName); if (!is_bool($map)) { $decoded = ASN1::decodeBER($v); if (!$decoded) { return false; } $v = ASN1::asn1map($decoded[0], $map); } } } $result[] = $v; } } return $result; } /** * Set a Distinguished Name * * @param mixed $dn * @param bool $merge optional * @param string $type optional * @return bool */ public function setDN($dn, $merge = false, $type = 'utf8String') { if (!$merge) { $this->dn = null; } if (is_array($dn)) { if (isset($dn['rdnSequence'])) { $this->dn = $dn; // No merge here. return true; } // handles stuff generated by openssl_x509_parse() foreach ($dn as $prop => $value) { if (!$this->setDNProp($prop, $value, $type)) { return false; } } return true; } // handles everything else $results = preg_split('#((?:^|, *|/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|x500UniqueIdentifier=|postalAddress=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); for ($i = 1; $i < count($results); $i += 2) { $prop = trim($results[$i], ', =/'); $value = $results[$i + 1]; if (!$this->setDNProp($prop, $value, $type)) { return false; } } return true; } /** * Get the Distinguished Name for a certificates subject * * @param mixed $format optional * @param array $dn optional * @return array|bool|string */ public function getDN($format = self::DN_ARRAY, $dn = null) { if (!isset($dn)) { $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn; } switch ((int) $format) { case self::DN_ARRAY: return $dn; case self::DN_ASN1: $filters = []; $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; ASN1::setFilters($filters); $this->mapOutDNs($dn, 'rdnSequence'); return ASN1::encodeDER($dn, Maps\Name::MAP); case self::DN_CANON: // No SEQUENCE around RDNs and all string values normalized as // trimmed lowercase UTF-8 with all spacing as one blank. // constructed RDNs will not be canonicalized $filters = []; $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; ASN1::setFilters($filters); $result = ''; $this->mapOutDNs($dn, 'rdnSequence'); foreach ($dn['rdnSequence'] as $rdn) { foreach ($rdn as $i => $attr) { $attr = &$rdn[$i]; if (is_array($attr['value'])) { foreach ($attr['value'] as $type => $v) { $type = array_search($type, ASN1::ANY_MAP, true); if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) { $v = ASN1::convert($v, $type); if ($v !== false) { $v = preg_replace('/\s+/', ' ', $v); $attr['value'] = strtolower(trim($v)); break; } } } } } $result .= ASN1::encodeDER($rdn, Maps\RelativeDistinguishedName::MAP); } return $result; case self::DN_HASH: $dn = $this->getDN(self::DN_CANON, $dn); $hash = new Hash('sha1'); $hash = $hash->hash($dn); extract(unpack('Vhash', $hash)); return strtolower(Strings::bin2hex(pack('N', $hash))); } // Default is to return a string. $start = true; $output = ''; $result = []; $filters = []; $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; ASN1::setFilters($filters); $this->mapOutDNs($dn, 'rdnSequence'); foreach ($dn['rdnSequence'] as $field) { $prop = $field[0]['type']; $value = $field[0]['value']; $delim = ', '; switch ($prop) { case 'id-at-countryName': $desc = 'C'; break; case 'id-at-stateOrProvinceName': $desc = 'ST'; break; case 'id-at-organizationName': $desc = 'O'; break; case 'id-at-organizationalUnitName': $desc = 'OU'; break; case 'id-at-commonName': $desc = 'CN'; break; case 'id-at-localityName': $desc = 'L'; break; case 'id-at-surname': $desc = 'SN'; break; case 'id-at-uniqueIdentifier': $delim = '/'; $desc = 'x500UniqueIdentifier'; break; case 'id-at-postalAddress': $delim = '/'; $desc = 'postalAddress'; break; default: $delim = '/'; $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop); } if (!$start) { $output .= $delim; } if (is_array($value)) { foreach ($value as $type => $v) { $type = array_search($type, ASN1::ANY_MAP, true); if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) { $v = ASN1::convert($v, $type); if ($v !== false) { $value = $v; break; } } } if (is_array($value)) { $value = array_pop($value); // Always strip data type. } } elseif (is_object($value) && $value instanceof Element) { $callback = function ($x) { return '\x' . bin2hex($x[0]); }; $value = strtoupper(preg_replace_callback('#[^\x20-\x7E]#', $callback, $value->element)); } $output .= $desc . '=' . $value; $result[$desc] = isset($result[$desc]) ? array_merge((array) $result[$desc], [$value]) : $value; $start = false; } return $format == self::DN_OPENSSL ? $result : $output; } /** * Get the Distinguished Name for a certificate/crl issuer * * @param int $format optional * @return mixed */ public function getIssuerDN($format = self::DN_ARRAY) { switch (true) { case !isset($this->currentCert) || !is_array($this->currentCert): break; case isset($this->currentCert['tbsCertificate']): return $this->getDN($format, $this->currentCert['tbsCertificate']['issuer']); case isset($this->currentCert['tbsCertList']): return $this->getDN($format, $this->currentCert['tbsCertList']['issuer']); } return false; } /** * Get the Distinguished Name for a certificate/csr subject * Alias of getDN() * * @param int $format optional * @return mixed */ public function getSubjectDN($format = self::DN_ARRAY) { switch (true) { case !empty($this->dn): return $this->getDN($format); case !isset($this->currentCert) || !is_array($this->currentCert): break; case isset($this->currentCert['tbsCertificate']): return $this->getDN($format, $this->currentCert['tbsCertificate']['subject']); case isset($this->currentCert['certificationRequestInfo']): return $this->getDN($format, $this->currentCert['certificationRequestInfo']['subject']); } return false; } /** * Get an individual Distinguished Name property for a certificate/crl issuer * * @param string $propName * @param bool $withType optional * @return mixed */ public function getIssuerDNProp($propName, $withType = false) { switch (true) { case !isset($this->currentCert) || !is_array($this->currentCert): break; case isset($this->currentCert['tbsCertificate']): return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType); case isset($this->currentCert['tbsCertList']): return $this->getDNProp($propName, $this->currentCert['tbsCertList']['issuer'], $withType); } return false; } /** * Get an individual Distinguished Name property for a certificate/csr subject * * @param string $propName * @param bool $withType optional * @return mixed */ public function getSubjectDNProp($propName, $withType = false) { switch (true) { case !empty($this->dn): return $this->getDNProp($propName, null, $withType); case !isset($this->currentCert) || !is_array($this->currentCert): break; case isset($this->currentCert['tbsCertificate']): return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['subject'], $withType); case isset($this->currentCert['certificationRequestInfo']): return $this->getDNProp($propName, $this->currentCert['certificationRequestInfo']['subject'], $withType); } return false; } /** * Get the certificate chain for the current cert * * @return mixed */ public function getChain() { $chain = [$this->currentCert]; if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { return false; } while (true) { $currentCert = $chain[count($chain) - 1]; for ($i = 0; $i < count($this->CAs); $i++) { $ca = $this->CAs[$i]; if ($currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']) { $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier', $currentCert); $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); switch (true) { case !is_array($authorityKey): case is_array($authorityKey) && isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: if ($currentCert === $ca) { break 3; } $chain[] = $ca; break 2; } } } if ($i == count($this->CAs)) { break; } } foreach ($chain as $key => $value) { $chain[$key] = new X509(); $chain[$key]->loadX509($value); } return $chain; } /** * Returns the current cert * * @return array|bool */ public function &getCurrentCert() { return $this->currentCert; } /** * Set public key * * Key needs to be a \phpseclib3\Crypt\RSA object * * @param PublicKey $key * @return void */ public function setPublicKey(PublicKey $key) { $this->publicKey = $key; } /** * Set private key * * Key needs to be a \phpseclib3\Crypt\RSA object * * @param PrivateKey $key */ public function setPrivateKey(PrivateKey $key) { $this->privateKey = $key; } /** * Set challenge * * Used for SPKAC CSR's * * @param string $challenge */ public function setChallenge($challenge) { $this->challenge = $challenge; } /** * Gets the public key * * Returns a \phpseclib3\Crypt\RSA object or a false. * * @return mixed */ public function getPublicKey() { if (isset($this->publicKey)) { return $this->publicKey; } if (isset($this->currentCert) && is_array($this->currentCert)) { $paths = [ 'tbsCertificate/subjectPublicKeyInfo', 'certificationRequestInfo/subjectPKInfo', 'publicKeyAndChallenge/spki' ]; foreach ($paths as $path) { $keyinfo = $this->subArray($this->currentCert, $path); if (!empty($keyinfo)) { break; } } } if (empty($keyinfo)) { return false; } $key = $keyinfo['subjectPublicKey']; switch ($keyinfo['algorithm']['algorithm']) { case 'id-RSASSA-PSS': return RSA::loadFormat('PSS', $key); case 'rsaEncryption': return RSA::loadFormat('PKCS8', $key)->withPadding(RSA::SIGNATURE_PKCS1); case 'id-ecPublicKey': case 'id-Ed25519': case 'id-Ed448': return EC::loadFormat('PKCS8', $key); case 'id-dsa': return DSA::loadFormat('PKCS8', $key); } return false; } /** * Load a Certificate Signing Request * * @param string $csr * @param int $mode * @return mixed */ public function loadCSR($csr, $mode = self::FORMAT_AUTO_DETECT) { if (is_array($csr) && isset($csr['certificationRequestInfo'])) { unset($this->currentCert); unset($this->currentKeyIdentifier); unset($this->signatureSubject); $this->dn = $csr['certificationRequestInfo']['subject']; if (!isset($this->dn)) { return false; } $this->currentCert = $csr; return $csr; } // see http://tools.ietf.org/html/rfc2986 if ($mode != self::FORMAT_DER) { $newcsr = ASN1::extractBER($csr); if ($mode == self::FORMAT_PEM && $csr == $newcsr) { return false; } $csr = $newcsr; } $orig = $csr; if ($csr === false) { $this->currentCert = false; return false; } $decoded = ASN1::decodeBER($csr); if (!$decoded) { $this->currentCert = false; return false; } $csr = ASN1::asn1map($decoded[0], Maps\CertificationRequest::MAP); if (!isset($csr) || $csr === false) { $this->currentCert = false; return false; } $this->mapInAttributes($csr, 'certificationRequestInfo/attributes'); $this->mapInDNs($csr, 'certificationRequestInfo/subject/rdnSequence'); $this->dn = $csr['certificationRequestInfo']['subject']; $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); $key = $csr['certificationRequestInfo']['subjectPKInfo']; $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP); $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'] = "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(base64_encode($key), 64) . "-----END PUBLIC KEY-----"; $this->currentKeyIdentifier = null; $this->currentCert = $csr; $this->publicKey = null; $this->publicKey = $this->getPublicKey(); return $csr; } /** * Save CSR request * * @param array $csr * @param int $format optional * @return string */ public function saveCSR(array $csr, $format = self::FORMAT_PEM) { if (!is_array($csr) || !isset($csr['certificationRequestInfo'])) { return false; } switch (true) { case !($algorithm = $this->subArray($csr, 'certificationRequestInfo/subjectPKInfo/algorithm/algorithm')): case is_object($csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']): break; default: $csr['certificationRequestInfo']['subjectPKInfo'] = new Element( base64_decode(preg_replace('#-.+-|[\r\n]#', '', $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'])) ); } $filters = []; $filters['certificationRequestInfo']['subject']['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; ASN1::setFilters($filters); $this->mapOutDNs($csr, 'certificationRequestInfo/subject/rdnSequence'); $this->mapOutAttributes($csr, 'certificationRequestInfo/attributes'); $csr = ASN1::encodeDER($csr, Maps\CertificationRequest::MAP); switch ($format) { case self::FORMAT_DER: return $csr; // case self::FORMAT_PEM: default: return "-----BEGIN CERTIFICATE REQUEST-----\r\n" . chunk_split(Strings::base64_encode($csr), 64) . '-----END CERTIFICATE REQUEST-----'; } } /** * Load a SPKAC CSR * * SPKAC's are produced by the HTML5 keygen element: * * https://developer.mozilla.org/en-US/docs/HTML/Element/keygen * * @param string $spkac * @return mixed */ public function loadSPKAC($spkac) { if (is_array($spkac) && isset($spkac['publicKeyAndChallenge'])) { unset($this->currentCert); unset($this->currentKeyIdentifier); unset($this->signatureSubject); $this->currentCert = $spkac; return $spkac; } // see http://www.w3.org/html/wg/drafts/html/master/forms.html#signedpublickeyandchallenge // OpenSSL produces SPKAC's that are preceded by the string SPKAC= $temp = preg_replace('#(?:SPKAC=)|[ \r\n\\\]#', '', $spkac); $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? Strings::base64_decode($temp) : false; if ($temp != false) { $spkac = $temp; } $orig = $spkac; if ($spkac === false) { $this->currentCert = false; return false; } $decoded = ASN1::decodeBER($spkac); if (!$decoded) { $this->currentCert = false; return false; } $spkac = ASN1::asn1map($decoded[0], Maps\SignedPublicKeyAndChallenge::MAP); if (!isset($spkac) || !is_array($spkac)) { $this->currentCert = false; return false; } $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); $key = $spkac['publicKeyAndChallenge']['spki']; $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP); $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'] = "-----BEGIN PUBLIC KEY-----\r\n" . chunk_split(base64_encode($key), 64) . "-----END PUBLIC KEY-----"; $this->currentKeyIdentifier = null; $this->currentCert = $spkac; $this->publicKey = null; $this->publicKey = $this->getPublicKey(); return $spkac; } /** * Save a SPKAC CSR request * * @param array $spkac * @param int $format optional * @return string */ public function saveSPKAC(array $spkac, $format = self::FORMAT_PEM) { if (!is_array($spkac) || !isset($spkac['publicKeyAndChallenge'])) { return false; } $algorithm = $this->subArray($spkac, 'publicKeyAndChallenge/spki/algorithm/algorithm'); switch (true) { case !$algorithm: case is_object($spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']): break; default: $spkac['publicKeyAndChallenge']['spki'] = new Element( base64_decode(preg_replace('#-.+-|[\r\n]#', '', $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'])) ); } $spkac = ASN1::encodeDER($spkac, Maps\SignedPublicKeyAndChallenge::MAP); switch ($format) { case self::FORMAT_DER: return $spkac; // case self::FORMAT_PEM: default: // OpenSSL's implementation of SPKAC requires the SPKAC be preceded by SPKAC= and since there are pretty much // no other SPKAC decoders phpseclib will use that same format return 'SPKAC=' . Strings::base64_encode($spkac); } } /** * Load a Certificate Revocation List * * @param string $crl * @param int $mode * @return mixed */ public function loadCRL($crl, $mode = self::FORMAT_AUTO_DETECT) { if (is_array($crl) && isset($crl['tbsCertList'])) { $this->currentCert = $crl; unset($this->signatureSubject); return $crl; } if ($mode != self::FORMAT_DER) { $newcrl = ASN1::extractBER($crl); if ($mode == self::FORMAT_PEM && $crl == $newcrl) { return false; } $crl = $newcrl; } $orig = $crl; if ($crl === false) { $this->currentCert = false; return false; } $decoded = ASN1::decodeBER($crl); if (!$decoded) { $this->currentCert = false; return false; } $crl = ASN1::asn1map($decoded[0], Maps\CertificateList::MAP); if (!isset($crl) || $crl === false) { $this->currentCert = false; return false; } $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); $this->mapInDNs($crl, 'tbsCertList/issuer/rdnSequence'); if ($this->isSubArrayValid($crl, 'tbsCertList/crlExtensions')) { $this->mapInExtensions($crl, 'tbsCertList/crlExtensions'); } if ($this->isSubArrayValid($crl, 'tbsCertList/revokedCertificates')) { $rclist_ref = &$this->subArrayUnchecked($crl, 'tbsCertList/revokedCertificates'); if ($rclist_ref) { $rclist = $crl['tbsCertList']['revokedCertificates']; foreach ($rclist as $i => $extension) { if ($this->isSubArrayValid($rclist, "$i/crlEntryExtensions")) { $this->mapInExtensions($rclist_ref, "$i/crlEntryExtensions"); } } } } $this->currentKeyIdentifier = null; $this->currentCert = $crl; return $crl; } /** * Save Certificate Revocation List. * * @param array $crl * @param int $format optional * @return string */ public function saveCRL(array $crl, $format = self::FORMAT_PEM) { if (!is_array($crl) || !isset($crl['tbsCertList'])) { return false; } $filters = []; $filters['tbsCertList']['issuer']['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; $filters['tbsCertList']['signature']['parameters'] = ['type' => ASN1::TYPE_UTF8_STRING]; $filters['signatureAlgorithm']['parameters'] = ['type' => ASN1::TYPE_UTF8_STRING]; if (empty($crl['tbsCertList']['signature']['parameters'])) { $filters['tbsCertList']['signature']['parameters'] = ['type' => ASN1::TYPE_NULL]; } if (empty($crl['signatureAlgorithm']['parameters'])) { $filters['signatureAlgorithm']['parameters'] = ['type' => ASN1::TYPE_NULL]; } ASN1::setFilters($filters); $this->mapOutDNs($crl, 'tbsCertList/issuer/rdnSequence'); $this->mapOutExtensions($crl, 'tbsCertList/crlExtensions'); $rclist = &$this->subArray($crl, 'tbsCertList/revokedCertificates'); if (is_array($rclist)) { foreach ($rclist as $i => $extension) { $this->mapOutExtensions($rclist, "$i/crlEntryExtensions"); } } $crl = ASN1::encodeDER($crl, Maps\CertificateList::MAP); switch ($format) { case self::FORMAT_DER: return $crl; // case self::FORMAT_PEM: default: return "-----BEGIN X509 CRL-----\r\n" . chunk_split(Strings::base64_encode($crl), 64) . '-----END X509 CRL-----'; } } /** * Helper function to build a time field according to RFC 3280 section * - 4.1.2.5 Validity * - 5.1.2.4 This Update * - 5.1.2.5 Next Update * - 5.1.2.6 Revoked Certificates * by choosing utcTime iff year of date given is before 2050 and generalTime else. * * @param string $date in format date('D, d M Y H:i:s O') * @return array|Element */ private function timeField($date) { if ($date instanceof Element) { return $date; } $dateObj = new \DateTimeImmutable($date, new \DateTimeZone('GMT')); $year = $dateObj->format('Y'); // the same way ASN1.php parses this if ($year < 2050) { return ['utcTime' => $date]; } else { return ['generalTime' => $date]; } } /** * Sign an X.509 certificate * * $issuer's private key needs to be loaded. * $subject can be either an existing X.509 cert (if you want to resign it), * a CSR or something with the DN and public key explicitly set. * * @return mixed */ public function sign(X509 $issuer, X509 $subject) { if (!is_object($issuer->privateKey) || empty($issuer->dn)) { return false; } if (isset($subject->publicKey) && !($subjectPublicKey = $subject->formatSubjectPublicKey())) { return false; } $currentCert = isset($this->currentCert) ? $this->currentCert : null; $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey); if (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) { $this->currentCert = $subject->currentCert; $this->currentCert['tbsCertificate']['signature'] = $signatureAlgorithm; $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; if (!empty($this->startDate)) { $this->currentCert['tbsCertificate']['validity']['notBefore'] = $this->timeField($this->startDate); } if (!empty($this->endDate)) { $this->currentCert['tbsCertificate']['validity']['notAfter'] = $this->timeField($this->endDate); } if (!empty($this->serialNumber)) { $this->currentCert['tbsCertificate']['serialNumber'] = $this->serialNumber; } if (!empty($subject->dn)) { $this->currentCert['tbsCertificate']['subject'] = $subject->dn; } if (!empty($subject->publicKey)) { $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey; } $this->removeExtension('id-ce-authorityKeyIdentifier'); if (isset($subject->domains)) { $this->removeExtension('id-ce-subjectAltName'); } } elseif (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertList'])) { return false; } else { if (!isset($subject->publicKey)) { return false; } $startDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O'); $endDate = new \DateTimeImmutable('+1 year', new \DateTimeZone(@date_default_timezone_get())); $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O'); /* "The serial number MUST be a positive integer" "Conforming CAs MUST NOT use serialNumber values longer than 20 octets." -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2 for the integer to be positive the leading bit needs to be 0 hence the application of a bitmap */ $serialNumber = !empty($this->serialNumber) ? $this->serialNumber : new BigInteger(Random::string(20) & ("\x7F" . str_repeat("\xFF", 19)), 256); $this->currentCert = [ 'tbsCertificate' => [ 'version' => 'v3', 'serialNumber' => $serialNumber, // $this->setSerialNumber() 'signature' => $signatureAlgorithm, 'issuer' => false, // this is going to be overwritten later 'validity' => [ 'notBefore' => $this->timeField($startDate), // $this->setStartDate() 'notAfter' => $this->timeField($endDate) // $this->setEndDate() ], 'subject' => $subject->dn, 'subjectPublicKeyInfo' => $subjectPublicKey ], 'signatureAlgorithm' => $signatureAlgorithm, 'signature' => false // this is going to be overwritten later ]; // Copy extensions from CSR. $csrexts = $subject->getAttribute('pkcs-9-at-extensionRequest', 0); if (!empty($csrexts)) { $this->currentCert['tbsCertificate']['extensions'] = $csrexts; } } $this->currentCert['tbsCertificate']['issuer'] = $issuer->dn; if (isset($issuer->currentKeyIdentifier)) { $this->setExtension('id-ce-authorityKeyIdentifier', [ //'authorityCertIssuer' => array( // array( // 'directoryName' => $issuer->dn // ) //), 'keyIdentifier' => $issuer->currentKeyIdentifier ]); //$extensions = &$this->currentCert['tbsCertificate']['extensions']; //if (isset($issuer->serialNumber)) { // $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber; //} //unset($extensions); } if (isset($subject->currentKeyIdentifier)) { $this->setExtension('id-ce-subjectKeyIdentifier', $subject->currentKeyIdentifier); } $altName = []; if (isset($subject->domains) && count($subject->domains)) { $altName = array_map(['\phpseclib3\File\X509', 'dnsName'], $subject->domains); } if (isset($subject->ipAddresses) && count($subject->ipAddresses)) { // should an IP address appear as the CN if no domain name is specified? idk //$ips = count($subject->domains) ? $subject->ipAddresses : array_slice($subject->ipAddresses, 1); $ipAddresses = []; foreach ($subject->ipAddresses as $ipAddress) { $encoded = $subject->ipAddress($ipAddress); if ($encoded !== false) { $ipAddresses[] = $encoded; } } if (count($ipAddresses)) { $altName = array_merge($altName, $ipAddresses); } } if (!empty($altName)) { $this->setExtension('id-ce-subjectAltName', $altName); } if ($this->caFlag) { $keyUsage = $this->getExtension('id-ce-keyUsage'); if (!$keyUsage) { $keyUsage = []; } $this->setExtension( 'id-ce-keyUsage', array_values(array_unique(array_merge($keyUsage, ['cRLSign', 'keyCertSign']))) ); $basicConstraints = $this->getExtension('id-ce-basicConstraints'); if (!$basicConstraints) { $basicConstraints = []; } $this->setExtension( 'id-ce-basicConstraints', array_merge(['cA' => true], $basicConstraints), true ); if (!isset($subject->currentKeyIdentifier)) { $this->setExtension('id-ce-subjectKeyIdentifier', $this->computeKeyIdentifier($this->currentCert), false, false); } } // resync $this->signatureSubject // save $tbsCertificate in case there are any \phpseclib3\File\ASN1\Element objects in it $tbsCertificate = $this->currentCert['tbsCertificate']; $this->loadX509($this->saveX509($this->currentCert)); $result = $this->currentCert; $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject); $result['tbsCertificate'] = $tbsCertificate; $this->currentCert = $currentCert; $this->signatureSubject = $signatureSubject; return $result; } /** * Sign a CSR * * @return mixed */ public function signCSR() { if (!is_object($this->privateKey) || empty($this->dn)) { return false; } $origPublicKey = $this->publicKey; $this->publicKey = $this->privateKey->getPublicKey(); $publicKey = $this->formatSubjectPublicKey(); $this->publicKey = $origPublicKey; $currentCert = isset($this->currentCert) ? $this->currentCert : null; $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey); if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['certificationRequestInfo'])) { $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; if (!empty($this->dn)) { $this->currentCert['certificationRequestInfo']['subject'] = $this->dn; } $this->currentCert['certificationRequestInfo']['subjectPKInfo'] = $publicKey; } else { $this->currentCert = [ 'certificationRequestInfo' => [ 'version' => 'v1', 'subject' => $this->dn, 'subjectPKInfo' => $publicKey, 'attributes' => [] ], 'signatureAlgorithm' => $signatureAlgorithm, 'signature' => false // this is going to be overwritten later ]; } // resync $this->signatureSubject // save $certificationRequestInfo in case there are any \phpseclib3\File\ASN1\Element objects in it $certificationRequestInfo = $this->currentCert['certificationRequestInfo']; $this->loadCSR($this->saveCSR($this->currentCert)); $result = $this->currentCert; $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject); $result['certificationRequestInfo'] = $certificationRequestInfo; $this->currentCert = $currentCert; $this->signatureSubject = $signatureSubject; return $result; } /** * Sign a SPKAC * * @return mixed */ public function signSPKAC() { if (!is_object($this->privateKey)) { return false; } $origPublicKey = $this->publicKey; $this->publicKey = $this->privateKey->getPublicKey(); $publicKey = $this->formatSubjectPublicKey(); $this->publicKey = $origPublicKey; $currentCert = isset($this->currentCert) ? $this->currentCert : null; $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey); // re-signing a SPKAC seems silly but since everything else supports re-signing why not? if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['publicKeyAndChallenge'])) { $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; $this->currentCert['publicKeyAndChallenge']['spki'] = $publicKey; if (!empty($this->challenge)) { // the bitwise AND ensures that the output is a valid IA5String $this->currentCert['publicKeyAndChallenge']['challenge'] = $this->challenge & str_repeat("\x7F", strlen($this->challenge)); } } else { $this->currentCert = [ 'publicKeyAndChallenge' => [ 'spki' => $publicKey, // quoting , // "A challenge string that is submitted along with the public key. Defaults to an empty string if not specified." // both Firefox and OpenSSL ("openssl spkac -key private.key") behave this way // we could alternatively do this instead if we ignored the specs: // Random::string(8) & str_repeat("\x7F", 8) 'challenge' => !empty($this->challenge) ? $this->challenge : '' ], 'signatureAlgorithm' => $signatureAlgorithm, 'signature' => false // this is going to be overwritten later ]; } // resync $this->signatureSubject // save $publicKeyAndChallenge in case there are any \phpseclib3\File\ASN1\Element objects in it $publicKeyAndChallenge = $this->currentCert['publicKeyAndChallenge']; $this->loadSPKAC($this->saveSPKAC($this->currentCert)); $result = $this->currentCert; $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject); $result['publicKeyAndChallenge'] = $publicKeyAndChallenge; $this->currentCert = $currentCert; $this->signatureSubject = $signatureSubject; return $result; } /** * Sign a CRL * * $issuer's private key needs to be loaded. * * @return mixed */ public function signCRL(X509 $issuer, X509 $crl) { if (!is_object($issuer->privateKey) || empty($issuer->dn)) { return false; } $currentCert = isset($this->currentCert) ? $this->currentCert : null; $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey); $thisUpdate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O'); if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) { $this->currentCert = $crl->currentCert; $this->currentCert['tbsCertList']['signature'] = $signatureAlgorithm; $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; } else { $this->currentCert = [ 'tbsCertList' => [ 'version' => 'v2', 'signature' => $signatureAlgorithm, 'issuer' => false, // this is going to be overwritten later 'thisUpdate' => $this->timeField($thisUpdate) // $this->setStartDate() ], 'signatureAlgorithm' => $signatureAlgorithm, 'signature' => false // this is going to be overwritten later ]; } $tbsCertList = &$this->currentCert['tbsCertList']; $tbsCertList['issuer'] = $issuer->dn; $tbsCertList['thisUpdate'] = $this->timeField($thisUpdate); if (!empty($this->endDate)) { $tbsCertList['nextUpdate'] = $this->timeField($this->endDate); // $this->setEndDate() } else { unset($tbsCertList['nextUpdate']); } if (!empty($this->serialNumber)) { $crlNumber = $this->serialNumber; } else { $crlNumber = $this->getExtension('id-ce-cRLNumber'); // "The CRL number is a non-critical CRL extension that conveys a // monotonically increasing sequence number for a given CRL scope and // CRL issuer. This extension allows users to easily determine when a // particular CRL supersedes another CRL." // -- https://tools.ietf.org/html/rfc5280#section-5.2.3 $crlNumber = $crlNumber !== false ? $crlNumber->add(new BigInteger(1)) : null; } $this->removeExtension('id-ce-authorityKeyIdentifier'); $this->removeExtension('id-ce-issuerAltName'); // Be sure version >= v2 if some extension found. $version = isset($tbsCertList['version']) ? $tbsCertList['version'] : 0; if (!$version) { if (!empty($tbsCertList['crlExtensions'])) { $version = 'v2'; // v2. } elseif (!empty($tbsCertList['revokedCertificates'])) { foreach ($tbsCertList['revokedCertificates'] as $cert) { if (!empty($cert['crlEntryExtensions'])) { $version = 'v2'; // v2. } } } if ($version) { $tbsCertList['version'] = $version; } } // Store additional extensions. if (!empty($tbsCertList['version'])) { // At least v2. if (!empty($crlNumber)) { $this->setExtension('id-ce-cRLNumber', $crlNumber); } if (isset($issuer->currentKeyIdentifier)) { $this->setExtension('id-ce-authorityKeyIdentifier', [ //'authorityCertIssuer' => array( // ] // 'directoryName' => $issuer->dn // ] //), 'keyIdentifier' => $issuer->currentKeyIdentifier ]); //$extensions = &$tbsCertList['crlExtensions']; //if (isset($issuer->serialNumber)) { // $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber; //} //unset($extensions); } $issuerAltName = $this->getExtension('id-ce-subjectAltName', $issuer->currentCert); if ($issuerAltName !== false) { $this->setExtension('id-ce-issuerAltName', $issuerAltName); } } if (empty($tbsCertList['revokedCertificates'])) { unset($tbsCertList['revokedCertificates']); } unset($tbsCertList); // resync $this->signatureSubject // save $tbsCertList in case there are any \phpseclib3\File\ASN1\Element objects in it $tbsCertList = $this->currentCert['tbsCertList']; $this->loadCRL($this->saveCRL($this->currentCert)); $result = $this->currentCert; $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject); $result['tbsCertList'] = $tbsCertList; $this->currentCert = $currentCert; $this->signatureSubject = $signatureSubject; return $result; } /** * Identify signature algorithm from key settings * * @param PrivateKey $key * @throws UnsupportedAlgorithmException if the algorithm is unsupported * @return array */ private static function identifySignatureAlgorithm(PrivateKey $key) { if ($key instanceof RSA) { if ($key->getPadding() & RSA::SIGNATURE_PSS) { $r = PSS::load($key->withPassword()->toString('PSS')); return [ 'algorithm' => 'id-RSASSA-PSS', 'parameters' => PSS::savePSSParams($r) ]; } switch ($key->getHash()) { case 'md2': case 'md5': case 'sha1': case 'sha224': case 'sha256': case 'sha384': case 'sha512': return [ 'algorithm' => $key->getHash() . 'WithRSAEncryption', 'parameters' => null ]; } throw new UnsupportedAlgorithmException('The only supported hash algorithms for RSA are: md2, md5, sha1, sha224, sha256, sha384, sha512'); } if ($key instanceof DSA) { switch ($key->getHash()) { case 'sha1': case 'sha224': case 'sha256': return ['algorithm' => 'id-dsa-with-' . $key->getHash()]; } throw new UnsupportedAlgorithmException('The only supported hash algorithms for DSA are: sha1, sha224, sha256'); } if ($key instanceof EC) { switch ($key->getCurve()) { case 'Ed25519': case 'Ed448': return ['algorithm' => 'id-' . $key->getCurve()]; } switch ($key->getHash()) { case 'sha1': case 'sha224': case 'sha256': case 'sha384': case 'sha512': return ['algorithm' => 'ecdsa-with-' . strtoupper($key->getHash())]; } throw new UnsupportedAlgorithmException('The only supported hash algorithms for EC are: sha1, sha224, sha256, sha384, sha512'); } throw new UnsupportedAlgorithmException('The only supported public key classes are: RSA, DSA, EC'); } /** * Set certificate start date * * @param \DateTimeInterface|string $date */ public function setStartDate($date) { if (!is_object($date) || !($date instanceof \DateTimeInterface)) { $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get())); } $this->startDate = $date->format('D, d M Y H:i:s O'); } /** * Set certificate end date * * @param \DateTimeInterface|string $date */ public function setEndDate($date) { /* To indicate that a certificate has no well-defined expiration date, the notAfter SHOULD be assigned the GeneralizedTime value of 99991231235959Z. -- http://tools.ietf.org/html/rfc5280#section-4.1.2.5 */ if (is_string($date) && strtolower($date) === 'lifetime') { $temp = '99991231235959Z'; $temp = chr(ASN1::TYPE_GENERALIZED_TIME) . ASN1::encodeLength(strlen($temp)) . $temp; $this->endDate = new Element($temp); } else { if (!is_object($date) || !($date instanceof \DateTimeInterface)) { $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get())); } $this->endDate = $date->format('D, d M Y H:i:s O'); } } /** * Set Serial Number * * @param string $serial * @param int $base optional */ public function setSerialNumber($serial, $base = -256) { $this->serialNumber = new BigInteger($serial, $base); } /** * Turns the certificate into a certificate authority * */ public function makeCA() { $this->caFlag = true; } /** * Check for validity of subarray * * This is intended for use in conjunction with _subArrayUnchecked(), * implementing the checks included in _subArray() but without copying * a potentially large array by passing its reference by-value to is_array(). * * @param array $root * @param string $path * @return boolean */ private function isSubArrayValid(array $root, $path) { if (!is_array($root)) { return false; } foreach (explode('/', $path) as $i) { if (!is_array($root)) { return false; } if (!isset($root[$i])) { return true; } $root = $root[$i]; } return true; } /** * Get a reference to a subarray * * This variant of _subArray() does no is_array() checking, * so $root should be checked with _isSubArrayValid() first. * * This is here for performance reasons: * Passing a reference (i.e. $root) by-value (i.e. to is_array()) * creates a copy. If $root is an especially large array, this is expensive. * * @param array $root * @param string $path absolute path with / as component separator * @param bool $create optional * @return array|false */ private function &subArrayUnchecked(array &$root, $path, $create = false) { $false = false; foreach (explode('/', $path) as $i) { if (!isset($root[$i])) { if (!$create) { return $false; } $root[$i] = []; } $root = &$root[$i]; } return $root; } /** * Get a reference to a subarray * * @param array $root * @param string $path absolute path with / as component separator * @param bool $create optional * @return array|false */ private function &subArray(&$root, $path, $create = false) { $false = false; if (!is_array($root)) { return $false; } foreach (explode('/', $path) as $i) { if (!is_array($root)) { return $false; } if (!isset($root[$i])) { if (!$create) { return $false; } $root[$i] = []; } $root = &$root[$i]; } return $root; } /** * Get a reference to an extension subarray * * @param array $root * @param string $path optional absolute path with / as component separator * @param bool $create optional * @return array|false */ private function &extensions(&$root, $path = null, $create = false) { if (!isset($root)) { $root = $this->currentCert; } switch (true) { case !empty($path): case !is_array($root): break; case isset($root['tbsCertificate']): $path = 'tbsCertificate/extensions'; break; case isset($root['tbsCertList']): $path = 'tbsCertList/crlExtensions'; break; case isset($root['certificationRequestInfo']): $pth = 'certificationRequestInfo/attributes'; $attributes = &$this->subArray($root, $pth, $create); if (is_array($attributes)) { foreach ($attributes as $key => $value) { if ($value['type'] == 'pkcs-9-at-extensionRequest') { $path = "$pth/$key/value/0"; break 2; } } if ($create) { $key = count($attributes); $attributes[] = ['type' => 'pkcs-9-at-extensionRequest', 'value' => []]; $path = "$pth/$key/value/0"; } } break; } $extensions = &$this->subArray($root, $path, $create); if (!is_array($extensions)) { $false = false; return $false; } return $extensions; } /** * Remove an Extension * * @param string $id * @param string $path optional * @return bool */ private function removeExtensionHelper($id, $path = null) { $extensions = &$this->extensions($this->currentCert, $path); if (!is_array($extensions)) { return false; } $result = false; foreach ($extensions as $key => $value) { if ($value['extnId'] == $id) { unset($extensions[$key]); $result = true; } } $extensions = array_values($extensions); // fix for https://bugs.php.net/75433 affecting PHP 7.2 if (!isset($extensions[0])) { $extensions = array_splice($extensions, 0, 0); } return $result; } /** * Get an Extension * * Returns the extension if it exists and false if not * * @param string $id * @param array $cert optional * @param string $path optional * @return mixed */ private function getExtensionHelper($id, $cert = null, $path = null) { $extensions = $this->extensions($cert, $path); if (!is_array($extensions)) { return false; } foreach ($extensions as $key => $value) { if ($value['extnId'] == $id) { return $value['extnValue']; } } return false; } /** * Returns a list of all extensions in use * * @param array $cert optional * @param string $path optional * @return array */ private function getExtensionsHelper($cert = null, $path = null) { $exts = $this->extensions($cert, $path); $extensions = []; if (is_array($exts)) { foreach ($exts as $extension) { $extensions[] = $extension['extnId']; } } return $extensions; } /** * Set an Extension * * @param string $id * @param mixed $value * @param bool $critical optional * @param bool $replace optional * @param string $path optional * @return bool */ private function setExtensionHelper($id, $value, $critical = false, $replace = true, $path = null) { $extensions = &$this->extensions($this->currentCert, $path, true); if (!is_array($extensions)) { return false; } $newext = ['extnId' => $id, 'critical' => $critical, 'extnValue' => $value]; foreach ($extensions as $key => $value) { if ($value['extnId'] == $id) { if (!$replace) { return false; } $extensions[$key] = $newext; return true; } } $extensions[] = $newext; return true; } /** * Remove a certificate, CSR or CRL Extension * * @param string $id * @return bool */ public function removeExtension($id) { return $this->removeExtensionHelper($id); } /** * Get a certificate, CSR or CRL Extension * * Returns the extension if it exists and false if not * * @param string $id * @param array $cert optional * @param string $path * @return mixed */ public function getExtension($id, $cert = null, $path = null) { return $this->getExtensionHelper($id, $cert, $path); } /** * Returns a list of all extensions in use in certificate, CSR or CRL * * @param array $cert optional * @param string $path optional * @return array */ public function getExtensions($cert = null, $path = null) { return $this->getExtensionsHelper($cert, $path); } /** * Set a certificate, CSR or CRL Extension * * @param string $id * @param mixed $value * @param bool $critical optional * @param bool $replace optional * @return bool */ public function setExtension($id, $value, $critical = false, $replace = true) { return $this->setExtensionHelper($id, $value, $critical, $replace); } /** * Remove a CSR attribute. * * @param string $id * @param int $disposition optional * @return bool */ public function removeAttribute($id, $disposition = self::ATTR_ALL) { $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes'); if (!is_array($attributes)) { return false; } $result = false; foreach ($attributes as $key => $attribute) { if ($attribute['type'] == $id) { $n = count($attribute['value']); switch (true) { case $disposition == self::ATTR_APPEND: case $disposition == self::ATTR_REPLACE: return false; case $disposition >= $n: $disposition -= $n; break; case $disposition == self::ATTR_ALL: case $n == 1: unset($attributes[$key]); $result = true; break; default: unset($attributes[$key]['value'][$disposition]); $attributes[$key]['value'] = array_values($attributes[$key]['value']); $result = true; break; } if ($result && $disposition != self::ATTR_ALL) { break; } } } $attributes = array_values($attributes); return $result; } /** * Get a CSR attribute * * Returns the attribute if it exists and false if not * * @param string $id * @param int $disposition optional * @param array $csr optional * @return mixed */ public function getAttribute($id, $disposition = self::ATTR_ALL, $csr = null) { if (empty($csr)) { $csr = $this->currentCert; } $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes'); if (!is_array($attributes)) { return false; } foreach ($attributes as $key => $attribute) { if ($attribute['type'] == $id) { $n = count($attribute['value']); switch (true) { case $disposition == self::ATTR_APPEND: case $disposition == self::ATTR_REPLACE: return false; case $disposition == self::ATTR_ALL: return $attribute['value']; case $disposition >= $n: $disposition -= $n; break; default: return $attribute['value'][$disposition]; } } } return false; } /** * Get all requested CSR extensions * * Returns the list of extensions if there are any and false if not * * @param array $csr optional * @return mixed */ public function getRequestedCertificateExtensions($csr = null) { if (empty($csr)) { $csr = $this->currentCert; } $requestedExtensions = $this->getAttribute('pkcs-9-at-extensionRequest'); if ($requestedExtensions === false) { return false; } return $this->getAttribute('pkcs-9-at-extensionRequest')[0]; } /** * Returns a list of all CSR attributes in use * * @param array $csr optional * @return array */ public function getAttributes($csr = null) { if (empty($csr)) { $csr = $this->currentCert; } $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes'); $attrs = []; if (is_array($attributes)) { foreach ($attributes as $attribute) { $attrs[] = $attribute['type']; } } return $attrs; } /** * Set a CSR attribute * * @param string $id * @param mixed $value * @param int $disposition optional * @return bool */ public function setAttribute($id, $value, $disposition = self::ATTR_ALL) { $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes', true); if (!is_array($attributes)) { return false; } switch ($disposition) { case self::ATTR_REPLACE: $disposition = self::ATTR_APPEND; // fall-through case self::ATTR_ALL: $this->removeAttribute($id); break; } foreach ($attributes as $key => $attribute) { if ($attribute['type'] == $id) { $n = count($attribute['value']); switch (true) { case $disposition == self::ATTR_APPEND: $last = $key; break; case $disposition >= $n: $disposition -= $n; break; default: $attributes[$key]['value'][$disposition] = $value; return true; } } } switch (true) { case $disposition >= 0: return false; case isset($last): $attributes[$last]['value'][] = $value; break; default: $attributes[] = ['type' => $id, 'value' => $disposition == self::ATTR_ALL ? $value : [$value]]; break; } return true; } /** * Sets the subject key identifier * * This is used by the id-ce-authorityKeyIdentifier and the id-ce-subjectKeyIdentifier extensions. * * @param string $value */ public function setKeyIdentifier($value) { if (empty($value)) { unset($this->currentKeyIdentifier); } else { $this->currentKeyIdentifier = $value; } } /** * Compute a public key identifier. * * Although key identifiers may be set to any unique value, this function * computes key identifiers from public key according to the two * recommended methods (4.2.1.2 RFC 3280). * Highly polymorphic: try to accept all possible forms of key: * - Key object * - \phpseclib3\File\X509 object with public or private key defined * - Certificate or CSR array * - \phpseclib3\File\ASN1\Element object * - PEM or DER string * * @param mixed $key optional * @param int $method optional * @return string binary key identifier */ public function computeKeyIdentifier($key = null, $method = 1) { if (is_null($key)) { $key = $this; } switch (true) { case is_string($key): break; case is_array($key) && isset($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): return $this->computeKeyIdentifier($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $method); case is_array($key) && isset($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']): return $this->computeKeyIdentifier($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], $method); case !is_object($key): return false; case $key instanceof Element: // Assume the element is a bitstring-packed key. $decoded = ASN1::decodeBER($key->element); if (!$decoded) { return false; } $raw = ASN1::asn1map($decoded[0], ['type' => ASN1::TYPE_BIT_STRING]); if (empty($raw)) { return false; } // If the key is private, compute identifier from its corresponding public key. $key = PublicKeyLoader::load($raw); if ($key instanceof PrivateKey) { // If private. return $this->computeKeyIdentifier($key, $method); } $key = $raw; // Is a public key. break; case $key instanceof X509: if (isset($key->publicKey)) { return $this->computeKeyIdentifier($key->publicKey, $method); } if (isset($key->privateKey)) { return $this->computeKeyIdentifier($key->privateKey, $method); } if (isset($key->currentCert['tbsCertificate']) || isset($key->currentCert['certificationRequestInfo'])) { return $this->computeKeyIdentifier($key->currentCert, $method); } return false; default: // Should be a key object (i.e.: \phpseclib3\Crypt\RSA). $key = $key->getPublicKey(); break; } // If in PEM format, convert to binary. $key = ASN1::extractBER($key); // Now we have the key string: compute its sha-1 sum. $hash = new Hash('sha1'); $hash = $hash->hash($key); if ($method == 2) { $hash = substr($hash, -8); $hash[0] = chr((ord($hash[0]) & 0x0F) | 0x40); } return $hash; } /** * Format a public key as appropriate * * @return array|false */ private function formatSubjectPublicKey() { $format = $this->publicKey instanceof RSA && ($this->publicKey->getPadding() & RSA::SIGNATURE_PSS) ? 'PSS' : 'PKCS8'; $publicKey = base64_decode(preg_replace('#-.+-|[\r\n]#', '', $this->publicKey->toString($format))); $decoded = ASN1::decodeBER($publicKey); if (!$decoded) { return false; } $mapped = ASN1::asn1map($decoded[0], Maps\SubjectPublicKeyInfo::MAP); if (!is_array($mapped)) { return false; } $mapped['subjectPublicKey'] = $this->publicKey->toString($format); return $mapped; } /** * Set the domain name's which the cert is to be valid for * * @param mixed ...$domains * @return void */ public function setDomain(...$domains) { $this->domains = $domains; $this->removeDNProp('id-at-commonName'); $this->setDNProp('id-at-commonName', $this->domains[0]); } /** * Set the IP Addresses's which the cert is to be valid for * * @param mixed[] ...$ipAddresses */ public function setIPAddress(...$ipAddresses) { $this->ipAddresses = $ipAddresses; /* if (!isset($this->domains)) { $this->removeDNProp('id-at-commonName'); $this->setDNProp('id-at-commonName', $this->ipAddresses[0]); } */ } /** * Helper function to build domain array * * @param string $domain * @return array */ private static function dnsName($domain) { return ['dNSName' => $domain]; } /** * Helper function to build IP Address array * * (IPv6 is not currently supported) * * @param string $address * @return array */ private function iPAddress($address) { return ['iPAddress' => $address]; } /** * Get the index of a revoked certificate. * * @param array $rclist * @param string $serial * @param bool $create optional * @return int|false */ private function revokedCertificate(array &$rclist, $serial, $create = false) { $serial = new BigInteger($serial); foreach ($rclist as $i => $rc) { if (!($serial->compare($rc['userCertificate']))) { return $i; } } if (!$create) { return false; } $i = count($rclist); $revocationDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); $rclist[] = ['userCertificate' => $serial, 'revocationDate' => $this->timeField($revocationDate->format('D, d M Y H:i:s O'))]; return $i; } /** * Revoke a certificate. * * @param string $serial * @param string $date optional * @return bool */ public function revoke($serial, $date = null) { if (isset($this->currentCert['tbsCertList'])) { if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) { if ($this->revokedCertificate($rclist, $serial) === false) { // If not yet revoked if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) { if (!empty($date)) { $rclist[$i]['revocationDate'] = $this->timeField($date); } return true; } } } } return false; } /** * Unrevoke a certificate. * * @param string $serial * @return bool */ public function unrevoke($serial) { if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { unset($rclist[$i]); $rclist = array_values($rclist); return true; } } return false; } /** * Get a revoked certificate. * * @param string $serial * @return mixed */ public function getRevoked($serial) { if (is_array($rclist = $this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { return $rclist[$i]; } } return false; } /** * List revoked certificates * * @param array $crl optional * @return array|bool */ public function listRevoked($crl = null) { if (!isset($crl)) { $crl = $this->currentCert; } if (!isset($crl['tbsCertList'])) { return false; } $result = []; if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) { foreach ($rclist as $rc) { $result[] = $rc['userCertificate']->toString(); } } return $result; } /** * Remove a Revoked Certificate Extension * * @param string $serial * @param string $id * @return bool */ public function removeRevokedCertificateExtension($serial, $id) { if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { return $this->removeExtensionHelper($id, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); } } return false; } /** * Get a Revoked Certificate Extension * * Returns the extension if it exists and false if not * * @param string $serial * @param string $id * @param array $crl optional * @return mixed */ public function getRevokedCertificateExtension($serial, $id, $crl = null) { if (!isset($crl)) { $crl = $this->currentCert; } if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) { if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { return $this->getExtension($id, $crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); } } return false; } /** * Returns a list of all extensions in use for a given revoked certificate * * @param string $serial * @param array $crl optional * @return array|bool */ public function getRevokedCertificateExtensions($serial, $crl = null) { if (!isset($crl)) { $crl = $this->currentCert; } if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) { if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { return $this->getExtensions($crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); } } return false; } /** * Set a Revoked Certificate Extension * * @param string $serial * @param string $id * @param mixed $value * @param bool $critical optional * @param bool $replace optional * @return bool */ public function setRevokedCertificateExtension($serial, $id, $value, $critical = false, $replace = true) { if (isset($this->currentCert['tbsCertList'])) { if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) { if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) { return $this->setExtensionHelper($id, $value, $critical, $replace, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); } } } return false; } /** * Register the mapping for a custom/unsupported extension. * * @param string $id * @param array $mapping */ public static function registerExtension($id, array $mapping) { if (isset(self::$extensions[$id]) && self::$extensions[$id] !== $mapping) { throw new \RuntimeException( 'Extension ' . $id . ' has already been defined with a different mapping.' ); } self::$extensions[$id] = $mapping; } /** * Register the mapping for a custom/unsupported extension. * * @param string $id * * @return array|null */ public static function getRegisteredExtension($id) { return isset(self::$extensions[$id]) ? self::$extensions[$id] : null; } /** * Register the mapping for a custom/unsupported extension. * * @param string $id * @param mixed $value * @param bool $critical * @param bool $replace */ public function setExtensionValue($id, $value, $critical = false, $replace = false) { $this->extensionValues[$id] = compact('critical', 'replace', 'value'); } } PK!Zvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Reductions/Barrett.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\BCMath\Reductions; use phpseclib3\Math\BigInteger\Engines\BCMath\Base; /** * PHP Barrett Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class Barrett extends Base { /** * Cache constants * * $cache[self::VARIABLE] tells us whether or not the cached data is still valid. * */ const VARIABLE = 0; /** * $cache[self::DATA] contains the cached data. * */ const DATA = 1; /** * Barrett Modular Reduction * * See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=14 HAC 14.3.3} / * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=165 MPM 6.2.5} for more information. Modified slightly, * so as not to require negative numbers (initially, this script didn't support negative numbers). * * Employs "folding", as described at * {@link http://www.cosic.esat.kuleuven.be/publications/thesis-149.pdf#page=66 thesis-149.pdf#page=66}. To quote from * it, "the idea [behind folding] is to find a value x' such that x (mod m) = x' (mod m), with x' being smaller than x." * * Unfortunately, the "Barrett Reduction with Folding" algorithm described in thesis-149.pdf is not, as written, all that * usable on account of (1) its not using reasonable radix points as discussed in * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=162 MPM 6.2.2} and (2) the fact that, even with reasonable * radix points, it only works when there are an even number of digits in the denominator. The reason for (2) is that * (x >> 1) + (x >> 1) != x / 2 + x / 2. If x is even, they're the same, but if x is odd, they're not. See the in-line * comments for details. * * @param string $n * @param string $m * @return string */ protected static function reduce($n, $m) { static $cache = [ self::VARIABLE => [], self::DATA => [] ]; $m_length = strlen($m); if (strlen($n) > 2 * $m_length) { return bcmod($n, $m); } // if (m.length >> 1) + 2 <= m.length then m is too small and n can't be reduced if ($m_length < 5) { return self::regularBarrett($n, $m); } // n = 2 * m.length $correctionNeeded = false; if ($m_length & 1) { $correctionNeeded = true; $n .= '0'; $m .= '0'; $m_length++; } if (($key = array_search($m, $cache[self::VARIABLE])) === false) { $key = count($cache[self::VARIABLE]); $cache[self::VARIABLE][] = $m; $lhs = '1' . str_repeat('0', $m_length + ($m_length >> 1)); $u = bcdiv($lhs, $m, 0); $m1 = bcsub($lhs, bcmul($u, $m)); $cache[self::DATA][] = [ 'u' => $u, // m.length >> 1 (technically (m.length >> 1) + 1) 'm1' => $m1 // m.length ]; } else { extract($cache[self::DATA][$key]); } $cutoff = $m_length + ($m_length >> 1); $lsd = substr($n, -$cutoff); $msd = substr($n, 0, -$cutoff); $temp = bcmul($msd, $m1); // m.length + (m.length >> 1) $n = bcadd($lsd, $temp); // m.length + (m.length >> 1) + 1 (so basically we're adding two same length numbers) //if ($m_length & 1) { // return self::regularBarrett($n, $m); //} // (m.length + (m.length >> 1) + 1) - (m.length - 1) == (m.length >> 1) + 2 $temp = substr($n, 0, -$m_length + 1); // if even: ((m.length >> 1) + 2) + (m.length >> 1) == m.length + 2 // if odd: ((m.length >> 1) + 2) + (m.length >> 1) == (m.length - 1) + 2 == m.length + 1 $temp = bcmul($temp, $u); // if even: (m.length + 2) - ((m.length >> 1) + 1) = m.length - (m.length >> 1) + 1 // if odd: (m.length + 1) - ((m.length >> 1) + 1) = m.length - (m.length >> 1) $temp = substr($temp, 0, -($m_length >> 1) - 1); // if even: (m.length - (m.length >> 1) + 1) + m.length = 2 * m.length - (m.length >> 1) + 1 // if odd: (m.length - (m.length >> 1)) + m.length = 2 * m.length - (m.length >> 1) $temp = bcmul($temp, $m); // at this point, if m had an odd number of digits, we'd be subtracting a 2 * m.length - (m.length >> 1) digit // number from a m.length + (m.length >> 1) + 1 digit number. ie. there'd be an extra digit and the while loop // following this comment would loop a lot (hence our calling _regularBarrett() in that situation). $result = bcsub($n, $temp); //if (bccomp($result, '0') < 0) { if ($result[0] == '-') { $temp = '1' . str_repeat('0', $m_length + 1); $result = bcadd($result, $temp); } while (bccomp($result, $m) >= 0) { $result = bcsub($result, $m); } return $correctionNeeded ? substr($result, 0, -1) : $result; } /** * (Regular) Barrett Modular Reduction * * For numbers with more than four digits BigInteger::_barrett() is faster. The difference between that and this * is that this function does not fold the denominator into a smaller form. * * @param string $x * @param string $n * @return string */ private static function regularBarrett($x, $n) { static $cache = [ self::VARIABLE => [], self::DATA => [] ]; $n_length = strlen($n); if (strlen($x) > 2 * $n_length) { return bcmod($x, $n); } if (($key = array_search($n, $cache[self::VARIABLE])) === false) { $key = count($cache[self::VARIABLE]); $cache[self::VARIABLE][] = $n; $lhs = '1' . str_repeat('0', 2 * $n_length); $cache[self::DATA][] = bcdiv($lhs, $n, 0); } $temp = substr($x, 0, -$n_length + 1); $temp = bcmul($temp, $cache[self::DATA][$key]); $temp = substr($temp, 0, -$n_length - 1); $r1 = substr($x, -$n_length - 1); $r2 = substr(bcmul($temp, $n), -$n_length - 1); $result = bcsub($r1, $r2); //if (bccomp($result, '0') < 0) { if ($result[0] == '-') { $q = '1' . str_repeat('0', $n_length + 1); $result = bcadd($result, $q); } while (bccomp($result, $n) >= 0) { $result = bcsub($result, $n); } return $result; } } PK!ϽRB B ^vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Reductions/EvalBarrett.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\BCMath\Reductions; use phpseclib3\Math\BigInteger\Engines\BCMath; use phpseclib3\Math\BigInteger\Engines\BCMath\Base; /** * PHP Barrett Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class EvalBarrett extends Base { /** * Custom Reduction Function * * @see self::generateCustomReduction */ private static $custom_reduction; /** * Barrett Modular Reduction * * This calls a dynamically generated loop unrolled function that's specific to a given modulo. * Array lookups are avoided as are if statements testing for how many bits the host OS supports, etc. * * @param string $n * @param string $m * @return string */ protected static function reduce($n, $m) { $inline = self::$custom_reduction; return $inline($n); } /** * Generate Custom Reduction * * @param BCMath $m * @param string $class * @return callable|void */ protected static function generateCustomReduction(BCMath $m, $class) { $m_length = strlen($m); if ($m_length < 5) { $code = 'return bcmod($x, $n);'; eval('$func = function ($n) { ' . $code . '};'); self::$custom_reduction = $func; return; } $lhs = '1' . str_repeat('0', $m_length + ($m_length >> 1)); $u = bcdiv($lhs, $m, 0); $m1 = bcsub($lhs, bcmul($u, $m)); $cutoff = $m_length + ($m_length >> 1); $m = "'$m'"; $u = "'$u'"; $m1 = "'$m1'"; $code = ' $lsd = substr($n, -' . $cutoff . '); $msd = substr($n, 0, -' . $cutoff . '); $temp = bcmul($msd, ' . $m1 . '); $n = bcadd($lsd, $temp); $temp = substr($n, 0, ' . (-$m_length + 1) . '); $temp = bcmul($temp, ' . $u . '); $temp = substr($temp, 0, ' . (-($m_length >> 1) - 1) . '); $temp = bcmul($temp, ' . $m . '); $result = bcsub($n, $temp); if ($result[0] == \'-\') { $temp = \'1' . str_repeat('0', $m_length + 1) . '\'; $result = bcadd($result, $temp); } while (bccomp($result, ' . $m . ') >= 0) { $result = bcsub($result, ' . $m . '); } return $result;'; eval('$func = function ($n) { ' . $code . '};'); self::$custom_reduction = $func; return $func; } } PK!E⨕g g Lvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Base.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\BCMath; use phpseclib3\Math\BigInteger\Engines\BCMath; /** * Sliding Window Exponentiation Engine * * @author Jim Wigginton */ abstract class Base extends BCMath { /** * Cache constants * * $cache[self::VARIABLE] tells us whether or not the cached data is still valid. * */ const VARIABLE = 0; /** * $cache[self::DATA] contains the cached data. * */ const DATA = 1; /** * Test for engine validity * * @return bool */ public static function isValidEngine() { return static::class != __CLASS__; } /** * Performs modular exponentiation. * * @param BCMath $x * @param BCMath $e * @param BCMath $n * @param string $class * @return BCMath */ protected static function powModHelper(BCMath $x, BCMath $e, BCMath $n, $class) { if (empty($e->value)) { $temp = new $class(); $temp->value = '1'; return $x->normalize($temp); } return $x->normalize(static::slidingWindow($x, $e, $n, $class)); } /** * Modular reduction preparation * * @param string $x * @param string $n * @param string $class * @see self::slidingWindow() * @return string */ protected static function prepareReduce($x, $n, $class) { return static::reduce($x, $n); } /** * Modular multiply * * @param string $x * @param string $y * @param string $n * @param string $class * @see self::slidingWindow() * @return string */ protected static function multiplyReduce($x, $y, $n, $class) { return static::reduce(bcmul($x, $y), $n); } /** * Modular square * * @param string $x * @param string $n * @param string $class * @see self::slidingWindow() * @return string */ protected static function squareReduce($x, $n, $class) { return static::reduce(bcmul($x, $x), $n); } } PK!>Ovendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/BuiltIn.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\BCMath; use phpseclib3\Math\BigInteger\Engines\BCMath; /** * Built-In BCMath Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class BuiltIn extends BCMath { /** * Performs modular exponentiation. * * @param BCMath $x * @param BCMath $e * @param BCMath $n * @return BCMath */ protected static function powModHelper(BCMath $x, BCMath $e, BCMath $n) { $temp = new BCMath(); $temp->value = bcpowmod($x->value, $e->value, $n->value); return $x->normalize($temp); } } PK!IIUvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/DefaultEngine.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\BCMath; use phpseclib3\Math\BigInteger\Engines\BCMath\Reductions\Barrett; /** * PHP Default Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class DefaultEngine extends Barrett { } PK!77Ovendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/OpenSSL.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\BCMath; use phpseclib3\Math\BigInteger\Engines\OpenSSL as Progenitor; /** * OpenSSL Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class OpenSSL extends Progenitor { } PK!5||Rvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP/DefaultEngine.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\GMP; use phpseclib3\Math\BigInteger\Engines\GMP; /** * GMP Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class DefaultEngine extends GMP { /** * Performs modular exponentiation. * * @param GMP $x * @param GMP $e * @param GMP $n * @return GMP */ protected static function powModHelper(GMP $x, GMP $e, GMP $n) { $temp = new GMP(); $temp->value = gmp_powm($x->value, $e->value, $n->value); return $x->normalize($temp); } } PK!ob<.<.Wvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Barrett.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP\Reductions; use phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\PHP\Base; /** * PHP Barrett Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class Barrett extends Base { /** * Barrett Modular Reduction * * See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=14 HAC 14.3.3} / * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=165 MPM 6.2.5} for more information. Modified slightly, * so as not to require negative numbers (initially, this script didn't support negative numbers). * * Employs "folding", as described at * {@link http://www.cosic.esat.kuleuven.be/publications/thesis-149.pdf#page=66 thesis-149.pdf#page=66}. To quote from * it, "the idea [behind folding] is to find a value x' such that x (mod m) = x' (mod m), with x' being smaller than x." * * Unfortunately, the "Barrett Reduction with Folding" algorithm described in thesis-149.pdf is not, as written, all that * usable on account of (1) its not using reasonable radix points as discussed in * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=162 MPM 6.2.2} and (2) the fact that, even with reasonable * radix points, it only works when there are an even number of digits in the denominator. The reason for (2) is that * (x >> 1) + (x >> 1) != x / 2 + x / 2. If x is even, they're the same, but if x is odd, they're not. See the in-line * comments for details. * * @param array $n * @param array $m * @param class-string $class * @return array */ protected static function reduce(array $n, array $m, $class) { static $cache = [ self::VARIABLE => [], self::DATA => [] ]; $m_length = count($m); // if (self::compareHelper($n, $static::square($m)) >= 0) { if (count($n) > 2 * $m_length) { $lhs = new $class(); $rhs = new $class(); $lhs->value = $n; $rhs->value = $m; list(, $temp) = $lhs->divide($rhs); return $temp->value; } // if (m.length >> 1) + 2 <= m.length then m is too small and n can't be reduced if ($m_length < 5) { return self::regularBarrett($n, $m, $class); } // n = 2 * m.length $correctionNeeded = false; if ($m_length & 1) { $correctionNeeded = true; array_unshift($n, 0); array_unshift($m, 0); $m_length++; } if (($key = array_search($m, $cache[self::VARIABLE])) === false) { $key = count($cache[self::VARIABLE]); $cache[self::VARIABLE][] = $m; $lhs = new $class(); $lhs_value = &$lhs->value; $lhs_value = self::array_repeat(0, $m_length + ($m_length >> 1)); $lhs_value[] = 1; $rhs = new $class(); $rhs->value = $m; list($u, $m1) = $lhs->divide($rhs); $u = $u->value; $m1 = $m1->value; $cache[self::DATA][] = [ 'u' => $u, // m.length >> 1 (technically (m.length >> 1) + 1) 'm1' => $m1 // m.length ]; } else { extract($cache[self::DATA][$key]); } $cutoff = $m_length + ($m_length >> 1); $lsd = array_slice($n, 0, $cutoff); // m.length + (m.length >> 1) $msd = array_slice($n, $cutoff); // m.length >> 1 $lsd = self::trim($lsd); $temp = $class::multiplyHelper($msd, false, $m1, false); // m.length + (m.length >> 1) $n = $class::addHelper($lsd, false, $temp[self::VALUE], false); // m.length + (m.length >> 1) + 1 (so basically we're adding two same length numbers) //if ($m_length & 1) { // return self::regularBarrett($n[self::VALUE], $m, $class); //} // (m.length + (m.length >> 1) + 1) - (m.length - 1) == (m.length >> 1) + 2 $temp = array_slice($n[self::VALUE], $m_length - 1); // if even: ((m.length >> 1) + 2) + (m.length >> 1) == m.length + 2 // if odd: ((m.length >> 1) + 2) + (m.length >> 1) == (m.length - 1) + 2 == m.length + 1 // note that these are upper bounds. let's say m.length is 2. then you'd be multiplying a // 3 digit number by a 1 digit number. if you're doing 999 * 9 (in base 10) the result will // be a 4 digit number. but if you're multiplying 111 * 1 then the result will be a 3 digit // number. $temp = $class::multiplyHelper($temp, false, $u, false); // if even: (m.length + 2) - ((m.length >> 1) + 1) = m.length - (m.length >> 1) + 1 // if odd: (m.length + 1) - ((m.length >> 1) + 1) = m.length - (m.length >> 1) $temp = array_slice($temp[self::VALUE], ($m_length >> 1) + 1); // if even: (m.length - (m.length >> 1) + 1) + m.length = 2 * m.length - (m.length >> 1) + 1 // if odd: (m.length - (m.length >> 1)) + m.length = 2 * m.length - (m.length >> 1) $temp = $class::multiplyHelper($temp, false, $m, false); // at this point, if m had an odd number of digits, we'd (probably) be subtracting a 2 * m.length - (m.length >> 1) // digit number from a m.length + (m.length >> 1) + 1 digit number. ie. there'd be an extra digit and the while loop // following this comment would loop a lot (hence our calling _regularBarrett() in that situation). $result = $class::subtractHelper($n[self::VALUE], false, $temp[self::VALUE], false); while (self::compareHelper($result[self::VALUE], $result[self::SIGN], $m, false) >= 0) { $result = $class::subtractHelper($result[self::VALUE], $result[self::SIGN], $m, false); } if ($correctionNeeded) { array_shift($result[self::VALUE]); } return $result[self::VALUE]; } /** * (Regular) Barrett Modular Reduction * * For numbers with more than four digits BigInteger::_barrett() is faster. The difference between that and this * is that this function does not fold the denominator into a smaller form. * * @param array $x * @param array $n * @param string $class * @return array */ private static function regularBarrett(array $x, array $n, $class) { static $cache = [ self::VARIABLE => [], self::DATA => [] ]; $n_length = count($n); if (count($x) > 2 * $n_length) { $lhs = new $class(); $rhs = new $class(); $lhs->value = $x; $rhs->value = $n; list(, $temp) = $lhs->divide($rhs); return $temp->value; } if (($key = array_search($n, $cache[self::VARIABLE])) === false) { $key = count($cache[self::VARIABLE]); $cache[self::VARIABLE][] = $n; $lhs = new $class(); $lhs_value = &$lhs->value; $lhs_value = self::array_repeat(0, 2 * $n_length); $lhs_value[] = 1; $rhs = new $class(); $rhs->value = $n; list($temp, ) = $lhs->divide($rhs); // m.length $cache[self::DATA][] = $temp->value; } // 2 * m.length - (m.length - 1) = m.length + 1 $temp = array_slice($x, $n_length - 1); // (m.length + 1) + m.length = 2 * m.length + 1 $temp = $class::multiplyHelper($temp, false, $cache[self::DATA][$key], false); // (2 * m.length + 1) - (m.length - 1) = m.length + 2 $temp = array_slice($temp[self::VALUE], $n_length + 1); // m.length + 1 $result = array_slice($x, 0, $n_length + 1); // m.length + 1 $temp = self::multiplyLower($temp, false, $n, false, $n_length + 1, $class); // $temp == array_slice($class::regularMultiply($temp, false, $n, false)->value, 0, $n_length + 1) if (self::compareHelper($result, false, $temp[self::VALUE], $temp[self::SIGN]) < 0) { $corrector_value = self::array_repeat(0, $n_length + 1); $corrector_value[count($corrector_value)] = 1; $result = $class::addHelper($result, false, $corrector_value, false); $result = $result[self::VALUE]; } // at this point, we're subtracting a number with m.length + 1 digits from another number with m.length + 1 digits $result = $class::subtractHelper($result, false, $temp[self::VALUE], $temp[self::SIGN]); while (self::compareHelper($result[self::VALUE], $result[self::SIGN], $n, false) > 0) { $result = $class::subtractHelper($result[self::VALUE], $result[self::SIGN], $n, false); } return $result[self::VALUE]; } /** * Performs long multiplication up to $stop digits * * If you're going to be doing array_slice($product->value, 0, $stop), some cycles can be saved. * * @see self::regularBarrett() * @param array $x_value * @param bool $x_negative * @param array $y_value * @param bool $y_negative * @param int $stop * @param string $class * @return array */ private static function multiplyLower(array $x_value, $x_negative, array $y_value, $y_negative, $stop, $class) { $x_length = count($x_value); $y_length = count($y_value); if (!$x_length || !$y_length) { // a 0 is being multiplied return [ self::VALUE => [], self::SIGN => false ]; } if ($x_length < $y_length) { $temp = $x_value; $x_value = $y_value; $y_value = $temp; $x_length = count($x_value); $y_length = count($y_value); } $product_value = self::array_repeat(0, $x_length + $y_length); // the following for loop could be removed if the for loop following it // (the one with nested for loops) initially set $i to 0, but // doing so would also make the result in one set of unnecessary adds, // since on the outermost loops first pass, $product->value[$k] is going // to always be 0 $carry = 0; for ($j = 0; $j < $x_length; ++$j) { // ie. $i = 0, $k = $i $temp = $x_value[$j] * $y_value[0] + $carry; // $product_value[$k] == 0 $carry = $class::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $product_value[$j] = (int) ($temp - $class::BASE_FULL * $carry); } if ($j < $stop) { $product_value[$j] = $carry; } // the above for loop is what the previous comment was talking about. the // following for loop is the "one with nested for loops" for ($i = 1; $i < $y_length; ++$i) { $carry = 0; for ($j = 0, $k = $i; $j < $x_length && $k < $stop; ++$j, ++$k) { $temp = $product_value[$k] + $x_value[$j] * $y_value[$i] + $carry; $carry = $class::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $product_value[$k] = (int) ($temp - $class::BASE_FULL * $carry); } if ($k < $stop) { $product_value[$k] = $carry; } } return [ self::VALUE => self::trim($product_value), self::SIGN => $x_negative != $y_negative ]; } } PK!kWvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Classic.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP\Reductions; use phpseclib3\Math\BigInteger\Engines\PHP\Base; /** * PHP Classic Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class Classic extends Base { /** * Regular Division * * @param array $x * @param array $n * @param string $class * @return array */ protected static function reduce(array $x, array $n, $class) { $lhs = new $class(); $lhs->value = $x; $rhs = new $class(); $rhs->value = $n; list(, $temp) = $lhs->divide($rhs); return $temp->value; } } PK!\DK > >[vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/EvalBarrett.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP\Reductions; use phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\PHP\Base; /** * PHP Dynamic Barrett Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class EvalBarrett extends Base { /** * Custom Reduction Function * * @see self::generateCustomReduction */ private static $custom_reduction; /** * Barrett Modular Reduction * * This calls a dynamically generated loop unrolled function that's specific to a given modulo. * Array lookups are avoided as are if statements testing for how many bits the host OS supports, etc. * * @param array $n * @param array $m * @param string $class * @return array */ protected static function reduce(array $n, array $m, $class) { $inline = self::$custom_reduction; return $inline($n); } /** * Generate Custom Reduction * * @param PHP $m * @param string $class * @return callable */ protected static function generateCustomReduction(PHP $m, $class) { $m_length = count($m->value); if ($m_length < 5) { $code = ' $lhs = new ' . $class . '(); $lhs->value = $x; $rhs = new ' . $class . '(); $rhs->value = [' . implode(',', array_map(self::class . '::float2string', $m->value)) . ']; list(, $temp) = $lhs->divide($rhs); return $temp->value; '; eval('$func = function ($x) { ' . $code . '};'); self::$custom_reduction = $func; //self::$custom_reduction = \Closure::bind($func, $m, $class); return $func; } $correctionNeeded = false; if ($m_length & 1) { $correctionNeeded = true; $m = clone $m; array_unshift($m->value, 0); $m_length++; } $lhs = new $class(); $lhs_value = &$lhs->value; $lhs_value = self::array_repeat(0, $m_length + ($m_length >> 1)); $lhs_value[] = 1; $rhs = new $class(); list($u, $m1) = $lhs->divide($m); if ($class::BASE != 26) { $u = $u->value; } else { $lhs_value = self::array_repeat(0, 2 * $m_length); $lhs_value[] = 1; $rhs = new $class(); list($u) = $lhs->divide($m); $u = $u->value; } $m = $m->value; $m1 = $m1->value; $cutoff = count($m) + (count($m) >> 1); $code = $correctionNeeded ? 'array_unshift($n, 0);' : ''; $code .= ' if (count($n) > ' . (2 * count($m)) . ') { $lhs = new ' . $class . '(); $rhs = new ' . $class . '(); $lhs->value = $n; $rhs->value = [' . implode(',', array_map(self::class . '::float2string', $m)) . ']; list(, $temp) = $lhs->divide($rhs); return $temp->value; } $lsd = array_slice($n, 0, ' . $cutoff . '); $msd = array_slice($n, ' . $cutoff . ');'; $code .= self::generateInlineTrim('msd'); $code .= self::generateInlineMultiply('msd', $m1, 'temp', $class); $code .= self::generateInlineAdd('lsd', 'temp', 'n', $class); $code .= '$temp = array_slice($n, ' . (count($m) - 1) . ');'; $code .= self::generateInlineMultiply('temp', $u, 'temp2', $class); $code .= self::generateInlineTrim('temp2'); $code .= $class::BASE == 26 ? '$temp = array_slice($temp2, ' . (count($m) + 1) . ');' : '$temp = array_slice($temp2, ' . ((count($m) >> 1) + 1) . ');'; $code .= self::generateInlineMultiply('temp', $m, 'temp2', $class); $code .= self::generateInlineTrim('temp2'); /* if ($class::BASE == 26) { $code.= '$n = array_slice($n, 0, ' . (count($m) + 1) . '); $temp2 = array_slice($temp2, 0, ' . (count($m) + 1) . ');'; } */ $code .= self::generateInlineSubtract2('n', 'temp2', 'temp', $class); $subcode = self::generateInlineSubtract1('temp', $m, 'temp2', $class); $subcode .= '$temp = $temp2;'; $code .= self::generateInlineCompare($m, 'temp', $subcode); if ($correctionNeeded) { $code .= 'array_shift($temp);'; } $code .= 'return $temp;'; eval('$func = function ($n) { ' . $code . '};'); self::$custom_reduction = $func; return $func; //self::$custom_reduction = \Closure::bind($func, $m, $class); } /** * Inline Trim * * Removes leading zeros * * @param string $name * @return string */ private static function generateInlineTrim($name) { return ' for ($i = count($' . $name . ') - 1; $i >= 0; --$i) { if ($' . $name . '[$i]) { break; } unset($' . $name . '[$i]); }'; } /** * Inline Multiply (unknown, known) * * @param string $input * @param array $arr * @param string $output * @param string $class * @return string */ private static function generateInlineMultiply($input, array $arr, $output, $class) { if (!count($arr)) { return 'return [];'; } $regular = ' $length = count($' . $input . '); if (!$length) { $' . $output . ' = []; }else{ $' . $output . ' = array_fill(0, $length + ' . count($arr) . ', 0); $carry = 0;'; for ($i = 0; $i < count($arr); $i++) { $regular .= ' $subtemp = $' . $input . '[0] * ' . $arr[$i]; $regular .= $i ? ' + $carry;' : ';'; $regular .= '$carry = '; $regular .= $class::BASE === 26 ? 'intval($subtemp / 0x4000000);' : '$subtemp >> 31;'; $regular .= '$' . $output . '[' . $i . '] = '; if ($class::BASE === 26) { $regular .= '(int) ('; } $regular .= '$subtemp - ' . $class::BASE_FULL . ' * $carry'; $regular .= $class::BASE === 26 ? ');' : ';'; } $regular .= '$' . $output . '[' . count($arr) . '] = $carry;'; $regular .= ' for ($i = 1; $i < $length; ++$i) {'; for ($j = 0; $j < count($arr); $j++) { $regular .= $j ? '$k++;' : '$k = $i;'; $regular .= ' $subtemp = $' . $output . '[$k] + $' . $input . '[$i] * ' . $arr[$j]; $regular .= $j ? ' + $carry;' : ';'; $regular .= '$carry = '; $regular .= $class::BASE === 26 ? 'intval($subtemp / 0x4000000);' : '$subtemp >> 31;'; $regular .= '$' . $output . '[$k] = '; if ($class::BASE === 26) { $regular .= '(int) ('; } $regular .= '$subtemp - ' . $class::BASE_FULL . ' * $carry'; $regular .= $class::BASE === 26 ? ');' : ';'; } $regular .= '$' . $output . '[++$k] = $carry; $carry = 0;'; $regular .= '}}'; //if (count($arr) < 2 * self::KARATSUBA_CUTOFF) { //} return $regular; } /** * Inline Addition * * @param string $x * @param string $y * @param string $result * @param string $class * @return string */ private static function generateInlineAdd($x, $y, $result, $class) { $code = ' $length = max(count($' . $x . '), count($' . $y . ')); $' . $result . ' = array_pad($' . $x . ', $length + 1, 0); $_' . $y . ' = array_pad($' . $y . ', $length, 0); $carry = 0; for ($i = 0, $j = 1; $j < $length; $i+=2, $j+=2) { $sum = ($' . $result . '[$j] + $_' . $y . '[$j]) * ' . $class::BASE_FULL . ' + $' . $result . '[$i] + $_' . $y . '[$i] + $carry; $carry = $sum >= ' . self::float2string($class::MAX_DIGIT2) . '; $sum = $carry ? $sum - ' . self::float2string($class::MAX_DIGIT2) . ' : $sum;'; $code .= $class::BASE === 26 ? '$upper = intval($sum / 0x4000000); $' . $result . '[$i] = (int) ($sum - ' . $class::BASE_FULL . ' * $upper);' : '$upper = $sum >> 31; $' . $result . '[$i] = $sum - ' . $class::BASE_FULL . ' * $upper;'; $code .= ' $' . $result . '[$j] = $upper; } if ($j == $length) { $sum = $' . $result . '[$i] + $_' . $y . '[$i] + $carry; $carry = $sum >= ' . self::float2string($class::BASE_FULL) . '; $' . $result . '[$i] = $carry ? $sum - ' . self::float2string($class::BASE_FULL) . ' : $sum; ++$i; } if ($carry) { for (; $' . $result . '[$i] == ' . $class::MAX_DIGIT . '; ++$i) { $' . $result . '[$i] = 0; } ++$' . $result . '[$i]; }'; $code .= self::generateInlineTrim($result); return $code; } /** * Inline Subtraction 2 * * For when $known is more digits than $unknown. This is the harder use case to optimize for. * * @param string $known * @param string $unknown * @param string $result * @param string $class * @return string */ private static function generateInlineSubtract2($known, $unknown, $result, $class) { $code = ' $' . $result . ' = $' . $known . '; $carry = 0; $size = count($' . $unknown . '); for ($i = 0, $j = 1; $j < $size; $i+= 2, $j+= 2) { $sum = ($' . $known . '[$j] - $' . $unknown . '[$j]) * ' . $class::BASE_FULL . ' + $' . $known . '[$i] - $' . $unknown . '[$i] - $carry; $carry = $sum < 0; if ($carry) { $sum+= ' . self::float2string($class::MAX_DIGIT2) . '; } $subtemp = '; $code .= $class::BASE === 26 ? 'intval($sum / 0x4000000);' : '$sum >> 31;'; $code .= '$' . $result . '[$i] = '; if ($class::BASE === 26) { $code .= '(int) ('; } $code .= '$sum - ' . $class::BASE_FULL . ' * $subtemp'; if ($class::BASE === 26) { $code .= ')'; } $code .= '; $' . $result . '[$j] = $subtemp; } if ($j == $size) { $sum = $' . $known . '[$i] - $' . $unknown . '[$i] - $carry; $carry = $sum < 0; $' . $result . '[$i] = $carry ? $sum + ' . $class::BASE_FULL . ' : $sum; ++$i; } if ($carry) { for (; !$' . $result . '[$i]; ++$i) { $' . $result . '[$i] = ' . $class::MAX_DIGIT . '; } --$' . $result . '[$i]; }'; $code .= self::generateInlineTrim($result); return $code; } /** * Inline Subtraction 1 * * For when $unknown is more digits than $known. This is the easier use case to optimize for. * * @param string $unknown * @param array $known * @param string $result * @param string $class * @return string */ private static function generateInlineSubtract1($unknown, array $known, $result, $class) { $code = '$' . $result . ' = $' . $unknown . ';'; for ($i = 0, $j = 1; $j < count($known); $i += 2, $j += 2) { $code .= '$sum = $' . $unknown . '[' . $j . '] * ' . $class::BASE_FULL . ' + $' . $unknown . '[' . $i . '] - '; $code .= self::float2string($known[$j] * $class::BASE_FULL + $known[$i]); if ($i != 0) { $code .= ' - $carry'; } $code .= '; if ($carry = $sum < 0) { $sum+= ' . self::float2string($class::MAX_DIGIT2) . '; } $subtemp = '; $code .= $class::BASE === 26 ? 'intval($sum / 0x4000000);' : '$sum >> 31;'; $code .= ' $' . $result . '[' . $i . '] = '; if ($class::BASE === 26) { $code .= ' (int) ('; } $code .= '$sum - ' . $class::BASE_FULL . ' * $subtemp'; if ($class::BASE === 26) { $code .= ')'; } $code .= '; $' . $result . '[' . $j . '] = $subtemp;'; } $code .= '$i = ' . $i . ';'; if ($j == count($known)) { $code .= ' $sum = $' . $unknown . '[' . $i . '] - ' . $known[$i] . ' - $carry; $carry = $sum < 0; $' . $result . '[' . $i . '] = $carry ? $sum + ' . $class::BASE_FULL . ' : $sum; ++$i;'; } $code .= ' if ($carry) { for (; !$' . $result . '[$i]; ++$i) { $' . $result . '[$i] = ' . $class::MAX_DIGIT . '; } --$' . $result . '[$i]; }'; $code .= self::generateInlineTrim($result); return $code; } /** * Inline Comparison * * If $unknown >= $known then loop * * @param array $known * @param string $unknown * @param string $subcode * @return string */ private static function generateInlineCompare(array $known, $unknown, $subcode) { $uniqid = uniqid(); $code = 'loop_' . $uniqid . ': $clength = count($' . $unknown . '); switch (true) { case $clength < ' . count($known) . ': goto end_' . $uniqid . '; case $clength > ' . count($known) . ':'; for ($i = count($known) - 1; $i >= 0; $i--) { $code .= ' case $' . $unknown . '[' . $i . '] > ' . $known[$i] . ': goto subcode_' . $uniqid . '; case $' . $unknown . '[' . $i . '] < ' . $known[$i] . ': goto end_' . $uniqid . ';'; } $code .= ' default: // do subcode } subcode_' . $uniqid . ':' . $subcode . ' goto loop_' . $uniqid . '; end_' . $uniqid . ':'; return $code; } /** * Convert a float to a string * * If you do echo floatval(pow(2, 52)) you'll get 4.6116860184274E+18. It /can/ be displayed without a loss of * precision but displayed in this way there will be precision loss, hence the need for this method. * * @param int|float $num * @return string */ private static function float2string($num) { if (!is_float($num)) { return (string) $num; } if ($num < 0) { return '-' . self::float2string(abs($num)); } $temp = ''; while ($num) { $temp = fmod($num, 10) . $temp; $num = floor($num / 10); } return $temp; } } PK!o o ^vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/MontgomeryMult.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP\Reductions; use phpseclib3\Math\BigInteger\Engines\PHP; /** * PHP Montgomery Modular Exponentiation Engine with interleaved multiplication * * @author Jim Wigginton */ abstract class MontgomeryMult extends Montgomery { /** * Montgomery Multiply * * Interleaves the montgomery reduction and long multiplication algorithms together as described in * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=13 HAC 14.36} * * @see self::_prepMontgomery() * @see self::_montgomery() * @param array $x * @param array $y * @param array $m * @param class-string $class * @return array */ public static function multiplyReduce(array $x, array $y, array $m, $class) { // the following code, although not callable, can be run independently of the above code // although the above code performed better in my benchmarks the following could might // perform better under different circumstances. in lieu of deleting it it's just been // made uncallable static $cache = [ self::VARIABLE => [], self::DATA => [] ]; if (($key = array_search($m, $cache[self::VARIABLE])) === false) { $key = count($cache[self::VARIABLE]); $cache[self::VARIABLE][] = $m; $cache[self::DATA][] = self::modInverse67108864($m, $class); } $n = max(count($x), count($y), count($m)); $x = array_pad($x, $n, 0); $y = array_pad($y, $n, 0); $m = array_pad($m, $n, 0); $a = [self::VALUE => self::array_repeat(0, $n + 1)]; for ($i = 0; $i < $n; ++$i) { $temp = $a[self::VALUE][0] + $x[$i] * $y[0]; $temp = $temp - $class::BASE_FULL * ($class::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31)); $temp = $temp * $cache[self::DATA][$key]; $temp = $temp - $class::BASE_FULL * ($class::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31)); $temp = $class::addHelper($class::regularMultiply([$x[$i]], $y), false, $class::regularMultiply([$temp], $m), false); $a = $class::addHelper($a[self::VALUE], false, $temp[self::VALUE], false); $a[self::VALUE] = array_slice($a[self::VALUE], 1); } if (self::compareHelper($a[self::VALUE], false, $m, false) >= 0) { $a = $class::subtractHelper($a[self::VALUE], false, $m, false); } return $a[self::VALUE]; } } PK!7 [Zvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Montgomery.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP\Reductions; use phpseclib3\Math\BigInteger\Engines\PHP\Montgomery as Progenitor; /** * PHP Montgomery Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class Montgomery extends Progenitor { /** * Prepare a number for use in Montgomery Modular Reductions * * @param array $x * @param array $n * @param string $class * @return array */ protected static function prepareReduce(array $x, array $n, $class) { $lhs = new $class(); $lhs->value = array_merge(self::array_repeat(0, count($n)), $x); $rhs = new $class(); $rhs->value = $n; list(, $temp) = $lhs->divide($rhs); return $temp->value; } /** * Montgomery Multiply * * Interleaves the montgomery reduction and long multiplication algorithms together as described in * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=13 HAC 14.36} * * @param array $x * @param array $n * @param string $class * @return array */ protected static function reduce(array $x, array $n, $class) { static $cache = [ self::VARIABLE => [], self::DATA => [] ]; if (($key = array_search($n, $cache[self::VARIABLE])) === false) { $key = count($cache[self::VARIABLE]); $cache[self::VARIABLE][] = $x; $cache[self::DATA][] = self::modInverse67108864($n, $class); } $k = count($n); $result = [self::VALUE => $x]; for ($i = 0; $i < $k; ++$i) { $temp = $result[self::VALUE][$i] * $cache[self::DATA][$key]; $temp = $temp - $class::BASE_FULL * ($class::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31)); $temp = $class::regularMultiply([$temp], $n); $temp = array_merge(self::array_repeat(0, $i), $temp); $result = $class::addHelper($result[self::VALUE], false, $temp, false); } $result[self::VALUE] = array_slice($result[self::VALUE], $k); if (self::compareHelper($result, false, $n, false) >= 0) { $result = $class::subtractHelper($result[self::VALUE], false, $n, false); } return $result[self::VALUE]; } /** * Modular Inverse of a number mod 2**26 (eg. 67108864) * * Based off of the bnpInvDigit function implemented and justified in the following URL: * * {@link http://www-cs-students.stanford.edu/~tjw/jsbn/jsbn.js} * * The following URL provides more info: * * {@link http://groups.google.com/group/sci.crypt/msg/7a137205c1be7d85} * * As for why we do all the bitmasking... strange things can happen when converting from floats to ints. For * instance, on some computers, var_dump((int) -4294967297) yields int(-1) and on others, it yields * int(-2147483648). To avoid problems stemming from this, we use bitmasks to guarantee that ints aren't * auto-converted to floats. The outermost bitmask is present because without it, there's no guarantee that * the "residue" returned would be the so-called "common residue". We use fmod, in the last step, because the * maximum possible $x is 26 bits and the maximum $result is 16 bits. Thus, we have to be able to handle up to * 40 bits, which only 64-bit floating points will support. * * Thanks to Pedro Gimeno Fortea for input! * * @param array $x * @param string $class * @return int */ protected static function modInverse67108864(array $x, $class) // 2**26 == 67,108,864 { $x = -$x[0]; $result = $x & 0x3; // x**-1 mod 2**2 $result = ($result * (2 - $x * $result)) & 0xF; // x**-1 mod 2**4 $result = ($result * (2 - ($x & 0xFF) * $result)) & 0xFF; // x**-1 mod 2**8 $result = ($result * ((2 - ($x & 0xFFFF) * $result) & 0xFFFF)) & 0xFFFF; // x**-1 mod 2**16 $result = $class::BASE == 26 ? fmod($result * (2 - fmod($x * $result, $class::BASE_FULL)), $class::BASE_FULL) : // x**-1 mod 2**26 ($result * (2 - ($x * $result) % $class::BASE_FULL)) % $class::BASE_FULL; return $result & $class::MAX_DIGIT; } } PK! ``Zvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/PowerOfTwo.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP\Reductions; use phpseclib3\Math\BigInteger\Engines\PHP\Base; /** * PHP Power Of Two Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class PowerOfTwo extends Base { /** * Prepare a number for use in Montgomery Modular Reductions * * @param array $x * @param array $n * @param string $class * @return array */ protected static function prepareReduce(array $x, array $n, $class) { return self::reduce($x, $n, $class); } /** * Power Of Two Reduction * * @param array $x * @param array $n * @param string $class * @return array */ protected static function reduce(array $x, array $n, $class) { $lhs = new $class(); $lhs->value = $x; $rhs = new $class(); $rhs->value = $n; $temp = new $class(); $temp->value = [1]; $result = $lhs->bitwise_and($rhs->subtract($temp)); return $result->value; } } PK!X4Ivendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Base.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\PHP; /** * PHP Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class Base extends PHP { /** * Cache constants * * $cache[self::VARIABLE] tells us whether or not the cached data is still valid. * */ const VARIABLE = 0; /** * $cache[self::DATA] contains the cached data. * */ const DATA = 1; /** * Test for engine validity * * @return bool */ public static function isValidEngine() { return static::class != __CLASS__; } /** * Performs modular exponentiation. * * The most naive approach to modular exponentiation has very unreasonable requirements, and * and although the approach involving repeated squaring does vastly better, it, too, is impractical * for our purposes. The reason being that division - by far the most complicated and time-consuming * of the basic operations (eg. +,-,*,/) - occurs multiple times within it. * * Modular reductions resolve this issue. Although an individual modular reduction takes more time * then an individual division, when performed in succession (with the same modulo), they're a lot faster. * * The two most commonly used modular reductions are Barrett and Montgomery reduction. Montgomery reduction, * although faster, only works when the gcd of the modulo and of the base being used is 1. In RSA, when the * base is a power of two, the modulo - a product of two primes - is always going to have a gcd of 1 (because * the product of two odd numbers is odd), but what about when RSA isn't used? * * In contrast, Barrett reduction has no such constraint. As such, some bigint implementations perform a * Barrett reduction after every operation in the modpow function. Others perform Barrett reductions when the * modulo is even and Montgomery reductions when the modulo is odd. BigInteger.java's modPow method, however, * uses a trick involving the Chinese Remainder Theorem to factor the even modulo into two numbers - one odd and * the other, a power of two - and recombine them, later. This is the method that this modPow function uses. * {@link http://islab.oregonstate.edu/papers/j34monex.pdf Montgomery Reduction with Even Modulus} elaborates. * * @param PHP $x * @param PHP $e * @param PHP $n * @param string $class * @return PHP */ protected static function powModHelper(PHP $x, PHP $e, PHP $n, $class) { if (empty($e->value)) { $temp = new $class(); $temp->value = [1]; return $x->normalize($temp); } if ($e->value == [1]) { list(, $temp) = $x->divide($n); return $x->normalize($temp); } if ($e->value == [2]) { $temp = new $class(); $temp->value = $class::square($x->value); list(, $temp) = $temp->divide($n); return $x->normalize($temp); } return $x->normalize(static::slidingWindow($x, $e, $n, $class)); } /** * Modular reduction preparation * * @param array $x * @param array $n * @param string $class * @see self::slidingWindow() * @return array */ protected static function prepareReduce(array $x, array $n, $class) { return static::reduce($x, $n, $class); } /** * Modular multiply * * @param array $x * @param array $y * @param array $n * @param string $class * @see self::slidingWindow() * @return array */ protected static function multiplyReduce(array $x, array $y, array $n, $class) { $temp = $class::multiplyHelper($x, false, $y, false); return static::reduce($temp[self::VALUE], $n, $class); } /** * Modular square * * @param array $x * @param array $n * @param string $class * @see self::slidingWindow() * @return array */ protected static function squareReduce(array $x, array $n, $class) { return static::reduce($class::square($x), $n, $class); } } PK!`IHHRvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/DefaultEngine.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\PHP\Reductions\EvalBarrett; /** * PHP Default Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class DefaultEngine extends EvalBarrett { } PK!0_ _ Ovendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Montgomery.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\Engine; use phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\PHP\Reductions\PowerOfTwo; /** * PHP Montgomery Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class Montgomery extends Base { /** * Test for engine validity * * @return bool */ public static function isValidEngine() { return static::class != __CLASS__; } /** * Performs modular exponentiation. * * @template T of Engine * @param Engine $x * @param Engine $e * @param Engine $n * @param class-string $class * @return T */ protected static function slidingWindow(Engine $x, Engine $e, Engine $n, $class) { // is the modulo odd? if ($n->value[0] & 1) { return parent::slidingWindow($x, $e, $n, $class); } // if it's not, it's even // find the lowest set bit (eg. the max pow of 2 that divides $n) for ($i = 0; $i < count($n->value); ++$i) { if ($n->value[$i]) { $temp = decbin($n->value[$i]); $j = strlen($temp) - strrpos($temp, '1') - 1; $j += $class::BASE * $i; break; } } // at this point, 2^$j * $n/(2^$j) == $n $mod1 = clone $n; $mod1->rshift($j); $mod2 = new $class(); $mod2->value = [1]; $mod2->lshift($j); $part1 = $mod1->value != [1] ? parent::slidingWindow($x, $e, $mod1, $class) : new $class(); $part2 = PowerOfTwo::slidingWindow($x, $e, $mod2, $class); $y1 = $mod2->modInverse($mod1); $y2 = $mod1->modInverse($mod2); $result = $part1->multiply($mod2); $result = $result->multiply($y1); $temp = $part2->multiply($mod1); $temp = $temp->multiply($y2); $result = $result->add($temp); list(, $result) = $result->divide($n); return $result; } } PK!b'"44Lvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/OpenSSL.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines\PHP; use phpseclib3\Math\BigInteger\Engines\OpenSSL as Progenitor; /** * OpenSSL Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class OpenSSL extends Progenitor { } PK!K+E+EGvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\BadConfigurationException; /** * BCMath Engine. * * @author Jim Wigginton */ class BCMath extends Engine { /** * Can Bitwise operations be done fast? * * @see parent::bitwise_leftRotate() * @see parent::bitwise_rightRotate() */ const FAST_BITWISE = false; /** * Engine Directory * * @see parent::setModExpEngine */ const ENGINE_DIR = 'BCMath'; /** * Test for engine validity * * @return bool * @see parent::__construct() */ public static function isValidEngine() { return extension_loaded('bcmath'); } /** * Default constructor * * @param mixed $x integer Base-10 number or base-$base number if $base set. * @param int $base * @see parent::__construct() */ public function __construct($x = 0, $base = 10) { if (!isset(static::$isValidEngine[static::class])) { static::$isValidEngine[static::class] = self::isValidEngine(); } if (!static::$isValidEngine[static::class]) { throw new BadConfigurationException('BCMath is not setup correctly on this system'); } $this->value = '0'; parent::__construct($x, $base); } /** * Initialize a BCMath BigInteger Engine instance * * @param int $base * @see parent::__construct() */ protected function initialize($base) { switch (abs($base)) { case 256: // round $len to the nearest 4 $len = (strlen($this->value) + 3) & ~3; $x = str_pad($this->value, $len, chr(0), STR_PAD_LEFT); $this->value = '0'; for ($i = 0; $i < $len; $i += 4) { $this->value = bcmul($this->value, '4294967296', 0); // 4294967296 == 2**32 $this->value = bcadd( $this->value, 0x1000000 * ord($x[$i]) + ((ord($x[$i + 1]) << 16) | (ord( $x[$i + 2] ) << 8) | ord($x[$i + 3])), 0 ); } if ($this->is_negative) { $this->value = '-' . $this->value; } break; case 16: $x = (strlen($this->value) & 1) ? '0' . $this->value : $this->value; $temp = new self(Strings::hex2bin($x), 256); $this->value = $this->is_negative ? '-' . $temp->value : $temp->value; $this->is_negative = false; break; case 10: // explicitly casting $x to a string is necessary, here, since doing $x[0] on -1 yields different // results then doing it on '-1' does (modInverse does $x[0]) $this->value = $this->value === '-' ? '0' : (string)$this->value; } } /** * Converts a BigInteger to a base-10 number. * * @return string */ public function toString() { if ($this->value === '0') { return '0'; } return ltrim($this->value, '0'); } /** * Converts a BigInteger to a byte string (eg. base-256). * * @param bool $twos_compliment * @return string */ public function toBytes($twos_compliment = false) { if ($twos_compliment) { return $this->toBytesHelper(); } $value = ''; $current = $this->value; if ($current[0] == '-') { $current = substr($current, 1); } while (bccomp($current, '0', 0) > 0) { $temp = bcmod($current, '16777216'); $value = chr($temp >> 16) . chr($temp >> 8) . chr($temp) . $value; $current = bcdiv($current, '16777216', 0); } return $this->precision > 0 ? substr(str_pad($value, $this->precision >> 3, chr(0), STR_PAD_LEFT), -($this->precision >> 3)) : ltrim($value, chr(0)); } /** * Adds two BigIntegers. * * @param BCMath $y * @return BCMath */ public function add(BCMath $y) { $temp = new self(); $temp->value = bcadd($this->value, $y->value); return $this->normalize($temp); } /** * Subtracts two BigIntegers. * * @param BCMath $y * @return BCMath */ public function subtract(BCMath $y) { $temp = new self(); $temp->value = bcsub($this->value, $y->value); return $this->normalize($temp); } /** * Multiplies two BigIntegers. * * @param BCMath $x * @return BCMath */ public function multiply(BCMath $x) { $temp = new self(); $temp->value = bcmul($this->value, $x->value); return $this->normalize($temp); } /** * Divides two BigIntegers. * * Returns an array whose first element contains the quotient and whose second element contains the * "common residue". If the remainder would be positive, the "common residue" and the remainder are the * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder * and the divisor (basically, the "common residue" is the first positive modulo). * * @param BCMath $y * @return array{static, static} */ public function divide(BCMath $y) { $quotient = new self(); $remainder = new self(); $quotient->value = bcdiv($this->value, $y->value, 0); $remainder->value = bcmod($this->value, $y->value); if ($remainder->value[0] == '-') { $remainder->value = bcadd($remainder->value, $y->value[0] == '-' ? substr($y->value, 1) : $y->value, 0); } return [$this->normalize($quotient), $this->normalize($remainder)]; } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * * @param BCMath $n * @return false|BCMath */ public function modInverse(BCMath $n) { return $this->modInverseHelper($n); } /** * Calculates the greatest common divisor and Bezout's identity. * * Say you have 693 and 609. The GCD is 21. Bezout's identity states that there exist integers x and y such that * 693*x + 609*y == 21. In point of fact, there are actually an infinite number of x and y combinations and which * combination is returned is dependent upon which mode is in use. See * {@link http://en.wikipedia.org/wiki/B%C3%A9zout%27s_identity Bezout's identity - Wikipedia} for more information. * * @param BCMath $n * @return array{gcd: static, x: static, y: static} */ public function extendedGCD(BCMath $n) { // it might be faster to use the binary xGCD algorithim here, as well, but (1) that algorithim works // best when the base is a power of 2 and (2) i don't think it'd make much difference, anyway. as is, // the basic extended euclidean algorithim is what we're using. $u = $this->value; $v = $n->value; $a = '1'; $b = '0'; $c = '0'; $d = '1'; while (bccomp($v, '0', 0) != 0) { $q = bcdiv($u, $v, 0); $temp = $u; $u = $v; $v = bcsub($temp, bcmul($v, $q, 0), 0); $temp = $a; $a = $c; $c = bcsub($temp, bcmul($a, $q, 0), 0); $temp = $b; $b = $d; $d = bcsub($temp, bcmul($b, $q, 0), 0); } return [ 'gcd' => $this->normalize(new static($u)), 'x' => $this->normalize(new static($a)), 'y' => $this->normalize(new static($b)) ]; } /** * Calculates the greatest common divisor * * Say you have 693 and 609. The GCD is 21. * * @param BCMath $n * @return BCMath */ public function gcd(BCMath $n) { extract($this->extendedGCD($n)); /** @var BCMath $gcd */ return $gcd; } /** * Absolute value. * * @return BCMath */ public function abs() { $temp = new static(); $temp->value = strlen($this->value) && $this->value[0] == '-' ? substr($this->value, 1) : $this->value; return $temp; } /** * Logical And * * @param BCMath $x * @return BCMath */ public function bitwise_and(BCMath $x) { return $this->bitwiseAndHelper($x); } /** * Logical Or * * @param BCMath $x * @return BCMath */ public function bitwise_or(BCMath $x) { return $this->bitwiseOrHelper($x); } /** * Logical Exclusive Or * * @param BCMath $x * @return BCMath */ public function bitwise_xor(BCMath $x) { return $this->bitwiseXorHelper($x); } /** * Logical Right Shift * * Shifts BigInteger's by $shift bits, effectively dividing by 2**$shift. * * @param int $shift * @return BCMath */ public function bitwise_rightShift($shift) { $temp = new static(); $temp->value = bcdiv($this->value, bcpow('2', $shift, 0), 0); return $this->normalize($temp); } /** * Logical Left Shift * * Shifts BigInteger's by $shift bits, effectively multiplying by 2**$shift. * * @param int $shift * @return BCMath */ public function bitwise_leftShift($shift) { $temp = new static(); $temp->value = bcmul($this->value, bcpow('2', $shift, 0), 0); return $this->normalize($temp); } /** * Compares two numbers. * * Although one might think !$x->compare($y) means $x != $y, it, in fact, means the opposite. The reason for this * is demonstrated thusly: * * $x > $y: $x->compare($y) > 0 * $x < $y: $x->compare($y) < 0 * $x == $y: $x->compare($y) == 0 * * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). * * {@internal Could return $this->subtract($x), but that's not as fast as what we do do.} * * @param BCMath $y * @return int in case < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. * @see self::equals() */ public function compare(BCMath $y) { return bccomp($this->value, $y->value, 0); } /** * Tests the equality of two numbers. * * If you need to see if one number is greater than or less than another number, use BigInteger::compare() * * @param BCMath $x * @return bool */ public function equals(BCMath $x) { return $this->value == $x->value; } /** * Performs modular exponentiation. * * @param BCMath $e * @param BCMath $n * @return BCMath */ public function modPow(BCMath $e, BCMath $n) { return $this->powModOuter($e, $n); } /** * Performs modular exponentiation. * * Alias for modPow(). * * @param BCMath $e * @param BCMath $n * @return BCMath */ public function powMod(BCMath $e, BCMath $n) { return $this->powModOuter($e, $n); } /** * Performs modular exponentiation. * * @param BCMath $e * @param BCMath $n * @return BCMath */ protected function powModInner(BCMath $e, BCMath $n) { try { $class = static::$modexpEngine[static::class]; return $class::powModHelper($this, $e, $n, static::class); } catch (\Exception $err) { return BCMath\DefaultEngine::powModHelper($this, $e, $n, static::class); } } /** * Normalize * * Removes leading zeros and truncates (if necessary) to maintain the appropriate precision * * @param BCMath $result * @return BCMath */ protected function normalize(BCMath $result) { $result->precision = $this->precision; $result->bitmask = $this->bitmask; if ($result->bitmask !== false) { $result->value = bcmod($result->value, $result->bitmask->value); } return $result; } /** * Generate a random prime number between a range * * If there's not a prime within the given range, false will be returned. * * @param BCMath $min * @param BCMath $max * @return false|BCMath */ public static function randomRangePrime(BCMath $min, BCMath $max) { return self::randomRangePrimeOuter($min, $max); } /** * Generate a random number between a range * * Returns a random number between $min and $max where $min and $max * can be defined using one of the two methods: * * BigInteger::randomRange($min, $max) * BigInteger::randomRange($max, $min) * * @param BCMath $min * @param BCMath $max * @return BCMath */ public static function randomRange(BCMath $min, BCMath $max) { return self::randomRangeHelper($min, $max); } /** * Make the current number odd * * If the current number is odd it'll be unchanged. If it's even, one will be added to it. * * @see self::randomPrime() */ protected function make_odd() { if (!$this->isOdd()) { $this->value = bcadd($this->value, '1'); } } /** * Test the number against small primes. * * @see self::isPrime() */ protected function testSmallPrimes() { if ($this->value === '1') { return false; } if ($this->value === '2') { return true; } if ($this->value[strlen($this->value) - 1] % 2 == 0) { return false; } $value = $this->value; foreach (self::PRIMES as $prime) { $r = bcmod($this->value, $prime); if ($r == '0') { return $this->value == $prime; } } return true; } /** * Scan for 1 and right shift by that amount * * ie. $s = gmp_scan1($n, 0) and $r = gmp_div_q($n, gmp_pow(gmp_init('2'), $s)); * * @param BCMath $r * @return int * @see self::isPrime() */ public static function scan1divide(BCMath $r) { $r_value = &$r->value; $s = 0; // if $n was 1, $r would be 0 and this would be an infinite loop, hence our $this->equals(static::$one[static::class]) check earlier while ($r_value[strlen($r_value) - 1] % 2 == 0) { $r_value = bcdiv($r_value, '2', 0); ++$s; } return $s; } /** * Performs exponentiation. * * @param BCMath $n * @return BCMath */ public function pow(BCMath $n) { $temp = new self(); $temp->value = bcpow($this->value, $n->value); return $this->normalize($temp); } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param BCMath ...$nums * @return BCMath */ public static function min(BCMath ...$nums) { return self::minHelper($nums); } /** * Return the maximum BigInteger between an arbitrary number of BigIntegers. * * @param BCMath ...$nums * @return BCMath */ public static function max(BCMath ...$nums) { return self::maxHelper($nums); } /** * Tests BigInteger to see if it is between two integers, inclusive * * @param BCMath $min * @param BCMath $max * @return bool */ public function between(BCMath $min, BCMath $max) { return $this->compare($min) >= 0 && $this->compare($max) <= 0; } /** * Set Bitmask * * @param int $bits * @return Engine * @see self::setPrecision() */ protected static function setBitmask($bits) { $temp = parent::setBitmask($bits); return $temp->add(static::$one[static::class]); } /** * Is Odd? * * @return bool */ public function isOdd() { return $this->value[strlen($this->value) - 1] % 2 == 1; } /** * Tests if a bit is set * * @return bool */ public function testBit($x) { return bccomp( bcmod($this->value, bcpow('2', $x + 1, 0)), bcpow('2', $x, 0), 0 ) >= 0; } /** * Is Negative? * * @return bool */ public function isNegative() { return strlen($this->value) && $this->value[0] == '-'; } /** * Negate * * Given $k, returns -$k * * @return BCMath */ public function negate() { $temp = clone $this; if (!strlen($temp->value)) { return $temp; } $temp->value = $temp->value[0] == '-' ? substr($this->value, 1) : '-' . $this->value; return $temp; } } PK!ݙGvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Random; use phpseclib3\Exception\BadConfigurationException; use phpseclib3\Math\BigInteger; /** * Base Engine. * * @author Jim Wigginton */ abstract class Engine implements \JsonSerializable { /* final protected */ const PRIMES = [ 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997, ]; /** * BigInteger(0) * * @var array, static> */ protected static $zero = []; /** * BigInteger(1) * * @var array, static> */ protected static $one = []; /** * BigInteger(2) * * @var array, static> */ protected static $two = []; /** * Modular Exponentiation Engine * * @var array, class-string> */ protected static $modexpEngine; /** * Engine Validity Flag * * @var array, bool> */ protected static $isValidEngine; /** * Holds the BigInteger's value * * @var \GMP|string|array|int */ protected $value; /** * Holds the BigInteger's sign * * @var bool */ protected $is_negative; /** * Precision * * @see static::setPrecision() * @var int */ protected $precision = -1; /** * Precision Bitmask * * @see static::setPrecision() * @var static|false */ protected $bitmask = false; /** * Recurring Modulo Function * * @var callable */ protected $reduce; /** * Mode independent value used for serialization. * * @see self::__sleep() * @see self::__wakeup() * @var string */ protected $hex; /** * Default constructor * * @param int|numeric-string $x integer Base-10 number or base-$base number if $base set. * @param int $base */ public function __construct($x = 0, $base = 10) { if (!array_key_exists(static::class, static::$zero)) { static::$zero[static::class] = null; // Placeholder to prevent infinite loop. static::$zero[static::class] = new static(0); static::$one[static::class] = new static(1); static::$two[static::class] = new static(2); } // '0' counts as empty() but when the base is 256 '0' is equal to ord('0') or 48 // '0' is the only value like this per http://php.net/empty if (empty($x) && (abs($base) != 256 || $x !== '0')) { return; } switch ($base) { case -256: case 256: if ($base == -256 && (ord($x[0]) & 0x80)) { $this->value = ~$x; $this->is_negative = true; } else { $this->value = $x; $this->is_negative = false; } $this->initialize($base); if ($this->is_negative) { $temp = $this->add(new static('-1')); $this->value = $temp->value; } break; case -16: case 16: if ($base > 0 && $x[0] == '-') { $this->is_negative = true; $x = substr($x, 1); } $x = preg_replace('#^(?:0x)?([A-Fa-f0-9]*).*#s', '$1', $x); $is_negative = false; if ($base < 0 && hexdec($x[0]) >= 8) { $this->is_negative = $is_negative = true; $x = Strings::bin2hex(~Strings::hex2bin($x)); } $this->value = $x; $this->initialize($base); if ($is_negative) { $temp = $this->add(new static('-1')); $this->value = $temp->value; } break; case -10: case 10: // (?value = preg_replace('#(?value) || $this->value == '-') { $this->value = '0'; } $this->initialize($base); break; case -2: case 2: if ($base > 0 && $x[0] == '-') { $this->is_negative = true; $x = substr($x, 1); } $x = preg_replace('#^([01]*).*#s', '$1', $x); $temp = new static(Strings::bits2bin($x), 128 * $base); // ie. either -16 or +16 $this->value = $temp->value; if ($temp->is_negative) { $this->is_negative = true; } break; default: // base not supported, so we'll let $this == 0 } } /** * Sets engine type. * * Throws an exception if the type is invalid * * @param class-string $engine */ public static function setModExpEngine($engine) { $fqengine = '\\phpseclib3\\Math\\BigInteger\\Engines\\' . static::ENGINE_DIR . '\\' . $engine; if (!class_exists($fqengine) || !method_exists($fqengine, 'isValidEngine')) { throw new \InvalidArgumentException("$engine is not a valid engine"); } if (!$fqengine::isValidEngine()) { throw new BadConfigurationException("$engine is not setup correctly on this system"); } static::$modexpEngine[static::class] = $fqengine; } /** * Converts a BigInteger to a byte string (eg. base-256). * * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're * saved as two's compliment. * @return string */ protected function toBytesHelper() { $comparison = $this->compare(new static()); if ($comparison == 0) { return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; } $temp = $comparison < 0 ? $this->add(new static(1)) : $this; $bytes = $temp->toBytes(); if (!strlen($bytes)) { // eg. if the number we're trying to convert is -1 $bytes = chr(0); } if (ord($bytes[0]) & 0x80) { $bytes = chr(0) . $bytes; } return $comparison < 0 ? ~$bytes : $bytes; } /** * Converts a BigInteger to a hex string (eg. base-16). * * @param bool $twos_compliment * @return string */ public function toHex($twos_compliment = false) { return Strings::bin2hex($this->toBytes($twos_compliment)); } /** * Converts a BigInteger to a bit string (eg. base-2). * * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're * saved as two's compliment. * * @param bool $twos_compliment * @return string */ public function toBits($twos_compliment = false) { $hex = $this->toBytes($twos_compliment); $bits = Strings::bin2bits($hex); $result = $this->precision > 0 ? substr($bits, -$this->precision) : ltrim($bits, '0'); if ($twos_compliment && $this->compare(new static()) > 0 && $this->precision <= 0) { return '0' . $result; } return $result; } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * * {@internal See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=21 HAC 14.64} for more information.} * * @param Engine $n * @return static|false */ protected function modInverseHelper(Engine $n) { // $x mod -$n == $x mod $n. $n = $n->abs(); if ($this->compare(static::$zero[static::class]) < 0) { $temp = $this->abs(); $temp = $temp->modInverse($n); return $this->normalize($n->subtract($temp)); } extract($this->extendedGCD($n)); /** * @var Engine $gcd * @var Engine $x */ if (!$gcd->equals(static::$one[static::class])) { return false; } $x = $x->compare(static::$zero[static::class]) < 0 ? $x->add($n) : $x; return $this->compare(static::$zero[static::class]) < 0 ? $this->normalize($n->subtract($x)) : $this->normalize($x); } /** * Serialize * * Will be called, automatically, when serialize() is called on a BigInteger object. * * @return array */ public function __sleep() { $this->hex = $this->toHex(true); $vars = ['hex']; if ($this->precision > 0) { $vars[] = 'precision'; } return $vars; } /** * Serialize * * Will be called, automatically, when unserialize() is called on a BigInteger object. * * @return void */ public function __wakeup() { $temp = new static($this->hex, -16); $this->value = $temp->value; $this->is_negative = $temp->is_negative; if ($this->precision > 0) { // recalculate $this->bitmask $this->setPrecision($this->precision); } } /** * JSON Serialize * * Will be called, automatically, when json_encode() is called on a BigInteger object. * * @return array{hex: string, precision?: int] */ #[\ReturnTypeWillChange] public function jsonSerialize() { $result = ['hex' => $this->toHex(true)]; if ($this->precision > 0) { $result['precision'] = $this->precision; } return $result; } /** * Converts a BigInteger to a base-10 number. * * @return string */ public function __toString() { return $this->toString(); } /** * __debugInfo() magic method * * Will be called, automatically, when print_r() or var_dump() are called * * @return array */ public function __debugInfo() { $result = [ 'value' => '0x' . $this->toHex(true), 'engine' => basename(static::class) ]; return $this->precision > 0 ? $result + ['precision' => $this->precision] : $result; } /** * Set Precision * * Some bitwise operations give different results depending on the precision being used. Examples include left * shift, not, and rotates. * * @param int $bits */ public function setPrecision($bits) { if ($bits < 1) { $this->precision = -1; $this->bitmask = false; return; } $this->precision = $bits; $this->bitmask = static::setBitmask($bits); $temp = $this->normalize($this); $this->value = $temp->value; } /** * Get Precision * * Returns the precision if it exists, -1 if it doesn't * * @return int */ public function getPrecision() { return $this->precision; } /** * Set Bitmask * @return static * @param int $bits * @see self::setPrecision() */ protected static function setBitmask($bits) { return new static(chr((1 << ($bits & 0x7)) - 1) . str_repeat(chr(0xFF), $bits >> 3), 256); } /** * Logical Not * * @return Engine|string */ public function bitwise_not() { // calculuate "not" without regard to $this->precision // (will always result in a smaller number. ie. ~1 isn't 1111 1110 - it's 0) $temp = $this->toBytes(); if ($temp == '') { return $this->normalize(static::$zero[static::class]); } $pre_msb = decbin(ord($temp[0])); $temp = ~$temp; $msb = decbin(ord($temp[0])); if (strlen($msb) == 8) { $msb = substr($msb, strpos($msb, '0')); } $temp[0] = chr(bindec($msb)); // see if we need to add extra leading 1's $current_bits = strlen($pre_msb) + 8 * strlen($temp) - 8; $new_bits = $this->precision - $current_bits; if ($new_bits <= 0) { return $this->normalize(new static($temp, 256)); } // generate as many leading 1's as we need to. $leading_ones = chr((1 << ($new_bits & 0x7)) - 1) . str_repeat(chr(0xFF), $new_bits >> 3); self::base256_lshift($leading_ones, $current_bits); $temp = str_pad($temp, strlen($leading_ones), chr(0), STR_PAD_LEFT); return $this->normalize(new static($leading_ones | $temp, 256)); } /** * Logical Left Shift * * Shifts binary strings $shift bits, essentially multiplying by 2**$shift. * * @param string $x * @param int $shift * @return void */ protected static function base256_lshift(&$x, $shift) { if ($shift == 0) { return; } $num_bytes = $shift >> 3; // eg. floor($shift/8) $shift &= 7; // eg. $shift % 8 $carry = 0; for ($i = strlen($x) - 1; $i >= 0; --$i) { $temp = ord($x[$i]) << $shift | $carry; $x[$i] = chr($temp); $carry = $temp >> 8; } $carry = ($carry != 0) ? chr($carry) : ''; $x = $carry . $x . str_repeat(chr(0), $num_bytes); } /** * Logical Left Rotate * * Instead of the top x bits being dropped they're appended to the shifted bit string. * * @param int $shift * @return Engine */ public function bitwise_leftRotate($shift) { $bits = $this->toBytes(); if ($this->precision > 0) { $precision = $this->precision; if (static::FAST_BITWISE) { $mask = $this->bitmask->toBytes(); } else { $mask = $this->bitmask->subtract(new static(1)); $mask = $mask->toBytes(); } } else { $temp = ord($bits[0]); for ($i = 0; $temp >> $i; ++$i) { } $precision = 8 * strlen($bits) - 8 + $i; $mask = chr((1 << ($precision & 0x7)) - 1) . str_repeat(chr(0xFF), $precision >> 3); } if ($shift < 0) { $shift += $precision; } $shift %= $precision; if (!$shift) { return clone $this; } $left = $this->bitwise_leftShift($shift); $left = $left->bitwise_and(new static($mask, 256)); $right = $this->bitwise_rightShift($precision - $shift); $result = static::FAST_BITWISE ? $left->bitwise_or($right) : $left->add($right); return $this->normalize($result); } /** * Logical Right Rotate * * Instead of the bottom x bits being dropped they're prepended to the shifted bit string. * * @param int $shift * @return Engine */ public function bitwise_rightRotate($shift) { return $this->bitwise_leftRotate(-$shift); } /** * Returns the smallest and largest n-bit number * * @param int $bits * @return array{min: static, max: static} */ public static function minMaxBits($bits) { $bytes = $bits >> 3; $min = str_repeat(chr(0), $bytes); $max = str_repeat(chr(0xFF), $bytes); $msb = $bits & 7; if ($msb) { $min = chr(1 << ($msb - 1)) . $min; $max = chr((1 << $msb) - 1) . $max; } else { $min[0] = chr(0x80); } return [ 'min' => new static($min, 256), 'max' => new static($max, 256) ]; } /** * Return the size of a BigInteger in bits * * @return int */ public function getLength() { return strlen($this->toBits()); } /** * Return the size of a BigInteger in bytes * * @return int */ public function getLengthInBytes() { return (int) ceil($this->getLength() / 8); } /** * Performs some pre-processing for powMod * * @param Engine $e * @param Engine $n * @return static|false */ protected function powModOuter(Engine $e, Engine $n) { $n = $this->bitmask !== false && $this->bitmask->compare($n) < 0 ? $this->bitmask : $n->abs(); if ($e->compare(new static()) < 0) { $e = $e->abs(); $temp = $this->modInverse($n); if ($temp === false) { return false; } return $this->normalize($temp->powModInner($e, $n)); } if ($this->compare($n) > 0) { list(, $temp) = $this->divide($n); return $temp->powModInner($e, $n); } return $this->powModInner($e, $n); } /** * Sliding Window k-ary Modular Exponentiation * * Based on {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=27 HAC 14.85} / * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=210 MPM 7.7}. In a departure from those algorithims, * however, this function performs a modular reduction after every multiplication and squaring operation. * As such, this function has the same preconditions that the reductions being used do. * * @template T of Engine * @param Engine $x * @param Engine $e * @param Engine $n * @param class-string $class * @return T */ protected static function slidingWindow(Engine $x, Engine $e, Engine $n, $class) { static $window_ranges = [7, 25, 81, 241, 673, 1793]; // from BigInteger.java's oddModPow function //static $window_ranges = [0, 7, 36, 140, 450, 1303, 3529]; // from MPM 7.3.1 $e_bits = $e->toBits(); $e_length = strlen($e_bits); // calculate the appropriate window size. // $window_size == 3 if $window_ranges is between 25 and 81, for example. for ($i = 0, $window_size = 1; $i < count($window_ranges) && $e_length > $window_ranges[$i]; ++$window_size, ++$i) { } $n_value = $n->value; if (method_exists(static::class, 'generateCustomReduction')) { static::generateCustomReduction($n, $class); } // precompute $this^0 through $this^$window_size $powers = []; $powers[1] = static::prepareReduce($x->value, $n_value, $class); $powers[2] = static::squareReduce($powers[1], $n_value, $class); // we do every other number since substr($e_bits, $i, $j+1) (see below) is supposed to end // in a 1. ie. it's supposed to be odd. $temp = 1 << ($window_size - 1); for ($i = 1; $i < $temp; ++$i) { $i2 = $i << 1; $powers[$i2 + 1] = static::multiplyReduce($powers[$i2 - 1], $powers[2], $n_value, $class); } $result = new $class(1); $result = static::prepareReduce($result->value, $n_value, $class); for ($i = 0; $i < $e_length;) { if (!$e_bits[$i]) { $result = static::squareReduce($result, $n_value, $class); ++$i; } else { for ($j = $window_size - 1; $j > 0; --$j) { if (!empty($e_bits[$i + $j])) { break; } } // eg. the length of substr($e_bits, $i, $j + 1) for ($k = 0; $k <= $j; ++$k) { $result = static::squareReduce($result, $n_value, $class); } $result = static::multiplyReduce($result, $powers[bindec(substr($e_bits, $i, $j + 1))], $n_value, $class); $i += $j + 1; } } $temp = new $class(); $temp->value = static::reduce($result, $n_value, $class); return $temp; } /** * Generates a random number of a certain size * * Bit length is equal to $size * * @param int $size * @return Engine */ public static function random($size) { extract(static::minMaxBits($size)); /** * @var BigInteger $min * @var BigInteger $max */ return static::randomRange($min, $max); } /** * Generates a random prime number of a certain size * * Bit length is equal to $size * * @param int $size * @return Engine */ public static function randomPrime($size) { extract(static::minMaxBits($size)); /** * @var static $min * @var static $max */ return static::randomRangePrime($min, $max); } /** * Performs some pre-processing for randomRangePrime * * @param Engine $min * @param Engine $max * @return static|false */ protected static function randomRangePrimeOuter(Engine $min, Engine $max) { $compare = $max->compare($min); if (!$compare) { return $min->isPrime() ? $min : false; } elseif ($compare < 0) { // if $min is bigger then $max, swap $min and $max $temp = $max; $max = $min; $min = $temp; } $length = $max->getLength(); if ($length > 8196) { throw new \RuntimeException("Generation of random prime numbers larger than 8196 has been disabled ($length)"); } $x = static::randomRange($min, $max); return static::randomRangePrimeInner($x, $min, $max); } /** * Generate a random number between a range * * Returns a random number between $min and $max where $min and $max * can be defined using one of the two methods: * * BigInteger::randomRange($min, $max) * BigInteger::randomRange($max, $min) * * @param Engine $min * @param Engine $max * @return Engine */ protected static function randomRangeHelper(Engine $min, Engine $max) { $compare = $max->compare($min); if (!$compare) { return $min; } elseif ($compare < 0) { // if $min is bigger then $max, swap $min and $max $temp = $max; $max = $min; $min = $temp; } if (!isset(static::$one[static::class])) { static::$one[static::class] = new static(1); } $max = $max->subtract($min->subtract(static::$one[static::class])); $size = strlen(ltrim($max->toBytes(), chr(0))); /* doing $random % $max doesn't work because some numbers will be more likely to occur than others. eg. if $max is 140 and $random's max is 255 then that'd mean both $random = 5 and $random = 145 would produce 5 whereas the only value of random that could produce 139 would be 139. ie. not all numbers would be equally likely. some would be more likely than others. creating a whole new random number until you find one that is within the range doesn't work because, for sufficiently small ranges, the likelihood that you'd get a number within that range would be pretty small. eg. with $random's max being 255 and if your $max being 1 the probability would be pretty high that $random would be greater than $max. phpseclib works around this using the technique described here: http://crypto.stackexchange.com/questions/5708/creating-a-small-number-from-a-cryptographically-secure-random-string */ $random_max = new static(chr(1) . str_repeat("\0", $size), 256); $random = new static(Random::string($size), 256); list($max_multiple) = $random_max->divide($max); $max_multiple = $max_multiple->multiply($max); while ($random->compare($max_multiple) >= 0) { $random = $random->subtract($max_multiple); $random_max = $random_max->subtract($max_multiple); $random = $random->bitwise_leftShift(8); $random = $random->add(new static(Random::string(1), 256)); $random_max = $random_max->bitwise_leftShift(8); list($max_multiple) = $random_max->divide($max); $max_multiple = $max_multiple->multiply($max); } list(, $random) = $random->divide($max); return $random->add($min); } /** * Performs some post-processing for randomRangePrime * * @param Engine $x * @param Engine $min * @param Engine $max * @return static|false */ protected static function randomRangePrimeInner(Engine $x, Engine $min, Engine $max) { if (!isset(static::$two[static::class])) { static::$two[static::class] = new static('2'); } $x->make_odd(); if ($x->compare($max) > 0) { // if $x > $max then $max is even and if $min == $max then no prime number exists between the specified range if ($min->equals($max)) { return false; } $x = clone $min; $x->make_odd(); } $initial_x = clone $x; while (true) { if ($x->isPrime()) { return $x; } $x = $x->add(static::$two[static::class]); if ($x->compare($max) > 0) { $x = clone $min; if ($x->equals(static::$two[static::class])) { return $x; } $x->make_odd(); } if ($x->equals($initial_x)) { return false; } } } /** * Sets the $t parameter for primality testing * * @return int */ protected function setupIsPrime() { $length = $this->getLengthInBytes(); // see HAC 4.49 "Note (controlling the error probability)" // @codingStandardsIgnoreStart if ($length >= 163) { $t = 2; } // floor(1300 / 8) else if ($length >= 106) { $t = 3; } // floor( 850 / 8) else if ($length >= 81 ) { $t = 4; } // floor( 650 / 8) else if ($length >= 68 ) { $t = 5; } // floor( 550 / 8) else if ($length >= 56 ) { $t = 6; } // floor( 450 / 8) else if ($length >= 50 ) { $t = 7; } // floor( 400 / 8) else if ($length >= 43 ) { $t = 8; } // floor( 350 / 8) else if ($length >= 37 ) { $t = 9; } // floor( 300 / 8) else if ($length >= 31 ) { $t = 12; } // floor( 250 / 8) else if ($length >= 25 ) { $t = 15; } // floor( 200 / 8) else if ($length >= 18 ) { $t = 18; } // floor( 150 / 8) else { $t = 27; } // @codingStandardsIgnoreEnd return $t; } /** * Tests Primality * * Uses the {@link http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin primality test}. * See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap4.pdf#page=8 HAC 4.24} for more info. * * @param int $t * @return bool */ protected function testPrimality($t) { if (!$this->testSmallPrimes()) { return false; } $n = clone $this; $n_1 = $n->subtract(static::$one[static::class]); $n_2 = $n->subtract(static::$two[static::class]); $r = clone $n_1; $s = static::scan1divide($r); for ($i = 0; $i < $t; ++$i) { $a = static::randomRange(static::$two[static::class], $n_2); $y = $a->modPow($r, $n); if (!$y->equals(static::$one[static::class]) && !$y->equals($n_1)) { for ($j = 1; $j < $s && !$y->equals($n_1); ++$j) { $y = $y->modPow(static::$two[static::class], $n); if ($y->equals(static::$one[static::class])) { return false; } } if (!$y->equals($n_1)) { return false; } } } return true; } /** * Checks a numer to see if it's prime * * Assuming the $t parameter is not set, this function has an error rate of 2**-80. The main motivation for the * $t parameter is distributability. BigInteger::randomPrime() can be distributed across multiple pageloads * on a website instead of just one. * * @param int|bool $t * @return bool */ public function isPrime($t = false) { // OpenSSL limits RSA keys to 16384 bits. The length of an RSA key is equal to the length of the modulo, which is // produced by multiplying the primes p and q by one another. The largest number two 8196 bit primes can produce is // a 16384 bit number so, basically, 8196 bit primes are the largest OpenSSL will generate and if that's the largest // that it'll generate it also stands to reason that that's the largest you'll be able to test primality on $length = $this->getLength(); if ($length > 8196) { throw new \RuntimeException("Primality testing is not supported for numbers larger than 8196 bits ($length)"); } if (!$t) { $t = $this->setupIsPrime(); } return $this->testPrimality($t); } /** * Performs a few preliminary checks on root * * @param int $n * @return Engine */ protected function rootHelper($n) { if ($n < 1) { return clone static::$zero[static::class]; } // we want positive exponents if ($this->compare(static::$one[static::class]) < 0) { return clone static::$zero[static::class]; } // we want positive numbers if ($this->compare(static::$two[static::class]) < 0) { return clone static::$one[static::class]; } // n-th root of 1 or 2 is 1 return $this->rootInner($n); } /** * Calculates the nth root of a biginteger. * * Returns the nth root of a positive biginteger, where n defaults to 2 * * {@internal This function is based off of {@link http://mathforum.org/library/drmath/view/52605.html this page} and {@link http://stackoverflow.com/questions/11242920/calculating-nth-root-with-bcmath-in-php this stackoverflow question}.} * * @param int $n * @return Engine */ protected function rootInner($n) { $n = new static($n); // g is our guess number $g = static::$two[static::class]; // while (g^n < num) g=g*2 while ($g->pow($n)->compare($this) < 0) { $g = $g->multiply(static::$two[static::class]); } // if (g^n==num) num is a power of 2, we're lucky, end of job // == 0 bccomp(bcpow($g, $n), $n->value)==0 if ($g->pow($n)->equals($this) > 0) { $root = $g; return $this->normalize($root); } // if we're here num wasn't a power of 2 :( $og = $g; // og means original guess and here is our upper bound $g = $g->divide(static::$two[static::class])[0]; // g is set to be our lower bound $step = $og->subtract($g)->divide(static::$two[static::class])[0]; // step is the half of upper bound - lower bound $g = $g->add($step); // we start at lower bound + step , basically in the middle of our interval // while step>1 while ($step->compare(static::$one[static::class]) == 1) { $guess = $g->pow($n); $step = $step->divide(static::$two[static::class])[0]; $comp = $guess->compare($this); // compare our guess with real number switch ($comp) { case -1: // if guess is lower we add the new step $g = $g->add($step); break; case 1: // if guess is higher we sub the new step $g = $g->subtract($step); break; case 0: // if guess is exactly the num we're done, we return the value $root = $g; break 2; } } if ($comp == 1) { $g = $g->subtract($step); } // whatever happened, g is the closest guess we can make so return it $root = $g; return $this->normalize($root); } /** * Calculates the nth root of a biginteger. * * @param int $n * @return Engine */ public function root($n = 2) { return $this->rootHelper($n); } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param array $nums * @return Engine */ protected static function minHelper(array $nums) { if (count($nums) == 1) { return $nums[0]; } $min = $nums[0]; for ($i = 1; $i < count($nums); $i++) { $min = $min->compare($nums[$i]) > 0 ? $nums[$i] : $min; } return $min; } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param array $nums * @return Engine */ protected static function maxHelper(array $nums) { if (count($nums) == 1) { return $nums[0]; } $max = $nums[0]; for ($i = 1; $i < count($nums); $i++) { $max = $max->compare($nums[$i]) < 0 ? $nums[$i] : $max; } return $max; } /** * Create Recurring Modulo Function * * Sometimes it may be desirable to do repeated modulos with the same number outside of * modular exponentiation * * @return callable */ public function createRecurringModuloFunction() { $class = static::class; $fqengine = !method_exists(static::$modexpEngine[static::class], 'reduce') ? '\\phpseclib3\\Math\\BigInteger\\Engines\\' . static::ENGINE_DIR . '\\DefaultEngine' : static::$modexpEngine[static::class]; if (method_exists($fqengine, 'generateCustomReduction')) { $func = $fqengine::generateCustomReduction($this, static::class); return eval('return function(' . static::class . ' $x) use ($func, $class) { $r = new $class(); $r->value = $func($x->value); return $r; };'); } $n = $this->value; return eval('return function(' . static::class . ' $x) use ($n, $fqengine, $class) { $r = new $class(); $r->value = $fqengine::reduce($x->value, $n, $class); return $r; };'); } /** * Calculates the greatest common divisor and Bezout's identity. * * @param Engine $n * @return array{gcd: Engine, x: Engine, y: Engine} */ protected function extendedGCDHelper(Engine $n) { $u = clone $this; $v = clone $n; $one = new static(1); $zero = new static(); $a = clone $one; $b = clone $zero; $c = clone $zero; $d = clone $one; while (!$v->equals($zero)) { list($q) = $u->divide($v); $temp = $u; $u = $v; $v = $temp->subtract($v->multiply($q)); $temp = $a; $a = $c; $c = $temp->subtract($a->multiply($q)); $temp = $b; $b = $d; $d = $temp->subtract($b->multiply($q)); } return [ 'gcd' => $u, 'x' => $a, 'y' => $b ]; } /** * Bitwise Split * * Splits BigInteger's into chunks of $split bits * * @param int $split * @return Engine[] */ public function bitwise_split($split) { if ($split < 1) { throw new \RuntimeException('Offset must be greater than 1'); } $mask = static::$one[static::class]->bitwise_leftShift($split)->subtract(static::$one[static::class]); $num = clone $this; $vals = []; while (!$num->equals(static::$zero[static::class])) { $vals[] = $num->bitwise_and($mask); $num = $num->bitwise_rightShift($split); } return array_reverse($vals); } /** * Logical And * * @param Engine $x * @return Engine */ protected function bitwiseAndHelper(Engine $x) { $left = $this->toBytes(true); $right = $x->toBytes(true); $length = max(strlen($left), strlen($right)); $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); return $this->normalize(new static($left & $right, -256)); } /** * Logical Or * * @param Engine $x * @return Engine */ protected function bitwiseOrHelper(Engine $x) { $left = $this->toBytes(true); $right = $x->toBytes(true); $length = max(strlen($left), strlen($right)); $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); return $this->normalize(new static($left | $right, -256)); } /** * Logical Exclusive Or * * @param Engine $x * @return Engine */ protected function bitwiseXorHelper(Engine $x) { $left = $this->toBytes(true); $right = $x->toBytes(true); $length = max(strlen($left), strlen($right)); $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); return $this->normalize(new static($left ^ $right, -256)); } } PK!Uw@@Dvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; use phpseclib3\Exception\BadConfigurationException; /** * GMP Engine. * * @author Jim Wigginton */ class GMP extends Engine { /** * Can Bitwise operations be done fast? * * @see parent::bitwise_leftRotate() * @see parent::bitwise_rightRotate() */ const FAST_BITWISE = true; /** * Engine Directory * * @see parent::setModExpEngine */ const ENGINE_DIR = 'GMP'; /** * Test for engine validity * * @return bool * @see parent::__construct() */ public static function isValidEngine() { return extension_loaded('gmp'); } /** * Default constructor * * @param mixed $x integer Base-10 number or base-$base number if $base set. * @param int $base * @see parent::__construct() */ public function __construct($x = 0, $base = 10) { if (!isset(static::$isValidEngine[static::class])) { static::$isValidEngine[static::class] = self::isValidEngine(); } if (!static::$isValidEngine[static::class]) { throw new BadConfigurationException('GMP is not setup correctly on this system'); } if ($x instanceof \GMP) { $this->value = $x; return; } $this->value = gmp_init(0); parent::__construct($x, $base); } /** * Initialize a GMP BigInteger Engine instance * * @param int $base * @see parent::__construct() */ protected function initialize($base) { switch (abs($base)) { case 256: $this->value = gmp_import($this->value); if ($this->is_negative) { $this->value = -$this->value; } break; case 16: $temp = $this->is_negative ? '-0x' . $this->value : '0x' . $this->value; $this->value = gmp_init($temp); break; case 10: $this->value = gmp_init(isset($this->value) ? $this->value : '0'); } } /** * Converts a BigInteger to a base-10 number. * * @return string */ public function toString() { return (string)$this->value; } /** * Converts a BigInteger to a bit string (eg. base-2). * * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're * saved as two's compliment. * * @param bool $twos_compliment * @return string */ public function toBits($twos_compliment = false) { $hex = $this->toHex($twos_compliment); $bits = gmp_strval(gmp_init($hex, 16), 2); if ($this->precision > 0) { $bits = substr($bits, -$this->precision); } if ($twos_compliment && $this->compare(new static()) > 0 && $this->precision <= 0) { return '0' . $bits; } return $bits; } /** * Converts a BigInteger to a byte string (eg. base-256). * * @param bool $twos_compliment * @return string */ public function toBytes($twos_compliment = false) { if ($twos_compliment) { return $this->toBytesHelper(); } if (gmp_cmp($this->value, gmp_init(0)) == 0) { return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; } $temp = gmp_export($this->value); return $this->precision > 0 ? substr(str_pad($temp, $this->precision >> 3, chr(0), STR_PAD_LEFT), -($this->precision >> 3)) : ltrim($temp, chr(0)); } /** * Adds two BigIntegers. * * @param GMP $y * @return GMP */ public function add(GMP $y) { $temp = new self(); $temp->value = $this->value + $y->value; return $this->normalize($temp); } /** * Subtracts two BigIntegers. * * @param GMP $y * @return GMP */ public function subtract(GMP $y) { $temp = new self(); $temp->value = $this->value - $y->value; return $this->normalize($temp); } /** * Multiplies two BigIntegers. * * @param GMP $x * @return GMP */ public function multiply(GMP $x) { $temp = new self(); $temp->value = $this->value * $x->value; return $this->normalize($temp); } /** * Divides two BigIntegers. * * Returns an array whose first element contains the quotient and whose second element contains the * "common residue". If the remainder would be positive, the "common residue" and the remainder are the * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder * and the divisor (basically, the "common residue" is the first positive modulo). * * @param GMP $y * @return array{GMP, GMP} */ public function divide(GMP $y) { $quotient = new self(); $remainder = new self(); list($quotient->value, $remainder->value) = gmp_div_qr($this->value, $y->value); if (gmp_sign($remainder->value) < 0) { $remainder->value = $remainder->value + gmp_abs($y->value); } return [$this->normalize($quotient), $this->normalize($remainder)]; } /** * Compares two numbers. * * Although one might think !$x->compare($y) means $x != $y, it, in fact, means the opposite. The reason for this * is demonstrated thusly: * * $x > $y: $x->compare($y) > 0 * $x < $y: $x->compare($y) < 0 * $x == $y: $x->compare($y) == 0 * * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). * * {@internal Could return $this->subtract($x), but that's not as fast as what we do do.} * * @param GMP $y * @return int in case < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. * @see self::equals() */ public function compare(GMP $y) { $r = gmp_cmp($this->value, $y->value); if ($r < -1) { $r = -1; } if ($r > 1) { $r = 1; } return $r; } /** * Tests the equality of two numbers. * * If you need to see if one number is greater than or less than another number, use BigInteger::compare() * * @param GMP $x * @return bool */ public function equals(GMP $x) { return $this->value == $x->value; } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * * @param GMP $n * @return false|GMP */ public function modInverse(GMP $n) { $temp = new self(); $temp->value = gmp_invert($this->value, $n->value); return $temp->value === false ? false : $this->normalize($temp); } /** * Calculates the greatest common divisor and Bezout's identity. * * Say you have 693 and 609. The GCD is 21. Bezout's identity states that there exist integers x and y such that * 693*x + 609*y == 21. In point of fact, there are actually an infinite number of x and y combinations and which * combination is returned is dependent upon which mode is in use. See * {@link http://en.wikipedia.org/wiki/B%C3%A9zout%27s_identity Bezout's identity - Wikipedia} for more information. * * @param GMP $n * @return GMP[] */ public function extendedGCD(GMP $n) { extract(gmp_gcdext($this->value, $n->value)); return [ 'gcd' => $this->normalize(new self($g)), 'x' => $this->normalize(new self($s)), 'y' => $this->normalize(new self($t)) ]; } /** * Calculates the greatest common divisor * * Say you have 693 and 609. The GCD is 21. * * @param GMP $n * @return GMP */ public function gcd(GMP $n) { $r = gmp_gcd($this->value, $n->value); return $this->normalize(new self($r)); } /** * Absolute value. * * @return GMP */ public function abs() { $temp = new self(); $temp->value = gmp_abs($this->value); return $temp; } /** * Logical And * * @param GMP $x * @return GMP */ public function bitwise_and(GMP $x) { $temp = new self(); $temp->value = $this->value & $x->value; return $this->normalize($temp); } /** * Logical Or * * @param GMP $x * @return GMP */ public function bitwise_or(GMP $x) { $temp = new self(); $temp->value = $this->value | $x->value; return $this->normalize($temp); } /** * Logical Exclusive Or * * @param GMP $x * @return GMP */ public function bitwise_xor(GMP $x) { $temp = new self(); $temp->value = $this->value ^ $x->value; return $this->normalize($temp); } /** * Logical Right Shift * * Shifts BigInteger's by $shift bits, effectively dividing by 2**$shift. * * @param int $shift * @return GMP */ public function bitwise_rightShift($shift) { // 0xFFFFFFFF >> 2 == -1 (on 32-bit systems) // gmp_init('0xFFFFFFFF') >> 2 == gmp_init('0x3FFFFFFF') $temp = new self(); $temp->value = $this->value >> $shift; return $this->normalize($temp); } /** * Logical Left Shift * * Shifts BigInteger's by $shift bits, effectively multiplying by 2**$shift. * * @param int $shift * @return GMP */ public function bitwise_leftShift($shift) { $temp = new self(); $temp->value = $this->value << $shift; return $this->normalize($temp); } /** * Performs modular exponentiation. * * @param GMP $e * @param GMP $n * @return GMP */ public function modPow(GMP $e, GMP $n) { return $this->powModOuter($e, $n); } /** * Performs modular exponentiation. * * Alias for modPow(). * * @param GMP $e * @param GMP $n * @return GMP */ public function powMod(GMP $e, GMP $n) { return $this->powModOuter($e, $n); } /** * Performs modular exponentiation. * * @param GMP $e * @param GMP $n * @return GMP */ protected function powModInner(GMP $e, GMP $n) { $class = static::$modexpEngine[static::class]; return $class::powModHelper($this, $e, $n); } /** * Normalize * * Removes leading zeros and truncates (if necessary) to maintain the appropriate precision * * @param GMP $result * @return GMP */ protected function normalize(GMP $result) { $result->precision = $this->precision; $result->bitmask = $this->bitmask; if ($result->bitmask !== false) { $flip = $result->value < 0; if ($flip) { $result->value = -$result->value; } $result->value = $result->value & $result->bitmask->value; if ($flip) { $result->value = -$result->value; } } return $result; } /** * Performs some post-processing for randomRangePrime * * @param Engine $x * @param Engine $min * @param Engine $max * @return GMP */ protected static function randomRangePrimeInner(Engine $x, Engine $min, Engine $max) { $p = gmp_nextprime($x->value); if ($p <= $max->value) { return new self($p); } if ($min->value != $x->value) { $x = new self($x->value - 1); } return self::randomRangePrime($min, $x); } /** * Generate a random prime number between a range * * If there's not a prime within the given range, false will be returned. * * @param GMP $min * @param GMP $max * @return false|GMP */ public static function randomRangePrime(GMP $min, GMP $max) { return self::randomRangePrimeOuter($min, $max); } /** * Generate a random number between a range * * Returns a random number between $min and $max where $min and $max * can be defined using one of the two methods: * * BigInteger::randomRange($min, $max) * BigInteger::randomRange($max, $min) * * @param GMP $min * @param GMP $max * @return GMP */ public static function randomRange(GMP $min, GMP $max) { return self::randomRangeHelper($min, $max); } /** * Make the current number odd * * If the current number is odd it'll be unchanged. If it's even, one will be added to it. * * @see self::randomPrime() */ protected function make_odd() { gmp_setbit($this->value, 0); } /** * Tests Primality * * @param int $t * @return bool */ protected function testPrimality($t) { return gmp_prob_prime($this->value, $t) != 0; } /** * Calculates the nth root of a biginteger. * * Returns the nth root of a positive biginteger, where n defaults to 2 * * @param int $n * @return GMP */ protected function rootInner($n) { $root = new self(); $root->value = gmp_root($this->value, $n); return $this->normalize($root); } /** * Performs exponentiation. * * @param GMP $n * @return GMP */ public function pow(GMP $n) { $temp = new self(); $temp->value = $this->value ** $n->value; return $this->normalize($temp); } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param GMP ...$nums * @return GMP */ public static function min(GMP ...$nums) { return self::minHelper($nums); } /** * Return the maximum BigInteger between an arbitrary number of BigIntegers. * * @param GMP ...$nums * @return GMP */ public static function max(GMP ...$nums) { return self::maxHelper($nums); } /** * Tests BigInteger to see if it is between two integers, inclusive * * @param GMP $min * @param GMP $max * @return bool */ public function between(GMP $min, GMP $max) { return $this->compare($min) >= 0 && $this->compare($max) <= 0; } /** * Create Recurring Modulo Function * * Sometimes it may be desirable to do repeated modulos with the same number outside of * modular exponentiation * * @return callable */ public function createRecurringModuloFunction() { $temp = $this->value; return function (GMP $x) use ($temp) { return new GMP($x->value % $temp); }; } /** * Scan for 1 and right shift by that amount * * ie. $s = gmp_scan1($n, 0) and $r = gmp_div_q($n, gmp_pow(gmp_init('2'), $s)); * * @param GMP $r * @return int */ public static function scan1divide(GMP $r) { $s = gmp_scan1($r->value, 0); $r->value >>= $s; return $s; } /** * Is Odd? * * @return bool */ public function isOdd() { return gmp_testbit($this->value, 0); } /** * Tests if a bit is set * * @return bool */ public function testBit($x) { return gmp_testbit($this->value, $x); } /** * Is Negative? * * @return bool */ public function isNegative() { return gmp_sign($this->value) == -1; } /** * Negate * * Given $k, returns -$k * * @return GMP */ public function negate() { $temp = clone $this; $temp->value = -$this->value; return $temp; } } PK!fmHvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/OpenSSL.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; use phpseclib3\Crypt\RSA\Formats\Keys\PKCS8; use phpseclib3\Math\BigInteger; /** * OpenSSL Modular Exponentiation Engine * * @author Jim Wigginton */ abstract class OpenSSL { /** * Test for engine validity * * @return bool */ public static function isValidEngine() { return extension_loaded('openssl') && static::class != __CLASS__; } /** * Performs modular exponentiation. * * @param Engine $x * @param Engine $e * @param Engine $n * @return Engine */ public static function powModHelper(Engine $x, Engine $e, Engine $n) { if ($n->getLengthInBytes() < 31 || $n->getLengthInBytes() > 16384) { throw new \OutOfRangeException('Only modulo between 31 and 16384 bits are accepted'); } $key = PKCS8::savePublicKey( new BigInteger($n), new BigInteger($e) ); $plaintext = str_pad($x->toBytes(), $n->getLengthInBytes(), "\0", STR_PAD_LEFT); // this is easily prone to failure. if the modulo is a multiple of 2 or 3 or whatever it // won't work and you'll get a "failure: error:0906D06C:PEM routines:PEM_read_bio:no start line" // error. i suppose, for even numbers, we could do what PHP\Montgomery.php does, but then what // about odd numbers divisible by 3, by 5, etc? if (!openssl_public_encrypt($plaintext, $result, $key, OPENSSL_NO_PADDING)) { throw new \UnexpectedValueException(openssl_error_string()); } $class = get_class($x); return new $class($result, 256); } } PK!\ Cy#y#Fvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP32.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; /** * Pure-PHP 32-bit Engine. * * Uses 64-bit floats if int size is 4 bits * * @author Jim Wigginton */ class PHP32 extends PHP { // Constants used by PHP.php const BASE = 26; const BASE_FULL = 0x4000000; const MAX_DIGIT = 0x3FFFFFF; const MSB = 0x2000000; /** * MAX10 in greatest MAX10LEN satisfying * MAX10 = 10**MAX10LEN <= 2**BASE. */ const MAX10 = 10000000; /** * MAX10LEN in greatest MAX10LEN satisfying * MAX10 = 10**MAX10LEN <= 2**BASE. */ const MAX10LEN = 7; const MAX_DIGIT2 = 4503599627370496; /** * Initialize a PHP32 BigInteger Engine instance * * @param int $base * @see parent::initialize() */ protected function initialize($base) { if ($base != 256 && $base != -256) { return parent::initialize($base); } $val = $this->value; $this->value = []; $vals = &$this->value; $i = strlen($val); if (!$i) { return; } while (true) { $i -= 4; if ($i < 0) { if ($i == -4) { break; } $val = substr($val, 0, 4 + $i); $val = str_pad($val, 4, "\0", STR_PAD_LEFT); if ($val == "\0\0\0\0") { break; } $i = 0; } list(, $digit) = unpack('N', substr($val, $i, 4)); if ($digit < 0) { $digit += 0xFFFFFFFF + 1; } $step = count($vals) & 3; if ($step) { $digit = (int) floor($digit / pow(2, 2 * $step)); } if ($step != 3) { $digit = (int) fmod($digit, static::BASE_FULL); $i++; } $vals[] = $digit; } while (end($vals) === 0) { array_pop($vals); } reset($vals); } /** * Test for engine validity * * @see parent::__construct() * @return bool */ public static function isValidEngine() { return PHP_INT_SIZE >= 4 && !self::testJITOnWindows(); } /** * Adds two BigIntegers. * * @param PHP32 $y * @return PHP32 */ public function add(PHP32 $y) { $temp = self::addHelper($this->value, $this->is_negative, $y->value, $y->is_negative); return $this->convertToObj($temp); } /** * Subtracts two BigIntegers. * * @param PHP32 $y * @return PHP32 */ public function subtract(PHP32 $y) { $temp = self::subtractHelper($this->value, $this->is_negative, $y->value, $y->is_negative); return $this->convertToObj($temp); } /** * Multiplies two BigIntegers. * * @param PHP32 $y * @return PHP32 */ public function multiply(PHP32 $y) { $temp = self::multiplyHelper($this->value, $this->is_negative, $y->value, $y->is_negative); return $this->convertToObj($temp); } /** * Divides two BigIntegers. * * Returns an array whose first element contains the quotient and whose second element contains the * "common residue". If the remainder would be positive, the "common residue" and the remainder are the * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder * and the divisor (basically, the "common residue" is the first positive modulo). * * @param PHP32 $y * @return array{PHP32, PHP32} */ public function divide(PHP32 $y) { return $this->divideHelper($y); } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * @param PHP32 $n * @return false|PHP32 */ public function modInverse(PHP32 $n) { return $this->modInverseHelper($n); } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * @param PHP32 $n * @return PHP32[] */ public function extendedGCD(PHP32 $n) { return $this->extendedGCDHelper($n); } /** * Calculates the greatest common divisor * * Say you have 693 and 609. The GCD is 21. * * @param PHP32 $n * @return PHP32 */ public function gcd(PHP32 $n) { return $this->extendedGCD($n)['gcd']; } /** * Logical And * * @param PHP32 $x * @return PHP32 */ public function bitwise_and(PHP32 $x) { return $this->bitwiseAndHelper($x); } /** * Logical Or * * @param PHP32 $x * @return PHP32 */ public function bitwise_or(PHP32 $x) { return $this->bitwiseOrHelper($x); } /** * Logical Exclusive Or * * @param PHP32 $x * @return PHP32 */ public function bitwise_xor(PHP32 $x) { return $this->bitwiseXorHelper($x); } /** * Compares two numbers. * * Although one might think !$x->compare($y) means $x != $y, it, in fact, means the opposite. The reason for this is * demonstrated thusly: * * $x > $y: $x->compare($y) > 0 * $x < $y: $x->compare($y) < 0 * $x == $y: $x->compare($y) == 0 * * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). * * {@internal Could return $this->subtract($x), but that's not as fast as what we do do.} * * @param PHP32 $y * @return int in case < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. * @see self::equals() */ public function compare(PHP32 $y) { return $this->compareHelper($this->value, $this->is_negative, $y->value, $y->is_negative); } /** * Tests the equality of two numbers. * * If you need to see if one number is greater than or less than another number, use BigInteger::compare() * * @param PHP32 $x * @return bool */ public function equals(PHP32 $x) { return $this->value === $x->value && $this->is_negative == $x->is_negative; } /** * Performs modular exponentiation. * * @param PHP32 $e * @param PHP32 $n * @return PHP32 */ public function modPow(PHP32 $e, PHP32 $n) { return $this->powModOuter($e, $n); } /** * Performs modular exponentiation. * * Alias for modPow(). * * @param PHP32 $e * @param PHP32 $n * @return PHP32 */ public function powMod(PHP32 $e, PHP32 $n) { return $this->powModOuter($e, $n); } /** * Generate a random prime number between a range * * If there's not a prime within the given range, false will be returned. * * @param PHP32 $min * @param PHP32 $max * @return false|PHP32 */ public static function randomRangePrime(PHP32 $min, PHP32 $max) { return self::randomRangePrimeOuter($min, $max); } /** * Generate a random number between a range * * Returns a random number between $min and $max where $min and $max * can be defined using one of the two methods: * * BigInteger::randomRange($min, $max) * BigInteger::randomRange($max, $min) * * @param PHP32 $min * @param PHP32 $max * @return PHP32 */ public static function randomRange(PHP32 $min, PHP32 $max) { return self::randomRangeHelper($min, $max); } /** * Performs exponentiation. * * @param PHP32 $n * @return PHP32 */ public function pow(PHP32 $n) { return $this->powHelper($n); } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param PHP32 ...$nums * @return PHP32 */ public static function min(PHP32 ...$nums) { return self::minHelper($nums); } /** * Return the maximum BigInteger between an arbitrary number of BigIntegers. * * @param PHP32 ...$nums * @return PHP32 */ public static function max(PHP32 ...$nums) { return self::maxHelper($nums); } /** * Tests BigInteger to see if it is between two integers, inclusive * * @param PHP32 $min * @param PHP32 $max * @return bool */ public function between(PHP32 $min, PHP32 $max) { return $this->compare($min) >= 0 && $this->compare($max) <= 0; } } PK!hOQ##Fvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP64.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; /** * Pure-PHP 64-bit Engine. * * Uses 64-bit integers if int size is 8 bits * * @author Jim Wigginton */ class PHP64 extends PHP { // Constants used by PHP.php const BASE = 31; const BASE_FULL = 0x80000000; const MAX_DIGIT = 0x7FFFFFFF; const MSB = 0x40000000; /** * MAX10 in greatest MAX10LEN satisfying * MAX10 = 10**MAX10LEN <= 2**BASE. */ const MAX10 = 1000000000; /** * MAX10LEN in greatest MAX10LEN satisfying * MAX10 = 10**MAX10LEN <= 2**BASE. */ const MAX10LEN = 9; const MAX_DIGIT2 = 4611686018427387904; /** * Initialize a PHP64 BigInteger Engine instance * * @param int $base * @see parent::initialize() */ protected function initialize($base) { if ($base != 256 && $base != -256) { return parent::initialize($base); } $val = $this->value; $this->value = []; $vals = &$this->value; $i = strlen($val); if (!$i) { return; } while (true) { $i -= 4; if ($i < 0) { if ($i == -4) { break; } $val = substr($val, 0, 4 + $i); $val = str_pad($val, 4, "\0", STR_PAD_LEFT); if ($val == "\0\0\0\0") { break; } $i = 0; } list(, $digit) = unpack('N', substr($val, $i, 4)); $step = count($vals) & 7; if (!$step) { $digit &= static::MAX_DIGIT; $i++; } else { $shift = 8 - $step; $digit >>= $shift; $shift = 32 - $shift; $digit &= (1 << $shift) - 1; $temp = $i > 0 ? ord($val[$i - 1]) : 0; $digit |= ($temp << $shift) & 0x7F000000; } $vals[] = $digit; } while (end($vals) === 0) { array_pop($vals); } reset($vals); } /** * Test for engine validity * * @see parent::__construct() * @return bool */ public static function isValidEngine() { return PHP_INT_SIZE >= 8 && !self::testJITOnWindows(); } /** * Adds two BigIntegers. * * @param PHP64 $y * @return PHP64 */ public function add(PHP64 $y) { $temp = self::addHelper($this->value, $this->is_negative, $y->value, $y->is_negative); return $this->convertToObj($temp); } /** * Subtracts two BigIntegers. * * @param PHP64 $y * @return PHP64 */ public function subtract(PHP64 $y) { $temp = self::subtractHelper($this->value, $this->is_negative, $y->value, $y->is_negative); return $this->convertToObj($temp); } /** * Multiplies two BigIntegers. * * @param PHP64 $y * @return PHP64 */ public function multiply(PHP64 $y) { $temp = self::multiplyHelper($this->value, $this->is_negative, $y->value, $y->is_negative); return $this->convertToObj($temp); } /** * Divides two BigIntegers. * * Returns an array whose first element contains the quotient and whose second element contains the * "common residue". If the remainder would be positive, the "common residue" and the remainder are the * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder * and the divisor (basically, the "common residue" is the first positive modulo). * * @param PHP64 $y * @return array{PHP64, PHP64} */ public function divide(PHP64 $y) { return $this->divideHelper($y); } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * @param PHP64 $n * @return false|PHP64 */ public function modInverse(PHP64 $n) { return $this->modInverseHelper($n); } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * @param PHP64 $n * @return PHP64[] */ public function extendedGCD(PHP64 $n) { return $this->extendedGCDHelper($n); } /** * Calculates the greatest common divisor * * Say you have 693 and 609. The GCD is 21. * * @param PHP64 $n * @return PHP64 */ public function gcd(PHP64 $n) { return $this->extendedGCD($n)['gcd']; } /** * Logical And * * @param PHP64 $x * @return PHP64 */ public function bitwise_and(PHP64 $x) { return $this->bitwiseAndHelper($x); } /** * Logical Or * * @param PHP64 $x * @return PHP64 */ public function bitwise_or(PHP64 $x) { return $this->bitwiseOrHelper($x); } /** * Logical Exclusive Or * * @param PHP64 $x * @return PHP64 */ public function bitwise_xor(PHP64 $x) { return $this->bitwiseXorHelper($x); } /** * Compares two numbers. * * Although one might think !$x->compare($y) means $x != $y, it, in fact, means the opposite. The reason for this is * demonstrated thusly: * * $x > $y: $x->compare($y) > 0 * $x < $y: $x->compare($y) < 0 * $x == $y: $x->compare($y) == 0 * * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). * * {@internal Could return $this->subtract($x), but that's not as fast as what we do do.} * * @param PHP64 $y * @return int in case < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. * @see self::equals() */ public function compare(PHP64 $y) { return parent::compareHelper($this->value, $this->is_negative, $y->value, $y->is_negative); } /** * Tests the equality of two numbers. * * If you need to see if one number is greater than or less than another number, use BigInteger::compare() * * @param PHP64 $x * @return bool */ public function equals(PHP64 $x) { return $this->value === $x->value && $this->is_negative == $x->is_negative; } /** * Performs modular exponentiation. * * @param PHP64 $e * @param PHP64 $n * @return PHP64 */ public function modPow(PHP64 $e, PHP64 $n) { return $this->powModOuter($e, $n); } /** * Performs modular exponentiation. * * Alias for modPow(). * * @param PHP64 $e * @param PHP64 $n * @return PHP64|false */ public function powMod(PHP64 $e, PHP64 $n) { return $this->powModOuter($e, $n); } /** * Generate a random prime number between a range * * If there's not a prime within the given range, false will be returned. * * @param PHP64 $min * @param PHP64 $max * @return false|PHP64 */ public static function randomRangePrime(PHP64 $min, PHP64 $max) { return self::randomRangePrimeOuter($min, $max); } /** * Generate a random number between a range * * Returns a random number between $min and $max where $min and $max * can be defined using one of the two methods: * * BigInteger::randomRange($min, $max) * BigInteger::randomRange($max, $min) * * @param PHP64 $min * @param PHP64 $max * @return PHP64 */ public static function randomRange(PHP64 $min, PHP64 $max) { return self::randomRangeHelper($min, $max); } /** * Performs exponentiation. * * @param PHP64 $n * @return PHP64 */ public function pow(PHP64 $n) { return $this->powHelper($n); } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param PHP64 ...$nums * @return PHP64 */ public static function min(PHP64 ...$nums) { return self::minHelper($nums); } /** * Return the maximum BigInteger between an arbitrary number of BigIntegers. * * @param PHP64 ...$nums * @return PHP64 */ public static function max(PHP64 ...$nums) { return self::maxHelper($nums); } /** * Tests BigInteger to see if it is between two integers, inclusive * * @param PHP64 $min * @param PHP64 $max * @return bool */ public function between(PHP64 $min, PHP64 $max) { return $this->compare($min) >= 0 && $this->compare($max) <= 0; } } PK!GAADvendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math\BigInteger\Engines; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\BadConfigurationException; /** * Pure-PHP Engine. * * @author Jim Wigginton */ abstract class PHP extends Engine { /**#@+ * Array constants * * Rather than create a thousands and thousands of new BigInteger objects in repeated function calls to add() and * multiply() or whatever, we'll just work directly on arrays, taking them in as parameters and returning them. * */ /** * $result[self::VALUE] contains the value. */ const VALUE = 0; /** * $result[self::SIGN] contains the sign. */ const SIGN = 1; /**#@-*/ /** * Karatsuba Cutoff * * At what point do we switch between Karatsuba multiplication and schoolbook long multiplication? * */ const KARATSUBA_CUTOFF = 25; /** * Can Bitwise operations be done fast? * * @see parent::bitwise_leftRotate() * @see parent::bitwise_rightRotate() */ const FAST_BITWISE = true; /** * Engine Directory * * @see parent::setModExpEngine */ const ENGINE_DIR = 'PHP'; /** * Default constructor * * @param mixed $x integer Base-10 number or base-$base number if $base set. * @param int $base * @return PHP * @see parent::__construct() */ public function __construct($x = 0, $base = 10) { if (!isset(static::$isValidEngine[static::class])) { static::$isValidEngine[static::class] = static::isValidEngine(); } if (!static::$isValidEngine[static::class]) { throw new BadConfigurationException(static::class . ' is not setup correctly on this system'); } $this->value = []; parent::__construct($x, $base); } /** * Initialize a PHP BigInteger Engine instance * * @param int $base * @see parent::__construct() */ protected function initialize($base) { switch (abs($base)) { case 16: $x = (strlen($this->value) & 1) ? '0' . $this->value : $this->value; $temp = new static(Strings::hex2bin($x), 256); $this->value = $temp->value; break; case 10: $temp = new static(); $multiplier = new static(); $multiplier->value = [static::MAX10]; $x = $this->value; if ($x[0] == '-') { $this->is_negative = true; $x = substr($x, 1); } $x = str_pad( $x, strlen($x) + ((static::MAX10LEN - 1) * strlen($x)) % static::MAX10LEN, 0, STR_PAD_LEFT ); while (strlen($x)) { $temp = $temp->multiply($multiplier); $temp = $temp->add(new static($this->int2bytes(substr($x, 0, static::MAX10LEN)), 256)); $x = substr($x, static::MAX10LEN); } $this->value = $temp->value; } } /** * Pads strings so that unpack may be used on them * * @param string $str * @return string */ protected function pad($str) { $length = strlen($str); $pad = 4 - (strlen($str) % 4); return str_pad($str, $length + $pad, "\0", STR_PAD_LEFT); } /** * Converts a BigInteger to a base-10 number. * * @return string */ public function toString() { if (!count($this->value)) { return '0'; } $temp = clone $this; $temp->bitmask = false; $temp->is_negative = false; $divisor = new static(); $divisor->value = [static::MAX10]; $result = ''; while (count($temp->value)) { list($temp, $mod) = $temp->divide($divisor); $result = str_pad( isset($mod->value[0]) ? $mod->value[0] : '', static::MAX10LEN, '0', STR_PAD_LEFT ) . $result; } $result = ltrim($result, '0'); if (empty($result)) { $result = '0'; } if ($this->is_negative) { $result = '-' . $result; } return $result; } /** * Converts a BigInteger to a byte string (eg. base-256). * * @param bool $twos_compliment * @return string */ public function toBytes($twos_compliment = false) { if ($twos_compliment) { return $this->toBytesHelper(); } if (!count($this->value)) { return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; } $result = $this->bitwise_small_split(8); $result = implode('', array_map('chr', $result)); return $this->precision > 0 ? str_pad( substr($result, -(($this->precision + 7) >> 3)), ($this->precision + 7) >> 3, chr(0), STR_PAD_LEFT ) : $result; } /** * Performs addition. * * @param array $x_value * @param bool $x_negative * @param array $y_value * @param bool $y_negative * @return array */ protected static function addHelper(array $x_value, $x_negative, array $y_value, $y_negative) { $x_size = count($x_value); $y_size = count($y_value); if ($x_size == 0) { return [ self::VALUE => $y_value, self::SIGN => $y_negative ]; } elseif ($y_size == 0) { return [ self::VALUE => $x_value, self::SIGN => $x_negative ]; } // subtract, if appropriate if ($x_negative != $y_negative) { if ($x_value == $y_value) { return [ self::VALUE => [], self::SIGN => false ]; } $temp = self::subtractHelper($x_value, false, $y_value, false); $temp[self::SIGN] = self::compareHelper($x_value, false, $y_value, false) > 0 ? $x_negative : $y_negative; return $temp; } if ($x_size < $y_size) { $size = $x_size; $value = $y_value; } else { $size = $y_size; $value = $x_value; } $value[count($value)] = 0; // just in case the carry adds an extra digit $carry = 0; for ($i = 0, $j = 1; $j < $size; $i += 2, $j += 2) { //$sum = $x_value[$j] * static::BASE_FULL + $x_value[$i] + $y_value[$j] * static::BASE_FULL + $y_value[$i] + $carry; $sum = ($x_value[$j] + $y_value[$j]) * static::BASE_FULL + $x_value[$i] + $y_value[$i] + $carry; $carry = $sum >= static::MAX_DIGIT2; // eg. floor($sum / 2**52); only possible values (in any base) are 0 and 1 $sum = $carry ? $sum - static::MAX_DIGIT2 : $sum; $temp = static::BASE === 26 ? intval($sum / 0x4000000) : ($sum >> 31); $value[$i] = (int)($sum - static::BASE_FULL * $temp); // eg. a faster alternative to fmod($sum, 0x4000000) $value[$j] = $temp; } if ($j == $size) { // ie. if $y_size is odd $sum = $x_value[$i] + $y_value[$i] + $carry; $carry = $sum >= static::BASE_FULL; $value[$i] = $carry ? $sum - static::BASE_FULL : $sum; ++$i; // ie. let $i = $j since we've just done $value[$i] } if ($carry) { for (; $value[$i] == static::MAX_DIGIT; ++$i) { $value[$i] = 0; } ++$value[$i]; } return [ self::VALUE => self::trim($value), self::SIGN => $x_negative ]; } /** * Performs subtraction. * * @param array $x_value * @param bool $x_negative * @param array $y_value * @param bool $y_negative * @return array */ public static function subtractHelper(array $x_value, $x_negative, array $y_value, $y_negative) { $x_size = count($x_value); $y_size = count($y_value); if ($x_size == 0) { return [ self::VALUE => $y_value, self::SIGN => !$y_negative ]; } elseif ($y_size == 0) { return [ self::VALUE => $x_value, self::SIGN => $x_negative ]; } // add, if appropriate (ie. -$x - +$y or +$x - -$y) if ($x_negative != $y_negative) { $temp = self::addHelper($x_value, false, $y_value, false); $temp[self::SIGN] = $x_negative; return $temp; } $diff = self::compareHelper($x_value, $x_negative, $y_value, $y_negative); if (!$diff) { return [ self::VALUE => [], self::SIGN => false ]; } // switch $x and $y around, if appropriate. if ((!$x_negative && $diff < 0) || ($x_negative && $diff > 0)) { $temp = $x_value; $x_value = $y_value; $y_value = $temp; $x_negative = !$x_negative; $x_size = count($x_value); $y_size = count($y_value); } // at this point, $x_value should be at least as big as - if not bigger than - $y_value $carry = 0; for ($i = 0, $j = 1; $j < $y_size; $i += 2, $j += 2) { $sum = ($x_value[$j] - $y_value[$j]) * static::BASE_FULL + $x_value[$i] - $y_value[$i] - $carry; $carry = $sum < 0; // eg. floor($sum / 2**52); only possible values (in any base) are 0 and 1 $sum = $carry ? $sum + static::MAX_DIGIT2 : $sum; $temp = static::BASE === 26 ? intval($sum / 0x4000000) : ($sum >> 31); $x_value[$i] = (int)($sum - static::BASE_FULL * $temp); $x_value[$j] = $temp; } if ($j == $y_size) { // ie. if $y_size is odd $sum = $x_value[$i] - $y_value[$i] - $carry; $carry = $sum < 0; $x_value[$i] = $carry ? $sum + static::BASE_FULL : $sum; ++$i; } if ($carry) { for (; !$x_value[$i]; ++$i) { $x_value[$i] = static::MAX_DIGIT; } --$x_value[$i]; } return [ self::VALUE => self::trim($x_value), self::SIGN => $x_negative ]; } /** * Performs multiplication. * * @param array $x_value * @param bool $x_negative * @param array $y_value * @param bool $y_negative * @return array */ protected static function multiplyHelper(array $x_value, $x_negative, array $y_value, $y_negative) { //if ( $x_value == $y_value ) { // return [ // self::VALUE => self::square($x_value), // self::SIGN => $x_sign != $y_value // ]; //} $x_length = count($x_value); $y_length = count($y_value); if (!$x_length || !$y_length) { // a 0 is being multiplied return [ self::VALUE => [], self::SIGN => false ]; } return [ self::VALUE => min($x_length, $y_length) < 2 * self::KARATSUBA_CUTOFF ? self::trim(self::regularMultiply($x_value, $y_value)) : self::trim(self::karatsuba($x_value, $y_value)), self::SIGN => $x_negative != $y_negative ]; } /** * Performs Karatsuba multiplication on two BigIntegers * * See {@link http://en.wikipedia.org/wiki/Karatsuba_algorithm Karatsuba algorithm} and * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=120 MPM 5.2.3}. * * @param array $x_value * @param array $y_value * @return array */ private static function karatsuba(array $x_value, array $y_value) { $m = min(count($x_value) >> 1, count($y_value) >> 1); if ($m < self::KARATSUBA_CUTOFF) { return self::regularMultiply($x_value, $y_value); } $x1 = array_slice($x_value, $m); $x0 = array_slice($x_value, 0, $m); $y1 = array_slice($y_value, $m); $y0 = array_slice($y_value, 0, $m); $z2 = self::karatsuba($x1, $y1); $z0 = self::karatsuba($x0, $y0); $z1 = self::addHelper($x1, false, $x0, false); $temp = self::addHelper($y1, false, $y0, false); $z1 = self::karatsuba($z1[self::VALUE], $temp[self::VALUE]); $temp = self::addHelper($z2, false, $z0, false); $z1 = self::subtractHelper($z1, false, $temp[self::VALUE], false); $z2 = array_merge(array_fill(0, 2 * $m, 0), $z2); $z1[self::VALUE] = array_merge(array_fill(0, $m, 0), $z1[self::VALUE]); $xy = self::addHelper($z2, false, $z1[self::VALUE], $z1[self::SIGN]); $xy = self::addHelper($xy[self::VALUE], $xy[self::SIGN], $z0, false); return $xy[self::VALUE]; } /** * Performs long multiplication on two BigIntegers * * Modeled after 'multiply' in MutableBigInteger.java. * * @param array $x_value * @param array $y_value * @return array */ protected static function regularMultiply(array $x_value, array $y_value) { $x_length = count($x_value); $y_length = count($y_value); if (!$x_length || !$y_length) { // a 0 is being multiplied return []; } $product_value = self::array_repeat(0, $x_length + $y_length); // the following for loop could be removed if the for loop following it // (the one with nested for loops) initially set $i to 0, but // doing so would also make the result in one set of unnecessary adds, // since on the outermost loops first pass, $product->value[$k] is going // to always be 0 $carry = 0; for ($j = 0; $j < $x_length; ++$j) { // ie. $i = 0 $temp = $x_value[$j] * $y_value[0] + $carry; // $product_value[$k] == 0 $carry = static::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $product_value[$j] = (int)($temp - static::BASE_FULL * $carry); } $product_value[$j] = $carry; // the above for loop is what the previous comment was talking about. the // following for loop is the "one with nested for loops" for ($i = 1; $i < $y_length; ++$i) { $carry = 0; for ($j = 0, $k = $i; $j < $x_length; ++$j, ++$k) { $temp = $product_value[$k] + $x_value[$j] * $y_value[$i] + $carry; $carry = static::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $product_value[$k] = (int)($temp - static::BASE_FULL * $carry); } $product_value[$k] = $carry; } return $product_value; } /** * Divides two BigIntegers. * * Returns an array whose first element contains the quotient and whose second element contains the * "common residue". If the remainder would be positive, the "common residue" and the remainder are the * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder * and the divisor (basically, the "common residue" is the first positive modulo). * * @return array{static, static} * @internal This function is based off of * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=9 HAC 14.20}. */ protected function divideHelper(PHP $y) { if (count($y->value) == 1) { list($q, $r) = $this->divide_digit($this->value, $y->value[0]); $quotient = new static(); $remainder = new static(); $quotient->value = $q; $remainder->value = [$r]; $quotient->is_negative = $this->is_negative != $y->is_negative; return [$this->normalize($quotient), $this->normalize($remainder)]; } $x = clone $this; $y = clone $y; $x_sign = $x->is_negative; $y_sign = $y->is_negative; $x->is_negative = $y->is_negative = false; $diff = $x->compare($y); if (!$diff) { $temp = new static(); $temp->value = [1]; $temp->is_negative = $x_sign != $y_sign; return [$this->normalize($temp), $this->normalize(static::$zero[static::class])]; } if ($diff < 0) { // if $x is negative, "add" $y. if ($x_sign) { $x = $y->subtract($x); } return [$this->normalize(static::$zero[static::class]), $this->normalize($x)]; } // normalize $x and $y as described in HAC 14.23 / 14.24 $msb = $y->value[count($y->value) - 1]; for ($shift = 0; !($msb & static::MSB); ++$shift) { $msb <<= 1; } $x->lshift($shift); $y->lshift($shift); $y_value = &$y->value; $x_max = count($x->value) - 1; $y_max = count($y->value) - 1; $quotient = new static(); $quotient_value = &$quotient->value; $quotient_value = self::array_repeat(0, $x_max - $y_max + 1); static $temp, $lhs, $rhs; if (!isset($temp)) { $temp = new static(); $lhs = new static(); $rhs = new static(); } if (static::class != get_class($temp)) { $temp = new static(); $lhs = new static(); $rhs = new static(); } $temp_value = &$temp->value; $rhs_value = &$rhs->value; // $temp = $y << ($x_max - $y_max-1) in base 2**26 $temp_value = array_merge(self::array_repeat(0, $x_max - $y_max), $y_value); while ($x->compare($temp) >= 0) { // calculate the "common residue" ++$quotient_value[$x_max - $y_max]; $x = $x->subtract($temp); $x_max = count($x->value) - 1; } for ($i = $x_max; $i >= $y_max + 1; --$i) { $x_value = &$x->value; $x_window = [ isset($x_value[$i]) ? $x_value[$i] : 0, isset($x_value[$i - 1]) ? $x_value[$i - 1] : 0, isset($x_value[$i - 2]) ? $x_value[$i - 2] : 0 ]; $y_window = [ $y_value[$y_max], ($y_max > 0) ? $y_value[$y_max - 1] : 0 ]; $q_index = $i - $y_max - 1; if ($x_window[0] == $y_window[0]) { $quotient_value[$q_index] = static::MAX_DIGIT; } else { $quotient_value[$q_index] = self::safe_divide( $x_window[0] * static::BASE_FULL + $x_window[1], $y_window[0] ); } $temp_value = [$y_window[1], $y_window[0]]; $lhs->value = [$quotient_value[$q_index]]; $lhs = $lhs->multiply($temp); $rhs_value = [$x_window[2], $x_window[1], $x_window[0]]; while ($lhs->compare($rhs) > 0) { --$quotient_value[$q_index]; $lhs->value = [$quotient_value[$q_index]]; $lhs = $lhs->multiply($temp); } $adjust = self::array_repeat(0, $q_index); $temp_value = [$quotient_value[$q_index]]; $temp = $temp->multiply($y); $temp_value = &$temp->value; if (count($temp_value)) { $temp_value = array_merge($adjust, $temp_value); } $x = $x->subtract($temp); if ($x->compare(static::$zero[static::class]) < 0) { $temp_value = array_merge($adjust, $y_value); $x = $x->add($temp); --$quotient_value[$q_index]; } $x_max = count($x_value) - 1; } // unnormalize the remainder $x->rshift($shift); $quotient->is_negative = $x_sign != $y_sign; // calculate the "common residue", if appropriate if ($x_sign) { $y->rshift($shift); $x = $y->subtract($x); } return [$this->normalize($quotient), $this->normalize($x)]; } /** * Divides a BigInteger by a regular integer * * abc / x = a00 / x + b0 / x + c / x * * @param array $dividend * @param int $divisor * @return array */ private static function divide_digit(array $dividend, $divisor) { $carry = 0; $result = []; for ($i = count($dividend) - 1; $i >= 0; --$i) { $temp = static::BASE_FULL * $carry + $dividend[$i]; $result[$i] = self::safe_divide($temp, $divisor); $carry = (int)($temp - $divisor * $result[$i]); } return [$result, $carry]; } /** * Single digit division * * Even if int64 is being used the division operator will return a float64 value * if the dividend is not evenly divisible by the divisor. Since a float64 doesn't * have the precision of int64 this is a problem so, when int64 is being used, * we'll guarantee that the dividend is divisible by first subtracting the remainder. * * @param int $x * @param int $y * @return int */ private static function safe_divide($x, $y) { if (static::BASE === 26) { return (int)($x / $y); } // static::BASE === 31 /** @var int */ return ($x - ($x % $y)) / $y; } /** * Convert an array / boolean to a PHP BigInteger object * * @param array $arr * @return static */ protected function convertToObj(array $arr) { $result = new static(); $result->value = $arr[self::VALUE]; $result->is_negative = $arr[self::SIGN]; return $this->normalize($result); } /** * Normalize * * Removes leading zeros and truncates (if necessary) to maintain the appropriate precision * * @param PHP $result * @return static */ protected function normalize(PHP $result) { $result->precision = $this->precision; $result->bitmask = $this->bitmask; $value = &$result->value; if (!count($value)) { $result->is_negative = false; return $result; } $value = static::trim($value); if (!empty($result->bitmask->value)) { $length = min(count($value), count($result->bitmask->value)); $value = array_slice($value, 0, $length); for ($i = 0; $i < $length; ++$i) { $value[$i] = $value[$i] & $result->bitmask->value[$i]; } $value = static::trim($value); } return $result; } /** * Compares two numbers. * * @param array $x_value * @param bool $x_negative * @param array $y_value * @param bool $y_negative * @return int * @see static::compare() */ protected static function compareHelper(array $x_value, $x_negative, array $y_value, $y_negative) { if ($x_negative != $y_negative) { return (!$x_negative && $y_negative) ? 1 : -1; } $result = $x_negative ? -1 : 1; if (count($x_value) != count($y_value)) { return (count($x_value) > count($y_value)) ? $result : -$result; } $size = max(count($x_value), count($y_value)); $x_value = array_pad($x_value, $size, 0); $y_value = array_pad($y_value, $size, 0); for ($i = count($x_value) - 1; $i >= 0; --$i) { if ($x_value[$i] != $y_value[$i]) { return ($x_value[$i] > $y_value[$i]) ? $result : -$result; } } return 0; } /** * Absolute value. * * @return PHP */ public function abs() { $temp = new static(); $temp->value = $this->value; return $temp; } /** * Trim * * Removes leading zeros * * @param list $value * @return list */ protected static function trim(array $value) { for ($i = count($value) - 1; $i >= 0; --$i) { if ($value[$i]) { break; } unset($value[$i]); } return $value; } /** * Logical Right Shift * * Shifts BigInteger's by $shift bits, effectively dividing by 2**$shift. * * @param int $shift * @return PHP */ public function bitwise_rightShift($shift) { $temp = new static(); // could just replace lshift with this, but then all lshift() calls would need to be rewritten // and I don't want to do that... $temp->value = $this->value; $temp->rshift($shift); return $this->normalize($temp); } /** * Logical Left Shift * * Shifts BigInteger's by $shift bits, effectively multiplying by 2**$shift. * * @param int $shift * @return PHP */ public function bitwise_leftShift($shift) { $temp = new static(); // could just replace _rshift with this, but then all _lshift() calls would need to be rewritten // and I don't want to do that... $temp->value = $this->value; $temp->lshift($shift); return $this->normalize($temp); } /** * Converts 32-bit integers to bytes. * * @param int $x * @return string */ private static function int2bytes($x) { return ltrim(pack('N', $x), chr(0)); } /** * Array Repeat * * @param int $input * @param int $multiplier * @return array */ protected static function array_repeat($input, $multiplier) { return $multiplier ? array_fill(0, $multiplier, $input) : []; } /** * Logical Left Shift * * Shifts BigInteger's by $shift bits. * * @param int $shift */ protected function lshift($shift) { if ($shift == 0) { return; } $num_digits = (int)($shift / static::BASE); $shift %= static::BASE; $shift = 1 << $shift; $carry = 0; for ($i = 0; $i < count($this->value); ++$i) { $temp = $this->value[$i] * $shift + $carry; $carry = static::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $this->value[$i] = (int)($temp - $carry * static::BASE_FULL); } if ($carry) { $this->value[count($this->value)] = $carry; } while ($num_digits--) { array_unshift($this->value, 0); } } /** * Logical Right Shift * * Shifts BigInteger's by $shift bits. * * @param int $shift */ protected function rshift($shift) { if ($shift == 0) { return; } $num_digits = (int)($shift / static::BASE); $shift %= static::BASE; $carry_shift = static::BASE - $shift; $carry_mask = (1 << $shift) - 1; if ($num_digits) { $this->value = array_slice($this->value, $num_digits); } $carry = 0; for ($i = count($this->value) - 1; $i >= 0; --$i) { $temp = $this->value[$i] >> $shift | $carry; $carry = ($this->value[$i] & $carry_mask) << $carry_shift; $this->value[$i] = $temp; } $this->value = static::trim($this->value); } /** * Performs modular exponentiation. * * @param PHP $e * @param PHP $n * @return PHP */ protected function powModInner(PHP $e, PHP $n) { try { $class = static::$modexpEngine[static::class]; return $class::powModHelper($this, $e, $n, static::class); } catch (\Exception $err) { return PHP\DefaultEngine::powModHelper($this, $e, $n, static::class); } } /** * Performs squaring * * @param list $x * @return list */ protected static function square(array $x) { return count($x) < 2 * self::KARATSUBA_CUTOFF ? self::trim(self::baseSquare($x)) : self::trim(self::karatsubaSquare($x)); } /** * Performs traditional squaring on two BigIntegers * * Squaring can be done faster than multiplying a number by itself can be. See * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=7 HAC 14.2.4} / * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=141 MPM 5.3} for more information. * * @param array $value * @return array */ protected static function baseSquare(array $value) { if (empty($value)) { return []; } $square_value = self::array_repeat(0, 2 * count($value)); for ($i = 0, $max_index = count($value) - 1; $i <= $max_index; ++$i) { $i2 = $i << 1; $temp = $square_value[$i2] + $value[$i] * $value[$i]; $carry = static::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $square_value[$i2] = (int)($temp - static::BASE_FULL * $carry); // note how we start from $i+1 instead of 0 as we do in multiplication. for ($j = $i + 1, $k = $i2 + 1; $j <= $max_index; ++$j, ++$k) { $temp = $square_value[$k] + 2 * $value[$j] * $value[$i] + $carry; $carry = static::BASE === 26 ? intval($temp / 0x4000000) : ($temp >> 31); $square_value[$k] = (int)($temp - static::BASE_FULL * $carry); } // the following line can yield values larger 2**15. at this point, PHP should switch // over to floats. $square_value[$i + $max_index + 1] = $carry; } return $square_value; } /** * Performs Karatsuba "squaring" on two BigIntegers * * See {@link http://en.wikipedia.org/wiki/Karatsuba_algorithm Karatsuba algorithm} and * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=151 MPM 5.3.4}. * * @param array $value * @return array */ protected static function karatsubaSquare(array $value) { $m = count($value) >> 1; if ($m < self::KARATSUBA_CUTOFF) { return self::baseSquare($value); } $x1 = array_slice($value, $m); $x0 = array_slice($value, 0, $m); $z2 = self::karatsubaSquare($x1); $z0 = self::karatsubaSquare($x0); $z1 = self::addHelper($x1, false, $x0, false); $z1 = self::karatsubaSquare($z1[self::VALUE]); $temp = self::addHelper($z2, false, $z0, false); $z1 = self::subtractHelper($z1, false, $temp[self::VALUE], false); $z2 = array_merge(array_fill(0, 2 * $m, 0), $z2); $z1[self::VALUE] = array_merge(array_fill(0, $m, 0), $z1[self::VALUE]); $xx = self::addHelper($z2, false, $z1[self::VALUE], $z1[self::SIGN]); $xx = self::addHelper($xx[self::VALUE], $xx[self::SIGN], $z0, false); return $xx[self::VALUE]; } /** * Make the current number odd * * If the current number is odd it'll be unchanged. If it's even, one will be added to it. * * @see self::randomPrime() */ protected function make_odd() { $this->value[0] |= 1; } /** * Test the number against small primes. * * @see self::isPrime() */ protected function testSmallPrimes() { if ($this->value == [1]) { return false; } if ($this->value == [2]) { return true; } if (~$this->value[0] & 1) { return false; } $value = $this->value; foreach (static::PRIMES as $prime) { list(, $r) = self::divide_digit($value, $prime); if (!$r) { return count($value) == 1 && $value[0] == $prime; } } return true; } /** * Scan for 1 and right shift by that amount * * ie. $s = gmp_scan1($n, 0) and $r = gmp_div_q($n, gmp_pow(gmp_init('2'), $s)); * * @param PHP $r * @return int * @see self::isPrime() */ public static function scan1divide(PHP $r) { $r_value = &$r->value; for ($i = 0, $r_length = count($r_value); $i < $r_length; ++$i) { $temp = ~$r_value[$i] & static::MAX_DIGIT; for ($j = 1; ($temp >> $j) & 1; ++$j) { } if ($j <= static::BASE) { break; } } $s = static::BASE * $i + $j; $r->rshift($s); return $s; } /** * Performs exponentiation. * * @param PHP $n * @return PHP */ protected function powHelper(PHP $n) { if ($n->compare(static::$zero[static::class]) == 0) { return new static(1); } // n^0 = 1 $temp = clone $this; while (!$n->equals(static::$one[static::class])) { $temp = $temp->multiply($this); $n = $n->subtract(static::$one[static::class]); } return $temp; } /** * Is Odd? * * @return bool */ public function isOdd() { return (bool)($this->value[0] & 1); } /** * Tests if a bit is set * * @return bool */ public function testBit($x) { $digit = (int) floor($x / static::BASE); $bit = $x % static::BASE; if (!isset($this->value[$digit])) { return false; } return (bool)($this->value[$digit] & (1 << $bit)); } /** * Is Negative? * * @return bool */ public function isNegative() { return $this->is_negative; } /** * Negate * * Given $k, returns -$k * * @return static */ public function negate() { $temp = clone $this; $temp->is_negative = !$temp->is_negative; return $temp; } /** * Bitwise Split * * Splits BigInteger's into chunks of $split bits * * @param int $split * @return list */ public function bitwise_split($split) { if ($split < 1) { throw new \RuntimeException('Offset must be greater than 1'); } $width = (int)($split / static::BASE); if (!$width) { $arr = $this->bitwise_small_split($split); return array_map(function ($digit) { $temp = new static(); $temp->value = $digit != 0 ? [$digit] : []; return $temp; }, $arr); } $vals = []; $val = $this->value; $i = $overflow = 0; $len = count($val); while ($i < $len) { $digit = []; if (!$overflow) { $digit = array_slice($val, $i, $width); $i += $width; $overflow = $split % static::BASE; if ($overflow) { $mask = (1 << $overflow) - 1; $temp = isset($val[$i]) ? $val[$i] : 0; $digit[] = $temp & $mask; } } else { $remaining = static::BASE - $overflow; $tempsplit = $split - $remaining; $tempwidth = (int)($tempsplit / static::BASE + 1); $digit = array_slice($val, $i, $tempwidth); $i += $tempwidth; $tempoverflow = $tempsplit % static::BASE; if ($tempoverflow) { $tempmask = (1 << $tempoverflow) - 1; $temp = isset($val[$i]) ? $val[$i] : 0; $digit[] = $temp & $tempmask; } $newbits = 0; for ($j = count($digit) - 1; $j >= 0; $j--) { $temp = $digit[$j] & $mask; $digit[$j] = ($digit[$j] >> $overflow) | ($newbits << $remaining); $newbits = $temp; } $overflow = $tempoverflow; $mask = $tempmask; } $temp = new static(); $temp->value = static::trim($digit); $vals[] = $temp; } return array_reverse($vals); } /** * Bitwise Split where $split < static::BASE * * @param int $split * @return list */ private function bitwise_small_split($split) { $vals = []; $val = $this->value; $mask = (1 << $split) - 1; $i = $overflow = 0; $len = count($val); $val[] = 0; $remaining = static::BASE; while ($i != $len) { $digit = $val[$i] & $mask; $val[$i] >>= $split; if (!$overflow) { $remaining -= $split; $overflow = $split <= $remaining ? 0 : $split - $remaining; if (!$remaining) { $i++; $remaining = static::BASE; $overflow = 0; } } elseif (++$i != $len) { $tempmask = (1 << $overflow) - 1; $digit |= ($val[$i] & $tempmask) << $remaining; $val[$i] >>= $overflow; $remaining = static::BASE - $overflow; $overflow = $split <= $remaining ? 0 : $split - $remaining; } $vals[] = $digit; } while ($vals[count($vals) - 1] == 0) { unset($vals[count($vals) - 1]); } return array_reverse($vals); } /** * @return bool */ protected static function testJITOnWindows() { // see https://github.com/php/php-src/issues/11917 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && function_exists('opcache_get_status') && PHP_VERSION_ID < 80213 && !defined('PHPSECLIB_ALLOW_JIT')) { $status = opcache_get_status(); if ($status && isset($status['jit']) && $status['jit']['enabled'] && $status['jit']['on']) { return true; } } return false; } /** * Return the size of a BigInteger in bits * * @return int */ public function getLength() { $max = count($this->value) - 1; return $max != -1 ? $max * static::BASE + intval(ceil(log($this->value[$max] + 1, 2))) : 0; } } PK!q1÷55Avendor/phpseclib/phpseclib/phpseclib/Math/BinaryField/Integer.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Math\BinaryField; use phpseclib3\Common\Functions\Strings; use phpseclib3\Math\BigInteger; use phpseclib3\Math\BinaryField; use phpseclib3\Math\Common\FiniteField\Integer as Base; /** * Binary Finite Fields * * @author Jim Wigginton */ class Integer extends Base { /** * Holds the BinaryField's value * * @var string */ protected $value; /** * Keeps track of current instance * * @var int */ protected $instanceID; /** * Holds the PrimeField's modulo * * @var array */ protected static $modulo; /** * Holds a pre-generated function to perform modulo reductions * * @var callable[] */ protected static $reduce; /** * Default constructor */ public function __construct($instanceID, $num = '') { $this->instanceID = $instanceID; if (!strlen($num)) { $this->value = ''; } else { $reduce = static::$reduce[$instanceID]; $this->value = $reduce($num); } } /** * Set the modulo for a given instance * @param int $instanceID * @param string $modulo */ public static function setModulo($instanceID, $modulo) { static::$modulo[$instanceID] = $modulo; } /** * Set the modulo for a given instance */ public static function setRecurringModuloFunction($instanceID, callable $function) { static::$reduce[$instanceID] = $function; } /** * Tests a parameter to see if it's of the right instance * * Throws an exception if the incorrect class is being utilized */ private static function checkInstance(self $x, self $y) { if ($x->instanceID != $y->instanceID) { throw new \UnexpectedValueException('The instances of the two BinaryField\Integer objects do not match'); } } /** * Tests the equality of two numbers. * * @return bool */ public function equals(self $x) { static::checkInstance($this, $x); return $this->value == $x->value; } /** * Compares two numbers. * * @return int */ public function compare(self $x) { static::checkInstance($this, $x); $a = $this->value; $b = $x->value; $length = max(strlen($a), strlen($b)); $a = str_pad($a, $length, "\0", STR_PAD_LEFT); $b = str_pad($b, $length, "\0", STR_PAD_LEFT); return strcmp($a, $b); } /** * Returns the degree of the polynomial * * @param string $x * @return int */ private static function deg($x) { $x = ltrim($x, "\0"); $xbit = decbin(ord($x[0])); $xlen = $xbit == '0' ? 0 : strlen($xbit); $len = strlen($x); if (!$len) { return -1; } return 8 * strlen($x) - 9 + $xlen; } /** * Perform polynomial division * * @return string[] * @link https://en.wikipedia.org/wiki/Polynomial_greatest_common_divisor#Euclidean_division */ private static function polynomialDivide($x, $y) { // in wikipedia's description of the algorithm, lc() is the leading coefficient. over a binary field that's // always going to be 1. $q = chr(0); $d = static::deg($y); $r = $x; while (($degr = static::deg($r)) >= $d) { $s = '1' . str_repeat('0', $degr - $d); $s = BinaryField::base2ToBase256($s); $length = max(strlen($s), strlen($q)); $q = !isset($q) ? $s : str_pad($q, $length, "\0", STR_PAD_LEFT) ^ str_pad($s, $length, "\0", STR_PAD_LEFT); $s = static::polynomialMultiply($s, $y); $length = max(strlen($r), strlen($s)); $r = str_pad($r, $length, "\0", STR_PAD_LEFT) ^ str_pad($s, $length, "\0", STR_PAD_LEFT); } return [ltrim($q, "\0"), ltrim($r, "\0")]; } /** * Perform polynomial multiplation in the traditional way * * @return string * @link https://en.wikipedia.org/wiki/Finite_field_arithmetic#Multiplication */ private static function regularPolynomialMultiply($x, $y) { $precomputed = [ltrim($x, "\0")]; $x = strrev(BinaryField::base256ToBase2($x)); $y = strrev(BinaryField::base256ToBase2($y)); if (strlen($x) == strlen($y)) { $length = strlen($x); } else { $length = max(strlen($x), strlen($y)); $x = str_pad($x, $length, '0'); $y = str_pad($y, $length, '0'); } $result = str_repeat('0', 2 * $length - 1); $result = BinaryField::base2ToBase256($result); $size = strlen($result); $x = strrev($x); // precompute left shift 1 through 7 for ($i = 1; $i < 8; $i++) { $precomputed[$i] = BinaryField::base2ToBase256($x . str_repeat('0', $i)); } for ($i = 0; $i < strlen($y); $i++) { if ($y[$i] == '1') { $temp = $precomputed[$i & 7] . str_repeat("\0", $i >> 3); $result ^= str_pad($temp, $size, "\0", STR_PAD_LEFT); } } return $result; } /** * Perform polynomial multiplation * * Uses karatsuba multiplication to reduce x-bit multiplications to a series of 32-bit multiplications * * @return string * @link https://en.wikipedia.org/wiki/Karatsuba_algorithm */ private static function polynomialMultiply($x, $y) { if (strlen($x) == strlen($y)) { $length = strlen($x); } else { $length = max(strlen($x), strlen($y)); $x = str_pad($x, $length, "\0", STR_PAD_LEFT); $y = str_pad($y, $length, "\0", STR_PAD_LEFT); } switch (true) { case PHP_INT_SIZE == 8 && $length <= 4: return $length != 4 ? self::subMultiply(str_pad($x, 4, "\0", STR_PAD_LEFT), str_pad($y, 4, "\0", STR_PAD_LEFT)) : self::subMultiply($x, $y); case PHP_INT_SIZE == 4 || $length > 32: return self::regularPolynomialMultiply($x, $y); } $m = $length >> 1; $x1 = substr($x, 0, -$m); $x0 = substr($x, -$m); $y1 = substr($y, 0, -$m); $y0 = substr($y, -$m); $z2 = self::polynomialMultiply($x1, $y1); $z0 = self::polynomialMultiply($x0, $y0); $z1 = self::polynomialMultiply( self::subAdd2($x1, $x0), self::subAdd2($y1, $y0) ); $z1 = self::subAdd3($z1, $z2, $z0); $xy = self::subAdd3( $z2 . str_repeat("\0", 2 * $m), $z1 . str_repeat("\0", $m), $z0 ); return ltrim($xy, "\0"); } /** * Perform polynomial multiplication on 2x 32-bit numbers, returning * a 64-bit number * * @param string $x * @param string $y * @return string * @link https://www.bearssl.org/constanttime.html#ghash-for-gcm */ private static function subMultiply($x, $y) { $x = unpack('N', $x)[1]; $y = unpack('N', $y)[1]; $x0 = $x & 0x11111111; $x1 = $x & 0x22222222; $x2 = $x & 0x44444444; $x3 = $x & 0x88888888; $y0 = $y & 0x11111111; $y1 = $y & 0x22222222; $y2 = $y & 0x44444444; $y3 = $y & 0x88888888; $z0 = ($x0 * $y0) ^ ($x1 * $y3) ^ ($x2 * $y2) ^ ($x3 * $y1); $z1 = ($x0 * $y1) ^ ($x1 * $y0) ^ ($x2 * $y3) ^ ($x3 * $y2); $z2 = ($x0 * $y2) ^ ($x1 * $y1) ^ ($x2 * $y0) ^ ($x3 * $y3); $z3 = ($x0 * $y3) ^ ($x1 * $y2) ^ ($x2 * $y1) ^ ($x3 * $y0); $z0 &= 0x1111111111111111; $z1 &= 0x2222222222222222; $z2 &= 0x4444444444444444; $z3 &= -8608480567731124088; // 0x8888888888888888 gets interpreted as a float $z = $z0 | $z1 | $z2 | $z3; return pack('J', $z); } /** * Adds two numbers * * @param string $x * @param string $y * @return string */ private static function subAdd2($x, $y) { $length = max(strlen($x), strlen($y)); $x = str_pad($x, $length, "\0", STR_PAD_LEFT); $y = str_pad($y, $length, "\0", STR_PAD_LEFT); return $x ^ $y; } /** * Adds three numbers * * @param string $x * @param string $y * @return string */ private static function subAdd3($x, $y, $z) { $length = max(strlen($x), strlen($y), strlen($z)); $x = str_pad($x, $length, "\0", STR_PAD_LEFT); $y = str_pad($y, $length, "\0", STR_PAD_LEFT); $z = str_pad($z, $length, "\0", STR_PAD_LEFT); return $x ^ $y ^ $z; } /** * Adds two BinaryFieldIntegers. * * @return static */ public function add(self $y) { static::checkInstance($this, $y); $length = strlen(static::$modulo[$this->instanceID]); $x = str_pad($this->value, $length, "\0", STR_PAD_LEFT); $y = str_pad($y->value, $length, "\0", STR_PAD_LEFT); return new static($this->instanceID, $x ^ $y); } /** * Subtracts two BinaryFieldIntegers. * * @return static */ public function subtract(self $x) { return $this->add($x); } /** * Multiplies two BinaryFieldIntegers. * * @return static */ public function multiply(self $y) { static::checkInstance($this, $y); return new static($this->instanceID, static::polynomialMultiply($this->value, $y->value)); } /** * Returns the modular inverse of a BinaryFieldInteger * * @return static */ public function modInverse() { $remainder0 = static::$modulo[$this->instanceID]; $remainder1 = $this->value; if ($remainder1 == '') { return new static($this->instanceID); } $aux0 = "\0"; $aux1 = "\1"; while ($remainder1 != "\1") { list($q, $r) = static::polynomialDivide($remainder0, $remainder1); $remainder0 = $remainder1; $remainder1 = $r; // the auxiliary in row n is given by the sum of the auxiliary in // row n-2 and the product of the quotient and the auxiliary in row // n-1 $temp = static::polynomialMultiply($aux1, $q); $aux = str_pad($aux0, strlen($temp), "\0", STR_PAD_LEFT) ^ str_pad($temp, strlen($aux0), "\0", STR_PAD_LEFT); $aux0 = $aux1; $aux1 = $aux; } $temp = new static($this->instanceID); $temp->value = ltrim($aux1, "\0"); return $temp; } /** * Divides two PrimeFieldIntegers. * * @return static */ public function divide(self $x) { static::checkInstance($this, $x); $x = $x->modInverse(); return $this->multiply($x); } /** * Negate * * A negative number can be written as 0-12. With modulos, 0 is the same thing as the modulo * so 0-12 is the same thing as modulo-12 * * @return object */ public function negate() { $x = str_pad($this->value, strlen(static::$modulo[$this->instanceID]), "\0", STR_PAD_LEFT); return new static($this->instanceID, $x ^ static::$modulo[$this->instanceID]); } /** * Returns the modulo * * @return string */ public static function getModulo($instanceID) { return static::$modulo[$instanceID]; } /** * Converts an Integer to a byte string (eg. base-256). * * @return string */ public function toBytes() { return str_pad($this->value, strlen(static::$modulo[$this->instanceID]), "\0", STR_PAD_LEFT); } /** * Converts an Integer to a hex string (eg. base-16). * * @return string */ public function toHex() { return Strings::bin2hex($this->toBytes()); } /** * Converts an Integer to a bit string (eg. base-2). * * @return string */ public function toBits() { //return str_pad(BinaryField::base256ToBase2($this->value), strlen(static::$modulo[$this->instanceID]), '0', STR_PAD_LEFT); return BinaryField::base256ToBase2($this->value); } /** * Converts an Integer to a BigInteger * * @return string */ public function toBigInteger() { return new BigInteger($this->value, 256); } /** * __toString() magic method * */ public function __toString() { return (string) $this->toBigInteger(); } /** * __debugInfo() magic method * */ public function __debugInfo() { return ['value' => $this->toHex()]; } } PK!@|Hvendor/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField/Integer.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Math\Common\FiniteField; /** * Finite Field Integer * * @author Jim Wigginton */ abstract class Integer implements \JsonSerializable { /** * JSON Serialize * * Will be called, automatically, when json_encode() is called on a BigInteger object. * * PHP Serialize isn't supported because unserializing would require the factory be * serialized as well and that just sounds like too much * * @return array{hex: string} */ #[\ReturnTypeWillChange] public function jsonSerialize() { return ['hex' => $this->toHex(true)]; } /** * Converts an Integer to a hex string (eg. base-16). * * @return string */ abstract public function toHex(); } PK!www@vendor/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Math\Common; /** * Finite Fields * * @author Jim Wigginton */ abstract class FiniteField { } PK!4 V0((@vendor/phpseclib/phpseclib/phpseclib/Math/PrimeField/Integer.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Math\PrimeField; use phpseclib3\Common\Functions\Strings; use phpseclib3\Math\BigInteger; use phpseclib3\Math\Common\FiniteField\Integer as Base; /** * Prime Finite Fields * * @author Jim Wigginton */ class Integer extends Base { /** * Holds the PrimeField's value * * @var BigInteger */ protected $value; /** * Keeps track of current instance * * @var int */ protected $instanceID; /** * Holds the PrimeField's modulo * * @var array */ protected static $modulo; /** * Holds a pre-generated function to perform modulo reductions * * @var array */ protected static $reduce; /** * Zero * * @var BigInteger */ protected static $zero; /** * Default constructor * * @param int $instanceID * @param BigInteger $num */ public function __construct($instanceID, $num = null) { $this->instanceID = $instanceID; if (!isset($num)) { $this->value = clone static::$zero[static::class]; } else { $reduce = static::$reduce[$instanceID]; $this->value = $reduce($num); } } /** * Set the modulo for a given instance * * @param int $instanceID * @return void */ public static function setModulo($instanceID, BigInteger $modulo) { static::$modulo[$instanceID] = $modulo; } /** * Set the modulo for a given instance * * @param int $instanceID * @return void */ public static function setRecurringModuloFunction($instanceID, callable $function) { static::$reduce[$instanceID] = $function; if (!isset(static::$zero[static::class])) { static::$zero[static::class] = new BigInteger(); } } /** * Delete the modulo for a given instance */ public static function cleanupCache($instanceID) { unset(static::$modulo[$instanceID]); unset(static::$reduce[$instanceID]); } /** * Returns the modulo * * @param int $instanceID * @return BigInteger */ public static function getModulo($instanceID) { return static::$modulo[$instanceID]; } /** * Tests a parameter to see if it's of the right instance * * Throws an exception if the incorrect class is being utilized * * @return void */ public static function checkInstance(self $x, self $y) { if ($x->instanceID != $y->instanceID) { throw new \UnexpectedValueException('The instances of the two PrimeField\Integer objects do not match'); } } /** * Tests the equality of two numbers. * * @return bool */ public function equals(self $x) { static::checkInstance($this, $x); return $this->value->equals($x->value); } /** * Compares two numbers. * * @return int */ public function compare(self $x) { static::checkInstance($this, $x); return $this->value->compare($x->value); } /** * Adds two PrimeFieldIntegers. * * @return static */ public function add(self $x) { static::checkInstance($this, $x); $temp = new static($this->instanceID); $temp->value = $this->value->add($x->value); if ($temp->value->compare(static::$modulo[$this->instanceID]) >= 0) { $temp->value = $temp->value->subtract(static::$modulo[$this->instanceID]); } return $temp; } /** * Subtracts two PrimeFieldIntegers. * * @return static */ public function subtract(self $x) { static::checkInstance($this, $x); $temp = new static($this->instanceID); $temp->value = $this->value->subtract($x->value); if ($temp->value->isNegative()) { $temp->value = $temp->value->add(static::$modulo[$this->instanceID]); } return $temp; } /** * Multiplies two PrimeFieldIntegers. * * @return static */ public function multiply(self $x) { static::checkInstance($this, $x); return new static($this->instanceID, $this->value->multiply($x->value)); } /** * Divides two PrimeFieldIntegers. * * @return static */ public function divide(self $x) { static::checkInstance($this, $x); $denominator = $x->value->modInverse(static::$modulo[$this->instanceID]); return new static($this->instanceID, $this->value->multiply($denominator)); } /** * Performs power operation on a PrimeFieldInteger. * * @return static */ public function pow(BigInteger $x) { $temp = new static($this->instanceID); $temp->value = $this->value->powMod($x, static::$modulo[$this->instanceID]); return $temp; } /** * Calculates the square root * * @link https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm * @return static|false */ public function squareRoot() { static $one, $two; if (!isset($one)) { $one = new BigInteger(1); $two = new BigInteger(2); } $reduce = static::$reduce[$this->instanceID]; $p_1 = static::$modulo[$this->instanceID]->subtract($one); $q = clone $p_1; $s = BigInteger::scan1divide($q); list($pow) = $p_1->divide($two); for ($z = $one; !$z->equals(static::$modulo[$this->instanceID]); $z = $z->add($one)) { $temp = $z->powMod($pow, static::$modulo[$this->instanceID]); if ($temp->equals($p_1)) { break; } } $m = new BigInteger($s); $c = $z->powMod($q, static::$modulo[$this->instanceID]); $t = $this->value->powMod($q, static::$modulo[$this->instanceID]); list($temp) = $q->add($one)->divide($two); $r = $this->value->powMod($temp, static::$modulo[$this->instanceID]); while (!$t->equals($one)) { for ($i = clone $one; $i->compare($m) < 0; $i = $i->add($one)) { if ($t->powMod($two->pow($i), static::$modulo[$this->instanceID])->equals($one)) { break; } } if ($i->compare($m) == 0) { return false; } $b = $c->powMod($two->pow($m->subtract($i)->subtract($one)), static::$modulo[$this->instanceID]); $m = $i; $c = $reduce($b->multiply($b)); $t = $reduce($t->multiply($c)); $r = $reduce($r->multiply($b)); } return new static($this->instanceID, $r); } /** * Is Odd? * * @return bool */ public function isOdd() { return $this->value->isOdd(); } /** * Negate * * A negative number can be written as 0-12. With modulos, 0 is the same thing as the modulo * so 0-12 is the same thing as modulo-12 * * @return static */ public function negate() { return new static($this->instanceID, static::$modulo[$this->instanceID]->subtract($this->value)); } /** * Converts an Integer to a byte string (eg. base-256). * * @return string */ public function toBytes() { if (isset(static::$modulo[$this->instanceID])) { $length = static::$modulo[$this->instanceID]->getLengthInBytes(); return str_pad($this->value->toBytes(), $length, "\0", STR_PAD_LEFT); } return $this->value->toBytes(); } /** * Converts an Integer to a hex string (eg. base-16). * * @return string */ public function toHex() { return Strings::bin2hex($this->toBytes()); } /** * Converts an Integer to a bit string (eg. base-2). * * @return string */ public function toBits() { // return $this->value->toBits(); static $length; if (!isset($length)) { $length = static::$modulo[$this->instanceID]->getLength(); } return str_pad($this->value->toBits(), $length, '0', STR_PAD_LEFT); } /** * Returns the w-ary non-adjacent form (wNAF) * * @param int $w optional * @return array */ public function getNAF($w = 1) { $w++; $mask = new BigInteger((1 << $w) - 1); $sub = new BigInteger(1 << $w); //$sub = new BigInteger(1 << ($w - 1)); $d = $this->toBigInteger(); $d_i = []; $i = 0; while ($d->compare(static::$zero[static::class]) > 0) { if ($d->isOdd()) { // start mods $bigInteger = $d->testBit($w - 1) ? $d->bitwise_and($mask)->subtract($sub) : //$sub->subtract($d->bitwise_and($mask)) : $d->bitwise_and($mask); // end mods $d = $d->subtract($bigInteger); $d_i[$i] = (int) $bigInteger->toString(); } else { $d_i[$i] = 0; } $shift = !$d->equals(static::$zero[static::class]) && $d->bitwise_and($mask)->equals(static::$zero[static::class]) ? $w : 1; // $w or $w + 1? $d = $d->bitwise_rightShift($shift); while (--$shift > 0) { $d_i[++$i] = 0; } $i++; } return $d_i; } /** * Converts an Integer to a BigInteger * * @return BigInteger */ public function toBigInteger() { return clone $this->value; } /** * __toString() magic method * * @return string */ public function __toString() { return (string) $this->value; } /** * __debugInfo() magic method * * @return array */ public function __debugInfo() { return ['value' => $this->toHex()]; } } PK!CdVV8vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger.phpnu[ * add($b); * * echo $c->toString(); // outputs 5 * ?> * * * @author Jim Wigginton * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Math; use phpseclib3\Exception\BadConfigurationException; use phpseclib3\Math\BigInteger\Engines\Engine; /** * Pure-PHP arbitrary precision integer arithmetic library. Supports base-2, base-10, base-16, and base-256 * numbers. * * @author Jim Wigginton */ class BigInteger implements \JsonSerializable { /** * Main Engine * * @var class-string */ private static $mainEngine; /** * Selected Engines * * @var list */ private static $engines; /** * The actual BigInteger object * * @var object */ private $value; /** * Mode independent value used for serialization. * * @see self::__sleep() * @see self::__wakeup() * @var string */ private $hex; /** * Precision (used only for serialization) * * @see self::__sleep() * @see self::__wakeup() * @var int */ private $precision; /** * Sets engine type. * * Throws an exception if the type is invalid * * @param string $main * @param list $modexps optional * @return void */ public static function setEngine($main, array $modexps = ['DefaultEngine']) { self::$engines = []; $fqmain = 'phpseclib3\\Math\\BigInteger\\Engines\\' . $main; if (!class_exists($fqmain) || !method_exists($fqmain, 'isValidEngine')) { throw new \InvalidArgumentException("$main is not a valid engine"); } if (!$fqmain::isValidEngine()) { throw new BadConfigurationException("$main is not setup correctly on this system"); } /** @var class-string $fqmain */ self::$mainEngine = $fqmain; $found = false; foreach ($modexps as $modexp) { try { $fqmain::setModExpEngine($modexp); $found = true; break; } catch (\Exception $e) { } } if (!$found) { throw new BadConfigurationException("No valid modular exponentiation engine found for $main"); } self::$engines = [$main, $modexp]; } /** * Returns the engine type * * @return string[] */ public static function getEngine() { self::initialize_static_variables(); return self::$engines; } /** * Initialize static variables */ private static function initialize_static_variables() { if (!isset(self::$mainEngine)) { $engines = [ ['GMP', ['DefaultEngine']], ['PHP64', ['OpenSSL']], ['BCMath', ['OpenSSL']], ['PHP32', ['OpenSSL']], ['PHP64', ['DefaultEngine']], ['PHP32', ['DefaultEngine']] ]; foreach ($engines as $engine) { try { self::setEngine($engine[0], $engine[1]); return; } catch (\Exception $e) { } } throw new \UnexpectedValueException('No valid BigInteger found. This is only possible when JIT is enabled on Windows and neither the GMP or BCMath extensions are available so either disable JIT or install GMP / BCMath'); } } /** * Converts base-2, base-10, base-16, and binary strings (base-256) to BigIntegers. * * If the second parameter - $base - is negative, then it will be assumed that the number's are encoded using * two's compliment. The sole exception to this is -10, which is treated the same as 10 is. * * @param string|int|Engine $x Base-10 number or base-$base number if $base set. * @param int $base */ public function __construct($x = 0, $base = 10) { self::initialize_static_variables(); if ($x instanceof self::$mainEngine) { $this->value = clone $x; } elseif ($x instanceof Engine) { $this->value = new static("$x"); $this->value->setPrecision($x->getPrecision()); } else { $this->value = new self::$mainEngine($x, $base); } } /** * Converts a BigInteger to a base-10 number. * * @return string */ public function toString() { return $this->value->toString(); } /** * __toString() magic method */ public function __toString() { return (string)$this->value; } /** * __debugInfo() magic method * * Will be called, automatically, when print_r() or var_dump() are called */ public function __debugInfo() { return $this->value->__debugInfo(); } /** * Converts a BigInteger to a byte string (eg. base-256). * * @param bool $twos_compliment * @return string */ public function toBytes($twos_compliment = false) { return $this->value->toBytes($twos_compliment); } /** * Converts a BigInteger to a hex string (eg. base-16). * * @param bool $twos_compliment * @return string */ public function toHex($twos_compliment = false) { return $this->value->toHex($twos_compliment); } /** * Converts a BigInteger to a bit string (eg. base-2). * * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're * saved as two's compliment. * * @param bool $twos_compliment * @return string */ public function toBits($twos_compliment = false) { return $this->value->toBits($twos_compliment); } /** * Adds two BigIntegers. * * @param BigInteger $y * @return BigInteger */ public function add(BigInteger $y) { return new static($this->value->add($y->value)); } /** * Subtracts two BigIntegers. * * @param BigInteger $y * @return BigInteger */ public function subtract(BigInteger $y) { return new static($this->value->subtract($y->value)); } /** * Multiplies two BigIntegers * * @param BigInteger $x * @return BigInteger */ public function multiply(BigInteger $x) { return new static($this->value->multiply($x->value)); } /** * Divides two BigIntegers. * * Returns an array whose first element contains the quotient and whose second element contains the * "common residue". If the remainder would be positive, the "common residue" and the remainder are the * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder * and the divisor (basically, the "common residue" is the first positive modulo). * * Here's an example: * * divide($b); * * echo $quotient->toString(); // outputs 0 * echo "\r\n"; * echo $remainder->toString(); // outputs 10 * ?> * * * @param BigInteger $y * @return BigInteger[] */ public function divide(BigInteger $y) { list($q, $r) = $this->value->divide($y->value); return [ new static($q), new static($r) ]; } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * * @param BigInteger $n * @return BigInteger */ public function modInverse(BigInteger $n) { return new static($this->value->modInverse($n->value)); } /** * Calculates modular inverses. * * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. * * @param BigInteger $n * @return BigInteger[] */ public function extendedGCD(BigInteger $n) { extract($this->value->extendedGCD($n->value)); /** * @var BigInteger $gcd * @var BigInteger $x * @var BigInteger $y */ return [ 'gcd' => new static($gcd), 'x' => new static($x), 'y' => new static($y) ]; } /** * Calculates the greatest common divisor * * Say you have 693 and 609. The GCD is 21. * * @param BigInteger $n * @return BigInteger */ public function gcd(BigInteger $n) { return new static($this->value->gcd($n->value)); } /** * Absolute value. * * @return BigInteger */ public function abs() { return new static($this->value->abs()); } /** * Set Precision * * Some bitwise operations give different results depending on the precision being used. Examples include left * shift, not, and rotates. * * @param int $bits */ public function setPrecision($bits) { $this->value->setPrecision($bits); } /** * Get Precision * * Returns the precision if it exists, false if it doesn't * * @return int|bool */ public function getPrecision() { return $this->value->getPrecision(); } /** * Serialize * * Will be called, automatically, when serialize() is called on a BigInteger object. * * __sleep() / __wakeup() have been around since PHP 4.0 * * \Serializable was introduced in PHP 5.1 and deprecated in PHP 8.1: * https://wiki.php.net/rfc/phase_out_serializable * * __serialize() / __unserialize() were introduced in PHP 7.4: * https://wiki.php.net/rfc/custom_object_serialization * * @return array */ public function __sleep() { $this->hex = $this->toHex(true); $vars = ['hex']; if ($this->getPrecision() > 0) { $vars[] = 'precision'; } return $vars; } /** * Serialize * * Will be called, automatically, when unserialize() is called on a BigInteger object. */ public function __wakeup() { $temp = new static($this->hex, -16); $this->value = $temp->value; if ($this->precision > 0) { // recalculate $this->bitmask $this->setPrecision($this->precision); } } /** * JSON Serialize * * Will be called, automatically, when json_encode() is called on a BigInteger object. * * @return array{hex: string, precision?: int] */ #[\ReturnTypeWillChange] public function jsonSerialize() { $result = ['hex' => $this->toHex(true)]; if ($this->precision > 0) { $result['precision'] = $this->getPrecision(); } return $result; } /** * Performs modular exponentiation. * * @param BigInteger $e * @param BigInteger $n * @return BigInteger */ public function powMod(BigInteger $e, BigInteger $n) { return new static($this->value->powMod($e->value, $n->value)); } /** * Performs modular exponentiation. * * @param BigInteger $e * @param BigInteger $n * @return BigInteger */ public function modPow(BigInteger $e, BigInteger $n) { return new static($this->value->modPow($e->value, $n->value)); } /** * Compares two numbers. * * Although one might think !$x->compare($y) means $x != $y, it, in fact, means the opposite. The reason for this * is demonstrated thusly: * * $x > $y: $x->compare($y) > 0 * $x < $y: $x->compare($y) < 0 * $x == $y: $x->compare($y) == 0 * * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). * * {@internal Could return $this->subtract($x), but that's not as fast as what we do do.} * * @param BigInteger $y * @return int in case < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. * @see self::equals() */ public function compare(BigInteger $y) { return $this->value->compare($y->value); } /** * Tests the equality of two numbers. * * If you need to see if one number is greater than or less than another number, use BigInteger::compare() * * @param BigInteger $x * @return bool */ public function equals(BigInteger $x) { return $this->value->equals($x->value); } /** * Logical Not * * @return BigInteger */ public function bitwise_not() { return new static($this->value->bitwise_not()); } /** * Logical And * * @param BigInteger $x * @return BigInteger */ public function bitwise_and(BigInteger $x) { return new static($this->value->bitwise_and($x->value)); } /** * Logical Or * * @param BigInteger $x * @return BigInteger */ public function bitwise_or(BigInteger $x) { return new static($this->value->bitwise_or($x->value)); } /** * Logical Exclusive Or * * @param BigInteger $x * @return BigInteger */ public function bitwise_xor(BigInteger $x) { return new static($this->value->bitwise_xor($x->value)); } /** * Logical Right Shift * * Shifts BigInteger's by $shift bits, effectively dividing by 2**$shift. * * @param int $shift * @return BigInteger */ public function bitwise_rightShift($shift) { return new static($this->value->bitwise_rightShift($shift)); } /** * Logical Left Shift * * Shifts BigInteger's by $shift bits, effectively multiplying by 2**$shift. * * @param int $shift * @return BigInteger */ public function bitwise_leftShift($shift) { return new static($this->value->bitwise_leftShift($shift)); } /** * Logical Left Rotate * * Instead of the top x bits being dropped they're appended to the shifted bit string. * * @param int $shift * @return BigInteger */ public function bitwise_leftRotate($shift) { return new static($this->value->bitwise_leftRotate($shift)); } /** * Logical Right Rotate * * Instead of the bottom x bits being dropped they're prepended to the shifted bit string. * * @param int $shift * @return BigInteger */ public function bitwise_rightRotate($shift) { return new static($this->value->bitwise_rightRotate($shift)); } /** * Returns the smallest and largest n-bit number * * @param int $bits * @return BigInteger[] */ public static function minMaxBits($bits) { self::initialize_static_variables(); $class = self::$mainEngine; extract($class::minMaxBits($bits)); /** @var BigInteger $min * @var BigInteger $max */ return [ 'min' => new static($min), 'max' => new static($max) ]; } /** * Return the size of a BigInteger in bits * * @return int */ public function getLength() { return $this->value->getLength(); } /** * Return the size of a BigInteger in bytes * * @return int */ public function getLengthInBytes() { return $this->value->getLengthInBytes(); } /** * Generates a random number of a certain size * * Bit length is equal to $size * * @param int $size * @return BigInteger */ public static function random($size) { self::initialize_static_variables(); $class = self::$mainEngine; return new static($class::random($size)); } /** * Generates a random prime number of a certain size * * Bit length is equal to $size * * @param int $size * @return BigInteger */ public static function randomPrime($size) { self::initialize_static_variables(); $class = self::$mainEngine; return new static($class::randomPrime($size)); } /** * Generate a random prime number between a range * * If there's not a prime within the given range, false will be returned. * * @param BigInteger $min * @param BigInteger $max * @return false|BigInteger */ public static function randomRangePrime(BigInteger $min, BigInteger $max) { $class = self::$mainEngine; return new static($class::randomRangePrime($min->value, $max->value)); } /** * Generate a random number between a range * * Returns a random number between $min and $max where $min and $max * can be defined using one of the two methods: * * BigInteger::randomRange($min, $max) * BigInteger::randomRange($max, $min) * * @param BigInteger $min * @param BigInteger $max * @return BigInteger */ public static function randomRange(BigInteger $min, BigInteger $max) { $class = self::$mainEngine; return new static($class::randomRange($min->value, $max->value)); } /** * Checks a numer to see if it's prime * * Assuming the $t parameter is not set, this function has an error rate of 2**-80. The main motivation for the * $t parameter is distributability. BigInteger::randomPrime() can be distributed across multiple pageloads * on a website instead of just one. * * @param int|bool $t * @return bool */ public function isPrime($t = false) { return $this->value->isPrime($t); } /** * Calculates the nth root of a biginteger. * * Returns the nth root of a positive biginteger, where n defaults to 2 * * @param int $n optional * @return BigInteger */ public function root($n = 2) { return new static($this->value->root($n)); } /** * Performs exponentiation. * * @param BigInteger $n * @return BigInteger */ public function pow(BigInteger $n) { return new static($this->value->pow($n->value)); } /** * Return the minimum BigInteger between an arbitrary number of BigIntegers. * * @param BigInteger ...$nums * @return BigInteger */ public static function min(BigInteger ...$nums) { $class = self::$mainEngine; $nums = array_map(function ($num) { return $num->value; }, $nums); return new static($class::min(...$nums)); } /** * Return the maximum BigInteger between an arbitrary number of BigIntegers. * * @param BigInteger ...$nums * @return BigInteger */ public static function max(BigInteger ...$nums) { $class = self::$mainEngine; $nums = array_map(function ($num) { return $num->value; }, $nums); return new static($class::max(...$nums)); } /** * Tests BigInteger to see if it is between two integers, inclusive * * @param BigInteger $min * @param BigInteger $max * @return bool */ public function between(BigInteger $min, BigInteger $max) { return $this->value->between($min->value, $max->value); } /** * Clone */ public function __clone() { $this->value = clone $this->value; } /** * Is Odd? * * @return bool */ public function isOdd() { return $this->value->isOdd(); } /** * Tests if a bit is set * * @param int $x * @return bool */ public function testBit($x) { return $this->value->testBit($x); } /** * Is Negative? * * @return bool */ public function isNegative() { return $this->value->isNegative(); } /** * Negate * * Given $k, returns -$k * * @return BigInteger */ public function negate() { return new static($this->value->negate()); } /** * Scan for 1 and right shift by that amount * * ie. $s = gmp_scan1($n, 0) and $r = gmp_div_q($n, gmp_pow(gmp_init('2'), $s)); * * @param BigInteger $r * @return int */ public static function scan1divide(BigInteger $r) { $class = self::$mainEngine; return $class::scan1divide($r->value); } /** * Create Recurring Modulo Function * * Sometimes it may be desirable to do repeated modulos with the same number outside of * modular exponentiation * * @return callable */ public function createRecurringModuloFunction() { $func = $this->value->createRecurringModuloFunction(); return function (BigInteger $x) use ($func) { return new static($func($x->value)); }; } /** * Bitwise Split * * Splits BigInteger's into chunks of $split bits * * @param int $split * @return BigInteger[] */ public function bitwise_split($split) { return array_map(function ($val) { return new static($val); }, $this->value->bitwise_split($split)); } } PK!e=G9vendor/phpseclib/phpseclib/phpseclib/Math/BinaryField.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License */ namespace phpseclib3\Math; use phpseclib3\Common\Functions\Strings; use phpseclib3\Math\BinaryField\Integer; use phpseclib3\Math\Common\FiniteField; /** * Binary Finite Fields * * @author Jim Wigginton */ class BinaryField extends FiniteField { /** * Instance Counter * * @var int */ private static $instanceCounter = 0; /** * Keeps track of current instance * * @var int */ protected $instanceID; /** @var BigInteger */ private $randomMax; /** * Default constructor */ public function __construct(...$indices) { $m = array_shift($indices); if ($m > 571) { /* sect571r1 and sect571k1 are the largest binary curves that https://www.secg.org/sec2-v2.pdf defines altho theoretically there may be legit reasons to use binary finite fields with larger degrees imposing a limit on the maximum size is both reasonable and precedented. in particular, http://tools.ietf.org/html/rfc4253#section-6.1 (The Secure Shell (SSH) Transport Layer Protocol) says "implementations SHOULD check that the packet length is reasonable in order for the implementation to avoid denial of service and/or buffer overflow attacks" */ throw new \OutOfBoundsException('Degrees larger than 571 are not supported'); } $val = str_repeat('0', $m) . '1'; foreach ($indices as $index) { $val[$index] = '1'; } $modulo = static::base2ToBase256(strrev($val)); $mStart = 2 * $m - 2; $t = ceil($m / 8); $finalMask = chr((1 << ($m % 8)) - 1); if ($finalMask == "\0") { $finalMask = "\xFF"; } $bitLen = $mStart + 1; $pad = ceil($bitLen / 8); $h = $bitLen & 7; $h = $h ? 8 - $h : 0; $r = rtrim(substr($val, 0, -1), '0'); $u = [static::base2ToBase256(strrev($r))]; for ($i = 1; $i < 8; $i++) { $u[] = static::base2ToBase256(strrev(str_repeat('0', $i) . $r)); } // implements algorithm 2.40 (in section 2.3.5) in "Guide to Elliptic Curve Cryptography" // with W = 8 $reduce = function ($c) use ($u, $mStart, $m, $t, $finalMask, $pad, $h) { $c = str_pad($c, $pad, "\0", STR_PAD_LEFT); for ($i = $mStart; $i >= $m;) { $g = $h >> 3; $mask = $h & 7; $mask = $mask ? 1 << (7 - $mask) : 0x80; for (; $mask > 0; $mask >>= 1, $i--, $h++) { if (ord($c[$g]) & $mask) { $temp = $i - $m; $j = $temp >> 3; $k = $temp & 7; $t1 = $j ? substr($c, 0, -$j) : $c; $length = strlen($t1); if ($length) { $t2 = str_pad($u[$k], $length, "\0", STR_PAD_LEFT); $temp = $t1 ^ $t2; $c = $j ? substr_replace($c, $temp, 0, $length) : $temp; } } } } $c = substr($c, -$t); if (strlen($c) == $t) { $c[0] = $c[0] & $finalMask; } return ltrim($c, "\0"); }; $this->instanceID = self::$instanceCounter++; Integer::setModulo($this->instanceID, $modulo); Integer::setRecurringModuloFunction($this->instanceID, $reduce); $this->randomMax = new BigInteger($modulo, 2); } /** * Returns an instance of a dynamically generated PrimeFieldInteger class * * @param string $num * @return Integer */ public function newInteger($num) { return new Integer($this->instanceID, $num instanceof BigInteger ? $num->toBytes() : $num); } /** * Returns an integer on the finite field between one and the prime modulo * * @return Integer */ public function randomInteger() { static $one; if (!isset($one)) { $one = new BigInteger(1); } return new Integer($this->instanceID, BigInteger::randomRange($one, $this->randomMax)->toBytes()); } /** * Returns the length of the modulo in bytes * * @return int */ public function getLengthInBytes() { return strlen(Integer::getModulo($this->instanceID)); } /** * Returns the length of the modulo in bits * * @return int */ public function getLength() { return strlen(Integer::getModulo($this->instanceID)) << 3; } /** * Converts a base-2 string to a base-256 string * * @param string $x * @param int|null $size * @return string */ public static function base2ToBase256($x, $size = null) { $str = Strings::bits2bin($x); $pad = strlen($x) >> 3; if (strlen($x) & 3) { $pad++; } $str = str_pad($str, $pad, "\0", STR_PAD_LEFT); if (isset($size)) { $str = str_pad($str, $size, "\0", STR_PAD_LEFT); } return $str; } /** * Converts a base-256 string to a base-2 string * * @param string $x * @return string */ public static function base256ToBase2($x) { if (function_exists('gmp_import')) { return gmp_strval(gmp_import($x), 2); } return Strings::bin2bits($x); } } PK!IV V 8vendor/phpseclib/phpseclib/phpseclib/Math/PrimeField.phpnu[ * @copyright 2017 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://pear.php.net/package/Math_BigInteger */ namespace phpseclib3\Math; use phpseclib3\Math\Common\FiniteField; use phpseclib3\Math\PrimeField\Integer; /** * Prime Finite Fields * * @author Jim Wigginton */ class PrimeField extends FiniteField { /** * Instance Counter * * @var int */ private static $instanceCounter = 0; /** * Keeps track of current instance * * @var int */ protected $instanceID; /** * Default constructor */ public function __construct(BigInteger $modulo) { if (!$modulo->isPrime()) { throw new \UnexpectedValueException('PrimeField requires a prime number be passed to the constructor'); } $this->instanceID = self::$instanceCounter++; Integer::setModulo($this->instanceID, $modulo); Integer::setRecurringModuloFunction($this->instanceID, $modulo->createRecurringModuloFunction()); } /** * Use a custom defined modular reduction function * * @return void */ public function setReduction(\Closure $func) { $this->reduce = $func->bindTo($this, $this); } /** * Returns an instance of a dynamically generated PrimeFieldInteger class * * @return Integer */ public function newInteger(BigInteger $num) { return new Integer($this->instanceID, $num); } /** * Returns an integer on the finite field between one and the prime modulo * * @return Integer */ public function randomInteger() { static $one; if (!isset($one)) { $one = new BigInteger(1); } return new Integer($this->instanceID, BigInteger::randomRange($one, Integer::getModulo($this->instanceID))); } /** * Returns the length of the modulo in bytes * * @return int */ public function getLengthInBytes() { return Integer::getModulo($this->instanceID)->getLengthInBytes(); } /** * Returns the length of the modulo in bits * * @return int */ public function getLength() { return Integer::getModulo($this->instanceID)->getLength(); } /** * Destructor */ public function __destruct() { Integer::cleanupCache($this->instanceID); } } PK!͍+%S%S8vendor/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.phpnu[ * @copyright 2013 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Net\SFTP; use phpseclib3\Crypt\Common\PrivateKey; use phpseclib3\Net\SFTP; use phpseclib3\Net\SSH2; /** * SFTP Stream Wrapper * * @author Jim Wigginton */ class Stream { /** * SFTP instances * * Rather than re-create the connection we re-use instances if possible * * @var array */ public static $instances; /** * SFTP instance * * @var object */ private $sftp; /** * Path * * @var string */ private $path; /** * Mode * * @var string */ private $mode; /** * Position * * @var int */ private $pos; /** * Size * * @var int */ private $size; /** * Directory entries * * @var array */ private $entries; /** * EOF flag * * @var bool */ private $eof; /** * Context resource * * Technically this needs to be publicly accessible so PHP can set it directly * * @var resource */ public $context; /** * Notification callback function * * @var callable */ private $notification; /** * Registers this class as a URL wrapper. * * @param string $protocol The wrapper name to be registered. * @return bool True on success, false otherwise. */ public static function register($protocol = 'sftp') { if (in_array($protocol, stream_get_wrappers(), true)) { return false; } return stream_wrapper_register($protocol, get_called_class()); } /** * The Constructor * */ public function __construct() { if (defined('NET_SFTP_STREAM_LOGGING')) { echo "__construct()\r\n"; } } /** * Path Parser * * Extract a path from a URI and actually connect to an SSH server if appropriate * * If "notification" is set as a context parameter the message code for successful login is * NET_SSH2_MSG_USERAUTH_SUCCESS. For a failed login it's NET_SSH2_MSG_USERAUTH_FAILURE. * * @param string $path * @return string */ protected function parse_path($path) { $orig = $path; extract(parse_url($path) + ['port' => 22]); if (isset($query)) { $path .= '?' . $query; } elseif (preg_match('/(\?|\?#)$/', $orig)) { $path .= '?'; } if (isset($fragment)) { $path .= '#' . $fragment; } elseif ($orig[strlen($orig) - 1] == '#') { $path .= '#'; } if (!isset($host)) { return false; } if (isset($this->context)) { $context = stream_context_get_params($this->context); if (isset($context['notification'])) { $this->notification = $context['notification']; } } if (preg_match('/^{[a-z0-9]+}$/i', $host)) { $host = SSH2::getConnectionByResourceId($host); if ($host === false) { return false; } $this->sftp = $host; } else { if (isset($this->context)) { $context = stream_context_get_options($this->context); } if (isset($context[$scheme]['session'])) { $sftp = $context[$scheme]['session']; } if (isset($context[$scheme]['sftp'])) { $sftp = $context[$scheme]['sftp']; } if (isset($sftp) && $sftp instanceof SFTP) { $this->sftp = $sftp; return $path; } if (isset($context[$scheme]['username'])) { $user = $context[$scheme]['username']; } if (isset($context[$scheme]['password'])) { $pass = $context[$scheme]['password']; } if (isset($context[$scheme]['privkey']) && $context[$scheme]['privkey'] instanceof PrivateKey) { $pass = $context[$scheme]['privkey']; } if (!isset($user) || !isset($pass)) { return false; } // casting $pass to a string is necessary in the event that it's a \phpseclib3\Crypt\RSA object if (isset(self::$instances[$host][$port][$user][(string) $pass])) { $this->sftp = self::$instances[$host][$port][$user][(string) $pass]; } else { $this->sftp = new SFTP($host, $port); $this->sftp->disableStatCache(); if (isset($this->notification) && is_callable($this->notification)) { /* if !is_callable($this->notification) we could do this: user_error('fopen(): failed to call user notifier', E_USER_WARNING); the ftp wrapper gives errors like that when the notifier isn't callable. i've opted not to do that, however, since the ftp wrapper gives the line on which the fopen occurred as the line number - not the line that the user_error is on. */ call_user_func($this->notification, STREAM_NOTIFY_CONNECT, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); call_user_func($this->notification, STREAM_NOTIFY_AUTH_REQUIRED, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); if (!$this->sftp->login($user, $pass)) { call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_ERR, 'Login Failure', NET_SSH2_MSG_USERAUTH_FAILURE, 0, 0); return false; } call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_INFO, 'Login Success', NET_SSH2_MSG_USERAUTH_SUCCESS, 0, 0); } else { if (!$this->sftp->login($user, $pass)) { return false; } } self::$instances[$host][$port][$user][(string) $pass] = $this->sftp; } } return $path; } /** * Opens file or URL * * @param string $path * @param string $mode * @param int $options * @param string $opened_path * @return bool */ private function _stream_open($path, $mode, $options, &$opened_path) { $path = $this->parse_path($path); if ($path === false) { return false; } $this->path = $path; $this->size = $this->sftp->filesize($path); $this->mode = preg_replace('#[bt]$#', '', $mode); $this->eof = false; if ($this->size === false) { if ($this->mode[0] == 'r') { return false; } else { $this->sftp->touch($path); $this->size = 0; } } else { switch ($this->mode[0]) { case 'x': return false; case 'w': $this->sftp->truncate($path, 0); $this->size = 0; } } $this->pos = $this->mode[0] != 'a' ? 0 : $this->size; return true; } /** * Read from stream * * @param int $count * @return mixed */ private function _stream_read($count) { switch ($this->mode) { case 'w': case 'a': case 'x': case 'c': return false; } // commented out because some files - eg. /dev/urandom - will say their size is 0 when in fact it's kinda infinite //if ($this->pos >= $this->size) { // $this->eof = true; // return false; //} $result = $this->sftp->get($this->path, false, $this->pos, $count); if (isset($this->notification) && is_callable($this->notification)) { if ($result === false) { call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); return 0; } // seems that PHP calls stream_read in 8k chunks call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($result), $this->size); } if (empty($result)) { // ie. false or empty string $this->eof = true; return false; } $this->pos += strlen($result); return $result; } /** * Write to stream * * @param string $data * @return int|false */ private function _stream_write($data) { switch ($this->mode) { case 'r': return false; } $result = $this->sftp->put($this->path, $data, SFTP::SOURCE_STRING, $this->pos); if (isset($this->notification) && is_callable($this->notification)) { if (!$result) { call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); return 0; } // seems that PHP splits up strings into 8k blocks before calling stream_write call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($data), strlen($data)); } if ($result === false) { return false; } $this->pos += strlen($data); if ($this->pos > $this->size) { $this->size = $this->pos; } $this->eof = false; return strlen($data); } /** * Retrieve the current position of a stream * * @return int */ private function _stream_tell() { return $this->pos; } /** * Tests for end-of-file on a file pointer * * In my testing there are four classes functions that normally effect the pointer: * fseek, fputs / fwrite, fgets / fread and ftruncate. * * Only fgets / fread, however, results in feof() returning true. do fputs($fp, 'aaa') on a blank file and feof() * will return false. do fread($fp, 1) and feof() will then return true. do fseek($fp, 10) on ablank file and feof() * will return false. do fread($fp, 1) and feof() will then return true. * * @return bool */ private function _stream_eof() { return $this->eof; } /** * Seeks to specific location in a stream * * @param int $offset * @param int $whence * @return bool */ private function _stream_seek($offset, $whence) { switch ($whence) { case SEEK_SET: if ($offset < 0) { return false; } break; case SEEK_CUR: $offset += $this->pos; break; case SEEK_END: $offset += $this->size; } $this->pos = $offset; $this->eof = false; return true; } /** * Change stream options * * @param string $path * @param int $option * @param mixed $var * @return bool */ private function _stream_metadata($path, $option, $var) { $path = $this->parse_path($path); if ($path === false) { return false; } // stream_metadata was introduced in PHP 5.4.0 but as of 5.4.11 the constants haven't been defined // see http://www.php.net/streamwrapper.stream-metadata and https://bugs.php.net/64246 // and https://github.com/php/php-src/blob/master/main/php_streams.h#L592 switch ($option) { case 1: // PHP_STREAM_META_TOUCH $time = isset($var[0]) ? $var[0] : null; $atime = isset($var[1]) ? $var[1] : null; return $this->sftp->touch($path, $time, $atime); case 2: // PHP_STREAM_OWNER_NAME case 3: // PHP_STREAM_GROUP_NAME return false; case 4: // PHP_STREAM_META_OWNER return $this->sftp->chown($path, $var); case 5: // PHP_STREAM_META_GROUP return $this->sftp->chgrp($path, $var); case 6: // PHP_STREAM_META_ACCESS return $this->sftp->chmod($path, $var) !== false; } } /** * Retrieve the underlaying resource * * @param int $cast_as * @return resource */ private function _stream_cast($cast_as) { return $this->sftp->fsock; } /** * Advisory file locking * * @param int $operation * @return bool */ private function _stream_lock($operation) { return false; } /** * Renames a file or directory * * Attempts to rename oldname to newname, moving it between directories if necessary. * If newname exists, it will be overwritten. This is a departure from what \phpseclib3\Net\SFTP * does. * * @param string $path_from * @param string $path_to * @return bool */ private function _rename($path_from, $path_to) { $path1 = parse_url($path_from); $path2 = parse_url($path_to); unset($path1['path'], $path2['path']); if ($path1 != $path2) { return false; } $path_from = $this->parse_path($path_from); $path_to = parse_url($path_to); if ($path_from === false) { return false; } $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2 // "It is an error if there already exists a file with the name specified by newpath." // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5 if (!$this->sftp->rename($path_from, $path_to)) { if ($this->sftp->stat($path_to)) { return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to); } return false; } return true; } /** * Open directory handle * * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and * removed in 5.4 I'm just going to ignore it. * * Also, nlist() is the best that this function is realistically going to be able to do. When an SFTP client * sends a SSH_FXP_READDIR packet you don't generally get info on just one file but on multiple files. Quoting * the SFTP specs: * * The SSH_FXP_NAME response has the following format: * * uint32 id * uint32 count * repeats count times: * string filename * string longname * ATTRS attrs * * @param string $path * @param int $options * @return bool */ private function _dir_opendir($path, $options) { $path = $this->parse_path($path); if ($path === false) { return false; } $this->pos = 0; $this->entries = $this->sftp->nlist($path); return $this->entries !== false; } /** * Read entry from directory handle * * @return mixed */ private function _dir_readdir() { if (isset($this->entries[$this->pos])) { return $this->entries[$this->pos++]; } return false; } /** * Rewind directory handle * * @return bool */ private function _dir_rewinddir() { $this->pos = 0; return true; } /** * Close directory handle * * @return bool */ private function _dir_closedir() { return true; } /** * Create a directory * * Only valid $options is STREAM_MKDIR_RECURSIVE * * @param string $path * @param int $mode * @param int $options * @return bool */ private function _mkdir($path, $mode, $options) { $path = $this->parse_path($path); if ($path === false) { return false; } return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE); } /** * Removes a directory * * Only valid $options is STREAM_MKDIR_RECURSIVE per , however, * does not have a $recursive parameter as mkdir() does so I don't know how * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as * $options. What does 8 correspond to? * * @param string $path * @param int $options * @return bool */ private function _rmdir($path, $options) { $path = $this->parse_path($path); if ($path === false) { return false; } return $this->sftp->rmdir($path); } /** * Flushes the output * * See . Always returns true because \phpseclib3\Net\SFTP doesn't cache stuff before writing * * @return bool */ private function _stream_flush() { return true; } /** * Retrieve information about a file resource * * @return mixed */ private function _stream_stat() { $results = $this->sftp->stat($this->path); if ($results === false) { return false; } return $results; } /** * Delete a file * * @param string $path * @return bool */ private function _unlink($path) { $path = $this->parse_path($path); if ($path === false) { return false; } return $this->sftp->delete($path, false); } /** * Retrieve information about a file * * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of \phpseclib3\Net\SFTP\Stream is quiet by default * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll * cross that bridge when and if it's reached * * @param string $path * @param int $flags * @return mixed */ private function _url_stat($path, $flags) { $path = $this->parse_path($path); if ($path === false) { return false; } $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path); if ($results === false) { return false; } return $results; } /** * Truncate stream * * @param int $new_size * @return bool */ private function _stream_truncate($new_size) { if (!$this->sftp->truncate($this->path, $new_size)) { return false; } $this->eof = false; $this->size = $new_size; return true; } /** * Change stream options * * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't. * The other two aren't supported because of limitations in \phpseclib3\Net\SFTP. * * @param int $option * @param int $arg1 * @param int $arg2 * @return bool */ private function _stream_set_option($option, $arg1, $arg2) { return false; } /** * Close an resource * */ private function _stream_close() { } /** * __call Magic Method * * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you. * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function * lets you figure that out. * * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method. * * @param string $name * @param array $arguments * @return mixed */ public function __call($name, array $arguments) { if (defined('NET_SFTP_STREAM_LOGGING')) { echo $name . '('; $last = count($arguments) - 1; foreach ($arguments as $i => $argument) { var_export($argument); if ($i != $last) { echo ','; } } echo ")\r\n"; } $name = '_' . $name; if (!method_exists($this, $name)) { return false; } return $this->$name(...$arguments); } } PK!''1vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.phpnu[ * login('username', 'password')) { * exit('Login Failed'); * } * * echo $sftp->pwd() . "\r\n"; * $sftp->put('filename.ext', 'hello, world!'); * print_r($sftp->nlist()); * ?> * * * @author Jim Wigginton * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Net; use phpseclib3\Common\Functions\Strings; use phpseclib3\Exception\FileNotFoundException; /** * Pure-PHP implementations of SFTP. * * @author Jim Wigginton */ class SFTP extends SSH2 { /** * SFTP channel constant * * \phpseclib3\Net\SSH2::exec() uses 0 and \phpseclib3\Net\SSH2::read() / \phpseclib3\Net\SSH2::write() use 1. * * @see \phpseclib3\Net\SSH2::send_channel_packet() * @see \phpseclib3\Net\SSH2::get_channel_packet() */ const CHANNEL = 0x100; /** * Reads data from a local file. * * @see \phpseclib3\Net\SFTP::put() */ const SOURCE_LOCAL_FILE = 1; /** * Reads data from a string. * * @see \phpseclib3\Net\SFTP::put() */ // this value isn't really used anymore but i'm keeping it reserved for historical reasons const SOURCE_STRING = 2; /** * Reads data from callback: * function callback($length) returns string to proceed, null for EOF * * @see \phpseclib3\Net\SFTP::put() */ const SOURCE_CALLBACK = 16; /** * Resumes an upload * * @see \phpseclib3\Net\SFTP::put() */ const RESUME = 4; /** * Append a local file to an already existing remote file * * @see \phpseclib3\Net\SFTP::put() */ const RESUME_START = 8; /** * Packet Types * * @see self::__construct() * @var array * @access private */ private static $packet_types = []; /** * Status Codes * * @see self::__construct() * @var array * @access private */ private static $status_codes = []; /** @var array */ private static $attributes; /** @var array */ private static $open_flags; /** @var array */ private static $open_flags5; /** @var array */ private static $file_types; /** * The Request ID * * The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support * concurrent actions, so it's somewhat academic, here. * * @var boolean * @see self::_send_sftp_packet() */ private $use_request_id = false; /** * The Packet Type * * The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support * concurrent actions, so it's somewhat academic, here. * * @var int * @see self::_get_sftp_packet() */ private $packet_type = -1; /** * Packet Buffer * * @var string * @see self::_get_sftp_packet() */ private $packet_buffer = ''; /** * Extensions supported by the server * * @var array * @see self::_initChannel() */ private $extensions = []; /** * Server SFTP version * * @var int * @see self::_initChannel() */ private $version; /** * Default Server SFTP version * * @var int * @see self::_initChannel() */ private $defaultVersion; /** * Preferred SFTP version * * @var int * @see self::_initChannel() */ private $preferredVersion = 3; /** * Current working directory * * @var string|bool * @see self::realpath() * @see self::chdir() */ private $pwd = false; /** * Packet Type Log * * @see self::getLog() * @var array */ private $packet_type_log = []; /** * Packet Log * * @see self::getLog() * @var array */ private $packet_log = []; /** * Real-time log file pointer * * @see self::_append_log() * @var resource|closed-resource */ private $realtime_log_file; /** * Real-time log file size * * @see self::_append_log() * @var int */ private $realtime_log_size; /** * Real-time log file wrap boolean * * @see self::_append_log() * @var bool */ private $realtime_log_wrap; /** * Current log size * * Should never exceed self::LOG_MAX_SIZE * * @var int */ private $log_size; /** * Error information * * @see self::getSFTPErrors() * @see self::getLastSFTPError() * @var array */ private $sftp_errors = []; /** * Stat Cache * * Rather than always having to open a directory and close it immediately there after to see if a file is a directory * we'll cache the results. * * @see self::_update_stat_cache() * @see self::_remove_from_stat_cache() * @see self::_query_stat_cache() * @var array */ private $stat_cache = []; /** * Max SFTP Packet Size * * @see self::__construct() * @see self::get() * @var int */ private $max_sftp_packet; /** * Stat Cache Flag * * @see self::disableStatCache() * @see self::enableStatCache() * @var bool */ private $use_stat_cache = true; /** * Sort Options * * @see self::_comparator() * @see self::setListOrder() * @var array */ protected $sortOptions = []; /** * Canonicalization Flag * * Determines whether or not paths should be canonicalized before being * passed on to the remote server. * * @see self::enablePathCanonicalization() * @see self::disablePathCanonicalization() * @see self::realpath() * @var bool */ private $canonicalize_paths = true; /** * Request Buffers * * @see self::_get_sftp_packet() * @var array */ private $requestBuffer = []; /** * Preserve timestamps on file downloads / uploads * * @see self::get() * @see self::put() * @var bool */ private $preserveTime = false; /** * Arbitrary Length Packets Flag * * Determines whether or not packets of any length should be allowed, * in cases where the server chooses the packet length (such as * directory listings). By default, packets are only allowed to be * 256 * 1024 bytes (SFTP_MAX_MSG_LENGTH from OpenSSH's sftp-common.h) * * @see self::enableArbitraryLengthPackets() * @see self::_get_sftp_packet() * @var bool */ private $allow_arbitrary_length_packets = false; /** * Was the last packet due to the channels being closed or not? * * @see self::get() * @see self::get_sftp_packet() * @var bool */ private $channel_close = false; /** * Has the SFTP channel been partially negotiated? * * @var bool */ private $partial_init = false; /** * Default Constructor. * * Connects to an SFTP server * * $host can either be a string, representing the host, or a stream resource. * * @param mixed $host * @param int $port * @param int $timeout */ public function __construct($host, $port = 22, $timeout = 10) { parent::__construct($host, $port, $timeout); $this->max_sftp_packet = 1 << 15; if (empty(self::$packet_types)) { self::$packet_types = [ 1 => 'NET_SFTP_INIT', 2 => 'NET_SFTP_VERSION', 3 => 'NET_SFTP_OPEN', 4 => 'NET_SFTP_CLOSE', 5 => 'NET_SFTP_READ', 6 => 'NET_SFTP_WRITE', 7 => 'NET_SFTP_LSTAT', 9 => 'NET_SFTP_SETSTAT', 10 => 'NET_SFTP_FSETSTAT', 11 => 'NET_SFTP_OPENDIR', 12 => 'NET_SFTP_READDIR', 13 => 'NET_SFTP_REMOVE', 14 => 'NET_SFTP_MKDIR', 15 => 'NET_SFTP_RMDIR', 16 => 'NET_SFTP_REALPATH', 17 => 'NET_SFTP_STAT', 18 => 'NET_SFTP_RENAME', 19 => 'NET_SFTP_READLINK', 20 => 'NET_SFTP_SYMLINK', 21 => 'NET_SFTP_LINK', 101 => 'NET_SFTP_STATUS', 102 => 'NET_SFTP_HANDLE', 103 => 'NET_SFTP_DATA', 104 => 'NET_SFTP_NAME', 105 => 'NET_SFTP_ATTRS', 200 => 'NET_SFTP_EXTENDED', 201 => 'NET_SFTP_EXTENDED_REPLY' ]; self::$status_codes = [ 0 => 'NET_SFTP_STATUS_OK', 1 => 'NET_SFTP_STATUS_EOF', 2 => 'NET_SFTP_STATUS_NO_SUCH_FILE', 3 => 'NET_SFTP_STATUS_PERMISSION_DENIED', 4 => 'NET_SFTP_STATUS_FAILURE', 5 => 'NET_SFTP_STATUS_BAD_MESSAGE', 6 => 'NET_SFTP_STATUS_NO_CONNECTION', 7 => 'NET_SFTP_STATUS_CONNECTION_LOST', 8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED', 9 => 'NET_SFTP_STATUS_INVALID_HANDLE', 10 => 'NET_SFTP_STATUS_NO_SUCH_PATH', 11 => 'NET_SFTP_STATUS_FILE_ALREADY_EXISTS', 12 => 'NET_SFTP_STATUS_WRITE_PROTECT', 13 => 'NET_SFTP_STATUS_NO_MEDIA', 14 => 'NET_SFTP_STATUS_NO_SPACE_ON_FILESYSTEM', 15 => 'NET_SFTP_STATUS_QUOTA_EXCEEDED', 16 => 'NET_SFTP_STATUS_UNKNOWN_PRINCIPAL', 17 => 'NET_SFTP_STATUS_LOCK_CONFLICT', 18 => 'NET_SFTP_STATUS_DIR_NOT_EMPTY', 19 => 'NET_SFTP_STATUS_NOT_A_DIRECTORY', 20 => 'NET_SFTP_STATUS_INVALID_FILENAME', 21 => 'NET_SFTP_STATUS_LINK_LOOP', 22 => 'NET_SFTP_STATUS_CANNOT_DELETE', 23 => 'NET_SFTP_STATUS_INVALID_PARAMETER', 24 => 'NET_SFTP_STATUS_FILE_IS_A_DIRECTORY', 25 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT', 26 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED', 27 => 'NET_SFTP_STATUS_DELETE_PENDING', 28 => 'NET_SFTP_STATUS_FILE_CORRUPT', 29 => 'NET_SFTP_STATUS_OWNER_INVALID', 30 => 'NET_SFTP_STATUS_GROUP_INVALID', 31 => 'NET_SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK' ]; // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1 // the order, in this case, matters quite a lot - see \phpseclib3\Net\SFTP::_parseAttributes() to understand why self::$attributes = [ 0x00000001 => 'NET_SFTP_ATTR_SIZE', 0x00000002 => 'NET_SFTP_ATTR_UIDGID', // defined in SFTPv3, removed in SFTPv4+ 0x00000080 => 'NET_SFTP_ATTR_OWNERGROUP', // defined in SFTPv4+ 0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS', 0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME', 0x00000010 => 'NET_SFTP_ATTR_CREATETIME', // SFTPv4+ 0x00000020 => 'NET_SFTP_ATTR_MODIFYTIME', 0x00000040 => 'NET_SFTP_ATTR_ACL', 0x00000100 => 'NET_SFTP_ATTR_SUBSECOND_TIMES', 0x00000200 => 'NET_SFTP_ATTR_BITS', // SFTPv5+ 0x00000400 => 'NET_SFTP_ATTR_ALLOCATION_SIZE', // SFTPv6+ 0x00000800 => 'NET_SFTP_ATTR_TEXT_HINT', 0x00001000 => 'NET_SFTP_ATTR_MIME_TYPE', 0x00002000 => 'NET_SFTP_ATTR_LINK_COUNT', 0x00004000 => 'NET_SFTP_ATTR_UNTRANSLATED_NAME', 0x00008000 => 'NET_SFTP_ATTR_CTIME', // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers // yields inconsistent behavior depending on how php is compiled. so we left shift -1 (which, in // two's compliment, consists of all 1 bits) by 31. on 64-bit systems this'll yield 0xFFFFFFFF80000000. // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored. (PHP_INT_SIZE == 4 ? (-1 << 31) : 0x80000000) => 'NET_SFTP_ATTR_EXTENDED' ]; // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3 // the flag definitions change somewhat in SFTPv5+. if SFTPv5+ support is added to this library, maybe name // the array for that $this->open5_flags and similarly alter the constant names. self::$open_flags = [ 0x00000001 => 'NET_SFTP_OPEN_READ', 0x00000002 => 'NET_SFTP_OPEN_WRITE', 0x00000004 => 'NET_SFTP_OPEN_APPEND', 0x00000008 => 'NET_SFTP_OPEN_CREATE', 0x00000010 => 'NET_SFTP_OPEN_TRUNCATE', 0x00000020 => 'NET_SFTP_OPEN_EXCL', 0x00000040 => 'NET_SFTP_OPEN_TEXT' // defined in SFTPv4 ]; // SFTPv5+ changed the flags up: // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3 self::$open_flags5 = [ // when SSH_FXF_ACCESS_DISPOSITION is a 3 bit field that controls how the file is opened 0x00000000 => 'NET_SFTP_OPEN_CREATE_NEW', 0x00000001 => 'NET_SFTP_OPEN_CREATE_TRUNCATE', 0x00000002 => 'NET_SFTP_OPEN_OPEN_EXISTING', 0x00000003 => 'NET_SFTP_OPEN_OPEN_OR_CREATE', 0x00000004 => 'NET_SFTP_OPEN_TRUNCATE_EXISTING', // the rest of the flags are not supported 0x00000008 => 'NET_SFTP_OPEN_APPEND_DATA', // "the offset field of SS_FXP_WRITE requests is ignored" 0x00000010 => 'NET_SFTP_OPEN_APPEND_DATA_ATOMIC', 0x00000020 => 'NET_SFTP_OPEN_TEXT_MODE', 0x00000040 => 'NET_SFTP_OPEN_BLOCK_READ', 0x00000080 => 'NET_SFTP_OPEN_BLOCK_WRITE', 0x00000100 => 'NET_SFTP_OPEN_BLOCK_DELETE', 0x00000200 => 'NET_SFTP_OPEN_BLOCK_ADVISORY', 0x00000400 => 'NET_SFTP_OPEN_NOFOLLOW', 0x00000800 => 'NET_SFTP_OPEN_DELETE_ON_CLOSE', 0x00001000 => 'NET_SFTP_OPEN_ACCESS_AUDIT_ALARM_INFO', 0x00002000 => 'NET_SFTP_OPEN_ACCESS_BACKUP', 0x00004000 => 'NET_SFTP_OPEN_BACKUP_STREAM', 0x00008000 => 'NET_SFTP_OPEN_OVERRIDE_OWNER', ]; // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 // see \phpseclib3\Net\SFTP::_parseLongname() for an explanation self::$file_types = [ 1 => 'NET_SFTP_TYPE_REGULAR', 2 => 'NET_SFTP_TYPE_DIRECTORY', 3 => 'NET_SFTP_TYPE_SYMLINK', 4 => 'NET_SFTP_TYPE_SPECIAL', 5 => 'NET_SFTP_TYPE_UNKNOWN', // the following types were first defined for use in SFTPv5+ // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2 6 => 'NET_SFTP_TYPE_SOCKET', 7 => 'NET_SFTP_TYPE_CHAR_DEVICE', 8 => 'NET_SFTP_TYPE_BLOCK_DEVICE', 9 => 'NET_SFTP_TYPE_FIFO' ]; self::define_array( self::$packet_types, self::$status_codes, self::$attributes, self::$open_flags, self::$open_flags5, self::$file_types ); } if (!defined('NET_SFTP_QUEUE_SIZE')) { define('NET_SFTP_QUEUE_SIZE', 32); } if (!defined('NET_SFTP_UPLOAD_QUEUE_SIZE')) { define('NET_SFTP_UPLOAD_QUEUE_SIZE', 1024); } } /** * Check a few things before SFTP functions are called * * @return bool */ private function precheck() { if (!($this->bitmap & SSH2::MASK_LOGIN)) { return false; } if ($this->pwd === false) { return $this->init_sftp_connection(); } return true; } /** * Partially initialize an SFTP connection * * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool */ private function partial_init_sftp_connection() { $response = $this->open_channel(self::CHANNEL, true); if ($response === true && $this->isTimeout()) { return false; } $packet = Strings::packSSH2( 'CNsbs', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL], 'subsystem', true, 'sftp' ); $this->send_binary_packet($packet); $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->get_channel_packet(self::CHANNEL, true); if ($response === false) { // from PuTTY's psftp.exe $command = "test -x /usr/lib/sftp-server && exec /usr/lib/sftp-server\n" . "test -x /usr/local/lib/sftp-server && exec /usr/local/lib/sftp-server\n" . "exec sftp-server"; // we don't do $this->exec($command, false) because exec() operates on a different channel and plus the SSH_MSG_CHANNEL_OPEN that exec() does // is redundant $packet = Strings::packSSH2( 'CNsCs', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL], 'exec', 1, $command ); $this->send_binary_packet($packet); $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->get_channel_packet(self::CHANNEL, true); if ($response === false) { return false; } } elseif ($response === true && $this->isTimeout()) { return false; } $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_DATA; $this->send_sftp_packet(NET_SFTP_INIT, "\0\0\0\3"); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_VERSION) { throw new \UnexpectedValueException('Expected NET_SFTP_VERSION. ' . 'Got packet type: ' . $this->packet_type); } $this->use_request_id = true; list($this->defaultVersion) = Strings::unpackSSH2('N', $response); while (!empty($response)) { list($key, $value) = Strings::unpackSSH2('ss', $response); $this->extensions[$key] = $value; } $this->partial_init = true; return true; } /** * (Re)initializes the SFTP channel * * @return bool */ private function init_sftp_connection() { if (!$this->partial_init && !$this->partial_init_sftp_connection()) { return false; } /* A Note on SFTPv4/5/6 support: states the following: "If the client wishes to interoperate with servers that support noncontiguous version numbers it SHOULD send '3'" Given that the server only sends its version number after the client has already done so, the above seems to be suggesting that v3 should be the default version. This makes sense given that v3 is the most popular. states the following; "If the server did not send the "versions" extension, or the version-from-list was not included, the server MAY send a status response describing the failure, but MUST then close the channel without processing any further requests." So what do you do if you have a client whose initial SSH_FXP_INIT packet says it implements v3 and a server whose initial SSH_FXP_VERSION reply says it implements v4 and only v4? If it only implements v4, the "versions" extension is likely not going to have been sent so version re-negotiation as discussed in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what \phpseclib3\Net\SFTP would do is close the channel and reopen it with a new and updated SSH_FXP_INIT packet. */ $this->version = $this->defaultVersion; if (isset($this->extensions['versions']) && (!$this->preferredVersion || $this->preferredVersion != $this->version)) { $versions = explode(',', $this->extensions['versions']); $supported = [6, 5, 4]; if ($this->preferredVersion) { $supported = array_diff($supported, [$this->preferredVersion]); array_unshift($supported, $this->preferredVersion); } foreach ($supported as $ver) { if (in_array($ver, $versions)) { if ($ver === $this->version) { break; } $this->version = (int) $ver; $packet = Strings::packSSH2('ss', 'version-select', "$ver"); $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); throw new \UnexpectedValueException('Expected NET_SFTP_STATUS_OK. ' . ' Got ' . $status); } break; } } } /* SFTPv4+ defines a 'newline' extension. SFTPv3 seems to have unofficial support for it via 'newline@vandyke.com', however, I'm not sure what 'newline@vandyke.com' is supposed to do (the fact that it's unofficial means that it's not in the official SFTPv3 specs) and 'newline@vandyke.com' / 'newline' are likely not drop-in substitutes for one another due to the fact that 'newline' comes with a SSH_FXF_TEXT bitmask whereas it seems unlikely that 'newline@vandyke.com' would. */ /* if (isset($this->extensions['newline@vandyke.com'])) { $this->extensions['newline'] = $this->extensions['newline@vandyke.com']; unset($this->extensions['newline@vandyke.com']); } */ if ($this->version < 2 || $this->version > 6) { return false; } $this->pwd = true; try { $this->pwd = $this->realpath('.'); } catch (\UnexpectedValueException $e) { if (!$this->canonicalize_paths) { throw $e; } $this->canonicalize_paths = false; $this->reset_sftp(); return $this->init_sftp_connection(); } $this->update_stat_cache($this->pwd, []); return true; } /** * Disable the stat cache * */ public function disableStatCache() { $this->use_stat_cache = false; } /** * Enable the stat cache * */ public function enableStatCache() { $this->use_stat_cache = true; } /** * Clear the stat cache * */ public function clearStatCache() { $this->stat_cache = []; } /** * Enable path canonicalization * */ public function enablePathCanonicalization() { $this->canonicalize_paths = true; } /** * Disable path canonicalization * * If this is enabled then $sftp->pwd() will not return the canonicalized absolute path * */ public function disablePathCanonicalization() { $this->canonicalize_paths = false; } /** * Enable arbitrary length packets * */ public function enableArbitraryLengthPackets() { $this->allow_arbitrary_length_packets = true; } /** * Disable arbitrary length packets * */ public function disableArbitraryLengthPackets() { $this->allow_arbitrary_length_packets = false; } /** * Returns the current directory name * * @return string|bool */ public function pwd() { if (!$this->precheck()) { return false; } return $this->pwd; } /** * Logs errors * * @param string $response * @param int $status */ private function logError($response, $status = -1) { if ($status == -1) { list($status) = Strings::unpackSSH2('N', $response); } $error = self::$status_codes[$status]; if ($this->version > 2) { list($message) = Strings::unpackSSH2('s', $response); $this->sftp_errors[] = "$error: $message"; } else { $this->sftp_errors[] = $error; } } /** * Canonicalize the Server-Side Path Name * * SFTP doesn't provide a mechanism by which the current working directory can be changed, so we'll emulate it. Returns * the absolute (canonicalized) path. * * If canonicalize_paths has been disabled using disablePathCanonicalization(), $path is returned as-is. * * @see self::chdir() * @see self::disablePathCanonicalization() * @param string $path * @throws \UnexpectedValueException on receipt of unexpected packets * @return mixed */ public function realpath($path) { if ($this->precheck() === false) { return false; } if (!$this->canonicalize_paths) { if ($this->pwd === true) { return '.'; } if (!strlen($path) || $path[0] != '/') { $path = $this->pwd . '/' . $path; } $parts = explode('/', $path); $afterPWD = $beforePWD = []; foreach ($parts as $part) { switch ($part) { //case '': // some SFTP servers /require/ double /'s. see https://github.com/phpseclib/phpseclib/pull/1137 case '.': break; case '..': if (!empty($afterPWD)) { array_pop($afterPWD); } else { $beforePWD[] = '..'; } break; default: $afterPWD[] = $part; } } $beforePWD = count($beforePWD) ? implode('/', $beforePWD) : '.'; return $beforePWD . '/' . implode('/', $afterPWD); } if ($this->pwd === true) { // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9 $this->send_sftp_packet(NET_SFTP_REALPATH, Strings::packSSH2('s', $path)); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_NAME: // although SSH_FXP_NAME is implemented differently in SFTPv3 than it is in SFTPv4+, the following // should work on all SFTP versions since the only part of the SSH_FXP_NAME packet the following looks // at is the first part and that part is defined the same in SFTP versions 3 through 6. list(, $filename) = Strings::unpackSSH2('Ns', $response); return $filename; case NET_SFTP_STATUS: $this->logError($response); return false; default: throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } } if (!strlen($path) || $path[0] != '/') { $path = $this->pwd . '/' . $path; } $path = explode('/', $path); $new = []; foreach ($path as $dir) { if (!strlen($dir)) { continue; } switch ($dir) { case '..': array_pop($new); // fall-through case '.': break; default: $new[] = $dir; } } return '/' . implode('/', $new); } /** * Changes the current directory * * @param string $dir * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool */ public function chdir($dir) { if (!$this->precheck()) { return false; } // assume current dir if $dir is empty if ($dir === '') { $dir = './'; // suffix a slash if needed } elseif ($dir[strlen($dir) - 1] != '/') { $dir .= '/'; } $dir = $this->realpath($dir); // confirm that $dir is, in fact, a valid directory if ($this->use_stat_cache && is_array($this->query_stat_cache($dir))) { $this->pwd = $dir; return true; } // we could do a stat on the alleged $dir to see if it's a directory but that doesn't tell us // the currently logged in user has the appropriate permissions or not. maybe you could see if // the file's uid / gid match the currently logged in user's uid / gid but how there's no easy // way to get those with SFTP $this->send_sftp_packet(NET_SFTP_OPENDIR, Strings::packSSH2('s', $dir)); // see \phpseclib3\Net\SFTP::nlist() for a more thorough explanation of the following $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: $handle = substr($response, 4); break; case NET_SFTP_STATUS: $this->logError($response); return false; default: throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS' . 'Got packet type: ' . $this->packet_type); } if (!$this->close_handle($handle)) { return false; } $this->update_stat_cache($dir, []); $this->pwd = $dir; return true; } /** * Returns a list of files in the given directory * * @param string $dir * @param bool $recursive * @return array|false */ public function nlist($dir = '.', $recursive = false) { return $this->nlist_helper($dir, $recursive, ''); } /** * Helper method for nlist * * @param string $dir * @param bool $recursive * @param string $relativeDir * @return array|false */ private function nlist_helper($dir, $recursive, $relativeDir) { $files = $this->readlist($dir, false); // If we get an int back, then that is an "unexpected" status. // We do not have a file list, so return false. if (is_int($files)) { return false; } if (!$recursive || $files === false) { return $files; } $result = []; foreach ($files as $value) { if ($value == '.' || $value == '..') { $result[] = $relativeDir . $value; continue; } if (is_array($this->query_stat_cache($this->realpath($dir . '/' . $value)))) { $temp = $this->nlist_helper($dir . '/' . $value, true, $relativeDir . $value . '/'); $temp = is_array($temp) ? $temp : []; $result = array_merge($result, $temp); } else { $result[] = $relativeDir . $value; } } return $result; } /** * Returns a detailed list of files in the given directory * * @param string $dir * @param bool $recursive * @return array|false */ public function rawlist($dir = '.', $recursive = false) { $files = $this->readlist($dir, true); // If we get an int back, then that is an "unexpected" status. // We do not have a file list, so return false. if (is_int($files)) { return false; } if (!$recursive || $files === false) { return $files; } static $depth = 0; foreach ($files as $key => $value) { if ($depth != 0 && $key == '..') { unset($files[$key]); continue; } $is_directory = false; if ($key != '.' && $key != '..') { if ($this->use_stat_cache) { $is_directory = is_array($this->query_stat_cache($this->realpath($dir . '/' . $key))); } else { $stat = $this->lstat($dir . '/' . $key); $is_directory = $stat && $stat['type'] === NET_SFTP_TYPE_DIRECTORY; } } if ($is_directory) { $depth++; $files[$key] = $this->rawlist($dir . '/' . $key, true); $depth--; } else { $files[$key] = (object) $value; } } return $files; } /** * Reads a list, be it detailed or not, of files in the given directory * * @param string $dir * @param bool $raw * @return array|false * @throws \UnexpectedValueException on receipt of unexpected packets */ private function readlist($dir, $raw = true) { if (!$this->precheck()) { return false; } $dir = $this->realpath($dir . '/'); if ($dir === false) { return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.2 $this->send_sftp_packet(NET_SFTP_OPENDIR, Strings::packSSH2('s', $dir)); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.2 // since 'handle' is the last field in the SSH_FXP_HANDLE packet, we'll just remove the first four bytes that // represent the length of the string and leave it at that $handle = substr($response, 4); break; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED list($status) = Strings::unpackSSH2('N', $response); $this->logError($response, $status); return $status; default: throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } $this->update_stat_cache($dir, []); $contents = []; while (true) { // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.2 // why multiple SSH_FXP_READDIR packets would be sent when the response to a single one can span arbitrarily many // SSH_MSG_CHANNEL_DATA messages is not known to me. $this->send_sftp_packet(NET_SFTP_READDIR, Strings::packSSH2('s', $handle)); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_NAME: list($count) = Strings::unpackSSH2('N', $response); for ($i = 0; $i < $count; $i++) { list($shortname) = Strings::unpackSSH2('s', $response); // SFTPv4 "removed the long filename from the names structure-- it can now be // built from information available in the attrs structure." if ($this->version < 4) { list($longname) = Strings::unpackSSH2('s', $response); } $attributes = $this->parseAttributes($response); if (!isset($attributes['type']) && $this->version < 4) { $fileType = $this->parseLongname($longname); if ($fileType) { $attributes['type'] = $fileType; } } $contents[$shortname] = $attributes + ['filename' => $shortname]; if (isset($attributes['type']) && $attributes['type'] == NET_SFTP_TYPE_DIRECTORY && ($shortname != '.' && $shortname != '..')) { $this->update_stat_cache($dir . '/' . $shortname, []); } else { if ($shortname == '..') { $temp = $this->realpath($dir . '/..') . '/.'; } else { $temp = $dir . '/' . $shortname; } $this->update_stat_cache($temp, (object) ['lstat' => $attributes]); } // SFTPv6 has an optional boolean end-of-list field, but we'll ignore that, since the // final SSH_FXP_STATUS packet should tell us that, already. } break; case NET_SFTP_STATUS: list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_EOF) { $this->logError($response, $status); return $status; } break 2; default: throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } } if (!$this->close_handle($handle)) { return false; } if (count($this->sortOptions)) { uasort($contents, [&$this, 'comparator']); } return $raw ? $contents : array_map('strval', array_keys($contents)); } /** * Compares two rawlist entries using parameters set by setListOrder() * * Intended for use with uasort() * * @param array $a * @param array $b * @return int */ private function comparator(array $a, array $b) { switch (true) { case $a['filename'] === '.' || $b['filename'] === '.': if ($a['filename'] === $b['filename']) { return 0; } return $a['filename'] === '.' ? -1 : 1; case $a['filename'] === '..' || $b['filename'] === '..': if ($a['filename'] === $b['filename']) { return 0; } return $a['filename'] === '..' ? -1 : 1; case isset($a['type']) && $a['type'] === NET_SFTP_TYPE_DIRECTORY: if (!isset($b['type'])) { return 1; } if ($b['type'] !== $a['type']) { return -1; } break; case isset($b['type']) && $b['type'] === NET_SFTP_TYPE_DIRECTORY: return 1; } foreach ($this->sortOptions as $sort => $order) { if (!isset($a[$sort]) || !isset($b[$sort])) { if (isset($a[$sort])) { return -1; } if (isset($b[$sort])) { return 1; } return 0; } switch ($sort) { case 'filename': $result = strcasecmp($a['filename'], $b['filename']); if ($result) { return $order === SORT_DESC ? -$result : $result; } break; case 'mode': $a[$sort] &= 07777; $b[$sort] &= 07777; // fall-through default: if ($a[$sort] === $b[$sort]) { break; } return $order === SORT_ASC ? $a[$sort] - $b[$sort] : $b[$sort] - $a[$sort]; } } } /** * Defines how nlist() and rawlist() will be sorted - if at all. * * If sorting is enabled directories and files will be sorted independently with * directories appearing before files in the resultant array that is returned. * * Any parameter returned by stat is a valid sort parameter for this function. * Filename comparisons are case insensitive. * * Examples: * * $sftp->setListOrder('filename', SORT_ASC); * $sftp->setListOrder('size', SORT_DESC, 'filename', SORT_ASC); * $sftp->setListOrder(true); * Separates directories from files but doesn't do any sorting beyond that * $sftp->setListOrder(); * Don't do any sort of sorting * * @param string ...$args */ public function setListOrder(...$args) { $this->sortOptions = []; if (empty($args)) { return; } $len = count($args) & 0x7FFFFFFE; for ($i = 0; $i < $len; $i += 2) { $this->sortOptions[$args[$i]] = $args[$i + 1]; } if (!count($this->sortOptions)) { $this->sortOptions = ['bogus' => true]; } } /** * Save files / directories to cache * * @param string $path * @param mixed $value */ private function update_stat_cache($path, $value) { if ($this->use_stat_cache === false) { return; } // preg_replace('#^/|/(?=/)|/$#', '', $dir) == str_replace('//', '/', trim($path, '/')) $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); $temp = &$this->stat_cache; $max = count($dirs) - 1; foreach ($dirs as $i => $dir) { // if $temp is an object that means one of two things. // 1. a file was deleted and changed to a directory behind phpseclib's back // 2. it's a symlink. when lstat is done it's unclear what it's a symlink to if (is_object($temp)) { $temp = []; } if (!isset($temp[$dir])) { $temp[$dir] = []; } if ($i === $max) { if (is_object($temp[$dir]) && is_object($value)) { if (!isset($value->stat) && isset($temp[$dir]->stat)) { $value->stat = $temp[$dir]->stat; } if (!isset($value->lstat) && isset($temp[$dir]->lstat)) { $value->lstat = $temp[$dir]->lstat; } } $temp[$dir] = $value; break; } $temp = &$temp[$dir]; } } /** * Remove files / directories from cache * * @param string $path * @return bool */ private function remove_from_stat_cache($path) { $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); $temp = &$this->stat_cache; $max = count($dirs) - 1; foreach ($dirs as $i => $dir) { if (!is_array($temp)) { return false; } if ($i === $max) { unset($temp[$dir]); return true; } if (!isset($temp[$dir])) { return false; } $temp = &$temp[$dir]; } } /** * Checks cache for path * * Mainly used by file_exists * * @param string $path * @return mixed */ private function query_stat_cache($path) { $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); $temp = &$this->stat_cache; foreach ($dirs as $dir) { if (!is_array($temp)) { return null; } if (!isset($temp[$dir])) { return null; } $temp = &$temp[$dir]; } return $temp; } /** * Returns general information about a file. * * Returns an array on success and false otherwise. * * @param string $filename * @return array|false */ public function stat($filename) { if (!$this->precheck()) { return false; } $filename = $this->realpath($filename); if ($filename === false) { return false; } if ($this->use_stat_cache) { $result = $this->query_stat_cache($filename); if (is_array($result) && isset($result['.']) && isset($result['.']->stat)) { return $result['.']->stat; } if (is_object($result) && isset($result->stat)) { return $result->stat; } } $stat = $this->stat_helper($filename, NET_SFTP_STAT); if ($stat === false) { $this->remove_from_stat_cache($filename); return false; } if (isset($stat['type'])) { if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename .= '/.'; } $this->update_stat_cache($filename, (object) ['stat' => $stat]); return $stat; } $pwd = $this->pwd; $stat['type'] = $this->chdir($filename) ? NET_SFTP_TYPE_DIRECTORY : NET_SFTP_TYPE_REGULAR; $this->pwd = $pwd; if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename .= '/.'; } $this->update_stat_cache($filename, (object) ['stat' => $stat]); return $stat; } /** * Returns general information about a file or symbolic link. * * Returns an array on success and false otherwise. * * @param string $filename * @return array|false */ public function lstat($filename) { if (!$this->precheck()) { return false; } $filename = $this->realpath($filename); if ($filename === false) { return false; } if ($this->use_stat_cache) { $result = $this->query_stat_cache($filename); if (is_array($result) && isset($result['.']) && isset($result['.']->lstat)) { return $result['.']->lstat; } if (is_object($result) && isset($result->lstat)) { return $result->lstat; } } $lstat = $this->stat_helper($filename, NET_SFTP_LSTAT); if ($lstat === false) { $this->remove_from_stat_cache($filename); return false; } if (isset($lstat['type'])) { if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename .= '/.'; } $this->update_stat_cache($filename, (object) ['lstat' => $lstat]); return $lstat; } $stat = $this->stat_helper($filename, NET_SFTP_STAT); if ($lstat != $stat) { $lstat = array_merge($lstat, ['type' => NET_SFTP_TYPE_SYMLINK]); $this->update_stat_cache($filename, (object) ['lstat' => $lstat]); return $stat; } $pwd = $this->pwd; $lstat['type'] = $this->chdir($filename) ? NET_SFTP_TYPE_DIRECTORY : NET_SFTP_TYPE_REGULAR; $this->pwd = $pwd; if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename .= '/.'; } $this->update_stat_cache($filename, (object) ['lstat' => $lstat]); return $lstat; } /** * Returns general information about a file or symbolic link * * Determines information without calling \phpseclib3\Net\SFTP::realpath(). * The second parameter can be either NET_SFTP_STAT or NET_SFTP_LSTAT. * * @param string $filename * @param int $type * @throws \UnexpectedValueException on receipt of unexpected packets * @return array|false */ private function stat_helper($filename, $type) { // SFTPv4+ adds an additional 32-bit integer field - flags - to the following: $packet = Strings::packSSH2('s', $filename); $this->send_sftp_packet($type, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_ATTRS: return $this->parseAttributes($response); case NET_SFTP_STATUS: $this->logError($response); return false; } throw new \UnexpectedValueException('Expected NET_SFTP_ATTRS or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } /** * Truncates a file to a given length * * @param string $filename * @param int $new_size * @return bool */ public function truncate($filename, $new_size) { $attr = Strings::packSSH2('NQ', NET_SFTP_ATTR_SIZE, $new_size); return $this->setstat($filename, $attr, false); } /** * Sets access and modification time of file. * * If the file does not exist, it will be created. * * @param string $filename * @param int $time * @param int $atime * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool */ public function touch($filename, $time = null, $atime = null) { if (!$this->precheck()) { return false; } $filename = $this->realpath($filename); if ($filename === false) { return false; } if (!isset($time)) { $time = time(); } if (!isset($atime)) { $atime = $time; } $attr = $this->version < 4 ? pack('N3', NET_SFTP_ATTR_ACCESSTIME, $atime, $time) : Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $atime, $time); $packet = Strings::packSSH2('s', $filename); $packet .= $this->version >= 5 ? pack('N2', 0, NET_SFTP_OPEN_OPEN_EXISTING) : pack('N', NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_EXCL); $packet .= $attr; $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: return $this->close_handle(substr($response, 4)); case NET_SFTP_STATUS: $this->logError($response); break; default: throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } return $this->setstat($filename, $attr, false); } /** * Changes file or directory owner * * $uid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string * would be of the form "user@dns_domain" but it does not need to be. * `$sftp->getSupportedVersions()['version']` will return the specific version * that's being used. * * Returns true on success or false on error. * * @param string $filename * @param int|string $uid * @param bool $recursive * @return bool */ public function chown($filename, $uid, $recursive = false) { /* quoting , "To avoid a representation that is tied to a particular underlying implementation at the client or server, the use of UTF-8 strings has been chosen. The string should be of the form "user@dns_domain". This will allow for a client and server that do not use the same local representation the ability to translate to a common syntax that can be interpreted by both. In the case where there is no translation available to the client or server, the attribute value must be constructed without the "@"." phpseclib _could_ auto append the dns_domain to $uid BUT what if it shouldn't have one? phpseclib would have no way of knowing so rather than guess phpseclib will just use whatever value the user provided */ $attr = $this->version < 4 ? // quoting , // "if the owner or group is specified as -1, then that ID is not changed" pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1) : // quoting , // "If either the owner or group field is zero length, the field should be // considered absent, and no change should be made to that specific field // during a modification operation" Strings::packSSH2('Nss', NET_SFTP_ATTR_OWNERGROUP, $uid, ''); return $this->setstat($filename, $attr, $recursive); } /** * Changes file or directory group * * $gid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string * would be of the form "user@dns_domain" but it does not need to be. * `$sftp->getSupportedVersions()['version']` will return the specific version * that's being used. * * Returns true on success or false on error. * * @param string $filename * @param int|string $gid * @param bool $recursive * @return bool */ public function chgrp($filename, $gid, $recursive = false) { $attr = $this->version < 4 ? pack('N3', NET_SFTP_ATTR_UIDGID, -1, $gid) : Strings::packSSH2('Nss', NET_SFTP_ATTR_OWNERGROUP, '', $gid); return $this->setstat($filename, $attr, $recursive); } /** * Set permissions on a file. * * Returns the new file permissions on success or false on error. * If $recursive is true than this just returns true or false. * * @param int $mode * @param string $filename * @param bool $recursive * @throws \UnexpectedValueException on receipt of unexpected packets * @return mixed */ public function chmod($mode, $filename, $recursive = false) { if (is_string($mode) && is_int($filename)) { $temp = $mode; $mode = $filename; $filename = $temp; } $attr = pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777); if (!$this->setstat($filename, $attr, $recursive)) { return false; } if ($recursive) { return true; } $filename = $this->realpath($filename); // rather than return what the permissions *should* be, we'll return what they actually are. this will also // tell us if the file actually exists. // incidentally, SFTPv4+ adds an additional 32-bit integer field - flags - to the following: $packet = pack('Na*', strlen($filename), $filename); $this->send_sftp_packet(NET_SFTP_STAT, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_ATTRS: $attrs = $this->parseAttributes($response); return $attrs['mode']; case NET_SFTP_STATUS: $this->logError($response); return false; } throw new \UnexpectedValueException('Expected NET_SFTP_ATTRS or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } /** * Sets information about a file * * @param string $filename * @param string $attr * @param bool $recursive * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool */ private function setstat($filename, $attr, $recursive) { if (!$this->precheck()) { return false; } $filename = $this->realpath($filename); if ($filename === false) { return false; } $this->remove_from_stat_cache($filename); if ($recursive) { $i = 0; $result = $this->setstat_recursive($filename, $attr, $i); $this->read_put_responses($i); return $result; } $packet = Strings::packSSH2('s', $filename); $packet .= $this->version >= 4 ? pack('a*Ca*', substr($attr, 0, 4), NET_SFTP_TYPE_UNKNOWN, substr($attr, 4)) : $attr; $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet); /* "Because some systems must use separate system calls to set various attributes, it is possible that a failure response will be returned, but yet some of the attributes may be have been successfully modified. If possible, servers SHOULD avoid this situation; however, clients MUST be aware that this is possible." -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6 */ $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); return false; } return true; } /** * Recursively sets information on directories on the SFTP server * * Minimizes directory lookups and SSH_FXP_STATUS requests for speed. * * @param string $path * @param string $attr * @param int $i * @return bool */ private function setstat_recursive($path, $attr, &$i) { if (!$this->read_put_responses($i)) { return false; } $i = 0; $entries = $this->readlist($path, true); if ($entries === false || is_int($entries)) { return $this->setstat($path, $attr, false); } // normally $entries would have at least . and .. but it might not if the directories // permissions didn't allow reading if (empty($entries)) { return false; } unset($entries['.'], $entries['..']); foreach ($entries as $filename => $props) { if (!isset($props['type'])) { return false; } $temp = $path . '/' . $filename; if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) { if (!$this->setstat_recursive($temp, $attr, $i)) { return false; } } else { $packet = Strings::packSSH2('s', $temp); $packet .= $this->version >= 4 ? pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) : $attr; $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet); $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->read_put_responses($i)) { return false; } $i = 0; } } } $packet = Strings::packSSH2('s', $path); $packet .= $this->version >= 4 ? pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) : $attr; $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet); $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->read_put_responses($i)) { return false; } $i = 0; } return true; } /** * Return the target of a symbolic link * * @param string $link * @throws \UnexpectedValueException on receipt of unexpected packets * @return mixed */ public function readlink($link) { if (!$this->precheck()) { return false; } $link = $this->realpath($link); $this->send_sftp_packet(NET_SFTP_READLINK, Strings::packSSH2('s', $link)); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_NAME: break; case NET_SFTP_STATUS: $this->logError($response); return false; default: throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($count) = Strings::unpackSSH2('N', $response); // the file isn't a symlink if (!$count) { return false; } list($filename) = Strings::unpackSSH2('s', $response); return $filename; } /** * Create a symlink * * symlink() creates a symbolic link to the existing target with the specified name link. * * @param string $target * @param string $link * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool */ public function symlink($target, $link) { if (!$this->precheck()) { return false; } //$target = $this->realpath($target); $link = $this->realpath($link); /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-12.1 : Changed the SYMLINK packet to be LINK and give it the ability to create hard links. Also change it's packet number because many implementation implemented SYMLINK with the arguments reversed. Hopefully the new argument names make it clear which way is which. */ if ($this->version == 6) { $type = NET_SFTP_LINK; $packet = Strings::packSSH2('ssC', $link, $target, 1); } else { $type = NET_SFTP_SYMLINK; /* quoting http://bxr.su/OpenBSD/usr.bin/ssh/PROTOCOL#347 : 3.1. sftp: Reversal of arguments to SSH_FXP_SYMLINK When OpenSSH's sftp-server was implemented, the order of the arguments to the SSH_FXP_SYMLINK method was inadvertently reversed. Unfortunately, the reversal was not noticed until the server was widely deployed. Since fixing this to follow the specification would cause incompatibility, the current order was retained. For correct operation, clients should send SSH_FXP_SYMLINK as follows: uint32 id string targetpath string linkpath */ $packet = substr($this->server_identifier, 0, 15) == 'SSH-2.0-OpenSSH' ? Strings::packSSH2('ss', $target, $link) : Strings::packSSH2('ss', $link, $target); } $this->send_sftp_packet($type, $packet); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); return false; } return true; } /** * Creates a directory. * * @param string $dir * @param int $mode * @param bool $recursive * @return bool */ public function mkdir($dir, $mode = -1, $recursive = false) { if (!$this->precheck()) { return false; } $dir = $this->realpath($dir); if ($recursive) { $dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir)); if (empty($dirs[0])) { array_shift($dirs); $dirs[0] = '/' . $dirs[0]; } for ($i = 0; $i < count($dirs); $i++) { $temp = array_slice($dirs, 0, $i + 1); $temp = implode('/', $temp); $result = $this->mkdir_helper($temp, $mode); } return $result; } return $this->mkdir_helper($dir, $mode); } /** * Helper function for directory creation * * @param string $dir * @param int $mode * @return bool */ private function mkdir_helper($dir, $mode) { // send SSH_FXP_MKDIR without any attributes (that's what the \0\0\0\0 is doing) $this->send_sftp_packet(NET_SFTP_MKDIR, Strings::packSSH2('s', $dir) . "\0\0\0\0"); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); return false; } if ($mode !== -1) { $this->chmod($mode, $dir); } return true; } /** * Removes a directory. * * @param string $dir * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool */ public function rmdir($dir) { if (!$this->precheck()) { return false; } $dir = $this->realpath($dir); if ($dir === false) { return false; } $this->send_sftp_packet(NET_SFTP_RMDIR, Strings::packSSH2('s', $dir)); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED? $this->logError($response, $status); return false; } $this->remove_from_stat_cache($dir); // the following will do a soft delete, which would be useful if you deleted a file // and then tried to do a stat on the deleted file. the above, in contrast, does // a hard delete //$this->update_stat_cache($dir, false); return true; } /** * Uploads a file to the SFTP server. * * By default, \phpseclib3\Net\SFTP::put() does not read from the local filesystem. $data is dumped directly into $remote_file. * So, for example, if you set $data to 'filename.ext' and then do \phpseclib3\Net\SFTP::get(), you will get a file, twelve bytes * long, containing 'filename.ext' as its contents. * * Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior. With self::SOURCE_LOCAL_FILE, $remote_file will * contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how * large $remote_file will be, as well. * * Setting $mode to self::SOURCE_CALLBACK will use $data as callback function, which gets only one parameter -- number * of bytes to return, and returns a string if there is some data or null if there is no more data * * If $data is a resource then it'll be used as a resource instead. * * Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take * care of that, yourself. * * $mode can take an additional two parameters - self::RESUME and self::RESUME_START. These are bitwise AND'd with * $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following: * * self::SOURCE_LOCAL_FILE | self::RESUME * * If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace * self::RESUME with self::RESUME_START. * * If $mode & (self::RESUME | self::RESUME_START) then self::RESUME_START will be assumed. * * $start and $local_start give you more fine grained control over this process and take precident over self::RESUME * when they're non-negative. ie. $start could let you write at the end of a file (like self::RESUME) or in the middle * of one. $local_start could let you start your reading from the end of a file (like self::RESUME_START) or in the * middle of one. * * Setting $local_start to > 0 or $mode | self::RESUME_START doesn't do anything unless $mode | self::SOURCE_LOCAL_FILE. * * {@internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - \phpseclib3\Net\SFTP::setMode().} * * @param string $remote_file * @param string|resource $data * @param int $mode * @param int $start * @param int $local_start * @param callable|null $progressCallback * @throws \UnexpectedValueException on receipt of unexpected packets * @throws \BadFunctionCallException if you're uploading via a callback and the callback function is invalid * @throws FileNotFoundException if you're uploading via a file and the file doesn't exist * @return bool */ public function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = -1, $local_start = -1, $progressCallback = null) { if (!$this->precheck()) { return false; } $remote_file = $this->realpath($remote_file); if ($remote_file === false) { return false; } $this->remove_from_stat_cache($remote_file); if ($this->version >= 5) { $flags = NET_SFTP_OPEN_OPEN_OR_CREATE; } else { $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE; // according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file." // in practice, it doesn't seem to do that. //$flags|= ($mode & self::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE; } if ($start >= 0) { $offset = $start; } elseif ($mode & (self::RESUME | self::RESUME_START)) { // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called $stat = $this->stat($remote_file); $offset = $stat !== false && $stat['size'] ? $stat['size'] : 0; } else { $offset = 0; if ($this->version >= 5) { $flags = NET_SFTP_OPEN_CREATE_TRUNCATE; } else { $flags |= NET_SFTP_OPEN_TRUNCATE; } } $this->remove_from_stat_cache($remote_file); $packet = Strings::packSSH2('s', $remote_file); $packet .= $this->version >= 5 ? pack('N3', 0, $flags, 0) : pack('N2', $flags, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: $handle = substr($response, 4); break; case NET_SFTP_STATUS: $this->logError($response); return false; default: throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3 $dataCallback = false; switch (true) { case $mode & self::SOURCE_CALLBACK: if (!is_callable($data)) { throw new \BadFunctionCallException("\$data should be is_callable() if you specify SOURCE_CALLBACK flag"); } $dataCallback = $data; // do nothing break; case is_resource($data): $mode = $mode & ~self::SOURCE_LOCAL_FILE; $info = stream_get_meta_data($data); if (isset($info['wrapper_type']) && $info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') { $fp = fopen('php://memory', 'w+'); stream_copy_to_stream($data, $fp); rewind($fp); } else { $fp = $data; } break; case $mode & self::SOURCE_LOCAL_FILE: if (!is_file($data)) { throw new FileNotFoundException("$data is not a valid file"); } $fp = @fopen($data, 'rb'); if (!$fp) { return false; } } if (isset($fp)) { $stat = fstat($fp); $size = !empty($stat) ? $stat['size'] : 0; if ($local_start >= 0) { fseek($fp, $local_start); $size -= $local_start; } elseif ($mode & self::RESUME) { fseek($fp, $offset); $size -= $offset; } } elseif ($dataCallback) { $size = 0; } else { $size = strlen($data); } $sent = 0; $size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size; $sftp_packet_size = $this->max_sftp_packet; // make the SFTP packet be exactly the SFTP packet size by including the bytes in the NET_SFTP_WRITE packets "header" $sftp_packet_size -= strlen($handle) + 25; $i = $j = 0; while ($dataCallback || ($size === 0 || $sent < $size)) { if ($dataCallback) { $temp = $dataCallback($sftp_packet_size); if (is_null($temp)) { break; } } else { $temp = isset($fp) ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size); if ($temp === false || $temp === '') { break; } } $subtemp = $offset + $sent; $packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp); try { $this->send_sftp_packet(NET_SFTP_WRITE, $packet, $j); } catch (\Exception $e) { if ($mode & self::SOURCE_LOCAL_FILE) { fclose($fp); } throw $e; } $sent += strlen($temp); if (is_callable($progressCallback)) { $progressCallback($sent); } $i++; $j++; if ($i == NET_SFTP_UPLOAD_QUEUE_SIZE) { if (!$this->read_put_responses($i)) { $i = 0; break; } $i = 0; } } $result = $this->close_handle($handle); if (!$this->read_put_responses($i)) { if ($mode & self::SOURCE_LOCAL_FILE) { fclose($fp); } $this->close_handle($handle); return false; } if ($mode & SFTP::SOURCE_LOCAL_FILE) { if (isset($fp) && is_resource($fp)) { fclose($fp); } if ($this->preserveTime) { $stat = stat($data); $attr = $this->version < 4 ? pack('N3', NET_SFTP_ATTR_ACCESSTIME, $stat['atime'], $stat['mtime']) : Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $stat['atime'], $stat['mtime']); if (!$this->setstat($remote_file, $attr, false)) { throw new \RuntimeException('Error setting file time'); } } } return $result; } /** * Reads multiple successive SSH_FXP_WRITE responses * * Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i * SSH_FXP_WRITEs, in succession, and then reading $i responses. * * @param int $i * @return bool * @throws \UnexpectedValueException on receipt of unexpected packets */ private function read_put_responses($i) { while ($i--) { $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); break; } } return $i < 0; } /** * Close handle * * @param string $handle * @return bool * @throws \UnexpectedValueException on receipt of unexpected packets */ private function close_handle($handle) { $this->send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle)); // "The client MUST release all resources associated with the handle regardless of the status." // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3 $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); return false; } return true; } /** * Downloads a file from the SFTP server. * * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if * the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the * operation. * * $offset and $length can be used to download files in chunks. * * @param string $remote_file * @param string|bool|resource|callable $local_file * @param int $offset * @param int $length * @param callable|null $progressCallback * @throws \UnexpectedValueException on receipt of unexpected packets * @return string|bool */ public function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null) { if (!$this->precheck()) { return false; } $remote_file = $this->realpath($remote_file); if ($remote_file === false) { return false; } $packet = Strings::packSSH2('s', $remote_file); $packet .= $this->version >= 5 ? pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) : pack('N2', NET_SFTP_OPEN_READ, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: $handle = substr($response, 4); break; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED $this->logError($response); return false; default: throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } if (is_resource($local_file)) { $fp = $local_file; $stat = fstat($fp); $res_offset = $stat['size']; } else { $res_offset = 0; if ($local_file !== false && !is_callable($local_file)) { $fp = fopen($local_file, 'wb'); if (!$fp) { return false; } } else { $content = ''; } } $fclose_check = $local_file !== false && !is_callable($local_file) && !is_resource($local_file); $start = $offset; $read = 0; while (true) { $i = 0; while ($i < NET_SFTP_QUEUE_SIZE && ($length < 0 || $read < $length)) { $tempoffset = $start + $read; $packet_size = $length > 0 ? min($this->max_sftp_packet, $length - $read) : $this->max_sftp_packet; $packet = Strings::packSSH2('sN3', $handle, $tempoffset / 4294967296, $tempoffset, $packet_size); try { $this->send_sftp_packet(NET_SFTP_READ, $packet, $i); } catch (\Exception $e) { if ($fclose_check) { fclose($fp); } throw $e; } $packet = null; $read += $packet_size; $i++; } if (!$i) { break; } $packets_sent = $i - 1; $clear_responses = false; while ($i > 0) { $i--; if ($clear_responses) { $this->get_sftp_packet($packets_sent - $i); continue; } else { $response = $this->get_sftp_packet($packets_sent - $i); } switch ($this->packet_type) { case NET_SFTP_DATA: $temp = substr($response, 4); $offset += strlen($temp); if ($local_file === false) { $content .= $temp; } elseif (is_callable($local_file)) { $local_file($temp); } else { fputs($fp, $temp); } if (is_callable($progressCallback)) { call_user_func($progressCallback, $offset); } $temp = null; break; case NET_SFTP_STATUS: // could, in theory, return false if !strlen($content) but we'll hold off for the time being $this->logError($response); $clear_responses = true; // don't break out of the loop yet, so we can read the remaining responses break; default: if ($fclose_check) { fclose($fp); } if ($this->channel_close) { $this->partial_init = false; $this->init_sftp_connection(); return false; } else { throw new \UnexpectedValueException('Expected NET_SFTP_DATA or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } } $response = null; } if ($clear_responses) { break; } } if ($fclose_check) { fclose($fp); if ($this->preserveTime) { $stat = $this->stat($remote_file); touch($local_file, $stat['mtime'], $stat['atime']); } } if (!$this->close_handle($handle)) { return false; } // if $content isn't set that means a file was written to return isset($content) ? $content : true; } /** * Deletes a file on the SFTP server. * * @param string $path * @param bool $recursive * @return bool * @throws \UnexpectedValueException on receipt of unexpected packets */ public function delete($path, $recursive = true) { if (!$this->precheck()) { return false; } if (is_object($path)) { // It's an object. Cast it as string before we check anything else. $path = (string) $path; } if (!is_string($path) || $path == '') { return false; } $path = $this->realpath($path); if ($path === false) { return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 $this->send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($path), $path)); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); if (!$recursive) { return false; } $i = 0; $result = $this->delete_recursive($path, $i); $this->read_put_responses($i); return $result; } $this->remove_from_stat_cache($path); return true; } /** * Recursively deletes directories on the SFTP server * * Minimizes directory lookups and SSH_FXP_STATUS requests for speed. * * @param string $path * @param int $i * @return bool */ private function delete_recursive($path, &$i) { if (!$this->read_put_responses($i)) { return false; } $i = 0; $entries = $this->readlist($path, true); // The folder does not exist at all, so we cannot delete it. if ($entries === NET_SFTP_STATUS_NO_SUCH_FILE) { return false; } // Normally $entries would have at least . and .. but it might not if the directories // permissions didn't allow reading. If this happens then default to an empty list of files. if ($entries === false || is_int($entries)) { $entries = []; } unset($entries['.'], $entries['..']); foreach ($entries as $filename => $props) { if (!isset($props['type'])) { return false; } $temp = $path . '/' . $filename; if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) { if (!$this->delete_recursive($temp, $i)) { return false; } } else { $this->send_sftp_packet(NET_SFTP_REMOVE, Strings::packSSH2('s', $temp)); $this->remove_from_stat_cache($temp); $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->read_put_responses($i)) { return false; } $i = 0; } } } $this->send_sftp_packet(NET_SFTP_RMDIR, Strings::packSSH2('s', $path)); $this->remove_from_stat_cache($path); $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->read_put_responses($i)) { return false; } $i = 0; } return true; } /** * Checks whether a file or directory exists * * @param string $path * @return bool */ public function file_exists($path) { if ($this->use_stat_cache) { if (!$this->precheck()) { return false; } $path = $this->realpath($path); $result = $this->query_stat_cache($path); if (isset($result)) { // return true if $result is an array or if it's an stdClass object return $result !== false; } } return $this->stat($path) !== false; } /** * Tells whether the filename is a directory * * @param string $path * @return bool */ public function is_dir($path) { $result = $this->get_stat_cache_prop($path, 'type'); if ($result === false) { return false; } return $result === NET_SFTP_TYPE_DIRECTORY; } /** * Tells whether the filename is a regular file * * @param string $path * @return bool */ public function is_file($path) { $result = $this->get_stat_cache_prop($path, 'type'); if ($result === false) { return false; } return $result === NET_SFTP_TYPE_REGULAR; } /** * Tells whether the filename is a symbolic link * * @param string $path * @return bool */ public function is_link($path) { $result = $this->get_lstat_cache_prop($path, 'type'); if ($result === false) { return false; } return $result === NET_SFTP_TYPE_SYMLINK; } /** * Tells whether a file exists and is readable * * @param string $path * @return bool */ public function is_readable($path) { if (!$this->precheck()) { return false; } $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_READ, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: return true; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED return false; default: throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } } /** * Tells whether the filename is writable * * @param string $path * @return bool */ public function is_writable($path) { if (!$this->precheck()) { return false; } $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_WRITE, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: return true; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED return false; default: throw new \UnexpectedValueException('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } } /** * Tells whether the filename is writeable * * Alias of is_writable * * @param string $path * @return bool */ public function is_writeable($path) { return $this->is_writable($path); } /** * Gets last access time of file * * @param string $path * @return mixed */ public function fileatime($path) { return $this->get_stat_cache_prop($path, 'atime'); } /** * Gets file modification time * * @param string $path * @return mixed */ public function filemtime($path) { return $this->get_stat_cache_prop($path, 'mtime'); } /** * Gets file permissions * * @param string $path * @return mixed */ public function fileperms($path) { return $this->get_stat_cache_prop($path, 'mode'); } /** * Gets file owner * * @param string $path * @return mixed */ public function fileowner($path) { return $this->get_stat_cache_prop($path, 'uid'); } /** * Gets file group * * @param string $path * @return mixed */ public function filegroup($path) { return $this->get_stat_cache_prop($path, 'gid'); } /** * Recursively go through rawlist() output to get the total filesize * * @return int */ private static function recursiveFilesize(array $files) { $size = 0; foreach ($files as $name => $file) { if ($name == '.' || $name == '..') { continue; } $size += is_array($file) ? self::recursiveFilesize($file) : $file->size; } return $size; } /** * Gets file size * * @param string $path * @param bool $recursive * @return mixed */ public function filesize($path, $recursive = false) { return !$recursive || $this->filetype($path) != 'dir' ? $this->get_stat_cache_prop($path, 'size') : self::recursiveFilesize($this->rawlist($path, true)); } /** * Gets file type * * @param string $path * @return string|false */ public function filetype($path) { $type = $this->get_stat_cache_prop($path, 'type'); if ($type === false) { return false; } switch ($type) { case NET_SFTP_TYPE_BLOCK_DEVICE: return 'block'; case NET_SFTP_TYPE_CHAR_DEVICE: return 'char'; case NET_SFTP_TYPE_DIRECTORY: return 'dir'; case NET_SFTP_TYPE_FIFO: return 'fifo'; case NET_SFTP_TYPE_REGULAR: return 'file'; case NET_SFTP_TYPE_SYMLINK: return 'link'; default: return false; } } /** * Return a stat properity * * Uses cache if appropriate. * * @param string $path * @param string $prop * @return mixed */ private function get_stat_cache_prop($path, $prop) { return $this->get_xstat_cache_prop($path, $prop, 'stat'); } /** * Return an lstat properity * * Uses cache if appropriate. * * @param string $path * @param string $prop * @return mixed */ private function get_lstat_cache_prop($path, $prop) { return $this->get_xstat_cache_prop($path, $prop, 'lstat'); } /** * Return a stat or lstat properity * * Uses cache if appropriate. * * @param string $path * @param string $prop * @param string $type * @return mixed */ private function get_xstat_cache_prop($path, $prop, $type) { if (!$this->precheck()) { return false; } if ($this->use_stat_cache) { $path = $this->realpath($path); $result = $this->query_stat_cache($path); if (is_object($result) && isset($result->$type)) { return $result->{$type}[$prop]; } } $result = $this->$type($path); if ($result === false || !isset($result[$prop])) { return false; } return $result[$prop]; } /** * Renames a file or a directory on the SFTP server. * * If the file already exists this will return false * * @param string $oldname * @param string $newname * @return bool * @throws \UnexpectedValueException on receipt of unexpected packets */ public function rename($oldname, $newname) { if (!$this->precheck()) { return false; } $oldname = $this->realpath($oldname); $newname = $this->realpath($newname); if ($oldname === false || $newname === false) { return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 $packet = Strings::packSSH2('ss', $oldname, $newname); if ($this->version >= 5) { /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-6.5 , 'flags' is 0 or a combination of: SSH_FXP_RENAME_OVERWRITE 0x00000001 SSH_FXP_RENAME_ATOMIC 0x00000002 SSH_FXP_RENAME_NATIVE 0x00000004 (none of these are currently supported) */ $packet .= "\0\0\0\0"; } $this->send_sftp_packet(NET_SFTP_RENAME, $packet); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); return false; } // don't move the stat cache entry over since this operation could very well change the // atime and mtime attributes //$this->update_stat_cache($newname, $this->query_stat_cache($oldname)); $this->remove_from_stat_cache($oldname); $this->remove_from_stat_cache($newname); return true; } /** * Parse Time * * See '7.7. Times' of draft-ietf-secsh-filexfer-13 for more info. * * @param string $key * @param int $flags * @param string $response * @return array */ private function parseTime($key, $flags, &$response) { $attr = []; list($attr[$key]) = Strings::unpackSSH2('Q', $response); if ($flags & NET_SFTP_ATTR_SUBSECOND_TIMES) { list($attr[$key . '-nseconds']) = Strings::unpackSSH2('N', $response); } return $attr; } /** * Parse Attributes * * See '7. File Attributes' of draft-ietf-secsh-filexfer-13 for more info. * * @param string $response * @return array */ protected function parseAttributes(&$response) { $attr = []; if ($this->version >= 4) { list($flags, $attr['type']) = Strings::unpackSSH2('NC', $response); } else { list($flags) = Strings::unpackSSH2('N', $response); } foreach (self::$attributes as $key => $value) { switch ($flags & $key) { case NET_SFTP_ATTR_UIDGID: if ($this->version > 3) { continue 2; } break; case NET_SFTP_ATTR_CREATETIME: case NET_SFTP_ATTR_MODIFYTIME: case NET_SFTP_ATTR_ACL: case NET_SFTP_ATTR_OWNERGROUP: case NET_SFTP_ATTR_SUBSECOND_TIMES: if ($this->version < 4) { continue 2; } break; case NET_SFTP_ATTR_BITS: if ($this->version < 5) { continue 2; } break; case NET_SFTP_ATTR_ALLOCATION_SIZE: case NET_SFTP_ATTR_TEXT_HINT: case NET_SFTP_ATTR_MIME_TYPE: case NET_SFTP_ATTR_LINK_COUNT: case NET_SFTP_ATTR_UNTRANSLATED_NAME: case NET_SFTP_ATTR_CTIME: if ($this->version < 6) { continue 2; } } switch ($flags & $key) { case NET_SFTP_ATTR_SIZE: // 0x00000001 // The size attribute is defined as an unsigned 64-bit integer. // The following will use floats on 32-bit platforms, if necessary. // As can be seen in the BigInteger class, floats are generally // IEEE 754 binary64 "double precision" on such platforms and // as such can represent integers of at least 2^50 without loss // of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB. list($attr['size']) = Strings::unpackSSH2('Q', $response); break; case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only) list($attr['uid'], $attr['gid']) = Strings::unpackSSH2('NN', $response); break; case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004 list($attr['mode']) = Strings::unpackSSH2('N', $response); $fileType = $this->parseMode($attr['mode']); if ($this->version < 4 && $fileType !== false) { $attr += ['type' => $fileType]; } break; case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 if ($this->version >= 4) { $attr += $this->parseTime('atime', $flags, $response); break; } list($attr['atime'], $attr['mtime']) = Strings::unpackSSH2('NN', $response); break; case NET_SFTP_ATTR_CREATETIME: // 0x00000010 (SFTPv4+) $attr += $this->parseTime('createtime', $flags, $response); break; case NET_SFTP_ATTR_MODIFYTIME: // 0x00000020 $attr += $this->parseTime('mtime', $flags, $response); break; case NET_SFTP_ATTR_ACL: // 0x00000040 // access control list // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-04#section-5.7 // currently unsupported list($count) = Strings::unpackSSH2('N', $response); for ($i = 0; $i < $count; $i++) { list($type, $flag, $mask, $who) = Strings::unpackSSH2('N3s', $result); } break; case NET_SFTP_ATTR_OWNERGROUP: // 0x00000080 list($attr['owner'], $attr['$group']) = Strings::unpackSSH2('ss', $response); break; case NET_SFTP_ATTR_SUBSECOND_TIMES: // 0x00000100 break; case NET_SFTP_ATTR_BITS: // 0x00000200 (SFTPv5+) // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-5.8 // currently unsupported // tells if you file is: // readonly, system, hidden, case inensitive, archive, encrypted, compressed, sparse // append only, immutable, sync list($attrib_bits, $attrib_bits_valid) = Strings::unpackSSH2('N2', $response); // if we were actually gonna implement the above it ought to be // $attr['attrib-bits'] and $attr['attrib-bits-valid'] // eg. - instead of _ break; case NET_SFTP_ATTR_ALLOCATION_SIZE: // 0x00000400 (SFTPv6+) // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.4 // represents the number of bytes that the file consumes on the disk. will // usually be larger than the 'size' field list($attr['allocation-size']) = Strings::unpackSSH2('Q', $response); break; case NET_SFTP_ATTR_TEXT_HINT: // 0x00000800 // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.10 // currently unsupported // tells if file is "known text", "guessed text", "known binary", "guessed binary" list($text_hint) = Strings::unpackSSH2('C', $response); // the above should be $attr['text-hint'] break; case NET_SFTP_ATTR_MIME_TYPE: // 0x00001000 // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.11 list($attr['mime-type']) = Strings::unpackSSH2('s', $response); break; case NET_SFTP_ATTR_LINK_COUNT: // 0x00002000 // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.12 list($attr['link-count']) = Strings::unpackSSH2('N', $response); break; case NET_SFTP_ATTR_UNTRANSLATED_NAME:// 0x00004000 // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.13 list($attr['untranslated-name']) = Strings::unpackSSH2('s', $response); break; case NET_SFTP_ATTR_CTIME: // 0x00008000 // 'ctime' contains the last time the file attributes were changed. The // exact meaning of this field depends on the server. $attr += $this->parseTime('ctime', $flags, $response); break; case NET_SFTP_ATTR_EXTENDED: // 0x80000000 list($count) = Strings::unpackSSH2('N', $response); for ($i = 0; $i < $count; $i++) { list($key, $value) = Strings::unpackSSH2('ss', $response); $attr[$key] = $value; } } } return $attr; } /** * Attempt to identify the file type * * Quoting the SFTP RFC, "Implementations MUST NOT send bits that are not defined" but they seem to anyway * * @param int $mode * @return int */ private function parseMode($mode) { // values come from http://lxr.free-electrons.com/source/include/uapi/linux/stat.h#L12 // see, also, http://linux.die.net/man/2/stat switch ($mode & 0170000) {// ie. 1111 0000 0000 0000 case 0000000: // no file type specified - figure out the file type using alternative means return false; case 0040000: return NET_SFTP_TYPE_DIRECTORY; case 0100000: return NET_SFTP_TYPE_REGULAR; case 0120000: return NET_SFTP_TYPE_SYMLINK; // new types introduced in SFTPv5+ // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2 case 0010000: // named pipe (fifo) return NET_SFTP_TYPE_FIFO; case 0020000: // character special return NET_SFTP_TYPE_CHAR_DEVICE; case 0060000: // block special return NET_SFTP_TYPE_BLOCK_DEVICE; case 0140000: // socket return NET_SFTP_TYPE_SOCKET; case 0160000: // whiteout // "SPECIAL should be used for files that are of // a known type which cannot be expressed in the protocol" return NET_SFTP_TYPE_SPECIAL; default: return NET_SFTP_TYPE_UNKNOWN; } } /** * Parse Longname * * SFTPv3 doesn't provide any easy way of identifying a file type. You could try to open * a file as a directory and see if an error is returned or you could try to parse the * SFTPv3-specific longname field of the SSH_FXP_NAME packet. That's what this function does. * The result is returned using the * {@link http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 SFTPv4 type constants}. * * If the longname is in an unrecognized format bool(false) is returned. * * @param string $longname * @return mixed */ private function parseLongname($longname) { // http://en.wikipedia.org/wiki/Unix_file_types // http://en.wikipedia.org/wiki/Filesystem_permissions#Notation_of_traditional_Unix_permissions if (preg_match('#^[^/]([r-][w-][xstST-]){3}#', $longname)) { switch ($longname[0]) { case '-': return NET_SFTP_TYPE_REGULAR; case 'd': return NET_SFTP_TYPE_DIRECTORY; case 'l': return NET_SFTP_TYPE_SYMLINK; default: return NET_SFTP_TYPE_SPECIAL; } } return false; } /** * Sends SFTP Packets * * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info. * * @param int $type * @param string $data * @param int $request_id * @see self::_get_sftp_packet() * @see self::send_channel_packet() * @return void */ private function send_sftp_packet($type, $data, $request_id = 1) { // in SSH2.php the timeout is cumulative per function call. eg. exec() will // timeout after 10s. but for SFTP.php it's cumulative per packet $this->curTimeout = $this->timeout; $this->is_timeout = false; $packet = $this->use_request_id ? pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) : pack('NCa*', strlen($data) + 1, $type, $data); $start = microtime(true); $this->send_channel_packet(self::CHANNEL, $packet); $stop = microtime(true); if (defined('NET_SFTP_LOGGING')) { $packet_type = '-> ' . self::$packet_types[$type] . ' (' . round($stop - $start, 4) . 's)'; $this->append_log($packet_type, $data); } } /** * Resets the SFTP channel for re-use */ private function reset_sftp() { $this->use_request_id = false; $this->pwd = false; $this->requestBuffer = []; $this->partial_init = false; } /** * Resets a connection for re-use */ protected function reset_connection() { parent::reset_connection(); $this->reset_sftp(); } /** * Receives SFTP Packets * * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info. * * Incidentally, the number of SSH_MSG_CHANNEL_DATA messages has no bearing on the number of SFTP packets present. * There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA * messages containing one SFTP packet. * * @see self::_send_sftp_packet() * @return string */ private function get_sftp_packet($request_id = null) { $this->channel_close = false; if (isset($request_id) && isset($this->requestBuffer[$request_id])) { $this->packet_type = $this->requestBuffer[$request_id]['packet_type']; $temp = $this->requestBuffer[$request_id]['packet']; unset($this->requestBuffer[$request_id]); return $temp; } // in SSH2.php the timeout is cumulative per function call. eg. exec() will // timeout after 10s. but for SFTP.php it's cumulative per packet $this->curTimeout = $this->timeout; $this->is_timeout = false; $start = microtime(true); // SFTP packet length while (strlen($this->packet_buffer) < 4) { $temp = $this->get_channel_packet(self::CHANNEL, true); if ($temp === true) { if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) { $this->channel_close = true; } $this->packet_type = false; $this->packet_buffer = ''; return false; } $this->packet_buffer .= $temp; } if (strlen($this->packet_buffer) < 4) { throw new \RuntimeException('Packet is too small'); } extract(unpack('Nlength', Strings::shift($this->packet_buffer, 4))); /** @var integer $length */ $tempLength = $length; $tempLength -= strlen($this->packet_buffer); // 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h if (!$this->allow_arbitrary_length_packets && !$this->use_request_id && $tempLength > 256 * 1024) { throw new \RuntimeException('Invalid Size'); } // SFTP packet type and data payload while ($tempLength > 0) { $temp = $this->get_channel_packet(self::CHANNEL, true); if ($temp === true) { if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) { $this->channel_close = true; } $this->packet_type = false; $this->packet_buffer = ''; return false; } $this->packet_buffer .= $temp; $tempLength -= strlen($temp); } $stop = microtime(true); $this->packet_type = ord(Strings::shift($this->packet_buffer)); if ($this->use_request_id) { extract(unpack('Npacket_id', Strings::shift($this->packet_buffer, 4))); // remove the request id $length -= 5; // account for the request id and the packet type } else { $length -= 1; // account for the packet type } $packet = Strings::shift($this->packet_buffer, $length); if (defined('NET_SFTP_LOGGING')) { $packet_type = '<- ' . self::$packet_types[$this->packet_type] . ' (' . round($stop - $start, 4) . 's)'; $this->append_log($packet_type, $packet); } if (isset($request_id) && $this->use_request_id && $packet_id != $request_id) { $this->requestBuffer[$packet_id] = [ 'packet_type' => $this->packet_type, 'packet' => $packet ]; return $this->get_sftp_packet($request_id); } return $packet; } /** * Logs data packets * * Makes sure that only the last 1MB worth of packets will be logged * * @param string $message_number * @param string $message */ private function append_log($message_number, $message) { $this->append_log_helper( NET_SFTP_LOGGING, $message_number, $message, $this->packet_type_log, $this->packet_log, $this->log_size, $this->realtime_log_file, $this->realtime_log_wrap, $this->realtime_log_size ); } /** * Returns a log of the packets that have been sent and received. * * Returns a string if NET_SFTP_LOGGING == self::LOG_COMPLEX, an array if NET_SFTP_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SFTP_LOGGING') * * @return array|string|false */ public function getSFTPLog() { if (!defined('NET_SFTP_LOGGING')) { return false; } switch (NET_SFTP_LOGGING) { case self::LOG_COMPLEX: return $this->format_log($this->packet_log, $this->packet_type_log); break; //case self::LOG_SIMPLE: default: return $this->packet_type_log; } } /** * Returns all errors on the SFTP layer * * @return array */ public function getSFTPErrors() { return $this->sftp_errors; } /** * Returns the last error on the SFTP layer * * @return string */ public function getLastSFTPError() { return count($this->sftp_errors) ? $this->sftp_errors[count($this->sftp_errors) - 1] : ''; } /** * Get supported SFTP versions * * @return array */ public function getSupportedVersions() { if (!($this->bitmap & SSH2::MASK_LOGIN)) { return false; } if (!$this->partial_init) { $this->partial_init_sftp_connection(); } $temp = ['version' => $this->defaultVersion]; if (isset($this->extensions['versions'])) { $temp['extensions'] = $this->extensions['versions']; } return $temp; } /** * Get supported SFTP extensions * * @return array */ public function getSupportedExtensions() { if (!($this->bitmap & SSH2::MASK_LOGIN)) { return false; } if (!$this->partial_init) { $this->partial_init_sftp_connection(); } return $this->extensions; } /** * Get supported SFTP versions * * @return int|false */ public function getNegotiatedVersion() { if (!$this->precheck()) { return false; } return $this->version; } /** * Set preferred version * * If you're preferred version isn't supported then the highest supported * version of SFTP will be utilized. Set to null or false or int(0) to * unset the preferred version * * @param int $version */ public function setPreferredVersion($version) { $this->preferredVersion = $version; } /** * Disconnect * * @param int $reason * @return false */ protected function disconnect_helper($reason) { $this->pwd = false; return parent::disconnect_helper($reason); } /** * Enable Date Preservation * */ public function enableDatePreservation() { $this->preserveTime = true; } /** * Disable Date Preservation * */ public function disableDatePreservation() { $this->preserveTime = false; } /** * POSIX Rename * * Where rename() fails "if there already exists a file with the name specified by newpath" * (draft-ietf-secsh-filexfer-02#section-6.5), posix_rename() overwrites the existing file in an atomic fashion. * ie. "there is no observable instant in time where the name does not refer to either the old or the new file" * (draft-ietf-secsh-filexfer-13#page-39). * * @param string $oldname * @param string $newname * @return bool */ public function posix_rename($oldname, $newname) { if (!$this->precheck()) { return false; } $oldname = $this->realpath($oldname); $newname = $this->realpath($newname); if ($oldname === false || $newname === false) { return false; } if ($this->version >= 5) { $packet = Strings::packSSH2('ssN', $oldname, $newname, 2); // 2 = SSH_FXP_RENAME_ATOMIC $this->send_sftp_packet(NET_SFTP_RENAME, $packet); } elseif (isset($this->extensions['posix-rename@openssh.com']) && $this->extensions['posix-rename@openssh.com'] === '1') { $packet = Strings::packSSH2('sss', 'posix-rename@openssh.com', $oldname, $newname); $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); } else { throw new \RuntimeException( "Extension 'posix-rename@openssh.com' is not supported by the server. " . "Call getSupportedVersions() to see a list of supported extension" ); } $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' . 'Got packet type: ' . $this->packet_type); } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED list($status) = Strings::unpackSSH2('N', $response); if ($status != NET_SFTP_STATUS_OK) { $this->logError($response, $status); return false; } // don't move the stat cache entry over since this operation could very well change the // atime and mtime attributes //$this->update_stat_cache($newname, $this->query_stat_cache($oldname)); $this->remove_from_stat_cache($oldname); $this->remove_from_stat_cache($newname); return true; } /** * Returns general information about a file system. * * The function statvfs() returns information about a mounted filesystem. * @see https://man7.org/linux/man-pages/man3/statvfs.3.html * * @param string $path * @return false|array{bsize: int, frsize: int, blocks: int, bfree: int, bavail: int, files: int, ffree: int, favail: int, fsid: int, flag: int, namemax: int} */ public function statvfs($path) { if (!$this->precheck()) { return false; } if (!isset($this->extensions['statvfs@openssh.com']) || $this->extensions['statvfs@openssh.com'] !== '2') { throw new \RuntimeException( "Extension 'statvfs@openssh.com' is not supported by the server. " . "Call getSupportedVersions() to see a list of supported extension" ); } $realpath = $this->realpath($path); if ($realpath === false) { return false; } $packet = Strings::packSSH2('ss', 'statvfs@openssh.com', $realpath); $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet); $response = $this->get_sftp_packet(); if ($this->packet_type !== NET_SFTP_EXTENDED_REPLY) { throw new \UnexpectedValueException( 'Expected SSH_FXP_EXTENDED_REPLY. ' . 'Got packet type: ' . $this->packet_type ); } /** * These requests return a SSH_FXP_STATUS reply on failure. On success they * return the following SSH_FXP_EXTENDED_REPLY reply: * * uint32 id * uint64 f_bsize file system block size * uint64 f_frsize fundamental fs block size * uint64 f_blocks number of blocks (unit f_frsize) * uint64 f_bfree free blocks in file system * uint64 f_bavail free blocks for non-root * uint64 f_files total file inodes * uint64 f_ffree free file inodes * uint64 f_favail free file inodes for to non-root * uint64 f_fsid file system id * uint64 f_flag bit mask of f_flag values * uint64 f_namemax maximum filename length */ return array_combine( ['bsize', 'frsize', 'blocks', 'bfree', 'bavail', 'files', 'ffree', 'favail', 'fsid', 'flag', 'namemax'], Strings::unpackSSH2('QQQQQQQQQQQ', $response) ); } } PK!Op  1vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.phpnu[ * login('username', 'password')) { * exit('Login Failed'); * } * * echo $ssh->exec('pwd'); * echo $ssh->exec('ls -la'); * ?> * * * * login('username', $key)) { * exit('Login Failed'); * } * * echo $ssh->read('username@username:~$'); * $ssh->write("ls -la\n"); * echo $ssh->read('username@username:~$'); * ?> * * * @author Jim Wigginton * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\Net; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Blowfish; use phpseclib3\Crypt\ChaCha20; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\Common\PrivateKey; use phpseclib3\Crypt\Common\PublicKey; use phpseclib3\Crypt\Common\SymmetricKey; use phpseclib3\Crypt\DH; use phpseclib3\Crypt\DSA; use phpseclib3\Crypt\EC; use phpseclib3\Crypt\Hash; use phpseclib3\Crypt\Random; use phpseclib3\Crypt\RC4; use phpseclib3\Crypt\Rijndael; use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\TripleDES; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification. use phpseclib3\Crypt\Twofish; use phpseclib3\Exception\ConnectionClosedException; use phpseclib3\Exception\InsufficientSetupException; use phpseclib3\Exception\InvalidPacketLengthException; use phpseclib3\Exception\NoSupportedAlgorithmsException; use phpseclib3\Exception\TimeoutException; use phpseclib3\Exception\UnableToConnectException; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\Exception\UnsupportedCurveException; use phpseclib3\Math\BigInteger; use phpseclib3\System\SSH\Agent; /** * Pure-PHP implementation of SSHv2. * * @author Jim Wigginton */ class SSH2 { /**#@+ * Compression Types * */ /** * No compression */ const NET_SSH2_COMPRESSION_NONE = 1; /** * zlib compression */ const NET_SSH2_COMPRESSION_ZLIB = 2; /** * zlib@openssh.com */ const NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH = 3; /**#@-*/ // Execution Bitmap Masks const MASK_CONSTRUCTOR = 0x00000001; const MASK_CONNECTED = 0x00000002; const MASK_LOGIN_REQ = 0x00000004; const MASK_LOGIN = 0x00000008; const MASK_SHELL = 0x00000010; const MASK_DISCONNECT = 0x00000020; /* * Channel constants * * RFC4254 refers not to client and server channels but rather to sender and recipient channels. we don't refer * to them in that way because RFC4254 toggles the meaning. the client sends a SSH_MSG_CHANNEL_OPEN message with * a sender channel and the server sends a SSH_MSG_CHANNEL_OPEN_CONFIRMATION in response, with a sender and a * recipient channel. at first glance, you might conclude that SSH_MSG_CHANNEL_OPEN_CONFIRMATION's sender channel * would be the same thing as SSH_MSG_CHANNEL_OPEN's sender channel, but it's not, per this snippet: * The 'recipient channel' is the channel number given in the original * open request, and 'sender channel' is the channel number allocated by * the other side. * * @see \phpseclib3\Net\SSH2::send_channel_packet() * @see \phpseclib3\Net\SSH2::get_channel_packet() */ const CHANNEL_EXEC = 1; // PuTTy uses 0x100 const CHANNEL_SHELL = 2; const CHANNEL_SUBSYSTEM = 3; const CHANNEL_AGENT_FORWARD = 4; const CHANNEL_KEEP_ALIVE = 5; /** * Returns the message numbers * * @see \phpseclib3\Net\SSH2::getLog() */ const LOG_SIMPLE = 1; /** * Returns the message content * * @see \phpseclib3\Net\SSH2::getLog() */ const LOG_COMPLEX = 2; /** * Outputs the content real-time */ const LOG_REALTIME = 3; /** * Dumps the content real-time to a file */ const LOG_REALTIME_FILE = 4; /** * Outputs the message numbers real-time */ const LOG_SIMPLE_REALTIME = 5; /* * Dumps the message numbers real-time */ const LOG_REALTIME_SIMPLE = 5; /** * Make sure that the log never gets larger than this * * @see \phpseclib3\Net\SSH2::getLog() */ const LOG_MAX_SIZE = 1048576; // 1024 * 1024 /** * Returns when a string matching $expect exactly is found * * @see \phpseclib3\Net\SSH2::read() */ const READ_SIMPLE = 1; /** * Returns when a string matching the regular expression $expect is found * * @see \phpseclib3\Net\SSH2::read() */ const READ_REGEX = 2; /** * Returns whenever a data packet is received. * * Some data packets may only contain a single character so it may be necessary * to call read() multiple times when using this option * * @see \phpseclib3\Net\SSH2::read() */ const READ_NEXT = 3; /** * The SSH identifier * * @var string */ private $identifier; /** * The Socket Object * * @var resource|closed-resource|null */ public $fsock; /** * Execution Bitmap * * The bits that are set represent functions that have been called already. This is used to determine * if a requisite function has been successfully executed. If not, an error should be thrown. * * @var int */ protected $bitmap = 0; /** * Error information * * @see self::getErrors() * @see self::getLastError() * @var array */ private $errors = []; /** * Server Identifier * * @see self::getServerIdentification() * @var string|false */ protected $server_identifier = false; /** * Key Exchange Algorithms * * @see self::getKexAlgorithims() * @var array|false */ private $kex_algorithms = false; /** * Key Exchange Algorithm * * @see self::getMethodsNegotiated() * @var string|false */ private $kex_algorithm = false; /** * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods * * @see self::_key_exchange() * @var int */ private $kex_dh_group_size_min = 1536; /** * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods * * @see self::_key_exchange() * @var int */ private $kex_dh_group_size_preferred = 2048; /** * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods * * @see self::_key_exchange() * @var int */ private $kex_dh_group_size_max = 4096; /** * Server Host Key Algorithms * * @see self::getServerHostKeyAlgorithms() * @var array|false */ private $server_host_key_algorithms = false; /** * Supported Private Key Algorithms * * In theory this should be the same as the Server Host Key Algorithms but, in practice, * some servers (eg. Azure) will support rsa-sha2-512 as a server host key algorithm but * not a private key algorithm * * @see self::privatekey_login() * @var array|false */ private $supported_private_key_algorithms = false; /** * Encryption Algorithms: Client to Server * * @see self::getEncryptionAlgorithmsClient2Server() * @var array|false */ private $encryption_algorithms_client_to_server = false; /** * Encryption Algorithms: Server to Client * * @see self::getEncryptionAlgorithmsServer2Client() * @var array|false */ private $encryption_algorithms_server_to_client = false; /** * MAC Algorithms: Client to Server * * @see self::getMACAlgorithmsClient2Server() * @var array|false */ private $mac_algorithms_client_to_server = false; /** * MAC Algorithms: Server to Client * * @see self::getMACAlgorithmsServer2Client() * @var array|false */ private $mac_algorithms_server_to_client = false; /** * Compression Algorithms: Client to Server * * @see self::getCompressionAlgorithmsClient2Server() * @var array|false */ private $compression_algorithms_client_to_server = false; /** * Compression Algorithms: Server to Client * * @see self::getCompressionAlgorithmsServer2Client() * @var array|false */ private $compression_algorithms_server_to_client = false; /** * Languages: Server to Client * * @see self::getLanguagesServer2Client() * @var array|false */ private $languages_server_to_client = false; /** * Languages: Client to Server * * @see self::getLanguagesClient2Server() * @var array|false */ private $languages_client_to_server = false; /** * Preferred Algorithms * * @see self::setPreferredAlgorithms() * @var array */ private $preferred = []; /** * Block Size for Server to Client Encryption * * "Note that the length of the concatenation of 'packet_length', * 'padding_length', 'payload', and 'random padding' MUST be a multiple * of the cipher block size or 8, whichever is larger. This constraint * MUST be enforced, even when using stream ciphers." * * -- http://tools.ietf.org/html/rfc4253#section-6 * * @see self::__construct() * @see self::_send_binary_packet() * @var int */ private $encrypt_block_size = 8; /** * Block Size for Client to Server Encryption * * @see self::__construct() * @see self::_get_binary_packet() * @var int */ private $decrypt_block_size = 8; /** * Server to Client Encryption Object * * @see self::_get_binary_packet() * @var SymmetricKey|false */ private $decrypt = false; /** * Decryption Algorithm Name * * @var string|null */ private $decryptName; /** * Decryption Invocation Counter * * Used by GCM * * @var string|null */ private $decryptInvocationCounter; /** * Fixed Part of Nonce * * Used by GCM * * @var string|null */ private $decryptFixedPart; /** * Server to Client Length Encryption Object * * @see self::_get_binary_packet() * @var object */ private $lengthDecrypt = false; /** * Client to Server Encryption Object * * @see self::_send_binary_packet() * @var SymmetricKey|false */ private $encrypt = false; /** * Encryption Algorithm Name * * @var string|null */ private $encryptName; /** * Encryption Invocation Counter * * Used by GCM * * @var string|null */ private $encryptInvocationCounter; /** * Fixed Part of Nonce * * Used by GCM * * @var string|null */ private $encryptFixedPart; /** * Client to Server Length Encryption Object * * @see self::_send_binary_packet() * @var object */ private $lengthEncrypt = false; /** * Client to Server HMAC Object * * @see self::_send_binary_packet() * @var object */ private $hmac_create = false; /** * Client to Server HMAC Name * * @var string|false */ private $hmac_create_name; /** * Client to Server ETM * * @var int|false */ private $hmac_create_etm; /** * Server to Client HMAC Object * * @see self::_get_binary_packet() * @var object */ private $hmac_check = false; /** * Server to Client HMAC Name * * @var string|false */ private $hmac_check_name; /** * Server to Client ETM * * @var int|false */ private $hmac_check_etm; /** * Size of server to client HMAC * * We need to know how big the HMAC will be for the server to client direction so that we know how many bytes to read. * For the client to server side, the HMAC object will make the HMAC as long as it needs to be. All we need to do is * append it. * * @see self::_get_binary_packet() * @var int */ private $hmac_size = false; /** * Server Public Host Key * * @see self::getServerPublicHostKey() * @var string */ private $server_public_host_key; /** * Session identifier * * "The exchange hash H from the first key exchange is additionally * used as the session identifier, which is a unique identifier for * this connection." * * -- http://tools.ietf.org/html/rfc4253#section-7.2 * * @see self::_key_exchange() * @var string */ private $session_id = false; /** * Exchange hash * * The current exchange hash * * @see self::_key_exchange() * @var string */ private $exchange_hash = false; /** * Message Numbers * * @see self::__construct() * @var array * @access private */ private static $message_numbers = []; /** * Disconnection Message 'reason codes' defined in RFC4253 * * @see self::__construct() * @var array * @access private */ private static $disconnect_reasons = []; /** * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254 * * @see self::__construct() * @var array * @access private */ private static $channel_open_failure_reasons = []; /** * Terminal Modes * * @link http://tools.ietf.org/html/rfc4254#section-8 * @see self::__construct() * @var array * @access private */ private static $terminal_modes = []; /** * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes * * @link http://tools.ietf.org/html/rfc4254#section-5.2 * @see self::__construct() * @var array * @access private */ private static $channel_extended_data_type_codes = []; /** * Send Sequence Number * * See 'Section 6.4. Data Integrity' of rfc4253 for more info. * * @see self::_send_binary_packet() * @var int */ private $send_seq_no = 0; /** * Get Sequence Number * * See 'Section 6.4. Data Integrity' of rfc4253 for more info. * * @see self::_get_binary_packet() * @var int */ private $get_seq_no = 0; /** * Server Channels * * Maps client channels to server channels * * @see self::get_channel_packet() * @see self::exec() * @var array */ protected $server_channels = []; /** * Channel Read Buffers * * If a client requests a packet from one channel but receives two packets from another those packets should * be placed in a buffer * * @see self::get_channel_packet() * @see self::exec() * @var array */ private $channel_buffers = []; /** * Channel Write Buffers * * If a client sends a packet and receives a timeout error mid-transmission, buffer the data written so it * can be de-duplicated upon resuming write * * @see self::send_channel_packet() * @var array */ private $channel_buffers_write = []; /** * Channel Status * * Contains the type of the last sent message * * @see self::get_channel_packet() * @var array */ protected $channel_status = []; /** * The identifier of the interactive channel which was opened most recently * * @see self::getInteractiveChannelId() * @var int */ private $channel_id_last_interactive = 0; /** * Packet Size * * Maximum packet size indexed by channel * * @see self::send_channel_packet() * @var array */ private $packet_size_client_to_server = []; /** * Message Number Log * * @see self::getLog() * @var array */ private $message_number_log = []; /** * Message Log * * @see self::getLog() * @var array */ private $message_log = []; /** * The Window Size * * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB) * * @var int * @see self::send_channel_packet() * @see self::exec() */ protected $window_size = 0x7FFFFFFF; /** * What we resize the window to * * When PuTTY resizes the window it doesn't add an additional 0x7FFFFFFF bytes - it adds 0x40000000 bytes. * Some SFTP clients (GoAnywhere) don't support adding 0x7FFFFFFF to the window size after the fact so * we'll just do what PuTTY does * * @var int * @see self::_send_channel_packet() * @see self::exec() */ private $window_resize = 0x40000000; /** * Window size, server to client * * Window size indexed by channel * * @see self::send_channel_packet() * @var array */ protected $window_size_server_to_client = []; /** * Window size, client to server * * Window size indexed by channel * * @see self::get_channel_packet() * @var array */ private $window_size_client_to_server = []; /** * Server signature * * Verified against $this->session_id * * @see self::getServerPublicHostKey() * @var string */ private $signature = ''; /** * Server signature format * * ssh-rsa or ssh-dss. * * @see self::getServerPublicHostKey() * @var string */ private $signature_format = ''; /** * Interactive Buffer * * @see self::read() * @var string */ private $interactiveBuffer = ''; /** * Current log size * * Should never exceed self::LOG_MAX_SIZE * * @see self::_send_binary_packet() * @see self::_get_binary_packet() * @var int */ private $log_size; /** * Timeout * * @see self::setTimeout() */ protected $timeout; /** * Current Timeout * * @see self::get_channel_packet() */ protected $curTimeout; /** * Keep Alive Interval * * @see self::setKeepAlive() */ private $keepAlive; /** * Real-time log file pointer * * @see self::_append_log() * @var resource|closed-resource */ private $realtime_log_file; /** * Real-time log file size * * @see self::_append_log() * @var int */ private $realtime_log_size; /** * Has the signature been validated? * * @see self::getServerPublicHostKey() * @var bool */ private $signature_validated = false; /** * Real-time log file wrap boolean * * @see self::_append_log() * @var bool */ private $realtime_log_wrap; /** * Flag to suppress stderr from output * * @see self::enableQuietMode() */ private $quiet_mode = false; /** * Time of last read/write network activity * * @var float */ private $last_packet = null; /** * Exit status returned from ssh if any * * @var int */ private $exit_status; /** * Flag to request a PTY when using exec() * * @var bool * @see self::enablePTY() */ private $request_pty = false; /** * Contents of stdError * * @var string */ private $stdErrorLog; /** * The Last Interactive Response * * @see self::_keyboard_interactive_process() * @var string */ private $last_interactive_response = ''; /** * Keyboard Interactive Request / Responses * * @see self::_keyboard_interactive_process() * @var array */ private $keyboard_requests_responses = []; /** * Banner Message * * Quoting from the RFC, "in some jurisdictions, sending a warning message before * authentication may be relevant for getting legal protection." * * @see self::_filter() * @see self::getBannerMessage() * @var string */ private $banner_message = ''; /** * Did read() timeout or return normally? * * @see self::isTimeout() * @var bool */ protected $is_timeout = false; /** * Log Boundary * * @see self::_format_log() * @var string */ private $log_boundary = ':'; /** * Log Long Width * * @see self::_format_log() * @var int */ private $log_long_width = 65; /** * Log Short Width * * @see self::_format_log() * @var int */ private $log_short_width = 16; /** * Hostname * * @see self::__construct() * @see self::_connect() * @var string */ private $host; /** * Port Number * * @see self::__construct() * @see self::_connect() * @var int */ private $port; /** * Number of columns for terminal window size * * @see self::getWindowColumns() * @see self::setWindowColumns() * @see self::setWindowSize() * @var int */ private $windowColumns = 80; /** * Number of columns for terminal window size * * @see self::getWindowRows() * @see self::setWindowRows() * @see self::setWindowSize() * @var int */ private $windowRows = 24; /** * Crypto Engine * * @see self::setCryptoEngine() * @see self::_key_exchange() * @var int */ private static $crypto_engine = false; /** * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario * * @var Agent */ private $agent; /** * Connection storage to replicates ssh2 extension functionality: * {@link http://php.net/manual/en/wrappers.ssh2.php#refsect1-wrappers.ssh2-examples} * * @var array> */ private static $connections; /** * Send the identification string first? * * @var bool */ private $send_id_string_first = true; /** * Send the key exchange initiation packet first? * * @var bool */ private $send_kex_first = true; /** * Some versions of OpenSSH incorrectly calculate the key size * * @var bool */ private $bad_key_size_fix = false; /** * Should we try to re-connect to re-establish keys? * * @var bool */ private $login_credentials_finalized = false; /** * Binary Packet Buffer * * @var object|null */ private $binary_packet_buffer = null; /** * Preferred Signature Format * * @var string|false */ protected $preferred_signature_format = false; /** * Authentication Credentials * * @var array */ protected $auth = []; /** * Terminal * * @var string */ private $term = 'vt100'; /** * The authentication methods that may productively continue authentication. * * @see https://tools.ietf.org/html/rfc4252#section-5.1 * @var array|null */ private $auth_methods_to_continue = null; /** * Compression method * * @var int */ private $compress = self::NET_SSH2_COMPRESSION_NONE; /** * Decompression method * * @var int */ private $decompress = self::NET_SSH2_COMPRESSION_NONE; /** * Compression context * * @var resource|false|null */ private $compress_context; /** * Decompression context * * @var resource|object */ private $decompress_context; /** * Regenerate Compression Context * * @var bool */ private $regenerate_compression_context = false; /** * Regenerate Decompression Context * * @var bool */ private $regenerate_decompression_context = false; /** * Smart multi-factor authentication flag * * @var bool */ private $smartMFA = true; /** * How many channels are currently opened * * @var int */ private $channelCount = 0; /** * Does the server support multiple channels? If not then error out * when multiple channels are attempted to be opened * * @var bool */ private $errorOnMultipleChannels; /** * Bytes Transferred Since Last Key Exchange * * Includes outbound and inbound totals * * @var int */ private $bytesTransferredSinceLastKEX = 0; /** * After how many transferred byte should phpseclib initiate a key re-exchange? * * @var int */ private $doKeyReexchangeAfterXBytes = 1024 * 1024 * 1024; /** * Has a key re-exchange been initialized? * * @var bool * @access private */ private $keyExchangeInProgress = false; /** * KEX Buffer * * If we're in the middle of a key exchange we want to buffer any additional packets we get until * the key exchange is over * * @see self::_get_binary_packet() * @see self::_key_exchange() * @see self::exec() * @var array * @access private */ private $kex_buffer = []; /** * Strict KEX Flag * * If kex-strict-s-v00@openssh.com is present in the first KEX packet it need not * be present in subsequent packet * * @see self::_key_exchange() * @see self::exec() * @var array * @access private */ private $strict_kex_flag = false; /** * Default Constructor. * * $host can either be a string, representing the host, or a stream resource. * If $host is a stream resource then $port doesn't do anything, altho $timeout * still will be used * * @param mixed $host * @param int $port * @param int $timeout * @see self::login() */ public function __construct($host, $port = 22, $timeout = 10) { if (empty(self::$message_numbers)) { self::$message_numbers = [ 1 => 'NET_SSH2_MSG_DISCONNECT', 2 => 'NET_SSH2_MSG_IGNORE', 3 => 'NET_SSH2_MSG_UNIMPLEMENTED', 4 => 'NET_SSH2_MSG_DEBUG', 5 => 'NET_SSH2_MSG_SERVICE_REQUEST', 6 => 'NET_SSH2_MSG_SERVICE_ACCEPT', 7 => 'NET_SSH2_MSG_EXT_INFO', // RFC 8308 20 => 'NET_SSH2_MSG_KEXINIT', 21 => 'NET_SSH2_MSG_NEWKEYS', 30 => 'NET_SSH2_MSG_KEXDH_INIT', 31 => 'NET_SSH2_MSG_KEXDH_REPLY', 50 => 'NET_SSH2_MSG_USERAUTH_REQUEST', 51 => 'NET_SSH2_MSG_USERAUTH_FAILURE', 52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS', 53 => 'NET_SSH2_MSG_USERAUTH_BANNER', 80 => 'NET_SSH2_MSG_GLOBAL_REQUEST', 81 => 'NET_SSH2_MSG_REQUEST_SUCCESS', 82 => 'NET_SSH2_MSG_REQUEST_FAILURE', 90 => 'NET_SSH2_MSG_CHANNEL_OPEN', 91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION', 92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE', 93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST', 94 => 'NET_SSH2_MSG_CHANNEL_DATA', 95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA', 96 => 'NET_SSH2_MSG_CHANNEL_EOF', 97 => 'NET_SSH2_MSG_CHANNEL_CLOSE', 98 => 'NET_SSH2_MSG_CHANNEL_REQUEST', 99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS', 100 => 'NET_SSH2_MSG_CHANNEL_FAILURE' ]; self::$disconnect_reasons = [ 1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT', 2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR', 3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED', 4 => 'NET_SSH2_DISCONNECT_RESERVED', 5 => 'NET_SSH2_DISCONNECT_MAC_ERROR', 6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR', 7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE', 8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED', 9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE', 10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST', 11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION', 12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS', 13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER', 14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE', 15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME' ]; self::$channel_open_failure_reasons = [ 1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED' ]; self::$terminal_modes = [ 0 => 'NET_SSH2_TTY_OP_END' ]; self::$channel_extended_data_type_codes = [ 1 => 'NET_SSH2_EXTENDED_DATA_STDERR' ]; self::define_array( self::$message_numbers, self::$disconnect_reasons, self::$channel_open_failure_reasons, self::$terminal_modes, self::$channel_extended_data_type_codes, [60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'], [60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'], [60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', 61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'], // RFC 4419 - diffie-hellman-group-exchange-sha{1,256} [30 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST_OLD', 31 => 'NET_SSH2_MSG_KEXDH_GEX_GROUP', 32 => 'NET_SSH2_MSG_KEXDH_GEX_INIT', 33 => 'NET_SSH2_MSG_KEXDH_GEX_REPLY', 34 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'], // RFC 5656 - Elliptic Curves (for curve25519-sha256@libssh.org) [30 => 'NET_SSH2_MSG_KEX_ECDH_INIT', 31 => 'NET_SSH2_MSG_KEX_ECDH_REPLY'] ); } /** * Typehint is required due to a bug in Psalm: https://github.com/vimeo/psalm/issues/7508 * @var \WeakReference|SSH2 */ self::$connections[$this->getResourceId()] = class_exists('WeakReference') ? \WeakReference::create($this) : $this; $this->timeout = $timeout; if (is_resource($host)) { $this->fsock = $host; return; } if (Strings::is_stringable($host)) { $this->host = $host; $this->port = $port; } } /** * Set Crypto Engine Mode * * Possible $engine values: * OpenSSL, mcrypt, Eval, PHP * * @param int $engine */ public static function setCryptoEngine($engine) { self::$crypto_engine = $engine; } /** * Send Identification String First * * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, * both sides MUST send an identification string". It does not say which side sends it first. In * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * */ public function sendIdentificationStringFirst() { $this->send_id_string_first = true; } /** * Send Identification String Last * * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, * both sides MUST send an identification string". It does not say which side sends it first. In * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * */ public function sendIdentificationStringLast() { $this->send_id_string_first = false; } /** * Send SSH_MSG_KEXINIT First * * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * */ public function sendKEXINITFirst() { $this->send_kex_first = true; } /** * Send SSH_MSG_KEXINIT Last * * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * */ public function sendKEXINITLast() { $this->send_kex_first = false; } /** * stream_select wrapper * * Quoting https://stackoverflow.com/a/14262151/569976, * "The general approach to `EINTR` is to simply handle the error and retry the operation again" * * This wrapper does that loop */ private static function stream_select(&$read, &$write, &$except, $seconds, $microseconds = null) { $remaining = $seconds + $microseconds / 1000000; $start = microtime(true); while (true) { $result = @stream_select($read, $write, $except, $seconds, $microseconds); if ($result !== false) { return $result; } $elapsed = microtime(true) - $start; $seconds = (int) ($remaining - floor($elapsed)); $microseconds = (int) (1000000 * ($remaining - $seconds)); if ($elapsed >= $remaining) { return false; } } } /** * Connect to an SSHv2 server * * @throws \UnexpectedValueException on receipt of unexpected packets * @throws \RuntimeException on other errors */ private function connect() { if ($this->bitmap & self::MASK_CONSTRUCTOR) { return; } $this->bitmap |= self::MASK_CONSTRUCTOR; $this->curTimeout = $this->timeout; if (!is_resource($this->fsock)) { $start = microtime(true); // with stream_select a timeout of 0 means that no timeout takes place; // with fsockopen a timeout of 0 means that you instantly timeout // to resolve this incompatibility a timeout of 100,000 will be used for fsockopen if timeout is 0 $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout == 0 ? 100000 : $this->curTimeout); if (!$this->fsock) { $host = $this->host . ':' . $this->port; throw new UnableToConnectException(rtrim("Cannot connect to $host. Error $errno. $errstr")); } $elapsed = microtime(true) - $start; if ($this->curTimeout) { $this->curTimeout -= $elapsed; if ($this->curTimeout < 0) { throw new \RuntimeException('Connection timed out whilst attempting to open socket connection'); } } if (defined('NET_SSH2_LOGGING')) { $this->append_log('(fsockopen took ' . round($elapsed, 4) . 's)', ''); } } $this->identifier = $this->generate_identifier(); if ($this->send_id_string_first) { $start = microtime(true); fputs($this->fsock, $this->identifier . "\r\n"); $elapsed = round(microtime(true) - $start, 4); if (defined('NET_SSH2_LOGGING')) { $this->append_log("-> (network: $elapsed)", $this->identifier . "\r\n"); } } /* According to the SSH2 specs, "The server MAY send other lines of data before sending the version string. Each line SHOULD be terminated by a Carriage Return and Line Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients MUST be able to process such lines." */ $data = ''; $totalElapsed = 0; while (!feof($this->fsock) && !preg_match('#(.*)^(SSH-(\d\.\d+).*)#ms', $data, $matches)) { $line = ''; while (true) { if ($this->curTimeout) { if ($this->curTimeout < 0) { throw new \RuntimeException('Connection timed out whilst receiving server identification string'); } $read = [$this->fsock]; $write = $except = null; $start = microtime(true); $sec = (int) floor($this->curTimeout); $usec = (int) (1000000 * ($this->curTimeout - $sec)); if (static::stream_select($read, $write, $except, $sec, $usec) === false) { throw new \RuntimeException('Connection timed out whilst receiving server identification string'); } $elapsed = microtime(true) - $start; $totalElapsed += $elapsed; $this->curTimeout -= $elapsed; } $temp = stream_get_line($this->fsock, 255, "\n"); if ($temp === false) { throw new \RuntimeException('Error reading SSH identification string; are you sure you\'re connecting to an SSH server?'); } $line .= $temp; if (strlen($temp) == 255) { continue; } $line .= "\n"; break; } $data .= $line; } if (defined('NET_SSH2_LOGGING')) { $this->append_log('<- (network: ' . round($totalElapsed, 4) . ')', $line); } if (feof($this->fsock)) { $this->bitmap = 0; throw new ConnectionClosedException('Connection closed by server; are you sure you\'re connected to an SSH server?'); } $extra = $matches[1]; $this->server_identifier = trim($data, "\r\n"); if (strlen($extra)) { $this->errors[] = $data; } if (version_compare($matches[3], '1.99', '<')) { $this->bitmap = 0; throw new UnableToConnectException("Cannot connect to SSH $matches[3] servers"); } // Ubuntu's OpenSSH from 5.8 to 6.9 didn't work with multiple channels. see // https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/1334916 for more info. // https://lists.ubuntu.com/archives/oneiric-changes/2011-July/005772.html discusses // when consolekit was incorporated. // https://marc.info/?l=openssh-unix-dev&m=163409903417589&w=2 discusses some of the // issues with how Ubuntu incorporated consolekit $pattern = '#^SSH-2\.0-OpenSSH_([\d.]+)[^ ]* Ubuntu-.*$#'; $match = preg_match($pattern, $this->server_identifier, $matches); $match = $match && version_compare('5.8', $matches[1], '<='); $match = $match && version_compare('6.9', $matches[1], '>='); $this->errorOnMultipleChannels = $match; if (!$this->send_id_string_first) { $start = microtime(true); fputs($this->fsock, $this->identifier . "\r\n"); $elapsed = round(microtime(true) - $start, 4); if (defined('NET_SSH2_LOGGING')) { $this->append_log("-> (network: $elapsed)", $this->identifier . "\r\n"); } } $this->last_packet = microtime(true); if (!$this->send_kex_first) { $response = $this->get_binary_packet_or_close(NET_SSH2_MSG_KEXINIT); $this->key_exchange($response); } if ($this->send_kex_first) { $this->key_exchange(); } $this->bitmap |= self::MASK_CONNECTED; return true; } /** * Generates the SSH identifier * * You should overwrite this method in your own class if you want to use another identifier * * @return string */ private function generate_identifier() { $identifier = 'SSH-2.0-phpseclib_3.0'; $ext = []; if (extension_loaded('sodium')) { $ext[] = 'libsodium'; } if (extension_loaded('openssl')) { $ext[] = 'openssl'; } elseif (extension_loaded('mcrypt')) { $ext[] = 'mcrypt'; } if (extension_loaded('gmp')) { $ext[] = 'gmp'; } elseif (extension_loaded('bcmath')) { $ext[] = 'bcmath'; } if (!empty($ext)) { $identifier .= ' (' . implode(', ', $ext) . ')'; } return $identifier; } /** * Key Exchange * * @return bool * @param string|bool $kexinit_payload_server optional * @throws \UnexpectedValueException on receipt of unexpected packets * @throws \RuntimeException on other errors * @throws NoSupportedAlgorithmsException when none of the algorithms phpseclib has loaded are compatible */ private function key_exchange($kexinit_payload_server = false) { $this->bytesTransferredSinceLastKEX = 0; $preferred = $this->preferred; // for the initial key exchange $send_kex is true (no key re-exchange has been started) // for phpseclib initiated key exchanges $send_kex is false $send_kex = !$this->keyExchangeInProgress; $this->keyExchangeInProgress = true; $kex_algorithms = isset($preferred['kex']) ? $preferred['kex'] : SSH2::getSupportedKEXAlgorithms(); $server_host_key_algorithms = isset($preferred['hostkey']) ? $preferred['hostkey'] : SSH2::getSupportedHostKeyAlgorithms(); $s2c_encryption_algorithms = isset($preferred['server_to_client']['crypt']) ? $preferred['server_to_client']['crypt'] : SSH2::getSupportedEncryptionAlgorithms(); $c2s_encryption_algorithms = isset($preferred['client_to_server']['crypt']) ? $preferred['client_to_server']['crypt'] : SSH2::getSupportedEncryptionAlgorithms(); $s2c_mac_algorithms = isset($preferred['server_to_client']['mac']) ? $preferred['server_to_client']['mac'] : SSH2::getSupportedMACAlgorithms(); $c2s_mac_algorithms = isset($preferred['client_to_server']['mac']) ? $preferred['client_to_server']['mac'] : SSH2::getSupportedMACAlgorithms(); $s2c_compression_algorithms = isset($preferred['server_to_client']['comp']) ? $preferred['server_to_client']['comp'] : SSH2::getSupportedCompressionAlgorithms(); $c2s_compression_algorithms = isset($preferred['client_to_server']['comp']) ? $preferred['client_to_server']['comp'] : SSH2::getSupportedCompressionAlgorithms(); $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']); // some SSH servers have buggy implementations of some of the above algorithms switch (true) { case $this->server_identifier == 'SSH-2.0-SSHD': case substr($this->server_identifier, 0, 13) == 'SSH-2.0-DLINK': if (!isset($preferred['server_to_client']['mac'])) { $s2c_mac_algorithms = array_values(array_diff( $s2c_mac_algorithms, ['hmac-sha1-96', 'hmac-md5-96'] )); } if (!isset($preferred['client_to_server']['mac'])) { $c2s_mac_algorithms = array_values(array_diff( $c2s_mac_algorithms, ['hmac-sha1-96', 'hmac-md5-96'] )); } break; case substr($this->server_identifier, 0, 24) == 'SSH-2.0-TurboFTP_SERVER_': if (!isset($preferred['server_to_client']['crypt'])) { $s2c_encryption_algorithms = array_values(array_diff( $s2c_encryption_algorithms, ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com'] )); } if (!isset($preferred['client_to_server']['crypt'])) { $c2s_encryption_algorithms = array_values(array_diff( $c2s_encryption_algorithms, ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com'] )); } } $client_cookie = Random::string(16); $kexinit_payload_client = pack('Ca*', NET_SSH2_MSG_KEXINIT, $client_cookie); $kexinit_payload_client .= Strings::packSSH2( 'L10bN', $kex_algorithms, $server_host_key_algorithms, $c2s_encryption_algorithms, $s2c_encryption_algorithms, $c2s_mac_algorithms, $s2c_mac_algorithms, $c2s_compression_algorithms, $s2c_compression_algorithms, [], // language, client to server [], // language, server to client false, // first_kex_packet_follows 0 // reserved for future extension ); if ($kexinit_payload_server === false && $send_kex) { $this->send_binary_packet($kexinit_payload_client); while (true) { $kexinit_payload_server = $this->get_binary_packet(); switch (ord($kexinit_payload_server[0])) { case NET_SSH2_MSG_KEXINIT: break 2; case NET_SSH2_MSG_DISCONNECT: return $this->handleDisconnect($kexinit_payload_server); } $this->kex_buffer[] = $kexinit_payload_server; } $send_kex = false; } $response = $kexinit_payload_server; Strings::shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT) $server_cookie = Strings::shift($response, 16); list( $this->kex_algorithms, $this->server_host_key_algorithms, $this->encryption_algorithms_client_to_server, $this->encryption_algorithms_server_to_client, $this->mac_algorithms_client_to_server, $this->mac_algorithms_server_to_client, $this->compression_algorithms_client_to_server, $this->compression_algorithms_server_to_client, $this->languages_client_to_server, $this->languages_server_to_client, $first_kex_packet_follows ) = Strings::unpackSSH2('L10C', $response); if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { if ($this->session_id === false) { // [kex-strict-s-v00@openssh.com is] only valid in the initial SSH2_MSG_KEXINIT and MUST be ignored // if [it is] present in subsequent SSH2_MSG_KEXINIT packets $this->strict_kex_flag = true; if (count($this->kex_buffer)) { throw new \UnexpectedValueException('Possible Terrapin Attack detected'); } } } $this->supported_private_key_algorithms = $this->server_host_key_algorithms; if ($send_kex) { $this->send_binary_packet($kexinit_payload_client); } // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange // we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the // diffie-hellman key exchange as fast as possible $decrypt = self::array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client); if (!$decrypt || ($decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt)) === null) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found'); } $encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server); if (!$encrypt || ($encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt)) === null) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible client to server encryption algorithms found'); } // through diffie-hellman key exchange a symmetric key is obtained $this->kex_algorithm = self::array_intersect_first($kex_algorithms, $this->kex_algorithms); if ($this->kex_algorithm === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible key exchange algorithms found'); } $server_host_key_algorithm = self::array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms); if ($server_host_key_algorithm === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible server host key algorithms found'); } $mac_algorithm_out = self::array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server); if ($mac_algorithm_out === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible client to server message authentication algorithms found'); } $mac_algorithm_in = self::array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client); if ($mac_algorithm_in === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible server to client message authentication algorithms found'); } $compression_map = [ 'none' => self::NET_SSH2_COMPRESSION_NONE, 'zlib' => self::NET_SSH2_COMPRESSION_ZLIB, 'zlib@openssh.com' => self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH ]; $compression_algorithm_in = self::array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client); if ($compression_algorithm_in === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible server to client compression algorithms found'); } $this->decompress = $compression_map[$compression_algorithm_in]; $compression_algorithm_out = self::array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server); if ($compression_algorithm_out === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new NoSupportedAlgorithmsException('No compatible client to server compression algorithms found'); } $this->compress = $compression_map[$compression_algorithm_out]; switch ($this->kex_algorithm) { case 'diffie-hellman-group15-sha512': case 'diffie-hellman-group16-sha512': case 'diffie-hellman-group17-sha512': case 'diffie-hellman-group18-sha512': case 'ecdh-sha2-nistp521': $kexHash = new Hash('sha512'); break; case 'ecdh-sha2-nistp384': $kexHash = new Hash('sha384'); break; case 'diffie-hellman-group-exchange-sha256': case 'diffie-hellman-group14-sha256': case 'ecdh-sha2-nistp256': case 'curve25519-sha256@libssh.org': case 'curve25519-sha256': $kexHash = new Hash('sha256'); break; default: $kexHash = new Hash('sha1'); } // Only relevant in diffie-hellman-group-exchange-sha{1,256}, otherwise empty. $exchange_hash_rfc4419 = ''; if (strpos($this->kex_algorithm, 'curve25519-sha256') === 0 || strpos($this->kex_algorithm, 'ecdh-sha2-nistp') === 0) { $curve = strpos($this->kex_algorithm, 'curve25519-sha256') === 0 ? 'Curve25519' : substr($this->kex_algorithm, 10); $ourPrivate = EC::createKey($curve); $ourPublicBytes = $ourPrivate->getPublicKey()->getEncodedCoordinates(); $clientKexInitMessage = 'NET_SSH2_MSG_KEX_ECDH_INIT'; $serverKexReplyMessage = 'NET_SSH2_MSG_KEX_ECDH_REPLY'; } else { if (strpos($this->kex_algorithm, 'diffie-hellman-group-exchange') === 0) { $dh_group_sizes_packed = pack( 'NNN', $this->kex_dh_group_size_min, $this->kex_dh_group_size_preferred, $this->kex_dh_group_size_max ); $packet = pack( 'Ca*', NET_SSH2_MSG_KEXDH_GEX_REQUEST, $dh_group_sizes_packed ); $this->send_binary_packet($packet); $this->updateLogHistory('UNKNOWN (34)', 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'); $response = $this->get_binary_packet_or_close(NET_SSH2_MSG_KEXDH_GEX_GROUP); list($type, $primeBytes, $gBytes) = Strings::unpackSSH2('Css', $response); $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEXDH_GEX_GROUP'); $prime = new BigInteger($primeBytes, -256); $g = new BigInteger($gBytes, -256); $exchange_hash_rfc4419 = $dh_group_sizes_packed . Strings::packSSH2( 'ss', $primeBytes, $gBytes ); $params = DH::createParameters($prime, $g); $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_GEX_INIT'; $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_GEX_REPLY'; } else { $params = DH::createParameters($this->kex_algorithm); $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_INIT'; $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_REPLY'; } $keyLength = min($kexHash->getLengthInBytes(), max($encryptKeyLength, $decryptKeyLength)); $ourPrivate = DH::createKey($params, 16 * $keyLength); // 2 * 8 * $keyLength $ourPublic = $ourPrivate->getPublicKey()->toBigInteger(); $ourPublicBytes = $ourPublic->toBytes(true); } $data = pack('CNa*', constant($clientKexInitMessage), strlen($ourPublicBytes), $ourPublicBytes); $this->send_binary_packet($data); switch ($clientKexInitMessage) { case 'NET_SSH2_MSG_KEX_ECDH_INIT': $this->updateLogHistory('NET_SSH2_MSG_KEXDH_INIT', 'NET_SSH2_MSG_KEX_ECDH_INIT'); break; case 'NET_SSH2_MSG_KEXDH_GEX_INIT': $this->updateLogHistory('UNKNOWN (32)', 'NET_SSH2_MSG_KEXDH_GEX_INIT'); } $response = $this->get_binary_packet_or_close(constant($serverKexReplyMessage)); list( $type, $server_public_host_key, $theirPublicBytes, $this->signature ) = Strings::unpackSSH2('Csss', $response); switch ($serverKexReplyMessage) { case 'NET_SSH2_MSG_KEX_ECDH_REPLY': $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEX_ECDH_REPLY'); break; case 'NET_SSH2_MSG_KEXDH_GEX_REPLY': $this->updateLogHistory('UNKNOWN (33)', 'NET_SSH2_MSG_KEXDH_GEX_REPLY'); } $this->server_public_host_key = $server_public_host_key; list($public_key_format) = Strings::unpackSSH2('s', $server_public_host_key); if (strlen($this->signature) < 4) { throw new \LengthException('The signature needs at least four bytes'); } $temp = unpack('Nlength', substr($this->signature, 0, 4)); $this->signature_format = substr($this->signature, 4, $temp['length']); $keyBytes = DH::computeSecret($ourPrivate, $theirPublicBytes); if (($keyBytes & "\xFF\x80") === "\x00\x00") { $keyBytes = substr($keyBytes, 1); } elseif (($keyBytes[0] & "\x80") === "\x80") { $keyBytes = "\0$keyBytes"; } $this->exchange_hash = Strings::packSSH2( 's5', $this->identifier, $this->server_identifier, $kexinit_payload_client, $kexinit_payload_server, $this->server_public_host_key ); $this->exchange_hash .= $exchange_hash_rfc4419; $this->exchange_hash .= Strings::packSSH2( 's3', $ourPublicBytes, $theirPublicBytes, $keyBytes ); $this->exchange_hash = $kexHash->hash($this->exchange_hash); if ($this->session_id === false) { $this->session_id = $this->exchange_hash; } switch ($server_host_key_algorithm) { case 'rsa-sha2-256': case 'rsa-sha2-512': //case 'ssh-rsa': $expected_key_format = 'ssh-rsa'; break; default: $expected_key_format = $server_host_key_algorithm; } if ($public_key_format != $expected_key_format || $this->signature_format != $server_host_key_algorithm) { switch (true) { case $this->signature_format == $server_host_key_algorithm: case $server_host_key_algorithm != 'rsa-sha2-256' && $server_host_key_algorithm != 'rsa-sha2-512': case $this->signature_format != 'ssh-rsa': $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); throw new \RuntimeException('Server Host Key Algorithm Mismatch (' . $this->signature_format . ' vs ' . $server_host_key_algorithm . ')'); } } $packet = pack('C', NET_SSH2_MSG_NEWKEYS); $this->send_binary_packet($packet); $this->get_binary_packet_or_close(NET_SSH2_MSG_NEWKEYS); $this->keyExchangeInProgress = false; if ($this->strict_kex_flag) { $this->get_seq_no = $this->send_seq_no = 0; } $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); $this->encrypt = self::encryption_algorithm_to_crypt_instance($encrypt); if ($this->encrypt) { if (self::$crypto_engine) { $this->encrypt->setPreferredEngine(self::$crypto_engine); } if ($this->encrypt->getBlockLengthInBytes()) { $this->encrypt_block_size = $this->encrypt->getBlockLengthInBytes(); } $this->encrypt->disablePadding(); if ($this->encrypt->usesIV()) { $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); while ($this->encrypt_block_size > strlen($iv)) { $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); } $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size)); } switch ($encrypt) { case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); $this->encryptFixedPart = substr($nonce, 0, 4); $this->encryptInvocationCounter = substr($nonce, 4, 8); // fall-through case 'chacha20-poly1305@openssh.com': break; default: $this->encrypt->enableContinuousBuffer(); } $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); while ($encryptKeyLength > strlen($key)) { $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } switch ($encrypt) { case 'chacha20-poly1305@openssh.com': $encryptKeyLength = 32; $this->lengthEncrypt = self::encryption_algorithm_to_crypt_instance($encrypt); $this->lengthEncrypt->setKey(substr($key, 32, 32)); } $this->encrypt->setKey(substr($key, 0, $encryptKeyLength)); $this->encryptName = $encrypt; } $this->decrypt = self::encryption_algorithm_to_crypt_instance($decrypt); if ($this->decrypt) { if (self::$crypto_engine) { $this->decrypt->setPreferredEngine(self::$crypto_engine); } if ($this->decrypt->getBlockLengthInBytes()) { $this->decrypt_block_size = $this->decrypt->getBlockLengthInBytes(); } $this->decrypt->disablePadding(); if ($this->decrypt->usesIV()) { $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); while ($this->decrypt_block_size > strlen($iv)) { $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); } $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); } switch ($decrypt) { case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': // see https://tools.ietf.org/html/rfc5647#section-7.1 $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); $this->decryptFixedPart = substr($nonce, 0, 4); $this->decryptInvocationCounter = substr($nonce, 4, 8); // fall-through case 'chacha20-poly1305@openssh.com': break; default: $this->decrypt->enableContinuousBuffer(); } $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id); while ($decryptKeyLength > strlen($key)) { $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } switch ($decrypt) { case 'chacha20-poly1305@openssh.com': $decryptKeyLength = 32; $this->lengthDecrypt = self::encryption_algorithm_to_crypt_instance($decrypt); $this->lengthDecrypt->setKey(substr($key, 32, 32)); } $this->decrypt->setKey(substr($key, 0, $decryptKeyLength)); $this->decryptName = $decrypt; } /* The "arcfour128" algorithm is the RC4 cipher, as described in [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream generated by the cipher MUST be discarded, and the first byte of the first encrypted packet MUST be encrypted using the 1537th byte of keystream. -- http://tools.ietf.org/html/rfc4345#section-4 */ if ($encrypt == 'arcfour128' || $encrypt == 'arcfour256') { $this->encrypt->encrypt(str_repeat("\0", 1536)); } if ($decrypt == 'arcfour128' || $decrypt == 'arcfour256') { $this->decrypt->decrypt(str_repeat("\0", 1536)); } if (!$this->encrypt->usesNonce()) { list($this->hmac_create, $createKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm_out); } else { $this->hmac_create = new \stdClass(); $this->hmac_create_name = $mac_algorithm_out; //$mac_algorithm_out = 'none'; $createKeyLength = 0; } if ($this->hmac_create instanceof Hash) { $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id); while ($createKeyLength > strlen($key)) { $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } $this->hmac_create->setKey(substr($key, 0, $createKeyLength)); $this->hmac_create_name = $mac_algorithm_out; $this->hmac_create_etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm_out); } if (!$this->decrypt->usesNonce()) { list($this->hmac_check, $checkKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm_in); $this->hmac_size = $this->hmac_check->getLengthInBytes(); } else { $this->hmac_check = new \stdClass(); $this->hmac_check_name = $mac_algorithm_in; //$mac_algorithm_in = 'none'; $checkKeyLength = 0; $this->hmac_size = 0; } if ($this->hmac_check instanceof Hash) { $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id); while ($checkKeyLength > strlen($key)) { $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } $this->hmac_check->setKey(substr($key, 0, $checkKeyLength)); $this->hmac_check_name = $mac_algorithm_in; $this->hmac_check_etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm_in); } $this->regenerate_compression_context = $this->regenerate_decompression_context = true; return true; } /** * Maps an encryption algorithm name to the number of key bytes. * * @param string $algorithm Name of the encryption algorithm * @return int|null Number of bytes as an integer or null for unknown */ private function encryption_algorithm_to_key_size($algorithm) { if ($this->bad_key_size_fix && self::bad_algorithm_candidate($algorithm)) { return 16; } switch ($algorithm) { case 'none': return 0; case 'aes128-gcm@openssh.com': case 'aes128-cbc': case 'aes128-ctr': case 'arcfour': case 'arcfour128': case 'blowfish-cbc': case 'blowfish-ctr': case 'twofish128-cbc': case 'twofish128-ctr': return 16; case '3des-cbc': case '3des-ctr': case 'aes192-cbc': case 'aes192-ctr': case 'twofish192-cbc': case 'twofish192-ctr': return 24; case 'aes256-gcm@openssh.com': case 'aes256-cbc': case 'aes256-ctr': case 'arcfour256': case 'twofish-cbc': case 'twofish256-cbc': case 'twofish256-ctr': return 32; case 'chacha20-poly1305@openssh.com': return 64; } return null; } /** * Maps an encryption algorithm name to an instance of a subclass of * \phpseclib3\Crypt\Common\SymmetricKey. * * @param string $algorithm Name of the encryption algorithm * @return SymmetricKey|null */ private static function encryption_algorithm_to_crypt_instance($algorithm) { switch ($algorithm) { case '3des-cbc': return new TripleDES('cbc'); case '3des-ctr': return new TripleDES('ctr'); case 'aes256-cbc': case 'aes192-cbc': case 'aes128-cbc': return new Rijndael('cbc'); case 'aes256-ctr': case 'aes192-ctr': case 'aes128-ctr': return new Rijndael('ctr'); case 'blowfish-cbc': return new Blowfish('cbc'); case 'blowfish-ctr': return new Blowfish('ctr'); case 'twofish128-cbc': case 'twofish192-cbc': case 'twofish256-cbc': case 'twofish-cbc': return new Twofish('cbc'); case 'twofish128-ctr': case 'twofish192-ctr': case 'twofish256-ctr': return new Twofish('ctr'); case 'arcfour': case 'arcfour128': case 'arcfour256': return new RC4(); case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': return new Rijndael('gcm'); case 'chacha20-poly1305@openssh.com': return new ChaCha20(); } return null; } /** * Maps an encryption algorithm name to an instance of a subclass of * \phpseclib3\Crypt\Hash. * * @param string $algorithm Name of the encryption algorithm * @return array{Hash, int}|null */ private static function mac_algorithm_to_hash_instance($algorithm) { switch ($algorithm) { case 'umac-64@openssh.com': case 'umac-64-etm@openssh.com': return [new Hash('umac-64'), 16]; case 'umac-128@openssh.com': case 'umac-128-etm@openssh.com': return [new Hash('umac-128'), 16]; case 'hmac-sha2-512': case 'hmac-sha2-512-etm@openssh.com': return [new Hash('sha512'), 64]; case 'hmac-sha2-256': case 'hmac-sha2-256-etm@openssh.com': return [new Hash('sha256'), 32]; case 'hmac-sha1': case 'hmac-sha1-etm@openssh.com': return [new Hash('sha1'), 20]; case 'hmac-sha1-96': return [new Hash('sha1-96'), 20]; case 'hmac-md5': return [new Hash('md5'), 16]; case 'hmac-md5-96': return [new Hash('md5-96'), 16]; } } /** * Tests whether or not proposed algorithm has a potential for issues * * @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291 * @param string $algorithm Name of the encryption algorithm * @return bool */ private static function bad_algorithm_candidate($algorithm) { switch ($algorithm) { case 'arcfour256': case 'aes192-ctr': case 'aes256-ctr': return true; } return false; } /** * Login * * The $password parameter can be a plaintext password, a \phpseclib3\Crypt\RSA|EC|DSA object, a \phpseclib3\System\SSH\Agent object or an array * * @param string $username * @param string|PrivateKey|array[]|Agent|null ...$args * @return bool * @see self::_login() */ public function login($username, ...$args) { if (!$this->login_credentials_finalized) { $this->auth[] = func_get_args(); } // try logging with 'none' as an authentication method first since that's what // PuTTY does if (substr($this->server_identifier, 0, 15) != 'SSH-2.0-CoreFTP' && $this->auth_methods_to_continue === null) { if ($this->sublogin($username)) { return true; } if (!count($args)) { return false; } } return $this->sublogin($username, ...$args); } /** * Login Helper * * @param string $username * @param string|PrivateKey|array[]|Agent|null ...$args * @return bool * @see self::_login_helper() */ protected function sublogin($username, ...$args) { if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { $this->connect(); } if (empty($args)) { return $this->login_helper($username); } foreach ($args as $arg) { switch (true) { case $arg instanceof PublicKey: throw new \UnexpectedValueException('A PublicKey object was passed to the login method instead of a PrivateKey object'); case $arg instanceof PrivateKey: case $arg instanceof Agent: case is_array($arg): case Strings::is_stringable($arg): break; default: throw new \UnexpectedValueException('$password needs to either be an instance of \phpseclib3\Crypt\Common\PrivateKey, \System\SSH\Agent, an array or a string'); } } while (count($args)) { if (!$this->auth_methods_to_continue || !$this->smartMFA) { $newargs = $args; $args = []; } else { $newargs = []; foreach ($this->auth_methods_to_continue as $method) { switch ($method) { case 'publickey': foreach ($args as $key => $arg) { if ($arg instanceof PrivateKey || $arg instanceof Agent) { $newargs[] = $arg; unset($args[$key]); break; } } break; case 'keyboard-interactive': $hasArray = $hasString = false; foreach ($args as $arg) { if ($hasArray || is_array($arg)) { $hasArray = true; break; } if ($hasString || Strings::is_stringable($arg)) { $hasString = true; break; } } if ($hasArray && $hasString) { foreach ($args as $key => $arg) { if (is_array($arg)) { $newargs[] = $arg; break 2; } } } // fall-through case 'password': foreach ($args as $key => $arg) { $newargs[] = $arg; unset($args[$key]); break; } } } } if (!count($newargs)) { return false; } foreach ($newargs as $arg) { if ($this->login_helper($username, $arg)) { $this->login_credentials_finalized = true; return true; } } } return false; } /** * Login Helper * * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} * by sending dummy SSH_MSG_IGNORE messages.} * * @param string $username * @param string|AsymmetricKey|array[]|Agent|null ...$args * @return bool * @throws \UnexpectedValueException on receipt of unexpected packets * @throws \RuntimeException on other errors */ private function login_helper($username, $password = null) { if (!($this->bitmap & self::MASK_CONNECTED)) { return false; } if (!($this->bitmap & self::MASK_LOGIN_REQ)) { $packet = Strings::packSSH2('Cs', NET_SSH2_MSG_SERVICE_REQUEST, 'ssh-userauth'); $this->send_binary_packet($packet); try { $response = $this->get_binary_packet_or_close(NET_SSH2_MSG_SERVICE_ACCEPT); } catch (InvalidPacketLengthException $e) { // the first opportunity to encounter the "bad key size" error if (!$this->bad_key_size_fix && $this->decryptName != null && self::bad_algorithm_candidate($this->decryptName)) { // bad_key_size_fix is only ever re-assigned to true here // retry the connection with that new setting but we'll // only try it once. $this->bad_key_size_fix = true; return $this->reconnect(); } throw $e; } list($type) = Strings::unpackSSH2('C', $response); list($service) = Strings::unpackSSH2('s', $response); if ($service != 'ssh-userauth') { $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); throw new \UnexpectedValueException('Expected SSH_MSG_SERVICE_ACCEPT'); } $this->bitmap |= self::MASK_LOGIN_REQ; } if (strlen($this->last_interactive_response)) { return !Strings::is_stringable($password) && !is_array($password) ? false : $this->keyboard_interactive_process($password); } if ($password instanceof PrivateKey) { return $this->privatekey_login($username, $password); } if ($password instanceof Agent) { return $this->ssh_agent_login($username, $password); } if (is_array($password)) { if ($this->keyboard_interactive_login($username, $password)) { $this->bitmap |= self::MASK_LOGIN; return true; } return false; } if (!isset($password)) { $packet = Strings::packSSH2( 'Cs3', NET_SSH2_MSG_USERAUTH_REQUEST, $username, 'ssh-connection', 'none' ); $this->send_binary_packet($packet); $response = $this->get_binary_packet_or_close(); list($type) = Strings::unpackSSH2('C', $response); switch ($type) { case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= self::MASK_LOGIN; return true; case NET_SSH2_MSG_USERAUTH_FAILURE: list($auth_methods) = Strings::unpackSSH2('L', $response); $this->auth_methods_to_continue = $auth_methods; // fall-through default: return false; } } $packet = Strings::packSSH2( 'Cs3bs', NET_SSH2_MSG_USERAUTH_REQUEST, $username, 'ssh-connection', 'password', false, $password ); // remove the username and password from the logged packet if (!defined('NET_SSH2_LOGGING')) { $logged = null; } else { $logged = Strings::packSSH2( 'Cs3bs', NET_SSH2_MSG_USERAUTH_REQUEST, $username, 'ssh-connection', 'password', false, 'password' ); } $this->send_binary_packet($packet, $logged); $response = $this->get_binary_packet_or_close(); list($type) = Strings::unpackSSH2('C', $response); switch ($type) { case NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'); list($message) = Strings::unpackSSH2('s', $response); $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $message; return $this->disconnect_helper(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER); case NET_SSH2_MSG_USERAUTH_FAILURE: // can we use keyboard-interactive authentication? if not then either the login is bad or the server employees // multi-factor authentication list($auth_methods, $partial_success) = Strings::unpackSSH2('Lb', $response); $this->auth_methods_to_continue = $auth_methods; if (!$partial_success && in_array('keyboard-interactive', $auth_methods)) { if ($this->keyboard_interactive_login($username, $password)) { $this->bitmap |= self::MASK_LOGIN; return true; } return false; } return false; case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= self::MASK_LOGIN; return true; } return false; } /** * Login via keyboard-interactive authentication * * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details. This is not a full-featured keyboard-interactive authenticator. * * @param string $username * @param string|array $password * @return bool */ private function keyboard_interactive_login($username, $password) { $packet = Strings::packSSH2( 'Cs5', NET_SSH2_MSG_USERAUTH_REQUEST, $username, 'ssh-connection', 'keyboard-interactive', '', // language tag '' // submethods ); $this->send_binary_packet($packet); return $this->keyboard_interactive_process($password); } /** * Handle the keyboard-interactive requests / responses. * * @param string|array ...$responses * @return bool * @throws \RuntimeException on connection error */ private function keyboard_interactive_process(...$responses) { if (strlen($this->last_interactive_response)) { $response = $this->last_interactive_response; } else { $orig = $response = $this->get_binary_packet_or_close(); } list($type) = Strings::unpackSSH2('C', $response); switch ($type) { case NET_SSH2_MSG_USERAUTH_INFO_REQUEST: list( , // name; may be empty , // instruction; may be empty , // language tag; may be empty $num_prompts ) = Strings::unpackSSH2('s3N', $response); for ($i = 0; $i < count($responses); $i++) { if (is_array($responses[$i])) { foreach ($responses[$i] as $key => $value) { $this->keyboard_requests_responses[$key] = $value; } unset($responses[$i]); } } $responses = array_values($responses); if (isset($this->keyboard_requests_responses)) { for ($i = 0; $i < $num_prompts; $i++) { list( $prompt, // prompt - ie. "Password: "; must not be empty // echo ) = Strings::unpackSSH2('sC', $response); foreach ($this->keyboard_requests_responses as $key => $value) { if (substr($prompt, 0, strlen($key)) == $key) { $responses[] = $value; break; } } } } // see http://tools.ietf.org/html/rfc4256#section-3.2 if (strlen($this->last_interactive_response)) { $this->last_interactive_response = ''; } else { $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST'); } if (!count($responses) && $num_prompts) { $this->last_interactive_response = $orig; return false; } /* After obtaining the requested information from the user, the client MUST respond with an SSH_MSG_USERAUTH_INFO_RESPONSE message. */ // see http://tools.ietf.org/html/rfc4256#section-3.4 $packet = $logged = pack('CN', NET_SSH2_MSG_USERAUTH_INFO_RESPONSE, count($responses)); for ($i = 0; $i < count($responses); $i++) { $packet .= Strings::packSSH2('s', $responses[$i]); $logged .= Strings::packSSH2('s', 'dummy-answer'); } $this->send_binary_packet($packet, $logged); $this->updateLogHistory('UNKNOWN (61)', 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'); /* After receiving the response, the server MUST send either an SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE, or another SSH_MSG_USERAUTH_INFO_REQUEST message. */ // maybe phpseclib should force close the connection after x request / responses? unless something like that is done // there could be an infinite loop of request / responses. return $this->keyboard_interactive_process(); case NET_SSH2_MSG_USERAUTH_SUCCESS: return true; case NET_SSH2_MSG_USERAUTH_FAILURE: list($auth_methods) = Strings::unpackSSH2('L', $response); $this->auth_methods_to_continue = $auth_methods; return false; } return false; } /** * Login with an ssh-agent provided key * * @param string $username * @param Agent $agent * @return bool */ private function ssh_agent_login($username, Agent $agent) { $this->agent = $agent; $keys = $agent->requestIdentities(); $orig_algorithms = $this->supported_private_key_algorithms; foreach ($keys as $key) { if ($this->privatekey_login($username, $key)) { return true; } $this->supported_private_key_algorithms = $orig_algorithms; } return false; } /** * Login with an RSA private key * * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} * by sending dummy SSH_MSG_IGNORE messages.} * * @param string $username * @param PrivateKey $privatekey * @return bool * @throws \RuntimeException on connection error */ private function privatekey_login($username, PrivateKey $privatekey) { $publickey = $privatekey->getPublicKey(); if ($publickey instanceof RSA) { $privatekey = $privatekey->withPadding(RSA::SIGNATURE_PKCS1); $algos = ['rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa']; if (isset($this->preferred['hostkey'])) { $algos = array_intersect($algos, $this->preferred['hostkey']); } $algo = self::array_intersect_first($algos, $this->supported_private_key_algorithms); switch ($algo) { case 'rsa-sha2-512': $hash = 'sha512'; $signatureType = 'rsa-sha2-512'; break; case 'rsa-sha2-256': $hash = 'sha256'; $signatureType = 'rsa-sha2-256'; break; //case 'ssh-rsa': default: $hash = 'sha1'; $signatureType = 'ssh-rsa'; } } elseif ($publickey instanceof EC) { $privatekey = $privatekey->withSignatureFormat('SSH2'); $curveName = $privatekey->getCurve(); switch ($curveName) { case 'Ed25519': $hash = 'sha512'; $signatureType = 'ssh-ed25519'; break; case 'secp256r1': // nistp256 $hash = 'sha256'; $signatureType = 'ecdsa-sha2-nistp256'; break; case 'secp384r1': // nistp384 $hash = 'sha384'; $signatureType = 'ecdsa-sha2-nistp384'; break; case 'secp521r1': // nistp521 $hash = 'sha512'; $signatureType = 'ecdsa-sha2-nistp521'; break; default: if (is_array($curveName)) { throw new UnsupportedCurveException('Specified Curves are not supported by SSH2'); } throw new UnsupportedCurveException('Named Curve of ' . $curveName . ' is not supported by phpseclib3\'s SSH2 implementation'); } } elseif ($publickey instanceof DSA) { $privatekey = $privatekey->withSignatureFormat('SSH2'); $hash = 'sha1'; $signatureType = 'ssh-dss'; } else { throw new UnsupportedAlgorithmException('Please use either an RSA key, an EC one or a DSA key'); } $publickeyStr = $publickey->toString('OpenSSH', ['binary' => true]); $part1 = Strings::packSSH2( 'Csss', NET_SSH2_MSG_USERAUTH_REQUEST, $username, 'ssh-connection', 'publickey' ); $part2 = Strings::packSSH2('ss', $signatureType, $publickeyStr); $packet = $part1 . chr(0) . $part2; $this->send_binary_packet($packet); $response = $this->get_binary_packet_or_close( NET_SSH2_MSG_USERAUTH_SUCCESS, NET_SSH2_MSG_USERAUTH_FAILURE, NET_SSH2_MSG_USERAUTH_PK_OK ); list($type) = Strings::unpackSSH2('C', $response); switch ($type) { case NET_SSH2_MSG_USERAUTH_FAILURE: list($auth_methods) = Strings::unpackSSH2('L', $response); if (in_array('publickey', $auth_methods) && substr($signatureType, 0, 9) == 'rsa-sha2-') { $this->supported_private_key_algorithms = array_diff($this->supported_private_key_algorithms, ['rsa-sha2-256', 'rsa-sha2-512']); return $this->privatekey_login($username, $privatekey); } $this->auth_methods_to_continue = $auth_methods; $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE'; return false; case NET_SSH2_MSG_USERAUTH_PK_OK: // we'll just take it on faith that the public key blob and the public key algorithm name are as // they should be $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PK_OK'); break; case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= self::MASK_LOGIN; return true; } $packet = $part1 . chr(1) . $part2; $privatekey = $privatekey->withHash($hash); $signature = $privatekey->sign(Strings::packSSH2('s', $this->session_id) . $packet); if ($publickey instanceof RSA) { $signature = Strings::packSSH2('ss', $signatureType, $signature); } $packet .= Strings::packSSH2('s', $signature); $this->send_binary_packet($packet); $response = $this->get_binary_packet_or_close( NET_SSH2_MSG_USERAUTH_SUCCESS, NET_SSH2_MSG_USERAUTH_FAILURE ); list($type) = Strings::unpackSSH2('C', $response); switch ($type) { case NET_SSH2_MSG_USERAUTH_FAILURE: // either the login is bad or the server employs multi-factor authentication list($auth_methods) = Strings::unpackSSH2('L', $response); $this->auth_methods_to_continue = $auth_methods; return false; case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= self::MASK_LOGIN; return true; } } /** * Return the currently configured timeout * * @return int */ public function getTimeout() { return $this->timeout; } /** * Set Timeout * * $ssh->exec('ping 127.0.0.1'); on a Linux host will never return and will run indefinitely. setTimeout() makes it so it'll timeout. * Setting $timeout to false or 0 will revert to the default socket timeout. * * @param mixed $timeout */ public function setTimeout($timeout) { $this->timeout = $this->curTimeout = $timeout; } /** * Set Keep Alive * * Sends an SSH2_MSG_IGNORE message every x seconds, if x is a positive non-zero number. * * @param int $interval */ public function setKeepAlive($interval) { $this->keepAlive = $interval; } /** * Get the output from stdError * */ public function getStdError() { return $this->stdErrorLog; } /** * Execute Command * * If $callback is set to false then \phpseclib3\Net\SSH2::get_channel_packet(self::CHANNEL_EXEC) will need to be called manually. * In all likelihood, this is not a feature you want to be taking advantage of. * * @param string $command * @param callable $callback * @return string|bool * @psalm-return ($callback is callable ? bool : string|bool) * @throws \RuntimeException on connection error */ public function exec($command, $callback = null) { $this->curTimeout = $this->timeout; $this->is_timeout = false; $this->stdErrorLog = ''; if (!$this->isAuthenticated()) { return false; } //if ($this->isPTYOpen()) { // throw new \RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.'); //} $this->open_channel(self::CHANNEL_EXEC); if ($this->request_pty === true) { $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); $packet = Strings::packSSH2( 'CNsCsN4s', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL_EXEC], 'pty-req', 1, $this->term, $this->windowColumns, $this->windowRows, 0, 0, $terminal_modes ); $this->send_binary_packet($packet); $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; if (!$this->get_channel_packet(self::CHANNEL_EXEC)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); throw new \RuntimeException('Unable to request pseudo-terminal'); } } // sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things // down. the one place where it might be desirable is if you're doing something like \phpseclib3\Net\SSH2::exec('ping localhost &'). // with a pty-req SSH_MSG_CHANNEL_REQUEST, exec() will return immediately and the ping process will then // then immediately terminate. without such a request exec() will loop indefinitely. the ping process won't end but // neither will your script. // although, in theory, the size of SSH_MSG_CHANNEL_REQUEST could exceed the maximum packet size established by // SSH_MSG_CHANNEL_OPEN_CONFIRMATION, RFC4254#section-5.1 states that the "maximum packet size" refers to the // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA. RFC4254#section-5.2 corroborates. $packet = Strings::packSSH2( 'CNsCs', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL_EXEC], 'exec', 1, $command ); $this->send_binary_packet($packet); $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; if (!$this->get_channel_packet(self::CHANNEL_EXEC)) { return false; } $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA; if ($this->request_pty === true) { $this->channel_id_last_interactive = self::CHANNEL_EXEC; return true; } $output = ''; while (true) { $temp = $this->get_channel_packet(self::CHANNEL_EXEC); switch (true) { case $temp === true: return is_callable($callback) ? true : $output; case $temp === false: return false; default: if (is_callable($callback)) { if ($callback($temp) === true) { $this->close_channel(self::CHANNEL_EXEC); return true; } } else { $output .= $temp; } } } } /** * How many channels are currently open? * * @return int */ public function getOpenChannelCount() { return $this->channelCount; } /** * Opens a channel * * @param string $channel * @param bool $skip_extended * @return bool */ protected function open_channel($channel, $skip_extended = false) { if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_CLOSE) { throw new \RuntimeException('Please close the channel (' . $channel . ') before trying to open it again'); } $this->channelCount++; if ($this->channelCount > 1 && $this->errorOnMultipleChannels) { throw new \RuntimeException("Ubuntu's OpenSSH from 5.8 to 6.9 doesn't work with multiple channels"); } // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to // be adjusted". 0x7FFFFFFF is, at 2GB, the max size. technically, it should probably be decremented, but, // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway. // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info $this->window_size_server_to_client[$channel] = $this->window_size; // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy // uses 0x4000, that's what will be used here, as well. $packet_size = 0x4000; $packet = Strings::packSSH2( 'CsN3', NET_SSH2_MSG_CHANNEL_OPEN, 'session', $channel, $this->window_size_server_to_client[$channel], $packet_size ); $this->send_binary_packet($packet); $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_OPEN; return $this->get_channel_packet($channel, $skip_extended); } /** * Creates an interactive shell * * Returns bool(true) if the shell was opened. * Returns bool(false) if the shell was already open. * * @see self::isShellOpen() * @see self::read() * @see self::write() * @return bool * @throws InsufficientSetupException if not authenticated * @throws \UnexpectedValueException on receipt of unexpected packets * @throws \RuntimeException on other errors */ public function openShell() { if (!$this->isAuthenticated()) { throw new InsufficientSetupException('Operation disallowed prior to login()'); } $this->open_channel(self::CHANNEL_SHELL); $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); $packet = Strings::packSSH2( 'CNsbsN4s', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL_SHELL], 'pty-req', true, // want reply $this->term, $this->windowColumns, $this->windowRows, 0, 0, $terminal_modes ); $this->send_binary_packet($packet); $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST; if (!$this->get_channel_packet(self::CHANNEL_SHELL)) { throw new \RuntimeException('Unable to request pty'); } $packet = Strings::packSSH2( 'CNsb', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL_SHELL], 'shell', true // want reply ); $this->send_binary_packet($packet); $response = $this->get_channel_packet(self::CHANNEL_SHELL); if ($response === false) { throw new \RuntimeException('Unable to request shell'); } $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA; $this->channel_id_last_interactive = self::CHANNEL_SHELL; $this->bitmap |= self::MASK_SHELL; return true; } /** * Return the channel to be used with read(), write(), and reset(), if none were specified * @deprecated for lack of transparency in intended channel target, to be potentially replaced * with method which guarantees open-ness of all yielded channels and throws * error for multiple open channels * @see self::read() * @see self::write() * @return int */ private function get_interactive_channel() { switch (true) { case $this->is_channel_status_data(self::CHANNEL_SUBSYSTEM): return self::CHANNEL_SUBSYSTEM; case $this->is_channel_status_data(self::CHANNEL_EXEC): return self::CHANNEL_EXEC; default: return self::CHANNEL_SHELL; } } /** * Indicates the DATA status on the given channel * * @param int $channel The channel number to evaluate * @return bool */ private function is_channel_status_data($channel) { return isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA; } /** * Return an available open channel * * @return int */ private function get_open_channel() { $channel = self::CHANNEL_EXEC; do { if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { return $channel; } } while ($channel++ < self::CHANNEL_SUBSYSTEM); return false; } /** * Request agent forwarding of remote server * * @return bool */ public function requestAgentForwarding() { $request_channel = $this->get_open_channel(); if ($request_channel === false) { return false; } $packet = Strings::packSSH2( 'CNsC', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[$request_channel], 'auth-agent-req@openssh.com', 1 ); $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST; $this->send_binary_packet($packet); if (!$this->get_channel_packet($request_channel)) { return false; } $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN; return true; } /** * Returns the output of an interactive shell * * Returns when there's a match for $expect, which can take the form of a string literal or, * if $mode == self::READ_REGEX, a regular expression. * * If not specifying a channel, an open interactive channel will be selected, or, if there are * no open channels, an interactive shell will be created. If there are multiple open * interactive channels, a legacy behavior will apply in which channel selection prioritizes * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive * channels, callers are discouraged from relying on this legacy behavior and should specify * the intended channel. * * @see self::write() * @param string $expect * @param int $mode One of the self::READ_* constants * @param int|null $channel Channel id returned by self::getInteractiveChannelId() * @return string|bool|null * @throws \RuntimeException on connection error * @throws InsufficientSetupException on unexpected channel status, possibly due to closure */ public function read($expect = '', $mode = self::READ_SIMPLE, $channel = null) { if (!$this->isAuthenticated()) { throw new InsufficientSetupException('Operation disallowed prior to login()'); } $this->curTimeout = $this->timeout; $this->is_timeout = false; if ($channel === null) { $channel = $this->get_interactive_channel(); } if (!$this->is_channel_status_data($channel) && empty($this->channel_buffers[$channel])) { if ($channel != self::CHANNEL_SHELL) { throw new InsufficientSetupException('Data is not available on channel'); } elseif (!$this->openShell()) { throw new \RuntimeException('Unable to initiate an interactive shell session'); } } if ($mode == self::READ_NEXT) { return $this->get_channel_packet($channel); } $match = $expect; while (true) { if ($mode == self::READ_REGEX) { preg_match($expect, substr($this->interactiveBuffer, -1024), $matches); $match = isset($matches[0]) ? $matches[0] : ''; } $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; if ($pos !== false) { return Strings::shift($this->interactiveBuffer, $pos + strlen($match)); } $response = $this->get_channel_packet($channel); if ($response === true) { return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer)); } $this->interactiveBuffer .= $response; } } /** * Inputs a command into an interactive shell. * * If not specifying a channel, an open interactive channel will be selected, or, if there are * no open channels, an interactive shell will be created. If there are multiple open * interactive channels, a legacy behavior will apply in which channel selection prioritizes * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive * channels, callers are discouraged from relying on this legacy behavior and should specify * the intended channel. * * @see SSH2::read() * @param string $cmd * @param int|null $channel Channel id returned by self::getInteractiveChannelId() * @return void * @throws \RuntimeException on connection error * @throws InsufficientSetupException on unexpected channel status, possibly due to closure * @throws TimeoutException if the write could not be completed within the requested self::setTimeout() */ public function write($cmd, $channel = null) { if (!$this->isAuthenticated()) { throw new InsufficientSetupException('Operation disallowed prior to login()'); } if ($channel === null) { $channel = $this->get_interactive_channel(); } if (!$this->is_channel_status_data($channel)) { if ($channel != self::CHANNEL_SHELL) { throw new InsufficientSetupException('Data is not available on channel'); } elseif (!$this->openShell()) { throw new \RuntimeException('Unable to initiate an interactive shell session'); } } $this->curTimeout = $this->timeout; $this->is_timeout = false; $this->send_channel_packet($channel, $cmd); } /** * Start a subsystem. * * Right now only one subsystem at a time is supported. To support multiple subsystem's stopSubsystem() could accept * a string that contained the name of the subsystem, but at that point, only one subsystem of each type could be opened. * To support multiple subsystem's of the same name maybe it'd be best if startSubsystem() generated a new channel id and * returns that and then that that was passed into stopSubsystem() but that'll be saved for a future date and implemented * if there's sufficient demand for such a feature. * * @see self::stopSubsystem() * @param string $subsystem * @return bool */ public function startSubsystem($subsystem) { $this->open_channel(self::CHANNEL_SUBSYSTEM); $packet = Strings::packSSH2( 'CNsCs', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[self::CHANNEL_SUBSYSTEM], 'subsystem', 1, $subsystem ); $this->send_binary_packet($packet); $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST; if (!$this->get_channel_packet(self::CHANNEL_SUBSYSTEM)) { return false; } $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA; $this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM; return true; } /** * Stops a subsystem. * * @see self::startSubsystem() * @return bool */ public function stopSubsystem() { if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) { $this->close_channel(self::CHANNEL_SUBSYSTEM); } return true; } /** * Closes a channel * * If read() timed out you might want to just close the channel and have it auto-restart on the next read() call * * If not specifying a channel, an open interactive channel will be selected. If there are * multiple open interactive channels, a legacy behavior will apply in which channel selection * prioritizes an active subsystem, the exec pty, and, lastly, the shell. If using multiple * interactive channels, callers are discouraged from relying on this legacy behavior and * should specify the intended channel. * * @param int|null $channel Channel id returned by self::getInteractiveChannelId() * @return void */ public function reset($channel = null) { if ($channel === null) { $channel = $this->get_interactive_channel(); } if ($this->isInteractiveChannelOpen($channel)) { $this->close_channel($channel); } } /** * Is timeout? * * Did exec() or read() return because they timed out or because they encountered the end? * */ public function isTimeout() { return $this->is_timeout; } /** * Disconnect * */ public function disconnect() { $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) { fclose($this->realtime_log_file); } unset(self::$connections[$this->getResourceId()]); } /** * Destructor. * * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call * disconnect(). * */ public function __destruct() { $this->disconnect(); } /** * Is the connection still active? * * $level has 3x possible values: * 0 (default): phpseclib takes a passive approach to see if the connection is still active by calling feof() * on the socket * 1: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_IGNORE * packet that doesn't require a response * 2: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_CHANNEL_OPEN * packet and imediately trying to close that channel. some routers, in particular, however, will only let you * open one channel, so this approach could yield false positives * * @param int $level * @return bool */ public function isConnected($level = 0) { if (!is_int($level) || $level < 0 || $level > 2) { throw new \InvalidArgumentException('$level must be 0, 1 or 2'); } if ($level == 0) { return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock); } try { if ($level == 1) { $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); } else { $this->open_channel(self::CHANNEL_KEEP_ALIVE); $this->close_channel(self::CHANNEL_KEEP_ALIVE); } return true; } catch (\Exception $e) { return false; } } /** * Have you successfully been logged in? * * @return bool */ public function isAuthenticated() { return (bool) ($this->bitmap & self::MASK_LOGIN); } /** * Is the interactive shell active? * * @return bool */ public function isShellOpen() { return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL); } /** * Is the exec pty active? * * @return bool */ public function isPTYOpen() { return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC); } /** * Is the given interactive channel active? * * @param int $channel Channel id returned by self::getInteractiveChannelId() * @return bool */ public function isInteractiveChannelOpen($channel) { return $this->isAuthenticated() && $this->is_channel_status_data($channel); } /** * Returns a channel identifier, presently of the last interactive channel opened, regardless of current status. * Returns 0 if no interactive channel has been opened. * * @see self::isInteractiveChannelOpen() * @return int */ public function getInteractiveChannelId() { return $this->channel_id_last_interactive; } /** * Pings a server connection, or tries to reconnect if the connection has gone down * * Inspired by http://php.net/manual/en/mysqli.ping.php * * @return bool */ public function ping() { if (!$this->isAuthenticated()) { if (!empty($this->auth)) { return $this->reconnect(); } return false; } try { $this->open_channel(self::CHANNEL_KEEP_ALIVE); } catch (\RuntimeException $e) { return $this->reconnect(); } $this->close_channel(self::CHANNEL_KEEP_ALIVE); return true; } /** * In situ reconnect method * * @return boolean */ private function reconnect() { $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); $this->connect(); foreach ($this->auth as $auth) { $result = $this->login(...$auth); } return $result; } /** * Resets a connection for re-use */ protected function reset_connection() { if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') { fclose($this->fsock); } $this->fsock = null; $this->bitmap = 0; $this->binary_packet_buffer = null; $this->decrypt = $this->encrypt = false; $this->decrypt_block_size = $this->encrypt_block_size = 8; $this->hmac_check = $this->hmac_create = false; $this->hmac_size = false; $this->session_id = false; $this->last_packet = null; $this->get_seq_no = $this->send_seq_no = 0; $this->channel_status = []; $this->channel_id_last_interactive = 0; $this->channel_buffers = []; $this->channel_buffers_write = []; } /** * @return int[] second and microsecond stream timeout options based on user-requested timeout and keep-alive, or the default socket timeout by default, which mirrors PHP socket streams. */ private function get_stream_timeout() { $sec = ini_get('default_socket_timeout'); $usec = 0; if ($this->curTimeout > 0) { $sec = (int) floor($this->curTimeout); $usec = (int) (1000000 * ($this->curTimeout - $sec)); } if ($this->keepAlive > 0) { $elapsed = microtime(true) - $this->last_packet; $timeout = max($this->keepAlive - $elapsed, 0); if (!$this->curTimeout || $timeout < $this->curTimeout) { $sec = (int) floor($timeout); $usec = (int) (1000000 * ($timeout - $sec)); } } return [$sec, $usec]; } /** * Retrieves the next packet with added timeout and type handling * * @param string $message_types Message types to enforce in response, closing if not met * @return string * @throws ConnectionClosedException If an error has occurred preventing read of the next packet */ private function get_binary_packet_or_close(...$message_types) { try { $packet = $this->get_binary_packet(); if (count($message_types) > 0 && !in_array(ord($packet[0]), $message_types)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); throw new ConnectionClosedException('Bad message type. Expected: #' . implode(', #', $message_types) . '. Got: #' . ord($packet[0])); } return $packet; } catch (TimeoutException $e) { $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); throw new ConnectionClosedException('Connection closed due to timeout'); } } /** * Gets Binary Packets * * See '6. Binary Packet Protocol' of rfc4253 for more info. * * @see self::_send_binary_packet() * @return string * @throws TimeoutException If user requested timeout was reached while waiting for next packet * @throws ConnectionClosedException If an error has occurred preventing read of the next packet */ private function get_binary_packet() { if (!is_resource($this->fsock)) { throw new \InvalidArgumentException('fsock is not a resource.'); } if (!$this->keyExchangeInProgress && count($this->kex_buffer)) { return $this->filter(array_shift($this->kex_buffer)); } if ($this->binary_packet_buffer == null) { // buffer the packet to permit continued reads across timeouts $this->binary_packet_buffer = (object) [ 'read_time' => 0, // the time to read the packet from the socket 'raw' => '', // the raw payload read from the socket 'plain' => '', // the packet in plain text, excluding packet_length header 'packet_length' => null, // the packet_length value pulled from the payload 'size' => $this->decrypt_block_size, // the total size of this packet to be read from the socket // initialize to read single block until packet_length is available ]; } $packet = $this->binary_packet_buffer; while (strlen($packet->raw) < $packet->size) { if (feof($this->fsock)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); throw new ConnectionClosedException('Connection closed by server'); } if ($this->curTimeout < 0) { $this->is_timeout = true; throw new TimeoutException('Timed out waiting for server'); } $this->send_keep_alive(); list($sec, $usec) = $this->get_stream_timeout(); stream_set_timeout($this->fsock, $sec, $usec); $start = microtime(true); $raw = stream_get_contents($this->fsock, $packet->size - strlen($packet->raw)); $elapsed = microtime(true) - $start; $packet->read_time += $elapsed; if ($this->curTimeout > 0) { $this->curTimeout -= $elapsed; } if ($raw === false) { $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); throw new ConnectionClosedException('Connection closed by server'); } elseif (!strlen($raw)) { continue; } $packet->raw .= $raw; if (!$packet->packet_length) { $this->get_binary_packet_size($packet); } } if (strlen($packet->raw) != $packet->size) { throw new \RuntimeException('Size of packet was not expected length'); } // destroy buffer as packet represents the entire payload and should be processed in full $this->binary_packet_buffer = null; // copy the raw payload, so as not to destroy original $raw = $packet->raw; if ($this->hmac_check instanceof Hash) { $hmac = Strings::pop($raw, $this->hmac_size); } $packet_length_header_size = 4; if ($this->decrypt) { switch ($this->decryptName) { case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': $this->decrypt->setNonce( $this->decryptFixedPart . $this->decryptInvocationCounter ); Strings::increment_str($this->decryptInvocationCounter); $this->decrypt->setAAD(Strings::shift($raw, $packet_length_header_size)); $this->decrypt->setTag(Strings::pop($raw, $this->decrypt_block_size)); $packet->plain = $this->decrypt->decrypt($raw); break; case 'chacha20-poly1305@openssh.com': // This should be impossible, but we are checking anyway to narrow the type for Psalm. if (!($this->decrypt instanceof ChaCha20)) { throw new \LogicException('$this->decrypt is not a ' . ChaCha20::class); } $this->decrypt->setNonce(pack('N2', 0, $this->get_seq_no)); $this->decrypt->setCounter(0); // this is the same approach that's implemented in Salsa20::createPoly1305Key() // but we don't want to use the same AEAD construction that RFC8439 describes // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) $this->decrypt->setPoly1305Key( $this->decrypt->encrypt(str_repeat("\0", 32)) ); $this->decrypt->setAAD(Strings::shift($raw, $packet_length_header_size)); $this->decrypt->setCounter(1); $this->decrypt->setTag(Strings::pop($raw, 16)); $packet->plain = $this->decrypt->decrypt($raw); break; default: if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) { // first block was already decrypted for contained packet_length header Strings::shift($raw, $this->decrypt_block_size); if (strlen($raw) > 0) { $packet->plain .= $this->decrypt->decrypt($raw); } } else { Strings::shift($raw, $packet_length_header_size); $packet->plain = $this->decrypt->decrypt($raw); } break; } } else { Strings::shift($raw, $packet_length_header_size); $packet->plain = $raw; } if ($this->hmac_check instanceof Hash) { $reconstructed = !$this->hmac_check_etm ? pack('Na*', $packet->packet_length, $packet->plain) : substr($packet->raw, 0, -$this->hmac_size); if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { $this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no)); if ($hmac != $this->hmac_check->hash($reconstructed)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); throw new ConnectionClosedException('Invalid UMAC'); } } else { if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) { $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); throw new ConnectionClosedException('Invalid HMAC'); } } } $padding_length = 0; $payload = $packet->plain; extract(unpack('Cpadding_length', Strings::shift($payload, 1))); if ($padding_length > 0) { Strings::pop($payload, $padding_length); } if (!$this->keyExchangeInProgress) { $this->bytesTransferredSinceLastKEX += $packet->packet_length + $padding_length + 5; } if (empty($payload)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); throw new ConnectionClosedException('Plaintext is too short'); } switch ($this->decompress) { case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: if (!$this->isAuthenticated()) { break; } // fall-through case self::NET_SSH2_COMPRESSION_ZLIB: if ($this->regenerate_decompression_context) { $this->regenerate_decompression_context = false; $cmf = ord($payload[0]); $cm = $cmf & 0x0F; if ($cm != 8) { // deflate throw new UnsupportedAlgorithmException("Only CM = 8 ('deflate') is supported ($cm)"); } $cinfo = ($cmf & 0xF0) >> 4; if ($cinfo > 7) { throw new \RuntimeException("CINFO above 7 is not allowed ($cinfo)"); } $windowSize = 1 << ($cinfo + 8); $flg = ord($payload[1]); //$fcheck = $flg && 0x0F; if ((($cmf << 8) | $flg) % 31) { throw new \RuntimeException('fcheck failed'); } $fdict = boolval($flg & 0x20); $flevel = ($flg & 0xC0) >> 6; $this->decompress_context = inflate_init(ZLIB_ENCODING_RAW, ['window' => $cinfo + 8]); $payload = substr($payload, 2); } if ($this->decompress_context) { $payload = inflate_add($this->decompress_context, $payload, ZLIB_PARTIAL_FLUSH); } } $this->get_seq_no++; if (defined('NET_SSH2_LOGGING')) { $current = microtime(true); $message_number = isset(self::$message_numbers[ord($payload[0])]) ? self::$message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; $message_number = '<- ' . $message_number . ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($packet->read_time, 4) . 's)'; $this->append_log($message_number, $payload); } $this->last_packet = microtime(true); if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) { $this->key_exchange(); } // don't filter if we're in the middle of a key exchange (since _filter might send out packets) return $this->keyExchangeInProgress ? $payload : $this->filter($payload); } /** * @param object $packet The packet object being constructed, passed by reference * The size, packet_length, and plain properties of this object may be modified in processing * @throws InvalidPacketLengthException if the packet length header is invalid */ private function get_binary_packet_size(&$packet) { $packet_length_header_size = 4; if (strlen($packet->raw) < $packet_length_header_size) { return; } $packet_length = 0; $added_validation_length = 0; // indicates when the packet length header is included when validating packet length against block size if ($this->decrypt) { switch ($this->decryptName) { case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': extract(unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size))); $packet->size = $packet_length_header_size + $packet_length + $this->decrypt_block_size; // expect tag break; case 'chacha20-poly1305@openssh.com': $this->lengthDecrypt->setNonce(pack('N2', 0, $this->get_seq_no)); $packet_length_header = $this->lengthDecrypt->decrypt(substr($packet->raw, 0, $packet_length_header_size)); extract(unpack('Npacket_length', $packet_length_header)); $packet->size = $packet_length_header_size + $packet_length + 16; // expect tag break; default: if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) { if (strlen($packet->raw) < $this->decrypt_block_size) { return; } $packet->plain = $this->decrypt->decrypt(substr($packet->raw, 0, $this->decrypt_block_size)); extract(unpack('Npacket_length', Strings::shift($packet->plain, $packet_length_header_size))); $packet->size = $packet_length_header_size + $packet_length; $added_validation_length = $packet_length_header_size; } else { extract(unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size))); $packet->size = $packet_length_header_size + $packet_length; } break; } } else { extract(unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size))); $packet->size = $packet_length_header_size + $packet_length; $added_validation_length = $packet_length_header_size; } // quoting , // "implementations SHOULD check that the packet length is reasonable" // PuTTY uses 0x9000 as the actual max packet size and so to shall we if ( $packet_length <= 0 || $packet_length > 0x9000 || ($packet_length + $added_validation_length) % $this->decrypt_block_size != 0 ) { $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); throw new InvalidPacketLengthException('Invalid packet length'); } if ($this->hmac_check instanceof Hash) { $packet->size += $this->hmac_size; } $packet->packet_length = $packet_length; } /** * Handle Disconnect * * Because some binary packets need to be ignored... * * @see self::filter() * @see self::key_exchange() * @return boolean * @access private */ private function handleDisconnect($payload) { Strings::shift($payload, 1); list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload); $this->errors[] = 'SSH_MSG_DISCONNECT: ' . self::$disconnect_reasons[$reason_code] . "\r\n$message"; $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); throw new ConnectionClosedException('Connection closed by server'); } /** * Filter Binary Packets * * Because some binary packets need to be ignored... * * @see self::_get_binary_packet() * @param string $payload * @return string */ private function filter($payload) { switch (ord($payload[0])) { case NET_SSH2_MSG_DISCONNECT: return $this->handleDisconnect($payload); case NET_SSH2_MSG_IGNORE: $payload = $this->get_binary_packet(); break; case NET_SSH2_MSG_DEBUG: Strings::shift($payload, 2); // second byte is "always_display" list($message) = Strings::unpackSSH2('s', $payload); $this->errors[] = "SSH_MSG_DEBUG: $message"; $payload = $this->get_binary_packet(); break; case NET_SSH2_MSG_UNIMPLEMENTED: break; // return payload case NET_SSH2_MSG_KEXINIT: // this is here for server initiated key re-exchanges after the initial key exchange if ($this->session_id !== false) { if (!$this->key_exchange($payload)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); throw new ConnectionClosedException('Key exchange failed'); } $payload = $this->get_binary_packet(); } break; case NET_SSH2_MSG_EXT_INFO: Strings::shift($payload, 1); list($nr_extensions) = Strings::unpackSSH2('N', $payload); for ($i = 0; $i < $nr_extensions; $i++) { list($extension_name, $extension_value) = Strings::unpackSSH2('ss', $payload); if ($extension_name == 'server-sig-algs') { $this->supported_private_key_algorithms = explode(',', $extension_value); } } $payload = $this->get_binary_packet(); } // see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) { Strings::shift($payload, 1); list($this->banner_message) = Strings::unpackSSH2('s', $payload); $payload = $this->get_binary_packet(); } // only called when we've already logged in if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) { switch (ord($payload[0])) { case NET_SSH2_MSG_CHANNEL_REQUEST: if (strlen($payload) == 31) { extract(unpack('cpacket_type/Nchannel/Nlength', $payload)); if (substr($payload, 9, $length) == 'keepalive@openssh.com' && isset($this->server_channels[$channel])) { if (ord(substr($payload, 9 + $length))) { // want reply $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_SUCCESS, $this->server_channels[$channel])); } $payload = $this->get_binary_packet(); } } break; case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4 Strings::shift($payload, 1); list($request_name) = Strings::unpackSSH2('s', $payload); $this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name"; $this->send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE)); $payload = $this->get_binary_packet(); break; case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 Strings::shift($payload, 1); list($data, $server_channel) = Strings::unpackSSH2('sN', $payload); switch ($data) { case 'auth-agent': case 'auth-agent@openssh.com': if (isset($this->agent)) { $new_channel = self::CHANNEL_AGENT_FORWARD; list( $remote_window_size, $remote_maximum_packet_size ) = Strings::unpackSSH2('NN', $payload); $this->packet_size_client_to_server[$new_channel] = $remote_window_size; $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; $this->window_size_client_to_server[$new_channel] = $this->window_size; $packet_size = 0x4000; $packet = pack( 'CN4', NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size ); $this->server_channels[$new_channel] = $server_channel; $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; $this->send_binary_packet($packet); } break; default: $packet = Strings::packSSH2( 'CN2ss', NET_SSH2_MSG_CHANNEL_OPEN_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, '', // description '' // language tag ); $this->send_binary_packet($packet); } $payload = $this->get_binary_packet(); break; } } return $payload; } /** * Enable Quiet Mode * * Suppress stderr from output * */ public function enableQuietMode() { $this->quiet_mode = true; } /** * Disable Quiet Mode * * Show stderr in output * */ public function disableQuietMode() { $this->quiet_mode = false; } /** * Returns whether Quiet Mode is enabled or not * * @see self::enableQuietMode() * @see self::disableQuietMode() * @return bool */ public function isQuietModeEnabled() { return $this->quiet_mode; } /** * Enable request-pty when using exec() * */ public function enablePTY() { $this->request_pty = true; } /** * Disable request-pty when using exec() * */ public function disablePTY() { if ($this->isPTYOpen()) { $this->close_channel(self::CHANNEL_EXEC); } $this->request_pty = false; } /** * Returns whether request-pty is enabled or not * * @see self::enablePTY() * @see self::disablePTY() * @return bool */ public function isPTYEnabled() { return $this->request_pty; } /** * Gets channel data * * Returns the data as a string. bool(true) is returned if: * * - the server closes the channel * - if the connection times out * - if a window adjust packet is received on the given negated client channel * - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS * - if the channel status is CHANNEL_CLOSE and the response was CHANNEL_CLOSE * * bool(false) is returned if: * * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_FAILURE * * @param int $client_channel Specifies the channel to return data for, and data received * on other channels is buffered. The respective negative value of a channel is * also supported for the case that the caller is awaiting adjustment of the data * window, and where data received on that respective channel is also buffered. * @param bool $skip_extended * @return mixed * @throws \RuntimeException on connection error */ protected function get_channel_packet($client_channel, $skip_extended = false) { if (!empty($this->channel_buffers[$client_channel])) { switch ($this->channel_status[$client_channel]) { case NET_SSH2_MSG_CHANNEL_REQUEST: foreach ($this->channel_buffers[$client_channel] as $i => $packet) { switch (ord($packet[0])) { case NET_SSH2_MSG_CHANNEL_SUCCESS: case NET_SSH2_MSG_CHANNEL_FAILURE: unset($this->channel_buffers[$client_channel][$i]); return substr($packet, 1); } } break; default: return substr(array_shift($this->channel_buffers[$client_channel]), 1); } } while (true) { try { $response = $this->get_binary_packet(); } catch (TimeoutException $e) { return true; } list($type) = Strings::unpackSSH2('C', $response); if (strlen($response) >= 4) { list($channel) = Strings::unpackSSH2('N', $response); } // will not be setup yet on incoming channel open request if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { $this->window_size_server_to_client[$channel] -= strlen($response); // resize the window, if appropriate if ($this->window_size_server_to_client[$channel] < 0) { // PuTTY does something more analogous to the following: //if ($this->window_size_server_to_client[$channel] < 0x3FFFFFFF) { $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_resize); $this->send_binary_packet($packet); $this->window_size_server_to_client[$channel] += $this->window_resize; } switch ($type) { case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: list($window_size) = Strings::unpackSSH2('N', $response); $this->window_size_client_to_server[$channel] += $window_size; if ($channel == -$client_channel) { return true; } continue 2; case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: /* if ($client_channel == self::CHANNEL_EXEC) { $this->send_channel_packet($client_channel, chr(0)); } */ // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR list($data_type_code, $data) = Strings::unpackSSH2('Ns', $response); $this->stdErrorLog .= $data; if ($skip_extended || $this->quiet_mode) { continue 2; } if ($client_channel == $channel && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA) { return $data; } $this->channel_buffers[$channel][] = chr($type) . $data; continue 2; case NET_SSH2_MSG_CHANNEL_REQUEST: if ($this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_CLOSE) { continue 2; } list($value) = Strings::unpackSSH2('s', $response); switch ($value) { case 'exit-signal': list( , // FALSE $signal_name, , // core dumped $error_message ) = Strings::unpackSSH2('bsbs', $response); $this->errors[] = "SSH_MSG_CHANNEL_REQUEST (exit-signal): $signal_name"; if (strlen($error_message)) { $this->errors[count($this->errors) - 1] .= "\r\n$error_message"; } $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF; continue 3; case 'exit-status': list(, $this->exit_status) = Strings::unpackSSH2('CN', $response); // "The client MAY ignore these messages." // -- http://tools.ietf.org/html/rfc4254#section-6.10 continue 3; default: // "Some systems may not implement signals, in which case they SHOULD ignore this message." // -- http://tools.ietf.org/html/rfc4254#section-6.9 continue 3; } } switch ($this->channel_status[$channel]) { case NET_SSH2_MSG_CHANNEL_OPEN: switch ($type) { case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: list( $this->server_channels[$channel], $window_size, $this->packet_size_client_to_server[$channel] ) = Strings::unpackSSH2('NNN', $response); if ($window_size < 0) { $window_size &= 0x7FFFFFFF; $window_size += 0x80000000; } $this->window_size_client_to_server[$channel] = $window_size; $result = $client_channel == $channel ? true : $this->get_channel_packet($client_channel, $skip_extended); $this->on_channel_open(); return $result; case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); throw new \RuntimeException('Unable to open channel'); default: if ($client_channel == $channel) { $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); throw new \RuntimeException('Unexpected response to open request'); } return $this->get_channel_packet($client_channel, $skip_extended); } break; case NET_SSH2_MSG_CHANNEL_REQUEST: switch ($type) { case NET_SSH2_MSG_CHANNEL_SUCCESS: return true; case NET_SSH2_MSG_CHANNEL_FAILURE: return false; case NET_SSH2_MSG_CHANNEL_DATA: list($data) = Strings::unpackSSH2('s', $response); $this->channel_buffers[$channel][] = chr($type) . $data; return $this->get_channel_packet($client_channel, $skip_extended); default: $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); throw new \RuntimeException('Unable to fulfill channel request'); } case NET_SSH2_MSG_CHANNEL_CLOSE: if ($client_channel == $channel && $type == NET_SSH2_MSG_CHANNEL_CLOSE) { return true; } return $this->get_channel_packet($client_channel, $skip_extended); } } // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA switch ($type) { case NET_SSH2_MSG_CHANNEL_DATA: /* if ($channel == self::CHANNEL_EXEC) { // SCP requires null packets, such as this, be sent. further, in the case of the ssh.com SSH server // this actually seems to make things twice as fast. more to the point, the message right after // SSH_MSG_CHANNEL_DATA (usually SSH_MSG_IGNORE) won't block for as long as it would have otherwise. // in OpenSSH it slows things down but only by a couple thousandths of a second. $this->send_channel_packet($channel, chr(0)); } */ list($data) = Strings::unpackSSH2('s', $response); if ($channel == self::CHANNEL_AGENT_FORWARD) { $agent_response = $this->agent->forwardData($data); if (!is_bool($agent_response)) { $this->send_channel_packet($channel, $agent_response); } break; } if ($client_channel == $channel) { return $data; } $this->channel_buffers[$channel][] = chr($type) . $data; break; case NET_SSH2_MSG_CHANNEL_CLOSE: $this->curTimeout = 5; $this->close_channel_bitmap($channel); if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) { $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); } $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE; $this->channelCount--; if ($client_channel == $channel) { return true; } // fall-through case NET_SSH2_MSG_CHANNEL_EOF: break; default: $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); throw new \RuntimeException("Error reading channel data ($type)"); } } } /** * Sends Binary Packets * * See '6. Binary Packet Protocol' of rfc4253 for more info. * * @param string $data * @param string $logged * @see self::_get_binary_packet() * @return void */ protected function send_binary_packet($data, $logged = null) { if (!is_resource($this->fsock) || feof($this->fsock)) { $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); throw new ConnectionClosedException('Connection closed prematurely'); } if (!isset($logged)) { $logged = $data; } switch ($this->compress) { case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: if (!$this->isAuthenticated()) { break; } // fall-through case self::NET_SSH2_COMPRESSION_ZLIB: if (!$this->regenerate_compression_context) { $header = ''; } else { $this->regenerate_compression_context = false; $this->compress_context = deflate_init(ZLIB_ENCODING_RAW, ['window' => 15]); $header = "\x78\x9C"; } if ($this->compress_context) { $data = $header . deflate_add($this->compress_context, $data, ZLIB_PARTIAL_FLUSH); } } // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9 $packet_length = strlen($data) + 9; if ($this->encrypt && $this->encrypt->usesNonce()) { $packet_length -= 4; } // round up to the nearest $this->encrypt_block_size $packet_length += (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size; // subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length $padding_length = $packet_length - strlen($data) - 5; switch (true) { case $this->encrypt && $this->encrypt->usesNonce(): case $this->hmac_create instanceof Hash && $this->hmac_create_etm: $padding_length += 4; $packet_length += 4; } $padding = Random::string($padding_length); // we subtract 4 from packet_length because the packet_length field isn't supposed to include itself $packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding); $hmac = ''; if ($this->hmac_create instanceof Hash && !$this->hmac_create_etm) { if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no)); $hmac = $this->hmac_create->hash($packet); } else { $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)); } } if ($this->encrypt) { switch ($this->encryptName) { case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': $this->encrypt->setNonce( $this->encryptFixedPart . $this->encryptInvocationCounter ); Strings::increment_str($this->encryptInvocationCounter); $this->encrypt->setAAD($temp = ($packet & "\xFF\xFF\xFF\xFF")); $packet = $temp . $this->encrypt->encrypt(substr($packet, 4)); break; case 'chacha20-poly1305@openssh.com': // This should be impossible, but we are checking anyway to narrow the type for Psalm. if (!($this->encrypt instanceof ChaCha20)) { throw new \LogicException('$this->encrypt is not a ' . ChaCha20::class); } $nonce = pack('N2', 0, $this->send_seq_no); $this->encrypt->setNonce($nonce); $this->lengthEncrypt->setNonce($nonce); $length = $this->lengthEncrypt->encrypt($packet & "\xFF\xFF\xFF\xFF"); $this->encrypt->setCounter(0); // this is the same approach that's implemented in Salsa20::createPoly1305Key() // but we don't want to use the same AEAD construction that RFC8439 describes // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) $this->encrypt->setPoly1305Key( $this->encrypt->encrypt(str_repeat("\0", 32)) ); $this->encrypt->setAAD($length); $this->encrypt->setCounter(1); $packet = $length . $this->encrypt->encrypt(substr($packet, 4)); break; default: $packet = $this->hmac_create instanceof Hash && $this->hmac_create_etm ? ($packet & "\xFF\xFF\xFF\xFF") . $this->encrypt->encrypt(substr($packet, 4)) : $this->encrypt->encrypt($packet); } } if ($this->hmac_create instanceof Hash && $this->hmac_create_etm) { if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no)); $hmac = $this->hmac_create->hash($packet); } else { $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)); } } $this->send_seq_no++; $packet .= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac; if (!$this->keyExchangeInProgress) { $this->bytesTransferredSinceLastKEX += strlen($packet); } $start = microtime(true); $sent = @fputs($this->fsock, $packet); $stop = microtime(true); if (defined('NET_SSH2_LOGGING')) { $current = microtime(true); $message_number = isset(self::$message_numbers[ord($logged[0])]) ? self::$message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; $message_number = '-> ' . $message_number . ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; $this->append_log($message_number, $logged); } $this->last_packet = microtime(true); if (strlen($packet) != $sent) { $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); $message = $sent === false ? 'Unable to write ' . strlen($packet) . ' bytes' : "Only $sent of " . strlen($packet) . " bytes were sent"; throw new \RuntimeException($message); } if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) { $this->key_exchange(); } } /** * Sends a keep-alive message, if keep-alive is enabled and interval is met */ private function send_keep_alive() { if ($this->bitmap & self::MASK_CONNECTED) { $elapsed = microtime(true) - $this->last_packet; if ($this->keepAlive > 0 && $elapsed >= $this->keepAlive) { $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); } } } /** * Logs data packets * * Makes sure that only the last 1MB worth of packets will be logged * * @param string $message_number * @param string $message */ private function append_log($message_number, $message) { $this->append_log_helper( NET_SSH2_LOGGING, $message_number, $message, $this->message_number_log, $this->message_log, $this->log_size, $this->realtime_log_file, $this->realtime_log_wrap, $this->realtime_log_size ); } /** * Logs data packet helper * * @param int $constant * @param string $message_number * @param string $message * @param array &$message_number_log * @param array &$message_log * @param int &$log_size * @param resource &$realtime_log_file * @param bool &$realtime_log_wrap * @param int &$realtime_log_size */ protected function append_log_helper($constant, $message_number, $message, array &$message_number_log, array &$message_log, &$log_size, &$realtime_log_file, &$realtime_log_wrap, &$realtime_log_size) { // remove the byte identifying the message type from all but the first two messages (ie. the identification strings) if (strlen($message_number) > 2) { Strings::shift($message); } switch ($constant) { // useful for benchmarks case self::LOG_SIMPLE: $message_number_log[] = $message_number; break; case self::LOG_SIMPLE_REALTIME: echo $message_number; echo PHP_SAPI == 'cli' ? "\r\n" : '
'; @flush(); @ob_flush(); break; // the most useful log for SSH2 case self::LOG_COMPLEX: $message_number_log[] = $message_number; $log_size += strlen($message); $message_log[] = $message; while ($log_size > self::LOG_MAX_SIZE) { $log_size -= strlen(array_shift($message_log)); array_shift($message_number_log); } break; // dump the output out realtime; packets may be interspersed with non packets, // passwords won't be filtered out and select other packets may not be correctly // identified case self::LOG_REALTIME: switch (PHP_SAPI) { case 'cli': $start = $stop = "\r\n"; break; default: $start = '
';
                        $stop = '
'; } echo $start . $this->format_log([$message], [$message_number]) . $stop; @flush(); @ob_flush(); break; // basically the same thing as self::LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILENAME // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE. // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily // at the beginning of the file case self::LOG_REALTIME_FILE: if (!isset($realtime_log_file)) { // PHP doesn't seem to like using constants in fopen() $filename = NET_SSH2_LOG_REALTIME_FILENAME; $fp = fopen($filename, 'w'); $realtime_log_file = $fp; } if (!is_resource($realtime_log_file)) { break; } $entry = $this->format_log([$message], [$message_number]); if ($realtime_log_wrap) { $temp = "<<< START >>>\r\n"; $entry .= $temp; fseek($realtime_log_file, ftell($realtime_log_file) - strlen($temp)); } $realtime_log_size += strlen($entry); if ($realtime_log_size > self::LOG_MAX_SIZE) { fseek($realtime_log_file, 0); $realtime_log_size = strlen($entry); $realtime_log_wrap = true; } fputs($realtime_log_file, $entry); break; case self::LOG_REALTIME_SIMPLE: echo $message_number; echo PHP_SAPI == 'cli' ? "\r\n" : '
'; } } /** * Sends channel data * * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate * * @param int $client_channel * @param string $data * @return void */ protected function send_channel_packet($client_channel, $data) { if ( isset($this->channel_buffers_write[$client_channel]) && strpos($data, $this->channel_buffers_write[$client_channel]) === 0 ) { // if buffer holds identical initial data content, resume send from the unmatched data portion $data = substr($data, strlen($this->channel_buffers_write[$client_channel])); } else { $this->channel_buffers_write[$client_channel] = ''; } while (strlen($data)) { if (!$this->window_size_client_to_server[$client_channel]) { // using an invalid channel will let the buffers be built up for the valid channels $this->get_channel_packet(-$client_channel); if ($this->isTimeout()) { throw new TimeoutException('Timed out waiting for server'); } elseif (!$this->window_size_client_to_server[$client_channel]) { throw new \RuntimeException('Data window was not adjusted'); } } /* The maximum amount of data allowed is determined by the maximum packet size for the channel, and the current window size, whichever is smaller. -- http://tools.ietf.org/html/rfc4254#section-5.2 */ $max_size = min( $this->packet_size_client_to_server[$client_channel], $this->window_size_client_to_server[$client_channel] ); $temp = Strings::shift($data, $max_size); $packet = Strings::packSSH2( 'CNs', NET_SSH2_MSG_CHANNEL_DATA, $this->server_channels[$client_channel], $temp ); $this->window_size_client_to_server[$client_channel] -= strlen($temp); $this->send_binary_packet($packet); $this->channel_buffers_write[$client_channel] .= $temp; } unset($this->channel_buffers_write[$client_channel]); } /** * Closes and flushes a channel * * \phpseclib3\Net\SSH2 doesn't properly close most channels. For exec() channels are normally closed by the server * and for SFTP channels are presumably closed when the client disconnects. This functions is intended * for SCP more than anything. * * @param int $client_channel * @param bool $want_reply * @return void */ private function close_channel($client_channel, $want_reply = false) { // see http://tools.ietf.org/html/rfc4254#section-5.3 $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); if (!$want_reply) { $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); } $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE; $this->channelCount--; $this->curTimeout = 5; while (!is_bool($this->get_channel_packet($client_channel))) { } if ($want_reply) { $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); } $this->close_channel_bitmap($client_channel); } /** * Maintains execution state bitmap in response to channel closure * * @param int $client_channel The channel number to maintain closure status of * @return void */ private function close_channel_bitmap($client_channel) { switch ($client_channel) { case self::CHANNEL_SHELL: // Shell status has been maintained in the bitmap for backwards // compatibility sake, but can be removed going forward if ($this->bitmap & self::MASK_SHELL) { $this->bitmap &= ~self::MASK_SHELL; } break; } } /** * Disconnect * * @param int $reason * @return false */ protected function disconnect_helper($reason) { if ($this->bitmap & self::MASK_DISCONNECT) { // Disregard subsequent disconnect requests return false; } $this->bitmap |= self::MASK_DISCONNECT; if ($this->isConnected()) { $data = Strings::packSSH2('CNss', NET_SSH2_MSG_DISCONNECT, $reason, '', ''); try { $this->send_binary_packet($data); } catch (\Exception $e) { } } $this->reset_connection(); return false; } /** * Define Array * * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of * named constants from it, using the value as the name of the constant and the index as the value of the constant. * If any of the constants that would be defined already exists, none of the constants will be defined. * * @param mixed[] ...$args * @access protected */ protected static function define_array(...$args) { foreach ($args as $arg) { foreach ($arg as $key => $value) { if (!defined($value)) { define($value, $key); } else { break 2; } } } } /** * Returns a log of the packets that have been sent and received. * * Returns a string if NET_SSH2_LOGGING == self::LOG_COMPLEX, an array if NET_SSH2_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING') * * @return array|false|string */ public function getLog() { if (!defined('NET_SSH2_LOGGING')) { return false; } switch (NET_SSH2_LOGGING) { case self::LOG_SIMPLE: return $this->message_number_log; case self::LOG_COMPLEX: $log = $this->format_log($this->message_log, $this->message_number_log); return PHP_SAPI == 'cli' ? $log : '
' . $log . '
'; default: return false; } } /** * Formats a log for printing * * @param array $message_log * @param array $message_number_log * @return string */ protected function format_log(array $message_log, array $message_number_log) { $output = ''; for ($i = 0; $i < count($message_log); $i++) { $output .= $message_number_log[$i]; $current_log = $message_log[$i]; $j = 0; if (strlen($current_log)) { $output .= "\r\n"; } do { if (strlen($current_log)) { $output .= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; } $fragment = Strings::shift($current_log, $this->log_short_width); $hex = substr(preg_replace_callback('#.#s', function ($matches) { return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); }, $fragment), strlen($this->log_boundary)); // replace non ASCII printable characters with dots // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters // also replace < with a . since < messes up the output on web browsers $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); $output .= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; $j++; } while (strlen($current_log)); $output .= "\r\n"; } return $output; } /** * Helper function for agent->on_channel_open() * * Used when channels are created to inform agent * of said channel opening. Must be called after * channel open confirmation received * */ private function on_channel_open() { if (isset($this->agent)) { $this->agent->registerChannelOpen($this); } } /** * Returns the first value of the intersection of two arrays or false if * the intersection is empty. The order is defined by the first parameter. * * @param array $array1 * @param array $array2 * @return mixed False if intersection is empty, else intersected value. */ private static function array_intersect_first(array $array1, array $array2) { foreach ($array1 as $value) { if (in_array($value, $array2)) { return $value; } } return false; } /** * Returns all errors / debug messages on the SSH layer * * If you are looking for messages from the SFTP layer, please see SFTP::getSFTPErrors() * * @return string[] */ public function getErrors() { return $this->errors; } /** * Returns the last error received on the SSH layer * * If you are looking for messages from the SFTP layer, please see SFTP::getLastSFTPError() * * @return string */ public function getLastError() { $count = count($this->errors); if ($count > 0) { return $this->errors[$count - 1]; } } /** * Return the server identification. * * @return string|false */ public function getServerIdentification() { $this->connect(); return $this->server_identifier; } /** * Returns a list of algorithms the server supports * * @return array */ public function getServerAlgorithms() { $this->connect(); return [ 'kex' => $this->kex_algorithms, 'hostkey' => $this->server_host_key_algorithms, 'client_to_server' => [ 'crypt' => $this->encryption_algorithms_client_to_server, 'mac' => $this->mac_algorithms_client_to_server, 'comp' => $this->compression_algorithms_client_to_server, 'lang' => $this->languages_client_to_server ], 'server_to_client' => [ 'crypt' => $this->encryption_algorithms_server_to_client, 'mac' => $this->mac_algorithms_server_to_client, 'comp' => $this->compression_algorithms_server_to_client, 'lang' => $this->languages_server_to_client ] ]; } /** * Returns a list of KEX algorithms that phpseclib supports * * @return array */ public static function getSupportedKEXAlgorithms() { $kex_algorithms = [ // Elliptic Curve Diffie-Hellman Key Agreement (ECDH) using // Curve25519. See doc/curve25519-sha256@libssh.org.txt in the // libssh repository for more information. 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', // RFC 5656 'ecdh-sha2-nistp384', // RFC 5656 'ecdh-sha2-nistp521', // RFC 5656 'diffie-hellman-group-exchange-sha256',// RFC 4419 'diffie-hellman-group-exchange-sha1', // RFC 4419 // Diffie-Hellman Key Agreement (DH) using integer modulo prime // groups. 'diffie-hellman-group14-sha256', 'diffie-hellman-group14-sha1', // REQUIRED 'diffie-hellman-group15-sha512', 'diffie-hellman-group16-sha512', 'diffie-hellman-group17-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group1-sha1', // REQUIRED ]; return $kex_algorithms; } /** * Returns a list of host key algorithms that phpseclib supports * * @return array */ public static function getSupportedHostKeyAlgorithms() { return [ 'ssh-ed25519', // https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-02 'ecdsa-sha2-nistp256', // RFC 5656 'ecdsa-sha2-nistp384', // RFC 5656 'ecdsa-sha2-nistp521', // RFC 5656 'rsa-sha2-256', // RFC 8332 'rsa-sha2-512', // RFC 8332 'ssh-rsa', // RECOMMENDED sign Raw RSA Key 'ssh-dss' // REQUIRED sign Raw DSS Key ]; } /** * Returns a list of symmetric key algorithms that phpseclib supports * * @return array */ public static function getSupportedEncryptionAlgorithms() { $algos = [ // from : 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com', // from : 'arcfour256', 'arcfour128', //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key // CTR modes from : 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key 'aes192-ctr', // RECOMMENDED AES with 192-bit key 'aes256-ctr', // RECOMMENDED AES with 256-bit key // from : // one of the big benefits of chacha20-poly1305 is speed. the problem is... // libsodium doesn't generate the poly1305 keys in the way ssh does and openssl's PHP bindings don't even // seem to support poly1305 currently. so even if libsodium or openssl are being used for the chacha20 // part, pure-PHP has to be used for the poly1305 part and that's gonna cause a big slow down. // speed-wise it winds up being faster to use AES (when openssl or mcrypt are available) and some HMAC // (which is always gonna be super fast to compute thanks to the hash extension, which // "is bundled and compiled into PHP by default") 'chacha20-poly1305@openssh.com', 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key 'twofish256-ctr', // OPTIONAL Twofish with 256-bit key 'aes128-cbc', // RECOMMENDED AES with a 128-bit key 'aes192-cbc', // OPTIONAL AES with a 192-bit key 'aes256-cbc', // OPTIONAL AES in CBC mode, with a 256-bit key 'twofish128-cbc', // OPTIONAL Twofish with a 128-bit key 'twofish192-cbc', // OPTIONAL Twofish with a 192-bit key 'twofish256-cbc', 'twofish-cbc', // OPTIONAL alias for "twofish256-cbc" // (this is being retained for historical reasons) 'blowfish-ctr', // OPTIONAL Blowfish in SDCTR mode 'blowfish-cbc', // OPTIONAL Blowfish in CBC mode '3des-ctr', // RECOMMENDED Three-key 3DES in SDCTR mode '3des-cbc', // REQUIRED three-key 3DES in CBC mode //'none' // OPTIONAL no encryption; NOT RECOMMENDED ]; if (self::$crypto_engine) { $engines = [self::$crypto_engine]; } else { $engines = [ 'libsodium', 'OpenSSL (GCM)', 'OpenSSL', 'mcrypt', 'Eval', 'PHP' ]; } $ciphers = []; foreach ($engines as $engine) { foreach ($algos as $algo) { $obj = self::encryption_algorithm_to_crypt_instance($algo); if ($obj instanceof Rijndael) { $obj->setKeyLength(preg_replace('#[^\d]#', '', $algo)); } switch ($algo) { // Eval engines do not exist for ChaCha20 or RC4 because they would not benefit from one. // to benefit from an Eval engine they'd need to loop a variable amount of times, they'd // need to do table lookups (eg. sbox subsitutions). ChaCha20 doesn't do either because // it's a so-called ARX cipher, meaning that the only operations it does are add (A), rotate (R) // and XOR (X). RC4 does do table lookups but being a stream cipher it works differently than // block ciphers. with RC4 you XOR the plaintext against a keystream and the keystream changes // as you encrypt stuff. the only table lookups are made against this keystream and thus table // lookups are kinda unavoidable. with AES and DES, however, the table lookups that are done // are done against substitution boxes (sboxes), which are invariant. // OpenSSL can't be used as an engine, either, because OpenSSL doesn't support continuous buffers // as SSH2 uses and altho you can emulate a continuous buffer with block ciphers you can't do so // with stream ciphers. As for ChaCha20... for the ChaCha20 part OpenSSL could prob be used but // the big slow down isn't with ChaCha20 - it's with Poly1305. SSH constructs the key for that // differently than how OpenSSL does it (OpenSSL does it as the RFC describes, SSH doesn't). // libsodium can't be used because it doesn't support RC4 and it doesn't construct the Poly1305 // keys in the same way that SSH does // mcrypt could prob be used for RC4 but mcrypt hasn't been included in PHP core for yearss case 'chacha20-poly1305@openssh.com': case 'arcfour128': case 'arcfour256': if ($engine != 'PHP') { continue 2; } break; case 'aes128-gcm@openssh.com': case 'aes256-gcm@openssh.com': if ($engine == 'OpenSSL') { continue 2; } $obj->setNonce('dummydummydu'); } if ($obj->isValidEngine($engine)) { $algos = array_diff($algos, [$algo]); $ciphers[] = $algo; } } } return $ciphers; } /** * Returns a list of MAC algorithms that phpseclib supports * * @return array */ public static function getSupportedMACAlgorithms() { return [ 'hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'hmac-sha1-etm@openssh.com', // from : 'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32) 'hmac-sha2-512',// OPTIONAL HMAC-SHA512 (digest length = key length = 64) 'hmac-sha1-96', // RECOMMENDED first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20) 'hmac-sha1', // REQUIRED HMAC-SHA1 (digest length = key length = 20) 'hmac-md5-96', // OPTIONAL first 96 bits of HMAC-MD5 (digest length = 12, key length = 16) 'hmac-md5', // OPTIONAL HMAC-MD5 (digest length = key length = 16) 'umac-64-etm@openssh.com', 'umac-128-etm@openssh.com', // from : 'umac-64@openssh.com', 'umac-128@openssh.com', //'none' // OPTIONAL no MAC; NOT RECOMMENDED ]; } /** * Returns a list of compression algorithms that phpseclib supports * * @return array */ public static function getSupportedCompressionAlgorithms() { $algos = ['none']; // REQUIRED no compression if (function_exists('deflate_init')) { $algos[] = 'zlib@openssh.com'; // https://datatracker.ietf.org/doc/html/draft-miller-secsh-compression-delayed $algos[] = 'zlib'; } return $algos; } /** * Return list of negotiated algorithms * * Uses the same format as https://www.php.net/ssh2-methods-negotiated * * @return array */ public function getAlgorithmsNegotiated() { $this->connect(); $compression_map = [ self::NET_SSH2_COMPRESSION_NONE => 'none', self::NET_SSH2_COMPRESSION_ZLIB => 'zlib', self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH => 'zlib@openssh.com' ]; return [ 'kex' => $this->kex_algorithm, 'hostkey' => $this->signature_format, 'client_to_server' => [ 'crypt' => $this->encryptName, 'mac' => $this->hmac_create_name, 'comp' => $compression_map[$this->compress], ], 'server_to_client' => [ 'crypt' => $this->decryptName, 'mac' => $this->hmac_check_name, 'comp' => $compression_map[$this->decompress], ] ]; } /** * Force multiple channels (even if phpseclib has decided to disable them) */ public function forceMultipleChannels() { $this->errorOnMultipleChannels = false; } /** * Allows you to set the terminal * * @param string $term */ public function setTerminal($term) { $this->term = $term; } /** * Accepts an associative array with up to four parameters as described at * * * @param array $methods */ public function setPreferredAlgorithms(array $methods) { $keys = ['client_to_server', 'server_to_client']; if (isset($methods['kex']) && is_string($methods['kex'])) { $methods['kex'] = explode(',', $methods['kex']); } if (isset($methods['hostkey']) && is_string($methods['hostkey'])) { $methods['hostkey'] = explode(',', $methods['hostkey']); } foreach ($keys as $key) { if (isset($methods[$key])) { $a = &$methods[$key]; if (isset($a['crypt']) && is_string($a['crypt'])) { $a['crypt'] = explode(',', $a['crypt']); } if (isset($a['comp']) && is_string($a['comp'])) { $a['comp'] = explode(',', $a['comp']); } if (isset($a['mac']) && is_string($a['mac'])) { $a['mac'] = explode(',', $a['mac']); } } } $preferred = $methods; if (isset($preferred['kex'])) { $preferred['kex'] = array_intersect( $preferred['kex'], static::getSupportedKEXAlgorithms() ); } if (isset($preferred['hostkey'])) { $preferred['hostkey'] = array_intersect( $preferred['hostkey'], static::getSupportedHostKeyAlgorithms() ); } foreach ($keys as $key) { if (isset($preferred[$key])) { $a = &$preferred[$key]; if (isset($a['crypt'])) { $a['crypt'] = array_intersect( $a['crypt'], static::getSupportedEncryptionAlgorithms() ); } if (isset($a['comp'])) { $a['comp'] = array_intersect( $a['comp'], static::getSupportedCompressionAlgorithms() ); } if (isset($a['mac'])) { $a['mac'] = array_intersect( $a['mac'], static::getSupportedMACAlgorithms() ); } } } $keys = [ 'kex', 'hostkey', 'client_to_server/crypt', 'client_to_server/comp', 'client_to_server/mac', 'server_to_client/crypt', 'server_to_client/comp', 'server_to_client/mac', ]; foreach ($keys as $key) { $p = $preferred; $m = $methods; $subkeys = explode('/', $key); foreach ($subkeys as $subkey) { if (!isset($p[$subkey])) { continue 2; } $p = $p[$subkey]; $m = $m[$subkey]; } if (count($p) != count($m)) { $diff = array_diff($m, $p); $msg = count($diff) == 1 ? ' is not a supported algorithm' : ' are not supported algorithms'; throw new UnsupportedAlgorithmException(implode(', ', $diff) . $msg); } } $this->preferred = $preferred; } /** * Returns the banner message. * * Quoting from the RFC, "in some jurisdictions, sending a warning message before * authentication may be relevant for getting legal protection." * * @return string */ public function getBannerMessage() { return $this->banner_message; } /** * Returns the server public host key. * * Caching this the first time you connect to a server and checking the result on subsequent connections * is recommended. Returns false if the server signature is not signed correctly with the public host key. * * @return string|false * @throws \RuntimeException on badly formatted keys * @throws NoSupportedAlgorithmsException when the key isn't in a supported format */ public function getServerPublicHostKey() { if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { $this->connect(); } $signature = $this->signature; $server_public_host_key = base64_encode($this->server_public_host_key); if ($this->signature_validated) { return $this->bitmap ? $this->signature_format . ' ' . $server_public_host_key : false; } $this->signature_validated = true; switch ($this->signature_format) { case 'ssh-ed25519': case 'ecdsa-sha2-nistp256': case 'ecdsa-sha2-nistp384': case 'ecdsa-sha2-nistp521': $key = EC::loadFormat('OpenSSH', $server_public_host_key) ->withSignatureFormat('SSH2'); switch ($this->signature_format) { case 'ssh-ed25519': $hash = 'sha512'; break; case 'ecdsa-sha2-nistp256': $hash = 'sha256'; break; case 'ecdsa-sha2-nistp384': $hash = 'sha384'; break; case 'ecdsa-sha2-nistp521': $hash = 'sha512'; } $key = $key->withHash($hash); break; case 'ssh-dss': $key = DSA::loadFormat('OpenSSH', $server_public_host_key) ->withSignatureFormat('SSH2') ->withHash('sha1'); break; case 'ssh-rsa': case 'rsa-sha2-256': case 'rsa-sha2-512': // could be ssh-rsa, rsa-sha2-256, rsa-sha2-512 // we don't check here because we already checked in key_exchange // some signatures have the type embedded within the message and some don't list(, $signature) = Strings::unpackSSH2('ss', $signature); $key = RSA::loadFormat('OpenSSH', $server_public_host_key) ->withPadding(RSA::SIGNATURE_PKCS1); switch ($this->signature_format) { case 'rsa-sha2-512': $hash = 'sha512'; break; case 'rsa-sha2-256': $hash = 'sha256'; break; //case 'ssh-rsa': default: $hash = 'sha1'; } $key = $key->withHash($hash); break; default: $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); throw new NoSupportedAlgorithmsException('Unsupported signature format'); } if (!$key->verify($this->exchange_hash, $signature)) { return $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); }; return $this->signature_format . ' ' . $server_public_host_key; } /** * Returns the exit status of an SSH command or false. * * @return false|int */ public function getExitStatus() { if (is_null($this->exit_status)) { return false; } return $this->exit_status; } /** * Returns the number of columns for the terminal window size. * * @return int */ public function getWindowColumns() { return $this->windowColumns; } /** * Returns the number of rows for the terminal window size. * * @return int */ public function getWindowRows() { return $this->windowRows; } /** * Sets the number of columns for the terminal window size. * * @param int $value */ public function setWindowColumns($value) { $this->windowColumns = $value; } /** * Sets the number of rows for the terminal window size. * * @param int $value */ public function setWindowRows($value) { $this->windowRows = $value; } /** * Sets the number of columns and rows for the terminal window size. * * @param int $columns * @param int $rows */ public function setWindowSize($columns = 80, $rows = 24) { $this->windowColumns = $columns; $this->windowRows = $rows; } /** * To String Magic Method * * @return string */ #[\ReturnTypeWillChange] public function __toString() { return $this->getResourceId(); } /** * Get Resource ID * * We use {} because that symbols should not be in URL according to * {@link http://tools.ietf.org/html/rfc3986#section-2 RFC}. * It will safe us from any conflicts, because otherwise regexp will * match all alphanumeric domains. * * @return string */ public function getResourceId() { return '{' . spl_object_hash($this) . '}'; } /** * Return existing connection * * @param string $id * * @return bool|SSH2 will return false if no such connection */ public static function getConnectionByResourceId($id) { if (isset(self::$connections[$id])) { return self::$connections[$id] instanceof \WeakReference ? self::$connections[$id]->get() : self::$connections[$id]; } return false; } /** * Return all excising connections * * @return array */ public static function getConnections() { if (!class_exists('WeakReference')) { /** @var array */ return self::$connections; } $temp = []; foreach (self::$connections as $key => $ref) { $temp[$key] = $ref->get(); } return $temp; } /* * Update packet types in log history * * @param string $old * @param string $new */ private function updateLogHistory($old, $new) { if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == self::LOG_COMPLEX) { $this->message_number_log[count($this->message_number_log) - 1] = str_replace( $old, $new, $this->message_number_log[count($this->message_number_log) - 1] ); } } /** * Return the list of authentication methods that may productively continue authentication. * * @see https://tools.ietf.org/html/rfc4252#section-5.1 * @return array|null */ public function getAuthMethodsToContinue() { return $this->auth_methods_to_continue; } /** * Enables "smart" multi-factor authentication (MFA) */ public function enableSmartMFA() { $this->smartMFA = true; } /** * Disables "smart" multi-factor authentication (MFA) */ public function disableSmartMFA() { $this->smartMFA = false; } /** * How many bytes until the next key re-exchange? * * @param int $bytes */ public function bytesUntilKeyReexchange($bytes) { $this->doKeyReexchangeAfterXBytes = $bytes; } } PK!Bx/#/#Bvendor/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.phpnu[ * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\System\SSH\Agent; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\PrivateKey; use phpseclib3\Crypt\Common\PublicKey; use phpseclib3\Crypt\DSA; use phpseclib3\Crypt\EC; use phpseclib3\Crypt\RSA; use phpseclib3\Exception\UnsupportedAlgorithmException; use phpseclib3\System\SSH\Agent; use phpseclib3\System\SSH\Common\Traits\ReadBytes; /** * Pure-PHP ssh-agent client identity object * * Instantiation should only be performed by \phpseclib3\System\SSH\Agent class. * This could be thought of as implementing an interface that phpseclib3\Crypt\RSA * implements. ie. maybe a Net_SSH_Auth_PublicKey interface or something. * The methods in this interface would be getPublicKey and sign since those are the * methods phpseclib looks for to perform public key authentication. * * @author Jim Wigginton * @internal */ class Identity implements PrivateKey { use ReadBytes; // Signature Flags // See https://tools.ietf.org/html/draft-miller-ssh-agent-00#section-5.3 const SSH_AGENT_RSA2_256 = 2; const SSH_AGENT_RSA2_512 = 4; /** * Key Object * * @var PublicKey * @see self::getPublicKey() */ private $key; /** * Key Blob * * @var string * @see self::sign() */ private $key_blob; /** * Socket Resource * * @var resource * @see self::sign() */ private $fsock; /** * Signature flags * * @var int * @see self::sign() * @see self::setHash() */ private $flags = 0; /** * Comment * * @var null|string */ private $comment; /** * Curve Aliases * * @var array */ private static $curveAliases = [ 'secp256r1' => 'nistp256', 'secp384r1' => 'nistp384', 'secp521r1' => 'nistp521', 'Ed25519' => 'Ed25519' ]; /** * Default Constructor. * * @param resource $fsock */ public function __construct($fsock) { $this->fsock = $fsock; } /** * Set Public Key * * Called by \phpseclib3\System\SSH\Agent::requestIdentities() * * @param PublicKey $key */ public function withPublicKey(PublicKey $key) { if ($key instanceof EC) { if (is_array($key->getCurve()) || !isset(self::$curveAliases[$key->getCurve()])) { throw new UnsupportedAlgorithmException('The only supported curves are nistp256, nistp384, nistp512 and Ed25519'); } } $new = clone $this; $new->key = $key; return $new; } /** * Set Public Key * * Called by \phpseclib3\System\SSH\Agent::requestIdentities(). The key blob could be extracted from $this->key * but this saves a small amount of computation. * * @param string $key_blob */ public function withPublicKeyBlob($key_blob) { $new = clone $this; $new->key_blob = $key_blob; return $new; } /** * Get Public Key * * Wrapper for $this->key->getPublicKey() * * @return mixed */ public function getPublicKey() { return $this->key; } /** * Sets the hash * * @param string $hash */ public function withHash($hash) { $new = clone $this; $hash = strtolower($hash); if ($this->key instanceof RSA) { $new->flags = 0; switch ($hash) { case 'sha1': break; case 'sha256': $new->flags = self::SSH_AGENT_RSA2_256; break; case 'sha512': $new->flags = self::SSH_AGENT_RSA2_512; break; default: throw new UnsupportedAlgorithmException('The only supported hashes for RSA are sha1, sha256 and sha512'); } } if ($this->key instanceof EC) { switch ($this->key->getCurve()) { case 'secp256r1': $expectedHash = 'sha256'; break; case 'secp384r1': $expectedHash = 'sha384'; break; //case 'secp521r1': //case 'Ed25519': default: $expectedHash = 'sha512'; } if ($hash != $expectedHash) { throw new UnsupportedAlgorithmException('The only supported hash for ' . self::$curveAliases[$this->key->getCurve()] . ' is ' . $expectedHash); } } if ($this->key instanceof DSA) { if ($hash != 'sha1') { throw new UnsupportedAlgorithmException('The only supported hash for DSA is sha1'); } } return $new; } /** * Sets the padding * * Only PKCS1 padding is supported * * @param string $padding */ public function withPadding($padding) { if (!$this->key instanceof RSA) { throw new UnsupportedAlgorithmException('Only RSA keys support padding'); } if ($padding != RSA::SIGNATURE_PKCS1 && $padding != RSA::SIGNATURE_RELAXED_PKCS1) { throw new UnsupportedAlgorithmException('ssh-agent can only create PKCS1 signatures'); } return $this; } /** * Determines the signature padding mode * * Valid values are: ASN1, SSH2, Raw * * @param string $format */ public function withSignatureFormat($format) { if ($this->key instanceof RSA) { throw new UnsupportedAlgorithmException('Only DSA and EC keys support signature format setting'); } if ($format != 'SSH2') { throw new UnsupportedAlgorithmException('Only SSH2-formatted signatures are currently supported'); } return $this; } /** * Returns the curve * * Returns a string if it's a named curve, an array if not * * @return string|array */ public function getCurve() { if (!$this->key instanceof EC) { throw new UnsupportedAlgorithmException('Only EC keys have curves'); } return $this->key->getCurve(); } /** * Create a signature * * See "2.6.2 Protocol 2 private key signature request" * * @param string $message * @return string * @throws \RuntimeException on connection errors * @throws UnsupportedAlgorithmException if the algorithm is unsupported */ public function sign($message) { // the last parameter (currently 0) is for flags and ssh-agent only defines one flag (for ssh-dss): SSH_AGENT_OLD_SIGNATURE $packet = Strings::packSSH2( 'CssN', Agent::SSH_AGENTC_SIGN_REQUEST, $this->key_blob, $message, $this->flags ); $packet = Strings::packSSH2('s', $packet); if (strlen($packet) != fputs($this->fsock, $packet)) { throw new \RuntimeException('Connection closed during signing'); } $length = current(unpack('N', $this->readBytes(4))); $packet = $this->readBytes($length); list($type, $signature_blob) = Strings::unpackSSH2('Cs', $packet); if ($type != Agent::SSH_AGENT_SIGN_RESPONSE) { throw new \RuntimeException('Unable to retrieve signature'); } if (!$this->key instanceof RSA) { return $signature_blob; } list($type, $signature_blob) = Strings::unpackSSH2('ss', $signature_blob); return $signature_blob; } /** * Returns the private key * * @param string $type * @param array $options optional * @return string */ public function toString($type, array $options = []) { throw new \RuntimeException('ssh-agent does not provide a mechanism to get the private key'); } /** * Sets the password * * @param string|bool $password * @return never */ public function withPassword($password = false) { throw new \RuntimeException('ssh-agent does not provide a mechanism to get the private key'); } /** * Sets the comment */ public function withComment($comment = null) { $new = clone $this; $new->comment = $comment; return $new; } /** * Returns the comment * * @return null|string */ public function getComment() { return $this->comment; } } PK!iKvendor/phpseclib/phpseclib/phpseclib/System/SSH/Common/Traits/ReadBytes.phpnu[ * @copyright 2015 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\System\SSH\Common\Traits; /** * ReadBytes trait * * @author Jim Wigginton */ trait ReadBytes { /** * Read data * * @param int $length * @throws \RuntimeException on connection errors */ public function readBytes($length) { $temp = fread($this->fsock, $length); if (strlen($temp) != $length) { throw new \RuntimeException("Expected $length bytes; got " . strlen($temp)); } return $temp; } } PK!0GMm.#.#9vendor/phpseclib/phpseclib/phpseclib/System/SSH/Agent.phpnu[ * login('username', $agent)) { * exit('Login Failed'); * } * * echo $ssh->exec('pwd'); * echo $ssh->exec('ls -la'); * ?> * * * @author Jim Wigginton * @copyright 2014 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ namespace phpseclib3\System\SSH; use phpseclib3\Common\Functions\Strings; use phpseclib3\Crypt\Common\PublicKey; use phpseclib3\Crypt\PublicKeyLoader; use phpseclib3\Crypt\RSA; use phpseclib3\Exception\BadConfigurationException; use phpseclib3\Net\SSH2; use phpseclib3\System\SSH\Agent\Identity; /** * Pure-PHP ssh-agent client identity factory * * requestIdentities() method pumps out \phpseclib3\System\SSH\Agent\Identity objects * * @author Jim Wigginton */ class Agent { use Common\Traits\ReadBytes; // Message numbers // to request SSH1 keys you have to use SSH_AGENTC_REQUEST_RSA_IDENTITIES (1) const SSH_AGENTC_REQUEST_IDENTITIES = 11; // this is the SSH2 response; the SSH1 response is SSH_AGENT_RSA_IDENTITIES_ANSWER (2). const SSH_AGENT_IDENTITIES_ANSWER = 12; // the SSH1 request is SSH_AGENTC_RSA_CHALLENGE (3) const SSH_AGENTC_SIGN_REQUEST = 13; // the SSH1 response is SSH_AGENT_RSA_RESPONSE (4) const SSH_AGENT_SIGN_RESPONSE = 14; // Agent forwarding status // no forwarding requested and not active const FORWARD_NONE = 0; // request agent forwarding when opportune const FORWARD_REQUEST = 1; // forwarding has been request and is active const FORWARD_ACTIVE = 2; /** * Unused */ const SSH_AGENT_FAILURE = 5; /** * Socket Resource * * @var resource */ private $fsock; /** * Agent forwarding status * * @var int */ private $forward_status = self::FORWARD_NONE; /** * Buffer for accumulating forwarded authentication * agent data arriving on SSH data channel destined * for agent unix socket * * @var string */ private $socket_buffer = ''; /** * Tracking the number of bytes we are expecting * to arrive for the agent socket on the SSH data * channel * * @var int */ private $expected_bytes = 0; /** * Default Constructor * * @return Agent * @throws BadConfigurationException if SSH_AUTH_SOCK cannot be found * @throws \RuntimeException on connection errors */ public function __construct($address = null) { if (!$address) { switch (true) { case isset($_SERVER['SSH_AUTH_SOCK']): $address = $_SERVER['SSH_AUTH_SOCK']; break; case isset($_ENV['SSH_AUTH_SOCK']): $address = $_ENV['SSH_AUTH_SOCK']; break; default: throw new BadConfigurationException('SSH_AUTH_SOCK not found'); } } if (in_array('unix', stream_get_transports())) { $this->fsock = fsockopen('unix://' . $address, 0, $errno, $errstr); if (!$this->fsock) { throw new \RuntimeException("Unable to connect to ssh-agent (Error $errno: $errstr)"); } } else { if (substr($address, 0, 9) != '\\\\.\\pipe\\' || strpos(substr($address, 9), '\\') !== false) { throw new \RuntimeException('Address is not formatted as a named pipe should be'); } $this->fsock = fopen($address, 'r+b'); if (!$this->fsock) { throw new \RuntimeException('Unable to open address'); } } } /** * Request Identities * * See "2.5.2 Requesting a list of protocol 2 keys" * Returns an array containing zero or more \phpseclib3\System\SSH\Agent\Identity objects * * @return array * @throws \RuntimeException on receipt of unexpected packets */ public function requestIdentities() { if (!$this->fsock) { return []; } $packet = pack('NC', 1, self::SSH_AGENTC_REQUEST_IDENTITIES); if (strlen($packet) != fputs($this->fsock, $packet)) { throw new \RuntimeException('Connection closed while requesting identities'); } $length = current(unpack('N', $this->readBytes(4))); $packet = $this->readBytes($length); list($type, $keyCount) = Strings::unpackSSH2('CN', $packet); if ($type != self::SSH_AGENT_IDENTITIES_ANSWER) { throw new \RuntimeException('Unable to request identities'); } $identities = []; for ($i = 0; $i < $keyCount; $i++) { list($key_blob, $comment) = Strings::unpackSSH2('ss', $packet); $temp = $key_blob; list($key_type) = Strings::unpackSSH2('s', $temp); switch ($key_type) { case 'ssh-rsa': case 'ssh-dss': case 'ssh-ed25519': case 'ecdsa-sha2-nistp256': case 'ecdsa-sha2-nistp384': case 'ecdsa-sha2-nistp521': $key = PublicKeyLoader::load($key_type . ' ' . base64_encode($key_blob)); } // resources are passed by reference by default if (isset($key)) { $identity = (new Identity($this->fsock)) ->withPublicKey($key) ->withPublicKeyBlob($key_blob) ->withComment($comment); $identities[] = $identity; unset($key); } } return $identities; } /** * Returns the SSH Agent identity matching a given public key or null if no identity is found * * @return ?Identity */ public function findIdentityByPublicKey(PublicKey $key) { $identities = $this->requestIdentities(); $key = (string) $key; foreach ($identities as $identity) { if (((string) $identity->getPublicKey()) == $key) { return $identity; } } return null; } /** * Signal that agent forwarding should * be requested when a channel is opened * * @return void */ public function startSSHForwarding() { if ($this->forward_status == self::FORWARD_NONE) { $this->forward_status = self::FORWARD_REQUEST; } } /** * Request agent forwarding of remote server * * @param SSH2 $ssh * @return bool */ private function request_forwarding(SSH2 $ssh) { if (!$ssh->requestAgentForwarding()) { return false; } $this->forward_status = self::FORWARD_ACTIVE; return true; } /** * On successful channel open * * This method is called upon successful channel * open to give the SSH Agent an opportunity * to take further action. i.e. request agent forwarding * * @param SSH2 $ssh */ public function registerChannelOpen(SSH2 $ssh) { if ($this->forward_status == self::FORWARD_REQUEST) { $this->request_forwarding($ssh); } } /** * Forward data to SSH Agent and return data reply * * @param string $data * @return string Data from SSH Agent * @throws \RuntimeException on connection errors */ public function forwardData($data) { if ($this->expected_bytes > 0) { $this->socket_buffer .= $data; $this->expected_bytes -= strlen($data); } else { $agent_data_bytes = current(unpack('N', $data)); $current_data_bytes = strlen($data); $this->socket_buffer = $data; if ($current_data_bytes != $agent_data_bytes + 4) { $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes; return false; } } if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) { throw new \RuntimeException('Connection closed attempting to forward data to SSH agent'); } $this->socket_buffer = ''; $this->expected_bytes = 0; $agent_reply_bytes = current(unpack('N', $this->readBytes(4))); $agent_reply_data = $this->readBytes($agent_reply_bytes); $agent_reply_data = current(unpack('a*', $agent_reply_data)); return pack('Na*', $agent_reply_bytes, $agent_reply_data); } } PK!y2vendor/phpseclib/phpseclib/phpseclib/bootstrap.phpnu[%vendor/phpseclib/phpseclib/BACKERS.mdnu[# Backers phpseclib ongoing development is made possible by [Tidelift](https://tidelift.com/subscription/pkg/packagist-phpseclib-phpseclib?utm_source=packagist-phpseclib-phpseclib&utm_medium=referral&utm_campaign=readme) and by contributions by users like you. Thank you. ## Backers - Allan Simon - [ChargeOver](https://chargeover.com/) - Raghu Veer Dendukuri - Zane Hooper - [Setasign](https://www.setasign.com/) - [Charles Severance](https://github.com/csev) - [Rachel Fish](https://github.com/itsrachelfish) - Tharyrok - [cjhaas](https://github.com/cjhaas) - [istiak-tridip](https://github.com/istiak-tridip) - [Anna Filina](https://github.com/afilina) - [blakemckeeby](https://github.com/blakemckeeby) - [ssddanbrown](https://github.com/ssddanbrown)PK!C (vendor/phpseclib/phpseclib/composer.jsonnu[{ "name": "phpseclib/phpseclib", "type": "library", "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", "keywords": [ "security", "crypto", "cryptography", "encryption", "signature", "signing", "rsa", "aes", "blowfish", "twofish", "ssh", "sftp", "x509", "x.509", "asn1", "asn.1", "BigInteger" ], "homepage": "http://phpseclib.sourceforge.net", "license": "MIT", "authors": [ { "name": "Jim Wigginton", "email": "terrafrost@php.net", "role": "Lead Developer" }, { "name": "Patrick Monnerat", "email": "pm@datasphere.ch", "role": "Developer" }, { "name": "Andreas Fischer", "email": "bantu@phpbb.com", "role": "Developer" }, { "name": "Hans-Jürgen Petrich", "email": "petrich@tronic-media.com", "role": "Developer" }, { "name": "Graham Campbell", "email": "graham@alt-three.com", "role": "Developer" } ], "require": { "php": ">=5.6.1", "paragonie/constant_time_encoding": "^1|^2|^3", "paragonie/random_compat": "^1.4|^2.0|^9.99.99" }, "require-dev": { "phpunit/phpunit": "*" }, "suggest": { "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.", "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", "ext-dom": "Install the DOM extension to load XML formatted public keys." }, "autoload": { "files": [ "phpseclib/bootstrap.php" ], "psr-4": { "phpseclib3\\": "phpseclib/" } }, "autoload-dev": { "psr-4": { "phpseclib3\\Tests\\": "tests/" } }, "config": { "sort-packages": true } } PK!s=j99"vendor/phpseclib/phpseclib/LICENSEnu[Copyright (c) 2011-2019 TerraFrost and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!֞F5 5 $vendor/phpseclib/phpseclib/README.mdnu[# phpseclib - PHP Secure Communications Library [![CI Status](https://github.com/phpseclib/phpseclib/actions/workflows/ci.yml/badge.svg?branch=3.0&event=push "CI Status")](https://github.com/phpseclib/phpseclib) ## Supporting phpseclib - [Become a backer or sponsor on Patreon](https://www.patreon.com/phpseclib) - [One-time donation via PayPal or crypto-currencies](http://sourceforge.net/donate/index.php?group_id=198487) - [Subscribe to Tidelift](https://tidelift.com/subscription/pkg/packagist-phpseclib-phpseclib?utm_source=packagist-phpseclib-phpseclib&utm_medium=referral&utm_campaign=readme) ## Introduction MIT-licensed pure-PHP implementations of the following: SSH-2, SFTP, X.509, an arbitrary-precision integer arithmetic library, Ed25519 / Ed449 / Curve25519 / Curve449, ECDSA / ECDH (with support for 66 curves), RSA (PKCS#1 v2.2 compliant), DSA / DH, DES / 3DES / RC4 / Rijndael / AES / Blowfish / Twofish / Salsa20 / ChaCha20, GCM / Poly1305 * [Browse Git](https://github.com/phpseclib/phpseclib) ## Documentation * [Documentation / Manual](https://phpseclib.com/) * [API Documentation](https://api.phpseclib.com/3.0/) (generated by Doctum) ## Branches ### master * Development Branch * Unstable API * Do not use in production ### 3.0 * Long term support (LTS) release * Major expansion of cryptographic primitives * Minimum PHP version: 5.6.1 * PSR-4 autoloading with namespace rooted at `\phpseclib3` * Install via Composer: `composer require phpseclib/phpseclib:~3.0` ### 2.0 * Long term support (LTS) release * Modernized version of 1.0 * Minimum PHP version: 5.3.3 * PSR-4 autoloading with namespace rooted at `\phpseclib` * Install via Composer: `composer require phpseclib/phpseclib:~2.0` ### 1.0 * Long term support (LTS) release * PHP4 compatible * Composer compatible (PSR-0 autoloading) * Install using Composer: `composer require phpseclib/phpseclib:~1.0` * [Download 1.0.23 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.23.zip/download) ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ## Support Need Support? * [Checkout Questions and Answers on Stack Overflow](http://stackoverflow.com/questions/tagged/phpseclib) * [Create a Support Ticket on GitHub](https://github.com/phpseclib/phpseclib/issues/new) * [Browse the Support Forum](http://www.frostjedi.com/phpbb/viewforum.php?f=46) (no longer in use) ## Special Thanks Special Thanks to our $50+ sponsors!: - Allan Simon - [ChargeOver](https://chargeover.com/) ## Contributing 1. Fork the Project 2. Ensure you have Composer installed (see [Composer Download Instructions](https://getcomposer.org/download/)) 3. Install Development Dependencies ```sh composer install ``` 4. Create a Feature Branch 5. Run continuous integration checks: ```sh composer global require php:^8.1 squizlabs/php_codesniffer friendsofphp/php-cs-fixer vimeo/psalm phpcs --standard=build/php_codesniffer.xml php-cs-fixer fix --config=build/php-cs-fixer.php --diff --dry-run --using-cache=no psalm --config=build/psalm.xml --no-cache --long-progress --report-show-info=false --output-format=text vendor/bin/phpunit --verbose --configuration tests/phpunit.xml ``` 6. Send us a Pull Request PK!K'vendor/psr/cache/src/CacheException.phpnu[=5.3.0" }, "autoload": { "psr-4": { "Psr\\Cache\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } } PK!Df88vendor/psr/cache/LICENSE.txtnu[Copyright (c) 2015 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!mvendor/psr/cache/README.mdnu[PSR Cache ========= This repository holds all interfaces defined by [PSR-6](http://www.php-fig.org/psr/psr-6/). Note that this is not a Cache implementation of its own. It is merely an interface that describes a Cache implementation. See the specification for more details. PK!18vendor/psr/container/src/ContainerExceptionInterface.phpnu[=7.4.0" }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" } } } PK!ӷ}d%%vendor/psr/container/.gitignorenu[composer.lock composer.phar /vendor/ PK!Opyyvendor/psr/container/LICENSEnu[The MIT License (MIT) Copyright (c) 2013-2016 container-interop Copyright (c) 2016 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!g?BBvendor/psr/container/README.mdnu[Container interface ============== This repository holds all interfaces related to [PSR-11 (Container Interface)][psr-url]. Note that this is not a Container implementation of its own. It is merely abstractions that describe the components of a Dependency Injection Container. The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. [psr-url]: https://www.php-fig.org/psr/psr-11/ [package-url]: https://packagist.org/packages/psr/container [implementation-url]: https://packagist.org/providers/psr/container-implementation PK!7vendor/psr/http-client/src/ClientExceptionInterface.phpnu[UU8vendor/psr/http-factory/src/ResponseFactoryInterface.phpnu[=7.1", "psr/http-message": "^1.0 || ^2.0" }, "autoload": { "psr-4": { "Psr\\Http\\Message\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } } PK!}]((vendor/psr/http-factory/LICENSEnu[MIT License Copyright (c) 2018 PHP-FIG Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!zwf,,!vendor/psr/http-factory/README.mdnu[HTTP Factories ============== This repository holds all interfaces related to [PSR-17 (HTTP Factories)][psr-url]. Note that this is not a HTTP Factory implementation of its own. It is merely interfaces that describe the components of a HTTP Factory. The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. [psr-url]: https://www.php-fig.org/psr/psr-17/ [package-url]: https://packagist.org/packages/psr/http-factory [implementation-url]: https://packagist.org/providers/psr/http-factory-implementation PK!6L%L%/vendor/psr/http-message/docs/PSR7-Interfaces.mdnu[# Interfaces The purpose of this list is to help in finding the methods when working with PSR-7. This can be considered as a cheatsheet for PSR-7 interfaces. The interfaces defined in PSR-7 are the following: | Class Name | Description | |---|---| | [Psr\Http\Message\MessageInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagemessageinterface) | Representation of a HTTP message | | [Psr\Http\Message\RequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagerequestinterface) | Representation of an outgoing, client-side request. | | [Psr\Http\Message\ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageserverrequestinterface) | Representation of an incoming, server-side HTTP request. | | [Psr\Http\Message\ResponseInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageresponseinterface) | Representation of an outgoing, server-side response. | | [Psr\Http\Message\StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) | Describes a data stream | | [Psr\Http\Message\UriInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuriinterface) | Value object representing a URI. | | [Psr\Http\Message\UploadedFileInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuploadedfileinterface) | Value object representing a file uploaded through an HTTP request. | ## `Psr\Http\Message\MessageInterface` Methods | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `getProtocolVersion()` | Retrieve HTTP protocol version | 1.0 or 1.1 | | `withProtocolVersion($version)` | Returns new message instance with given HTTP protocol version | | | `getHeaders()` | Retrieve all HTTP Headers | [Request Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields), [Response Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields) | | `hasHeader($name)` | Checks if HTTP Header with given name exists | | | `getHeader($name)` | Retrieves a array with the values for a single header | | | `getHeaderLine($name)` | Retrieves a comma-separated string of the values for a single header | | | `withHeader($name, $value)` | Returns new message instance with given HTTP Header | if the header existed in the original instance, replaces the header value from the original message with the value provided when creating the new instance. | | `withAddedHeader($name, $value)` | Returns new message instance with appended value to given header | If header already exists value will be appended, if not a new header will be created | | `withoutHeader($name)` | Removes HTTP Header with given name| | | `getBody()` | Retrieves the HTTP Message Body | Returns object implementing `StreamInterface`| | `withBody(StreamInterface $body)` | Returns new message instance with given HTTP Message Body | | ## `Psr\Http\Message\RequestInterface` Methods Same methods as `Psr\Http\Message\MessageInterface` + the following methods: | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `getRequestTarget()` | Retrieves the message's request target | origin-form, absolute-form, authority-form, asterisk-form ([RFC7230](https://www.rfc-editor.org/rfc/rfc7230.txt)) | | `withRequestTarget($requestTarget)` | Return a new message instance with the specific request-target | | | `getMethod()` | Retrieves the HTTP method of the request. | GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE (defined in [RFC7231](https://tools.ietf.org/html/rfc7231)), PATCH (defined in [RFC5789](https://tools.ietf.org/html/rfc5789)) | | `withMethod($method)` | Returns a new message instance with the provided HTTP method | | | `getUri()` | Retrieves the URI instance | | | `withUri(UriInterface $uri, $preserveHost = false)` | Returns a new message instance with the provided URI | | ## `Psr\Http\Message\ServerRequestInterface` Methods Same methods as `Psr\Http\Message\RequestInterface` + the following methods: | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `getServerParams() ` | Retrieve server parameters | Typically derived from `$_SERVER` | | `getCookieParams()` | Retrieves cookies sent by the client to the server. | Typically derived from `$_COOKIES` | | `withCookieParams(array $cookies)` | Returns a new request instance with the specified cookies | | | `withQueryParams(array $query)` | Returns a new request instance with the specified query string arguments | | | `getUploadedFiles()` | Retrieve normalized file upload data | | | `withUploadedFiles(array $uploadedFiles)` | Returns a new request instance with the specified uploaded files | | | `getParsedBody()` | Retrieve any parameters provided in the request body | | | `withParsedBody($data)` | Returns a new request instance with the specified body parameters | | | `getAttributes()` | Retrieve attributes derived from the request | | | `getAttribute($name, $default = null)` | Retrieve a single derived request attribute | | | `withAttribute($name, $value)` | Returns a new request instance with the specified derived request attribute | | | `withoutAttribute($name)` | Returns a new request instance that without the specified derived request attribute | | ## `Psr\Http\Message\ResponseInterface` Methods: Same methods as `Psr\Http\Message\MessageInterface` + the following methods: | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `getStatusCode()` | Gets the response status code. | | | `withStatus($code, $reasonPhrase = '')` | Returns a new response instance with the specified status code and, optionally, reason phrase. | | | `getReasonPhrase()` | Gets the response reason phrase associated with the status code. | | ## `Psr\Http\Message\StreamInterface` Methods | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `__toString()` | Reads all data from the stream into a string, from the beginning to end. | | | `close()` | Closes the stream and any underlying resources. | | | `detach()` | Separates any underlying resources from the stream. | | | `getSize()` | Get the size of the stream if known. | | | `eof()` | Returns true if the stream is at the end of the stream.| | | `isSeekable()` | Returns whether or not the stream is seekable. | | | `seek($offset, $whence = SEEK_SET)` | Seek to a position in the stream. | | | `rewind()` | Seek to the beginning of the stream. | | | `isWritable()` | Returns whether or not the stream is writable. | | | `write($string)` | Write data to the stream. | | | `isReadable()` | Returns whether or not the stream is readable. | | | `read($length)` | Read data from the stream. | | | `getContents()` | Returns the remaining contents in a string | | | `getMetadata($key = null)()` | Get stream metadata as an associative array or retrieve a specific key. | | ## `Psr\Http\Message\UriInterface` Methods | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `getScheme()` | Retrieve the scheme component of the URI. | | | `getAuthority()` | Retrieve the authority component of the URI. | | | `getUserInfo()` | Retrieve the user information component of the URI. | | | `getHost()` | Retrieve the host component of the URI. | | | `getPort()` | Retrieve the port component of the URI. | | | `getPath()` | Retrieve the path component of the URI. | | | `getQuery()` | Retrieve the query string of the URI. | | | `getFragment()` | Retrieve the fragment component of the URI. | | | `withScheme($scheme)` | Return an instance with the specified scheme. | | | `withUserInfo($user, $password = null)` | Return an instance with the specified user information. | | | `withHost($host)` | Return an instance with the specified host. | | | `withPort($port)` | Return an instance with the specified port. | | | `withPath($path)` | Return an instance with the specified path. | | | `withQuery($query)` | Return an instance with the specified query string. | | | `withFragment($fragment)` | Return an instance with the specified URI fragment. | | | `__toString()` | Return the string representation as a URI reference. | | ## `Psr\Http\Message\UploadedFileInterface` Methods | Method Name | Description | Notes | |------------------------------------| ----------- | ----- | | `getStream()` | Retrieve a stream representing the uploaded file. | | | `moveTo($targetPath)` | Move the uploaded file to a new location. | | | `getSize()` | Retrieve the file size. | | | `getError()` | Retrieve the error associated with the uploaded file. | | | `getClientFilename()` | Retrieve the filename sent by the client. | | | `getClientMediaType()` | Retrieve the media type sent by the client. | | > `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. > When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. PK!z Xtt*vendor/psr/http-message/docs/PSR7-Usage.mdnu[### PSR-7 Usage All PSR-7 applications comply with these interfaces They were created to establish a standard between middleware implementations. > `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. > When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. The following examples will illustrate how basic operations are done in PSR-7. ##### Examples For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc) All PSR-7 implementations should have the same behaviour. The following will be assumed: `$request` is an object of `Psr\Http\Message\RequestInterface` and `$response` is an object implementing `Psr\Http\Message\RequestInterface` ### Working with HTTP Headers #### Adding headers to response: ```php $response->withHeader('My-Custom-Header', 'My Custom Message'); ``` #### Appending values to headers ```php $response->withAddedHeader('My-Custom-Header', 'The second message'); ``` #### Checking if header exists: ```php $request->hasHeader('My-Custom-Header'); // will return false $response->hasHeader('My-Custom-Header'); // will return true ``` > Note: My-Custom-Header was only added in the Response #### Getting comma-separated values from a header (also applies to request) ```php // getting value from request headers $request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8" // getting value from response headers $response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message" ``` #### Getting array of value from a header (also applies to request) ```php // getting value from request headers $request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"] // getting value from response headers $response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"] ``` #### Removing headers from HTTP Messages ```php // removing a header from Request, removing deprecated "Content-MD5" header $request->withoutHeader('Content-MD5'); // removing a header from Response // effect: the browser won't know the size of the stream // the browser will download the stream till it ends $response->withoutHeader('Content-Length'); ``` ### Working with HTTP Message Body When working with the PSR-7 there are two methods of implementation: #### 1. Getting the body separately > This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented. ```php $body = $response->getBody(); // operations on body, eg. read, write, seek // ... // replacing the old body $response->withBody($body); // this last statement is optional as we working with objects // in this case the "new" body is same with the "old" one // the $body variable has the same value as the one in $request, only the reference is passed ``` #### 2. Working directly on response > This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required ```php $response->getBody()->write('hello'); ``` ### Getting the body contents The following snippet gets the contents of a stream contents. > Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream. ```php $body = $response->getBody(); $body->rewind(); // or $body->seek(0); $bodyText = $body->getContents(); ``` > Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended. ### Append to body ```php $response->getBody()->write('Hello'); // writing directly $body = $request->getBody(); // which is a `StreamInterface` $body->write('xxxxx'); ``` ### Prepend to body Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended. The following example will explain the behaviour of streams. ```php // assuming our response is initially empty $body = $repsonse->getBody(); // writing the string "abcd" $body->write('abcd'); // seeking to start of stream $body->seek(0); // writing 'ef' $body->write('ef'); // at this point the stream contains "efcd" ``` #### Prepending by rewriting separately ```php // assuming our response body stream only contains: "abcd" $body = $response->getBody(); $body->rewind(); $contents = $body->getContents(); // abcd // seeking the stream to beginning $body->rewind(); $body->write('ef'); // stream contains "efcd" $body->write($contents); // stream contains "efabcd" ``` > Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`. #### Prepending by using contents as a string ```php $body = $response->getBody(); $body->rewind(); $contents = $body->getContents(); // efabcd $contents = 'ef'.$contents; $body->rewind(); $body->write($contents); ``` PK!9R0vendor/psr/http-message/src/MessageInterface.phpnu[getHeaders() as $name => $values) { * echo $name . ": " . implode(", ", $values); * } * * // Emit headers iteratively: * foreach ($message->getHeaders() as $name => $values) { * foreach ($values as $value) { * header(sprintf('%s: %s', $name, $value), false); * } * } * * While header names are not case-sensitive, getHeaders() will preserve the * exact case in which headers were originally specified. * * @return string[][] Returns an associative array of the message's headers. Each * key MUST be a header name, and each value MUST be an array of strings * for that header. */ public function getHeaders(): array; /** * Checks if a header exists by the given case-insensitive name. * * @param string $name Case-insensitive header field name. * @return bool Returns true if any header names match the given header * name using a case-insensitive string comparison. Returns false if * no matching header name is found in the message. */ public function hasHeader(string $name): bool; /** * Retrieves a message header value by the given case-insensitive name. * * This method returns an array of all the header values of the given * case-insensitive header name. * * If the header does not appear in the message, this method MUST return an * empty array. * * @param string $name Case-insensitive header field name. * @return string[] An array of string values as provided for the given * header. If the header does not appear in the message, this method MUST * return an empty array. */ public function getHeader(string $name): array; /** * Retrieves a comma-separated string of the values for a single header. * * This method returns all of the header values of the given * case-insensitive header name as a string concatenated together using * a comma. * * NOTE: Not all header values may be appropriately represented using * comma concatenation. For such headers, use getHeader() instead * and supply your own delimiter when concatenating. * * If the header does not appear in the message, this method MUST return * an empty string. * * @param string $name Case-insensitive header field name. * @return string A string of values as provided for the given header * concatenated together using a comma. If the header does not appear in * the message, this method MUST return an empty string. */ public function getHeaderLine(string $name): string; /** * Return an instance with the provided value replacing the specified header. * * While header names are case-insensitive, the casing of the header will * be preserved by this function, and returned from getHeaders(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new and/or updated header and value. * * @param string $name Case-insensitive header field name. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withHeader(string $name, $value): MessageInterface; /** * Return an instance with the specified header appended with the given value. * * Existing values for the specified header will be maintained. The new * value(s) will be appended to the existing list. If the header did not * exist previously, it will be added. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new header and/or value. * * @param string $name Case-insensitive header field name to add. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withAddedHeader(string $name, $value): MessageInterface; /** * Return an instance without the specified header. * * Header resolution MUST be done without case-sensitivity. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the named header. * * @param string $name Case-insensitive header field name to remove. * @return static */ public function withoutHeader(string $name): MessageInterface; /** * Gets the body of the message. * * @return StreamInterface Returns the body as a stream. */ public function getBody(): StreamInterface; /** * Return an instance with the specified message body. * * The body MUST be a StreamInterface object. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return a new instance that has the * new body stream. * * @param StreamInterface $body Body. * @return static * @throws \InvalidArgumentException When the body is not valid. */ public function withBody(StreamInterface $body): MessageInterface; } PK!jj0vendor/psr/http-message/src/RequestInterface.phpnu[getQuery()` * or from the `QUERY_STRING` server param. * * @return array */ public function getQueryParams(): array; /** * Return an instance with the specified query string arguments. * * These values SHOULD remain immutable over the course of the incoming * request. They MAY be injected during instantiation, such as from PHP's * $_GET superglobal, or MAY be derived from some other value such as the * URI. In cases where the arguments are parsed from the URI, the data * MUST be compatible with what PHP's parse_str() would return for * purposes of how duplicate query parameters are handled, and how nested * sets are handled. * * Setting query string arguments MUST NOT change the URI stored by the * request, nor the values in the server params. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated query string arguments. * * @param array $query Array of query string arguments, typically from * $_GET. * @return static */ public function withQueryParams(array $query): ServerRequestInterface; /** * Retrieve normalized file upload data. * * This method returns upload metadata in a normalized tree, with each leaf * an instance of Psr\Http\Message\UploadedFileInterface. * * These values MAY be prepared from $_FILES or the message body during * instantiation, or MAY be injected via withUploadedFiles(). * * @return array An array tree of UploadedFileInterface instances; an empty * array MUST be returned if no data is present. */ public function getUploadedFiles(): array; /** * Create a new instance with the specified uploaded files. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param array $uploadedFiles An array tree of UploadedFileInterface instances. * @return static * @throws \InvalidArgumentException if an invalid structure is provided. */ public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; /** * Retrieve any parameters provided in the request body. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, this method MUST * return the contents of $_POST. * * Otherwise, this method may return any results of deserializing * the request body content; as parsing returns structured content, the * potential types MUST be arrays or objects only. A null value indicates * the absence of body content. * * @return null|array|object The deserialized body parameters, if any. * These will typically be an array or object. */ public function getParsedBody(); /** * Return an instance with the specified body parameters. * * These MAY be injected during instantiation. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, use this method * ONLY to inject the contents of $_POST. * * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of * deserializing the request body content. Deserialization/parsing returns * structured data, and, as such, this method ONLY accepts arrays or objects, * or a null value if nothing was available to parse. * * As an example, if content negotiation determines that the request data * is a JSON payload, this method could be used to create a request * instance with the deserialized parameters. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param null|array|object $data The deserialized body data. This will * typically be in an array or object. * @return static * @throws \InvalidArgumentException if an unsupported argument type is * provided. */ public function withParsedBody($data): ServerRequestInterface; /** * Retrieve attributes derived from the request. * * The request "attributes" may be used to allow injection of any * parameters derived from the request: e.g., the results of path * match operations; the results of decrypting cookies; the results of * deserializing non-form-encoded message bodies; etc. Attributes * will be application and request specific, and CAN be mutable. * * @return array Attributes derived from the request. */ public function getAttributes(): array; /** * Retrieve a single derived request attribute. * * Retrieves a single derived request attribute as described in * getAttributes(). If the attribute has not been previously set, returns * the default value as provided. * * This method obviates the need for a hasAttribute() method, as it allows * specifying a default value to return if the attribute is not found. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $default Default value to return if the attribute does not exist. * @return mixed */ public function getAttribute(string $name, $default = null); /** * Return an instance with the specified derived request attribute. * * This method allows setting a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated attribute. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $value The value of the attribute. * @return static */ public function withAttribute(string $name, $value): ServerRequestInterface; /** * Return an instance that removes the specified derived request attribute. * * This method allows removing a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the attribute. * * @see getAttributes() * @param string $name The attribute name. * @return static */ public function withoutAttribute(string $name): ServerRequestInterface; } PK!9///vendor/psr/http-message/src/StreamInterface.phpnu[ * [user-info@]host[:port] * * * If the port component is not set or is the standard port for the current * scheme, it SHOULD NOT be included. * * @see https://tools.ietf.org/html/rfc3986#section-3.2 * @return string The URI authority, in "[user-info@]host[:port]" format. */ public function getAuthority(): string; /** * Retrieve the user information component of the URI. * * If no user information is present, this method MUST return an empty * string. * * If a user is present in the URI, this will return that value; * additionally, if the password is also present, it will be appended to the * user value, with a colon (":") separating the values. * * The trailing "@" character is not part of the user information and MUST * NOT be added. * * @return string The URI user information, in "username[:password]" format. */ public function getUserInfo(): string; /** * Retrieve the host component of the URI. * * If no host is present, this method MUST return an empty string. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.2.2. * * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 * @return string The URI host. */ public function getHost(): string; /** * Retrieve the port component of the URI. * * If a port is present, and it is non-standard for the current scheme, * this method MUST return it as an integer. If the port is the standard port * used with the current scheme, this method SHOULD return null. * * If no port is present, and no scheme is present, this method MUST return * a null value. * * If no port is present, but a scheme is present, this method MAY return * the standard port for that scheme, but SHOULD return null. * * @return null|int The URI port. */ public function getPort(): ?int; /** * Retrieve the path component of the URI. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Normally, the empty path "" and absolute path "/" are considered equal as * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically * do this normalization because in contexts with a trimmed base path, e.g. * the front controller, this difference becomes significant. It's the task * of the user to handle both "" and "/". * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.3. * * As an example, if the value should include a slash ("/") not intended as * delimiter between path segments, that value MUST be passed in encoded * form (e.g., "%2F") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.3 * @return string The URI path. */ public function getPath(): string; /** * Retrieve the query string of the URI. * * If no query string is present, this method MUST return an empty string. * * The leading "?" character is not part of the query and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.4. * * As an example, if a value in a key/value pair of the query string should * include an ampersand ("&") not intended as a delimiter between values, * that value MUST be passed in encoded form (e.g., "%26") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.4 * @return string The URI query string. */ public function getQuery(): string; /** * Retrieve the fragment component of the URI. * * If no fragment is present, this method MUST return an empty string. * * The leading "#" character is not part of the fragment and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.5. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.5 * @return string The URI fragment. */ public function getFragment(): string; /** * Return an instance with the specified scheme. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified scheme. * * Implementations MUST support the schemes "http" and "https" case * insensitively, and MAY accommodate other schemes if required. * * An empty scheme is equivalent to removing the scheme. * * @param string $scheme The scheme to use with the new instance. * @return static A new instance with the specified scheme. * @throws \InvalidArgumentException for invalid or unsupported schemes. */ public function withScheme(string $scheme): UriInterface; /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; an empty string for the user is equivalent to removing user * information. * * @param string $user The user name to use for authority. * @param null|string $password The password associated with $user. * @return static A new instance with the specified user information. */ public function withUserInfo(string $user, ?string $password = null): UriInterface; /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * An empty host value is equivalent to removing the host. * * @param string $host The hostname to use with the new instance. * @return static A new instance with the specified host. * @throws \InvalidArgumentException for invalid hostnames. */ public function withHost(string $host): UriInterface; /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * Implementations MUST raise an exception for ports outside the * established TCP and UDP port ranges. * * A null value provided for the port is equivalent to removing the port * information. * * @param null|int $port The port to use with the new instance; a null value * removes the port information. * @return static A new instance with the specified port. * @throws \InvalidArgumentException for invalid ports. */ public function withPort(?int $port): UriInterface; /** * Return an instance with the specified path. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified path. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * If the path is intended to be domain-relative rather than path relative then * it must begin with a slash ("/"). Paths not starting with a slash ("/") * are assumed to be relative to some base path known to the application or * consumer. * * Users can provide both encoded and decoded path characters. * Implementations ensure the correct encoding as outlined in getPath(). * * @param string $path The path to use with the new instance. * @return static A new instance with the specified path. * @throws \InvalidArgumentException for invalid paths. */ public function withPath(string $path): UriInterface; /** * Return an instance with the specified query string. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified query string. * * Users can provide both encoded and decoded query characters. * Implementations ensure the correct encoding as outlined in getQuery(). * * An empty query string value is equivalent to removing the query string. * * @param string $query The query string to use with the new instance. * @return static A new instance with the specified query string. * @throws \InvalidArgumentException for invalid query strings. */ public function withQuery(string $query): UriInterface; /** * Return an instance with the specified URI fragment. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified URI fragment. * * Users can provide both encoded and decoded fragment characters. * Implementations ensure the correct encoding as outlined in getFragment(). * * An empty fragment value is equivalent to removing the fragment. * * @param string $fragment The fragment to use with the new instance. * @return static A new instance with the specified fragment. */ public function withFragment(string $fragment): UriInterface; /** * Return the string representation as a URI reference. * * Depending on which components of the URI are present, the resulting * string is either a full URI or relative reference according to RFC 3986, * Section 4.1. The method concatenates the various components of the URI, * using the appropriate delimiters: * * - If a scheme is present, it MUST be suffixed by ":". * - If an authority is present, it MUST be prefixed by "//". * - The path can be concatenated without delimiters. But there are two * cases where the path has to be adjusted to make the URI reference * valid as PHP does not allow to throw an exception in __toString(): * - If the path is rootless and an authority is present, the path MUST * be prefixed by "/". * - If the path is starting with more than one "/" and no authority is * present, the starting slashes MUST be reduced to one. * - If a query is present, it MUST be prefixed by "?". * - If a fragment is present, it MUST be prefixed by "#". * * @see http://tools.ietf.org/html/rfc3986#section-4.1 * @return string */ public function __toString(): string; } PK!:\Y33$vendor/psr/http-message/CHANGELOG.mdnu[# Changelog All notable changes to this project will be documented in this file, in reverse chronological order by release. ## 1.0.1 - 2016-08-06 ### Added - Nothing. ### Deprecated - Nothing. ### Removed - Nothing. ### Fixed - Updated all `@return self` annotation references in interfaces to use `@return static`, which more closelly follows the semantics of the specification. - Updated the `MessageInterface::getHeaders()` return annotation to use the value `string[][]`, indicating the format is a nested array of strings. - Updated the `@link` annotation for `RequestInterface::withRequestTarget()` to point to the correct section of RFC 7230. - Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation to add the parameter name (`$uploadedFiles`). - Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()` method to correctly reference the method parameter (it was referencing an incorrect parameter name previously). ## 1.0.0 - 2016-05-18 Initial stable release; reflects accepted PSR-7 specification. PK!Lo$ss%vendor/psr/http-message/composer.jsonnu[{ "name": "psr/http-message", "description": "Common interface for HTTP messages", "keywords": ["psr", "psr-7", "http", "http-message", "request", "response"], "homepage": "https://github.com/php-fig/http-message", "license": "MIT", "authors": [ { "name": "PHP-FIG", "homepage": "https://www.php-fig.org/" } ], "require": { "php": "^7.2 || ^8.0" }, "autoload": { "psr-4": { "Psr\\Http\\Message\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } } } PK!==vendor/psr/http-message/LICENSEnu[Copyright (c) 2014 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!!vendor/psr/http-message/README.mdnu[PSR Http Message ================ This repository holds all interfaces/classes/traits related to [PSR-7](http://www.php-fig.org/psr/psr-7/). Note that this is not a HTTP message implementation of its own. It is merely an interface that describes a HTTP message. See the specification for more details. Usage ----- Before reading the usage guide we recommend reading the PSR-7 interfaces method list: * [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md) * [`PSR-7 Usage Guide`](docs/PSR7-Usage.md)PK!HTg)vendor/psr/log/Psr/Log/Test/DummyTest.phpnu[ ". * * Example ->error('Foo') would yield "error Foo". * * @return string[] */ abstract public function getLogs(); public function testImplements() { $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger()); } /** * @dataProvider provideLevelsAndMessages */ public function testLogsAtAllLevels($level, $message) { $logger = $this->getLogger(); $logger->{$level}($message, array('user' => 'Bob')); $logger->log($level, $message, array('user' => 'Bob')); $expected = array( $level.' message of level '.$level.' with context: Bob', $level.' message of level '.$level.' with context: Bob', ); $this->assertEquals($expected, $this->getLogs()); } public function provideLevelsAndMessages() { return array( LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'), LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'), LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'), LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'), LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'), LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'), LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'), LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'), ); } /** * @expectedException \Psr\Log\InvalidArgumentException */ public function testThrowsOnInvalidLevel() { $logger = $this->getLogger(); $logger->log('invalid level', 'Foo'); } public function testContextReplacement() { $logger = $this->getLogger(); $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar')); $expected = array('info {Message {nothing} Bob Bar a}'); $this->assertEquals($expected, $this->getLogs()); } public function testObjectCastToString() { if (method_exists($this, 'createPartialMock')) { $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString')); } else { $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString')); } $dummy->expects($this->once()) ->method('__toString') ->will($this->returnValue('DUMMY')); $this->getLogger()->warning($dummy); $expected = array('warning DUMMY'); $this->assertEquals($expected, $this->getLogs()); } public function testContextCanContainAnything() { $closed = fopen('php://memory', 'r'); fclose($closed); $context = array( 'bool' => true, 'null' => null, 'string' => 'Foo', 'int' => 0, 'float' => 0.5, 'nested' => array('with object' => new DummyTest), 'object' => new \DateTime, 'resource' => fopen('php://memory', 'r'), 'closed' => $closed, ); $this->getLogger()->warning('Crazy context data', $context); $expected = array('warning Crazy context data'); $this->assertEquals($expected, $this->getLogs()); } public function testContextExceptionKeyCanBeExceptionOrOtherValues() { $logger = $this->getLogger(); $logger->warning('Random message', array('exception' => 'oops')); $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail'))); $expected = array( 'warning Random message', 'critical Uncaught Exception!' ); $this->assertEquals($expected, $this->getLogs()); } } PK! *vendor/psr/log/Psr/Log/Test/TestLogger.phpnu[ $level, 'message' => $message, 'context' => $context, ]; $this->recordsByLevel[$record['level']][] = $record; $this->records[] = $record; } public function hasRecords($level) { return isset($this->recordsByLevel[$level]); } public function hasRecord($record, $level) { if (is_string($record)) { $record = ['message' => $record]; } return $this->hasRecordThatPasses(function ($rec) use ($record) { if ($rec['message'] !== $record['message']) { return false; } if (isset($record['context']) && $rec['context'] !== $record['context']) { return false; } return true; }, $level); } public function hasRecordThatContains($message, $level) { return $this->hasRecordThatPasses(function ($rec) use ($message) { return strpos($rec['message'], $message) !== false; }, $level); } public function hasRecordThatMatches($regex, $level) { return $this->hasRecordThatPasses(function ($rec) use ($regex) { return preg_match($regex, $rec['message']) > 0; }, $level); } public function hasRecordThatPasses(callable $predicate, $level) { if (!isset($this->recordsByLevel[$level])) { return false; } foreach ($this->recordsByLevel[$level] as $i => $rec) { if (call_user_func($predicate, $rec, $i)) { return true; } } return false; } public function __call($method, $args) { if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; $level = strtolower($matches[2]); if (method_exists($this, $genericMethod)) { $args[] = $level; return call_user_func_array([$this, $genericMethod], $args); } } throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()'); } public function reset() { $this->records = []; $this->recordsByLevel = []; } } PK!G )vendor/psr/log/Psr/Log/AbstractLogger.phpnu[log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } } PK! X1``3vendor/psr/log/Psr/Log/InvalidArgumentException.phpnu[logger = $logger; } } PK!1b!q* * *vendor/psr/log/Psr/Log/LoggerInterface.phpnu[log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ abstract public function log($level, $message, array $context = array()); } PK!PP#vendor/psr/log/Psr/Log/LogLevel.phpnu[logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()) { // noop } } PK!՞22vendor/psr/log/composer.jsonnu[{ "name": "psr/log", "description": "Common interface for logging libraries", "keywords": ["psr", "psr-3", "log"], "homepage": "https://github.com/php-fig/log", "license": "MIT", "authors": [ { "name": "PHP-FIG", "homepage": "https://www.php-fig.org/" } ], "require": { "php": ">=5.3.0" }, "autoload": { "psr-4": { "Psr\\Log\\": "Psr/Log/" } }, "extra": { "branch-alias": { "dev-master": "1.1.x-dev" } } } PK!pO==vendor/psr/log/LICENSEnu[Copyright (c) 2012 PHP Framework Interoperability Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!'BBvendor/psr/log/README.mdnu[PSR Log ======= This repository holds all interfaces/classes/traits related to [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). Note that this is not a logger of its own. It is merely an interface that describes a logger. See the specification for more details. Installation ------------ ```bash composer require psr/log ``` Usage ----- If you need a logger, you can use the interface like this: ```php logger = $logger; } public function doSomething() { if ($this->logger) { $this->logger->info('Doing work'); } try { $this->doSomethingElse(); } catch (Exception $exception) { $this->logger->error('Oh no!', array('exception' => $exception)); } // do something useful } } ``` You can then pick one of the implementations of the interface to get a logger. If you want to implement the interface, you can require this package and implement `Psr\Log\LoggerInterface` in your code. Please read the [specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) for details. PK!zhh4vendor/ralouphie/getallheaders/src/getallheaders.phpnu[ 'Content-Type', 'CONTENT_LENGTH' => 'Content-Length', 'CONTENT_MD5' => 'Content-Md5', ); foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) === 'HTTP_') { $key = substr($key, 5); if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) { $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); $headers[$key] = $value; } } elseif (isset($copy_server[$key])) { $headers[$copy_server[$key]] = $value; } } if (!isset($headers['Authorization'])) { if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } elseif (isset($_SERVER['PHP_AUTH_USER'])) { $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; } } return $headers; } } PK!G,vendor/ralouphie/getallheaders/composer.jsonnu[{ "name": "ralouphie/getallheaders", "description": "A polyfill for getallheaders.", "license": "MIT", "authors": [ { "name": "Ralph Khattar", "email": "ralph.khattar@gmail.com" } ], "require": { "php": ">=5.6" }, "require-dev": { "phpunit/phpunit": "^5 || ^6.5", "php-coveralls/php-coveralls": "^2.1" }, "autoload": { "files": ["src/getallheaders.php"] }, "autoload-dev": { "psr-4": { "getallheaders\\Tests\\": "tests/" } } } PK!Ka88&vendor/ralouphie/getallheaders/LICENSEnu[The MIT License (MIT) Copyright (c) 2014 Ralph Khattar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!\@@(vendor/ralouphie/getallheaders/README.mdnu[getallheaders ============= PHP `getallheaders()` polyfill. Compatible with PHP >= 5.3. [![Build Status](https://travis-ci.org/ralouphie/getallheaders.svg?branch=master)](https://travis-ci.org/ralouphie/getallheaders) [![Coverage Status](https://coveralls.io/repos/ralouphie/getallheaders/badge.png?branch=master)](https://coveralls.io/r/ralouphie/getallheaders?branch=master) [![Latest Stable Version](https://poser.pugx.org/ralouphie/getallheaders/v/stable.png)](https://packagist.org/packages/ralouphie/getallheaders) [![Latest Unstable Version](https://poser.pugx.org/ralouphie/getallheaders/v/unstable.png)](https://packagist.org/packages/ralouphie/getallheaders) [![License](https://poser.pugx.org/ralouphie/getallheaders/license.png)](https://packagist.org/packages/ralouphie/getallheaders) This is a simple polyfill for [`getallheaders()`](http://www.php.net/manual/en/function.getallheaders.php). ## Install For PHP version **`>= 5.6`**: ``` composer require ralouphie/getallheaders ``` For PHP version **`< 5.6`**: ``` composer require ralouphie/getallheaders "^2" ``` PK!h{#1vendor/symfony/deprecation-contracts/CHANGELOG.mdnu[CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md PK!,HII2vendor/symfony/deprecation-contracts/composer.jsonnu[{ "name": "symfony/deprecation-contracts", "type": "library", "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.1" }, "autoload": { "files": [ "function.php" ] }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } PK!rg1vendor/symfony/deprecation-contracts/function.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!function_exists('trigger_deprecation')) { /** * Triggers a silenced deprecation notice. * * @param string $package The name of the Composer package that is triggering the deprecation * @param string $version The version of the package that introduced the deprecation * @param string $message The message of the deprecation * @param mixed ...$args Values to insert in the message using printf() formatting * * @author Nicolas Grekas */ function trigger_deprecation(string $package, string $version, string $message, ...$args): void { @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); } } PK! K,,,vendor/symfony/deprecation-contracts/LICENSEnu[Copyright (c) 2020-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!3.vendor/symfony/deprecation-contracts/README.mdnu[Symfony Deprecation Contracts ============================= A generic function and convention to trigger deprecation notices. This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. The function requires at least 3 arguments: - the name of the Composer package that is triggering the deprecation - the version of the package that introduced the deprecation - the message of the deprecation - more arguments can be provided: they will be inserted in the message using `printf()` formatting Example: ```php trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); ``` This will generate the following message: `Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty `function trigger_deprecation() {}` in your application. PK!F)rr-vendor/symfony/polyfill-ctype/bootstrap80.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Ctype as p; if (!function_exists('ctype_alnum')) { function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); } } if (!function_exists('ctype_alpha')) { function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); } } if (!function_exists('ctype_cntrl')) { function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); } } if (!function_exists('ctype_digit')) { function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); } } if (!function_exists('ctype_graph')) { function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); } } if (!function_exists('ctype_lower')) { function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); } } if (!function_exists('ctype_print')) { function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); } } if (!function_exists('ctype_punct')) { function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); } } if (!function_exists('ctype_space')) { function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); } } if (!function_exists('ctype_upper')) { function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); } } if (!function_exists('ctype_xdigit')) { function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); } } PK!jQ9@@+vendor/symfony/polyfill-ctype/bootstrap.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Ctype as p; if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; } if (!function_exists('ctype_alnum')) { function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); } } if (!function_exists('ctype_alpha')) { function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); } } if (!function_exists('ctype_cntrl')) { function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); } } if (!function_exists('ctype_digit')) { function ctype_digit($text) { return p\Ctype::ctype_digit($text); } } if (!function_exists('ctype_graph')) { function ctype_graph($text) { return p\Ctype::ctype_graph($text); } } if (!function_exists('ctype_lower')) { function ctype_lower($text) { return p\Ctype::ctype_lower($text); } } if (!function_exists('ctype_print')) { function ctype_print($text) { return p\Ctype::ctype_print($text); } } if (!function_exists('ctype_punct')) { function ctype_punct($text) { return p\Ctype::ctype_punct($text); } } if (!function_exists('ctype_space')) { function ctype_space($text) { return p\Ctype::ctype_space($text); } } if (!function_exists('ctype_upper')) { function ctype_upper($text) { return p\Ctype::ctype_upper($text); } } if (!function_exists('ctype_xdigit')) { function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); } } PK!e+vendor/symfony/polyfill-ctype/composer.jsonnu[{ "name": "symfony/polyfill-ctype", "type": "library", "description": "Symfony polyfill for ctype functions", "keywords": ["polyfill", "compatibility", "portable", "ctype"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Gert de Pagter", "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "provide": { "ext-ctype": "*" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Ctype\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-ctype": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } PK!xs'vendor/symfony/polyfill-ctype/Ctype.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Ctype; /** * Ctype implementation through regex. * * @internal * * @author Gert de Pagter */ final class Ctype { /** * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise. * * @see https://php.net/ctype-alnum * * @param mixed $text * * @return bool */ public static function ctype_alnum($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text); } /** * Returns TRUE if every character in text is a letter, FALSE otherwise. * * @see https://php.net/ctype-alpha * * @param mixed $text * * @return bool */ public static function ctype_alpha($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text); } /** * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise. * * @see https://php.net/ctype-cntrl * * @param mixed $text * * @return bool */ public static function ctype_cntrl($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text); } /** * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise. * * @see https://php.net/ctype-digit * * @param mixed $text * * @return bool */ public static function ctype_digit($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text); } /** * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise. * * @see https://php.net/ctype-graph * * @param mixed $text * * @return bool */ public static function ctype_graph($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text); } /** * Returns TRUE if every character in text is a lowercase letter. * * @see https://php.net/ctype-lower * * @param mixed $text * * @return bool */ public static function ctype_lower($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text); } /** * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all. * * @see https://php.net/ctype-print * * @param mixed $text * * @return bool */ public static function ctype_print($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text); } /** * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise. * * @see https://php.net/ctype-punct * * @param mixed $text * * @return bool */ public static function ctype_punct($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text); } /** * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters. * * @see https://php.net/ctype-space * * @param mixed $text * * @return bool */ public static function ctype_space($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text); } /** * Returns TRUE if every character in text is an uppercase letter. * * @see https://php.net/ctype-upper * * @param mixed $text * * @return bool */ public static function ctype_upper($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text); } /** * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise. * * @see https://php.net/ctype-xdigit * * @param mixed $text * * @return bool */ public static function ctype_xdigit($text) { $text = self::convert_int_to_char_for_ctype($text, __FUNCTION__); return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text); } /** * Converts integers to their char versions according to normal ctype behaviour, if needed. * * If an integer between -128 and 255 inclusive is provided, * it is interpreted as the ASCII value of a single character * (negative values have 256 added in order to allow characters in the Extended ASCII range). * Any other integer is interpreted as a string containing the decimal digits of the integer. * * @param mixed $int * @param string $function * * @return mixed */ private static function convert_int_to_char_for_ctype($int, $function) { if (!\is_int($int)) { return $int; } if ($int < -128 || $int > 255) { return (string) $int; } if (\PHP_VERSION_ID >= 80100) { @trigger_error($function.'(): Argument of type int will be interpreted as string in the future', \E_USER_DEPRECATED); } if ($int < 0) { $int += 256; } return \chr($int); } } PK!,,%vendor/symfony/polyfill-ctype/LICENSEnu[Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!lHk^^'vendor/symfony/polyfill-ctype/README.mdnu[Symfony Polyfill / Ctype ======================== This component provides `ctype_*` functions to users who run php versions without the ctype extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). PK!%Fvendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.phpnu[ 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'ÿ' => 'ÿ', 'Ā' => 'Ā', 'ā' => 'ā', 'Ă' => 'Ă', 'ă' => 'ă', 'Ą' => 'Ą', 'ą' => 'ą', 'Ć' => 'Ć', 'ć' => 'ć', 'Ĉ' => 'Ĉ', 'ĉ' => 'ĉ', 'Ċ' => 'Ċ', 'ċ' => 'ċ', 'Č' => 'Č', 'č' => 'č', 'Ď' => 'Ď', 'ď' => 'ď', 'Ē' => 'Ē', 'ē' => 'ē', 'Ĕ' => 'Ĕ', 'ĕ' => 'ĕ', 'Ė' => 'Ė', 'ė' => 'ė', 'Ę' => 'Ę', 'ę' => 'ę', 'Ě' => 'Ě', 'ě' => 'ě', 'Ĝ' => 'Ĝ', 'ĝ' => 'ĝ', 'Ğ' => 'Ğ', 'ğ' => 'ğ', 'Ġ' => 'Ġ', 'ġ' => 'ġ', 'Ģ' => 'Ģ', 'ģ' => 'ģ', 'Ĥ' => 'Ĥ', 'ĥ' => 'ĥ', 'Ĩ' => 'Ĩ', 'ĩ' => 'ĩ', 'Ī' => 'Ī', 'ī' => 'ī', 'Ĭ' => 'Ĭ', 'ĭ' => 'ĭ', 'Į' => 'Į', 'į' => 'į', 'İ' => 'İ', 'Ĵ' => 'Ĵ', 'ĵ' => 'ĵ', 'Ķ' => 'Ķ', 'ķ' => 'ķ', 'Ĺ' => 'Ĺ', 'ĺ' => 'ĺ', 'Ļ' => 'Ļ', 'ļ' => 'ļ', 'Ľ' => 'Ľ', 'ľ' => 'ľ', 'Ń' => 'Ń', 'ń' => 'ń', 'Ņ' => 'Ņ', 'ņ' => 'ņ', 'Ň' => 'Ň', 'ň' => 'ň', 'Ō' => 'Ō', 'ō' => 'ō', 'Ŏ' => 'Ŏ', 'ŏ' => 'ŏ', 'Ő' => 'Ő', 'ő' => 'ő', 'Ŕ' => 'Ŕ', 'ŕ' => 'ŕ', 'Ŗ' => 'Ŗ', 'ŗ' => 'ŗ', 'Ř' => 'Ř', 'ř' => 'ř', 'Ś' => 'Ś', 'ś' => 'ś', 'Ŝ' => 'Ŝ', 'ŝ' => 'ŝ', 'Ş' => 'Ş', 'ş' => 'ş', 'Š' => 'Š', 'š' => 'š', 'Ţ' => 'Ţ', 'ţ' => 'ţ', 'Ť' => 'Ť', 'ť' => 'ť', 'Ũ' => 'Ũ', 'ũ' => 'ũ', 'Ū' => 'Ū', 'ū' => 'ū', 'Ŭ' => 'Ŭ', 'ŭ' => 'ŭ', 'Ů' => 'Ů', 'ů' => 'ů', 'Ű' => 'Ű', 'ű' => 'ű', 'Ų' => 'Ų', 'ų' => 'ų', 'Ŵ' => 'Ŵ', 'ŵ' => 'ŵ', 'Ŷ' => 'Ŷ', 'ŷ' => 'ŷ', 'Ÿ' => 'Ÿ', 'Ź' => 'Ź', 'ź' => 'ź', 'Ż' => 'Ż', 'ż' => 'ż', 'Ž' => 'Ž', 'ž' => 'ž', 'Ơ' => 'Ơ', 'ơ' => 'ơ', 'Ư' => 'Ư', 'ư' => 'ư', 'Ǎ' => 'Ǎ', 'ǎ' => 'ǎ', 'Ǐ' => 'Ǐ', 'ǐ' => 'ǐ', 'Ǒ' => 'Ǒ', 'ǒ' => 'ǒ', 'Ǔ' => 'Ǔ', 'ǔ' => 'ǔ', 'Ǖ' => 'Ǖ', 'ǖ' => 'ǖ', 'Ǘ' => 'Ǘ', 'ǘ' => 'ǘ', 'Ǚ' => 'Ǚ', 'ǚ' => 'ǚ', 'Ǜ' => 'Ǜ', 'ǜ' => 'ǜ', 'Ǟ' => 'Ǟ', 'ǟ' => 'ǟ', 'Ǡ' => 'Ǡ', 'ǡ' => 'ǡ', 'Ǣ' => 'Ǣ', 'ǣ' => 'ǣ', 'Ǧ' => 'Ǧ', 'ǧ' => 'ǧ', 'Ǩ' => 'Ǩ', 'ǩ' => 'ǩ', 'Ǫ' => 'Ǫ', 'ǫ' => 'ǫ', 'Ǭ' => 'Ǭ', 'ǭ' => 'ǭ', 'Ǯ' => 'Ǯ', 'ǯ' => 'ǯ', 'ǰ' => 'ǰ', 'Ǵ' => 'Ǵ', 'ǵ' => 'ǵ', 'Ǹ' => 'Ǹ', 'ǹ' => 'ǹ', 'Ǻ' => 'Ǻ', 'ǻ' => 'ǻ', 'Ǽ' => 'Ǽ', 'ǽ' => 'ǽ', 'Ǿ' => 'Ǿ', 'ǿ' => 'ǿ', 'Ȁ' => 'Ȁ', 'ȁ' => 'ȁ', 'Ȃ' => 'Ȃ', 'ȃ' => 'ȃ', 'Ȅ' => 'Ȅ', 'ȅ' => 'ȅ', 'Ȇ' => 'Ȇ', 'ȇ' => 'ȇ', 'Ȉ' => 'Ȉ', 'ȉ' => 'ȉ', 'Ȋ' => 'Ȋ', 'ȋ' => 'ȋ', 'Ȍ' => 'Ȍ', 'ȍ' => 'ȍ', 'Ȏ' => 'Ȏ', 'ȏ' => 'ȏ', 'Ȑ' => 'Ȑ', 'ȑ' => 'ȑ', 'Ȓ' => 'Ȓ', 'ȓ' => 'ȓ', 'Ȕ' => 'Ȕ', 'ȕ' => 'ȕ', 'Ȗ' => 'Ȗ', 'ȗ' => 'ȗ', 'Ș' => 'Ș', 'ș' => 'ș', 'Ț' => 'Ț', 'ț' => 'ț', 'Ȟ' => 'Ȟ', 'ȟ' => 'ȟ', 'Ȧ' => 'Ȧ', 'ȧ' => 'ȧ', 'Ȩ' => 'Ȩ', 'ȩ' => 'ȩ', 'Ȫ' => 'Ȫ', 'ȫ' => 'ȫ', 'Ȭ' => 'Ȭ', 'ȭ' => 'ȭ', 'Ȯ' => 'Ȯ', 'ȯ' => 'ȯ', 'Ȱ' => 'Ȱ', 'ȱ' => 'ȱ', 'Ȳ' => 'Ȳ', 'ȳ' => 'ȳ', '΅' => '΅', 'Ά' => 'Ά', 'Έ' => 'Έ', 'Ή' => 'Ή', 'Ί' => 'Ί', 'Ό' => 'Ό', 'Ύ' => 'Ύ', 'Ώ' => 'Ώ', 'ΐ' => 'ΐ', 'Ϊ' => 'Ϊ', 'Ϋ' => 'Ϋ', 'ά' => 'ά', 'έ' => 'έ', 'ή' => 'ή', 'ί' => 'ί', 'ΰ' => 'ΰ', 'ϊ' => 'ϊ', 'ϋ' => 'ϋ', 'ό' => 'ό', 'ύ' => 'ύ', 'ώ' => 'ώ', 'ϓ' => 'ϓ', 'ϔ' => 'ϔ', 'Ѐ' => 'Ѐ', 'Ё' => 'Ё', 'Ѓ' => 'Ѓ', 'Ї' => 'Ї', 'Ќ' => 'Ќ', 'Ѝ' => 'Ѝ', 'Ў' => 'Ў', 'Й' => 'Й', 'й' => 'й', 'ѐ' => 'ѐ', 'ё' => 'ё', 'ѓ' => 'ѓ', 'ї' => 'ї', 'ќ' => 'ќ', 'ѝ' => 'ѝ', 'ў' => 'ў', 'Ѷ' => 'Ѷ', 'ѷ' => 'ѷ', 'Ӂ' => 'Ӂ', 'ӂ' => 'ӂ', 'Ӑ' => 'Ӑ', 'ӑ' => 'ӑ', 'Ӓ' => 'Ӓ', 'ӓ' => 'ӓ', 'Ӗ' => 'Ӗ', 'ӗ' => 'ӗ', 'Ӛ' => 'Ӛ', 'ӛ' => 'ӛ', 'Ӝ' => 'Ӝ', 'ӝ' => 'ӝ', 'Ӟ' => 'Ӟ', 'ӟ' => 'ӟ', 'Ӣ' => 'Ӣ', 'ӣ' => 'ӣ', 'Ӥ' => 'Ӥ', 'ӥ' => 'ӥ', 'Ӧ' => 'Ӧ', 'ӧ' => 'ӧ', 'Ӫ' => 'Ӫ', 'ӫ' => 'ӫ', 'Ӭ' => 'Ӭ', 'ӭ' => 'ӭ', 'Ӯ' => 'Ӯ', 'ӯ' => 'ӯ', 'Ӱ' => 'Ӱ', 'ӱ' => 'ӱ', 'Ӳ' => 'Ӳ', 'ӳ' => 'ӳ', 'Ӵ' => 'Ӵ', 'ӵ' => 'ӵ', 'Ӹ' => 'Ӹ', 'ӹ' => 'ӹ', 'آ' => 'آ', 'أ' => 'أ', 'ؤ' => 'ؤ', 'إ' => 'إ', 'ئ' => 'ئ', 'ۀ' => 'ۀ', 'ۂ' => 'ۂ', 'ۓ' => 'ۓ', 'ऩ' => 'ऩ', 'ऱ' => 'ऱ', 'ऴ' => 'ऴ', 'ো' => 'ো', 'ৌ' => 'ৌ', 'ୈ' => 'ୈ', 'ୋ' => 'ୋ', 'ୌ' => 'ୌ', 'ஔ' => 'ஔ', 'ொ' => 'ொ', 'ோ' => 'ோ', 'ௌ' => 'ௌ', 'ై' => 'ై', 'ೀ' => 'ೀ', 'ೇ' => 'ೇ', 'ೈ' => 'ೈ', 'ೊ' => 'ೊ', 'ೋ' => 'ೋ', 'ൊ' => 'ൊ', 'ോ' => 'ോ', 'ൌ' => 'ൌ', 'ේ' => 'ේ', 'ො' => 'ො', 'ෝ' => 'ෝ', 'ෞ' => 'ෞ', 'ဦ' => 'ဦ', 'ᬆ' => 'ᬆ', 'ᬈ' => 'ᬈ', 'ᬊ' => 'ᬊ', 'ᬌ' => 'ᬌ', 'ᬎ' => 'ᬎ', 'ᬒ' => 'ᬒ', 'ᬻ' => 'ᬻ', 'ᬽ' => 'ᬽ', 'ᭀ' => 'ᭀ', 'ᭁ' => 'ᭁ', 'ᭃ' => 'ᭃ', 'Ḁ' => 'Ḁ', 'ḁ' => 'ḁ', 'Ḃ' => 'Ḃ', 'ḃ' => 'ḃ', 'Ḅ' => 'Ḅ', 'ḅ' => 'ḅ', 'Ḇ' => 'Ḇ', 'ḇ' => 'ḇ', 'Ḉ' => 'Ḉ', 'ḉ' => 'ḉ', 'Ḋ' => 'Ḋ', 'ḋ' => 'ḋ', 'Ḍ' => 'Ḍ', 'ḍ' => 'ḍ', 'Ḏ' => 'Ḏ', 'ḏ' => 'ḏ', 'Ḑ' => 'Ḑ', 'ḑ' => 'ḑ', 'Ḓ' => 'Ḓ', 'ḓ' => 'ḓ', 'Ḕ' => 'Ḕ', 'ḕ' => 'ḕ', 'Ḗ' => 'Ḗ', 'ḗ' => 'ḗ', 'Ḙ' => 'Ḙ', 'ḙ' => 'ḙ', 'Ḛ' => 'Ḛ', 'ḛ' => 'ḛ', 'Ḝ' => 'Ḝ', 'ḝ' => 'ḝ', 'Ḟ' => 'Ḟ', 'ḟ' => 'ḟ', 'Ḡ' => 'Ḡ', 'ḡ' => 'ḡ', 'Ḣ' => 'Ḣ', 'ḣ' => 'ḣ', 'Ḥ' => 'Ḥ', 'ḥ' => 'ḥ', 'Ḧ' => 'Ḧ', 'ḧ' => 'ḧ', 'Ḩ' => 'Ḩ', 'ḩ' => 'ḩ', 'Ḫ' => 'Ḫ', 'ḫ' => 'ḫ', 'Ḭ' => 'Ḭ', 'ḭ' => 'ḭ', 'Ḯ' => 'Ḯ', 'ḯ' => 'ḯ', 'Ḱ' => 'Ḱ', 'ḱ' => 'ḱ', 'Ḳ' => 'Ḳ', 'ḳ' => 'ḳ', 'Ḵ' => 'Ḵ', 'ḵ' => 'ḵ', 'Ḷ' => 'Ḷ', 'ḷ' => 'ḷ', 'Ḹ' => 'Ḹ', 'ḹ' => 'ḹ', 'Ḻ' => 'Ḻ', 'ḻ' => 'ḻ', 'Ḽ' => 'Ḽ', 'ḽ' => 'ḽ', 'Ḿ' => 'Ḿ', 'ḿ' => 'ḿ', 'Ṁ' => 'Ṁ', 'ṁ' => 'ṁ', 'Ṃ' => 'Ṃ', 'ṃ' => 'ṃ', 'Ṅ' => 'Ṅ', 'ṅ' => 'ṅ', 'Ṇ' => 'Ṇ', 'ṇ' => 'ṇ', 'Ṉ' => 'Ṉ', 'ṉ' => 'ṉ', 'Ṋ' => 'Ṋ', 'ṋ' => 'ṋ', 'Ṍ' => 'Ṍ', 'ṍ' => 'ṍ', 'Ṏ' => 'Ṏ', 'ṏ' => 'ṏ', 'Ṑ' => 'Ṑ', 'ṑ' => 'ṑ', 'Ṓ' => 'Ṓ', 'ṓ' => 'ṓ', 'Ṕ' => 'Ṕ', 'ṕ' => 'ṕ', 'Ṗ' => 'Ṗ', 'ṗ' => 'ṗ', 'Ṙ' => 'Ṙ', 'ṙ' => 'ṙ', 'Ṛ' => 'Ṛ', 'ṛ' => 'ṛ', 'Ṝ' => 'Ṝ', 'ṝ' => 'ṝ', 'Ṟ' => 'Ṟ', 'ṟ' => 'ṟ', 'Ṡ' => 'Ṡ', 'ṡ' => 'ṡ', 'Ṣ' => 'Ṣ', 'ṣ' => 'ṣ', 'Ṥ' => 'Ṥ', 'ṥ' => 'ṥ', 'Ṧ' => 'Ṧ', 'ṧ' => 'ṧ', 'Ṩ' => 'Ṩ', 'ṩ' => 'ṩ', 'Ṫ' => 'Ṫ', 'ṫ' => 'ṫ', 'Ṭ' => 'Ṭ', 'ṭ' => 'ṭ', 'Ṯ' => 'Ṯ', 'ṯ' => 'ṯ', 'Ṱ' => 'Ṱ', 'ṱ' => 'ṱ', 'Ṳ' => 'Ṳ', 'ṳ' => 'ṳ', 'Ṵ' => 'Ṵ', 'ṵ' => 'ṵ', 'Ṷ' => 'Ṷ', 'ṷ' => 'ṷ', 'Ṹ' => 'Ṹ', 'ṹ' => 'ṹ', 'Ṻ' => 'Ṻ', 'ṻ' => 'ṻ', 'Ṽ' => 'Ṽ', 'ṽ' => 'ṽ', 'Ṿ' => 'Ṿ', 'ṿ' => 'ṿ', 'Ẁ' => 'Ẁ', 'ẁ' => 'ẁ', 'Ẃ' => 'Ẃ', 'ẃ' => 'ẃ', 'Ẅ' => 'Ẅ', 'ẅ' => 'ẅ', 'Ẇ' => 'Ẇ', 'ẇ' => 'ẇ', 'Ẉ' => 'Ẉ', 'ẉ' => 'ẉ', 'Ẋ' => 'Ẋ', 'ẋ' => 'ẋ', 'Ẍ' => 'Ẍ', 'ẍ' => 'ẍ', 'Ẏ' => 'Ẏ', 'ẏ' => 'ẏ', 'Ẑ' => 'Ẑ', 'ẑ' => 'ẑ', 'Ẓ' => 'Ẓ', 'ẓ' => 'ẓ', 'Ẕ' => 'Ẕ', 'ẕ' => 'ẕ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẛ' => 'ẛ', 'Ạ' => 'Ạ', 'ạ' => 'ạ', 'Ả' => 'Ả', 'ả' => 'ả', 'Ấ' => 'Ấ', 'ấ' => 'ấ', 'Ầ' => 'Ầ', 'ầ' => 'ầ', 'Ẩ' => 'Ẩ', 'ẩ' => 'ẩ', 'Ẫ' => 'Ẫ', 'ẫ' => 'ẫ', 'Ậ' => 'Ậ', 'ậ' => 'ậ', 'Ắ' => 'Ắ', 'ắ' => 'ắ', 'Ằ' => 'Ằ', 'ằ' => 'ằ', 'Ẳ' => 'Ẳ', 'ẳ' => 'ẳ', 'Ẵ' => 'Ẵ', 'ẵ' => 'ẵ', 'Ặ' => 'Ặ', 'ặ' => 'ặ', 'Ẹ' => 'Ẹ', 'ẹ' => 'ẹ', 'Ẻ' => 'Ẻ', 'ẻ' => 'ẻ', 'Ẽ' => 'Ẽ', 'ẽ' => 'ẽ', 'Ế' => 'Ế', 'ế' => 'ế', 'Ề' => 'Ề', 'ề' => 'ề', 'Ể' => 'Ể', 'ể' => 'ể', 'Ễ' => 'Ễ', 'ễ' => 'ễ', 'Ệ' => 'Ệ', 'ệ' => 'ệ', 'Ỉ' => 'Ỉ', 'ỉ' => 'ỉ', 'Ị' => 'Ị', 'ị' => 'ị', 'Ọ' => 'Ọ', 'ọ' => 'ọ', 'Ỏ' => 'Ỏ', 'ỏ' => 'ỏ', 'Ố' => 'Ố', 'ố' => 'ố', 'Ồ' => 'Ồ', 'ồ' => 'ồ', 'Ổ' => 'Ổ', 'ổ' => 'ổ', 'Ỗ' => 'Ỗ', 'ỗ' => 'ỗ', 'Ộ' => 'Ộ', 'ộ' => 'ộ', 'Ớ' => 'Ớ', 'ớ' => 'ớ', 'Ờ' => 'Ờ', 'ờ' => 'ờ', 'Ở' => 'Ở', 'ở' => 'ở', 'Ỡ' => 'Ỡ', 'ỡ' => 'ỡ', 'Ợ' => 'Ợ', 'ợ' => 'ợ', 'Ụ' => 'Ụ', 'ụ' => 'ụ', 'Ủ' => 'Ủ', 'ủ' => 'ủ', 'Ứ' => 'Ứ', 'ứ' => 'ứ', 'Ừ' => 'Ừ', 'ừ' => 'ừ', 'Ử' => 'Ử', 'ử' => 'ử', 'Ữ' => 'Ữ', 'ữ' => 'ữ', 'Ự' => 'Ự', 'ự' => 'ự', 'Ỳ' => 'Ỳ', 'ỳ' => 'ỳ', 'Ỵ' => 'Ỵ', 'ỵ' => 'ỵ', 'Ỷ' => 'Ỷ', 'ỷ' => 'ỷ', 'Ỹ' => 'Ỹ', 'ỹ' => 'ỹ', 'ἀ' => 'ἀ', 'ἁ' => 'ἁ', 'ἂ' => 'ἂ', 'ἃ' => 'ἃ', 'ἄ' => 'ἄ', 'ἅ' => 'ἅ', 'ἆ' => 'ἆ', 'ἇ' => 'ἇ', 'Ἀ' => 'Ἀ', 'Ἁ' => 'Ἁ', 'Ἂ' => 'Ἂ', 'Ἃ' => 'Ἃ', 'Ἄ' => 'Ἄ', 'Ἅ' => 'Ἅ', 'Ἆ' => 'Ἆ', 'Ἇ' => 'Ἇ', 'ἐ' => 'ἐ', 'ἑ' => 'ἑ', 'ἒ' => 'ἒ', 'ἓ' => 'ἓ', 'ἔ' => 'ἔ', 'ἕ' => 'ἕ', 'Ἐ' => 'Ἐ', 'Ἑ' => 'Ἑ', 'Ἒ' => 'Ἒ', 'Ἓ' => 'Ἓ', 'Ἔ' => 'Ἔ', 'Ἕ' => 'Ἕ', 'ἠ' => 'ἠ', 'ἡ' => 'ἡ', 'ἢ' => 'ἢ', 'ἣ' => 'ἣ', 'ἤ' => 'ἤ', 'ἥ' => 'ἥ', 'ἦ' => 'ἦ', 'ἧ' => 'ἧ', 'Ἠ' => 'Ἠ', 'Ἡ' => 'Ἡ', 'Ἢ' => 'Ἢ', 'Ἣ' => 'Ἣ', 'Ἤ' => 'Ἤ', 'Ἥ' => 'Ἥ', 'Ἦ' => 'Ἦ', 'Ἧ' => 'Ἧ', 'ἰ' => 'ἰ', 'ἱ' => 'ἱ', 'ἲ' => 'ἲ', 'ἳ' => 'ἳ', 'ἴ' => 'ἴ', 'ἵ' => 'ἵ', 'ἶ' => 'ἶ', 'ἷ' => 'ἷ', 'Ἰ' => 'Ἰ', 'Ἱ' => 'Ἱ', 'Ἲ' => 'Ἲ', 'Ἳ' => 'Ἳ', 'Ἴ' => 'Ἴ', 'Ἵ' => 'Ἵ', 'Ἶ' => 'Ἶ', 'Ἷ' => 'Ἷ', 'ὀ' => 'ὀ', 'ὁ' => 'ὁ', 'ὂ' => 'ὂ', 'ὃ' => 'ὃ', 'ὄ' => 'ὄ', 'ὅ' => 'ὅ', 'Ὀ' => 'Ὀ', 'Ὁ' => 'Ὁ', 'Ὂ' => 'Ὂ', 'Ὃ' => 'Ὃ', 'Ὄ' => 'Ὄ', 'Ὅ' => 'Ὅ', 'ὐ' => 'ὐ', 'ὑ' => 'ὑ', 'ὒ' => 'ὒ', 'ὓ' => 'ὓ', 'ὔ' => 'ὔ', 'ὕ' => 'ὕ', 'ὖ' => 'ὖ', 'ὗ' => 'ὗ', 'Ὑ' => 'Ὑ', 'Ὓ' => 'Ὓ', 'Ὕ' => 'Ὕ', 'Ὗ' => 'Ὗ', 'ὠ' => 'ὠ', 'ὡ' => 'ὡ', 'ὢ' => 'ὢ', 'ὣ' => 'ὣ', 'ὤ' => 'ὤ', 'ὥ' => 'ὥ', 'ὦ' => 'ὦ', 'ὧ' => 'ὧ', 'Ὠ' => 'Ὠ', 'Ὡ' => 'Ὡ', 'Ὢ' => 'Ὢ', 'Ὣ' => 'Ὣ', 'Ὤ' => 'Ὤ', 'Ὥ' => 'Ὥ', 'Ὦ' => 'Ὦ', 'Ὧ' => 'Ὧ', 'ὰ' => 'ὰ', 'ὲ' => 'ὲ', 'ὴ' => 'ὴ', 'ὶ' => 'ὶ', 'ὸ' => 'ὸ', 'ὺ' => 'ὺ', 'ὼ' => 'ὼ', 'ᾀ' => 'ᾀ', 'ᾁ' => 'ᾁ', 'ᾂ' => 'ᾂ', 'ᾃ' => 'ᾃ', 'ᾄ' => 'ᾄ', 'ᾅ' => 'ᾅ', 'ᾆ' => 'ᾆ', 'ᾇ' => 'ᾇ', 'ᾈ' => 'ᾈ', 'ᾉ' => 'ᾉ', 'ᾊ' => 'ᾊ', 'ᾋ' => 'ᾋ', 'ᾌ' => 'ᾌ', 'ᾍ' => 'ᾍ', 'ᾎ' => 'ᾎ', 'ᾏ' => 'ᾏ', 'ᾐ' => 'ᾐ', 'ᾑ' => 'ᾑ', 'ᾒ' => 'ᾒ', 'ᾓ' => 'ᾓ', 'ᾔ' => 'ᾔ', 'ᾕ' => 'ᾕ', 'ᾖ' => 'ᾖ', 'ᾗ' => 'ᾗ', 'ᾘ' => 'ᾘ', 'ᾙ' => 'ᾙ', 'ᾚ' => 'ᾚ', 'ᾛ' => 'ᾛ', 'ᾜ' => 'ᾜ', 'ᾝ' => 'ᾝ', 'ᾞ' => 'ᾞ', 'ᾟ' => 'ᾟ', 'ᾠ' => 'ᾠ', 'ᾡ' => 'ᾡ', 'ᾢ' => 'ᾢ', 'ᾣ' => 'ᾣ', 'ᾤ' => 'ᾤ', 'ᾥ' => 'ᾥ', 'ᾦ' => 'ᾦ', 'ᾧ' => 'ᾧ', 'ᾨ' => 'ᾨ', 'ᾩ' => 'ᾩ', 'ᾪ' => 'ᾪ', 'ᾫ' => 'ᾫ', 'ᾬ' => 'ᾬ', 'ᾭ' => 'ᾭ', 'ᾮ' => 'ᾮ', 'ᾯ' => 'ᾯ', 'ᾰ' => 'ᾰ', 'ᾱ' => 'ᾱ', 'ᾲ' => 'ᾲ', 'ᾳ' => 'ᾳ', 'ᾴ' => 'ᾴ', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾷ', 'Ᾰ' => 'Ᾰ', 'Ᾱ' => 'Ᾱ', 'Ὰ' => 'Ὰ', 'ᾼ' => 'ᾼ', '῁' => '῁', 'ῂ' => 'ῂ', 'ῃ' => 'ῃ', 'ῄ' => 'ῄ', 'ῆ' => 'ῆ', 'ῇ' => 'ῇ', 'Ὲ' => 'Ὲ', 'Ὴ' => 'Ὴ', 'ῌ' => 'ῌ', '῍' => '῍', '῎' => '῎', '῏' => '῏', 'ῐ' => 'ῐ', 'ῑ' => 'ῑ', 'ῒ' => 'ῒ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'Ῐ' => 'Ῐ', 'Ῑ' => 'Ῑ', 'Ὶ' => 'Ὶ', '῝' => '῝', '῞' => '῞', '῟' => '῟', 'ῠ' => 'ῠ', 'ῡ' => 'ῡ', 'ῢ' => 'ῢ', 'ῤ' => 'ῤ', 'ῥ' => 'ῥ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'Ῠ' => 'Ῠ', 'Ῡ' => 'Ῡ', 'Ὺ' => 'Ὺ', 'Ῥ' => 'Ῥ', '῭' => '῭', 'ῲ' => 'ῲ', 'ῳ' => 'ῳ', 'ῴ' => 'ῴ', 'ῶ' => 'ῶ', 'ῷ' => 'ῷ', 'Ὸ' => 'Ὸ', 'Ὼ' => 'Ὼ', 'ῼ' => 'ῼ', '↚' => '↚', '↛' => '↛', '↮' => '↮', '⇍' => '⇍', '⇎' => '⇎', '⇏' => '⇏', '∄' => '∄', '∉' => '∉', '∌' => '∌', '∤' => '∤', '∦' => '∦', '≁' => '≁', '≄' => '≄', '≇' => '≇', '≉' => '≉', '≠' => '≠', '≢' => '≢', '≭' => '≭', '≮' => '≮', '≯' => '≯', '≰' => '≰', '≱' => '≱', '≴' => '≴', '≵' => '≵', '≸' => '≸', '≹' => '≹', '⊀' => '⊀', '⊁' => '⊁', '⊄' => '⊄', '⊅' => '⊅', '⊈' => '⊈', '⊉' => '⊉', '⊬' => '⊬', '⊭' => '⊭', '⊮' => '⊮', '⊯' => '⊯', '⋠' => '⋠', '⋡' => '⋡', '⋢' => '⋢', '⋣' => '⋣', '⋪' => '⋪', '⋫' => '⋫', '⋬' => '⋬', '⋭' => '⋭', 'が' => 'が', 'ぎ' => 'ぎ', 'ぐ' => 'ぐ', 'げ' => 'げ', 'ご' => 'ご', 'ざ' => 'ざ', 'じ' => 'じ', 'ず' => 'ず', 'ぜ' => 'ぜ', 'ぞ' => 'ぞ', 'だ' => 'だ', 'ぢ' => 'ぢ', 'づ' => 'づ', 'で' => 'で', 'ど' => 'ど', 'ば' => 'ば', 'ぱ' => 'ぱ', 'び' => 'び', 'ぴ' => 'ぴ', 'ぶ' => 'ぶ', 'ぷ' => 'ぷ', 'べ' => 'べ', 'ぺ' => 'ぺ', 'ぼ' => 'ぼ', 'ぽ' => 'ぽ', 'ゔ' => 'ゔ', 'ゞ' => 'ゞ', 'ガ' => 'ガ', 'ギ' => 'ギ', 'グ' => 'グ', 'ゲ' => 'ゲ', 'ゴ' => 'ゴ', 'ザ' => 'ザ', 'ジ' => 'ジ', 'ズ' => 'ズ', 'ゼ' => 'ゼ', 'ゾ' => 'ゾ', 'ダ' => 'ダ', 'ヂ' => 'ヂ', 'ヅ' => 'ヅ', 'デ' => 'デ', 'ド' => 'ド', 'バ' => 'バ', 'パ' => 'パ', 'ビ' => 'ビ', 'ピ' => 'ピ', 'ブ' => 'ブ', 'プ' => 'プ', 'ベ' => 'ベ', 'ペ' => 'ペ', 'ボ' => 'ボ', 'ポ' => 'ポ', 'ヴ' => 'ヴ', 'ヷ' => 'ヷ', 'ヸ' => 'ヸ', 'ヹ' => 'ヹ', 'ヺ' => 'ヺ', 'ヾ' => 'ヾ', '𑂚' => '𑂚', '𑂜' => '𑂜', '𑂫' => '𑂫', '𑄮' => '𑄮', '𑄯' => '𑄯', '𑍋' => '𑍋', '𑍌' => '𑍌', '𑒻' => '𑒻', '𑒼' => '𑒼', '𑒾' => '𑒾', '𑖺' => '𑖺', '𑖻' => '𑖻', '𑤸' => '𑤸', ); PK!je{{Tvendor/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.phpnu[ 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'ÿ' => 'ÿ', 'Ā' => 'Ā', 'ā' => 'ā', 'Ă' => 'Ă', 'ă' => 'ă', 'Ą' => 'Ą', 'ą' => 'ą', 'Ć' => 'Ć', 'ć' => 'ć', 'Ĉ' => 'Ĉ', 'ĉ' => 'ĉ', 'Ċ' => 'Ċ', 'ċ' => 'ċ', 'Č' => 'Č', 'č' => 'č', 'Ď' => 'Ď', 'ď' => 'ď', 'Ē' => 'Ē', 'ē' => 'ē', 'Ĕ' => 'Ĕ', 'ĕ' => 'ĕ', 'Ė' => 'Ė', 'ė' => 'ė', 'Ę' => 'Ę', 'ę' => 'ę', 'Ě' => 'Ě', 'ě' => 'ě', 'Ĝ' => 'Ĝ', 'ĝ' => 'ĝ', 'Ğ' => 'Ğ', 'ğ' => 'ğ', 'Ġ' => 'Ġ', 'ġ' => 'ġ', 'Ģ' => 'Ģ', 'ģ' => 'ģ', 'Ĥ' => 'Ĥ', 'ĥ' => 'ĥ', 'Ĩ' => 'Ĩ', 'ĩ' => 'ĩ', 'Ī' => 'Ī', 'ī' => 'ī', 'Ĭ' => 'Ĭ', 'ĭ' => 'ĭ', 'Į' => 'Į', 'į' => 'į', 'İ' => 'İ', 'Ĵ' => 'Ĵ', 'ĵ' => 'ĵ', 'Ķ' => 'Ķ', 'ķ' => 'ķ', 'Ĺ' => 'Ĺ', 'ĺ' => 'ĺ', 'Ļ' => 'Ļ', 'ļ' => 'ļ', 'Ľ' => 'Ľ', 'ľ' => 'ľ', 'Ń' => 'Ń', 'ń' => 'ń', 'Ņ' => 'Ņ', 'ņ' => 'ņ', 'Ň' => 'Ň', 'ň' => 'ň', 'Ō' => 'Ō', 'ō' => 'ō', 'Ŏ' => 'Ŏ', 'ŏ' => 'ŏ', 'Ő' => 'Ő', 'ő' => 'ő', 'Ŕ' => 'Ŕ', 'ŕ' => 'ŕ', 'Ŗ' => 'Ŗ', 'ŗ' => 'ŗ', 'Ř' => 'Ř', 'ř' => 'ř', 'Ś' => 'Ś', 'ś' => 'ś', 'Ŝ' => 'Ŝ', 'ŝ' => 'ŝ', 'Ş' => 'Ş', 'ş' => 'ş', 'Š' => 'Š', 'š' => 'š', 'Ţ' => 'Ţ', 'ţ' => 'ţ', 'Ť' => 'Ť', 'ť' => 'ť', 'Ũ' => 'Ũ', 'ũ' => 'ũ', 'Ū' => 'Ū', 'ū' => 'ū', 'Ŭ' => 'Ŭ', 'ŭ' => 'ŭ', 'Ů' => 'Ů', 'ů' => 'ů', 'Ű' => 'Ű', 'ű' => 'ű', 'Ų' => 'Ų', 'ų' => 'ų', 'Ŵ' => 'Ŵ', 'ŵ' => 'ŵ', 'Ŷ' => 'Ŷ', 'ŷ' => 'ŷ', 'Ÿ' => 'Ÿ', 'Ź' => 'Ź', 'ź' => 'ź', 'Ż' => 'Ż', 'ż' => 'ż', 'Ž' => 'Ž', 'ž' => 'ž', 'Ơ' => 'Ơ', 'ơ' => 'ơ', 'Ư' => 'Ư', 'ư' => 'ư', 'Ǎ' => 'Ǎ', 'ǎ' => 'ǎ', 'Ǐ' => 'Ǐ', 'ǐ' => 'ǐ', 'Ǒ' => 'Ǒ', 'ǒ' => 'ǒ', 'Ǔ' => 'Ǔ', 'ǔ' => 'ǔ', 'Ǖ' => 'Ǖ', 'ǖ' => 'ǖ', 'Ǘ' => 'Ǘ', 'ǘ' => 'ǘ', 'Ǚ' => 'Ǚ', 'ǚ' => 'ǚ', 'Ǜ' => 'Ǜ', 'ǜ' => 'ǜ', 'Ǟ' => 'Ǟ', 'ǟ' => 'ǟ', 'Ǡ' => 'Ǡ', 'ǡ' => 'ǡ', 'Ǣ' => 'Ǣ', 'ǣ' => 'ǣ', 'Ǧ' => 'Ǧ', 'ǧ' => 'ǧ', 'Ǩ' => 'Ǩ', 'ǩ' => 'ǩ', 'Ǫ' => 'Ǫ', 'ǫ' => 'ǫ', 'Ǭ' => 'Ǭ', 'ǭ' => 'ǭ', 'Ǯ' => 'Ǯ', 'ǯ' => 'ǯ', 'ǰ' => 'ǰ', 'Ǵ' => 'Ǵ', 'ǵ' => 'ǵ', 'Ǹ' => 'Ǹ', 'ǹ' => 'ǹ', 'Ǻ' => 'Ǻ', 'ǻ' => 'ǻ', 'Ǽ' => 'Ǽ', 'ǽ' => 'ǽ', 'Ǿ' => 'Ǿ', 'ǿ' => 'ǿ', 'Ȁ' => 'Ȁ', 'ȁ' => 'ȁ', 'Ȃ' => 'Ȃ', 'ȃ' => 'ȃ', 'Ȅ' => 'Ȅ', 'ȅ' => 'ȅ', 'Ȇ' => 'Ȇ', 'ȇ' => 'ȇ', 'Ȉ' => 'Ȉ', 'ȉ' => 'ȉ', 'Ȋ' => 'Ȋ', 'ȋ' => 'ȋ', 'Ȍ' => 'Ȍ', 'ȍ' => 'ȍ', 'Ȏ' => 'Ȏ', 'ȏ' => 'ȏ', 'Ȑ' => 'Ȑ', 'ȑ' => 'ȑ', 'Ȓ' => 'Ȓ', 'ȓ' => 'ȓ', 'Ȕ' => 'Ȕ', 'ȕ' => 'ȕ', 'Ȗ' => 'Ȗ', 'ȗ' => 'ȗ', 'Ș' => 'Ș', 'ș' => 'ș', 'Ț' => 'Ț', 'ț' => 'ț', 'Ȟ' => 'Ȟ', 'ȟ' => 'ȟ', 'Ȧ' => 'Ȧ', 'ȧ' => 'ȧ', 'Ȩ' => 'Ȩ', 'ȩ' => 'ȩ', 'Ȫ' => 'Ȫ', 'ȫ' => 'ȫ', 'Ȭ' => 'Ȭ', 'ȭ' => 'ȭ', 'Ȯ' => 'Ȯ', 'ȯ' => 'ȯ', 'Ȱ' => 'Ȱ', 'ȱ' => 'ȱ', 'Ȳ' => 'Ȳ', 'ȳ' => 'ȳ', '̀' => '̀', '́' => '́', '̓' => '̓', '̈́' => '̈́', 'ʹ' => 'ʹ', ';' => ';', '΅' => '΅', 'Ά' => 'Ά', '·' => '·', 'Έ' => 'Έ', 'Ή' => 'Ή', 'Ί' => 'Ί', 'Ό' => 'Ό', 'Ύ' => 'Ύ', 'Ώ' => 'Ώ', 'ΐ' => 'ΐ', 'Ϊ' => 'Ϊ', 'Ϋ' => 'Ϋ', 'ά' => 'ά', 'έ' => 'έ', 'ή' => 'ή', 'ί' => 'ί', 'ΰ' => 'ΰ', 'ϊ' => 'ϊ', 'ϋ' => 'ϋ', 'ό' => 'ό', 'ύ' => 'ύ', 'ώ' => 'ώ', 'ϓ' => 'ϓ', 'ϔ' => 'ϔ', 'Ѐ' => 'Ѐ', 'Ё' => 'Ё', 'Ѓ' => 'Ѓ', 'Ї' => 'Ї', 'Ќ' => 'Ќ', 'Ѝ' => 'Ѝ', 'Ў' => 'Ў', 'Й' => 'Й', 'й' => 'й', 'ѐ' => 'ѐ', 'ё' => 'ё', 'ѓ' => 'ѓ', 'ї' => 'ї', 'ќ' => 'ќ', 'ѝ' => 'ѝ', 'ў' => 'ў', 'Ѷ' => 'Ѷ', 'ѷ' => 'ѷ', 'Ӂ' => 'Ӂ', 'ӂ' => 'ӂ', 'Ӑ' => 'Ӑ', 'ӑ' => 'ӑ', 'Ӓ' => 'Ӓ', 'ӓ' => 'ӓ', 'Ӗ' => 'Ӗ', 'ӗ' => 'ӗ', 'Ӛ' => 'Ӛ', 'ӛ' => 'ӛ', 'Ӝ' => 'Ӝ', 'ӝ' => 'ӝ', 'Ӟ' => 'Ӟ', 'ӟ' => 'ӟ', 'Ӣ' => 'Ӣ', 'ӣ' => 'ӣ', 'Ӥ' => 'Ӥ', 'ӥ' => 'ӥ', 'Ӧ' => 'Ӧ', 'ӧ' => 'ӧ', 'Ӫ' => 'Ӫ', 'ӫ' => 'ӫ', 'Ӭ' => 'Ӭ', 'ӭ' => 'ӭ', 'Ӯ' => 'Ӯ', 'ӯ' => 'ӯ', 'Ӱ' => 'Ӱ', 'ӱ' => 'ӱ', 'Ӳ' => 'Ӳ', 'ӳ' => 'ӳ', 'Ӵ' => 'Ӵ', 'ӵ' => 'ӵ', 'Ӹ' => 'Ӹ', 'ӹ' => 'ӹ', 'آ' => 'آ', 'أ' => 'أ', 'ؤ' => 'ؤ', 'إ' => 'إ', 'ئ' => 'ئ', 'ۀ' => 'ۀ', 'ۂ' => 'ۂ', 'ۓ' => 'ۓ', 'ऩ' => 'ऩ', 'ऱ' => 'ऱ', 'ऴ' => 'ऴ', 'क़' => 'क़', 'ख़' => 'ख़', 'ग़' => 'ग़', 'ज़' => 'ज़', 'ड़' => 'ड़', 'ढ़' => 'ढ़', 'फ़' => 'फ़', 'य़' => 'य़', 'ো' => 'ো', 'ৌ' => 'ৌ', 'ড়' => 'ড়', 'ঢ়' => 'ঢ়', 'য়' => 'য়', 'ਲ਼' => 'ਲ਼', 'ਸ਼' => 'ਸ਼', 'ਖ਼' => 'ਖ਼', 'ਗ਼' => 'ਗ਼', 'ਜ਼' => 'ਜ਼', 'ਫ਼' => 'ਫ਼', 'ୈ' => 'ୈ', 'ୋ' => 'ୋ', 'ୌ' => 'ୌ', 'ଡ଼' => 'ଡ଼', 'ଢ଼' => 'ଢ଼', 'ஔ' => 'ஔ', 'ொ' => 'ொ', 'ோ' => 'ோ', 'ௌ' => 'ௌ', 'ై' => 'ై', 'ೀ' => 'ೀ', 'ೇ' => 'ೇ', 'ೈ' => 'ೈ', 'ೊ' => 'ೊ', 'ೋ' => 'ೋ', 'ൊ' => 'ൊ', 'ോ' => 'ോ', 'ൌ' => 'ൌ', 'ේ' => 'ේ', 'ො' => 'ො', 'ෝ' => 'ෝ', 'ෞ' => 'ෞ', 'གྷ' => 'གྷ', 'ཌྷ' => 'ཌྷ', 'དྷ' => 'དྷ', 'བྷ' => 'བྷ', 'ཛྷ' => 'ཛྷ', 'ཀྵ' => 'ཀྵ', 'ཱི' => 'ཱི', 'ཱུ' => 'ཱུ', 'ྲྀ' => 'ྲྀ', 'ླྀ' => 'ླྀ', 'ཱྀ' => 'ཱྀ', 'ྒྷ' => 'ྒྷ', 'ྜྷ' => 'ྜྷ', 'ྡྷ' => 'ྡྷ', 'ྦྷ' => 'ྦྷ', 'ྫྷ' => 'ྫྷ', 'ྐྵ' => 'ྐྵ', 'ဦ' => 'ဦ', 'ᬆ' => 'ᬆ', 'ᬈ' => 'ᬈ', 'ᬊ' => 'ᬊ', 'ᬌ' => 'ᬌ', 'ᬎ' => 'ᬎ', 'ᬒ' => 'ᬒ', 'ᬻ' => 'ᬻ', 'ᬽ' => 'ᬽ', 'ᭀ' => 'ᭀ', 'ᭁ' => 'ᭁ', 'ᭃ' => 'ᭃ', 'Ḁ' => 'Ḁ', 'ḁ' => 'ḁ', 'Ḃ' => 'Ḃ', 'ḃ' => 'ḃ', 'Ḅ' => 'Ḅ', 'ḅ' => 'ḅ', 'Ḇ' => 'Ḇ', 'ḇ' => 'ḇ', 'Ḉ' => 'Ḉ', 'ḉ' => 'ḉ', 'Ḋ' => 'Ḋ', 'ḋ' => 'ḋ', 'Ḍ' => 'Ḍ', 'ḍ' => 'ḍ', 'Ḏ' => 'Ḏ', 'ḏ' => 'ḏ', 'Ḑ' => 'Ḑ', 'ḑ' => 'ḑ', 'Ḓ' => 'Ḓ', 'ḓ' => 'ḓ', 'Ḕ' => 'Ḕ', 'ḕ' => 'ḕ', 'Ḗ' => 'Ḗ', 'ḗ' => 'ḗ', 'Ḙ' => 'Ḙ', 'ḙ' => 'ḙ', 'Ḛ' => 'Ḛ', 'ḛ' => 'ḛ', 'Ḝ' => 'Ḝ', 'ḝ' => 'ḝ', 'Ḟ' => 'Ḟ', 'ḟ' => 'ḟ', 'Ḡ' => 'Ḡ', 'ḡ' => 'ḡ', 'Ḣ' => 'Ḣ', 'ḣ' => 'ḣ', 'Ḥ' => 'Ḥ', 'ḥ' => 'ḥ', 'Ḧ' => 'Ḧ', 'ḧ' => 'ḧ', 'Ḩ' => 'Ḩ', 'ḩ' => 'ḩ', 'Ḫ' => 'Ḫ', 'ḫ' => 'ḫ', 'Ḭ' => 'Ḭ', 'ḭ' => 'ḭ', 'Ḯ' => 'Ḯ', 'ḯ' => 'ḯ', 'Ḱ' => 'Ḱ', 'ḱ' => 'ḱ', 'Ḳ' => 'Ḳ', 'ḳ' => 'ḳ', 'Ḵ' => 'Ḵ', 'ḵ' => 'ḵ', 'Ḷ' => 'Ḷ', 'ḷ' => 'ḷ', 'Ḹ' => 'Ḹ', 'ḹ' => 'ḹ', 'Ḻ' => 'Ḻ', 'ḻ' => 'ḻ', 'Ḽ' => 'Ḽ', 'ḽ' => 'ḽ', 'Ḿ' => 'Ḿ', 'ḿ' => 'ḿ', 'Ṁ' => 'Ṁ', 'ṁ' => 'ṁ', 'Ṃ' => 'Ṃ', 'ṃ' => 'ṃ', 'Ṅ' => 'Ṅ', 'ṅ' => 'ṅ', 'Ṇ' => 'Ṇ', 'ṇ' => 'ṇ', 'Ṉ' => 'Ṉ', 'ṉ' => 'ṉ', 'Ṋ' => 'Ṋ', 'ṋ' => 'ṋ', 'Ṍ' => 'Ṍ', 'ṍ' => 'ṍ', 'Ṏ' => 'Ṏ', 'ṏ' => 'ṏ', 'Ṑ' => 'Ṑ', 'ṑ' => 'ṑ', 'Ṓ' => 'Ṓ', 'ṓ' => 'ṓ', 'Ṕ' => 'Ṕ', 'ṕ' => 'ṕ', 'Ṗ' => 'Ṗ', 'ṗ' => 'ṗ', 'Ṙ' => 'Ṙ', 'ṙ' => 'ṙ', 'Ṛ' => 'Ṛ', 'ṛ' => 'ṛ', 'Ṝ' => 'Ṝ', 'ṝ' => 'ṝ', 'Ṟ' => 'Ṟ', 'ṟ' => 'ṟ', 'Ṡ' => 'Ṡ', 'ṡ' => 'ṡ', 'Ṣ' => 'Ṣ', 'ṣ' => 'ṣ', 'Ṥ' => 'Ṥ', 'ṥ' => 'ṥ', 'Ṧ' => 'Ṧ', 'ṧ' => 'ṧ', 'Ṩ' => 'Ṩ', 'ṩ' => 'ṩ', 'Ṫ' => 'Ṫ', 'ṫ' => 'ṫ', 'Ṭ' => 'Ṭ', 'ṭ' => 'ṭ', 'Ṯ' => 'Ṯ', 'ṯ' => 'ṯ', 'Ṱ' => 'Ṱ', 'ṱ' => 'ṱ', 'Ṳ' => 'Ṳ', 'ṳ' => 'ṳ', 'Ṵ' => 'Ṵ', 'ṵ' => 'ṵ', 'Ṷ' => 'Ṷ', 'ṷ' => 'ṷ', 'Ṹ' => 'Ṹ', 'ṹ' => 'ṹ', 'Ṻ' => 'Ṻ', 'ṻ' => 'ṻ', 'Ṽ' => 'Ṽ', 'ṽ' => 'ṽ', 'Ṿ' => 'Ṿ', 'ṿ' => 'ṿ', 'Ẁ' => 'Ẁ', 'ẁ' => 'ẁ', 'Ẃ' => 'Ẃ', 'ẃ' => 'ẃ', 'Ẅ' => 'Ẅ', 'ẅ' => 'ẅ', 'Ẇ' => 'Ẇ', 'ẇ' => 'ẇ', 'Ẉ' => 'Ẉ', 'ẉ' => 'ẉ', 'Ẋ' => 'Ẋ', 'ẋ' => 'ẋ', 'Ẍ' => 'Ẍ', 'ẍ' => 'ẍ', 'Ẏ' => 'Ẏ', 'ẏ' => 'ẏ', 'Ẑ' => 'Ẑ', 'ẑ' => 'ẑ', 'Ẓ' => 'Ẓ', 'ẓ' => 'ẓ', 'Ẕ' => 'Ẕ', 'ẕ' => 'ẕ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẛ' => 'ẛ', 'Ạ' => 'Ạ', 'ạ' => 'ạ', 'Ả' => 'Ả', 'ả' => 'ả', 'Ấ' => 'Ấ', 'ấ' => 'ấ', 'Ầ' => 'Ầ', 'ầ' => 'ầ', 'Ẩ' => 'Ẩ', 'ẩ' => 'ẩ', 'Ẫ' => 'Ẫ', 'ẫ' => 'ẫ', 'Ậ' => 'Ậ', 'ậ' => 'ậ', 'Ắ' => 'Ắ', 'ắ' => 'ắ', 'Ằ' => 'Ằ', 'ằ' => 'ằ', 'Ẳ' => 'Ẳ', 'ẳ' => 'ẳ', 'Ẵ' => 'Ẵ', 'ẵ' => 'ẵ', 'Ặ' => 'Ặ', 'ặ' => 'ặ', 'Ẹ' => 'Ẹ', 'ẹ' => 'ẹ', 'Ẻ' => 'Ẻ', 'ẻ' => 'ẻ', 'Ẽ' => 'Ẽ', 'ẽ' => 'ẽ', 'Ế' => 'Ế', 'ế' => 'ế', 'Ề' => 'Ề', 'ề' => 'ề', 'Ể' => 'Ể', 'ể' => 'ể', 'Ễ' => 'Ễ', 'ễ' => 'ễ', 'Ệ' => 'Ệ', 'ệ' => 'ệ', 'Ỉ' => 'Ỉ', 'ỉ' => 'ỉ', 'Ị' => 'Ị', 'ị' => 'ị', 'Ọ' => 'Ọ', 'ọ' => 'ọ', 'Ỏ' => 'Ỏ', 'ỏ' => 'ỏ', 'Ố' => 'Ố', 'ố' => 'ố', 'Ồ' => 'Ồ', 'ồ' => 'ồ', 'Ổ' => 'Ổ', 'ổ' => 'ổ', 'Ỗ' => 'Ỗ', 'ỗ' => 'ỗ', 'Ộ' => 'Ộ', 'ộ' => 'ộ', 'Ớ' => 'Ớ', 'ớ' => 'ớ', 'Ờ' => 'Ờ', 'ờ' => 'ờ', 'Ở' => 'Ở', 'ở' => 'ở', 'Ỡ' => 'Ỡ', 'ỡ' => 'ỡ', 'Ợ' => 'Ợ', 'ợ' => 'ợ', 'Ụ' => 'Ụ', 'ụ' => 'ụ', 'Ủ' => 'Ủ', 'ủ' => 'ủ', 'Ứ' => 'Ứ', 'ứ' => 'ứ', 'Ừ' => 'Ừ', 'ừ' => 'ừ', 'Ử' => 'Ử', 'ử' => 'ử', 'Ữ' => 'Ữ', 'ữ' => 'ữ', 'Ự' => 'Ự', 'ự' => 'ự', 'Ỳ' => 'Ỳ', 'ỳ' => 'ỳ', 'Ỵ' => 'Ỵ', 'ỵ' => 'ỵ', 'Ỷ' => 'Ỷ', 'ỷ' => 'ỷ', 'Ỹ' => 'Ỹ', 'ỹ' => 'ỹ', 'ἀ' => 'ἀ', 'ἁ' => 'ἁ', 'ἂ' => 'ἂ', 'ἃ' => 'ἃ', 'ἄ' => 'ἄ', 'ἅ' => 'ἅ', 'ἆ' => 'ἆ', 'ἇ' => 'ἇ', 'Ἀ' => 'Ἀ', 'Ἁ' => 'Ἁ', 'Ἂ' => 'Ἂ', 'Ἃ' => 'Ἃ', 'Ἄ' => 'Ἄ', 'Ἅ' => 'Ἅ', 'Ἆ' => 'Ἆ', 'Ἇ' => 'Ἇ', 'ἐ' => 'ἐ', 'ἑ' => 'ἑ', 'ἒ' => 'ἒ', 'ἓ' => 'ἓ', 'ἔ' => 'ἔ', 'ἕ' => 'ἕ', 'Ἐ' => 'Ἐ', 'Ἑ' => 'Ἑ', 'Ἒ' => 'Ἒ', 'Ἓ' => 'Ἓ', 'Ἔ' => 'Ἔ', 'Ἕ' => 'Ἕ', 'ἠ' => 'ἠ', 'ἡ' => 'ἡ', 'ἢ' => 'ἢ', 'ἣ' => 'ἣ', 'ἤ' => 'ἤ', 'ἥ' => 'ἥ', 'ἦ' => 'ἦ', 'ἧ' => 'ἧ', 'Ἠ' => 'Ἠ', 'Ἡ' => 'Ἡ', 'Ἢ' => 'Ἢ', 'Ἣ' => 'Ἣ', 'Ἤ' => 'Ἤ', 'Ἥ' => 'Ἥ', 'Ἦ' => 'Ἦ', 'Ἧ' => 'Ἧ', 'ἰ' => 'ἰ', 'ἱ' => 'ἱ', 'ἲ' => 'ἲ', 'ἳ' => 'ἳ', 'ἴ' => 'ἴ', 'ἵ' => 'ἵ', 'ἶ' => 'ἶ', 'ἷ' => 'ἷ', 'Ἰ' => 'Ἰ', 'Ἱ' => 'Ἱ', 'Ἲ' => 'Ἲ', 'Ἳ' => 'Ἳ', 'Ἴ' => 'Ἴ', 'Ἵ' => 'Ἵ', 'Ἶ' => 'Ἶ', 'Ἷ' => 'Ἷ', 'ὀ' => 'ὀ', 'ὁ' => 'ὁ', 'ὂ' => 'ὂ', 'ὃ' => 'ὃ', 'ὄ' => 'ὄ', 'ὅ' => 'ὅ', 'Ὀ' => 'Ὀ', 'Ὁ' => 'Ὁ', 'Ὂ' => 'Ὂ', 'Ὃ' => 'Ὃ', 'Ὄ' => 'Ὄ', 'Ὅ' => 'Ὅ', 'ὐ' => 'ὐ', 'ὑ' => 'ὑ', 'ὒ' => 'ὒ', 'ὓ' => 'ὓ', 'ὔ' => 'ὔ', 'ὕ' => 'ὕ', 'ὖ' => 'ὖ', 'ὗ' => 'ὗ', 'Ὑ' => 'Ὑ', 'Ὓ' => 'Ὓ', 'Ὕ' => 'Ὕ', 'Ὗ' => 'Ὗ', 'ὠ' => 'ὠ', 'ὡ' => 'ὡ', 'ὢ' => 'ὢ', 'ὣ' => 'ὣ', 'ὤ' => 'ὤ', 'ὥ' => 'ὥ', 'ὦ' => 'ὦ', 'ὧ' => 'ὧ', 'Ὠ' => 'Ὠ', 'Ὡ' => 'Ὡ', 'Ὢ' => 'Ὢ', 'Ὣ' => 'Ὣ', 'Ὤ' => 'Ὤ', 'Ὥ' => 'Ὥ', 'Ὦ' => 'Ὦ', 'Ὧ' => 'Ὧ', 'ὰ' => 'ὰ', 'ά' => 'ά', 'ὲ' => 'ὲ', 'έ' => 'έ', 'ὴ' => 'ὴ', 'ή' => 'ή', 'ὶ' => 'ὶ', 'ί' => 'ί', 'ὸ' => 'ὸ', 'ό' => 'ό', 'ὺ' => 'ὺ', 'ύ' => 'ύ', 'ὼ' => 'ὼ', 'ώ' => 'ώ', 'ᾀ' => 'ᾀ', 'ᾁ' => 'ᾁ', 'ᾂ' => 'ᾂ', 'ᾃ' => 'ᾃ', 'ᾄ' => 'ᾄ', 'ᾅ' => 'ᾅ', 'ᾆ' => 'ᾆ', 'ᾇ' => 'ᾇ', 'ᾈ' => 'ᾈ', 'ᾉ' => 'ᾉ', 'ᾊ' => 'ᾊ', 'ᾋ' => 'ᾋ', 'ᾌ' => 'ᾌ', 'ᾍ' => 'ᾍ', 'ᾎ' => 'ᾎ', 'ᾏ' => 'ᾏ', 'ᾐ' => 'ᾐ', 'ᾑ' => 'ᾑ', 'ᾒ' => 'ᾒ', 'ᾓ' => 'ᾓ', 'ᾔ' => 'ᾔ', 'ᾕ' => 'ᾕ', 'ᾖ' => 'ᾖ', 'ᾗ' => 'ᾗ', 'ᾘ' => 'ᾘ', 'ᾙ' => 'ᾙ', 'ᾚ' => 'ᾚ', 'ᾛ' => 'ᾛ', 'ᾜ' => 'ᾜ', 'ᾝ' => 'ᾝ', 'ᾞ' => 'ᾞ', 'ᾟ' => 'ᾟ', 'ᾠ' => 'ᾠ', 'ᾡ' => 'ᾡ', 'ᾢ' => 'ᾢ', 'ᾣ' => 'ᾣ', 'ᾤ' => 'ᾤ', 'ᾥ' => 'ᾥ', 'ᾦ' => 'ᾦ', 'ᾧ' => 'ᾧ', 'ᾨ' => 'ᾨ', 'ᾩ' => 'ᾩ', 'ᾪ' => 'ᾪ', 'ᾫ' => 'ᾫ', 'ᾬ' => 'ᾬ', 'ᾭ' => 'ᾭ', 'ᾮ' => 'ᾮ', 'ᾯ' => 'ᾯ', 'ᾰ' => 'ᾰ', 'ᾱ' => 'ᾱ', 'ᾲ' => 'ᾲ', 'ᾳ' => 'ᾳ', 'ᾴ' => 'ᾴ', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾷ', 'Ᾰ' => 'Ᾰ', 'Ᾱ' => 'Ᾱ', 'Ὰ' => 'Ὰ', 'Ά' => 'Ά', 'ᾼ' => 'ᾼ', 'ι' => 'ι', '῁' => '῁', 'ῂ' => 'ῂ', 'ῃ' => 'ῃ', 'ῄ' => 'ῄ', 'ῆ' => 'ῆ', 'ῇ' => 'ῇ', 'Ὲ' => 'Ὲ', 'Έ' => 'Έ', 'Ὴ' => 'Ὴ', 'Ή' => 'Ή', 'ῌ' => 'ῌ', '῍' => '῍', '῎' => '῎', '῏' => '῏', 'ῐ' => 'ῐ', 'ῑ' => 'ῑ', 'ῒ' => 'ῒ', 'ΐ' => 'ΐ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'Ῐ' => 'Ῐ', 'Ῑ' => 'Ῑ', 'Ὶ' => 'Ὶ', 'Ί' => 'Ί', '῝' => '῝', '῞' => '῞', '῟' => '῟', 'ῠ' => 'ῠ', 'ῡ' => 'ῡ', 'ῢ' => 'ῢ', 'ΰ' => 'ΰ', 'ῤ' => 'ῤ', 'ῥ' => 'ῥ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'Ῠ' => 'Ῠ', 'Ῡ' => 'Ῡ', 'Ὺ' => 'Ὺ', 'Ύ' => 'Ύ', 'Ῥ' => 'Ῥ', '῭' => '῭', '΅' => '΅', '`' => '`', 'ῲ' => 'ῲ', 'ῳ' => 'ῳ', 'ῴ' => 'ῴ', 'ῶ' => 'ῶ', 'ῷ' => 'ῷ', 'Ὸ' => 'Ὸ', 'Ό' => 'Ό', 'Ὼ' => 'Ὼ', 'Ώ' => 'Ώ', 'ῼ' => 'ῼ', '´' => '´', ' ' => ' ', ' ' => ' ', 'Ω' => 'Ω', 'K' => 'K', 'Å' => 'Å', '↚' => '↚', '↛' => '↛', '↮' => '↮', '⇍' => '⇍', '⇎' => '⇎', '⇏' => '⇏', '∄' => '∄', '∉' => '∉', '∌' => '∌', '∤' => '∤', '∦' => '∦', '≁' => '≁', '≄' => '≄', '≇' => '≇', '≉' => '≉', '≠' => '≠', '≢' => '≢', '≭' => '≭', '≮' => '≮', '≯' => '≯', '≰' => '≰', '≱' => '≱', '≴' => '≴', '≵' => '≵', '≸' => '≸', '≹' => '≹', '⊀' => '⊀', '⊁' => '⊁', '⊄' => '⊄', '⊅' => '⊅', '⊈' => '⊈', '⊉' => '⊉', '⊬' => '⊬', '⊭' => '⊭', '⊮' => '⊮', '⊯' => '⊯', '⋠' => '⋠', '⋡' => '⋡', '⋢' => '⋢', '⋣' => '⋣', '⋪' => '⋪', '⋫' => '⋫', '⋬' => '⋬', '⋭' => '⋭', '〈' => '〈', '〉' => '〉', '⫝̸' => '⫝̸', 'が' => 'が', 'ぎ' => 'ぎ', 'ぐ' => 'ぐ', 'げ' => 'げ', 'ご' => 'ご', 'ざ' => 'ざ', 'じ' => 'じ', 'ず' => 'ず', 'ぜ' => 'ぜ', 'ぞ' => 'ぞ', 'だ' => 'だ', 'ぢ' => 'ぢ', 'づ' => 'づ', 'で' => 'で', 'ど' => 'ど', 'ば' => 'ば', 'ぱ' => 'ぱ', 'び' => 'び', 'ぴ' => 'ぴ', 'ぶ' => 'ぶ', 'ぷ' => 'ぷ', 'べ' => 'べ', 'ぺ' => 'ぺ', 'ぼ' => 'ぼ', 'ぽ' => 'ぽ', 'ゔ' => 'ゔ', 'ゞ' => 'ゞ', 'ガ' => 'ガ', 'ギ' => 'ギ', 'グ' => 'グ', 'ゲ' => 'ゲ', 'ゴ' => 'ゴ', 'ザ' => 'ザ', 'ジ' => 'ジ', 'ズ' => 'ズ', 'ゼ' => 'ゼ', 'ゾ' => 'ゾ', 'ダ' => 'ダ', 'ヂ' => 'ヂ', 'ヅ' => 'ヅ', 'デ' => 'デ', 'ド' => 'ド', 'バ' => 'バ', 'パ' => 'パ', 'ビ' => 'ビ', 'ピ' => 'ピ', 'ブ' => 'ブ', 'プ' => 'プ', 'ベ' => 'ベ', 'ペ' => 'ペ', 'ボ' => 'ボ', 'ポ' => 'ポ', 'ヴ' => 'ヴ', 'ヷ' => 'ヷ', 'ヸ' => 'ヸ', 'ヹ' => 'ヹ', 'ヺ' => 'ヺ', 'ヾ' => 'ヾ', '豈' => '豈', '更' => '更', '車' => '車', '賈' => '賈', '滑' => '滑', '串' => '串', '句' => '句', '龜' => '龜', '龜' => '龜', '契' => '契', '金' => '金', '喇' => '喇', '奈' => '奈', '懶' => '懶', '癩' => '癩', '羅' => '羅', '蘿' => '蘿', '螺' => '螺', '裸' => '裸', '邏' => '邏', '樂' => '樂', '洛' => '洛', '烙' => '烙', '珞' => '珞', '落' => '落', '酪' => '酪', '駱' => '駱', '亂' => '亂', '卵' => '卵', '欄' => '欄', '爛' => '爛', '蘭' => '蘭', '鸞' => '鸞', '嵐' => '嵐', '濫' => '濫', '藍' => '藍', '襤' => '襤', '拉' => '拉', '臘' => '臘', '蠟' => '蠟', '廊' => '廊', '朗' => '朗', '浪' => '浪', '狼' => '狼', '郎' => '郎', '來' => '來', '冷' => '冷', '勞' => '勞', '擄' => '擄', '櫓' => '櫓', '爐' => '爐', '盧' => '盧', '老' => '老', '蘆' => '蘆', '虜' => '虜', '路' => '路', '露' => '露', '魯' => '魯', '鷺' => '鷺', '碌' => '碌', '祿' => '祿', '綠' => '綠', '菉' => '菉', '錄' => '錄', '鹿' => '鹿', '論' => '論', '壟' => '壟', '弄' => '弄', '籠' => '籠', '聾' => '聾', '牢' => '牢', '磊' => '磊', '賂' => '賂', '雷' => '雷', '壘' => '壘', '屢' => '屢', '樓' => '樓', '淚' => '淚', '漏' => '漏', '累' => '累', '縷' => '縷', '陋' => '陋', '勒' => '勒', '肋' => '肋', '凜' => '凜', '凌' => '凌', '稜' => '稜', '綾' => '綾', '菱' => '菱', '陵' => '陵', '讀' => '讀', '拏' => '拏', '樂' => '樂', '諾' => '諾', '丹' => '丹', '寧' => '寧', '怒' => '怒', '率' => '率', '異' => '異', '北' => '北', '磻' => '磻', '便' => '便', '復' => '復', '不' => '不', '泌' => '泌', '數' => '數', '索' => '索', '參' => '參', '塞' => '塞', '省' => '省', '葉' => '葉', '說' => '說', '殺' => '殺', '辰' => '辰', '沈' => '沈', '拾' => '拾', '若' => '若', '掠' => '掠', '略' => '略', '亮' => '亮', '兩' => '兩', '凉' => '凉', '梁' => '梁', '糧' => '糧', '良' => '良', '諒' => '諒', '量' => '量', '勵' => '勵', '呂' => '呂', '女' => '女', '廬' => '廬', '旅' => '旅', '濾' => '濾', '礪' => '礪', '閭' => '閭', '驪' => '驪', '麗' => '麗', '黎' => '黎', '力' => '力', '曆' => '曆', '歷' => '歷', '轢' => '轢', '年' => '年', '憐' => '憐', '戀' => '戀', '撚' => '撚', '漣' => '漣', '煉' => '煉', '璉' => '璉', '秊' => '秊', '練' => '練', '聯' => '聯', '輦' => '輦', '蓮' => '蓮', '連' => '連', '鍊' => '鍊', '列' => '列', '劣' => '劣', '咽' => '咽', '烈' => '烈', '裂' => '裂', '說' => '說', '廉' => '廉', '念' => '念', '捻' => '捻', '殮' => '殮', '簾' => '簾', '獵' => '獵', '令' => '令', '囹' => '囹', '寧' => '寧', '嶺' => '嶺', '怜' => '怜', '玲' => '玲', '瑩' => '瑩', '羚' => '羚', '聆' => '聆', '鈴' => '鈴', '零' => '零', '靈' => '靈', '領' => '領', '例' => '例', '禮' => '禮', '醴' => '醴', '隸' => '隸', '惡' => '惡', '了' => '了', '僚' => '僚', '寮' => '寮', '尿' => '尿', '料' => '料', '樂' => '樂', '燎' => '燎', '療' => '療', '蓼' => '蓼', '遼' => '遼', '龍' => '龍', '暈' => '暈', '阮' => '阮', '劉' => '劉', '杻' => '杻', '柳' => '柳', '流' => '流', '溜' => '溜', '琉' => '琉', '留' => '留', '硫' => '硫', '紐' => '紐', '類' => '類', '六' => '六', '戮' => '戮', '陸' => '陸', '倫' => '倫', '崙' => '崙', '淪' => '淪', '輪' => '輪', '律' => '律', '慄' => '慄', '栗' => '栗', '率' => '率', '隆' => '隆', '利' => '利', '吏' => '吏', '履' => '履', '易' => '易', '李' => '李', '梨' => '梨', '泥' => '泥', '理' => '理', '痢' => '痢', '罹' => '罹', '裏' => '裏', '裡' => '裡', '里' => '里', '離' => '離', '匿' => '匿', '溺' => '溺', '吝' => '吝', '燐' => '燐', '璘' => '璘', '藺' => '藺', '隣' => '隣', '鱗' => '鱗', '麟' => '麟', '林' => '林', '淋' => '淋', '臨' => '臨', '立' => '立', '笠' => '笠', '粒' => '粒', '狀' => '狀', '炙' => '炙', '識' => '識', '什' => '什', '茶' => '茶', '刺' => '刺', '切' => '切', '度' => '度', '拓' => '拓', '糖' => '糖', '宅' => '宅', '洞' => '洞', '暴' => '暴', '輻' => '輻', '行' => '行', '降' => '降', '見' => '見', '廓' => '廓', '兀' => '兀', '嗀' => '嗀', '塚' => '塚', '晴' => '晴', '凞' => '凞', '猪' => '猪', '益' => '益', '礼' => '礼', '神' => '神', '祥' => '祥', '福' => '福', '靖' => '靖', '精' => '精', '羽' => '羽', '蘒' => '蘒', '諸' => '諸', '逸' => '逸', '都' => '都', '飯' => '飯', '飼' => '飼', '館' => '館', '鶴' => '鶴', '郞' => '郞', '隷' => '隷', '侮' => '侮', '僧' => '僧', '免' => '免', '勉' => '勉', '勤' => '勤', '卑' => '卑', '喝' => '喝', '嘆' => '嘆', '器' => '器', '塀' => '塀', '墨' => '墨', '層' => '層', '屮' => '屮', '悔' => '悔', '慨' => '慨', '憎' => '憎', '懲' => '懲', '敏' => '敏', '既' => '既', '暑' => '暑', '梅' => '梅', '海' => '海', '渚' => '渚', '漢' => '漢', '煮' => '煮', '爫' => '爫', '琢' => '琢', '碑' => '碑', '社' => '社', '祉' => '祉', '祈' => '祈', '祐' => '祐', '祖' => '祖', '祝' => '祝', '禍' => '禍', '禎' => '禎', '穀' => '穀', '突' => '突', '節' => '節', '練' => '練', '縉' => '縉', '繁' => '繁', '署' => '署', '者' => '者', '臭' => '臭', '艹' => '艹', '艹' => '艹', '著' => '著', '褐' => '褐', '視' => '視', '謁' => '謁', '謹' => '謹', '賓' => '賓', '贈' => '贈', '辶' => '辶', '逸' => '逸', '難' => '難', '響' => '響', '頻' => '頻', '恵' => '恵', '𤋮' => '𤋮', '舘' => '舘', '並' => '並', '况' => '况', '全' => '全', '侀' => '侀', '充' => '充', '冀' => '冀', '勇' => '勇', '勺' => '勺', '喝' => '喝', '啕' => '啕', '喙' => '喙', '嗢' => '嗢', '塚' => '塚', '墳' => '墳', '奄' => '奄', '奔' => '奔', '婢' => '婢', '嬨' => '嬨', '廒' => '廒', '廙' => '廙', '彩' => '彩', '徭' => '徭', '惘' => '惘', '慎' => '慎', '愈' => '愈', '憎' => '憎', '慠' => '慠', '懲' => '懲', '戴' => '戴', '揄' => '揄', '搜' => '搜', '摒' => '摒', '敖' => '敖', '晴' => '晴', '朗' => '朗', '望' => '望', '杖' => '杖', '歹' => '歹', '殺' => '殺', '流' => '流', '滛' => '滛', '滋' => '滋', '漢' => '漢', '瀞' => '瀞', '煮' => '煮', '瞧' => '瞧', '爵' => '爵', '犯' => '犯', '猪' => '猪', '瑱' => '瑱', '甆' => '甆', '画' => '画', '瘝' => '瘝', '瘟' => '瘟', '益' => '益', '盛' => '盛', '直' => '直', '睊' => '睊', '着' => '着', '磌' => '磌', '窱' => '窱', '節' => '節', '类' => '类', '絛' => '絛', '練' => '練', '缾' => '缾', '者' => '者', '荒' => '荒', '華' => '華', '蝹' => '蝹', '襁' => '襁', '覆' => '覆', '視' => '視', '調' => '調', '諸' => '諸', '請' => '請', '謁' => '謁', '諾' => '諾', '諭' => '諭', '謹' => '謹', '變' => '變', '贈' => '贈', '輸' => '輸', '遲' => '遲', '醙' => '醙', '鉶' => '鉶', '陼' => '陼', '難' => '難', '靖' => '靖', '韛' => '韛', '響' => '響', '頋' => '頋', '頻' => '頻', '鬒' => '鬒', '龜' => '龜', '𢡊' => '𢡊', '𢡄' => '𢡄', '𣏕' => '𣏕', '㮝' => '㮝', '䀘' => '䀘', '䀹' => '䀹', '𥉉' => '𥉉', '𥳐' => '𥳐', '𧻓' => '𧻓', '齃' => '齃', '龎' => '龎', 'יִ' => 'יִ', 'ײַ' => 'ײַ', 'שׁ' => 'שׁ', 'שׂ' => 'שׂ', 'שּׁ' => 'שּׁ', 'שּׂ' => 'שּׂ', 'אַ' => 'אַ', 'אָ' => 'אָ', 'אּ' => 'אּ', 'בּ' => 'בּ', 'גּ' => 'גּ', 'דּ' => 'דּ', 'הּ' => 'הּ', 'וּ' => 'וּ', 'זּ' => 'זּ', 'טּ' => 'טּ', 'יּ' => 'יּ', 'ךּ' => 'ךּ', 'כּ' => 'כּ', 'לּ' => 'לּ', 'מּ' => 'מּ', 'נּ' => 'נּ', 'סּ' => 'סּ', 'ףּ' => 'ףּ', 'פּ' => 'פּ', 'צּ' => 'צּ', 'קּ' => 'קּ', 'רּ' => 'רּ', 'שּ' => 'שּ', 'תּ' => 'תּ', 'וֹ' => 'וֹ', 'בֿ' => 'בֿ', 'כֿ' => 'כֿ', 'פֿ' => 'פֿ', '𑂚' => '𑂚', '𑂜' => '𑂜', '𑂫' => '𑂫', '𑄮' => '𑄮', '𑄯' => '𑄯', '𑍋' => '𑍋', '𑍌' => '𑍌', '𑒻' => '𑒻', '𑒼' => '𑒼', '𑒾' => '𑒾', '𑖺' => '𑖺', '𑖻' => '𑖻', '𑤸' => '𑤸', '𝅗𝅥' => '𝅗𝅥', '𝅘𝅥' => '𝅘𝅥', '𝅘𝅥𝅮' => '𝅘𝅥𝅮', '𝅘𝅥𝅯' => '𝅘𝅥𝅯', '𝅘𝅥𝅰' => '𝅘𝅥𝅰', '𝅘𝅥𝅱' => '𝅘𝅥𝅱', '𝅘𝅥𝅲' => '𝅘𝅥𝅲', '𝆹𝅥' => '𝆹𝅥', '𝆺𝅥' => '𝆺𝅥', '𝆹𝅥𝅮' => '𝆹𝅥𝅮', '𝆺𝅥𝅮' => '𝆺𝅥𝅮', '𝆹𝅥𝅯' => '𝆹𝅥𝅯', '𝆺𝅥𝅯' => '𝆺𝅥𝅯', '丽' => '丽', '丸' => '丸', '乁' => '乁', '𠄢' => '𠄢', '你' => '你', '侮' => '侮', '侻' => '侻', '倂' => '倂', '偺' => '偺', '備' => '備', '僧' => '僧', '像' => '像', '㒞' => '㒞', '𠘺' => '𠘺', '免' => '免', '兔' => '兔', '兤' => '兤', '具' => '具', '𠔜' => '𠔜', '㒹' => '㒹', '內' => '內', '再' => '再', '𠕋' => '𠕋', '冗' => '冗', '冤' => '冤', '仌' => '仌', '冬' => '冬', '况' => '况', '𩇟' => '𩇟', '凵' => '凵', '刃' => '刃', '㓟' => '㓟', '刻' => '刻', '剆' => '剆', '割' => '割', '剷' => '剷', '㔕' => '㔕', '勇' => '勇', '勉' => '勉', '勤' => '勤', '勺' => '勺', '包' => '包', '匆' => '匆', '北' => '北', '卉' => '卉', '卑' => '卑', '博' => '博', '即' => '即', '卽' => '卽', '卿' => '卿', '卿' => '卿', '卿' => '卿', '𠨬' => '𠨬', '灰' => '灰', '及' => '及', '叟' => '叟', '𠭣' => '𠭣', '叫' => '叫', '叱' => '叱', '吆' => '吆', '咞' => '咞', '吸' => '吸', '呈' => '呈', '周' => '周', '咢' => '咢', '哶' => '哶', '唐' => '唐', '啓' => '啓', '啣' => '啣', '善' => '善', '善' => '善', '喙' => '喙', '喫' => '喫', '喳' => '喳', '嗂' => '嗂', '圖' => '圖', '嘆' => '嘆', '圗' => '圗', '噑' => '噑', '噴' => '噴', '切' => '切', '壮' => '壮', '城' => '城', '埴' => '埴', '堍' => '堍', '型' => '型', '堲' => '堲', '報' => '報', '墬' => '墬', '𡓤' => '𡓤', '売' => '売', '壷' => '壷', '夆' => '夆', '多' => '多', '夢' => '夢', '奢' => '奢', '𡚨' => '𡚨', '𡛪' => '𡛪', '姬' => '姬', '娛' => '娛', '娧' => '娧', '姘' => '姘', '婦' => '婦', '㛮' => '㛮', '㛼' => '㛼', '嬈' => '嬈', '嬾' => '嬾', '嬾' => '嬾', '𡧈' => '𡧈', '寃' => '寃', '寘' => '寘', '寧' => '寧', '寳' => '寳', '𡬘' => '𡬘', '寿' => '寿', '将' => '将', '当' => '当', '尢' => '尢', '㞁' => '㞁', '屠' => '屠', '屮' => '屮', '峀' => '峀', '岍' => '岍', '𡷤' => '𡷤', '嵃' => '嵃', '𡷦' => '𡷦', '嵮' => '嵮', '嵫' => '嵫', '嵼' => '嵼', '巡' => '巡', '巢' => '巢', '㠯' => '㠯', '巽' => '巽', '帨' => '帨', '帽' => '帽', '幩' => '幩', '㡢' => '㡢', '𢆃' => '𢆃', '㡼' => '㡼', '庰' => '庰', '庳' => '庳', '庶' => '庶', '廊' => '廊', '𪎒' => '𪎒', '廾' => '廾', '𢌱' => '𢌱', '𢌱' => '𢌱', '舁' => '舁', '弢' => '弢', '弢' => '弢', '㣇' => '㣇', '𣊸' => '𣊸', '𦇚' => '𦇚', '形' => '形', '彫' => '彫', '㣣' => '㣣', '徚' => '徚', '忍' => '忍', '志' => '志', '忹' => '忹', '悁' => '悁', '㤺' => '㤺', '㤜' => '㤜', '悔' => '悔', '𢛔' => '𢛔', '惇' => '惇', '慈' => '慈', '慌' => '慌', '慎' => '慎', '慌' => '慌', '慺' => '慺', '憎' => '憎', '憲' => '憲', '憤' => '憤', '憯' => '憯', '懞' => '懞', '懲' => '懲', '懶' => '懶', '成' => '成', '戛' => '戛', '扝' => '扝', '抱' => '抱', '拔' => '拔', '捐' => '捐', '𢬌' => '𢬌', '挽' => '挽', '拼' => '拼', '捨' => '捨', '掃' => '掃', '揤' => '揤', '𢯱' => '𢯱', '搢' => '搢', '揅' => '揅', '掩' => '掩', '㨮' => '㨮', '摩' => '摩', '摾' => '摾', '撝' => '撝', '摷' => '摷', '㩬' => '㩬', '敏' => '敏', '敬' => '敬', '𣀊' => '𣀊', '旣' => '旣', '書' => '書', '晉' => '晉', '㬙' => '㬙', '暑' => '暑', '㬈' => '㬈', '㫤' => '㫤', '冒' => '冒', '冕' => '冕', '最' => '最', '暜' => '暜', '肭' => '肭', '䏙' => '䏙', '朗' => '朗', '望' => '望', '朡' => '朡', '杞' => '杞', '杓' => '杓', '𣏃' => '𣏃', '㭉' => '㭉', '柺' => '柺', '枅' => '枅', '桒' => '桒', '梅' => '梅', '𣑭' => '𣑭', '梎' => '梎', '栟' => '栟', '椔' => '椔', '㮝' => '㮝', '楂' => '楂', '榣' => '榣', '槪' => '槪', '檨' => '檨', '𣚣' => '𣚣', '櫛' => '櫛', '㰘' => '㰘', '次' => '次', '𣢧' => '𣢧', '歔' => '歔', '㱎' => '㱎', '歲' => '歲', '殟' => '殟', '殺' => '殺', '殻' => '殻', '𣪍' => '𣪍', '𡴋' => '𡴋', '𣫺' => '𣫺', '汎' => '汎', '𣲼' => '𣲼', '沿' => '沿', '泍' => '泍', '汧' => '汧', '洖' => '洖', '派' => '派', '海' => '海', '流' => '流', '浩' => '浩', '浸' => '浸', '涅' => '涅', '𣴞' => '𣴞', '洴' => '洴', '港' => '港', '湮' => '湮', '㴳' => '㴳', '滋' => '滋', '滇' => '滇', '𣻑' => '𣻑', '淹' => '淹', '潮' => '潮', '𣽞' => '𣽞', '𣾎' => '𣾎', '濆' => '濆', '瀹' => '瀹', '瀞' => '瀞', '瀛' => '瀛', '㶖' => '㶖', '灊' => '灊', '災' => '災', '灷' => '灷', '炭' => '炭', '𠔥' => '𠔥', '煅' => '煅', '𤉣' => '𤉣', '熜' => '熜', '𤎫' => '𤎫', '爨' => '爨', '爵' => '爵', '牐' => '牐', '𤘈' => '𤘈', '犀' => '犀', '犕' => '犕', '𤜵' => '𤜵', '𤠔' => '𤠔', '獺' => '獺', '王' => '王', '㺬' => '㺬', '玥' => '玥', '㺸' => '㺸', '㺸' => '㺸', '瑇' => '瑇', '瑜' => '瑜', '瑱' => '瑱', '璅' => '璅', '瓊' => '瓊', '㼛' => '㼛', '甤' => '甤', '𤰶' => '𤰶', '甾' => '甾', '𤲒' => '𤲒', '異' => '異', '𢆟' => '𢆟', '瘐' => '瘐', '𤾡' => '𤾡', '𤾸' => '𤾸', '𥁄' => '𥁄', '㿼' => '㿼', '䀈' => '䀈', '直' => '直', '𥃳' => '𥃳', '𥃲' => '𥃲', '𥄙' => '𥄙', '𥄳' => '𥄳', '眞' => '眞', '真' => '真', '真' => '真', '睊' => '睊', '䀹' => '䀹', '瞋' => '瞋', '䁆' => '䁆', '䂖' => '䂖', '𥐝' => '𥐝', '硎' => '硎', '碌' => '碌', '磌' => '磌', '䃣' => '䃣', '𥘦' => '𥘦', '祖' => '祖', '𥚚' => '𥚚', '𥛅' => '𥛅', '福' => '福', '秫' => '秫', '䄯' => '䄯', '穀' => '穀', '穊' => '穊', '穏' => '穏', '𥥼' => '𥥼', '𥪧' => '𥪧', '𥪧' => '𥪧', '竮' => '竮', '䈂' => '䈂', '𥮫' => '𥮫', '篆' => '篆', '築' => '築', '䈧' => '䈧', '𥲀' => '𥲀', '糒' => '糒', '䊠' => '䊠', '糨' => '糨', '糣' => '糣', '紀' => '紀', '𥾆' => '𥾆', '絣' => '絣', '䌁' => '䌁', '緇' => '緇', '縂' => '縂', '繅' => '繅', '䌴' => '䌴', '𦈨' => '𦈨', '𦉇' => '𦉇', '䍙' => '䍙', '𦋙' => '𦋙', '罺' => '罺', '𦌾' => '𦌾', '羕' => '羕', '翺' => '翺', '者' => '者', '𦓚' => '𦓚', '𦔣' => '𦔣', '聠' => '聠', '𦖨' => '𦖨', '聰' => '聰', '𣍟' => '𣍟', '䏕' => '䏕', '育' => '育', '脃' => '脃', '䐋' => '䐋', '脾' => '脾', '媵' => '媵', '𦞧' => '𦞧', '𦞵' => '𦞵', '𣎓' => '𣎓', '𣎜' => '𣎜', '舁' => '舁', '舄' => '舄', '辞' => '辞', '䑫' => '䑫', '芑' => '芑', '芋' => '芋', '芝' => '芝', '劳' => '劳', '花' => '花', '芳' => '芳', '芽' => '芽', '苦' => '苦', '𦬼' => '𦬼', '若' => '若', '茝' => '茝', '荣' => '荣', '莭' => '莭', '茣' => '茣', '莽' => '莽', '菧' => '菧', '著' => '著', '荓' => '荓', '菊' => '菊', '菌' => '菌', '菜' => '菜', '𦰶' => '𦰶', '𦵫' => '𦵫', '𦳕' => '𦳕', '䔫' => '䔫', '蓱' => '蓱', '蓳' => '蓳', '蔖' => '蔖', '𧏊' => '𧏊', '蕤' => '蕤', '𦼬' => '𦼬', '䕝' => '䕝', '䕡' => '䕡', '𦾱' => '𦾱', '𧃒' => '𧃒', '䕫' => '䕫', '虐' => '虐', '虜' => '虜', '虧' => '虧', '虩' => '虩', '蚩' => '蚩', '蚈' => '蚈', '蜎' => '蜎', '蛢' => '蛢', '蝹' => '蝹', '蜨' => '蜨', '蝫' => '蝫', '螆' => '螆', '䗗' => '䗗', '蟡' => '蟡', '蠁' => '蠁', '䗹' => '䗹', '衠' => '衠', '衣' => '衣', '𧙧' => '𧙧', '裗' => '裗', '裞' => '裞', '䘵' => '䘵', '裺' => '裺', '㒻' => '㒻', '𧢮' => '𧢮', '𧥦' => '𧥦', '䚾' => '䚾', '䛇' => '䛇', '誠' => '誠', '諭' => '諭', '變' => '變', '豕' => '豕', '𧲨' => '𧲨', '貫' => '貫', '賁' => '賁', '贛' => '贛', '起' => '起', '𧼯' => '𧼯', '𠠄' => '𠠄', '跋' => '跋', '趼' => '趼', '跰' => '跰', '𠣞' => '𠣞', '軔' => '軔', '輸' => '輸', '𨗒' => '𨗒', '𨗭' => '𨗭', '邔' => '邔', '郱' => '郱', '鄑' => '鄑', '𨜮' => '𨜮', '鄛' => '鄛', '鈸' => '鈸', '鋗' => '鋗', '鋘' => '鋘', '鉼' => '鉼', '鏹' => '鏹', '鐕' => '鐕', '𨯺' => '𨯺', '開' => '開', '䦕' => '䦕', '閷' => '閷', '𨵷' => '𨵷', '䧦' => '䧦', '雃' => '雃', '嶲' => '嶲', '霣' => '霣', '𩅅' => '𩅅', '𩈚' => '𩈚', '䩮' => '䩮', '䩶' => '䩶', '韠' => '韠', '𩐊' => '𩐊', '䪲' => '䪲', '𩒖' => '𩒖', '頋' => '頋', '頋' => '頋', '頩' => '頩', '𩖶' => '𩖶', '飢' => '飢', '䬳' => '䬳', '餩' => '餩', '馧' => '馧', '駂' => '駂', '駾' => '駾', '䯎' => '䯎', '𩬰' => '𩬰', '鬒' => '鬒', '鱀' => '鱀', '鳽' => '鳽', '䳎' => '䳎', '䳭' => '䳭', '鵧' => '鵧', '𪃎' => '𪃎', '䳸' => '䳸', '𪄅' => '𪄅', '𪈎' => '𪈎', '𪊑' => '𪊑', '麻' => '麻', '䵖' => '䵖', '黹' => '黹', '黾' => '黾', '鼅' => '鼅', '鼏' => '鼏', '鼖' => '鼖', '鼻' => '鼻', '𪘀' => '𪘀', ); PK! D5D5Lvendor/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.phpnu[ 230, '́' => 230, '̂' => 230, '̃' => 230, '̄' => 230, '̅' => 230, '̆' => 230, '̇' => 230, '̈' => 230, '̉' => 230, '̊' => 230, '̋' => 230, '̌' => 230, '̍' => 230, '̎' => 230, '̏' => 230, '̐' => 230, '̑' => 230, '̒' => 230, '̓' => 230, '̔' => 230, '̕' => 232, '̖' => 220, '̗' => 220, '̘' => 220, '̙' => 220, '̚' => 232, '̛' => 216, '̜' => 220, '̝' => 220, '̞' => 220, '̟' => 220, '̠' => 220, '̡' => 202, '̢' => 202, '̣' => 220, '̤' => 220, '̥' => 220, '̦' => 220, '̧' => 202, '̨' => 202, '̩' => 220, '̪' => 220, '̫' => 220, '̬' => 220, '̭' => 220, '̮' => 220, '̯' => 220, '̰' => 220, '̱' => 220, '̲' => 220, '̳' => 220, '̴' => 1, '̵' => 1, '̶' => 1, '̷' => 1, '̸' => 1, '̹' => 220, '̺' => 220, '̻' => 220, '̼' => 220, '̽' => 230, '̾' => 230, '̿' => 230, '̀' => 230, '́' => 230, '͂' => 230, '̓' => 230, '̈́' => 230, 'ͅ' => 240, '͆' => 230, '͇' => 220, '͈' => 220, '͉' => 220, '͊' => 230, '͋' => 230, '͌' => 230, '͍' => 220, '͎' => 220, '͐' => 230, '͑' => 230, '͒' => 230, '͓' => 220, '͔' => 220, '͕' => 220, '͖' => 220, '͗' => 230, '͘' => 232, '͙' => 220, '͚' => 220, '͛' => 230, '͜' => 233, '͝' => 234, '͞' => 234, '͟' => 233, '͠' => 234, '͡' => 234, '͢' => 233, 'ͣ' => 230, 'ͤ' => 230, 'ͥ' => 230, 'ͦ' => 230, 'ͧ' => 230, 'ͨ' => 230, 'ͩ' => 230, 'ͪ' => 230, 'ͫ' => 230, 'ͬ' => 230, 'ͭ' => 230, 'ͮ' => 230, 'ͯ' => 230, '҃' => 230, '҄' => 230, '҅' => 230, '҆' => 230, '҇' => 230, '֑' => 220, '֒' => 230, '֓' => 230, '֔' => 230, '֕' => 230, '֖' => 220, '֗' => 230, '֘' => 230, '֙' => 230, '֚' => 222, '֛' => 220, '֜' => 230, '֝' => 230, '֞' => 230, '֟' => 230, '֠' => 230, '֡' => 230, '֢' => 220, '֣' => 220, '֤' => 220, '֥' => 220, '֦' => 220, '֧' => 220, '֨' => 230, '֩' => 230, '֪' => 220, '֫' => 230, '֬' => 230, '֭' => 222, '֮' => 228, '֯' => 230, 'ְ' => 10, 'ֱ' => 11, 'ֲ' => 12, 'ֳ' => 13, 'ִ' => 14, 'ֵ' => 15, 'ֶ' => 16, 'ַ' => 17, 'ָ' => 18, 'ֹ' => 19, 'ֺ' => 19, 'ֻ' => 20, 'ּ' => 21, 'ֽ' => 22, 'ֿ' => 23, 'ׁ' => 24, 'ׂ' => 25, 'ׄ' => 230, 'ׅ' => 220, 'ׇ' => 18, 'ؐ' => 230, 'ؑ' => 230, 'ؒ' => 230, 'ؓ' => 230, 'ؔ' => 230, 'ؕ' => 230, 'ؖ' => 230, 'ؗ' => 230, 'ؘ' => 30, 'ؙ' => 31, 'ؚ' => 32, 'ً' => 27, 'ٌ' => 28, 'ٍ' => 29, 'َ' => 30, 'ُ' => 31, 'ِ' => 32, 'ّ' => 33, 'ْ' => 34, 'ٓ' => 230, 'ٔ' => 230, 'ٕ' => 220, 'ٖ' => 220, 'ٗ' => 230, '٘' => 230, 'ٙ' => 230, 'ٚ' => 230, 'ٛ' => 230, 'ٜ' => 220, 'ٝ' => 230, 'ٞ' => 230, 'ٟ' => 220, 'ٰ' => 35, 'ۖ' => 230, 'ۗ' => 230, 'ۘ' => 230, 'ۙ' => 230, 'ۚ' => 230, 'ۛ' => 230, 'ۜ' => 230, '۟' => 230, '۠' => 230, 'ۡ' => 230, 'ۢ' => 230, 'ۣ' => 220, 'ۤ' => 230, 'ۧ' => 230, 'ۨ' => 230, '۪' => 220, '۫' => 230, '۬' => 230, 'ۭ' => 220, 'ܑ' => 36, 'ܰ' => 230, 'ܱ' => 220, 'ܲ' => 230, 'ܳ' => 230, 'ܴ' => 220, 'ܵ' => 230, 'ܶ' => 230, 'ܷ' => 220, 'ܸ' => 220, 'ܹ' => 220, 'ܺ' => 230, 'ܻ' => 220, 'ܼ' => 220, 'ܽ' => 230, 'ܾ' => 220, 'ܿ' => 230, '݀' => 230, '݁' => 230, '݂' => 220, '݃' => 230, '݄' => 220, '݅' => 230, '݆' => 220, '݇' => 230, '݈' => 220, '݉' => 230, '݊' => 230, '߫' => 230, '߬' => 230, '߭' => 230, '߮' => 230, '߯' => 230, '߰' => 230, '߱' => 230, '߲' => 220, '߳' => 230, '߽' => 220, 'ࠖ' => 230, 'ࠗ' => 230, '࠘' => 230, '࠙' => 230, 'ࠛ' => 230, 'ࠜ' => 230, 'ࠝ' => 230, 'ࠞ' => 230, 'ࠟ' => 230, 'ࠠ' => 230, 'ࠡ' => 230, 'ࠢ' => 230, 'ࠣ' => 230, 'ࠥ' => 230, 'ࠦ' => 230, 'ࠧ' => 230, 'ࠩ' => 230, 'ࠪ' => 230, 'ࠫ' => 230, 'ࠬ' => 230, '࠭' => 230, '࡙' => 220, '࡚' => 220, '࡛' => 220, '࣓' => 220, 'ࣔ' => 230, 'ࣕ' => 230, 'ࣖ' => 230, 'ࣗ' => 230, 'ࣘ' => 230, 'ࣙ' => 230, 'ࣚ' => 230, 'ࣛ' => 230, 'ࣜ' => 230, 'ࣝ' => 230, 'ࣞ' => 230, 'ࣟ' => 230, '࣠' => 230, '࣡' => 230, 'ࣣ' => 220, 'ࣤ' => 230, 'ࣥ' => 230, 'ࣦ' => 220, 'ࣧ' => 230, 'ࣨ' => 230, 'ࣩ' => 220, '࣪' => 230, '࣫' => 230, '࣬' => 230, '࣭' => 220, '࣮' => 220, '࣯' => 220, 'ࣰ' => 27, 'ࣱ' => 28, 'ࣲ' => 29, 'ࣳ' => 230, 'ࣴ' => 230, 'ࣵ' => 230, 'ࣶ' => 220, 'ࣷ' => 230, 'ࣸ' => 230, 'ࣹ' => 220, 'ࣺ' => 220, 'ࣻ' => 230, 'ࣼ' => 230, 'ࣽ' => 230, 'ࣾ' => 230, 'ࣿ' => 230, '़' => 7, '्' => 9, '॑' => 230, '॒' => 220, '॓' => 230, '॔' => 230, '়' => 7, '্' => 9, '৾' => 230, '਼' => 7, '੍' => 9, '઼' => 7, '્' => 9, '଼' => 7, '୍' => 9, '்' => 9, '్' => 9, 'ౕ' => 84, 'ౖ' => 91, '಼' => 7, '್' => 9, '഻' => 9, '഼' => 9, '്' => 9, '්' => 9, 'ุ' => 103, 'ู' => 103, 'ฺ' => 9, '่' => 107, '้' => 107, '๊' => 107, '๋' => 107, 'ຸ' => 118, 'ູ' => 118, '຺' => 9, '່' => 122, '້' => 122, '໊' => 122, '໋' => 122, '༘' => 220, '༙' => 220, '༵' => 220, '༷' => 220, '༹' => 216, 'ཱ' => 129, 'ི' => 130, 'ུ' => 132, 'ེ' => 130, 'ཻ' => 130, 'ོ' => 130, 'ཽ' => 130, 'ྀ' => 130, 'ྂ' => 230, 'ྃ' => 230, '྄' => 9, '྆' => 230, '྇' => 230, '࿆' => 220, '့' => 7, '္' => 9, '်' => 9, 'ႍ' => 220, '፝' => 230, '፞' => 230, '፟' => 230, '᜔' => 9, '᜴' => 9, '្' => 9, '៝' => 230, 'ᢩ' => 228, '᤹' => 222, '᤺' => 230, '᤻' => 220, 'ᨗ' => 230, 'ᨘ' => 220, '᩠' => 9, '᩵' => 230, '᩶' => 230, '᩷' => 230, '᩸' => 230, '᩹' => 230, '᩺' => 230, '᩻' => 230, '᩼' => 230, '᩿' => 220, '᪰' => 230, '᪱' => 230, '᪲' => 230, '᪳' => 230, '᪴' => 230, '᪵' => 220, '᪶' => 220, '᪷' => 220, '᪸' => 220, '᪹' => 220, '᪺' => 220, '᪻' => 230, '᪼' => 230, '᪽' => 220, 'ᪿ' => 220, 'ᫀ' => 220, '᬴' => 7, '᭄' => 9, '᭫' => 230, '᭬' => 220, '᭭' => 230, '᭮' => 230, '᭯' => 230, '᭰' => 230, '᭱' => 230, '᭲' => 230, '᭳' => 230, '᮪' => 9, '᮫' => 9, '᯦' => 7, '᯲' => 9, '᯳' => 9, '᰷' => 7, '᳐' => 230, '᳑' => 230, '᳒' => 230, '᳔' => 1, '᳕' => 220, '᳖' => 220, '᳗' => 220, '᳘' => 220, '᳙' => 220, '᳚' => 230, '᳛' => 230, '᳜' => 220, '᳝' => 220, '᳞' => 220, '᳟' => 220, '᳠' => 230, '᳢' => 1, '᳣' => 1, '᳤' => 1, '᳥' => 1, '᳦' => 1, '᳧' => 1, '᳨' => 1, '᳭' => 220, '᳴' => 230, '᳸' => 230, '᳹' => 230, '᷀' => 230, '᷁' => 230, '᷂' => 220, '᷃' => 230, '᷄' => 230, '᷅' => 230, '᷆' => 230, '᷇' => 230, '᷈' => 230, '᷉' => 230, '᷊' => 220, '᷋' => 230, '᷌' => 230, '᷍' => 234, '᷎' => 214, '᷏' => 220, '᷐' => 202, '᷑' => 230, '᷒' => 230, 'ᷓ' => 230, 'ᷔ' => 230, 'ᷕ' => 230, 'ᷖ' => 230, 'ᷗ' => 230, 'ᷘ' => 230, 'ᷙ' => 230, 'ᷚ' => 230, 'ᷛ' => 230, 'ᷜ' => 230, 'ᷝ' => 230, 'ᷞ' => 230, 'ᷟ' => 230, 'ᷠ' => 230, 'ᷡ' => 230, 'ᷢ' => 230, 'ᷣ' => 230, 'ᷤ' => 230, 'ᷥ' => 230, 'ᷦ' => 230, 'ᷧ' => 230, 'ᷨ' => 230, 'ᷩ' => 230, 'ᷪ' => 230, 'ᷫ' => 230, 'ᷬ' => 230, 'ᷭ' => 230, 'ᷮ' => 230, 'ᷯ' => 230, 'ᷰ' => 230, 'ᷱ' => 230, 'ᷲ' => 230, 'ᷳ' => 230, 'ᷴ' => 230, '᷵' => 230, '᷶' => 232, '᷷' => 228, '᷸' => 228, '᷹' => 220, '᷻' => 230, '᷼' => 233, '᷽' => 220, '᷾' => 230, '᷿' => 220, '⃐' => 230, '⃑' => 230, '⃒' => 1, '⃓' => 1, '⃔' => 230, '⃕' => 230, '⃖' => 230, '⃗' => 230, '⃘' => 1, '⃙' => 1, '⃚' => 1, '⃛' => 230, '⃜' => 230, '⃡' => 230, '⃥' => 1, '⃦' => 1, '⃧' => 230, '⃨' => 220, '⃩' => 230, '⃪' => 1, '⃫' => 1, '⃬' => 220, '⃭' => 220, '⃮' => 220, '⃯' => 220, '⃰' => 230, '⳯' => 230, '⳰' => 230, '⳱' => 230, '⵿' => 9, 'ⷠ' => 230, 'ⷡ' => 230, 'ⷢ' => 230, 'ⷣ' => 230, 'ⷤ' => 230, 'ⷥ' => 230, 'ⷦ' => 230, 'ⷧ' => 230, 'ⷨ' => 230, 'ⷩ' => 230, 'ⷪ' => 230, 'ⷫ' => 230, 'ⷬ' => 230, 'ⷭ' => 230, 'ⷮ' => 230, 'ⷯ' => 230, 'ⷰ' => 230, 'ⷱ' => 230, 'ⷲ' => 230, 'ⷳ' => 230, 'ⷴ' => 230, 'ⷵ' => 230, 'ⷶ' => 230, 'ⷷ' => 230, 'ⷸ' => 230, 'ⷹ' => 230, 'ⷺ' => 230, 'ⷻ' => 230, 'ⷼ' => 230, 'ⷽ' => 230, 'ⷾ' => 230, 'ⷿ' => 230, '〪' => 218, '〫' => 228, '〬' => 232, '〭' => 222, '〮' => 224, '〯' => 224, '゙' => 8, '゚' => 8, '꙯' => 230, 'ꙴ' => 230, 'ꙵ' => 230, 'ꙶ' => 230, 'ꙷ' => 230, 'ꙸ' => 230, 'ꙹ' => 230, 'ꙺ' => 230, 'ꙻ' => 230, '꙼' => 230, '꙽' => 230, 'ꚞ' => 230, 'ꚟ' => 230, '꛰' => 230, '꛱' => 230, '꠆' => 9, '꠬' => 9, '꣄' => 9, '꣠' => 230, '꣡' => 230, '꣢' => 230, '꣣' => 230, '꣤' => 230, '꣥' => 230, '꣦' => 230, '꣧' => 230, '꣨' => 230, '꣩' => 230, '꣪' => 230, '꣫' => 230, '꣬' => 230, '꣭' => 230, '꣮' => 230, '꣯' => 230, '꣰' => 230, '꣱' => 230, '꤫' => 220, '꤬' => 220, '꤭' => 220, '꥓' => 9, '꦳' => 7, '꧀' => 9, 'ꪰ' => 230, 'ꪲ' => 230, 'ꪳ' => 230, 'ꪴ' => 220, 'ꪷ' => 230, 'ꪸ' => 230, 'ꪾ' => 230, '꪿' => 230, '꫁' => 230, '꫶' => 9, '꯭' => 9, 'ﬞ' => 26, '︠' => 230, '︡' => 230, '︢' => 230, '︣' => 230, '︤' => 230, '︥' => 230, '︦' => 230, '︧' => 220, '︨' => 220, '︩' => 220, '︪' => 220, '︫' => 220, '︬' => 220, '︭' => 220, '︮' => 230, '︯' => 230, '𐇽' => 220, '𐋠' => 220, '𐍶' => 230, '𐍷' => 230, '𐍸' => 230, '𐍹' => 230, '𐍺' => 230, '𐨍' => 220, '𐨏' => 230, '𐨸' => 230, '𐨹' => 1, '𐨺' => 220, '𐨿' => 9, '𐫥' => 230, '𐫦' => 220, '𐴤' => 230, '𐴥' => 230, '𐴦' => 230, '𐴧' => 230, '𐺫' => 230, '𐺬' => 230, '𐽆' => 220, '𐽇' => 220, '𐽈' => 230, '𐽉' => 230, '𐽊' => 230, '𐽋' => 220, '𐽌' => 230, '𐽍' => 220, '𐽎' => 220, '𐽏' => 220, '𐽐' => 220, '𑁆' => 9, '𑁿' => 9, '𑂹' => 9, '𑂺' => 7, '𑄀' => 230, '𑄁' => 230, '𑄂' => 230, '𑄳' => 9, '𑄴' => 9, '𑅳' => 7, '𑇀' => 9, '𑇊' => 7, '𑈵' => 9, '𑈶' => 7, '𑋩' => 7, '𑋪' => 9, '𑌻' => 7, '𑌼' => 7, '𑍍' => 9, '𑍦' => 230, '𑍧' => 230, '𑍨' => 230, '𑍩' => 230, '𑍪' => 230, '𑍫' => 230, '𑍬' => 230, '𑍰' => 230, '𑍱' => 230, '𑍲' => 230, '𑍳' => 230, '𑍴' => 230, '𑑂' => 9, '𑑆' => 7, '𑑞' => 230, '𑓂' => 9, '𑓃' => 7, '𑖿' => 9, '𑗀' => 7, '𑘿' => 9, '𑚶' => 9, '𑚷' => 7, '𑜫' => 9, '𑠹' => 9, '𑠺' => 7, '𑤽' => 9, '𑤾' => 9, '𑥃' => 7, '𑧠' => 9, '𑨴' => 9, '𑩇' => 9, '𑪙' => 9, '𑰿' => 9, '𑵂' => 7, '𑵄' => 9, '𑵅' => 9, '𑶗' => 9, '𖫰' => 1, '𖫱' => 1, '𖫲' => 1, '𖫳' => 1, '𖫴' => 1, '𖬰' => 230, '𖬱' => 230, '𖬲' => 230, '𖬳' => 230, '𖬴' => 230, '𖬵' => 230, '𖬶' => 230, '𖿰' => 6, '𖿱' => 6, '𛲞' => 1, '𝅥' => 216, '𝅦' => 216, '𝅧' => 1, '𝅨' => 1, '𝅩' => 1, '𝅭' => 226, '𝅮' => 216, '𝅯' => 216, '𝅰' => 216, '𝅱' => 216, '𝅲' => 216, '𝅻' => 220, '𝅼' => 220, '𝅽' => 220, '𝅾' => 220, '𝅿' => 220, '𝆀' => 220, '𝆁' => 220, '𝆂' => 220, '𝆅' => 230, '𝆆' => 230, '𝆇' => 230, '𝆈' => 230, '𝆉' => 230, '𝆊' => 220, '𝆋' => 220, '𝆪' => 230, '𝆫' => 230, '𝆬' => 230, '𝆭' => 230, '𝉂' => 230, '𝉃' => 230, '𝉄' => 230, '𞀀' => 230, '𞀁' => 230, '𞀂' => 230, '𞀃' => 230, '𞀄' => 230, '𞀅' => 230, '𞀆' => 230, '𞀈' => 230, '𞀉' => 230, '𞀊' => 230, '𞀋' => 230, '𞀌' => 230, '𞀍' => 230, '𞀎' => 230, '𞀏' => 230, '𞀐' => 230, '𞀑' => 230, '𞀒' => 230, '𞀓' => 230, '𞀔' => 230, '𞀕' => 230, '𞀖' => 230, '𞀗' => 230, '𞀘' => 230, '𞀛' => 230, '𞀜' => 230, '𞀝' => 230, '𞀞' => 230, '𞀟' => 230, '𞀠' => 230, '𞀡' => 230, '𞀣' => 230, '𞀤' => 230, '𞀦' => 230, '𞀧' => 230, '𞀨' => 230, '𞀩' => 230, '𞀪' => 230, '𞄰' => 230, '𞄱' => 230, '𞄲' => 230, '𞄳' => 230, '𞄴' => 230, '𞄵' => 230, '𞄶' => 230, '𞋬' => 230, '𞋭' => 230, '𞋮' => 230, '𞋯' => 230, '𞣐' => 220, '𞣑' => 220, '𞣒' => 220, '𞣓' => 220, '𞣔' => 220, '𞣕' => 220, '𞣖' => 220, '𞥄' => 230, '𞥅' => 230, '𞥆' => 230, '𞥇' => 230, '𞥈' => 230, '𞥉' => 230, '𞥊' => 7, ); PK!c,ooXvendor/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.phpnu[ ' ', '¨' => ' ̈', 'ª' => 'a', '¯' => ' ̄', '²' => '2', '³' => '3', '´' => ' ́', 'µ' => 'μ', '¸' => ' ̧', '¹' => '1', 'º' => 'o', '¼' => '1⁄4', '½' => '1⁄2', '¾' => '3⁄4', 'IJ' => 'IJ', 'ij' => 'ij', 'Ŀ' => 'L·', 'ŀ' => 'l·', 'ʼn' => 'ʼn', 'ſ' => 's', 'DŽ' => 'DŽ', 'Dž' => 'Dž', 'dž' => 'dž', 'LJ' => 'LJ', 'Lj' => 'Lj', 'lj' => 'lj', 'NJ' => 'NJ', 'Nj' => 'Nj', 'nj' => 'nj', 'DZ' => 'DZ', 'Dz' => 'Dz', 'dz' => 'dz', 'ʰ' => 'h', 'ʱ' => 'ɦ', 'ʲ' => 'j', 'ʳ' => 'r', 'ʴ' => 'ɹ', 'ʵ' => 'ɻ', 'ʶ' => 'ʁ', 'ʷ' => 'w', 'ʸ' => 'y', '˘' => ' ̆', '˙' => ' ̇', '˚' => ' ̊', '˛' => ' ̨', '˜' => ' ̃', '˝' => ' ̋', 'ˠ' => 'ɣ', 'ˡ' => 'l', 'ˢ' => 's', 'ˣ' => 'x', 'ˤ' => 'ʕ', 'ͺ' => ' ͅ', '΄' => ' ́', '΅' => ' ̈́', 'ϐ' => 'β', 'ϑ' => 'θ', 'ϒ' => 'Υ', 'ϓ' => 'Ύ', 'ϔ' => 'Ϋ', 'ϕ' => 'φ', 'ϖ' => 'π', 'ϰ' => 'κ', 'ϱ' => 'ρ', 'ϲ' => 'ς', 'ϴ' => 'Θ', 'ϵ' => 'ε', 'Ϲ' => 'Σ', 'և' => 'եւ', 'ٵ' => 'اٴ', 'ٶ' => 'وٴ', 'ٷ' => 'ۇٴ', 'ٸ' => 'يٴ', 'ำ' => 'ํา', 'ຳ' => 'ໍາ', 'ໜ' => 'ຫນ', 'ໝ' => 'ຫມ', '༌' => '་', 'ཷ' => 'ྲཱྀ', 'ཹ' => 'ླཱྀ', 'ჼ' => 'ნ', 'ᴬ' => 'A', 'ᴭ' => 'Æ', 'ᴮ' => 'B', 'ᴰ' => 'D', 'ᴱ' => 'E', 'ᴲ' => 'Ǝ', 'ᴳ' => 'G', 'ᴴ' => 'H', 'ᴵ' => 'I', 'ᴶ' => 'J', 'ᴷ' => 'K', 'ᴸ' => 'L', 'ᴹ' => 'M', 'ᴺ' => 'N', 'ᴼ' => 'O', 'ᴽ' => 'Ȣ', 'ᴾ' => 'P', 'ᴿ' => 'R', 'ᵀ' => 'T', 'ᵁ' => 'U', 'ᵂ' => 'W', 'ᵃ' => 'a', 'ᵄ' => 'ɐ', 'ᵅ' => 'ɑ', 'ᵆ' => 'ᴂ', 'ᵇ' => 'b', 'ᵈ' => 'd', 'ᵉ' => 'e', 'ᵊ' => 'ə', 'ᵋ' => 'ɛ', 'ᵌ' => 'ɜ', 'ᵍ' => 'g', 'ᵏ' => 'k', 'ᵐ' => 'm', 'ᵑ' => 'ŋ', 'ᵒ' => 'o', 'ᵓ' => 'ɔ', 'ᵔ' => 'ᴖ', 'ᵕ' => 'ᴗ', 'ᵖ' => 'p', 'ᵗ' => 't', 'ᵘ' => 'u', 'ᵙ' => 'ᴝ', 'ᵚ' => 'ɯ', 'ᵛ' => 'v', 'ᵜ' => 'ᴥ', 'ᵝ' => 'β', 'ᵞ' => 'γ', 'ᵟ' => 'δ', 'ᵠ' => 'φ', 'ᵡ' => 'χ', 'ᵢ' => 'i', 'ᵣ' => 'r', 'ᵤ' => 'u', 'ᵥ' => 'v', 'ᵦ' => 'β', 'ᵧ' => 'γ', 'ᵨ' => 'ρ', 'ᵩ' => 'φ', 'ᵪ' => 'χ', 'ᵸ' => 'н', 'ᶛ' => 'ɒ', 'ᶜ' => 'c', 'ᶝ' => 'ɕ', 'ᶞ' => 'ð', 'ᶟ' => 'ɜ', 'ᶠ' => 'f', 'ᶡ' => 'ɟ', 'ᶢ' => 'ɡ', 'ᶣ' => 'ɥ', 'ᶤ' => 'ɨ', 'ᶥ' => 'ɩ', 'ᶦ' => 'ɪ', 'ᶧ' => 'ᵻ', 'ᶨ' => 'ʝ', 'ᶩ' => 'ɭ', 'ᶪ' => 'ᶅ', 'ᶫ' => 'ʟ', 'ᶬ' => 'ɱ', 'ᶭ' => 'ɰ', 'ᶮ' => 'ɲ', 'ᶯ' => 'ɳ', 'ᶰ' => 'ɴ', 'ᶱ' => 'ɵ', 'ᶲ' => 'ɸ', 'ᶳ' => 'ʂ', 'ᶴ' => 'ʃ', 'ᶵ' => 'ƫ', 'ᶶ' => 'ʉ', 'ᶷ' => 'ʊ', 'ᶸ' => 'ᴜ', 'ᶹ' => 'ʋ', 'ᶺ' => 'ʌ', 'ᶻ' => 'z', 'ᶼ' => 'ʐ', 'ᶽ' => 'ʑ', 'ᶾ' => 'ʒ', 'ᶿ' => 'θ', 'ẚ' => 'aʾ', 'ẛ' => 'ṡ', '᾽' => ' ̓', '᾿' => ' ̓', '῀' => ' ͂', '῁' => ' ̈͂', '῍' => ' ̓̀', '῎' => ' ̓́', '῏' => ' ̓͂', '῝' => ' ̔̀', '῞' => ' ̔́', '῟' => ' ̔͂', '῭' => ' ̈̀', '΅' => ' ̈́', '´' => ' ́', '῾' => ' ̔', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', ' ' => ' ', '‑' => '‐', '‗' => ' ̳', '․' => '.', '‥' => '..', '…' => '...', ' ' => ' ', '″' => '′′', '‴' => '′′′', '‶' => '‵‵', '‷' => '‵‵‵', '‼' => '!!', '‾' => ' ̅', '⁇' => '??', '⁈' => '?!', '⁉' => '!?', '⁗' => '′′′′', ' ' => ' ', '⁰' => '0', 'ⁱ' => 'i', '⁴' => '4', '⁵' => '5', '⁶' => '6', '⁷' => '7', '⁸' => '8', '⁹' => '9', '⁺' => '+', '⁻' => '−', '⁼' => '=', '⁽' => '(', '⁾' => ')', 'ⁿ' => 'n', '₀' => '0', '₁' => '1', '₂' => '2', '₃' => '3', '₄' => '4', '₅' => '5', '₆' => '6', '₇' => '7', '₈' => '8', '₉' => '9', '₊' => '+', '₋' => '−', '₌' => '=', '₍' => '(', '₎' => ')', 'ₐ' => 'a', 'ₑ' => 'e', 'ₒ' => 'o', 'ₓ' => 'x', 'ₔ' => 'ə', 'ₕ' => 'h', 'ₖ' => 'k', 'ₗ' => 'l', 'ₘ' => 'm', 'ₙ' => 'n', 'ₚ' => 'p', 'ₛ' => 's', 'ₜ' => 't', '₨' => 'Rs', '℀' => 'a/c', '℁' => 'a/s', 'ℂ' => 'C', '℃' => '°C', '℅' => 'c/o', '℆' => 'c/u', 'ℇ' => 'Ɛ', '℉' => '°F', 'ℊ' => 'g', 'ℋ' => 'H', 'ℌ' => 'H', 'ℍ' => 'H', 'ℎ' => 'h', 'ℏ' => 'ħ', 'ℐ' => 'I', 'ℑ' => 'I', 'ℒ' => 'L', 'ℓ' => 'l', 'ℕ' => 'N', '№' => 'No', 'ℙ' => 'P', 'ℚ' => 'Q', 'ℛ' => 'R', 'ℜ' => 'R', 'ℝ' => 'R', '℠' => 'SM', '℡' => 'TEL', '™' => 'TM', 'ℤ' => 'Z', 'ℨ' => 'Z', 'ℬ' => 'B', 'ℭ' => 'C', 'ℯ' => 'e', 'ℰ' => 'E', 'ℱ' => 'F', 'ℳ' => 'M', 'ℴ' => 'o', 'ℵ' => 'א', 'ℶ' => 'ב', 'ℷ' => 'ג', 'ℸ' => 'ד', 'ℹ' => 'i', '℻' => 'FAX', 'ℼ' => 'π', 'ℽ' => 'γ', 'ℾ' => 'Γ', 'ℿ' => 'Π', '⅀' => '∑', 'ⅅ' => 'D', 'ⅆ' => 'd', 'ⅇ' => 'e', 'ⅈ' => 'i', 'ⅉ' => 'j', '⅐' => '1⁄7', '⅑' => '1⁄9', '⅒' => '1⁄10', '⅓' => '1⁄3', '⅔' => '2⁄3', '⅕' => '1⁄5', '⅖' => '2⁄5', '⅗' => '3⁄5', '⅘' => '4⁄5', '⅙' => '1⁄6', '⅚' => '5⁄6', '⅛' => '1⁄8', '⅜' => '3⁄8', '⅝' => '5⁄8', '⅞' => '7⁄8', '⅟' => '1⁄', 'Ⅰ' => 'I', 'Ⅱ' => 'II', 'Ⅲ' => 'III', 'Ⅳ' => 'IV', 'Ⅴ' => 'V', 'Ⅵ' => 'VI', 'Ⅶ' => 'VII', 'Ⅷ' => 'VIII', 'Ⅸ' => 'IX', 'Ⅹ' => 'X', 'Ⅺ' => 'XI', 'Ⅻ' => 'XII', 'Ⅼ' => 'L', 'Ⅽ' => 'C', 'Ⅾ' => 'D', 'Ⅿ' => 'M', 'ⅰ' => 'i', 'ⅱ' => 'ii', 'ⅲ' => 'iii', 'ⅳ' => 'iv', 'ⅴ' => 'v', 'ⅵ' => 'vi', 'ⅶ' => 'vii', 'ⅷ' => 'viii', 'ⅸ' => 'ix', 'ⅹ' => 'x', 'ⅺ' => 'xi', 'ⅻ' => 'xii', 'ⅼ' => 'l', 'ⅽ' => 'c', 'ⅾ' => 'd', 'ⅿ' => 'm', '↉' => '0⁄3', '∬' => '∫∫', '∭' => '∫∫∫', '∯' => '∮∮', '∰' => '∮∮∮', '①' => '1', '②' => '2', '③' => '3', '④' => '4', '⑤' => '5', '⑥' => '6', '⑦' => '7', '⑧' => '8', '⑨' => '9', '⑩' => '10', '⑪' => '11', '⑫' => '12', '⑬' => '13', '⑭' => '14', '⑮' => '15', '⑯' => '16', '⑰' => '17', '⑱' => '18', '⑲' => '19', '⑳' => '20', '⑴' => '(1)', '⑵' => '(2)', '⑶' => '(3)', '⑷' => '(4)', '⑸' => '(5)', '⑹' => '(6)', '⑺' => '(7)', '⑻' => '(8)', '⑼' => '(9)', '⑽' => '(10)', '⑾' => '(11)', '⑿' => '(12)', '⒀' => '(13)', '⒁' => '(14)', '⒂' => '(15)', '⒃' => '(16)', '⒄' => '(17)', '⒅' => '(18)', '⒆' => '(19)', '⒇' => '(20)', '⒈' => '1.', '⒉' => '2.', '⒊' => '3.', '⒋' => '4.', '⒌' => '5.', '⒍' => '6.', '⒎' => '7.', '⒏' => '8.', '⒐' => '9.', '⒑' => '10.', '⒒' => '11.', '⒓' => '12.', '⒔' => '13.', '⒕' => '14.', '⒖' => '15.', '⒗' => '16.', '⒘' => '17.', '⒙' => '18.', '⒚' => '19.', '⒛' => '20.', '⒜' => '(a)', '⒝' => '(b)', '⒞' => '(c)', '⒟' => '(d)', '⒠' => '(e)', '⒡' => '(f)', '⒢' => '(g)', '⒣' => '(h)', '⒤' => '(i)', '⒥' => '(j)', '⒦' => '(k)', '⒧' => '(l)', '⒨' => '(m)', '⒩' => '(n)', '⒪' => '(o)', '⒫' => '(p)', '⒬' => '(q)', '⒭' => '(r)', '⒮' => '(s)', '⒯' => '(t)', '⒰' => '(u)', '⒱' => '(v)', '⒲' => '(w)', '⒳' => '(x)', '⒴' => '(y)', '⒵' => '(z)', 'Ⓐ' => 'A', 'Ⓑ' => 'B', 'Ⓒ' => 'C', 'Ⓓ' => 'D', 'Ⓔ' => 'E', 'Ⓕ' => 'F', 'Ⓖ' => 'G', 'Ⓗ' => 'H', 'Ⓘ' => 'I', 'Ⓙ' => 'J', 'Ⓚ' => 'K', 'Ⓛ' => 'L', 'Ⓜ' => 'M', 'Ⓝ' => 'N', 'Ⓞ' => 'O', 'Ⓟ' => 'P', 'Ⓠ' => 'Q', 'Ⓡ' => 'R', 'Ⓢ' => 'S', 'Ⓣ' => 'T', 'Ⓤ' => 'U', 'Ⓥ' => 'V', 'Ⓦ' => 'W', 'Ⓧ' => 'X', 'Ⓨ' => 'Y', 'Ⓩ' => 'Z', 'ⓐ' => 'a', 'ⓑ' => 'b', 'ⓒ' => 'c', 'ⓓ' => 'd', 'ⓔ' => 'e', 'ⓕ' => 'f', 'ⓖ' => 'g', 'ⓗ' => 'h', 'ⓘ' => 'i', 'ⓙ' => 'j', 'ⓚ' => 'k', 'ⓛ' => 'l', 'ⓜ' => 'm', 'ⓝ' => 'n', 'ⓞ' => 'o', 'ⓟ' => 'p', 'ⓠ' => 'q', 'ⓡ' => 'r', 'ⓢ' => 's', 'ⓣ' => 't', 'ⓤ' => 'u', 'ⓥ' => 'v', 'ⓦ' => 'w', 'ⓧ' => 'x', 'ⓨ' => 'y', 'ⓩ' => 'z', '⓪' => '0', '⨌' => '∫∫∫∫', '⩴' => '::=', '⩵' => '==', '⩶' => '===', 'ⱼ' => 'j', 'ⱽ' => 'V', 'ⵯ' => 'ⵡ', '⺟' => '母', '⻳' => '龟', '⼀' => '一', '⼁' => '丨', '⼂' => '丶', '⼃' => '丿', '⼄' => '乙', '⼅' => '亅', '⼆' => '二', '⼇' => '亠', '⼈' => '人', '⼉' => '儿', '⼊' => '入', '⼋' => '八', '⼌' => '冂', '⼍' => '冖', '⼎' => '冫', '⼏' => '几', '⼐' => '凵', '⼑' => '刀', '⼒' => '力', '⼓' => '勹', '⼔' => '匕', '⼕' => '匚', '⼖' => '匸', '⼗' => '十', '⼘' => '卜', '⼙' => '卩', '⼚' => '厂', '⼛' => '厶', '⼜' => '又', '⼝' => '口', '⼞' => '囗', '⼟' => '土', '⼠' => '士', '⼡' => '夂', '⼢' => '夊', '⼣' => '夕', '⼤' => '大', '⼥' => '女', '⼦' => '子', '⼧' => '宀', '⼨' => '寸', '⼩' => '小', '⼪' => '尢', '⼫' => '尸', '⼬' => '屮', '⼭' => '山', '⼮' => '巛', '⼯' => '工', '⼰' => '己', '⼱' => '巾', '⼲' => '干', '⼳' => '幺', '⼴' => '广', '⼵' => '廴', '⼶' => '廾', '⼷' => '弋', '⼸' => '弓', '⼹' => '彐', '⼺' => '彡', '⼻' => '彳', '⼼' => '心', '⼽' => '戈', '⼾' => '戶', '⼿' => '手', '⽀' => '支', '⽁' => '攴', '⽂' => '文', '⽃' => '斗', '⽄' => '斤', '⽅' => '方', '⽆' => '无', '⽇' => '日', '⽈' => '曰', '⽉' => '月', '⽊' => '木', '⽋' => '欠', '⽌' => '止', '⽍' => '歹', '⽎' => '殳', '⽏' => '毋', '⽐' => '比', '⽑' => '毛', '⽒' => '氏', '⽓' => '气', '⽔' => '水', '⽕' => '火', '⽖' => '爪', '⽗' => '父', '⽘' => '爻', '⽙' => '爿', '⽚' => '片', '⽛' => '牙', '⽜' => '牛', '⽝' => '犬', '⽞' => '玄', '⽟' => '玉', '⽠' => '瓜', '⽡' => '瓦', '⽢' => '甘', '⽣' => '生', '⽤' => '用', '⽥' => '田', '⽦' => '疋', '⽧' => '疒', '⽨' => '癶', '⽩' => '白', '⽪' => '皮', '⽫' => '皿', '⽬' => '目', '⽭' => '矛', '⽮' => '矢', '⽯' => '石', '⽰' => '示', '⽱' => '禸', '⽲' => '禾', '⽳' => '穴', '⽴' => '立', '⽵' => '竹', '⽶' => '米', '⽷' => '糸', '⽸' => '缶', '⽹' => '网', '⽺' => '羊', '⽻' => '羽', '⽼' => '老', '⽽' => '而', '⽾' => '耒', '⽿' => '耳', '⾀' => '聿', '⾁' => '肉', '⾂' => '臣', '⾃' => '自', '⾄' => '至', '⾅' => '臼', '⾆' => '舌', '⾇' => '舛', '⾈' => '舟', '⾉' => '艮', '⾊' => '色', '⾋' => '艸', '⾌' => '虍', '⾍' => '虫', '⾎' => '血', '⾏' => '行', '⾐' => '衣', '⾑' => '襾', '⾒' => '見', '⾓' => '角', '⾔' => '言', '⾕' => '谷', '⾖' => '豆', '⾗' => '豕', '⾘' => '豸', '⾙' => '貝', '⾚' => '赤', '⾛' => '走', '⾜' => '足', '⾝' => '身', '⾞' => '車', '⾟' => '辛', '⾠' => '辰', '⾡' => '辵', '⾢' => '邑', '⾣' => '酉', '⾤' => '釆', '⾥' => '里', '⾦' => '金', '⾧' => '長', '⾨' => '門', '⾩' => '阜', '⾪' => '隶', '⾫' => '隹', '⾬' => '雨', '⾭' => '靑', '⾮' => '非', '⾯' => '面', '⾰' => '革', '⾱' => '韋', '⾲' => '韭', '⾳' => '音', '⾴' => '頁', '⾵' => '風', '⾶' => '飛', '⾷' => '食', '⾸' => '首', '⾹' => '香', '⾺' => '馬', '⾻' => '骨', '⾼' => '高', '⾽' => '髟', '⾾' => '鬥', '⾿' => '鬯', '⿀' => '鬲', '⿁' => '鬼', '⿂' => '魚', '⿃' => '鳥', '⿄' => '鹵', '⿅' => '鹿', '⿆' => '麥', '⿇' => '麻', '⿈' => '黃', '⿉' => '黍', '⿊' => '黑', '⿋' => '黹', '⿌' => '黽', '⿍' => '鼎', '⿎' => '鼓', '⿏' => '鼠', '⿐' => '鼻', '⿑' => '齊', '⿒' => '齒', '⿓' => '龍', '⿔' => '龜', '⿕' => '龠', ' ' => ' ', '〶' => '〒', '〸' => '十', '〹' => '卄', '〺' => '卅', '゛' => ' ゙', '゜' => ' ゚', 'ゟ' => 'より', 'ヿ' => 'コト', 'ㄱ' => 'ᄀ', 'ㄲ' => 'ᄁ', 'ㄳ' => 'ᆪ', 'ㄴ' => 'ᄂ', 'ㄵ' => 'ᆬ', 'ㄶ' => 'ᆭ', 'ㄷ' => 'ᄃ', 'ㄸ' => 'ᄄ', 'ㄹ' => 'ᄅ', 'ㄺ' => 'ᆰ', 'ㄻ' => 'ᆱ', 'ㄼ' => 'ᆲ', 'ㄽ' => 'ᆳ', 'ㄾ' => 'ᆴ', 'ㄿ' => 'ᆵ', 'ㅀ' => 'ᄚ', 'ㅁ' => 'ᄆ', 'ㅂ' => 'ᄇ', 'ㅃ' => 'ᄈ', 'ㅄ' => 'ᄡ', 'ㅅ' => 'ᄉ', 'ㅆ' => 'ᄊ', 'ㅇ' => 'ᄋ', 'ㅈ' => 'ᄌ', 'ㅉ' => 'ᄍ', 'ㅊ' => 'ᄎ', 'ㅋ' => 'ᄏ', 'ㅌ' => 'ᄐ', 'ㅍ' => 'ᄑ', 'ㅎ' => 'ᄒ', 'ㅏ' => 'ᅡ', 'ㅐ' => 'ᅢ', 'ㅑ' => 'ᅣ', 'ㅒ' => 'ᅤ', 'ㅓ' => 'ᅥ', 'ㅔ' => 'ᅦ', 'ㅕ' => 'ᅧ', 'ㅖ' => 'ᅨ', 'ㅗ' => 'ᅩ', 'ㅘ' => 'ᅪ', 'ㅙ' => 'ᅫ', 'ㅚ' => 'ᅬ', 'ㅛ' => 'ᅭ', 'ㅜ' => 'ᅮ', 'ㅝ' => 'ᅯ', 'ㅞ' => 'ᅰ', 'ㅟ' => 'ᅱ', 'ㅠ' => 'ᅲ', 'ㅡ' => 'ᅳ', 'ㅢ' => 'ᅴ', 'ㅣ' => 'ᅵ', 'ㅤ' => 'ᅠ', 'ㅥ' => 'ᄔ', 'ㅦ' => 'ᄕ', 'ㅧ' => 'ᇇ', 'ㅨ' => 'ᇈ', 'ㅩ' => 'ᇌ', 'ㅪ' => 'ᇎ', 'ㅫ' => 'ᇓ', 'ㅬ' => 'ᇗ', 'ㅭ' => 'ᇙ', 'ㅮ' => 'ᄜ', 'ㅯ' => 'ᇝ', 'ㅰ' => 'ᇟ', 'ㅱ' => 'ᄝ', 'ㅲ' => 'ᄞ', 'ㅳ' => 'ᄠ', 'ㅴ' => 'ᄢ', 'ㅵ' => 'ᄣ', 'ㅶ' => 'ᄧ', 'ㅷ' => 'ᄩ', 'ㅸ' => 'ᄫ', 'ㅹ' => 'ᄬ', 'ㅺ' => 'ᄭ', 'ㅻ' => 'ᄮ', 'ㅼ' => 'ᄯ', 'ㅽ' => 'ᄲ', 'ㅾ' => 'ᄶ', 'ㅿ' => 'ᅀ', 'ㆀ' => 'ᅇ', 'ㆁ' => 'ᅌ', 'ㆂ' => 'ᇱ', 'ㆃ' => 'ᇲ', 'ㆄ' => 'ᅗ', 'ㆅ' => 'ᅘ', 'ㆆ' => 'ᅙ', 'ㆇ' => 'ᆄ', 'ㆈ' => 'ᆅ', 'ㆉ' => 'ᆈ', 'ㆊ' => 'ᆑ', 'ㆋ' => 'ᆒ', 'ㆌ' => 'ᆔ', 'ㆍ' => 'ᆞ', 'ㆎ' => 'ᆡ', '㆒' => '一', '㆓' => '二', '㆔' => '三', '㆕' => '四', '㆖' => '上', '㆗' => '中', '㆘' => '下', '㆙' => '甲', '㆚' => '乙', '㆛' => '丙', '㆜' => '丁', '㆝' => '天', '㆞' => '地', '㆟' => '人', '㈀' => '(ᄀ)', '㈁' => '(ᄂ)', '㈂' => '(ᄃ)', '㈃' => '(ᄅ)', '㈄' => '(ᄆ)', '㈅' => '(ᄇ)', '㈆' => '(ᄉ)', '㈇' => '(ᄋ)', '㈈' => '(ᄌ)', '㈉' => '(ᄎ)', '㈊' => '(ᄏ)', '㈋' => '(ᄐ)', '㈌' => '(ᄑ)', '㈍' => '(ᄒ)', '㈎' => '(가)', '㈏' => '(나)', '㈐' => '(다)', '㈑' => '(라)', '㈒' => '(마)', '㈓' => '(바)', '㈔' => '(사)', '㈕' => '(아)', '㈖' => '(자)', '㈗' => '(차)', '㈘' => '(카)', '㈙' => '(타)', '㈚' => '(파)', '㈛' => '(하)', '㈜' => '(주)', '㈝' => '(오전)', '㈞' => '(오후)', '㈠' => '(一)', '㈡' => '(二)', '㈢' => '(三)', '㈣' => '(四)', '㈤' => '(五)', '㈥' => '(六)', '㈦' => '(七)', '㈧' => '(八)', '㈨' => '(九)', '㈩' => '(十)', '㈪' => '(月)', '㈫' => '(火)', '㈬' => '(水)', '㈭' => '(木)', '㈮' => '(金)', '㈯' => '(土)', '㈰' => '(日)', '㈱' => '(株)', '㈲' => '(有)', '㈳' => '(社)', '㈴' => '(名)', '㈵' => '(特)', '㈶' => '(財)', '㈷' => '(祝)', '㈸' => '(労)', '㈹' => '(代)', '㈺' => '(呼)', '㈻' => '(学)', '㈼' => '(監)', '㈽' => '(企)', '㈾' => '(資)', '㈿' => '(協)', '㉀' => '(祭)', '㉁' => '(休)', '㉂' => '(自)', '㉃' => '(至)', '㉄' => '問', '㉅' => '幼', '㉆' => '文', '㉇' => '箏', '㉐' => 'PTE', '㉑' => '21', '㉒' => '22', '㉓' => '23', '㉔' => '24', '㉕' => '25', '㉖' => '26', '㉗' => '27', '㉘' => '28', '㉙' => '29', '㉚' => '30', '㉛' => '31', '㉜' => '32', '㉝' => '33', '㉞' => '34', '㉟' => '35', '㉠' => 'ᄀ', '㉡' => 'ᄂ', '㉢' => 'ᄃ', '㉣' => 'ᄅ', '㉤' => 'ᄆ', '㉥' => 'ᄇ', '㉦' => 'ᄉ', '㉧' => 'ᄋ', '㉨' => 'ᄌ', '㉩' => 'ᄎ', '㉪' => 'ᄏ', '㉫' => 'ᄐ', '㉬' => 'ᄑ', '㉭' => 'ᄒ', '㉮' => '가', '㉯' => '나', '㉰' => '다', '㉱' => '라', '㉲' => '마', '㉳' => '바', '㉴' => '사', '㉵' => '아', '㉶' => '자', '㉷' => '차', '㉸' => '카', '㉹' => '타', '㉺' => '파', '㉻' => '하', '㉼' => '참고', '㉽' => '주의', '㉾' => '우', '㊀' => '一', '㊁' => '二', '㊂' => '三', '㊃' => '四', '㊄' => '五', '㊅' => '六', '㊆' => '七', '㊇' => '八', '㊈' => '九', '㊉' => '十', '㊊' => '月', '㊋' => '火', '㊌' => '水', '㊍' => '木', '㊎' => '金', '㊏' => '土', '㊐' => '日', '㊑' => '株', '㊒' => '有', '㊓' => '社', '㊔' => '名', '㊕' => '特', '㊖' => '財', '㊗' => '祝', '㊘' => '労', '㊙' => '秘', '㊚' => '男', '㊛' => '女', '㊜' => '適', '㊝' => '優', '㊞' => '印', '㊟' => '注', '㊠' => '項', '㊡' => '休', '㊢' => '写', '㊣' => '正', '㊤' => '上', '㊥' => '中', '㊦' => '下', '㊧' => '左', '㊨' => '右', '㊩' => '医', '㊪' => '宗', '㊫' => '学', '㊬' => '監', '㊭' => '企', '㊮' => '資', '㊯' => '協', '㊰' => '夜', '㊱' => '36', '㊲' => '37', '㊳' => '38', '㊴' => '39', '㊵' => '40', '㊶' => '41', '㊷' => '42', '㊸' => '43', '㊹' => '44', '㊺' => '45', '㊻' => '46', '㊼' => '47', '㊽' => '48', '㊾' => '49', '㊿' => '50', '㋀' => '1月', '㋁' => '2月', '㋂' => '3月', '㋃' => '4月', '㋄' => '5月', '㋅' => '6月', '㋆' => '7月', '㋇' => '8月', '㋈' => '9月', '㋉' => '10月', '㋊' => '11月', '㋋' => '12月', '㋌' => 'Hg', '㋍' => 'erg', '㋎' => 'eV', '㋏' => 'LTD', '㋐' => 'ア', '㋑' => 'イ', '㋒' => 'ウ', '㋓' => 'エ', '㋔' => 'オ', '㋕' => 'カ', '㋖' => 'キ', '㋗' => 'ク', '㋘' => 'ケ', '㋙' => 'コ', '㋚' => 'サ', '㋛' => 'シ', '㋜' => 'ス', '㋝' => 'セ', '㋞' => 'ソ', '㋟' => 'タ', '㋠' => 'チ', '㋡' => 'ツ', '㋢' => 'テ', '㋣' => 'ト', '㋤' => 'ナ', '㋥' => 'ニ', '㋦' => 'ヌ', '㋧' => 'ネ', '㋨' => 'ノ', '㋩' => 'ハ', '㋪' => 'ヒ', '㋫' => 'フ', '㋬' => 'ヘ', '㋭' => 'ホ', '㋮' => 'マ', '㋯' => 'ミ', '㋰' => 'ム', '㋱' => 'メ', '㋲' => 'モ', '㋳' => 'ヤ', '㋴' => 'ユ', '㋵' => 'ヨ', '㋶' => 'ラ', '㋷' => 'リ', '㋸' => 'ル', '㋹' => 'レ', '㋺' => 'ロ', '㋻' => 'ワ', '㋼' => 'ヰ', '㋽' => 'ヱ', '㋾' => 'ヲ', '㋿' => '令和', '㌀' => 'アパート', '㌁' => 'アルファ', '㌂' => 'アンペア', '㌃' => 'アール', '㌄' => 'イニング', '㌅' => 'インチ', '㌆' => 'ウォン', '㌇' => 'エスクード', '㌈' => 'エーカー', '㌉' => 'オンス', '㌊' => 'オーム', '㌋' => 'カイリ', '㌌' => 'カラット', '㌍' => 'カロリー', '㌎' => 'ガロン', '㌏' => 'ガンマ', '㌐' => 'ギガ', '㌑' => 'ギニー', '㌒' => 'キュリー', '㌓' => 'ギルダー', '㌔' => 'キロ', '㌕' => 'キログラム', '㌖' => 'キロメートル', '㌗' => 'キロワット', '㌘' => 'グラム', '㌙' => 'グラムトン', '㌚' => 'クルゼイロ', '㌛' => 'クローネ', '㌜' => 'ケース', '㌝' => 'コルナ', '㌞' => 'コーポ', '㌟' => 'サイクル', '㌠' => 'サンチーム', '㌡' => 'シリング', '㌢' => 'センチ', '㌣' => 'セント', '㌤' => 'ダース', '㌥' => 'デシ', '㌦' => 'ドル', '㌧' => 'トン', '㌨' => 'ナノ', '㌩' => 'ノット', '㌪' => 'ハイツ', '㌫' => 'パーセント', '㌬' => 'パーツ', '㌭' => 'バーレル', '㌮' => 'ピアストル', '㌯' => 'ピクル', '㌰' => 'ピコ', '㌱' => 'ビル', '㌲' => 'ファラッド', '㌳' => 'フィート', '㌴' => 'ブッシェル', '㌵' => 'フラン', '㌶' => 'ヘクタール', '㌷' => 'ペソ', '㌸' => 'ペニヒ', '㌹' => 'ヘルツ', '㌺' => 'ペンス', '㌻' => 'ページ', '㌼' => 'ベータ', '㌽' => 'ポイント', '㌾' => 'ボルト', '㌿' => 'ホン', '㍀' => 'ポンド', '㍁' => 'ホール', '㍂' => 'ホーン', '㍃' => 'マイクロ', '㍄' => 'マイル', '㍅' => 'マッハ', '㍆' => 'マルク', '㍇' => 'マンション', '㍈' => 'ミクロン', '㍉' => 'ミリ', '㍊' => 'ミリバール', '㍋' => 'メガ', '㍌' => 'メガトン', '㍍' => 'メートル', '㍎' => 'ヤード', '㍏' => 'ヤール', '㍐' => 'ユアン', '㍑' => 'リットル', '㍒' => 'リラ', '㍓' => 'ルピー', '㍔' => 'ルーブル', '㍕' => 'レム', '㍖' => 'レントゲン', '㍗' => 'ワット', '㍘' => '0点', '㍙' => '1点', '㍚' => '2点', '㍛' => '3点', '㍜' => '4点', '㍝' => '5点', '㍞' => '6点', '㍟' => '7点', '㍠' => '8点', '㍡' => '9点', '㍢' => '10点', '㍣' => '11点', '㍤' => '12点', '㍥' => '13点', '㍦' => '14点', '㍧' => '15点', '㍨' => '16点', '㍩' => '17点', '㍪' => '18点', '㍫' => '19点', '㍬' => '20点', '㍭' => '21点', '㍮' => '22点', '㍯' => '23点', '㍰' => '24点', '㍱' => 'hPa', '㍲' => 'da', '㍳' => 'AU', '㍴' => 'bar', '㍵' => 'oV', '㍶' => 'pc', '㍷' => 'dm', '㍸' => 'dm2', '㍹' => 'dm3', '㍺' => 'IU', '㍻' => '平成', '㍼' => '昭和', '㍽' => '大正', '㍾' => '明治', '㍿' => '株式会社', '㎀' => 'pA', '㎁' => 'nA', '㎂' => 'μA', '㎃' => 'mA', '㎄' => 'kA', '㎅' => 'KB', '㎆' => 'MB', '㎇' => 'GB', '㎈' => 'cal', '㎉' => 'kcal', '㎊' => 'pF', '㎋' => 'nF', '㎌' => 'μF', '㎍' => 'μg', '㎎' => 'mg', '㎏' => 'kg', '㎐' => 'Hz', '㎑' => 'kHz', '㎒' => 'MHz', '㎓' => 'GHz', '㎔' => 'THz', '㎕' => 'μl', '㎖' => 'ml', '㎗' => 'dl', '㎘' => 'kl', '㎙' => 'fm', '㎚' => 'nm', '㎛' => 'μm', '㎜' => 'mm', '㎝' => 'cm', '㎞' => 'km', '㎟' => 'mm2', '㎠' => 'cm2', '㎡' => 'm2', '㎢' => 'km2', '㎣' => 'mm3', '㎤' => 'cm3', '㎥' => 'm3', '㎦' => 'km3', '㎧' => 'm∕s', '㎨' => 'm∕s2', '㎩' => 'Pa', '㎪' => 'kPa', '㎫' => 'MPa', '㎬' => 'GPa', '㎭' => 'rad', '㎮' => 'rad∕s', '㎯' => 'rad∕s2', '㎰' => 'ps', '㎱' => 'ns', '㎲' => 'μs', '㎳' => 'ms', '㎴' => 'pV', '㎵' => 'nV', '㎶' => 'μV', '㎷' => 'mV', '㎸' => 'kV', '㎹' => 'MV', '㎺' => 'pW', '㎻' => 'nW', '㎼' => 'μW', '㎽' => 'mW', '㎾' => 'kW', '㎿' => 'MW', '㏀' => 'kΩ', '㏁' => 'MΩ', '㏂' => 'a.m.', '㏃' => 'Bq', '㏄' => 'cc', '㏅' => 'cd', '㏆' => 'C∕kg', '㏇' => 'Co.', '㏈' => 'dB', '㏉' => 'Gy', '㏊' => 'ha', '㏋' => 'HP', '㏌' => 'in', '㏍' => 'KK', '㏎' => 'KM', '㏏' => 'kt', '㏐' => 'lm', '㏑' => 'ln', '㏒' => 'log', '㏓' => 'lx', '㏔' => 'mb', '㏕' => 'mil', '㏖' => 'mol', '㏗' => 'PH', '㏘' => 'p.m.', '㏙' => 'PPM', '㏚' => 'PR', '㏛' => 'sr', '㏜' => 'Sv', '㏝' => 'Wb', '㏞' => 'V∕m', '㏟' => 'A∕m', '㏠' => '1日', '㏡' => '2日', '㏢' => '3日', '㏣' => '4日', '㏤' => '5日', '㏥' => '6日', '㏦' => '7日', '㏧' => '8日', '㏨' => '9日', '㏩' => '10日', '㏪' => '11日', '㏫' => '12日', '㏬' => '13日', '㏭' => '14日', '㏮' => '15日', '㏯' => '16日', '㏰' => '17日', '㏱' => '18日', '㏲' => '19日', '㏳' => '20日', '㏴' => '21日', '㏵' => '22日', '㏶' => '23日', '㏷' => '24日', '㏸' => '25日', '㏹' => '26日', '㏺' => '27日', '㏻' => '28日', '㏼' => '29日', '㏽' => '30日', '㏾' => '31日', '㏿' => 'gal', 'ꚜ' => 'ъ', 'ꚝ' => 'ь', 'ꝰ' => 'ꝯ', 'ꟸ' => 'Ħ', 'ꟹ' => 'œ', 'ꭜ' => 'ꜧ', 'ꭝ' => 'ꬷ', 'ꭞ' => 'ɫ', 'ꭟ' => 'ꭒ', 'ꭩ' => 'ʍ', 'ff' => 'ff', 'fi' => 'fi', 'fl' => 'fl', 'ffi' => 'ffi', 'ffl' => 'ffl', 'ſt' => 'st', 'st' => 'st', 'ﬓ' => 'մն', 'ﬔ' => 'մե', 'ﬕ' => 'մի', 'ﬖ' => 'վն', 'ﬗ' => 'մխ', 'ﬠ' => 'ע', 'ﬡ' => 'א', 'ﬢ' => 'ד', 'ﬣ' => 'ה', 'ﬤ' => 'כ', 'ﬥ' => 'ל', 'ﬦ' => 'ם', 'ﬧ' => 'ר', 'ﬨ' => 'ת', '﬩' => '+', 'ﭏ' => 'אל', 'ﭐ' => 'ٱ', 'ﭑ' => 'ٱ', 'ﭒ' => 'ٻ', 'ﭓ' => 'ٻ', 'ﭔ' => 'ٻ', 'ﭕ' => 'ٻ', 'ﭖ' => 'پ', 'ﭗ' => 'پ', 'ﭘ' => 'پ', 'ﭙ' => 'پ', 'ﭚ' => 'ڀ', 'ﭛ' => 'ڀ', 'ﭜ' => 'ڀ', 'ﭝ' => 'ڀ', 'ﭞ' => 'ٺ', 'ﭟ' => 'ٺ', 'ﭠ' => 'ٺ', 'ﭡ' => 'ٺ', 'ﭢ' => 'ٿ', 'ﭣ' => 'ٿ', 'ﭤ' => 'ٿ', 'ﭥ' => 'ٿ', 'ﭦ' => 'ٹ', 'ﭧ' => 'ٹ', 'ﭨ' => 'ٹ', 'ﭩ' => 'ٹ', 'ﭪ' => 'ڤ', 'ﭫ' => 'ڤ', 'ﭬ' => 'ڤ', 'ﭭ' => 'ڤ', 'ﭮ' => 'ڦ', 'ﭯ' => 'ڦ', 'ﭰ' => 'ڦ', 'ﭱ' => 'ڦ', 'ﭲ' => 'ڄ', 'ﭳ' => 'ڄ', 'ﭴ' => 'ڄ', 'ﭵ' => 'ڄ', 'ﭶ' => 'ڃ', 'ﭷ' => 'ڃ', 'ﭸ' => 'ڃ', 'ﭹ' => 'ڃ', 'ﭺ' => 'چ', 'ﭻ' => 'چ', 'ﭼ' => 'چ', 'ﭽ' => 'چ', 'ﭾ' => 'ڇ', 'ﭿ' => 'ڇ', 'ﮀ' => 'ڇ', 'ﮁ' => 'ڇ', 'ﮂ' => 'ڍ', 'ﮃ' => 'ڍ', 'ﮄ' => 'ڌ', 'ﮅ' => 'ڌ', 'ﮆ' => 'ڎ', 'ﮇ' => 'ڎ', 'ﮈ' => 'ڈ', 'ﮉ' => 'ڈ', 'ﮊ' => 'ژ', 'ﮋ' => 'ژ', 'ﮌ' => 'ڑ', 'ﮍ' => 'ڑ', 'ﮎ' => 'ک', 'ﮏ' => 'ک', 'ﮐ' => 'ک', 'ﮑ' => 'ک', 'ﮒ' => 'گ', 'ﮓ' => 'گ', 'ﮔ' => 'گ', 'ﮕ' => 'گ', 'ﮖ' => 'ڳ', 'ﮗ' => 'ڳ', 'ﮘ' => 'ڳ', 'ﮙ' => 'ڳ', 'ﮚ' => 'ڱ', 'ﮛ' => 'ڱ', 'ﮜ' => 'ڱ', 'ﮝ' => 'ڱ', 'ﮞ' => 'ں', 'ﮟ' => 'ں', 'ﮠ' => 'ڻ', 'ﮡ' => 'ڻ', 'ﮢ' => 'ڻ', 'ﮣ' => 'ڻ', 'ﮤ' => 'ۀ', 'ﮥ' => 'ۀ', 'ﮦ' => 'ہ', 'ﮧ' => 'ہ', 'ﮨ' => 'ہ', 'ﮩ' => 'ہ', 'ﮪ' => 'ھ', 'ﮫ' => 'ھ', 'ﮬ' => 'ھ', 'ﮭ' => 'ھ', 'ﮮ' => 'ے', 'ﮯ' => 'ے', 'ﮰ' => 'ۓ', 'ﮱ' => 'ۓ', 'ﯓ' => 'ڭ', 'ﯔ' => 'ڭ', 'ﯕ' => 'ڭ', 'ﯖ' => 'ڭ', 'ﯗ' => 'ۇ', 'ﯘ' => 'ۇ', 'ﯙ' => 'ۆ', 'ﯚ' => 'ۆ', 'ﯛ' => 'ۈ', 'ﯜ' => 'ۈ', 'ﯝ' => 'ۇٴ', 'ﯞ' => 'ۋ', 'ﯟ' => 'ۋ', 'ﯠ' => 'ۅ', 'ﯡ' => 'ۅ', 'ﯢ' => 'ۉ', 'ﯣ' => 'ۉ', 'ﯤ' => 'ې', 'ﯥ' => 'ې', 'ﯦ' => 'ې', 'ﯧ' => 'ې', 'ﯨ' => 'ى', 'ﯩ' => 'ى', 'ﯪ' => 'ئا', 'ﯫ' => 'ئا', 'ﯬ' => 'ئە', 'ﯭ' => 'ئە', 'ﯮ' => 'ئو', 'ﯯ' => 'ئو', 'ﯰ' => 'ئۇ', 'ﯱ' => 'ئۇ', 'ﯲ' => 'ئۆ', 'ﯳ' => 'ئۆ', 'ﯴ' => 'ئۈ', 'ﯵ' => 'ئۈ', 'ﯶ' => 'ئې', 'ﯷ' => 'ئې', 'ﯸ' => 'ئې', 'ﯹ' => 'ئى', 'ﯺ' => 'ئى', 'ﯻ' => 'ئى', 'ﯼ' => 'ی', 'ﯽ' => 'ی', 'ﯾ' => 'ی', 'ﯿ' => 'ی', 'ﰀ' => 'ئج', 'ﰁ' => 'ئح', 'ﰂ' => 'ئم', 'ﰃ' => 'ئى', 'ﰄ' => 'ئي', 'ﰅ' => 'بج', 'ﰆ' => 'بح', 'ﰇ' => 'بخ', 'ﰈ' => 'بم', 'ﰉ' => 'بى', 'ﰊ' => 'بي', 'ﰋ' => 'تج', 'ﰌ' => 'تح', 'ﰍ' => 'تخ', 'ﰎ' => 'تم', 'ﰏ' => 'تى', 'ﰐ' => 'تي', 'ﰑ' => 'ثج', 'ﰒ' => 'ثم', 'ﰓ' => 'ثى', 'ﰔ' => 'ثي', 'ﰕ' => 'جح', 'ﰖ' => 'جم', 'ﰗ' => 'حج', 'ﰘ' => 'حم', 'ﰙ' => 'خج', 'ﰚ' => 'خح', 'ﰛ' => 'خم', 'ﰜ' => 'سج', 'ﰝ' => 'سح', 'ﰞ' => 'سخ', 'ﰟ' => 'سم', 'ﰠ' => 'صح', 'ﰡ' => 'صم', 'ﰢ' => 'ضج', 'ﰣ' => 'ضح', 'ﰤ' => 'ضخ', 'ﰥ' => 'ضم', 'ﰦ' => 'طح', 'ﰧ' => 'طم', 'ﰨ' => 'ظم', 'ﰩ' => 'عج', 'ﰪ' => 'عم', 'ﰫ' => 'غج', 'ﰬ' => 'غم', 'ﰭ' => 'فج', 'ﰮ' => 'فح', 'ﰯ' => 'فخ', 'ﰰ' => 'فم', 'ﰱ' => 'فى', 'ﰲ' => 'في', 'ﰳ' => 'قح', 'ﰴ' => 'قم', 'ﰵ' => 'قى', 'ﰶ' => 'قي', 'ﰷ' => 'كا', 'ﰸ' => 'كج', 'ﰹ' => 'كح', 'ﰺ' => 'كخ', 'ﰻ' => 'كل', 'ﰼ' => 'كم', 'ﰽ' => 'كى', 'ﰾ' => 'كي', 'ﰿ' => 'لج', 'ﱀ' => 'لح', 'ﱁ' => 'لخ', 'ﱂ' => 'لم', 'ﱃ' => 'لى', 'ﱄ' => 'لي', 'ﱅ' => 'مج', 'ﱆ' => 'مح', 'ﱇ' => 'مخ', 'ﱈ' => 'مم', 'ﱉ' => 'مى', 'ﱊ' => 'مي', 'ﱋ' => 'نج', 'ﱌ' => 'نح', 'ﱍ' => 'نخ', 'ﱎ' => 'نم', 'ﱏ' => 'نى', 'ﱐ' => 'ني', 'ﱑ' => 'هج', 'ﱒ' => 'هم', 'ﱓ' => 'هى', 'ﱔ' => 'هي', 'ﱕ' => 'يج', 'ﱖ' => 'يح', 'ﱗ' => 'يخ', 'ﱘ' => 'يم', 'ﱙ' => 'يى', 'ﱚ' => 'يي', 'ﱛ' => 'ذٰ', 'ﱜ' => 'رٰ', 'ﱝ' => 'ىٰ', 'ﱞ' => ' ٌّ', 'ﱟ' => ' ٍّ', 'ﱠ' => ' َّ', 'ﱡ' => ' ُّ', 'ﱢ' => ' ِّ', 'ﱣ' => ' ّٰ', 'ﱤ' => 'ئر', 'ﱥ' => 'ئز', 'ﱦ' => 'ئم', 'ﱧ' => 'ئن', 'ﱨ' => 'ئى', 'ﱩ' => 'ئي', 'ﱪ' => 'بر', 'ﱫ' => 'بز', 'ﱬ' => 'بم', 'ﱭ' => 'بن', 'ﱮ' => 'بى', 'ﱯ' => 'بي', 'ﱰ' => 'تر', 'ﱱ' => 'تز', 'ﱲ' => 'تم', 'ﱳ' => 'تن', 'ﱴ' => 'تى', 'ﱵ' => 'تي', 'ﱶ' => 'ثر', 'ﱷ' => 'ثز', 'ﱸ' => 'ثم', 'ﱹ' => 'ثن', 'ﱺ' => 'ثى', 'ﱻ' => 'ثي', 'ﱼ' => 'فى', 'ﱽ' => 'في', 'ﱾ' => 'قى', 'ﱿ' => 'قي', 'ﲀ' => 'كا', 'ﲁ' => 'كل', 'ﲂ' => 'كم', 'ﲃ' => 'كى', 'ﲄ' => 'كي', 'ﲅ' => 'لم', 'ﲆ' => 'لى', 'ﲇ' => 'لي', 'ﲈ' => 'ما', 'ﲉ' => 'مم', 'ﲊ' => 'نر', 'ﲋ' => 'نز', 'ﲌ' => 'نم', 'ﲍ' => 'نن', 'ﲎ' => 'نى', 'ﲏ' => 'ني', 'ﲐ' => 'ىٰ', 'ﲑ' => 'ير', 'ﲒ' => 'يز', 'ﲓ' => 'يم', 'ﲔ' => 'ين', 'ﲕ' => 'يى', 'ﲖ' => 'يي', 'ﲗ' => 'ئج', 'ﲘ' => 'ئح', 'ﲙ' => 'ئخ', 'ﲚ' => 'ئم', 'ﲛ' => 'ئه', 'ﲜ' => 'بج', 'ﲝ' => 'بح', 'ﲞ' => 'بخ', 'ﲟ' => 'بم', 'ﲠ' => 'به', 'ﲡ' => 'تج', 'ﲢ' => 'تح', 'ﲣ' => 'تخ', 'ﲤ' => 'تم', 'ﲥ' => 'ته', 'ﲦ' => 'ثم', 'ﲧ' => 'جح', 'ﲨ' => 'جم', 'ﲩ' => 'حج', 'ﲪ' => 'حم', 'ﲫ' => 'خج', 'ﲬ' => 'خم', 'ﲭ' => 'سج', 'ﲮ' => 'سح', 'ﲯ' => 'سخ', 'ﲰ' => 'سم', 'ﲱ' => 'صح', 'ﲲ' => 'صخ', 'ﲳ' => 'صم', 'ﲴ' => 'ضج', 'ﲵ' => 'ضح', 'ﲶ' => 'ضخ', 'ﲷ' => 'ضم', 'ﲸ' => 'طح', 'ﲹ' => 'ظم', 'ﲺ' => 'عج', 'ﲻ' => 'عم', 'ﲼ' => 'غج', 'ﲽ' => 'غم', 'ﲾ' => 'فج', 'ﲿ' => 'فح', 'ﳀ' => 'فخ', 'ﳁ' => 'فم', 'ﳂ' => 'قح', 'ﳃ' => 'قم', 'ﳄ' => 'كج', 'ﳅ' => 'كح', 'ﳆ' => 'كخ', 'ﳇ' => 'كل', 'ﳈ' => 'كم', 'ﳉ' => 'لج', 'ﳊ' => 'لح', 'ﳋ' => 'لخ', 'ﳌ' => 'لم', 'ﳍ' => 'له', 'ﳎ' => 'مج', 'ﳏ' => 'مح', 'ﳐ' => 'مخ', 'ﳑ' => 'مم', 'ﳒ' => 'نج', 'ﳓ' => 'نح', 'ﳔ' => 'نخ', 'ﳕ' => 'نم', 'ﳖ' => 'نه', 'ﳗ' => 'هج', 'ﳘ' => 'هم', 'ﳙ' => 'هٰ', 'ﳚ' => 'يج', 'ﳛ' => 'يح', 'ﳜ' => 'يخ', 'ﳝ' => 'يم', 'ﳞ' => 'يه', 'ﳟ' => 'ئم', 'ﳠ' => 'ئه', 'ﳡ' => 'بم', 'ﳢ' => 'به', 'ﳣ' => 'تم', 'ﳤ' => 'ته', 'ﳥ' => 'ثم', 'ﳦ' => 'ثه', 'ﳧ' => 'سم', 'ﳨ' => 'سه', 'ﳩ' => 'شم', 'ﳪ' => 'شه', 'ﳫ' => 'كل', 'ﳬ' => 'كم', 'ﳭ' => 'لم', 'ﳮ' => 'نم', 'ﳯ' => 'نه', 'ﳰ' => 'يم', 'ﳱ' => 'يه', 'ﳲ' => 'ـَّ', 'ﳳ' => 'ـُّ', 'ﳴ' => 'ـِّ', 'ﳵ' => 'طى', 'ﳶ' => 'طي', 'ﳷ' => 'عى', 'ﳸ' => 'عي', 'ﳹ' => 'غى', 'ﳺ' => 'غي', 'ﳻ' => 'سى', 'ﳼ' => 'سي', 'ﳽ' => 'شى', 'ﳾ' => 'شي', 'ﳿ' => 'حى', 'ﴀ' => 'حي', 'ﴁ' => 'جى', 'ﴂ' => 'جي', 'ﴃ' => 'خى', 'ﴄ' => 'خي', 'ﴅ' => 'صى', 'ﴆ' => 'صي', 'ﴇ' => 'ضى', 'ﴈ' => 'ضي', 'ﴉ' => 'شج', 'ﴊ' => 'شح', 'ﴋ' => 'شخ', 'ﴌ' => 'شم', 'ﴍ' => 'شر', 'ﴎ' => 'سر', 'ﴏ' => 'صر', 'ﴐ' => 'ضر', 'ﴑ' => 'طى', 'ﴒ' => 'طي', 'ﴓ' => 'عى', 'ﴔ' => 'عي', 'ﴕ' => 'غى', 'ﴖ' => 'غي', 'ﴗ' => 'سى', 'ﴘ' => 'سي', 'ﴙ' => 'شى', 'ﴚ' => 'شي', 'ﴛ' => 'حى', 'ﴜ' => 'حي', 'ﴝ' => 'جى', 'ﴞ' => 'جي', 'ﴟ' => 'خى', 'ﴠ' => 'خي', 'ﴡ' => 'صى', 'ﴢ' => 'صي', 'ﴣ' => 'ضى', 'ﴤ' => 'ضي', 'ﴥ' => 'شج', 'ﴦ' => 'شح', 'ﴧ' => 'شخ', 'ﴨ' => 'شم', 'ﴩ' => 'شر', 'ﴪ' => 'سر', 'ﴫ' => 'صر', 'ﴬ' => 'ضر', 'ﴭ' => 'شج', 'ﴮ' => 'شح', 'ﴯ' => 'شخ', 'ﴰ' => 'شم', 'ﴱ' => 'سه', 'ﴲ' => 'شه', 'ﴳ' => 'طم', 'ﴴ' => 'سج', 'ﴵ' => 'سح', 'ﴶ' => 'سخ', 'ﴷ' => 'شج', 'ﴸ' => 'شح', 'ﴹ' => 'شخ', 'ﴺ' => 'طم', 'ﴻ' => 'ظم', 'ﴼ' => 'اً', 'ﴽ' => 'اً', 'ﵐ' => 'تجم', 'ﵑ' => 'تحج', 'ﵒ' => 'تحج', 'ﵓ' => 'تحم', 'ﵔ' => 'تخم', 'ﵕ' => 'تمج', 'ﵖ' => 'تمح', 'ﵗ' => 'تمخ', 'ﵘ' => 'جمح', 'ﵙ' => 'جمح', 'ﵚ' => 'حمي', 'ﵛ' => 'حمى', 'ﵜ' => 'سحج', 'ﵝ' => 'سجح', 'ﵞ' => 'سجى', 'ﵟ' => 'سمح', 'ﵠ' => 'سمح', 'ﵡ' => 'سمج', 'ﵢ' => 'سمم', 'ﵣ' => 'سمم', 'ﵤ' => 'صحح', 'ﵥ' => 'صحح', 'ﵦ' => 'صمم', 'ﵧ' => 'شحم', 'ﵨ' => 'شحم', 'ﵩ' => 'شجي', 'ﵪ' => 'شمخ', 'ﵫ' => 'شمخ', 'ﵬ' => 'شمم', 'ﵭ' => 'شمم', 'ﵮ' => 'ضحى', 'ﵯ' => 'ضخم', 'ﵰ' => 'ضخم', 'ﵱ' => 'طمح', 'ﵲ' => 'طمح', 'ﵳ' => 'طمم', 'ﵴ' => 'طمي', 'ﵵ' => 'عجم', 'ﵶ' => 'عمم', 'ﵷ' => 'عمم', 'ﵸ' => 'عمى', 'ﵹ' => 'غمم', 'ﵺ' => 'غمي', 'ﵻ' => 'غمى', 'ﵼ' => 'فخم', 'ﵽ' => 'فخم', 'ﵾ' => 'قمح', 'ﵿ' => 'قمم', 'ﶀ' => 'لحم', 'ﶁ' => 'لحي', 'ﶂ' => 'لحى', 'ﶃ' => 'لجج', 'ﶄ' => 'لجج', 'ﶅ' => 'لخم', 'ﶆ' => 'لخم', 'ﶇ' => 'لمح', 'ﶈ' => 'لمح', 'ﶉ' => 'محج', 'ﶊ' => 'محم', 'ﶋ' => 'محي', 'ﶌ' => 'مجح', 'ﶍ' => 'مجم', 'ﶎ' => 'مخج', 'ﶏ' => 'مخم', 'ﶒ' => 'مجخ', 'ﶓ' => 'همج', 'ﶔ' => 'همم', 'ﶕ' => 'نحم', 'ﶖ' => 'نحى', 'ﶗ' => 'نجم', 'ﶘ' => 'نجم', 'ﶙ' => 'نجى', 'ﶚ' => 'نمي', 'ﶛ' => 'نمى', 'ﶜ' => 'يمم', 'ﶝ' => 'يمم', 'ﶞ' => 'بخي', 'ﶟ' => 'تجي', 'ﶠ' => 'تجى', 'ﶡ' => 'تخي', 'ﶢ' => 'تخى', 'ﶣ' => 'تمي', 'ﶤ' => 'تمى', 'ﶥ' => 'جمي', 'ﶦ' => 'جحى', 'ﶧ' => 'جمى', 'ﶨ' => 'سخى', 'ﶩ' => 'صحي', 'ﶪ' => 'شحي', 'ﶫ' => 'ضحي', 'ﶬ' => 'لجي', 'ﶭ' => 'لمي', 'ﶮ' => 'يحي', 'ﶯ' => 'يجي', 'ﶰ' => 'يمي', 'ﶱ' => 'ممي', 'ﶲ' => 'قمي', 'ﶳ' => 'نحي', 'ﶴ' => 'قمح', 'ﶵ' => 'لحم', 'ﶶ' => 'عمي', 'ﶷ' => 'كمي', 'ﶸ' => 'نجح', 'ﶹ' => 'مخي', 'ﶺ' => 'لجم', 'ﶻ' => 'كمم', 'ﶼ' => 'لجم', 'ﶽ' => 'نجح', 'ﶾ' => 'جحي', 'ﶿ' => 'حجي', 'ﷀ' => 'مجي', 'ﷁ' => 'فمي', 'ﷂ' => 'بحي', 'ﷃ' => 'كمم', 'ﷄ' => 'عجم', 'ﷅ' => 'صمم', 'ﷆ' => 'سخي', 'ﷇ' => 'نجي', 'ﷰ' => 'صلے', 'ﷱ' => 'قلے', 'ﷲ' => 'الله', 'ﷳ' => 'اكبر', 'ﷴ' => 'محمد', 'ﷵ' => 'صلعم', 'ﷶ' => 'رسول', 'ﷷ' => 'عليه', 'ﷸ' => 'وسلم', 'ﷹ' => 'صلى', 'ﷺ' => 'صلى الله عليه وسلم', 'ﷻ' => 'جل جلاله', '﷼' => 'ریال', '︐' => ',', '︑' => '、', '︒' => '。', '︓' => ':', '︔' => ';', '︕' => '!', '︖' => '?', '︗' => '〖', '︘' => '〗', '︙' => '...', '︰' => '..', '︱' => '—', '︲' => '–', '︳' => '_', '︴' => '_', '︵' => '(', '︶' => ')', '︷' => '{', '︸' => '}', '︹' => '〔', '︺' => '〕', '︻' => '【', '︼' => '】', '︽' => '《', '︾' => '》', '︿' => '〈', '﹀' => '〉', '﹁' => '「', '﹂' => '」', '﹃' => '『', '﹄' => '』', '﹇' => '[', '﹈' => ']', '﹉' => ' ̅', '﹊' => ' ̅', '﹋' => ' ̅', '﹌' => ' ̅', '﹍' => '_', '﹎' => '_', '﹏' => '_', '﹐' => ',', '﹑' => '、', '﹒' => '.', '﹔' => ';', '﹕' => ':', '﹖' => '?', '﹗' => '!', '﹘' => '—', '﹙' => '(', '﹚' => ')', '﹛' => '{', '﹜' => '}', '﹝' => '〔', '﹞' => '〕', '﹟' => '#', '﹠' => '&', '﹡' => '*', '﹢' => '+', '﹣' => '-', '﹤' => '<', '﹥' => '>', '﹦' => '=', '﹨' => '\\', '﹩' => '$', '﹪' => '%', '﹫' => '@', 'ﹰ' => ' ً', 'ﹱ' => 'ـً', 'ﹲ' => ' ٌ', 'ﹴ' => ' ٍ', 'ﹶ' => ' َ', 'ﹷ' => 'ـَ', 'ﹸ' => ' ُ', 'ﹹ' => 'ـُ', 'ﹺ' => ' ِ', 'ﹻ' => 'ـِ', 'ﹼ' => ' ّ', 'ﹽ' => 'ـّ', 'ﹾ' => ' ْ', 'ﹿ' => 'ـْ', 'ﺀ' => 'ء', 'ﺁ' => 'آ', 'ﺂ' => 'آ', 'ﺃ' => 'أ', 'ﺄ' => 'أ', 'ﺅ' => 'ؤ', 'ﺆ' => 'ؤ', 'ﺇ' => 'إ', 'ﺈ' => 'إ', 'ﺉ' => 'ئ', 'ﺊ' => 'ئ', 'ﺋ' => 'ئ', 'ﺌ' => 'ئ', 'ﺍ' => 'ا', 'ﺎ' => 'ا', 'ﺏ' => 'ب', 'ﺐ' => 'ب', 'ﺑ' => 'ب', 'ﺒ' => 'ب', 'ﺓ' => 'ة', 'ﺔ' => 'ة', 'ﺕ' => 'ت', 'ﺖ' => 'ت', 'ﺗ' => 'ت', 'ﺘ' => 'ت', 'ﺙ' => 'ث', 'ﺚ' => 'ث', 'ﺛ' => 'ث', 'ﺜ' => 'ث', 'ﺝ' => 'ج', 'ﺞ' => 'ج', 'ﺟ' => 'ج', 'ﺠ' => 'ج', 'ﺡ' => 'ح', 'ﺢ' => 'ح', 'ﺣ' => 'ح', 'ﺤ' => 'ح', 'ﺥ' => 'خ', 'ﺦ' => 'خ', 'ﺧ' => 'خ', 'ﺨ' => 'خ', 'ﺩ' => 'د', 'ﺪ' => 'د', 'ﺫ' => 'ذ', 'ﺬ' => 'ذ', 'ﺭ' => 'ر', 'ﺮ' => 'ر', 'ﺯ' => 'ز', 'ﺰ' => 'ز', 'ﺱ' => 'س', 'ﺲ' => 'س', 'ﺳ' => 'س', 'ﺴ' => 'س', 'ﺵ' => 'ش', 'ﺶ' => 'ش', 'ﺷ' => 'ش', 'ﺸ' => 'ش', 'ﺹ' => 'ص', 'ﺺ' => 'ص', 'ﺻ' => 'ص', 'ﺼ' => 'ص', 'ﺽ' => 'ض', 'ﺾ' => 'ض', 'ﺿ' => 'ض', 'ﻀ' => 'ض', 'ﻁ' => 'ط', 'ﻂ' => 'ط', 'ﻃ' => 'ط', 'ﻄ' => 'ط', 'ﻅ' => 'ظ', 'ﻆ' => 'ظ', 'ﻇ' => 'ظ', 'ﻈ' => 'ظ', 'ﻉ' => 'ع', 'ﻊ' => 'ع', 'ﻋ' => 'ع', 'ﻌ' => 'ع', 'ﻍ' => 'غ', 'ﻎ' => 'غ', 'ﻏ' => 'غ', 'ﻐ' => 'غ', 'ﻑ' => 'ف', 'ﻒ' => 'ف', 'ﻓ' => 'ف', 'ﻔ' => 'ف', 'ﻕ' => 'ق', 'ﻖ' => 'ق', 'ﻗ' => 'ق', 'ﻘ' => 'ق', 'ﻙ' => 'ك', 'ﻚ' => 'ك', 'ﻛ' => 'ك', 'ﻜ' => 'ك', 'ﻝ' => 'ل', 'ﻞ' => 'ل', 'ﻟ' => 'ل', 'ﻠ' => 'ل', 'ﻡ' => 'م', 'ﻢ' => 'م', 'ﻣ' => 'م', 'ﻤ' => 'م', 'ﻥ' => 'ن', 'ﻦ' => 'ن', 'ﻧ' => 'ن', 'ﻨ' => 'ن', 'ﻩ' => 'ه', 'ﻪ' => 'ه', 'ﻫ' => 'ه', 'ﻬ' => 'ه', 'ﻭ' => 'و', 'ﻮ' => 'و', 'ﻯ' => 'ى', 'ﻰ' => 'ى', 'ﻱ' => 'ي', 'ﻲ' => 'ي', 'ﻳ' => 'ي', 'ﻴ' => 'ي', 'ﻵ' => 'لآ', 'ﻶ' => 'لآ', 'ﻷ' => 'لأ', 'ﻸ' => 'لأ', 'ﻹ' => 'لإ', 'ﻺ' => 'لإ', 'ﻻ' => 'لا', 'ﻼ' => 'لا', '!' => '!', '"' => '"', '#' => '#', '$' => '$', '%' => '%', '&' => '&', ''' => '\'', '(' => '(', ')' => ')', '*' => '*', '+' => '+', ',' => ',', '-' => '-', '.' => '.', '/' => '/', '0' => '0', '1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', ':' => ':', ';' => ';', '<' => '<', '=' => '=', '>' => '>', '?' => '?', '@' => '@', 'A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E', 'F' => 'F', 'G' => 'G', 'H' => 'H', 'I' => 'I', 'J' => 'J', 'K' => 'K', 'L' => 'L', 'M' => 'M', 'N' => 'N', 'O' => 'O', 'P' => 'P', 'Q' => 'Q', 'R' => 'R', 'S' => 'S', 'T' => 'T', 'U' => 'U', 'V' => 'V', 'W' => 'W', 'X' => 'X', 'Y' => 'Y', 'Z' => 'Z', '[' => '[', '\' => '\\', ']' => ']', '^' => '^', '_' => '_', '`' => '`', 'a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e', 'f' => 'f', 'g' => 'g', 'h' => 'h', 'i' => 'i', 'j' => 'j', 'k' => 'k', 'l' => 'l', 'm' => 'm', 'n' => 'n', 'o' => 'o', 'p' => 'p', 'q' => 'q', 'r' => 'r', 's' => 's', 't' => 't', 'u' => 'u', 'v' => 'v', 'w' => 'w', 'x' => 'x', 'y' => 'y', 'z' => 'z', '{' => '{', '|' => '|', '}' => '}', '~' => '~', '⦅' => '⦅', '⦆' => '⦆', '。' => '。', '「' => '「', '」' => '」', '、' => '、', '・' => '・', 'ヲ' => 'ヲ', 'ァ' => 'ァ', 'ィ' => 'ィ', 'ゥ' => 'ゥ', 'ェ' => 'ェ', 'ォ' => 'ォ', 'ャ' => 'ャ', 'ュ' => 'ュ', 'ョ' => 'ョ', 'ッ' => 'ッ', 'ー' => 'ー', 'ア' => 'ア', 'イ' => 'イ', 'ウ' => 'ウ', 'エ' => 'エ', 'オ' => 'オ', 'カ' => 'カ', 'キ' => 'キ', 'ク' => 'ク', 'ケ' => 'ケ', 'コ' => 'コ', 'サ' => 'サ', 'シ' => 'シ', 'ス' => 'ス', 'セ' => 'セ', 'ソ' => 'ソ', 'タ' => 'タ', 'チ' => 'チ', 'ツ' => 'ツ', 'テ' => 'テ', 'ト' => 'ト', 'ナ' => 'ナ', 'ニ' => 'ニ', 'ヌ' => 'ヌ', 'ネ' => 'ネ', 'ノ' => 'ノ', 'ハ' => 'ハ', 'ヒ' => 'ヒ', 'フ' => 'フ', 'ヘ' => 'ヘ', 'ホ' => 'ホ', 'マ' => 'マ', 'ミ' => 'ミ', 'ム' => 'ム', 'メ' => 'メ', 'モ' => 'モ', 'ヤ' => 'ヤ', 'ユ' => 'ユ', 'ヨ' => 'ヨ', 'ラ' => 'ラ', 'リ' => 'リ', 'ル' => 'ル', 'レ' => 'レ', 'ロ' => 'ロ', 'ワ' => 'ワ', 'ン' => 'ン', '゙' => '゙', '゚' => '゚', 'ᅠ' => 'ᅠ', 'ᄀ' => 'ᄀ', 'ᄁ' => 'ᄁ', 'ᆪ' => 'ᆪ', 'ᄂ' => 'ᄂ', 'ᆬ' => 'ᆬ', 'ᆭ' => 'ᆭ', 'ᄃ' => 'ᄃ', 'ᄄ' => 'ᄄ', 'ᄅ' => 'ᄅ', 'ᆰ' => 'ᆰ', 'ᆱ' => 'ᆱ', 'ᆲ' => 'ᆲ', 'ᆳ' => 'ᆳ', 'ᆴ' => 'ᆴ', 'ᆵ' => 'ᆵ', 'ᄚ' => 'ᄚ', 'ᄆ' => 'ᄆ', 'ᄇ' => 'ᄇ', 'ᄈ' => 'ᄈ', 'ᄡ' => 'ᄡ', 'ᄉ' => 'ᄉ', 'ᄊ' => 'ᄊ', 'ᄋ' => 'ᄋ', 'ᄌ' => 'ᄌ', 'ᄍ' => 'ᄍ', 'ᄎ' => 'ᄎ', 'ᄏ' => 'ᄏ', 'ᄐ' => 'ᄐ', 'ᄑ' => 'ᄑ', 'ᄒ' => 'ᄒ', 'ᅡ' => 'ᅡ', 'ᅢ' => 'ᅢ', 'ᅣ' => 'ᅣ', 'ᅤ' => 'ᅤ', 'ᅥ' => 'ᅥ', 'ᅦ' => 'ᅦ', 'ᅧ' => 'ᅧ', 'ᅨ' => 'ᅨ', 'ᅩ' => 'ᅩ', 'ᅪ' => 'ᅪ', 'ᅫ' => 'ᅫ', 'ᅬ' => 'ᅬ', 'ᅭ' => 'ᅭ', 'ᅮ' => 'ᅮ', 'ᅯ' => 'ᅯ', 'ᅰ' => 'ᅰ', 'ᅱ' => 'ᅱ', 'ᅲ' => 'ᅲ', 'ᅳ' => 'ᅳ', 'ᅴ' => 'ᅴ', 'ᅵ' => 'ᅵ', '¢' => '¢', '£' => '£', '¬' => '¬', ' ̄' => ' ̄', '¦' => '¦', '¥' => '¥', '₩' => '₩', '│' => '│', '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '■' => '■', '○' => '○', '𝐀' => 'A', '𝐁' => 'B', '𝐂' => 'C', '𝐃' => 'D', '𝐄' => 'E', '𝐅' => 'F', '𝐆' => 'G', '𝐇' => 'H', '𝐈' => 'I', '𝐉' => 'J', '𝐊' => 'K', '𝐋' => 'L', '𝐌' => 'M', '𝐍' => 'N', '𝐎' => 'O', '𝐏' => 'P', '𝐐' => 'Q', '𝐑' => 'R', '𝐒' => 'S', '𝐓' => 'T', '𝐔' => 'U', '𝐕' => 'V', '𝐖' => 'W', '𝐗' => 'X', '𝐘' => 'Y', '𝐙' => 'Z', '𝐚' => 'a', '𝐛' => 'b', '𝐜' => 'c', '𝐝' => 'd', '𝐞' => 'e', '𝐟' => 'f', '𝐠' => 'g', '𝐡' => 'h', '𝐢' => 'i', '𝐣' => 'j', '𝐤' => 'k', '𝐥' => 'l', '𝐦' => 'm', '𝐧' => 'n', '𝐨' => 'o', '𝐩' => 'p', '𝐪' => 'q', '𝐫' => 'r', '𝐬' => 's', '𝐭' => 't', '𝐮' => 'u', '𝐯' => 'v', '𝐰' => 'w', '𝐱' => 'x', '𝐲' => 'y', '𝐳' => 'z', '𝐴' => 'A', '𝐵' => 'B', '𝐶' => 'C', '𝐷' => 'D', '𝐸' => 'E', '𝐹' => 'F', '𝐺' => 'G', '𝐻' => 'H', '𝐼' => 'I', '𝐽' => 'J', '𝐾' => 'K', '𝐿' => 'L', '𝑀' => 'M', '𝑁' => 'N', '𝑂' => 'O', '𝑃' => 'P', '𝑄' => 'Q', '𝑅' => 'R', '𝑆' => 'S', '𝑇' => 'T', '𝑈' => 'U', '𝑉' => 'V', '𝑊' => 'W', '𝑋' => 'X', '𝑌' => 'Y', '𝑍' => 'Z', '𝑎' => 'a', '𝑏' => 'b', '𝑐' => 'c', '𝑑' => 'd', '𝑒' => 'e', '𝑓' => 'f', '𝑔' => 'g', '𝑖' => 'i', '𝑗' => 'j', '𝑘' => 'k', '𝑙' => 'l', '𝑚' => 'm', '𝑛' => 'n', '𝑜' => 'o', '𝑝' => 'p', '𝑞' => 'q', '𝑟' => 'r', '𝑠' => 's', '𝑡' => 't', '𝑢' => 'u', '𝑣' => 'v', '𝑤' => 'w', '𝑥' => 'x', '𝑦' => 'y', '𝑧' => 'z', '𝑨' => 'A', '𝑩' => 'B', '𝑪' => 'C', '𝑫' => 'D', '𝑬' => 'E', '𝑭' => 'F', '𝑮' => 'G', '𝑯' => 'H', '𝑰' => 'I', '𝑱' => 'J', '𝑲' => 'K', '𝑳' => 'L', '𝑴' => 'M', '𝑵' => 'N', '𝑶' => 'O', '𝑷' => 'P', '𝑸' => 'Q', '𝑹' => 'R', '𝑺' => 'S', '𝑻' => 'T', '𝑼' => 'U', '𝑽' => 'V', '𝑾' => 'W', '𝑿' => 'X', '𝒀' => 'Y', '𝒁' => 'Z', '𝒂' => 'a', '𝒃' => 'b', '𝒄' => 'c', '𝒅' => 'd', '𝒆' => 'e', '𝒇' => 'f', '𝒈' => 'g', '𝒉' => 'h', '𝒊' => 'i', '𝒋' => 'j', '𝒌' => 'k', '𝒍' => 'l', '𝒎' => 'm', '𝒏' => 'n', '𝒐' => 'o', '𝒑' => 'p', '𝒒' => 'q', '𝒓' => 'r', '𝒔' => 's', '𝒕' => 't', '𝒖' => 'u', '𝒗' => 'v', '𝒘' => 'w', '𝒙' => 'x', '𝒚' => 'y', '𝒛' => 'z', '𝒜' => 'A', '𝒞' => 'C', '𝒟' => 'D', '𝒢' => 'G', '𝒥' => 'J', '𝒦' => 'K', '𝒩' => 'N', '𝒪' => 'O', '𝒫' => 'P', '𝒬' => 'Q', '𝒮' => 'S', '𝒯' => 'T', '𝒰' => 'U', '𝒱' => 'V', '𝒲' => 'W', '𝒳' => 'X', '𝒴' => 'Y', '𝒵' => 'Z', '𝒶' => 'a', '𝒷' => 'b', '𝒸' => 'c', '𝒹' => 'd', '𝒻' => 'f', '𝒽' => 'h', '𝒾' => 'i', '𝒿' => 'j', '𝓀' => 'k', '𝓁' => 'l', '𝓂' => 'm', '𝓃' => 'n', '𝓅' => 'p', '𝓆' => 'q', '𝓇' => 'r', '𝓈' => 's', '𝓉' => 't', '𝓊' => 'u', '𝓋' => 'v', '𝓌' => 'w', '𝓍' => 'x', '𝓎' => 'y', '𝓏' => 'z', '𝓐' => 'A', '𝓑' => 'B', '𝓒' => 'C', '𝓓' => 'D', '𝓔' => 'E', '𝓕' => 'F', '𝓖' => 'G', '𝓗' => 'H', '𝓘' => 'I', '𝓙' => 'J', '𝓚' => 'K', '𝓛' => 'L', '𝓜' => 'M', '𝓝' => 'N', '𝓞' => 'O', '𝓟' => 'P', '𝓠' => 'Q', '𝓡' => 'R', '𝓢' => 'S', '𝓣' => 'T', '𝓤' => 'U', '𝓥' => 'V', '𝓦' => 'W', '𝓧' => 'X', '𝓨' => 'Y', '𝓩' => 'Z', '𝓪' => 'a', '𝓫' => 'b', '𝓬' => 'c', '𝓭' => 'd', '𝓮' => 'e', '𝓯' => 'f', '𝓰' => 'g', '𝓱' => 'h', '𝓲' => 'i', '𝓳' => 'j', '𝓴' => 'k', '𝓵' => 'l', '𝓶' => 'm', '𝓷' => 'n', '𝓸' => 'o', '𝓹' => 'p', '𝓺' => 'q', '𝓻' => 'r', '𝓼' => 's', '𝓽' => 't', '𝓾' => 'u', '𝓿' => 'v', '𝔀' => 'w', '𝔁' => 'x', '𝔂' => 'y', '𝔃' => 'z', '𝔄' => 'A', '𝔅' => 'B', '𝔇' => 'D', '𝔈' => 'E', '𝔉' => 'F', '𝔊' => 'G', '𝔍' => 'J', '𝔎' => 'K', '𝔏' => 'L', '𝔐' => 'M', '𝔑' => 'N', '𝔒' => 'O', '𝔓' => 'P', '𝔔' => 'Q', '𝔖' => 'S', '𝔗' => 'T', '𝔘' => 'U', '𝔙' => 'V', '𝔚' => 'W', '𝔛' => 'X', '𝔜' => 'Y', '𝔞' => 'a', '𝔟' => 'b', '𝔠' => 'c', '𝔡' => 'd', '𝔢' => 'e', '𝔣' => 'f', '𝔤' => 'g', '𝔥' => 'h', '𝔦' => 'i', '𝔧' => 'j', '𝔨' => 'k', '𝔩' => 'l', '𝔪' => 'm', '𝔫' => 'n', '𝔬' => 'o', '𝔭' => 'p', '𝔮' => 'q', '𝔯' => 'r', '𝔰' => 's', '𝔱' => 't', '𝔲' => 'u', '𝔳' => 'v', '𝔴' => 'w', '𝔵' => 'x', '𝔶' => 'y', '𝔷' => 'z', '𝔸' => 'A', '𝔹' => 'B', '𝔻' => 'D', '𝔼' => 'E', '𝔽' => 'F', '𝔾' => 'G', '𝕀' => 'I', '𝕁' => 'J', '𝕂' => 'K', '𝕃' => 'L', '𝕄' => 'M', '𝕆' => 'O', '𝕊' => 'S', '𝕋' => 'T', '𝕌' => 'U', '𝕍' => 'V', '𝕎' => 'W', '𝕏' => 'X', '𝕐' => 'Y', '𝕒' => 'a', '𝕓' => 'b', '𝕔' => 'c', '𝕕' => 'd', '𝕖' => 'e', '𝕗' => 'f', '𝕘' => 'g', '𝕙' => 'h', '𝕚' => 'i', '𝕛' => 'j', '𝕜' => 'k', '𝕝' => 'l', '𝕞' => 'm', '𝕟' => 'n', '𝕠' => 'o', '𝕡' => 'p', '𝕢' => 'q', '𝕣' => 'r', '𝕤' => 's', '𝕥' => 't', '𝕦' => 'u', '𝕧' => 'v', '𝕨' => 'w', '𝕩' => 'x', '𝕪' => 'y', '𝕫' => 'z', '𝕬' => 'A', '𝕭' => 'B', '𝕮' => 'C', '𝕯' => 'D', '𝕰' => 'E', '𝕱' => 'F', '𝕲' => 'G', '𝕳' => 'H', '𝕴' => 'I', '𝕵' => 'J', '𝕶' => 'K', '𝕷' => 'L', '𝕸' => 'M', '𝕹' => 'N', '𝕺' => 'O', '𝕻' => 'P', '𝕼' => 'Q', '𝕽' => 'R', '𝕾' => 'S', '𝕿' => 'T', '𝖀' => 'U', '𝖁' => 'V', '𝖂' => 'W', '𝖃' => 'X', '𝖄' => 'Y', '𝖅' => 'Z', '𝖆' => 'a', '𝖇' => 'b', '𝖈' => 'c', '𝖉' => 'd', '𝖊' => 'e', '𝖋' => 'f', '𝖌' => 'g', '𝖍' => 'h', '𝖎' => 'i', '𝖏' => 'j', '𝖐' => 'k', '𝖑' => 'l', '𝖒' => 'm', '𝖓' => 'n', '𝖔' => 'o', '𝖕' => 'p', '𝖖' => 'q', '𝖗' => 'r', '𝖘' => 's', '𝖙' => 't', '𝖚' => 'u', '𝖛' => 'v', '𝖜' => 'w', '𝖝' => 'x', '𝖞' => 'y', '𝖟' => 'z', '𝖠' => 'A', '𝖡' => 'B', '𝖢' => 'C', '𝖣' => 'D', '𝖤' => 'E', '𝖥' => 'F', '𝖦' => 'G', '𝖧' => 'H', '𝖨' => 'I', '𝖩' => 'J', '𝖪' => 'K', '𝖫' => 'L', '𝖬' => 'M', '𝖭' => 'N', '𝖮' => 'O', '𝖯' => 'P', '𝖰' => 'Q', '𝖱' => 'R', '𝖲' => 'S', '𝖳' => 'T', '𝖴' => 'U', '𝖵' => 'V', '𝖶' => 'W', '𝖷' => 'X', '𝖸' => 'Y', '𝖹' => 'Z', '𝖺' => 'a', '𝖻' => 'b', '𝖼' => 'c', '𝖽' => 'd', '𝖾' => 'e', '𝖿' => 'f', '𝗀' => 'g', '𝗁' => 'h', '𝗂' => 'i', '𝗃' => 'j', '𝗄' => 'k', '𝗅' => 'l', '𝗆' => 'm', '𝗇' => 'n', '𝗈' => 'o', '𝗉' => 'p', '𝗊' => 'q', '𝗋' => 'r', '𝗌' => 's', '𝗍' => 't', '𝗎' => 'u', '𝗏' => 'v', '𝗐' => 'w', '𝗑' => 'x', '𝗒' => 'y', '𝗓' => 'z', '𝗔' => 'A', '𝗕' => 'B', '𝗖' => 'C', '𝗗' => 'D', '𝗘' => 'E', '𝗙' => 'F', '𝗚' => 'G', '𝗛' => 'H', '𝗜' => 'I', '𝗝' => 'J', '𝗞' => 'K', '𝗟' => 'L', '𝗠' => 'M', '𝗡' => 'N', '𝗢' => 'O', '𝗣' => 'P', '𝗤' => 'Q', '𝗥' => 'R', '𝗦' => 'S', '𝗧' => 'T', '𝗨' => 'U', '𝗩' => 'V', '𝗪' => 'W', '𝗫' => 'X', '𝗬' => 'Y', '𝗭' => 'Z', '𝗮' => 'a', '𝗯' => 'b', '𝗰' => 'c', '𝗱' => 'd', '𝗲' => 'e', '𝗳' => 'f', '𝗴' => 'g', '𝗵' => 'h', '𝗶' => 'i', '𝗷' => 'j', '𝗸' => 'k', '𝗹' => 'l', '𝗺' => 'm', '𝗻' => 'n', '𝗼' => 'o', '𝗽' => 'p', '𝗾' => 'q', '𝗿' => 'r', '𝘀' => 's', '𝘁' => 't', '𝘂' => 'u', '𝘃' => 'v', '𝘄' => 'w', '𝘅' => 'x', '𝘆' => 'y', '𝘇' => 'z', '𝘈' => 'A', '𝘉' => 'B', '𝘊' => 'C', '𝘋' => 'D', '𝘌' => 'E', '𝘍' => 'F', '𝘎' => 'G', '𝘏' => 'H', '𝘐' => 'I', '𝘑' => 'J', '𝘒' => 'K', '𝘓' => 'L', '𝘔' => 'M', '𝘕' => 'N', '𝘖' => 'O', '𝘗' => 'P', '𝘘' => 'Q', '𝘙' => 'R', '𝘚' => 'S', '𝘛' => 'T', '𝘜' => 'U', '𝘝' => 'V', '𝘞' => 'W', '𝘟' => 'X', '𝘠' => 'Y', '𝘡' => 'Z', '𝘢' => 'a', '𝘣' => 'b', '𝘤' => 'c', '𝘥' => 'd', '𝘦' => 'e', '𝘧' => 'f', '𝘨' => 'g', '𝘩' => 'h', '𝘪' => 'i', '𝘫' => 'j', '𝘬' => 'k', '𝘭' => 'l', '𝘮' => 'm', '𝘯' => 'n', '𝘰' => 'o', '𝘱' => 'p', '𝘲' => 'q', '𝘳' => 'r', '𝘴' => 's', '𝘵' => 't', '𝘶' => 'u', '𝘷' => 'v', '𝘸' => 'w', '𝘹' => 'x', '𝘺' => 'y', '𝘻' => 'z', '𝘼' => 'A', '𝘽' => 'B', '𝘾' => 'C', '𝘿' => 'D', '𝙀' => 'E', '𝙁' => 'F', '𝙂' => 'G', '𝙃' => 'H', '𝙄' => 'I', '𝙅' => 'J', '𝙆' => 'K', '𝙇' => 'L', '𝙈' => 'M', '𝙉' => 'N', '𝙊' => 'O', '𝙋' => 'P', '𝙌' => 'Q', '𝙍' => 'R', '𝙎' => 'S', '𝙏' => 'T', '𝙐' => 'U', '𝙑' => 'V', '𝙒' => 'W', '𝙓' => 'X', '𝙔' => 'Y', '𝙕' => 'Z', '𝙖' => 'a', '𝙗' => 'b', '𝙘' => 'c', '𝙙' => 'd', '𝙚' => 'e', '𝙛' => 'f', '𝙜' => 'g', '𝙝' => 'h', '𝙞' => 'i', '𝙟' => 'j', '𝙠' => 'k', '𝙡' => 'l', '𝙢' => 'm', '𝙣' => 'n', '𝙤' => 'o', '𝙥' => 'p', '𝙦' => 'q', '𝙧' => 'r', '𝙨' => 's', '𝙩' => 't', '𝙪' => 'u', '𝙫' => 'v', '𝙬' => 'w', '𝙭' => 'x', '𝙮' => 'y', '𝙯' => 'z', '𝙰' => 'A', '𝙱' => 'B', '𝙲' => 'C', '𝙳' => 'D', '𝙴' => 'E', '𝙵' => 'F', '𝙶' => 'G', '𝙷' => 'H', '𝙸' => 'I', '𝙹' => 'J', '𝙺' => 'K', '𝙻' => 'L', '𝙼' => 'M', '𝙽' => 'N', '𝙾' => 'O', '𝙿' => 'P', '𝚀' => 'Q', '𝚁' => 'R', '𝚂' => 'S', '𝚃' => 'T', '𝚄' => 'U', '𝚅' => 'V', '𝚆' => 'W', '𝚇' => 'X', '𝚈' => 'Y', '𝚉' => 'Z', '𝚊' => 'a', '𝚋' => 'b', '𝚌' => 'c', '𝚍' => 'd', '𝚎' => 'e', '𝚏' => 'f', '𝚐' => 'g', '𝚑' => 'h', '𝚒' => 'i', '𝚓' => 'j', '𝚔' => 'k', '𝚕' => 'l', '𝚖' => 'm', '𝚗' => 'n', '𝚘' => 'o', '𝚙' => 'p', '𝚚' => 'q', '𝚛' => 'r', '𝚜' => 's', '𝚝' => 't', '𝚞' => 'u', '𝚟' => 'v', '𝚠' => 'w', '𝚡' => 'x', '𝚢' => 'y', '𝚣' => 'z', '𝚤' => 'ı', '𝚥' => 'ȷ', '𝚨' => 'Α', '𝚩' => 'Β', '𝚪' => 'Γ', '𝚫' => 'Δ', '𝚬' => 'Ε', '𝚭' => 'Ζ', '𝚮' => 'Η', '𝚯' => 'Θ', '𝚰' => 'Ι', '𝚱' => 'Κ', '𝚲' => 'Λ', '𝚳' => 'Μ', '𝚴' => 'Ν', '𝚵' => 'Ξ', '𝚶' => 'Ο', '𝚷' => 'Π', '𝚸' => 'Ρ', '𝚹' => 'Θ', '𝚺' => 'Σ', '𝚻' => 'Τ', '𝚼' => 'Υ', '𝚽' => 'Φ', '𝚾' => 'Χ', '𝚿' => 'Ψ', '𝛀' => 'Ω', '𝛁' => '∇', '𝛂' => 'α', '𝛃' => 'β', '𝛄' => 'γ', '𝛅' => 'δ', '𝛆' => 'ε', '𝛇' => 'ζ', '𝛈' => 'η', '𝛉' => 'θ', '𝛊' => 'ι', '𝛋' => 'κ', '𝛌' => 'λ', '𝛍' => 'μ', '𝛎' => 'ν', '𝛏' => 'ξ', '𝛐' => 'ο', '𝛑' => 'π', '𝛒' => 'ρ', '𝛓' => 'ς', '𝛔' => 'σ', '𝛕' => 'τ', '𝛖' => 'υ', '𝛗' => 'φ', '𝛘' => 'χ', '𝛙' => 'ψ', '𝛚' => 'ω', '𝛛' => '∂', '𝛜' => 'ε', '𝛝' => 'θ', '𝛞' => 'κ', '𝛟' => 'φ', '𝛠' => 'ρ', '𝛡' => 'π', '𝛢' => 'Α', '𝛣' => 'Β', '𝛤' => 'Γ', '𝛥' => 'Δ', '𝛦' => 'Ε', '𝛧' => 'Ζ', '𝛨' => 'Η', '𝛩' => 'Θ', '𝛪' => 'Ι', '𝛫' => 'Κ', '𝛬' => 'Λ', '𝛭' => 'Μ', '𝛮' => 'Ν', '𝛯' => 'Ξ', '𝛰' => 'Ο', '𝛱' => 'Π', '𝛲' => 'Ρ', '𝛳' => 'Θ', '𝛴' => 'Σ', '𝛵' => 'Τ', '𝛶' => 'Υ', '𝛷' => 'Φ', '𝛸' => 'Χ', '𝛹' => 'Ψ', '𝛺' => 'Ω', '𝛻' => '∇', '𝛼' => 'α', '𝛽' => 'β', '𝛾' => 'γ', '𝛿' => 'δ', '𝜀' => 'ε', '𝜁' => 'ζ', '𝜂' => 'η', '𝜃' => 'θ', '𝜄' => 'ι', '𝜅' => 'κ', '𝜆' => 'λ', '𝜇' => 'μ', '𝜈' => 'ν', '𝜉' => 'ξ', '𝜊' => 'ο', '𝜋' => 'π', '𝜌' => 'ρ', '𝜍' => 'ς', '𝜎' => 'σ', '𝜏' => 'τ', '𝜐' => 'υ', '𝜑' => 'φ', '𝜒' => 'χ', '𝜓' => 'ψ', '𝜔' => 'ω', '𝜕' => '∂', '𝜖' => 'ε', '𝜗' => 'θ', '𝜘' => 'κ', '𝜙' => 'φ', '𝜚' => 'ρ', '𝜛' => 'π', '𝜜' => 'Α', '𝜝' => 'Β', '𝜞' => 'Γ', '𝜟' => 'Δ', '𝜠' => 'Ε', '𝜡' => 'Ζ', '𝜢' => 'Η', '𝜣' => 'Θ', '𝜤' => 'Ι', '𝜥' => 'Κ', '𝜦' => 'Λ', '𝜧' => 'Μ', '𝜨' => 'Ν', '𝜩' => 'Ξ', '𝜪' => 'Ο', '𝜫' => 'Π', '𝜬' => 'Ρ', '𝜭' => 'Θ', '𝜮' => 'Σ', '𝜯' => 'Τ', '𝜰' => 'Υ', '𝜱' => 'Φ', '𝜲' => 'Χ', '𝜳' => 'Ψ', '𝜴' => 'Ω', '𝜵' => '∇', '𝜶' => 'α', '𝜷' => 'β', '𝜸' => 'γ', '𝜹' => 'δ', '𝜺' => 'ε', '𝜻' => 'ζ', '𝜼' => 'η', '𝜽' => 'θ', '𝜾' => 'ι', '𝜿' => 'κ', '𝝀' => 'λ', '𝝁' => 'μ', '𝝂' => 'ν', '𝝃' => 'ξ', '𝝄' => 'ο', '𝝅' => 'π', '𝝆' => 'ρ', '𝝇' => 'ς', '𝝈' => 'σ', '𝝉' => 'τ', '𝝊' => 'υ', '𝝋' => 'φ', '𝝌' => 'χ', '𝝍' => 'ψ', '𝝎' => 'ω', '𝝏' => '∂', '𝝐' => 'ε', '𝝑' => 'θ', '𝝒' => 'κ', '𝝓' => 'φ', '𝝔' => 'ρ', '𝝕' => 'π', '𝝖' => 'Α', '𝝗' => 'Β', '𝝘' => 'Γ', '𝝙' => 'Δ', '𝝚' => 'Ε', '𝝛' => 'Ζ', '𝝜' => 'Η', '𝝝' => 'Θ', '𝝞' => 'Ι', '𝝟' => 'Κ', '𝝠' => 'Λ', '𝝡' => 'Μ', '𝝢' => 'Ν', '𝝣' => 'Ξ', '𝝤' => 'Ο', '𝝥' => 'Π', '𝝦' => 'Ρ', '𝝧' => 'Θ', '𝝨' => 'Σ', '𝝩' => 'Τ', '𝝪' => 'Υ', '𝝫' => 'Φ', '𝝬' => 'Χ', '𝝭' => 'Ψ', '𝝮' => 'Ω', '𝝯' => '∇', '𝝰' => 'α', '𝝱' => 'β', '𝝲' => 'γ', '𝝳' => 'δ', '𝝴' => 'ε', '𝝵' => 'ζ', '𝝶' => 'η', '𝝷' => 'θ', '𝝸' => 'ι', '𝝹' => 'κ', '𝝺' => 'λ', '𝝻' => 'μ', '𝝼' => 'ν', '𝝽' => 'ξ', '𝝾' => 'ο', '𝝿' => 'π', '𝞀' => 'ρ', '𝞁' => 'ς', '𝞂' => 'σ', '𝞃' => 'τ', '𝞄' => 'υ', '𝞅' => 'φ', '𝞆' => 'χ', '𝞇' => 'ψ', '𝞈' => 'ω', '𝞉' => '∂', '𝞊' => 'ε', '𝞋' => 'θ', '𝞌' => 'κ', '𝞍' => 'φ', '𝞎' => 'ρ', '𝞏' => 'π', '𝞐' => 'Α', '𝞑' => 'Β', '𝞒' => 'Γ', '𝞓' => 'Δ', '𝞔' => 'Ε', '𝞕' => 'Ζ', '𝞖' => 'Η', '𝞗' => 'Θ', '𝞘' => 'Ι', '𝞙' => 'Κ', '𝞚' => 'Λ', '𝞛' => 'Μ', '𝞜' => 'Ν', '𝞝' => 'Ξ', '𝞞' => 'Ο', '𝞟' => 'Π', '𝞠' => 'Ρ', '𝞡' => 'Θ', '𝞢' => 'Σ', '𝞣' => 'Τ', '𝞤' => 'Υ', '𝞥' => 'Φ', '𝞦' => 'Χ', '𝞧' => 'Ψ', '𝞨' => 'Ω', '𝞩' => '∇', '𝞪' => 'α', '𝞫' => 'β', '𝞬' => 'γ', '𝞭' => 'δ', '𝞮' => 'ε', '𝞯' => 'ζ', '𝞰' => 'η', '𝞱' => 'θ', '𝞲' => 'ι', '𝞳' => 'κ', '𝞴' => 'λ', '𝞵' => 'μ', '𝞶' => 'ν', '𝞷' => 'ξ', '𝞸' => 'ο', '𝞹' => 'π', '𝞺' => 'ρ', '𝞻' => 'ς', '𝞼' => 'σ', '𝞽' => 'τ', '𝞾' => 'υ', '𝞿' => 'φ', '𝟀' => 'χ', '𝟁' => 'ψ', '𝟂' => 'ω', '𝟃' => '∂', '𝟄' => 'ε', '𝟅' => 'θ', '𝟆' => 'κ', '𝟇' => 'φ', '𝟈' => 'ρ', '𝟉' => 'π', '𝟊' => 'Ϝ', '𝟋' => 'ϝ', '𝟎' => '0', '𝟏' => '1', '𝟐' => '2', '𝟑' => '3', '𝟒' => '4', '𝟓' => '5', '𝟔' => '6', '𝟕' => '7', '𝟖' => '8', '𝟗' => '9', '𝟘' => '0', '𝟙' => '1', '𝟚' => '2', '𝟛' => '3', '𝟜' => '4', '𝟝' => '5', '𝟞' => '6', '𝟟' => '7', '𝟠' => '8', '𝟡' => '9', '𝟢' => '0', '𝟣' => '1', '𝟤' => '2', '𝟥' => '3', '𝟦' => '4', '𝟧' => '5', '𝟨' => '6', '𝟩' => '7', '𝟪' => '8', '𝟫' => '9', '𝟬' => '0', '𝟭' => '1', '𝟮' => '2', '𝟯' => '3', '𝟰' => '4', '𝟱' => '5', '𝟲' => '6', '𝟳' => '7', '𝟴' => '8', '𝟵' => '9', '𝟶' => '0', '𝟷' => '1', '𝟸' => '2', '𝟹' => '3', '𝟺' => '4', '𝟻' => '5', '𝟼' => '6', '𝟽' => '7', '𝟾' => '8', '𝟿' => '9', '𞸀' => 'ا', '𞸁' => 'ب', '𞸂' => 'ج', '𞸃' => 'د', '𞸅' => 'و', '𞸆' => 'ز', '𞸇' => 'ح', '𞸈' => 'ط', '𞸉' => 'ي', '𞸊' => 'ك', '𞸋' => 'ل', '𞸌' => 'م', '𞸍' => 'ن', '𞸎' => 'س', '𞸏' => 'ع', '𞸐' => 'ف', '𞸑' => 'ص', '𞸒' => 'ق', '𞸓' => 'ر', '𞸔' => 'ش', '𞸕' => 'ت', '𞸖' => 'ث', '𞸗' => 'خ', '𞸘' => 'ذ', '𞸙' => 'ض', '𞸚' => 'ظ', '𞸛' => 'غ', '𞸜' => 'ٮ', '𞸝' => 'ں', '𞸞' => 'ڡ', '𞸟' => 'ٯ', '𞸡' => 'ب', '𞸢' => 'ج', '𞸤' => 'ه', '𞸧' => 'ح', '𞸩' => 'ي', '𞸪' => 'ك', '𞸫' => 'ل', '𞸬' => 'م', '𞸭' => 'ن', '𞸮' => 'س', '𞸯' => 'ع', '𞸰' => 'ف', '𞸱' => 'ص', '𞸲' => 'ق', '𞸴' => 'ش', '𞸵' => 'ت', '𞸶' => 'ث', '𞸷' => 'خ', '𞸹' => 'ض', '𞸻' => 'غ', '𞹂' => 'ج', '𞹇' => 'ح', '𞹉' => 'ي', '𞹋' => 'ل', '𞹍' => 'ن', '𞹎' => 'س', '𞹏' => 'ع', '𞹑' => 'ص', '𞹒' => 'ق', '𞹔' => 'ش', '𞹗' => 'خ', '𞹙' => 'ض', '𞹛' => 'غ', '𞹝' => 'ں', '𞹟' => 'ٯ', '𞹡' => 'ب', '𞹢' => 'ج', '𞹤' => 'ه', '𞹧' => 'ح', '𞹨' => 'ط', '𞹩' => 'ي', '𞹪' => 'ك', '𞹬' => 'م', '𞹭' => 'ن', '𞹮' => 'س', '𞹯' => 'ع', '𞹰' => 'ف', '𞹱' => 'ص', '𞹲' => 'ق', '𞹴' => 'ش', '𞹵' => 'ت', '𞹶' => 'ث', '𞹷' => 'خ', '𞹹' => 'ض', '𞹺' => 'ظ', '𞹻' => 'غ', '𞹼' => 'ٮ', '𞹾' => 'ڡ', '𞺀' => 'ا', '𞺁' => 'ب', '𞺂' => 'ج', '𞺃' => 'د', '𞺄' => 'ه', '𞺅' => 'و', '𞺆' => 'ز', '𞺇' => 'ح', '𞺈' => 'ط', '𞺉' => 'ي', '𞺋' => 'ل', '𞺌' => 'م', '𞺍' => 'ن', '𞺎' => 'س', '𞺏' => 'ع', '𞺐' => 'ف', '𞺑' => 'ص', '𞺒' => 'ق', '𞺓' => 'ر', '𞺔' => 'ش', '𞺕' => 'ت', '𞺖' => 'ث', '𞺗' => 'خ', '𞺘' => 'ذ', '𞺙' => 'ض', '𞺚' => 'ظ', '𞺛' => 'غ', '𞺡' => 'ب', '𞺢' => 'ج', '𞺣' => 'د', '𞺥' => 'و', '𞺦' => 'ز', '𞺧' => 'ح', '𞺨' => 'ط', '𞺩' => 'ي', '𞺫' => 'ل', '𞺬' => 'م', '𞺭' => 'ن', '𞺮' => 'س', '𞺯' => 'ع', '𞺰' => 'ف', '𞺱' => 'ص', '𞺲' => 'ق', '𞺳' => 'ر', '𞺴' => 'ش', '𞺵' => 'ت', '𞺶' => 'ث', '𞺷' => 'خ', '𞺸' => 'ذ', '𞺹' => 'ض', '𞺺' => 'ظ', '𞺻' => 'غ', '🄀' => '0.', '🄁' => '0,', '🄂' => '1,', '🄃' => '2,', '🄄' => '3,', '🄅' => '4,', '🄆' => '5,', '🄇' => '6,', '🄈' => '7,', '🄉' => '8,', '🄊' => '9,', '🄐' => '(A)', '🄑' => '(B)', '🄒' => '(C)', '🄓' => '(D)', '🄔' => '(E)', '🄕' => '(F)', '🄖' => '(G)', '🄗' => '(H)', '🄘' => '(I)', '🄙' => '(J)', '🄚' => '(K)', '🄛' => '(L)', '🄜' => '(M)', '🄝' => '(N)', '🄞' => '(O)', '🄟' => '(P)', '🄠' => '(Q)', '🄡' => '(R)', '🄢' => '(S)', '🄣' => '(T)', '🄤' => '(U)', '🄥' => '(V)', '🄦' => '(W)', '🄧' => '(X)', '🄨' => '(Y)', '🄩' => '(Z)', '🄪' => '〔S〕', '🄫' => 'C', '🄬' => 'R', '🄭' => 'CD', '🄮' => 'WZ', '🄰' => 'A', '🄱' => 'B', '🄲' => 'C', '🄳' => 'D', '🄴' => 'E', '🄵' => 'F', '🄶' => 'G', '🄷' => 'H', '🄸' => 'I', '🄹' => 'J', '🄺' => 'K', '🄻' => 'L', '🄼' => 'M', '🄽' => 'N', '🄾' => 'O', '🄿' => 'P', '🅀' => 'Q', '🅁' => 'R', '🅂' => 'S', '🅃' => 'T', '🅄' => 'U', '🅅' => 'V', '🅆' => 'W', '🅇' => 'X', '🅈' => 'Y', '🅉' => 'Z', '🅊' => 'HV', '🅋' => 'MV', '🅌' => 'SD', '🅍' => 'SS', '🅎' => 'PPV', '🅏' => 'WC', '🅪' => 'MC', '🅫' => 'MD', '🅬' => 'MR', '🆐' => 'DJ', '🈀' => 'ほか', '🈁' => 'ココ', '🈂' => 'サ', '🈐' => '手', '🈑' => '字', '🈒' => '双', '🈓' => 'デ', '🈔' => '二', '🈕' => '多', '🈖' => '解', '🈗' => '天', '🈘' => '交', '🈙' => '映', '🈚' => '無', '🈛' => '料', '🈜' => '前', '🈝' => '後', '🈞' => '再', '🈟' => '新', '🈠' => '初', '🈡' => '終', '🈢' => '生', '🈣' => '販', '🈤' => '声', '🈥' => '吹', '🈦' => '演', '🈧' => '投', '🈨' => '捕', '🈩' => '一', '🈪' => '三', '🈫' => '遊', '🈬' => '左', '🈭' => '中', '🈮' => '右', '🈯' => '指', '🈰' => '走', '🈱' => '打', '🈲' => '禁', '🈳' => '空', '🈴' => '合', '🈵' => '満', '🈶' => '有', '🈷' => '月', '🈸' => '申', '🈹' => '割', '🈺' => '営', '🈻' => '配', '🉀' => '〔本〕', '🉁' => '〔三〕', '🉂' => '〔二〕', '🉃' => '〔安〕', '🉄' => '〔点〕', '🉅' => '〔打〕', '🉆' => '〔盗〕', '🉇' => '〔勝〕', '🉈' => '〔敗〕', '🉐' => '得', '🉑' => '可', '🯰' => '0', '🯱' => '1', '🯲' => '2', '🯳' => '3', '🯴' => '4', '🯵' => '5', '🯶' => '6', '🯷' => '7', '🯸' => '8', '🯹' => '9', ); PK!H~5vendor/symfony/polyfill-intl-normalizer/bootstrap.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Intl\Normalizer as p; if (!function_exists('normalizer_is_normalized')) { function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); } } if (!function_exists('normalizer_normalize')) { function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); } } PK! rT5vendor/symfony/polyfill-intl-normalizer/composer.jsonnu[{ "name": "symfony/polyfill-intl-normalizer", "type": "library", "description": "Symfony polyfill for intl's Normalizer class and related functions", "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "normalizer"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "suggest": { "ext-intl": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } PK!H,,/vendor/symfony/polyfill-intl-normalizer/LICENSEnu[Copyright (c) 2015-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!ud%d%6vendor/symfony/polyfill-intl-normalizer/Normalizer.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Intl\Normalizer; /** * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension. * * It has been validated with Unicode 6.3 Normalization Conformance Test. * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations. * * @author Nicolas Grekas * * @internal */ class Normalizer { public const FORM_D = \Normalizer::FORM_D; public const FORM_KD = \Normalizer::FORM_KD; public const FORM_C = \Normalizer::FORM_C; public const FORM_KC = \Normalizer::FORM_KC; public const NFD = \Normalizer::NFD; public const NFKD = \Normalizer::NFKD; public const NFC = \Normalizer::NFC; public const NFKC = \Normalizer::NFKC; private static $C; private static $D; private static $KD; private static $cC; private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; public static function isNormalized(string $s, int $form = self::FORM_C) { if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) { return false; } if (!isset($s[strspn($s, self::$ASCII)])) { return true; } if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) { return true; } return self::normalize($s, $form) === $s; } public static function normalize(string $s, int $form = self::FORM_C) { if (!preg_match('//u', $s)) { return false; } switch ($form) { case self::NFC: $C = true; $K = false; break; case self::NFD: $C = false; $K = false; break; case self::NFKC: $C = true; $K = true; break; case self::NFKD: $C = false; $K = true; break; default: if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) { return $s; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form'); } if ('' === $s) { return ''; } if ($K && null === self::$KD) { self::$KD = self::getData('compatibilityDecomposition'); } if (null === self::$D) { self::$D = self::getData('canonicalDecomposition'); self::$cC = self::getData('combiningClass'); } if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) { mb_internal_encoding('8bit'); } $r = self::decompose($s, $K); if ($C) { if (null === self::$C) { self::$C = self::getData('canonicalComposition'); } $r = self::recompose($r); } if (null !== $mbEncoding) { mb_internal_encoding($mbEncoding); } return $r; } private static function recompose($s) { $ASCII = self::$ASCII; $compMap = self::$C; $combClass = self::$cC; $ulenMask = self::$ulenMask; $result = $tail = ''; $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"]; $len = \strlen($s); $lastUchr = substr($s, 0, $i); $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0; while ($i < $len) { if ($s[$i] < "\x80") { // ASCII chars if ($tail) { $lastUchr .= $tail; $tail = ''; } if ($j = strspn($s, $ASCII, $i + 1)) { $lastUchr .= substr($s, $i, $j); $i += $j; } $result .= $lastUchr; $lastUchr = $s[$i]; $lastUcls = 0; ++$i; continue; } $ulen = $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr || $lastUcls) { // Table lookup and combining chars composition $ucls = $combClass[$uchr] ?? 0; if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) { $lastUchr = $compMap[$lastUchr.$uchr]; } elseif ($lastUcls = $ucls) { $tail .= $uchr; } else { if ($tail) { $lastUchr .= $tail; $tail = ''; } $result .= $lastUchr; $lastUchr = $uchr; } } else { // Hangul chars $L = \ord($lastUchr[2]) - 0x80; $V = \ord($uchr[2]) - 0xA1; $T = 0; $uchr = substr($s, $i + $ulen, 3); if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") { $T = \ord($uchr[2]) - 0xA7; 0 > $T && $T += 0x40; $ulen += 3; } $L = 0xAC00 + ($L * 21 + $V) * 28 + $T; $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F); } $i += $ulen; } return $result.$lastUchr.$tail; } private static function decompose($s, $c) { $result = ''; $ASCII = self::$ASCII; $decompMap = self::$D; $combClass = self::$cC; $ulenMask = self::$ulenMask; if ($c) { $compatMap = self::$KD; } $c = []; $i = 0; $len = \strlen($s); while ($i < $len) { if ($s[$i] < "\x80") { // ASCII chars if ($c) { ksort($c); $result .= implode('', $c); $c = []; } $j = 1 + strspn($s, $ASCII, $i + 1); $result .= substr($s, $i, $j); $i += $j; continue; } $ulen = $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) { // Table lookup if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) { $uchr = $j; $j = \strlen($uchr); $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"]; if ($ulen != $j) { // Put trailing chars in $s $j -= $ulen; $i -= $j; if (0 > $i) { $s = str_repeat(' ', -$i).$s; $len -= $i; $i = 0; } while ($j--) { $s[$i + $j] = $uchr[$ulen + $j]; } $uchr = substr($uchr, 0, $ulen); } } if (isset($combClass[$uchr])) { // Combining chars, for sorting if (!isset($c[$combClass[$uchr]])) { $c[$combClass[$uchr]] = ''; } $c[$combClass[$uchr]] .= $uchr; continue; } } else { // Hangul chars $uchr = unpack('C*', $uchr); $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80; $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588)) ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28)); if ($j %= 28) { $uchr .= $j < 25 ? ("\xE1\x86".\chr(0xA7 + $j)) : ("\xE1\x87".\chr(0x67 + $j)); } } if ($c) { ksort($c); $result .= implode('', $c); $c = []; } $result .= $uchr; } if ($c) { ksort($c); $result .= implode('', $c); } return $result; } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } } PK!+tK1vendor/symfony/polyfill-intl-normalizer/README.mdnu[Symfony Polyfill / Intl: Normalizer =================================== This component provides a fallback implementation for the [`Normalizer`](https://php.net/Normalizer) class provided by the [Intl](https://php.net/intl) extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). PK!|a a Bvendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.phpnu[ 'i̇', 'µ' => 'μ', 'ſ' => 's', 'ͅ' => 'ι', 'ς' => 'σ', 'ϐ' => 'β', 'ϑ' => 'θ', 'ϕ' => 'φ', 'ϖ' => 'π', 'ϰ' => 'κ', 'ϱ' => 'ρ', 'ϵ' => 'ε', 'ẛ' => 'ṡ', 'ι' => 'ι', 'ß' => 'ss', 'ʼn' => 'ʼn', 'ǰ' => 'ǰ', 'ΐ' => 'ΐ', 'ΰ' => 'ΰ', 'և' => 'եւ', 'ẖ' => 'ẖ', 'ẗ' => 'ẗ', 'ẘ' => 'ẘ', 'ẙ' => 'ẙ', 'ẚ' => 'aʾ', 'ẞ' => 'ss', 'ὐ' => 'ὐ', 'ὒ' => 'ὒ', 'ὔ' => 'ὔ', 'ὖ' => 'ὖ', 'ᾀ' => 'ἀι', 'ᾁ' => 'ἁι', 'ᾂ' => 'ἂι', 'ᾃ' => 'ἃι', 'ᾄ' => 'ἄι', 'ᾅ' => 'ἅι', 'ᾆ' => 'ἆι', 'ᾇ' => 'ἇι', 'ᾈ' => 'ἀι', 'ᾉ' => 'ἁι', 'ᾊ' => 'ἂι', 'ᾋ' => 'ἃι', 'ᾌ' => 'ἄι', 'ᾍ' => 'ἅι', 'ᾎ' => 'ἆι', 'ᾏ' => 'ἇι', 'ᾐ' => 'ἠι', 'ᾑ' => 'ἡι', 'ᾒ' => 'ἢι', 'ᾓ' => 'ἣι', 'ᾔ' => 'ἤι', 'ᾕ' => 'ἥι', 'ᾖ' => 'ἦι', 'ᾗ' => 'ἧι', 'ᾘ' => 'ἠι', 'ᾙ' => 'ἡι', 'ᾚ' => 'ἢι', 'ᾛ' => 'ἣι', 'ᾜ' => 'ἤι', 'ᾝ' => 'ἥι', 'ᾞ' => 'ἦι', 'ᾟ' => 'ἧι', 'ᾠ' => 'ὠι', 'ᾡ' => 'ὡι', 'ᾢ' => 'ὢι', 'ᾣ' => 'ὣι', 'ᾤ' => 'ὤι', 'ᾥ' => 'ὥι', 'ᾦ' => 'ὦι', 'ᾧ' => 'ὧι', 'ᾨ' => 'ὠι', 'ᾩ' => 'ὡι', 'ᾪ' => 'ὢι', 'ᾫ' => 'ὣι', 'ᾬ' => 'ὤι', 'ᾭ' => 'ὥι', 'ᾮ' => 'ὦι', 'ᾯ' => 'ὧι', 'ᾲ' => 'ὰι', 'ᾳ' => 'αι', 'ᾴ' => 'άι', 'ᾶ' => 'ᾶ', 'ᾷ' => 'ᾶι', 'ᾼ' => 'αι', 'ῂ' => 'ὴι', 'ῃ' => 'ηι', 'ῄ' => 'ήι', 'ῆ' => 'ῆ', 'ῇ' => 'ῆι', 'ῌ' => 'ηι', 'ῒ' => 'ῒ', 'ῖ' => 'ῖ', 'ῗ' => 'ῗ', 'ῢ' => 'ῢ', 'ῤ' => 'ῤ', 'ῦ' => 'ῦ', 'ῧ' => 'ῧ', 'ῲ' => 'ὼι', 'ῳ' => 'ωι', 'ῴ' => 'ώι', 'ῶ' => 'ῶ', 'ῷ' => 'ῶι', 'ῼ' => 'ωι', 'ff' => 'ff', 'fi' => 'fi', 'fl' => 'fl', 'ffi' => 'ffi', 'ffl' => 'ffl', 'ſt' => 'st', 'st' => 'st', 'ﬓ' => 'մն', 'ﬔ' => 'մե', 'ﬕ' => 'մի', 'ﬖ' => 'վն', 'ﬗ' => 'մխ', ]; PK!d__@vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.phpnu[ 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', 'À' => 'à', 'Á' => 'á', 'Â' => 'â', 'Ã' => 'ã', 'Ä' => 'ä', 'Å' => 'å', 'Æ' => 'æ', 'Ç' => 'ç', 'È' => 'è', 'É' => 'é', 'Ê' => 'ê', 'Ë' => 'ë', 'Ì' => 'ì', 'Í' => 'í', 'Î' => 'î', 'Ï' => 'ï', 'Ð' => 'ð', 'Ñ' => 'ñ', 'Ò' => 'ò', 'Ó' => 'ó', 'Ô' => 'ô', 'Õ' => 'õ', 'Ö' => 'ö', 'Ø' => 'ø', 'Ù' => 'ù', 'Ú' => 'ú', 'Û' => 'û', 'Ü' => 'ü', 'Ý' => 'ý', 'Þ' => 'þ', 'Ā' => 'ā', 'Ă' => 'ă', 'Ą' => 'ą', 'Ć' => 'ć', 'Ĉ' => 'ĉ', 'Ċ' => 'ċ', 'Č' => 'č', 'Ď' => 'ď', 'Đ' => 'đ', 'Ē' => 'ē', 'Ĕ' => 'ĕ', 'Ė' => 'ė', 'Ę' => 'ę', 'Ě' => 'ě', 'Ĝ' => 'ĝ', 'Ğ' => 'ğ', 'Ġ' => 'ġ', 'Ģ' => 'ģ', 'Ĥ' => 'ĥ', 'Ħ' => 'ħ', 'Ĩ' => 'ĩ', 'Ī' => 'ī', 'Ĭ' => 'ĭ', 'Į' => 'į', 'İ' => 'i̇', 'IJ' => 'ij', 'Ĵ' => 'ĵ', 'Ķ' => 'ķ', 'Ĺ' => 'ĺ', 'Ļ' => 'ļ', 'Ľ' => 'ľ', 'Ŀ' => 'ŀ', 'Ł' => 'ł', 'Ń' => 'ń', 'Ņ' => 'ņ', 'Ň' => 'ň', 'Ŋ' => 'ŋ', 'Ō' => 'ō', 'Ŏ' => 'ŏ', 'Ő' => 'ő', 'Œ' => 'œ', 'Ŕ' => 'ŕ', 'Ŗ' => 'ŗ', 'Ř' => 'ř', 'Ś' => 'ś', 'Ŝ' => 'ŝ', 'Ş' => 'ş', 'Š' => 'š', 'Ţ' => 'ţ', 'Ť' => 'ť', 'Ŧ' => 'ŧ', 'Ũ' => 'ũ', 'Ū' => 'ū', 'Ŭ' => 'ŭ', 'Ů' => 'ů', 'Ű' => 'ű', 'Ų' => 'ų', 'Ŵ' => 'ŵ', 'Ŷ' => 'ŷ', 'Ÿ' => 'ÿ', 'Ź' => 'ź', 'Ż' => 'ż', 'Ž' => 'ž', 'Ɓ' => 'ɓ', 'Ƃ' => 'ƃ', 'Ƅ' => 'ƅ', 'Ɔ' => 'ɔ', 'Ƈ' => 'ƈ', 'Ɖ' => 'ɖ', 'Ɗ' => 'ɗ', 'Ƌ' => 'ƌ', 'Ǝ' => 'ǝ', 'Ə' => 'ə', 'Ɛ' => 'ɛ', 'Ƒ' => 'ƒ', 'Ɠ' => 'ɠ', 'Ɣ' => 'ɣ', 'Ɩ' => 'ɩ', 'Ɨ' => 'ɨ', 'Ƙ' => 'ƙ', 'Ɯ' => 'ɯ', 'Ɲ' => 'ɲ', 'Ɵ' => 'ɵ', 'Ơ' => 'ơ', 'Ƣ' => 'ƣ', 'Ƥ' => 'ƥ', 'Ʀ' => 'ʀ', 'Ƨ' => 'ƨ', 'Ʃ' => 'ʃ', 'Ƭ' => 'ƭ', 'Ʈ' => 'ʈ', 'Ư' => 'ư', 'Ʊ' => 'ʊ', 'Ʋ' => 'ʋ', 'Ƴ' => 'ƴ', 'Ƶ' => 'ƶ', 'Ʒ' => 'ʒ', 'Ƹ' => 'ƹ', 'Ƽ' => 'ƽ', 'DŽ' => 'dž', 'Dž' => 'dž', 'LJ' => 'lj', 'Lj' => 'lj', 'NJ' => 'nj', 'Nj' => 'nj', 'Ǎ' => 'ǎ', 'Ǐ' => 'ǐ', 'Ǒ' => 'ǒ', 'Ǔ' => 'ǔ', 'Ǖ' => 'ǖ', 'Ǘ' => 'ǘ', 'Ǚ' => 'ǚ', 'Ǜ' => 'ǜ', 'Ǟ' => 'ǟ', 'Ǡ' => 'ǡ', 'Ǣ' => 'ǣ', 'Ǥ' => 'ǥ', 'Ǧ' => 'ǧ', 'Ǩ' => 'ǩ', 'Ǫ' => 'ǫ', 'Ǭ' => 'ǭ', 'Ǯ' => 'ǯ', 'DZ' => 'dz', 'Dz' => 'dz', 'Ǵ' => 'ǵ', 'Ƕ' => 'ƕ', 'Ƿ' => 'ƿ', 'Ǹ' => 'ǹ', 'Ǻ' => 'ǻ', 'Ǽ' => 'ǽ', 'Ǿ' => 'ǿ', 'Ȁ' => 'ȁ', 'Ȃ' => 'ȃ', 'Ȅ' => 'ȅ', 'Ȇ' => 'ȇ', 'Ȉ' => 'ȉ', 'Ȋ' => 'ȋ', 'Ȍ' => 'ȍ', 'Ȏ' => 'ȏ', 'Ȑ' => 'ȑ', 'Ȓ' => 'ȓ', 'Ȕ' => 'ȕ', 'Ȗ' => 'ȗ', 'Ș' => 'ș', 'Ț' => 'ț', 'Ȝ' => 'ȝ', 'Ȟ' => 'ȟ', 'Ƞ' => 'ƞ', 'Ȣ' => 'ȣ', 'Ȥ' => 'ȥ', 'Ȧ' => 'ȧ', 'Ȩ' => 'ȩ', 'Ȫ' => 'ȫ', 'Ȭ' => 'ȭ', 'Ȯ' => 'ȯ', 'Ȱ' => 'ȱ', 'Ȳ' => 'ȳ', 'Ⱥ' => 'ⱥ', 'Ȼ' => 'ȼ', 'Ƚ' => 'ƚ', 'Ⱦ' => 'ⱦ', 'Ɂ' => 'ɂ', 'Ƀ' => 'ƀ', 'Ʉ' => 'ʉ', 'Ʌ' => 'ʌ', 'Ɇ' => 'ɇ', 'Ɉ' => 'ɉ', 'Ɋ' => 'ɋ', 'Ɍ' => 'ɍ', 'Ɏ' => 'ɏ', 'Ͱ' => 'ͱ', 'Ͳ' => 'ͳ', 'Ͷ' => 'ͷ', 'Ϳ' => 'ϳ', 'Ά' => 'ά', 'Έ' => 'έ', 'Ή' => 'ή', 'Ί' => 'ί', 'Ό' => 'ό', 'Ύ' => 'ύ', 'Ώ' => 'ώ', 'Α' => 'α', 'Β' => 'β', 'Γ' => 'γ', 'Δ' => 'δ', 'Ε' => 'ε', 'Ζ' => 'ζ', 'Η' => 'η', 'Θ' => 'θ', 'Ι' => 'ι', 'Κ' => 'κ', 'Λ' => 'λ', 'Μ' => 'μ', 'Ν' => 'ν', 'Ξ' => 'ξ', 'Ο' => 'ο', 'Π' => 'π', 'Ρ' => 'ρ', 'Σ' => 'σ', 'Τ' => 'τ', 'Υ' => 'υ', 'Φ' => 'φ', 'Χ' => 'χ', 'Ψ' => 'ψ', 'Ω' => 'ω', 'Ϊ' => 'ϊ', 'Ϋ' => 'ϋ', 'Ϗ' => 'ϗ', 'Ϙ' => 'ϙ', 'Ϛ' => 'ϛ', 'Ϝ' => 'ϝ', 'Ϟ' => 'ϟ', 'Ϡ' => 'ϡ', 'Ϣ' => 'ϣ', 'Ϥ' => 'ϥ', 'Ϧ' => 'ϧ', 'Ϩ' => 'ϩ', 'Ϫ' => 'ϫ', 'Ϭ' => 'ϭ', 'Ϯ' => 'ϯ', 'ϴ' => 'θ', 'Ϸ' => 'ϸ', 'Ϲ' => 'ϲ', 'Ϻ' => 'ϻ', 'Ͻ' => 'ͻ', 'Ͼ' => 'ͼ', 'Ͽ' => 'ͽ', 'Ѐ' => 'ѐ', 'Ё' => 'ё', 'Ђ' => 'ђ', 'Ѓ' => 'ѓ', 'Є' => 'є', 'Ѕ' => 'ѕ', 'І' => 'і', 'Ї' => 'ї', 'Ј' => 'ј', 'Љ' => 'љ', 'Њ' => 'њ', 'Ћ' => 'ћ', 'Ќ' => 'ќ', 'Ѝ' => 'ѝ', 'Ў' => 'ў', 'Џ' => 'џ', 'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', 'Е' => 'е', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и', 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н', 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', 'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь', 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я', 'Ѡ' => 'ѡ', 'Ѣ' => 'ѣ', 'Ѥ' => 'ѥ', 'Ѧ' => 'ѧ', 'Ѩ' => 'ѩ', 'Ѫ' => 'ѫ', 'Ѭ' => 'ѭ', 'Ѯ' => 'ѯ', 'Ѱ' => 'ѱ', 'Ѳ' => 'ѳ', 'Ѵ' => 'ѵ', 'Ѷ' => 'ѷ', 'Ѹ' => 'ѹ', 'Ѻ' => 'ѻ', 'Ѽ' => 'ѽ', 'Ѿ' => 'ѿ', 'Ҁ' => 'ҁ', 'Ҋ' => 'ҋ', 'Ҍ' => 'ҍ', 'Ҏ' => 'ҏ', 'Ґ' => 'ґ', 'Ғ' => 'ғ', 'Ҕ' => 'ҕ', 'Җ' => 'җ', 'Ҙ' => 'ҙ', 'Қ' => 'қ', 'Ҝ' => 'ҝ', 'Ҟ' => 'ҟ', 'Ҡ' => 'ҡ', 'Ң' => 'ң', 'Ҥ' => 'ҥ', 'Ҧ' => 'ҧ', 'Ҩ' => 'ҩ', 'Ҫ' => 'ҫ', 'Ҭ' => 'ҭ', 'Ү' => 'ү', 'Ұ' => 'ұ', 'Ҳ' => 'ҳ', 'Ҵ' => 'ҵ', 'Ҷ' => 'ҷ', 'Ҹ' => 'ҹ', 'Һ' => 'һ', 'Ҽ' => 'ҽ', 'Ҿ' => 'ҿ', 'Ӏ' => 'ӏ', 'Ӂ' => 'ӂ', 'Ӄ' => 'ӄ', 'Ӆ' => 'ӆ', 'Ӈ' => 'ӈ', 'Ӊ' => 'ӊ', 'Ӌ' => 'ӌ', 'Ӎ' => 'ӎ', 'Ӑ' => 'ӑ', 'Ӓ' => 'ӓ', 'Ӕ' => 'ӕ', 'Ӗ' => 'ӗ', 'Ә' => 'ә', 'Ӛ' => 'ӛ', 'Ӝ' => 'ӝ', 'Ӟ' => 'ӟ', 'Ӡ' => 'ӡ', 'Ӣ' => 'ӣ', 'Ӥ' => 'ӥ', 'Ӧ' => 'ӧ', 'Ө' => 'ө', 'Ӫ' => 'ӫ', 'Ӭ' => 'ӭ', 'Ӯ' => 'ӯ', 'Ӱ' => 'ӱ', 'Ӳ' => 'ӳ', 'Ӵ' => 'ӵ', 'Ӷ' => 'ӷ', 'Ӹ' => 'ӹ', 'Ӻ' => 'ӻ', 'Ӽ' => 'ӽ', 'Ӿ' => 'ӿ', 'Ԁ' => 'ԁ', 'Ԃ' => 'ԃ', 'Ԅ' => 'ԅ', 'Ԇ' => 'ԇ', 'Ԉ' => 'ԉ', 'Ԋ' => 'ԋ', 'Ԍ' => 'ԍ', 'Ԏ' => 'ԏ', 'Ԑ' => 'ԑ', 'Ԓ' => 'ԓ', 'Ԕ' => 'ԕ', 'Ԗ' => 'ԗ', 'Ԙ' => 'ԙ', 'Ԛ' => 'ԛ', 'Ԝ' => 'ԝ', 'Ԟ' => 'ԟ', 'Ԡ' => 'ԡ', 'Ԣ' => 'ԣ', 'Ԥ' => 'ԥ', 'Ԧ' => 'ԧ', 'Ԩ' => 'ԩ', 'Ԫ' => 'ԫ', 'Ԭ' => 'ԭ', 'Ԯ' => 'ԯ', 'Ա' => 'ա', 'Բ' => 'բ', 'Գ' => 'գ', 'Դ' => 'դ', 'Ե' => 'ե', 'Զ' => 'զ', 'Է' => 'է', 'Ը' => 'ը', 'Թ' => 'թ', 'Ժ' => 'ժ', 'Ի' => 'ի', 'Լ' => 'լ', 'Խ' => 'խ', 'Ծ' => 'ծ', 'Կ' => 'կ', 'Հ' => 'հ', 'Ձ' => 'ձ', 'Ղ' => 'ղ', 'Ճ' => 'ճ', 'Մ' => 'մ', 'Յ' => 'յ', 'Ն' => 'ն', 'Շ' => 'շ', 'Ո' => 'ո', 'Չ' => 'չ', 'Պ' => 'պ', 'Ջ' => 'ջ', 'Ռ' => 'ռ', 'Ս' => 'ս', 'Վ' => 'վ', 'Տ' => 'տ', 'Ր' => 'ր', 'Ց' => 'ց', 'Ւ' => 'ւ', 'Փ' => 'փ', 'Ք' => 'ք', 'Օ' => 'օ', 'Ֆ' => 'ֆ', 'Ⴀ' => 'ⴀ', 'Ⴁ' => 'ⴁ', 'Ⴂ' => 'ⴂ', 'Ⴃ' => 'ⴃ', 'Ⴄ' => 'ⴄ', 'Ⴅ' => 'ⴅ', 'Ⴆ' => 'ⴆ', 'Ⴇ' => 'ⴇ', 'Ⴈ' => 'ⴈ', 'Ⴉ' => 'ⴉ', 'Ⴊ' => 'ⴊ', 'Ⴋ' => 'ⴋ', 'Ⴌ' => 'ⴌ', 'Ⴍ' => 'ⴍ', 'Ⴎ' => 'ⴎ', 'Ⴏ' => 'ⴏ', 'Ⴐ' => 'ⴐ', 'Ⴑ' => 'ⴑ', 'Ⴒ' => 'ⴒ', 'Ⴓ' => 'ⴓ', 'Ⴔ' => 'ⴔ', 'Ⴕ' => 'ⴕ', 'Ⴖ' => 'ⴖ', 'Ⴗ' => 'ⴗ', 'Ⴘ' => 'ⴘ', 'Ⴙ' => 'ⴙ', 'Ⴚ' => 'ⴚ', 'Ⴛ' => 'ⴛ', 'Ⴜ' => 'ⴜ', 'Ⴝ' => 'ⴝ', 'Ⴞ' => 'ⴞ', 'Ⴟ' => 'ⴟ', 'Ⴠ' => 'ⴠ', 'Ⴡ' => 'ⴡ', 'Ⴢ' => 'ⴢ', 'Ⴣ' => 'ⴣ', 'Ⴤ' => 'ⴤ', 'Ⴥ' => 'ⴥ', 'Ⴧ' => 'ⴧ', 'Ⴭ' => 'ⴭ', 'Ꭰ' => 'ꭰ', 'Ꭱ' => 'ꭱ', 'Ꭲ' => 'ꭲ', 'Ꭳ' => 'ꭳ', 'Ꭴ' => 'ꭴ', 'Ꭵ' => 'ꭵ', 'Ꭶ' => 'ꭶ', 'Ꭷ' => 'ꭷ', 'Ꭸ' => 'ꭸ', 'Ꭹ' => 'ꭹ', 'Ꭺ' => 'ꭺ', 'Ꭻ' => 'ꭻ', 'Ꭼ' => 'ꭼ', 'Ꭽ' => 'ꭽ', 'Ꭾ' => 'ꭾ', 'Ꭿ' => 'ꭿ', 'Ꮀ' => 'ꮀ', 'Ꮁ' => 'ꮁ', 'Ꮂ' => 'ꮂ', 'Ꮃ' => 'ꮃ', 'Ꮄ' => 'ꮄ', 'Ꮅ' => 'ꮅ', 'Ꮆ' => 'ꮆ', 'Ꮇ' => 'ꮇ', 'Ꮈ' => 'ꮈ', 'Ꮉ' => 'ꮉ', 'Ꮊ' => 'ꮊ', 'Ꮋ' => 'ꮋ', 'Ꮌ' => 'ꮌ', 'Ꮍ' => 'ꮍ', 'Ꮎ' => 'ꮎ', 'Ꮏ' => 'ꮏ', 'Ꮐ' => 'ꮐ', 'Ꮑ' => 'ꮑ', 'Ꮒ' => 'ꮒ', 'Ꮓ' => 'ꮓ', 'Ꮔ' => 'ꮔ', 'Ꮕ' => 'ꮕ', 'Ꮖ' => 'ꮖ', 'Ꮗ' => 'ꮗ', 'Ꮘ' => 'ꮘ', 'Ꮙ' => 'ꮙ', 'Ꮚ' => 'ꮚ', 'Ꮛ' => 'ꮛ', 'Ꮜ' => 'ꮜ', 'Ꮝ' => 'ꮝ', 'Ꮞ' => 'ꮞ', 'Ꮟ' => 'ꮟ', 'Ꮠ' => 'ꮠ', 'Ꮡ' => 'ꮡ', 'Ꮢ' => 'ꮢ', 'Ꮣ' => 'ꮣ', 'Ꮤ' => 'ꮤ', 'Ꮥ' => 'ꮥ', 'Ꮦ' => 'ꮦ', 'Ꮧ' => 'ꮧ', 'Ꮨ' => 'ꮨ', 'Ꮩ' => 'ꮩ', 'Ꮪ' => 'ꮪ', 'Ꮫ' => 'ꮫ', 'Ꮬ' => 'ꮬ', 'Ꮭ' => 'ꮭ', 'Ꮮ' => 'ꮮ', 'Ꮯ' => 'ꮯ', 'Ꮰ' => 'ꮰ', 'Ꮱ' => 'ꮱ', 'Ꮲ' => 'ꮲ', 'Ꮳ' => 'ꮳ', 'Ꮴ' => 'ꮴ', 'Ꮵ' => 'ꮵ', 'Ꮶ' => 'ꮶ', 'Ꮷ' => 'ꮷ', 'Ꮸ' => 'ꮸ', 'Ꮹ' => 'ꮹ', 'Ꮺ' => 'ꮺ', 'Ꮻ' => 'ꮻ', 'Ꮼ' => 'ꮼ', 'Ꮽ' => 'ꮽ', 'Ꮾ' => 'ꮾ', 'Ꮿ' => 'ꮿ', 'Ᏸ' => 'ᏸ', 'Ᏹ' => 'ᏹ', 'Ᏺ' => 'ᏺ', 'Ᏻ' => 'ᏻ', 'Ᏼ' => 'ᏼ', 'Ᏽ' => 'ᏽ', 'Ა' => 'ა', 'Ბ' => 'ბ', 'Გ' => 'გ', 'Დ' => 'დ', 'Ე' => 'ე', 'Ვ' => 'ვ', 'Ზ' => 'ზ', 'Თ' => 'თ', 'Ი' => 'ი', 'Კ' => 'კ', 'Ლ' => 'ლ', 'Მ' => 'მ', 'Ნ' => 'ნ', 'Ო' => 'ო', 'Პ' => 'პ', 'Ჟ' => 'ჟ', 'Რ' => 'რ', 'Ს' => 'ს', 'Ტ' => 'ტ', 'Უ' => 'უ', 'Ფ' => 'ფ', 'Ქ' => 'ქ', 'Ღ' => 'ღ', 'Ყ' => 'ყ', 'Შ' => 'შ', 'Ჩ' => 'ჩ', 'Ც' => 'ც', 'Ძ' => 'ძ', 'Წ' => 'წ', 'Ჭ' => 'ჭ', 'Ხ' => 'ხ', 'Ჯ' => 'ჯ', 'Ჰ' => 'ჰ', 'Ჱ' => 'ჱ', 'Ჲ' => 'ჲ', 'Ჳ' => 'ჳ', 'Ჴ' => 'ჴ', 'Ჵ' => 'ჵ', 'Ჶ' => 'ჶ', 'Ჷ' => 'ჷ', 'Ჸ' => 'ჸ', 'Ჹ' => 'ჹ', 'Ჺ' => 'ჺ', 'Ჽ' => 'ჽ', 'Ჾ' => 'ჾ', 'Ჿ' => 'ჿ', 'Ḁ' => 'ḁ', 'Ḃ' => 'ḃ', 'Ḅ' => 'ḅ', 'Ḇ' => 'ḇ', 'Ḉ' => 'ḉ', 'Ḋ' => 'ḋ', 'Ḍ' => 'ḍ', 'Ḏ' => 'ḏ', 'Ḑ' => 'ḑ', 'Ḓ' => 'ḓ', 'Ḕ' => 'ḕ', 'Ḗ' => 'ḗ', 'Ḙ' => 'ḙ', 'Ḛ' => 'ḛ', 'Ḝ' => 'ḝ', 'Ḟ' => 'ḟ', 'Ḡ' => 'ḡ', 'Ḣ' => 'ḣ', 'Ḥ' => 'ḥ', 'Ḧ' => 'ḧ', 'Ḩ' => 'ḩ', 'Ḫ' => 'ḫ', 'Ḭ' => 'ḭ', 'Ḯ' => 'ḯ', 'Ḱ' => 'ḱ', 'Ḳ' => 'ḳ', 'Ḵ' => 'ḵ', 'Ḷ' => 'ḷ', 'Ḹ' => 'ḹ', 'Ḻ' => 'ḻ', 'Ḽ' => 'ḽ', 'Ḿ' => 'ḿ', 'Ṁ' => 'ṁ', 'Ṃ' => 'ṃ', 'Ṅ' => 'ṅ', 'Ṇ' => 'ṇ', 'Ṉ' => 'ṉ', 'Ṋ' => 'ṋ', 'Ṍ' => 'ṍ', 'Ṏ' => 'ṏ', 'Ṑ' => 'ṑ', 'Ṓ' => 'ṓ', 'Ṕ' => 'ṕ', 'Ṗ' => 'ṗ', 'Ṙ' => 'ṙ', 'Ṛ' => 'ṛ', 'Ṝ' => 'ṝ', 'Ṟ' => 'ṟ', 'Ṡ' => 'ṡ', 'Ṣ' => 'ṣ', 'Ṥ' => 'ṥ', 'Ṧ' => 'ṧ', 'Ṩ' => 'ṩ', 'Ṫ' => 'ṫ', 'Ṭ' => 'ṭ', 'Ṯ' => 'ṯ', 'Ṱ' => 'ṱ', 'Ṳ' => 'ṳ', 'Ṵ' => 'ṵ', 'Ṷ' => 'ṷ', 'Ṹ' => 'ṹ', 'Ṻ' => 'ṻ', 'Ṽ' => 'ṽ', 'Ṿ' => 'ṿ', 'Ẁ' => 'ẁ', 'Ẃ' => 'ẃ', 'Ẅ' => 'ẅ', 'Ẇ' => 'ẇ', 'Ẉ' => 'ẉ', 'Ẋ' => 'ẋ', 'Ẍ' => 'ẍ', 'Ẏ' => 'ẏ', 'Ẑ' => 'ẑ', 'Ẓ' => 'ẓ', 'Ẕ' => 'ẕ', 'ẞ' => 'ß', 'Ạ' => 'ạ', 'Ả' => 'ả', 'Ấ' => 'ấ', 'Ầ' => 'ầ', 'Ẩ' => 'ẩ', 'Ẫ' => 'ẫ', 'Ậ' => 'ậ', 'Ắ' => 'ắ', 'Ằ' => 'ằ', 'Ẳ' => 'ẳ', 'Ẵ' => 'ẵ', 'Ặ' => 'ặ', 'Ẹ' => 'ẹ', 'Ẻ' => 'ẻ', 'Ẽ' => 'ẽ', 'Ế' => 'ế', 'Ề' => 'ề', 'Ể' => 'ể', 'Ễ' => 'ễ', 'Ệ' => 'ệ', 'Ỉ' => 'ỉ', 'Ị' => 'ị', 'Ọ' => 'ọ', 'Ỏ' => 'ỏ', 'Ố' => 'ố', 'Ồ' => 'ồ', 'Ổ' => 'ổ', 'Ỗ' => 'ỗ', 'Ộ' => 'ộ', 'Ớ' => 'ớ', 'Ờ' => 'ờ', 'Ở' => 'ở', 'Ỡ' => 'ỡ', 'Ợ' => 'ợ', 'Ụ' => 'ụ', 'Ủ' => 'ủ', 'Ứ' => 'ứ', 'Ừ' => 'ừ', 'Ử' => 'ử', 'Ữ' => 'ữ', 'Ự' => 'ự', 'Ỳ' => 'ỳ', 'Ỵ' => 'ỵ', 'Ỷ' => 'ỷ', 'Ỹ' => 'ỹ', 'Ỻ' => 'ỻ', 'Ỽ' => 'ỽ', 'Ỿ' => 'ỿ', 'Ἀ' => 'ἀ', 'Ἁ' => 'ἁ', 'Ἂ' => 'ἂ', 'Ἃ' => 'ἃ', 'Ἄ' => 'ἄ', 'Ἅ' => 'ἅ', 'Ἆ' => 'ἆ', 'Ἇ' => 'ἇ', 'Ἐ' => 'ἐ', 'Ἑ' => 'ἑ', 'Ἒ' => 'ἒ', 'Ἓ' => 'ἓ', 'Ἔ' => 'ἔ', 'Ἕ' => 'ἕ', 'Ἠ' => 'ἠ', 'Ἡ' => 'ἡ', 'Ἢ' => 'ἢ', 'Ἣ' => 'ἣ', 'Ἤ' => 'ἤ', 'Ἥ' => 'ἥ', 'Ἦ' => 'ἦ', 'Ἧ' => 'ἧ', 'Ἰ' => 'ἰ', 'Ἱ' => 'ἱ', 'Ἲ' => 'ἲ', 'Ἳ' => 'ἳ', 'Ἴ' => 'ἴ', 'Ἵ' => 'ἵ', 'Ἶ' => 'ἶ', 'Ἷ' => 'ἷ', 'Ὀ' => 'ὀ', 'Ὁ' => 'ὁ', 'Ὂ' => 'ὂ', 'Ὃ' => 'ὃ', 'Ὄ' => 'ὄ', 'Ὅ' => 'ὅ', 'Ὑ' => 'ὑ', 'Ὓ' => 'ὓ', 'Ὕ' => 'ὕ', 'Ὗ' => 'ὗ', 'Ὠ' => 'ὠ', 'Ὡ' => 'ὡ', 'Ὢ' => 'ὢ', 'Ὣ' => 'ὣ', 'Ὤ' => 'ὤ', 'Ὥ' => 'ὥ', 'Ὦ' => 'ὦ', 'Ὧ' => 'ὧ', 'ᾈ' => 'ᾀ', 'ᾉ' => 'ᾁ', 'ᾊ' => 'ᾂ', 'ᾋ' => 'ᾃ', 'ᾌ' => 'ᾄ', 'ᾍ' => 'ᾅ', 'ᾎ' => 'ᾆ', 'ᾏ' => 'ᾇ', 'ᾘ' => 'ᾐ', 'ᾙ' => 'ᾑ', 'ᾚ' => 'ᾒ', 'ᾛ' => 'ᾓ', 'ᾜ' => 'ᾔ', 'ᾝ' => 'ᾕ', 'ᾞ' => 'ᾖ', 'ᾟ' => 'ᾗ', 'ᾨ' => 'ᾠ', 'ᾩ' => 'ᾡ', 'ᾪ' => 'ᾢ', 'ᾫ' => 'ᾣ', 'ᾬ' => 'ᾤ', 'ᾭ' => 'ᾥ', 'ᾮ' => 'ᾦ', 'ᾯ' => 'ᾧ', 'Ᾰ' => 'ᾰ', 'Ᾱ' => 'ᾱ', 'Ὰ' => 'ὰ', 'Ά' => 'ά', 'ᾼ' => 'ᾳ', 'Ὲ' => 'ὲ', 'Έ' => 'έ', 'Ὴ' => 'ὴ', 'Ή' => 'ή', 'ῌ' => 'ῃ', 'Ῐ' => 'ῐ', 'Ῑ' => 'ῑ', 'Ὶ' => 'ὶ', 'Ί' => 'ί', 'Ῠ' => 'ῠ', 'Ῡ' => 'ῡ', 'Ὺ' => 'ὺ', 'Ύ' => 'ύ', 'Ῥ' => 'ῥ', 'Ὸ' => 'ὸ', 'Ό' => 'ό', 'Ὼ' => 'ὼ', 'Ώ' => 'ώ', 'ῼ' => 'ῳ', 'Ω' => 'ω', 'K' => 'k', 'Å' => 'å', 'Ⅎ' => 'ⅎ', 'Ⅰ' => 'ⅰ', 'Ⅱ' => 'ⅱ', 'Ⅲ' => 'ⅲ', 'Ⅳ' => 'ⅳ', 'Ⅴ' => 'ⅴ', 'Ⅵ' => 'ⅵ', 'Ⅶ' => 'ⅶ', 'Ⅷ' => 'ⅷ', 'Ⅸ' => 'ⅸ', 'Ⅹ' => 'ⅹ', 'Ⅺ' => 'ⅺ', 'Ⅻ' => 'ⅻ', 'Ⅼ' => 'ⅼ', 'Ⅽ' => 'ⅽ', 'Ⅾ' => 'ⅾ', 'Ⅿ' => 'ⅿ', 'Ↄ' => 'ↄ', 'Ⓐ' => 'ⓐ', 'Ⓑ' => 'ⓑ', 'Ⓒ' => 'ⓒ', 'Ⓓ' => 'ⓓ', 'Ⓔ' => 'ⓔ', 'Ⓕ' => 'ⓕ', 'Ⓖ' => 'ⓖ', 'Ⓗ' => 'ⓗ', 'Ⓘ' => 'ⓘ', 'Ⓙ' => 'ⓙ', 'Ⓚ' => 'ⓚ', 'Ⓛ' => 'ⓛ', 'Ⓜ' => 'ⓜ', 'Ⓝ' => 'ⓝ', 'Ⓞ' => 'ⓞ', 'Ⓟ' => 'ⓟ', 'Ⓠ' => 'ⓠ', 'Ⓡ' => 'ⓡ', 'Ⓢ' => 'ⓢ', 'Ⓣ' => 'ⓣ', 'Ⓤ' => 'ⓤ', 'Ⓥ' => 'ⓥ', 'Ⓦ' => 'ⓦ', 'Ⓧ' => 'ⓧ', 'Ⓨ' => 'ⓨ', 'Ⓩ' => 'ⓩ', 'Ⰰ' => 'ⰰ', 'Ⰱ' => 'ⰱ', 'Ⰲ' => 'ⰲ', 'Ⰳ' => 'ⰳ', 'Ⰴ' => 'ⰴ', 'Ⰵ' => 'ⰵ', 'Ⰶ' => 'ⰶ', 'Ⰷ' => 'ⰷ', 'Ⰸ' => 'ⰸ', 'Ⰹ' => 'ⰹ', 'Ⰺ' => 'ⰺ', 'Ⰻ' => 'ⰻ', 'Ⰼ' => 'ⰼ', 'Ⰽ' => 'ⰽ', 'Ⰾ' => 'ⰾ', 'Ⰿ' => 'ⰿ', 'Ⱀ' => 'ⱀ', 'Ⱁ' => 'ⱁ', 'Ⱂ' => 'ⱂ', 'Ⱃ' => 'ⱃ', 'Ⱄ' => 'ⱄ', 'Ⱅ' => 'ⱅ', 'Ⱆ' => 'ⱆ', 'Ⱇ' => 'ⱇ', 'Ⱈ' => 'ⱈ', 'Ⱉ' => 'ⱉ', 'Ⱊ' => 'ⱊ', 'Ⱋ' => 'ⱋ', 'Ⱌ' => 'ⱌ', 'Ⱍ' => 'ⱍ', 'Ⱎ' => 'ⱎ', 'Ⱏ' => 'ⱏ', 'Ⱐ' => 'ⱐ', 'Ⱑ' => 'ⱑ', 'Ⱒ' => 'ⱒ', 'Ⱓ' => 'ⱓ', 'Ⱔ' => 'ⱔ', 'Ⱕ' => 'ⱕ', 'Ⱖ' => 'ⱖ', 'Ⱗ' => 'ⱗ', 'Ⱘ' => 'ⱘ', 'Ⱙ' => 'ⱙ', 'Ⱚ' => 'ⱚ', 'Ⱛ' => 'ⱛ', 'Ⱜ' => 'ⱜ', 'Ⱝ' => 'ⱝ', 'Ⱞ' => 'ⱞ', 'Ⱡ' => 'ⱡ', 'Ɫ' => 'ɫ', 'Ᵽ' => 'ᵽ', 'Ɽ' => 'ɽ', 'Ⱨ' => 'ⱨ', 'Ⱪ' => 'ⱪ', 'Ⱬ' => 'ⱬ', 'Ɑ' => 'ɑ', 'Ɱ' => 'ɱ', 'Ɐ' => 'ɐ', 'Ɒ' => 'ɒ', 'Ⱳ' => 'ⱳ', 'Ⱶ' => 'ⱶ', 'Ȿ' => 'ȿ', 'Ɀ' => 'ɀ', 'Ⲁ' => 'ⲁ', 'Ⲃ' => 'ⲃ', 'Ⲅ' => 'ⲅ', 'Ⲇ' => 'ⲇ', 'Ⲉ' => 'ⲉ', 'Ⲋ' => 'ⲋ', 'Ⲍ' => 'ⲍ', 'Ⲏ' => 'ⲏ', 'Ⲑ' => 'ⲑ', 'Ⲓ' => 'ⲓ', 'Ⲕ' => 'ⲕ', 'Ⲗ' => 'ⲗ', 'Ⲙ' => 'ⲙ', 'Ⲛ' => 'ⲛ', 'Ⲝ' => 'ⲝ', 'Ⲟ' => 'ⲟ', 'Ⲡ' => 'ⲡ', 'Ⲣ' => 'ⲣ', 'Ⲥ' => 'ⲥ', 'Ⲧ' => 'ⲧ', 'Ⲩ' => 'ⲩ', 'Ⲫ' => 'ⲫ', 'Ⲭ' => 'ⲭ', 'Ⲯ' => 'ⲯ', 'Ⲱ' => 'ⲱ', 'Ⲳ' => 'ⲳ', 'Ⲵ' => 'ⲵ', 'Ⲷ' => 'ⲷ', 'Ⲹ' => 'ⲹ', 'Ⲻ' => 'ⲻ', 'Ⲽ' => 'ⲽ', 'Ⲿ' => 'ⲿ', 'Ⳁ' => 'ⳁ', 'Ⳃ' => 'ⳃ', 'Ⳅ' => 'ⳅ', 'Ⳇ' => 'ⳇ', 'Ⳉ' => 'ⳉ', 'Ⳋ' => 'ⳋ', 'Ⳍ' => 'ⳍ', 'Ⳏ' => 'ⳏ', 'Ⳑ' => 'ⳑ', 'Ⳓ' => 'ⳓ', 'Ⳕ' => 'ⳕ', 'Ⳗ' => 'ⳗ', 'Ⳙ' => 'ⳙ', 'Ⳛ' => 'ⳛ', 'Ⳝ' => 'ⳝ', 'Ⳟ' => 'ⳟ', 'Ⳡ' => 'ⳡ', 'Ⳣ' => 'ⳣ', 'Ⳬ' => 'ⳬ', 'Ⳮ' => 'ⳮ', 'Ⳳ' => 'ⳳ', 'Ꙁ' => 'ꙁ', 'Ꙃ' => 'ꙃ', 'Ꙅ' => 'ꙅ', 'Ꙇ' => 'ꙇ', 'Ꙉ' => 'ꙉ', 'Ꙋ' => 'ꙋ', 'Ꙍ' => 'ꙍ', 'Ꙏ' => 'ꙏ', 'Ꙑ' => 'ꙑ', 'Ꙓ' => 'ꙓ', 'Ꙕ' => 'ꙕ', 'Ꙗ' => 'ꙗ', 'Ꙙ' => 'ꙙ', 'Ꙛ' => 'ꙛ', 'Ꙝ' => 'ꙝ', 'Ꙟ' => 'ꙟ', 'Ꙡ' => 'ꙡ', 'Ꙣ' => 'ꙣ', 'Ꙥ' => 'ꙥ', 'Ꙧ' => 'ꙧ', 'Ꙩ' => 'ꙩ', 'Ꙫ' => 'ꙫ', 'Ꙭ' => 'ꙭ', 'Ꚁ' => 'ꚁ', 'Ꚃ' => 'ꚃ', 'Ꚅ' => 'ꚅ', 'Ꚇ' => 'ꚇ', 'Ꚉ' => 'ꚉ', 'Ꚋ' => 'ꚋ', 'Ꚍ' => 'ꚍ', 'Ꚏ' => 'ꚏ', 'Ꚑ' => 'ꚑ', 'Ꚓ' => 'ꚓ', 'Ꚕ' => 'ꚕ', 'Ꚗ' => 'ꚗ', 'Ꚙ' => 'ꚙ', 'Ꚛ' => 'ꚛ', 'Ꜣ' => 'ꜣ', 'Ꜥ' => 'ꜥ', 'Ꜧ' => 'ꜧ', 'Ꜩ' => 'ꜩ', 'Ꜫ' => 'ꜫ', 'Ꜭ' => 'ꜭ', 'Ꜯ' => 'ꜯ', 'Ꜳ' => 'ꜳ', 'Ꜵ' => 'ꜵ', 'Ꜷ' => 'ꜷ', 'Ꜹ' => 'ꜹ', 'Ꜻ' => 'ꜻ', 'Ꜽ' => 'ꜽ', 'Ꜿ' => 'ꜿ', 'Ꝁ' => 'ꝁ', 'Ꝃ' => 'ꝃ', 'Ꝅ' => 'ꝅ', 'Ꝇ' => 'ꝇ', 'Ꝉ' => 'ꝉ', 'Ꝋ' => 'ꝋ', 'Ꝍ' => 'ꝍ', 'Ꝏ' => 'ꝏ', 'Ꝑ' => 'ꝑ', 'Ꝓ' => 'ꝓ', 'Ꝕ' => 'ꝕ', 'Ꝗ' => 'ꝗ', 'Ꝙ' => 'ꝙ', 'Ꝛ' => 'ꝛ', 'Ꝝ' => 'ꝝ', 'Ꝟ' => 'ꝟ', 'Ꝡ' => 'ꝡ', 'Ꝣ' => 'ꝣ', 'Ꝥ' => 'ꝥ', 'Ꝧ' => 'ꝧ', 'Ꝩ' => 'ꝩ', 'Ꝫ' => 'ꝫ', 'Ꝭ' => 'ꝭ', 'Ꝯ' => 'ꝯ', 'Ꝺ' => 'ꝺ', 'Ꝼ' => 'ꝼ', 'Ᵹ' => 'ᵹ', 'Ꝿ' => 'ꝿ', 'Ꞁ' => 'ꞁ', 'Ꞃ' => 'ꞃ', 'Ꞅ' => 'ꞅ', 'Ꞇ' => 'ꞇ', 'Ꞌ' => 'ꞌ', 'Ɥ' => 'ɥ', 'Ꞑ' => 'ꞑ', 'Ꞓ' => 'ꞓ', 'Ꞗ' => 'ꞗ', 'Ꞙ' => 'ꞙ', 'Ꞛ' => 'ꞛ', 'Ꞝ' => 'ꞝ', 'Ꞟ' => 'ꞟ', 'Ꞡ' => 'ꞡ', 'Ꞣ' => 'ꞣ', 'Ꞥ' => 'ꞥ', 'Ꞧ' => 'ꞧ', 'Ꞩ' => 'ꞩ', 'Ɦ' => 'ɦ', 'Ɜ' => 'ɜ', 'Ɡ' => 'ɡ', 'Ɬ' => 'ɬ', 'Ɪ' => 'ɪ', 'Ʞ' => 'ʞ', 'Ʇ' => 'ʇ', 'Ʝ' => 'ʝ', 'Ꭓ' => 'ꭓ', 'Ꞵ' => 'ꞵ', 'Ꞷ' => 'ꞷ', 'Ꞹ' => 'ꞹ', 'Ꞻ' => 'ꞻ', 'Ꞽ' => 'ꞽ', 'Ꞿ' => 'ꞿ', 'Ꟃ' => 'ꟃ', 'Ꞔ' => 'ꞔ', 'Ʂ' => 'ʂ', 'Ᶎ' => 'ᶎ', 'Ꟈ' => 'ꟈ', 'Ꟊ' => 'ꟊ', 'Ꟶ' => 'ꟶ', 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', '𐐀' => '𐐨', '𐐁' => '𐐩', '𐐂' => '𐐪', '𐐃' => '𐐫', '𐐄' => '𐐬', '𐐅' => '𐐭', '𐐆' => '𐐮', '𐐇' => '𐐯', '𐐈' => '𐐰', '𐐉' => '𐐱', '𐐊' => '𐐲', '𐐋' => '𐐳', '𐐌' => '𐐴', '𐐍' => '𐐵', '𐐎' => '𐐶', '𐐏' => '𐐷', '𐐐' => '𐐸', '𐐑' => '𐐹', '𐐒' => '𐐺', '𐐓' => '𐐻', '𐐔' => '𐐼', '𐐕' => '𐐽', '𐐖' => '𐐾', '𐐗' => '𐐿', '𐐘' => '𐑀', '𐐙' => '𐑁', '𐐚' => '𐑂', '𐐛' => '𐑃', '𐐜' => '𐑄', '𐐝' => '𐑅', '𐐞' => '𐑆', '𐐟' => '𐑇', '𐐠' => '𐑈', '𐐡' => '𐑉', '𐐢' => '𐑊', '𐐣' => '𐑋', '𐐤' => '𐑌', '𐐥' => '𐑍', '𐐦' => '𐑎', '𐐧' => '𐑏', '𐒰' => '𐓘', '𐒱' => '𐓙', '𐒲' => '𐓚', '𐒳' => '𐓛', '𐒴' => '𐓜', '𐒵' => '𐓝', '𐒶' => '𐓞', '𐒷' => '𐓟', '𐒸' => '𐓠', '𐒹' => '𐓡', '𐒺' => '𐓢', '𐒻' => '𐓣', '𐒼' => '𐓤', '𐒽' => '𐓥', '𐒾' => '𐓦', '𐒿' => '𐓧', '𐓀' => '𐓨', '𐓁' => '𐓩', '𐓂' => '𐓪', '𐓃' => '𐓫', '𐓄' => '𐓬', '𐓅' => '𐓭', '𐓆' => '𐓮', '𐓇' => '𐓯', '𐓈' => '𐓰', '𐓉' => '𐓱', '𐓊' => '𐓲', '𐓋' => '𐓳', '𐓌' => '𐓴', '𐓍' => '𐓵', '𐓎' => '𐓶', '𐓏' => '𐓷', '𐓐' => '𐓸', '𐓑' => '𐓹', '𐓒' => '𐓺', '𐓓' => '𐓻', '𐲀' => '𐳀', '𐲁' => '𐳁', '𐲂' => '𐳂', '𐲃' => '𐳃', '𐲄' => '𐳄', '𐲅' => '𐳅', '𐲆' => '𐳆', '𐲇' => '𐳇', '𐲈' => '𐳈', '𐲉' => '𐳉', '𐲊' => '𐳊', '𐲋' => '𐳋', '𐲌' => '𐳌', '𐲍' => '𐳍', '𐲎' => '𐳎', '𐲏' => '𐳏', '𐲐' => '𐳐', '𐲑' => '𐳑', '𐲒' => '𐳒', '𐲓' => '𐳓', '𐲔' => '𐳔', '𐲕' => '𐳕', '𐲖' => '𐳖', '𐲗' => '𐳗', '𐲘' => '𐳘', '𐲙' => '𐳙', '𐲚' => '𐳚', '𐲛' => '𐳛', '𐲜' => '𐳜', '𐲝' => '𐳝', '𐲞' => '𐳞', '𐲟' => '𐳟', '𐲠' => '𐳠', '𐲡' => '𐳡', '𐲢' => '𐳢', '𐲣' => '𐳣', '𐲤' => '𐳤', '𐲥' => '𐳥', '𐲦' => '𐳦', '𐲧' => '𐳧', '𐲨' => '𐳨', '𐲩' => '𐳩', '𐲪' => '𐳪', '𐲫' => '𐳫', '𐲬' => '𐳬', '𐲭' => '𐳭', '𐲮' => '𐳮', '𐲯' => '𐳯', '𐲰' => '𐳰', '𐲱' => '𐳱', '𐲲' => '𐳲', '𑢠' => '𑣀', '𑢡' => '𑣁', '𑢢' => '𑣂', '𑢣' => '𑣃', '𑢤' => '𑣄', '𑢥' => '𑣅', '𑢦' => '𑣆', '𑢧' => '𑣇', '𑢨' => '𑣈', '𑢩' => '𑣉', '𑢪' => '𑣊', '𑢫' => '𑣋', '𑢬' => '𑣌', '𑢭' => '𑣍', '𑢮' => '𑣎', '𑢯' => '𑣏', '𑢰' => '𑣐', '𑢱' => '𑣑', '𑢲' => '𑣒', '𑢳' => '𑣓', '𑢴' => '𑣔', '𑢵' => '𑣕', '𑢶' => '𑣖', '𑢷' => '𑣗', '𑢸' => '𑣘', '𑢹' => '𑣙', '𑢺' => '𑣚', '𑢻' => '𑣛', '𑢼' => '𑣜', '𑢽' => '𑣝', '𑢾' => '𑣞', '𑢿' => '𑣟', '𖹀' => '𖹠', '𖹁' => '𖹡', '𖹂' => '𖹢', '𖹃' => '𖹣', '𖹄' => '𖹤', '𖹅' => '𖹥', '𖹆' => '𖹦', '𖹇' => '𖹧', '𖹈' => '𖹨', '𖹉' => '𖹩', '𖹊' => '𖹪', '𖹋' => '𖹫', '𖹌' => '𖹬', '𖹍' => '𖹭', '𖹎' => '𖹮', '𖹏' => '𖹯', '𖹐' => '𖹰', '𖹑' => '𖹱', '𖹒' => '𖹲', '𖹓' => '𖹳', '𖹔' => '𖹴', '𖹕' => '𖹵', '𖹖' => '𖹶', '𖹗' => '𖹷', '𖹘' => '𖹸', '𖹙' => '𖹹', '𖹚' => '𖹺', '𖹛' => '𖹻', '𖹜' => '𖹼', '𖹝' => '𖹽', '𖹞' => '𖹾', '𖹟' => '𖹿', '𞤀' => '𞤢', '𞤁' => '𞤣', '𞤂' => '𞤤', '𞤃' => '𞤥', '𞤄' => '𞤦', '𞤅' => '𞤧', '𞤆' => '𞤨', '𞤇' => '𞤩', '𞤈' => '𞤪', '𞤉' => '𞤫', '𞤊' => '𞤬', '𞤋' => '𞤭', '𞤌' => '𞤮', '𞤍' => '𞤯', '𞤎' => '𞤰', '𞤏' => '𞤱', '𞤐' => '𞤲', '𞤑' => '𞤳', '𞤒' => '𞤴', '𞤓' => '𞤵', '𞤔' => '𞤶', '𞤕' => '𞤷', '𞤖' => '𞤸', '𞤗' => '𞤹', '𞤘' => '𞤺', '𞤙' => '𞤻', '𞤚' => '𞤼', '𞤛' => '𞤽', '𞤜' => '𞤾', '𞤝' => '𞤿', '𞤞' => '𞥀', '𞤟' => '𞥁', '𞤠' => '𞥂', '𞤡' => '𞥃', ); PK!>|zK99Fvendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.phpnu[ 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', 'µ' => 'Μ', 'à' => 'À', 'á' => 'Á', 'â' => 'Â', 'ã' => 'Ã', 'ä' => 'Ä', 'å' => 'Å', 'æ' => 'Æ', 'ç' => 'Ç', 'è' => 'È', 'é' => 'É', 'ê' => 'Ê', 'ë' => 'Ë', 'ì' => 'Ì', 'í' => 'Í', 'î' => 'Î', 'ï' => 'Ï', 'ð' => 'Ð', 'ñ' => 'Ñ', 'ò' => 'Ò', 'ó' => 'Ó', 'ô' => 'Ô', 'õ' => 'Õ', 'ö' => 'Ö', 'ø' => 'Ø', 'ù' => 'Ù', 'ú' => 'Ú', 'û' => 'Û', 'ü' => 'Ü', 'ý' => 'Ý', 'þ' => 'Þ', 'ÿ' => 'Ÿ', 'ā' => 'Ā', 'ă' => 'Ă', 'ą' => 'Ą', 'ć' => 'Ć', 'ĉ' => 'Ĉ', 'ċ' => 'Ċ', 'č' => 'Č', 'ď' => 'Ď', 'đ' => 'Đ', 'ē' => 'Ē', 'ĕ' => 'Ĕ', 'ė' => 'Ė', 'ę' => 'Ę', 'ě' => 'Ě', 'ĝ' => 'Ĝ', 'ğ' => 'Ğ', 'ġ' => 'Ġ', 'ģ' => 'Ģ', 'ĥ' => 'Ĥ', 'ħ' => 'Ħ', 'ĩ' => 'Ĩ', 'ī' => 'Ī', 'ĭ' => 'Ĭ', 'į' => 'Į', 'ı' => 'I', 'ij' => 'IJ', 'ĵ' => 'Ĵ', 'ķ' => 'Ķ', 'ĺ' => 'Ĺ', 'ļ' => 'Ļ', 'ľ' => 'Ľ', 'ŀ' => 'Ŀ', 'ł' => 'Ł', 'ń' => 'Ń', 'ņ' => 'Ņ', 'ň' => 'Ň', 'ŋ' => 'Ŋ', 'ō' => 'Ō', 'ŏ' => 'Ŏ', 'ő' => 'Ő', 'œ' => 'Œ', 'ŕ' => 'Ŕ', 'ŗ' => 'Ŗ', 'ř' => 'Ř', 'ś' => 'Ś', 'ŝ' => 'Ŝ', 'ş' => 'Ş', 'š' => 'Š', 'ţ' => 'Ţ', 'ť' => 'Ť', 'ŧ' => 'Ŧ', 'ũ' => 'Ũ', 'ū' => 'Ū', 'ŭ' => 'Ŭ', 'ů' => 'Ů', 'ű' => 'Ű', 'ų' => 'Ų', 'ŵ' => 'Ŵ', 'ŷ' => 'Ŷ', 'ź' => 'Ź', 'ż' => 'Ż', 'ž' => 'Ž', 'ſ' => 'S', 'ƀ' => 'Ƀ', 'ƃ' => 'Ƃ', 'ƅ' => 'Ƅ', 'ƈ' => 'Ƈ', 'ƌ' => 'Ƌ', 'ƒ' => 'Ƒ', 'ƕ' => 'Ƕ', 'ƙ' => 'Ƙ', 'ƚ' => 'Ƚ', 'ƞ' => 'Ƞ', 'ơ' => 'Ơ', 'ƣ' => 'Ƣ', 'ƥ' => 'Ƥ', 'ƨ' => 'Ƨ', 'ƭ' => 'Ƭ', 'ư' => 'Ư', 'ƴ' => 'Ƴ', 'ƶ' => 'Ƶ', 'ƹ' => 'Ƹ', 'ƽ' => 'Ƽ', 'ƿ' => 'Ƿ', 'Dž' => 'DŽ', 'dž' => 'DŽ', 'Lj' => 'LJ', 'lj' => 'LJ', 'Nj' => 'NJ', 'nj' => 'NJ', 'ǎ' => 'Ǎ', 'ǐ' => 'Ǐ', 'ǒ' => 'Ǒ', 'ǔ' => 'Ǔ', 'ǖ' => 'Ǖ', 'ǘ' => 'Ǘ', 'ǚ' => 'Ǚ', 'ǜ' => 'Ǜ', 'ǝ' => 'Ǝ', 'ǟ' => 'Ǟ', 'ǡ' => 'Ǡ', 'ǣ' => 'Ǣ', 'ǥ' => 'Ǥ', 'ǧ' => 'Ǧ', 'ǩ' => 'Ǩ', 'ǫ' => 'Ǫ', 'ǭ' => 'Ǭ', 'ǯ' => 'Ǯ', 'Dz' => 'DZ', 'dz' => 'DZ', 'ǵ' => 'Ǵ', 'ǹ' => 'Ǹ', 'ǻ' => 'Ǻ', 'ǽ' => 'Ǽ', 'ǿ' => 'Ǿ', 'ȁ' => 'Ȁ', 'ȃ' => 'Ȃ', 'ȅ' => 'Ȅ', 'ȇ' => 'Ȇ', 'ȉ' => 'Ȉ', 'ȋ' => 'Ȋ', 'ȍ' => 'Ȍ', 'ȏ' => 'Ȏ', 'ȑ' => 'Ȑ', 'ȓ' => 'Ȓ', 'ȕ' => 'Ȕ', 'ȗ' => 'Ȗ', 'ș' => 'Ș', 'ț' => 'Ț', 'ȝ' => 'Ȝ', 'ȟ' => 'Ȟ', 'ȣ' => 'Ȣ', 'ȥ' => 'Ȥ', 'ȧ' => 'Ȧ', 'ȩ' => 'Ȩ', 'ȫ' => 'Ȫ', 'ȭ' => 'Ȭ', 'ȯ' => 'Ȯ', 'ȱ' => 'Ȱ', 'ȳ' => 'Ȳ', 'ȼ' => 'Ȼ', 'ȿ' => 'Ȿ', 'ɀ' => 'Ɀ', 'ɂ' => 'Ɂ', 'ɇ' => 'Ɇ', 'ɉ' => 'Ɉ', 'ɋ' => 'Ɋ', 'ɍ' => 'Ɍ', 'ɏ' => 'Ɏ', 'ɐ' => 'Ɐ', 'ɑ' => 'Ɑ', 'ɒ' => 'Ɒ', 'ɓ' => 'Ɓ', 'ɔ' => 'Ɔ', 'ɖ' => 'Ɖ', 'ɗ' => 'Ɗ', 'ə' => 'Ə', 'ɛ' => 'Ɛ', 'ɜ' => 'Ɜ', 'ɠ' => 'Ɠ', 'ɡ' => 'Ɡ', 'ɣ' => 'Ɣ', 'ɥ' => 'Ɥ', 'ɦ' => 'Ɦ', 'ɨ' => 'Ɨ', 'ɩ' => 'Ɩ', 'ɪ' => 'Ɪ', 'ɫ' => 'Ɫ', 'ɬ' => 'Ɬ', 'ɯ' => 'Ɯ', 'ɱ' => 'Ɱ', 'ɲ' => 'Ɲ', 'ɵ' => 'Ɵ', 'ɽ' => 'Ɽ', 'ʀ' => 'Ʀ', 'ʂ' => 'Ʂ', 'ʃ' => 'Ʃ', 'ʇ' => 'Ʇ', 'ʈ' => 'Ʈ', 'ʉ' => 'Ʉ', 'ʊ' => 'Ʊ', 'ʋ' => 'Ʋ', 'ʌ' => 'Ʌ', 'ʒ' => 'Ʒ', 'ʝ' => 'Ʝ', 'ʞ' => 'Ʞ', 'ͅ' => 'Ι', 'ͱ' => 'Ͱ', 'ͳ' => 'Ͳ', 'ͷ' => 'Ͷ', 'ͻ' => 'Ͻ', 'ͼ' => 'Ͼ', 'ͽ' => 'Ͽ', 'ά' => 'Ά', 'έ' => 'Έ', 'ή' => 'Ή', 'ί' => 'Ί', 'α' => 'Α', 'β' => 'Β', 'γ' => 'Γ', 'δ' => 'Δ', 'ε' => 'Ε', 'ζ' => 'Ζ', 'η' => 'Η', 'θ' => 'Θ', 'ι' => 'Ι', 'κ' => 'Κ', 'λ' => 'Λ', 'μ' => 'Μ', 'ν' => 'Ν', 'ξ' => 'Ξ', 'ο' => 'Ο', 'π' => 'Π', 'ρ' => 'Ρ', 'ς' => 'Σ', 'σ' => 'Σ', 'τ' => 'Τ', 'υ' => 'Υ', 'φ' => 'Φ', 'χ' => 'Χ', 'ψ' => 'Ψ', 'ω' => 'Ω', 'ϊ' => 'Ϊ', 'ϋ' => 'Ϋ', 'ό' => 'Ό', 'ύ' => 'Ύ', 'ώ' => 'Ώ', 'ϐ' => 'Β', 'ϑ' => 'Θ', 'ϕ' => 'Φ', 'ϖ' => 'Π', 'ϗ' => 'Ϗ', 'ϙ' => 'Ϙ', 'ϛ' => 'Ϛ', 'ϝ' => 'Ϝ', 'ϟ' => 'Ϟ', 'ϡ' => 'Ϡ', 'ϣ' => 'Ϣ', 'ϥ' => 'Ϥ', 'ϧ' => 'Ϧ', 'ϩ' => 'Ϩ', 'ϫ' => 'Ϫ', 'ϭ' => 'Ϭ', 'ϯ' => 'Ϯ', 'ϰ' => 'Κ', 'ϱ' => 'Ρ', 'ϲ' => 'Ϲ', 'ϳ' => 'Ϳ', 'ϵ' => 'Ε', 'ϸ' => 'Ϸ', 'ϻ' => 'Ϻ', 'а' => 'А', 'б' => 'Б', 'в' => 'В', 'г' => 'Г', 'д' => 'Д', 'е' => 'Е', 'ж' => 'Ж', 'з' => 'З', 'и' => 'И', 'й' => 'Й', 'к' => 'К', 'л' => 'Л', 'м' => 'М', 'н' => 'Н', 'о' => 'О', 'п' => 'П', 'р' => 'Р', 'с' => 'С', 'т' => 'Т', 'у' => 'У', 'ф' => 'Ф', 'х' => 'Х', 'ц' => 'Ц', 'ч' => 'Ч', 'ш' => 'Ш', 'щ' => 'Щ', 'ъ' => 'Ъ', 'ы' => 'Ы', 'ь' => 'Ь', 'э' => 'Э', 'ю' => 'Ю', 'я' => 'Я', 'ѐ' => 'Ѐ', 'ё' => 'Ё', 'ђ' => 'Ђ', 'ѓ' => 'Ѓ', 'є' => 'Є', 'ѕ' => 'Ѕ', 'і' => 'І', 'ї' => 'Ї', 'ј' => 'Ј', 'љ' => 'Љ', 'њ' => 'Њ', 'ћ' => 'Ћ', 'ќ' => 'Ќ', 'ѝ' => 'Ѝ', 'ў' => 'Ў', 'џ' => 'Џ', 'ѡ' => 'Ѡ', 'ѣ' => 'Ѣ', 'ѥ' => 'Ѥ', 'ѧ' => 'Ѧ', 'ѩ' => 'Ѩ', 'ѫ' => 'Ѫ', 'ѭ' => 'Ѭ', 'ѯ' => 'Ѯ', 'ѱ' => 'Ѱ', 'ѳ' => 'Ѳ', 'ѵ' => 'Ѵ', 'ѷ' => 'Ѷ', 'ѹ' => 'Ѹ', 'ѻ' => 'Ѻ', 'ѽ' => 'Ѽ', 'ѿ' => 'Ѿ', 'ҁ' => 'Ҁ', 'ҋ' => 'Ҋ', 'ҍ' => 'Ҍ', 'ҏ' => 'Ҏ', 'ґ' => 'Ґ', 'ғ' => 'Ғ', 'ҕ' => 'Ҕ', 'җ' => 'Җ', 'ҙ' => 'Ҙ', 'қ' => 'Қ', 'ҝ' => 'Ҝ', 'ҟ' => 'Ҟ', 'ҡ' => 'Ҡ', 'ң' => 'Ң', 'ҥ' => 'Ҥ', 'ҧ' => 'Ҧ', 'ҩ' => 'Ҩ', 'ҫ' => 'Ҫ', 'ҭ' => 'Ҭ', 'ү' => 'Ү', 'ұ' => 'Ұ', 'ҳ' => 'Ҳ', 'ҵ' => 'Ҵ', 'ҷ' => 'Ҷ', 'ҹ' => 'Ҹ', 'һ' => 'Һ', 'ҽ' => 'Ҽ', 'ҿ' => 'Ҿ', 'ӂ' => 'Ӂ', 'ӄ' => 'Ӄ', 'ӆ' => 'Ӆ', 'ӈ' => 'Ӈ', 'ӊ' => 'Ӊ', 'ӌ' => 'Ӌ', 'ӎ' => 'Ӎ', 'ӏ' => 'Ӏ', 'ӑ' => 'Ӑ', 'ӓ' => 'Ӓ', 'ӕ' => 'Ӕ', 'ӗ' => 'Ӗ', 'ә' => 'Ә', 'ӛ' => 'Ӛ', 'ӝ' => 'Ӝ', 'ӟ' => 'Ӟ', 'ӡ' => 'Ӡ', 'ӣ' => 'Ӣ', 'ӥ' => 'Ӥ', 'ӧ' => 'Ӧ', 'ө' => 'Ө', 'ӫ' => 'Ӫ', 'ӭ' => 'Ӭ', 'ӯ' => 'Ӯ', 'ӱ' => 'Ӱ', 'ӳ' => 'Ӳ', 'ӵ' => 'Ӵ', 'ӷ' => 'Ӷ', 'ӹ' => 'Ӹ', 'ӻ' => 'Ӻ', 'ӽ' => 'Ӽ', 'ӿ' => 'Ӿ', 'ԁ' => 'Ԁ', 'ԃ' => 'Ԃ', 'ԅ' => 'Ԅ', 'ԇ' => 'Ԇ', 'ԉ' => 'Ԉ', 'ԋ' => 'Ԋ', 'ԍ' => 'Ԍ', 'ԏ' => 'Ԏ', 'ԑ' => 'Ԑ', 'ԓ' => 'Ԓ', 'ԕ' => 'Ԕ', 'ԗ' => 'Ԗ', 'ԙ' => 'Ԙ', 'ԛ' => 'Ԛ', 'ԝ' => 'Ԝ', 'ԟ' => 'Ԟ', 'ԡ' => 'Ԡ', 'ԣ' => 'Ԣ', 'ԥ' => 'Ԥ', 'ԧ' => 'Ԧ', 'ԩ' => 'Ԩ', 'ԫ' => 'Ԫ', 'ԭ' => 'Ԭ', 'ԯ' => 'Ԯ', 'ա' => 'Ա', 'բ' => 'Բ', 'գ' => 'Գ', 'դ' => 'Դ', 'ե' => 'Ե', 'զ' => 'Զ', 'է' => 'Է', 'ը' => 'Ը', 'թ' => 'Թ', 'ժ' => 'Ժ', 'ի' => 'Ի', 'լ' => 'Լ', 'խ' => 'Խ', 'ծ' => 'Ծ', 'կ' => 'Կ', 'հ' => 'Հ', 'ձ' => 'Ձ', 'ղ' => 'Ղ', 'ճ' => 'Ճ', 'մ' => 'Մ', 'յ' => 'Յ', 'ն' => 'Ն', 'շ' => 'Շ', 'ո' => 'Ո', 'չ' => 'Չ', 'պ' => 'Պ', 'ջ' => 'Ջ', 'ռ' => 'Ռ', 'ս' => 'Ս', 'վ' => 'Վ', 'տ' => 'Տ', 'ր' => 'Ր', 'ց' => 'Ց', 'ւ' => 'Ւ', 'փ' => 'Փ', 'ք' => 'Ք', 'օ' => 'Օ', 'ֆ' => 'Ֆ', 'ა' => 'Ა', 'ბ' => 'Ბ', 'გ' => 'Გ', 'დ' => 'Დ', 'ე' => 'Ე', 'ვ' => 'Ვ', 'ზ' => 'Ზ', 'თ' => 'Თ', 'ი' => 'Ი', 'კ' => 'Კ', 'ლ' => 'Ლ', 'მ' => 'Მ', 'ნ' => 'Ნ', 'ო' => 'Ო', 'პ' => 'Პ', 'ჟ' => 'Ჟ', 'რ' => 'Რ', 'ს' => 'Ს', 'ტ' => 'Ტ', 'უ' => 'Უ', 'ფ' => 'Ფ', 'ქ' => 'Ქ', 'ღ' => 'Ღ', 'ყ' => 'Ყ', 'შ' => 'Შ', 'ჩ' => 'Ჩ', 'ც' => 'Ც', 'ძ' => 'Ძ', 'წ' => 'Წ', 'ჭ' => 'Ჭ', 'ხ' => 'Ხ', 'ჯ' => 'Ჯ', 'ჰ' => 'Ჰ', 'ჱ' => 'Ჱ', 'ჲ' => 'Ჲ', 'ჳ' => 'Ჳ', 'ჴ' => 'Ჴ', 'ჵ' => 'Ჵ', 'ჶ' => 'Ჶ', 'ჷ' => 'Ჷ', 'ჸ' => 'Ჸ', 'ჹ' => 'Ჹ', 'ჺ' => 'Ჺ', 'ჽ' => 'Ჽ', 'ჾ' => 'Ჾ', 'ჿ' => 'Ჿ', 'ᏸ' => 'Ᏸ', 'ᏹ' => 'Ᏹ', 'ᏺ' => 'Ᏺ', 'ᏻ' => 'Ᏻ', 'ᏼ' => 'Ᏼ', 'ᏽ' => 'Ᏽ', 'ᲀ' => 'В', 'ᲁ' => 'Д', 'ᲂ' => 'О', 'ᲃ' => 'С', 'ᲄ' => 'Т', 'ᲅ' => 'Т', 'ᲆ' => 'Ъ', 'ᲇ' => 'Ѣ', 'ᲈ' => 'Ꙋ', 'ᵹ' => 'Ᵹ', 'ᵽ' => 'Ᵽ', 'ᶎ' => 'Ᶎ', 'ḁ' => 'Ḁ', 'ḃ' => 'Ḃ', 'ḅ' => 'Ḅ', 'ḇ' => 'Ḇ', 'ḉ' => 'Ḉ', 'ḋ' => 'Ḋ', 'ḍ' => 'Ḍ', 'ḏ' => 'Ḏ', 'ḑ' => 'Ḑ', 'ḓ' => 'Ḓ', 'ḕ' => 'Ḕ', 'ḗ' => 'Ḗ', 'ḙ' => 'Ḙ', 'ḛ' => 'Ḛ', 'ḝ' => 'Ḝ', 'ḟ' => 'Ḟ', 'ḡ' => 'Ḡ', 'ḣ' => 'Ḣ', 'ḥ' => 'Ḥ', 'ḧ' => 'Ḧ', 'ḩ' => 'Ḩ', 'ḫ' => 'Ḫ', 'ḭ' => 'Ḭ', 'ḯ' => 'Ḯ', 'ḱ' => 'Ḱ', 'ḳ' => 'Ḳ', 'ḵ' => 'Ḵ', 'ḷ' => 'Ḷ', 'ḹ' => 'Ḹ', 'ḻ' => 'Ḻ', 'ḽ' => 'Ḽ', 'ḿ' => 'Ḿ', 'ṁ' => 'Ṁ', 'ṃ' => 'Ṃ', 'ṅ' => 'Ṅ', 'ṇ' => 'Ṇ', 'ṉ' => 'Ṉ', 'ṋ' => 'Ṋ', 'ṍ' => 'Ṍ', 'ṏ' => 'Ṏ', 'ṑ' => 'Ṑ', 'ṓ' => 'Ṓ', 'ṕ' => 'Ṕ', 'ṗ' => 'Ṗ', 'ṙ' => 'Ṙ', 'ṛ' => 'Ṛ', 'ṝ' => 'Ṝ', 'ṟ' => 'Ṟ', 'ṡ' => 'Ṡ', 'ṣ' => 'Ṣ', 'ṥ' => 'Ṥ', 'ṧ' => 'Ṧ', 'ṩ' => 'Ṩ', 'ṫ' => 'Ṫ', 'ṭ' => 'Ṭ', 'ṯ' => 'Ṯ', 'ṱ' => 'Ṱ', 'ṳ' => 'Ṳ', 'ṵ' => 'Ṵ', 'ṷ' => 'Ṷ', 'ṹ' => 'Ṹ', 'ṻ' => 'Ṻ', 'ṽ' => 'Ṽ', 'ṿ' => 'Ṿ', 'ẁ' => 'Ẁ', 'ẃ' => 'Ẃ', 'ẅ' => 'Ẅ', 'ẇ' => 'Ẇ', 'ẉ' => 'Ẉ', 'ẋ' => 'Ẋ', 'ẍ' => 'Ẍ', 'ẏ' => 'Ẏ', 'ẑ' => 'Ẑ', 'ẓ' => 'Ẓ', 'ẕ' => 'Ẕ', 'ẛ' => 'Ṡ', 'ạ' => 'Ạ', 'ả' => 'Ả', 'ấ' => 'Ấ', 'ầ' => 'Ầ', 'ẩ' => 'Ẩ', 'ẫ' => 'Ẫ', 'ậ' => 'Ậ', 'ắ' => 'Ắ', 'ằ' => 'Ằ', 'ẳ' => 'Ẳ', 'ẵ' => 'Ẵ', 'ặ' => 'Ặ', 'ẹ' => 'Ẹ', 'ẻ' => 'Ẻ', 'ẽ' => 'Ẽ', 'ế' => 'Ế', 'ề' => 'Ề', 'ể' => 'Ể', 'ễ' => 'Ễ', 'ệ' => 'Ệ', 'ỉ' => 'Ỉ', 'ị' => 'Ị', 'ọ' => 'Ọ', 'ỏ' => 'Ỏ', 'ố' => 'Ố', 'ồ' => 'Ồ', 'ổ' => 'Ổ', 'ỗ' => 'Ỗ', 'ộ' => 'Ộ', 'ớ' => 'Ớ', 'ờ' => 'Ờ', 'ở' => 'Ở', 'ỡ' => 'Ỡ', 'ợ' => 'Ợ', 'ụ' => 'Ụ', 'ủ' => 'Ủ', 'ứ' => 'Ứ', 'ừ' => 'Ừ', 'ử' => 'Ử', 'ữ' => 'Ữ', 'ự' => 'Ự', 'ỳ' => 'Ỳ', 'ỵ' => 'Ỵ', 'ỷ' => 'Ỷ', 'ỹ' => 'Ỹ', 'ỻ' => 'Ỻ', 'ỽ' => 'Ỽ', 'ỿ' => 'Ỿ', 'ἀ' => 'Ἀ', 'ἁ' => 'Ἁ', 'ἂ' => 'Ἂ', 'ἃ' => 'Ἃ', 'ἄ' => 'Ἄ', 'ἅ' => 'Ἅ', 'ἆ' => 'Ἆ', 'ἇ' => 'Ἇ', 'ἐ' => 'Ἐ', 'ἑ' => 'Ἑ', 'ἒ' => 'Ἒ', 'ἓ' => 'Ἓ', 'ἔ' => 'Ἔ', 'ἕ' => 'Ἕ', 'ἠ' => 'Ἠ', 'ἡ' => 'Ἡ', 'ἢ' => 'Ἢ', 'ἣ' => 'Ἣ', 'ἤ' => 'Ἤ', 'ἥ' => 'Ἥ', 'ἦ' => 'Ἦ', 'ἧ' => 'Ἧ', 'ἰ' => 'Ἰ', 'ἱ' => 'Ἱ', 'ἲ' => 'Ἲ', 'ἳ' => 'Ἳ', 'ἴ' => 'Ἴ', 'ἵ' => 'Ἵ', 'ἶ' => 'Ἶ', 'ἷ' => 'Ἷ', 'ὀ' => 'Ὀ', 'ὁ' => 'Ὁ', 'ὂ' => 'Ὂ', 'ὃ' => 'Ὃ', 'ὄ' => 'Ὄ', 'ὅ' => 'Ὅ', 'ὑ' => 'Ὑ', 'ὓ' => 'Ὓ', 'ὕ' => 'Ὕ', 'ὗ' => 'Ὗ', 'ὠ' => 'Ὠ', 'ὡ' => 'Ὡ', 'ὢ' => 'Ὢ', 'ὣ' => 'Ὣ', 'ὤ' => 'Ὤ', 'ὥ' => 'Ὥ', 'ὦ' => 'Ὦ', 'ὧ' => 'Ὧ', 'ὰ' => 'Ὰ', 'ά' => 'Ά', 'ὲ' => 'Ὲ', 'έ' => 'Έ', 'ὴ' => 'Ὴ', 'ή' => 'Ή', 'ὶ' => 'Ὶ', 'ί' => 'Ί', 'ὸ' => 'Ὸ', 'ό' => 'Ό', 'ὺ' => 'Ὺ', 'ύ' => 'Ύ', 'ὼ' => 'Ὼ', 'ώ' => 'Ώ', 'ᾀ' => 'ἈΙ', 'ᾁ' => 'ἉΙ', 'ᾂ' => 'ἊΙ', 'ᾃ' => 'ἋΙ', 'ᾄ' => 'ἌΙ', 'ᾅ' => 'ἍΙ', 'ᾆ' => 'ἎΙ', 'ᾇ' => 'ἏΙ', 'ᾐ' => 'ἨΙ', 'ᾑ' => 'ἩΙ', 'ᾒ' => 'ἪΙ', 'ᾓ' => 'ἫΙ', 'ᾔ' => 'ἬΙ', 'ᾕ' => 'ἭΙ', 'ᾖ' => 'ἮΙ', 'ᾗ' => 'ἯΙ', 'ᾠ' => 'ὨΙ', 'ᾡ' => 'ὩΙ', 'ᾢ' => 'ὪΙ', 'ᾣ' => 'ὫΙ', 'ᾤ' => 'ὬΙ', 'ᾥ' => 'ὭΙ', 'ᾦ' => 'ὮΙ', 'ᾧ' => 'ὯΙ', 'ᾰ' => 'Ᾰ', 'ᾱ' => 'Ᾱ', 'ᾳ' => 'ΑΙ', 'ι' => 'Ι', 'ῃ' => 'ΗΙ', 'ῐ' => 'Ῐ', 'ῑ' => 'Ῑ', 'ῠ' => 'Ῠ', 'ῡ' => 'Ῡ', 'ῥ' => 'Ῥ', 'ῳ' => 'ΩΙ', 'ⅎ' => 'Ⅎ', 'ⅰ' => 'Ⅰ', 'ⅱ' => 'Ⅱ', 'ⅲ' => 'Ⅲ', 'ⅳ' => 'Ⅳ', 'ⅴ' => 'Ⅴ', 'ⅵ' => 'Ⅵ', 'ⅶ' => 'Ⅶ', 'ⅷ' => 'Ⅷ', 'ⅸ' => 'Ⅸ', 'ⅹ' => 'Ⅹ', 'ⅺ' => 'Ⅺ', 'ⅻ' => 'Ⅻ', 'ⅼ' => 'Ⅼ', 'ⅽ' => 'Ⅽ', 'ⅾ' => 'Ⅾ', 'ⅿ' => 'Ⅿ', 'ↄ' => 'Ↄ', 'ⓐ' => 'Ⓐ', 'ⓑ' => 'Ⓑ', 'ⓒ' => 'Ⓒ', 'ⓓ' => 'Ⓓ', 'ⓔ' => 'Ⓔ', 'ⓕ' => 'Ⓕ', 'ⓖ' => 'Ⓖ', 'ⓗ' => 'Ⓗ', 'ⓘ' => 'Ⓘ', 'ⓙ' => 'Ⓙ', 'ⓚ' => 'Ⓚ', 'ⓛ' => 'Ⓛ', 'ⓜ' => 'Ⓜ', 'ⓝ' => 'Ⓝ', 'ⓞ' => 'Ⓞ', 'ⓟ' => 'Ⓟ', 'ⓠ' => 'Ⓠ', 'ⓡ' => 'Ⓡ', 'ⓢ' => 'Ⓢ', 'ⓣ' => 'Ⓣ', 'ⓤ' => 'Ⓤ', 'ⓥ' => 'Ⓥ', 'ⓦ' => 'Ⓦ', 'ⓧ' => 'Ⓧ', 'ⓨ' => 'Ⓨ', 'ⓩ' => 'Ⓩ', 'ⰰ' => 'Ⰰ', 'ⰱ' => 'Ⰱ', 'ⰲ' => 'Ⰲ', 'ⰳ' => 'Ⰳ', 'ⰴ' => 'Ⰴ', 'ⰵ' => 'Ⰵ', 'ⰶ' => 'Ⰶ', 'ⰷ' => 'Ⰷ', 'ⰸ' => 'Ⰸ', 'ⰹ' => 'Ⰹ', 'ⰺ' => 'Ⰺ', 'ⰻ' => 'Ⰻ', 'ⰼ' => 'Ⰼ', 'ⰽ' => 'Ⰽ', 'ⰾ' => 'Ⰾ', 'ⰿ' => 'Ⰿ', 'ⱀ' => 'Ⱀ', 'ⱁ' => 'Ⱁ', 'ⱂ' => 'Ⱂ', 'ⱃ' => 'Ⱃ', 'ⱄ' => 'Ⱄ', 'ⱅ' => 'Ⱅ', 'ⱆ' => 'Ⱆ', 'ⱇ' => 'Ⱇ', 'ⱈ' => 'Ⱈ', 'ⱉ' => 'Ⱉ', 'ⱊ' => 'Ⱊ', 'ⱋ' => 'Ⱋ', 'ⱌ' => 'Ⱌ', 'ⱍ' => 'Ⱍ', 'ⱎ' => 'Ⱎ', 'ⱏ' => 'Ⱏ', 'ⱐ' => 'Ⱐ', 'ⱑ' => 'Ⱑ', 'ⱒ' => 'Ⱒ', 'ⱓ' => 'Ⱓ', 'ⱔ' => 'Ⱔ', 'ⱕ' => 'Ⱕ', 'ⱖ' => 'Ⱖ', 'ⱗ' => 'Ⱗ', 'ⱘ' => 'Ⱘ', 'ⱙ' => 'Ⱙ', 'ⱚ' => 'Ⱚ', 'ⱛ' => 'Ⱛ', 'ⱜ' => 'Ⱜ', 'ⱝ' => 'Ⱝ', 'ⱞ' => 'Ⱞ', 'ⱡ' => 'Ⱡ', 'ⱥ' => 'Ⱥ', 'ⱦ' => 'Ⱦ', 'ⱨ' => 'Ⱨ', 'ⱪ' => 'Ⱪ', 'ⱬ' => 'Ⱬ', 'ⱳ' => 'Ⱳ', 'ⱶ' => 'Ⱶ', 'ⲁ' => 'Ⲁ', 'ⲃ' => 'Ⲃ', 'ⲅ' => 'Ⲅ', 'ⲇ' => 'Ⲇ', 'ⲉ' => 'Ⲉ', 'ⲋ' => 'Ⲋ', 'ⲍ' => 'Ⲍ', 'ⲏ' => 'Ⲏ', 'ⲑ' => 'Ⲑ', 'ⲓ' => 'Ⲓ', 'ⲕ' => 'Ⲕ', 'ⲗ' => 'Ⲗ', 'ⲙ' => 'Ⲙ', 'ⲛ' => 'Ⲛ', 'ⲝ' => 'Ⲝ', 'ⲟ' => 'Ⲟ', 'ⲡ' => 'Ⲡ', 'ⲣ' => 'Ⲣ', 'ⲥ' => 'Ⲥ', 'ⲧ' => 'Ⲧ', 'ⲩ' => 'Ⲩ', 'ⲫ' => 'Ⲫ', 'ⲭ' => 'Ⲭ', 'ⲯ' => 'Ⲯ', 'ⲱ' => 'Ⲱ', 'ⲳ' => 'Ⲳ', 'ⲵ' => 'Ⲵ', 'ⲷ' => 'Ⲷ', 'ⲹ' => 'Ⲹ', 'ⲻ' => 'Ⲻ', 'ⲽ' => 'Ⲽ', 'ⲿ' => 'Ⲿ', 'ⳁ' => 'Ⳁ', 'ⳃ' => 'Ⳃ', 'ⳅ' => 'Ⳅ', 'ⳇ' => 'Ⳇ', 'ⳉ' => 'Ⳉ', 'ⳋ' => 'Ⳋ', 'ⳍ' => 'Ⳍ', 'ⳏ' => 'Ⳏ', 'ⳑ' => 'Ⳑ', 'ⳓ' => 'Ⳓ', 'ⳕ' => 'Ⳕ', 'ⳗ' => 'Ⳗ', 'ⳙ' => 'Ⳙ', 'ⳛ' => 'Ⳛ', 'ⳝ' => 'Ⳝ', 'ⳟ' => 'Ⳟ', 'ⳡ' => 'Ⳡ', 'ⳣ' => 'Ⳣ', 'ⳬ' => 'Ⳬ', 'ⳮ' => 'Ⳮ', 'ⳳ' => 'Ⳳ', 'ⴀ' => 'Ⴀ', 'ⴁ' => 'Ⴁ', 'ⴂ' => 'Ⴂ', 'ⴃ' => 'Ⴃ', 'ⴄ' => 'Ⴄ', 'ⴅ' => 'Ⴅ', 'ⴆ' => 'Ⴆ', 'ⴇ' => 'Ⴇ', 'ⴈ' => 'Ⴈ', 'ⴉ' => 'Ⴉ', 'ⴊ' => 'Ⴊ', 'ⴋ' => 'Ⴋ', 'ⴌ' => 'Ⴌ', 'ⴍ' => 'Ⴍ', 'ⴎ' => 'Ⴎ', 'ⴏ' => 'Ⴏ', 'ⴐ' => 'Ⴐ', 'ⴑ' => 'Ⴑ', 'ⴒ' => 'Ⴒ', 'ⴓ' => 'Ⴓ', 'ⴔ' => 'Ⴔ', 'ⴕ' => 'Ⴕ', 'ⴖ' => 'Ⴖ', 'ⴗ' => 'Ⴗ', 'ⴘ' => 'Ⴘ', 'ⴙ' => 'Ⴙ', 'ⴚ' => 'Ⴚ', 'ⴛ' => 'Ⴛ', 'ⴜ' => 'Ⴜ', 'ⴝ' => 'Ⴝ', 'ⴞ' => 'Ⴞ', 'ⴟ' => 'Ⴟ', 'ⴠ' => 'Ⴠ', 'ⴡ' => 'Ⴡ', 'ⴢ' => 'Ⴢ', 'ⴣ' => 'Ⴣ', 'ⴤ' => 'Ⴤ', 'ⴥ' => 'Ⴥ', 'ⴧ' => 'Ⴧ', 'ⴭ' => 'Ⴭ', 'ꙁ' => 'Ꙁ', 'ꙃ' => 'Ꙃ', 'ꙅ' => 'Ꙅ', 'ꙇ' => 'Ꙇ', 'ꙉ' => 'Ꙉ', 'ꙋ' => 'Ꙋ', 'ꙍ' => 'Ꙍ', 'ꙏ' => 'Ꙏ', 'ꙑ' => 'Ꙑ', 'ꙓ' => 'Ꙓ', 'ꙕ' => 'Ꙕ', 'ꙗ' => 'Ꙗ', 'ꙙ' => 'Ꙙ', 'ꙛ' => 'Ꙛ', 'ꙝ' => 'Ꙝ', 'ꙟ' => 'Ꙟ', 'ꙡ' => 'Ꙡ', 'ꙣ' => 'Ꙣ', 'ꙥ' => 'Ꙥ', 'ꙧ' => 'Ꙧ', 'ꙩ' => 'Ꙩ', 'ꙫ' => 'Ꙫ', 'ꙭ' => 'Ꙭ', 'ꚁ' => 'Ꚁ', 'ꚃ' => 'Ꚃ', 'ꚅ' => 'Ꚅ', 'ꚇ' => 'Ꚇ', 'ꚉ' => 'Ꚉ', 'ꚋ' => 'Ꚋ', 'ꚍ' => 'Ꚍ', 'ꚏ' => 'Ꚏ', 'ꚑ' => 'Ꚑ', 'ꚓ' => 'Ꚓ', 'ꚕ' => 'Ꚕ', 'ꚗ' => 'Ꚗ', 'ꚙ' => 'Ꚙ', 'ꚛ' => 'Ꚛ', 'ꜣ' => 'Ꜣ', 'ꜥ' => 'Ꜥ', 'ꜧ' => 'Ꜧ', 'ꜩ' => 'Ꜩ', 'ꜫ' => 'Ꜫ', 'ꜭ' => 'Ꜭ', 'ꜯ' => 'Ꜯ', 'ꜳ' => 'Ꜳ', 'ꜵ' => 'Ꜵ', 'ꜷ' => 'Ꜷ', 'ꜹ' => 'Ꜹ', 'ꜻ' => 'Ꜻ', 'ꜽ' => 'Ꜽ', 'ꜿ' => 'Ꜿ', 'ꝁ' => 'Ꝁ', 'ꝃ' => 'Ꝃ', 'ꝅ' => 'Ꝅ', 'ꝇ' => 'Ꝇ', 'ꝉ' => 'Ꝉ', 'ꝋ' => 'Ꝋ', 'ꝍ' => 'Ꝍ', 'ꝏ' => 'Ꝏ', 'ꝑ' => 'Ꝑ', 'ꝓ' => 'Ꝓ', 'ꝕ' => 'Ꝕ', 'ꝗ' => 'Ꝗ', 'ꝙ' => 'Ꝙ', 'ꝛ' => 'Ꝛ', 'ꝝ' => 'Ꝝ', 'ꝟ' => 'Ꝟ', 'ꝡ' => 'Ꝡ', 'ꝣ' => 'Ꝣ', 'ꝥ' => 'Ꝥ', 'ꝧ' => 'Ꝧ', 'ꝩ' => 'Ꝩ', 'ꝫ' => 'Ꝫ', 'ꝭ' => 'Ꝭ', 'ꝯ' => 'Ꝯ', 'ꝺ' => 'Ꝺ', 'ꝼ' => 'Ꝼ', 'ꝿ' => 'Ꝿ', 'ꞁ' => 'Ꞁ', 'ꞃ' => 'Ꞃ', 'ꞅ' => 'Ꞅ', 'ꞇ' => 'Ꞇ', 'ꞌ' => 'Ꞌ', 'ꞑ' => 'Ꞑ', 'ꞓ' => 'Ꞓ', 'ꞔ' => 'Ꞔ', 'ꞗ' => 'Ꞗ', 'ꞙ' => 'Ꞙ', 'ꞛ' => 'Ꞛ', 'ꞝ' => 'Ꞝ', 'ꞟ' => 'Ꞟ', 'ꞡ' => 'Ꞡ', 'ꞣ' => 'Ꞣ', 'ꞥ' => 'Ꞥ', 'ꞧ' => 'Ꞧ', 'ꞩ' => 'Ꞩ', 'ꞵ' => 'Ꞵ', 'ꞷ' => 'Ꞷ', 'ꞹ' => 'Ꞹ', 'ꞻ' => 'Ꞻ', 'ꞽ' => 'Ꞽ', 'ꞿ' => 'Ꞿ', 'ꟃ' => 'Ꟃ', 'ꟈ' => 'Ꟈ', 'ꟊ' => 'Ꟊ', 'ꟶ' => 'Ꟶ', 'ꭓ' => 'Ꭓ', 'ꭰ' => 'Ꭰ', 'ꭱ' => 'Ꭱ', 'ꭲ' => 'Ꭲ', 'ꭳ' => 'Ꭳ', 'ꭴ' => 'Ꭴ', 'ꭵ' => 'Ꭵ', 'ꭶ' => 'Ꭶ', 'ꭷ' => 'Ꭷ', 'ꭸ' => 'Ꭸ', 'ꭹ' => 'Ꭹ', 'ꭺ' => 'Ꭺ', 'ꭻ' => 'Ꭻ', 'ꭼ' => 'Ꭼ', 'ꭽ' => 'Ꭽ', 'ꭾ' => 'Ꭾ', 'ꭿ' => 'Ꭿ', 'ꮀ' => 'Ꮀ', 'ꮁ' => 'Ꮁ', 'ꮂ' => 'Ꮂ', 'ꮃ' => 'Ꮃ', 'ꮄ' => 'Ꮄ', 'ꮅ' => 'Ꮅ', 'ꮆ' => 'Ꮆ', 'ꮇ' => 'Ꮇ', 'ꮈ' => 'Ꮈ', 'ꮉ' => 'Ꮉ', 'ꮊ' => 'Ꮊ', 'ꮋ' => 'Ꮋ', 'ꮌ' => 'Ꮌ', 'ꮍ' => 'Ꮍ', 'ꮎ' => 'Ꮎ', 'ꮏ' => 'Ꮏ', 'ꮐ' => 'Ꮐ', 'ꮑ' => 'Ꮑ', 'ꮒ' => 'Ꮒ', 'ꮓ' => 'Ꮓ', 'ꮔ' => 'Ꮔ', 'ꮕ' => 'Ꮕ', 'ꮖ' => 'Ꮖ', 'ꮗ' => 'Ꮗ', 'ꮘ' => 'Ꮘ', 'ꮙ' => 'Ꮙ', 'ꮚ' => 'Ꮚ', 'ꮛ' => 'Ꮛ', 'ꮜ' => 'Ꮜ', 'ꮝ' => 'Ꮝ', 'ꮞ' => 'Ꮞ', 'ꮟ' => 'Ꮟ', 'ꮠ' => 'Ꮠ', 'ꮡ' => 'Ꮡ', 'ꮢ' => 'Ꮢ', 'ꮣ' => 'Ꮣ', 'ꮤ' => 'Ꮤ', 'ꮥ' => 'Ꮥ', 'ꮦ' => 'Ꮦ', 'ꮧ' => 'Ꮧ', 'ꮨ' => 'Ꮨ', 'ꮩ' => 'Ꮩ', 'ꮪ' => 'Ꮪ', 'ꮫ' => 'Ꮫ', 'ꮬ' => 'Ꮬ', 'ꮭ' => 'Ꮭ', 'ꮮ' => 'Ꮮ', 'ꮯ' => 'Ꮯ', 'ꮰ' => 'Ꮰ', 'ꮱ' => 'Ꮱ', 'ꮲ' => 'Ꮲ', 'ꮳ' => 'Ꮳ', 'ꮴ' => 'Ꮴ', 'ꮵ' => 'Ꮵ', 'ꮶ' => 'Ꮶ', 'ꮷ' => 'Ꮷ', 'ꮸ' => 'Ꮸ', 'ꮹ' => 'Ꮹ', 'ꮺ' => 'Ꮺ', 'ꮻ' => 'Ꮻ', 'ꮼ' => 'Ꮼ', 'ꮽ' => 'Ꮽ', 'ꮾ' => 'Ꮾ', 'ꮿ' => 'Ꮿ', 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', '𐐨' => '𐐀', '𐐩' => '𐐁', '𐐪' => '𐐂', '𐐫' => '𐐃', '𐐬' => '𐐄', '𐐭' => '𐐅', '𐐮' => '𐐆', '𐐯' => '𐐇', '𐐰' => '𐐈', '𐐱' => '𐐉', '𐐲' => '𐐊', '𐐳' => '𐐋', '𐐴' => '𐐌', '𐐵' => '𐐍', '𐐶' => '𐐎', '𐐷' => '𐐏', '𐐸' => '𐐐', '𐐹' => '𐐑', '𐐺' => '𐐒', '𐐻' => '𐐓', '𐐼' => '𐐔', '𐐽' => '𐐕', '𐐾' => '𐐖', '𐐿' => '𐐗', '𐑀' => '𐐘', '𐑁' => '𐐙', '𐑂' => '𐐚', '𐑃' => '𐐛', '𐑄' => '𐐜', '𐑅' => '𐐝', '𐑆' => '𐐞', '𐑇' => '𐐟', '𐑈' => '𐐠', '𐑉' => '𐐡', '𐑊' => '𐐢', '𐑋' => '𐐣', '𐑌' => '𐐤', '𐑍' => '𐐥', '𐑎' => '𐐦', '𐑏' => '𐐧', '𐓘' => '𐒰', '𐓙' => '𐒱', '𐓚' => '𐒲', '𐓛' => '𐒳', '𐓜' => '𐒴', '𐓝' => '𐒵', '𐓞' => '𐒶', '𐓟' => '𐒷', '𐓠' => '𐒸', '𐓡' => '𐒹', '𐓢' => '𐒺', '𐓣' => '𐒻', '𐓤' => '𐒼', '𐓥' => '𐒽', '𐓦' => '𐒾', '𐓧' => '𐒿', '𐓨' => '𐓀', '𐓩' => '𐓁', '𐓪' => '𐓂', '𐓫' => '𐓃', '𐓬' => '𐓄', '𐓭' => '𐓅', '𐓮' => '𐓆', '𐓯' => '𐓇', '𐓰' => '𐓈', '𐓱' => '𐓉', '𐓲' => '𐓊', '𐓳' => '𐓋', '𐓴' => '𐓌', '𐓵' => '𐓍', '𐓶' => '𐓎', '𐓷' => '𐓏', '𐓸' => '𐓐', '𐓹' => '𐓑', '𐓺' => '𐓒', '𐓻' => '𐓓', '𐳀' => '𐲀', '𐳁' => '𐲁', '𐳂' => '𐲂', '𐳃' => '𐲃', '𐳄' => '𐲄', '𐳅' => '𐲅', '𐳆' => '𐲆', '𐳇' => '𐲇', '𐳈' => '𐲈', '𐳉' => '𐲉', '𐳊' => '𐲊', '𐳋' => '𐲋', '𐳌' => '𐲌', '𐳍' => '𐲍', '𐳎' => '𐲎', '𐳏' => '𐲏', '𐳐' => '𐲐', '𐳑' => '𐲑', '𐳒' => '𐲒', '𐳓' => '𐲓', '𐳔' => '𐲔', '𐳕' => '𐲕', '𐳖' => '𐲖', '𐳗' => '𐲗', '𐳘' => '𐲘', '𐳙' => '𐲙', '𐳚' => '𐲚', '𐳛' => '𐲛', '𐳜' => '𐲜', '𐳝' => '𐲝', '𐳞' => '𐲞', '𐳟' => '𐲟', '𐳠' => '𐲠', '𐳡' => '𐲡', '𐳢' => '𐲢', '𐳣' => '𐲣', '𐳤' => '𐲤', '𐳥' => '𐲥', '𐳦' => '𐲦', '𐳧' => '𐲧', '𐳨' => '𐲨', '𐳩' => '𐲩', '𐳪' => '𐲪', '𐳫' => '𐲫', '𐳬' => '𐲬', '𐳭' => '𐲭', '𐳮' => '𐲮', '𐳯' => '𐲯', '𐳰' => '𐲰', '𐳱' => '𐲱', '𐳲' => '𐲲', '𑣀' => '𑢠', '𑣁' => '𑢡', '𑣂' => '𑢢', '𑣃' => '𑢣', '𑣄' => '𑢤', '𑣅' => '𑢥', '𑣆' => '𑢦', '𑣇' => '𑢧', '𑣈' => '𑢨', '𑣉' => '𑢩', '𑣊' => '𑢪', '𑣋' => '𑢫', '𑣌' => '𑢬', '𑣍' => '𑢭', '𑣎' => '𑢮', '𑣏' => '𑢯', '𑣐' => '𑢰', '𑣑' => '𑢱', '𑣒' => '𑢲', '𑣓' => '𑢳', '𑣔' => '𑢴', '𑣕' => '𑢵', '𑣖' => '𑢶', '𑣗' => '𑢷', '𑣘' => '𑢸', '𑣙' => '𑢹', '𑣚' => '𑢺', '𑣛' => '𑢻', '𑣜' => '𑢼', '𑣝' => '𑢽', '𑣞' => '𑢾', '𑣟' => '𑢿', '𖹠' => '𖹀', '𖹡' => '𖹁', '𖹢' => '𖹂', '𖹣' => '𖹃', '𖹤' => '𖹄', '𖹥' => '𖹅', '𖹦' => '𖹆', '𖹧' => '𖹇', '𖹨' => '𖹈', '𖹩' => '𖹉', '𖹪' => '𖹊', '𖹫' => '𖹋', '𖹬' => '𖹌', '𖹭' => '𖹍', '𖹮' => '𖹎', '𖹯' => '𖹏', '𖹰' => '𖹐', '𖹱' => '𖹑', '𖹲' => '𖹒', '𖹳' => '𖹓', '𖹴' => '𖹔', '𖹵' => '𖹕', '𖹶' => '𖹖', '𖹷' => '𖹗', '𖹸' => '𖹘', '𖹹' => '𖹙', '𖹺' => '𖹚', '𖹻' => '𖹛', '𖹼' => '𖹜', '𖹽' => '𖹝', '𖹾' => '𖹞', '𖹿' => '𖹟', '𞤢' => '𞤀', '𞤣' => '𞤁', '𞤤' => '𞤂', '𞤥' => '𞤃', '𞤦' => '𞤄', '𞤧' => '𞤅', '𞤨' => '𞤆', '𞤩' => '𞤇', '𞤪' => '𞤈', '𞤫' => '𞤉', '𞤬' => '𞤊', '𞤭' => '𞤋', '𞤮' => '𞤌', '𞤯' => '𞤍', '𞤰' => '𞤎', '𞤱' => '𞤏', '𞤲' => '𞤐', '𞤳' => '𞤑', '𞤴' => '𞤒', '𞤵' => '𞤓', '𞤶' => '𞤔', '𞤷' => '𞤕', '𞤸' => '𞤖', '𞤹' => '𞤗', '𞤺' => '𞤘', '𞤻' => '𞤙', '𞤼' => '𞤚', '𞤽' => '𞤛', '𞤾' => '𞤜', '𞤿' => '𞤝', '𞥀' => '𞤞', '𞥁' => '𞤟', '𞥂' => '𞤠', '𞥃' => '𞤡', 'ß' => 'SS', 'ff' => 'FF', 'fi' => 'FI', 'fl' => 'FL', 'ffi' => 'FFI', 'ffl' => 'FFL', 'ſt' => 'ST', 'st' => 'ST', 'և' => 'ԵՒ', 'ﬓ' => 'ՄՆ', 'ﬔ' => 'ՄԵ', 'ﬕ' => 'ՄԻ', 'ﬖ' => 'ՎՆ', 'ﬗ' => 'ՄԽ', 'ʼn' => 'ʼN', 'ΐ' => 'Ϊ́', 'ΰ' => 'Ϋ́', 'ǰ' => 'J̌', 'ẖ' => 'H̱', 'ẗ' => 'T̈', 'ẘ' => 'W̊', 'ẙ' => 'Y̊', 'ẚ' => 'Aʾ', 'ὐ' => 'Υ̓', 'ὒ' => 'Υ̓̀', 'ὔ' => 'Υ̓́', 'ὖ' => 'Υ̓͂', 'ᾶ' => 'Α͂', 'ῆ' => 'Η͂', 'ῒ' => 'Ϊ̀', 'ΐ' => 'Ϊ́', 'ῖ' => 'Ι͂', 'ῗ' => 'Ϊ͂', 'ῢ' => 'Ϋ̀', 'ΰ' => 'Ϋ́', 'ῤ' => 'Ρ̓', 'ῦ' => 'Υ͂', 'ῧ' => 'Ϋ͂', 'ῶ' => 'Ω͂', 'ᾈ' => 'ἈΙ', 'ᾉ' => 'ἉΙ', 'ᾊ' => 'ἊΙ', 'ᾋ' => 'ἋΙ', 'ᾌ' => 'ἌΙ', 'ᾍ' => 'ἍΙ', 'ᾎ' => 'ἎΙ', 'ᾏ' => 'ἏΙ', 'ᾘ' => 'ἨΙ', 'ᾙ' => 'ἩΙ', 'ᾚ' => 'ἪΙ', 'ᾛ' => 'ἫΙ', 'ᾜ' => 'ἬΙ', 'ᾝ' => 'ἭΙ', 'ᾞ' => 'ἮΙ', 'ᾟ' => 'ἯΙ', 'ᾨ' => 'ὨΙ', 'ᾩ' => 'ὩΙ', 'ᾪ' => 'ὪΙ', 'ᾫ' => 'ὫΙ', 'ᾬ' => 'ὬΙ', 'ᾭ' => 'ὭΙ', 'ᾮ' => 'ὮΙ', 'ᾯ' => 'ὯΙ', 'ᾼ' => 'ΑΙ', 'ῌ' => 'ΗΙ', 'ῼ' => 'ΩΙ', 'ᾲ' => 'ᾺΙ', 'ᾴ' => 'ΆΙ', 'ῂ' => 'ῊΙ', 'ῄ' => 'ΉΙ', 'ῲ' => 'ῺΙ', 'ῴ' => 'ΏΙ', 'ᾷ' => 'Α͂Ι', 'ῇ' => 'Η͂Ι', 'ῷ' => 'Ω͂Ι', ); PK!v7-ӽ .vendor/symfony/polyfill-mbstring/bootstrap.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } } if (!function_exists('mb_decode_mimeheader')) { function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } } if (!function_exists('mb_encode_mimeheader')) { function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } } if (!function_exists('mb_decode_numericentity')) { function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } } if (!function_exists('mb_encode_numericentity')) { function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } } if (!function_exists('mb_convert_case')) { function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } } if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } } if (!function_exists('mb_language')) { function mb_language($language = null) { return p\Mbstring::mb_language($language); } } if (!function_exists('mb_list_encodings')) { function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } } if (!function_exists('mb_encoding_aliases')) { function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } } if (!function_exists('mb_check_encoding')) { function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } } if (!function_exists('mb_detect_encoding')) { function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } } if (!function_exists('mb_detect_order')) { function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } } if (!function_exists('mb_parse_str')) { function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; } } if (!function_exists('mb_strlen')) { function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } } if (!function_exists('mb_strpos')) { function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strtolower')) { function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } } if (!function_exists('mb_strtoupper')) { function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } } if (!function_exists('mb_substitute_character')) { function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } } if (!function_exists('mb_substr')) { function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } } if (!function_exists('mb_stripos')) { function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_stristr')) { function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrchr')) { function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrichr')) { function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strripos')) { function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strrpos')) { function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strstr')) { function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_get_info')) { function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } } if (!function_exists('mb_http_output')) { function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } } if (!function_exists('mb_strwidth')) { function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } } if (!function_exists('mb_substr_count')) { function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } } if (!function_exists('mb_output_handler')) { function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } } if (!function_exists('mb_http_input')) { function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); } } if (!function_exists('mb_convert_variables')) { function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); } } if (!function_exists('mb_ord')) { function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } } if (!function_exists('mb_chr')) { function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } } if (!function_exists('mb_scrub')) { function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } } if (!function_exists('mb_str_split')) { function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } } if (!function_exists('mb_str_pad')) { function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } } if (!function_exists('mb_ucfirst')) { function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); } } if (!function_exists('mb_lcfirst')) { function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); } } if (!function_exists('mb_trim')) { function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); } } if (!function_exists('mb_ltrim')) { function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); } } if (!function_exists('mb_rtrim')) { function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); } } if (extension_loaded('mbstring')) { return; } if (!defined('MB_CASE_UPPER')) { define('MB_CASE_UPPER', 0); } if (!defined('MB_CASE_LOWER')) { define('MB_CASE_LOWER', 1); } if (!defined('MB_CASE_TITLE')) { define('MB_CASE_TITLE', 2); } PK!J'D.vendor/symfony/polyfill-mbstring/composer.jsonnu[{ "name": "symfony/polyfill-mbstring", "type": "library", "description": "Symfony polyfill for the Mbstring extension", "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "provide": { "ext-mbstring": "*" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" }, "files": [ "bootstrap.php" ] }, "suggest": { "ext-mbstring": "For best performance" }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } PK!H,,(vendor/symfony/polyfill-mbstring/LICENSEnu[Copyright (c) 2015-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!ŮJJ-vendor/symfony/polyfill-mbstring/Mbstring.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Mbstring; /** * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. * * Implemented: * - mb_chr - Returns a specific character from its Unicode code point * - mb_convert_encoding - Convert character encoding * - mb_convert_variables - Convert character code in variable(s) * - mb_decode_mimeheader - Decode string in MIME header field * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED * - mb_decode_numericentity - Decode HTML numeric string reference to character * - mb_encode_numericentity - Encode character to HTML numeric string reference * - mb_convert_case - Perform case folding on a string * - mb_detect_encoding - Detect character encoding * - mb_get_info - Get internal settings of mbstring * - mb_http_input - Detect HTTP input character encoding * - mb_http_output - Set/Get HTTP output character encoding * - mb_internal_encoding - Set/Get internal character encoding * - mb_list_encodings - Returns an array of all supported encodings * - mb_ord - Returns the Unicode code point of a character * - mb_output_handler - Callback function converts character encoding in output buffer * - mb_scrub - Replaces ill-formed byte sequences with substitute characters * - mb_strlen - Get string length * - mb_strpos - Find position of first occurrence of string in a string * - mb_strrpos - Find position of last occurrence of a string in a string * - mb_str_split - Convert a string to an array * - mb_strtolower - Make a string lowercase * - mb_strtoupper - Make a string uppercase * - mb_substitute_character - Set/Get substitution character * - mb_substr - Get part of string * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive * - mb_stristr - Finds first occurrence of a string within another, case insensitive * - mb_strrchr - Finds the last occurrence of a character in a string within another * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive * - mb_strstr - Finds first occurrence of a string within another * - mb_strwidth - Return width of string * - mb_substr_count - Count the number of substring occurrences * - mb_ucfirst - Make a string's first character uppercase * - mb_lcfirst - Make a string's first character lowercase * - mb_trim - Strip whitespace (or other characters) from the beginning and end of a string * - mb_ltrim - Strip whitespace (or other characters) from the beginning of a string * - mb_rtrim - Strip whitespace (or other characters) from the end of a string * * Not implemented: * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) * - mb_ereg_* - Regular expression with multibyte support * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable * - mb_preferred_mime_name - Get MIME charset string * - mb_regex_encoding - Returns current encoding for multibyte regex as string * - mb_regex_set_options - Set/Get the default options for mbregex functions * - mb_send_mail - Send encoded mail * - mb_split - Split multibyte string using regular expression * - mb_strcut - Get part of string * - mb_strimwidth - Get truncated string with specified width * * @author Nicolas Grekas * * @internal */ final class Mbstring { public const MB_CASE_FOLD = \PHP_INT_MAX; private const SIMPLE_CASE_FOLD = [ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; private static $encodingList = ['ASCII', 'UTF-8']; private static $language = 'neutral'; private static $internalEncoding = 'UTF-8'; public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) { if (\is_array($s)) { $r = []; foreach ($s as $str) { $r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding); } return $r; } if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) { $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); } else { $fromEncoding = self::getEncoding($fromEncoding); } $toEncoding = self::getEncoding($toEncoding); if ('BASE64' === $fromEncoding) { $s = base64_decode($s); $fromEncoding = $toEncoding; } if ('BASE64' === $toEncoding) { return base64_encode($s); } if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { $fromEncoding = 'Windows-1252'; } if ('UTF-8' !== $fromEncoding) { $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); } return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s); } if ('HTML-ENTITIES' === $fromEncoding) { $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8'); $fromEncoding = 'UTF-8'; } return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); } public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars) { $ok = true; array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { $ok = false; } }); return $ok ? $fromEncoding : false; } public static function mb_decode_mimeheader($s) { return iconv_mime_decode($s, 2, self::$internalEncoding); } public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) { trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING); } public static function mb_decode_numericentity($s, $convmap, $encoding = null) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; } if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); return ''; // Instead of null (cf. mb_encode_numericentity). } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $cnt = floor(\count($convmap) / 4) * 4; for ($i = 0; $i < $cnt; $i += 4) { // collector_decode_htmlnumericentity ignores $convmap[$i + 3] $convmap[$i] += $convmap[$i + 2]; $convmap[$i + 1] += $convmap[$i + 2]; } $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; for ($i = 0; $i < $cnt; $i += 4) { if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { return self::mb_chr($c - $convmap[$i + 2]); } } return $m[0]; }, $s); if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) { trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; } if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING); return null; // Instead of '' (cf. mb_decode_numericentity). } if (null !== $is_hex && !\is_scalar($is_hex)) { trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING); return null; } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $cnt = floor(\count($convmap) / 4) * 4; $i = 0; $len = \strlen($s); $result = ''; while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; $c = self::mb_ord($uchr); for ($j = 0; $j < $cnt; $j += 4) { if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; continue 2; } } $result .= $uchr; } if (null === $encoding) { return $result; } return iconv('UTF-8', $encoding.'//IGNORE', $result); } public static function mb_convert_case($s, $mode, $encoding = null) { $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } if (\MB_CASE_TITLE == $mode) { static $titleRegexp = null; if (null === $titleRegexp) { $titleRegexp = self::getData('titleCaseRegexp'); } $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s); } else { if (\MB_CASE_UPPER == $mode) { static $upper = null; if (null === $upper) { $upper = self::getData('upperCase'); } $map = $upper; } else { if (self::MB_CASE_FOLD === $mode) { static $caseFolding = null; if (null === $caseFolding) { $caseFolding = self::getData('caseFolding'); } $s = strtr($s, $caseFolding); } static $lower = null; if (null === $lower) { $lower = self::getData('lowerCase'); } $map = $lower; } static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; $i = 0; $len = \strlen($s); while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if (isset($map[$uchr])) { $uchr = $map[$uchr]; $nlen = \strlen($uchr); if ($nlen == $ulen) { $nlen = $i; do { $s[--$nlen] = $uchr[--$ulen]; } while ($ulen); } else { $s = substr_replace($s, $uchr, $i - $ulen, $ulen); $len += $nlen - $ulen; $i += $nlen - $ulen; } } } } if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_internal_encoding($encoding = null) { if (null === $encoding) { return self::$internalEncoding; } $normalizedEncoding = self::getEncoding($encoding); if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) { self::$internalEncoding = $normalizedEncoding; return true; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding)); } public static function mb_language($lang = null) { if (null === $lang) { return self::$language; } switch ($normalizedLang = strtolower($lang)) { case 'uni': case 'neutral': self::$language = $normalizedLang; return true; } if (80000 > \PHP_VERSION_ID) { return false; } throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang)); } public static function mb_list_encodings() { return ['UTF-8']; } public static function mb_encoding_aliases($encoding) { switch (strtoupper($encoding)) { case 'UTF8': case 'UTF-8': return ['utf8']; } return false; } public static function mb_check_encoding($var = null, $encoding = null) { if (null === $encoding) { if (null === $var) { return false; } $encoding = self::$internalEncoding; } if (!\is_array($var)) { return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var); } foreach ($var as $key => $value) { if (!self::mb_check_encoding($key, $encoding)) { return false; } if (!self::mb_check_encoding($value, $encoding)) { return false; } } return true; } public static function mb_detect_encoding($str, $encodingList = null, $strict = false) { if (null === $encodingList) { $encodingList = self::$encodingList; } else { if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); } foreach ($encodingList as $enc) { switch ($enc) { case 'ASCII': if (!preg_match('/[\x80-\xFF]/', $str)) { return $enc; } break; case 'UTF8': case 'UTF-8': if (preg_match('//u', $str)) { return 'UTF-8'; } break; default: if (0 === strncmp($enc, 'ISO-8859-', 9)) { return $enc; } } } return false; } public static function mb_detect_order($encodingList = null) { if (null === $encodingList) { return self::$encodingList; } if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); foreach ($encodingList as $enc) { switch ($enc) { default: if (strncmp($enc, 'ISO-8859-', 9)) { return false; } // no break case 'ASCII': case 'UTF8': case 'UTF-8': } } self::$encodingList = $encodingList; return true; } public static function mb_strlen($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return \strlen($s); } return @iconv_strlen($s, $encoding); } public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strpos($haystack, $needle, $offset); } $needle = (string) $needle; if ('' === $needle) { if (80000 > \PHP_VERSION_ID) { trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING); return false; } return 0; } return iconv_strpos($haystack, $needle, $offset, $encoding); } public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strrpos($haystack, $needle, $offset); } if ($offset != (int) $offset) { $offset = 0; } elseif ($offset = (int) $offset) { if ($offset < 0) { if (0 > $offset += self::mb_strlen($needle)) { $haystack = self::mb_substr($haystack, 0, $offset, $encoding); } $offset = 0; } else { $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); } } $pos = '' !== $needle || 80000 > \PHP_VERSION_ID ? iconv_strrpos($haystack, $needle, $encoding) : self::mb_strlen($haystack, $encoding); return false !== $pos ? $offset + $pos : false; } public static function mb_str_split($string, $split_length = 1, $encoding = null) { if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING); return null; } if (1 > $split_length = (int) $split_length) { if (80000 > \PHP_VERSION_ID) { trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); return false; } throw new \ValueError('Argument #2 ($length) must be greater than 0'); } if (null === $encoding) { $encoding = mb_internal_encoding(); } if ('UTF-8' === $encoding = self::getEncoding($encoding)) { $rx = '/('; while (65535 < $split_length) { $rx .= '.{65535}'; $split_length -= 65535; } $rx .= '.{'.$split_length.'})/us'; return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); } $result = []; $length = mb_strlen($string, $encoding); for ($i = 0; $i < $length; $i += $split_length) { $result[] = mb_substr($string, $i, $split_length, $encoding); } return $result; } public static function mb_strtolower($s, $encoding = null) { return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding); } public static function mb_strtoupper($s, $encoding = null) { return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding); } public static function mb_substitute_character($c = null) { if (null === $c) { return 'none'; } if (0 === strcasecmp($c, 'none')) { return true; } if (80000 > \PHP_VERSION_ID) { return false; } if (\is_int($c) || 'long' === $c || 'entity' === $c) { return false; } throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint'); } public static function mb_substr($s, $start, $length = null, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return (string) substr($s, $start, null === $length ? 2147483647 : $length); } if ($start < 0) { $start = iconv_strlen($s, $encoding) + $start; if ($start < 0) { $start = 0; } } if (null === $length) { $length = 2147483647; } elseif ($length < 0) { $length = iconv_strlen($s, $encoding) + $length - $start; if ($length < 0) { return ''; } } return (string) iconv_substr($s, $start, $length, $encoding); } public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [ self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding), self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding), ]); return self::mb_strpos($haystack, $needle, $offset, $encoding); } public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) { $pos = self::mb_stripos($haystack, $needle, 0, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { $pos = strrpos($haystack, $needle); } else { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = iconv_strrpos($haystack, $needle, $encoding); } return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = self::mb_strripos($haystack, $needle, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding); $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding); $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack); $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle); return self::mb_strrpos($haystack, $needle, $offset, $encoding); } public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) { $pos = strpos($haystack, $needle); if (false === $pos) { return false; } if ($part) { return substr($haystack, 0, $pos); } return substr($haystack, $pos); } public static function mb_get_info($type = 'all') { $info = [ 'internal_encoding' => self::$internalEncoding, 'http_output' => 'pass', 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', 'func_overload' => 0, 'func_overload_list' => 'no overload', 'mail_charset' => 'UTF-8', 'mail_header_encoding' => 'BASE64', 'mail_body_encoding' => 'BASE64', 'illegal_chars' => 0, 'encoding_translation' => 'Off', 'language' => self::$language, 'detect_order' => self::$encodingList, 'substitute_character' => 'none', 'strict_detection' => 'Off', ]; if ('all' === $type) { return $info; } if (isset($info[$type])) { return $info[$type]; } return false; } public static function mb_http_input($type = '') { return false; } public static function mb_http_output($encoding = null) { return null !== $encoding ? 'pass' === $encoding : 'pass'; } public static function mb_strwidth($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('UTF-8' !== $encoding) { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); return ($wide << 1) + iconv_strlen($s, 'UTF-8'); } public static function mb_substr_count($haystack, $needle, $encoding = null) { return substr_count($haystack, $needle); } public static function mb_output_handler($contents, $status) { return $contents; } public static function mb_chr($code, $encoding = null) { if (0x80 > $code %= 0x200000) { $s = \chr($code); } elseif (0x800 > $code) { $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, $encoding, 'UTF-8'); } return $s; } public static function mb_ord($s, $encoding = null) { if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, 'UTF-8', $encoding); } if (1 === \strlen($s)) { return \ord($s); } $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; if (0xF0 <= $code) { return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; } if (0xE0 <= $code) { return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; } if (0xC0 <= $code) { return (($code - 0xC0) << 6) + $s[2] - 0x80; } return $code; } public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string { if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); } if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given'); } if (self::mb_strlen($pad_string, $encoding) <= 0) { throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); } $paddingRequired = $length - self::mb_strlen($string, $encoding); if ($paddingRequired < 1) { return $string; } switch ($pad_type) { case \STR_PAD_LEFT: return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; case \STR_PAD_RIGHT: return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); default: $leftPaddingLength = floor($paddingRequired / 2); $rightPaddingLength = $paddingRequired - $leftPaddingLength; return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); } } public static function mb_ucfirst(string $string, ?string $encoding = null): string { if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); } $firstChar = mb_substr($string, 0, 1, $encoding); $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); return $firstChar.mb_substr($string, 1, null, $encoding); } public static function mb_lcfirst(string $string, ?string $encoding = null): string { if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given'); } $firstChar = mb_substr($string, 0, 1, $encoding); $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); return $firstChar.mb_substr($string, 1, null, $encoding); } private static function getSubpart($pos, $part, $haystack, $encoding) { if (false === $pos) { return false; } if ($part) { return self::mb_substr($haystack, 0, $pos, $encoding); } return self::mb_substr($haystack, $pos, null, $encoding); } private static function html_encoding_callback(array $m) { $i = 1; $entities = ''; $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8')); while (isset($m[$i])) { if (0x80 > $m[$i]) { $entities .= \chr($m[$i++]); continue; } if (0xF0 <= $m[$i]) { $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } elseif (0xE0 <= $m[$i]) { $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } else { $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; } $entities .= '&#'.$c.';'; } return $entities; } private static function title_case(array $s) { return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } private static function getEncoding($encoding) { if (null === $encoding) { return self::$internalEncoding; } if ('UTF-8' === $encoding) { return 'UTF-8'; } $encoding = strtoupper($encoding); if ('8BIT' === $encoding || 'BINARY' === $encoding) { return 'CP850'; } if ('UTF8' === $encoding) { return 'UTF-8'; } return $encoding; } public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); } public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); } public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return self::mb_internal_trim('{[%s]+$}D', $string, $characters, $encoding, __FUNCTION__); } private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string { if (null === $encoding) { $encoding = self::mb_internal_encoding(); } else { self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given'); } if ('' === $characters) { return null === $encoding ? $string : self::mb_convert_encoding($string, $encoding); } if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $string)) { $string = @iconv('UTF-8', 'UTF-8//IGNORE', $string); } if (null !== $characters && !preg_match('//u', $characters)) { $characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters); } } else { $string = iconv($encoding, 'UTF-8//IGNORE', $string); if (null !== $characters) { $characters = iconv($encoding, 'UTF-8//IGNORE', $characters); } } if (null === $characters) { $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; } else { $characters = preg_quote($characters); } $string = preg_replace(sprintf($regex, $characters), '', $string); if (null === $encoding) { return $string; } return iconv('UTF-8', $encoding.'//IGNORE', $string); } private static function assertEncoding(string $encoding, string $errorFormat): void { try { $validEncoding = @self::mb_check_encoding('', $encoding); } catch (\ValueError $e) { throw new \ValueError(sprintf($errorFormat, $encoding)); } // BC for PHP 7.3 and lower if (!$validEncoding) { throw new \ValueError(sprintf($errorFormat, $encoding)); } } } PK!A`rr*vendor/symfony/polyfill-mbstring/README.mdnu[Symfony Polyfill / Mbstring =========================== This component provides a partial, native PHP implementation for the [Mbstring](https://php.net/mbstring) extension. More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). PK!8SEE?vendor/symfony/polyfill-php73/Resources/stubs/JsonException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 70300) { class JsonException extends Exception { } } PK!|+vendor/symfony/polyfill-php73/bootstrap.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php73 as p; if (\PHP_VERSION_ID >= 70300) { return; } if (!function_exists('is_countable')) { function is_countable($value) { return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement; } } if (!function_exists('hrtime')) { require_once __DIR__.'/Php73.php'; p\Php73::$startAt = (int) microtime(true); function hrtime($as_number = false) { return p\Php73::hrtime($as_number); } } if (!function_exists('array_key_first')) { function array_key_first(array $array) { foreach ($array as $key => $value) { return $key; } } } if (!function_exists('array_key_last')) { function array_key_last(array $array) { return key(array_slice($array, -1, 1, true)); } } PK!t+vendor/symfony/polyfill-php73/composer.jsonnu[{ "name": "symfony/polyfill-php73", "type": "library", "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php73\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } PK!,,%vendor/symfony/polyfill-php73/LICENSEnu[Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!J * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php73; /** * @author Gabriel Caruso * @author Ion Bazan * * @internal */ final class Php73 { public static $startAt = 1533462603; /** * @param bool $asNum * * @return array|float|int */ public static function hrtime($asNum = false) { $ns = microtime(false); $s = substr($ns, 11) - self::$startAt; $ns = 1E9 * (float) $ns; if ($asNum) { $ns += $s * 1E9; return \PHP_INT_SIZE === 4 ? $ns : (int) $ns; } return [$s, (int) $ns]; } } PK!m//'vendor/symfony/polyfill-php73/README.mdnu[Symfony Polyfill / Php73 ======================== This component provides functions added to PHP 7.3 core: - [`array_key_first`](https://php.net/array_key_first) - [`array_key_last`](https://php.net/array_key_last) - [`hrtime`](https://php.net/function.hrtime) - [`is_countable`](https://php.net/is_countable) - [`JsonException`](https://php.net/JsonException) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). PK!MK<;vendor/symfony/polyfill-php80/Resources/stubs/Attribute.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #[Attribute(Attribute::TARGET_CLASS)] final class Attribute { public const TARGET_CLASS = 1; public const TARGET_FUNCTION = 2; public const TARGET_METHOD = 4; public const TARGET_PROPERTY = 8; public const TARGET_CLASS_CONSTANT = 16; public const TARGET_PARAMETER = 32; public const TARGET_ALL = 63; public const IS_REPEATABLE = 64; /** @var int */ public $flags; public function __construct(int $flags = self::TARGET_ALL) { $this->flags = $flags; } } PK!=7T8ww:vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) { class PhpToken extends Symfony\Polyfill\Php80\PhpToken { } } PK!t]\ڌ<vendor/symfony/polyfill-php80/Resources/stubs/Stringable.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { interface Stringable { /** * @return string */ public function __toString(); } } PK!ֈ+GGEvendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { class UnhandledMatchError extends Error { } } PK!g>><vendor/symfony/polyfill-php80/Resources/stubs/ValueError.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80000) { class ValueError extends Error { } } PK!.+vendor/symfony/polyfill-php80/bootstrap.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php80 as p; if (\PHP_VERSION_ID >= 80000) { return; } if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) { define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN); } if (!function_exists('fdiv')) { function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); } } if (!function_exists('preg_last_error_msg')) { function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); } } if (!function_exists('str_contains')) { function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_starts_with')) { function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('str_ends_with')) { function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); } } if (!function_exists('get_debug_type')) { function get_debug_type($value): string { return p\Php80::get_debug_type($value); } } if (!function_exists('get_resource_id')) { function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); } } PK!Jm+vendor/symfony/polyfill-php80/composer.jsonnu[{ "name": "symfony/polyfill-php80", "type": "library", "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Ion Bazan", "email": "ion.bazan@gmail.com" }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php80\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } PK! K,,%vendor/symfony/polyfill-php80/LICENSEnu[Copyright (c) 2020-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!cH 'vendor/symfony/polyfill-php80/Php80.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php80; /** * @author Ion Bazan * @author Nico Oelgart * @author Nicolas Grekas * * @internal */ final class Php80 { public static function fdiv(float $dividend, float $divisor): float { return @($dividend / $divisor); } public static function get_debug_type($value): string { switch (true) { case null === $value: return 'null'; case \is_bool($value): return 'bool'; case \is_string($value): return 'string'; case \is_array($value): return 'array'; case \is_int($value): return 'int'; case \is_float($value): return 'float'; case \is_object($value): break; case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class'; default: if (null === $type = @get_resource_type($value)) { return 'unknown'; } if ('Unknown' === $type) { $type = 'closed'; } return "resource ($type)"; } $class = \get_class($value); if (false === strpos($class, '@')) { return $class; } return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; } public static function get_resource_id($res): int { if (!\is_resource($res) && null === @get_resource_type($res)) { throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res))); } return (int) $res; } public static function preg_last_error_msg(): string { switch (preg_last_error()) { case \PREG_INTERNAL_ERROR: return 'Internal error'; case \PREG_BAD_UTF8_ERROR: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; case \PREG_BAD_UTF8_OFFSET_ERROR: return 'The offset did not correspond to the beginning of a valid UTF-8 code point'; case \PREG_BACKTRACK_LIMIT_ERROR: return 'Backtrack limit exhausted'; case \PREG_RECURSION_LIMIT_ERROR: return 'Recursion limit exhausted'; case \PREG_JIT_STACKLIMIT_ERROR: return 'JIT stack limit exhausted'; case \PREG_NO_ERROR: return 'No error'; default: return 'Unknown error'; } } public static function str_contains(string $haystack, string $needle): bool { return '' === $needle || false !== strpos($haystack, $needle); } public static function str_starts_with(string $haystack, string $needle): bool { return 0 === strncmp($haystack, $needle, \strlen($needle)); } public static function str_ends_with(string $haystack, string $needle): bool { if ('' === $needle || $needle === $haystack) { return true; } if ('' === $haystack) { return false; } $needleLength = \strlen($needle); return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength); } } PK!]f*vendor/symfony/polyfill-php80/PhpToken.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php80; /** * @author Fedonyuk Anton * * @internal */ class PhpToken implements \Stringable { /** * @var int */ public $id; /** * @var string */ public $text; /** * @var int */ public $line; /** * @var int */ public $pos; public function __construct(int $id, string $text, int $line = -1, int $position = -1) { $this->id = $id; $this->text = $text; $this->line = $line; $this->pos = $position; } public function getTokenName(): ?string { if ('UNKNOWN' === $name = token_name($this->id)) { $name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text; } return $name; } /** * @param int|string|array $kind */ public function is($kind): bool { foreach ((array) $kind as $value) { if (\in_array($value, [$this->id, $this->text], true)) { return true; } } return false; } public function isIgnorable(): bool { return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true); } public function __toString(): string { return (string) $this->text; } /** * @return static[] */ public static function tokenize(string $code, int $flags = 0): array { $line = 1; $position = 0; $tokens = token_get_all($code, $flags); foreach ($tokens as $index => $token) { if (\is_string($token)) { $id = \ord($token); $text = $token; } else { [$id, $text, $line] = $token; } $tokens[$index] = new static($id, $text, $line, $position); $position += \strlen($text); } return $tokens; } } PK!"tF'vendor/symfony/polyfill-php80/README.mdnu[Symfony Polyfill / Php80 ======================== This component provides features added to PHP 8.0 core: - [`Stringable`](https://php.net/stringable) interface - [`fdiv`](https://php.net/fdiv) - [`ValueError`](https://php.net/valueerror) class - [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class - `FILTER_VALIDATE_BOOL` constant - [`get_debug_type`](https://php.net/get_debug_type) - [`PhpToken`](https://php.net/phptoken) class - [`preg_last_error_msg`](https://php.net/preg_last_error_msg) - [`str_contains`](https://php.net/str_contains) - [`str_starts_with`](https://php.net/str_starts_with) - [`str_ends_with`](https://php.net/str_ends_with) - [`get_resource_id`](https://php.net/get_resource_id) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). PK!JT@vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID >= 70400 && extension_loaded('curl')) { /** * @property string $data */ class CURLStringFile extends CURLFile { private $data; public function __construct(string $data, string $postname, string $mime = 'application/octet-stream') { $this->data = $data; parent::__construct('data://application/octet-stream;base64,'.base64_encode($data), $mime, $postname); } public function __set(string $name, $value): void { if ('data' !== $name) { $this->$name = $value; return; } if (is_object($value) ? !method_exists($value, '__toString') : !is_scalar($value)) { throw new TypeError('Cannot assign '.gettype($value).' to property CURLStringFile::$data of type string'); } $this->name = 'data://application/octet-stream;base64,'.base64_encode($value); } public function __isset(string $name): bool { return isset($this->$name); } public function &__get(string $name) { return $this->$name; } } } PK!5+Fvendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (\PHP_VERSION_ID < 80100) { #[Attribute(Attribute::TARGET_METHOD)] final class ReturnTypeWillChange { public function __construct() { } } } PK!<P+vendor/symfony/polyfill-php81/bootstrap.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Php81 as p; if (\PHP_VERSION_ID >= 80100) { return; } if (defined('MYSQLI_REFRESH_SLAVE') && !defined('MYSQLI_REFRESH_REPLICA')) { define('MYSQLI_REFRESH_REPLICA', 64); } if (!function_exists('array_is_list')) { function array_is_list(array $array): bool { return p\Php81::array_is_list($array); } } if (!function_exists('enum_exists')) { function enum_exists(string $enum, bool $autoload = true): bool { return $autoload && class_exists($enum) && false; } } PK!+vendor/symfony/polyfill-php81/composer.jsonnu[{ "name": "symfony/polyfill-php81", "type": "library", "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", "keywords": ["polyfill", "shim", "compatibility", "portable"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2" }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Php81\\": "" }, "files": [ "bootstrap.php" ], "classmap": [ "Resources/stubs" ] }, "minimum-stability": "dev", "extra": { "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } } } PK!0,,%vendor/symfony/polyfill-php81/LICENSEnu[Copyright (c) 2021-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!$'vendor/symfony/polyfill-php81/Php81.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Php81; /** * @author Nicolas Grekas * * @internal */ final class Php81 { public static function array_is_list(array $array): bool { if ([] === $array || $array === array_values($array)) { return true; } $nextKey = -1; foreach ($array as $k => $v) { if ($k !== ++$nextKey) { return false; } } return true; } } PK!'vendor/symfony/polyfill-php81/README.mdnu[Symfony Polyfill / Php81 ======================== This component provides features added to PHP 8.1 core: - [`array_is_list`](https://php.net/array_is_list) - [`enum_exists`](https://php.net/enum-exists) - [`MYSQLI_REFRESH_REPLICA`](https://php.net/mysqli.constants#constantmysqli-refresh-replica) constant - [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types) - [`CURLStringFile`](https://php.net/CURLStringFile) (but only if PHP >= 7.4 is used) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). License ======= This library is released under the [MIT license](LICENSE). PK!2pg>g><vendor/symfony/translation-contracts/Test/TranslatorTest.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Translation\Test; use PHPUnit\Framework\TestCase; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; /** * Test should cover all languages mentioned on http://translate.sourceforge.net/wiki/l10n/pluralforms * and Plural forms mentioned on http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms. * * See also https://developer.mozilla.org/en/Localization_and_Plurals which mentions 15 rules having a maximum of 6 forms. * The mozilla code is also interesting to check for. * * As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199 * * The goal to cover all languages is to far fetched so this test case is smaller. * * @author Clemens Tolboom clemens@build2be.nl */ class TranslatorTest extends TestCase { private $defaultLocale; protected function setUp(): void { $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); } protected function tearDown(): void { \Locale::setDefault($this->defaultLocale); } /** * @return TranslatorInterface */ public function getTranslator() { return new class() implements TranslatorInterface { use TranslatorTrait; }; } /** * @dataProvider getTransTests */ public function testTrans($expected, $id, $parameters) { $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($id, $parameters)); } /** * @dataProvider getTransChoiceTests */ public function testTransChoiceWithExplicitLocale($expected, $id, $number) { $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); } /** * @requires extension intl * * @dataProvider getTransChoiceTests */ public function testTransChoiceWithDefaultLocale($expected, $id, $number) { $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); } /** * @dataProvider getTransChoiceTests */ public function testTransChoiceWithEnUsPosix($expected, $id, $number) { $translator = $this->getTranslator(); $translator->setLocale('en_US_POSIX'); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); } public function testGetSetLocale() { $translator = $this->getTranslator(); $this->assertEquals('en', $translator->getLocale()); } /** * @requires extension intl */ public function testGetLocaleReturnsDefaultLocaleIfNotSet() { $translator = $this->getTranslator(); \Locale::setDefault('pt_BR'); $this->assertEquals('pt_BR', $translator->getLocale()); \Locale::setDefault('en'); $this->assertEquals('en', $translator->getLocale()); } public static function getTransTests() { return [ ['Symfony is great!', 'Symfony is great!', []], ['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']], ]; } public static function getTransChoiceTests() { return [ ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], ['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1], ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10], ['There are 0 apples', 'There is 1 apple|There are %count% apples', 0], ['There is 1 apple', 'There is 1 apple|There are %count% apples', 1], ['There are 10 apples', 'There is 1 apple|There are %count% apples', 10], // custom validation messages may be coded with a fixed value ['There are 2 apples', 'There are 2 apples', 2], ]; } /** * @dataProvider getInterval */ public function testInterval($expected, $number, $interval) { $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', ['%count%' => $number])); } public static function getInterval() { return [ ['foo', 3, '{1,2, 3 ,4}'], ['bar', 10, '{1,2, 3 ,4}'], ['bar', 3, '[1,2]'], ['foo', 1, '[1,2]'], ['foo', 2, '[1,2]'], ['bar', 1, ']1,2['], ['bar', 2, ']1,2['], ['foo', log(0), '[-Inf,2['], ['foo', -log(0), '[-2,+Inf]'], ]; } /** * @dataProvider getChooseTests */ public function testChoose($expected, $id, $number, $locale = null) { $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number], null, $locale)); } public function testReturnMessageIfExactlyOneStandardRuleIsGiven() { $translator = $this->getTranslator(); $this->assertEquals('There are two apples', $translator->trans('There are two apples', ['%count%' => 2])); } /** * @dataProvider getNonMatchingMessages */ public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) { $this->expectException(\InvalidArgumentException::class); $translator = $this->getTranslator(); $translator->trans($id, ['%count%' => $number]); } public static function getNonMatchingMessages() { return [ ['{0} There are no apples|{1} There is one apple', 2], ['{1} There is one apple|]1,Inf] There are %count% apples', 0], ['{1} There is one apple|]2,Inf] There are %count% apples', 2], ['{0} There are no apples|There is one apple', 2], ]; } public static function getChooseTests() { return [ ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], ['There are no apples', '{0}There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], ['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1], ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10], ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10], ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10], ['There are 0 apples', 'There is one apple|There are %count% apples', 0], ['There is one apple', 'There is one apple|There are %count% apples', 1], ['There are 10 apples', 'There is one apple|There are %count% apples', 10], ['There are 0 apples', 'one: There is one apple|more: There are %count% apples', 0], ['There is one apple', 'one: There is one apple|more: There are %count% apples', 1], ['There are 10 apples', 'one: There is one apple|more: There are %count% apples', 10], ['There are no apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 0], ['There is one apple', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 1], ['There are 10 apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10], ['', '{0}|{1} There is one apple|]1,Inf] There are %count% apples', 0], ['', '{0} There are no apples|{1}|]1,Inf] There are %count% apples', 1], // Indexed only tests which are Gettext PoFile* compatible strings. ['There are 0 apples', 'There is one apple|There are %count% apples', 0], ['There is one apple', 'There is one apple|There are %count% apples', 1], ['There are 2 apples', 'There is one apple|There are %count% apples', 2], // Tests for float numbers ['There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7], ['There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1], ['There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7], ['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0], ['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0], ['There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0], // Test texts with new-lines // with double-quotes and \n in id & double-quotes and actual newlines in text ["This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a new-line in it. Selector > 1.', 0], // with double-quotes and \n in id and single-quotes and actual newlines in text ["This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a new-line in it. Selector > 1.', 1], ["This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a new-line in it. Selector > 1.', 5], // with double-quotes and id split across lines ['This is a text with a new-line in it. Selector = 1.', '{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a new-line in it. Selector > 1.', 1], // with single-quotes and id split across lines ['This is a text with a new-line in it. Selector > 1.', '{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a new-line in it. Selector > 1.', 5], // with single-quotes and \n in text ['This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0], // with double-quotes and id split across lines ["This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1], // escape pipe ['This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0], // Empty plural set (2 plural forms) from a .PO file ['', '|', 1], // Empty plural set (3 plural forms) from a .PO file ['', '||', 1], // Floating values ['1.5 liters', '%count% liter|%count% liters', 1.5], ['1.5 litre', '%count% litre|%count% litres', 1.5, 'fr'], // Negative values ['-1 degree', '%count% degree|%count% degrees', -1], ['-1 degré', '%count% degré|%count% degrés', -1], ['-1.5 degrees', '%count% degree|%count% degrees', -1.5], ['-1.5 degré', '%count% degré|%count% degrés', -1.5, 'fr'], ['-2 degrees', '%count% degree|%count% degrees', -2], ['-2 degrés', '%count% degré|%count% degrés', -2], ]; } /** * @dataProvider failingLangcodes */ public function testFailedLangcodes($nplural, $langCodes) { $matrix = $this->generateTestData($langCodes); $this->validateMatrix($nplural, $matrix, false); } /** * @dataProvider successLangcodes */ public function testLangcodes($nplural, $langCodes) { $matrix = $this->generateTestData($langCodes); $this->validateMatrix($nplural, $matrix); } /** * This array should contain all currently known langcodes. * * As it is impossible to have this ever complete we should try as hard as possible to have it almost complete. * * @return array */ public static function successLangcodes() { return [ ['1', ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky']], ['2', ['nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM', 'en_US_POSIX']], ['3', ['be', 'bs', 'cs', 'hr']], ['4', ['cy', 'mt', 'sl']], ['6', ['ar']], ]; } /** * This array should be at least empty within the near future. * * This both depends on a complete list trying to add above as understanding * the plural rules of the current failing languages. * * @return array with nplural together with langcodes */ public static function failingLangcodes() { return [ ['1', ['fa']], ['2', ['jbo']], ['3', ['cbs']], ['4', ['gd', 'kw']], ['5', ['ga']], ]; } /** * We validate only on the plural coverage. Thus the real rules is not tested. * * @param string $nplural Plural expected * @param array $matrix Containing langcodes and their plural index values * @param bool $expectSuccess */ protected function validateMatrix($nplural, $matrix, $expectSuccess = true) { foreach ($matrix as $langCode => $data) { $indexes = array_flip($data); if ($expectSuccess) { $this->assertCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms."); } else { $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); } } } protected function generateTestData($langCodes) { $translator = new class() { use TranslatorTrait { getPluralizationRule as public; } }; $matrix = []; foreach ($langCodes as $langCode) { for ($count = 0; $count < 200; ++$count) { $plural = $translator->getPluralizationRule($count, $langCode); $matrix[$langCode][$count] = $plural; } } return $matrix; } } PK!h{#1vendor/symfony/translation-contracts/CHANGELOG.mdnu[CHANGELOG ========= The changelog is maintained for all Symfony contracts at the following URL: https://github.com/symfony/contracts/blob/main/CHANGELOG.md PK!"2vendor/symfony/translation-contracts/composer.jsonnu[{ "name": "symfony/translation-contracts", "type": "library", "description": "Generic abstractions related to translation", "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2.5" }, "suggest": { "symfony/translation-implementation": "" }, "autoload": { "psr-4": { "Symfony\\Contracts\\Translation\\": "" } }, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", "url": "https://github.com/symfony/contracts" } } } PK!,,,vendor/symfony/translation-contracts/LICENSEnu[Copyright (c) 2018-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!њq=vendor/symfony/translation-contracts/LocaleAwareInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Translation; interface LocaleAwareInterface { /** * Sets the current locale. * * @param string $locale The locale * * @throws \InvalidArgumentException If the locale contains invalid characters */ public function setLocale(string $locale); /** * Returns the current locale. * * @return string */ public function getLocale(); } PK! )TT.vendor/symfony/translation-contracts/README.mdnu[Symfony Translation Contracts ============================= A set of abstractions extracted out of the Symfony components. Can be used to build on semantics that the Symfony components proved useful - and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. PK!j>vendor/symfony/translation-contracts/TranslatableInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Translation; /** * @author Nicolas Grekas */ interface TranslatableInterface { public function trans(TranslatorInterface $translator, ?string $locale = null): string; } PK!IOl <vendor/symfony/translation-contracts/TranslatorInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Translation; /** * @author Fabien Potencier * * @method string getLocale() Returns the default locale */ interface TranslatorInterface { /** * Translates the given message. * * When a number is provided as a parameter named "%count%", the message is parsed for plural * forms and a translation is chosen according to this number using the following rules: * * Given a message with different plural translations separated by a * pipe (|), this method returns the correct portion of the message based * on the given number, locale and the pluralization rules in the message * itself. * * The message supports two different types of pluralization rules: * * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples * indexed: There is one apple|There are %count% apples * * The indexed solution can also contain labels (e.g. one: There is one apple). * This is purely for making the translations more clear - it does not * affect the functionality. * * The two methods can also be mixed: * {0} There are no apples|one: There is one apple|more: There are %count% apples * * An interval can represent a finite set of numbers: * {1,2,3,4} * * An interval can represent numbers between two numbers: * [1, +Inf] * ]-1,2[ * * The left delimiter can be [ (inclusive) or ] (exclusive). * The right delimiter can be [ (exclusive) or ] (inclusive). * Beside numbers, you can use -Inf and +Inf for the infinite. * * @see https://en.wikipedia.org/wiki/ISO_31-11 * * @param string $id The message id (may also be an object that can be cast to string) * @param array $parameters An array of parameters for the message * @param string|null $domain The domain for the message or null to use the default * @param string|null $locale The locale or null to use the default * * @return string * * @throws \InvalidArgumentException If the locale contains invalid characters */ public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null); } PK!ؐ!!8vendor/symfony/translation-contracts/TranslatorTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Contracts\Translation; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * A trait to help implement TranslatorInterface and LocaleAwareInterface. * * @author Fabien Potencier */ trait TranslatorTrait { private $locale; /** * {@inheritdoc} */ public function setLocale(string $locale) { $this->locale = $locale; } /** * {@inheritdoc} * * @return string */ public function getLocale() { return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { if (null === $id || '' === $id) { return ''; } if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) { return strtr($id, $parameters); } $number = (float) $parameters['%count%']; $locale = $locale ?: $this->getLocale(); $parts = []; if (preg_match('/^\|++$/', $id)) { $parts = explode('|', $id); } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { $parts = $matches[0]; } $intervalRegexp = <<<'EOF' /^(?P ({\s* (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) \s*}) | (?P[\[\]]) \s* (?P-Inf|\-?\d+(\.\d+)?) \s*,\s* (?P\+?Inf|\-?\d+(\.\d+)?) \s* (?P[\[\]]) )\s*(?P.*?)$/xs EOF; $standardRules = []; foreach ($parts as $part) { $part = trim(str_replace('||', '|', $part)); // try to match an explicit rule, then fallback to the standard ones if (preg_match($intervalRegexp, $part, $matches)) { if ($matches[2]) { foreach (explode(',', $matches[3]) as $n) { if ($number == $n) { return strtr($matches['message'], $parameters); } } } else { $leftNumber = '-Inf' === $matches['left'] ? -\INF : (float) $matches['left']; $rightNumber = is_numeric($matches['right']) ? (float) $matches['right'] : \INF; if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) ) { return strtr($matches['message'], $parameters); } } } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { $standardRules[] = $matches[1]; } else { $standardRules[] = $part; } } $position = $this->getPluralizationRule($number, $locale); if (!isset($standardRules[$position])) { // when there's exactly one rule given, and that rule is a standard // rule, use this rule if (1 === \count($parts) && isset($standardRules[0])) { return strtr($standardRules[0], $parameters); } $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); if (class_exists(InvalidArgumentException::class)) { throw new InvalidArgumentException($message); } throw new \InvalidArgumentException($message); } return strtr($standardRules[$position], $parameters); } /** * Returns the plural position to use for the given locale and number. * * The plural rules are derived from code of the Zend Framework (2010-09-25), * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) */ private function getPluralizationRule(float $number, string $locale): int { $number = abs($number); switch ('pt_BR' !== $locale && 'en_US_POSIX' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) { case 'af': case 'bn': case 'bg': case 'ca': case 'da': case 'de': case 'el': case 'en': case 'en_US_POSIX': case 'eo': case 'es': case 'et': case 'eu': case 'fa': case 'fi': case 'fo': case 'fur': case 'fy': case 'gl': case 'gu': case 'ha': case 'he': case 'hu': case 'is': case 'it': case 'ku': case 'lb': case 'ml': case 'mn': case 'mr': case 'nah': case 'nb': case 'ne': case 'nl': case 'nn': case 'no': case 'oc': case 'om': case 'or': case 'pa': case 'pap': case 'ps': case 'pt': case 'so': case 'sq': case 'sv': case 'sw': case 'ta': case 'te': case 'tk': case 'ur': case 'zu': return (1 == $number) ? 0 : 1; case 'am': case 'bh': case 'fil': case 'fr': case 'gun': case 'hi': case 'hy': case 'ln': case 'mg': case 'nso': case 'pt_BR': case 'ti': case 'wa': return ($number < 2) ? 0 : 1; case 'be': case 'bs': case 'hr': case 'ru': case 'sh': case 'sr': case 'uk': return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); case 'cs': case 'sk': return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); case 'ga': return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); case 'lt': return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); case 'sl': return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); case 'mk': return (1 == $number % 10) ? 0 : 1; case 'mt': return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); case 'lv': return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); case 'pl': return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); case 'cy': return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); case 'ro': return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); case 'ar': return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); default: return 0; } } } PK!*!*!1vendor/symfony/validator/Command/DebugCommand.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Mapping\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * A console command to debug Validators information. * * @author Loïc Frémont */ class DebugCommand extends Command { protected static $defaultName = 'debug:validator'; protected static $defaultDescription = 'Display validation constraints for classes'; private $validator; public function __construct(MetadataFactoryInterface $validator) { parent::__construct(); $this->validator = $validator; } protected function configure() { $this ->addArgument('class', InputArgument::REQUIRED, 'A fully qualified class name or a path') ->addOption('show-all', null, InputOption::VALUE_NONE, 'Show all classes even if they have no validation constraints') ->setDescription(self::$defaultDescription) ->setHelp(<<<'EOF' The %command.name% 'App\Entity\Dummy' command dumps the validators for the dummy class. The %command.name% src/ command dumps the validators for the `src` directory. EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $class = $input->getArgument('class'); if (class_exists($class)) { $this->dumpValidatorsForClass($input, $output, $class); return 0; } try { foreach ($this->getResourcesByPath($class) as $class) { $this->dumpValidatorsForClass($input, $output, $class); } } catch (DirectoryNotFoundException $exception) { $io = new SymfonyStyle($input, $output); $io->error(sprintf('Neither class nor path were found with "%s" argument.', $input->getArgument('class'))); return 1; } return 0; } private function dumpValidatorsForClass(InputInterface $input, OutputInterface $output, string $class): void { $io = new SymfonyStyle($input, $output); $title = sprintf('%s', $class); $rows = []; $dump = new Dumper($output); /** @var ClassMetadataInterface $classMetadata */ $classMetadata = $this->validator->getMetadataFor($class); foreach ($this->getClassConstraintsData($classMetadata) as $data) { $rows[] = [ '-', $data['class'], implode(', ', $data['groups']), $dump($data['options']), ]; } foreach ($this->getConstrainedPropertiesData($classMetadata) as $propertyName => $constraintsData) { foreach ($constraintsData as $data) { $rows[] = [ $propertyName, $data['class'], implode(', ', $data['groups']), $dump($data['options']), ]; } } if (!$rows) { if (false === $input->getOption('show-all')) { return; } $io->section($title); $io->text('No validators were found for this class.'); return; } $io->section($title); $table = new Table($output); $table->setHeaders(['Property', 'Name', 'Groups', 'Options']); $table->setRows($rows); $table->setColumnMaxWidth(3, 80); $table->render(); } private function getClassConstraintsData(ClassMetadataInterface $classMetadata): iterable { foreach ($classMetadata->getConstraints() as $constraint) { yield [ 'class' => \get_class($constraint), 'groups' => $constraint->groups, 'options' => $this->getConstraintOptions($constraint), ]; } } private function getConstrainedPropertiesData(ClassMetadataInterface $classMetadata): array { $data = []; foreach ($classMetadata->getConstrainedProperties() as $constrainedProperty) { $data[$constrainedProperty] = $this->getPropertyData($classMetadata, $constrainedProperty); } return $data; } private function getPropertyData(ClassMetadataInterface $classMetadata, string $constrainedProperty): array { $data = []; $propertyMetadata = $classMetadata->getPropertyMetadata($constrainedProperty); foreach ($propertyMetadata as $metadata) { $autoMapingStrategy = 'Not supported'; if ($metadata instanceof GenericMetadata) { switch ($metadata->getAutoMappingStrategy()) { case AutoMappingStrategy::ENABLED: $autoMapingStrategy = 'Enabled'; break; case AutoMappingStrategy::DISABLED: $autoMapingStrategy = 'Disabled'; break; case AutoMappingStrategy::NONE: $autoMapingStrategy = 'None'; break; } } $traversalStrategy = 'None'; if (TraversalStrategy::TRAVERSE === $metadata->getTraversalStrategy()) { $traversalStrategy = 'Traverse'; } if (TraversalStrategy::IMPLICIT === $metadata->getTraversalStrategy()) { $traversalStrategy = 'Implicit'; } $data[] = [ 'class' => 'property options', 'groups' => [], 'options' => [ 'cascadeStrategy' => CascadingStrategy::CASCADE === $metadata->getCascadingStrategy() ? 'Cascade' : 'None', 'autoMappingStrategy' => $autoMapingStrategy, 'traversalStrategy' => $traversalStrategy, ], ]; foreach ($metadata->getConstraints() as $constraint) { $data[] = [ 'class' => \get_class($constraint), 'groups' => $constraint->groups, 'options' => $this->getConstraintOptions($constraint), ]; } } return $data; } private function getConstraintOptions(Constraint $constraint): array { $options = []; foreach (array_keys(get_object_vars($constraint)) as $propertyName) { // Groups are dumped on a specific column. if ('groups' === $propertyName) { continue; } $options[$propertyName] = $constraint->$propertyName; } ksort($options); return $options; } private function getResourcesByPath(string $path): array { $finder = new Finder(); $finder->files()->in($path)->name('*.php')->sortByName(true); $classes = []; foreach ($finder as $file) { $fileContent = file_get_contents($file->getRealPath()); preg_match('/namespace (.+);/', $fileContent, $matches); $namespace = $matches[1] ?? null; if (!preg_match('/class +([^{ ]+)/', $fileContent, $matches)) { // no class found continue; } $className = trim($matches[1]); if (null !== $namespace) { $classes[] = $namespace.'\\'.$className; } else { $classes[] = $className; } } return $classes; } } PK!dL  ;vendor/symfony/validator/Constraints/AbstractComparison.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; /** * Used for the comparison of values. * * @author Daniel Holmes * @author Bernhard Schussek */ abstract class AbstractComparison extends Constraint { public $message; public $value; public $propertyPath; /** * {@inheritdoc} * * @param mixed $value the value to compare or a set of options */ public function __construct($value = null, $propertyPath = null, ?string $message = null, ?array $groups = null, $payload = null, array $options = []) { if (\is_array($value)) { $options = array_merge($value, $options); } elseif (null !== $value) { $options['value'] = $value; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->propertyPath = $propertyPath ?? $this->propertyPath; if (null === $this->value && null === $this->propertyPath) { throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires either the "value" or "propertyPath" option to be set.', static::class)); } if (null !== $this->value && null !== $this->propertyPath) { throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "value" or "propertyPath" options to be set, not both.', static::class)); } if (null !== $this->propertyPath && !class_exists(PropertyAccess::class)) { throw new LogicException(sprintf('The "%s" constraint requires the Symfony PropertyAccess component to use the "propertyPath" option.', static::class)); } } /** * {@inheritdoc} */ public function getDefaultOption() { return 'value'; } } PK!r##Dvendor/symfony/validator/Constraints/AbstractComparisonValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Provides a base class for the validation of property comparisons. * * @author Daniel Holmes * @author Bernhard Schussek */ abstract class AbstractComparisonValidator extends ConstraintValidator { private $propertyAccessor; public function __construct(?PropertyAccessorInterface $propertyAccessor = null) { $this->propertyAccessor = $propertyAccessor; } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof AbstractComparison) { throw new UnexpectedTypeException($constraint, AbstractComparison::class); } if (null === $value) { return; } if ($path = $constraint->propertyPath) { if (null === $object = $this->context->getObject()) { return; } try { $comparedValue = $this->getPropertyAccessor()->getValue($object, $path); } catch (NoSuchPropertyException $e) { throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: ', $path, get_debug_type($constraint)).$e->getMessage(), 0, $e); } catch (UninitializedPropertyException $e) { $comparedValue = null; } } else { $comparedValue = $constraint->value; } // Convert strings to DateTimes if comparing another DateTime // This allows to compare with any date/time value supported by // the DateTime constructor: // https://php.net/datetime.formats if (\is_string($comparedValue) && $value instanceof \DateTimeInterface) { // If $value is immutable, convert the compared value to a DateTimeImmutable too, otherwise use DateTime $dateTimeClass = $value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class; try { $comparedValue = new $dateTimeClass($comparedValue); } catch (\Exception $e) { throw new ConstraintDefinitionException(sprintf('The compared value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $comparedValue, $dateTimeClass, get_debug_type($constraint))); } } if (!$this->compareValues($value, $comparedValue)) { $violationBuilder = $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value, self::OBJECT_TO_STRING | self::PRETTY_DATE)) ->setParameter('{{ compared_value }}', $this->formatValue($comparedValue, self::OBJECT_TO_STRING | self::PRETTY_DATE)) ->setParameter('{{ compared_value_type }}', $this->formatTypeOf($comparedValue)) ->setCode($this->getErrorCode()); if (null !== $path) { $violationBuilder->setParameter('{{ compared_value_path }}', $path); } $violationBuilder->addViolation(); } } private function getPropertyAccessor(): PropertyAccessorInterface { if (null === $this->propertyAccessor) { $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); } return $this->propertyAccessor; } /** * Compares the two given values to find if their relationship is valid. * * @param mixed $value1 The first value to compare * @param mixed $value2 The second value to compare * * @return bool */ abstract protected function compareValues($value1, $value2); /** * Returns the error code used if the comparison fails. * * @return string|null */ protected function getErrorCode() { return null; } } PK!?',vendor/symfony/validator/Constraints/All.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class All extends Composite { public $constraints = []; public function __construct($constraints = null, ?array $groups = null, $payload = null) { parent::__construct($constraints ?? [], $groups, $payload); } public function getDefaultOption() { return 'constraints'; } public function getRequiredOptions() { return ['constraints']; } protected function getCompositeOption() { return 'constraints'; } } PK!L555vendor/symfony/validator/Constraints/AllValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class AllValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof All) { throw new UnexpectedTypeException($constraint, All::class); } if (null === $value) { return; } if (!\is_array($value) && !$value instanceof \Traversable) { throw new UnexpectedValueException($value, 'iterable'); } $context = $this->context; $validator = $context->getValidator()->inContext($context); foreach ($value as $key => $element) { $validator->atPath('['.$key.']')->validate($element, $constraint->constraints); } } } PK!v   5vendor/symfony/validator/Constraints/AtLeastOneOf.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Przemysław Bogusz */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class AtLeastOneOf extends Composite { public const AT_LEAST_ONE_OF_ERROR = 'f27e6d6c-261a-4056-b391-6673a623531c'; protected static $errorNames = [ self::AT_LEAST_ONE_OF_ERROR => 'AT_LEAST_ONE_OF_ERROR', ]; public $constraints = []; public $message = 'This value should satisfy at least one of the following constraints:'; public $messageCollection = 'Each element of this collection should satisfy its own set of constraints.'; public $includeInternalMessages = true; public function __construct($constraints = null, ?array $groups = null, $payload = null, ?string $message = null, ?string $messageCollection = null, ?bool $includeInternalMessages = null) { parent::__construct($constraints ?? [], $groups, $payload); $this->message = $message ?? $this->message; $this->messageCollection = $messageCollection ?? $this->messageCollection; $this->includeInternalMessages = $includeInternalMessages ?? $this->includeInternalMessages; } public function getDefaultOption() { return 'constraints'; } public function getRequiredOptions() { return ['constraints']; } protected function getCompositeOption() { return 'constraints'; } } PK!/' ' >vendor/symfony/validator/Constraints/AtLeastOneOfValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Przemysław Bogusz */ class AtLeastOneOfValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof AtLeastOneOf) { throw new UnexpectedTypeException($constraint, AtLeastOneOf::class); } $validator = $this->context->getValidator(); // Build a first violation to have the base message of the constraint translated $baseMessageContext = clone $this->context; $baseMessageContext->buildViolation($constraint->message)->addViolation(); $baseViolations = $baseMessageContext->getViolations(); $messages = [(string) $baseViolations->get(\count($baseViolations) - 1)->getMessage()]; foreach ($constraint->constraints as $key => $item) { if (!\in_array($this->context->getGroup(), $item->groups, true)) { continue; } $context = $this->context; $executionContext = clone $this->context; $executionContext->setNode($value, $this->context->getObject(), $this->context->getMetadata(), $this->context->getPropertyPath()); $violations = $validator->inContext($executionContext)->validate($value, $item, $this->context->getGroup())->getViolations(); $this->context = $context; if (\count($this->context->getViolations()) === \count($violations)) { return; } if ($constraint->includeInternalMessages) { $message = ' ['.($key + 1).'] '; if ($item instanceof All || $item instanceof Collection) { $message .= $constraint->messageCollection; } else { $message .= $violations->get(\count($violations) - 1)->getMessage(); } $messages[] = $message; } } $this->context->buildViolation(implode('', $messages)) ->setCode(AtLeastOneOf::AT_LEAST_ONE_OF_ERROR) ->addViolation() ; } } PK!Vqq,vendor/symfony/validator/Constraints/Bic.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Countries; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyPathInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Michael Hirschler */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Bic extends Constraint { public const INVALID_LENGTH_ERROR = '66dad313-af0b-4214-8566-6c799be9789c'; public const INVALID_CHARACTERS_ERROR = 'f424c529-7add-4417-8f2d-4b656e4833e2'; public const INVALID_BANK_CODE_ERROR = '00559357-6170-4f29-aebd-d19330aa19cf'; public const INVALID_COUNTRY_CODE_ERROR = '1ce76f8d-3c1f-451c-9e62-fe9c3ed486ae'; public const INVALID_CASE_ERROR = '11884038-3312-4ae5-9d04-699f782130c7'; public const INVALID_IBAN_COUNTRY_CODE_ERROR = '29a2c3bb-587b-4996-b6f5-53081364cea5'; protected static $errorNames = [ self::INVALID_LENGTH_ERROR => 'INVALID_LENGTH_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::INVALID_BANK_CODE_ERROR => 'INVALID_BANK_CODE_ERROR', self::INVALID_COUNTRY_CODE_ERROR => 'INVALID_COUNTRY_CODE_ERROR', self::INVALID_CASE_ERROR => 'INVALID_CASE_ERROR', ]; public $message = 'This is not a valid Business Identifier Code (BIC).'; public $ibanMessage = 'This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.'; public $iban; public $ibanPropertyPath; /** * {@inheritdoc} * * @param string|PropertyPathInterface|null $ibanPropertyPath */ public function __construct(?array $options = null, ?string $message = null, ?string $iban = null, $ibanPropertyPath = null, ?string $ibanMessage = null, ?array $groups = null, $payload = null) { if (!class_exists(Countries::class)) { throw new LogicException('The Intl component is required to use the Bic constraint. Try running "composer require symfony/intl".'); } if (null !== $ibanPropertyPath && !\is_string($ibanPropertyPath) && !$ibanPropertyPath instanceof PropertyPathInterface) { throw new \TypeError(sprintf('"%s": Expected argument $ibanPropertyPath to be either null, a string or an instance of "%s", got "%s".', __METHOD__, PropertyPathInterface::class, get_debug_type($ibanPropertyPath))); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->ibanMessage = $ibanMessage ?? $this->ibanMessage; $this->iban = $iban ?? $this->iban; $this->ibanPropertyPath = $ibanPropertyPath ?? $this->ibanPropertyPath; if (null !== $this->iban && null !== $this->ibanPropertyPath) { throw new ConstraintDefinitionException('The "iban" and "ibanPropertyPath" options of the Iban constraint cannot be used at the same time.'); } if (null !== $this->ibanPropertyPath && !class_exists(PropertyAccess::class)) { throw new LogicException(sprintf('The "symfony/property-access" component is required to use the "%s" constraint with the "ibanPropertyPath" option.', self::class)); } } } PK!0хTT5vendor/symfony/validator/Constraints/BicValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Countries; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Michael Hirschler * * @see https://en.wikipedia.org/wiki/ISO_9362#Structure */ class BicValidator extends ConstraintValidator { // Reference: https://www.iban.com/structure private const BIC_COUNTRY_TO_IBAN_COUNTRY_MAP = [ // FR includes: 'GF' => 'FR', // French Guiana 'PF' => 'FR', // French Polynesia 'TF' => 'FR', // French Southern Territories 'GP' => 'FR', // Guadeloupe 'MQ' => 'FR', // Martinique 'YT' => 'FR', // Mayotte 'NC' => 'FR', // New Caledonia 'RE' => 'FR', // Reunion 'BL' => 'FR', // Saint Barthelemy 'MF' => 'FR', // Saint Martin (French part) 'PM' => 'FR', // Saint Pierre and Miquelon 'WF' => 'FR', // Wallis and Futuna Islands // GB includes: 'JE' => 'GB', // Jersey 'IM' => 'GB', // Isle of Man 'GG' => 'GB', // Guernsey 'VG' => 'GB', // British Virgin Islands // FI includes: 'AX' => 'FI', // Aland Islands // ES includes: 'IC' => 'ES', // Canary Islands 'EA' => 'ES', // Ceuta and Melilla ]; private $propertyAccessor; public function __construct(?PropertyAccessor $propertyAccessor = null) { $this->propertyAccessor = $propertyAccessor; } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Bic) { throw new UnexpectedTypeException($constraint, Bic::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $canonicalize = str_replace(' ', '', $value); // the bic must be either 8 or 11 characters long if (!\in_array(\strlen($canonicalize), [8, 11])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Bic::INVALID_LENGTH_ERROR) ->addViolation(); return; } // must contain alphanumeric values only if (!ctype_alnum($canonicalize)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Bic::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } $bicCountryCode = substr($canonicalize, 4, 2); if (!isset(self::BIC_COUNTRY_TO_IBAN_COUNTRY_MAP[$bicCountryCode]) && !Countries::exists($bicCountryCode)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Bic::INVALID_COUNTRY_CODE_ERROR) ->addViolation(); return; } // should contain uppercase characters only if (strtoupper($canonicalize) !== $canonicalize) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Bic::INVALID_CASE_ERROR) ->addViolation(); return; } // check against an IBAN $iban = $constraint->iban; $path = $constraint->ibanPropertyPath; if ($path && null !== $object = $this->context->getObject()) { try { $iban = $this->getPropertyAccessor()->getValue($object, $path); } catch (NoSuchPropertyException $e) { throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: ', $path, get_debug_type($constraint)).$e->getMessage(), 0, $e); } catch (UninitializedPropertyException $e) { $iban = null; } } if (!$iban) { return; } $ibanCountryCode = substr($iban, 0, 2); if (ctype_alpha($ibanCountryCode) && !$this->bicAndIbanCountriesMatch($bicCountryCode, $ibanCountryCode)) { $this->context->buildViolation($constraint->ibanMessage) ->setParameter('{{ value }}', $this->formatValue($value)) ->setParameter('{{ iban }}', $iban) ->setCode(Bic::INVALID_IBAN_COUNTRY_CODE_ERROR) ->addViolation(); } } private function getPropertyAccessor(): PropertyAccessor { if (null === $this->propertyAccessor) { if (!class_exists(PropertyAccess::class)) { throw new LogicException('Unable to use property path as the Symfony PropertyAccess component is not installed.'); } $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); } return $this->propertyAccessor; } private function bicAndIbanCountriesMatch(string $bicCountryCode, string $ibanCountryCode): bool { return $ibanCountryCode === $bicCountryCode || $ibanCountryCode === (self::BIC_COUNTRY_TO_IBAN_COUNTRY_MAP[$bicCountryCode] ?? null); } } PK! m'o11.vendor/symfony/validator/Constraints/Blank.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Blank extends Constraint { public const NOT_BLANK_ERROR = '183ad2de-533d-4796-a439-6d3c3852b549'; protected static $errorNames = [ self::NOT_BLANK_ERROR => 'NOT_BLANK_ERROR', ]; public $message = 'This value should be blank.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; } } PK!+@@7vendor/symfony/validator/Constraints/BlankValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class BlankValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Blank) { throw new UnexpectedTypeException($constraint, Blank::class); } if ('' !== $value && null !== $value) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Blank::NOT_BLANK_ERROR) ->addViolation(); } } } PK!j1vendor/symfony/validator/Constraints/Callback.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Callback extends Constraint { /** * @var string|callable */ public $callback; /** * {@inheritdoc} * * @param array|string|callable $callback The callback or a set of options */ public function __construct($callback = null, ?array $groups = null, $payload = null, array $options = []) { // Invocation through annotations with an array parameter only if (\is_array($callback) && 1 === \count($callback) && isset($callback['value'])) { $callback = $callback['value']; } if (!\is_array($callback) || (!isset($callback['callback']) && !isset($callback['groups']) && !isset($callback['payload']))) { $options['callback'] = $callback; } else { $options = array_merge($callback, $options); } parent::__construct($options, $groups, $payload); } /** * {@inheritdoc} */ public function getDefaultOption() { return 'callback'; } /** * {@inheritdoc} */ public function getTargets() { return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT]; } } PK!מ]]:vendor/symfony/validator/Constraints/CallbackValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validator for Callback constraint. * * @author Bernhard Schussek */ class CallbackValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($object, Constraint $constraint) { if (!$constraint instanceof Callback) { throw new UnexpectedTypeException($constraint, Callback::class); } $method = $constraint->callback; if ($method instanceof \Closure) { $method($object, $this->context, $constraint->payload); } elseif (\is_array($method)) { if (!\is_callable($method)) { if (isset($method[0]) && \is_object($method[0])) { $method[0] = \get_class($method[0]); } throw new ConstraintDefinitionException(json_encode($method).' targeted by Callback constraint is not a valid callable.'); } $method($object, $this->context, $constraint->payload); } elseif (null !== $object) { if (!method_exists($object, $method)) { throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by Callback constraint does not exist in class "%s".', $method, get_debug_type($object))); } $reflMethod = new \ReflectionMethod($object, $method); if ($reflMethod->isStatic()) { $reflMethod->invoke(null, $object, $this->context, $constraint->payload); } else { $reflMethod->invoke($object, $this->context, $constraint->payload); } } } } PK!_BH3vendor/symfony/validator/Constraints/CardScheme.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * Metadata for the CardSchemeValidator. * * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Tim Nagel * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class CardScheme extends Constraint { public const AMEX = 'AMEX'; public const CHINA_UNIONPAY = 'CHINA_UNIONPAY'; public const DINERS = 'DINERS'; public const DISCOVER = 'DISCOVER'; public const INSTAPAYMENT = 'INSTAPAYMENT'; public const JCB = 'JCB'; public const LASER = 'LASER'; public const MAESTRO = 'MAESTRO'; public const MASTERCARD = 'MASTERCARD'; public const MIR = 'MIR'; public const UATP = 'UATP'; public const VISA = 'VISA'; public const NOT_NUMERIC_ERROR = 'a2ad9231-e827-485f-8a1e-ef4d9a6d5c2e'; public const INVALID_FORMAT_ERROR = 'a8faedbf-1c2f-4695-8d22-55783be8efed'; protected static $errorNames = [ self::NOT_NUMERIC_ERROR => 'NOT_NUMERIC_ERROR', self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; public $message = 'Unsupported card type or invalid card number.'; public $schemes; /** * {@inheritdoc} * * @param array|string $schemes The schemes to validate against or a set of options */ public function __construct($schemes, ?string $message = null, ?array $groups = null, $payload = null, array $options = []) { if (\is_array($schemes) && \is_string(key($schemes))) { $options = array_merge($schemes, $options); } else { $options['value'] = $schemes; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } public function getDefaultOption() { return 'schemes'; } public function getRequiredOptions() { return ['schemes']; } } PK!Lj<vendor/symfony/validator/Constraints/CardSchemeValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validates that a card number belongs to a specified scheme. * * @author Tim Nagel * @author Bernhard Schussek * * @see https://en.wikipedia.org/wiki/Payment_card_number * @see https://www.regular-expressions.info/creditcard.html */ class CardSchemeValidator extends ConstraintValidator { protected $schemes = [ // American Express card numbers start with 34 or 37 and have 15 digits. CardScheme::AMEX => [ '/^3[47][0-9]{13}$/D', ], // China UnionPay cards start with 62 and have between 16 and 19 digits. // Please note that these cards do not follow Luhn Algorithm as a checksum. CardScheme::CHINA_UNIONPAY => [ '/^62[0-9]{14,17}$/D', ], // Diners Club card numbers begin with 300 through 305, 36 or 38. All have 14 digits. // There are Diners Club cards that begin with 5 and have 16 digits. // These are a joint venture between Diners Club and MasterCard, and should be processed like a MasterCard. CardScheme::DINERS => [ '/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/D', ], // Discover card numbers begin with 6011, 622126 through 622925, 644 through 649 or 65. // All have 16 digits. CardScheme::DISCOVER => [ '/^6011[0-9]{12}$/D', '/^64[4-9][0-9]{13}$/D', '/^65[0-9]{14}$/D', '/^622(12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|91[0-9]|92[0-5])[0-9]{10}$/D', ], // InstaPayment cards begin with 637 through 639 and have 16 digits. CardScheme::INSTAPAYMENT => [ '/^63[7-9][0-9]{13}$/D', ], // JCB cards beginning with 2131 or 1800 have 15 digits. // JCB cards beginning with 35 have 16 digits. CardScheme::JCB => [ '/^(?:2131|1800|35[0-9]{3})[0-9]{11}$/D', ], // Laser cards begin with either 6304, 6706, 6709 or 6771 and have between 16 and 19 digits. CardScheme::LASER => [ '/^(6304|670[69]|6771)[0-9]{12,15}$/D', ], // Maestro international cards begin with 675900..675999 and have between 12 and 19 digits. // Maestro UK cards begin with either 500000..509999 or 560000..699999 and have between 12 and 19 digits. CardScheme::MAESTRO => [ '/^(6759[0-9]{2})[0-9]{6,13}$/D', '/^(50[0-9]{4})[0-9]{6,13}$/D', '/^5[6-9][0-9]{10,17}$/D', '/^6[0-9]{11,18}$/D', ], // All MasterCard numbers start with the numbers 51 through 55. All have 16 digits. // October 2016 MasterCard numbers can also start with 222100 through 272099. CardScheme::MASTERCARD => [ '/^5[1-5][0-9]{14}$/D', '/^2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12})$/D', ], // Payment system MIR numbers start with 220, then 1 digit from 0 to 4, then between 12 and 15 digits CardScheme::MIR => [ '/^220[0-4][0-9]{12,15}$/D', ], // All UATP card numbers start with a 1 and have a length of 15 digits. CardScheme::UATP => [ '/^1[0-9]{14}$/D', ], // All Visa card numbers start with a 4 and have a length of 13, 16, or 19 digits. CardScheme::VISA => [ '/^4([0-9]{12}|[0-9]{15}|[0-9]{18})$/D', ], ]; /** * Validates a creditcard belongs to a specified scheme. * * @param mixed $value */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof CardScheme) { throw new UnexpectedTypeException($constraint, CardScheme::class); } if (null === $value || '' === $value) { return; } if (!is_numeric($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(CardScheme::NOT_NUMERIC_ERROR) ->addViolation(); return; } $schemes = array_flip((array) $constraint->schemes); $schemeRegexes = array_intersect_key($this->schemes, $schemes); foreach ($schemeRegexes as $regexes) { foreach ($regexes as $regex) { if (preg_match($regex, $value)) { return; } } } $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(CardScheme::INVALID_FORMAT_ERROR) ->addViolation(); } } PK! EA0vendor/symfony/validator/Constraints/Cascade.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @Annotation * @Target({"CLASS"}) * * @author Jules Pietri */ #[\Attribute(\Attribute::TARGET_CLASS)] class Cascade extends Constraint { public function __construct(?array $options = null) { if (\is_array($options) && \array_key_exists('groups', $options)) { throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } parent::__construct($options); } /** * {@inheritdoc} */ public function getTargets() { return self::CLASS_CONSTRAINT; } } PK!,R R /vendor/symfony/validator/Constraints/Choice.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Choice extends Constraint { public const NO_SUCH_CHOICE_ERROR = '8e179f1b-97aa-4560-a02f-2a8b42e49df7'; public const TOO_FEW_ERROR = '11edd7eb-5872-4b6e-9f12-89923999fd0e'; public const TOO_MANY_ERROR = '9bd98e49-211c-433f-8630-fd1c2d0f08c3'; protected static $errorNames = [ self::NO_SUCH_CHOICE_ERROR => 'NO_SUCH_CHOICE_ERROR', self::TOO_FEW_ERROR => 'TOO_FEW_ERROR', self::TOO_MANY_ERROR => 'TOO_MANY_ERROR', ]; public $choices; public $callback; public $multiple = false; public $strict = true; public $min; public $max; public $message = 'The value you selected is not a valid choice.'; public $multipleMessage = 'One or more of the given values is invalid.'; public $minMessage = 'You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices.'; public $maxMessage = 'You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.'; /** * {@inheritdoc} */ public function getDefaultOption() { return 'choices'; } public function __construct( $options = [], ?array $choices = null, $callback = null, ?bool $multiple = null, ?bool $strict = null, ?int $min = null, ?int $max = null, ?string $message = null, ?string $multipleMessage = null, ?string $minMessage = null, ?string $maxMessage = null, $groups = null, $payload = null ) { if (\is_array($options) && $options && array_is_list($options)) { $choices = $choices ?? $options; $options = []; } if (null !== $choices) { $options['value'] = $choices; } parent::__construct($options, $groups, $payload); $this->callback = $callback ?? $this->callback; $this->multiple = $multiple ?? $this->multiple; $this->strict = $strict ?? $this->strict; $this->min = $min ?? $this->min; $this->max = $max ?? $this->max; $this->message = $message ?? $this->message; $this->multipleMessage = $multipleMessage ?? $this->multipleMessage; $this->minMessage = $minMessage ?? $this->minMessage; $this->maxMessage = $maxMessage ?? $this->maxMessage; } } PK!&8vendor/symfony/validator/Constraints/ChoiceValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * ChoiceValidator validates that the value is one of the expected values. * * @author Fabien Potencier * @author Florian Eckerstorfer * @author Bernhard Schussek */ class ChoiceValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Choice) { throw new UnexpectedTypeException($constraint, Choice::class); } if (!\is_array($constraint->choices) && !$constraint->callback) { throw new ConstraintDefinitionException('Either "choices" or "callback" must be specified on constraint Choice.'); } if (null === $value) { return; } if ($constraint->multiple && !\is_array($value)) { throw new UnexpectedValueException($value, 'array'); } if ($constraint->callback) { if (!\is_callable($choices = [$this->context->getObject(), $constraint->callback]) && !\is_callable($choices = [$this->context->getClassName(), $constraint->callback]) && !\is_callable($choices = $constraint->callback) ) { throw new ConstraintDefinitionException('The Choice constraint expects a valid callback.'); } $choices = $choices(); if (!is_array($choices)) { throw new ConstraintDefinitionException(sprintf('The Choice constraint callback "%s" is expected to return an array, but returned "%s".', trim($this->formatValue($constraint->callback), '"'), get_debug_type($choices))); } } else { $choices = $constraint->choices; } if (true !== $constraint->strict) { throw new \RuntimeException('The "strict" option of the Choice constraint should not be used.'); } if ($constraint->multiple) { foreach ($value as $_value) { if (!\in_array($_value, $choices, true)) { $this->context->buildViolation($constraint->multipleMessage) ->setParameter('{{ value }}', $this->formatValue($_value)) ->setParameter('{{ choices }}', $this->formatValues($choices)) ->setCode(Choice::NO_SUCH_CHOICE_ERROR) ->setInvalidValue($_value) ->addViolation(); return; } } $count = \count($value); if (null !== $constraint->min && $count < $constraint->min) { $this->context->buildViolation($constraint->minMessage) ->setParameter('{{ limit }}', $constraint->min) ->setPlural((int) $constraint->min) ->setCode(Choice::TOO_FEW_ERROR) ->addViolation(); return; } if (null !== $constraint->max && $count > $constraint->max) { $this->context->buildViolation($constraint->maxMessage) ->setParameter('{{ limit }}', $constraint->max) ->setPlural((int) $constraint->max) ->setCode(Choice::TOO_MANY_ERROR) ->addViolation(); return; } } elseif (!\in_array($value, $choices, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setParameter('{{ choices }}', $this->formatValues($choices)) ->setCode(Choice::NO_SUCH_CHOICE_ERROR) ->addViolation(); } } } PK!їiߘ -vendor/symfony/validator/Constraints/Cidr.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Validates that a value is a valid CIDR notation. * * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Sorin Pop * @author Calin Bolea */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Cidr extends Constraint { public const INVALID_CIDR_ERROR = '5649e53a-5afb-47c5-a360-ffbab3be8567'; public const OUT_OF_RANGE_ERROR = 'b9f14a51-acbd-401a-a078-8c6b204ab32f'; protected static $errorNames = [ self::INVALID_CIDR_ERROR => 'INVALID_CIDR_ERROR', self::OUT_OF_RANGE_ERROR => 'OUT_OF_RANGE_VIOLATION', ]; private const NET_MAXES = [ Ip::ALL => 128, Ip::V4 => 32, Ip::V6 => 128, ]; public $version = Ip::ALL; public $message = 'This value is not a valid CIDR notation.'; public $netmaskRangeViolationMessage = 'The value of the netmask should be between {{ min }} and {{ max }}.'; public $netmaskMin = 0; public $netmaskMax; public function __construct( ?array $options = null, ?string $version = null, ?int $netmaskMin = null, ?int $netmaskMax = null, ?string $message = null, ?array $groups = null, $payload = null ) { $this->version = $version ?? $options['version'] ?? $this->version; if (!\in_array($this->version, array_keys(self::NET_MAXES))) { throw new ConstraintDefinitionException(sprintf('The option "version" must be one of "%s".', implode('", "', array_keys(self::NET_MAXES)))); } $this->netmaskMin = $netmaskMin ?? $options['netmaskMin'] ?? $this->netmaskMin; $this->netmaskMax = $netmaskMax ?? $options['netmaskMax'] ?? self::NET_MAXES[$this->version]; $this->message = $message ?? $this->message; unset($options['netmaskMin'], $options['netmaskMax'], $options['version']); if ($this->netmaskMin < 0 || $this->netmaskMax > self::NET_MAXES[$this->version] || $this->netmaskMin > $this->netmaskMax) { throw new ConstraintDefinitionException(sprintf('The netmask range must be between 0 and %d.', self::NET_MAXES[$this->version])); } parent::__construct($options, $groups, $payload); } } PK!FcI I 6vendor/symfony/validator/Constraints/CidrValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; class CidrValidator extends ConstraintValidator { public function validate($value, Constraint $constraint): void { if (!$constraint instanceof Cidr) { throw new UnexpectedTypeException($constraint, Cidr::class); } if (null === $value || '' === $value) { return; } if (!\is_string($value)) { throw new UnexpectedValueException($value, 'string'); } $cidrParts = explode('/', $value, 2); if (!isset($cidrParts[1]) || !ctype_digit($cidrParts[1]) || '' === $cidrParts[0] ) { $this->context ->buildViolation($constraint->message) ->setCode(Cidr::INVALID_CIDR_ERROR) ->addViolation(); return; } $ipAddress = $cidrParts[0]; $netmask = (int) $cidrParts[1]; $validV4 = Ip::V6 !== $constraint->version && filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) && $netmask <= 32; $validV6 = Ip::V4 !== $constraint->version && filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6); if (!$validV4 && !$validV6) { $this->context ->buildViolation($constraint->message) ->setCode(Cidr::INVALID_CIDR_ERROR) ->addViolation(); return; } if ($netmask < $constraint->netmaskMin || $netmask > $constraint->netmaskMax) { $this->context ->buildViolation($constraint->netmaskRangeViolationMessage) ->setParameter('{{ min }}', $constraint->netmaskMin) ->setParameter('{{ max }}', $constraint->netmaskMax) ->setCode(Cidr::OUT_OF_RANGE_ERROR) ->addViolation(); } } } PK!WL!OO3vendor/symfony/validator/Constraints/Collection.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Collection extends Composite { public const MISSING_FIELD_ERROR = '2fa2158c-2a7f-484b-98aa-975522539ff8'; public const NO_SUCH_FIELD_ERROR = '7703c766-b5d5-4cef-ace7-ae0dd82304e9'; protected static $errorNames = [ self::MISSING_FIELD_ERROR => 'MISSING_FIELD_ERROR', self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR', ]; public $fields = []; public $allowExtraFields = false; public $allowMissingFields = false; public $extraFieldsMessage = 'This field was not expected.'; public $missingFieldsMessage = 'This field is missing.'; /** * {@inheritdoc} */ public function __construct($fields = null, ?array $groups = null, $payload = null, ?bool $allowExtraFields = null, ?bool $allowMissingFields = null, ?string $extraFieldsMessage = null, ?string $missingFieldsMessage = null) { if (self::isFieldsOption($fields)) { $fields = ['fields' => $fields]; } parent::__construct($fields, $groups, $payload); $this->allowExtraFields = $allowExtraFields ?? $this->allowExtraFields; $this->allowMissingFields = $allowMissingFields ?? $this->allowMissingFields; $this->extraFieldsMessage = $extraFieldsMessage ?? $this->extraFieldsMessage; $this->missingFieldsMessage = $missingFieldsMessage ?? $this->missingFieldsMessage; } /** * {@inheritdoc} */ protected function initializeNestedConstraints() { parent::initializeNestedConstraints(); if (!\is_array($this->fields)) { throw new ConstraintDefinitionException(sprintf('The option "fields" is expected to be an array in constraint "%s".', __CLASS__)); } foreach ($this->fields as $fieldName => $field) { // the XmlFileLoader and YamlFileLoader pass the field Optional // and Required constraint as an array with exactly one element if (\is_array($field) && 1 == \count($field)) { $this->fields[$fieldName] = $field = $field[0]; } if (!$field instanceof Optional && !$field instanceof Required) { $this->fields[$fieldName] = new Required($field); } } } public function getRequiredOptions() { return ['fields']; } protected function getCompositeOption() { return 'fields'; } private static function isFieldsOption($options): bool { if (!\is_array($options)) { return false; } foreach ($options as $optionOrField) { if ($optionOrField instanceof Constraint) { return true; } if (null === $optionOrField) { continue; } if (!\is_array($optionOrField)) { return false; } if ($optionOrField && !($optionOrField[0] ?? null) instanceof Constraint) { return false; } } return true; } } PK!× <vendor/symfony/validator/Constraints/CollectionValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class CollectionValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Collection) { throw new UnexpectedTypeException($constraint, Collection::class); } if (null === $value) { return; } if (!\is_array($value) && !($value instanceof \Traversable && $value instanceof \ArrayAccess)) { throw new UnexpectedValueException($value, 'array|(Traversable&ArrayAccess)'); } // We need to keep the initialized context when CollectionValidator // calls itself recursively (Collection constraints can be nested). // Since the context of the validator is overwritten when initialize() // is called for the nested constraint, the outer validator is // acting on the wrong context when the nested validation terminates. // // A better solution - which should be approached in Symfony 3.0 - is to // remove the initialize() method and pass the context as last argument // to validate() instead. $context = $this->context; foreach ($constraint->fields as $field => $fieldConstraint) { // bug fix issue #2779 $existsInArray = \is_array($value) && \array_key_exists($field, $value); $existsInArrayAccess = $value instanceof \ArrayAccess && $value->offsetExists($field); if ($existsInArray || $existsInArrayAccess) { if (\count($fieldConstraint->constraints) > 0) { $context->getValidator() ->inContext($context) ->atPath('['.$field.']') ->validate($value[$field], $fieldConstraint->constraints); } } elseif (!$fieldConstraint instanceof Optional && !$constraint->allowMissingFields) { $context->buildViolation($constraint->missingFieldsMessage) ->atPath('['.$field.']') ->setParameter('{{ field }}', $this->formatValue($field)) ->setInvalidValue(null) ->setCode(Collection::MISSING_FIELD_ERROR) ->addViolation(); } } if (!$constraint->allowExtraFields) { foreach ($value as $field => $fieldValue) { if (!isset($constraint->fields[$field])) { $context->buildViolation($constraint->extraFieldsMessage) ->atPath('['.$field.']') ->setParameter('{{ field }}', $this->formatValue($field)) ->setInvalidValue($fieldValue) ->setCode(Collection::NO_SUCH_FIELD_ERROR) ->addViolation(); } } } } } PK!82vendor/symfony/validator/Constraints/Composite.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * A constraint that is composed of other constraints. * * You should never use the nested constraint instances anywhere else, because * their groups are adapted when passed to the constructor of this class. * * If you want to create your own composite constraint, extend this class and * let {@link getCompositeOption()} return the name of the property which * contains the nested constraints. * * @author Bernhard Schussek */ abstract class Composite extends Constraint { /** * {@inheritdoc} * * The groups of the composite and its nested constraints are made * consistent using the following strategy: * * - If groups are passed explicitly to the composite constraint, but * not to the nested constraints, the options of the composite * constraint are copied to the nested constraints; * * - If groups are passed explicitly to the nested constraints, but not * to the composite constraint, the groups of all nested constraints * are merged and used as groups for the composite constraint; * * - If groups are passed explicitly to both the composite and its nested * constraints, the groups of the nested constraints must be a subset * of the groups of the composite constraint. If not, a * {@link ConstraintDefinitionException} is thrown. * * All this is done in the constructor, because constraints can then be * cached. When constraints are loaded from the cache, no more group * checks need to be done. */ public function __construct($options = null, ?array $groups = null, $payload = null) { parent::__construct($options, $groups, $payload); $this->initializeNestedConstraints(); /* @var Constraint[] $nestedConstraints */ $compositeOption = $this->getCompositeOption(); $nestedConstraints = $this->$compositeOption; if (!\is_array($nestedConstraints)) { $nestedConstraints = [$nestedConstraints]; } foreach ($nestedConstraints as $constraint) { if (!$constraint instanceof Constraint) { if (\is_object($constraint)) { $constraint = \get_class($constraint); } throw new ConstraintDefinitionException(sprintf('The value "%s" is not an instance of Constraint in constraint "%s".', $constraint, static::class)); } if ($constraint instanceof Valid) { throw new ConstraintDefinitionException(sprintf('The constraint Valid cannot be nested inside constraint "%s". You can only declare the Valid constraint directly on a field or method.', static::class)); } } if (!isset(((array) $this)['groups'])) { $mergedGroups = []; foreach ($nestedConstraints as $constraint) { foreach ($constraint->groups as $group) { $mergedGroups[$group] = true; } } // prevent empty composite constraint to have empty groups $this->groups = array_keys($mergedGroups) ?: [self::DEFAULT_GROUP]; $this->$compositeOption = $nestedConstraints; return; } foreach ($nestedConstraints as $constraint) { if (isset(((array) $constraint)['groups'])) { $excessGroups = array_diff($constraint->groups, $this->groups); if (\count($excessGroups) > 0) { throw new ConstraintDefinitionException(sprintf('The group(s) "%s" passed to the constraint "%s" should also be passed to its containing constraint "%s".', implode('", "', $excessGroups), get_debug_type($constraint), static::class)); } } else { $constraint->groups = $this->groups; } } $this->$compositeOption = $nestedConstraints; } /** * {@inheritdoc} * * Implicit group names are forwarded to nested constraints. */ public function addImplicitGroupName(string $group) { parent::addImplicitGroupName($group); /** @var Constraint[] $nestedConstraints */ $nestedConstraints = $this->{$this->getCompositeOption()}; foreach ($nestedConstraints as $constraint) { $constraint->addImplicitGroupName($group); } } /** * Returns the name of the property that contains the nested constraints. * * @return string */ abstract protected function getCompositeOption(); /** * @internal Used by metadata * * @return Constraint[] */ public function getNestedConstraints() { /* @var Constraint[] $nestedConstraints */ return $this->{$this->getCompositeOption()}; } /** * Initializes the nested constraints. * * This method can be overwritten in subclasses to clean up the nested * constraints passed to the constructor. * * @see Collection::initializeNestedConstraints() */ protected function initializeNestedConstraints() { } } PK!c1vendor/symfony/validator/Constraints/Compound.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Extend this class to create a reusable set of constraints. * * @author Maxime Steinhausser */ abstract class Compound extends Composite { /** @var Constraint[] */ public $constraints = []; public function __construct($options = null) { if (isset($options[$this->getCompositeOption()])) { throw new ConstraintDefinitionException(sprintf('You can\'t redefine the "%s" option. Use the "%s::getConstraints()" method instead.', $this->getCompositeOption(), __CLASS__)); } $this->constraints = $this->getConstraints($this->normalizeOptions($options)); parent::__construct($options); } final protected function getCompositeOption(): string { return 'constraints'; } final public function validatedBy(): string { return CompoundValidator::class; } /** * @return Constraint[] */ abstract protected function getConstraints(array $options): array; } PK!w:vendor/symfony/validator/Constraints/CompoundValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Maxime Steinhausser */ class CompoundValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) { if (!$constraint instanceof Compound) { throw new UnexpectedTypeException($constraint, Compound::class); } $context = $this->context; $validator = $context->getValidator()->inContext($context); $validator->validate($value, $constraint->constraints); } } PK!l .vendor/symfony/validator/Constraints/Count.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\MissingOptionsException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Count extends Constraint { public const TOO_FEW_ERROR = 'bef8e338-6ae5-4caf-b8e2-50e7b0579e69'; public const TOO_MANY_ERROR = '756b1212-697c-468d-a9ad-50dd783bb169'; public const NOT_EQUAL_COUNT_ERROR = '9fe5d43f-3784-4ece-a0e1-473fc02dadbc'; public const NOT_DIVISIBLE_BY_ERROR = DivisibleBy::NOT_DIVISIBLE_BY; protected static $errorNames = [ self::TOO_FEW_ERROR => 'TOO_FEW_ERROR', self::TOO_MANY_ERROR => 'TOO_MANY_ERROR', self::NOT_EQUAL_COUNT_ERROR => 'NOT_EQUAL_COUNT_ERROR', self::NOT_DIVISIBLE_BY_ERROR => 'NOT_DIVISIBLE_BY_ERROR', ]; public $minMessage = 'This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.'; public $maxMessage = 'This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.'; public $exactMessage = 'This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.'; public $divisibleByMessage = 'The number of elements in this collection should be a multiple of {{ compared_value }}.'; public $min; public $max; public $divisibleBy; /** * {@inheritdoc} * * @param int|array|null $exactly The expected exact count or a set of options */ public function __construct( $exactly = null, ?int $min = null, ?int $max = null, ?int $divisibleBy = null, ?string $exactMessage = null, ?string $minMessage = null, ?string $maxMessage = null, ?string $divisibleByMessage = null, ?array $groups = null, $payload = null, array $options = [] ) { if (\is_array($exactly)) { $options = array_merge($exactly, $options); $exactly = $options['value'] ?? null; } $min = $min ?? $options['min'] ?? null; $max = $max ?? $options['max'] ?? null; unset($options['value'], $options['min'], $options['max']); if (null !== $exactly && null === $min && null === $max) { $min = $max = $exactly; } parent::__construct($options, $groups, $payload); $this->min = $min; $this->max = $max; $this->divisibleBy = $divisibleBy ?? $this->divisibleBy; $this->exactMessage = $exactMessage ?? $this->exactMessage; $this->minMessage = $minMessage ?? $this->minMessage; $this->maxMessage = $maxMessage ?? $this->maxMessage; $this->divisibleByMessage = $divisibleByMessage ?? $this->divisibleByMessage; if (null === $this->min && null === $this->max && null === $this->divisibleBy) { throw new MissingOptionsException(sprintf('Either option "min", "max" or "divisibleBy" must be given for constraint "%s".', __CLASS__), ['min', 'max', 'divisibleBy']); } } } PK!x]0vendor/symfony/validator/Constraints/Country.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Countries; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Country extends Constraint { public const NO_SUCH_COUNTRY_ERROR = '8f900c12-61bd-455d-9398-996cd040f7f0'; protected static $errorNames = [ self::NO_SUCH_COUNTRY_ERROR => 'NO_SUCH_COUNTRY_ERROR', ]; public $message = 'This value is not a valid country.'; public $alpha3 = false; public function __construct( ?array $options = null, ?string $message = null, ?bool $alpha3 = null, ?array $groups = null, $payload = null ) { if (!class_exists(Countries::class)) { throw new LogicException('The Intl component is required to use the Country constraint. Try running "composer require symfony/intl".'); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->alpha3 = $alpha3 ?? $this->alpha3; } } PK! JJ9vendor/symfony/validator/Constraints/CountryValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Countries; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value is a valid country code. * * @author Bernhard Schussek */ class CountryValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Country) { throw new UnexpectedTypeException($constraint, Country::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ($constraint->alpha3 ? !Countries::alpha3CodeExists($value) : !Countries::exists($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Country::NO_SUCH_COUNTRY_ERROR) ->addViolation(); } } } PK!Kǭ+ + 7vendor/symfony/validator/Constraints/CountValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class CountValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Count) { throw new UnexpectedTypeException($constraint, Count::class); } if (null === $value) { return; } if (!\is_array($value) && !$value instanceof \Countable) { throw new UnexpectedValueException($value, 'array|\Countable'); } $count = \count($value); if (null !== $constraint->max && $count > $constraint->max) { $exactlyOptionEnabled = $constraint->min == $constraint->max; $this->context->buildViolation($exactlyOptionEnabled ? $constraint->exactMessage : $constraint->maxMessage) ->setParameter('{{ count }}', $count) ->setParameter('{{ limit }}', $constraint->max) ->setInvalidValue($value) ->setPlural((int) $constraint->max) ->setCode($exactlyOptionEnabled ? Count::NOT_EQUAL_COUNT_ERROR : Count::TOO_MANY_ERROR) ->addViolation(); return; } if (null !== $constraint->min && $count < $constraint->min) { $exactlyOptionEnabled = $constraint->min == $constraint->max; $this->context->buildViolation($exactlyOptionEnabled ? $constraint->exactMessage : $constraint->minMessage) ->setParameter('{{ count }}', $count) ->setParameter('{{ limit }}', $constraint->min) ->setInvalidValue($value) ->setPlural((int) $constraint->min) ->setCode($exactlyOptionEnabled ? Count::NOT_EQUAL_COUNT_ERROR : Count::TOO_FEW_ERROR) ->addViolation(); return; } if (null !== $constraint->divisibleBy) { $this->context ->getValidator() ->inContext($this->context) ->validate($count, [ new DivisibleBy([ 'value' => $constraint->divisibleBy, 'message' => $constraint->divisibleByMessage, ]), ], $this->context->getGroup()); } } } PK!XW;;1vendor/symfony/validator/Constraints/CssColor.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Mathieu Santostefano */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class CssColor extends Constraint { public const HEX_LONG = 'hex_long'; public const HEX_LONG_WITH_ALPHA = 'hex_long_with_alpha'; public const HEX_SHORT = 'hex_short'; public const HEX_SHORT_WITH_ALPHA = 'hex_short_with_alpha'; public const BASIC_NAMED_COLORS = 'basic_named_colors'; public const EXTENDED_NAMED_COLORS = 'extended_named_colors'; public const SYSTEM_COLORS = 'system_colors'; public const KEYWORDS = 'keywords'; public const RGB = 'rgb'; public const RGBA = 'rgba'; public const HSL = 'hsl'; public const HSLA = 'hsla'; public const INVALID_FORMAT_ERROR = '454ab47b-aacf-4059-8f26-184b2dc9d48d'; protected static $errorNames = [ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; /** * @var string[] */ private static $validationModes = [ self::HEX_LONG, self::HEX_LONG_WITH_ALPHA, self::HEX_SHORT, self::HEX_SHORT_WITH_ALPHA, self::BASIC_NAMED_COLORS, self::EXTENDED_NAMED_COLORS, self::SYSTEM_COLORS, self::KEYWORDS, self::RGB, self::RGBA, self::HSL, self::HSLA, ]; public $message = 'This value is not a valid CSS color.'; public $formats; /** * @param array|string $formats The types of CSS colors allowed (e.g. hexadecimal only, RGB and HSL only, etc.). */ public function __construct($formats = [], ?string $message = null, ?array $groups = null, $payload = null, ?array $options = null) { $validationModesAsString = implode(', ', self::$validationModes); if (!$formats) { $options['value'] = self::$validationModes; } elseif (\is_array($formats) && \is_string(key($formats))) { $options = array_merge($formats, $options ?? []); } elseif (\is_array($formats)) { if ([] === array_intersect(self::$validationModes, $formats)) { throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString)); } $options['value'] = $formats; } elseif (\is_string($formats)) { if (!\in_array($formats, self::$validationModes)) { throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString)); } $options['value'] = [$formats]; } else { throw new InvalidArgumentException('The "formats" parameter type is not valid. It should be a string or an array.'); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } public function getDefaultOption(): string { return 'formats'; } public function getRequiredOptions(): array { return ['formats']; } } PK!>Y:vendor/symfony/validator/Constraints/CssColorValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Mathieu Santostefano */ class CssColorValidator extends ConstraintValidator { private const PATTERN_HEX_LONG = '/^#[0-9a-f]{6}$/iD'; private const PATTERN_HEX_LONG_WITH_ALPHA = '/^#[0-9a-f]{8}$/iD'; private const PATTERN_HEX_SHORT = '/^#[0-9a-f]{3}$/iD'; private const PATTERN_HEX_SHORT_WITH_ALPHA = '/^#[0-9a-f]{4}$/iD'; // List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors private const PATTERN_BASIC_NAMED_COLORS = '/^(black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua)$/iD'; // List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors private const PATTERN_EXTENDED_NAMED_COLORS = '/^(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)$/iD'; // List comes from https://drafts.csswg.org/css-color/#css-system-colors private const PATTERN_SYSTEM_COLORS = '/^(Canvas|CanvasText|LinkText|VisitedText|ActiveText|ButtonFace|ButtonText|ButtonBorder|Field|FieldText|Highlight|HighlightText|SelectedItem|SelectedItemText|Mark|MarkText|GrayText)$/iD'; private const PATTERN_KEYWORDS = '/^(transparent|currentColor)$/iD'; private const PATTERN_RGB = '/^rgb\(\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s*\)$/iD'; private const PATTERN_RGBA = '/^rgba\(\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|0?\.\d+|1(\.0)?)\s*\)$/iD'; private const PATTERN_HSL = '/^hsl\(\s*(0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s*(0|100|\d{1,2})%,\s*(0|100|\d{1,2})%\s*\)$/iD'; private const PATTERN_HSLA = '/^hsla\(\s*(0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s*(0|100|\d{1,2})%,\s*(0|100|\d{1,2})%,\s*(0|0?\.\d+|1(\.0)?)\s*\)$/iD'; private const COLOR_PATTERNS = [ CssColor::HEX_LONG => self::PATTERN_HEX_LONG, CssColor::HEX_LONG_WITH_ALPHA => self::PATTERN_HEX_LONG_WITH_ALPHA, CssColor::HEX_SHORT => self::PATTERN_HEX_SHORT, CssColor::HEX_SHORT_WITH_ALPHA => self::PATTERN_HEX_SHORT_WITH_ALPHA, CssColor::BASIC_NAMED_COLORS => self::PATTERN_BASIC_NAMED_COLORS, CssColor::EXTENDED_NAMED_COLORS => self::PATTERN_EXTENDED_NAMED_COLORS, CssColor::SYSTEM_COLORS => self::PATTERN_SYSTEM_COLORS, CssColor::KEYWORDS => self::PATTERN_KEYWORDS, CssColor::RGB => self::PATTERN_RGB, CssColor::RGBA => self::PATTERN_RGBA, CssColor::HSL => self::PATTERN_HSL, CssColor::HSLA => self::PATTERN_HSLA, ]; /** * {@inheritdoc} */ public function validate($value, Constraint $constraint): void { if (!$constraint instanceof CssColor) { throw new UnexpectedTypeException($constraint, CssColor::class); } if (null === $value || '' === $value) { return; } if (!\is_string($value)) { throw new UnexpectedValueException($value, 'string'); } $formats = array_flip((array) $constraint->formats); $formatRegexes = array_intersect_key(self::COLOR_PATTERNS, $formats); foreach ($formatRegexes as $regex) { if (preg_match($regex, (string) $value)) { return; } } $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(CssColor::INVALID_FORMAT_ERROR) ->addViolation(); } } PK!mu1vendor/symfony/validator/Constraints/Currency.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Currencies; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Miha Vrhovnik * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Currency extends Constraint { public const NO_SUCH_CURRENCY_ERROR = '69945ac1-2db4-405f-bec7-d2772f73df52'; protected static $errorNames = [ self::NO_SUCH_CURRENCY_ERROR => 'NO_SUCH_CURRENCY_ERROR', ]; public $message = 'This value is not a valid currency.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { if (!class_exists(Currencies::class)) { throw new LogicException('The Intl component is required to use the Currency constraint. Try running "composer require symfony/intl".'); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!4QCC:vendor/symfony/validator/Constraints/CurrencyValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Currencies; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value is a valid currency. * * @author Miha Vrhovnik * @author Bernhard Schussek */ class CurrencyValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Currency) { throw new UnexpectedTypeException($constraint, Currency::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (!Currencies::exists($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Currency::NO_SUCH_CURRENCY_ERROR) ->addViolation(); } } } PK!; -vendor/symfony/validator/Constraints/Date.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Date extends Constraint { public const INVALID_FORMAT_ERROR = '69819696-02ac-4a99-9ff0-14e127c4d1bc'; public const INVALID_DATE_ERROR = '3c184ce5-b31d-4de7-8b76-326da7b2be93'; protected static $errorNames = [ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', self::INVALID_DATE_ERROR => 'INVALID_DATE_ERROR', ]; public $message = 'This value is not a valid date.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!|ܗ1vendor/symfony/validator/Constraints/DateTime.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class DateTime extends Constraint { public const INVALID_FORMAT_ERROR = '1a9da513-2640-4f84-9b6a-4d99dcddc628'; public const INVALID_DATE_ERROR = 'd52afa47-620d-4d99-9f08-f4d85b36e33c'; public const INVALID_TIME_ERROR = '5e797c9d-74f7-4098-baa3-94390c447b27'; protected static $errorNames = [ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', self::INVALID_DATE_ERROR => 'INVALID_DATE_ERROR', self::INVALID_TIME_ERROR => 'INVALID_TIME_ERROR', ]; public $format = 'Y-m-d H:i:s'; public $message = 'This value is not a valid datetime.'; /** * {@inheritdoc} * * @param string|array|null $format */ public function __construct($format = null, ?string $message = null, ?array $groups = null, $payload = null, array $options = []) { if (\is_array($format)) { $options = array_merge($format, $options); } elseif (null !== $format) { $options['value'] = $format; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } public function getDefaultOption() { return 'format'; } } PK!  :vendor/symfony/validator/Constraints/DateTimeValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek * @author Diego Saint Esteben */ class DateTimeValidator extends DateValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof DateTime) { throw new UnexpectedTypeException($constraint, DateTime::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; \DateTime::createFromFormat($constraint->format, $value); $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warnings' => []]; if (0 < $errors['error_count']) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(DateTime::INVALID_FORMAT_ERROR) ->addViolation(); return; } if (str_ends_with($constraint->format, '+')) { $errors['warnings'] = array_filter($errors['warnings'], function ($warning) { return 'Trailing data' !== $warning; }); } foreach ($errors['warnings'] as $warning) { if ('The parsed date was invalid' === $warning) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(DateTime::INVALID_DATE_ERROR) ->addViolation(); } elseif ('The parsed time was invalid' === $warning) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(DateTime::INVALID_TIME_ERROR) ->addViolation(); } else { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(DateTime::INVALID_FORMAT_ERROR) ->addViolation(); } } } } PK!JA6vendor/symfony/validator/Constraints/DateValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class DateValidator extends ConstraintValidator { public const PATTERN = '/^(?\d{4})-(?\d{2})-(?\d{2})$/D'; /** * Checks whether a date is valid. * * @internal */ public static function checkDate(int $year, int $month, int $day): bool { return checkdate($month, $day, $year); } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Date) { throw new UnexpectedTypeException($constraint, Date::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (!preg_match(static::PATTERN, $value, $matches)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Date::INVALID_FORMAT_ERROR) ->addViolation(); return; } if (!self::checkDate( $matches['year'] ?? $matches[1], $matches['month'] ?? $matches[2], $matches['day'] ?? $matches[3] )) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Date::INVALID_DATE_ERROR) ->addViolation(); } } } PK!ܴ**;vendor/symfony/validator/Constraints/DisableAutoMapping.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Disables auto mapping. * * Using the annotations on a property has higher precedence than using it on a class, * which has higher precedence than any configuration that might be defined outside the class. * * @Annotation * * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)] class DisableAutoMapping extends Constraint { public function __construct(?array $options = null) { if (\is_array($options) && \array_key_exists('groups', $options)) { throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } parent::__construct($options); } /** * {@inheritdoc} */ public function getTargets() { return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT]; } } PK!~224vendor/symfony/validator/Constraints/DivisibleBy.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Colin O'Dell */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class DivisibleBy extends AbstractComparison { public const NOT_DIVISIBLE_BY = '6d99d6c3-1464-4ccf-bdc7-14d083cf455c'; protected static $errorNames = [ self::NOT_DIVISIBLE_BY => 'NOT_DIVISIBLE_BY', ]; public $message = 'This value should be a multiple of {{ compared_value }}.'; } PK! kBB=vendor/symfony/validator/Constraints/DivisibleByValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates that values are a multiple of the given number. * * @author Colin O'Dell */ class DivisibleByValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { if (!is_numeric($value1)) { throw new UnexpectedValueException($value1, 'numeric'); } if (!is_numeric($value2)) { throw new UnexpectedValueException($value2, 'numeric'); } if (!$value2 = abs($value2)) { return false; } if (\is_int($value1 = abs($value1)) && \is_int($value2)) { return 0 === ($value1 % $value2); } if (!$remainder = fmod($value1, $value2)) { return true; } if (\is_float($value2) && \INF !== $value2) { $quotient = $value1 / $value2; $rounded = round($quotient); return sprintf('%.12e', $quotient) === sprintf('%.12e', $rounded); } return sprintf('%.12e', $value2) === sprintf('%.12e', $remainder); } /** * {@inheritdoc} */ protected function getErrorCode() { return DivisibleBy::NOT_DIVISIBLE_BY; } } PK!& & .vendor/symfony/validator/Constraints/Email.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Egulias\EmailValidator\EmailValidator as StrictEmailValidator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Email extends Constraint { public const VALIDATION_MODE_HTML5 = 'html5'; public const VALIDATION_MODE_STRICT = 'strict'; public const VALIDATION_MODE_LOOSE = 'loose'; public const INVALID_FORMAT_ERROR = 'bd79c0ab-ddba-46cc-a703-a7a4b08de310'; protected static $errorNames = [ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', ]; /** * @var string[] * * @internal */ public static $validationModes = [ self::VALIDATION_MODE_HTML5, self::VALIDATION_MODE_STRICT, self::VALIDATION_MODE_LOOSE, ]; public $message = 'This value is not a valid email address.'; public $mode; public $normalizer; public function __construct( ?array $options = null, ?string $message = null, ?string $mode = null, ?callable $normalizer = null, ?array $groups = null, $payload = null ) { if (\is_array($options) && \array_key_exists('mode', $options) && !\in_array($options['mode'], self::$validationModes, true)) { throw new InvalidArgumentException('The "mode" parameter value is not valid.'); } if (null !== $mode && !\in_array($mode, self::$validationModes, true)) { throw new InvalidArgumentException('The "mode" parameter value is not valid.'); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->mode = $mode ?? $this->mode; $this->normalizer = $normalizer ?? $this->normalizer; if (self::VALIDATION_MODE_STRICT === $this->mode && !class_exists(StrictEmailValidator::class)) { throw new LogicException(sprintf('The "egulias/email-validator" component is required to use the "%s" constraint in strict mode.', __CLASS__)); } if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } } PK!'aމ7vendor/symfony/validator/Constraints/EmailValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Egulias\EmailValidator\EmailValidator as EguliasEmailValidator; use Egulias\EmailValidator\Validation\EmailValidation; use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class EmailValidator extends ConstraintValidator { private const PATTERN_HTML5 = '/^[a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/D'; private const PATTERN_LOOSE = '/^.+\@\S+\.\S+$/D'; private const EMAIL_PATTERNS = [ Email::VALIDATION_MODE_LOOSE => self::PATTERN_LOOSE, Email::VALIDATION_MODE_HTML5 => self::PATTERN_HTML5, ]; private $defaultMode; public function __construct(string $defaultMode = Email::VALIDATION_MODE_LOOSE) { if (!\in_array($defaultMode, Email::$validationModes, true)) { throw new \InvalidArgumentException('The "defaultMode" parameter value is not valid.'); } $this->defaultMode = $defaultMode; } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Email) { throw new UnexpectedTypeException($constraint, Email::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ('' === $value) { return; } if (null !== $constraint->normalizer) { $value = ($constraint->normalizer)($value); } if (null === $constraint->mode) { if (Email::VALIDATION_MODE_STRICT === $this->defaultMode && !class_exists(EguliasEmailValidator::class)) { throw new LogicException(sprintf('The "egulias/email-validator" component is required to make the "%s" constraint default to strict mode.', Email::class)); } $constraint->mode = $this->defaultMode; } if (!\in_array($constraint->mode, Email::$validationModes, true)) { throw new \InvalidArgumentException(sprintf('The "%s::$mode" parameter value is not valid.', get_debug_type($constraint))); } if (Email::VALIDATION_MODE_STRICT === $constraint->mode) { $strictValidator = new EguliasEmailValidator(); if (interface_exists(EmailValidation::class) && !$strictValidator->isValid($value, new NoRFCWarningsValidation())) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) ->addViolation(); return; } elseif (!interface_exists(EmailValidation::class) && !$strictValidator->isValid($value, false, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) ->addViolation(); return; } } elseif (!preg_match(self::EMAIL_PATTERNS[$constraint->mode], $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) ->addViolation(); return; } } } PK!;((:vendor/symfony/validator/Constraints/EnableAutoMapping.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Enables auto mapping. * * Using the annotations on a property has higher precedence than using it on a class, * which has higher precedence than any configuration that might be defined outside the class. * * @Annotation * * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)] class EnableAutoMapping extends Constraint { public function __construct(?array $options = null) { if (\is_array($options) && \array_key_exists('groups', $options)) { throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } parent::__construct($options); } /** * {@inheritdoc} */ public function getTargets() { return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT]; } } PK!Rkl]]0vendor/symfony/validator/Constraints/EqualTo.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class EqualTo extends AbstractComparison { public const NOT_EQUAL_ERROR = '478618a7-95ba-473d-9101-cabd45e49115'; protected static $errorNames = [ self::NOT_EQUAL_ERROR => 'NOT_EQUAL_ERROR', ]; public $message = 'This value should be equal to {{ compared_value }}.'; } PK!kS-9vendor/symfony/validator/Constraints/EqualToValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are equal (==). * * @author Daniel Holmes * @author Bernhard Schussek */ class EqualToValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return $value1 == $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return EqualTo::NOT_EQUAL_ERROR; } } PK!SS2vendor/symfony/validator/Constraints/Existence.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @author Bernhard Schussek */ abstract class Existence extends Composite { public $constraints = []; public function getDefaultOption() { return 'constraints'; } protected function getCompositeOption() { return 'constraints'; } } PK!$_Avendor/symfony/validator/Constraints/ExpressionLanguageSyntax.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Andrey Sevastianov */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class ExpressionLanguageSyntax extends Constraint { public const EXPRESSION_LANGUAGE_SYNTAX_ERROR = '1766a3f3-ff03-40eb-b053-ab7aa23d988a'; protected static $errorNames = [ self::EXPRESSION_LANGUAGE_SYNTAX_ERROR => 'EXPRESSION_LANGUAGE_SYNTAX_ERROR', ]; public $message = 'This value should be a valid expression.'; public $service; public $allowedVariables; public function __construct(?array $options = null, ?string $message = null, ?string $service = null, ?array $allowedVariables = null, ?array $groups = null, $payload = null) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->service = $service ?? $this->service; $this->allowedVariables = $allowedVariables ?? $this->allowedVariables; } /** * {@inheritdoc} */ public function validatedBy() { return $this->service ?? static::class.'Validator'; } } PK!HR܋Jvendor/symfony/validator/Constraints/ExpressionLanguageSyntaxValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\ExpressionLanguage\SyntaxError; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Andrey Sevastianov */ class ExpressionLanguageSyntaxValidator extends ConstraintValidator { private $expressionLanguage; public function __construct(?ExpressionLanguage $expressionLanguage = null) { $this->expressionLanguage = $expressionLanguage; } /** * {@inheritdoc} */ public function validate($expression, Constraint $constraint): void { if (!$constraint instanceof ExpressionLanguageSyntax) { throw new UnexpectedTypeException($constraint, ExpressionLanguageSyntax::class); } if (!\is_string($expression)) { throw new UnexpectedValueException($expression, 'string'); } if (null === $this->expressionLanguage) { $this->expressionLanguage = new ExpressionLanguage(); } try { $this->expressionLanguage->lint($expression, $constraint->allowedVariables); } catch (SyntaxError $exception) { $this->context->buildViolation($constraint->message) ->setParameter('{{ syntax_error }}', $this->formatValue($exception->getMessage())) ->setInvalidValue((string) $expression) ->setCode(ExpressionLanguageSyntax::EXPRESSION_LANGUAGE_SYNTAX_ERROR) ->addViolation(); } } } PK!VV V 3vendor/symfony/validator/Constraints/Expression.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\ExpressionLanguage\Expression as ExpressionObject; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) * * @author Fabien Potencier * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class Expression extends Constraint { public const EXPRESSION_FAILED_ERROR = '6b3befbc-2f01-4ddf-be21-b57898905284'; protected static $errorNames = [ self::EXPRESSION_FAILED_ERROR => 'EXPRESSION_FAILED_ERROR', ]; public $message = 'This value is not valid.'; public $expression; public $values = []; /** * {@inheritdoc} * * @param string|ExpressionObject|array $expression The expression to evaluate or an array of options */ public function __construct( $expression, ?string $message = null, ?array $values = null, ?array $groups = null, $payload = null, array $options = [] ) { if (!class_exists(ExpressionLanguage::class)) { throw new LogicException(sprintf('The "symfony/expression-language" component is required to use the "%s" constraint.', __CLASS__)); } if (\is_array($expression)) { $options = array_merge($expression, $options); } elseif (!\is_string($expression) && !$expression instanceof ExpressionObject) { throw new \TypeError(sprintf('"%s": Expected argument $expression to be either a string, an instance of "%s" or an array, got "%s".', __METHOD__, ExpressionObject::class, get_debug_type($expression))); } else { $options['value'] = $expression; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->values = $values ?? $this->values; } /** * {@inheritdoc} */ public function getDefaultOption() { return 'expression'; } /** * {@inheritdoc} */ public function getRequiredOptions() { return ['expression']; } /** * {@inheritdoc} */ public function getTargets() { return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT]; } /** * {@inheritdoc} */ public function validatedBy() { return 'validator.expression'; } } PK!=OLL<vendor/symfony/validator/Constraints/ExpressionValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Fabien Potencier * @author Bernhard Schussek */ class ExpressionValidator extends ConstraintValidator { private $expressionLanguage; public function __construct(?ExpressionLanguage $expressionLanguage = null) { $this->expressionLanguage = $expressionLanguage; } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Expression) { throw new UnexpectedTypeException($constraint, Expression::class); } $variables = $constraint->values; $variables['value'] = $value; $variables['this'] = $this->context->getObject(); if (!$this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value, self::OBJECT_TO_STRING)) ->setCode(Expression::EXPRESSION_FAILED_ERROR) ->addViolation(); } } private function getExpressionLanguage(): ExpressionLanguage { if (null === $this->expressionLanguage) { $this->expressionLanguage = new ExpressionLanguage(); } return $this->expressionLanguage; } } PK!tCC-vendor/symfony/validator/Constraints/File.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @property int $maxSize * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class File extends Constraint { // Check the Image constraint for clashes if adding new constants here public const NOT_FOUND_ERROR = 'd2a3fb6e-7ddc-4210-8fbf-2ab345ce1998'; public const NOT_READABLE_ERROR = 'c20c92a4-5bfa-4202-9477-28e800e0f6ff'; public const EMPTY_ERROR = '5d743385-9775-4aa5-8ff5-495fb1e60137'; public const TOO_LARGE_ERROR = 'df8637af-d466-48c6-a59d-e7126250a654'; public const INVALID_MIME_TYPE_ERROR = '744f00bc-4389-4c74-92de-9a43cde55534'; protected static $errorNames = [ self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR', self::NOT_READABLE_ERROR => 'NOT_READABLE_ERROR', self::EMPTY_ERROR => 'EMPTY_ERROR', self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR', ]; public $binaryFormat; public $mimeTypes = []; public $notFoundMessage = 'The file could not be found.'; public $notReadableMessage = 'The file is not readable.'; public $maxSizeMessage = 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.'; public $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; public $disallowEmptyMessage = 'An empty file is not allowed.'; public $uploadIniSizeErrorMessage = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'; public $uploadFormSizeErrorMessage = 'The file is too large.'; public $uploadPartialErrorMessage = 'The file was only partially uploaded.'; public $uploadNoFileErrorMessage = 'No file was uploaded.'; public $uploadNoTmpDirErrorMessage = 'No temporary folder was configured in php.ini.'; public $uploadCantWriteErrorMessage = 'Cannot write temporary file to disk.'; public $uploadExtensionErrorMessage = 'A PHP extension caused the upload to fail.'; public $uploadErrorMessage = 'The file could not be uploaded.'; protected $maxSize; /** * {@inheritdoc} * * @param int|string|null $maxSize * @param string[]|string|null $mimeTypes */ public function __construct( ?array $options = null, $maxSize = null, ?bool $binaryFormat = null, $mimeTypes = null, ?string $notFoundMessage = null, ?string $notReadableMessage = null, ?string $maxSizeMessage = null, ?string $mimeTypesMessage = null, ?string $disallowEmptyMessage = null, ?string $uploadIniSizeErrorMessage = null, ?string $uploadFormSizeErrorMessage = null, ?string $uploadPartialErrorMessage = null, ?string $uploadNoFileErrorMessage = null, ?string $uploadNoTmpDirErrorMessage = null, ?string $uploadCantWriteErrorMessage = null, ?string $uploadExtensionErrorMessage = null, ?string $uploadErrorMessage = null, ?array $groups = null, $payload = null ) { if (null !== $maxSize && !\is_int($maxSize) && !\is_string($maxSize)) { throw new \TypeError(sprintf('"%s": Expected argument $maxSize to be either null, an integer or a string, got "%s".', __METHOD__, get_debug_type($maxSize))); } if (null !== $mimeTypes && !\is_array($mimeTypes) && !\is_string($mimeTypes)) { throw new \TypeError(sprintf('"%s": Expected argument $mimeTypes to be either null, an array or a string, got "%s".', __METHOD__, get_debug_type($mimeTypes))); } parent::__construct($options, $groups, $payload); $this->maxSize = $maxSize ?? $this->maxSize; $this->binaryFormat = $binaryFormat ?? $this->binaryFormat; $this->mimeTypes = $mimeTypes ?? $this->mimeTypes; $this->notFoundMessage = $notFoundMessage ?? $this->notFoundMessage; $this->notReadableMessage = $notReadableMessage ?? $this->notReadableMessage; $this->maxSizeMessage = $maxSizeMessage ?? $this->maxSizeMessage; $this->mimeTypesMessage = $mimeTypesMessage ?? $this->mimeTypesMessage; $this->disallowEmptyMessage = $disallowEmptyMessage ?? $this->disallowEmptyMessage; $this->uploadIniSizeErrorMessage = $uploadIniSizeErrorMessage ?? $this->uploadIniSizeErrorMessage; $this->uploadFormSizeErrorMessage = $uploadFormSizeErrorMessage ?? $this->uploadFormSizeErrorMessage; $this->uploadPartialErrorMessage = $uploadPartialErrorMessage ?? $this->uploadPartialErrorMessage; $this->uploadNoFileErrorMessage = $uploadNoFileErrorMessage ?? $this->uploadNoFileErrorMessage; $this->uploadNoTmpDirErrorMessage = $uploadNoTmpDirErrorMessage ?? $this->uploadNoTmpDirErrorMessage; $this->uploadCantWriteErrorMessage = $uploadCantWriteErrorMessage ?? $this->uploadCantWriteErrorMessage; $this->uploadExtensionErrorMessage = $uploadExtensionErrorMessage ?? $this->uploadExtensionErrorMessage; $this->uploadErrorMessage = $uploadErrorMessage ?? $this->uploadErrorMessage; if (null !== $this->maxSize) { $this->normalizeBinaryFormat($this->maxSize); } } public function __set(string $option, $value) { if ('maxSize' === $option) { $this->normalizeBinaryFormat($value); return; } parent::__set($option, $value); } public function __get(string $option) { if ('maxSize' === $option) { return $this->maxSize; } return parent::__get($option); } public function __isset(string $option) { if ('maxSize' === $option) { return true; } return parent::__isset($option); } /** * @param int|string $maxSize */ private function normalizeBinaryFormat($maxSize) { $factors = [ 'k' => 1000, 'ki' => 1 << 10, 'm' => 1000 * 1000, 'mi' => 1 << 20, 'g' => 1000 * 1000 * 1000, 'gi' => 1 << 30, ]; if (ctype_digit((string) $maxSize)) { $this->maxSize = (int) $maxSize; $this->binaryFormat = $this->binaryFormat ?? false; } elseif (preg_match('/^(\d++)('.implode('|', array_keys($factors)).')$/i', $maxSize, $matches)) { $this->maxSize = $matches[1] * $factors[$unit = strtolower($matches[2])]; $this->binaryFormat = $this->binaryFormat ?? (2 === \strlen($unit)); } else { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size.', $maxSize)); } } } PK!@3%%6vendor/symfony/validator/Constraints/FileValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\HttpFoundation\File\File as FileObject; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class FileValidator extends ConstraintValidator { public const KB_BYTES = 1000; public const MB_BYTES = 1000000; public const KIB_BYTES = 1024; public const MIB_BYTES = 1048576; private const SUFFICES = [ 1 => 'bytes', self::KB_BYTES => 'kB', self::MB_BYTES => 'MB', self::KIB_BYTES => 'KiB', self::MIB_BYTES => 'MiB', ]; /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof File) { throw new UnexpectedTypeException($constraint, File::class); } if (null === $value || '' === $value) { return; } if ($value instanceof UploadedFile && !$value->isValid()) { switch ($value->getError()) { case \UPLOAD_ERR_INI_SIZE: $iniLimitSize = UploadedFile::getMaxFilesize(); if ($constraint->maxSize && $constraint->maxSize < $iniLimitSize) { $limitInBytes = $constraint->maxSize; $binaryFormat = $constraint->binaryFormat; } else { $limitInBytes = $iniLimitSize; $binaryFormat = $constraint->binaryFormat ?? true; } [, $limitAsString, $suffix] = $this->factorizeSizes(0, $limitInBytes, $binaryFormat); $this->context->buildViolation($constraint->uploadIniSizeErrorMessage) ->setParameter('{{ limit }}', $limitAsString) ->setParameter('{{ suffix }}', $suffix) ->setCode((string) \UPLOAD_ERR_INI_SIZE) ->addViolation(); return; case \UPLOAD_ERR_FORM_SIZE: $this->context->buildViolation($constraint->uploadFormSizeErrorMessage) ->setCode((string) \UPLOAD_ERR_FORM_SIZE) ->addViolation(); return; case \UPLOAD_ERR_PARTIAL: $this->context->buildViolation($constraint->uploadPartialErrorMessage) ->setCode((string) \UPLOAD_ERR_PARTIAL) ->addViolation(); return; case \UPLOAD_ERR_NO_FILE: $this->context->buildViolation($constraint->uploadNoFileErrorMessage) ->setCode((string) \UPLOAD_ERR_NO_FILE) ->addViolation(); return; case \UPLOAD_ERR_NO_TMP_DIR: $this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage) ->setCode((string) \UPLOAD_ERR_NO_TMP_DIR) ->addViolation(); return; case \UPLOAD_ERR_CANT_WRITE: $this->context->buildViolation($constraint->uploadCantWriteErrorMessage) ->setCode((string) \UPLOAD_ERR_CANT_WRITE) ->addViolation(); return; case \UPLOAD_ERR_EXTENSION: $this->context->buildViolation($constraint->uploadExtensionErrorMessage) ->setCode((string) \UPLOAD_ERR_EXTENSION) ->addViolation(); return; default: $this->context->buildViolation($constraint->uploadErrorMessage) ->setCode((string) $value->getError()) ->addViolation(); return; } } if (!\is_scalar($value) && !$value instanceof FileObject && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $path = $value instanceof FileObject ? $value->getPathname() : (string) $value; if (!is_file($path)) { $this->context->buildViolation($constraint->notFoundMessage) ->setParameter('{{ file }}', $this->formatValue($path)) ->setCode(File::NOT_FOUND_ERROR) ->addViolation(); return; } if (!is_readable($path)) { $this->context->buildViolation($constraint->notReadableMessage) ->setParameter('{{ file }}', $this->formatValue($path)) ->setCode(File::NOT_READABLE_ERROR) ->addViolation(); return; } $sizeInBytes = filesize($path); $basename = $value instanceof UploadedFile ? $value->getClientOriginalName() : basename($path); if (0 === $sizeInBytes) { $this->context->buildViolation($constraint->disallowEmptyMessage) ->setParameter('{{ file }}', $this->formatValue($path)) ->setParameter('{{ name }}', $this->formatValue($basename)) ->setCode(File::EMPTY_ERROR) ->addViolation(); return; } if ($constraint->maxSize) { $limitInBytes = $constraint->maxSize; if ($sizeInBytes > $limitInBytes) { [$sizeAsString, $limitAsString, $suffix] = $this->factorizeSizes($sizeInBytes, $limitInBytes, $constraint->binaryFormat); $this->context->buildViolation($constraint->maxSizeMessage) ->setParameter('{{ file }}', $this->formatValue($path)) ->setParameter('{{ size }}', $sizeAsString) ->setParameter('{{ limit }}', $limitAsString) ->setParameter('{{ suffix }}', $suffix) ->setParameter('{{ name }}', $this->formatValue($basename)) ->setCode(File::TOO_LARGE_ERROR) ->addViolation(); return; } } if ($constraint->mimeTypes) { if ($value instanceof FileObject) { $mime = $value->getMimeType(); } elseif (class_exists(MimeTypes::class)) { $mime = MimeTypes::getDefault()->guessMimeType($path); } elseif (!class_exists(FileObject::class)) { throw new LogicException('You cannot validate the mime-type of files as the Mime component is not installed. Try running "composer require symfony/mime".'); } else { $mime = (new FileObject($value))->getMimeType(); } $mimeTypes = (array) $constraint->mimeTypes; foreach ($mimeTypes as $mimeType) { if ($mimeType === $mime) { return; } if ($discrete = strstr($mimeType, '/*', true)) { if (strstr($mime, '/', true) === $discrete) { return; } } } $this->context->buildViolation($constraint->mimeTypesMessage) ->setParameter('{{ file }}', $this->formatValue($path)) ->setParameter('{{ type }}', $this->formatValue($mime)) ->setParameter('{{ types }}', $this->formatValues($mimeTypes)) ->setParameter('{{ name }}', $this->formatValue($basename)) ->setCode(File::INVALID_MIME_TYPE_ERROR) ->addViolation(); } } private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool { return \strlen($double) > \strlen(round($double, $numberOfDecimals)); } /** * Convert the limit to the smallest possible number * (i.e. try "MB", then "kB", then "bytes"). * * @param int|float $limit */ private function factorizeSizes(int $size, $limit, bool $binaryFormat): array { if ($binaryFormat) { $coef = self::MIB_BYTES; $coefFactor = self::KIB_BYTES; } else { $coef = self::MB_BYTES; $coefFactor = self::KB_BYTES; } $limitAsString = (string) ($limit / $coef); // Restrict the limit to 2 decimals (without rounding! we // need the precise value) while (self::moreDecimalsThan($limitAsString, 2)) { $coef /= $coefFactor; $limitAsString = (string) ($limit / $coef); } // Convert size to the same measure, but round to 2 decimals $sizeAsString = (string) round($size / $coef, 2); // If the size and limit produce the same string output // (due to rounding), reduce the coefficient while ($sizeAsString === $limitAsString) { $coef /= $coefFactor; $limitAsString = (string) ($limit / $coef); $sizeAsString = (string) round($size / $coef, 2); } return [$sizeAsString, $limitAsString, self::SUFFICES[$coef]]; } } PK!rr;vendor/symfony/validator/Constraints/GreaterThanOrEqual.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class GreaterThanOrEqual extends AbstractComparison { public const TOO_LOW_ERROR = 'ea4e51d1-3342-48bd-87f1-9e672cd90cad'; protected static $errorNames = [ self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', ]; public $message = 'This value should be greater than or equal to {{ compared_value }}.'; } PK!ZX%JJDvendor/symfony/validator/Constraints/GreaterThanOrEqualValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are greater than or equal to the previous (>=). * * @author Daniel Holmes * @author Bernhard Schussek */ class GreaterThanOrEqualValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return null === $value2 || $value1 >= $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return GreaterThanOrEqual::TOO_LOW_ERROR; } } PK!Y071__4vendor/symfony/validator/Constraints/GreaterThan.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class GreaterThan extends AbstractComparison { public const TOO_LOW_ERROR = '778b7ae0-84d3-481a-9dec-35fdb64b1d78'; protected static $errorNames = [ self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', ]; public $message = 'This value should be greater than {{ compared_value }}.'; } PK!2..=vendor/symfony/validator/Constraints/GreaterThanValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are greater than the previous (>). * * @author Daniel Holmes * @author Bernhard Schussek */ class GreaterThanValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return null === $value2 || $value1 > $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return GreaterThan::TOO_LOW_ERROR; } } PK!F 6vendor/symfony/validator/Constraints/GroupSequence.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * A sequence of validation groups. * * When validating a group sequence, each group will only be validated if all * of the previous groups in the sequence succeeded. For example: * * $validator->validate($address, null, new GroupSequence(['Basic', 'Strict'])); * * In the first step, all constraints that belong to the group "Basic" will be * validated. If none of the constraints fail, the validator will then validate * the constraints in group "Strict". This is useful, for example, if "Strict" * contains expensive checks that require a lot of CPU or slow, external * services. You usually don't want to run expensive checks if any of the cheap * checks fail. * * When adding metadata to a class, you can override the "Default" group of * that class with a group sequence: * /** * * @GroupSequence({"Address", "Strict"}) * *\/ * class Address * { * // ... * } * * Whenever you validate that object in the "Default" group, the group sequence * will be validated: * * $validator->validate($address); * * If you want to execute the constraints of the "Default" group for a class * with an overridden default group, pass the class name as group name instead: * * $validator->validate($address, null, "Address") * * @Annotation * @Target({"CLASS", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS)] class GroupSequence { /** * The groups in the sequence. * * @var array */ public $groups; /** * The group in which cascaded objects are validated when validating * this sequence. * * By default, cascaded objects are validated in each of the groups of * the sequence. * * If a class has a group sequence attached, that sequence replaces the * "Default" group. When validating that class in the "Default" group, the * group sequence is used instead, but still the "Default" group should be * cascaded to other objects. * * @var string|GroupSequence */ public $cascadedGroup; /** * Creates a new group sequence. * * @param array $groups The groups in the sequence */ public function __construct(array $groups) { // Support for Doctrine annotations $this->groups = $groups['value'] ?? $groups; } } PK!2]>vendor/symfony/validator/Constraints/GroupSequenceProvider.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Annotation to define a group sequence provider. * * @Annotation * @Target({"CLASS", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS)] class GroupSequenceProvider { } PK!TQm21vendor/symfony/validator/Constraints/Hostname.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Dmitrii Poddubnyi */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Hostname extends Constraint { public const INVALID_HOSTNAME_ERROR = '7057ffdb-0af4-4f7e-bd5e-e9acfa6d7a2d'; protected static $errorNames = [ self::INVALID_HOSTNAME_ERROR => 'INVALID_HOSTNAME_ERROR', ]; public $message = 'This value is not a valid hostname.'; public $requireTld = true; public function __construct( ?array $options = null, ?string $message = null, ?bool $requireTld = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->requireTld = $requireTld ?? $this->requireTld; } } PK!2/:vendor/symfony/validator/Constraints/HostnameValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Dmitrii Poddubnyi */ class HostnameValidator extends ConstraintValidator { /** * https://tools.ietf.org/html/rfc2606. */ private const RESERVED_TLDS = [ 'example', 'invalid', 'localhost', 'test', ]; public function validate($value, Constraint $constraint) { if (!$constraint instanceof Hostname) { throw new UnexpectedTypeException($constraint, Hostname::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ('' === $value) { return; } if (!$this->isValid($value) || ($constraint->requireTld && !$this->hasValidTld($value))) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Hostname::INVALID_HOSTNAME_ERROR) ->addViolation(); } } private function isValid(string $domain): bool { return false !== filter_var($domain, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME); } private function hasValidTld(string $domain): bool { return false !== strpos($domain, '.') && !\in_array(substr($domain, strrpos($domain, '.') + 1), self::RESERVED_TLDS, true); } } PK!ʉ  -vendor/symfony/validator/Constraints/Iban.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Manuel Reinhard * @author Michael Schummel * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Iban extends Constraint { public const INVALID_COUNTRY_CODE_ERROR = 'de78ee2c-bd50-44e2-aec8-3d8228aeadb9'; public const INVALID_CHARACTERS_ERROR = '8d3d85e4-784f-4719-a5bc-d9e40d45a3a5'; public const CHECKSUM_FAILED_ERROR = 'b9401321-f9bf-4dcb-83c1-f31094440795'; public const INVALID_FORMAT_ERROR = 'c8d318f1-2ecc-41ba-b983-df70d225cf5a'; public const NOT_SUPPORTED_COUNTRY_CODE_ERROR = 'e2c259f3-4b46-48e6-b72e-891658158ec8'; protected static $errorNames = [ self::INVALID_COUNTRY_CODE_ERROR => 'INVALID_COUNTRY_CODE_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::CHECKSUM_FAILED_ERROR => 'CHECKSUM_FAILED_ERROR', self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', self::NOT_SUPPORTED_COUNTRY_CODE_ERROR => 'NOT_SUPPORTED_COUNTRY_CODE_ERROR', ]; public $message = 'This is not a valid International Bank Account Number (IBAN).'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!Ӿ336vendor/symfony/validator/Constraints/IbanValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Manuel Reinhard * @author Michael Schummel * @author Bernhard Schussek */ class IbanValidator extends ConstraintValidator { /** * IBAN country specific formats. * * The first 2 characters from an IBAN format are the two-character ISO country code. * The following 2 characters represent the check digits calculated from the rest of the IBAN characters. * The rest are up to thirty alphanumeric characters for * a BBAN (Basic Bank Account Number) which has a fixed length per country and, * included within it, a bank identifier with a fixed position and a fixed length per country * * @see Resources/bin/sync-iban-formats.php * @see https://www.swift.com/swift-resource/11971/download?language=en * @see https://en.wikipedia.org/wiki/International_Bank_Account_Number */ private const FORMATS = [ // auto-generated 'AD' => 'AD\d{2}\d{4}\d{4}[\dA-Z]{12}', // Andorra 'AE' => 'AE\d{2}\d{3}\d{16}', // United Arab Emirates (The) 'AL' => 'AL\d{2}\d{8}[\dA-Z]{16}', // Albania 'AO' => 'AO\d{2}\d{21}', // Angola 'AT' => 'AT\d{2}\d{5}\d{11}', // Austria 'AX' => 'FI\d{2}\d{3}\d{11}', // Finland 'AZ' => 'AZ\d{2}[A-Z]{4}[\dA-Z]{20}', // Azerbaijan 'BA' => 'BA\d{2}\d{3}\d{3}\d{8}\d{2}', // Bosnia and Herzegovina 'BE' => 'BE\d{2}\d{3}\d{7}\d{2}', // Belgium 'BF' => 'BF\d{2}[\dA-Z]{2}\d{22}', // Burkina Faso 'BG' => 'BG\d{2}[A-Z]{4}\d{4}\d{2}[\dA-Z]{8}', // Bulgaria 'BH' => 'BH\d{2}[A-Z]{4}[\dA-Z]{14}', // Bahrain 'BI' => 'BI\d{2}\d{5}\d{5}\d{11}\d{2}', // Burundi 'BJ' => 'BJ\d{2}[\dA-Z]{2}\d{22}', // Benin 'BL' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'BR' => 'BR\d{2}\d{8}\d{5}\d{10}[A-Z]{1}[\dA-Z]{1}', // Brazil 'BY' => 'BY\d{2}[\dA-Z]{4}\d{4}[\dA-Z]{16}', // Republic of Belarus 'CF' => 'CF\d{2}\d{23}', // Central African Republic 'CG' => 'CG\d{2}\d{23}', // Congo, Republic of the 'CH' => 'CH\d{2}\d{5}[\dA-Z]{12}', // Switzerland 'CI' => 'CI\d{2}[A-Z]{1}\d{23}', // Côte d'Ivoire 'CM' => 'CM\d{2}\d{23}', // Cameroon 'CR' => 'CR\d{2}\d{4}\d{14}', // Costa Rica 'CV' => 'CV\d{2}\d{21}', // Cabo Verde 'CY' => 'CY\d{2}\d{3}\d{5}[\dA-Z]{16}', // Cyprus 'CZ' => 'CZ\d{2}\d{4}\d{6}\d{10}', // Czechia 'DE' => 'DE\d{2}\d{8}\d{10}', // Germany 'DJ' => 'DJ\d{2}\d{5}\d{5}\d{11}\d{2}', // Djibouti 'DK' => 'DK\d{2}\d{4}\d{9}\d{1}', // Denmark 'DO' => 'DO\d{2}[\dA-Z]{4}\d{20}', // Dominican Republic 'DZ' => 'DZ\d{2}\d{22}', // Algeria 'EE' => 'EE\d{2}\d{2}\d{2}\d{11}\d{1}', // Estonia 'EG' => 'EG\d{2}\d{4}\d{4}\d{17}', // Egypt 'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain 'FI' => 'FI\d{2}\d{3}\d{11}', // Finland 'FK' => 'FK\d{2}[A-Z]{2}\d{12}', // Falkland Islands 'FO' => 'FO\d{2}\d{4}\d{9}\d{1}', // Faroe Islands 'FR' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'GA' => 'GA\d{2}\d{23}', // Gabon 'GB' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom 'GE' => 'GE\d{2}[A-Z]{2}\d{16}', // Georgia 'GF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'GG' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom 'GI' => 'GI\d{2}[A-Z]{4}[\dA-Z]{15}', // Gibraltar 'GL' => 'GL\d{2}\d{4}\d{9}\d{1}', // Greenland 'GP' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'GQ' => 'GQ\d{2}\d{23}', // Equatorial Guinea 'GR' => 'GR\d{2}\d{3}\d{4}[\dA-Z]{16}', // Greece 'GT' => 'GT\d{2}[\dA-Z]{4}[\dA-Z]{20}', // Guatemala 'GW' => 'GW\d{2}[\dA-Z]{2}\d{19}', // Guinea-Bissau 'HN' => 'HN\d{2}[A-Z]{4}\d{20}', // Honduras 'HR' => 'HR\d{2}\d{7}\d{10}', // Croatia 'HU' => 'HU\d{2}\d{3}\d{4}\d{1}\d{15}\d{1}', // Hungary 'IE' => 'IE\d{2}[A-Z]{4}\d{6}\d{8}', // Ireland 'IL' => 'IL\d{2}\d{3}\d{3}\d{13}', // Israel 'IM' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom 'IQ' => 'IQ\d{2}[A-Z]{4}\d{3}\d{12}', // Iraq 'IR' => 'IR\d{2}\d{22}', // Iran 'IS' => 'IS\d{2}\d{4}\d{2}\d{6}\d{10}', // Iceland 'IT' => 'IT\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // Italy 'JE' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom 'JO' => 'JO\d{2}[A-Z]{4}\d{4}[\dA-Z]{18}', // Jordan 'KM' => 'KM\d{2}\d{23}', // Comoros 'KW' => 'KW\d{2}[A-Z]{4}[\dA-Z]{22}', // Kuwait 'KZ' => 'KZ\d{2}\d{3}[\dA-Z]{13}', // Kazakhstan 'LB' => 'LB\d{2}\d{4}[\dA-Z]{20}', // Lebanon 'LC' => 'LC\d{2}[A-Z]{4}[\dA-Z]{24}', // Saint Lucia 'LI' => 'LI\d{2}\d{5}[\dA-Z]{12}', // Liechtenstein 'LT' => 'LT\d{2}\d{5}\d{11}', // Lithuania 'LU' => 'LU\d{2}\d{3}[\dA-Z]{13}', // Luxembourg 'LV' => 'LV\d{2}[A-Z]{4}[\dA-Z]{13}', // Latvia 'LY' => 'LY\d{2}\d{3}\d{3}\d{15}', // Libya 'MA' => 'MA\d{2}\d{24}', // Morocco 'MC' => 'MC\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Monaco 'MD' => 'MD\d{2}[\dA-Z]{2}[\dA-Z]{18}', // Moldova 'ME' => 'ME\d{2}\d{3}\d{13}\d{2}', // Montenegro 'MF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'MG' => 'MG\d{2}\d{23}', // Madagascar 'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia 'ML' => 'ML\d{2}[\dA-Z]{2}\d{22}', // Mali 'MN' => 'MN\d{2}\d{4}\d{12}', // Mongolia 'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'MR' => 'MR\d{2}\d{5}\d{5}\d{11}\d{2}', // Mauritania 'MT' => 'MT\d{2}[A-Z]{4}\d{5}[\dA-Z]{18}', // Malta 'MU' => 'MU\d{2}[A-Z]{4}\d{2}\d{2}\d{12}\d{3}[A-Z]{3}', // Mauritius 'MZ' => 'MZ\d{2}\d{21}', // Mozambique 'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'NE' => 'NE\d{2}[A-Z]{2}\d{22}', // Niger 'NI' => 'NI\d{2}[A-Z]{4}\d{24}', // Nicaragua 'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // Netherlands (The) 'NO' => 'NO\d{2}\d{4}\d{6}\d{1}', // Norway 'OM' => 'OM\d{2}\d{3}[\dA-Z]{16}', // Oman 'PF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'PK' => 'PK\d{2}[A-Z]{4}[\dA-Z]{16}', // Pakistan 'PL' => 'PL\d{2}\d{8}\d{16}', // Poland 'PM' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'PS' => 'PS\d{2}[A-Z]{4}[\dA-Z]{21}', // Palestine, State of 'PT' => 'PT\d{2}\d{4}\d{4}\d{11}\d{2}', // Portugal 'QA' => 'QA\d{2}[A-Z]{4}[\dA-Z]{21}', // Qatar 'RE' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'RO' => 'RO\d{2}[A-Z]{4}[\dA-Z]{16}', // Romania 'RS' => 'RS\d{2}\d{3}\d{13}\d{2}', // Serbia 'RU' => 'RU\d{2}\d{9}\d{5}[\dA-Z]{15}', // Russia 'SA' => 'SA\d{2}\d{2}[\dA-Z]{18}', // Saudi Arabia 'SC' => 'SC\d{2}[A-Z]{4}\d{2}\d{2}\d{16}[A-Z]{3}', // Seychelles 'SD' => 'SD\d{2}\d{2}\d{12}', // Sudan 'SE' => 'SE\d{2}\d{3}\d{16}\d{1}', // Sweden 'SI' => 'SI\d{2}\d{5}\d{8}\d{2}', // Slovenia 'SK' => 'SK\d{2}\d{4}\d{6}\d{10}', // Slovakia 'SM' => 'SM\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // San Marino 'SN' => 'SN\d{2}[A-Z]{2}\d{22}', // Senegal 'SO' => 'SO\d{2}\d{4}\d{3}\d{12}', // Somalia 'ST' => 'ST\d{2}\d{4}\d{4}\d{11}\d{2}', // Sao Tome and Principe 'SV' => 'SV\d{2}[A-Z]{4}\d{20}', // El Salvador 'TD' => 'TD\d{2}\d{23}', // Chad 'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'TG' => 'TG\d{2}[A-Z]{2}\d{22}', // Togo 'TL' => 'TL\d{2}\d{3}\d{14}\d{2}', // Timor-Leste 'TN' => 'TN\d{2}\d{2}\d{3}\d{13}\d{2}', // Tunisia 'TR' => 'TR\d{2}\d{5}\d{1}[\dA-Z]{16}', // Turkey 'UA' => 'UA\d{2}\d{6}[\dA-Z]{19}', // Ukraine 'VA' => 'VA\d{2}\d{3}\d{15}', // Vatican City State 'VG' => 'VG\d{2}[A-Z]{4}\d{16}', // Virgin Islands 'WF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France 'XK' => 'XK\d{2}\d{4}\d{10}\d{2}', // Kosovo 'YE' => 'YE\d{2}[A-Z]{4}\d{4}[\dA-Z]{18}', // Yemen 'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France ]; /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Iban) { throw new UnexpectedTypeException($constraint, Iban::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; // Remove spaces and convert to uppercase $canonicalized = str_replace(' ', '', strtoupper($value)); // The IBAN must contain only digits and characters... if (!ctype_alnum($canonicalized)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Iban::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } // ...start with a two-letter country code $countryCode = substr($canonicalized, 0, 2); if (!ctype_alpha($countryCode)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Iban::INVALID_COUNTRY_CODE_ERROR) ->addViolation(); return; } // ...have a format available if (!\array_key_exists($countryCode, self::FORMATS)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Iban::NOT_SUPPORTED_COUNTRY_CODE_ERROR) ->addViolation(); return; } // ...and have a valid format if (!preg_match('/^'.self::FORMATS[$countryCode].'$/', $canonicalized) ) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Iban::INVALID_FORMAT_ERROR) ->addViolation(); return; } // Check digits should always between 2 and 98 // A ECBS document (https://www.ecbs.org/Download/EBS204_V3.PDF) replicates part of the ISO/IEC 7064:2003 standard as a method for generating check digits in the range 02 to 98. $checkDigits = (int) substr($canonicalized, 2, 2); if ($checkDigits < 2 || $checkDigits > 98) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Iban::CHECKSUM_FAILED_ERROR) ->addViolation(); return; } // Move the first four characters to the end // e.g. CH93 0076 2011 6238 5295 7 // -> 0076 2011 6238 5295 7 CH93 $canonicalized = substr($canonicalized, 4).substr($canonicalized, 0, 4); // Convert all remaining letters to their ordinals // The result is an integer, which is too large for PHP's int // data type, so we store it in a string instead. // e.g. 0076 2011 6238 5295 7 CH93 // -> 0076 2011 6238 5295 7 121893 $checkSum = self::toBigInt($canonicalized); // Do a modulo-97 operation on the large integer // We cannot use PHP's modulo operator, so we calculate the // modulo step-wisely instead if (1 !== self::bigModulo97($checkSum)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Iban::CHECKSUM_FAILED_ERROR) ->addViolation(); } } private static function toBigInt(string $string): string { $chars = str_split($string); $bigInt = ''; foreach ($chars as $char) { // Convert uppercase characters to ordinals, starting with 10 for "A" if (ctype_upper($char)) { $bigInt .= (\ord($char) - 55); continue; } // Simply append digits $bigInt .= $char; } return $bigInt; } private static function bigModulo97(string $bigInt): int { $parts = str_split($bigInt, 7); $rest = 0; foreach ($parts as $part) { $rest = ($rest.$part) % 97; } return $rest; } } PK!\*&[4vendor/symfony/validator/Constraints/IdenticalTo.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class IdenticalTo extends AbstractComparison { public const NOT_IDENTICAL_ERROR = '2a8cc50f-58a2-4536-875e-060a2ce69ed5'; protected static $errorNames = [ self::NOT_IDENTICAL_ERROR => 'NOT_IDENTICAL_ERROR', ]; public $message = 'This value should be identical to {{ compared_value_type }} {{ compared_value }}.'; } PK!0>=vendor/symfony/validator/Constraints/IdenticalToValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are identical (===). * * @author Daniel Holmes * @author Bernhard Schussek */ class IdenticalToValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return $value1 === $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return IdenticalTo::NOT_IDENTICAL_ERROR; } } PK!{ _$_$.vendor/symfony/validator/Constraints/Image.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Benjamin Dulau * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Image extends File { public const SIZE_NOT_DETECTED_ERROR = '6d55c3f4-e58e-4fe3-91ee-74b492199956'; public const TOO_WIDE_ERROR = '7f87163d-878f-47f5-99ba-a8eb723a1ab2'; public const TOO_NARROW_ERROR = '9afbd561-4f90-4a27-be62-1780fc43604a'; public const TOO_HIGH_ERROR = '7efae81c-4877-47ba-aa65-d01ccb0d4645'; public const TOO_LOW_ERROR = 'aef0cb6a-c07f-4894-bc08-1781420d7b4c'; public const TOO_FEW_PIXEL_ERROR = '1b06b97d-ae48-474e-978f-038a74854c43'; public const TOO_MANY_PIXEL_ERROR = 'ee0804e8-44db-4eac-9775-be91aaf72ce1'; public const RATIO_TOO_BIG_ERROR = '70cafca6-168f-41c9-8c8c-4e47a52be643'; public const RATIO_TOO_SMALL_ERROR = '59b8c6ef-bcf2-4ceb-afff-4642ed92f12e'; public const SQUARE_NOT_ALLOWED_ERROR = '5d41425b-facb-47f7-a55a-de9fbe45cb46'; public const LANDSCAPE_NOT_ALLOWED_ERROR = '6f895685-7cf2-4d65-b3da-9029c5581d88'; public const PORTRAIT_NOT_ALLOWED_ERROR = '65608156-77da-4c79-a88c-02ef6d18c782'; public const CORRUPTED_IMAGE_ERROR = '5d4163f3-648f-4e39-87fd-cc5ea7aad2d1'; // Include the mapping from the base class protected static $errorNames = [ self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR', self::NOT_READABLE_ERROR => 'NOT_READABLE_ERROR', self::EMPTY_ERROR => 'EMPTY_ERROR', self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR', self::SIZE_NOT_DETECTED_ERROR => 'SIZE_NOT_DETECTED_ERROR', self::TOO_WIDE_ERROR => 'TOO_WIDE_ERROR', self::TOO_NARROW_ERROR => 'TOO_NARROW_ERROR', self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', self::TOO_FEW_PIXEL_ERROR => 'TOO_FEW_PIXEL_ERROR', self::TOO_MANY_PIXEL_ERROR => 'TOO_MANY_PIXEL_ERROR', self::RATIO_TOO_BIG_ERROR => 'RATIO_TOO_BIG_ERROR', self::RATIO_TOO_SMALL_ERROR => 'RATIO_TOO_SMALL_ERROR', self::SQUARE_NOT_ALLOWED_ERROR => 'SQUARE_NOT_ALLOWED_ERROR', self::LANDSCAPE_NOT_ALLOWED_ERROR => 'LANDSCAPE_NOT_ALLOWED_ERROR', self::PORTRAIT_NOT_ALLOWED_ERROR => 'PORTRAIT_NOT_ALLOWED_ERROR', self::CORRUPTED_IMAGE_ERROR => 'CORRUPTED_IMAGE_ERROR', ]; public $mimeTypes = 'image/*'; public $minWidth; public $maxWidth; public $maxHeight; public $minHeight; public $maxRatio; public $minRatio; public $minPixels; public $maxPixels; public $allowSquare = true; public $allowLandscape = true; public $allowPortrait = true; public $detectCorrupted = false; // The constant for a wrong MIME type is taken from the parent class. public $mimeTypesMessage = 'This file is not a valid image.'; public $sizeNotDetectedMessage = 'The size of the image could not be detected.'; public $maxWidthMessage = 'The image width is too big ({{ width }}px). Allowed maximum width is {{ max_width }}px.'; public $minWidthMessage = 'The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.'; public $maxHeightMessage = 'The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.'; public $minHeightMessage = 'The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.'; public $minPixelsMessage = 'The image has too few pixels ({{ pixels }} pixels). Minimum amount expected is {{ min_pixels }} pixels.'; public $maxPixelsMessage = 'The image has too many pixels ({{ pixels }} pixels). Maximum amount expected is {{ max_pixels }} pixels.'; public $maxRatioMessage = 'The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.'; public $minRatioMessage = 'The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.'; public $allowSquareMessage = 'The image is square ({{ width }}x{{ height }}px). Square images are not allowed.'; public $allowLandscapeMessage = 'The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.'; public $allowPortraitMessage = 'The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.'; public $corruptedMessage = 'The image file is corrupted.'; /** * {@inheritdoc} * * @param int|float $maxRatio * @param int|float $minRatio * @param int|float $minPixels * @param int|float $maxPixels */ public function __construct( ?array $options = null, $maxSize = null, ?bool $binaryFormat = null, ?array $mimeTypes = null, ?int $minWidth = null, ?int $maxWidth = null, ?int $maxHeight = null, ?int $minHeight = null, $maxRatio = null, $minRatio = null, $minPixels = null, $maxPixels = null, ?bool $allowSquare = null, ?bool $allowLandscape = null, ?bool $allowPortrait = null, ?bool $detectCorrupted = null, ?string $notFoundMessage = null, ?string $notReadableMessage = null, ?string $maxSizeMessage = null, ?string $mimeTypesMessage = null, ?string $disallowEmptyMessage = null, ?string $uploadIniSizeErrorMessage = null, ?string $uploadFormSizeErrorMessage = null, ?string $uploadPartialErrorMessage = null, ?string $uploadNoFileErrorMessage = null, ?string $uploadNoTmpDirErrorMessage = null, ?string $uploadCantWriteErrorMessage = null, ?string $uploadExtensionErrorMessage = null, ?string $uploadErrorMessage = null, ?string $sizeNotDetectedMessage = null, ?string $maxWidthMessage = null, ?string $minWidthMessage = null, ?string $maxHeightMessage = null, ?string $minHeightMessage = null, ?string $minPixelsMessage = null, ?string $maxPixelsMessage = null, ?string $maxRatioMessage = null, ?string $minRatioMessage = null, ?string $allowSquareMessage = null, ?string $allowLandscapeMessage = null, ?string $allowPortraitMessage = null, ?string $corruptedMessage = null, ?array $groups = null, $payload = null ) { parent::__construct( $options, $maxSize, $binaryFormat, $mimeTypes, $notFoundMessage, $notReadableMessage, $maxSizeMessage, $mimeTypesMessage, $disallowEmptyMessage, $uploadIniSizeErrorMessage, $uploadFormSizeErrorMessage, $uploadPartialErrorMessage, $uploadNoFileErrorMessage, $uploadNoTmpDirErrorMessage, $uploadCantWriteErrorMessage, $uploadExtensionErrorMessage, $uploadErrorMessage, $groups, $payload ); $this->minWidth = $minWidth ?? $this->minWidth; $this->maxWidth = $maxWidth ?? $this->maxWidth; $this->maxHeight = $maxHeight ?? $this->maxHeight; $this->minHeight = $minHeight ?? $this->minHeight; $this->maxRatio = $maxRatio ?? $this->maxRatio; $this->minRatio = $minRatio ?? $this->minRatio; $this->minPixels = $minPixels ?? $this->minPixels; $this->maxPixels = $maxPixels ?? $this->maxPixels; $this->allowSquare = $allowSquare ?? $this->allowSquare; $this->allowLandscape = $allowLandscape ?? $this->allowLandscape; $this->allowPortrait = $allowPortrait ?? $this->allowPortrait; $this->detectCorrupted = $detectCorrupted ?? $this->detectCorrupted; $this->sizeNotDetectedMessage = $sizeNotDetectedMessage ?? $this->sizeNotDetectedMessage; $this->maxWidthMessage = $maxWidthMessage ?? $this->maxWidthMessage; $this->minWidthMessage = $minWidthMessage ?? $this->minWidthMessage; $this->maxHeightMessage = $maxHeightMessage ?? $this->maxHeightMessage; $this->minHeightMessage = $minHeightMessage ?? $this->minHeightMessage; $this->minPixelsMessage = $minPixelsMessage ?? $this->minPixelsMessage; $this->maxPixelsMessage = $maxPixelsMessage ?? $this->maxPixelsMessage; $this->maxRatioMessage = $maxRatioMessage ?? $this->maxRatioMessage; $this->minRatioMessage = $minRatioMessage ?? $this->minRatioMessage; $this->allowSquareMessage = $allowSquareMessage ?? $this->allowSquareMessage; $this->allowLandscapeMessage = $allowLandscapeMessage ?? $this->allowLandscapeMessage; $this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage; $this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage; } } PK!$$7vendor/symfony/validator/Constraints/ImageValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validates whether a value is a valid image file and is valid * against minWidth, maxWidth, minHeight and maxHeight constraints. * * @author Benjamin Dulau * @author Bernhard Schussek */ class ImageValidator extends FileValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Image) { throw new UnexpectedTypeException($constraint, Image::class); } $violations = \count($this->context->getViolations()); parent::validate($value, $constraint); $failed = \count($this->context->getViolations()) !== $violations; if ($failed || null === $value || '' === $value) { return; } if (null === $constraint->minWidth && null === $constraint->maxWidth && null === $constraint->minHeight && null === $constraint->maxHeight && null === $constraint->minPixels && null === $constraint->maxPixels && null === $constraint->minRatio && null === $constraint->maxRatio && $constraint->allowSquare && $constraint->allowLandscape && $constraint->allowPortrait && !$constraint->detectCorrupted) { return; } $size = @getimagesize($value); if (empty($size) || (0 === $size[0]) || (0 === $size[1])) { $this->context->buildViolation($constraint->sizeNotDetectedMessage) ->setCode(Image::SIZE_NOT_DETECTED_ERROR) ->addViolation(); return; } $width = $size[0]; $height = $size[1]; if ($constraint->minWidth) { if (!ctype_digit((string) $constraint->minWidth)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum width.', $constraint->minWidth)); } if ($width < $constraint->minWidth) { $this->context->buildViolation($constraint->minWidthMessage) ->setParameter('{{ width }}', $width) ->setParameter('{{ min_width }}', $constraint->minWidth) ->setCode(Image::TOO_NARROW_ERROR) ->addViolation(); return; } } if ($constraint->maxWidth) { if (!ctype_digit((string) $constraint->maxWidth)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth)); } if ($width > $constraint->maxWidth) { $this->context->buildViolation($constraint->maxWidthMessage) ->setParameter('{{ width }}', $width) ->setParameter('{{ max_width }}', $constraint->maxWidth) ->setCode(Image::TOO_WIDE_ERROR) ->addViolation(); return; } } if ($constraint->minHeight) { if (!ctype_digit((string) $constraint->minHeight)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum height.', $constraint->minHeight)); } if ($height < $constraint->minHeight) { $this->context->buildViolation($constraint->minHeightMessage) ->setParameter('{{ height }}', $height) ->setParameter('{{ min_height }}', $constraint->minHeight) ->setCode(Image::TOO_LOW_ERROR) ->addViolation(); return; } } if ($constraint->maxHeight) { if (!ctype_digit((string) $constraint->maxHeight)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight)); } if ($height > $constraint->maxHeight) { $this->context->buildViolation($constraint->maxHeightMessage) ->setParameter('{{ height }}', $height) ->setParameter('{{ max_height }}', $constraint->maxHeight) ->setCode(Image::TOO_HIGH_ERROR) ->addViolation(); } } $pixels = $width * $height; if (null !== $constraint->minPixels) { if (!ctype_digit((string) $constraint->minPixels)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels)); } if ($pixels < $constraint->minPixels) { $this->context->buildViolation($constraint->minPixelsMessage) ->setParameter('{{ pixels }}', $pixels) ->setParameter('{{ min_pixels }}', $constraint->minPixels) ->setParameter('{{ height }}', $height) ->setParameter('{{ width }}', $width) ->setCode(Image::TOO_FEW_PIXEL_ERROR) ->addViolation(); } } if (null !== $constraint->maxPixels) { if (!ctype_digit((string) $constraint->maxPixels)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels)); } if ($pixels > $constraint->maxPixels) { $this->context->buildViolation($constraint->maxPixelsMessage) ->setParameter('{{ pixels }}', $pixels) ->setParameter('{{ max_pixels }}', $constraint->maxPixels) ->setParameter('{{ height }}', $height) ->setParameter('{{ width }}', $width) ->setCode(Image::TOO_MANY_PIXEL_ERROR) ->addViolation(); } } $ratio = round($width / $height, 2); if (null !== $constraint->minRatio) { if (!is_numeric((string) $constraint->minRatio)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum ratio.', $constraint->minRatio)); } if ($ratio < round($constraint->minRatio, 2)) { $this->context->buildViolation($constraint->minRatioMessage) ->setParameter('{{ ratio }}', $ratio) ->setParameter('{{ min_ratio }}', round($constraint->minRatio, 2)) ->setCode(Image::RATIO_TOO_SMALL_ERROR) ->addViolation(); } } if (null !== $constraint->maxRatio) { if (!is_numeric((string) $constraint->maxRatio)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum ratio.', $constraint->maxRatio)); } if ($ratio > round($constraint->maxRatio, 2)) { $this->context->buildViolation($constraint->maxRatioMessage) ->setParameter('{{ ratio }}', $ratio) ->setParameter('{{ max_ratio }}', round($constraint->maxRatio, 2)) ->setCode(Image::RATIO_TOO_BIG_ERROR) ->addViolation(); } } if (!$constraint->allowSquare && $width == $height) { $this->context->buildViolation($constraint->allowSquareMessage) ->setParameter('{{ width }}', $width) ->setParameter('{{ height }}', $height) ->setCode(Image::SQUARE_NOT_ALLOWED_ERROR) ->addViolation(); } if (!$constraint->allowLandscape && $width > $height) { $this->context->buildViolation($constraint->allowLandscapeMessage) ->setParameter('{{ width }}', $width) ->setParameter('{{ height }}', $height) ->setCode(Image::LANDSCAPE_NOT_ALLOWED_ERROR) ->addViolation(); } if (!$constraint->allowPortrait && $width < $height) { $this->context->buildViolation($constraint->allowPortraitMessage) ->setParameter('{{ width }}', $width) ->setParameter('{{ height }}', $height) ->setCode(Image::PORTRAIT_NOT_ALLOWED_ERROR) ->addViolation(); } if ($constraint->detectCorrupted) { if (!\function_exists('imagecreatefromstring')) { throw new LogicException('Corrupted images detection requires installed and enabled GD extension.'); } $resource = @imagecreatefromstring(file_get_contents($value)); if (false === $resource) { $this->context->buildViolation($constraint->corruptedMessage) ->setCode(Image::CORRUPTED_IMAGE_ERROR) ->addViolation(); return; } imagedestroy($resource); } } } PK!qiD= = +vendor/symfony/validator/Constraints/Ip.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * Validates that a value is a valid IP address. * * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek * @author Joseph Bielawski */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Ip extends Constraint { public const V4 = '4'; public const V6 = '6'; public const ALL = 'all'; // adds FILTER_FLAG_NO_PRIV_RANGE flag (skip private ranges) public const V4_NO_PRIV = '4_no_priv'; public const V6_NO_PRIV = '6_no_priv'; public const ALL_NO_PRIV = 'all_no_priv'; // adds FILTER_FLAG_NO_RES_RANGE flag (skip reserved ranges) public const V4_NO_RES = '4_no_res'; public const V6_NO_RES = '6_no_res'; public const ALL_NO_RES = 'all_no_res'; // adds FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE flags (skip both) public const V4_ONLY_PUBLIC = '4_public'; public const V6_ONLY_PUBLIC = '6_public'; public const ALL_ONLY_PUBLIC = 'all_public'; public const INVALID_IP_ERROR = 'b1b427ae-9f6f-41b0-aa9b-84511fbb3c5b'; protected static $versions = [ self::V4, self::V6, self::ALL, self::V4_NO_PRIV, self::V6_NO_PRIV, self::ALL_NO_PRIV, self::V4_NO_RES, self::V6_NO_RES, self::ALL_NO_RES, self::V4_ONLY_PUBLIC, self::V6_ONLY_PUBLIC, self::ALL_ONLY_PUBLIC, ]; protected static $errorNames = [ self::INVALID_IP_ERROR => 'INVALID_IP_ERROR', ]; public $version = self::V4; public $message = 'This is not a valid IP address.'; public $normalizer; /** * {@inheritdoc} */ public function __construct( ?array $options = null, ?string $version = null, ?string $message = null, ?callable $normalizer = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->version = $version ?? $this->version; $this->message = $message ?? $this->message; $this->normalizer = $normalizer ?? $this->normalizer; if (!\in_array($this->version, self::$versions)) { throw new ConstraintDefinitionException(sprintf('The option "version" must be one of "%s".', implode('", "', self::$versions))); } if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } } PK!m^U_ _ 4vendor/symfony/validator/Constraints/IpValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value is a valid IP address. * * @author Bernhard Schussek * @author Joseph Bielawski */ class IpValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Ip) { throw new UnexpectedTypeException($constraint, Ip::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (null !== $constraint->normalizer) { $value = ($constraint->normalizer)($value); } switch ($constraint->version) { case Ip::V4: $flag = \FILTER_FLAG_IPV4; break; case Ip::V6: $flag = \FILTER_FLAG_IPV6; break; case Ip::V4_NO_PRIV: $flag = \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE; break; case Ip::V6_NO_PRIV: $flag = \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE; break; case Ip::ALL_NO_PRIV: $flag = \FILTER_FLAG_NO_PRIV_RANGE; break; case Ip::V4_NO_RES: $flag = \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_RES_RANGE; break; case Ip::V6_NO_RES: $flag = \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_RES_RANGE; break; case Ip::ALL_NO_RES: $flag = \FILTER_FLAG_NO_RES_RANGE; break; case Ip::V4_ONLY_PUBLIC: $flag = \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE; break; case Ip::V6_ONLY_PUBLIC: $flag = \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE; break; case Ip::ALL_ONLY_PUBLIC: $flag = \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE; break; default: $flag = 0; break; } if (!filter_var($value, \FILTER_VALIDATE_IP, $flag)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Ip::INVALID_IP_ERROR) ->addViolation(); } } } PK! -vendor/symfony/validator/Constraints/Isbn.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author The Whole Life To Learn * @author Manuel Reinhard * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Isbn extends Constraint { public const ISBN_10 = 'isbn10'; public const ISBN_13 = 'isbn13'; public const TOO_SHORT_ERROR = '949acbb0-8ef5-43ed-a0e9-032dfd08ae45'; public const TOO_LONG_ERROR = '3171387d-f80a-47b3-bd6e-60598545316a'; public const INVALID_CHARACTERS_ERROR = '23d21cea-da99-453d-98b1-a7d916fbb339'; public const CHECKSUM_FAILED_ERROR = '2881c032-660f-46b6-8153-d352d9706640'; public const TYPE_NOT_RECOGNIZED_ERROR = 'fa54a457-f042-441f-89c4-066ee5bdd3e1'; protected static $errorNames = [ self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::CHECKSUM_FAILED_ERROR => 'CHECKSUM_FAILED_ERROR', self::TYPE_NOT_RECOGNIZED_ERROR => 'TYPE_NOT_RECOGNIZED_ERROR', ]; public $isbn10Message = 'This value is not a valid ISBN-10.'; public $isbn13Message = 'This value is not a valid ISBN-13.'; public $bothIsbnMessage = 'This value is neither a valid ISBN-10 nor a valid ISBN-13.'; public $type; public $message; /** * {@inheritdoc} * * @param string|array|null $type The ISBN standard to validate or a set of options */ public function __construct( $type = null, ?string $message = null, ?string $isbn10Message = null, ?string $isbn13Message = null, ?string $bothIsbnMessage = null, ?array $groups = null, $payload = null, array $options = [] ) { if (\is_array($type)) { $options = array_merge($type, $options); } elseif (null !== $type) { $options['value'] = $type; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->isbn10Message = $isbn10Message ?? $this->isbn10Message; $this->isbn13Message = $isbn13Message ?? $this->isbn13Message; $this->bothIsbnMessage = $bothIsbnMessage ?? $this->bothIsbnMessage; } /** * {@inheritdoc} */ public function getDefaultOption() { return 'type'; } } PK!6vendor/symfony/validator/Constraints/IsbnValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether the value is a valid ISBN-10 or ISBN-13. * * @author The Whole Life To Learn * @author Manuel Reinhard * @author Bernhard Schussek * * @see https://en.wikipedia.org/wiki/Isbn */ class IsbnValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Isbn) { throw new UnexpectedTypeException($constraint, Isbn::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; $canonical = str_replace('-', '', $value); // Explicitly validate against ISBN-10 if (Isbn::ISBN_10 === $constraint->type) { if (true !== ($code = $this->validateIsbn10($canonical))) { $this->context->buildViolation($this->getMessage($constraint, $constraint->type)) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode($code) ->addViolation(); } return; } // Explicitly validate against ISBN-13 if (Isbn::ISBN_13 === $constraint->type) { if (true !== ($code = $this->validateIsbn13($canonical))) { $this->context->buildViolation($this->getMessage($constraint, $constraint->type)) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode($code) ->addViolation(); } return; } // Try both ISBNs // First, try ISBN-10 $code = $this->validateIsbn10($canonical); // The ISBN can only be an ISBN-13 if the value was too long for ISBN-10 if (Isbn::TOO_LONG_ERROR === $code) { // Try ISBN-13 now $code = $this->validateIsbn13($canonical); // If too short, this means we have 11 or 12 digits if (Isbn::TOO_SHORT_ERROR === $code) { $code = Isbn::TYPE_NOT_RECOGNIZED_ERROR; } } if (true !== $code) { $this->context->buildViolation($this->getMessage($constraint)) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode($code) ->addViolation(); } } protected function validateIsbn10(string $isbn) { // Choose an algorithm so that ERROR_INVALID_CHARACTERS is preferred // over ERROR_TOO_SHORT/ERROR_TOO_LONG // Otherwise "0-45122-5244" passes, but "0-45122_5244" reports // "too long" // Error priority: // 1. ERROR_INVALID_CHARACTERS // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG // 3. ERROR_CHECKSUM_FAILED $checkSum = 0; for ($i = 0; $i < 10; ++$i) { // If we test the length before the loop, we get an ERROR_TOO_SHORT // when actually an ERROR_INVALID_CHARACTERS is wanted, e.g. for // "0-45122_5244" (typo) if (!isset($isbn[$i])) { return Isbn::TOO_SHORT_ERROR; } if ('X' === $isbn[$i]) { $digit = 10; } elseif (ctype_digit($isbn[$i])) { $digit = $isbn[$i]; } else { return Isbn::INVALID_CHARACTERS_ERROR; } $checkSum += $digit * (10 - $i); } if (isset($isbn[$i])) { return Isbn::TOO_LONG_ERROR; } return 0 === $checkSum % 11 ? true : Isbn::CHECKSUM_FAILED_ERROR; } protected function validateIsbn13(string $isbn) { // Error priority: // 1. ERROR_INVALID_CHARACTERS // 2. ERROR_TOO_SHORT/ERROR_TOO_LONG // 3. ERROR_CHECKSUM_FAILED if (!ctype_digit($isbn)) { return Isbn::INVALID_CHARACTERS_ERROR; } $length = \strlen($isbn); if ($length < 13) { return Isbn::TOO_SHORT_ERROR; } if ($length > 13) { return Isbn::TOO_LONG_ERROR; } $checkSum = 0; for ($i = 0; $i < 13; $i += 2) { $checkSum += $isbn[$i]; } for ($i = 1; $i < 12; $i += 2) { $checkSum += $isbn[$i] * 3; } return 0 === $checkSum % 10 ? true : Isbn::CHECKSUM_FAILED_ERROR; } protected function getMessage(Isbn $constraint, ?string $type = null) { if (null !== $constraint->message) { return $constraint->message; } elseif (Isbn::ISBN_10 === $type) { return $constraint->isbn10Message; } elseif (Isbn::ISBN_13 === $type) { return $constraint->isbn13Message; } return $constraint->bothIsbnMessage; } } PK!Db_F330vendor/symfony/validator/Constraints/IsFalse.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class IsFalse extends Constraint { public const NOT_FALSE_ERROR = 'd53a91b0-def3-426a-83d7-269da7ab4200'; protected static $errorNames = [ self::NOT_FALSE_ERROR => 'NOT_FALSE_ERROR', ]; public $message = 'This value should be false.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; } } PK!^rr9vendor/symfony/validator/Constraints/IsFalseValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class IsFalseValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof IsFalse) { throw new UnexpectedTypeException($constraint, IsFalse::class); } if (null === $value || false === $value || 0 === $value || '0' === $value) { return; } $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(IsFalse::NOT_FALSE_ERROR) ->addViolation(); } } PK!{| * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Laurent Masforné */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Isin extends Constraint { public const VALIDATION_LENGTH = 12; public const VALIDATION_PATTERN = '/[A-Z]{2}[A-Z0-9]{9}[0-9]{1}/'; public const INVALID_LENGTH_ERROR = '88738dfc-9ed5-ba1e-aebe-402a2a9bf58e'; public const INVALID_PATTERN_ERROR = '3d08ce0-ded9-a93d-9216-17ac21265b65e'; public const INVALID_CHECKSUM_ERROR = '32089b-0ee1-93ba-399e-aa232e62f2d29d'; protected static $errorNames = [ self::INVALID_LENGTH_ERROR => 'INVALID_LENGTH_ERROR', self::INVALID_PATTERN_ERROR => 'INVALID_PATTERN_ERROR', self::INVALID_CHECKSUM_ERROR => 'INVALID_CHECKSUM_ERROR', ]; public $message = 'This value is not a valid International Securities Identification Number (ISIN).'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!g2 6vendor/symfony/validator/Constraints/IsinValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Laurent Masforné * * @see https://en.wikipedia.org/wiki/International_Securities_Identification_Number */ class IsinValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Isin) { throw new UnexpectedTypeException($constraint, Isin::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = strtoupper($value); if (Isin::VALIDATION_LENGTH !== \strlen($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Isin::INVALID_LENGTH_ERROR) ->addViolation(); return; } if (!preg_match(Isin::VALIDATION_PATTERN, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Isin::INVALID_PATTERN_ERROR) ->addViolation(); return; } if (!$this->isCorrectChecksum($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Isin::INVALID_CHECKSUM_ERROR) ->addViolation(); } } private function isCorrectChecksum(string $input): bool { $characters = str_split($input); foreach ($characters as $i => $char) { $characters[$i] = \intval($char, 36); } $number = implode('', $characters); return 0 === $this->context->getValidator()->validate($number, new Luhn())->count(); } } PK!*../vendor/symfony/validator/Constraints/IsNull.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class IsNull extends Constraint { public const NOT_NULL_ERROR = '60d2f30b-8cfa-4372-b155-9656634de120'; protected static $errorNames = [ self::NOT_NULL_ERROR => 'NOT_NULL_ERROR', ]; public $message = 'This value should be null.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; } } PK!k5228vendor/symfony/validator/Constraints/IsNullValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class IsNullValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof IsNull) { throw new UnexpectedTypeException($constraint, IsNull::class); } if (null !== $value) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(IsNull::NOT_NULL_ERROR) ->addViolation(); } } } PK!cc-vendor/symfony/validator/Constraints/Issn.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Antonio J. García Lagar * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Issn extends Constraint { public const TOO_SHORT_ERROR = '6a20dd3d-f463-4460-8e7b-18a1b98abbfb'; public const TOO_LONG_ERROR = '37cef893-5871-464e-8b12-7fb79324833c'; public const MISSING_HYPHEN_ERROR = '2983286f-8134-4693-957a-1ec4ef887b15'; public const INVALID_CHARACTERS_ERROR = 'a663d266-37c2-4ece-a914-ae891940c588'; public const INVALID_CASE_ERROR = '7b6dd393-7523-4a6c-b84d-72b91bba5e1a'; public const CHECKSUM_FAILED_ERROR = 'b0f92dbc-667c-48de-b526-ad9586d43e85'; protected static $errorNames = [ self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', self::MISSING_HYPHEN_ERROR => 'MISSING_HYPHEN_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::INVALID_CASE_ERROR => 'INVALID_CASE_ERROR', self::CHECKSUM_FAILED_ERROR => 'CHECKSUM_FAILED_ERROR', ]; public $message = 'This value is not a valid ISSN.'; public $caseSensitive = false; public $requireHyphen = false; public function __construct( ?array $options = null, ?string $message = null, ?bool $caseSensitive = null, ?bool $requireHyphen = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->caseSensitive = $caseSensitive ?? $this->caseSensitive; $this->requireHyphen = $requireHyphen ?? $this->requireHyphen; } } PK!,ee6vendor/symfony/validator/Constraints/IssnValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether the value is a valid ISSN. * * @author Antonio J. García Lagar * @author Bernhard Schussek * * @see https://en.wikipedia.org/wiki/Issn */ class IssnValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Issn) { throw new UnexpectedTypeException($constraint, Issn::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; $canonical = $value; // 1234-567X // ^ if (isset($canonical[4]) && '-' === $canonical[4]) { // remove hyphen $canonical = substr($canonical, 0, 4).substr($canonical, 5); } elseif ($constraint->requireHyphen) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::MISSING_HYPHEN_ERROR) ->addViolation(); return; } $length = \strlen($canonical); if ($length < 8) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::TOO_SHORT_ERROR) ->addViolation(); return; } if ($length > 8) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::TOO_LONG_ERROR) ->addViolation(); return; } // 1234567X // ^^^^^^^ digits only if (!ctype_digit(substr($canonical, 0, 7))) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } // 1234567X // ^ digit, x or X if (!ctype_digit($canonical[7]) && 'x' !== $canonical[7] && 'X' !== $canonical[7]) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } // 1234567X // ^ case-sensitive? if ($constraint->caseSensitive && 'x' === $canonical[7]) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::INVALID_CASE_ERROR) ->addViolation(); return; } // Calculate a checksum. "X" equals 10. $checkSum = 'X' === $canonical[7] || 'x' === $canonical[7] ? 10 : $canonical[7]; for ($i = 0; $i < 7; ++$i) { // Multiply the first digit by 8, the second by 7, etc. $checkSum += (8 - $i) * (int) $canonical[$i]; } if (0 !== $checkSum % 11) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Issn::CHECKSUM_FAILED_ERROR) ->addViolation(); } } } PK!bx../vendor/symfony/validator/Constraints/IsTrue.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class IsTrue extends Constraint { public const NOT_TRUE_ERROR = '2beabf1c-54c0-4882-a928-05249b26e23b'; protected static $errorNames = [ self::NOT_TRUE_ERROR => 'NOT_TRUE_ERROR', ]; public $message = 'This value should be true.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; } } PK!s8vendor/symfony/validator/Constraints/IsTrueValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class IsTrueValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof IsTrue) { throw new UnexpectedTypeException($constraint, IsTrue::class); } if (null === $value) { return; } if (true !== $value && 1 !== $value && '1' !== $value) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(IsTrue::NOT_TRUE_ERROR) ->addViolation(); } } } PK!33-vendor/symfony/validator/Constraints/Json.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Imad ZAIRIG */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Json extends Constraint { public const INVALID_JSON_ERROR = '0789c8ad-2d2b-49a4-8356-e2ce63998504'; protected static $errorNames = [ self::INVALID_JSON_ERROR => 'INVALID_JSON_ERROR', ]; public $message = 'This value should be valid JSON.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!+6vendor/symfony/validator/Constraints/JsonValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Imad ZAIRIG */ class JsonValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Json) { throw new UnexpectedTypeException($constraint, Json::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; json_decode($value); if (\JSON_ERROR_NONE !== json_last_error()) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Json::INVALID_JSON_ERROR) ->addViolation(); } } } PK!?I`1vendor/symfony/validator/Constraints/Language.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Languages; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Language extends Constraint { public const NO_SUCH_LANGUAGE_ERROR = 'ee65fec4-9a20-4202-9f39-ca558cd7bdf7'; protected static $errorNames = [ self::NO_SUCH_LANGUAGE_ERROR => 'NO_SUCH_LANGUAGE_ERROR', ]; public $message = 'This value is not a valid language.'; public $alpha3 = false; public function __construct( ?array $options = null, ?string $message = null, ?bool $alpha3 = null, ?array $groups = null, $payload = null ) { if (!class_exists(Languages::class)) { throw new LogicException('The Intl component is required to use the Language constraint. Try running "composer require symfony/intl".'); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->alpha3 = $alpha3 ?? $this->alpha3; } } PK!(PP:vendor/symfony/validator/Constraints/LanguageValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Languages; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value is a valid language code. * * @author Bernhard Schussek */ class LanguageValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Language) { throw new UnexpectedTypeException($constraint, Language::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ($constraint->alpha3 ? !Languages::alpha3CodeExists($value) : !Languages::exists($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Language::NO_SUCH_LANGUAGE_ERROR) ->addViolation(); } } } PK!dd/vendor/symfony/validator/Constraints/Length.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\MissingOptionsException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Length extends Constraint { public const TOO_SHORT_ERROR = '9ff3fdc4-b214-49db-8718-39c315e33d45'; public const TOO_LONG_ERROR = 'd94b19cc-114f-4f44-9cc4-4138e80a87b9'; public const NOT_EQUAL_LENGTH_ERROR = '4b6f5c76-22b4-409d-af16-fbe823ba9332'; public const INVALID_CHARACTERS_ERROR = '35e6a710-aa2e-4719-b58e-24b35749b767'; protected static $errorNames = [ self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', self::NOT_EQUAL_LENGTH_ERROR => 'NOT_EQUAL_LENGTH_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', ]; public $maxMessage = 'This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less.'; public $minMessage = 'This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more.'; public $exactMessage = 'This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.'; public $charsetMessage = 'This value does not match the expected {{ charset }} charset.'; public $max; public $min; public $charset = 'UTF-8'; public $normalizer; public $allowEmptyString = false; /** * {@inheritdoc} * * @param int|array|null $exactly The expected exact length or a set of options */ public function __construct( $exactly = null, ?int $min = null, ?int $max = null, ?string $charset = null, ?callable $normalizer = null, ?string $exactMessage = null, ?string $minMessage = null, ?string $maxMessage = null, ?string $charsetMessage = null, ?array $groups = null, $payload = null, array $options = [] ) { if (\is_array($exactly)) { $options = array_merge($exactly, $options); $exactly = $options['value'] ?? null; } $min = $min ?? $options['min'] ?? null; $max = $max ?? $options['max'] ?? null; unset($options['value'], $options['min'], $options['max']); if (null !== $exactly && null === $min && null === $max) { $min = $max = $exactly; } parent::__construct($options, $groups, $payload); $this->min = $min; $this->max = $max; $this->charset = $charset ?? $this->charset; $this->normalizer = $normalizer ?? $this->normalizer; $this->exactMessage = $exactMessage ?? $this->exactMessage; $this->minMessage = $minMessage ?? $this->minMessage; $this->maxMessage = $maxMessage ?? $this->maxMessage; $this->charsetMessage = $charsetMessage ?? $this->charsetMessage; if (null === $this->min && null === $this->max) { throw new MissingOptionsException(sprintf('Either option "min" or "max" must be given for constraint "%s".', __CLASS__), ['min', 'max']); } if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } if (isset($options['allowEmptyString'])) { trigger_deprecation('symfony/validator', '5.2', sprintf('The "allowEmptyString" option of the "%s" constraint is deprecated.', self::class)); } } } PK!! 8vendor/symfony/validator/Constraints/LengthValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class LengthValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Length) { throw new UnexpectedTypeException($constraint, Length::class); } if (null === $value || ('' === $value && $constraint->allowEmptyString)) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $stringValue = (string) $value; if (null !== $constraint->normalizer) { $stringValue = ($constraint->normalizer)($stringValue); } try { $invalidCharset = !@mb_check_encoding($stringValue, $constraint->charset); } catch (\ValueError $e) { if (!str_starts_with($e->getMessage(), 'mb_check_encoding(): Argument #2 ($encoding) must be a valid encoding')) { throw $e; } $invalidCharset = true; } if ($invalidCharset) { $this->context->buildViolation($constraint->charsetMessage) ->setParameter('{{ value }}', $this->formatValue($stringValue)) ->setParameter('{{ charset }}', $constraint->charset) ->setInvalidValue($value) ->setCode(Length::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } $length = mb_strlen($stringValue, $constraint->charset); if (null !== $constraint->max && $length > $constraint->max) { $exactlyOptionEnabled = $constraint->min == $constraint->max; $this->context->buildViolation($exactlyOptionEnabled ? $constraint->exactMessage : $constraint->maxMessage) ->setParameter('{{ value }}', $this->formatValue($stringValue)) ->setParameter('{{ limit }}', $constraint->max) ->setInvalidValue($value) ->setPlural((int) $constraint->max) ->setCode($exactlyOptionEnabled ? Length::NOT_EQUAL_LENGTH_ERROR : Length::TOO_LONG_ERROR) ->addViolation(); return; } if (null !== $constraint->min && $length < $constraint->min) { $exactlyOptionEnabled = $constraint->min == $constraint->max; $this->context->buildViolation($exactlyOptionEnabled ? $constraint->exactMessage : $constraint->minMessage) ->setParameter('{{ value }}', $this->formatValue($stringValue)) ->setParameter('{{ limit }}', $constraint->min) ->setInvalidValue($value) ->setPlural((int) $constraint->min) ->setCode($exactlyOptionEnabled ? Length::NOT_EQUAL_LENGTH_ERROR : Length::TOO_SHORT_ERROR) ->addViolation(); } } } PK!ᚂoo8vendor/symfony/validator/Constraints/LessThanOrEqual.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class LessThanOrEqual extends AbstractComparison { public const TOO_HIGH_ERROR = '30fbb013-d015-4232-8b3b-8f3be97a7e14'; protected static $errorNames = [ self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; public $message = 'This value should be less than or equal to {{ compared_value }}.'; } PK!dBBAvendor/symfony/validator/Constraints/LessThanOrEqualValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are less than or equal to the previous (<=). * * @author Daniel Holmes * @author Bernhard Schussek */ class LessThanOrEqualValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return null === $value2 || $value1 <= $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return LessThanOrEqual::TOO_HIGH_ERROR; } } PK!R\\1vendor/symfony/validator/Constraints/LessThan.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class LessThan extends AbstractComparison { public const TOO_HIGH_ERROR = '079d7420-2d13-460c-8756-de810eeb37d2'; protected static $errorNames = [ self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', ]; public $message = 'This value should be less than {{ compared_value }}.'; } PK!ed&&:vendor/symfony/validator/Constraints/LessThanValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are less than the previous (<). * * @author Daniel Holmes * @author Bernhard Schussek */ class LessThanValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return null === $value2 || $value1 < $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return LessThan::TOO_HIGH_ERROR; } } PK!V9/vendor/symfony/validator/Constraints/Locale.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Locales; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\LogicException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Locale extends Constraint { public const NO_SUCH_LOCALE_ERROR = 'a0af4293-1f1a-4a1c-a328-979cba6182a2'; protected static $errorNames = [ self::NO_SUCH_LOCALE_ERROR => 'NO_SUCH_LOCALE_ERROR', ]; public $message = 'This value is not a valid locale.'; public $canonicalize = true; public function __construct( ?array $options = null, ?string $message = null, ?bool $canonicalize = null, ?array $groups = null, $payload = null ) { if (!class_exists(Locales::class)) { throw new LogicException('The Intl component is required to use the Locale constraint. Try running "composer require symfony/intl".'); } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->canonicalize = $canonicalize ?? $this->canonicalize; } } PK!9$8vendor/symfony/validator/Constraints/LocaleValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Locales; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value is a valid locale code. * * @author Bernhard Schussek */ class LocaleValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Locale) { throw new UnexpectedTypeException($constraint, Locale::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $inputValue = (string) $value; $value = $inputValue; if ($constraint->canonicalize) { $value = \Locale::canonicalize($value); } if (!Locales::exists($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($inputValue)) ->setCode(Locale::NO_SUCH_LOCALE_ERROR) ->addViolation(); } } } PK!K#-vendor/symfony/validator/Constraints/Luhn.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * Metadata for the LuhnValidator. * * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Tim Nagel * @author Greg Knapp http://gregk.me/2011/php-implementation-of-bank-card-luhn-algorithm/ * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Luhn extends Constraint { public const INVALID_CHARACTERS_ERROR = 'dfad6d23-1b74-4374-929b-5cbb56fc0d9e'; public const CHECKSUM_FAILED_ERROR = '4d760774-3f50-4cd5-a6d5-b10a3299d8d3'; protected static $errorNames = [ self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::CHECKSUM_FAILED_ERROR => 'CHECKSUM_FAILED_ERROR', ]; public $message = 'Invalid card number.'; public function __construct( ?array $options = null, ?string $message = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!Z 6vendor/symfony/validator/Constraints/LuhnValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates a PAN using the LUHN Algorithm. * * For a list of example card numbers that are used to test this * class, please see the LuhnValidatorTest class. * * @see http://en.wikipedia.org/wiki/Luhn_algorithm * * @author Tim Nagel * @author Greg Knapp http://gregk.me/2011/php-implementation-of-bank-card-luhn-algorithm/ * @author Bernhard Schussek */ class LuhnValidator extends ConstraintValidator { /** * Validates a credit card number with the Luhn algorithm. * * @param mixed $value * * @throws UnexpectedTypeException when the given credit card number is no string */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Luhn) { throw new UnexpectedTypeException($constraint, Luhn::class); } if (null === $value || '' === $value) { return; } // Work with strings only, because long numbers are represented as floats // internally and don't work with strlen() if (!\is_string($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (!ctype_digit($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Luhn::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } $checkSum = 0; $length = \strlen($value); // Starting with the last digit and walking left, add every second // digit to the check sum // e.g. 7 9 9 2 7 3 9 8 7 1 3 // ^ ^ ^ ^ ^ ^ // = 7 + 9 + 7 + 9 + 7 + 3 for ($i = $length - 1; $i >= 0; $i -= 2) { $checkSum += $value[$i]; } // Starting with the second last digit and walking left, double every // second digit and add it to the check sum // For doubles greater than 9, sum the individual digits // e.g. 7 9 9 2 7 3 9 8 7 1 3 // ^ ^ ^ ^ ^ // = 1+8 + 4 + 6 + 1+6 + 2 for ($i = $length - 2; $i >= 0; $i -= 2) { $checkSum += array_sum(str_split((int) $value[$i] * 2)); } if (0 === $checkSum || 0 !== $checkSum % 10) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Luhn::CHECKSUM_FAILED_ERROR) ->addViolation(); } } } PK!\u7vendor/symfony/validator/Constraints/NegativeOrZero.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Jan Schädlich */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class NegativeOrZero extends LessThanOrEqual { use ZeroComparisonConstraintTrait; public $message = 'This value should be either negative or zero.'; } PK!);1vendor/symfony/validator/Constraints/Negative.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Jan Schädlich */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Negative extends LessThan { use ZeroComparisonConstraintTrait; public $message = 'This value should be negative.'; } PK!qRw1vendor/symfony/validator/Constraints/NotBlank.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class NotBlank extends Constraint { public const IS_BLANK_ERROR = 'c1051bb4-d103-4f74-8988-acbcafc7fdc3'; protected static $errorNames = [ self::IS_BLANK_ERROR => 'IS_BLANK_ERROR', ]; public $message = 'This value should not be blank.'; public $allowNull = false; public $normalizer; public function __construct(?array $options = null, ?string $message = null, ?bool $allowNull = null, ?callable $normalizer = null, ?array $groups = null, $payload = null) { parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; $this->allowNull = $allowNull ?? $this->allowNull; $this->normalizer = $normalizer ?? $this->normalizer; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } } PK!nn:vendor/symfony/validator/Constraints/NotBlankValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek * @author Kévin Dunglas */ class NotBlankValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof NotBlank) { throw new UnexpectedTypeException($constraint, NotBlank::class); } if ($constraint->allowNull && null === $value) { return; } if (\is_string($value) && null !== $constraint->normalizer) { $value = ($constraint->normalizer)($value); } if (false === $value || (empty($value) && '0' != $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(NotBlank::IS_BLANK_ERROR) ->addViolation(); } } } PK!?vendor/symfony/validator/Constraints/NotCompromisedPassword.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * Checks if a password has been leaked in a data breach. * * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Kévin Dunglas */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class NotCompromisedPassword extends Constraint { public const COMPROMISED_PASSWORD_ERROR = 'd9bcdbfe-a9d6-4bfa-a8ff-da5fd93e0f6d'; protected static $errorNames = [self::COMPROMISED_PASSWORD_ERROR => 'COMPROMISED_PASSWORD_ERROR']; public $message = 'This password has been leaked in a data breach, it must not be used. Please use another password.'; public $threshold = 1; public $skipOnError = false; public function __construct( ?array $options = null, ?string $message = null, ?int $threshold = null, ?bool $skipOnError = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->threshold = $threshold ?? $this->threshold; $this->skipOnError = $skipOnError ?? $this->skipOnError; } } PK!u Hvendor/symfony/validator/Constraints/NotCompromisedPasswordValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * Checks if a password has been leaked in a data breach using haveibeenpwned.com's API. * Use a k-anonymity model to protect the password being searched for. * * @see https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange * * @author Kévin Dunglas */ class NotCompromisedPasswordValidator extends ConstraintValidator { private const DEFAULT_API_ENDPOINT = 'https://api.pwnedpasswords.com/range/%s'; private $httpClient; private $charset; private $enabled; private $endpoint; public function __construct(?HttpClientInterface $httpClient = null, string $charset = 'UTF-8', bool $enabled = true, ?string $endpoint = null) { if (null === $httpClient && !class_exists(HttpClient::class)) { throw new \LogicException(sprintf('The "%s" class requires the "HttpClient" component. Try running "composer require symfony/http-client".', self::class)); } $this->httpClient = $httpClient ?? HttpClient::create(); $this->charset = $charset; $this->enabled = $enabled; $this->endpoint = $endpoint ?? self::DEFAULT_API_ENDPOINT; } /** * {@inheritdoc} * * @throws ExceptionInterface */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof NotCompromisedPassword) { throw new UnexpectedTypeException($constraint, NotCompromisedPassword::class); } if (!$this->enabled) { return; } if (null !== $value && !\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ('' === $value) { return; } if ('UTF-8' !== $this->charset) { $value = mb_convert_encoding($value, 'UTF-8', $this->charset); } $hash = strtoupper(sha1($value)); $hashPrefix = substr($hash, 0, 5); $url = sprintf($this->endpoint, $hashPrefix); try { $result = $this->httpClient->request('GET', $url)->getContent(); } catch (ExceptionInterface $e) { if ($constraint->skipOnError) { return; } throw $e; } foreach (explode("\r\n", $result) as $line) { if (!str_contains($line, ':')) { continue; } [$hashSuffix, $count] = explode(':', $line); if ($hashPrefix.$hashSuffix === $hash && $constraint->threshold <= (int) $count) { $this->context->buildViolation($constraint->message) ->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR) ->addViolation(); return; } } } } PK!aa3vendor/symfony/validator/Constraints/NotEqualTo.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class NotEqualTo extends AbstractComparison { public const IS_EQUAL_ERROR = 'aa2e33da-25c8-4d76-8c6c-812f02ea89dd'; protected static $errorNames = [ self::IS_EQUAL_ERROR => 'IS_EQUAL_ERROR', ]; public $message = 'This value should not be equal to {{ compared_value }}.'; } PK!f<  <vendor/symfony/validator/Constraints/NotEqualToValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values are all unequal (!=). * * @author Daniel Holmes * @author Bernhard Schussek */ class NotEqualToValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return $value1 != $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return NotEqualTo::IS_EQUAL_ERROR; } } PK!d7vendor/symfony/validator/Constraints/NotIdenticalTo.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Daniel Holmes * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class NotIdenticalTo extends AbstractComparison { public const IS_IDENTICAL_ERROR = '4aaac518-0dda-4129-a6d9-e216b9b454a0'; protected static $errorNames = [ self::IS_IDENTICAL_ERROR => 'IS_IDENTICAL_ERROR', ]; public $message = 'This value should not be identical to {{ compared_value_type }} {{ compared_value }}.'; } PK!C@vendor/symfony/validator/Constraints/NotIdenticalToValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Validates values aren't identical (!==). * * @author Daniel Holmes * @author Bernhard Schussek */ class NotIdenticalToValidator extends AbstractComparisonValidator { /** * {@inheritdoc} */ protected function compareValues($value1, $value2) { return $value1 !== $value2; } /** * {@inheritdoc} */ protected function getErrorCode() { return NotIdenticalTo::IS_IDENTICAL_ERROR; } } PK!e000vendor/symfony/validator/Constraints/NotNull.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class NotNull extends Constraint { public const IS_NULL_ERROR = 'ad32d13f-c3d4-423b-909a-857b961eb720'; protected static $errorNames = [ self::IS_NULL_ERROR => 'IS_NULL_ERROR', ]; public $message = 'This value should not be null.'; public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { parent::__construct($options ?? [], $groups, $payload); $this->message = $message ?? $this->message; } } PK!559vendor/symfony/validator/Constraints/NotNullValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class NotNullValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof NotNull) { throw new UnexpectedTypeException($constraint, NotNull::class); } if (null === $value) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(NotNull::IS_NULL_ERROR) ->addViolation(); } } } PK!P{>vendor/symfony/validator/Constraints/NumberConstraintTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; trigger_deprecation('symfony/validator', '5.2', '%s is deprecated.', NumberConstraintTrait::class); /** * @author Jan Schädlich * * @deprecated since Symfony 5.2 */ trait NumberConstraintTrait { private function configureNumberConstraintOptions($options): array { if (null === $options) { $options = []; } elseif (!\is_array($options)) { $options = [$this->getDefaultOption() => $options]; } if (isset($options['propertyPath'])) { throw new ConstraintDefinitionException(sprintf('The "propertyPath" option of the "%s" constraint cannot be set.', static::class)); } if (isset($options['value'])) { throw new ConstraintDefinitionException(sprintf('The "value" option of the "%s" constraint cannot be set.', static::class)); } $options['value'] = 0; return $options; } } PK!rR>1vendor/symfony/validator/Constraints/Optional.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"ANNOTATION"}) * * @author Bernhard Schussek */ class Optional extends Existence { } PK!ɬ7vendor/symfony/validator/Constraints/PositiveOrZero.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Jan Schädlich */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class PositiveOrZero extends GreaterThanOrEqual { use ZeroComparisonConstraintTrait; public $message = 'This value should be either positive or zero.'; } PK!Ob1vendor/symfony/validator/Constraints/Positive.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Jan Schädlich */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Positive extends GreaterThan { use ZeroComparisonConstraintTrait; public $message = 'This value should be positive.'; } PK!Đ .vendor/symfony/validator/Constraints/Range.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyPathInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\MissingOptionsException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Range extends Constraint { public const INVALID_CHARACTERS_ERROR = 'ad9a9798-7a99-4df7-8ce9-46e416a1e60b'; public const NOT_IN_RANGE_ERROR = '04b91c99-a946-4221-afc5-e65ebac401eb'; public const TOO_HIGH_ERROR = '2d28afcb-e32e-45fb-a815-01c431a86a69'; public const TOO_LOW_ERROR = '76454e69-502c-46c5-9643-f447d837c4d5'; protected static $errorNames = [ self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::NOT_IN_RANGE_ERROR => 'NOT_IN_RANGE_ERROR', self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', ]; public $notInRangeMessage = 'This value should be between {{ min }} and {{ max }}.'; public $minMessage = 'This value should be {{ limit }} or more.'; public $maxMessage = 'This value should be {{ limit }} or less.'; public $invalidMessage = 'This value should be a valid number.'; public $invalidDateTimeMessage = 'This value should be a valid datetime.'; public $min; public $minPropertyPath; public $max; public $maxPropertyPath; /** * @internal */ public $deprecatedMinMessageSet = false; /** * @internal */ public $deprecatedMaxMessageSet = false; /** * {@inheritdoc} * * @param string|PropertyPathInterface|null $minPropertyPath * @param string|PropertyPathInterface|null $maxPropertyPath */ public function __construct( ?array $options = null, ?string $notInRangeMessage = null, ?string $minMessage = null, ?string $maxMessage = null, ?string $invalidMessage = null, ?string $invalidDateTimeMessage = null, $min = null, $minPropertyPath = null, $max = null, $maxPropertyPath = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->notInRangeMessage = $notInRangeMessage ?? $this->notInRangeMessage; $this->minMessage = $minMessage ?? $this->minMessage; $this->maxMessage = $maxMessage ?? $this->maxMessage; $this->invalidMessage = $invalidMessage ?? $this->invalidMessage; $this->invalidDateTimeMessage = $invalidDateTimeMessage ?? $this->invalidDateTimeMessage; $this->min = $min ?? $this->min; $this->minPropertyPath = $minPropertyPath ?? $this->minPropertyPath; $this->max = $max ?? $this->max; $this->maxPropertyPath = $maxPropertyPath ?? $this->maxPropertyPath; if (null === $this->min && null === $this->minPropertyPath && null === $this->max && null === $this->maxPropertyPath) { throw new MissingOptionsException(sprintf('Either option "min", "minPropertyPath", "max" or "maxPropertyPath" must be given for constraint "%s".', __CLASS__), ['min', 'minPropertyPath', 'max', 'maxPropertyPath']); } if (null !== $this->min && null !== $this->minPropertyPath) { throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "min" or "minPropertyPath" options to be set, not both.', static::class)); } if (null !== $this->max && null !== $this->maxPropertyPath) { throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires only one of the "max" or "maxPropertyPath" options to be set, not both.', static::class)); } if ((null !== $this->minPropertyPath || null !== $this->maxPropertyPath) && !class_exists(PropertyAccess::class)) { throw new LogicException(sprintf('The "%s" constraint requires the Symfony PropertyAccess component to use the "minPropertyPath" or "maxPropertyPath" option.', static::class)); } if (null !== $this->min && null !== $this->max) { $this->deprecatedMinMessageSet = isset($options['minMessage']) || null !== $minMessage; $this->deprecatedMaxMessageSet = isset($options['maxMessage']) || null !== $maxMessage; // BC layer, should throw a ConstraintDefinitionException in 6.0 if ($this->deprecatedMinMessageSet || $this->deprecatedMaxMessageSet) { trigger_deprecation('symfony/validator', '4.4', '"minMessage" and "maxMessage" are deprecated when the "min" and "max" options are both set. Use "notInRangeMessage" instead.'); } } } } PK!7vendor/symfony/validator/Constraints/RangeValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class RangeValidator extends ConstraintValidator { private $propertyAccessor; public function __construct(?PropertyAccessorInterface $propertyAccessor = null) { $this->propertyAccessor = $propertyAccessor; } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Range) { throw new UnexpectedTypeException($constraint, Range::class); } if (null === $value) { return; } $min = $this->getLimit($constraint->minPropertyPath, $constraint->min, $constraint); $max = $this->getLimit($constraint->maxPropertyPath, $constraint->max, $constraint); if (!is_numeric($value) && !$value instanceof \DateTimeInterface) { if ($this->isParsableDatetimeString($min) && $this->isParsableDatetimeString($max)) { $this->context->buildViolation($constraint->invalidDateTimeMessage) ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) ->setCode(Range::INVALID_CHARACTERS_ERROR) ->addViolation(); } else { $this->context->buildViolation($constraint->invalidMessage) ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) ->setCode(Range::INVALID_CHARACTERS_ERROR) ->addViolation(); } return; } // Convert strings to DateTimes if comparing another DateTime // This allows to compare with any date/time value supported by // the DateTime constructor: // https://php.net/datetime.formats if ($value instanceof \DateTimeInterface) { $dateTimeClass = null; if (\is_string($min)) { $dateTimeClass = $value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class; try { $min = new $dateTimeClass($min); } catch (\Exception $e) { throw new ConstraintDefinitionException(sprintf('The min value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $min, $dateTimeClass, get_debug_type($constraint))); } } if (\is_string($max)) { $dateTimeClass = $dateTimeClass ?: ($value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class); try { $max = new $dateTimeClass($max); } catch (\Exception $e) { throw new ConstraintDefinitionException(sprintf('The max value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $max, $dateTimeClass, get_debug_type($constraint))); } } } $hasLowerLimit = null !== $min; $hasUpperLimit = null !== $max; if ($hasLowerLimit && $hasUpperLimit && ($value < $min || $value > $max)) { $message = $constraint->notInRangeMessage; $code = Range::NOT_IN_RANGE_ERROR; if ($value < $min && $constraint->deprecatedMinMessageSet) { $message = $constraint->minMessage; $code = Range::TOO_LOW_ERROR; } if ($value > $max && $constraint->deprecatedMaxMessageSet) { $message = $constraint->maxMessage; $code = Range::TOO_HIGH_ERROR; } $violationBuilder = $this->context->buildViolation($message) ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) ->setParameter('{{ min }}', $this->formatValue($min, self::PRETTY_DATE)) ->setParameter('{{ max }}', $this->formatValue($max, self::PRETTY_DATE)) ->setCode($code); if (null !== $constraint->maxPropertyPath) { $violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath); } if (null !== $constraint->minPropertyPath) { $violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath); } $violationBuilder->addViolation(); return; } if ($hasUpperLimit && $value > $max) { $violationBuilder = $this->context->buildViolation($constraint->maxMessage) ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) ->setParameter('{{ limit }}', $this->formatValue($max, self::PRETTY_DATE)) ->setCode(Range::TOO_HIGH_ERROR); if (null !== $constraint->maxPropertyPath) { $violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath); } if (null !== $constraint->minPropertyPath) { $violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath); } $violationBuilder->addViolation(); return; } if ($hasLowerLimit && $value < $min) { $violationBuilder = $this->context->buildViolation($constraint->minMessage) ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) ->setParameter('{{ limit }}', $this->formatValue($min, self::PRETTY_DATE)) ->setCode(Range::TOO_LOW_ERROR); if (null !== $constraint->maxPropertyPath) { $violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath); } if (null !== $constraint->minPropertyPath) { $violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath); } $violationBuilder->addViolation(); } } private function getLimit(?string $propertyPath, $default, Constraint $constraint) { if (null === $propertyPath) { return $default; } if (null === $object = $this->context->getObject()) { return $default; } try { return $this->getPropertyAccessor()->getValue($object, $propertyPath); } catch (NoSuchPropertyException $e) { throw new ConstraintDefinitionException(sprintf('Invalid property path "%s" provided to "%s" constraint: ', $propertyPath, get_debug_type($constraint)).$e->getMessage(), 0, $e); } catch (UninitializedPropertyException $e) { return null; } } private function getPropertyAccessor(): PropertyAccessorInterface { if (null === $this->propertyAccessor) { $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); } return $this->propertyAccessor; } private function isParsableDatetimeString($boundary): bool { if (null === $boundary) { return true; } if (!\is_string($boundary)) { return false; } try { new \DateTime($boundary); } catch (\Exception $e) { return false; } return true; } } PK!˼.vendor/symfony/validator/Constraints/Regex.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Regex extends Constraint { public const REGEX_FAILED_ERROR = 'de1e3db3-5ed4-4941-aae4-59f3667cc3a3'; protected static $errorNames = [ self::REGEX_FAILED_ERROR => 'REGEX_FAILED_ERROR', ]; public $message = 'This value is not valid.'; public $pattern; public $htmlPattern; public $match = true; public $normalizer; /** * {@inheritdoc} * * @param string|array $pattern The pattern to evaluate or an array of options */ public function __construct( $pattern, ?string $message = null, ?string $htmlPattern = null, ?bool $match = null, ?callable $normalizer = null, ?array $groups = null, $payload = null, array $options = [] ) { if (\is_array($pattern)) { $options = array_merge($pattern, $options); } elseif (null !== $pattern) { $options['value'] = $pattern; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->htmlPattern = $htmlPattern ?? $this->htmlPattern; $this->match = $match ?? $this->match; $this->normalizer = $normalizer ?? $this->normalizer; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } /** * {@inheritdoc} */ public function getDefaultOption() { return 'pattern'; } /** * {@inheritdoc} */ public function getRequiredOptions() { return ['pattern']; } /** * Converts the htmlPattern to a suitable format for HTML5 pattern. * Example: /^[a-z]+$/ would be converted to [a-z]+ * However, if options are specified, it cannot be converted. * * @see http://dev.w3.org/html5/spec/single-page.html#the-pattern-attribute * * @return string|null */ public function getHtmlPattern() { // If htmlPattern is specified, use it if (null !== $this->htmlPattern) { return empty($this->htmlPattern) ? null : $this->htmlPattern; } // Quit if delimiters not at very beginning/end (e.g. when options are passed) if ($this->pattern[0] !== $this->pattern[\strlen($this->pattern) - 1]) { return null; } $delimiter = $this->pattern[0]; // Unescape the delimiter $pattern = str_replace('\\'.$delimiter, $delimiter, substr($this->pattern, 1, -1)); // If the pattern is inverted, we can wrap it in // ((?!pattern).)* if (!$this->match) { return '((?!'.$pattern.').)*'; } // If the pattern contains an or statement, wrap the pattern in // .*(pattern).* and quit. Otherwise we'd need to parse the pattern if (str_contains($pattern, '|')) { return '.*('.$pattern.').*'; } // Trim leading ^, otherwise prepend .* $pattern = '^' === $pattern[0] ? substr($pattern, 1) : '.*'.$pattern; // Trim trailing $, otherwise append .* $pattern = '$' === $pattern[\strlen($pattern) - 1] ? substr($pattern, 0, -1) : $pattern.'.*'; return $pattern; } } PK!{,7vendor/symfony/validator/Constraints/RegexValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value match or not given regexp pattern. * * @author Bernhard Schussek * @author Joseph Bielawski */ class RegexValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Regex) { throw new UnexpectedTypeException($constraint, Regex::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (null !== $constraint->normalizer) { $value = ($constraint->normalizer)($value); } if ($constraint->match xor preg_match($constraint->pattern, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Regex::REGEX_FAILED_ERROR) ->addViolation(); } } } PK!t * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * @Annotation * @Target({"ANNOTATION"}) * * @author Bernhard Schussek */ class Required extends Existence { } PK!225vendor/symfony/validator/Constraints/Sequentially.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; /** * Use this constraint to sequentially validate nested constraints. * Validation for the nested constraints collection will stop at first violation. * * @Annotation * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) * * @author Maxime Steinhausser */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Sequentially extends Composite { public $constraints = []; public function __construct($constraints = null, ?array $groups = null, $payload = null) { parent::__construct($constraints ?? [], $groups, $payload); } public function getDefaultOption() { return 'constraints'; } public function getRequiredOptions() { return ['constraints']; } protected function getCompositeOption() { return 'constraints'; } public function getTargets() { return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT]; } } PK!h>`>vendor/symfony/validator/Constraints/SequentiallyValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Maxime Steinhausser */ class SequentiallyValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Sequentially) { throw new UnexpectedTypeException($constraint, Sequentially::class); } $context = $this->context; $validator = $context->getValidator()->inContext($context); $originalCount = $validator->getViolations()->count(); foreach ($constraint->constraints as $c) { if ($originalCount !== $validator->validate($value, $c)->getViolations()->count()) { break; } } } } PK!-vendor/symfony/validator/Constraints/Time.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Time extends Constraint { public const INVALID_FORMAT_ERROR = '9d27b2bb-f755-4fbf-b725-39b1edbdebdf'; public const INVALID_TIME_ERROR = '8532f9e1-84b2-4d67-8989-0818bc38533b'; protected static $errorNames = [ self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', self::INVALID_TIME_ERROR => 'INVALID_TIME_ERROR', ]; public $message = 'This value is not a valid time.'; public function __construct( ?array $options = null, ?string $message = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!FjPP6vendor/symfony/validator/Constraints/TimeValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class TimeValidator extends ConstraintValidator { public const PATTERN = '/^(\d{2}):(\d{2}):(\d{2})$/D'; /** * Checks whether a time is valid. * * @internal */ public static function checkTime(int $hour, int $minute, float $second): bool { return $hour >= 0 && $hour < 24 && $minute >= 0 && $minute < 60 && $second >= 0 && $second < 60; } /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Time) { throw new UnexpectedTypeException($constraint, Time::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (!preg_match(static::PATTERN, $value, $matches)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Time::INVALID_FORMAT_ERROR) ->addViolation(); return; } if (!self::checkTime($matches[1], $matches[2], $matches[3])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Time::INVALID_TIME_ERROR) ->addViolation(); } } } PK! 1vendor/symfony/validator/Constraints/Timezone.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Javier Spagnoletti * @author Hugo Hamon */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Timezone extends Constraint { public const TIMEZONE_IDENTIFIER_ERROR = '5ce113e6-5e64-4ea2-90fe-d2233956db13'; public const TIMEZONE_IDENTIFIER_IN_ZONE_ERROR = 'b57767b1-36c0-40ac-a3d7-629420c775b8'; public const TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR = 'c4a22222-dc92-4fc0-abb0-d95b268c7d0b'; public const TIMEZONE_IDENTIFIER_INTL_ERROR = '45863c26-88dc-41ba-bf53-c73bd1f7e90d'; public $zone = \DateTimeZone::ALL; public $countryCode; public $intlCompatible = false; public $message = 'This value is not a valid timezone.'; protected static $errorNames = [ self::TIMEZONE_IDENTIFIER_ERROR => 'TIMEZONE_IDENTIFIER_ERROR', self::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR => 'TIMEZONE_IDENTIFIER_IN_ZONE_ERROR', self::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR => 'TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR', self::TIMEZONE_IDENTIFIER_INTL_ERROR => 'TIMEZONE_IDENTIFIER_INTL_ERROR', ]; /** * {@inheritdoc} * * @param int|array|null $zone A combination of {@see \DateTimeZone} class constants or a set of options */ public function __construct( $zone = null, ?string $message = null, ?string $countryCode = null, ?bool $intlCompatible = null, ?array $groups = null, $payload = null, array $options = [] ) { if (\is_array($zone)) { $options = array_merge($zone, $options); } elseif (null !== $zone) { $options['value'] = $zone; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->countryCode = $countryCode ?? $this->countryCode; $this->intlCompatible = $intlCompatible ?? $this->intlCompatible; if (null === $this->countryCode) { if (0 >= $this->zone || \DateTimeZone::ALL_WITH_BC < $this->zone) { throw new ConstraintDefinitionException('The option "zone" must be a valid range of "\DateTimeZone" constants.'); } } elseif (\DateTimeZone::PER_COUNTRY !== (\DateTimeZone::PER_COUNTRY & $this->zone)) { throw new ConstraintDefinitionException('The option "countryCode" can only be used when the "zone" option is configured with "\DateTimeZone::PER_COUNTRY".'); } if ($this->intlCompatible && !class_exists(\IntlTimeZone::class)) { throw new ConstraintDefinitionException('The option "intlCompatible" can only be used when the PHP intl extension is available.'); } } /** * {@inheritdoc} */ public function getDefaultOption() { return 'zone'; } } PK!h__:vendor/symfony/validator/Constraints/TimezoneValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Intl\Exception\MissingResourceException; use Symfony\Component\Intl\Timezones; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether a value is a valid timezone identifier. * * @author Javier Spagnoletti * @author Hugo Hamon */ class TimezoneValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Timezone) { throw new UnexpectedTypeException($constraint, Timezone::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ($constraint->intlCompatible && 'Etc/Unknown' === \IntlTimeZone::createTimeZone($value)->getID()) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Timezone::TIMEZONE_IDENTIFIER_INTL_ERROR) ->addViolation(); return; } if ( \in_array($value, self::getPhpTimezones($constraint->zone, $constraint->countryCode), true) || \in_array($value, self::getIntlTimezones($constraint->zone, $constraint->countryCode), true) ) { return; } if ($constraint->countryCode) { $code = Timezone::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR; } elseif (\DateTimeZone::ALL !== $constraint->zone) { $code = Timezone::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR; } else { $code = Timezone::TIMEZONE_IDENTIFIER_ERROR; } $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode($code) ->addViolation(); } private static function getPhpTimezones(int $zone, ?string $countryCode = null): array { if (null !== $countryCode) { try { return @\DateTimeZone::listIdentifiers($zone, $countryCode) ?: []; } catch (\ValueError $e) { return []; } } return \DateTimeZone::listIdentifiers($zone); } private static function getIntlTimezones(int $zone, ?string $countryCode = null): array { if (!class_exists(Timezones::class)) { return []; } if (null !== $countryCode) { try { return Timezones::forCountryCode($countryCode); } catch (MissingResourceException $e) { return []; } } $timezones = Timezones::getIds(); if (\DateTimeZone::ALL === (\DateTimeZone::ALL & $zone)) { return $timezones; } $filtered = []; foreach ((new \ReflectionClass(\DateTimeZone::class))->getConstants() as $const => $flag) { if ($flag !== ($flag & $zone)) { continue; } $filtered[] = array_filter($timezones, static function ($id) use ($const) { return 0 === stripos($id, $const.'/'); }); } return $filtered ? array_merge(...$filtered) : []; } } PK!iF߿1vendor/symfony/validator/Constraints/Traverse.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @Annotation * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_CLASS)] class Traverse extends Constraint { public $traverse = true; /** * @param bool|array|null $traverse */ public function __construct($traverse = null) { if (\is_array($traverse) && \array_key_exists('groups', $traverse)) { throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__)); } parent::__construct($traverse); } /** * {@inheritdoc} */ public function getDefaultOption() { return 'traverse'; } /** * {@inheritdoc} */ public function getTargets() { return self::CLASS_CONSTRAINT; } } PK!oR-vendor/symfony/validator/Constraints/Type.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Type extends Constraint { public const INVALID_TYPE_ERROR = 'ba785a8c-82cb-4283-967c-3cf342181b40'; protected static $errorNames = [ self::INVALID_TYPE_ERROR => 'INVALID_TYPE_ERROR', ]; public $message = 'This value should be of type {{ type }}.'; public $type; /** * {@inheritdoc} * * @param string|array $type One ore multiple types to validate against or a set of options */ public function __construct($type, ?string $message = null, ?array $groups = null, $payload = null, array $options = []) { if (\is_array($type) && \is_string(key($type))) { $options = array_merge($type, $options); } elseif (null !== $type) { $options['value'] = $type; } parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } /** * {@inheritdoc} */ public function getDefaultOption() { return 'type'; } /** * {@inheritdoc} */ public function getRequiredOptions() { return ['type']; } } PK!^W 6vendor/symfony/validator/Constraints/TypeValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Bernhard Schussek */ class TypeValidator extends ConstraintValidator { private const VALIDATION_FUNCTIONS = [ 'bool' => 'is_bool', 'boolean' => 'is_bool', 'int' => 'is_int', 'integer' => 'is_int', 'long' => 'is_int', 'float' => 'is_float', 'double' => 'is_float', 'real' => 'is_float', 'numeric' => 'is_numeric', 'string' => 'is_string', 'scalar' => 'is_scalar', 'array' => 'is_array', 'iterable' => 'is_iterable', 'countable' => 'is_countable', 'callable' => 'is_callable', 'object' => 'is_object', 'resource' => 'is_resource', 'null' => 'is_null', 'alnum' => 'ctype_alnum', 'alpha' => 'ctype_alpha', 'cntrl' => 'ctype_cntrl', 'digit' => 'ctype_digit', 'graph' => 'ctype_graph', 'lower' => 'ctype_lower', 'print' => 'ctype_print', 'punct' => 'ctype_punct', 'space' => 'ctype_space', 'upper' => 'ctype_upper', 'xdigit' => 'ctype_xdigit', ]; /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Type) { throw new UnexpectedTypeException($constraint, Type::class); } if (null === $value) { return; } $types = (array) $constraint->type; foreach ($types as $type) { $type = strtolower($type); if (isset(self::VALIDATION_FUNCTIONS[$type]) && self::VALIDATION_FUNCTIONS[$type]($value)) { return; } if ($value instanceof $type) { return; } } $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setParameter('{{ type }}', implode('|', $types)) ->setCode(Type::INVALID_TYPE_ERROR) ->addViolation(); } } PK!-vendor/symfony/validator/Constraints/Ulid.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * * @author Laurent Clouet */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Ulid extends Constraint { public const TOO_SHORT_ERROR = '7b44804e-37d5-4df4-9bdd-b738d4a45bb4'; public const TOO_LONG_ERROR = '9608249f-6da1-4d53-889e-9864b58c4d37'; public const INVALID_CHARACTERS_ERROR = 'e4155739-5135-4258-9c81-ae7b44b5311e'; public const TOO_LARGE_ERROR = 'df8cfb9a-ce6d-4a69-ae5a-eea7ab6f278b'; protected static $errorNames = [ self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', ]; public $message = 'This is not a valid ULID.'; public function __construct( ?array $options = null, ?string $message = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; } } PK!/` 6vendor/symfony/validator/Constraints/UlidValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether the value is a valid ULID (Universally Unique Lexicographically Sortable Identifier). * Cf https://github.com/ulid/spec for ULID specifications. * * @author Laurent Clouet */ class UlidValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Ulid) { throw new UnexpectedTypeException($constraint, Ulid::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (26 !== \strlen($value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(26 > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR) ->addViolation(); return; } if (\strlen($value) !== strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Ulid::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ' // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings if ($value[0] > '7') { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Ulid::TOO_LARGE_ERROR) ->addViolation(); } } } PK!R$$/vendor/symfony/validator/Constraints/Unique.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Yevgeniy Zholkevskiy */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Unique extends Constraint { public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a'; protected static $errorNames = [ self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE', ]; public $message = 'This collection should contain only unique elements.'; public $normalizer; public function __construct( ?array $options = null, ?string $message = null, ?callable $normalizer = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->normalizer = $normalizer ?? $this->normalizer; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } } PK!08vendor/symfony/validator/Constraints/UniqueValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Yevgeniy Zholkevskiy */ class UniqueValidator extends ConstraintValidator { /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Unique) { throw new UnexpectedTypeException($constraint, Unique::class); } if (null === $value) { return; } if (!\is_array($value) && !$value instanceof \IteratorAggregate) { throw new UnexpectedValueException($value, 'array|IteratorAggregate'); } $collectionElements = []; $normalizer = $this->getNormalizer($constraint); foreach ($value as $element) { $element = $normalizer($element); if (\in_array($element, $collectionElements, true)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($element)) ->setCode(Unique::IS_NOT_UNIQUE) ->addViolation(); return; } $collectionElements[] = $element; } } private function getNormalizer(Unique $unique): callable { if (null === $unique->normalizer) { return static function ($value) { return $value; }; } return $unique->normalizer; } } PK!9V11,vendor/symfony/validator/Constraints/Url.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Url extends Constraint { public const INVALID_URL_ERROR = '57c2f299-1154-4870-89bb-ef3b1f5ad229'; protected static $errorNames = [ self::INVALID_URL_ERROR => 'INVALID_URL_ERROR', ]; public $message = 'This value is not a valid URL.'; public $protocols = ['http', 'https']; public $relativeProtocol = false; public $normalizer; public function __construct( ?array $options = null, ?string $message = null, ?array $protocols = null, ?bool $relativeProtocol = null, ?callable $normalizer = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->protocols = $protocols ?? $this->protocols; $this->relativeProtocol = $relativeProtocol ?? $this->relativeProtocol; $this->normalizer = $normalizer ?? $this->normalizer; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } } PK!w2VV5vendor/symfony/validator/Constraints/UrlValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * @author Bernhard Schussek */ class UrlValidator extends ConstraintValidator { public const PATTERN = '~^ (%s):// # protocol (((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth ( (?: (?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode | (?:[\pL\pN\pS\pM\-\_]++\.)+[\pL\pN\pM]++ # a multi-level domain name | [a-z0-9\-\_]++ # a single-level domain name )\.? | # or \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address | # or \[ (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::)))) \] # an IPv6 address ) (:[0-9]+)? # a port (optional) (?:/ (?:[\pL\pN\pS\pM\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional) (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional) $~ixuD'; /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Url) { throw new UnexpectedTypeException($constraint, Url::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if ('' === $value) { return; } if (null !== $constraint->normalizer) { $value = ($constraint->normalizer)($value); } $pattern = $constraint->relativeProtocol ? str_replace('(%s):', '(?:(%s):)?', static::PATTERN) : static::PATTERN; $pattern = sprintf($pattern, implode('|', $constraint->protocols)); if (!preg_match($pattern, $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Url::INVALID_URL_ERROR) ->addViolation(); return; } } } PK!ݸ9 -vendor/symfony/validator/Constraints/Uuid.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\InvalidArgumentException; /** * @Annotation * * @author Colin O'Dell * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Uuid extends Constraint { public const TOO_SHORT_ERROR = 'aa314679-dac9-4f54-bf97-b2049df8f2a3'; public const TOO_LONG_ERROR = '494897dd-36f8-4d31-8923-71a8d5f3000d'; public const INVALID_CHARACTERS_ERROR = '51120b12-a2bc-41bf-aa53-cd73daf330d0'; public const INVALID_HYPHEN_PLACEMENT_ERROR = '98469c83-0309-4f5d-bf95-a496dcaa869c'; public const INVALID_VERSION_ERROR = '21ba13b4-b185-4882-ac6f-d147355987eb'; public const INVALID_VARIANT_ERROR = '164ef693-2b9d-46de-ad7f-836201f0c2db'; protected static $errorNames = [ self::TOO_SHORT_ERROR => 'TOO_SHORT_ERROR', self::TOO_LONG_ERROR => 'TOO_LONG_ERROR', self::INVALID_CHARACTERS_ERROR => 'INVALID_CHARACTERS_ERROR', self::INVALID_HYPHEN_PLACEMENT_ERROR => 'INVALID_HYPHEN_PLACEMENT_ERROR', self::INVALID_VERSION_ERROR => 'INVALID_VERSION_ERROR', self::INVALID_VARIANT_ERROR => 'INVALID_VARIANT_ERROR', ]; // Possible versions defined by RFC 9562/4122 public const V1_MAC = 1; public const V2_DCE = 2; public const V3_MD5 = 3; public const V4_RANDOM = 4; public const V5_SHA1 = 5; public const V6_SORTABLE = 6; public const ALL_VERSIONS = [ self::V1_MAC, self::V2_DCE, self::V3_MD5, self::V4_RANDOM, self::V5_SHA1, self::V6_SORTABLE, ]; /** * Message to display when validation fails. * * @var string */ public $message = 'This is not a valid UUID.'; /** * Strict mode only allows UUIDs that meet the formal definition and formatting per RFC 9562/4122. * * Set this to `false` to allow legacy formats with different dash positioning or wrapping characters * * @var bool */ public $strict = true; /** * Array of allowed versions (see version constants above). * * All UUID versions are allowed by default * * @var int[] */ public $versions = self::ALL_VERSIONS; public $normalizer; /** * {@inheritdoc} * * @param int[]|null $versions */ public function __construct( ?array $options = null, ?string $message = null, ?array $versions = null, ?bool $strict = null, ?callable $normalizer = null, ?array $groups = null, $payload = null ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->versions = $versions ?? $this->versions; $this->strict = $strict ?? $this->strict; $this->normalizer = $normalizer ?? $this->normalizer; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); } } } PK!ƤU!!6vendor/symfony/validator/Constraints/UuidValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Validates whether the value is a valid UUID (also known as GUID). * * Strict validation will allow a UUID as specified per RFC 9562/4122. * Loose validation will allow any type of UUID. * * @author Colin O'Dell * @author Bernhard Schussek * * @see https://datatracker.ietf.org/doc/html/rfc9562 * @see https://en.wikipedia.org/wiki/Universally_unique_identifier */ class UuidValidator extends ConstraintValidator { // The strict pattern matches UUIDs like this: // xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx // Roughly speaking: // x = any hexadecimal character // M = any allowed version {1..6} // N = any allowed variant {8, 9, a, b} public const STRICT_LENGTH = 36; public const STRICT_FIRST_HYPHEN_POSITION = 8; public const STRICT_LAST_HYPHEN_POSITION = 23; public const STRICT_VERSION_POSITION = 14; public const STRICT_VARIANT_POSITION = 19; // The loose pattern validates similar yet non-compliant UUIDs. // Hyphens are completely optional. If present, they should only appear // between every fourth character: // xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx // xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxx-xxxx // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx // The value can also be wrapped with characters like []{}: // {xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx} // Neither the version nor the variant is validated by this pattern. public const LOOSE_MAX_LENGTH = 39; public const LOOSE_FIRST_HYPHEN_POSITION = 4; /** * {@inheritdoc} */ public function validate($value, Constraint $constraint) { if (!$constraint instanceof Uuid) { throw new UnexpectedTypeException($constraint, Uuid::class); } if (null === $value || '' === $value) { return; } if (!\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { throw new UnexpectedValueException($value, 'string'); } $value = (string) $value; if (null !== $constraint->normalizer) { $value = ($constraint->normalizer)($value); } if ($constraint->strict) { $this->validateStrict($value, $constraint); return; } $this->validateLoose($value, $constraint); } private function validateLoose(string $value, Uuid $constraint) { // Error priority: // 1. ERROR_INVALID_CHARACTERS // 2. ERROR_INVALID_HYPHEN_PLACEMENT // 3. ERROR_TOO_SHORT/ERROR_TOO_LONG // Trim any wrapping characters like [] or {} used by some legacy systems $trimmed = trim($value, '[]{}'); // Position of the next expected hyphen $h = self::LOOSE_FIRST_HYPHEN_POSITION; // Expected length $l = self::LOOSE_MAX_LENGTH; for ($i = 0; $i < $l; ++$i) { // Check length if (!isset($trimmed[$i])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::TOO_SHORT_ERROR) ->addViolation(); return; } // Hyphens must occur every fifth position // xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx // ^ ^ ^ ^ ^ ^ ^ if ('-' === $trimmed[$i]) { if ($i !== $h) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_HYPHEN_PLACEMENT_ERROR) ->addViolation(); return; } $h += 5; continue; } // Missing hyphens are ignored if ($i === $h) { $h += 4; --$l; } // Check characters if (!ctype_xdigit($trimmed[$i])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } } // Check length again if (isset($trimmed[$i])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::TOO_LONG_ERROR) ->addViolation(); } } private function validateStrict(string $value, Uuid $constraint) { // Error priority: // 1. ERROR_INVALID_CHARACTERS // 2. ERROR_INVALID_HYPHEN_PLACEMENT // 3. ERROR_TOO_SHORT/ERROR_TOO_LONG // 4. ERROR_INVALID_VERSION // 5. ERROR_INVALID_VARIANT // Position of the next expected hyphen $h = self::STRICT_FIRST_HYPHEN_POSITION; for ($i = 0; $i < self::STRICT_LENGTH; ++$i) { // Check length if (!isset($value[$i])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::TOO_SHORT_ERROR) ->addViolation(); return; } // Check hyphen placement // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx // ^ ^ ^ ^ if ('-' === $value[$i]) { if ($i !== $h) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_HYPHEN_PLACEMENT_ERROR) ->addViolation(); return; } // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx // ^ if ($h < self::STRICT_LAST_HYPHEN_POSITION) { $h += 5; } continue; } // Check characters if (!ctype_xdigit($value[$i])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_CHARACTERS_ERROR) ->addViolation(); return; } // Missing hyphen if ($i === $h) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_HYPHEN_PLACEMENT_ERROR) ->addViolation(); return; } } // Check length again if (isset($value[$i])) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::TOO_LONG_ERROR) ->addViolation(); } // Check version if (!\in_array($value[self::STRICT_VERSION_POSITION], $constraint->versions)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_VERSION_ERROR) ->addViolation(); } // Check variant - first two bits must equal "10" // 0b10xx // & 0b1100 (12) // = 0b1000 (8) if (8 !== (hexdec($value[self::STRICT_VARIANT_POSITION]) & 12)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Uuid::INVALID_VARIANT_ERROR) ->addViolation(); } } } PK!Jo$$.vendor/symfony/validator/Constraints/Valid.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; /** * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Valid extends Constraint { public $traverse = true; public function __construct(?array $options = null, ?array $groups = null, $payload = null, ?bool $traverse = null) { parent::__construct($options ?? [], $groups, $payload); $this->traverse = $traverse ?? $this->traverse; } public function __get(string $option) { if ('groups' === $option) { // when this is reached, no groups have been configured return null; } return parent::__get($option); } /** * {@inheritdoc} */ public function addImplicitGroupName(string $group) { if (null !== $this->groups) { parent::addImplicitGroupName($group); } } } PK!?7vendor/symfony/validator/Constraints/ValidValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * @author Christian Flothmann */ class ValidValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) { if (!$constraint instanceof Valid) { throw new UnexpectedTypeException($constraint, Valid::class); } if (null === $value) { return; } $this->context ->getValidator() ->inContext($this->context) ->validate($value, null, $this->context->getGroup()); } } PK!eKFvendor/symfony/validator/Constraints/ZeroComparisonConstraintTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * @internal * * @author Jan Schädlich * @author Alexander M. Turek */ trait ZeroComparisonConstraintTrait { public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) { if (null === $options) { $options = []; } if (isset($options['propertyPath'])) { throw new ConstraintDefinitionException(sprintf('The "propertyPath" option of the "%s" constraint cannot be set.', static::class)); } if (isset($options['value'])) { throw new ConstraintDefinitionException(sprintf('The "value" option of the "%s" constraint cannot be set.', static::class)); } parent::__construct(0, null, $message, $groups, $payload, $options); } public function validatedBy(): string { return parent::class.'Validator'; } } PK! bsEvendor/symfony/validator/Context/ExecutionContextFactoryInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\Validator\ValidatorInterface; /** * Creates instances of {@link ExecutionContextInterface}. * * You can use a custom factory if you want to customize the execution context * that is passed through the validation run. * * @author Bernhard Schussek */ interface ExecutionContextFactoryInterface { /** * Creates a new execution context. * * @param mixed $root The root value of the validated * object graph * * @return ExecutionContextInterface */ public function createContext(ValidatorInterface $validator, $root); } PK!pPD<vendor/symfony/validator/Context/ExecutionContextFactory.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * Creates new {@link ExecutionContext} instances. * * @author Bernhard Schussek * * @internal version 2.5. Code against ExecutionContextFactoryInterface instead. */ class ExecutionContextFactory implements ExecutionContextFactoryInterface { private $translator; private $translationDomain; public function __construct(TranslatorInterface $translator, ?string $translationDomain = null) { $this->translator = $translator; $this->translationDomain = $translationDomain; } /** * {@inheritdoc} */ public function createContext(ValidatorInterface $validator, $root) { return new ExecutionContext( $validator, $root, $this->translator, $this->translationDomain ); } } PK!5:+:+>vendor/symfony/validator/Context/ExecutionContextInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; /** * The context of a validation run. * * The context collects all violations generated during the validation. By * default, validators execute all validations in a new context: * * $violations = $validator->validate($object); * * When you make another call to the validator, while the validation is in * progress, the violations will be isolated from each other: * * public function validate($value, Constraint $constraint) * { * $validator = $this->context->getValidator(); * * // The violations are not added to $this->context * $violations = $validator->validate($value); * } * * However, if you want to add the violations to the current context, use the * {@link ValidatorInterface::inContext()} method: * * public function validate($value, Constraint $constraint) * { * $validator = $this->context->getValidator(); * * // The violations are added to $this->context * $validator * ->inContext($this->context) * ->validate($value) * ; * } * * Additionally, the context provides information about the current state of * the validator, such as the currently validated class, the name of the * currently validated property and more. These values change over time, so you * cannot store a context and expect that the methods still return the same * results later on. * * @author Bernhard Schussek */ interface ExecutionContextInterface { /** * Adds a violation at the current node of the validation graph. * * @param string|\Stringable $message The error message as a string or a stringable object * @param array $params The parameters substituted in the error message */ public function addViolation(string $message, array $params = []); /** * Returns a builder for adding a violation with extended information. * * Call {@link ConstraintViolationBuilderInterface::addViolation()} to * add the violation when you're done with the configuration: * * $context->buildViolation('Please enter a number between %min% and %max%.') * ->setParameter('%min%', '3') * ->setParameter('%max%', '10') * ->setTranslationDomain('number_validation') * ->addViolation(); * * @param string|\Stringable $message The error message as a string or a stringable object * @param array $parameters The parameters substituted in the error message * * @return ConstraintViolationBuilderInterface */ public function buildViolation(string $message, array $parameters = []); /** * Returns the validator. * * Useful if you want to validate additional constraints: * * public function validate($value, Constraint $constraint) * { * $validator = $this->context->getValidator(); * * $violations = $validator->validate($value, new Length(['min' => 3])); * * if (count($violations) > 0) { * // ... * } * } * * @return ValidatorInterface */ public function getValidator(); /** * Returns the currently validated object. * * If the validator is currently validating a class constraint, the * object of that class is returned. If it is validating a property or * getter constraint, the object that the property/getter belongs to is * returned. * * In other cases, null is returned. * * @return object|null */ public function getObject(); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param mixed $value The validated value * @param object|null $object The currently validated object * @param string $propertyPath The property path to the current value */ public function setNode($value, ?object $object, ?MetadataInterface $metadata, string $propertyPath); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string|null $group The validated group */ public function setGroup(?string $group); /** * Warning: Should not be called by user code, to be used by the validator engine only. */ public function setConstraint(Constraint $constraint); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence */ public function markGroupAsValidated(string $cacheKey, string $groupHash); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * * @return bool */ public function isGroupValidated(string $cacheKey, string $groupHash); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint */ public function markConstraintAsValidated(string $cacheKey, string $constraintHash); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @return bool */ public function isConstraintValidated(string $cacheKey, string $constraintHash); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string $cacheKey The hash of the object * * @see ObjectInitializerInterface */ public function markObjectAsInitialized(string $cacheKey); /** * Warning: Should not be called by user code, to be used by the validator engine only. * * @param string $cacheKey The hash of the object * * @return bool * * @see ObjectInitializerInterface */ public function isObjectInitialized(string $cacheKey); /** * Returns the violations generated by the validator so far. * * @return ConstraintViolationListInterface */ public function getViolations(); /** * Returns the value at which validation was started in the object graph. * * The validator, when given an object, traverses the properties and * related objects and their properties. The root of the validation is the * object from which the traversal started. * * The current value is returned by {@link getValue}. * * @return mixed */ public function getRoot(); /** * Returns the value that the validator is currently validating. * * If you want to retrieve the object that was originally passed to the * validator, use {@link getRoot}. * * @return mixed */ public function getValue(); /** * Returns the metadata for the currently validated value. * * With the core implementation, this method returns a * {@link Mapping\ClassMetadataInterface} instance if the current value is an object, * a {@link Mapping\PropertyMetadata} instance if the current value is * the value of a property and a {@link Mapping\GetterMetadata} instance if * the validated value is the result of a getter method. * * If the validated value is neither of these, for example if the validator * has been called with a plain value and constraint, this method returns * null. * * @return MetadataInterface|null */ public function getMetadata(); /** * Returns the validation group that is currently being validated. * * @return string|null */ public function getGroup(); /** * Returns the class name of the current node. * * If the metadata of the current node does not implement * {@link Mapping\ClassMetadataInterface} or if no metadata is available for the * current node, this method returns null. * * @return string|null */ public function getClassName(); /** * Returns the property name of the current node. * * If the metadata of the current node does not implement * {@link PropertyMetadataInterface} or if no metadata is available for the * current node, this method returns null. * * @return string|null */ public function getPropertyName(); /** * Returns the property path to the value that the validator is currently * validating. * * For example, take the following object graph: * *
     * (Person)---($address: Address)---($street: string)
     * 
* * When the Person instance is passed to the validator, the * property path is initially empty. When the $address property * of that person is validated, the property path is "address". When * the $street property of the related Address instance * is validated, the property path is "address.street". * * Properties of objects are prefixed with a dot in the property path. * Indices of arrays or objects implementing the {@link \ArrayAccess} * interface are enclosed in brackets. For example, if the property in * the previous example is $addresses and contains an array * of Address instance, the property path generated for the * $street property of one of these addresses is for example * "addresses[0].street". * * @param string $subPath Optional. The suffix appended to the current * property path. * * @return string The current property path. The result may be an empty * string if the validator is currently validating the * root value of the validation graph. */ public function getPropertyPath(string $subPath = ''); } PK!x!!5vendor/symfony/validator/Context/ExecutionContext.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\MemberMetadata; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\LazyProperty; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * The context used and created by {@link ExecutionContextFactory}. * * @author Bernhard Schussek * * @see ExecutionContextInterface * * @internal since version 2.5. Code against ExecutionContextInterface instead. */ class ExecutionContext implements ExecutionContextInterface { /** * @var ValidatorInterface */ private $validator; /** * The root value of the validated object graph. * * @var mixed */ private $root; /** * @var TranslatorInterface */ private $translator; /** * @var string|null */ private $translationDomain; /** * The violations generated in the current context. * * @var ConstraintViolationList */ private $violations; /** * The currently validated value. * * @var mixed */ private $value; /** * The currently validated object. * * @var object|null */ private $object; /** * The property path leading to the current value. * * @var string */ private $propertyPath = ''; /** * The current validation metadata. * * @var MetadataInterface|null */ private $metadata; /** * The currently validated group. * * @var string|null */ private $group; /** * The currently validated constraint. * * @var Constraint|null */ private $constraint; /** * Stores which objects have been validated in which group. * * @var bool[][] */ private $validatedObjects = []; /** * Stores which class constraint has been validated for which object. * * @var bool[] */ private $validatedConstraints = []; /** * Stores which objects have been initialized. * * @var bool[] */ private $initializedObjects; /** * @var \SplObjectStorage */ private $cachedObjectsRefs; /** * @param mixed $root The root value of the validated object graph * * @internal Called by {@link ExecutionContextFactory}. Should not be used in user code. */ public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, ?string $translationDomain = null) { $this->validator = $validator; $this->root = $root; $this->translator = $translator; $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); $this->cachedObjectsRefs = new \SplObjectStorage(); } /** * {@inheritdoc} */ public function setNode($value, ?object $object, ?MetadataInterface $metadata, string $propertyPath) { $this->value = $value; $this->object = $object; $this->metadata = $metadata; $this->propertyPath = $propertyPath; } /** * {@inheritdoc} */ public function setGroup(?string $group) { $this->group = $group; } /** * {@inheritdoc} */ public function setConstraint(Constraint $constraint) { $this->constraint = $constraint; } /** * {@inheritdoc} */ public function addViolation(string $message, array $parameters = []) { $this->violations->add(new ConstraintViolation( $this->translator->trans($message, $parameters, $this->translationDomain), $message, $parameters, $this->root, $this->propertyPath, $this->getValue(), null, null, $this->constraint )); } /** * {@inheritdoc} */ public function buildViolation(string $message, array $parameters = []): ConstraintViolationBuilderInterface { return new ConstraintViolationBuilder( $this->violations, $this->constraint, $message, $parameters, $this->root, $this->propertyPath, $this->getValue(), $this->translator, $this->translationDomain ); } /** * {@inheritdoc} */ public function getViolations(): ConstraintViolationListInterface { return $this->violations; } /** * {@inheritdoc} */ public function getValidator(): ValidatorInterface { return $this->validator; } /** * {@inheritdoc} */ public function getRoot() { return $this->root; } /** * {@inheritdoc} */ public function getValue() { if ($this->value instanceof LazyProperty) { return $this->value->getPropertyValue(); } return $this->value; } /** * {@inheritdoc} */ public function getObject() { return $this->object; } /** * {@inheritdoc} */ public function getMetadata(): ?MetadataInterface { return $this->metadata; } /** * {@inheritdoc} */ public function getGroup(): ?string { return $this->group; } public function getConstraint(): ?Constraint { return $this->constraint; } /** * {@inheritdoc} */ public function getClassName(): ?string { return $this->metadata instanceof MemberMetadata || $this->metadata instanceof ClassMetadataInterface ? $this->metadata->getClassName() : null; } /** * {@inheritdoc} */ public function getPropertyName(): ?string { return $this->metadata instanceof PropertyMetadataInterface ? $this->metadata->getPropertyName() : null; } /** * {@inheritdoc} */ public function getPropertyPath(string $subPath = ''): string { return PropertyPath::append($this->propertyPath, $subPath); } /** * {@inheritdoc} */ public function markGroupAsValidated(string $cacheKey, string $groupHash) { if (!isset($this->validatedObjects[$cacheKey])) { $this->validatedObjects[$cacheKey] = []; } $this->validatedObjects[$cacheKey][$groupHash] = true; } /** * {@inheritdoc} */ public function isGroupValidated(string $cacheKey, string $groupHash): bool { return isset($this->validatedObjects[$cacheKey][$groupHash]); } /** * {@inheritdoc} */ public function markConstraintAsValidated(string $cacheKey, string $constraintHash) { $this->validatedConstraints[$cacheKey.':'.$constraintHash] = true; } /** * {@inheritdoc} */ public function isConstraintValidated(string $cacheKey, string $constraintHash): bool { return isset($this->validatedConstraints[$cacheKey.':'.$constraintHash]); } /** * {@inheritdoc} */ public function markObjectAsInitialized(string $cacheKey) { $this->initializedObjects[$cacheKey] = true; } /** * {@inheritdoc} */ public function isObjectInitialized(string $cacheKey): bool { return isset($this->initializedObjects[$cacheKey]); } /** * @internal */ public function generateCacheKey(object $object): string { if (!isset($this->cachedObjectsRefs[$object])) { $this->cachedObjectsRefs[$object] = spl_object_hash($object); } return $this->cachedObjectsRefs[$object]; } public function __clone() { $this->violations = clone $this->violations; } } PK!4$t Avendor/symfony/validator/DataCollector/ValidatorDataCollector.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\DataCollector; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Validator\Validator\TraceableValidator; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Caster\ClassStub; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\Stub; /** * @author Maxime Steinhausser * * @final */ class ValidatorDataCollector extends DataCollector implements LateDataCollectorInterface { private $validator; public function __construct(TraceableValidator $validator) { $this->validator = $validator; $this->reset(); } /** * {@inheritdoc} */ public function collect(Request $request, Response $response, ?\Throwable $exception = null) { // Everything is collected once, on kernel terminate. } public function reset() { $this->data = [ 'calls' => $this->cloneVar([]), 'violations_count' => 0, ]; } /** * {@inheritdoc} */ public function lateCollect() { $collected = $this->validator->getCollectedData(); $this->data['calls'] = $this->cloneVar($collected); $this->data['violations_count'] = array_reduce($collected, function ($previous, $item) { return $previous + \count($item['violations']); }, 0); } public function getCalls(): Data { return $this->data['calls']; } public function getViolationsCount(): int { return $this->data['violations_count']; } /** * {@inheritdoc} */ public function getName(): string { return 'validator'; } protected function getCasters(): array { return parent::getCasters() + [ \Exception::class => function (\Exception $e, array $a, Stub $s) { foreach (["\0Exception\0previous", "\0Exception\0trace"] as $k) { if (isset($a[$k])) { unset($a[$k]); ++$s->cut; } } return $a; }, FormInterface::class => function (FormInterface $f, array $a) { return [ Caster::PREFIX_VIRTUAL.'name' => $f->getName(), Caster::PREFIX_VIRTUAL.'type_class' => new ClassStub(\get_class($f->getConfig()->getType()->getInnerType())), Caster::PREFIX_VIRTUAL.'data' => $f->getData(), ]; }, ]; } } PK!r r Pvendor/symfony/validator/DependencyInjection/AddAutoMappingConfigurationPass.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * Injects the automapping configuration as last argument of loaders tagged with the "validator.auto_mapper" tag. * * @author Kévin Dunglas */ class AddAutoMappingConfigurationPass implements CompilerPassInterface { private $validatorBuilderService; private $tag; public function __construct(string $validatorBuilderService = 'validator.builder', string $tag = 'validator.auto_mapper') { if (0 < \func_num_args()) { trigger_deprecation('symfony/validator', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); } $this->validatorBuilderService = $validatorBuilderService; $this->tag = $tag; } /** * {@inheritdoc} */ public function process(ContainerBuilder $container) { if (!$container->hasParameter('validator.auto_mapping') || !$container->hasDefinition($this->validatorBuilderService)) { return; } $config = $container->getParameter('validator.auto_mapping'); $globalNamespaces = []; $servicesToNamespaces = []; foreach ($config as $namespace => $value) { if ([] === $value['services']) { $globalNamespaces[] = $namespace; continue; } foreach ($value['services'] as $service) { $servicesToNamespaces[$service][] = $namespace; } } $validatorBuilder = $container->getDefinition($this->validatorBuilderService); foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) { $regexp = $this->getRegexp(array_merge($globalNamespaces, $servicesToNamespaces[$id] ?? [])); $validatorBuilder->addMethodCall('addLoader', [new Reference($id)]); $container->getDefinition($id)->setArgument('$classValidatorRegexp', $regexp); } $container->getParameterBag()->remove('validator.auto_mapping'); } /** * Builds a regexp to check if a class is auto-mapped. */ private function getRegexp(array $patterns): ?string { if (!$patterns) { return null; } $regexps = []; foreach ($patterns as $pattern) { // Escape namespace $regex = preg_quote(ltrim($pattern, '\\')); // Wildcards * and ** $regex = strtr($regex, ['\\*\\*' => '.*?', '\\*' => '[^\\\\]*?']); // If this class does not end by a slash, anchor the end if (!str_ends_with($regex, '\\')) { $regex .= '$'; } $regexps[] = '^'.$regex; } return sprintf('{%s}', implode('|', $regexps)); } } PK!ELvendor/symfony/validator/DependencyInjection/AddConstraintValidatorsPass.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * @author Johannes M. Schmitt * @author Robin Chalas */ class AddConstraintValidatorsPass implements CompilerPassInterface { private $validatorFactoryServiceId; private $constraintValidatorTag; public function __construct(string $validatorFactoryServiceId = 'validator.validator_factory', string $constraintValidatorTag = 'validator.constraint_validator') { if (0 < \func_num_args()) { trigger_deprecation('symfony/validator', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); } $this->validatorFactoryServiceId = $validatorFactoryServiceId; $this->constraintValidatorTag = $constraintValidatorTag; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->validatorFactoryServiceId)) { return; } $validators = []; foreach ($container->findTaggedServiceIds($this->constraintValidatorTag, true) as $id => $attributes) { $definition = $container->getDefinition($id); if (isset($attributes[0]['alias'])) { $validators[$attributes[0]['alias']] = new Reference($id); } $validators[$definition->getClass()] = new Reference($id); } $container ->getDefinition($this->validatorFactoryServiceId) ->replaceArgument(0, ServiceLocatorTagPass::register($container, $validators)) ; } } PK!.:55Mvendor/symfony/validator/DependencyInjection/AddValidatorInitializersPass.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * @author Fabien Potencier * @author Robin Chalas */ class AddValidatorInitializersPass implements CompilerPassInterface { private $builderService; private $initializerTag; public function __construct(string $builderService = 'validator.builder', string $initializerTag = 'validator.initializer') { if (0 < \func_num_args()) { trigger_deprecation('symfony/validator', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); } $this->builderService = $builderService; $this->initializerTag = $initializerTag; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->builderService)) { return; } $initializers = []; foreach ($container->findTaggedServiceIds($this->initializerTag, true) as $id => $attributes) { $initializers[] = new Reference($id); } $container->getDefinition($this->builderService)->addMethodCall('addObjectInitializers', [$initializers]); } } PK!y=vendor/symfony/validator/Exception/BadMethodCallException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * Base BadMethodCallException for the Validator component. * * @author Bernhard Schussek */ class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface { } PK!1"ccDvendor/symfony/validator/Exception/ConstraintDefinitionException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class ConstraintDefinitionException extends ValidatorException { } PK!8`9vendor/symfony/validator/Exception/ExceptionInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * Base ExceptionInterface for the Validator component. * * @author Bernhard Schussek */ interface ExceptionInterface extends \Throwable { } PK!M#^^?vendor/symfony/validator/Exception/GroupDefinitionException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class GroupDefinitionException extends ValidatorException { } PK!\?vendor/symfony/validator/Exception/InvalidArgumentException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * Base InvalidArgumentException for the Validator component. * * @author Bernhard Schussek */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } PK!vއYY>vendor/symfony/validator/Exception/InvalidOptionsException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class InvalidOptionsException extends ValidatorException { private $options; public function __construct(string $message, array $options) { parent::__construct($message); $this->options = $options; } public function getOptions() { return $this->options; } } PK!ݬXoo5vendor/symfony/validator/Exception/LogicException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class LogicException extends \LogicException implements ExceptionInterface { } PK!QVV7vendor/symfony/validator/Exception/MappingException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class MappingException extends ValidatorException { } PK!OfYY>vendor/symfony/validator/Exception/MissingOptionsException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class MissingOptionsException extends ValidatorException { private $options; public function __construct(string $message, array $options) { parent::__construct($message); $this->options = $options; } public function getOptions() { return $this->options; } } PK!x>vendor/symfony/validator/Exception/NoSuchMetadataException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * @author Bernhard Schussek */ class NoSuchMetadataException extends ValidatorException { } PK!|;vendor/symfony/validator/Exception/OutOfBoundsException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * Base OutOfBoundsException for the Validator component. * * @author Bernhard Schussek */ class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface { } PK!z7vendor/symfony/validator/Exception/RuntimeException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * Base RuntimeException for the Validator component. * * @author Bernhard Schussek */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } PK!;p"">vendor/symfony/validator/Exception/UnexpectedTypeException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class UnexpectedTypeException extends ValidatorException { public function __construct($value, string $expectedType) { parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, get_debug_type($value))); } } PK!x?vendor/symfony/validator/Exception/UnexpectedValueException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * @author Christian Flothmann */ class UnexpectedValueException extends UnexpectedTypeException { private $expectedType; public function __construct($value, string $expectedType) { parent::__construct($value, $expectedType); $this->expectedType = $expectedType; } public function getExpectedType(): string { return $this->expectedType; } } PK!ԟCvendor/symfony/validator/Exception/UnsupportedMetadataException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; /** * @author Bernhard Schussek */ class UnsupportedMetadataException extends InvalidArgumentException { } PK!"S@vendor/symfony/validator/Exception/ValidationFailedException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; use Symfony\Component\Validator\ConstraintViolationListInterface; /** * @author Jan Vernieuwe */ class ValidationFailedException extends RuntimeException { private $violations; private $value; public function __construct($value, ConstraintViolationListInterface $violations) { $this->violations = $violations; $this->value = $value; parent::__construct($violations); } public function getValue() { return $this->value; } public function getViolations(): ConstraintViolationListInterface { return $this->violations; } } PK! $VV9vendor/symfony/validator/Exception/ValidatorException.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Exception; class ValidatorException extends RuntimeException { } PK!LEvendor/symfony/validator/Mapping/Factory/BlackHoleMetadataFactory.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Factory; use Symfony\Component\Validator\Exception\LogicException; /** * Metadata factory that does not store metadata. * * This implementation is useful if you want to validate values against * constraints only and you don't need to add constraints to classes and * properties. * * @author Fabien Potencier */ class BlackHoleMetadataFactory implements MetadataFactoryInterface { /** * {@inheritdoc} */ public function getMetadataFor($value) { throw new LogicException('This class does not support metadata.'); } /** * {@inheritdoc} */ public function hasMetadataFor($value) { return false; } } PK!NckYGvendor/symfony/validator/Mapping/Factory/LazyLoadingMetadataFactory.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Factory; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; /** * Creates new {@link ClassMetadataInterface} instances. * * Whenever {@link getMetadataFor()} is called for the first time with a given * class name or object of that class, a new metadata instance is created and * returned. On subsequent requests for the same class, the same metadata * instance will be returned. * * You can optionally pass a {@link LoaderInterface} instance to the constructor. * Whenever a new metadata instance is created, it is passed to the loader, * which can configure the metadata based on configuration loaded from the * filesystem or a database. If you want to use multiple loaders, wrap them in a * {@link LoaderChain}. * * You can also optionally pass a {@link CacheInterface} instance to the * constructor. This cache will be used for persisting the generated metadata * between multiple PHP requests. * * @author Bernhard Schussek */ class LazyLoadingMetadataFactory implements MetadataFactoryInterface { protected $loader; protected $cache; /** * The loaded metadata, indexed by class name. * * @var ClassMetadata[] */ protected $loadedClasses = []; public function __construct(?LoaderInterface $loader = null, ?CacheItemPoolInterface $cache = null) { $this->loader = $loader; $this->cache = $cache; } /** * {@inheritdoc} * * If the method was called with the same class name (or an object of that * class) before, the same metadata instance is returned. * * If the factory was configured with a cache, this method will first look * for an existing metadata instance in the cache. If an existing instance * is found, it will be returned without further ado. * * Otherwise, a new metadata instance is created. If the factory was * configured with a loader, the metadata is passed to the * {@link LoaderInterface::loadClassMetadata()} method for further * configuration. At last, the new object is returned. */ public function getMetadataFor($value) { if (!\is_object($value) && !\is_string($value)) { throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: "%s".', get_debug_type($value))); } $class = ltrim(\is_object($value) ? \get_class($value) : $value, '\\'); if (isset($this->loadedClasses[$class])) { return $this->loadedClasses[$class]; } if (!class_exists($class) && !interface_exists($class, false)) { throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class)); } $cacheItem = null === $this->cache ? null : $this->cache->getItem($this->escapeClassName($class)); if ($cacheItem && $cacheItem->isHit()) { $metadata = $cacheItem->get(); // Include constraints from the parent class $this->mergeConstraints($metadata); return $this->loadedClasses[$class] = $metadata; } $metadata = new ClassMetadata($class); if (null !== $this->loader) { $this->loader->loadClassMetadata($metadata); } if (null !== $cacheItem) { $this->cache->save($cacheItem->set($metadata)); } // Include constraints from the parent class $this->mergeConstraints($metadata); return $this->loadedClasses[$class] = $metadata; } private function mergeConstraints(ClassMetadata $metadata) { if ($metadata->getReflectionClass()->isInterface()) { return; } // Include constraints from the parent class if ($parent = $metadata->getReflectionClass()->getParentClass()) { $metadata->mergeConstraints($this->getMetadataFor($parent->name)); } // Include constraints from all directly implemented interfaces foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { if ('Symfony\Component\Validator\GroupSequenceProviderInterface' === $interface->name) { continue; } if ($parent && \in_array($interface->getName(), $parent->getInterfaceNames(), true)) { continue; } $metadata->mergeConstraints($this->getMetadataFor($interface->name)); } } /** * {@inheritdoc} */ public function hasMetadataFor($value) { if (!\is_object($value) && !\is_string($value)) { return false; } $class = ltrim(\is_object($value) ? \get_class($value) : $value, '\\'); return class_exists($class) || interface_exists($class, false); } /** * Replaces backslashes by dots in a class name. */ private function escapeClassName(string $class): string { if (str_contains($class, '@')) { // anonymous class: replace all PSR6-reserved characters return str_replace(["\0", '\\', '/', '@', ':', '{', '}', '(', ')'], '.', $class); } return str_replace('\\', '.', $class); } } PK!ZZEvendor/symfony/validator/Mapping/Factory/MetadataFactoryInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Factory; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\MetadataInterface; /** * Returns {@link \Symfony\Component\Validator\Mapping\MetadataInterface} instances for values. * * @author Bernhard Schussek */ interface MetadataFactoryInterface { /** * Returns the metadata for the given value. * * @param mixed $value Some value * * @return MetadataInterface * * @throws NoSuchMetadataException If no metadata exists for the given value */ public function getMetadataFor($value); /** * Returns whether the class is able to return metadata for the given value. * * @param mixed $value Some value * * @return bool */ public function hasMetadataFor($value); } PK!TO--`vendor/symfony/validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsdnu[ PK!)ɀ :vendor/symfony/validator/Mapping/Loader/AbstractLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\MappingException; /** * Base loader for validation metadata. * * This loader supports the loading of constraints from Symfony's default * namespace (see {@link DEFAULT_NAMESPACE}) using the short class names of * those constraints. Constraints can also be loaded using their fully * qualified class names. At last, namespace aliases can be defined to load * constraints with the syntax "alias:ShortName". * * @author Bernhard Schussek */ abstract class AbstractLoader implements LoaderInterface { /** * The namespace to load constraints from by default. */ public const DEFAULT_NAMESPACE = '\\Symfony\\Component\\Validator\\Constraints\\'; protected $namespaces = []; /** * Adds a namespace alias. * * The namespace alias can be used to reference constraints from specific * namespaces in {@link newConstraint()}: * * $this->addNamespaceAlias('mynamespace', '\\Acme\\Package\\Constraints\\'); * * $constraint = $this->newConstraint('mynamespace:NotNull'); */ protected function addNamespaceAlias(string $alias, string $namespace) { $this->namespaces[$alias] = $namespace; } /** * Creates a new constraint instance for the given constraint name. * * @param string $name The constraint name. Either a constraint relative * to the default constraint namespace, or a fully * qualified class name. Alternatively, the constraint * may be preceded by a namespace alias and a colon. * The namespace alias must have been defined using * {@link addNamespaceAlias()}. * @param mixed $options The constraint options * * @return Constraint * * @throws MappingException If the namespace prefix is undefined */ protected function newConstraint(string $name, $options = null) { if (str_contains($name, '\\') && class_exists($name)) { $className = $name; } elseif (str_contains($name, ':')) { [$prefix, $className] = explode(':', $name, 2); if (!isset($this->namespaces[$prefix])) { throw new MappingException(sprintf('Undefined namespace prefix "%s".', $prefix)); } $className = $this->namespaces[$prefix].$className; } else { $className = self::DEFAULT_NAMESPACE.$name; } return new $className($options); } } PK!?hR<vendor/symfony/validator/Mapping/Loader/AnnotationLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Doctrine\Common\Annotations\Reader; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequenceProvider; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Loads validation metadata using a Doctrine annotation {@link Reader} or using PHP 8 attributes. * * @author Bernhard Schussek * @author Alexander M. Turek */ class AnnotationLoader implements LoaderInterface { protected $reader; public function __construct(?Reader $reader = null) { $this->reader = $reader; } /** * {@inheritdoc} */ public function loadClassMetadata(ClassMetadata $metadata) { $reflClass = $metadata->getReflectionClass(); $className = $reflClass->name; $success = false; foreach ($this->getAnnotations($reflClass) as $constraint) { if ($constraint instanceof GroupSequence) { $metadata->setGroupSequence($constraint->groups); } elseif ($constraint instanceof GroupSequenceProvider) { $metadata->setGroupSequenceProvider(true); } elseif ($constraint instanceof Constraint) { $metadata->addConstraint($constraint); } $success = true; } foreach ($reflClass->getProperties() as $property) { if ($property->getDeclaringClass()->name === $className) { foreach ($this->getAnnotations($property) as $constraint) { if ($constraint instanceof Constraint) { $metadata->addPropertyConstraint($property->name, $constraint); } $success = true; } } } foreach ($reflClass->getMethods() as $method) { if ($method->getDeclaringClass()->name === $className) { foreach ($this->getAnnotations($method) as $constraint) { if ($constraint instanceof Callback) { $constraint->callback = $method->getName(); $metadata->addConstraint($constraint); } elseif ($constraint instanceof Constraint) { if (preg_match('/^(get|is|has)(.+)$/i', $method->name, $matches)) { $metadata->addGetterMethodConstraint(lcfirst($matches[2]), $matches[0], $constraint); } else { throw new MappingException(sprintf('The constraint on "%s::%s()" cannot be added. Constraints can only be added on methods beginning with "get", "is" or "has".', $className, $method->name)); } } $success = true; } } } return $success; } /** * @param \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflection */ private function getAnnotations(object $reflection): iterable { $dedup = []; if (\PHP_VERSION_ID >= 80000) { foreach ($reflection->getAttributes(GroupSequence::class) as $attribute) { $dedup[] = $attribute->newInstance(); yield $attribute->newInstance(); } foreach ($reflection->getAttributes(GroupSequenceProvider::class) as $attribute) { $dedup[] = $attribute->newInstance(); yield $attribute->newInstance(); } foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { $dedup[] = $attribute->newInstance(); yield $attribute->newInstance(); } } if (!$this->reader) { return; } $annotations = []; if ($reflection instanceof \ReflectionClass) { $annotations = $this->reader->getClassAnnotations($reflection); } if ($reflection instanceof \ReflectionMethod) { $annotations = $this->reader->getMethodAnnotations($reflection); } if ($reflection instanceof \ReflectionProperty) { $annotations = $this->reader->getPropertyAnnotations($reflection); } foreach ($dedup as $annotation) { if ($annotation instanceof Constraint) { $annotation->groups; // trigger initialization of the "groups" property } } foreach ($annotations as $annotation) { if ($annotation instanceof Constraint) { $annotation->groups; // trigger initialization of the "groups" property } if (!\in_array($annotation, $dedup, false)) { yield $annotation; } } } } PK!P<vendor/symfony/validator/Mapping/Loader/AutoMappingTrait.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Mapping\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Utility methods to create auto mapping loaders. * * @author Kévin Dunglas */ trait AutoMappingTrait { private function isAutoMappingEnabledForClass(ClassMetadata $metadata, ?string $classValidatorRegexp = null): bool { // Check if AutoMapping constraint is set first if (AutoMappingStrategy::NONE !== $strategy = $metadata->getAutoMappingStrategy()) { return AutoMappingStrategy::ENABLED === $strategy; } // Fallback on the config return null !== $classValidatorRegexp && preg_match($classValidatorRegexp, $metadata->getClassName()); } } PK!jS$$6vendor/symfony/validator/Mapping/Loader/FileLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Exception\MappingException; /** * Base loader for loading validation metadata from a file. * * @author Bernhard Schussek * * @see YamlFileLoader * @see XmlFileLoader */ abstract class FileLoader extends AbstractLoader { protected $file; /** * Creates a new loader. * * @param string $file The mapping file to load * * @throws MappingException If the file does not exist or is not readable */ public function __construct(string $file) { if (!is_file($file)) { throw new MappingException(sprintf('The mapping file "%s" does not exist.', $file)); } if (!is_readable($file)) { throw new MappingException(sprintf('The mapping file "%s" is not readable.', $file)); } if (!stream_is_local($this->file)) { throw new MappingException(sprintf('The mapping file "%s" is not a local file.', $file)); } $this->file = $file; } } PK!PSF $$7vendor/symfony/validator/Mapping/Loader/FilesLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; /** * Base loader for loading validation metadata from a list of files. * * @author Bulat Shakirzyanov * @author Bernhard Schussek * * @see YamlFilesLoader * @see XmlFilesLoader */ abstract class FilesLoader extends LoaderChain { /** * Creates a new loader. * * @param array $paths An array of file paths */ public function __construct(array $paths) { parent::__construct($this->getFileLoaders($paths)); } /** * Returns an array of file loaders for the given file paths. * * @return LoaderInterface[] */ protected function getFileLoaders(array $paths) { $loaders = []; foreach ($paths as $path) { $loaders[] = $this->getFileLoaderInstance($path); } return $loaders; } /** * Creates a loader for the given file path. * * @return LoaderInterface */ abstract protected function getFileLoaderInstance(string $path); } PK!7vendor/symfony/validator/Mapping/Loader/LoaderChain.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Loads validation metadata from multiple {@link LoaderInterface} instances. * * Pass the loaders when constructing the chain. Once * {@link loadClassMetadata()} is called, that method will be called on all * loaders in the chain. * * @author Bernhard Schussek */ class LoaderChain implements LoaderInterface { protected $loaders; /** * @param LoaderInterface[] $loaders The metadata loaders to use * * @throws MappingException If any of the loaders has an invalid type */ public function __construct(array $loaders) { foreach ($loaders as $loader) { if (!$loader instanceof LoaderInterface) { throw new MappingException(sprintf('Class "%s" is expected to implement LoaderInterface.', get_debug_type($loader))); } } $this->loaders = $loaders; } /** * {@inheritdoc} */ public function loadClassMetadata(ClassMetadata $metadata) { $success = false; foreach ($this->loaders as $loader) { $success = $loader->loadClassMetadata($metadata) || $success; } return $success; } /** * @return LoaderInterface[] */ public function getLoaders() { return $this->loaders; } } PK!IE;vendor/symfony/validator/Mapping/Loader/LoaderInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Loads validation metadata into {@link ClassMetadata} instances. * * @author Bernhard Schussek */ interface LoaderInterface { /** * Loads validation metadata into a {@link ClassMetadata} instance. * * @return bool */ public function loadClassMetadata(ClassMetadata $metadata); } PK!80B  >vendor/symfony/validator/Mapping/Loader/PropertyInfoLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type as PropertyInfoType; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\Mapping\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Guesses and loads the appropriate constraints using PropertyInfo. * * @author Kévin Dunglas */ final class PropertyInfoLoader implements LoaderInterface { use AutoMappingTrait; private $listExtractor; private $typeExtractor; private $accessExtractor; private $classValidatorRegexp; public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor, PropertyAccessExtractorInterface $accessExtractor, ?string $classValidatorRegexp = null) { $this->listExtractor = $listExtractor; $this->typeExtractor = $typeExtractor; $this->accessExtractor = $accessExtractor; $this->classValidatorRegexp = $classValidatorRegexp; } /** * {@inheritdoc} */ public function loadClassMetadata(ClassMetadata $metadata): bool { $className = $metadata->getClassName(); if (!$properties = $this->listExtractor->getProperties($className)) { return false; } $loaded = false; $enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp); foreach ($properties as $property) { if (false === $this->accessExtractor->isWritable($className, $property)) { continue; } if (!property_exists($className, $property)) { continue; } $types = $this->typeExtractor->getTypes($className, $property); if (null === $types) { continue; } $enabledForProperty = $enabledForClass; $hasTypeConstraint = false; $hasNotNullConstraint = false; $hasNotBlankConstraint = false; $allConstraint = null; foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) { // Enabling or disabling auto-mapping explicitly always takes precedence if (AutoMappingStrategy::DISABLED === $propertyMetadata->getAutoMappingStrategy()) { continue 2; } if (AutoMappingStrategy::ENABLED === $propertyMetadata->getAutoMappingStrategy()) { $enabledForProperty = true; } foreach ($propertyMetadata->getConstraints() as $constraint) { if ($constraint instanceof Type) { $hasTypeConstraint = true; } elseif ($constraint instanceof NotNull) { $hasNotNullConstraint = true; } elseif ($constraint instanceof NotBlank) { $hasNotBlankConstraint = true; } elseif ($constraint instanceof All) { $allConstraint = $constraint; } } } if (!$enabledForProperty) { continue; } $loaded = true; $builtinTypes = []; $nullable = false; $scalar = true; foreach ($types as $type) { $builtinTypes[] = $type->getBuiltinType(); if ($scalar && !\in_array($type->getBuiltinType(), [PropertyInfoType::BUILTIN_TYPE_INT, PropertyInfoType::BUILTIN_TYPE_FLOAT, PropertyInfoType::BUILTIN_TYPE_STRING, PropertyInfoType::BUILTIN_TYPE_BOOL], true)) { $scalar = false; } if (!$nullable && $type->isNullable()) { $nullable = true; } } if (!$hasTypeConstraint) { if (1 === \count($builtinTypes)) { if ($types[0]->isCollection() && \count($collectionValueType = $types[0]->getCollectionValueTypes()) > 0) { [$collectionValueType] = $collectionValueType; $this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata); } $metadata->addPropertyConstraint($property, $this->getTypeConstraint($builtinTypes[0], $types[0])); } elseif ($scalar) { $metadata->addPropertyConstraint($property, new Type(['type' => 'scalar'])); } } if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) { $metadata->addPropertyConstraint($property, new NotNull()); } } return $loaded; } private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type { if (PropertyInfoType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $className = $type->getClassName()) { return new Type(['type' => $className]); } return new Type(['type' => $builtinType]); } private function handleAllConstraint(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata) { $containsTypeConstraint = false; $containsNotNullConstraint = false; if (null !== $allConstraint) { foreach ($allConstraint->constraints as $constraint) { if ($constraint instanceof Type) { $containsTypeConstraint = true; } elseif ($constraint instanceof NotNull) { $containsNotNullConstraint = true; } } } $constraints = []; if (!$containsNotNullConstraint && !$propertyInfoType->isNullable()) { $constraints[] = new NotNull(); } if (!$containsTypeConstraint) { $constraints[] = $this->getTypeConstraint($propertyInfoType->getBuiltinType(), $propertyInfoType); } if (null === $allConstraint) { $metadata->addPropertyConstraint($property, new All(['constraints' => $constraints])); } else { $allConstraint->constraints = array_merge($allConstraint->constraints, $constraints); } } } PK!&2>vendor/symfony/validator/Mapping/Loader/StaticMethodLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Loads validation metadata by calling a static method on the loaded class. * * @author Bernhard Schussek */ class StaticMethodLoader implements LoaderInterface { protected $methodName; /** * Creates a new loader. * * @param string $methodName The name of the static method to call */ public function __construct(string $methodName = 'loadValidatorMetadata') { $this->methodName = $methodName; } /** * {@inheritdoc} */ public function loadClassMetadata(ClassMetadata $metadata) { /** @var \ReflectionClass $reflClass */ $reflClass = $metadata->getReflectionClass(); if (!$reflClass->isInterface() && $reflClass->hasMethod($this->methodName)) { $reflMethod = $reflClass->getMethod($this->methodName); if ($reflMethod->isAbstract()) { return false; } if (!$reflMethod->isStatic()) { throw new MappingException(sprintf('The method "%s::%s()" should be static.', $reflClass->name, $this->methodName)); } if ($reflMethod->getDeclaringClass()->name != $reflClass->name) { return false; } $reflMethod->invoke(null, $metadata); return true; } return false; } } PK!O!779vendor/symfony/validator/Mapping/Loader/XmlFileLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Loads validation metadata from an XML file. * * @author Bernhard Schussek */ class XmlFileLoader extends FileLoader { /** * The XML nodes of the mapping file. * * @var \SimpleXMLElement[]|null */ protected $classes; /** * {@inheritdoc} */ public function loadClassMetadata(ClassMetadata $metadata) { if (null === $this->classes) { $this->loadClassesFromXml(); } if (isset($this->classes[$metadata->getClassName()])) { $classDescription = $this->classes[$metadata->getClassName()]; $this->loadClassMetadataFromXml($metadata, $classDescription); return true; } return false; } /** * Return the names of the classes mapped in this file. * * @return string[] */ public function getMappedClasses() { if (null === $this->classes) { $this->loadClassesFromXml(); } return array_keys($this->classes); } /** * Parses a collection of "constraint" XML nodes. * * @param \SimpleXMLElement $nodes The XML nodes * * @return Constraint[] */ protected function parseConstraints(\SimpleXMLElement $nodes) { $constraints = []; foreach ($nodes as $node) { if (\count($node) > 0) { if (\count($node->value) > 0) { $options = $this->parseValues($node->value); } elseif (\count($node->constraint) > 0) { $options = $this->parseConstraints($node->constraint); } elseif (\count($node->option) > 0) { $options = $this->parseOptions($node->option); } else { $options = []; } } elseif ('' !== (string) $node) { $options = XmlUtils::phpize(trim($node)); } else { $options = null; } $constraints[] = $this->newConstraint((string) $node['name'], $options); } return $constraints; } /** * Parses a collection of "value" XML nodes. * * @param \SimpleXMLElement $nodes The XML nodes * * @return array */ protected function parseValues(\SimpleXMLElement $nodes) { $values = []; foreach ($nodes as $node) { if (\count($node) > 0) { if (\count($node->value) > 0) { $value = $this->parseValues($node->value); } elseif (\count($node->constraint) > 0) { $value = $this->parseConstraints($node->constraint); } else { $value = []; } } else { $value = trim($node); } if (isset($node['key'])) { $values[(string) $node['key']] = $value; } else { $values[] = $value; } } return $values; } /** * Parses a collection of "option" XML nodes. * * @param \SimpleXMLElement $nodes The XML nodes * * @return array */ protected function parseOptions(\SimpleXMLElement $nodes) { $options = []; foreach ($nodes as $node) { if (\count($node) > 0) { if (\count($node->value) > 0) { $value = $this->parseValues($node->value); } elseif (\count($node->constraint) > 0) { $value = $this->parseConstraints($node->constraint); } else { $value = []; } } else { $value = XmlUtils::phpize($node); if (\is_string($value)) { $value = trim($value); } } $options[(string) $node['name']] = $value; } return $options; } /** * Loads the XML class descriptions from the given file. * * @return \SimpleXMLElement * * @throws MappingException If the file could not be loaded */ protected function parseFile(string $path) { try { $dom = XmlUtils::loadFile($path, __DIR__.'/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd'); } catch (\Exception $e) { throw new MappingException($e->getMessage(), $e->getCode(), $e); } return simplexml_import_dom($dom); } private function loadClassesFromXml() { // This method may throw an exception. Do not modify the class' // state before it completes $xml = $this->parseFile($this->file); $this->classes = []; foreach ($xml->namespace as $namespace) { $this->addNamespaceAlias((string) $namespace['prefix'], trim((string) $namespace)); } foreach ($xml->class as $class) { $this->classes[(string) $class['name']] = $class; } } private function loadClassMetadataFromXml(ClassMetadata $metadata, \SimpleXMLElement $classDescription) { if (\count($classDescription->{'group-sequence-provider'}) > 0) { $metadata->setGroupSequenceProvider(true); } foreach ($classDescription->{'group-sequence'} as $groupSequence) { if (\count($groupSequence->value) > 0) { $metadata->setGroupSequence($this->parseValues($groupSequence[0]->value)); } } foreach ($this->parseConstraints($classDescription->constraint) as $constraint) { $metadata->addConstraint($constraint); } foreach ($classDescription->property as $property) { foreach ($this->parseConstraints($property->constraint) as $constraint) { $metadata->addPropertyConstraint((string) $property['name'], $constraint); } } foreach ($classDescription->getter as $getter) { foreach ($this->parseConstraints($getter->constraint) as $constraint) { $metadata->addGetterConstraint((string) $getter['property'], $constraint); } } } } PK!:vendor/symfony/validator/Mapping/Loader/XmlFilesLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; /** * Loads validation metadata from a list of XML files. * * @author Bulat Shakirzyanov * @author Bernhard Schussek * * @see FilesLoader */ class XmlFilesLoader extends FilesLoader { /** * {@inheritdoc} */ public function getFileLoaderInstance(string $file) { return new XmlFileLoader($file); } } PK!w :vendor/symfony/validator/Mapping/Loader/YamlFileLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Yaml; /** * Loads validation metadata from a YAML file. * * @author Bernhard Schussek */ class YamlFileLoader extends FileLoader { /** * An array of YAML class descriptions. * * @var array */ protected $classes = null; /** * Caches the used YAML parser. * * @var YamlParser */ private $yamlParser; /** * {@inheritdoc} */ public function loadClassMetadata(ClassMetadata $metadata) { if (null === $this->classes) { $this->loadClassesFromYaml(); } if (isset($this->classes[$metadata->getClassName()])) { $classDescription = $this->classes[$metadata->getClassName()]; $this->loadClassMetadataFromYaml($metadata, $classDescription); return true; } return false; } /** * Return the names of the classes mapped in this file. * * @return string[] */ public function getMappedClasses() { if (null === $this->classes) { $this->loadClassesFromYaml(); } return array_keys($this->classes); } /** * Parses a collection of YAML nodes. * * @param array $nodes The YAML nodes * * @return array */ protected function parseNodes(array $nodes) { $values = []; foreach ($nodes as $name => $childNodes) { if (is_numeric($name) && \is_array($childNodes) && 1 === \count($childNodes)) { $options = current($childNodes); if (\is_array($options)) { $options = $this->parseNodes($options); } $values[] = $this->newConstraint(key($childNodes), $options); } else { if (\is_array($childNodes)) { $childNodes = $this->parseNodes($childNodes); } $values[$name] = $childNodes; } } return $values; } /** * Loads the YAML class descriptions from the given file. * * @throws \InvalidArgumentException If the file could not be loaded or did * not contain a YAML array */ private function parseFile(string $path): array { try { $classes = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: ', $path).$e->getMessage(), 0, $e); } // empty file if (null === $classes) { return []; } // not an array if (!\is_array($classes)) { throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $this->file)); } return $classes; } private function loadClassesFromYaml() { if (null === $this->yamlParser) { $this->yamlParser = new YamlParser(); } $this->classes = $this->parseFile($this->file); if (isset($this->classes['namespaces'])) { foreach ($this->classes['namespaces'] as $alias => $namespace) { $this->addNamespaceAlias($alias, $namespace); } unset($this->classes['namespaces']); } } private function loadClassMetadataFromYaml(ClassMetadata $metadata, array $classDescription) { if (isset($classDescription['group_sequence_provider'])) { $metadata->setGroupSequenceProvider( (bool) $classDescription['group_sequence_provider'] ); } if (isset($classDescription['group_sequence'])) { $metadata->setGroupSequence($classDescription['group_sequence']); } if (isset($classDescription['constraints']) && \is_array($classDescription['constraints'])) { foreach ($this->parseNodes($classDescription['constraints']) as $constraint) { $metadata->addConstraint($constraint); } } if (isset($classDescription['properties']) && \is_array($classDescription['properties'])) { foreach ($classDescription['properties'] as $property => $constraints) { if (null !== $constraints) { foreach ($this->parseNodes($constraints) as $constraint) { $metadata->addPropertyConstraint($property, $constraint); } } } } if (isset($classDescription['getters']) && \is_array($classDescription['getters'])) { foreach ($classDescription['getters'] as $getter => $constraints) { if (null !== $constraints) { foreach ($this->parseNodes($constraints) as $constraint) { $metadata->addGetterConstraint($getter, $constraint); } } } } } } PK!`f;vendor/symfony/validator/Mapping/Loader/YamlFilesLoader.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping\Loader; /** * Loads validation metadata from a list of YAML files. * * @author Bulat Shakirzyanov * @author Bernhard Schussek * * @see FilesLoader */ class YamlFilesLoader extends FilesLoader { /** * {@inheritdoc} */ public function getFileLoaderInstance(string $file) { return new YamlFileLoader($file); } } PK!.> %%8vendor/symfony/validator/Mapping/AutoMappingStrategy.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; /** * Specifies how the auto-mapping feature should behave. * * @author Maxime Steinhausser */ final class AutoMappingStrategy { /** * Nothing explicitly set, rely on auto-mapping configured regex. */ public const NONE = 0; /** * Explicitly enabled. */ public const ENABLED = 1; /** * Explicitly disabled. */ public const DISABLED = 2; /** * Not instantiable. */ private function __construct() { } } PK!Ҿݚ6vendor/symfony/validator/Mapping/CascadingStrategy.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; /** * Specifies whether an object should be cascaded. * * Cascading is relevant for any node type but class nodes. If such a node * contains an object of value, and if cascading is enabled, then the node * traverser will try to find class metadata for that object and validate the * object against that metadata. * * If no metadata is found for a cascaded object, and if that object implements * {@link \Traversable}, the node traverser will iterate over the object and * cascade each object or collection contained within, unless iteration is * prohibited by the specified {@link TraversalStrategy}. * * Although the constants currently represent a boolean switch, they are * implemented as bit mask in order to allow future extensions. * * @author Bernhard Schussek * * @see TraversalStrategy */ class CascadingStrategy { /** * Specifies that a node should not be cascaded. */ public const NONE = 1; /** * Specifies that a node should be cascaded. */ public const CASCADE = 2; /** * Not instantiable. */ private function __construct() { } } PK!t" " ;vendor/symfony/validator/Mapping/ClassMetadataInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\GroupSequenceProviderInterface; /** * Stores all metadata needed for validating objects of specific class. * * Most importantly, the metadata stores the constraints against which an object * and its properties should be validated. * * Additionally, the metadata stores whether the "Default" group is overridden * by a group sequence for that class and whether instances of that class * should be traversed or not. * * @author Bernhard Schussek * * @see MetadataInterface * @see GroupSequence * @see GroupSequenceProviderInterface * @see TraversalStrategy */ interface ClassMetadataInterface extends MetadataInterface { /** * Returns the names of all constrained properties. * * @return string[] */ public function getConstrainedProperties(); /** * Returns whether the "Default" group is overridden by a group sequence. * * If it is, you can access the group sequence with {@link getGroupSequence()}. * * @return bool */ public function hasGroupSequence(); /** * Returns the group sequence that overrides the "Default" group for this * class. * * @return GroupSequence|null */ public function getGroupSequence(); /** * Returns whether the "Default" group is overridden by a dynamic group * sequence obtained by the validated objects. * * If this method returns true, the class must implement * {@link GroupSequenceProviderInterface}. * This interface will be used to obtain the group sequence when an object * of this class is validated. * * @return bool */ public function isGroupSequenceProvider(); /** * Check if there's any metadata attached to the given named property. * * @param string $property The property name * * @return bool */ public function hasPropertyMetadata(string $property); /** * Returns all metadata instances for the given named property. * * If your implementation does not support properties, throw an exception * in this method (for example a BadMethodCallException). * * @param string $property The property name * * @return PropertyMetadataInterface[] */ public function getPropertyMetadata(string $property); /** * Returns the name of the backing PHP class. * * @return string */ public function getClassName(); } PK!n4D@@2vendor/symfony/validator/Mapping/ClassMetadata.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Cascade; use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\GroupDefinitionException; /** * Default implementation of {@link ClassMetadataInterface}. * * This class supports serialization and cloning. * * @author Bernhard Schussek * @author Fabien Potencier */ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface { /** * @var string * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getClassName()} instead. */ public $name; /** * @var string * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getDefaultGroup()} instead. */ public $defaultGroup; /** * @var MemberMetadata[][] * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getPropertyMetadata()} instead. */ public $members = []; /** * @var PropertyMetadata[] * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getPropertyMetadata()} instead. */ public $properties = []; /** * @var GetterMetadata[] * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getPropertyMetadata()} instead. */ public $getters = []; /** * @var array * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getGroupSequence()} instead. */ public $groupSequence = []; /** * @var bool * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link isGroupSequenceProvider()} instead. */ public $groupSequenceProvider = false; /** * The strategy for traversing traversable objects. * * By default, only instances of {@link \Traversable} are traversed. * * @var int * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getTraversalStrategy()} instead. */ public $traversalStrategy = TraversalStrategy::IMPLICIT; /** * @var \ReflectionClass */ private $reflClass; public function __construct(string $class) { $this->name = $class; // class name without namespace if (false !== $nsSep = strrpos($class, '\\')) { $this->defaultGroup = substr($class, $nsSep + 1); } else { $this->defaultGroup = $class; } } /** * {@inheritdoc} */ public function __sleep() { $parentProperties = parent::__sleep(); // Don't store the cascading strategy. Classes never cascade. unset($parentProperties[array_search('cascadingStrategy', $parentProperties)]); return array_merge($parentProperties, [ 'getters', 'groupSequence', 'groupSequenceProvider', 'members', 'name', 'properties', 'defaultGroup', ]); } /** * {@inheritdoc} */ public function getClassName() { return $this->name; } /** * Returns the name of the default group for this class. * * For each class, the group "Default" is an alias for the group * "", where is the non-namespaced name of the * class. All constraints implicitly or explicitly assigned to group * "Default" belong to both of these groups, unless the class defines * a group sequence. * * If a class defines a group sequence, validating the class in "Default" * will validate the group sequence. The constraints assigned to "Default" * can still be validated by validating the class in "". * * @return string */ public function getDefaultGroup() { return $this->defaultGroup; } /** * {@inheritdoc} * * If the constraint {@link Cascade} is added, the cascading strategy will be * changed to {@link CascadingStrategy::CASCADE}. * * If the constraint {@link Traverse} is added, the traversal strategy will be * changed. Depending on the $traverse property of that constraint, * the traversal strategy will be set to one of the following: * * - {@link TraversalStrategy::IMPLICIT} by default * - {@link TraversalStrategy::NONE} if $traverse is disabled * - {@link TraversalStrategy::TRAVERSE} if $traverse is enabled */ public function addConstraint(Constraint $constraint) { $this->checkConstraint($constraint); if ($constraint instanceof Traverse) { if ($constraint->traverse) { // If traverse is true, traversal should be explicitly enabled $this->traversalStrategy = TraversalStrategy::TRAVERSE; } else { // If traverse is false, traversal should be explicitly disabled $this->traversalStrategy = TraversalStrategy::NONE; } // The constraint is not added return $this; } if ($constraint instanceof Cascade) { if (\PHP_VERSION_ID < 70400) { throw new ConstraintDefinitionException(sprintf('The constraint "%s" requires PHP 7.4.', Cascade::class)); } $this->cascadingStrategy = CascadingStrategy::CASCADE; foreach ($this->getReflectionClass()->getProperties() as $property) { if ($this->canCascade($property->getType())) { $this->addPropertyConstraint($property->getName(), new Valid()); } } // The constraint is not added return $this; } $constraint->addImplicitGroupName($this->getDefaultGroup()); parent::addConstraint($constraint); return $this; } /** * Adds a constraint to the given property. * * @return $this */ public function addPropertyConstraint(string $property, Constraint $constraint) { if (!isset($this->properties[$property])) { $this->properties[$property] = new PropertyMetadata($this->getClassName(), $property); $this->addPropertyMetadata($this->properties[$property]); } $constraint->addImplicitGroupName($this->getDefaultGroup()); $this->properties[$property]->addConstraint($constraint); return $this; } /** * @param Constraint[] $constraints * * @return $this */ public function addPropertyConstraints(string $property, array $constraints) { foreach ($constraints as $constraint) { $this->addPropertyConstraint($property, $constraint); } return $this; } /** * Adds a constraint to the getter of the given property. * * The name of the getter is assumed to be the name of the property with an * uppercased first letter and the prefix "get", "is" or "has". * * @return $this */ public function addGetterConstraint(string $property, Constraint $constraint) { if (!isset($this->getters[$property])) { $this->getters[$property] = new GetterMetadata($this->getClassName(), $property); $this->addPropertyMetadata($this->getters[$property]); } $constraint->addImplicitGroupName($this->getDefaultGroup()); $this->getters[$property]->addConstraint($constraint); return $this; } /** * Adds a constraint to the getter of the given property. * * @return $this */ public function addGetterMethodConstraint(string $property, string $method, Constraint $constraint) { if (!isset($this->getters[$property])) { $this->getters[$property] = new GetterMetadata($this->getClassName(), $property, $method); $this->addPropertyMetadata($this->getters[$property]); } $constraint->addImplicitGroupName($this->getDefaultGroup()); $this->getters[$property]->addConstraint($constraint); return $this; } /** * @param Constraint[] $constraints * * @return $this */ public function addGetterConstraints(string $property, array $constraints) { foreach ($constraints as $constraint) { $this->addGetterConstraint($property, $constraint); } return $this; } /** * @param Constraint[] $constraints * * @return $this */ public function addGetterMethodConstraints(string $property, string $method, array $constraints) { foreach ($constraints as $constraint) { $this->addGetterMethodConstraint($property, $method, $constraint); } return $this; } /** * Merges the constraints of the given metadata into this object. */ public function mergeConstraints(self $source) { if ($source->isGroupSequenceProvider()) { $this->setGroupSequenceProvider(true); } foreach ($source->getConstraints() as $constraint) { $this->addConstraint(clone $constraint); } foreach ($source->getConstrainedProperties() as $property) { foreach ($source->getPropertyMetadata($property) as $member) { $member = clone $member; foreach ($member->getConstraints() as $constraint) { if (\in_array($constraint::DEFAULT_GROUP, $constraint->groups, true)) { $member->constraintsByGroup[$this->getDefaultGroup()][] = $constraint; } $constraint->addImplicitGroupName($this->getDefaultGroup()); } if ($member instanceof MemberMetadata && !$member->isPrivate($this->name)) { $property = $member->getPropertyName(); $this->members[$property][] = $member; if ($member instanceof PropertyMetadata && !isset($this->properties[$property])) { $this->properties[$property] = $member; } elseif ($member instanceof GetterMetadata && !isset($this->getters[$property])) { $this->getters[$property] = $member; } } else { $this->addPropertyMetadata($member); } } } } /** * {@inheritdoc} */ public function hasPropertyMetadata(string $property) { return \array_key_exists($property, $this->members); } /** * {@inheritdoc} */ public function getPropertyMetadata(string $property) { return $this->members[$property] ?? []; } /** * {@inheritdoc} */ public function getConstrainedProperties() { return array_keys($this->members); } /** * Sets the default group sequence for this class. * * @param string[]|GroupSequence $groupSequence An array of group names * * @return $this * * @throws GroupDefinitionException */ public function setGroupSequence($groupSequence) { if ($this->isGroupSequenceProvider()) { throw new GroupDefinitionException('Defining a static group sequence is not allowed with a group sequence provider.'); } if (\is_array($groupSequence)) { $groupSequence = new GroupSequence($groupSequence); } if (\in_array(Constraint::DEFAULT_GROUP, $groupSequence->groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is not allowed in group sequences.', Constraint::DEFAULT_GROUP)); } if (!\in_array($this->getDefaultGroup(), $groupSequence->groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is missing in the group sequence.', $this->getDefaultGroup())); } $this->groupSequence = $groupSequence; return $this; } /** * {@inheritdoc} */ public function hasGroupSequence() { return $this->groupSequence && \count($this->groupSequence->groups) > 0; } /** * {@inheritdoc} */ public function getGroupSequence() { return $this->groupSequence; } /** * Returns a ReflectionClass instance for this class. * * @return \ReflectionClass */ public function getReflectionClass() { if (!$this->reflClass) { $this->reflClass = new \ReflectionClass($this->getClassName()); } return $this->reflClass; } /** * Sets whether a group sequence provider should be used. * * @throws GroupDefinitionException */ public function setGroupSequenceProvider(bool $active) { if ($this->hasGroupSequence()) { throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence.'); } if (!$this->getReflectionClass()->implementsInterface('Symfony\Component\Validator\GroupSequenceProviderInterface')) { throw new GroupDefinitionException(sprintf('Class "%s" must implement GroupSequenceProviderInterface.', $this->name)); } $this->groupSequenceProvider = $active; } /** * {@inheritdoc} */ public function isGroupSequenceProvider() { return $this->groupSequenceProvider; } /** * {@inheritdoc} */ public function getCascadingStrategy() { return $this->cascadingStrategy; } private function addPropertyMetadata(PropertyMetadataInterface $metadata) { $property = $metadata->getPropertyName(); $this->members[$property][] = $metadata; } private function checkConstraint(Constraint $constraint) { if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets(), true)) { throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', get_debug_type($constraint))); } if ($constraint instanceof Composite) { foreach ($constraint->getNestedConstraints() as $nestedConstraint) { $this->checkConstraint($nestedConstraint); } } } private function canCascade(?\ReflectionType $type = null): bool { if (null === $type) { return false; } if ($type instanceof \ReflectionIntersectionType) { foreach ($type->getTypes() as $nestedType) { if ($this->canCascade($nestedType)) { return true; } } return false; } if ($type instanceof \ReflectionUnionType) { foreach ($type->getTypes() as $nestedType) { if (!$this->canCascade($nestedType)) { return false; } } return true; } return $type instanceof \ReflectionNamedType && (\in_array($type->getName(), ['array', 'null'], true) || class_exists($type->getName())); } } PK!(,,4vendor/symfony/validator/Mapping/GenericMetadata.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Cascade; use Symfony\Component\Validator\Constraints\DisableAutoMapping; use Symfony\Component\Validator\Constraints\EnableAutoMapping; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * A generic container of {@link Constraint} objects. * * This class supports serialization and cloning. * * @author Bernhard Schussek */ class GenericMetadata implements MetadataInterface { /** * @var Constraint[] * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getConstraints()} and {@link findConstraints()} instead. */ public $constraints = []; /** * @var array * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link findConstraints()} instead. */ public $constraintsByGroup = []; /** * The strategy for cascading objects. * * By default, objects are not cascaded. * * @var int * * @see CascadingStrategy * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getCascadingStrategy()} instead. */ public $cascadingStrategy = CascadingStrategy::NONE; /** * The strategy for traversing traversable objects. * * By default, traversable objects are not traversed. * * @var int * * @see TraversalStrategy * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getTraversalStrategy()} instead. */ public $traversalStrategy = TraversalStrategy::NONE; /** * Is auto-mapping enabled? * * @var int * * @see AutoMappingStrategy * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getAutoMappingStrategy()} instead. */ public $autoMappingStrategy = AutoMappingStrategy::NONE; /** * Returns the names of the properties that should be serialized. * * @return string[] */ public function __sleep() { return [ 'constraints', 'constraintsByGroup', 'cascadingStrategy', 'traversalStrategy', 'autoMappingStrategy', ]; } /** * Clones this object. */ public function __clone() { $constraints = $this->constraints; $this->constraints = []; $this->constraintsByGroup = []; foreach ($constraints as $constraint) { $this->addConstraint(clone $constraint); } } /** * Adds a constraint. * * If the constraint {@link Valid} is added, the cascading strategy will be * changed to {@link CascadingStrategy::CASCADE}. Depending on the * $traverse property of that constraint, the traversal strategy * will be set to one of the following: * * - {@link TraversalStrategy::IMPLICIT} if $traverse is enabled * - {@link TraversalStrategy::NONE} if $traverse is disabled * * @return $this * * @throws ConstraintDefinitionException When trying to add the {@link Cascade} * or {@link Traverse} constraint */ public function addConstraint(Constraint $constraint) { if ($constraint instanceof Traverse || $constraint instanceof Cascade) { throw new ConstraintDefinitionException(sprintf('The constraint "%s" can only be put on classes. Please use "Symfony\Component\Validator\Constraints\Valid" instead.', get_debug_type($constraint))); } if ($constraint instanceof Valid && null === $constraint->groups) { $this->cascadingStrategy = CascadingStrategy::CASCADE; if ($constraint->traverse) { $this->traversalStrategy = TraversalStrategy::IMPLICIT; } else { $this->traversalStrategy = TraversalStrategy::NONE; } return $this; } if ($constraint instanceof DisableAutoMapping || $constraint instanceof EnableAutoMapping) { $this->autoMappingStrategy = $constraint instanceof EnableAutoMapping ? AutoMappingStrategy::ENABLED : AutoMappingStrategy::DISABLED; // The constraint is not added return $this; } $this->constraints[] = $constraint; foreach ($constraint->groups as $group) { $this->constraintsByGroup[$group][] = $constraint; } return $this; } /** * Adds an list of constraints. * * @param Constraint[] $constraints The constraints to add * * @return $this */ public function addConstraints(array $constraints) { foreach ($constraints as $constraint) { $this->addConstraint($constraint); } return $this; } /** * {@inheritdoc} */ public function getConstraints() { return $this->constraints; } /** * Returns whether this element has any constraints. * * @return bool */ public function hasConstraints() { return \count($this->constraints) > 0; } /** * {@inheritdoc} * * Aware of the global group (* group). */ public function findConstraints(string $group) { return $this->constraintsByGroup[$group] ?? []; } /** * {@inheritdoc} */ public function getCascadingStrategy() { return $this->cascadingStrategy; } /** * {@inheritdoc} */ public function getTraversalStrategy() { return $this->traversalStrategy; } /** * @see AutoMappingStrategy */ public function getAutoMappingStrategy(): int { return $this->autoMappingStrategy; } } PK!x6 6 3vendor/symfony/validator/Mapping/GetterMetadata.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Exception\ValidatorException; /** * Stores all metadata needed for validating a class property via its getter * method. * * A property getter is any method that is equal to the property's name, * prefixed with "get", "is" or "has". That method will be used to access the * property's value. * * The getter will be invoked by reflection, so the access of private and * protected getters is supported. * * This class supports serialization and cloning. * * @author Bernhard Schussek * * @see PropertyMetadataInterface */ class GetterMetadata extends MemberMetadata { /** * @param string $class The class the getter is defined on * @param string $property The property which the getter returns * @param string|null $method The method that is called to retrieve the value being validated (null for auto-detection) * * @throws ValidatorException */ public function __construct(string $class, string $property, ?string $method = null) { if (null === $method) { $getMethod = 'get'.ucfirst($property); $isMethod = 'is'.ucfirst($property); $hasMethod = 'has'.ucfirst($property); if (method_exists($class, $getMethod)) { $method = $getMethod; } elseif (method_exists($class, $isMethod)) { $method = $isMethod; } elseif (method_exists($class, $hasMethod)) { $method = $hasMethod; } else { throw new ValidatorException(sprintf('Neither of these methods exist in class "%s": "%s", "%s", "%s".', $class, $getMethod, $isMethod, $hasMethod)); } } elseif (!method_exists($class, $method)) { throw new ValidatorException(sprintf('The "%s()" method does not exist in class "%s".', $method, $class)); } parent::__construct($class, $method, $property); } /** * {@inheritdoc} */ public function getPropertyValue($object) { return $this->newReflectionMember($object)->invoke($object); } /** * {@inheritdoc} */ protected function newReflectionMember($objectOrClassName) { return new \ReflectionMethod($objectOrClassName, $this->getName()); } } PK!CwW3vendor/symfony/validator/Mapping/MemberMetadata.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Stores all metadata needed for validating a class property. * * The method of accessing the property's value must be specified by subclasses * by implementing the {@link newReflectionMember()} method. * * This class supports serialization and cloning. * * @author Bernhard Schussek * * @see PropertyMetadataInterface */ abstract class MemberMetadata extends GenericMetadata implements PropertyMetadataInterface { /** * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getClassName()} instead. */ public $class; /** * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getName()} instead. */ public $name; /** * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use * {@link getPropertyName()} instead. */ public $property; /** * @var \ReflectionMethod[]|\ReflectionProperty[] */ private $reflMember = []; /** * @param string $class The name of the class this member is defined on * @param string $name The name of the member * @param string $property The property the member belongs to */ public function __construct(string $class, string $name, string $property) { $this->class = $class; $this->name = $name; $this->property = $property; } /** * {@inheritdoc} */ public function addConstraint(Constraint $constraint) { $this->checkConstraint($constraint); parent::addConstraint($constraint); return $this; } /** * {@inheritdoc} */ public function __sleep() { return array_merge(parent::__sleep(), [ 'class', 'name', 'property', ]); } /** * Returns the name of the member. * * @return string */ public function getName() { return $this->name; } /** * {@inheritdoc} */ public function getClassName() { return $this->class; } /** * {@inheritdoc} */ public function getPropertyName() { return $this->property; } /** * Returns whether this member is public. * * @param object|string $objectOrClassName The object or the class name * * @return bool */ public function isPublic($objectOrClassName) { return $this->getReflectionMember($objectOrClassName)->isPublic(); } /** * Returns whether this member is protected. * * @param object|string $objectOrClassName The object or the class name * * @return bool */ public function isProtected($objectOrClassName) { return $this->getReflectionMember($objectOrClassName)->isProtected(); } /** * Returns whether this member is private. * * @param object|string $objectOrClassName The object or the class name * * @return bool */ public function isPrivate($objectOrClassName) { return $this->getReflectionMember($objectOrClassName)->isPrivate(); } /** * Returns the reflection instance for accessing the member's value. * * @param object|string $objectOrClassName The object or the class name * * @return \ReflectionMethod|\ReflectionProperty */ public function getReflectionMember($objectOrClassName) { $className = \is_string($objectOrClassName) ? $objectOrClassName : \get_class($objectOrClassName); if (!isset($this->reflMember[$className])) { $this->reflMember[$className] = $this->newReflectionMember($objectOrClassName); } return $this->reflMember[$className]; } /** * Creates a new reflection instance for accessing the member's value. * * @param object|string $objectOrClassName The object or the class name * * @return \ReflectionMethod|\ReflectionProperty */ abstract protected function newReflectionMember($objectOrClassName); private function checkConstraint(Constraint $constraint) { if (!\in_array(Constraint::PROPERTY_CONSTRAINT, (array) $constraint->getTargets(), true)) { throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on properties or getters.', get_debug_type($constraint))); } if ($constraint instanceof Composite) { foreach ($constraint->getNestedConstraints() as $nestedConstraint) { $this->checkConstraint($nestedConstraint); } } } } PK!Lbu6vendor/symfony/validator/Mapping/MetadataInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; /** * A container for validation metadata. * * Most importantly, the metadata stores the constraints against which an object * and its properties should be validated. * * Additionally, the metadata stores whether objects should be validated * against their class' metadata and whether traversable objects should be * traversed or not. * * @author Bernhard Schussek * * @see CascadingStrategy * @see TraversalStrategy */ interface MetadataInterface { /** * Returns the strategy for cascading objects. * * @return int * * @see CascadingStrategy */ public function getCascadingStrategy(); /** * Returns the strategy for traversing traversable objects. * * @return int * * @see TraversalStrategy */ public function getTraversalStrategy(); /** * Returns all constraints of this element. * * @return Constraint[] */ public function getConstraints(); /** * Returns all constraints for a given validation group. * * @param string $group The validation group * * @return Constraint[] */ public function findConstraints(string $group); } PK!/:>vendor/symfony/validator/Mapping/PropertyMetadataInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; /** * Stores all metadata needed for validating the value of a class property. * * Most importantly, the metadata stores the constraints against which the * property's value should be validated. * * Additionally, the metadata stores whether objects stored in the property * should be validated against their class' metadata and whether traversable * objects should be traversed or not. * * @author Bernhard Schussek * * @see MetadataInterface * @see CascadingStrategy * @see TraversalStrategy */ interface PropertyMetadataInterface extends MetadataInterface { /** * Returns the name of the property. * * @return string */ public function getPropertyName(); /** * Extracts the value of the property from the given container. * * @param mixed $containingValue The container to extract the property value from * * @return mixed */ public function getPropertyValue($containingValue); } PK!驔t 5vendor/symfony/validator/Mapping/PropertyMetadata.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Exception\ValidatorException; /** * Stores all metadata needed for validating a class property. * * The value of the property is obtained by directly accessing the property. * The property will be accessed by reflection, so the access of private and * protected properties is supported. * * This class supports serialization and cloning. * * @author Bernhard Schussek * * @see PropertyMetadataInterface */ class PropertyMetadata extends MemberMetadata { /** * @param string $class The class this property is defined on * @param string $name The name of this property * * @throws ValidatorException */ public function __construct(string $class, string $name) { if (!property_exists($class, $name)) { throw new ValidatorException(sprintf('Property "%s" does not exist in class "%s".', $name, $class)); } parent::__construct($class, $name, $name); } /** * {@inheritdoc} */ public function getPropertyValue($object) { $reflProperty = $this->getReflectionMember($object); if (\PHP_VERSION_ID >= 70400 && $reflProperty->hasType() && !$reflProperty->isInitialized($object)) { // There is no way to check if a property has been unset or if it is uninitialized. // When trying to access an uninitialized property, __get method is triggered. // If __get method is not present, no fallback is possible // Otherwise we need to catch an Error in case we are trying to access an uninitialized but set property. if (!method_exists($object, '__get')) { return null; } try { return $reflProperty->getValue($object); } catch (\Error $e) { return null; } } return $reflProperty->getValue($object); } /** * {@inheritdoc} */ protected function newReflectionMember($objectOrClassName) { $originalClass = \is_string($objectOrClassName) ? $objectOrClassName : \get_class($objectOrClassName); while (!property_exists($objectOrClassName, $this->getName())) { $objectOrClassName = get_parent_class($objectOrClassName); if (false === $objectOrClassName) { throw new ValidatorException(sprintf('Property "%s" does not exist in class "%s".', $this->getName(), $originalClass)); } } $member = new \ReflectionProperty($objectOrClassName, $this->getName()); $member->setAccessible(true); return $member; } } PK!XZ6vendor/symfony/validator/Mapping/TraversalStrategy.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Mapping; /** * Specifies whether and how a traversable object should be traversed. * * If the node traverser traverses a node whose value is an instance of * {@link \Traversable}, and if that node is either a class node or if * cascading is enabled, then the node's traversal strategy will be checked. * Depending on the requested traversal strategy, the node traverser will * iterate over the object and cascade each object or collection returned by * the iterator. * * The traversal strategy is ignored for arrays. Arrays are always iterated. * * @author Bernhard Schussek * * @see CascadingStrategy */ class TraversalStrategy { /** * Specifies that a node's value should be iterated only if it is an * instance of {@link \Traversable}. */ public const IMPLICIT = 1; /** * Specifies that a node's value should never be iterated. */ public const NONE = 2; /** * Specifies that a node's value should always be iterated. If the value is * not an instance of {@link \Traversable}, an exception should be thrown. */ public const TRAVERSE = 4; /** * Not instantiable. */ private function __construct() { } } PK!FF=vendor/symfony/validator/Test/ConstraintValidatorTestCase.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Test; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\IsIdentical; use PHPUnit\Framework\Constraint\IsInstanceOf; use PHPUnit\Framework\Constraint\IsNull; use PHPUnit\Framework\Constraint\LogicalOr; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\PropertyMetadata; use Symfony\Component\Validator\Validator\ContextualValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * A test case to ease testing Constraint Validators. * * @author Bernhard Schussek */ abstract class ConstraintValidatorTestCase extends TestCase { /** * @var ExecutionContextInterface */ protected $context; /** * @var ConstraintValidatorInterface */ protected $validator; protected $group; protected $metadata; protected $object; protected $value; protected $root; protected $propertyPath; protected $constraint; protected $defaultTimezone; private $defaultLocale; private $expectedViolations; private $call; protected function setUp(): void { $this->group = 'MyGroup'; $this->metadata = null; $this->object = null; $this->value = 'InvalidValue'; $this->root = 'root'; $this->propertyPath = 'property.path'; // Initialize the context with some constraint so that we can // successfully build a violation. $this->constraint = new NotNull(); $this->context = $this->createContext(); $this->validator = $this->createValidator(); $this->validator->initialize($this->context); if (class_exists(\Locale::class)) { $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); } $this->expectedViolations = []; $this->call = 0; $this->setDefaultTimezone('UTC'); } protected function tearDown(): void { $this->restoreDefaultTimezone(); if (class_exists(\Locale::class)) { \Locale::setDefault($this->defaultLocale); } } protected function setDefaultTimezone(?string $defaultTimezone) { // Make sure this method cannot be called twice before calling // also restoreDefaultTimezone() if (null === $this->defaultTimezone) { $this->defaultTimezone = date_default_timezone_get(); date_default_timezone_set($defaultTimezone); } } protected function restoreDefaultTimezone() { if (null !== $this->defaultTimezone) { date_default_timezone_set($this->defaultTimezone); $this->defaultTimezone = null; } } protected function createContext() { $translator = $this->createMock(TranslatorInterface::class); $translator->expects($this->any())->method('trans')->willReturnArgument(0); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->any()) ->method('validate') ->willReturnCallback(function () { return $this->expectedViolations[$this->call++] ?? new ConstraintViolationList(); }); $context = new ExecutionContext($validator, $this->root, $translator); $context->setGroup($this->group); $context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath); $context->setConstraint($this->constraint); $contextualValidatorMockBuilder = $this->getMockBuilder(AssertingContextualValidator::class) ->setConstructorArgs([$context]); $contextualValidatorMethods = [ 'atPath', 'validate', 'validateProperty', 'validatePropertyValue', 'getViolations', ]; $contextualValidatorMockBuilder->onlyMethods($contextualValidatorMethods); $contextualValidator = $contextualValidatorMockBuilder->getMock(); $contextualValidator->expects($this->any()) ->method('atPath') ->willReturnCallback(function ($path) use ($contextualValidator) { return $contextualValidator->doAtPath($path); }); $contextualValidator->expects($this->any()) ->method('validate') ->willReturnCallback(function ($value, $constraints = null, $groups = null) use ($contextualValidator) { return $contextualValidator->doValidate($value, $constraints, $groups); }); $contextualValidator->expects($this->any()) ->method('validateProperty') ->willReturnCallback(function ($object, $propertyName, $groups = null) use ($contextualValidator) { return $contextualValidator->validateProperty($object, $propertyName, $groups); }); $contextualValidator->expects($this->any()) ->method('validatePropertyValue') ->willReturnCallback(function ($objectOrClass, $propertyName, $value, $groups = null) use ($contextualValidator) { return $contextualValidator->doValidatePropertyValue($objectOrClass, $propertyName, $value, $groups); }); $contextualValidator->expects($this->any()) ->method('getViolations') ->willReturnCallback(function () use ($contextualValidator) { return $contextualValidator->doGetViolations(); }); $validator->expects($this->any()) ->method('inContext') ->with($context) ->willReturn($contextualValidator); return $context; } protected function setGroup(?string $group) { $this->group = $group; $this->context->setGroup($group); } protected function setObject($object) { $this->object = $object; $this->metadata = \is_object($object) ? new ClassMetadata(\get_class($object)) : null; $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath); } protected function setProperty($object, $property) { $this->object = $object; $this->metadata = \is_object($object) ? new PropertyMetadata(\get_class($object), $property) : null; $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath); } protected function setValue($value) { $this->value = $value; $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath); } protected function setRoot($root) { $this->root = $root; $this->context = $this->createContext(); $this->validator->initialize($this->context); } protected function setPropertyPath(string $propertyPath) { $this->propertyPath = $propertyPath; $this->context->setNode($this->value, $this->object, $this->metadata, $this->propertyPath); } protected function expectNoValidate() { $validator = $this->context->getValidator()->inContext($this->context); $validator->expectNoValidate(); } protected function expectValidateAt(int $i, string $propertyPath, $value, $group) { $validator = $this->context->getValidator()->inContext($this->context); $validator->expectValidation($i, $propertyPath, $value, $group, function ($passedConstraints) { $expectedConstraints = LogicalOr::fromConstraints(new IsNull(), new IsIdentical([]), new IsInstanceOf(Valid::class)); Assert::assertThat($passedConstraints, $expectedConstraints); }); } protected function expectValidateValue(int $i, $value, array $constraints = [], $group = null) { $contextualValidator = $this->context->getValidator()->inContext($this->context); $contextualValidator->expectValidation($i, null, $value, $group, function ($passedConstraints) use ($constraints) { if (\is_array($constraints) && !\is_array($passedConstraints)) { $passedConstraints = [$passedConstraints]; } Assert::assertEquals($constraints, $passedConstraints); }); } protected function expectFailingValueValidation(int $i, $value, array $constraints, $group, ConstraintViolationInterface $violation) { $contextualValidator = $this->context->getValidator()->inContext($this->context); $contextualValidator->expectValidation($i, null, $value, $group, function ($passedConstraints) use ($constraints) { if (\is_array($constraints) && !\is_array($passedConstraints)) { $passedConstraints = [$passedConstraints]; } Assert::assertEquals($constraints, $passedConstraints); }, $violation); } protected function expectValidateValueAt(int $i, string $propertyPath, $value, $constraints, $group = null) { $contextualValidator = $this->context->getValidator()->inContext($this->context); $contextualValidator->expectValidation($i, $propertyPath, $value, $group, function ($passedConstraints) use ($constraints) { Assert::assertEquals($constraints, $passedConstraints); }); } protected function expectViolationsAt($i, $value, Constraint $constraint) { $context = $this->createContext(); $validatorClassname = $constraint->validatedBy(); $validator = new $validatorClassname(); $validator->initialize($context); $validator->validate($value, $constraint); $this->expectedViolations[] = $context->getViolations(); return $context->getViolations(); } protected function assertNoViolation() { $this->assertSame(0, $violationsCount = \count($this->context->getViolations()), sprintf('0 violation expected. Got %u.', $violationsCount)); } /** * @return ConstraintViolationAssertion */ protected function buildViolation($message) { return new ConstraintViolationAssertion($this->context, $message, $this->constraint); } abstract protected function createValidator(); } final class ConstraintViolationAssertion { /** * @var ExecutionContextInterface */ private $context; /** * @var ConstraintViolationAssertion[] */ private $assertions; private $message; private $parameters = []; private $invalidValue = 'InvalidValue'; private $propertyPath = 'property.path'; private $plural; private $code; private $constraint; private $cause; /** * @internal */ public function __construct(ExecutionContextInterface $context, string $message, ?Constraint $constraint = null, array $assertions = []) { $this->context = $context; $this->message = $message; $this->constraint = $constraint; $this->assertions = $assertions; } /** * @return $this */ public function atPath(string $path) { $this->propertyPath = $path; return $this; } /** * @return $this */ public function setParameter(string $key, string $value) { $this->parameters[$key] = $value; return $this; } /** * @return $this */ public function setParameters(array $parameters) { $this->parameters = $parameters; return $this; } /** * @return $this */ public function setTranslationDomain($translationDomain) { // no-op for BC return $this; } /** * @return $this */ public function setInvalidValue($invalidValue) { $this->invalidValue = $invalidValue; return $this; } /** * @return $this */ public function setPlural(int $number) { $this->plural = $number; return $this; } /** * @return $this */ public function setCode(string $code) { $this->code = $code; return $this; } /** * @return $this */ public function setCause($cause) { $this->cause = $cause; return $this; } public function buildNextViolation(string $message): self { $assertions = $this->assertions; $assertions[] = $this; return new self($this->context, $message, $this->constraint, $assertions); } public function assertRaised() { $expected = []; foreach ($this->assertions as $assertion) { $expected[] = $assertion->getViolation(); } $expected[] = $this->getViolation(); $violations = iterator_to_array($this->context->getViolations()); Assert::assertSame($expectedCount = \count($expected), $violationsCount = \count($violations), sprintf('%u violation(s) expected. Got %u.', $expectedCount, $violationsCount)); reset($violations); foreach ($expected as $violation) { Assert::assertEquals($violation, current($violations)); next($violations); } } private function getViolation(): ConstraintViolation { return new ConstraintViolation( $this->message, $this->message, $this->parameters, $this->context->getRoot(), $this->propertyPath, $this->invalidValue, $this->plural, $this->code, $this->constraint, $this->cause ); } } /** * @internal */ class AssertingContextualValidator implements ContextualValidatorInterface { private $context; private $expectNoValidate = false; private $atPathCalls = -1; private $expectedAtPath = []; private $validateCalls = -1; private $expectedValidate = []; public function __construct(ExecutionContextInterface $context) { $this->context = $context; } public function __destruct() { if ($this->expectedAtPath) { throw new ExpectationFailedException('Some expected validation calls for paths were not done.'); } if ($this->expectedValidate) { throw new ExpectationFailedException('Some expected validation calls for values were not done.'); } } public function atPath(string $path) { } /** * @return $this */ public function doAtPath(string $path) { Assert::assertFalse($this->expectNoValidate, 'No validation calls have been expected.'); if (!isset($this->expectedAtPath[++$this->atPathCalls])) { throw new ExpectationFailedException(sprintf('Validation for property path "%s" was not expected.', $path)); } $expectedPath = $this->expectedAtPath[$this->atPathCalls]; unset($this->expectedAtPath[$this->atPathCalls]); Assert::assertSame($expectedPath, $path); return $this; } public function validate($value, $constraints = null, $groups = null) { } /** * @return $this */ public function doValidate($value, $constraints = null, $groups = null) { Assert::assertFalse($this->expectNoValidate, 'No validation calls have been expected.'); if (!isset($this->expectedValidate[++$this->validateCalls])) { return $this; } [$expectedValue, $expectedGroup, $expectedConstraints, $violation] = $this->expectedValidate[$this->validateCalls]; unset($this->expectedValidate[$this->validateCalls]); Assert::assertSame($expectedValue, $value); $expectedConstraints($constraints); Assert::assertSame($expectedGroup, $groups); if (null !== $violation) { $this->context->addViolation($violation->getMessage(), $violation->getParameters()); } return $this; } public function validateProperty(object $object, string $propertyName, $groups = null) { } /** * @return $this */ public function doValidateProperty(object $object, string $propertyName, $groups = null) { return $this; } public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null) { } /** * @return $this */ public function doValidatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null) { return $this; } public function getViolations(): ConstraintViolationListInterface { } public function doGetViolations() { return $this->context->getViolations(); } public function expectNoValidate() { $this->expectNoValidate = true; } public function expectValidation(string $call, ?string $propertyPath, $value, $group, callable $constraints, ?ConstraintViolationInterface $violation = null) { if (null !== $propertyPath) { $this->expectedAtPath[$call] = $propertyPath; } $this->expectedValidate[$call] = [$value, $group, $constraints, $violation]; } } PK!LL.vendor/symfony/validator/Util/PropertyPath.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Util; /** * Contains utility methods for dealing with property paths. * * For more extensive functionality, use Symfony's PropertyAccess component. * * @author Bernhard Schussek */ class PropertyPath { /** * Appends a path to a given property path. * * If the base path is empty, the appended path will be returned unchanged. * If the base path is not empty, and the appended path starts with a * squared opening bracket ("["), the concatenation of the two paths is * returned. Otherwise, the concatenation of the two paths is returned, * separated by a dot ("."). * * @return string */ public static function append(string $basePath, string $subPath) { if ('' !== $subPath) { if ('[' === $subPath[0]) { return $basePath.$subPath; } return '' !== $basePath ? $basePath.'.'.$subPath : $subPath; } return $basePath; } /** * Not instantiable. */ private function __construct() { } } PK!& & Cvendor/symfony/validator/Validator/ContextualValidatorInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationListInterface; /** * A validator in a specific execution context. * * @author Bernhard Schussek */ interface ContextualValidatorInterface { /** * Appends the given path to the property path of the context. * * If called multiple times, the path will always be reset to the context's * original path with the given path appended to it. * * @return $this */ public function atPath(string $path); /** * Validates a value against a constraint or a list of constraints. * * If no constraint is passed, the constraint * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. * * @param mixed $value The value to validate * @param Constraint|Constraint[]|null $constraints The constraint(s) to validate against * @param string|GroupSequence|array|null $groups The validation groups to validate. If none is given, "Default" is assumed * * @return $this */ public function validate($value, $constraints = null, $groups = null); /** * Validates a property of an object against the constraints specified * for this property. * * @param string $propertyName The name of the validated property * @param string|GroupSequence|array|null $groups The validation groups to validate. If none is given, "Default" is assumed * * @return $this */ public function validateProperty(object $object, string $propertyName, $groups = null); /** * Validates a value against the constraints specified for an object's * property. * * @param object|string $objectOrClass The object or its class name * @param string $propertyName The name of the property * @param mixed $value The value to validate against the property's constraints * @param string|GroupSequence|array|null $groups The validation groups to validate. If none is given, "Default" is assumed * * @return $this */ public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null); /** * Returns the violations that have been generated so far in the context * of the validator. * * @return ConstraintViolationListInterface */ public function getViolations(); } PK!3vendor/symfony/validator/Validator/LazyProperty.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Validator; /** * A wrapper for a callable initializing a property from a getter. * * @internal */ class LazyProperty { private $propertyValueCallback; public function __construct(\Closure $propertyValueCallback) { $this->propertyValueCallback = $propertyValueCallback; } public function getPropertyValue() { return ($this->propertyValueCallback)(); } } PK!uuCvendor/symfony/validator/Validator/RecursiveContextualValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\Existence; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Mapping\GetterMetadata; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Util\PropertyPath; /** * Recursive implementation of {@link ContextualValidatorInterface}. * * @author Bernhard Schussek */ class RecursiveContextualValidator implements ContextualValidatorInterface { private $context; private $defaultPropertyPath; private $defaultGroups; private $metadataFactory; private $validatorFactory; private $objectInitializers; /** * Creates a validator for the given context. * * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = []) { $this->context = $context; $this->defaultPropertyPath = $context->getPropertyPath(); $this->defaultGroups = [$context->getGroup() ?: Constraint::DEFAULT_GROUP]; $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; $this->objectInitializers = $objectInitializers; } /** * {@inheritdoc} */ public function atPath(string $path) { $this->defaultPropertyPath = $this->context->getPropertyPath($path); return $this; } /** * {@inheritdoc} */ public function validate($value, $constraints = null, $groups = null) { $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $previousValue = $this->context->getValue(); $previousObject = $this->context->getObject(); $previousMetadata = $this->context->getMetadata(); $previousPath = $this->context->getPropertyPath(); $previousGroup = $this->context->getGroup(); $previousConstraint = null; if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) { $previousConstraint = $this->context->getConstraint(); } // If explicit constraints are passed, validate the value against // those constraints if (null !== $constraints) { // You can pass a single constraint or an array of constraints // Make sure to deal with an array in the rest of the code if (!\is_array($constraints)) { $constraints = [$constraints]; } $metadata = new GenericMetadata(); $metadata->addConstraints($constraints); $this->validateGenericNode( $value, $previousObject, \is_object($value) ? $this->generateCacheKey($value) : null, $metadata, $this->defaultPropertyPath, $groups, null, TraversalStrategy::IMPLICIT, $this->context ); $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath); $this->context->setGroup($previousGroup); if (null !== $previousConstraint) { $this->context->setConstraint($previousConstraint); } return $this; } // If an object is passed without explicit constraints, validate that // object against the constraints defined for the object's class if (\is_object($value)) { $this->validateObject( $value, $this->defaultPropertyPath, $groups, TraversalStrategy::IMPLICIT, $this->context ); $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath); $this->context->setGroup($previousGroup); return $this; } // If an array is passed without explicit constraints, validate each // object in the array if (\is_array($value)) { $this->validateEachObjectIn( $value, $this->defaultPropertyPath, $groups, $this->context ); $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath); $this->context->setGroup($previousGroup); return $this; } throw new RuntimeException(sprintf('Cannot validate values of type "%s" automatically. Please provide a constraint.', get_debug_type($value))); } /** * {@inheritdoc} */ public function validateProperty(object $object, string $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata))); } $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $cacheKey = $this->generateCacheKey($object); $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName); $previousValue = $this->context->getValue(); $previousObject = $this->context->getObject(); $previousMetadata = $this->context->getMetadata(); $previousPath = $this->context->getPropertyPath(); $previousGroup = $this->context->getGroup(); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($object); $this->validateGenericNode( $propertyValue, $object, $cacheKey.':'.\get_class($object).':'.$propertyName, $propertyMetadata, $propertyPath, $groups, null, TraversalStrategy::IMPLICIT, $this->context ); } $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath); $this->context->setGroup($previousGroup); return $this; } /** * {@inheritdoc} */ public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass); if (!$classMetadata instanceof ClassMetadataInterface) { throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata))); } $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; if (\is_object($objectOrClass)) { $object = $objectOrClass; $class = \get_class($object); $cacheKey = $this->generateCacheKey($objectOrClass); $propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName); } else { // $objectOrClass contains a class name $object = null; $class = $objectOrClass; $cacheKey = null; $propertyPath = $this->defaultPropertyPath; } $previousValue = $this->context->getValue(); $previousObject = $this->context->getObject(); $previousMetadata = $this->context->getMetadata(); $previousPath = $this->context->getPropertyPath(); $previousGroup = $this->context->getGroup(); foreach ($propertyMetadatas as $propertyMetadata) { $this->validateGenericNode( $value, $object, $cacheKey.':'.$class.':'.$propertyName, $propertyMetadata, $propertyPath, $groups, null, TraversalStrategy::IMPLICIT, $this->context ); } $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath); $this->context->setGroup($previousGroup); return $this; } /** * {@inheritdoc} */ public function getViolations() { return $this->context->getViolations(); } /** * Normalizes the given group or list of groups to an array. * * @param string|GroupSequence|array $groups The groups to normalize * * @return array */ protected function normalizeGroups($groups) { if (\is_array($groups)) { return $groups; } return [$groups]; } /** * Validates an object against the constraints defined for its class. * * If no metadata is available for the class, but the class is an instance * of {@link \Traversable} and the selected traversal strategy allows * traversal, the object will be iterated and each nested object will be * validated instead. * * @throws NoSuchMetadataException If the object has no associated metadata * and does not implement {@link \Traversable} * or if traversal is disabled via the * $traversalStrategy argument * @throws UnsupportedMetadataException If the metadata returned by the * metadata factory does not implement * {@link ClassMetadataInterface} */ private function validateObject(object $object, string $propertyPath, array $groups, int $traversalStrategy, ExecutionContextInterface $context) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { throw new UnsupportedMetadataException(sprintf('The metadata factory should return instances of "Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata))); } $this->validateClassNode( $object, $this->generateCacheKey($object), $classMetadata, $propertyPath, $groups, null, $traversalStrategy, $context ); } catch (NoSuchMetadataException $e) { // Rethrow if not Traversable if (!$object instanceof \Traversable) { throw $e; } // Rethrow unless IMPLICIT or TRAVERSE if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { throw $e; } $this->validateEachObjectIn( $object, $propertyPath, $groups, $context ); } } /** * Validates each object in a collection against the constraints defined * for their classes. * * Nested arrays are also iterated. */ private function validateEachObjectIn(iterable $collection, string $propertyPath, array $groups, ExecutionContextInterface $context) { foreach ($collection as $key => $value) { if (\is_array($value)) { // Also traverse nested arrays $this->validateEachObjectIn( $value, $propertyPath.'['.$key.']', $groups, $context ); continue; } // Scalar and null values in the collection are ignored if (\is_object($value)) { $this->validateObject( $value, $propertyPath.'['.$key.']', $groups, TraversalStrategy::IMPLICIT, $context ); } } } /** * Validates a class node. * * A class node is a combination of an object with a {@link ClassMetadataInterface} * instance. Each class node (conceptually) has zero or more succeeding * property nodes: * * (Article:class node) * \ * ($title:property node) * * This method validates the passed objects against all constraints defined * at class level. It furthermore triggers the validation of each of the * class' properties against the constraints for that property. * * If the selected traversal strategy allows traversal, the object is * iterated and each nested object is validated against its own constraints. * The object is not traversed if traversal is disabled in the class * metadata. * * If the passed groups contain the group "Default", the validator will * check whether the "Default" group has been replaced by a group sequence * in the class metadata. If this is the case, the group sequence is * validated instead. * * @throws UnsupportedMetadataException If a property metadata does not * implement {@link PropertyMetadataInterface} * @throws ConstraintDefinitionException If traversal was enabled but the * object does not implement * {@link \Traversable} * * @see TraversalStrategy */ private function validateClassNode(object $object, ?string $cacheKey, ClassMetadataInterface $metadata, string $propertyPath, array $groups, ?array $cascadedGroups, int $traversalStrategy, ExecutionContextInterface $context) { $context->setNode($object, $object, $metadata, $propertyPath); if (!$context->isObjectInitialized($cacheKey)) { foreach ($this->objectInitializers as $initializer) { $initializer->initialize($object); } $context->markObjectAsInitialized($cacheKey); } foreach ($groups as $key => $group) { // If the "Default" group is replaced by a group sequence, remember // to cascade the "Default" group when traversing the group // sequence $defaultOverridden = false; // Use the object hash for group sequences $groupHash = \is_object($group) ? $this->generateCacheKey($group, true) : $group; if ($context->isGroupValidated($cacheKey, $groupHash)) { // Skip this group when validating the properties and when // traversing the object unset($groups[$key]); continue; } $context->markGroupAsValidated($cacheKey, $groupHash); // Replace the "Default" group by the group sequence defined // for the class, if applicable. // This is done after checking the cache, so that // spl_object_hash() isn't called for this sequence and // "Default" is used instead in the cache. This is useful // if the getters below return different group sequences in // every call. if (Constraint::DEFAULT_GROUP === $group) { if ($metadata->hasGroupSequence()) { // The group sequence is statically defined for the class $group = $metadata->getGroupSequence(); $defaultOverridden = true; } elseif ($metadata->isGroupSequenceProvider()) { // The group sequence is dynamically obtained from the validated // object /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */ $group = $object->getGroupSequence(); $defaultOverridden = true; if (!$group instanceof GroupSequence) { $group = new GroupSequence($group); } } } // If the groups (=[,G3,G4]) contain a group sequence // (=), then call validateClassNode() with each entry of the // group sequence and abort if necessary (G1, G2) if ($group instanceof GroupSequence) { $this->stepThroughGroupSequence( $object, $object, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, $defaultOverridden ? Constraint::DEFAULT_GROUP : null, $context ); // Skip the group sequence when validating properties, because // stepThroughGroupSequence() already validates the properties unset($groups[$key]); continue; } $this->validateInGroup($object, $cacheKey, $metadata, $group, $context); } // If no more groups should be validated for the property nodes, // we can safely quit if (0 === \count($groups)) { return; } // Validate all properties against their constraints foreach ($metadata->getConstrainedProperties() as $propertyName) { // If constraints are defined both on the getter of a property as // well as on the property itself, then getPropertyMetadata() // returns two metadata objects, not just one foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { if (!$propertyMetadata instanceof PropertyMetadataInterface) { throw new UnsupportedMetadataException(sprintf('The property metadata instances should implement "Symfony\Component\Validator\Mapping\PropertyMetadataInterface", got: "%s".', get_debug_type($propertyMetadata))); } if ($propertyMetadata instanceof GetterMetadata) { $propertyValue = new LazyProperty(static function () use ($propertyMetadata, $object) { return $propertyMetadata->getPropertyValue($object); }); } else { $propertyValue = $propertyMetadata->getPropertyValue($object); } $this->validateGenericNode( $propertyValue, $object, $cacheKey.':'.\get_class($object).':'.$propertyName, $propertyMetadata, PropertyPath::append($propertyPath, $propertyName), $groups, $cascadedGroups, TraversalStrategy::IMPLICIT, $context ); } } // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the class' metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { $traversalStrategy = $metadata->getTraversalStrategy(); } // Traverse only if IMPLICIT or TRAVERSE if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { return; } // If IMPLICIT, stop unless we deal with a Traversable if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) { return; } // If TRAVERSE, fail if we have no Traversable if (!$object instanceof \Traversable) { throw new ConstraintDefinitionException(sprintf('Traversal was enabled for "%s", but this class does not implement "\Traversable".', get_debug_type($object))); } $this->validateEachObjectIn( $object, $propertyPath, $groups, $context ); } /** * Validates a node that is not a class node. * * Currently, two such node types exist: * * - property nodes, which consist of the value of an object's * property together with a {@link PropertyMetadataInterface} instance * - generic nodes, which consist of a value and some arbitrary * constraints defined in a {@link MetadataInterface} container * * In both cases, the value is validated against all constraints defined * in the passed metadata object. Then, if the value is an instance of * {@link \Traversable} and the selected traversal strategy permits it, * the value is traversed and each nested object validated against its own * constraints. If the value is an array, it is traversed regardless of * the given strategy. * * @see TraversalStrategy */ private function validateGenericNode($value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadata, string $propertyPath, array $groups, ?array $cascadedGroups, int $traversalStrategy, ExecutionContextInterface $context) { $context->setNode($value, $object, $metadata, $propertyPath); foreach ($groups as $key => $group) { if ($group instanceof GroupSequence) { $this->stepThroughGroupSequence( $value, $object, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, null, $context ); // Skip the group sequence when cascading, as the cascading // logic is already done in stepThroughGroupSequence() unset($groups[$key]); continue; } $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); } if (0 === \count($groups)) { return; } if (null === $value) { return; } $cascadingStrategy = $metadata->getCascadingStrategy(); // Quit unless we cascade if (!($cascadingStrategy & CascadingStrategy::CASCADE)) { return; } // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the node's metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { $traversalStrategy = $metadata->getTraversalStrategy(); } // The $cascadedGroups property is set, if the "Default" group is // overridden by a group sequence // See validateClassNode() $cascadedGroups = null !== $cascadedGroups && \count($cascadedGroups) > 0 ? $cascadedGroups : $groups; if ($value instanceof LazyProperty) { $value = $value->getPropertyValue(); if (null === $value) { return; } } if (\is_array($value)) { // Arrays are always traversed, independent of the specified // traversal strategy $this->validateEachObjectIn( $value, $propertyPath, $cascadedGroups, $context ); return; } if (!\is_object($value)) { throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: "%s".', \gettype($value))); } $this->validateObject( $value, $propertyPath, $cascadedGroups, $traversalStrategy, $context ); // Currently, the traversal strategy can only be TRAVERSE for a // generic node if the cascading strategy is CASCADE. Thus, traversable // objects will always be handled within validateObject() and there's // nothing more to do here. // see GenericMetadata::addConstraint() } /** * Sequentially validates a node's value in each group of a group sequence. * * If any of the constraints generates a violation, subsequent groups in the * group sequence are skipped. */ private function stepThroughGroupSequence($value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadata, string $propertyPath, int $traversalStrategy, GroupSequence $groupSequence, ?string $cascadedGroup, ExecutionContextInterface $context) { $violationCount = \count($context->getViolations()); $cascadedGroups = $cascadedGroup ? [$cascadedGroup] : null; foreach ($groupSequence->groups as $groupInSequence) { $groups = (array) $groupInSequence; if ($metadata instanceof ClassMetadataInterface) { $this->validateClassNode( $value, $cacheKey, $metadata, $propertyPath, $groups, $cascadedGroups, $traversalStrategy, $context ); } else { $this->validateGenericNode( $value, $object, $cacheKey, $metadata, $propertyPath, $groups, $cascadedGroups, $traversalStrategy, $context ); } // Abort sequence validation if a violation was generated if (\count($context->getViolations()) > $violationCount) { break; } } } /** * Validates a node's value against all constraints in the given group. * * @param mixed $value The validated value */ private function validateInGroup($value, ?string $cacheKey, MetadataInterface $metadata, string $group, ExecutionContextInterface $context) { $context->setGroup($group); foreach ($metadata->findConstraints($group) as $constraint) { if ($constraint instanceof Existence) { continue; } // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups if (null !== $cacheKey) { $constraintHash = $this->generateCacheKey($constraint, true); // instanceof Valid: In case of using a Valid constraint with many groups // it makes a reference object get validated by each group if ($constraint instanceof Composite || $constraint instanceof Valid) { $constraintHash .= $group; } if ($context->isConstraintValidated($cacheKey, $constraintHash)) { continue; } $context->markConstraintAsValidated($cacheKey, $constraintHash); } $context->setConstraint($constraint); $validator = $this->validatorFactory->getInstance($constraint); $validator->initialize($context); if ($value instanceof LazyProperty) { $value = $value->getPropertyValue(); } try { $validator->validate($value, $constraint); } catch (UnexpectedValueException $e) { $context->buildViolation('This value should be of type {{ type }}.') ->setParameter('{{ type }}', $e->getExpectedType()) ->addViolation(); } } } private function generateCacheKey(object $object, bool $dependsOnPropertyPath = false): string { if ($this->context instanceof ExecutionContext) { $cacheKey = $this->context->generateCacheKey($object); } else { $cacheKey = spl_object_hash($object); } if ($dependsOnPropertyPath) { $cacheKey .= $this->context->getPropertyPath(); } return $cacheKey; } } PK!6b b 9vendor/symfony/validator/Validator/RecursiveValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\ObjectInitializerInterface; /** * Recursive implementation of {@link ValidatorInterface}. * * @author Bernhard Schussek */ class RecursiveValidator implements ValidatorInterface { protected $contextFactory; protected $metadataFactory; protected $validatorFactory; protected $objectInitializers; /** * Creates a new validator. * * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = []) { $this->contextFactory = $contextFactory; $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; $this->objectInitializers = $objectInitializers; } /** * {@inheritdoc} */ public function startContext($root = null) { return new RecursiveContextualValidator( $this->contextFactory->createContext($this, $root), $this->metadataFactory, $this->validatorFactory, $this->objectInitializers ); } /** * {@inheritdoc} */ public function inContext(ExecutionContextInterface $context) { return new RecursiveContextualValidator( $context, $this->metadataFactory, $this->validatorFactory, $this->objectInitializers ); } /** * {@inheritdoc} */ public function getMetadataFor($object) { return $this->metadataFactory->getMetadataFor($object); } /** * {@inheritdoc} */ public function hasMetadataFor($object) { return $this->metadataFactory->hasMetadataFor($object); } /** * {@inheritdoc} */ public function validate($value, $constraints = null, $groups = null) { return $this->startContext($value) ->validate($value, $constraints, $groups) ->getViolations(); } /** * {@inheritdoc} */ public function validateProperty(object $object, string $propertyName, $groups = null) { return $this->startContext($object) ->validateProperty($object, $propertyName, $groups) ->getViolations(); } /** * {@inheritdoc} */ public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null) { // If a class name is passed, take $value as root return $this->startContext(\is_object($objectOrClass) ? $objectOrClass : $value) ->validatePropertyValue($objectOrClass, $propertyName, $value, $groups) ->getViolations(); } } PK!Yr{s 9vendor/symfony/validator/Validator/TraceableValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Contracts\Service\ResetInterface; /** * Collects some data about validator calls. * * @author Maxime Steinhausser */ class TraceableValidator implements ValidatorInterface, ResetInterface { private $validator; private $collectedData = []; public function __construct(ValidatorInterface $validator) { $this->validator = $validator; } /** * @return array */ public function getCollectedData() { return $this->collectedData; } public function reset() { $this->collectedData = []; } /** * {@inheritdoc} */ public function getMetadataFor($value) { return $this->validator->getMetadataFor($value); } /** * {@inheritdoc} */ public function hasMetadataFor($value) { return $this->validator->hasMetadataFor($value); } /** * {@inheritdoc} */ public function validate($value, $constraints = null, $groups = null) { $violations = $this->validator->validate($value, $constraints, $groups); $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 7); $file = $trace[0]['file']; $line = $trace[0]['line']; for ($i = 1; $i < 7; ++$i) { if (isset($trace[$i]['class'], $trace[$i]['function']) && 'validate' === $trace[$i]['function'] && is_a($trace[$i]['class'], ValidatorInterface::class, true) ) { $file = $trace[$i]['file']; $line = $trace[$i]['line']; while (++$i < 7) { if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($trace[$i]['function'], 'call_user_func')) { $file = $trace[$i]['file']; $line = $trace[$i]['line']; break; } } break; } } $name = str_replace('\\', '/', $file); $name = substr($name, strrpos($name, '/') + 1); $this->collectedData[] = [ 'caller' => compact('name', 'file', 'line'), 'context' => compact('value', 'constraints', 'groups'), 'violations' => iterator_to_array($violations), ]; return $violations; } /** * {@inheritdoc} */ public function validateProperty(object $object, string $propertyName, $groups = null) { return $this->validator->validateProperty($object, $propertyName, $groups); } /** * {@inheritdoc} */ public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null) { return $this->validator->validatePropertyValue($objectOrClass, $propertyName, $value, $groups); } /** * {@inheritdoc} */ public function startContext() { return $this->validator->startContext(); } /** * {@inheritdoc} */ public function inContext(ExecutionContextInterface $context) { return $this->validator->inContext($context); } } PK!:9vendor/symfony/validator/Validator/ValidatorInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; /** * Validates PHP values against constraints. * * @author Bernhard Schussek */ interface ValidatorInterface extends MetadataFactoryInterface { /** * Validates a value against a constraint or a list of constraints. * * If no constraint is passed, the constraint * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. * * @param mixed $value The value to validate * @param Constraint|Constraint[] $constraints The constraint(s) to validate against * @param string|GroupSequence|array|null $groups The validation groups to validate. If none is given, "Default" is assumed * * @return ConstraintViolationListInterface A list of constraint violations * If the list is empty, validation * succeeded */ public function validate($value, $constraints = null, $groups = null); /** * Validates a property of an object against the constraints specified * for this property. * * @param string $propertyName The name of the validated property * @param string|GroupSequence|array|null $groups The validation groups to validate. If none is given, "Default" is assumed * * @return ConstraintViolationListInterface A list of constraint violations * If the list is empty, validation * succeeded */ public function validateProperty(object $object, string $propertyName, $groups = null); /** * Validates a value against the constraints specified for an object's * property. * * @param object|string $objectOrClass The object or its class name * @param string $propertyName The name of the property * @param mixed $value The value to validate against the property's constraints * @param string|GroupSequence|array|null $groups The validation groups to validate. If none is given, "Default" is assumed * * @return ConstraintViolationListInterface A list of constraint violations * If the list is empty, validation * succeeded */ public function validatePropertyValue($objectOrClass, string $propertyName, $value, $groups = null); /** * Starts a new validation context and returns a validator for that context. * * The returned validator collects all violations generated within its * context. You can access these violations with the * {@link ContextualValidatorInterface::getViolations()} method. * * @return ContextualValidatorInterface */ public function startContext(); /** * Returns a validator in the given execution context. * * The returned validator adds all generated violations to the given * context. * * @return ContextualValidatorInterface */ public function inContext(ExecutionContextInterface $context); } PK!X`  Jvendor/symfony/validator/Violation/ConstraintViolationBuilderInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Violation; /** * Builds {@link \Symfony\Component\Validator\ConstraintViolationInterface} * objects. * * Use the various methods on this interface to configure the built violation. * Finally, call {@link addViolation()} to add the violation to the current * execution context. * * @author Bernhard Schussek */ interface ConstraintViolationBuilderInterface { /** * Stores the property path at which the violation should be generated. * * The passed path will be appended to the current property path of the * execution context. * * @param string $path The property path * * @return $this */ public function atPath(string $path); /** * Sets a parameter to be inserted into the violation message. * * @param string $key The name of the parameter * @param string $value The value to be inserted in the parameter's place * * @return $this */ public function setParameter(string $key, string $value); /** * Sets all parameters to be inserted into the violation message. * * @param array $parameters An array with the parameter names as keys and * the values to be inserted in their place as * values * * @return $this */ public function setParameters(array $parameters); /** * Sets the translation domain which should be used for translating the * violation message. * * @param string $translationDomain The translation domain * * @return $this * * @see \Symfony\Contracts\Translation\TranslatorInterface */ public function setTranslationDomain(string $translationDomain); /** * Sets the invalid value that caused this violation. * * @param mixed $invalidValue The invalid value * * @return $this */ public function setInvalidValue($invalidValue); /** * Sets the number which determines how the plural form of the violation * message is chosen when it is translated. * * @param int $number The number for determining the plural form * * @return $this * * @see \Symfony\Contracts\Translation\TranslatorInterface::trans() */ public function setPlural(int $number); /** * Sets the violation code. * * @param string|null $code The violation code * * @return $this */ public function setCode(?string $code); /** * Sets the cause of the violation. * * @param mixed $cause The cause of the violation * * @return $this */ public function setCause($cause); /** * Adds the violation to the current execution context. */ public function addViolation(); } PK!ޢZ(""Avendor/symfony/validator/Violation/ConstraintViolationBuilder.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator\Violation; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Contracts\Translation\TranslatorInterface; /** * Default implementation of {@link ConstraintViolationBuilderInterface}. * * @author Bernhard Schussek * * @internal since version 2.5. Code against ConstraintViolationBuilderInterface instead. */ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface { private $violations; private $message; private $parameters; private $root; private $invalidValue; private $propertyPath; private $translator; private $translationDomain; private $plural; private $constraint; private $code; /** * @var mixed */ private $cause; /** * @param string $message The error message as a string or a stringable object */ public function __construct(ConstraintViolationList $violations, ?Constraint $constraint, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = null) { $this->violations = $violations; $this->message = $message; $this->parameters = $parameters; $this->root = $root; $this->propertyPath = $propertyPath; $this->invalidValue = $invalidValue; $this->translator = $translator; $this->translationDomain = $translationDomain; $this->constraint = $constraint; } /** * {@inheritdoc} */ public function atPath(string $path) { $this->propertyPath = PropertyPath::append($this->propertyPath, $path); return $this; } /** * {@inheritdoc} */ public function setParameter(string $key, string $value) { $this->parameters[$key] = $value; return $this; } /** * {@inheritdoc} */ public function setParameters(array $parameters) { $this->parameters = $parameters; return $this; } /** * {@inheritdoc} */ public function setTranslationDomain(string $translationDomain) { $this->translationDomain = $translationDomain; return $this; } /** * {@inheritdoc} */ public function setInvalidValue($invalidValue) { $this->invalidValue = $invalidValue; return $this; } /** * {@inheritdoc} */ public function setPlural(int $number) { $this->plural = $number; return $this; } /** * {@inheritdoc} */ public function setCode(?string $code) { $this->code = $code; return $this; } /** * {@inheritdoc} */ public function setCause($cause) { $this->cause = $cause; return $this; } /** * {@inheritdoc} */ public function addViolation() { if (null === $this->plural) { $translatedMessage = $this->translator->trans( $this->message, $this->parameters, $this->translationDomain ); } else { $translatedMessage = $this->translator->trans( $this->message, ['%count%' => $this->plural] + $this->parameters, $this->translationDomain ); } $this->violations->add(new ConstraintViolation( $translatedMessage, $this->message, $this->parameters, $this->root, $this->propertyPath, $this->invalidValue, $this->plural, $this->code, $this->constraint, $this->cause )); } } PK!UHgDD%vendor/symfony/validator/CHANGELOG.mdnu[CHANGELOG ========= 5.4 --- * Add a `Cidr` constraint to validate CIDR notations * Add a `CssColor` constraint to validate CSS colors * Add support for `ConstraintViolationList::createFromMessage()` * Add error's uid to `Count` and `Length` constraints with "exactly" option enabled 5.3 --- * Add the `normalizer` option to the `Unique` constraint * Add `Validation::createIsValidCallable()` that returns true/false instead of throwing exceptions 5.2.0 ----- * added a `Cascade` constraint to ease validating nested typed object properties * deprecated the `allowEmptyString` option of the `Length` constraint Before: ```php use Symfony\Component\Validator\Constraints as Assert; /** * @Assert\Length(min=5, allowEmptyString=true) */ ``` After: ```php use Symfony\Component\Validator\Constraints as Assert; /** * @Assert\AtLeastOneOf({ * @Assert\Blank(), * @Assert\Length(min=5) * }) */ ``` * added the `Isin` constraint and validator * added the `ULID` constraint and validator * added support for UUIDv6 in `Uuid` constraint * enabled the validator to load constraints from PHP attributes * deprecated the `NumberConstraintTrait` trait * deprecated setting or creating a Doctrine annotation reader via `ValidatorBuilder::enableAnnotationMapping()`, pass `true` as first parameter and additionally call `setDoctrineAnnotationReader()` or `addDefaultDoctrineAnnotationReader()` to set up the annotation reader 5.1.0 ----- * Add `AtLeastOneOf` constraint that is considered to be valid if at least one of the nested constraints is valid * added the `Hostname` constraint and validator * added the `alpha3` option to the `Country` and `Language` constraints * allow to define a reusable set of constraints by extending the `Compound` constraint * added `Sequentially` constraint, to sequentially validate a set of constraints (any violation raised will prevent further validation of the nested constraints) * added the `divisibleBy` option to the `Count` constraint * added the `ExpressionLanguageSyntax` constraint 5.0.0 ----- * an `ExpressionLanguage` instance or null must be passed as the first argument of `ExpressionValidator::__construct()` * removed the `checkDNS` and `dnsMessage` options of the `Url` constraint * removed the `checkMX`, `checkHost` and `strict` options of the `Email` constraint * removed support for validating instances of `\DateTimeInterface` in `DateTimeValidator`, `DateValidator` and `TimeValidator` * removed support for using the `Bic`, `Country`, `Currency`, `Language` and `Locale` constraints without `symfony/intl` * removed support for using the `Email` constraint without `egulias/email-validator` * removed support for using the `Expression` constraint without `symfony/expression-language` * changed default value of `canonicalize` option of `Locale` constraint to `true` * removed `ValidatorBuilderInterface` * passing a null message when instantiating a `ConstraintViolation` is not allowed * changed the default value of `Length::$allowEmptyString` to `false` and made it optional * removed `Symfony\Component\Validator\Mapping\Cache\CacheInterface` in favor of PSR-6. * removed `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead. 4.4.0 ----- * [BC BREAK] using null as `$classValidatorRegexp` value in `PropertyInfoLoader::__construct` will not enable auto-mapping for all classes anymore, use `'{.*}'` instead. * added `EnableAutoMapping` and `DisableAutoMapping` constraints to enable or disable auto mapping for class or a property * using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()` method in 5.0 * deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. Pass it as the first argument instead. * added the `compared_value_path` parameter in violations when using any comparison constraint with the `propertyPath` option. * added support for checking an array of types in `TypeValidator` * added a new `allowEmptyString` option to the `Length` constraint to allow rejecting empty strings when `min` is set, by setting it to `false`. * Added new `minPropertyPath` and `maxPropertyPath` options to `Range` constraint in order to get the value to compare from an array or object * added the `min_limit_path` and `max_limit_path` parameters in violations when using `Range` constraint with respectively the `minPropertyPath` and `maxPropertyPath` options * added a new `notInRangeMessage` option to the `Range` constraint that will be used in the violation builder when both `min` and `max` are not null * added ability to use stringable objects as violation messages * Overriding the methods `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` without the `void` return-type is deprecated. * deprecated `Symfony\Component\Validator\Mapping\Cache\CacheInterface` in favor of PSR-6. * deprecated `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead. * Marked the `ValidatorDataCollector` class as `@final`. 4.3.0 ----- * added `Timezone` constraint * added `NotCompromisedPassword` constraint * added options `iban` and `ibanPropertyPath` to Bic constraint * added UATP cards support to `CardSchemeValidator` * added option `allowNull` to NotBlank constraint * added `Json` constraint * added `Unique` constraint * added a new `normalizer` option to the string constraints and to the `NotBlank` constraint * added `Positive` constraint * added `PositiveOrZero` constraint * added `Negative` constraint * added `NegativeOrZero` constraint 4.2.0 ----- * added a new `UnexpectedValueException` that can be thrown by constraint validators, these exceptions are caught by the validator and are converted into constraint violations * added `DivisibleBy` constraint * decoupled from `symfony/translation` by using `Symfony\Contracts\Translation\TranslatorInterface` * deprecated `ValidatorBuilderInterface` * made `ValidatorBuilder::setTranslator()` final * marked `format` the default option in `DateTime` constraint * deprecated validating instances of `\DateTimeInterface` in `DateTimeValidator`, `DateValidator` and `TimeValidator`. * deprecated using the `Bic`, `Country`, `Currency`, `Language` and `Locale` constraints without `symfony/intl` * deprecated using the `Email` constraint without `egulias/email-validator` * deprecated using the `Expression` constraint without `symfony/expression-language` 4.1.0 ----- * Deprecated the `checkDNS` and `dnsMessage` options of the `Url` constraint. * added a `values` option to the `Expression` constraint * Deprecated use of `Locale` constraint without setting `true` at "canonicalize" option, which will be the default value in 5.0 4.0.0 ----- * Setting the `strict` option of the `Choice` constraint to anything but `true` is not supported anymore. * removed the `DateTimeValidator::PATTERN` constant * removed the `AbstractConstraintValidatorTest` class * removed support for setting the `checkDNS` option of the `Url` constraint to `true` 3.4.0 ----- * added support for validation groups to the `Valid` constraint * not setting the `strict` option of the `Choice` constraint to `true` is deprecated and will throw an exception in Symfony 4.0 * setting the `checkDNS` option of the `Url` constraint to `true` is deprecated in favor of the `Url::CHECK_DNS_TYPE_*` constants values and will throw an exception in Symfony 4.0 * added min/max amount of pixels check to `Image` constraint via `minPixels` and `maxPixels` * added a new "propertyPath" option to comparison constraints in order to get the value to compare from an array or object 3.3.0 ----- * added `AddValidatorInitializersPass` * added `AddConstraintValidatorsPass` * added `ContainerConstraintValidatorFactory` 3.2.0 ----- * deprecated `Tests\Constraints\AbstractConstraintValidatorTest` in favor of `Test\ConstraintValidatorTestCase` * added support for PHP constants in YAML configuration files 3.1.0 ----- * deprecated `DateTimeValidator::PATTERN` constant * added a `format` option to the `DateTime` constraint 2.8.0 ----- * added the BIC (SWIFT-Code) validator 2.7.0 ----- * deprecated `DefaultTranslator` in favor of `Symfony\Component\Translation\IdentityTranslator` * deprecated PHP7-incompatible constraints (Null, True, False) and related validators (NullValidator, TrueValidator, FalseValidator) in favor of their `Is`-prefixed equivalent 2.6.0 ----- * [BC BREAK] `FileValidator` disallow empty files * [BC BREAK] `UserPasswordValidator` source message change * [BC BREAK] added internal `ExecutionContextInterface::setConstraint()` * added `ConstraintViolation::getConstraint()` * [BC BREAK] The `ExpressionValidator` will now evaluate the Expression even when the property value is null or an empty string * deprecated `ClassMetadata::hasMemberMetadatas()` * deprecated `ClassMetadata::getMemberMetadatas()` * deprecated `ClassMetadata::addMemberMetadata()` * [BC BREAK] added `Mapping\MetadataInterface::getConstraints()` * added generic "payload" option to all constraints for attaching domain-specific data * [BC BREAK] added `ConstraintViolationBuilderInterface::setCause()` 2.5.0 ----- * deprecated `ApcCache` in favor of `DoctrineCache` * added `DoctrineCache` to adapt any Doctrine cache * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` * [BC BREAK] changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array * `Callback` can now be put onto properties (useful when you pass a closure to the constraint) * deprecated `ClassBasedInterface` * deprecated `MetadataInterface` * deprecated `PropertyMetadataInterface` * deprecated `PropertyMetadataContainerInterface` * deprecated `Mapping\ElementMetadata` * added `Mapping\MetadataInterface` * added `Mapping\ClassMetadataInterface` * added `Mapping\PropertyMetadataInterface` * added `Mapping\GenericMetadata` * added `Mapping\CascadingStrategy` * added `Mapping\TraversalStrategy` * deprecated `Mapping\ClassMetadata::accept()` * deprecated `Mapping\MemberMetadata::accept()` * removed array type hint of `Mapping\ClassMetadata::setGroupSequence()` * deprecated `MetadataFactoryInterface` * deprecated `Mapping\BlackholeMetadataFactory` * deprecated `Mapping\ClassMetadataFactory` * added `Mapping\Factory\MetadataFactoryInterface` * added `Mapping\Factory\BlackHoleMetadataFactory` * added `Mapping\Factory\LazyLoadingMetadataFactory` * deprecated `ExecutionContextInterface` * deprecated `ExecutionContext` * deprecated `GlobalExecutionContextInterface` * added `Context\ExecutionContextInterface` * added `Context\ExecutionContext` * added `Context\ExecutionContextFactoryInterface` * added `Context\ExecutionContextFactory` * deprecated `ValidatorInterface` * deprecated `Validator` * deprecated `ValidationVisitorInterface` * deprecated `ValidationVisitor` * added `Validator\ValidatorInterface` * added `Validator\RecursiveValidator` * added `Validator\ContextualValidatorInterface` * added `Validator\RecursiveContextualValidator` * added `Violation\ConstraintViolationBuilderInterface` * added `Violation\ConstraintViolationBuilder` * added `ConstraintViolation::getParameters()` * added `ConstraintViolation::getPlural()` * added `Constraints\Traverse` * deprecated `$deep` property in `Constraints\Valid` * added `ValidatorBuilderInterface::setApiVersion()` * added `Validation::API_VERSION_2_4` * added `Validation::API_VERSION_2_5` * added `Exception\OutOfBoundsException` * added `Exception\UnsupportedMetadataException` * made `Exception\ValidatorException` extend `Exception\RuntimeException` * added `Util\PropertyPath` * made the PropertyAccess component an optional dependency * deprecated `ValidatorBuilder::setPropertyAccessor()` * deprecated `validate` and `validateValue` on `Validator\Context\ExecutionContext` use `getValidator()` together with `inContext()` instead 2.4.0 ----- * added a constraint the uses the expression language * added `minRatio`, `maxRatio`, `allowSquare`, `allowLandscape`, and `allowPortrait` to Image validator 2.3.29 ------ * fixed compatibility with PHP7 and up by introducing new constraints (IsNull, IsTrue, IsFalse) and related validators (IsNullValidator, IsTrueValidator, IsFalseValidator) 2.3.0 ----- * added the ISBN, ISSN, and IBAN validators * copied the constraints `Optional` and `Required` to the `Symfony\Component\Validator\Constraints\` namespace and deprecated the original classes. * added comparison validators (EqualTo, NotEqualTo, LessThan, LessThanOrEqualTo, GreaterThan, GreaterThanOrEqualTo, IdenticalTo, NotIdenticalTo) 2.2.0 ----- * added a CardScheme validator * added a Luhn validator * moved @api-tags from `Validator` to `ValidatorInterface` * moved @api-tags from `ConstraintViolation` to the new `ConstraintViolationInterface` * moved @api-tags from `ConstraintViolationList` to the new `ConstraintViolationListInterface` * moved @api-tags from `ExecutionContext` to the new `ExecutionContextInterface` * [BC BREAK] `ConstraintValidatorInterface::initialize` is now type hinted against `ExecutionContextInterface` instead of `ExecutionContext` * [BC BREAK] changed the visibility of the properties in `Validator` from protected to private * deprecated `ClassMetadataFactoryInterface` in favor of the new `MetadataFactoryInterface` * deprecated `ClassMetadataFactory::getClassMetadata` in favor of `getMetadataFor` * created `MetadataInterface`, `PropertyMetadataInterface`, `ClassBasedInterface` and `PropertyMetadataContainerInterface` * deprecated `GraphWalker` in favor of the new `ValidationVisitorInterface` * deprecated `ExecutionContext::addViolationAtPath` * deprecated `ExecutionContext::addViolationAtSubPath` in favor of `ExecutionContextInterface::addViolationAt` * deprecated `ExecutionContext::getCurrentClass` in favor of `ExecutionContextInterface::getClassName` * deprecated `ExecutionContext::getCurrentProperty` in favor of `ExecutionContextInterface::getPropertyName` * deprecated `ExecutionContext::getCurrentValue` in favor of `ExecutionContextInterface::getValue` * deprecated `ExecutionContext::getGraphWalker` in favor of `ExecutionContextInterface::validate` and `ExecutionContextInterface::validateValue` * improved `ValidatorInterface::validateValue` to accept arrays of constraints * changed `ValidatorInterface::getMetadataFactory` to return a `MetadataFactoryInterface` instead of a `ClassMetadataFactoryInterface` * removed `ClassMetadataFactoryInterface` type hint from `ValidatorBuilderInterface::setMetadataFactory`. As of Symfony 2.3, this method will be typed against `MetadataFactoryInterface` instead. * [BC BREAK] the switches `traverse` and `deep` in the `Valid` constraint and in `GraphWalker::walkReference` are ignored for arrays now. Arrays are always traversed recursively. * added dependency to Translation component * violation messages are now translated with a TranslatorInterface implementation * [BC BREAK] inserted argument `$message` in the constructor of `ConstraintViolation` * [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `ExecutionContext` * [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `GraphWalker` * [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `ValidationVisitor` * [BC BREAK] inserted arguments `$translator` and `$translationDomain` in the constructor of `Validator` * [BC BREAK] added `setTranslator()` and `setTranslationDomain()` to `ValidatorBuilderInterface` * improved the Validator to support pluralized messages by default * [BC BREAK] changed the source of all pluralized messages in the translation files to the pluralized version * added ExceptionInterface, BadMethodCallException and InvalidArgumentException 2.1.0 ----- * added support for `ctype_*` assertions in `TypeValidator` * improved the ImageValidator with min width, max width, min height, and max height constraints * added support for MIME with wildcard in FileValidator * changed Collection validator to add "missing" and "extra" errors to individual fields * changed default value for `extraFieldsMessage` and `missingFieldsMessage` in Collection constraint * made ExecutionContext immutable * deprecated Constraint methods `setMessage`, `getMessageTemplate` and `getMessageParameters` * added support for dynamic group sequences with the GroupSequenceProvider pattern * [BC BREAK] ConstraintValidatorInterface method `isValid` has been renamed to `validate`, its return value was dropped. ConstraintValidator still contains `isValid` for BC * [BC BREAK] collections in fields annotated with `Valid` are not traversed recursively anymore by default. `Valid` contains a new property `deep` which enables the BC behavior. * added Count constraint * added Length constraint * added Range constraint * deprecated the Min and Max constraints * deprecated the MinLength and MaxLength constraints * added Validation and ValidatorBuilderInterface * deprecated ValidatorContext, ValidatorContextInterface and ValidatorFactory PK! &vendor/symfony/validator/composer.jsonnu[{ "name": "symfony/validator", "type": "library", "description": "Provides tools to validate values", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "~1.0", "symfony/polyfill-php80": "^1.16", "symfony/polyfill-php81": "^1.22", "symfony/translation-contracts": "^1.1|^2|^3" }, "require-dev": { "symfony/console": "^4.4|^5.0|^6.0", "symfony/finder": "^4.4|^5.0|^6.0", "symfony/http-client": "^4.4|^5.0|^6.0", "symfony/http-foundation": "^4.4|^5.0|^6.0", "symfony/http-kernel": "^4.4|^5.0|^6.0", "symfony/intl": "^4.4|^5.0|^6.0", "symfony/yaml": "^4.4|^5.0|^6.0", "symfony/config": "^4.4|^5.0|^6.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", "symfony/expression-language": "^5.1|^6.0", "symfony/cache": "^4.4|^5.0|^6.0", "symfony/mime": "^4.4|^5.0|^6.0", "symfony/property-access": "^5.4|^6.0", "symfony/property-info": "^5.3|^6.0", "symfony/translation": "^5.4.35|~6.3.12|^6.4.3", "doctrine/annotations": "^1.13|^2", "doctrine/cache": "^1.11|^2.0", "egulias/email-validator": "^2.1.10|^3|^4" }, "conflict": { "doctrine/annotations": "<1.13", "doctrine/cache": "<1.11", "doctrine/lexer": "<1.1", "symfony/dependency-injection": "<4.4", "symfony/expression-language": "<5.1", "symfony/http-kernel": "<4.4", "symfony/intl": "<4.4", "symfony/property-info": "<5.3", "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3", "symfony/yaml": "<4.4" }, "suggest": { "psr/cache-implementation": "For using the mapping cache.", "symfony/http-foundation": "", "symfony/intl": "", "symfony/translation": "For translating validation errors.", "symfony/yaml": "", "symfony/config": "", "egulias/email-validator": "Strict (RFC compliant) email validation", "symfony/property-access": "For accessing properties within comparison constraints", "symfony/property-info": "To automatically add NotNull and Type constraints", "symfony/expression-language": "For using the Expression validator and the ExpressionLanguageSyntax constraints" }, "autoload": { "psr-4": { "Symfony\\Component\\Validator\\": "" }, "exclude-from-classmap": [ "/Tests/", "/Resources/bin/" ] }, "minimum-stability": "dev" } PK!s'''vendor/symfony/validator/Constraint.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\InvalidOptionsException; use Symfony\Component\Validator\Exception\MissingOptionsException; /** * Contains the properties of a constraint definition. * * A constraint can be defined on a class, a property or a getter method. * The Constraint class encapsulates all the configuration required for * validating this class, property or getter result successfully. * * Constraint instances are immutable and serializable. * * @author Bernhard Schussek */ abstract class Constraint { /** * The name of the group given to all constraints with no explicit group. */ public const DEFAULT_GROUP = 'Default'; /** * Marks a constraint that can be put onto classes. */ public const CLASS_CONSTRAINT = 'class'; /** * Marks a constraint that can be put onto properties. */ public const PROPERTY_CONSTRAINT = 'property'; /** * Maps error codes to the names of their constants. */ protected static $errorNames = []; /** * Domain-specific data attached to a constraint. * * @var mixed */ public $payload; /** * The groups that the constraint belongs to. * * @var string[] */ public $groups; /** * Returns the name of the given error code. * * @return string * * @throws InvalidArgumentException If the error code does not exist */ public static function getErrorName(string $errorCode) { if (!isset(static::$errorNames[$errorCode])) { throw new InvalidArgumentException(sprintf('The error code "%s" does not exist for constraint of type "%s".', $errorCode, static::class)); } return static::$errorNames[$errorCode]; } /** * Initializes the constraint with options. * * You should pass an associative array. The keys should be the names of * existing properties in this class. The values should be the value for these * properties. * * Alternatively you can override the method getDefaultOption() to return the * name of an existing property. If no associative array is passed, this * property is set instead. * * You can force that certain options are set by overriding * getRequiredOptions() to return the names of these options. If any * option is not set here, an exception is thrown. * * @param mixed $options The options (as associative array) * or the value for the default * option (any other type) * @param string[] $groups An array of validation groups * @param mixed $payload Domain-specific data attached to a constraint * * @throws InvalidOptionsException When you pass the names of non-existing * options * @throws MissingOptionsException When you don't pass any of the options * returned by getRequiredOptions() * @throws ConstraintDefinitionException When you don't pass an associative * array, but getDefaultOption() returns * null */ public function __construct($options = null, ?array $groups = null, $payload = null) { unset($this->groups); // enable lazy initialization $options = $this->normalizeOptions($options); if (null !== $groups) { $options['groups'] = $groups; } $options['payload'] = $payload ?? $options['payload'] ?? null; foreach ($options as $name => $value) { $this->$name = $value; } } protected function normalizeOptions($options): array { $normalizedOptions = []; $defaultOption = $this->getDefaultOption(); $invalidOptions = []; $missingOptions = array_flip((array) $this->getRequiredOptions()); $knownOptions = get_class_vars(static::class); if (\is_array($options) && isset($options['value']) && !property_exists($this, 'value')) { if (null === $defaultOption) { throw new ConstraintDefinitionException(sprintf('No default option is configured for constraint "%s".', static::class)); } $options[$defaultOption] = $options['value']; unset($options['value']); } if (\is_array($options)) { reset($options); } if ($options && \is_array($options) && \is_string(key($options))) { foreach ($options as $option => $value) { if (\array_key_exists($option, $knownOptions)) { $normalizedOptions[$option] = $value; unset($missingOptions[$option]); } else { $invalidOptions[] = $option; } } } elseif (null !== $options && !(\is_array($options) && 0 === \count($options))) { if (null === $defaultOption) { throw new ConstraintDefinitionException(sprintf('No default option is configured for constraint "%s".', static::class)); } if (\array_key_exists($defaultOption, $knownOptions)) { $normalizedOptions[$defaultOption] = $options; unset($missingOptions[$defaultOption]); } else { $invalidOptions[] = $defaultOption; } } if (\count($invalidOptions) > 0) { throw new InvalidOptionsException(sprintf('The options "%s" do not exist in constraint "%s".', implode('", "', $invalidOptions), static::class), $invalidOptions); } if (\count($missingOptions) > 0) { throw new MissingOptionsException(sprintf('The options "%s" must be set for constraint "%s".', implode('", "', array_keys($missingOptions)), static::class), array_keys($missingOptions)); } return $normalizedOptions; } /** * Sets the value of a lazily initialized option. * * Corresponding properties are added to the object on first access. Hence * this method will be called at most once per constraint instance and * option name. * * @param mixed $value The value to set * * @throws InvalidOptionsException If an invalid option name is given */ public function __set(string $option, $value) { if ('groups' === $option) { $this->groups = (array) $value; return; } throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint "%s".', $option, static::class), [$option]); } /** * Returns the value of a lazily initialized option. * * Corresponding properties are added to the object on first access. Hence * this method will be called at most once per constraint instance and * option name. * * @return mixed * * @throws InvalidOptionsException If an invalid option name is given */ public function __get(string $option) { if ('groups' === $option) { $this->groups = [self::DEFAULT_GROUP]; return $this->groups; } throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint "%s".', $option, static::class), [$option]); } /** * @return bool */ public function __isset(string $option) { return 'groups' === $option; } /** * Adds the given group if this constraint is in the Default group. */ public function addImplicitGroupName(string $group) { if (null === $this->groups && \array_key_exists('groups', (array) $this)) { throw new \LogicException(sprintf('"%s::$groups" is set to null. Did you forget to call "%s::__construct()"?', static::class, self::class)); } if (\in_array(self::DEFAULT_GROUP, $this->groups) && !\in_array($group, $this->groups)) { $this->groups[] = $group; } } /** * Returns the name of the default option. * * Override this method to define a default option. * * @return string|null * * @see __construct() */ public function getDefaultOption() { return null; } /** * Returns the name of the required options. * * Override this method if you want to define required options. * * @return string[] * * @see __construct() */ public function getRequiredOptions() { return []; } /** * Returns the name of the class that validates this constraint. * * By default, this is the fully qualified name of the constraint class * suffixed with "Validator". You can override this method to change that * behavior. * * @return string */ public function validatedBy() { return static::class.'Validator'; } /** * Returns whether the constraint can be put onto classes, properties or * both. * * This method should return one or more of the constants * Constraint::CLASS_CONSTRAINT and Constraint::PROPERTY_CONSTRAINT. * * @return string|string[] One or more constant values */ public function getTargets() { return self::PROPERTY_CONSTRAINT; } /** * Optimizes the serialized value to minimize storage space. * * @internal */ public function __sleep(): array { // Initialize "groups" option if it is not set $this->groups; return array_keys(get_object_vars($this)); } } PK!=@vendor/symfony/validator/ConstraintValidatorFactoryInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; /** * Specifies an object able to return the correct ConstraintValidatorInterface * instance given a Constraint object. */ interface ConstraintValidatorFactoryInterface { /** * Given a Constraint, this returns the ConstraintValidatorInterface * object that should be used to verify its validity. * * @return ConstraintValidatorInterface */ public function getInstance(Constraint $constraint); } PK!/!7vendor/symfony/validator/ConstraintValidatorFactory.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Constraints\ExpressionValidator; /** * Default implementation of the ConstraintValidatorFactoryInterface. * * This enforces the convention that the validatedBy() method on any * Constraint will return the class name of the ConstraintValidator that * should validate the Constraint. * * @author Bernhard Schussek */ class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface { protected $validators = []; public function __construct() { } /** * {@inheritdoc} */ public function getInstance(Constraint $constraint) { $className = $constraint->validatedBy(); if (!isset($this->validators[$className])) { $this->validators[$className] = 'validator.expression' === $className ? new ExpressionValidator() : new $className(); } return $this->validators[$className]; } } PK!.9vendor/symfony/validator/ConstraintValidatorInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * @author Bernhard Schussek */ interface ConstraintValidatorInterface { /** * Initializes the constraint validator. */ public function initialize(ExecutionContextInterface $context); /** * Checks if the passed value is valid. * * @param mixed $value The value that should be validated */ public function validate($value, Constraint $constraint); } PK!,0vendor/symfony/validator/ConstraintValidator.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * Base class for constraint validators. * * @author Bernhard Schussek */ abstract class ConstraintValidator implements ConstraintValidatorInterface { /** * Whether to format {@link \DateTime} objects, either with the {@link \IntlDateFormatter} * (if it is available) or as RFC-3339 dates ("Y-m-d H:i:s"). */ public const PRETTY_DATE = 1; /** * Whether to cast objects with a "__toString()" method to strings. */ public const OBJECT_TO_STRING = 2; /** * @var ExecutionContextInterface */ protected $context; /** * {@inheritdoc} */ public function initialize(ExecutionContextInterface $context) { $this->context = $context; } /** * Returns a string representation of the type of the value. * * This method should be used if you pass the type of a value as * message parameter to a constraint violation. Note that such * parameters should usually not be included in messages aimed at * non-technical people. * * @param mixed $value The value to return the type of * * @return string */ protected function formatTypeOf($value) { return get_debug_type($value); } /** * Returns a string representation of the value. * * This method returns the equivalent PHP tokens for most scalar types * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped * in double quotes ("). Objects, arrays and resources are formatted as * "object", "array" and "resource". If the $format bitmask contains * the PRETTY_DATE bit, then {@link \DateTime} objects will be formatted * with the {@link \IntlDateFormatter}. If it is not available, they will be * formatted as RFC-3339 dates ("Y-m-d H:i:s"). * * Be careful when passing message parameters to a constraint violation * that (may) contain objects, arrays or resources. These parameters * should only be displayed for technical users. Non-technical users * won't know what an "object", "array" or "resource" is and will be * confused by the violation message. * * @param mixed $value The value to format as string * @param int $format A bitwise combination of the format * constants in this class * * @return string */ protected function formatValue($value, int $format = 0) { if (($format & self::PRETTY_DATE) && $value instanceof \DateTimeInterface) { if (class_exists(\IntlDateFormatter::class)) { $formatter = new \IntlDateFormatter(\Locale::getDefault(), \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, 'UTC'); return $formatter->format(new \DateTime( $value->format('Y-m-d H:i:s.u'), new \DateTimeZone('UTC') )); } return $value->format('Y-m-d H:i:s'); } if ($value instanceof \UnitEnum) { return $value->name; } if (\is_object($value)) { if (($format & self::OBJECT_TO_STRING) && method_exists($value, '__toString')) { return $value->__toString(); } return 'object'; } if (\is_array($value)) { return 'array'; } if (\is_string($value)) { return '"'.$value.'"'; } if (\is_resource($value)) { return 'resource'; } if (null === $value) { return 'null'; } if (false === $value) { return 'false'; } if (true === $value) { return 'true'; } return (string) $value; } /** * Returns a string representation of a list of values. * * Each of the values is converted to a string using * {@link formatValue()}. The values are then concatenated with commas. * * @param array $values A list of values * @param int $format A bitwise combination of the format * constants in this class * * @return string * * @see formatValue() */ protected function formatValues(array $values, int $format = 0) { foreach ($values as $key => $value) { $values[$key] = $this->formatValue($value, $format); } return implode(', ', $values); } } PK!\FF9vendor/symfony/validator/ConstraintViolationInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; /** * A violation of a constraint that happened during validation. * * For each constraint that fails during validation one or more violations are * created. The violations store the violation message, the path to the failing * element in the validation graph and the root element that was originally * passed to the validator. For example, take the following graph: * * (Person)---(firstName: string) * \ * (address: Address)---(street: string) * * If the Person object is validated and validation fails for the * "firstName" property, the generated violation has the Person * instance as root and the property path "firstName". If validation fails * for the "street" property of the related Address instance, the root * element is still the person, but the property path is "address.street". * * @author Bernhard Schussek */ interface ConstraintViolationInterface { /** * Returns the violation message. * * @return string|\Stringable */ public function getMessage(); /** * Returns the raw violation message. * * The raw violation message contains placeholders for the parameters * returned by {@link getParameters}. Typically you'll pass the * message template and parameters to a translation engine. * * @return string The raw violation message */ public function getMessageTemplate(); /** * Returns the parameters to be inserted into the raw violation message. * * @return array a possibly empty list of parameters indexed by the names * that appear in the message template * * @see getMessageTemplate() */ public function getParameters(); /** * Returns a number for pluralizing the violation message. * * For example, the message template could have different translation based * on a parameter "choices": * *
    *
  • Please select exactly one entry. (choices=1)
  • *
  • Please select two entries. (choices=2)
  • *
* * This method returns the value of the parameter for choosing the right * pluralization form (in this case "choices"). * * @return int|null The number to use to pluralize of the message */ public function getPlural(); /** * Returns the root element of the validation. * * @return mixed The value that was passed originally to the validator when * the validation was started. Because the validator traverses * the object graph, the value at which the violation occurs * is not necessarily the value that was originally validated. */ public function getRoot(); /** * Returns the property path from the root element to the violation. * * @return string The property path indicates how the validator reached * the invalid value from the root element. If the root * element is a Person instance with a property * "address" that contains an Address instance * with an invalid property "street", the generated property * path is "address.street". Property access is denoted by * dots, while array access is denoted by square brackets, * for example "addresses[1].street". */ public function getPropertyPath(); /** * Returns the value that caused the violation. * * @return mixed the invalid value that caused the validated constraint to * fail */ public function getInvalidValue(); /** * Returns a machine-digestible error code for the violation. * * @return string|null */ public function getCode(); } PK!9=vendor/symfony/validator/ConstraintViolationListInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; /** * A list of constraint violations. * * @author Bernhard Schussek * * @extends \ArrayAccess * @extends \Traversable */ interface ConstraintViolationListInterface extends \Traversable, \Countable, \ArrayAccess { /** * Adds a constraint violation to this list. */ public function add(ConstraintViolationInterface $violation); /** * Merges an existing violation list into this list. */ public function addAll(self $otherList); /** * Returns the violation at a given offset. * * @param int $offset The offset of the violation * * @return ConstraintViolationInterface * * @throws \OutOfBoundsException if the offset does not exist */ public function get(int $offset); /** * Returns whether the given offset exists. * * @param int $offset The violation offset * * @return bool */ public function has(int $offset); /** * Sets a violation at a given offset. * * @param int $offset The violation offset */ public function set(int $offset, ConstraintViolationInterface $violation); /** * Removes a violation at a given offset. * * @param int $offset The offset to remove */ public function remove(int $offset); } PK!=4vendor/symfony/validator/ConstraintViolationList.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; /** * Default implementation of {@ConstraintViolationListInterface}. * * @author Bernhard Schussek * * @implements \IteratorAggregate */ class ConstraintViolationList implements \IteratorAggregate, ConstraintViolationListInterface { /** * @var list */ private $violations = []; /** * Creates a new constraint violation list. * * @param iterable $violations The constraint violations to add to the list */ public function __construct(iterable $violations = []) { foreach ($violations as $violation) { $this->add($violation); } } public static function createFromMessage(string $message): self { $self = new self(); $self->add(new ConstraintViolation($message, '', [], null, '', null)); return $self; } /** * Converts the violation into a string for debugging purposes. * * @return string */ public function __toString() { $string = ''; foreach ($this->violations as $violation) { $string .= $violation."\n"; } return $string; } /** * {@inheritdoc} */ public function add(ConstraintViolationInterface $violation) { $this->violations[] = $violation; } /** * {@inheritdoc} */ public function addAll(ConstraintViolationListInterface $otherList) { foreach ($otherList as $violation) { $this->violations[] = $violation; } } /** * {@inheritdoc} */ public function get(int $offset) { if (!isset($this->violations[$offset])) { throw new \OutOfBoundsException(sprintf('The offset "%s" does not exist.', $offset)); } return $this->violations[$offset]; } /** * {@inheritdoc} */ public function has(int $offset) { return isset($this->violations[$offset]); } /** * {@inheritdoc} */ public function set(int $offset, ConstraintViolationInterface $violation) { $this->violations[$offset] = $violation; } /** * {@inheritdoc} */ public function remove(int $offset) { unset($this->violations[$offset]); } /** * {@inheritdoc} * * @return \ArrayIterator */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->violations); } /** * @return int */ #[\ReturnTypeWillChange] public function count() { return \count($this->violations); } /** * @return bool */ #[\ReturnTypeWillChange] public function offsetExists($offset) { return $this->has($offset); } /** * {@inheritdoc} * * @return ConstraintViolationInterface */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); } /** * {@inheritdoc} * * @return void */ #[\ReturnTypeWillChange] public function offsetSet($offset, $violation) { if (null === $offset) { $this->add($violation); } else { $this->set($offset, $violation); } } /** * {@inheritdoc} * * @return void */ #[\ReturnTypeWillChange] public function offsetUnset($offset) { $this->remove($offset); } /** * Creates iterator for errors with specific codes. * * @param string|string[] $codes The codes to find * * @return static */ public function findByCodes($codes) { $codes = (array) $codes; $violations = []; foreach ($this as $violation) { if (\in_array($violation->getCode(), $codes, true)) { $violations[] = $violation; } } return new static($violations); } } PK!0vendor/symfony/validator/ConstraintViolation.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; /** * Default implementation of {@ConstraintViolationInterface}. * * @author Bernhard Schussek */ class ConstraintViolation implements ConstraintViolationInterface { private $message; private $messageTemplate; private $parameters; private $plural; private $root; private $propertyPath; private $invalidValue; private $constraint; private $code; private $cause; /** * Creates a new constraint violation. * * @param string|\Stringable $message The violation message as a string or a stringable object * @param string|null $messageTemplate The raw violation message * @param array $parameters The parameters to substitute in the * raw violation message * @param mixed $root The value originally passed to the * validator * @param string|null $propertyPath The property path from the root * value to the invalid value * @param mixed $invalidValue The invalid value that caused this * violation * @param int|null $plural The number for determining the plural * form when translating the message * @param string|null $code The error code of the violation * @param Constraint|null $constraint The constraint whose validation * caused the violation * @param mixed $cause The cause of the violation */ public function __construct($message, ?string $messageTemplate, array $parameters, $root, ?string $propertyPath, $invalidValue, ?int $plural = null, ?string $code = null, ?Constraint $constraint = null, $cause = null) { if (!\is_string($message) && !(\is_object($message) && method_exists($message, '__toString'))) { throw new \TypeError('Constraint violation message should be a string or an object which implements the __toString() method.'); } $this->message = $message; $this->messageTemplate = $messageTemplate; $this->parameters = $parameters; $this->plural = $plural; $this->root = $root; $this->propertyPath = $propertyPath; $this->invalidValue = $invalidValue; $this->constraint = $constraint; $this->code = $code; $this->cause = $cause; } /** * Converts the violation into a string for debugging purposes. * * @return string */ public function __toString() { if (\is_object($this->root)) { $class = 'Object('.\get_class($this->root).')'; } elseif (\is_array($this->root)) { $class = 'Array'; } else { $class = (string) $this->root; } $propertyPath = (string) $this->propertyPath; if ('' !== $propertyPath && '[' !== $propertyPath[0] && '' !== $class) { $class .= '.'; } if (null !== ($code = $this->code) && '' !== $code) { $code = ' (code '.$code.')'; } return $class.$propertyPath.":\n ".$this->getMessage().$code; } /** * {@inheritdoc} */ public function getMessageTemplate() { return (string) $this->messageTemplate; } /** * {@inheritdoc} */ public function getParameters() { return $this->parameters; } /** * {@inheritdoc} */ public function getPlural() { return $this->plural; } /** * {@inheritdoc} */ public function getMessage() { return $this->message; } /** * {@inheritdoc} */ public function getRoot() { return $this->root; } /** * {@inheritdoc} */ public function getPropertyPath() { return (string) $this->propertyPath; } /** * {@inheritdoc} */ public function getInvalidValue() { return $this->invalidValue; } /** * Returns the constraint whose validation caused the violation. * * @return Constraint|null */ public function getConstraint() { return $this->constraint; } /** * Returns the cause of the violation. * * @return mixed */ public function getCause() { return $this->cause; } /** * {@inheritdoc} */ public function getCode() { return $this->code; } } PK!D@vendor/symfony/validator/ContainerConstraintValidatorFactory.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Psr\Container\ContainerInterface; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; /** * Uses a service container to create constraint validators. * * @author Kris Wallsmith */ class ContainerConstraintValidatorFactory implements ConstraintValidatorFactoryInterface { private $container; private $validators; public function __construct(ContainerInterface $container) { $this->container = $container; $this->validators = []; } /** * {@inheritdoc} * * @throws ValidatorException When the validator class does not exist * @throws UnexpectedTypeException When the validator is not an instance of ConstraintValidatorInterface */ public function getInstance(Constraint $constraint) { $name = $constraint->validatedBy(); if (!isset($this->validators[$name])) { if ($this->container->has($name)) { $this->validators[$name] = $this->container->get($name); } else { if (!class_exists($name)) { throw new ValidatorException(sprintf('Constraint validator "%s" does not exist or is not enabled. Check the "validatedBy" method in your constraint class "%s".', $name, get_debug_type($constraint))); } $this->validators[$name] = new $name(); } } if (!$this->validators[$name] instanceof ConstraintValidatorInterface) { throw new UnexpectedTypeException($this->validators[$name], ConstraintValidatorInterface::class); } return $this->validators[$name]; } } PK!-;vendor/symfony/validator/GroupSequenceProviderInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Constraints\GroupSequence; /** * Defines the interface for a group sequence provider. */ interface GroupSequenceProviderInterface { /** * Returns which validation groups should be used for a certain state * of the object. * * @return string[]|string[][]|GroupSequence */ public function getGroupSequence(); } PK!U,, vendor/symfony/validator/LICENSEnu[Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!"7vendor/symfony/validator/ObjectInitializerInterface.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; /** * Prepares an object for validation. * * Concrete implementations of this interface are used by {@link Validator\ContextualValidatorInterface} * to initialize objects just before validating them. * * @author Fabien Potencier * @author Bernhard Schussek */ interface ObjectInitializerInterface { public function initialize(object $object); } PK!~ %CC"vendor/symfony/validator/README.mdnu[Validator Component =================== The Validator component provides tools to validate values following the [JSR-303 Bean Validation specification][1]. Resources --------- * [Documentation](https://symfony.com/doc/current/components/validator.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) [1]: https://jcp.org/en/jsr/detail?id=303 PK!$͘ 'vendor/symfony/validator/Validation.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\Validator\ValidatorInterface; /** * Entry point for the Validator component. * * @author Bernhard Schussek */ final class Validation { /** * Creates a callable chain of constraints. * * @param Constraint|ValidatorInterface|null $constraintOrValidator * * @return callable($value) */ public static function createCallable($constraintOrValidator = null, Constraint ...$constraints): callable { $validator = self::createIsValidCallable($constraintOrValidator, ...$constraints); return static function ($value) use ($validator) { if (!$validator($value, $violations)) { throw new ValidationFailedException($value, $violations); } return $value; }; } /** * Creates a callable that returns true/false instead of throwing validation exceptions. * * @param Constraint|ValidatorInterface|null $constraintOrValidator * * @return callable($value, &$violations = null): bool */ public static function createIsValidCallable($constraintOrValidator = null, Constraint ...$constraints): callable { $validator = $constraintOrValidator; if ($constraintOrValidator instanceof Constraint) { $constraints = \func_get_args(); $validator = null; } elseif (null !== $constraintOrValidator && !$constraintOrValidator instanceof ValidatorInterface) { throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a "%s" or a "%s" object, "%s" given.', __METHOD__, Constraint::class, ValidatorInterface::class, get_debug_type($constraintOrValidator))); } $validator = $validator ?? self::createValidator(); return static function ($value, &$violations = null) use ($constraints, $validator) { $violations = $validator->validate($value, $constraints); return 0 === $violations->count(); }; } /** * Creates a new validator. * * If you want to configure the validator, use * {@link createValidatorBuilder()} instead. */ public static function createValidator(): ValidatorInterface { return self::createValidatorBuilder()->getValidator(); } /** * Creates a configurable builder for validator objects. */ public static function createValidatorBuilder(): ValidatorBuilder { return new ValidatorBuilder(); } /** * This class cannot be instantiated. */ private function __construct() { } } PK!==66-vendor/symfony/validator/ValidatorBuilder.phpnu[ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Validator; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Cache\ArrayCache; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; // Help opcache.preload discover always-needed symbols class_exists(TranslatorInterface::class); class_exists(LocaleAwareInterface::class); class_exists(TranslatorTrait::class); /** * @author Bernhard Schussek */ class ValidatorBuilder { private $initializers = []; private $loaders = []; private $xmlMappings = []; private $yamlMappings = []; private $methodMappings = []; /** * @var Reader|null */ private $annotationReader; private $enableAnnotationMapping = false; /** * @var MetadataFactoryInterface|null */ private $metadataFactory; /** * @var ConstraintValidatorFactoryInterface|null */ private $validatorFactory; /** * @var CacheItemPoolInterface|null */ private $mappingCache; /** * @var TranslatorInterface|null */ private $translator; /** * @var string|null */ private $translationDomain; /** * Adds an object initializer to the validator. * * @return $this */ public function addObjectInitializer(ObjectInitializerInterface $initializer) { $this->initializers[] = $initializer; return $this; } /** * Adds a list of object initializers to the validator. * * @param ObjectInitializerInterface[] $initializers * * @return $this */ public function addObjectInitializers(array $initializers) { $this->initializers = array_merge($this->initializers, $initializers); return $this; } /** * Adds an XML constraint mapping file to the validator. * * @return $this */ public function addXmlMapping(string $path) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot add custom mappings after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->xmlMappings[] = $path; return $this; } /** * Adds a list of XML constraint mapping files to the validator. * * @param string[] $paths The paths to the mapping files * * @return $this */ public function addXmlMappings(array $paths) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot add custom mappings after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->xmlMappings = array_merge($this->xmlMappings, $paths); return $this; } /** * Adds a YAML constraint mapping file to the validator. * * @param string $path The path to the mapping file * * @return $this */ public function addYamlMapping(string $path) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot add custom mappings after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->yamlMappings[] = $path; return $this; } /** * Adds a list of YAML constraint mappings file to the validator. * * @param string[] $paths The paths to the mapping files * * @return $this */ public function addYamlMappings(array $paths) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot add custom mappings after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->yamlMappings = array_merge($this->yamlMappings, $paths); return $this; } /** * Enables constraint mapping using the given static method. * * @return $this */ public function addMethodMapping(string $methodName) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot add custom mappings after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->methodMappings[] = $methodName; return $this; } /** * Enables constraint mapping using the given static methods. * * @param string[] $methodNames The names of the methods * * @return $this */ public function addMethodMappings(array $methodNames) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot add custom mappings after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->methodMappings = array_merge($this->methodMappings, $methodNames); return $this; } /** * Enables annotation based constraint mapping. * * @param bool $skipDoctrineAnnotations * * @return $this */ public function enableAnnotationMapping(/* bool $skipDoctrineAnnotations = true */) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot enable annotation mapping after setting a custom metadata factory. Configure your metadata factory instead.'); } $skipDoctrineAnnotations = 1 > \func_num_args() ? false : func_get_arg(0); if (false === $skipDoctrineAnnotations || null === $skipDoctrineAnnotations) { trigger_deprecation('symfony/validator', '5.2', 'Not passing true as first argument to "%s" is deprecated. Pass true and call "addDefaultDoctrineAnnotationReader()" if you want to enable annotation mapping with Doctrine Annotations.', __METHOD__); $this->addDefaultDoctrineAnnotationReader(); } elseif ($skipDoctrineAnnotations instanceof Reader) { trigger_deprecation('symfony/validator', '5.2', 'Passing an instance of "%s" as first argument to "%s" is deprecated. Pass true instead and call setDoctrineAnnotationReader() if you want to enable annotation mapping with Doctrine Annotations.', get_debug_type($skipDoctrineAnnotations), __METHOD__); $this->setDoctrineAnnotationReader($skipDoctrineAnnotations); } elseif (true !== $skipDoctrineAnnotations) { throw new \TypeError(sprintf('"%s": Argument 1 is expected to be a boolean, "%s" given.', __METHOD__, get_debug_type($skipDoctrineAnnotations))); } $this->enableAnnotationMapping = true; return $this; } /** * Disables annotation based constraint mapping. * * @return $this */ public function disableAnnotationMapping() { $this->enableAnnotationMapping = false; $this->annotationReader = null; return $this; } /** * @return $this */ public function setDoctrineAnnotationReader(?Reader $reader): self { $this->annotationReader = $reader; return $this; } /** * @return $this */ public function addDefaultDoctrineAnnotationReader(): self { $this->annotationReader = $this->createAnnotationReader(); return $this; } /** * Sets the class metadata factory used by the validator. * * @return $this */ public function setMetadataFactory(MetadataFactoryInterface $metadataFactory) { if (\count($this->xmlMappings) > 0 || \count($this->yamlMappings) > 0 || \count($this->methodMappings) > 0 || $this->enableAnnotationMapping) { throw new ValidatorException('You cannot set a custom metadata factory after adding custom mappings. You should do either of both.'); } $this->metadataFactory = $metadataFactory; return $this; } /** * Sets the cache for caching class metadata. * * @return $this */ public function setMappingCache(CacheItemPoolInterface $cache) { if (null !== $this->metadataFactory) { throw new ValidatorException('You cannot set a custom mapping cache after setting a custom metadata factory. Configure your metadata factory instead.'); } $this->mappingCache = $cache; return $this; } /** * Sets the constraint validator factory used by the validator. * * @return $this */ public function setConstraintValidatorFactory(ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; return $this; } /** * Sets the translator used for translating violation messages. * * @return $this */ public function setTranslator(TranslatorInterface $translator) { $this->translator = $translator; return $this; } /** * Sets the default translation domain of violation messages. * * The same message can have different translations in different domains. * Pass the domain that is used for violation messages by default to this * method. * * @return $this */ public function setTranslationDomain(?string $translationDomain) { $this->translationDomain = $translationDomain; return $this; } /** * @return $this */ public function addLoader(LoaderInterface $loader) { $this->loaders[] = $loader; return $this; } /** * @return LoaderInterface[] */ public function getLoaders() { $loaders = []; foreach ($this->xmlMappings as $xmlMapping) { $loaders[] = new XmlFileLoader($xmlMapping); } foreach ($this->yamlMappings as $yamlMappings) { $loaders[] = new YamlFileLoader($yamlMappings); } foreach ($this->methodMappings as $methodName) { $loaders[] = new StaticMethodLoader($methodName); } if ($this->enableAnnotationMapping) { $loaders[] = new AnnotationLoader($this->annotationReader); } return array_merge($loaders, $this->loaders); } /** * Builds and returns a new validator object. * * @return ValidatorInterface */ public function getValidator() { $metadataFactory = $this->metadataFactory; if (!$metadataFactory) { $loaders = $this->getLoaders(); $loader = null; if (\count($loaders) > 1) { $loader = new LoaderChain($loaders); } elseif (1 === \count($loaders)) { $loader = $loaders[0]; } $metadataFactory = new LazyLoadingMetadataFactory($loader, $this->mappingCache); } $validatorFactory = $this->validatorFactory ?? new ConstraintValidatorFactory(); $translator = $this->translator; if (null === $translator) { $translator = new class() implements TranslatorInterface, LocaleAwareInterface { use TranslatorTrait; }; // Force the locale to be 'en' when no translator is provided rather than relying on the Intl default locale // This avoids depending on Intl or the stub implementation being available. It also ensures that Symfony // validation messages are pluralized properly even when the default locale gets changed because they are in // English. $translator->setLocale('en'); } $contextFactory = new ExecutionContextFactory($translator, $this->translationDomain); return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $this->initializers); } private function createAnnotationReader(): Reader { if (!class_exists(AnnotationReader::class)) { throw new LogicException('Enabling annotation based constraint mapping requires the packages doctrine/annotations and symfony/cache to be installed.'); } if (class_exists(ArrayAdapter::class)) { return new PsrCachedReader(new AnnotationReader(), new ArrayAdapter()); } if (class_exists(CachedReader::class) && class_exists(ArrayCache::class)) { trigger_deprecation('symfony/validator', '5.4', 'Enabling annotation based constraint mapping without having symfony/cache installed is deprecated.'); return new CachedReader(new AnnotationReader(), new ArrayCache()); } throw new LogicException('Enabling annotation based constraint mapping requires the packages doctrine/annotations and symfony/cache to be installed.'); } } PK![vendor/autoload.phpnu[form ?>

Select one variation > Google for WooCommerce.', 'google-listings-and-ads' ); ?>

render_partial( 'inputs/form', [ 'form' => $form ] ); ?>
PK!x!$views/attributes/variations-form.phpnu[children; ?> is_root ) : ?>

PK! ::views/bulk-edit/shop_coupon.phpnu[
PK!views/inputs/checkbox.phpnu[input; $input['value'] = $input['value'] ?? false; $input['value'] = wc_bool_to_string( $input['value'] ); $input['wrapper_class'] = sprintf( '%s %s', $input['wrapper_class'] ?? '', 'options' ); woocommerce_wp_checkbox( $input ); PK!)Hviews/inputs/datetime.phpnu[input; $input['class'] = $input['class'] ?? ''; $input['wrapper_class'] = $input['wrapper_class'] ?? ''; $input['name'] = $input['name'] ?? $input['id']; $input['desc_tip'] = $input['desc_tip'] ?? false; $input['date'] = $input['date'] ?? ''; $input['time'] = $input['time'] ?? ''; echo '

'; // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo ' '; echo ' '; // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! empty( $input['description'] ) && false !== $input['desc_tip'] ) { echo wc_help_tip( $input['description'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } if ( ! empty( $input['description'] ) && false === $input['desc_tip'] ) { echo '' . wp_kses_post( $input['description'] ) . ''; } echo '

'; PK!F,,views/inputs/decimal.phpnu[input; $input['type'] = 'text'; $input['data_type'] = 'decimal'; woocommerce_wp_text_input( $input ); PK!xviews/inputs/form.phpnu[form; ?>
render_partial( path_join( 'inputs/', $form['type'] ), [ 'input' => $form ] ); } if ( ! empty( $form['children'] ) ) { foreach ( $form['children'] as $form ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->render_partial( 'inputs/form', [ 'form' => $form ] ); } } ?>
PK!0\}}views/inputs/integer.phpnu[input; $input['type'] = 'number'; // Not so "custom" but standard `` attribute. $input['custom_attributes'] = [ 'min' => '0', ]; woocommerce_wp_text_input( $input ); PK!$views/inputs/select.phpnu[input; woocommerce_wp_select( $input ); PK!.~'views/inputs/select-with-text-input.phpnu[input; PK!Lviews/inputs/text.phpnu[input; woocommerce_wp_text_input( $input ); PK! ψ %views/meta-box/channel_visibility.phpnu[product_id; /** @var WC_Product $product */ $product = $this->product; $channel_visibility = $this->channel_visibility; /** @var string */ $field_id = $this->field_id; /** @var bool */ $is_setup_complete = $this->is_setup_complete; /** @var string */ $get_started_url = $this->get_started_url; /** @var string $sync_status */ if ( SyncStatus::HAS_ERRORS === $this->sync_status ) { $sync_status = __( 'Issues detected', 'google-listings-and-ads' ); } elseif ( ! is_null( $this->sync_status ) ) { $sync_status = ucfirst( str_replace( '-', ' ', $this->sync_status ) ); } $show_status = ! empty( $sync_status ) && $channel_visibility === ChannelVisibility::SYNC_AND_SHOW && $this->sync_status !== SyncStatus::SYNCED; /** @var array $issues */ $issues = $this->issues; $has_issues = ! empty( $issues ); $visibility_box_class = $has_issues ? 'notice-warning' : ''; $visibility_box_style = $has_issues ? 'border-left-style: solid' : 'background-color:#efefef'; $input_description = ''; $input_disabled = false; if ( ! $product->is_visible() ) { $channel_visibility = ChannelVisibility::DONT_SYNC_AND_SHOW; $show_status = false; $input_disabled = true; $input_description = __( 'This product cannot be shown on any channel because it is hidden from your store catalog.', 'google-listings-and-ads' ); } $custom_attributes = []; if ( $input_disabled ) { $custom_attributes['disabled'] = 'disabled'; } ?>
$field_id, 'value' => $channel_visibility, 'label' => __( 'Google for WooCommerce', 'google-listings-and-ads' ), 'description' => $input_description, 'desc_tip' => false, 'options' => ChannelVisibility::get_value_options(), 'custom_attributes' => $custom_attributes, 'wrapper_class' => 'form-row form-row-full', ] ); ?>

PK!$ZZ,views/meta-box/coupon_channel_visibility.phpnu[coupon_id; /** * * @var WC_Coupon $coupon */ $coupon = $this->coupon; $channel_visibility = $this->channel_visibility; /** * * @var string */ $field_id = $this->field_id; /** * * @var bool */ $is_setup_complete = $this->is_setup_complete; /** * * @var bool */ $is_channel_supported = $this->is_channel_supported; /** * * @var string */ $get_started_url = $this->get_started_url; /** * * @var string $sync_status */ $is_synced = false; if ( SyncStatus::HAS_ERRORS === $this->sync_status ) { $sync_status = __( 'Issues detected', 'google-listings-and-ads' ); } elseif ( SyncStatus::PENDING === $this->sync_status ) { $sync_status = __( 'Pending for sync', 'google-listings-and-ads' ); } elseif ( SyncStatus::SYNCED === $this->sync_status ) { $is_synced = true; $sync_status = __( 'Sent to Google', 'google-listings-and-ads' ); } elseif ( ! is_null( $this->sync_status ) ) { $sync_status = ucfirst( str_replace( '-', ' ', $this->sync_status ) ); } $show_status = $channel_visibility === ChannelVisibility::SYNC_AND_SHOW && ( ! is_null( $this->sync_status ) ); $check_email_notice = __( 'Check your email for updates.', 'google-listings-and-ads' ); /** * * @var array $issues */ $issues = $this->issues; $has_issues = ! empty( $issues ); $input_description = ''; $input_disabled = false; if ( ! CouponSyncer::is_coupon_supported( $coupon ) ) { $channel_visibility = ChannelVisibility::DONT_SYNC_AND_SHOW; $show_status = false; $input_disabled = true; $input_description = $coupon->get_virtual() ? __( 'This coupon cannot be shown on public channel because it is hidden from your store.', 'google-listings-and-ads' ) : __( 'This coupon cannot be shown because the coupon restrictions are not supported to share in Google channel.', 'google-listings-and-ads' ); } elseif ( ! $is_channel_supported ) { $channel_visibility = ChannelVisibility::DONT_SYNC_AND_SHOW; $show_status = false; $input_disabled = true; $input_description = __( 'This coupon visibility channel has not been supported in your store base country yet.', 'google-listings-and-ads' ); } $custom_attributes = []; if ( $input_disabled ) { $custom_attributes['disabled'] = 'disabled'; } ?>
$field_id, 'value' => $channel_visibility, 'label' => __( 'Google for WooCommerce', 'google-listings-and-ads' ), 'description' => $input_description, 'desc_tip' => false, 'options' => [ ChannelVisibility::SYNC_AND_SHOW => __( 'Show coupon on Google', 'google-listings-and-ads' ), ChannelVisibility::DONT_SYNC_AND_SHOW => __( "Don't show coupon on Google", 'google-listings-and-ads' ), ], 'custom_attributes' => $custom_attributes, 'wrapper_class' => 'form-row form-row-full', ] ); ?>

PK! changelog.txtnu[*** Google for WooCommerce Changelog *** = 2.9.7 - 2025-01-28 = * Tweak - Adjust some query and table functions. * Tweak - Allow mapping of a Google ID without a prefix. = 2.9.6 - 2025-01-21 = * Tweak - Resolve some of the plugin check errors and warnings. * Tweak - Set specific flags for html_entity_decode. = 2.9.5 - 2025-01-15 = * Dev - Fix E2E tests in WC 9.6. * Dev - Fix tests in WooCommerce 9.6. * Fix - Avoid the negative shipping rate taken from WooCommerce settings leading to false onboarding completion and make its error message more explicit. * Fix - Show leaving prompt in Edit Ads Campaign if the form was modified. * Fix - Sync shipping max times to Google Merchant Center. * Tweak - Add an admin service provider for WP Admin requests. * Tweak - Conditionally load job classes. * Tweak - Consistent usage of ContainerAware. * Tweak - Pop up confirmation prompt on the Shipping page before saving changes. * Tweak - Refactor installer class to load dependencies when needed. * Tweak - Remove unused Http proxy class. * Tweak - WC 9.6 compatibility. * Update - Drop support for WordPress < 6.1. * Update - Move tax rate setup from the Edit free listings page to the Settings page. * Update - Move the Edit free listings page to a dedicated page and rename the page to Shipping. = 2.9.4 - 2024-12-25 = * Fix - Clear previous errors after completing sync. * Tweak - Adjust conditions for MCM. * Update - Google Ads API to v18. = 2.9.3 - 2024-12-18 = * Dev - Eliminate the duplicate functions used to group shipping time data. * Dev - Improve E2E tests for related products. * Fix - Budget Recommendation data not populating on install. * Fix - PHP 8.4 package compatibility. * Fix - Prevent duplicate conversion and purchase event tracking. * Fix - The saved max shipping time is not showing after revisiting the free listings editing page. * Tweak - Drop Jetpack packages and switch to packages bundled with WooCommerce. * Tweak - WC 9.5 compatibility. * Update - Drop support for WooCommerce < 7.9. = 2.9.2 - 2024-12-11 = * Dev - Centralize frontend images to the dedicated directory. * Fix - Ensure coupon brand restrictions are uploaded to Google Merchant Center. * Fix - Initial php 8.4 compatibility. * Fix - Prevent Warning when Saving a Variation if YOAST is activated. * Fix - The country selector in setup/edit free listing flow from being hidden. = 2.9.1 - 2024-12-03 = * Fix - Make the tab navigation tabs wrap when the screen narrows. * Fix - Prevent translations from being called early. * Fix - The initial values of the form on free listings editing page may be empty. * Update - New shipping settings. = 2.9.0 - 2024-11-26 = * Add - Add GTIN Migration Job. * Add - Banner for GTIN MIgration. * Add - Confirmation modal when user skips without creating a campaign during onboarding. * Add - During onboarding, automatically create Google Merchant Center or Google Ads accounts when the connected Google account doesn't have a respective existing one. * Add - GTIN Migration API Controller. * Add - Show promotion for Google Ads campaign on the Dashboard page. * Add - Show promotion for Google Ads campaign on the Product Feed page. * Add - Support YOAST SEO GTIN field in the migration tools. * Add - WP CLI Command for GTIN Migration. * Dev - Adding tests for GTIN migration tool. * Dev - Tweak E2E tests for GTIN migration in versions > 2.8.7. * Fix - Add margin for separating notices in the admin. * Fix - Hide or disable GTIN in Product Block editor. * Fix - Hide/disable GTIN also when YOAST is active. * Fix - Prevent fatal in GTIN MIgration AS Job. * Fix rebranding tour on mobile. * Tweak - Prepare GTIN with the correct format before sending it to MC. * Tweak - Adjust plugin version to hide GTIN. * Tweak - Change to use a banner to present the ad credit offer during onboarding. * Tweak - Move FAQs to the bottom of pages. * Tweak - Remove the word "Paid" from the plugin. * Tweak - Remove unused methods from ProductMetaQueryHelper. * Tweak - Set the default value of the tax rate to destination-based during onboarding. * Tweak - Show campaign setup fields immediately during onboarding. * Tweak - Swap performance cards on the Dashboard page. * Update - Adjust the minimum average daily cost of a campaign to 30% of the highest recommended value among audience countries. * Update - Automatically preselect a Google Ads account when there is only one, as well as adjust the UI presentation. * Update - Change the campaign setup and creation to use the recommended budget as the initial value and adjust its description. * Update - Consolidate the campaign setup UI in the onboarding flow with the one in the Ads-onboarding flow. * Update - Hide or disable GTIN field in favor of the new native GTIN located in the WooCommerce Product Inventory tab. * Update - Hides WordPress.com account connection setting from the onboarding flow if already connected. * Update - Hides the tax rate setting during onboarding. * Update - Introduce new UI for Google accounts setup during the onboarding. * Update - Merge the billing setup into the campaign setup for the Ads-onboarding flow. * Update - Merge the store address setting in Step 3 of the onboarding flow into Step 1 and remove Step 3 along with the contact phone verification. * Update - Product adapter to map GTIN value from WooCommerce core field if it's available. * Update - Remove ads audience field from paid ads setup during onboarding. * Update - Remove the Pre-Launch Checklist from the onboarding flow. * Update - Remove the contact phone from the Settings page. * Update - Remove the language setting from onboarding. * Update - Restrict the GTIN field based on the version of WooCommerce installed and the initial version of G4W installed. * Update - When the accounts have been connected before, skip accounts setup step during the Ads-onboarding. = 2.8.7 - 2024-11-14 = * Dev - WordPress 6.7 Compatibility: Fix the issue that E2E test can't log in to wp-admin. * Fix - Remove a Google Ads API vendor file that prints php information. * Fix - WordPress 6.7 Compatibility: Avoid the block toolbar appearing when interacting blockified product editor. * Tweak - WC 9.4 compatibility. * Tweak - WP 6.7 compatibility. * Tweak - WordPress 6.7 Compatibility: Adjust the layout of the radio control to align well with the extended content. * Tweak - WordPress 6.7 Compatibility: Avoid errors in the database where a TEXT type can't have a default value. = 2.8.6 - 2024-10-02 = * Dev - Fix missing blueprint dependency. * Tweak - Adjust WP Proxy Response to force the string type for the price fields. * Tweak - Logic for Delete notifications. = 2.8.5 - 2024-09-05 = * Break - Remove WooCommerce Navigation integration. * Fix - Issue with syncing shipping rates with more than two decimals. * Fix - Log exceptions triggered by assets being enqueued before being registered. * Tweak - Use remote-site-status to check the WPCOM Auth status. * Tweak - WC 9.3.0 compatibility. = 2.8.4 - 2024-08-28 = * Dev - Align namespaces for unit tests. * Dev - Avoid accidentally using the event object to reset the asset group values in the CampaignAssetsForm component. * Dev - Migrate jest tests to use Node.js 20. * Dev - Rewrite the replacer of JSON.stringify in getReportKey to ensure it returns the same key regardless of the query keys' order. * Dev - Upgrade to use Node.js 20 and bump npm dependencies. * Dev - Use a fixed SKU number when testing product adapter. * Fix - issue with comma separators for Shipping Rates. * Tweak - Connect Test Page errors when WPCOM token is not connected. * Tweak - Make the Tooltip use the new placement prop when WordPress >= 6.4. * Tweak - Replace deprecated event.keyCode with event.code for the verification code inputs in the contact information setting. * Tweak - Update the copy in the "Linked accounts" of the accounts connection setting to include Google Ads account. = 2.8.3 - 2024-08-20 = * Fix - Return empty array props as empty objects in WCOM Proxy responses. * Tweak - Display additional context in error message when Google Ads account limit reached. * Tweak - Upgrade readme details in WPORG. = 2.8.2 - 2024-08-14 = * Fix - Disconnecting all accounts when WPCOM connection is not granted. * Fix - Error when Google Merchant Center account is undefined while checking the notification service enabled property. * Tweak - Label campaigns for the web version and the WooCommerce Mobile app. * Tweak - Update FAQS in Getting Started page. * Tweak - Update WP.org plugin FAQs. * Tweak - Update WPORG plugin page header image. * Tweak - Update get started page. * Tweak - WC 9.2.0 compatibility. * Update - Block validation to support error context. = 2.8.1 - 2024-08-06 = * Add - Enable labeling of Ads campaigns. * Tweak - Update doc links references. * Update - Enable Page Size Parameter in Campaigns Endpoint. = 2.8.0 - 2024-07-31 = * Add Google API Pull method. * Rebranding Google Listings and Ads with Google for WooCommerce. = 2.7.7 - 2024-07-24 = * Dev - Fix E2E tests failed with WC 9.1. * Tweak - Make campaign preview card responsive. = 2.7.6 - 2024-07-09 = * Dev - Update connect server URL in test proxy configuration. * Tweak - WC 9.1 compatibility. * Tweak - WP 6.6 compatibility. = 2.7.5 - 2024-06-26 = * Add - Add an query parameter `campaign=saved` to the dashboard URL after the campaign was created. = 2.7.4 - 2024-06-25 = * Add - Integration with the WP Consent API plugin. * Dev - Add E2E tests for WP Consent API integration. * Tweak - Add docs note about WP Consent API integration. = 2.7.3 - 2024-06-18 = * Fix - Fatal error when loading campaign in the marketing overview section. * Tweak - Replace woo.com references with woocommerce.com. = 2.7.2 - 2024-06-10 = * Add - Google Ads account invitation acceptance step to the connection process. * Fix - Show tracking snapshots in WPCLI. * Tweak - Adjust click event tracking when connecting, disconnecting, and opening billing setup for Google Ads account. * Tweak - Adjust event tracking for the creating and claim buttons of Google Ads account. * Tweak - WC 9.0 compatibility. * Update - Enable users to seamlessly set up conversion tracking, without having to set up merchant center first or requiring campaign creation. * Update - Move the Google Ads account connection process from step 4 to step 1 of the onboarding flow. = 2.7.1 - 2024-05-29 = * Dev - Add info about Legacy Google Ads API Client Library in Readme. * Fix - Prevent PHP Warning when Statistics is null. * Update - Implement Account Request Review Requests in the extension. = 2.7.0 - 2024-05-14 = * Fix - Convert `lbs` to `lb` when mapping WC products to Google products. * Fix - E2E tests * Tweak - WC 8.9 compatibility. * Update - Update Google API to V16. = 2.6.9 - 2024-05-07 = * Tweak - Confirm issues are present when retrieving product status. = 2.6.8 - 2024-04-23 = * Tweak - Remove deprecated event properties marked as removable after Q1 2024 from the onboarding event tracking. * Tweak - Update tags in readme.txt. * Update - Restrict product types to be limited to only 10 when converting from categories. = 2.6.7 - 2024-04-16 = * Dev - Add E2E tests for the integration in the classic product editor. * Dev - Update e2e test environment to install WooCommerce earlier. * Fix - Exception in request review. = 2.6.6 - 2024-04-09 = * Dev - Add snippet to bypass WooCommerce dependency in E2E tests. * Tweak - WC 8.8 compatibility. = 2.6.5 - 2024-04-04 = * Dev - Update test proxy port. * Tweak - Show Review Inbox Notices when 11 clicks and 1 Conversion. = 2.6.4 - 2024-03-26 = * Add - Filter WC REST API responses for gla_syncable param. * Add - Missing functions for the WPCOM OAuth flow. * Add - Notify when product changes. * Dev - Add a manual workflow run for PHP unit tests. * Update - Refactor Product Stats. = 2.6.3 - 2024-03-19 = * Fix - Handle parse JSON exception when Creating Ads Account. * Fix - Inline Javascript encoding for gtag events. * Fix - Undefined keys `offers_free_shipping ` or `free_shipping_threshold`. * Tweak - Add WP 6.5 Require plugins header. * Update - Newer version of bcmath_compat and phpseclib packages. * Update - Set default connect server URL to api.woocommerce.com. * Update is_gtag_page to support Google Analytics for WooCommerce version 2.0.0+. = 2.6.2 - 2024-03-12 = * Dev - Fix E2E tests for gtags consent mode. * Fix - Fatal error when getAdsLinks response is null. * Fix - WordPress 6.4 Compatibility: The modal closed event is not sent when clicking on its overlay. * Tweak - WC 8.7 compatibility. * Tweak - WP 6.5 compatibility. = 2.6.1 - 2024-03-05 = * Add - Consider `ga_gtag_enabled=yes` for WCGAI >= 2. * Add - Google Analytics consent mode support. * Add - Support for Google Analytics for WooCommerce version 2.0.0 and above. * Dev - Avoid the test-data plugin occasionally missing in the E2E test environment. * Fix - Improve WordPress.com account handling. = 2.6.0 - 2024-02-27 = * Add - Support the new product editor (Product Block Editor). * Dev - Fix the compatibility issue in starting E2E test environment due to the default charset change in MariaDB v11.3.1. * Fix - 401 handling for connected Ads accounts. = 2.5.18 - 2024-02-20 = * Fix - Prevent product queries by IDs if no arguments are supplied. = 2.5.17 - 2024-02-07 = * Dev - Add manual QIT workflow. * Dev - Upload coverage report for JS unit tests to codecov. * Fix - Only sync selected categories as product type. * Fix - Prevent notifications from sending request to Google API when disconnected. * Tweak - WC 8.6 compatibility. = 2.5.16 - 2024-01-30 = * Add - Include connected accounts in tracks from the backend. * Add - Include plugin version, Google Merchant Center account ID, and Google Ads account ID in all frontend tracking events. * Add - Send the related tracking event with the account ID to be connected when connecting to an existing Google Merchant Center or Google Ads account. * Add - Tracking for completed events. * Dev - Generate coverage report with xdebug. * Fix - Context not tracked in Create Campaign FAQs. * Fix - WordPress 6.4 Compatibility: Set an appropriate width for the content in the Popover component. = 2.5.15 - 2024-01-09 = * Dev - Update link for developer.woo.com. * Tweak - WC 8.5 compatibility. * Update - Upgrade google/apiclient for PHP 8.3 compatibility. = 2.5.14 - 2023-12-18 = * Dev - Include PHP 8.3 in tested versions for PHPunit. * Fix - Item price in purchase event. * Tweak - Track Budgets and Audience in Onboarding. * Tweak - WC 8.4 compatibility. * Update - Change to require Google Ads connection during the onboarding. = 2.5.13 - 2023-12-06 = * Fix - Change Budget Recommendations values. * Tweak - Use a single daily budget instead of a range. = 2.5.12 - 2023-11-22 = * Dev - Fix E2E gtag events tests. * Dev - Update WordPress CS to 3.0. * Dev - Update phpunit polyfills to 1.1 for WP 6.4. * Tweak - Add filter to be able to build custom shipping method rate handers. * Tweak - Remove rewrite rules flush. = 2.5.11 - 2023-11-07 = * Add - Record tracking events for moving steps on the campaign creation and editing pages. * Tweak - Add tracking for campaign count. * Tweak - WC 8.3 compatibility. * Tweak - WP 6.4 compatibility. * Update - Use new Woo.com domain. = 2.5.10 - 2023-10-18 = * Tweak - Add correct Destinations for Supported Countries in Coupons. * Tweak - Declare cart_checkout_blocks feature compatibility. = 2.5.9 - 2023-10-10 = * Dev - E2E - Setup Google Ads Step 2 - Create your paid campaign. * Dev - E2E - Setup Google Ads Step 3 - Setup billing data. * Dev - E2E tests - Track gtag event on specific page. * Dev - Prevent Prefix Vendor to be added twice. * Fix - Avoid creating two campaigns after completing the Google Ads onboarding. * Fix - The auto-refresh processing of billing status in the Google Ads onboarding flow. = 2.5.8 - 2023-10-03 = * Add - Privacy policy guide section. * Add - The missing tracking to onboarding when changing steps. * Dev - Adjust the conditions for loading JS and CSS assets, and configure them with lazy loading and code splitting. * Dev - E2E - Onboarding Step 4 - Complete your campaign. * Fix - Remove AttributeMapping new feature inbox notification. * Tweak - Add UTM parameters to documentation link. * Tweak - The properties of the gla_setup_mc and gla_setup_ads tracking events to reduce their confusion. * Tweak - WC 8.2 compatibility. = 2.5.7 - 2023-09-20 = * Dev - E2E - Ads a paid campaign Step 1 - Connect Ads Account. * Dev - E2E - Onboarding Step 2 - Configure product listings. * Dev - E2E - Onboarding Step 3 - Confirm store requirements. * Fix - Performance issue with GoogleAdsFailures::init. = 2.5.6 - 2023-09-14 = * Dev - E2E - Onboarding Step 1 - Set up accounts - Connect Merchant Center account. * FIx - Undesired margin in Paid Campaign Creation Success Modal. * Fix - Adjust target on click events preventing GLA ID to be Undefined. * Fix - Tweak E2E tests for WC 8.1. * Fix - WooCommerce Subscriptions compatibility: Fix the visible issue of the "Google Listings and Ads" tab and "Channel visibility" meta box for some unsupported product types. * Tweak - WC 8.1.0 compatibility. = 2.5.5 - 2023-09-05 = * Dev - Add E2E tests - Dashboard - Edit Free Listings. * Dev - Clean up workarounds for WooCommerce < 6.8. * Dev - Externalize all WooCommerce JavaScript packages via Dependency Extraction Webpack Plugin (DEWP) and remove the selective bundling implementation that gradually externalizes packages into DEWP. * Dev - Update DEWP related tools and docs. * Fix - Fix Taxonomy Attribute Mapping for Product Variations. = 2.5.4 - 2023-08-29 = * Dev - Override vulnerability packages: xmlhttprequest-ssl and ws. * Dev - Update trigger method in Hooks Generator Workflow. = 2.5.3 - 2023-08-22 = * Dev - Add Action for Hooks Documentation Generator. * Dev - Allow E2E testing with Release Candidates. * Dev - Convert E2E tests from Puppeteer to Playwright. * Dev - Externalize all WordPress JavaScript packages via Dependency Extraction Webpack Plugin (DEWP). * Dev - Fetch WooCommerce L-1 versions for our tests. * Dev - Remove legacy HooksDocsGenerator.php file. * Dev - Use `merge-trunk-develop-pr` action. * Tweak - Apply consistent admin theme colors to common UI components. * Update - Google API Client Services package to v0.312. * Update - Google Ads library to API V14. (package v19.2.0). = 2.5.2 - 2023-08-08 = * Fix - Remove `add_woocommerce_extended_task_list_item` and `remove_woocommerce_extended_task_list_item` hooks. * Fix - WordPress 6.3 compatibility: The forms and image selector may not work due to "setImmediate" deprecation. * Tweak - Use the latest API to add an item to the WC tasks list. * Tweak - WC 8.0 compatibility. * Tweak - WP 6.3 compatibility. = 2.5.1 - 2023-08-01 = * Dev - Setup wp-env for E2E tests. * Dev - automate merging trunk to develop after a release. * Fix - Fix support for "add_to_cart" event in Products (Beta) block. * Fix - Prevent PHP 8.2 deprecation messages. * Tweak - Ability to filter products for syncing via `gla_filter_product_query_args` apply_filters hook. * Update - Show validation errors on steps 2 and 3 of the onboarding flow when unable to continue. = 2.5.0 - 2023-07-18 = * Tweak - Add Tip with information with Campaign assets are imported. * Tweak - Provide more detailed error reasons when unable to complete site verification for the Google Merchant Center account being connected in the onboarding flow. = 2.4.11 - 2023-07-11 = * Add - Client name and plugin version to requests. * Dev - Enable unit testing for PHP 8.1. * Dev - Set engines for the repository. * Fix - Avoid continuing to save settings to Google Merchant Center after the shipping time save failed on the Edit Free Listings page. * Fix - Avoid errors when clearing all audience countries in the onboarding flow. * Fix - Incorrectly display South America in the audience location selector after selecting Saudi Arabia. * Fix - Remove deprecated $border-width-focus variable. * Fix - Show a general error message when the phone number verification request is failed. * Tweak - Add placeholder in the Attribute Mapping table when there are no rules available. * Tweak - Changes for title, descriptions and FAQ in PMAX Optimized Campaigns. * Tweak - Make some error messages clearer when errors occur in querying or modifying data. * Tweak - Make the error message clearer for errors that occur in getting or updating a Google Merchant Center account. * Tweak - WC 7.9 compatibility. = 2.4.10 - 2023-06-13 = * Tweak - WC 7.8 compatibility. = 2.4.9 - 2023-06-08 = * Fix - Prefix psr/http-client package. = 2.4.8 - 2023-06-08 = * Fix - Prefix Psr\Http\Message package to prevent conflicts with other plugins. = 2.4.7 - 2023-06-07 = * Fix - Adapt the `is_virtual` property value for Product Bundles to avoid applying incorrect shipping rates in products synchronization. * Update - Google API Client Services package to v0.302. * Update - Google API Client package to v2.15. * Update - Google Ads library to API V13. (package v19.1.0). = 2.4.6 - 2023-05-30 = * Add - Filters for manually mapping product IDs. * Tweak - Adjust the MCM filter to always show in channels. = 2.4.5 - 2023-05-09 = * Fix - Bug in Attribute Mapping with Taxonomy based rules not being applied in variations. * Fix - Missing spaces between the card layouts on the Get Started page. * Tweak - WC 7.7 compatibility. = 2.4.4 - 2023-05-02 = * Dev - Fix SEMGREP warnings. * Fix - Prefix Psr\Container package to prevent conflicts with other plugins. = 2.4.3 - 2023-04-25 = * Dev - Add PHP Code coverage report as GitHub action. * Dev - Unit test support for PHP 8.2. * Dev - Use "willReturnOnConsecutiveCalls" instead of "at" for unit tests. * Fix - Prevent creating assets for non-Performance Max campaigns. * Fix - The fatal errors caused by adding any US Armed Forces location to WooCommerce Shipping setting. * Fix - Use Ads account currency in the WooCommerce marketing dashboard. * Tweak - Redirect users between Dashboard and Get Started pages as required. = 2.4.2 - 2023-03-29 = * Tweak - WC 7.6 compatibility. = 2.4.1 - 2023-03-14 = * Tweak - WC 7.5 compatibility. * Tweak - WP 6.2 compatibility. = 2.4.0 - 2023-03-07 = * Add - Support for the Assets of Performance Max campaigns. * Dev - Externalize Panel, PanelBody, and PanelRow. * Dev - Externalize the KeyboardShortcuts component. * Dev - Increase maximum payload size in the test proxy. * Fix - The blank Product Feed page after completing the onboarding flow. * Tweak - Make the popover of the tooltip can be closed properly. = 2.3.10 - 2023-02-21 = * Add "Working with DEWP.md" to exclude list. * Add - Integration with WooCommerce Multichannel Marketing. * Tweak - Remove unnecessary PMax migration banners. * Tweak - Remove unnecessary woocommerce_loop_add_to_cart_link filter param. = 2.3.9 - 2023-02-15 = * Dev - Update phpunit to version 9.5. * Fix - Prefix Google Service packages to prevent plugin conflicts. * Tweak - Improve PHP 8.1 compatibility. * Tweak - Show admin notice when PHP 32 bits is being used. * Tweak - WC 7.4 compatibility. * Update - Google Ads library to API V12. * Update - Google Content library to API 2.13. = 2.3.8 - 2023-01-24 = * Fix - Product feed table footer rendering a zero when there are no products. = 2.3.7 - 2023-01-17 = * Tweak - Pre-select a default MC account. = 2.3.6 - 2023-01-10 = * Dev - Use extracted Button component from @wordpress/components package. * Fix - i18n for "View Reports" button. * Tweak - WooCommerce 7.3 Compatibility with Customer Effort Score prompt. = 2.3.5 - 2022-12-28 = * Tweak - Adjust copy in Attribute Mapping section. * Tweak - Retrieve a published product as a landing page URL. * Tweak - Simplify report controller parameters. = 2.3.4 - 2022-12-20 = * Tweak - Improve image validation error messages. = 2.3.3 - 2022-12-14 = * Fix - Tours API Endpoint. * Tweak - WC 7.2 compatibility. * Update - Drop support for WordPress < 5.9. = 2.3.2 - 2022-12-06 = * Dev - Adjusted parts of the post-install process to work on machines without `grep` and `find`. . * Dev - Adjusted post-install process to mention when files have their class-expectations modified but retain their original namespace. * Fix - Certain inbox notifications were shown before setup completed. * Fix - Delete products in GMC when force delete a product or change catalog visibility to hidden. = 2.3.1 - 2022-11-22 = * Add - Attribute Mapping Feature. * Dev - Add script to list DEWPed dependencies' versions for a given WC version. * Fix - Incorrect product statistics count. * Fix - Yoast global identifiers for variable products. * Tweak - Remove unused GRPC packages. * Tweak - WC 7.1 compatibility. * Update - Change multipack attribute input to be native number input, to improve accessibility. * Update - Drop support for WC < 6.9. = 2.2.1 - 2022-11-15 = * Add - Declare compatibility for High Performance Order Storage. * Dev - Selectively externalize bundled packages. * Fix - E2E Testing: Reduce the false positive rate and adjust the running events on GitHub Actions. * Fix - Move the order of Google Listings and Ads below the Coupons in the Marketing menu of WooCommerce admin page. * Fix - WC 6.9 compatibility: Shipping time settings should not appear after selecting the "complex" shipping option. * Fix - WC 6.9 compatibility: The free shipping threshold should be cleared after selecting the "No" free shipping option. * Fix - WC 6.9 compatibility: The selected free shipping option should be reset after setting all shipping rates to 0. * Fix - WC 7.1 compatibility: Fixing the forms in the free listings setup may cause infinite requesting state updates which lead to a blank page or issue a lot of API requests. * Fix - WordPress 5.9 Compatibility: Visually hide descriptions of external link icons. * Fix - WordPress 6.1 Compatibility: Popover and Tooltip components should be displayed as floating. * Fix - WordPress 6.1 Compatibility: The size of navigation icons in Datepicker component should not be a giant size. * Tweak - WC 7.1 compatibility. * Tweak - WP 6.1 compatibility. * Update - ISO3166 package version 4.1. = 2.2.0 - 2022-10-18 = * Add - Ad previews in the post-onboarding ads setup flow. * Add - Combine the audience and shipping steps for the onboarding flow and the editing free listings page. * Add - Streamlined Free Listings + Paid Ads for the onboarding flow. * Add - The disclaimer of Comparison Shopping Service of the accounts setup of onboarding flow. * Add - The submission success modal on the Product Feed page after the onboarding is completed along with paid ads setup. * Fix - A validateDOMNesting warning in the accounts setup step of the onboarding flow. * Fix - Free Listings + Paid Ads: Add the paid ads previews to the boost product listings section. * Fix - Remove - Support for WC < 6.8. * Fix - Shipping time values flash during the onboarding setup. * Fix - Steppers on the onboarding flow allow switching to later steps when the current step is not yet finished. * Fix - The "Or, create a new Google Ads account" button at the footer of the Google Ads account setup is clickable when connecting an existing account. * Fix - The incorrect active status style for a disabled button. * Tweak - Use different titles for the free listings setup of the onboarding and editing pages. * Update - Change the steppers in the onboarding flow to only allow going back to the previous steps. * Update - Detect the verification status of the phone number in the contact information settings. * Update - Layouts and copywriting of the Get Started page and the onboarding flow. * Update - Logos of Google Merchant Center and Google Ads. * Update - Open the billing setup page of Google Ads via a popup window and add an alternative hyperlink to open the same setup page. * Update - The FAQs in the paid ads setup and the campaign setup page. = 2.1.4 - 2022-10-04 = * Add - Policy Compliance Checks in the onboarding flow. * Tweak - WC 7.0 compatibility. = 2.1.3 - 2022-09-27 = * Fix - Avoid truncate for issues with more than 100 characters length. * Fix - Update Size Type Attribute available values. * Tweak - Update Website not Claimed issue information. = 2.1.2 - 2022-09-15 = * Fix - WooCommerce 6.7 compatibility issues. * Tweak - WC 6.9 compatibility. = 2.1.1 - 2022-09-06 = * Dev - Run PHPCS checks for unit tests. * Fix - A compatibility issue with WooCommerce 6.9 which prevents interaction with the input field of the paid campaign budget. * Fix - Fatal error if a null rate specified for flat rate methods with shipping classes. * Tweak - Add a filter to disable GTag tracking. * Tweak - Updated plugin icons. = 2.1.0 - 2022-08-23 = * Add - Automatically sync WooCommerce shipping settings with Merchant Center. * Add - Get shipping rates suggestions for provinces/states and postal codes. * Add - Option to automatically sync the shipping rates based on the store shipping zone configurations. * Add - Sync the shipping rates for states/provinces and postal codes to Merchant Center. * Fix - A compatibility issue with WC 6.5+ that the store country might be undefined and further break the onboarding setup. * Tweak - Generate random ID for postcode regions when syncing shipping settings. = 2.0.4 - 2022-08-16 = * Dev - E2E Fix for redirecting to single product page. * Dev - Remove wc-admin installation from E2E env setup. * Fix - Handle multiple errors in the Edit free listings page. * Fix - Hide WooCommerce System messages in the plugin screen. . * Fix - Onload conflict when tracking events. = 2.0.3 - 2022-08-09 = * Add - Campaign Conversion Status for detecting converted campaigns. * Add - Gtag event tracking. * Add - Inbox notification for PMax migration. * Add - Includes removed campaign in the program report section. * Add - Pmax migration banner dashboard. * Add - Pmax migration banner reports. * Add - Tooltip in reports section for SSC Campaigns. * Add - Track add to cart events from all buttons including Gutenberg blocks. * Fix - Add Woo gTag remarketing and conversion signals. = 2.0.2 - 2022-07-29 = * Fix - Disable identifier_exists field. * Tweak - Propagate errors for saveSettings. * Tweak - Refactor SCSS variables. * Tweak - Remove PHP 8.0 specific code of Symfony polyfills. * Tweak - Revert migration applicable version value. * Tweak - Update change log records type. * Tweak - WC 6.8 compatibility. * Update - Google Ads library to API V11. = 2.0.1 - 2022-07-12 = * Dev - A script to generate a list of hooks that defined or used in GLA. * Dev - GH workflow to set PR labels. * Add - Normalizer Polyfill. * Dev - changed the changelog types list. * Fix - Compatibility with History Navigation v5. * Fix - Encoding product names in Issues Table . * Tweak - Remove try and catch in saveTargetAudience action. = 2.0.0 - 2022-07-05 = * Add - Filter Ads accounts to exclude manager and test accounts. * Add - Return account names when retrieving the list of existing accounts. * Fix - Normalize image URLs before validation. * Tweak - WooCommerce 6.7 compatibility. = 1.13.6 - 2022-06-21 = * Fix - Cannot disconnect Jetpack when other activated plugins are using Jetpack connection. * Fix - Compatibility CES prompts with WC 6.6.0. * Fix - Multiple CES prompts on the Dashboard Page. = 1.13.5 - 2022-06-15 = * Fix - Avoid losing focus when selecting an option in Tree Select Control. * Fix - Bump node-forge from 1.2.1 to 1.3.1. * Tweak - Clear input search filter after selecting an option. * Tweak - Disable Review Request in Standalone Accounts. * Tweak - Update copy for Free and Enhanced Listings merge * Tweak - WC 6.6 compatibility. = 1.13.4 - 2022-06-07 = * Fix - Adding Github Actions for storybook. * Fix - Do not show error notice when Merchant Center review request API call failed. * Fix - Do not store URL matches transient until fully connected. * Fix - Fix GitHub Workflow paths. * Fix - Use commit instead of branch for storybook dependency. * Tweak - Always compare site URL hash without trailing slash. * Tweak - Compliance Policy links. * Tweak - WC 6.6 compatibility. = 1.13.3 - 2022-05-31 = * Add - Add six more promotion supported countries. * Fix - Allow unicode for Manufacturer Part Number (MPN) value. * Fix - Avoid to show Unsaved Values confirmation in Edit Free Listing when no values has been changed. * Fix - Prevent repeated account URL retrievals. * Fix - Update tracking docs. * Tweak - Replace storybook deps in favor of woocommerce-grow/storybook. * Tweak - Simplify and centralize the processing of internal states for the TreeSelectControl component. * Update - budget recommendation conversion rate. = 1.13.2 - 2022-05-25 = * Fix - Prevent repeated account URL retrievals. = 1.13.1 - 2022-05-24 = * Fix - Missing ShoppingPerformanceView error when viewing report data. * Fix - Update the start/end date in the timePeriod message of coupon following google.protobuf.Timestamp. * Tweak - Show MC Issues resolution steps in the UI. = 1.13.0 - 2022-05-18 = * Add - Extending Update All Products Test Suit. * Add - Request a Google Merchant Account Review for disapproved accounts. * Fix - Address a crash problem of TreeSelectControl component when the dropdown is not showing and press the Up or Down key. * Fix - Edit shipping rate modal disappears after auto-save shipping rate in Setup MC. * Fix - Prevent product sync if the site URL does not match the originally claimed URL. * Fix - Revert filtering only Shopping destination for account issues. * Fix - The unsaved prompt might pop up when the countries of the target audience are the same when navigating away from the free listings edit page. * Fix - Unit tests for WooCommerce 6.5. * Fix - Validation for shipping rates and shipping times in Setup MC and Edit Free Listings. * Tweak - Add helper class to obtain supported countries of a continent. * Tweak - Adjust the implementation of focus navigation for the TreeSelectControl component. * Tweak - Cleanup unused Google Ads services. * Tweak - Drop support for WooCommerce < 6.0. * Tweak - Enhance event name for documentation link and update tracking document. * Tweak - WooCommerce 6.5 compatibility. * Tweak - WordPress 6.0 compatibility. * Update - Improved UX in the Product Feed Issues table. * Update - Use a shared helper method to get supported countries of a continent for /mc/countries API. = 1.12.8 - 2022-05-05 = * Update - Add the FAQs card for UX improvements on get started page. * Update - Add the benefits card for UX improvements on get started page. * Update - Add the customer quotes card for UX improvements on get started page. * Update - Add the features card for UX improvements on get started page. * Update - Add the first card with a CTA and a video for UX improvements on get started page. * Update - Add the get started card for UX improvements on get started page. = 1.12.7 - 2022-05-04 = * Fix - Label UI for selecting countries (TreeSelectControl / SupportedCountrySelect). * Tweak - Refactor, remove `record*Event` utils. * Tweak - Upgrade @wordpress/scripts to 22.1.0, and the related packages were upgraded to the corresponding versions. * Tweak - Upgrade the packages of the e2e testing. * Tweak - Upgrade webpack config to v5, and enhance the config. = 1.12.6 - 2022-04-29 = * Fix - Update all products job syncing products = 1.12.5 - 2022-04-12 = * Fix - Cache Yoast SEO values per product, to ensure unique values. * Fix - Feature/tree select control component. * Fix - Prompt to reconnect when a Jetpack disconnect is detected. * Tweak - Automatically generate Tracking events docs from JSDoc. * Tweak - Move Tracking events docs to JSDoc. = 1.12.4 - 2022-04-06 = * Fix - Prevent fatal errors when migrating or syncing merchant settings. = 1.12.3 - 2022-04-05 = * Fix - Shipping times section not showing up and unable to proceed through the Setup Merchant Center flow. = 1.12.2 - 2022-04-05 = * Add - Unit test for AdsConversionAction. * Add - Unit test for AdsReport. * Fix - Prevent uncaught exception when Merchant account is not connected and we send a tracker snapshot. * Tweak - DB migration for shipping rates. * Update - Create all new campaigns as PMax campaigns. = 1.12.1 - 2022-04-01 = * Fix - Change shipping time options based on shipping rate options, to address missing shipping times data when shipping rates option is set to automatic or simple flat option, and shipping times option is set to complex manual option. * Fix - Do not sync shipping rates if the shipping time setting is set to complex. = 1.12.0 - 2022-03-29 = * Add - Additional data points for tracker snapshot. * Add - Enables merchants to select multiple countries as their audience when creating a Google Ads campaign (Smart Shopping Campaign). * Add - Google Listings and Ads product attributes icon. * Add - Integration with WooCommerce Shipping Zone to automatically sync shipping settings to Merchant Center. * FIx - Show right link and message in Paid Campaigns report when there is no data available. * Fix - Cleanup synced products locally when disconnecting Merchant Center account. * Fix - Combine duplicate account issues per country. * Fix - Fatal error when the plugin is activated before WooCommerce. * Fix - Fix incorrect HTTP status code when campaign creation and edit APIs call fails. * Fix - Limit failed delete retries to 5 and schedule again after one minute. * Fix - Performance issue related with NoteInitializer class. * Tweak - Add message to advise users to only connect Google Ads child account, not manager account. * Tweak - Catch errors related to invalid top-level domains specifically, and throw an error when the site's URL ends with an invalid top-level domain. * Tweak - Improve Ads error messages returned by the API. * Tweak - Simplify the format processing of number and amount values for all report metrics. * Update - Budget recommendation API supports for multiple countries. * Update - Campaigns APIs support for multiple countries. * Update - Change Campaign operations to batch requests. * Update - Refactor the Middleware class. * Update - Remove delete operations for campaign budget and ads group. * Update - WP-CLI dev dependencies. = 1.11.1 - 2022-02-10 = * Fix - Prevent a fatal error in case an existing Merchant Center account has an invalid domain. = 1.11.0 - 2022-02-02 = * Add - Unit tests for the Ads AccountController and AccountService. * Fix - Failure handling was not correctly displayed when the ads campaign creation and editing failed. * Fix - Fix the incorrect text color of the disabled "Disconnect account" buttons on the Settings page. * Fix - Makes country dropdown list always below the input box. * Fix - Prevent page flickering when loading admin pages of this extension. * Tweak - Change the importing way of lodash package to reduce the bundle size by 4 KB. * Tweak - Clean up outdated workarounds for WooCommerce 5.7. * Update - Google Ads API to V9. * Update - Google Content API library to 2.12.1. * Update - Inbox notifications have update promotion information from Google. * Update - Update Google Ads credit incentive in WordPress.org plugin landing page. * Update - Update Google Ads credit incentive info in plugin UI. = 1.10.0 - 2022-01-13 = * Add - Bulk update channel visibility for coupons. * Add - CES prompts for initial setup and campaign creation. * Add - Pre-fill shipping rates during free listing configuration wizard. * Add - Pre-fill shipping rates in Setup Merchant Center flow based on store's shipping settings. * Add - Sync products' shipping label/class to Merchant Center. * Fix - Drop WC 5.7 support. * Fix - Fatal error when creating Ads account without Site Title. * Fix - Fix/1078 shipping values flash during onboarding setup. * Fix - Fixing coupon test issue. * Fix - Group shipping rate by price and currency, and display the right currency in shipping rate input. * Fix - MC address validation. * Fix - Remove WC's `is_ajax` (deprecated in 6.1) in favor of proxied WP `wp_doing_ajax`. * Fix - Removed state/region address validation . * Fix - Replace `cloneDeep` within `.~/data/reducer.js` with functions that would not mutate other references of the state tree. * Tweak - WC 6.1 compatibility. = 1.9.0 - 2021-12-15 = * Add - Pre-fill target audience countries during onboarding using WooCommerce shipping zones. #1131 * Add - Pre-fill target audience countries with suggestions based on WooCommerce settings. #1145 * Fix - Fatal error on plugin deactivation. #1142 * Fix - Fix UI loading flicker in Setup MC Step 2 "Choose your audience" page. #1146 * Fix - Fix e2e test after copy update. #1134 * Tweak - Display help cursor for tooltip. #1130 * Tweak - Update product channel visibility's styling to match that of coupons channel visibility. #1135 * Tweak - Updated `@wordpress/scripts@17.1.0`. #1132 * Tweak - WC 6.0 compatibility. * Tweak - WP 5.9 compatibility. * Update - min. WC version to 5.7. #1110 = 1.8.0 - 2021-11-30 = * Add - Allow connecting to a different Google account in Setup MC. - #1072 * Add - Disconnect Google Merchant Center account when switching Google account. - #1109 * Add - Display account name and domain in Google Merchant Center cards. - #1112 * Add - track events for UX improvements. - #1124 * Fix - Refetch list of GMC accounts when users choose to connect to a different GMC accounts. - #1123 * Tweak - Improve UX and adjust UI style for the accounts connection step of the Google Ads & Paid Campaign setup page. - #1102 * Tweak - Minor layout tweak in Google Ads card in Setup Ads. - #1114 * Update - Display better explanation tip for Google Merchant Center in Setup MC Step 1. - #1075 * Update - UX improvement on Google account card in Setup MC. - #1072 * Update - UX improvements on Google Merchant Center section in Setup MC flow. - #1094 * Update - UX improvements on account connections in Setup Merchant Center flow. - #1119 * Update - Update the WordPress.com account connection UI to the newer design. - #1068 = 1.7.0 - 2021-11-24 = * Add - The partial authorization feature of Google account to the onboarding setup, Google Ads setup and reconnection pages. * Add - Accept login_hint when generating OAuth URL. * Add - Review request inbox notification after 10 conversions and 100 free listing clicks * Update – Add support for retrieving the name and domain from the Google API * Fix - Add support for Norwegian language, nb and nn * Fix - Report tabs lose active state when changing chart. * Tweak - Update `in_stock` and `out_of_stock` availability enums * Tweak - Retry async jobs on timeout * Tweak - Reduce the bundle size of the index.js file. * Tweak - refactored legacy WC menu highlighting effect. = 1.6.0 - 2021-11-09 = * Add - Coupon/promotion integration with Merchant Center. * Tweak - WooCommerce 5.9 compatibility. = 1.5.1 - 2021-10-13 = * Update - Changed minimum version of WordPress to 5.6 and WooCommerce to 5.5. * Fix - Change the way of getting WooCommerce admin settings to fix a compatibility issue in WooCommerce 5.8. * Tweak - WooCommerce 5.8 compatibility. = 1.5.0 - 2021-10-01 = * Add - Verify user's phone number via SMS or phone call at the last step of the onboarding flow and on the settings page. And update the verified phone number to user's connected Google Merchant Center account. * Add - Allow backorder stock availability for products. * Add - Set pre-order availability for products using the WooCommerce Pre-Orders extension. * Add - Warning notice when the Ads' currency is different from the store's one. * Add - Unit tests for the Merchant Google Service class. * Fix - Retry Merchant account creation after detecting invalid terms. * Fix - Render Ads Account's currency in Dashboard's table. * Fix - Don't render `DifferentCurrencyNotice` when the Ads account is disconnected. * Fix - Limit the number of synced additional product images to 10. * Fix - Split contact information settings page to phone and address settings. * Fix - Update phone number and store address pages flow. * Fix - Correct spelling/capitalization of "WordPress.com". * Fix - PHP notice when creating a product variation. * Fix - Bump E2E-related devDeps, bump tested WC version. * Tweak - Hide channel visibility box and attributes tab if the setup is not completed. * Tweak - Added a few more e2e tests and utils. * Tweak - WC 5.7 compatibility. = 1.4.3 - 2021-09-08 = * Fix - PHP notice when creating a product variation. * Tweak - Hide channel visibility box and attributes tab if the setup is not completed. = 1.4.2 - 2021-08-24 = * Fix - Fix a potential fatal error when WooCommerce isn't active while activating Google Listings and Ads. * Fix - Fix margin/padding styles for the AppButton when having spinner/icon/text. * Fix - Make audience country searchable in Setup Ads. * Fix - Remove file autoloads for namespaced packages. * Tweak - Remove all "STEP [NUMBER]" texts from step headers on the onboarding setup, paid campaign setup, and free listings edit pages. * Tweak - WC 5.5 compatibility. = 1.4.1 - 2021-08-16 = * Fix - Allow connection test page for other admin users. * Fix - Allow spaces in paths when prefixing vendor namespaces. * Fix - Database error: "Specified key was too long". i.e. removed the `product_issue` index from the `merchant_issues` table. * Fix - Fatal error when activating plugin with no Merchant Center account connected. * Fix - Some pre-sync errors being skipped in the product issues table. * Fix - display the correct currency actually used for the paid campaign budget. * Tweak - Limit the product descriptions to 5000 characters when syncing. = 1.4.0 - 2021-08-09 = * Add - Filter to allow applying shortcodes to product description. * Add - New contact information feature. * Fix - Add `woocommerce_gla_product_attribute_values` filter to allow overriding all product attributes. * Fix - Invalid Google IDs meta value causing fatal failure. * Fix - Load deprecated functions from Guzzle which are required for the GAX library. * Fix - Process all batches when updating products. * Tweak - Removed SVGs from JS bundle. * Tweak - Use the WordPress date and time formats on the Product Feed page. = 1.3.0 - 2021-07-27 = * Fix - Bump WordPress tested version to 5.8. * Fix - Code formatting with Prettier. * Fix - Disable the "Complete setup" button if the free shipping price is not yet entered when setting up Merchant Center for the first time. * Fix - Fix: add eslint-plugin-import to help catch JavaScript import errors. * Fix - Import `Button` from `@wordpress/components` in Switch URL flow. * Fix - Remove unused code. * Fix - Shows a Jetpack connected success text instead of blank when viewing the onboarding setup and the settings pages as a non jetpack owner account. * Fix - Skip orphaned variations instead of throwing errors when syncing products. * Tweak - Add filters for adjusting description. * Tweak - Allow safe HTML tags for product descriptions. * Tweak - Replaced `` with WP's `