PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 13.6
Jetpack – WP Security, Backup, Speed, & Growth v13.6
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / custom-css / custom-css.php

custom-css.php in Jetpack – WP Security, Backup, Speed, & Growth 13.6, at modules/custom-css/custom-css.php

1,366 lines 41.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
4
5 /**
6 * Alternate Custom CSS source for 4.7 compat.
7 *
8 * @since 4.4.2
9 *
10 * @package automattic/jetpack
11 */
12
13 use Automattic\Jetpack\Assets;
14 use Automattic\Jetpack\Redirect;
15
16 if ( ! class_exists( 'Jetpack_Custom_CSS_Enhancements' ) ) {
17 /**
18 * Class Jetpack_Custom_CSS_Enhancements
19 */
20 class Jetpack_Custom_CSS_Enhancements {
21
22 /**
23 * Set up the actions and filters needed for our compatability layer on top of core's Custom CSS implementation.
24 */
25 public static function add_hooks() {
26 add_action( 'init', array( __CLASS__, 'init' ) );
27 add_action( 'customize_controls_enqueue_scripts', array( __CLASS__, 'customize_controls_enqueue_scripts' ) );
28 add_action( 'customize_register', array( __CLASS__, 'customize_register' ) );
29 add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 20, 2 );
30 add_action( 'customize_preview_init', array( __CLASS__, 'customize_preview_init' ) );
31 add_filter( '_wp_post_revision_fields', array( __CLASS__, 'wp_post_revision_fields' ), 10, 2 );
32 add_action( 'load-revision.php', array( __CLASS__, 'load_revision_php' ) );
33
34 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'wp_enqueue_scripts' ) );
35 add_action( 'admin_footer', array( __CLASS__, 'update_initial_state' ) );
36 add_action( 'wp_body_open', array( __CLASS__, 'display_frontend_warning' ) );
37
38 // Handle Sass/LESS.
39 add_filter( 'customize_value_custom_css', array( __CLASS__, 'customize_value_custom_css' ), 10, 2 );
40 add_filter( 'customize_update_custom_css_post_content_args', array( __CLASS__, 'customize_update_custom_css_post_content_args' ), 10, 3 );
41 add_filter( 'update_custom_css_data', array( __CLASS__, 'update_custom_css_data' ), 10, 2 );
42
43 // Stuff for stripping out the theme's default stylesheet...
44 add_filter( 'stylesheet_uri', array( __CLASS__, 'style_filter' ) );
45 add_filter( 'safecss_skip_stylesheet', array( __CLASS__, 'preview_skip_stylesheet' ) );
46
47 // Stuff for overriding content width...
48 add_action( 'customize_preview_init', array( __CLASS__, 'preview_content_width' ) );
49 add_filter( 'jetpack_content_width', array( __CLASS__, 'jetpack_content_width' ) );
50 add_filter( 'editor_max_image_size', array( __CLASS__, 'editor_max_image_size' ), 10, 3 );
51 add_action( 'template_redirect', array( __CLASS__, 'set_content_width' ) );
52 add_action( 'admin_init', array( __CLASS__, 'set_content_width' ) );
53 }
54
55 /**
56 * Things that we do on init.
57 */
58 public static function init() {
59
60 wp_register_style( 'jetpack-codemirror', plugins_url( 'custom-css/css/codemirror.css', __FILE__ ), array(), '20120905' );
61 wp_register_style( 'jetpack-customizer-css', plugins_url( 'custom-css/css/customizer-control.css', __FILE__ ), array(), '20140728' );
62 wp_register_script( 'jetpack-codemirror', plugins_url( 'custom-css/js/codemirror.min.js', __FILE__ ), array(), '3.16', true );
63
64 $src = Assets::get_file_url_for_environment(
65 '_inc/build/custom-css/custom-css/js/core-customizer-css.core-4.9.min.js',
66 'modules/custom-css/custom-css/js/core-customizer-css.core-4.9.js'
67 );
68 wp_register_script(
69 'jetpack-customizer-css',
70 $src,
71 array(
72 'jquery',
73 'customize-controls',
74 'underscore',
75 ),
76 JETPACK__VERSION,
77 true
78 );
79
80 wp_register_script(
81 'jetpack-customizer-css-preview',
82 Assets::get_file_url_for_environment(
83 '_inc/build/custom-css/custom-css/js/core-customizer-css-preview.min.js',
84 'modules/custom-css/custom-css/js/core-customizer-css-preview.js'
85 ),
86 array( 'jquery', 'customize-selective-refresh' ),
87 JETPACK__VERSION,
88 true
89 );
90
91 remove_action( 'wp_head', 'wp_custom_css_cb', 11 ); // 4.7.0 had it at 11, 4.7.1 moved it to 101.
92 remove_action( 'wp_head', 'wp_custom_css_cb', 101 );
93 add_action( 'wp_head', array( __CLASS__, 'wp_custom_css_cb' ), 101 );
94
95 if ( isset( $_GET['custom-css'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
96 self::print_linked_custom_css();
97 }
98 }
99
100 /**
101 * Things that we do on init when the Customize Preview is loading.
102 */
103 public static function customize_preview_init() {
104 add_filter( 'wp_get_custom_css', array( __CLASS__, 'customize_preview_wp_get_custom_css' ) );
105 }
106
107 /**
108 * Print the current Custom CSS. This is for linking instead of printing directly.
109 *
110 * @return never
111 */
112 public static function print_linked_custom_css() {
113 header( 'Content-type: text/css' );
114 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS ) . ' GMT' );
115 echo wp_get_custom_css(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
116 exit;
117 }
118
119 /**
120 * Re-map the Edit CSS capability.
121 *
122 * Core, by default, restricts this to users that have `unfiltered_html` which
123 * would make the feature unusable in multi-site by non-super-admins, due to Core
124 * not shipping any solid sanitization.
125 *
126 * We're expanding who can use it, and then conditionally applying CSSTidy
127 * sanitization to users that do not have the `unfiltered_html` capability.
128 *
129 * @param array $caps Returns the user's actual capabilities.
130 * @param string $cap Capability name.
131 *
132 * @return array $caps
133 */
134 public static function map_meta_cap( $caps, $cap ) {
135 if ( 'edit_css' === $cap ) {
136 $caps = array( 'edit_theme_options' );
137 }
138 return $caps;
139 }
140
141 /**
142 * Shows Preprocessor code in the Revisions screen, and ensures that post_content_filtered
143 * is maintained on revisions
144 *
145 * @param array $fields Post fields pertinent to revisions.
146 * @param array $post A post array being processed for insertion as a post revision.
147 *
148 * @return array $fields Modified array to include post_content_filtered.
149 */
150 public static function wp_post_revision_fields( $fields, $post ) {
151 // None of the fields in $post are required to be passed in this filter.
152 if ( ! isset( $post['post_type'] ) || ! isset( $post['ID'] ) ) {
153 return $fields;
154 }
155
156 // If we're passed in a revision, go get the main post instead.
157 if ( 'revision' === $post['post_type'] ) {
158 $main_post_id = wp_is_post_revision( $post['ID'] );
159 $post = get_post( $main_post_id, ARRAY_A );
160 }
161 if ( 'custom_css' === $post['post_type'] ) {
162 $fields['post_content'] = __( 'CSS', 'jetpack' );
163 $fields['post_content_filtered'] = __( 'Preprocessor', 'jetpack' );
164 }
165 return $fields;
166 }
167
168 /**
169 * Get the published custom CSS post.
170 *
171 * @param string $stylesheet Optional. A theme object stylesheet name. Defaults to the current theme.
172 * @return WP_Post|null
173 */
174 public static function get_css_post( $stylesheet = '' ) {
175 return wp_get_custom_css_post( $stylesheet );
176 }
177
178 /**
179 * Override Core's `wp_custom_css_cb` method to provide linking to custom css.
180 */
181 public static function wp_custom_css_cb() {
182 $styles = wp_get_custom_css();
183 if ( ! $styles ) {
184 return;
185 }
186
187 $should_embed = strlen( $styles ) < 2000;
188 /** This filter is documented in projects/plugins/jetpack/modules/custom-css/custom-css.php */
189 $should_embed = apply_filters( 'safecss_embed_style', $should_embed, $styles );
190
191 if ( $should_embed || is_customize_preview() ) {
192 printf(
193 '<style type="text/css" id="wp-custom-css">%1$s</style>',
194 wp_strip_all_tags( $styles ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
195 );
196 } else {
197 // Add a cache buster to the url.
198 $url = home_url( '/' );
199 $url = add_query_arg( 'custom-css', substr( md5( $styles ), -10 ), $url );
200
201 printf(
202 '<link rel="stylesheet" type="text/css" id="wp-custom-css" href="%1$s" />', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
203 esc_url( $url )
204 );
205 }
206 }
207
208 /**
209 * Get the ID of a Custom CSS post tying to a given stylesheet.
210 *
211 * @param string $stylesheet Stylesheet name.
212 *
213 * @return int $post_id Post ID.
214 */
215 public static function post_id( $stylesheet = '' ) {
216 $post = self::get_css_post( $stylesheet );
217 if ( $post instanceof WP_Post ) {
218 return $post->ID;
219 }
220 return 0;
221 }
222
223 /**
224 * Partial for use in the Customizer.
225 */
226 public static function echo_custom_css_partial() {
227 echo wp_get_custom_css(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
228 }
229
230 /**
231 * Admin page!
232 *
233 * This currently has two main uses -- firstly to display the css for an inactive
234 * theme if there are no revisions attached it to a legacy bug, and secondly to
235 * handle folks that have bookmarkes in their browser going to the old page for
236 * managing Custom CSS in Jetpack.
237 *
238 * If we ever add back in a non-Customizer CSS editor, this would be the place.
239 */
240 public static function admin_page() {
241 $post = null;
242 $stylesheet = null;
243 if ( isset( $_GET['id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
244 $post_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site
245 $post = get_post( $post_id );
246 if ( $post instanceof WP_Post && 'custom_css' === $post->post_type ) {
247 $stylesheet = $post->post_title;
248 }
249 }
250 ?>
251 <div class="wrap">
252 <?php self::revisions_switcher_box( $stylesheet ); ?>
253 <h1>
254 <?php
255 if ( $post ) {
256 printf( 'Custom CSS for &#8220;%1$s&#8221;', wp_get_theme( $stylesheet )->Name ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
257 } else {
258 esc_html_e( 'Custom CSS', 'jetpack' );
259 }
260 if ( current_user_can( 'customize' ) ) {
261 printf(
262 ' <a class="page-title-action hide-if-no-customize" href="%1$s">%2$s</a>',
263 esc_url( self::customizer_link() ),
264 esc_html__( 'Manage with Live Preview', 'jetpack' )
265 );
266 }
267 ?>
268 </h1>
269 <p><?php esc_html_e( 'Custom CSS is now managed in the Customizer.', 'jetpack' ); ?></p>
270 <?php if ( $post ) : ?>
271 <div class="revisions">
272 <h3><?php esc_html_e( 'CSS', 'jetpack' ); ?></h3>
273 <textarea class="widefat" readonly><?php echo esc_textarea( $post->post_content ); ?></textarea>
274 <?php if ( $post->post_content_filtered ) : ?>
275 <h3><?php esc_html_e( 'Preprocessor', 'jetpack' ); ?></h3>
276 <textarea class="widefat" readonly><?php echo esc_textarea( $post->post_content_filtered ); ?></textarea>
277 <?php endif; ?>
278 </div>
279 <?php endif; ?>
280 </div>
281
282 <style>
283 .other-themes-wrap {
284 float: right;
285 background-color: #fff;
286 -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.1);
287 box-shadow: 0 1px 3px rgba(0,0,0,0.1);
288 padding: 5px 10px;
289 margin-bottom: 10px;
290 }
291 .other-themes-wrap label {
292 display: block;
293 margin-bottom: 10px;
294 }
295 .other-themes-wrap select {
296 float: left;
297 width: 77%;
298 }
299 .other-themes-wrap button {
300 float: right;
301 width: 20%;
302 }
303 .revisions {
304 clear: both;
305 }
306 .revisions textarea {
307 min-height: 300px;
308 background: #fff;
309 }
310 </style>
311 <script>
312 (function($){
313 var $switcher = $('.other-themes-wrap');
314 $switcher.find('button').on('click', function(e){
315 e.preventDefault();
316 if ( $switcher.find('select').val() ) {
317 window.location.href = $switcher.find('select').val();
318 }
319 });
320 })(jQuery);
321 </script>
322 <?php
323 }
324
325 /**
326 * Build the URL to deep link to the Customizer.
327 *
328 * You can modify the return url via $args.
329 *
330 * @param array $args Array of parameters.
331 * @return string
332 */
333 public static function customizer_link( $args = array() ) {
334 $args = wp_parse_args(
335 $args,
336 array(
337 'return_url' => isset( $_SERVER['REQUEST_URI'] ) ? rawurlencode( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : '',
338 )
339 );
340
341 return add_query_arg(
342 array(
343 array(
344 'autofocus' => array(
345 'section' => 'custom_css',
346 ),
347 ),
348 'return' => $args['return_url'],
349 ),
350 admin_url( 'customize.php' )
351 );
352 }
353
354 /**
355 * Handle the enqueueing and localizing for scripts to be used in the Customizer.
356 */
357 public static function customize_controls_enqueue_scripts() {
358 global $wp_customize;
359
360 wp_enqueue_style( 'jetpack-customizer-css' );
361 wp_enqueue_script( 'jetpack-customizer-css' );
362
363 $setting = $wp_customize->get_setting( 'jetpack_custom_css[replace]' );
364 $content_help = __( 'Set a different media width for full size images.', 'jetpack' );
365
366 if ( ! empty( $GLOBALS['content_width'] ) ) {
367 $content_help .= sprintf(
368 // translators: the theme name and then the default width.
369 _n( ' The default media width for the <strong>%1$s</strong> theme is %2$d pixel.', ' The default media width for the <strong>%1$s</strong> theme is %2$d pixels.', (int) $GLOBALS['content_width'], 'jetpack' ),
370 wp_get_theme()->Name,
371 (int) $GLOBALS['content_width']
372 );
373 }
374
375 wp_localize_script(
376 'jetpack-customizer-css',
377 '_jp_css_settings',
378 array(
379 /** This filter is documented in modules/custom-css/custom-css.php */
380 'useRichEditor' => ! jetpack_is_mobile() && apply_filters( 'safecss_use_ace', true ),
381 'areThereCssRevisions' => self::are_there_css_revisions(),
382 'startFresh' => isset( $setting ) && $setting->value(),
383 'revisionsUrl' => self::get_revisions_url(),
384 'cssHelpUrl' => '//en.support.wordpress.com/custom-design/editing-css/',
385 'l10n' => array(
386 'mode' => __( 'Start Fresh (deprecated)', 'jetpack' ),
387 'mobile' => __( 'On Mobile', 'jetpack' ),
388 'contentWidth' => $content_help,
389 'revisions' => _x( 'See full history', 'Toolbar button to see full CSS revision history', 'jetpack' ),
390 'css_help_title' => _x( 'Help', 'Toolbar button to get help with custom CSS', 'jetpack' ),
391 'startFreshCustomizerWarning' => __( "The Start Fresh option in the Additional CSS panel is enabled, which means the theme's original CSS is not applied. This option will no longer be supported after August 6, 2024.", 'jetpack' ),
392 ),
393 )
394 );
395 }
396
397 /**
398 * Check whether there are CSS Revisions for a given theme.
399 *
400 * Going forward, there should always be, but this was necessitated
401 * early on by https://core.trac.wordpress.org/ticket/30854
402 *
403 * @param string $stylesheet Stylesheet name.
404 *
405 * @return bool|null|WP_Post
406 */
407 public static function are_there_css_revisions( $stylesheet = '' ) {
408 $post = wp_get_custom_css_post( $stylesheet );
409 if ( empty( $post ) ) {
410 return $post;
411 }
412 return (bool) wp_get_post_revisions( $post );
413 }
414
415 /**
416 * Core doesn't have a function to get the revisions url for a given post ID.
417 *
418 * @param string $stylesheet Stylesheet name.
419 *
420 * @return null|string|void
421 */
422 public static function get_revisions_url( $stylesheet = '' ) {
423 $post = wp_get_custom_css_post( $stylesheet );
424
425 // If we have any currently saved customizations...
426 if ( $post instanceof WP_Post ) {
427 $revisions = wp_get_post_revisions( $post->ID, array( 'posts_per_page' => 1 ) );
428 if ( empty( $revisions ) || is_wp_error( $revisions ) ) {
429 return admin_url( 'themes.php?page=editcss' );
430 }
431 $revision = reset( $revisions );
432 return get_edit_post_link( $revision->ID );
433 }
434
435 return admin_url( 'themes.php?page=editcss' );
436 }
437
438 /**
439 * Get a map of all theme names and theme stylesheets for mapping stuff.
440 *
441 * @return array
442 */
443 public static function get_themes() {
444 $themes = wp_get_themes( array( 'errors' => null ) );
445 $all = array();
446 foreach ( $themes as $theme ) {
447 $all[ $theme->name ] = $theme->stylesheet;
448 }
449 return $all;
450 }
451
452 /**
453 * When we need to get all themes that have Custom CSS saved.
454 *
455 * @return array
456 */
457 public static function get_all_themes_with_custom_css() {
458 $themes = self::get_themes();
459 $custom_css = get_posts(
460 array(
461 'post_type' => 'custom_css',
462 'post_status' => get_post_stati(),
463 'number' => -1,
464 'order' => 'DESC',
465 'orderby' => 'modified',
466 )
467 );
468 $return = array();
469
470 foreach ( $custom_css as $post ) {
471 $stylesheet = $post->post_title;
472 $label = array_search( $stylesheet, $themes, true );
473
474 if ( ! $label ) {
475 continue;
476 }
477
478 $return[ $stylesheet ] = array(
479 'label' => $label,
480 'post' => $post,
481 );
482 }
483
484 return $return;
485 }
486
487 /**
488 * Handle the enqueueing of scripts for customize previews.
489 */
490 public static function wp_enqueue_scripts() {
491 if ( is_customize_preview() ) {
492 wp_enqueue_script( 'jetpack-customizer-css-preview' );
493 wp_localize_script(
494 'jetpack-customizer-css-preview',
495 'jpCustomizerCssPreview',
496 array(
497 /** This filter is documented in modules/custom-css/custom-css.php */
498 'preprocessors' => apply_filters( 'jetpack_custom_css_preprocessors', array() ),
499 )
500 );
501 }
502 }
503
504 /**
505 * Sanitize the CSS for users without `unfiltered_html`.
506 *
507 * @param string $css Input CSS.
508 * @param array $args Array of CSS options.
509 *
510 * @return mixed|string
511 */
512 public static function sanitize_css( $css, $args = array() ) {
513 $args = wp_parse_args(
514 $args,
515 array(
516 'force' => false,
517 'preprocessor' => null,
518 )
519 );
520
521 if ( $args['force'] || ! current_user_can( 'unfiltered_html' ) ) {
522
523 $warnings = array();
524
525 safecss_class();
526 $csstidy = new csstidy();
527 $csstidy->optimise = new safecss( $csstidy );
528
529 $csstidy->set_cfg( 'remove_bslash', false );
530 $csstidy->set_cfg( 'compress_colors', false );
531 $csstidy->set_cfg( 'compress_font-weight', false );
532 $csstidy->set_cfg( 'optimise_shorthands', 0 );
533 $csstidy->set_cfg( 'remove_last_;', false );
534 $csstidy->set_cfg( 'case_properties', false );
535 $csstidy->set_cfg( 'discard_invalid_properties', true );
536 $csstidy->set_cfg( 'css_level', 'CSS3.0' );
537 $csstidy->set_cfg( 'preserve_css', true );
538 $csstidy->set_cfg( 'template', __DIR__ . '/csstidy/wordpress-standard.tpl' );
539
540 // Test for some preg_replace stuff.
541 $prev = $css;
542 $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $css );
543 // prevent content: '\3434' from turning into '\\3434'.
544 $css = str_replace( array( '\'\\\\', '"\\\\' ), array( '\'\\', '"\\' ), $css );
545 if ( $css !== $prev ) {
546 $warnings[] = 'preg_replace found stuff';
547 }
548
549 // Some people put weird stuff in their CSS, KSES tends to be greedy.
550 $css = str_replace( '<=', '&lt;=', $css );
551
552 // Test for some kses stuff.
553 $prev = $css;
554 // Why KSES instead of strip_tags? Who knows?
555 $css = wp_kses_split( $css, array(), array() );
556 $css = str_replace( '&gt;', '>', $css ); // kses replaces lone '>' with &gt;
557 // Why both KSES and strip_tags? Because we just added some '>'.
558 $css = strip_tags( $css ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- scared to update this to wp_strip_all_tags since we're building a CSS file here.
559
560 if ( $css !== $prev ) {
561 $warnings[] = 'kses found stuff';
562 }
563
564 // if we're not using a preprocessor.
565 if ( ! $args['preprocessor'] ) {
566
567 /** This action is documented in modules/custom-css/custom-css.php */
568 do_action( 'safecss_parse_pre', $csstidy, $css, $args );
569
570 $csstidy->parse( $css );
571
572 /** This action is documented in modules/custom-css/custom-css.php */
573 do_action( 'safecss_parse_post', $csstidy, $warnings, $args );
574
575 $css = $csstidy->print->plain();
576 }
577 }
578 return $css;
579 }
580
581 /**
582 * Override $content_width in customizer previews.
583 */
584 public static function preview_content_width() {
585 global $wp_customize;
586 if ( ! is_customize_preview() ) {
587 return;
588 }
589
590 $setting = $wp_customize->get_setting( 'jetpack_custom_css[content_width]' );
591 if ( ! $setting ) {
592 return;
593 }
594
595 $customized_content_width = (int) $setting->post_value();
596 if ( ! empty( $customized_content_width ) ) {
597 $GLOBALS['content_width'] = $customized_content_width;
598 }
599 }
600
601 /**
602 * Filter the current theme's stylesheet for potentially nullifying it.
603 *
604 * @param string $current Stylesheet URI for the current theme/child theme.
605 *
606 * @return mixed|void
607 */
608 public static function style_filter( $current ) {
609 if ( is_admin() ) {
610 return $current;
611 } elseif ( self::is_freetrial() && ( ! self::is_preview() || ! current_user_can( 'switch_themes' ) ) ) {
612 return $current;
613 } elseif ( self::skip_stylesheet() ) {
614 /** This filter is documented in modules/custom-css/custom-css.php */
615 return apply_filters( 'safecss_style_filter_url', plugins_url( 'custom-css/css/blank.css', __FILE__ ) );
616 }
617
618 return $current;
619 }
620
621 /**
622 * Determine whether or not we should have the theme skip its main stylesheet.
623 *
624 * @return mixed The truthiness of this value determines whether the stylesheet should be skipped.
625 */
626 public static function skip_stylesheet() {
627 /** This filter is documented in modules/custom-css/custom-css.php */
628 $skip_stylesheet = apply_filters( 'safecss_skip_stylesheet', null );
629 if ( $skip_stylesheet !== null ) {
630 return $skip_stylesheet;
631 }
632
633 $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
634 if ( isset( $jetpack_custom_css['replace'] ) ) {
635 return $jetpack_custom_css['replace'];
636 }
637
638 return false;
639 }
640
641 /**
642 * Override $content_width in customizer previews.
643 *
644 * Runs on `safecss_skip_stylesheet` filter.
645 *
646 * @param bool $skip_value Should the stylesheet be skipped.
647 *
648 * @return null|bool
649 */
650 public static function preview_skip_stylesheet( $skip_value ) {
651 global $wp_customize;
652 if ( ! is_customize_preview() ) {
653 return $skip_value;
654 }
655
656 $setting = $wp_customize->get_setting( 'jetpack_custom_css[replace]' );
657 if ( ! $setting ) {
658 return $skip_value;
659 }
660
661 $customized_replace = $setting->post_value();
662 if ( null !== $customized_replace ) {
663 return $customized_replace;
664 }
665
666 return $skip_value;
667 }
668
669 /**
670 * Add Custom CSS section and controls.
671 *
672 * @param WP_Customize_Manager $wp_customize WP_Customize_Manager instance.
673 */
674 public static function customize_register( $wp_customize ) {
675
676 /**
677 * SETTINGS.
678 */
679
680 $wp_customize->add_setting(
681 'jetpack_custom_css[preprocessor]',
682 array(
683 'default' => '',
684 'transport' => 'postMessage',
685 'sanitize_callback' => array( __CLASS__, 'sanitize_preprocessor' ),
686 )
687 );
688
689 $wp_customize->add_setting(
690 'jetpack_custom_css[replace]',
691 array(
692 'default' => false,
693 'transport' => 'refresh',
694 )
695 );
696
697 $wp_customize->add_setting(
698 'jetpack_custom_css[content_width]',
699 array(
700 'default' => '',
701 'transport' => 'refresh',
702 'sanitize_callback' => array( __CLASS__, 'intval_base10' ),
703 )
704 );
705
706 // Add custom sanitization to the core css customizer setting.
707 foreach ( $wp_customize->settings() as $setting ) {
708 if ( $setting instanceof WP_Customize_Custom_CSS_Setting ) {
709 add_filter( "customize_sanitize_{$setting->id}", array( __CLASS__, 'sanitize_css_callback' ), 10, 2 );
710 }
711 }
712
713 /**
714 * CONTROLS.
715 */
716
717 // Overwrite or Tweak the Core Control.
718 $core_custom_css = $wp_customize->get_control( 'custom_css' );
719 if ( $core_custom_css ) {
720 if ( $core_custom_css instanceof WP_Customize_Code_Editor_Control ) {
721 // In WP 4.9, we let the Core CodeMirror control keep running the show, but hook into it to tweak stuff.
722 $types = array(
723 'default' => 'text/css',
724 'less' => 'text/x-less',
725 'sass' => 'text/x-scss',
726 );
727 $preprocessor = $wp_customize->get_setting( 'jetpack_custom_css[preprocessor]' )->value();
728 if ( isset( $types[ $preprocessor ] ) ) {
729 $core_custom_css->code_type = $types[ $preprocessor ];
730 }
731 } else {
732 // Core < 4.9 Fallback
733 $core_custom_css->type = 'jetpackCss';
734 }
735 }
736
737 $wp_customize->selective_refresh->add_partial(
738 'custom_css',
739 array(
740 'type' => 'custom_css',
741 'selector' => '#wp-custom-css',
742 'container_inclusive' => false,
743 'fallback_refresh' => false,
744 'settings' => array(
745 'custom_css[' . $wp_customize->get_stylesheet() . ']',
746 'jetpack_custom_css[preprocessor]',
747 ),
748 'render_callback' => array( __CLASS__, 'echo_custom_css_partial' ),
749 )
750 );
751
752 $wp_customize->add_control(
753 'wpcom_custom_css_content_width_control',
754 array(
755 'type' => 'text',
756 'label' => __( 'Media Width (deprecated)', 'jetpack' ),
757 'section' => 'custom_css',
758 'settings' => 'jetpack_custom_css[content_width]',
759 )
760 );
761
762 $wp_customize->add_control(
763 'jetpack_css_mode_control',
764 array(
765 'type' => 'checkbox',
766 'label' => __( 'Don\'t use the theme\'s original CSS.', 'jetpack' ),
767 'section' => 'custom_css',
768 'settings' => 'jetpack_custom_css[replace]',
769 )
770 );
771
772 /**
773 * An action to grab on to if another Jetpack Module would like to add its own controls.
774 *
775 * @module custom-css
776 *
777 * @since 4.4.2
778 *
779 * @param $wp_customize The WP_Customize object.
780 */
781 do_action( 'jetpack_custom_css_customizer_controls', $wp_customize );
782
783 /** This filter is documented in modules/custom-css/custom-css.php */
784 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
785 if ( ! empty( $preprocessors ) ) {
786 $preprocessor_choices = array(
787 '' => __( 'None', 'jetpack' ),
788 );
789
790 foreach ( $preprocessors as $preprocessor_key => $processor ) {
791 $preprocessor_choices[ $preprocessor_key ] = $processor['name'];
792 }
793
794 $wp_customize->add_control(
795 'jetpack_css_preprocessors_control',
796 array(
797 'type' => 'select',
798 'choices' => $preprocessor_choices,
799 'label' => __( 'Preprocessor (deprecated)', 'jetpack' ),
800 'section' => 'custom_css',
801 'settings' => 'jetpack_custom_css[preprocessor]',
802 )
803 );
804 }
805 }
806
807 /**
808 * The callback to handle sanitizing the CSS. Takes different arguments, hence the proxy function.
809 *
810 * @param mixed $css Value of the setting.
811 *
812 * @return mixed|string
813 */
814 public static function sanitize_css_callback( $css ) {
815 global $wp_customize;
816 return self::sanitize_css(
817 $css,
818 array(
819 'preprocessor' => $wp_customize->get_setting( 'jetpack_custom_css[preprocessor]' )->value(),
820 )
821 );
822 }
823
824 /**
825 * Flesh out for wpcom.
826 *
827 * @todo
828 *
829 * @return bool
830 */
831 public static function is_freetrial() {
832 return false;
833 }
834
835 /**
836 * Flesh out for wpcom.
837 *
838 * @todo
839 *
840 * @return bool
841 */
842 public static function is_preview() {
843 return false;
844 }
845
846 /**
847 * Output the custom css for customize preview.
848 *
849 * @param string $css Custom CSS content.
850 *
851 * @return mixed
852 */
853 public static function customize_preview_wp_get_custom_css( $css ) {
854 global $wp_customize;
855
856 $preprocessor = $wp_customize->get_setting( 'jetpack_custom_css[preprocessor]' )->value();
857
858 // If it's empty, just return.
859 if ( empty( $preprocessor ) ) {
860 return $css;
861 }
862
863 /** This filter is documented in modules/custom-css/custom-css.php */
864 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
865 if ( isset( $preprocessors[ $preprocessor ] ) ) {
866 return call_user_func( $preprocessors[ $preprocessor ]['callback'], $css );
867 }
868
869 return $css;
870 }
871
872 /**
873 * Add CSS preprocessing to our CSS if it is supported.
874 *
875 * @param mixed $css Value of the setting.
876 * @param WP_Customize_Setting $setting WP_Customize_Setting instance.
877 *
878 * @return string
879 */
880 public static function customize_value_custom_css( $css, $setting ) {
881 // Find the current preprocessor.
882 $preprocessor = null;
883 $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
884 if ( isset( $jetpack_custom_css['preprocessor'] ) ) {
885 $preprocessor = $jetpack_custom_css['preprocessor'];
886 }
887
888 // If it's not supported, just return.
889 /** This filter is documented in modules/custom-css/custom-css.php */
890 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
891 if ( ! isset( $preprocessors[ $preprocessor ] ) ) {
892 return $css;
893 }
894
895 // Swap it for the `post_content_filtered` instead.
896 $post = wp_get_custom_css_post( $setting->stylesheet );
897 if ( $post && ! empty( $post->post_content_filtered ) ) {
898 $css = $post->post_content_filtered;
899 }
900
901 return $css;
902 }
903
904 /**
905 * Store the original pre-processed CSS in `post_content_filtered`
906 * and then store processed CSS in `post_content`.
907 *
908 * @param array $args Content post args.
909 * @param string $css Original CSS being updated.
910 *
911 * @return mixed
912 */
913 public static function customize_update_custom_css_post_content_args( $args, $css ) {
914 // Find the current preprocessor.
915 $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
916 if ( empty( $jetpack_custom_css['preprocessor'] ) ) {
917 return $args;
918 }
919
920 $preprocessor = $jetpack_custom_css['preprocessor'];
921 /** This filter is documented in modules/custom-css/custom-css.php */
922 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
923
924 // If it's empty, just return.
925 if ( empty( $preprocessor ) ) {
926 return $args;
927 }
928
929 if ( isset( $preprocessors[ $preprocessor ] ) ) {
930 $args['post_content_filtered'] = $css;
931 $args['post_content'] = call_user_func( $preprocessors[ $preprocessor ]['callback'], $css );
932 }
933
934 return $args;
935 }
936
937 /**
938 * Filter to handle the processing of preprocessed css on save.
939 *
940 * @param array $args Custom CSS options.
941 *
942 * @return mixed
943 */
944 public static function update_custom_css_data( $args ) {
945 // Find the current preprocessor.
946 $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
947 if ( empty( $jetpack_custom_css['preprocessor'] ) ) {
948 return $args;
949 }
950
951 /** This filter is documented in modules/custom-css/custom-css.php */
952 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
953 $preprocessor = $jetpack_custom_css['preprocessor'];
954
955 // If we have a preprocessor specified ...
956 if ( isset( $preprocessors[ $preprocessor ] ) ) {
957 // And no other preprocessor has run ...
958 if ( empty( $args['preprocessed'] ) ) {
959 $args['preprocessed'] = $args['css'];
960 $args['css'] = call_user_func( $preprocessors[ $preprocessor ]['callback'], $args['css'] );
961 } else {
962 trigger_error( 'Jetpack CSS Preprocessor specified, but something else has already modified the argument.', E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
963 }
964 }
965
966 return $args;
967 }
968
969 /**
970 * When on the edit screen, make sure the custom content width
971 * setting is applied to the large image size.
972 *
973 * @param array $dims Array of image dimensions (width and height).
974 * @param string $size Size of the resulting image.
975 * @param null $context Context the image is being resized for. `edit` or `display`.
976 *
977 * @return array
978 */
979 public static function editor_max_image_size( $dims, $size = 'medium', $context = null ) {
980 list( $width, $height ) = $dims;
981
982 if ( 'large' === $size && 'edit' === $context ) {
983 $width = Jetpack::get_content_width();
984 }
985
986 return array( $width, $height );
987 }
988
989 /**
990 * Override the content_width with a custom value if one is set.
991 *
992 * @param int $content_width Content Width value to be updated.
993 *
994 * @return int
995 */
996 public static function jetpack_content_width( $content_width ) {
997 $custom_content_width = 0;
998
999 $jetpack_custom_css = get_theme_mod( 'jetpack_custom_css', array() );
1000 if ( isset( $jetpack_custom_css['content_width'] ) ) {
1001 $custom_content_width = $jetpack_custom_css['content_width'];
1002 }
1003
1004 if ( $custom_content_width > 0 ) {
1005 return $custom_content_width;
1006 }
1007
1008 return $content_width;
1009 }
1010
1011 /**
1012 * Return whether the Start Fresh option of the CSS editor is enabled.
1013 *
1014 * @return boolean
1015 */
1016 public static function is_start_fresh_option_enabled() {
1017 if ( wp_is_block_theme() ) {
1018 return false;
1019 }
1020
1021 $start_fresh = null;
1022
1023 // $wp_customize is not available here. Let's get the value of the `replace` option from the last
1024 // customize_changeset posts.
1025 $posts = get_posts(
1026 array(
1027 'post_type' => 'customize_changeset',
1028 'post_status' => 'trash',
1029 'order_by' => 'post_modified',
1030 'order' => 'DESC',
1031 )
1032 );
1033
1034 // Bail as soon as we find a post that defines the `replace` option.
1035 foreach ( $posts as $post ) {
1036 $content = $post->post_content;
1037
1038 if ( empty( $content ) ) {
1039 continue;
1040 }
1041
1042 $parsed_content = json_decode( $content, true );
1043
1044 if ( empty( $parsed_content ) ) {
1045 continue;
1046 }
1047
1048 foreach ( $parsed_content as $key => $data ) {
1049 if ( str_ends_with( $key, '::jetpack_custom_css[replace]' ) ) {
1050 $start_fresh = $data['value'];
1051 break;
1052 }
1053 }
1054
1055 if ( isset( $start_fresh ) ) {
1056 break;
1057 }
1058 }
1059
1060 return $start_fresh;
1061 }
1062
1063 /**
1064 * Display a deprecation warning on the frontend for site admins only
1065 */
1066 public static function display_frontend_warning() {
1067 if ( ! current_user_can( 'edit_themes' ) || ! current_user_can( 'edit_theme_options' ) || ! self::is_start_fresh_option_enabled() ) {
1068 return;
1069 }
1070
1071 $notice = '';
1072 $notice .= '<style>';
1073 $notice .= '
1074 .jp-custom-css__deprecation-warning {
1075 position: absolute;
1076 top: 2.5rem;
1077 left: 50%;
1078 z-index: 99999;
1079 transform: translateX( -50% );
1080
1081 display: flex;
1082 align-items: flex-start;
1083 width: 80ch;
1084
1085 background-color: #fef8ee;
1086 border: solid 1px #ccc;
1087 border-left: solid 5px #f0b849;
1088 box-shadow: 0 0 4px 4px rgba( 0, 0, 0, 0.1 );
1089
1090 font-size: 1rem;
1091 }
1092
1093 .jp-custom-css__deprecation-warning p {
1094 margin: 0;
1095 padding: 1em;
1096 }
1097
1098 .jp-custom-css__deprecation-warning button {
1099 min-width: 48px;
1100 min-height: 48px;
1101
1102 border-color: transparent;
1103 background-color: transparent;
1104
1105 font-size: 1.25rem;
1106 font-weight: bold;
1107 }
1108 ';
1109 $notice .= '</style>';
1110 $notice .= '<div class="jp-custom-css__deprecation-warning">';
1111 $notice .= '<p>' . wp_kses(
1112 sprintf(
1113 // translators: 1: URL to the CSS customization panel, 2: URL to the theme stylesheet documentation.
1114 __(
1115 'The <i>Start Fresh</i> option in the <a href="%1$s">CSS customization panel</a> is enabled, which means the theme\'s original CSS is not applied. <b>This option will no longer be supported after August 6, 2024.</b> <a href="%2$s">See how to keep your site intact.</a>',
1116 'jetpack'
1117 ),
1118 esc_url( admin_url( 'customize.php?autofocus%5Bsection%5D=custom_css' ) ),
1119 esc_url( Redirect::get_url( 'jetpack-support-custom-css' ) )
1120 ),
1121 array(
1122 'i' => array(),
1123 'b' => array(),
1124 'a' => array(
1125 'href' => array(),
1126 'target' => array(),
1127 ),
1128 )
1129 ) . '</p>';
1130 $notice .= '<button aria-label="' . esc_html__( 'Dismiss', 'jetpack' ) . '">&times;</button>';
1131 $notice .= '</div>';
1132 $notice .= '<script>';
1133 $notice .= 'document.querySelector(".jp-custom-css__deprecation-warning button").addEventListener("click", (e) => e.currentTarget.parentNode.remove() );';
1134 $notice .= '</script>';
1135
1136 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1137 echo $notice;
1138 }
1139
1140 /**
1141 * Update the initial state to include the `replace` option (Start Fresh) in the module data.
1142 */
1143 public static function update_initial_state() {
1144 if ( 'toplevel_page_jetpack' !== get_current_screen()->base ) {
1145 return;
1146 }
1147
1148 $val = self::is_start_fresh_option_enabled() ? 'true' : 'false';
1149
1150 wp_add_inline_script(
1151 'react-plugin',
1152 "
1153 try {
1154 var options = window.Initial_State?.getModules?.['custom-css']?.options || {};
1155 options.replace = $val;
1156 } catch (e) {}
1157 ",
1158 'after'
1159 );
1160 }
1161
1162 /**
1163 * Currently this filter function gets called on
1164 * 'template_redirect' action and
1165 * 'admin_init' action
1166 */
1167 public static function set_content_width() {
1168 // Don't apply this filter on the Edit CSS page.
1169 if ( isset( $_GET['page'] ) && 'editcss' === $_GET['page'] && is_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nothing is changed on the site.
1170 return;
1171 }
1172
1173 $GLOBALS['content_width'] = Jetpack::get_content_width();
1174 }
1175
1176 /**
1177 * Make sure the preprocessor we're saving is one we know about.
1178 *
1179 * @param string $preprocessor The preprocessor to sanitize.
1180 *
1181 * @return null|string
1182 */
1183 public static function sanitize_preprocessor( $preprocessor ) {
1184 /** This filter is documented in modules/custom-css/custom-css.php */
1185 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
1186 if ( empty( $preprocessor ) || array_key_exists( $preprocessor, $preprocessors ) ) {
1187 return $preprocessor;
1188 }
1189 return null;
1190 }
1191
1192 /**
1193 * Get the base10 intval.
1194 *
1195 * This is used as a setting's sanitize_callback; we can't use just plain
1196 * intval because the second argument is not what intval() expects.
1197 *
1198 * @access public
1199 *
1200 * @param mixed $value Number to convert.
1201 * @return int Integer.
1202 */
1203 public static function intval_base10( $value ) {
1204 return (int) $value;
1205 }
1206
1207 /**
1208 * Add a footer action on revision.php to print some customizations for the theme switcher.
1209 */
1210 public static function load_revision_php() {
1211 add_action( 'admin_footer', array( __CLASS__, 'revision_admin_footer' ) );
1212 }
1213
1214 /**
1215 * Print the theme switcher on revision.php and move it into place.
1216 */
1217 public static function revision_admin_footer() {
1218 $post = get_post();
1219 if ( 'custom_css' !== $post->post_type ) {
1220 return;
1221 }
1222 $stylesheet = $post->post_title;
1223 ?>
1224 <script type="text/html" id="tmpl-other-themes-switcher">
1225 <?php self::revisions_switcher_box( $stylesheet ); ?>
1226 </script>
1227 <style>
1228 .other-themes-wrap {
1229 float: right;
1230 background-color: #fff;
1231 -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1232 box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1233 padding: 5px 10px;
1234 margin-bottom: 10px;
1235 }
1236 .other-themes-wrap label {
1237 display: block;
1238 margin-bottom: 10px;
1239 }
1240 .other-themes-wrap select {
1241 float: left;
1242 width: 77%;
1243 }
1244 .other-themes-wrap button {
1245 float: right;
1246 width: 20%;
1247 }
1248 .revisions {
1249 clear: both;
1250 }
1251 /* Hide the back-to-post link */
1252 .long-header + a {
1253 display: none;
1254 }
1255 </style>
1256 <script>
1257 (function($){
1258 var switcher = $('#tmpl-other-themes-switcher').html(),
1259 qty = $( switcher ).find('select option').length,
1260 $switcher;
1261
1262 if ( qty >= 3 ) {
1263 $('h1.long-header').before( switcher );
1264 $switcher = $('.other-themes-wrap');
1265 $switcher.find('button').on('click', function(e){
1266 e.preventDefault();
1267 if ( $switcher.find('select').val() ) {
1268 window.location.href = $switcher.find('select').val();
1269 }
1270 })
1271 }
1272 })(jQuery);
1273 </script>
1274 <?php
1275 }
1276
1277 /**
1278 * The HTML for the theme revision switcher box.
1279 *
1280 * @param string $stylesheet Stylesheet name.
1281 */
1282 public static function revisions_switcher_box( $stylesheet = '' ) {
1283 $themes = self::get_all_themes_with_custom_css();
1284 ?>
1285 <div class="other-themes-wrap">
1286 <label for="other-themes"><?php esc_html_e( 'Select another theme to view its custom CSS.', 'jetpack' ); ?></label>
1287 <select id="other-themes">
1288 <option value=""><?php esc_html_e( 'Select a theme&hellip;', 'jetpack' ); ?></option>
1289 <?php
1290 foreach ( $themes as $theme_stylesheet => $data ) {
1291 $revisions = wp_get_post_revisions( $data['post']->ID, array( 'posts_per_page' => 1 ) );
1292 if ( ! $revisions ) {
1293 ?>
1294 <option value="<?php echo esc_url( add_query_arg( 'id', $data['post']->ID, menu_page_url( 'editcss', 0 ) ) ); ?>" <?php disabled( $stylesheet, $theme_stylesheet ); ?>>
1295 <?php echo esc_html( $data['label'] ); ?>
1296 <?php
1297 // translators: how long ago the stylesheet was modified.
1298 printf( esc_html__( '(modified %s ago)', 'jetpack' ), human_time_diff( strtotime( $data['post']->post_modified_gmt ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1299 ?>
1300 </option>
1301 <?php
1302 continue;
1303 }
1304 $revision = array_shift( $revisions );
1305 ?>
1306 <option value="<?php echo esc_url( get_edit_post_link( $revision->ID ) ); ?>" <?php disabled( $stylesheet, $theme_stylesheet ); ?>>
1307 <?php echo esc_html( $data['label'] ); ?>
1308 <?php
1309 // translators: how long ago the stylesheet was modified.
1310 printf( esc_html__( '(modified %s ago)', 'jetpack' ), human_time_diff( strtotime( $data['post']->post_modified_gmt ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1311 ?>
1312 </option>
1313 <?php
1314 }
1315 ?>
1316 </select>
1317 <button class="button" id="other_theme_custom_css_switcher"><?php esc_html_e( 'Switch', 'jetpack' ); ?></button>
1318 </div>
1319 <?php
1320 }
1321 }
1322 }
1323
1324 Jetpack_Custom_CSS_Enhancements::add_hooks();
1325
1326 if ( ! function_exists( 'safecss_class' ) ) :
1327 /**
1328 * Load in the class only when needed. Makes lighter load by having one less class in memory.
1329 */
1330 function safecss_class() {
1331 // Wrapped so we don't need the parent class just to load the plugin.
1332 if ( class_exists( 'safecss' ) ) {
1333 return;
1334 }
1335
1336 require_once __DIR__ . '/csstidy/class.csstidy.php';
1337
1338 /**
1339 * Class safecss
1340 */
1341 class safecss extends csstidy_optimise { // phpcs:ignore
1342
1343 /**
1344 * Optimises $css after parsing.
1345 */
1346 public function postparse() { // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
1347
1348 /** This action is documented in modules/custom-css/custom-css.php */
1349 do_action( 'csstidy_optimize_postparse', $this );
1350
1351 return parent::postparse();
1352 }
1353
1354 /**
1355 * Optimises a sub-value.
1356 */
1357 public function subvalue() { // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
1358
1359 /** This action is documented in modules/custom-css/custom-css.php */
1360 do_action( 'csstidy_optimize_subvalue', $this );
1361
1362 return parent::subvalue();
1363 }
1364 }
1365 }
1366 endif;