您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

class-wp-customize-manager.php 140 KiB

3 年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368
  1. <?php
  2. /**
  3. * WordPress Customize Manager classes
  4. *
  5. * @package WordPress
  6. * @subpackage Customize
  7. * @since 3.4.0
  8. */
  9. /**
  10. * Customize Manager class.
  11. *
  12. * Bootstraps the Customize experience on the server-side.
  13. *
  14. * Sets up the theme-switching process if a theme other than the active one is
  15. * being previewed and customized.
  16. *
  17. * Serves as a factory for Customize Controls and Settings, and
  18. * instantiates default Customize Controls and Settings.
  19. *
  20. * @since 3.4.0
  21. */
  22. final class WP_Customize_Manager {
  23. /**
  24. * An instance of the theme being previewed.
  25. *
  26. * @since 3.4.0
  27. * @access protected
  28. * @var WP_Theme
  29. */
  30. protected $theme;
  31. /**
  32. * The directory name of the previously active theme (within the theme_root).
  33. *
  34. * @since 3.4.0
  35. * @access protected
  36. * @var string
  37. */
  38. protected $original_stylesheet;
  39. /**
  40. * Whether this is a Customizer pageload.
  41. *
  42. * @since 3.4.0
  43. * @access protected
  44. * @var bool
  45. */
  46. protected $previewing = false;
  47. /**
  48. * Methods and properties dealing with managing widgets in the Customizer.
  49. *
  50. * @since 3.9.0
  51. * @access public
  52. * @var WP_Customize_Widgets
  53. */
  54. public $widgets;
  55. /**
  56. * Methods and properties dealing with managing nav menus in the Customizer.
  57. *
  58. * @since 4.3.0
  59. * @access public
  60. * @var WP_Customize_Nav_Menus
  61. */
  62. public $nav_menus;
  63. /**
  64. * Methods and properties dealing with selective refresh in the Customizer preview.
  65. *
  66. * @since 4.5.0
  67. * @access public
  68. * @var WP_Customize_Selective_Refresh
  69. */
  70. public $selective_refresh;
  71. /**
  72. * Registered instances of WP_Customize_Setting.
  73. *
  74. * @since 3.4.0
  75. * @access protected
  76. * @var array
  77. */
  78. protected $settings = array();
  79. /**
  80. * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
  81. *
  82. * @since 4.0.0
  83. * @access protected
  84. * @var array
  85. */
  86. protected $containers = array();
  87. /**
  88. * Registered instances of WP_Customize_Panel.
  89. *
  90. * @since 4.0.0
  91. * @access protected
  92. * @var array
  93. */
  94. protected $panels = array();
  95. /**
  96. * List of core components.
  97. *
  98. * @since 4.5.0
  99. * @access protected
  100. * @var array
  101. */
  102. protected $components = array( 'widgets', 'nav_menus' );
  103. /**
  104. * Registered instances of WP_Customize_Section.
  105. *
  106. * @since 3.4.0
  107. * @access protected
  108. * @var array
  109. */
  110. protected $sections = array();
  111. /**
  112. * Registered instances of WP_Customize_Control.
  113. *
  114. * @since 3.4.0
  115. * @access protected
  116. * @var array
  117. */
  118. protected $controls = array();
  119. /**
  120. * Panel types that may be rendered from JS templates.
  121. *
  122. * @since 4.3.0
  123. * @access protected
  124. * @var array
  125. */
  126. protected $registered_panel_types = array();
  127. /**
  128. * Section types that may be rendered from JS templates.
  129. *
  130. * @since 4.3.0
  131. * @access protected
  132. * @var array
  133. */
  134. protected $registered_section_types = array();
  135. /**
  136. * Control types that may be rendered from JS templates.
  137. *
  138. * @since 4.1.0
  139. * @access protected
  140. * @var array
  141. */
  142. protected $registered_control_types = array();
  143. /**
  144. * Initial URL being previewed.
  145. *
  146. * @since 4.4.0
  147. * @access protected
  148. * @var string
  149. */
  150. protected $preview_url;
  151. /**
  152. * URL to link the user to when closing the Customizer.
  153. *
  154. * @since 4.4.0
  155. * @access protected
  156. * @var string
  157. */
  158. protected $return_url;
  159. /**
  160. * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
  161. *
  162. * @since 4.4.0
  163. * @access protected
  164. * @var array
  165. */
  166. protected $autofocus = array();
  167. /**
  168. * Messenger channel.
  169. *
  170. * @since 4.7.0
  171. * @access protected
  172. * @var string
  173. */
  174. protected $messenger_channel;
  175. /**
  176. * Unsanitized values for Customize Settings parsed from $_POST['customized'].
  177. *
  178. * @var array
  179. */
  180. private $_post_values;
  181. /**
  182. * Changeset UUID.
  183. *
  184. * @since 4.7.0
  185. * @access private
  186. * @var string
  187. */
  188. private $_changeset_uuid;
  189. /**
  190. * Changeset post ID.
  191. *
  192. * @since 4.7.0
  193. * @access private
  194. * @var int|false
  195. */
  196. private $_changeset_post_id;
  197. /**
  198. * Changeset data loaded from a customize_changeset post.
  199. *
  200. * @since 4.7.0
  201. * @access private
  202. * @var array
  203. */
  204. private $_changeset_data;
  205. /**
  206. * Constructor.
  207. *
  208. * @since 3.4.0
  209. * @since 4.7.0 Added $args param.
  210. *
  211. * @param array $args {
  212. * Args.
  213. *
  214. * @type string $changeset_uuid Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID.
  215. * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
  216. * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param.
  217. * }
  218. */
  219. public function __construct( $args = array() ) {
  220. $args = array_merge(
  221. array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ),
  222. $args
  223. );
  224. // Note that the UUID format will be validated in the setup_theme() method.
  225. if ( ! isset( $args['changeset_uuid'] ) ) {
  226. $args['changeset_uuid'] = wp_generate_uuid4();
  227. }
  228. // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
  229. if ( ! isset( $args['theme'] ) ) {
  230. if ( isset( $_REQUEST['customize_theme'] ) ) {
  231. $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
  232. } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
  233. $args['theme'] = wp_unslash( $_REQUEST['theme'] );
  234. }
  235. }
  236. if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
  237. $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
  238. }
  239. $this->original_stylesheet = get_stylesheet();
  240. $this->theme = wp_get_theme( $args['theme'] );
  241. $this->messenger_channel = $args['messenger_channel'];
  242. $this->_changeset_uuid = $args['changeset_uuid'];
  243. require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
  244. require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
  245. require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
  246. require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
  247. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' );
  248. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' );
  249. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' );
  250. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' );
  251. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' );
  252. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' );
  253. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' );
  254. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' );
  255. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' );
  256. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' );
  257. require_once( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' );
  258. require_once( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' );
  259. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' );
  260. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' );
  261. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' );
  262. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' );
  263. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' );
  264. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' );
  265. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
  266. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
  267. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
  268. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
  269. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' );
  270. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' );
  271. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
  272. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
  273. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
  274. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
  275. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
  276. /**
  277. * Filters the core Customizer components to load.
  278. *
  279. * This allows Core components to be excluded from being instantiated by
  280. * filtering them out of the array. Note that this filter generally runs
  281. * during the {@see 'plugins_loaded'} action, so it cannot be added
  282. * in a theme.
  283. *
  284. * @since 4.4.0
  285. *
  286. * @see WP_Customize_Manager::__construct()
  287. *
  288. * @param array $components List of core components to load.
  289. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  290. */
  291. $components = apply_filters( 'customize_loaded_components', $this->components, $this );
  292. require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
  293. $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
  294. if ( in_array( 'widgets', $components, true ) ) {
  295. require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
  296. $this->widgets = new WP_Customize_Widgets( $this );
  297. }
  298. if ( in_array( 'nav_menus', $components, true ) ) {
  299. require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
  300. $this->nav_menus = new WP_Customize_Nav_Menus( $this );
  301. }
  302. add_action( 'setup_theme', array( $this, 'setup_theme' ) );
  303. add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
  304. // Do not spawn cron (especially the alternate cron) while running the Customizer.
  305. remove_action( 'init', 'wp_cron' );
  306. // Do not run update checks when rendering the controls.
  307. remove_action( 'admin_init', '_maybe_update_core' );
  308. remove_action( 'admin_init', '_maybe_update_plugins' );
  309. remove_action( 'admin_init', '_maybe_update_themes' );
  310. add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
  311. add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
  312. add_action( 'customize_register', array( $this, 'register_controls' ) );
  313. add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
  314. add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
  315. add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
  316. // Render Panel, Section, and Control templates.
  317. add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
  318. add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
  319. add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
  320. // Export header video settings with the partial response.
  321. add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
  322. // Export the settings to JS via the _wpCustomizeSettings variable.
  323. add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
  324. }
  325. /**
  326. * Return true if it's an Ajax request.
  327. *
  328. * @since 3.4.0
  329. * @since 4.2.0 Added `$action` param.
  330. * @access public
  331. *
  332. * @param string|null $action Whether the supplied Ajax action is being run.
  333. * @return bool True if it's an Ajax request, false otherwise.
  334. */
  335. public function doing_ajax( $action = null ) {
  336. if ( ! wp_doing_ajax() ) {
  337. return false;
  338. }
  339. if ( ! $action ) {
  340. return true;
  341. } else {
  342. /*
  343. * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
  344. * to check before admin-ajax.php gets to that point.
  345. */
  346. return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
  347. }
  348. }
  349. /**
  350. * Custom wp_die wrapper. Returns either the standard message for UI
  351. * or the Ajax message.
  352. *
  353. * @since 3.4.0
  354. *
  355. * @param mixed $ajax_message Ajax return
  356. * @param mixed $message UI message
  357. */
  358. protected function wp_die( $ajax_message, $message = null ) {
  359. if ( $this->doing_ajax() ) {
  360. wp_die( $ajax_message );
  361. }
  362. if ( ! $message ) {
  363. $message = __( 'Cheatin&#8217; uh?' );
  364. }
  365. if ( $this->messenger_channel ) {
  366. ob_start();
  367. wp_enqueue_scripts();
  368. wp_print_scripts( array( 'customize-base' ) );
  369. $settings = array(
  370. 'messengerArgs' => array(
  371. 'channel' => $this->messenger_channel,
  372. 'url' => wp_customize_url(),
  373. ),
  374. 'error' => $ajax_message,
  375. );
  376. ?>
  377. <script>
  378. ( function( api, settings ) {
  379. var preview = new api.Messenger( settings.messengerArgs );
  380. preview.send( 'iframe-loading-error', settings.error );
  381. } )( wp.customize, <?php echo wp_json_encode( $settings ) ?> );
  382. </script>
  383. <?php
  384. $message .= ob_get_clean();
  385. }
  386. wp_die( $message );
  387. }
  388. /**
  389. * Return the Ajax wp_die() handler if it's a customized request.
  390. *
  391. * @since 3.4.0
  392. * @deprecated 4.7.0
  393. *
  394. * @return callable Die handler.
  395. */
  396. public function wp_die_handler() {
  397. _deprecated_function( __METHOD__, '4.7.0' );
  398. if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
  399. return '_ajax_wp_die_handler';
  400. }
  401. return '_default_wp_die_handler';
  402. }
  403. /**
  404. * Start preview and customize theme.
  405. *
  406. * Check if customize query variable exist. Init filters to filter the current theme.
  407. *
  408. * @since 3.4.0
  409. */
  410. public function setup_theme() {
  411. global $pagenow;
  412. // Check permissions for customize.php access since this method is called before customize.php can run any code,
  413. if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
  414. if ( ! is_user_logged_in() ) {
  415. auth_redirect();
  416. } else {
  417. wp_die(
  418. '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
  419. '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
  420. 403
  421. );
  422. }
  423. return;
  424. }
  425. if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $this->_changeset_uuid ) ) {
  426. $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
  427. }
  428. /*
  429. * If unauthenticated then require a valid changeset UUID to load the preview.
  430. * In this way, the UUID serves as a secret key. If the messenger channel is present,
  431. * then send unauthenticated code to prompt re-auth.
  432. */
  433. if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
  434. $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
  435. }
  436. if ( ! headers_sent() ) {
  437. send_origin_headers();
  438. }
  439. // Hide the admin bar if we're embedded in the customizer iframe.
  440. if ( $this->messenger_channel ) {
  441. show_admin_bar( false );
  442. }
  443. if ( $this->is_theme_active() ) {
  444. // Once the theme is loaded, we'll validate it.
  445. add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
  446. } else {
  447. // If the requested theme is not the active theme and the user doesn't have the
  448. // switch_themes cap, bail.
  449. if ( ! current_user_can( 'switch_themes' ) ) {
  450. $this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
  451. }
  452. // If the theme has errors while loading, bail.
  453. if ( $this->theme()->errors() ) {
  454. $this->wp_die( -1, $this->theme()->errors()->get_error_message() );
  455. }
  456. // If the theme isn't allowed per multisite settings, bail.
  457. if ( ! $this->theme()->is_allowed() ) {
  458. $this->wp_die( -1, __( 'The requested theme does not exist.' ) );
  459. }
  460. }
  461. /*
  462. * Import theme starter content for fresh installs when landing in the customizer.
  463. * Import starter content at after_setup_theme:100 so that any
  464. * add_theme_support( 'starter-content' ) calls will have been made.
  465. */
  466. if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
  467. add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
  468. }
  469. $this->start_previewing_theme();
  470. }
  471. /**
  472. * Callback to validate a theme once it is loaded
  473. *
  474. * @since 3.4.0
  475. */
  476. public function after_setup_theme() {
  477. $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
  478. if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
  479. wp_redirect( 'themes.php?broken=true' );
  480. exit;
  481. }
  482. }
  483. /**
  484. * If the theme to be previewed isn't the active theme, add filter callbacks
  485. * to swap it out at runtime.
  486. *
  487. * @since 3.4.0
  488. */
  489. public function start_previewing_theme() {
  490. // Bail if we're already previewing.
  491. if ( $this->is_preview() ) {
  492. return;
  493. }
  494. $this->previewing = true;
  495. if ( ! $this->is_theme_active() ) {
  496. add_filter( 'template', array( $this, 'get_template' ) );
  497. add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
  498. add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
  499. // @link: https://core.trac.wordpress.org/ticket/20027
  500. add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
  501. add_filter( 'pre_option_template', array( $this, 'get_template' ) );
  502. // Handle custom theme roots.
  503. add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
  504. add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
  505. }
  506. /**
  507. * Fires once the Customizer theme preview has started.
  508. *
  509. * @since 3.4.0
  510. *
  511. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  512. */
  513. do_action( 'start_previewing_theme', $this );
  514. }
  515. /**
  516. * Stop previewing the selected theme.
  517. *
  518. * Removes filters to change the current theme.
  519. *
  520. * @since 3.4.0
  521. */
  522. public function stop_previewing_theme() {
  523. if ( ! $this->is_preview() ) {
  524. return;
  525. }
  526. $this->previewing = false;
  527. if ( ! $this->is_theme_active() ) {
  528. remove_filter( 'template', array( $this, 'get_template' ) );
  529. remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
  530. remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
  531. // @link: https://core.trac.wordpress.org/ticket/20027
  532. remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
  533. remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
  534. // Handle custom theme roots.
  535. remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
  536. remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
  537. }
  538. /**
  539. * Fires once the Customizer theme preview has stopped.
  540. *
  541. * @since 3.4.0
  542. *
  543. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  544. */
  545. do_action( 'stop_previewing_theme', $this );
  546. }
  547. /**
  548. * Get the changeset UUID.
  549. *
  550. * @since 4.7.0
  551. * @access public
  552. *
  553. * @return string UUID.
  554. */
  555. public function changeset_uuid() {
  556. return $this->_changeset_uuid;
  557. }
  558. /**
  559. * Get the theme being customized.
  560. *
  561. * @since 3.4.0
  562. *
  563. * @return WP_Theme
  564. */
  565. public function theme() {
  566. if ( ! $this->theme ) {
  567. $this->theme = wp_get_theme();
  568. }
  569. return $this->theme;
  570. }
  571. /**
  572. * Get the registered settings.
  573. *
  574. * @since 3.4.0
  575. *
  576. * @return array
  577. */
  578. public function settings() {
  579. return $this->settings;
  580. }
  581. /**
  582. * Get the registered controls.
  583. *
  584. * @since 3.4.0
  585. *
  586. * @return array
  587. */
  588. public function controls() {
  589. return $this->controls;
  590. }
  591. /**
  592. * Get the registered containers.
  593. *
  594. * @since 4.0.0
  595. *
  596. * @return array
  597. */
  598. public function containers() {
  599. return $this->containers;
  600. }
  601. /**
  602. * Get the registered sections.
  603. *
  604. * @since 3.4.0
  605. *
  606. * @return array
  607. */
  608. public function sections() {
  609. return $this->sections;
  610. }
  611. /**
  612. * Get the registered panels.
  613. *
  614. * @since 4.0.0
  615. * @access public
  616. *
  617. * @return array Panels.
  618. */
  619. public function panels() {
  620. return $this->panels;
  621. }
  622. /**
  623. * Checks if the current theme is active.
  624. *
  625. * @since 3.4.0
  626. *
  627. * @return bool
  628. */
  629. public function is_theme_active() {
  630. return $this->get_stylesheet() == $this->original_stylesheet;
  631. }
  632. /**
  633. * Register styles/scripts and initialize the preview of each setting
  634. *
  635. * @since 3.4.0
  636. */
  637. public function wp_loaded() {
  638. /**
  639. * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
  640. *
  641. * @since 3.4.0
  642. *
  643. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  644. */
  645. do_action( 'customize_register', $this );
  646. /*
  647. * Note that settings must be previewed here even outside the customizer preview
  648. * and also in the customizer pane itself. This is to enable loading an existing
  649. * changeset into the customizer. Previewing the settings only has to be prevented
  650. * in the case of a customize_save action because then update_option()
  651. * may short-circuit because it will detect that there are no changes to
  652. * make.
  653. */
  654. if ( ! $this->doing_ajax( 'customize_save' ) ) {
  655. foreach ( $this->settings as $setting ) {
  656. $setting->preview();
  657. }
  658. }
  659. if ( $this->is_preview() && ! is_admin() ) {
  660. $this->customize_preview_init();
  661. }
  662. }
  663. /**
  664. * Prevents Ajax requests from following redirects when previewing a theme
  665. * by issuing a 200 response instead of a 30x.
  666. *
  667. * Instead, the JS will sniff out the location header.
  668. *
  669. * @since 3.4.0
  670. * @deprecated 4.7.0
  671. *
  672. * @param int $status Status.
  673. * @return int
  674. */
  675. public function wp_redirect_status( $status ) {
  676. _deprecated_function( __FUNCTION__, '4.7.0' );
  677. if ( $this->is_preview() && ! is_admin() ) {
  678. return 200;
  679. }
  680. return $status;
  681. }
  682. /**
  683. * Find the changeset post ID for a given changeset UUID.
  684. *
  685. * @since 4.7.0
  686. * @access public
  687. *
  688. * @param string $uuid Changeset UUID.
  689. * @return int|null Returns post ID on success and null on failure.
  690. */
  691. public function find_changeset_post_id( $uuid ) {
  692. $cache_group = 'customize_changeset_post';
  693. $changeset_post_id = wp_cache_get( $uuid, $cache_group );
  694. if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
  695. return $changeset_post_id;
  696. }
  697. $changeset_post_query = new WP_Query( array(
  698. 'post_type' => 'customize_changeset',
  699. 'post_status' => get_post_stati(),
  700. 'name' => $uuid,
  701. 'posts_per_page' => 1,
  702. 'no_found_rows' => true,
  703. 'cache_results' => true,
  704. 'update_post_meta_cache' => false,
  705. 'update_post_term_cache' => false,
  706. 'lazy_load_term_meta' => false,
  707. ) );
  708. if ( ! empty( $changeset_post_query->posts ) ) {
  709. // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
  710. $changeset_post_id = $changeset_post_query->posts[0]->ID;
  711. wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
  712. return $changeset_post_id;
  713. }
  714. return null;
  715. }
  716. /**
  717. * Get the changeset post id for the loaded changeset.
  718. *
  719. * @since 4.7.0
  720. * @access public
  721. *
  722. * @return int|null Post ID on success or null if there is no post yet saved.
  723. */
  724. public function changeset_post_id() {
  725. if ( ! isset( $this->_changeset_post_id ) ) {
  726. $post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
  727. if ( ! $post_id ) {
  728. $post_id = false;
  729. }
  730. $this->_changeset_post_id = $post_id;
  731. }
  732. if ( false === $this->_changeset_post_id ) {
  733. return null;
  734. }
  735. return $this->_changeset_post_id;
  736. }
  737. /**
  738. * Get the data stored in a changeset post.
  739. *
  740. * @since 4.7.0
  741. * @access protected
  742. *
  743. * @param int $post_id Changeset post ID.
  744. * @return array|WP_Error Changeset data or WP_Error on error.
  745. */
  746. protected function get_changeset_post_data( $post_id ) {
  747. if ( ! $post_id ) {
  748. return new WP_Error( 'empty_post_id' );
  749. }
  750. $changeset_post = get_post( $post_id );
  751. if ( ! $changeset_post ) {
  752. return new WP_Error( 'missing_post' );
  753. }
  754. if ( 'customize_changeset' !== $changeset_post->post_type ) {
  755. return new WP_Error( 'wrong_post_type' );
  756. }
  757. $changeset_data = json_decode( $changeset_post->post_content, true );
  758. if ( function_exists( 'json_last_error' ) && json_last_error() ) {
  759. return new WP_Error( 'json_parse_error', '', json_last_error() );
  760. }
  761. if ( ! is_array( $changeset_data ) ) {
  762. return new WP_Error( 'expected_array' );
  763. }
  764. return $changeset_data;
  765. }
  766. /**
  767. * Get changeset data.
  768. *
  769. * @since 4.7.0
  770. * @access public
  771. *
  772. * @return array Changeset data.
  773. */
  774. public function changeset_data() {
  775. if ( isset( $this->_changeset_data ) ) {
  776. return $this->_changeset_data;
  777. }
  778. $changeset_post_id = $this->changeset_post_id();
  779. if ( ! $changeset_post_id ) {
  780. $this->_changeset_data = array();
  781. } else {
  782. $data = $this->get_changeset_post_data( $changeset_post_id );
  783. if ( ! is_wp_error( $data ) ) {
  784. $this->_changeset_data = $data;
  785. } else {
  786. $this->_changeset_data = array();
  787. }
  788. }
  789. return $this->_changeset_data;
  790. }
  791. /**
  792. * Starter content setting IDs.
  793. *
  794. * @since 4.7.0
  795. * @access private
  796. * @var array
  797. */
  798. protected $pending_starter_content_settings_ids = array();
  799. /**
  800. * Import theme starter content into the customized state.
  801. *
  802. * @since 4.7.0
  803. * @access public
  804. *
  805. * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
  806. */
  807. function import_theme_starter_content( $starter_content = array() ) {
  808. if ( empty( $starter_content ) ) {
  809. $starter_content = get_theme_starter_content();
  810. }
  811. $changeset_data = array();
  812. if ( $this->changeset_post_id() ) {
  813. $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
  814. }
  815. $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
  816. $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
  817. $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
  818. $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
  819. $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
  820. $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
  821. // Widgets.
  822. $max_widget_numbers = array();
  823. foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
  824. $sidebar_widget_ids = array();
  825. foreach ( $widgets as $widget ) {
  826. list( $id_base, $instance ) = $widget;
  827. if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
  828. // When $settings is an array-like object, get an intrinsic array for use with array_keys().
  829. $settings = get_option( "widget_{$id_base}", array() );
  830. if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
  831. $settings = $settings->getArrayCopy();
  832. }
  833. // Find the max widget number for this type.
  834. $widget_numbers = array_keys( $settings );
  835. if ( count( $widget_numbers ) > 0 ) {
  836. $widget_numbers[] = 1;
  837. $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
  838. } else {
  839. $max_widget_numbers[ $id_base ] = 1;
  840. }
  841. }
  842. $max_widget_numbers[ $id_base ] += 1;
  843. $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
  844. $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
  845. $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
  846. if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
  847. $this->set_post_value( $setting_id, $setting_value );
  848. $this->pending_starter_content_settings_ids[] = $setting_id;
  849. }
  850. $sidebar_widget_ids[] = $widget_id;
  851. }
  852. $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
  853. if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
  854. $this->set_post_value( $setting_id, $sidebar_widget_ids );
  855. $this->pending_starter_content_settings_ids[] = $setting_id;
  856. }
  857. }
  858. $starter_content_auto_draft_post_ids = array();
  859. if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
  860. $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
  861. }
  862. // Make an index of all the posts needed and what their slugs are.
  863. $needed_posts = array();
  864. $attachments = $this->prepare_starter_content_attachments( $attachments );
  865. foreach ( $attachments as $attachment ) {
  866. $key = 'attachment:' . $attachment['post_name'];
  867. $needed_posts[ $key ] = true;
  868. }
  869. foreach ( array_keys( $posts ) as $post_symbol ) {
  870. if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
  871. unset( $posts[ $post_symbol ] );
  872. continue;
  873. }
  874. if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
  875. $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
  876. }
  877. if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
  878. $posts[ $post_symbol ]['post_type'] = 'post';
  879. }
  880. $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
  881. }
  882. $all_post_slugs = array_merge(
  883. wp_list_pluck( $attachments, 'post_name' ),
  884. wp_list_pluck( $posts, 'post_name' )
  885. );
  886. /*
  887. * Obtain all post types referenced in starter content to use in query.
  888. * This is needed because 'any' will not account for post types not yet registered.
  889. */
  890. $post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
  891. // Re-use auto-draft starter content posts referenced in the current customized state.
  892. $existing_starter_content_posts = array();
  893. if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
  894. $existing_posts_query = new WP_Query( array(
  895. 'post__in' => $starter_content_auto_draft_post_ids,
  896. 'post_status' => 'auto-draft',
  897. 'post_type' => $post_types,
  898. 'posts_per_page' => -1,
  899. ) );
  900. foreach ( $existing_posts_query->posts as $existing_post ) {
  901. $post_name = $existing_post->post_name;
  902. if ( empty( $post_name ) ) {
  903. $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
  904. }
  905. $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
  906. }
  907. }
  908. // Re-use non-auto-draft posts.
  909. if ( ! empty( $all_post_slugs ) ) {
  910. $existing_posts_query = new WP_Query( array(
  911. 'post_name__in' => $all_post_slugs,
  912. 'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
  913. 'post_type' => 'any',
  914. 'posts_per_page' => -1,
  915. ) );
  916. foreach ( $existing_posts_query->posts as $existing_post ) {
  917. $key = $existing_post->post_type . ':' . $existing_post->post_name;
  918. if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
  919. $existing_starter_content_posts[ $key ] = $existing_post;
  920. }
  921. }
  922. }
  923. // Attachments are technically posts but handled differently.
  924. if ( ! empty( $attachments ) ) {
  925. $attachment_ids = array();
  926. foreach ( $attachments as $symbol => $attachment ) {
  927. $file_array = array(
  928. 'name' => $attachment['file_name'],
  929. );
  930. $file_path = $attachment['file_path'];
  931. $attachment_id = null;
  932. $attached_file = null;
  933. if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
  934. $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
  935. $attachment_id = $attachment_post->ID;
  936. $attached_file = get_attached_file( $attachment_id );
  937. if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
  938. $attachment_id = null;
  939. $attached_file = null;
  940. } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
  941. // Re-generate attachment metadata since it was previously generated for a different theme.
  942. $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
  943. wp_update_attachment_metadata( $attachment_id, $metadata );
  944. update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
  945. }
  946. }
  947. // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
  948. if ( ! $attachment_id ) {
  949. // Copy file to temp location so that original file won't get deleted from theme after sideloading.
  950. $temp_file_name = wp_tempnam( basename( $file_path ) );
  951. if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
  952. $file_array['tmp_name'] = $temp_file_name;
  953. }
  954. if ( empty( $file_array['tmp_name'] ) ) {
  955. continue;
  956. }
  957. $attachment_post_data = array_merge(
  958. wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
  959. array(
  960. 'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
  961. )
  962. );
  963. // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
  964. // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
  965. // See https://bugs.php.net/bug.php?id=65701
  966. if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
  967. clearstatcache();
  968. }
  969. $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
  970. if ( is_wp_error( $attachment_id ) ) {
  971. continue;
  972. }
  973. update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
  974. update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
  975. }
  976. $attachment_ids[ $symbol ] = $attachment_id;
  977. }
  978. $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
  979. }
  980. // Posts & pages.
  981. if ( ! empty( $posts ) ) {
  982. foreach ( array_keys( $posts ) as $post_symbol ) {
  983. if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
  984. continue;
  985. }
  986. $post_type = $posts[ $post_symbol ]['post_type'];
  987. if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
  988. $post_name = $posts[ $post_symbol ]['post_name'];
  989. } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
  990. $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
  991. } else {
  992. continue;
  993. }
  994. // Use existing auto-draft post if one already exists with the same type and name.
  995. if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
  996. $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
  997. continue;
  998. }
  999. // Translate the featured image symbol.
  1000. if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
  1001. && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
  1002. && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
  1003. $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
  1004. }
  1005. if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
  1006. $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
  1007. }
  1008. $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
  1009. if ( $r instanceof WP_Post ) {
  1010. $posts[ $post_symbol ]['ID'] = $r->ID;
  1011. }
  1012. }
  1013. $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
  1014. }
  1015. // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
  1016. if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
  1017. $setting_id = 'nav_menus_created_posts';
  1018. $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
  1019. $this->pending_starter_content_settings_ids[] = $setting_id;
  1020. }
  1021. // Nav menus.
  1022. $placeholder_id = -1;
  1023. $reused_nav_menu_setting_ids = array();
  1024. foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
  1025. $nav_menu_term_id = null;
  1026. $nav_menu_setting_id = null;
  1027. $matches = array();
  1028. // Look for an existing placeholder menu with starter content to re-use.
  1029. foreach ( $changeset_data as $setting_id => $setting_params ) {
  1030. $can_reuse = (
  1031. ! empty( $setting_params['starter_content'] )
  1032. &&
  1033. ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
  1034. &&
  1035. preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
  1036. );
  1037. if ( $can_reuse ) {
  1038. $nav_menu_term_id = intval( $matches['nav_menu_id'] );
  1039. $nav_menu_setting_id = $setting_id;
  1040. $reused_nav_menu_setting_ids[] = $setting_id;
  1041. break;
  1042. }
  1043. }
  1044. if ( ! $nav_menu_term_id ) {
  1045. while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
  1046. $placeholder_id--;
  1047. }
  1048. $nav_menu_term_id = $placeholder_id;
  1049. $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
  1050. }
  1051. $this->set_post_value( $nav_menu_setting_id, array(
  1052. 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
  1053. ) );
  1054. $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
  1055. // @todo Add support for menu_item_parent.
  1056. $position = 0;
  1057. foreach ( $nav_menu['items'] as $nav_menu_item ) {
  1058. $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
  1059. if ( ! isset( $nav_menu_item['position'] ) ) {
  1060. $nav_menu_item['position'] = $position++;
  1061. }
  1062. $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
  1063. if ( isset( $nav_menu_item['object_id'] ) ) {
  1064. if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
  1065. $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
  1066. if ( empty( $nav_menu_item['title'] ) ) {
  1067. $original_object = get_post( $nav_menu_item['object_id'] );
  1068. $nav_menu_item['title'] = $original_object->post_title;
  1069. }
  1070. } else {
  1071. continue;
  1072. }
  1073. } else {
  1074. $nav_menu_item['object_id'] = 0;
  1075. }
  1076. if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
  1077. $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
  1078. $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
  1079. }
  1080. }
  1081. $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
  1082. if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
  1083. $this->set_post_value( $setting_id, $nav_menu_term_id );
  1084. $this->pending_starter_content_settings_ids[] = $setting_id;
  1085. }
  1086. }
  1087. // Options.
  1088. foreach ( $options as $name => $value ) {
  1089. if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
  1090. if ( isset( $posts[ $matches['symbol'] ] ) ) {
  1091. $value = $posts[ $matches['symbol'] ]['ID'];
  1092. } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
  1093. $value = $attachment_ids[ $matches['symbol'] ];
  1094. } else {
  1095. continue;
  1096. }
  1097. }
  1098. if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
  1099. $this->set_post_value( $name, $value );
  1100. $this->pending_starter_content_settings_ids[] = $name;
  1101. }
  1102. }
  1103. // Theme mods.
  1104. foreach ( $theme_mods as $name => $value ) {
  1105. if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
  1106. if ( isset( $posts[ $matches['symbol'] ] ) ) {
  1107. $value = $posts[ $matches['symbol'] ]['ID'];
  1108. } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
  1109. $value = $attachment_ids[ $matches['symbol'] ];
  1110. } else {
  1111. continue;
  1112. }
  1113. }
  1114. // Handle header image as special case since setting has a legacy format.
  1115. if ( 'header_image' === $name ) {
  1116. $name = 'header_image_data';
  1117. $metadata = wp_get_attachment_metadata( $value );
  1118. if ( empty( $metadata ) ) {
  1119. continue;
  1120. }
  1121. $value = array(
  1122. 'attachment_id' => $value,
  1123. 'url' => wp_get_attachment_url( $value ),
  1124. 'height' => $metadata['height'],
  1125. 'width' => $metadata['width'],
  1126. );
  1127. } elseif ( 'background_image' === $name ) {
  1128. $value = wp_get_attachment_url( $value );
  1129. }
  1130. if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
  1131. $this->set_post_value( $name, $value );
  1132. $this->pending_starter_content_settings_ids[] = $name;
  1133. }
  1134. }
  1135. if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
  1136. if ( did_action( 'customize_register' ) ) {
  1137. $this->_save_starter_content_changeset();
  1138. } else {
  1139. add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
  1140. }
  1141. }
  1142. }
  1143. /**
  1144. * Prepare starter content attachments.
  1145. *
  1146. * Ensure that the attachments are valid and that they have slugs and file name/path.
  1147. *
  1148. * @since 4.7.0
  1149. * @access private
  1150. *
  1151. * @param array $attachments Attachments.
  1152. * @return array Prepared attachments.
  1153. */
  1154. protected function prepare_starter_content_attachments( $attachments ) {
  1155. $prepared_attachments = array();
  1156. if ( empty( $attachments ) ) {
  1157. return $prepared_attachments;
  1158. }
  1159. // Such is The WordPress Way.
  1160. require_once( ABSPATH . 'wp-admin/includes/file.php' );
  1161. require_once( ABSPATH . 'wp-admin/includes/media.php' );
  1162. require_once( ABSPATH . 'wp-admin/includes/image.php' );
  1163. foreach ( $attachments as $symbol => $attachment ) {
  1164. // A file is required and URLs to files are not currently allowed.
  1165. if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
  1166. continue;
  1167. }
  1168. $file_path = null;
  1169. if ( file_exists( $attachment['file'] ) ) {
  1170. $file_path = $attachment['file']; // Could be absolute path to file in plugin.
  1171. } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
  1172. $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
  1173. } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
  1174. $file_path = get_template_directory() . '/' . $attachment['file'];
  1175. } else {
  1176. continue;
  1177. }
  1178. $file_name = basename( $attachment['file'] );
  1179. // Skip file types that are not recognized.
  1180. $checked_filetype = wp_check_filetype( $file_name );
  1181. if ( empty( $checked_filetype['type'] ) ) {
  1182. continue;
  1183. }
  1184. // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
  1185. if ( empty( $attachment['post_name'] ) ) {
  1186. if ( ! empty( $attachment['post_title'] ) ) {
  1187. $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
  1188. } else {
  1189. $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
  1190. }
  1191. }
  1192. $attachment['file_name'] = $file_name;
  1193. $attachment['file_path'] = $file_path;
  1194. $prepared_attachments[ $symbol ] = $attachment;
  1195. }
  1196. return $prepared_attachments;
  1197. }
  1198. /**
  1199. * Save starter content changeset.
  1200. *
  1201. * @since 4.7.0
  1202. * @access private
  1203. */
  1204. public function _save_starter_content_changeset() {
  1205. if ( empty( $this->pending_starter_content_settings_ids ) ) {
  1206. return;
  1207. }
  1208. $this->save_changeset_post( array(
  1209. 'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
  1210. 'starter_content' => true,
  1211. ) );
  1212. $this->pending_starter_content_settings_ids = array();
  1213. }
  1214. /**
  1215. * Get dirty pre-sanitized setting values in the current customized state.
  1216. *
  1217. * The returned array consists of a merge of three sources:
  1218. * 1. If the theme is not currently active, then the base array is any stashed
  1219. * theme mods that were modified previously but never published.
  1220. * 2. The values from the current changeset, if it exists.
  1221. * 3. If the user can customize, the values parsed from the incoming
  1222. * `$_POST['customized']` JSON data.
  1223. * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
  1224. *
  1225. * The name "unsanitized_post_values" is a carry-over from when the customized
  1226. * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
  1227. * the value returned will come from the current changeset post and from the
  1228. * incoming post data.
  1229. *
  1230. * @since 4.1.1
  1231. * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
  1232. *
  1233. * @param array $args {
  1234. * Args.
  1235. *
  1236. * @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
  1237. * @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
  1238. * }
  1239. * @return array
  1240. */
  1241. public function unsanitized_post_values( $args = array() ) {
  1242. $args = array_merge(
  1243. array(
  1244. 'exclude_changeset' => false,
  1245. 'exclude_post_data' => ! current_user_can( 'customize' ),
  1246. ),
  1247. $args
  1248. );
  1249. $values = array();
  1250. // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
  1251. if ( ! $this->is_theme_active() ) {
  1252. $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
  1253. $stylesheet = $this->get_stylesheet();
  1254. if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
  1255. $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
  1256. }
  1257. }
  1258. if ( ! $args['exclude_changeset'] ) {
  1259. foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
  1260. if ( ! array_key_exists( 'value', $setting_params ) ) {
  1261. continue;
  1262. }
  1263. if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
  1264. // Ensure that theme mods values are only used if they were saved under the current theme.
  1265. $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
  1266. if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
  1267. $values[ $matches['setting_id'] ] = $setting_params['value'];
  1268. }
  1269. } else {
  1270. $values[ $setting_id ] = $setting_params['value'];
  1271. }
  1272. }
  1273. }
  1274. if ( ! $args['exclude_post_data'] ) {
  1275. if ( ! isset( $this->_post_values ) ) {
  1276. if ( isset( $_POST['customized'] ) ) {
  1277. $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
  1278. } else {
  1279. $post_values = array();
  1280. }
  1281. if ( is_array( $post_values ) ) {
  1282. $this->_post_values = $post_values;
  1283. } else {
  1284. $this->_post_values = array();
  1285. }
  1286. }
  1287. $values = array_merge( $values, $this->_post_values );
  1288. }
  1289. return $values;
  1290. }
  1291. /**
  1292. * Returns the sanitized value for a given setting from the current customized state.
  1293. *
  1294. * The name "post_value" is a carry-over from when the customized state was exclusively
  1295. * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
  1296. * from the current changeset post and from the incoming post data.
  1297. *
  1298. * @since 3.4.0
  1299. * @since 4.1.1 Introduced the `$default` parameter.
  1300. * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
  1301. * @access public
  1302. *
  1303. * @see WP_REST_Server::dispatch()
  1304. * @see WP_Rest_Request::sanitize_params()
  1305. * @see WP_Rest_Request::has_valid_params()
  1306. *
  1307. * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
  1308. * @param mixed $default Value returned $setting has no post value (added in 4.2.0)
  1309. * or the post value is invalid (added in 4.6.0).
  1310. * @return string|mixed $post_value Sanitized value or the $default provided.
  1311. */
  1312. public function post_value( $setting, $default = null ) {
  1313. $post_values = $this->unsanitized_post_values();
  1314. if ( ! array_key_exists( $setting->id, $post_values ) ) {
  1315. return $default;
  1316. }
  1317. $value = $post_values[ $setting->id ];
  1318. $valid = $setting->validate( $value );
  1319. if ( is_wp_error( $valid ) ) {
  1320. return $default;
  1321. }
  1322. $value = $setting->sanitize( $value );
  1323. if ( is_null( $value ) || is_wp_error( $value ) ) {
  1324. return $default;
  1325. }
  1326. return $value;
  1327. }
  1328. /**
  1329. * Override a setting's value in the current customized state.
  1330. *
  1331. * The name "post_value" is a carry-over from when the customized state was
  1332. * exclusively sourced from `$_POST['customized']`.
  1333. *
  1334. * @since 4.2.0
  1335. * @access public
  1336. *
  1337. * @param string $setting_id ID for the WP_Customize_Setting instance.
  1338. * @param mixed $value Post value.
  1339. */
  1340. public function set_post_value( $setting_id, $value ) {
  1341. $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
  1342. $this->_post_values[ $setting_id ] = $value;
  1343. /**
  1344. * Announce when a specific setting's unsanitized post value has been set.
  1345. *
  1346. * Fires when the WP_Customize_Manager::set_post_value() method is called.
  1347. *
  1348. * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
  1349. *
  1350. * @since 4.4.0
  1351. *
  1352. * @param mixed $value Unsanitized setting post value.
  1353. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  1354. */
  1355. do_action( "customize_post_value_set_{$setting_id}", $value, $this );
  1356. /**
  1357. * Announce when any setting's unsanitized post value has been set.
  1358. *
  1359. * Fires when the WP_Customize_Manager::set_post_value() method is called.
  1360. *
  1361. * This is useful for `WP_Customize_Setting` instances to watch
  1362. * in order to update a cached previewed value.
  1363. *
  1364. * @since 4.4.0
  1365. *
  1366. * @param string $setting_id Setting ID.
  1367. * @param mixed $value Unsanitized setting post value.
  1368. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  1369. */
  1370. do_action( 'customize_post_value_set', $setting_id, $value, $this );
  1371. }
  1372. /**
  1373. * Print JavaScript settings.
  1374. *
  1375. * @since 3.4.0
  1376. */
  1377. public function customize_preview_init() {
  1378. /*
  1379. * Now that Customizer previews are loaded into iframes via GET requests
  1380. * and natural URLs with transaction UUIDs added, we need to ensure that
  1381. * the responses are never cached by proxies. In practice, this will not
  1382. * be needed if the user is logged-in anyway. But if anonymous access is
  1383. * allowed then the auth cookies would not be sent and WordPress would
  1384. * not send no-cache headers by default.
  1385. */
  1386. if ( ! headers_sent() ) {
  1387. nocache_headers();
  1388. header( 'X-Robots: noindex, nofollow, noarchive' );
  1389. }
  1390. add_action( 'wp_head', 'wp_no_robots' );
  1391. add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
  1392. /*
  1393. * If preview is being served inside the customizer preview iframe, and
  1394. * if the user doesn't have customize capability, then it is assumed
  1395. * that the user's session has expired and they need to re-authenticate.
  1396. */
  1397. if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
  1398. $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
  1399. return;
  1400. }
  1401. $this->prepare_controls();
  1402. add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
  1403. wp_enqueue_script( 'customize-preview' );
  1404. wp_enqueue_style( 'customize-preview' );
  1405. add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
  1406. add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
  1407. add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
  1408. add_filter( 'get_edit_post_link', '__return_empty_string' );
  1409. /**
  1410. * Fires once the Customizer preview has initialized and JavaScript
  1411. * settings have been printed.
  1412. *
  1413. * @since 3.4.0
  1414. *
  1415. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  1416. */
  1417. do_action( 'customize_preview_init', $this );
  1418. }
  1419. /**
  1420. * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
  1421. *
  1422. * @since 4.7.0
  1423. * @access public
  1424. *
  1425. * @param array $headers Headers.
  1426. * @return array Headers.
  1427. */
  1428. public function filter_iframe_security_headers( $headers ) {
  1429. $customize_url = admin_url( 'customize.php' );
  1430. $headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
  1431. $headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
  1432. return $headers;
  1433. }
  1434. /**
  1435. * Add customize state query params to a given URL if preview is allowed.
  1436. *
  1437. * @since 4.7.0
  1438. * @access public
  1439. * @see wp_redirect()
  1440. * @see WP_Customize_Manager::get_allowed_url()
  1441. *
  1442. * @param string $url URL.
  1443. * @return string URL.
  1444. */
  1445. public function add_state_query_params( $url ) {
  1446. $parsed_original_url = wp_parse_url( $url );
  1447. $is_allowed = false;
  1448. foreach ( $this->get_allowed_urls() as $allowed_url ) {
  1449. $parsed_allowed_url = wp_parse_url( $allowed_url );
  1450. $is_allowed = (
  1451. $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
  1452. &&
  1453. $parsed_allowed_url['host'] === $parsed_original_url['host']
  1454. &&
  1455. 0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
  1456. );
  1457. if ( $is_allowed ) {
  1458. break;
  1459. }
  1460. }
  1461. if ( $is_allowed ) {
  1462. $query_params = array(
  1463. 'customize_changeset_uuid' => $this->changeset_uuid(),
  1464. );
  1465. if ( ! $this->is_theme_active() ) {
  1466. $query_params['customize_theme'] = $this->get_stylesheet();
  1467. }
  1468. if ( $this->messenger_channel ) {
  1469. $query_params['customize_messenger_channel'] = $this->messenger_channel;
  1470. }
  1471. $url = add_query_arg( $query_params, $url );
  1472. }
  1473. return $url;
  1474. }
  1475. /**
  1476. * Prevent sending a 404 status when returning the response for the customize
  1477. * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
  1478. *
  1479. * @since 4.0.0
  1480. * @deprecated 4.7.0
  1481. * @access public
  1482. */
  1483. public function customize_preview_override_404_status() {
  1484. _deprecated_function( __METHOD__, '4.7.0' );
  1485. }
  1486. /**
  1487. * Print base element for preview frame.
  1488. *
  1489. * @since 3.4.0
  1490. * @deprecated 4.7.0
  1491. */
  1492. public function customize_preview_base() {
  1493. _deprecated_function( __METHOD__, '4.7.0' );
  1494. }
  1495. /**
  1496. * Print a workaround to handle HTML5 tags in IE < 9.
  1497. *
  1498. * @since 3.4.0
  1499. * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
  1500. */
  1501. public function customize_preview_html5() {
  1502. _deprecated_function( __FUNCTION__, '4.7.0' );
  1503. }
  1504. /**
  1505. * Print CSS for loading indicators for the Customizer preview.
  1506. *
  1507. * @since 4.2.0
  1508. * @access public
  1509. */
  1510. public function customize_preview_loading_style() {
  1511. ?><style>
  1512. body.wp-customizer-unloading {
  1513. opacity: 0.25;
  1514. cursor: progress !important;
  1515. -webkit-transition: opacity 0.5s;
  1516. transition: opacity 0.5s;
  1517. }
  1518. body.wp-customizer-unloading * {
  1519. pointer-events: none !important;
  1520. }
  1521. form.customize-unpreviewable,
  1522. form.customize-unpreviewable input,
  1523. form.customize-unpreviewable select,
  1524. form.customize-unpreviewable button,
  1525. a.customize-unpreviewable,
  1526. area.customize-unpreviewable {
  1527. cursor: not-allowed !important;
  1528. }
  1529. </style><?php
  1530. }
  1531. /**
  1532. * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
  1533. *
  1534. * This ensures that the admin bar will be shown. It also ensures that link navigation will
  1535. * work as expected since the parent frame is not being sent the URL to navigate to.
  1536. *
  1537. * @since 4.7.0
  1538. * @access public
  1539. */
  1540. public function remove_frameless_preview_messenger_channel() {
  1541. if ( ! $this->messenger_channel ) {
  1542. return;
  1543. }
  1544. ?>
  1545. <script>
  1546. ( function() {
  1547. var urlParser, oldQueryParams, newQueryParams, i;
  1548. if ( parent !== window ) {
  1549. return;
  1550. }
  1551. urlParser = document.createElement( 'a' );
  1552. urlParser.href = location.href;
  1553. oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
  1554. newQueryParams = [];
  1555. for ( i = 0; i < oldQueryParams.length; i += 1 ) {
  1556. if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
  1557. newQueryParams.push( oldQueryParams[ i ] );
  1558. }
  1559. }
  1560. urlParser.search = newQueryParams.join( '&' );
  1561. if ( urlParser.search !== location.search ) {
  1562. location.replace( urlParser.href );
  1563. }
  1564. } )();
  1565. </script>
  1566. <?php
  1567. }
  1568. /**
  1569. * Print JavaScript settings for preview frame.
  1570. *
  1571. * @since 3.4.0
  1572. */
  1573. public function customize_preview_settings() {
  1574. $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
  1575. $setting_validities = $this->validate_setting_values( $post_values );
  1576. $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
  1577. // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
  1578. $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
  1579. $state_query_params = array(
  1580. 'customize_theme',
  1581. 'customize_changeset_uuid',
  1582. 'customize_messenger_channel',
  1583. );
  1584. $self_url = remove_query_arg( $state_query_params, $self_url );
  1585. $allowed_urls = $this->get_allowed_urls();
  1586. $allowed_hosts = array();
  1587. foreach ( $allowed_urls as $allowed_url ) {
  1588. $parsed = wp_parse_url( $allowed_url );
  1589. if ( empty( $parsed['host'] ) ) {
  1590. continue;
  1591. }
  1592. $host = $parsed['host'];
  1593. if ( ! empty( $parsed['port'] ) ) {
  1594. $host .= ':' . $parsed['port'];
  1595. }
  1596. $allowed_hosts[] = $host;
  1597. }
  1598. $switched_locale = switch_to_locale( get_user_locale() );
  1599. $l10n = array(
  1600. 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
  1601. 'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
  1602. 'formUnpreviewable' => __( 'This form is not live-previewable.' ),
  1603. );
  1604. if ( $switched_locale ) {
  1605. restore_previous_locale();
  1606. }
  1607. $settings = array(
  1608. 'changeset' => array(
  1609. 'uuid' => $this->_changeset_uuid,
  1610. ),
  1611. 'timeouts' => array(
  1612. 'selectiveRefresh' => 250,
  1613. 'keepAliveSend' => 1000,
  1614. ),
  1615. 'theme' => array(
  1616. 'stylesheet' => $this->get_stylesheet(),
  1617. 'active' => $this->is_theme_active(),
  1618. ),
  1619. 'url' => array(
  1620. 'self' => $self_url,
  1621. 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
  1622. 'allowedHosts' => array_unique( $allowed_hosts ),
  1623. 'isCrossDomain' => $this->is_cross_domain(),
  1624. ),
  1625. 'channel' => $this->messenger_channel,
  1626. 'activePanels' => array(),
  1627. 'activeSections' => array(),
  1628. 'activeControls' => array(),
  1629. 'settingValidities' => $exported_setting_validities,
  1630. 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
  1631. 'l10n' => $l10n,
  1632. '_dirty' => array_keys( $post_values ),
  1633. );
  1634. foreach ( $this->panels as $panel_id => $panel ) {
  1635. if ( $panel->check_capabilities() ) {
  1636. $settings['activePanels'][ $panel_id ] = $panel->active();
  1637. foreach ( $panel->sections as $section_id => $section ) {
  1638. if ( $section->check_capabilities() ) {
  1639. $settings['activeSections'][ $section_id ] = $section->active();
  1640. }
  1641. }
  1642. }
  1643. }
  1644. foreach ( $this->sections as $id => $section ) {
  1645. if ( $section->check_capabilities() ) {
  1646. $settings['activeSections'][ $id ] = $section->active();
  1647. }
  1648. }
  1649. foreach ( $this->controls as $id => $control ) {
  1650. if ( $control->check_capabilities() ) {
  1651. $settings['activeControls'][ $id ] = $control->active();
  1652. }
  1653. }
  1654. ?>
  1655. <script type="text/javascript">
  1656. var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
  1657. _wpCustomizeSettings.values = {};
  1658. (function( v ) {
  1659. <?php
  1660. /*
  1661. * Serialize settings separately from the initial _wpCustomizeSettings
  1662. * serialization in order to avoid a peak memory usage spike.
  1663. * @todo We may not even need to export the values at all since the pane syncs them anyway.
  1664. */
  1665. foreach ( $this->settings as $id => $setting ) {
  1666. if ( $setting->check_capabilities() ) {
  1667. printf(
  1668. "v[%s] = %s;\n",
  1669. wp_json_encode( $id ),
  1670. wp_json_encode( $setting->js_value() )
  1671. );
  1672. }
  1673. }
  1674. ?>
  1675. })( _wpCustomizeSettings.values );
  1676. </script>
  1677. <?php
  1678. }
  1679. /**
  1680. * Prints a signature so we can ensure the Customizer was properly executed.
  1681. *
  1682. * @since 3.4.0
  1683. * @deprecated 4.7.0
  1684. */
  1685. public function customize_preview_signature() {
  1686. _deprecated_function( __METHOD__, '4.7.0' );
  1687. }
  1688. /**
  1689. * Removes the signature in case we experience a case where the Customizer was not properly executed.
  1690. *
  1691. * @since 3.4.0
  1692. * @deprecated 4.7.0
  1693. *
  1694. * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
  1695. * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
  1696. */
  1697. public function remove_preview_signature( $return = null ) {
  1698. _deprecated_function( __METHOD__, '4.7.0' );
  1699. return $return;
  1700. }
  1701. /**
  1702. * Is it a theme preview?
  1703. *
  1704. * @since 3.4.0
  1705. *
  1706. * @return bool True if it's a preview, false if not.
  1707. */
  1708. public function is_preview() {
  1709. return (bool) $this->previewing;
  1710. }
  1711. /**
  1712. * Retrieve the template name of the previewed theme.
  1713. *
  1714. * @since 3.4.0
  1715. *
  1716. * @return string Template name.
  1717. */
  1718. public function get_template() {
  1719. return $this->theme()->get_template();
  1720. }
  1721. /**
  1722. * Retrieve the stylesheet name of the previewed theme.
  1723. *
  1724. * @since 3.4.0
  1725. *
  1726. * @return string Stylesheet name.
  1727. */
  1728. public function get_stylesheet() {
  1729. return $this->theme()->get_stylesheet();
  1730. }
  1731. /**
  1732. * Retrieve the template root of the previewed theme.
  1733. *
  1734. * @since 3.4.0
  1735. *
  1736. * @return string Theme root.
  1737. */
  1738. public function get_template_root() {
  1739. return get_raw_theme_root( $this->get_template(), true );
  1740. }
  1741. /**
  1742. * Retrieve the stylesheet root of the previewed theme.
  1743. *
  1744. * @since 3.4.0
  1745. *
  1746. * @return string Theme root.
  1747. */
  1748. public function get_stylesheet_root() {
  1749. return get_raw_theme_root( $this->get_stylesheet(), true );
  1750. }
  1751. /**
  1752. * Filters the current theme and return the name of the previewed theme.
  1753. *
  1754. * @since 3.4.0
  1755. *
  1756. * @param $current_theme {@internal Parameter is not used}
  1757. * @return string Theme name.
  1758. */
  1759. public function current_theme( $current_theme ) {
  1760. return $this->theme()->display('Name');
  1761. }
  1762. /**
  1763. * Validates setting values.
  1764. *
  1765. * Validation is skipped for unregistered settings or for values that are
  1766. * already null since they will be skipped anyway. Sanitization is applied
  1767. * to values that pass validation, and values that become null or `WP_Error`
  1768. * after sanitizing are marked invalid.
  1769. *
  1770. * @since 4.6.0
  1771. * @access public
  1772. *
  1773. * @see WP_REST_Request::has_valid_params()
  1774. * @see WP_Customize_Setting::validate()
  1775. *
  1776. * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
  1777. * @param array $options {
  1778. * Options.
  1779. *
  1780. * @type bool $validate_existence Whether a setting's existence will be checked.
  1781. * @type bool $validate_capability Whether the setting capability will be checked.
  1782. * }
  1783. * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
  1784. */
  1785. public function validate_setting_values( $setting_values, $options = array() ) {
  1786. $options = wp_parse_args( $options, array(
  1787. 'validate_capability' => false,
  1788. 'validate_existence' => false,
  1789. ) );
  1790. $validities = array();
  1791. foreach ( $setting_values as $setting_id => $unsanitized_value ) {
  1792. $setting = $this->get_setting( $setting_id );
  1793. if ( ! $setting ) {
  1794. if ( $options['validate_existence'] ) {
  1795. $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
  1796. }
  1797. continue;
  1798. }
  1799. if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
  1800. $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
  1801. } else {
  1802. if ( is_null( $unsanitized_value ) ) {
  1803. continue;
  1804. }
  1805. $validity = $setting->validate( $unsanitized_value );
  1806. }
  1807. if ( ! is_wp_error( $validity ) ) {
  1808. /** This filter is documented in wp-includes/class-wp-customize-setting.php */
  1809. $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
  1810. if ( ! empty( $late_validity->errors ) ) {
  1811. $validity = $late_validity;
  1812. }
  1813. }
  1814. if ( ! is_wp_error( $validity ) ) {
  1815. $value = $setting->sanitize( $unsanitized_value );
  1816. if ( is_null( $value ) ) {
  1817. $validity = false;
  1818. } elseif ( is_wp_error( $value ) ) {
  1819. $validity = $value;
  1820. }
  1821. }
  1822. if ( false === $validity ) {
  1823. $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
  1824. }
  1825. $validities[ $setting_id ] = $validity;
  1826. }
  1827. return $validities;
  1828. }
  1829. /**
  1830. * Prepares setting validity for exporting to the client (JS).
  1831. *
  1832. * Converts `WP_Error` instance into array suitable for passing into the
  1833. * `wp.customize.Notification` JS model.
  1834. *
  1835. * @since 4.6.0
  1836. * @access public
  1837. *
  1838. * @param true|WP_Error $validity Setting validity.
  1839. * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
  1840. * to their respective `message` and `data` to pass into the
  1841. * `wp.customize.Notification` JS model.
  1842. */
  1843. public function prepare_setting_validity_for_js( $validity ) {
  1844. if ( is_wp_error( $validity ) ) {
  1845. $notification = array();
  1846. foreach ( $validity->errors as $error_code => $error_messages ) {
  1847. $notification[ $error_code ] = array(
  1848. 'message' => join( ' ', $error_messages ),
  1849. 'data' => $validity->get_error_data( $error_code ),
  1850. );
  1851. }
  1852. return $notification;
  1853. } else {
  1854. return true;
  1855. }
  1856. }
  1857. /**
  1858. * Handle customize_save WP Ajax request to save/update a changeset.
  1859. *
  1860. * @since 3.4.0
  1861. * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
  1862. */
  1863. public function save() {
  1864. if ( ! is_user_logged_in() ) {
  1865. wp_send_json_error( 'unauthenticated' );
  1866. }
  1867. if ( ! $this->is_preview() ) {
  1868. wp_send_json_error( 'not_preview' );
  1869. }
  1870. $action = 'save-customize_' . $this->get_stylesheet();
  1871. if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
  1872. wp_send_json_error( 'invalid_nonce' );
  1873. }
  1874. $changeset_post_id = $this->changeset_post_id();
  1875. if ( empty( $changeset_post_id ) ) {
  1876. if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
  1877. wp_send_json_error( 'cannot_create_changeset_post' );
  1878. }
  1879. } else {
  1880. if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
  1881. wp_send_json_error( 'cannot_edit_changeset_post' );
  1882. }
  1883. }
  1884. if ( ! empty( $_POST['customize_changeset_data'] ) ) {
  1885. $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
  1886. if ( ! is_array( $input_changeset_data ) ) {
  1887. wp_send_json_error( 'invalid_customize_changeset_data' );
  1888. }
  1889. } else {
  1890. $input_changeset_data = array();
  1891. }
  1892. // Validate title.
  1893. $changeset_title = null;
  1894. if ( isset( $_POST['customize_changeset_title'] ) ) {
  1895. $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
  1896. }
  1897. // Validate changeset status param.
  1898. $is_publish = null;
  1899. $changeset_status = null;
  1900. if ( isset( $_POST['customize_changeset_status'] ) ) {
  1901. $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
  1902. if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
  1903. wp_send_json_error( 'bad_customize_changeset_status', 400 );
  1904. }
  1905. $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
  1906. if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
  1907. wp_send_json_error( 'changeset_publish_unauthorized', 403 );
  1908. }
  1909. }
  1910. /*
  1911. * Validate changeset date param. Date is assumed to be in local time for
  1912. * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
  1913. * is parsed with strtotime() so that ISO date format may be supplied
  1914. * or a string like "+10 minutes".
  1915. */
  1916. $changeset_date_gmt = null;
  1917. if ( isset( $_POST['customize_changeset_date'] ) ) {
  1918. $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
  1919. if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
  1920. $mm = substr( $changeset_date, 5, 2 );
  1921. $jj = substr( $changeset_date, 8, 2 );
  1922. $aa = substr( $changeset_date, 0, 4 );
  1923. $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
  1924. if ( ! $valid_date ) {
  1925. wp_send_json_error( 'bad_customize_changeset_date', 400 );
  1926. }
  1927. $changeset_date_gmt = get_gmt_from_date( $changeset_date );
  1928. } else {
  1929. $timestamp = strtotime( $changeset_date );
  1930. if ( ! $timestamp ) {
  1931. wp_send_json_error( 'bad_customize_changeset_date', 400 );
  1932. }
  1933. $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
  1934. }
  1935. }
  1936. $r = $this->save_changeset_post( array(
  1937. 'status' => $changeset_status,
  1938. 'title' => $changeset_title,
  1939. 'date_gmt' => $changeset_date_gmt,
  1940. 'data' => $input_changeset_data,
  1941. ) );
  1942. if ( is_wp_error( $r ) ) {
  1943. $response = array(
  1944. 'message' => $r->get_error_message(),
  1945. 'code' => $r->get_error_code(),
  1946. );
  1947. if ( is_array( $r->get_error_data() ) ) {
  1948. $response = array_merge( $response, $r->get_error_data() );
  1949. } else {
  1950. $response['data'] = $r->get_error_data();
  1951. }
  1952. } else {
  1953. $response = $r;
  1954. // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
  1955. $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
  1956. if ( $is_publish && 'trash' === $response['changeset_status'] ) {
  1957. $response['changeset_status'] = 'publish';
  1958. }
  1959. if ( 'publish' === $response['changeset_status'] ) {
  1960. $response['next_changeset_uuid'] = wp_generate_uuid4();
  1961. }
  1962. }
  1963. if ( isset( $response['setting_validities'] ) ) {
  1964. $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
  1965. }
  1966. /**
  1967. * Filters response data for a successful customize_save Ajax request.
  1968. *
  1969. * This filter does not apply if there was a nonce or authentication failure.
  1970. *
  1971. * @since 4.2.0
  1972. *
  1973. * @param array $response Additional information passed back to the 'saved'
  1974. * event on `wp.customize`.
  1975. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  1976. */
  1977. $response = apply_filters( 'customize_save_response', $response, $this );
  1978. if ( is_wp_error( $r ) ) {
  1979. wp_send_json_error( $response );
  1980. } else {
  1981. wp_send_json_success( $response );
  1982. }
  1983. }
  1984. /**
  1985. * Save the post for the loaded changeset.
  1986. *
  1987. * @since 4.7.0
  1988. * @access public
  1989. *
  1990. * @param array $args {
  1991. * Args for changeset post.
  1992. *
  1993. * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values.
  1994. * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
  1995. * @type string $title Post title. Optional.
  1996. * @type string $date_gmt Date in GMT. Optional.
  1997. * @type int $user_id ID for user who is saving the changeset. Optional, defaults to the current user ID.
  1998. * @type bool $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
  1999. * }
  2000. *
  2001. * @return array|WP_Error Returns array on success and WP_Error with array data on error.
  2002. */
  2003. function save_changeset_post( $args = array() ) {
  2004. $args = array_merge(
  2005. array(
  2006. 'status' => null,
  2007. 'title' => null,
  2008. 'data' => array(),
  2009. 'date_gmt' => null,
  2010. 'user_id' => get_current_user_id(),
  2011. 'starter_content' => false,
  2012. ),
  2013. $args
  2014. );
  2015. $changeset_post_id = $this->changeset_post_id();
  2016. $existing_changeset_data = array();
  2017. if ( $changeset_post_id ) {
  2018. $existing_status = get_post_status( $changeset_post_id );
  2019. if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
  2020. return new WP_Error( 'changeset_already_published' );
  2021. }
  2022. $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
  2023. }
  2024. // Fail if attempting to publish but publish hook is missing.
  2025. if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
  2026. return new WP_Error( 'missing_publish_callback' );
  2027. }
  2028. // Validate date.
  2029. $now = gmdate( 'Y-m-d H:i:59' );
  2030. if ( $args['date_gmt'] ) {
  2031. $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
  2032. if ( ! $is_future_dated ) {
  2033. return new WP_Error( 'not_future_date' ); // Only future dates are allowed.
  2034. }
  2035. if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
  2036. return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
  2037. }
  2038. $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
  2039. if ( $will_remain_auto_draft ) {
  2040. return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
  2041. }
  2042. } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
  2043. // Fail if the new status is future but the existing post's date is not in the future.
  2044. $changeset_post = get_post( $changeset_post_id );
  2045. if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
  2046. return new WP_Error( 'not_future_date' );
  2047. }
  2048. }
  2049. // The request was made via wp.customize.previewer.save().
  2050. $update_transactionally = (bool) $args['status'];
  2051. $allow_revision = (bool) $args['status'];
  2052. // Amend post values with any supplied data.
  2053. foreach ( $args['data'] as $setting_id => $setting_params ) {
  2054. if ( array_key_exists( 'value', $setting_params ) ) {
  2055. $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
  2056. }
  2057. }
  2058. // Note that in addition to post data, this will include any stashed theme mods.
  2059. $post_values = $this->unsanitized_post_values( array(
  2060. 'exclude_changeset' => true,
  2061. 'exclude_post_data' => false,
  2062. ) );
  2063. $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
  2064. /*
  2065. * Get list of IDs for settings that have values different from what is currently
  2066. * saved in the changeset. By skipping any values that are already the same, the
  2067. * subset of changed settings can be passed into validate_setting_values to prevent
  2068. * an underprivileged modifying a single setting for which they have the capability
  2069. * from being blocked from saving. This also prevents a user from touching of the
  2070. * previous saved settings and overriding the associated user_id if they made no change.
  2071. */
  2072. $changed_setting_ids = array();
  2073. foreach ( $post_values as $setting_id => $setting_value ) {
  2074. $setting = $this->get_setting( $setting_id );
  2075. if ( $setting && 'theme_mod' === $setting->type ) {
  2076. $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
  2077. } else {
  2078. $prefixed_setting_id = $setting_id;
  2079. }
  2080. $is_value_changed = (
  2081. ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
  2082. ||
  2083. ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
  2084. ||
  2085. $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
  2086. );
  2087. if ( $is_value_changed ) {
  2088. $changed_setting_ids[] = $setting_id;
  2089. }
  2090. }
  2091. /**
  2092. * Fires before save validation happens.
  2093. *
  2094. * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
  2095. * at this point to catch any settings registered after `customize_register`.
  2096. * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
  2097. *
  2098. * @since 4.6.0
  2099. *
  2100. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  2101. */
  2102. do_action( 'customize_save_validation_before', $this );
  2103. // Validate settings.
  2104. $validated_values = array_merge(
  2105. array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
  2106. $post_values
  2107. );
  2108. $setting_validities = $this->validate_setting_values( $validated_values, array(
  2109. 'validate_capability' => true,
  2110. 'validate_existence' => true,
  2111. ) );
  2112. $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
  2113. /*
  2114. * Short-circuit if there are invalid settings the update is transactional.
  2115. * A changeset update is transactional when a status is supplied in the request.
  2116. */
  2117. if ( $update_transactionally && $invalid_setting_count > 0 ) {
  2118. $response = array(
  2119. 'setting_validities' => $setting_validities,
  2120. 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
  2121. );
  2122. return new WP_Error( 'transaction_fail', '', $response );
  2123. }
  2124. // Obtain/merge data for changeset.
  2125. $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
  2126. $data = $original_changeset_data;
  2127. if ( is_wp_error( $data ) ) {
  2128. $data = array();
  2129. }
  2130. // Ensure that all post values are included in the changeset data.
  2131. foreach ( $post_values as $setting_id => $post_value ) {
  2132. if ( ! isset( $args['data'][ $setting_id ] ) ) {
  2133. $args['data'][ $setting_id ] = array();
  2134. }
  2135. if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
  2136. $args['data'][ $setting_id ]['value'] = $post_value;
  2137. }
  2138. }
  2139. foreach ( $args['data'] as $setting_id => $setting_params ) {
  2140. $setting = $this->get_setting( $setting_id );
  2141. if ( ! $setting || ! $setting->check_capabilities() ) {
  2142. continue;
  2143. }
  2144. // Skip updating changeset for invalid setting values.
  2145. if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
  2146. continue;
  2147. }
  2148. $changeset_setting_id = $setting_id;
  2149. if ( 'theme_mod' === $setting->type ) {
  2150. $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
  2151. }
  2152. if ( null === $setting_params ) {
  2153. // Remove setting from changeset entirely.
  2154. unset( $data[ $changeset_setting_id ] );
  2155. } else {
  2156. if ( ! isset( $data[ $changeset_setting_id ] ) ) {
  2157. $data[ $changeset_setting_id ] = array();
  2158. }
  2159. // Merge any additional setting params that have been supplied with the existing params.
  2160. $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
  2161. // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
  2162. if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
  2163. continue;
  2164. }
  2165. $data[ $changeset_setting_id ] = array_merge(
  2166. $merged_setting_params,
  2167. array(
  2168. 'type' => $setting->type,
  2169. 'user_id' => $args['user_id'],
  2170. )
  2171. );
  2172. // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
  2173. if ( empty( $args['starter_content'] ) ) {
  2174. unset( $data[ $changeset_setting_id ]['starter_content'] );
  2175. }
  2176. }
  2177. }
  2178. $filter_context = array(
  2179. 'uuid' => $this->changeset_uuid(),
  2180. 'title' => $args['title'],
  2181. 'status' => $args['status'],
  2182. 'date_gmt' => $args['date_gmt'],
  2183. 'post_id' => $changeset_post_id,
  2184. 'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
  2185. 'manager' => $this,
  2186. );
  2187. /**
  2188. * Filters the settings' data that will be persisted into the changeset.
  2189. *
  2190. * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
  2191. *
  2192. * @since 4.7.0
  2193. *
  2194. * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
  2195. * @param array $context {
  2196. * Filter context.
  2197. *
  2198. * @type string $uuid Changeset UUID.
  2199. * @type string $title Requested title for the changeset post.
  2200. * @type string $status Requested status for the changeset post.
  2201. * @type string $date_gmt Requested date for the changeset post in MySQL format and GMT timezone.
  2202. * @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet.
  2203. * @type array $previous_data Previous data contained in the changeset.
  2204. * @type WP_Customize_Manager $manager Manager instance.
  2205. * }
  2206. */
  2207. $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
  2208. // Switch theme if publishing changes now.
  2209. if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
  2210. // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
  2211. $this->stop_previewing_theme();
  2212. switch_theme( $this->get_stylesheet() );
  2213. update_option( 'theme_switched_via_customizer', true );
  2214. $this->start_previewing_theme();
  2215. }
  2216. // Gather the data for wp_insert_post()/wp_update_post().
  2217. $json_options = 0;
  2218. if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
  2219. $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
  2220. }
  2221. $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
  2222. $post_array = array(
  2223. 'post_content' => wp_json_encode( $data, $json_options ),
  2224. );
  2225. if ( $args['title'] ) {
  2226. $post_array['post_title'] = $args['title'];
  2227. }
  2228. if ( $changeset_post_id ) {
  2229. $post_array['ID'] = $changeset_post_id;
  2230. } else {
  2231. $post_array['post_type'] = 'customize_changeset';
  2232. $post_array['post_name'] = $this->changeset_uuid();
  2233. $post_array['post_status'] = 'auto-draft';
  2234. }
  2235. if ( $args['status'] ) {
  2236. $post_array['post_status'] = $args['status'];
  2237. }
  2238. // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
  2239. if ( 'publish' === $args['status'] ) {
  2240. $post_array['post_date_gmt'] = '0000-00-00 00:00:00';
  2241. $post_array['post_date'] = '0000-00-00 00:00:00';
  2242. } elseif ( $args['date_gmt'] ) {
  2243. $post_array['post_date_gmt'] = $args['date_gmt'];
  2244. $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
  2245. } elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
  2246. /*
  2247. * Keep bumping the date for the auto-draft whenever it is modified;
  2248. * this extends its life, preserving it from garbage-collection via
  2249. * wp_delete_auto_drafts().
  2250. */
  2251. $post_array['post_date'] = current_time( 'mysql' );
  2252. $post_array['post_date_gmt'] = '';
  2253. }
  2254. $this->store_changeset_revision = $allow_revision;
  2255. add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
  2256. // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
  2257. $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
  2258. if ( $has_kses ) {
  2259. kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
  2260. }
  2261. // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
  2262. if ( $changeset_post_id ) {
  2263. $post_array['edit_date'] = true; // Prevent date clearing.
  2264. $r = wp_update_post( wp_slash( $post_array ), true );
  2265. } else {
  2266. $r = wp_insert_post( wp_slash( $post_array ), true );
  2267. if ( ! is_wp_error( $r ) ) {
  2268. $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
  2269. }
  2270. }
  2271. if ( $has_kses ) {
  2272. kses_init_filters();
  2273. }
  2274. $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
  2275. remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
  2276. $response = array(
  2277. 'setting_validities' => $setting_validities,
  2278. );
  2279. if ( is_wp_error( $r ) ) {
  2280. $response['changeset_post_save_failure'] = $r->get_error_code();
  2281. return new WP_Error( 'changeset_post_save_failure', '', $response );
  2282. }
  2283. return $response;
  2284. }
  2285. /**
  2286. * Whether a changeset revision should be made.
  2287. *
  2288. * @since 4.7.0
  2289. * @access private
  2290. * @var bool
  2291. */
  2292. protected $store_changeset_revision;
  2293. /**
  2294. * Filters whether a changeset has changed to create a new revision.
  2295. *
  2296. * Note that this will not be called while a changeset post remains in auto-draft status.
  2297. *
  2298. * @since 4.7.0
  2299. * @access private
  2300. *
  2301. * @param bool $post_has_changed Whether the post has changed.
  2302. * @param WP_Post $last_revision The last revision post object.
  2303. * @param WP_Post $post The post object.
  2304. *
  2305. * @return bool Whether a revision should be made.
  2306. */
  2307. public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
  2308. unset( $last_revision );
  2309. if ( 'customize_changeset' === $post->post_type ) {
  2310. $post_has_changed = $this->store_changeset_revision;
  2311. }
  2312. return $post_has_changed;
  2313. }
  2314. /**
  2315. * Publish changeset values.
  2316. *
  2317. * This will the values contained in a changeset, even changesets that do not
  2318. * correspond to current manager instance. This is called by
  2319. * `_wp_customize_publish_changeset()` when a customize_changeset post is
  2320. * transitioned to the `publish` status. As such, this method should not be
  2321. * called directly and instead `wp_publish_post()` should be used.
  2322. *
  2323. * Please note that if the settings in the changeset are for a non-activated
  2324. * theme, the theme must first be switched to (via `switch_theme()`) before
  2325. * invoking this method.
  2326. *
  2327. * @since 4.7.0
  2328. * @access private
  2329. * @see _wp_customize_publish_changeset()
  2330. *
  2331. * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
  2332. * @return true|WP_Error True or error info.
  2333. */
  2334. public function _publish_changeset_values( $changeset_post_id ) {
  2335. $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
  2336. if ( is_wp_error( $publishing_changeset_data ) ) {
  2337. return $publishing_changeset_data;
  2338. }
  2339. $changeset_post = get_post( $changeset_post_id );
  2340. /*
  2341. * Temporarily override the changeset context so that it will be read
  2342. * in calls to unsanitized_post_values() and so that it will be available
  2343. * on the $wp_customize object passed to hooks during the save logic.
  2344. */
  2345. $previous_changeset_post_id = $this->_changeset_post_id;
  2346. $this->_changeset_post_id = $changeset_post_id;
  2347. $previous_changeset_uuid = $this->_changeset_uuid;
  2348. $this->_changeset_uuid = $changeset_post->post_name;
  2349. $previous_changeset_data = $this->_changeset_data;
  2350. $this->_changeset_data = $publishing_changeset_data;
  2351. // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
  2352. $setting_user_ids = array();
  2353. $theme_mod_settings = array();
  2354. $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
  2355. $matches = array();
  2356. foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
  2357. $actual_setting_id = null;
  2358. $is_theme_mod_setting = (
  2359. isset( $setting_params['value'] )
  2360. &&
  2361. isset( $setting_params['type'] )
  2362. &&
  2363. 'theme_mod' === $setting_params['type']
  2364. &&
  2365. preg_match( $namespace_pattern, $raw_setting_id, $matches )
  2366. );
  2367. if ( $is_theme_mod_setting ) {
  2368. if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
  2369. $theme_mod_settings[ $matches['stylesheet'] ] = array();
  2370. }
  2371. $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
  2372. if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
  2373. $actual_setting_id = $matches['setting_id'];
  2374. }
  2375. } else {
  2376. $actual_setting_id = $raw_setting_id;
  2377. }
  2378. // Keep track of the user IDs for settings actually for this theme.
  2379. if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
  2380. $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
  2381. }
  2382. }
  2383. $changeset_setting_values = $this->unsanitized_post_values( array(
  2384. 'exclude_post_data' => true,
  2385. 'exclude_changeset' => false,
  2386. ) );
  2387. $changeset_setting_ids = array_keys( $changeset_setting_values );
  2388. $this->add_dynamic_settings( $changeset_setting_ids );
  2389. /**
  2390. * Fires once the theme has switched in the Customizer, but before settings
  2391. * have been saved.
  2392. *
  2393. * @since 3.4.0
  2394. *
  2395. * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
  2396. */
  2397. do_action( 'customize_save', $this );
  2398. /*
  2399. * Ensure that all settings will allow themselves to be saved. Note that
  2400. * this is safe because the setting would have checked the capability
  2401. * when the setting value was written into the changeset. So this is why
  2402. * an additional capability check is not required here.
  2403. */
  2404. $original_setting_capabilities = array();
  2405. foreach ( $changeset_setting_ids as $setting_id ) {
  2406. $setting = $this->get_setting( $setting_id );
  2407. if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
  2408. $original_setting_capabilities[ $setting->id ] = $setting->capability;
  2409. $setting->capability = 'exist';
  2410. }
  2411. }
  2412. $original_user_id = get_current_user_id();
  2413. foreach ( $changeset_setting_ids as $setting_id ) {
  2414. $setting = $this->get_setting( $setting_id );
  2415. if ( $setting ) {
  2416. /*
  2417. * Set the current user to match the user who saved the value into
  2418. * the changeset so that any filters that apply during the save
  2419. * process will respect the original user's capabilities. This
  2420. * will ensure, for example, that KSES won't strip unsafe HTML
  2421. * when a scheduled changeset publishes via WP Cron.
  2422. */
  2423. if ( isset( $setting_user_ids[ $setting_id ] ) ) {
  2424. wp_set_current_user( $setting_user_ids[ $setting_id ] );
  2425. } else {
  2426. wp_set_current_user( $original_user_id );
  2427. }
  2428. $setting->save();
  2429. }
  2430. }
  2431. wp_set_current_user( $original_user_id );
  2432. // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
  2433. if ( did_action( 'switch_theme' ) ) {
  2434. $other_theme_mod_settings = $theme_mod_settings;
  2435. unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
  2436. $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
  2437. }
  2438. /**
  2439. * Fires after Customize settings have been saved.
  2440. *
  2441. * @since 3.6.0
  2442. *
  2443. * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
  2444. */
  2445. do_action( 'customize_save_after', $this );
  2446. // Restore original capabilities.
  2447. foreach ( $original_setting_capabilities as $setting_id => $capability ) {
  2448. $setting = $this->get_setting( $setting_id );
  2449. if ( $setting ) {
  2450. $setting->capability = $capability;
  2451. }
  2452. }
  2453. // Restore original changeset data.
  2454. $this->_changeset_data = $previous_changeset_data;
  2455. $this->_changeset_post_id = $previous_changeset_post_id;
  2456. $this->_changeset_uuid = $previous_changeset_uuid;
  2457. return true;
  2458. }
  2459. /**
  2460. * Update stashed theme mod settings.
  2461. *
  2462. * @since 4.7.0
  2463. * @access private
  2464. *
  2465. * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
  2466. * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
  2467. */
  2468. protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
  2469. $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
  2470. if ( empty( $stashed_theme_mod_settings ) ) {
  2471. $stashed_theme_mod_settings = array();
  2472. }
  2473. // Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
  2474. unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
  2475. // Merge inactive theme mods with the stashed theme mod settings.
  2476. foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
  2477. if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
  2478. $stashed_theme_mod_settings[ $stylesheet ] = array();
  2479. }
  2480. $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
  2481. $stashed_theme_mod_settings[ $stylesheet ],
  2482. $theme_mod_settings
  2483. );
  2484. }
  2485. $autoload = false;
  2486. $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
  2487. if ( ! $result ) {
  2488. return false;
  2489. }
  2490. return $stashed_theme_mod_settings;
  2491. }
  2492. /**
  2493. * Refresh nonces for the current preview.
  2494. *
  2495. * @since 4.2.0
  2496. */
  2497. public function refresh_nonces() {
  2498. if ( ! $this->is_preview() ) {
  2499. wp_send_json_error( 'not_preview' );
  2500. }
  2501. wp_send_json_success( $this->get_nonces() );
  2502. }
  2503. /**
  2504. * Add a customize setting.
  2505. *
  2506. * @since 3.4.0
  2507. * @since 4.5.0 Return added WP_Customize_Setting instance.
  2508. * @access public
  2509. *
  2510. * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
  2511. * @param array $args Setting arguments; passed to WP_Customize_Setting
  2512. * constructor.
  2513. * @return WP_Customize_Setting The instance of the setting that was added.
  2514. */
  2515. public function add_setting( $id, $args = array() ) {
  2516. if ( $id instanceof WP_Customize_Setting ) {
  2517. $setting = $id;
  2518. } else {
  2519. $class = 'WP_Customize_Setting';
  2520. /** This filter is documented in wp-includes/class-wp-customize-manager.php */
  2521. $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
  2522. /** This filter is documented in wp-includes/class-wp-customize-manager.php */
  2523. $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
  2524. $setting = new $class( $this, $id, $args );
  2525. }
  2526. $this->settings[ $setting->id ] = $setting;
  2527. return $setting;
  2528. }
  2529. /**
  2530. * Register any dynamically-created settings, such as those from $_POST['customized']
  2531. * that have no corresponding setting created.
  2532. *
  2533. * This is a mechanism to "wake up" settings that have been dynamically created
  2534. * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
  2535. * loads, the dynamically-created settings then will get created and previewed
  2536. * even though they are not directly created statically with code.
  2537. *
  2538. * @since 4.2.0
  2539. * @access public
  2540. *
  2541. * @param array $setting_ids The setting IDs to add.
  2542. * @return array The WP_Customize_Setting objects added.
  2543. */
  2544. public function add_dynamic_settings( $setting_ids ) {
  2545. $new_settings = array();
  2546. foreach ( $setting_ids as $setting_id ) {
  2547. // Skip settings already created
  2548. if ( $this->get_setting( $setting_id ) ) {
  2549. continue;
  2550. }
  2551. $setting_args = false;
  2552. $setting_class = 'WP_Customize_Setting';
  2553. /**
  2554. * Filters a dynamic setting's constructor args.
  2555. *
  2556. * For a dynamic setting to be registered, this filter must be employed
  2557. * to override the default false value with an array of args to pass to
  2558. * the WP_Customize_Setting constructor.
  2559. *
  2560. * @since 4.2.0
  2561. *
  2562. * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
  2563. * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
  2564. */
  2565. $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
  2566. if ( false === $setting_args ) {
  2567. continue;
  2568. }
  2569. /**
  2570. * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
  2571. *
  2572. * @since 4.2.0
  2573. *
  2574. * @param string $setting_class WP_Customize_Setting or a subclass.
  2575. * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
  2576. * @param array $setting_args WP_Customize_Setting or a subclass.
  2577. */
  2578. $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
  2579. $setting = new $setting_class( $this, $setting_id, $setting_args );
  2580. $this->add_setting( $setting );
  2581. $new_settings[] = $setting;
  2582. }
  2583. return $new_settings;
  2584. }
  2585. /**
  2586. * Retrieve a customize setting.
  2587. *
  2588. * @since 3.4.0
  2589. *
  2590. * @param string $id Customize Setting ID.
  2591. * @return WP_Customize_Setting|void The setting, if set.
  2592. */
  2593. public function get_setting( $id ) {
  2594. if ( isset( $this->settings[ $id ] ) ) {
  2595. return $this->settings[ $id ];
  2596. }
  2597. }
  2598. /**
  2599. * Remove a customize setting.
  2600. *
  2601. * @since 3.4.0
  2602. *
  2603. * @param string $id Customize Setting ID.
  2604. */
  2605. public function remove_setting( $id ) {
  2606. unset( $this->settings[ $id ] );
  2607. }
  2608. /**
  2609. * Add a customize panel.
  2610. *
  2611. * @since 4.0.0
  2612. * @since 4.5.0 Return added WP_Customize_Panel instance.
  2613. * @access public
  2614. *
  2615. * @param WP_Customize_Panel|string $id Customize Panel object, or Panel ID.
  2616. * @param array $args Optional. Panel arguments. Default empty array.
  2617. *
  2618. * @return WP_Customize_Panel The instance of the panel that was added.
  2619. */
  2620. public function add_panel( $id, $args = array() ) {
  2621. if ( $id instanceof WP_Customize_Panel ) {
  2622. $panel = $id;
  2623. } else {
  2624. $panel = new WP_Customize_Panel( $this, $id, $args );
  2625. }
  2626. $this->panels[ $panel->id ] = $panel;
  2627. return $panel;
  2628. }
  2629. /**
  2630. * Retrieve a customize panel.
  2631. *
  2632. * @since 4.0.0
  2633. * @access public
  2634. *
  2635. * @param string $id Panel ID to get.
  2636. * @return WP_Customize_Panel|void Requested panel instance, if set.
  2637. */
  2638. public function get_panel( $id ) {
  2639. if ( isset( $this->panels[ $id ] ) ) {
  2640. return $this->panels[ $id ];
  2641. }
  2642. }
  2643. /**
  2644. * Remove a customize panel.
  2645. *
  2646. * @since 4.0.0
  2647. * @access public
  2648. *
  2649. * @param string $id Panel ID to remove.
  2650. */
  2651. public function remove_panel( $id ) {
  2652. // Removing core components this way is _doing_it_wrong().
  2653. if ( in_array( $id, $this->components, true ) ) {
  2654. /* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
  2655. $message = sprintf( __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
  2656. $id,
  2657. '<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
  2658. );
  2659. _doing_it_wrong( __METHOD__, $message, '4.5.0' );
  2660. }
  2661. unset( $this->panels[ $id ] );
  2662. }
  2663. /**
  2664. * Register a customize panel type.
  2665. *
  2666. * Registered types are eligible to be rendered via JS and created dynamically.
  2667. *
  2668. * @since 4.3.0
  2669. * @access public
  2670. *
  2671. * @see WP_Customize_Panel
  2672. *
  2673. * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
  2674. */
  2675. public function register_panel_type( $panel ) {
  2676. $this->registered_panel_types[] = $panel;
  2677. }
  2678. /**
  2679. * Render JS templates for all registered panel types.
  2680. *
  2681. * @since 4.3.0
  2682. * @access public
  2683. */
  2684. public function render_panel_templates() {
  2685. foreach ( $this->registered_panel_types as $panel_type ) {
  2686. $panel = new $panel_type( $this, 'temp', array() );
  2687. $panel->print_template();
  2688. }
  2689. }
  2690. /**
  2691. * Add a customize section.
  2692. *
  2693. * @since 3.4.0
  2694. * @since 4.5.0 Return added WP_Customize_Section instance.
  2695. * @access public
  2696. *
  2697. * @param WP_Customize_Section|string $id Customize Section object, or Section ID.
  2698. * @param array $args Section arguments.
  2699. *
  2700. * @return WP_Customize_Section The instance of the section that was added.
  2701. */
  2702. public function add_section( $id, $args = array() ) {
  2703. if ( $id instanceof WP_Customize_Section ) {
  2704. $section = $id;
  2705. } else {
  2706. $section = new WP_Customize_Section( $this, $id, $args );
  2707. }
  2708. $this->sections[ $section->id ] = $section;
  2709. return $section;
  2710. }
  2711. /**
  2712. * Retrieve a customize section.
  2713. *
  2714. * @since 3.4.0
  2715. *
  2716. * @param string $id Section ID.
  2717. * @return WP_Customize_Section|void The section, if set.
  2718. */
  2719. public function get_section( $id ) {
  2720. if ( isset( $this->sections[ $id ] ) )
  2721. return $this->sections[ $id ];
  2722. }
  2723. /**
  2724. * Remove a customize section.
  2725. *
  2726. * @since 3.4.0
  2727. *
  2728. * @param string $id Section ID.
  2729. */
  2730. public function remove_section( $id ) {
  2731. unset( $this->sections[ $id ] );
  2732. }
  2733. /**
  2734. * Register a customize section type.
  2735. *
  2736. * Registered types are eligible to be rendered via JS and created dynamically.
  2737. *
  2738. * @since 4.3.0
  2739. * @access public
  2740. *
  2741. * @see WP_Customize_Section
  2742. *
  2743. * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
  2744. */
  2745. public function register_section_type( $section ) {
  2746. $this->registered_section_types[] = $section;
  2747. }
  2748. /**
  2749. * Render JS templates for all registered section types.
  2750. *
  2751. * @since 4.3.0
  2752. * @access public
  2753. */
  2754. public function render_section_templates() {
  2755. foreach ( $this->registered_section_types as $section_type ) {
  2756. $section = new $section_type( $this, 'temp', array() );
  2757. $section->print_template();
  2758. }
  2759. }
  2760. /**
  2761. * Add a customize control.
  2762. *
  2763. * @since 3.4.0
  2764. * @since 4.5.0 Return added WP_Customize_Control instance.
  2765. * @access public
  2766. *
  2767. * @param WP_Customize_Control|string $id Customize Control object, or ID.
  2768. * @param array $args Control arguments; passed to WP_Customize_Control
  2769. * constructor.
  2770. * @return WP_Customize_Control The instance of the control that was added.
  2771. */
  2772. public function add_control( $id, $args = array() ) {
  2773. if ( $id instanceof WP_Customize_Control ) {
  2774. $control = $id;
  2775. } else {
  2776. $control = new WP_Customize_Control( $this, $id, $args );
  2777. }
  2778. $this->controls[ $control->id ] = $control;
  2779. return $control;
  2780. }
  2781. /**
  2782. * Retrieve a customize control.
  2783. *
  2784. * @since 3.4.0
  2785. *
  2786. * @param string $id ID of the control.
  2787. * @return WP_Customize_Control|void The control object, if set.
  2788. */
  2789. public function get_control( $id ) {
  2790. if ( isset( $this->controls[ $id ] ) )
  2791. return $this->controls[ $id ];
  2792. }
  2793. /**
  2794. * Remove a customize control.
  2795. *
  2796. * @since 3.4.0
  2797. *
  2798. * @param string $id ID of the control.
  2799. */
  2800. public function remove_control( $id ) {
  2801. unset( $this->controls[ $id ] );
  2802. }
  2803. /**
  2804. * Register a customize control type.
  2805. *
  2806. * Registered types are eligible to be rendered via JS and created dynamically.
  2807. *
  2808. * @since 4.1.0
  2809. * @access public
  2810. *
  2811. * @param string $control Name of a custom control which is a subclass of
  2812. * WP_Customize_Control.
  2813. */
  2814. public function register_control_type( $control ) {
  2815. $this->registered_control_types[] = $control;
  2816. }
  2817. /**
  2818. * Render JS templates for all registered control types.
  2819. *
  2820. * @since 4.1.0
  2821. * @access public
  2822. */
  2823. public function render_control_templates() {
  2824. foreach ( $this->registered_control_types as $control_type ) {
  2825. $control = new $control_type( $this, 'temp', array(
  2826. 'settings' => array(),
  2827. ) );
  2828. $control->print_template();
  2829. }
  2830. ?>
  2831. <script type="text/html" id="tmpl-customize-control-notifications">
  2832. <ul>
  2833. <# _.each( data.notifications, function( notification ) { #>
  2834. <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
  2835. <# } ); #>
  2836. </ul>
  2837. </script>
  2838. <?php
  2839. }
  2840. /**
  2841. * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
  2842. *
  2843. * @since 3.4.0
  2844. * @deprecated 4.7.0 Use wp_list_sort()
  2845. *
  2846. * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
  2847. * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
  2848. * @return int
  2849. */
  2850. protected function _cmp_priority( $a, $b ) {
  2851. _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
  2852. if ( $a->priority === $b->priority ) {
  2853. return $a->instance_number - $b->instance_number;
  2854. } else {
  2855. return $a->priority - $b->priority;
  2856. }
  2857. }
  2858. /**
  2859. * Prepare panels, sections, and controls.
  2860. *
  2861. * For each, check if required related components exist,
  2862. * whether the user has the necessary capabilities,
  2863. * and sort by priority.
  2864. *
  2865. * @since 3.4.0
  2866. */
  2867. public function prepare_controls() {
  2868. $controls = array();
  2869. $this->controls = wp_list_sort( $this->controls, array(
  2870. 'priority' => 'ASC',
  2871. 'instance_number' => 'ASC',
  2872. ), 'ASC', true );
  2873. foreach ( $this->controls as $id => $control ) {
  2874. if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
  2875. continue;
  2876. }
  2877. $this->sections[ $control->section ]->controls[] = $control;
  2878. $controls[ $id ] = $control;
  2879. }
  2880. $this->controls = $controls;
  2881. // Prepare sections.
  2882. $this->sections = wp_list_sort( $this->sections, array(
  2883. 'priority' => 'ASC',
  2884. 'instance_number' => 'ASC',
  2885. ), 'ASC', true );
  2886. $sections = array();
  2887. foreach ( $this->sections as $section ) {
  2888. if ( ! $section->check_capabilities() ) {
  2889. continue;
  2890. }
  2891. $section->controls = wp_list_sort( $section->controls, array(
  2892. 'priority' => 'ASC',
  2893. 'instance_number' => 'ASC',
  2894. ) );
  2895. if ( ! $section->panel ) {
  2896. // Top-level section.
  2897. $sections[ $section->id ] = $section;
  2898. } else {
  2899. // This section belongs to a panel.
  2900. if ( isset( $this->panels [ $section->panel ] ) ) {
  2901. $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
  2902. }
  2903. }
  2904. }
  2905. $this->sections = $sections;
  2906. // Prepare panels.
  2907. $this->panels = wp_list_sort( $this->panels, array(
  2908. 'priority' => 'ASC',
  2909. 'instance_number' => 'ASC',
  2910. ), 'ASC', true );
  2911. $panels = array();
  2912. foreach ( $this->panels as $panel ) {
  2913. if ( ! $panel->check_capabilities() ) {
  2914. continue;
  2915. }
  2916. $panel->sections = wp_list_sort( $panel->sections, array(
  2917. 'priority' => 'ASC',
  2918. 'instance_number' => 'ASC',
  2919. ), 'ASC', true );
  2920. $panels[ $panel->id ] = $panel;
  2921. }
  2922. $this->panels = $panels;
  2923. // Sort panels and top-level sections together.
  2924. $this->containers = array_merge( $this->panels, $this->sections );
  2925. $this->containers = wp_list_sort( $this->containers, array(
  2926. 'priority' => 'ASC',
  2927. 'instance_number' => 'ASC',
  2928. ), 'ASC', true );
  2929. }
  2930. /**
  2931. * Enqueue scripts for customize controls.
  2932. *
  2933. * @since 3.4.0
  2934. */
  2935. public function enqueue_control_scripts() {
  2936. foreach ( $this->controls as $control ) {
  2937. $control->enqueue();
  2938. }
  2939. }
  2940. /**
  2941. * Determine whether the user agent is iOS.
  2942. *
  2943. * @since 4.4.0
  2944. * @access public
  2945. *
  2946. * @return bool Whether the user agent is iOS.
  2947. */
  2948. public function is_ios() {
  2949. return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
  2950. }
  2951. /**
  2952. * Get the template string for the Customizer pane document title.
  2953. *
  2954. * @since 4.4.0
  2955. * @access public
  2956. *
  2957. * @return string The template string for the document title.
  2958. */
  2959. public function get_document_title_template() {
  2960. if ( $this->is_theme_active() ) {
  2961. /* translators: %s: document title from the preview */
  2962. $document_title_tmpl = __( 'Customize: %s' );
  2963. } else {
  2964. /* translators: %s: document title from the preview */
  2965. $document_title_tmpl = __( 'Live Preview: %s' );
  2966. }
  2967. $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
  2968. return $document_title_tmpl;
  2969. }
  2970. /**
  2971. * Set the initial URL to be previewed.
  2972. *
  2973. * URL is validated.
  2974. *
  2975. * @since 4.4.0
  2976. * @access public
  2977. *
  2978. * @param string $preview_url URL to be previewed.
  2979. */
  2980. public function set_preview_url( $preview_url ) {
  2981. $preview_url = esc_url_raw( $preview_url );
  2982. $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
  2983. }
  2984. /**
  2985. * Get the initial URL to be previewed.
  2986. *
  2987. * @since 4.4.0
  2988. * @access public
  2989. *
  2990. * @return string URL being previewed.
  2991. */
  2992. public function get_preview_url() {
  2993. if ( empty( $this->preview_url ) ) {
  2994. $preview_url = home_url( '/' );
  2995. } else {
  2996. $preview_url = $this->preview_url;
  2997. }
  2998. return $preview_url;
  2999. }
  3000. /**
  3001. * Determines whether the admin and the frontend are on different domains.
  3002. *
  3003. * @since 4.7.0
  3004. * @access public
  3005. *
  3006. * @return bool Whether cross-domain.
  3007. */
  3008. public function is_cross_domain() {
  3009. $admin_origin = wp_parse_url( admin_url() );
  3010. $home_origin = wp_parse_url( home_url() );
  3011. $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
  3012. return $cross_domain;
  3013. }
  3014. /**
  3015. * Get URLs allowed to be previewed.
  3016. *
  3017. * If the front end and the admin are served from the same domain, load the
  3018. * preview over ssl if the Customizer is being loaded over ssl. This avoids
  3019. * insecure content warnings. This is not attempted if the admin and front end
  3020. * are on different domains to avoid the case where the front end doesn't have
  3021. * ssl certs. Domain mapping plugins can allow other urls in these conditions
  3022. * using the customize_allowed_urls filter.
  3023. *
  3024. * @since 4.7.0
  3025. * @access public
  3026. *
  3027. * @returns array Allowed URLs.
  3028. */
  3029. public function get_allowed_urls() {
  3030. $allowed_urls = array( home_url( '/' ) );
  3031. if ( is_ssl() && ! $this->is_cross_domain() ) {
  3032. $allowed_urls[] = home_url( '/', 'https' );
  3033. }
  3034. /**
  3035. * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
  3036. *
  3037. * @since 3.4.0
  3038. *
  3039. * @param array $allowed_urls An array of allowed URLs.
  3040. */
  3041. $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
  3042. return $allowed_urls;
  3043. }
  3044. /**
  3045. * Get messenger channel.
  3046. *
  3047. * @since 4.7.0
  3048. * @access public
  3049. *
  3050. * @return string Messenger channel.
  3051. */
  3052. public function get_messenger_channel() {
  3053. return $this->messenger_channel;
  3054. }
  3055. /**
  3056. * Set URL to link the user to when closing the Customizer.
  3057. *
  3058. * URL is validated.
  3059. *
  3060. * @since 4.4.0
  3061. * @access public
  3062. *
  3063. * @param string $return_url URL for return link.
  3064. */
  3065. public function set_return_url( $return_url ) {
  3066. $return_url = esc_url_raw( $return_url );
  3067. $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
  3068. $return_url = wp_validate_redirect( $return_url );
  3069. $this->return_url = $return_url;
  3070. }
  3071. /**
  3072. * Get URL to link the user to when closing the Customizer.
  3073. *
  3074. * @since 4.4.0
  3075. * @access public
  3076. *
  3077. * @return string URL for link to close Customizer.
  3078. */
  3079. public function get_return_url() {
  3080. $referer = wp_get_referer();
  3081. $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
  3082. if ( $this->return_url ) {
  3083. $return_url = $this->return_url;
  3084. } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
  3085. $return_url = $referer;
  3086. } else if ( $this->preview_url ) {
  3087. $return_url = $this->preview_url;
  3088. } else {
  3089. $return_url = home_url( '/' );
  3090. }
  3091. return $return_url;
  3092. }
  3093. /**
  3094. * Set the autofocused constructs.
  3095. *
  3096. * @since 4.4.0
  3097. * @access public
  3098. *
  3099. * @param array $autofocus {
  3100. * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
  3101. *
  3102. * @type string [$control] ID for control to be autofocused.
  3103. * @type string [$section] ID for section to be autofocused.
  3104. * @type string [$panel] ID for panel to be autofocused.
  3105. * }
  3106. */
  3107. public function set_autofocus( $autofocus ) {
  3108. $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
  3109. }
  3110. /**
  3111. * Get the autofocused constructs.
  3112. *
  3113. * @since 4.4.0
  3114. * @access public
  3115. *
  3116. * @return array {
  3117. * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
  3118. *
  3119. * @type string [$control] ID for control to be autofocused.
  3120. * @type string [$section] ID for section to be autofocused.
  3121. * @type string [$panel] ID for panel to be autofocused.
  3122. * }
  3123. */
  3124. public function get_autofocus() {
  3125. return $this->autofocus;
  3126. }
  3127. /**
  3128. * Get nonces for the Customizer.
  3129. *
  3130. * @since 4.5.0
  3131. * @return array Nonces.
  3132. */
  3133. public function get_nonces() {
  3134. $nonces = array(
  3135. 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
  3136. 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
  3137. );
  3138. /**
  3139. * Filters nonces for Customizer.
  3140. *
  3141. * @since 4.2.0
  3142. *
  3143. * @param array $nonces Array of refreshed nonces for save and
  3144. * preview actions.
  3145. * @param WP_Customize_Manager $this WP_Customize_Manager instance.
  3146. */
  3147. $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
  3148. return $nonces;
  3149. }
  3150. /**
  3151. * Print JavaScript settings for parent window.
  3152. *
  3153. * @since 4.4.0
  3154. */
  3155. public function customize_pane_settings() {
  3156. $login_url = add_query_arg( array(
  3157. 'interim-login' => 1,
  3158. 'customize-login' => 1,
  3159. ), wp_login_url() );
  3160. // Ensure dirty flags are set for modified settings.
  3161. foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
  3162. $setting = $this->get_setting( $setting_id );
  3163. if ( $setting ) {
  3164. $setting->dirty = true;
  3165. }
  3166. }
  3167. // Prepare Customizer settings to pass to JavaScript.
  3168. $settings = array(
  3169. 'changeset' => array(
  3170. 'uuid' => $this->changeset_uuid(),
  3171. 'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
  3172. ),
  3173. 'timeouts' => array(
  3174. 'windowRefresh' => 250,
  3175. 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
  3176. 'keepAliveCheck' => 2500,
  3177. 'reflowPaneContents' => 100,
  3178. 'previewFrameSensitivity' => 2000,
  3179. ),
  3180. 'theme' => array(
  3181. 'stylesheet' => $this->get_stylesheet(),
  3182. 'active' => $this->is_theme_active(),
  3183. ),
  3184. 'url' => array(
  3185. 'preview' => esc_url_raw( $this->get_preview_url() ),
  3186. 'parent' => esc_url_raw( admin_url() ),
  3187. 'activated' => esc_url_raw( home_url( '/' ) ),
  3188. 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
  3189. 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
  3190. 'isCrossDomain' => $this->is_cross_domain(),
  3191. 'home' => esc_url_raw( home_url( '/' ) ),
  3192. 'login' => esc_url_raw( $login_url ),
  3193. ),
  3194. 'browser' => array(
  3195. 'mobile' => wp_is_mobile(),
  3196. 'ios' => $this->is_ios(),
  3197. ),
  3198. 'panels' => array(),
  3199. 'sections' => array(),
  3200. 'nonce' => $this->get_nonces(),
  3201. 'autofocus' => $this->get_autofocus(),
  3202. 'documentTitleTmpl' => $this->get_document_title_template(),
  3203. 'previewableDevices' => $this->get_previewable_devices(),
  3204. );
  3205. // Prepare Customize Section objects to pass to JavaScript.
  3206. foreach ( $this->sections() as $id => $section ) {
  3207. if ( $section->check_capabilities() ) {
  3208. $settings['sections'][ $id ] = $section->json();
  3209. }
  3210. }
  3211. // Prepare Customize Panel objects to pass to JavaScript.
  3212. foreach ( $this->panels() as $panel_id => $panel ) {
  3213. if ( $panel->check_capabilities() ) {
  3214. $settings['panels'][ $panel_id ] = $panel->json();
  3215. foreach ( $panel->sections as $section_id => $section ) {
  3216. if ( $section->check_capabilities() ) {
  3217. $settings['sections'][ $section_id ] = $section->json();
  3218. }
  3219. }
  3220. }
  3221. }
  3222. ?>
  3223. <script type="text/javascript">
  3224. var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
  3225. _wpCustomizeSettings.controls = {};
  3226. _wpCustomizeSettings.settings = {};
  3227. <?php
  3228. // Serialize settings one by one to improve memory usage.
  3229. echo "(function ( s ){\n";
  3230. foreach ( $this->settings() as $setting ) {
  3231. if ( $setting->check_capabilities() ) {
  3232. printf(
  3233. "s[%s] = %s;\n",
  3234. wp_json_encode( $setting->id ),
  3235. wp_json_encode( $setting->json() )
  3236. );
  3237. }
  3238. }
  3239. echo "})( _wpCustomizeSettings.settings );\n";
  3240. // Serialize controls one by one to improve memory usage.
  3241. echo "(function ( c ){\n";
  3242. foreach ( $this->controls() as $control ) {
  3243. if ( $control->check_capabilities() ) {
  3244. printf(
  3245. "c[%s] = %s;\n",
  3246. wp_json_encode( $control->id ),
  3247. wp_json_encode( $control->json() )
  3248. );
  3249. }
  3250. }
  3251. echo "})( _wpCustomizeSettings.controls );\n";
  3252. ?>
  3253. </script>
  3254. <?php
  3255. }
  3256. /**
  3257. * Returns a list of devices to allow previewing.
  3258. *
  3259. * @access public
  3260. * @since 4.5.0
  3261. *
  3262. * @return array List of devices with labels and default setting.
  3263. */
  3264. public function get_previewable_devices() {
  3265. $devices = array(
  3266. 'desktop' => array(
  3267. 'label' => __( 'Enter desktop preview mode' ),
  3268. 'default' => true,
  3269. ),
  3270. 'tablet' => array(
  3271. 'label' => __( 'Enter tablet preview mode' ),
  3272. ),
  3273. 'mobile' => array(
  3274. 'label' => __( 'Enter mobile preview mode' ),
  3275. ),
  3276. );
  3277. /**
  3278. * Filters the available devices to allow previewing in the Customizer.
  3279. *
  3280. * @since 4.5.0
  3281. *
  3282. * @see WP_Customize_Manager::get_previewable_devices()
  3283. *
  3284. * @param array $devices List of devices with labels and default setting.
  3285. */
  3286. $devices = apply_filters( 'customize_previewable_devices', $devices );
  3287. return $devices;
  3288. }
  3289. /**
  3290. * Register some default controls.
  3291. *
  3292. * @since 3.4.0
  3293. */
  3294. public function register_controls() {
  3295. /* Panel, Section, and Control Types */
  3296. $this->register_panel_type( 'WP_Customize_Panel' );
  3297. $this->register_section_type( 'WP_Customize_Section' );
  3298. $this->register_section_type( 'WP_Customize_Sidebar_Section' );
  3299. $this->register_control_type( 'WP_Customize_Color_Control' );
  3300. $this->register_control_type( 'WP_Customize_Media_Control' );
  3301. $this->register_control_type( 'WP_Customize_Upload_Control' );
  3302. $this->register_control_type( 'WP_Customize_Image_Control' );
  3303. $this->register_control_type( 'WP_Customize_Background_Image_Control' );
  3304. $this->register_control_type( 'WP_Customize_Background_Position_Control' );
  3305. $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
  3306. $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
  3307. $this->register_control_type( 'WP_Customize_Theme_Control' );
  3308. /* Themes */
  3309. $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
  3310. 'title' => $this->theme()->display( 'Name' ),
  3311. 'capability' => 'switch_themes',
  3312. 'priority' => 0,
  3313. ) ) );
  3314. // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
  3315. $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
  3316. 'capability' => 'switch_themes',
  3317. ) ) );
  3318. require_once( ABSPATH . 'wp-admin/includes/theme.php' );
  3319. // Theme Controls.
  3320. // Add a control for the active/original theme.
  3321. if ( ! $this->is_theme_active() ) {
  3322. $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
  3323. $active_theme = current( $themes );
  3324. $active_theme['isActiveTheme'] = true;
  3325. $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
  3326. 'theme' => $active_theme,
  3327. 'section' => 'themes',
  3328. 'settings' => 'active_theme',
  3329. ) ) );
  3330. }
  3331. $themes = wp_prepare_themes_for_js();
  3332. foreach ( $themes as $theme ) {
  3333. if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
  3334. continue;
  3335. }
  3336. $theme_id = 'theme_' . $theme['id'];
  3337. $theme['isActiveTheme'] = false;
  3338. $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
  3339. 'theme' => $theme,
  3340. 'section' => 'themes',
  3341. 'settings' => 'active_theme',
  3342. ) ) );
  3343. }
  3344. /* Site Identity */
  3345. $this->add_section( 'title_tagline', array(
  3346. 'title' => __( 'Site Identity' ),
  3347. 'priority' => 20,
  3348. ) );
  3349. $this->add_setting( 'blogname', array(
  3350. 'default' => get_option( 'blogname' ),
  3351. 'type' => 'option',
  3352. 'capability' => 'manage_options',
  3353. ) );
  3354. $this->add_control( 'blogname', array(
  3355. 'label' => __( 'Site Title' ),
  3356. 'section' => 'title_tagline',
  3357. ) );
  3358. $this->add_setting( 'blogdescription', array(
  3359. 'default' => get_option( 'blogdescription' ),
  3360. 'type' => 'option',
  3361. 'capability' => 'manage_options',
  3362. ) );
  3363. $this->add_control( 'blogdescription', array(
  3364. 'label' => __( 'Tagline' ),
  3365. 'section' => 'title_tagline',
  3366. ) );
  3367. // Add a setting to hide header text if the theme doesn't support custom headers.
  3368. if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
  3369. $this->add_setting( 'header_text', array(
  3370. 'theme_supports' => array( 'custom-logo', 'header-text' ),
  3371. 'default' => 1,
  3372. 'sanitize_callback' => 'absint',
  3373. ) );
  3374. $this->add_control( 'header_text', array(
  3375. 'label' => __( 'Display Site Title and Tagline' ),
  3376. 'section' => 'title_tagline',
  3377. 'settings' => 'header_text',
  3378. 'type' => 'checkbox',
  3379. ) );
  3380. }
  3381. $this->add_setting( 'site_icon', array(
  3382. 'type' => 'option',
  3383. 'capability' => 'manage_options',
  3384. 'transport' => 'postMessage', // Previewed with JS in the Customizer controls window.
  3385. ) );
  3386. $this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
  3387. 'label' => __( 'Site Icon' ),
  3388. 'description' => sprintf(
  3389. /* translators: %s: site icon size in pixels */
  3390. __( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least %s pixels wide and tall.' ),
  3391. '<strong>512</strong>'
  3392. ),
  3393. 'section' => 'title_tagline',
  3394. 'priority' => 60,
  3395. 'height' => 512,
  3396. 'width' => 512,
  3397. ) ) );
  3398. $this->add_setting( 'custom_logo', array(
  3399. 'theme_supports' => array( 'custom-logo' ),
  3400. 'transport' => 'postMessage',
  3401. ) );
  3402. $custom_logo_args = get_theme_support( 'custom-logo' );
  3403. $this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
  3404. 'label' => __( 'Logo' ),
  3405. 'section' => 'title_tagline',
  3406. 'priority' => 8,
  3407. 'height' => $custom_logo_args[0]['height'],
  3408. 'width' => $custom_logo_args[0]['width'],
  3409. 'flex_height' => $custom_logo_args[0]['flex-height'],
  3410. 'flex_width' => $custom_logo_args[0]['flex-width'],
  3411. 'button_labels' => array(
  3412. 'select' => __( 'Select logo' ),
  3413. 'change' => __( 'Change logo' ),
  3414. 'remove' => __( 'Remove' ),
  3415. 'default' => __( 'Default' ),
  3416. 'placeholder' => __( 'No logo selected' ),
  3417. 'frame_title' => __( 'Select logo' ),
  3418. 'frame_button' => __( 'Choose logo' ),
  3419. ),
  3420. ) ) );
  3421. $this->selective_refresh->add_partial( 'custom_logo', array(
  3422. 'settings' => array( 'custom_logo' ),
  3423. 'selector' => '.custom-logo-link',
  3424. 'render_callback' => array( $this, '_render_custom_logo_partial' ),
  3425. 'container_inclusive' => true,
  3426. ) );
  3427. /* Colors */
  3428. $this->add_section( 'colors', array(
  3429. 'title' => __( 'Colors' ),
  3430. 'priority' => 40,
  3431. ) );
  3432. $this->add_setting( 'header_textcolor', array(
  3433. 'theme_supports' => array( 'custom-header', 'header-text' ),
  3434. 'default' => get_theme_support( 'custom-header', 'default-text-color' ),
  3435. 'sanitize_callback' => array( $this, '_sanitize_header_textcolor' ),
  3436. 'sanitize_js_callback' => 'maybe_hash_hex_color',
  3437. ) );
  3438. // Input type: checkbox
  3439. // With custom value
  3440. $this->add_control( 'display_header_text', array(
  3441. 'settings' => 'header_textcolor',
  3442. 'label' => __( 'Display Site Title and Tagline' ),
  3443. 'section' => 'title_tagline',
  3444. 'type' => 'checkbox',
  3445. 'priority' => 40,
  3446. ) );
  3447. $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
  3448. 'label' => __( 'Header Text Color' ),
  3449. 'section' => 'colors',
  3450. ) ) );
  3451. // Input type: Color
  3452. // With sanitize_callback
  3453. $this->add_setting( 'background_color', array(
  3454. 'default' => get_theme_support( 'custom-background', 'default-color' ),
  3455. 'theme_supports' => 'custom-background',
  3456. 'sanitize_callback' => 'sanitize_hex_color_no_hash',
  3457. 'sanitize_js_callback' => 'maybe_hash_hex_color',
  3458. ) );
  3459. $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
  3460. 'label' => __( 'Background Color' ),
  3461. 'section' => 'colors',
  3462. ) ) );
  3463. /* Custom Header */
  3464. if ( current_theme_supports( 'custom-header', 'video' ) ) {
  3465. $title = __( 'Header Media' );
  3466. $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
  3467. // @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
  3468. $description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
  3469. $description .= '<li class="notice notice-info">' . __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ) . '</li>';
  3470. $description .= '</ul></div>';
  3471. $width = absint( get_theme_support( 'custom-header', 'width' ) );
  3472. $height = absint( get_theme_support( 'custom-header', 'height' ) );
  3473. if ( $width && $height ) {
  3474. $control_description = sprintf(
  3475. /* translators: 1: .mp4, 2: header size in pixels */
  3476. __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
  3477. '<code>.mp4</code>',
  3478. sprintf( '<strong>%s &times; %s</strong>', $width, $height )
  3479. );
  3480. } elseif ( $width ) {
  3481. $control_description = sprintf(
  3482. /* translators: 1: .mp4, 2: header width in pixels */
  3483. __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
  3484. '<code>.mp4</code>',
  3485. sprintf( '<strong>%s</strong>', $width )
  3486. );
  3487. } else {
  3488. $control_description = sprintf(
  3489. /* translators: 1: .mp4, 2: header height in pixels */
  3490. __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
  3491. '<code>.mp4</code>',
  3492. sprintf( '<strong>%s</strong>', $height )
  3493. );
  3494. }
  3495. } else {
  3496. $title = __( 'Header Image' );
  3497. $description = '';
  3498. $control_description = '';
  3499. }
  3500. $this->add_section( 'header_image', array(
  3501. 'title' => $title,
  3502. 'description' => $description,
  3503. 'theme_supports' => 'custom-header',
  3504. 'priority' => 60,
  3505. ) );
  3506. $this->add_setting( 'header_video', array(
  3507. 'theme_supports' => array( 'custom-header', 'video' ),
  3508. 'transport' => 'postMessage',
  3509. 'sanitize_callback' => 'absint',
  3510. 'validate_callback' => array( $this, '_validate_header_video' ),
  3511. ) );
  3512. $this->add_setting( 'external_header_video', array(
  3513. 'theme_supports' => array( 'custom-header', 'video' ),
  3514. 'transport' => 'postMessage',
  3515. 'sanitize_callback' => array( $this, '_sanitize_external_header_video' ),
  3516. 'validate_callback' => array( $this, '_validate_external_header_video' ),
  3517. ) );
  3518. $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
  3519. 'default' => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
  3520. 'theme_supports' => 'custom-header',
  3521. ) ) );
  3522. $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
  3523. 'theme_supports' => 'custom-header',
  3524. ) ) );
  3525. /*
  3526. * Switch image settings to postMessage when video support is enabled since
  3527. * it entails that the_custom_header_markup() will be used, and thus selective
  3528. * refresh can be utilized.
  3529. */
  3530. if ( current_theme_supports( 'custom-header', 'video' ) ) {
  3531. $this->get_setting( 'header_image' )->transport = 'postMessage';
  3532. $this->get_setting( 'header_image_data' )->transport = 'postMessage';
  3533. }
  3534. $this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
  3535. 'theme_supports' => array( 'custom-header', 'video' ),
  3536. 'label' => __( 'Header Video' ),
  3537. 'description' => $control_description,
  3538. 'section' => 'header_image',
  3539. 'mime_type' => 'video',
  3540. // @todo These button_labels can be removed once WP_Customize_Media_Control provides mime_type-specific labels automatically. See <https://core.trac.wordpress.org/ticket/38796>.
  3541. 'button_labels' => array(
  3542. 'select' => __( 'Select Video' ),
  3543. 'change' => __( 'Change Video' ),
  3544. 'placeholder' => __( 'No video selected' ),
  3545. 'frame_title' => __( 'Select Video' ),
  3546. 'frame_button' => __( 'Choose Video' ),
  3547. ),
  3548. 'active_callback' => 'is_header_video_active',
  3549. ) ) );
  3550. $this->add_control( 'external_header_video', array(
  3551. 'theme_supports' => array( 'custom-header', 'video' ),
  3552. 'type' => 'url',
  3553. 'description' => __( 'Or, enter a YouTube URL:' ),
  3554. 'section' => 'header_image',
  3555. 'active_callback'=> 'is_front_page',
  3556. ) );
  3557. $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
  3558. $this->selective_refresh->add_partial( 'custom_header', array(
  3559. 'selector' => '#wp-custom-header',
  3560. 'render_callback' => 'the_custom_header_markup',
  3561. 'settings' => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
  3562. 'container_inclusive' => true,
  3563. ) );
  3564. /* Custom Background */
  3565. $this->add_section( 'background_image', array(
  3566. 'title' => __( 'Background Image' ),
  3567. 'theme_supports' => 'custom-background',
  3568. 'priority' => 80,
  3569. ) );
  3570. $this->add_setting( 'background_image', array(
  3571. 'default' => get_theme_support( 'custom-background', 'default-image' ),
  3572. 'theme_supports' => 'custom-background',
  3573. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3574. ) );
  3575. $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
  3576. 'theme_supports' => 'custom-background',
  3577. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3578. ) ) );
  3579. $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
  3580. $this->add_setting( 'background_preset', array(
  3581. 'default' => get_theme_support( 'custom-background', 'default-preset' ),
  3582. 'theme_supports' => 'custom-background',
  3583. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3584. ) );
  3585. $this->add_control( 'background_preset', array(
  3586. 'label' => _x( 'Preset', 'Background Preset' ),
  3587. 'section' => 'background_image',
  3588. 'type' => 'select',
  3589. 'choices' => array(
  3590. 'default' => _x( 'Default', 'Default Preset' ),
  3591. 'fill' => __( 'Fill Screen' ),
  3592. 'fit' => __( 'Fit to Screen' ),
  3593. 'repeat' => _x( 'Repeat', 'Repeat Image' ),
  3594. 'custom' => _x( 'Custom', 'Custom Preset' ),
  3595. ),
  3596. ) );
  3597. $this->add_setting( 'background_position_x', array(
  3598. 'default' => get_theme_support( 'custom-background', 'default-position-x' ),
  3599. 'theme_supports' => 'custom-background',
  3600. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3601. ) );
  3602. $this->add_setting( 'background_position_y', array(
  3603. 'default' => get_theme_support( 'custom-background', 'default-position-y' ),
  3604. 'theme_supports' => 'custom-background',
  3605. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3606. ) );
  3607. $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
  3608. 'label' => __( 'Image Position' ),
  3609. 'section' => 'background_image',
  3610. 'settings' => array(
  3611. 'x' => 'background_position_x',
  3612. 'y' => 'background_position_y',
  3613. ),
  3614. ) ) );
  3615. $this->add_setting( 'background_size', array(
  3616. 'default' => get_theme_support( 'custom-background', 'default-size' ),
  3617. 'theme_supports' => 'custom-background',
  3618. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3619. ) );
  3620. $this->add_control( 'background_size', array(
  3621. 'label' => __( 'Image Size' ),
  3622. 'section' => 'background_image',
  3623. 'type' => 'select',
  3624. 'choices' => array(
  3625. 'auto' => __( 'Original' ),
  3626. 'contain' => __( 'Fit to Screen' ),
  3627. 'cover' => __( 'Fill Screen' ),
  3628. ),
  3629. ) );
  3630. $this->add_setting( 'background_repeat', array(
  3631. 'default' => get_theme_support( 'custom-background', 'default-repeat' ),
  3632. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3633. 'theme_supports' => 'custom-background',
  3634. ) );
  3635. $this->add_control( 'background_repeat', array(
  3636. 'label' => __( 'Repeat Background Image' ),
  3637. 'section' => 'background_image',
  3638. 'type' => 'checkbox',
  3639. ) );
  3640. $this->add_setting( 'background_attachment', array(
  3641. 'default' => get_theme_support( 'custom-background', 'default-attachment' ),
  3642. 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
  3643. 'theme_supports' => 'custom-background',
  3644. ) );
  3645. $this->add_control( 'background_attachment', array(
  3646. 'label' => __( 'Scroll with Page' ),
  3647. 'section' => 'background_image',
  3648. 'type' => 'checkbox',
  3649. ) );
  3650. // If the theme is using the default background callback, we can update
  3651. // the background CSS using postMessage.
  3652. if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
  3653. foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
  3654. $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
  3655. }
  3656. }
  3657. /*
  3658. * Static Front Page
  3659. * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
  3660. * The following replicates behavior from options-reading.php.
  3661. */
  3662. $this->add_section( 'static_front_page', array(
  3663. 'title' => __( 'Static Front Page' ),
  3664. 'priority' => 120,
  3665. 'description' => __( 'Your theme supports a static front page.' ),
  3666. 'active_callback' => array( $this, 'has_published_pages' ),
  3667. ) );
  3668. $this->add_setting( 'show_on_front', array(
  3669. 'default' => get_option( 'show_on_front' ),
  3670. 'capability' => 'manage_options',
  3671. 'type' => 'option',
  3672. ) );
  3673. $this->add_control( 'show_on_front', array(
  3674. 'label' => __( 'Front page displays' ),
  3675. 'section' => 'static_front_page',
  3676. 'type' => 'radio',
  3677. 'choices' => array(
  3678. 'posts' => __( 'Your latest posts' ),
  3679. 'page' => __( 'A static page' ),
  3680. ),
  3681. ) );
  3682. $this->add_setting( 'page_on_front', array(
  3683. 'type' => 'option',
  3684. 'capability' => 'manage_options',
  3685. ) );
  3686. $this->add_control( 'page_on_front', array(
  3687. 'label' => __( 'Front page' ),
  3688. 'section' => 'static_front_page',
  3689. 'type' => 'dropdown-pages',
  3690. 'allow_addition' => true,
  3691. ) );
  3692. $this->add_setting( 'page_for_posts', array(
  3693. 'type' => 'option',
  3694. 'capability' => 'manage_options',
  3695. ) );
  3696. $this->add_control( 'page_for_posts', array(
  3697. 'label' => __( 'Posts page' ),
  3698. 'section' => 'static_front_page',
  3699. 'type' => 'dropdown-pages',
  3700. 'allow_addition' => true,
  3701. ) );
  3702. /* Custom CSS */
  3703. $this->add_section( 'custom_css', array(
  3704. 'title' => __( 'Additional CSS' ),
  3705. 'priority' => 200,
  3706. 'description_hidden' => true,
  3707. 'description' => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
  3708. __( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
  3709. esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
  3710. __( 'Learn more about CSS' ),
  3711. __( '(link opens in a new window)' )
  3712. ),
  3713. ) );
  3714. $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
  3715. 'capability' => 'edit_css',
  3716. 'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
  3717. ) );
  3718. $this->add_setting( $custom_css_setting );
  3719. $this->add_control( 'custom_css', array(
  3720. 'type' => 'textarea',
  3721. 'section' => 'custom_css',
  3722. 'settings' => array( 'default' => $custom_css_setting->id ),
  3723. 'input_attrs' => array(
  3724. 'class' => 'code', // Ensures contents displayed as LTR instead of RTL.
  3725. ),
  3726. ) );
  3727. }
  3728. /**
  3729. * Return whether there are published pages.
  3730. *
  3731. * Used as active callback for static front page section and controls.
  3732. *
  3733. * @access private
  3734. * @since 4.7.0
  3735. *
  3736. * @returns bool Whether there are published (or to be published) pages.
  3737. */
  3738. public function has_published_pages() {
  3739. $setting = $this->get_setting( 'nav_menus_created_posts' );
  3740. if ( $setting ) {
  3741. foreach ( $setting->value() as $post_id ) {
  3742. if ( 'page' === get_post_type( $post_id ) ) {
  3743. return true;
  3744. }
  3745. }
  3746. }
  3747. return 0 !== count( get_pages() );
  3748. }
  3749. /**
  3750. * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
  3751. *
  3752. * @since 4.2.0
  3753. * @access public
  3754. *
  3755. * @see add_dynamic_settings()
  3756. */
  3757. public function register_dynamic_settings() {
  3758. $setting_ids = array_keys( $this->unsanitized_post_values() );
  3759. $this->add_dynamic_settings( $setting_ids );
  3760. }
  3761. /**
  3762. * Callback for validating the header_textcolor value.
  3763. *
  3764. * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
  3765. * Returns default text color if hex color is empty.
  3766. *
  3767. * @since 3.4.0
  3768. *
  3769. * @param string $color
  3770. * @return mixed
  3771. */
  3772. public function _sanitize_header_textcolor( $color ) {
  3773. if ( 'blank' === $color )
  3774. return 'blank';
  3775. $color = sanitize_hex_color_no_hash( $color );
  3776. if ( empty( $color ) )
  3777. $color = get_theme_support( 'custom-header', 'default-text-color' );
  3778. return $color;
  3779. }
  3780. /**
  3781. * Callback for validating a background setting value.
  3782. *
  3783. * @since 4.7.0
  3784. * @access private
  3785. *
  3786. * @param string $value Repeat value.
  3787. * @param WP_Customize_Setting $setting Setting.
  3788. * @return string|WP_Error Background value or validation error.
  3789. */
  3790. public function _sanitize_background_setting( $value, $setting ) {
  3791. if ( 'background_repeat' === $setting->id ) {
  3792. if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
  3793. return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
  3794. }
  3795. } elseif ( 'background_attachment' === $setting->id ) {
  3796. if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
  3797. return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
  3798. }
  3799. } elseif ( 'background_position_x' === $setting->id ) {
  3800. if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
  3801. return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
  3802. }
  3803. } elseif ( 'background_position_y' === $setting->id ) {
  3804. if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
  3805. return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
  3806. }
  3807. } elseif ( 'background_size' === $setting->id ) {
  3808. if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
  3809. return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
  3810. }
  3811. } elseif ( 'background_preset' === $setting->id ) {
  3812. if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
  3813. return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
  3814. }
  3815. } elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
  3816. $value = empty( $value ) ? '' : esc_url_raw( $value );
  3817. } else {
  3818. return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
  3819. }
  3820. return $value;
  3821. }
  3822. /**
  3823. * Export header video settings to facilitate selective refresh.
  3824. *
  3825. * @since 4.7.0
  3826. *
  3827. * @param array $response Response.
  3828. * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
  3829. * @param array $partials Array of partials.
  3830. * @return array
  3831. */
  3832. public function export_header_video_settings( $response, $selective_refresh, $partials ) {
  3833. if ( isset( $partials['custom_header'] ) ) {
  3834. $response['custom_header_settings'] = get_header_video_settings();
  3835. }
  3836. return $response;
  3837. }
  3838. /**
  3839. * Callback for validating the header_video value.
  3840. *
  3841. * Ensures that the selected video is less than 8MB and provides an error message.
  3842. *
  3843. * @since 4.7.0
  3844. *
  3845. * @param WP_Error $validity
  3846. * @param mixed $value
  3847. * @return mixed
  3848. */
  3849. public function _validate_header_video( $validity, $value ) {
  3850. $video = get_attached_file( absint( $value ) );
  3851. if ( $video ) {
  3852. $size = filesize( $video );
  3853. if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
  3854. $validity->add( 'size_too_large',
  3855. __( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
  3856. );
  3857. }
  3858. if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
  3859. $validity->add( 'invalid_file_type', sprintf(
  3860. /* translators: 1: .mp4, 2: .mov */
  3861. __( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
  3862. '<code>.mp4</code>',
  3863. '<code>.mov</code>'
  3864. ) );
  3865. }
  3866. }
  3867. return $validity;
  3868. }
  3869. /**
  3870. * Callback for validating the external_header_video value.
  3871. *
  3872. * Ensures that the provided URL is supported.
  3873. *
  3874. * @since 4.7.0
  3875. *
  3876. * @param WP_Error $validity
  3877. * @param mixed $value
  3878. * @return mixed
  3879. */
  3880. public function _validate_external_header_video( $validity, $value ) {
  3881. $video = esc_url_raw( $value );
  3882. if ( $video ) {
  3883. if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
  3884. $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
  3885. }
  3886. }
  3887. return $validity;
  3888. }
  3889. /**
  3890. * Callback for sanitizing the external_header_video value.
  3891. *
  3892. * @since 4.7.1
  3893. *
  3894. * @param string $value URL.
  3895. * @return string Sanitized URL.
  3896. */
  3897. public function _sanitize_external_header_video( $value ) {
  3898. return esc_url_raw( trim( $value ) );
  3899. }
  3900. /**
  3901. * Callback for rendering the custom logo, used in the custom_logo partial.
  3902. *
  3903. * This method exists because the partial object and context data are passed
  3904. * into a partial's render_callback so we cannot use get_custom_logo() as
  3905. * the render_callback directly since it expects a blog ID as the first
  3906. * argument. When WP no longer supports PHP 5.3, this method can be removed
  3907. * in favor of an anonymous function.
  3908. *
  3909. * @see WP_Customize_Manager::register_controls()
  3910. *
  3911. * @since 4.5.0
  3912. * @access private
  3913. *
  3914. * @return string Custom logo.
  3915. */
  3916. public function _render_custom_logo_partial() {
  3917. return get_custom_logo();
  3918. }
  3919. }