PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 11.4.1
Jetpack – WP Security, Backup, Speed, & Growth v11.4.1
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 11.4.1, at modules/custom-css/custom-css.php

1,955 lines 59.4 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 use Automattic\Jetpack\Assets;
4 use Automattic\Jetpack\Device_Detection\User_Agent_Info;
5 use Automattic\Jetpack\Redirect;
6
7 /**
8 * Custom CSS class.
9 */
10 class Jetpack_Custom_CSS {
11
12 /**
13 * Initialize the class.
14 */
15 public static function init() {
16 add_action( 'switch_theme', array( __CLASS__, 'reset' ) );
17 add_action( 'wp_restore_post_revision', array( __CLASS__, 'restore_revision' ), 10, 2 );
18
19 // Save revisions for posts of type safecss.
20 add_action( 'load-revision.php', array( __CLASS__, 'add_revision_redirect' ) );
21
22 // Override the edit link, the default link causes a redirect loop
23 add_filter( 'get_edit_post_link', array( __CLASS__, 'revision_post_link' ), 10, 3 );
24
25 // Overwrite the content width global variable if one is set in the custom css
26 add_action( 'template_redirect', array( __CLASS__, 'set_content_width' ) );
27 add_action( 'admin_init', array( __CLASS__, 'set_content_width' ) );
28
29 if ( ! is_admin() ) {
30 add_filter( 'stylesheet_uri', array( __CLASS__, 'style_filter' ) );
31 }
32
33 define(
34 'SAFECSS_USE_ACE',
35 ! jetpack_is_mobile() &&
36 ! User_Agent_Info::is_ipad() &&
37 /**
38 * Should the Custom CSS module use ACE to process CSS.
39 *
40 * @see https://ace.c9.io/
41 *
42 * @module custom-css
43 *
44 * @since 1.7.0
45 *
46 * @param bool true Use ACE to process the Custom CSS. Default to true.
47 */
48 apply_filters( 'safecss_use_ace', true )
49 );
50
51 // Register safecss as a custom post_type
52 // Explicit capability definitions are largely unnecessary because the posts are manipulated in code via an options page, managing CSS revisions does check the capabilities, so let's ensure that the proper caps are checked.
53 register_post_type(
54 'safecss',
55 array(
56 /**
57 * These are the defaults
58 * 'exclude_from_search' => true,
59 * 'public' => false,
60 * 'publicly_queryable' => false,
61 * 'show_ui' => false,
62 */
63 'supports' => array( 'revisions' ),
64 'label' => 'Custom CSS',
65 'can_export' => false,
66 'rewrite' => false,
67 'capabilities' => array(
68 'edit_post' => 'edit_theme_options',
69 'read_post' => 'read',
70 'delete_post' => 'edit_theme_options',
71 'edit_posts' => 'edit_theme_options',
72 'edit_others_posts' => 'edit_theme_options',
73 'publish_posts' => 'edit_theme_options',
74 'read_private_posts' => 'read',
75 ),
76 )
77 );
78
79 // Short-circuit WP if this is a CSS stylesheet request
80 if ( isset( $_GET['custom-css'] ) ) {
81 header( 'Content-Type: text/css', true, 200 );
82 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + 31536000 ) . ' GMT' ); // 1 year
83 self::print_css();
84 exit;
85 }
86
87 add_action( 'admin_enqueue_scripts', array( 'Jetpack_Custom_CSS', 'enqueue_scripts' ) );
88
89 if ( isset( $_GET['page'] ) && 'editcss' === $_GET['page'] && is_admin() ) {
90 // Do migration routine if necessary
91 self::upgrade();
92
93 /**
94 * Allows additional work when migrating safecss from wp_options to wp_post.
95 *
96 * @module custom-css
97 *
98 * @since 1.7.0
99 */
100 do_action( 'safecss_migrate_post' );
101 }
102
103 /**
104 * Never embed the style in the head on wpcom.
105 * Yes, this filter should be added to an unsynced file on wpcom, but
106 * there is no good syntactically-correct location to put it yet.
107 *
108 * @link https://github.com/Automattic/jetpack/commit/a1be114e9179f64d147124727a58e2cf76c7e5a1#commitcomment-7763921
109 */
110 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
111 add_filter( 'safecss_embed_style', '__return_false' );
112 } else {
113 add_filter( 'safecss_embed_style', array( 'Jetpack_Custom_CSS', 'should_we_inline_custom_css' ), 10, 2 );
114 }
115
116 add_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 );
117
118 add_filter( 'jetpack_content_width', array( 'Jetpack_Custom_CSS', 'jetpack_content_width' ) );
119 add_filter( 'editor_max_image_size', array( 'Jetpack_Custom_CSS', 'editor_max_image_size' ), 10, 3 );
120
121 if ( ! current_user_can( 'switch_themes' ) && ! is_super_admin() ) {
122 return;
123 }
124
125 add_action( 'admin_menu', array( 'Jetpack_Custom_CSS', 'menu' ) );
126
127 if ( isset( $_POST['safecss'] ) && ( ! isset( $_SERVER['REQUEST_URI'] ) || false === strstr( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'options.php' ) ) ) {
128 check_admin_referer( 'safecss' );
129
130 $save_result = self::save(
131 array(
132 'css' => filter_var( wp_unslash( $_POST['safecss'] ) ),
133 'is_preview' => isset( $_POST['action'] ) && $_POST['action'] === 'preview',
134 'preprocessor' => isset( $_POST['custom_css_preprocessor'] ) ? sanitize_key( $_POST['custom_css_preprocessor'] ) : '',
135 'add_to_existing' => isset( $_POST['add_to_existing'] ) ? $_POST['add_to_existing'] === 'true' : true,
136 'content_width' => isset( $_POST['custom_content_width'] ) ? intval( $_POST['custom_content_width'] ) : false,
137 )
138 );
139
140 if ( $_POST['action'] === 'preview' ) {
141 wp_safe_redirect( add_query_arg( 'csspreview', 'true', get_option( 'home' ) ) );
142 exit;
143 }
144
145 if ( $save_result ) {
146 add_action( 'admin_notices', array( 'Jetpack_Custom_CSS', 'saved_message' ) );
147 }
148 }
149
150 // Modify all internal links so that preview state persists
151 if ( self::is_preview() ) {
152 ob_start( array( 'Jetpack_Custom_CSS', 'buffer' ) );
153 }
154 }
155
156 /**
157 * Save new custom CSS. This should be the entry point for any third-party code using Jetpack_Custom_CSS
158 * to save CSS.
159 *
160 * @param array $args Array of arguments:
161 * string $css The CSS (or LESS or Sass)
162 * bool $is_preview Whether this CSS is preview or published
163 * string preprocessor Which CSS preprocessor to use
164 * bool $add_to_existing Whether this CSS replaces the theme's CSS or supplements it.
165 * int $content_width A custom $content_width to go along with this CSS.
166 * @return int The post ID of the saved Custom CSS post.
167 */
168 public static function save( $args = array() ) {
169 $defaults = array(
170 'css' => '',
171 'is_preview' => false,
172 'preprocessor' => '',
173 'add_to_existing' => true,
174 'content_width' => false,
175 );
176
177 $args = wp_parse_args( $args, $defaults );
178
179 if (
180 $args['content_width']
181 && (int) $args['content_width'] > 0
182 && (
183 ! isset( $GLOBALS['content_width'] )
184 || $args['content_width'] !== $GLOBALS['content_width']
185 )
186 ) {
187 $args['content_width'] = (int) $args['content_width'];
188 } else {
189 $args['content_width'] = false;
190 }
191
192 /**
193 * Fires prior to saving custom css values. Necessitated because the
194 * core WordPress save_pre filters were removed:
195 * - content_save_pre
196 * - content_filtered_save_pre
197 *
198 * @module custom-css
199 *
200 * @since 1.7.0
201 *
202 * @param array $args {
203 * Array of custom CSS arguments.
204 * @type string $css The CSS (or LESS or Sass).
205 * @type bool $is_preview Whether this CSS is preview or published.
206 * @type string preprocessor Which CSS preprocessor to use.
207 * @type bool $add_to_existing Whether this CSS replaces the theme's CSS or supplements it.
208 * @type int $content_width A custom $content_width to go along with this CSS.
209 * }
210 */
211 do_action( 'safecss_save_pre', $args );
212
213 $warnings = array();
214
215 safecss_class();
216 $csstidy = new csstidy();
217 $csstidy->optimise = new safecss( $csstidy );
218
219 $csstidy->set_cfg( 'remove_bslash', false );
220 $csstidy->set_cfg( 'compress_colors', false );
221 $csstidy->set_cfg( 'compress_font-weight', false );
222 $csstidy->set_cfg( 'optimise_shorthands', 0 );
223 $csstidy->set_cfg( 'remove_last_;', false );
224 $csstidy->set_cfg( 'case_properties', false );
225 $csstidy->set_cfg( 'discard_invalid_properties', true );
226 $csstidy->set_cfg( 'css_level', 'CSS3.0' );
227 $csstidy->set_cfg( 'preserve_css', true );
228 $csstidy->set_cfg( 'template', __DIR__ . '/csstidy/wordpress-standard.tpl' );
229
230 $prev = $args['css'];
231 $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $prev );
232 // prevent content: '\3434' from turning into '\\3434'
233 $css = str_replace( array( '\'\\\\', '"\\\\' ), array( '\'\\', '"\\' ), $css );
234
235 if ( $css !== $prev ) {
236 $warnings[] = 'preg_replace found stuff';
237 }
238
239 // Some people put weird stuff in their CSS, KSES tends to be greedy
240 $css = str_replace( '<=', '&lt;=', $css );
241 // Why KSES instead of strip_tags? Who knows?
242 $prev = $css;
243 $css = wp_kses_split( $prev, array(), array() );
244 $css = str_replace( '&gt;', '>', $css ); // kses replaces lone '>' with &gt;
245 // Why both KSES and strip_tags? Because we just added some '>'.
246 $css = strip_tags( $css ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags
247
248 if ( $css !== $prev ) {
249 $warnings[] = 'kses found stuff';
250 }
251
252 // if we're not using a preprocessor
253 if ( ! $args['preprocessor'] ) {
254
255 /**
256 * Fires before parsing the css with CSSTidy, but only if
257 * the preprocessor is not configured for use.
258 *
259 * @module custom-css
260 *
261 * @since 1.7.0
262 *
263 * @param obj $csstidy The csstidy object.
264 * @param string $css Custom CSS.
265 * @param array $args Array of custom CSS arguments.
266 */
267 do_action( 'safecss_parse_pre', $csstidy, $css, $args );
268
269 $csstidy->parse( $css );
270
271 /**
272 * Fires after parsing the css with CSSTidy, but only if
273 * the preprocessor is not cinfigured for use.
274 *
275 * @module custom-css
276 *
277 * @since 1.7.0
278 *
279 * @param obj $csstidy The csstidy object.
280 * @param array $warnings Array of warnings.
281 * @param array $args Array of custom CSS arguments.
282 */
283 do_action( 'safecss_parse_post', $csstidy, $warnings, $args );
284
285 $css = $csstidy->print->plain();
286 }
287
288 if ( $args['add_to_existing'] ) {
289 $add_to_existing = 'yes';
290 } else {
291 $add_to_existing = 'no';
292 }
293
294 if ( $args['is_preview'] || self::is_freetrial() ) {
295 // Save the CSS
296 $safecss_revision_id = self::save_revision( $css, true, $args['preprocessor'] );
297
298 // Cache Buster
299 update_option( 'safecss_preview_rev', (int) get_option( 'safecss_preview_rev' ) + 1 );
300
301 update_metadata( 'post', $safecss_revision_id, 'custom_css_add', $add_to_existing );
302 update_metadata( 'post', $safecss_revision_id, 'content_width', $args['content_width'] );
303 update_metadata( 'post', $safecss_revision_id, 'custom_css_preprocessor', $args['preprocessor'] );
304
305 delete_option( 'safecss_add' );
306 delete_option( 'safecss_content_width' );
307
308 if ( $args['is_preview'] ) {
309 return $safecss_revision_id;
310 }
311
312 /**
313 * Fires after saving Custom CSS.
314 *
315 * @module custom-css
316 *
317 * @since 1.7.0
318 */
319 do_action( 'safecss_save_preview_post' );
320 }
321
322 // Save the CSS
323 $safecss_post_id = self::save_revision( $css, false, $args['preprocessor'] );
324
325 $safecss_post_revision = self::get_current_revision();
326
327 update_option( 'safecss_rev', (int) get_option( 'safecss_rev' ) + 1 );
328
329 update_post_meta( $safecss_post_id, 'custom_css_add', $add_to_existing );
330 update_post_meta( $safecss_post_id, 'content_width', $args['content_width'] );
331 update_post_meta( $safecss_post_id, 'custom_css_preprocessor', $args['preprocessor'] );
332
333 delete_option( 'safecss_add' );
334 delete_option( 'safecss_content_width' );
335
336 update_metadata( 'post', $safecss_post_revision['ID'], 'custom_css_add', $add_to_existing );
337 update_metadata( 'post', $safecss_post_revision['ID'], 'content_width', $args['content_width'] );
338 update_metadata( 'post', $safecss_post_revision['ID'], 'custom_css_preprocessor', $args['preprocessor'] );
339
340 delete_option( 'safecss_preview_add' );
341
342 return $safecss_post_id;
343 }
344
345 /**
346 * Get the published custom CSS post.
347 *
348 * @return array
349 */
350 public static function get_post() {
351 $custom_css_post_id = self::post_id();
352
353 if ( $custom_css_post_id ) {
354 return get_post( $custom_css_post_id, ARRAY_A );
355 }
356
357 return array();
358 }
359
360 /**
361 * Get the post ID of the published custom CSS post.
362 *
363 * @return int|bool The post ID if it exists; false otherwise.
364 */
365 public static function post_id() {
366 /**
367 * Filter the ID of the post where Custom CSS is stored, before the ID is retrieved.
368 *
369 * If the callback function returns a non-null value, then post_id() will immediately
370 * return that value, instead of retrieving the normal post ID.
371 *
372 * @module custom-css
373 *
374 * @since 3.8.1
375 *
376 * @param null null The ID to return instead of the normal ID.
377 */
378 $custom_css_post_id = apply_filters( 'jetpack_custom_css_pre_post_id', null );
379 if ( $custom_css_post_id !== null ) {
380 return $custom_css_post_id;
381 }
382
383 $custom_css_post_id = wp_cache_get( 'custom_css_post_id' );
384
385 if ( false === $custom_css_post_id ) {
386 $custom_css_posts = get_posts(
387 array(
388 'posts_per_page' => 1,
389 'post_type' => 'safecss',
390 'post_status' => 'publish',
391 'orderby' => 'date',
392 'order' => 'DESC',
393 )
394 );
395
396 if ( count( $custom_css_posts ) > 0 ) {
397 $custom_css_post_id = $custom_css_posts[0]->ID;
398 } else {
399 $custom_css_post_id = 0;
400 }
401
402 // Save post_id=0 to note that no safecss post exists.
403 wp_cache_set( 'custom_css_post_id', $custom_css_post_id );
404 }
405
406 if ( ! $custom_css_post_id ) {
407 return false;
408 }
409
410 return $custom_css_post_id;
411 }
412
413 /**
414 * Get the current revision of the original safecss record
415 *
416 * @return object
417 */
418 public static function get_current_revision() {
419 $safecss_post = self::get_post();
420
421 if ( empty( $safecss_post ) ) {
422 return false;
423 }
424
425 $revisions = wp_get_post_revisions(
426 $safecss_post['ID'],
427 array(
428 'posts_per_page' => 1,
429 'orderby' => 'date',
430 'order' => 'DESC',
431 )
432 );
433
434 // Empty array if no revisions exist
435 if ( empty( $revisions ) ) {
436 // Return original post
437 return $safecss_post;
438 } else {
439 // Return the first entry in $revisions, this will be the current revision
440 $current_revision = get_object_vars( array_shift( $revisions ) );
441 return $current_revision;
442 }
443 }
444
445 /**
446 * Save new revision of CSS
447 * Checks to see if content was modified before really saving
448 *
449 * @param string $css - the CSS.
450 * @param bool $is_preview - if we're in preview mode.
451 * @param string $preprocessor - what preprocessor we're using.
452 *
453 * @return bool|int If nothing was saved, returns false. If a post
454 * or revision was saved, returns the post ID.
455 */
456 public static function save_revision( $css, $is_preview = false, $preprocessor = '' ) {
457 $safecss_post = self::get_post();
458
459 $compressed_css = self::minify( $css, $preprocessor );
460
461 // If null, there was no original safecss record, so create one
462 if ( ! $safecss_post ) {
463 if ( ! $css ) {
464 return false;
465 }
466
467 $post = array();
468 $post['post_content'] = wp_slash( $css );
469 $post['post_title'] = 'safecss';
470 $post['post_status'] = 'publish';
471 $post['post_type'] = 'safecss';
472 $post['post_content_filtered'] = wp_slash( $compressed_css );
473
474 // Set excerpt to current theme, for display in revisions list
475 $current_theme = wp_get_theme();
476 $post['post_excerpt'] = $current_theme->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
477
478 add_filter( 'wp_insert_post_data', array( __CLASS__, 'restore_unsafe_postcss_content' ), 9, 3 );
479 // Insert the CSS into wp_posts
480 $post_id = wp_insert_post( $post );
481 remove_filter( 'wp_insert_post_data', array( __CLASS__, 'restore_unsafe_postcss_content' ), 9 );
482 wp_cache_set( 'custom_css_post_id', $post_id );
483 return $post_id;
484 }
485
486 // Update CSS in post array with new value passed to this function
487 $safecss_post['post_content'] = $css;
488 $safecss_post['post_content_filtered'] = $compressed_css;
489
490 // Set excerpt to current theme, for display in revisions list
491 $current_theme = wp_get_theme();
492 $safecss_post['post_excerpt'] = $current_theme->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
493
494 // Don't carry over last revision's timestamps, otherwise revisions all have matching timestamps
495 unset( $safecss_post['post_date'] );
496 unset( $safecss_post['post_date_gmt'] );
497 unset( $safecss_post['post_modified'] );
498 unset( $safecss_post['post_modified_gmt'] );
499
500 // Do not update post if we are only saving a preview
501 if ( false === $is_preview ) {
502 $safecss_post['post_content'] = wp_slash( $safecss_post['post_content'] );
503 $safecss_post['post_content_filtered'] = wp_slash( $safecss_post['post_content_filtered'] );
504 add_filter( 'wp_insert_post_data', array( __CLASS__, 'restore_unsafe_postcss_content' ), 9, 3 );
505 $post_id = wp_update_post( $safecss_post );
506 remove_filter( 'wp_insert_post_data', array( __CLASS__, 'restore_unsafe_postcss_content' ), 9 );
507 wp_cache_set( 'custom_css_post_id', $post_id );
508 return $post_id;
509 } elseif ( ! defined( 'DOING_MIGRATE' ) ) {
510 add_filter( 'wp_insert_post_data', array( __CLASS__, 'restore_unsafe_postcss_content' ), 9, 3 );
511 $revision = _wp_put_post_revision( $safecss_post );
512 remove_filter( 'wp_insert_post_data', array( __CLASS__, 'restore_unsafe_postcss_content' ), 9 );
513 return $revision;
514 }
515 }
516
517 /**
518 * Restore Unsafe Post CSS Content.
519 *
520 * @param array $data The post data being filtered.
521 * @param array $postarray Unused.
522 * @param array $unsanitized The unsanitized post data.
523 *
524 * @return array Post data.
525 */
526 public static function restore_unsafe_postcss_content( $data, $postarray, $unsanitized ) {
527 $replace_content =
528 isset( $data['post_type'] ) &&
529 isset( $unsanitized['post_content'] ) &&
530 (
531 'safecss' === $data['post_type'] ||
532 (
533 'revision' === $data['post_type'] &&
534 ! empty( $data['post_parent'] ) &&
535 'safecss' === get_post_type( $data['post_parent'] )
536 )
537 );
538 if ( $replace_content ) {
539 $data['post_content'] = $unsanitized['post_content'];
540 }
541 return $data;
542 }
543
544 /**
545 * Prevent the stylesheet from being enqued.
546 *
547 * @return bool
548 */
549 public static function skip_stylesheet() {
550 /**
551 * Prevent the Custom CSS stylesheet from being enqueued.
552 *
553 * @module custom-css
554 *
555 * @since 2.2.1
556 *
557 * @param null Should the stylesheet be skipped. Default to null. Anything else will force the stylesheet to be skipped.
558 */
559 $skip_stylesheet = apply_filters( 'safecss_skip_stylesheet', null );
560
561 if ( null !== $skip_stylesheet ) {
562 return $skip_stylesheet;
563 } elseif ( self::is_customizer_preview() ) {
564 return false;
565 } else {
566 if ( self::is_preview() ) {
567 $safecss_post = self::get_current_revision();
568
569 if ( $safecss_post ) {
570 return (bool) ( get_post_meta( $safecss_post['ID'], 'custom_css_add', true ) === 'no' );
571 } else {
572 return (bool) ( get_option( 'safecss_preview_add' ) === 'no' );
573 }
574 } else {
575 $custom_css_post_id = self::post_id();
576
577 if ( $custom_css_post_id ) {
578 $custom_css_add = get_post_meta( $custom_css_post_id, 'custom_css_add', true );
579
580 // It is possible for the CSS to be stored in a post but for the safecss_add option
581 // to have not been upgraded yet if the user hasn't opened their Custom CSS editor
582 // since October 2012.
583 if ( ! empty( $custom_css_add ) ) {
584 return (bool) ( $custom_css_add === 'no' );
585 }
586 }
587
588 return (bool) ( Jetpack_Options::get_option_and_ensure_autoload( 'safecss_add', '' ) === 'no' );
589 }
590 }
591 }
592
593 /**
594 * Checks if we're in a preview mode.
595 *
596 * @return bool
597 */
598 public static function is_preview() {
599 return isset( $_GET['csspreview'] ) && $_GET['csspreview'] === 'true'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
600 }
601
602 /**
603 * Currently this filter function gets called on
604 * 'template_redirect' action and
605 * 'admin_init' action
606 */
607 public static function set_content_width() {
608 // Don't apply this filter on the Edit CSS page
609 if ( isset( $_GET ) && isset( $_GET['page'] ) && 'editcss' === $_GET['page'] && is_admin() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nothing changing on the site, it's not applying a filter if set.
610 return;
611 }
612
613 $GLOBALS['content_width'] = Jetpack::get_content_width();
614 }
615
616 /**
617 * False when the site has the Custom Design upgrade.
618 * Used only on WordPress.com.
619 *
620 * @return bool
621 * @todo see if we can remove this, I don't believe WordPress.com uses free trials anymore.
622 */
623 public static function is_freetrial() {
624 /**
625 * Determine if a WordPress.com site uses a Free trial of the Custom Design Upgrade.
626 * Used only on WordPress.com.
627 *
628 * @module custom-css
629 *
630 * @since 1.7.0
631 *
632 * @param bool false Does the site use a Free trial of the Custom Design Upgrade. Default to false.
633 */
634 return apply_filters( 'safecss_is_freetrial', false );
635 }
636
637 /**
638 * Get the preprocessor key.
639 *
640 * @return string|false
641 */
642 public static function get_preprocessor_key() {
643 $safecss_post = self::get_current_revision();
644 return get_post_meta( $safecss_post['ID'], 'custom_css_preprocessor', true );
645 }
646
647 /**
648 * Get the prepocessor.
649 *
650 * @return string|null
651 */
652 public static function get_preprocessor() {
653 /** This filter is documented in modules/custom-css/custom-css.php */
654 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
655 $selected_preprocessor_key = self::get_preprocessor_key();
656 $selected_preprocessor = isset( $preprocessors[ $selected_preprocessor_key ] ) ? $preprocessors[ $selected_preprocessor_key ] : null;
657 return $selected_preprocessor;
658 }
659
660 /**
661 * Get the CSS.
662 *
663 * @param boolean $compressed - if the CSS is compressed.
664 *
665 * @return string
666 */
667 public static function get_css( $compressed = false ) {
668 /**
669 * Filter the Custom CSS returned.
670 * Can be used to return an error, or no CSS at all.
671 *
672 * @module custom-css
673 *
674 * @since 1.7.0
675 *
676 * @param bool false Should we return an error instead of the Custom CSS. Default to false.
677 */
678 $default_css = apply_filters( 'safecss_get_css_error', false );
679
680 if ( $default_css !== false ) {
681 return $default_css;
682 }
683
684 $option = ( self::is_preview() || self::is_freetrial() ) ? 'safecss_preview' : 'safecss';
685 $css = '';
686
687 if ( 'safecss' === $option ) {
688 // Don't bother checking for a migrated 'safecss' option if it never existed.
689 if ( false === get_option( 'safecss' ) || get_option( 'safecss_revision_migrated' ) ) {
690 $safecss_post = self::get_post();
691 if ( ! empty( $safecss_post ) ) {
692 $css = ( $compressed && $safecss_post['post_content_filtered'] ) ? $safecss_post['post_content_filtered'] : $safecss_post['post_content'];
693 }
694 } else {
695 $current_revision = self::get_current_revision();
696 if ( false === $current_revision ) {
697 $css = '';
698 } else {
699 $css = ( $compressed && $current_revision['post_content_filtered'] ) ? $current_revision['post_content_filtered'] : $current_revision['post_content'];
700 }
701 }
702
703 // Fix for un-migrated Custom CSS
704 if ( empty( $safecss_post ) ) {
705 $_css = get_option( 'safecss' );
706 if ( ! empty( $_css ) ) {
707 $css = $_css;
708 }
709 }
710 } elseif ( 'safecss_preview' === $option ) {
711 $safecss_post = self::get_current_revision();
712 $css = $safecss_post['post_content'];
713 $css = self::minify( $css, get_post_meta( $safecss_post['ID'], 'custom_css_preprocessor', true ) );
714 }
715
716 $css = str_replace( array( '\\\00BB \\\0020', '\0BB \020', '0BB 020' ), '\00BB \0020', $css );
717
718 /**
719 * Filter the Custom CSS returned from the editor.
720 *
721 * @module custom-css
722 *
723 * @since 1.7.0
724 *
725 * @param string $css Custom CSS.
726 */
727 $css = apply_filters( 'safecss_css', $css );
728
729 return $css;
730 }
731
732 /**
733 * Replace insecure URLs.
734 *
735 * @param string $css - the CSS.
736 *
737 * @return string
738 */
739 public static function replace_insecure_urls( $css ) {
740 if ( ! function_exists( '_sa_get_frontend_https_url_replacement_map' ) ) {
741 return $css;
742 }
743 list( $http_urls, $secure_urls ) = _sa_get_frontend_https_url_replacement_map();
744
745 return str_replace( $http_urls, $secure_urls, $css );
746 }
747
748 /**
749 * Print the CSS.
750 */
751 public static function print_css() {
752
753 /**
754 * Fires right before printing the custom CSS inside the <head> element.
755 *
756 * @module custom-css
757 *
758 * @since 1.7.0
759 */
760 do_action( 'safecss_print_pre' );
761 $css = self::get_css( true );
762 echo self::replace_insecure_urls( $css ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
763 }
764
765 /**
766 * If the CSS is less than 2,000 characters, inline it! otherwise return what was passed in.
767 *
768 * @param bool $should_we if we should inline the CSS.
769 * @param string $css - the CSS object.
770 *
771 * @return bool
772 */
773 public static function should_we_inline_custom_css( $should_we, $css ) {
774 return ( strlen( $css ) < 2000 ) ? true : $should_we;
775 }
776
777 /**
778 * Add the link tag to inline CSS.
779 */
780 public static function link_tag() {
781 global $blog_id, $current_blog;
782
783 if (
784 /**
785 * Do not include any CSS on the page if the CSS includes an error.
786 * Setting this filter to true stops any Custom CSS from being enqueued.
787 *
788 * @module custom-css
789 *
790 * @since 1.7.0
791 *
792 * @param bool false Does the CSS include an error. Default to false.
793 */
794 apply_filters( 'safecss_style_error', false )
795 ) {
796 return;
797 }
798
799 if ( ! is_super_admin() && isset( $current_blog ) && ( $current_blog->spam || $current_blog->deleted ) ) {
800 return;
801 }
802
803 if ( self::is_customizer_preview() ) {
804 return;
805 }
806
807 $css = '';
808 $option = self::is_preview() ? 'safecss_preview' : 'safecss';
809
810 if ( 'safecss' === $option ) {
811 if ( Jetpack_Options::get_option_and_ensure_autoload( 'safecss_revision_migrated', '0' ) ) {
812 $safecss_post = self::get_post();
813
814 if ( ! empty( $safecss_post['post_content'] ) ) {
815 $css = $safecss_post['post_content'];
816 }
817 } else {
818 $current_revision = self::get_current_revision();
819
820 if ( ! empty( $current_revision['post_content'] ) ) {
821 $css = $current_revision['post_content'];
822 }
823 }
824
825 // Fix for un-migrated Custom CSS
826 if ( empty( $safecss_post ) ) {
827 $_css = Jetpack_Options::get_option_and_ensure_autoload( 'safecss', '' );
828 if ( ! empty( $_css ) ) {
829 $css = $_css;
830 }
831 }
832 }
833
834 if ( 'safecss_preview' === $option ) {
835 $safecss_post = self::get_current_revision();
836
837 if ( ! empty( $safecss_post['post_content'] ) ) {
838 $css = $safecss_post['post_content'];
839 }
840 }
841
842 $css = str_replace( array( '\\\00BB \\\0020', '\0BB \020', '0BB 020' ), '\00BB \0020', $css );
843
844 if ( $css === '' ) {
845 return;
846 }
847
848 if (
849 /**
850 * Allow inserting CSS inline instead of through a separate file.
851 *
852 * @module custom-css
853 *
854 * @since 3.4.0
855 *
856 * @param bool false Should the CSS be added inline instead of through a separate file. Default to false.
857 * @param string $css Custom CSS.
858 */
859 apply_filters( 'safecss_embed_style', false, $css )
860 ) {
861
862 echo "\r\n" . '<style id="custom-css-css">' . self::get_css( true ) . "</style>\r\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
863
864 } else {
865
866 $href = home_url( '/' );
867 $href = add_query_arg( 'custom-css', 1, $href );
868 $href = add_query_arg( 'csblog', $blog_id, $href );
869 $href = add_query_arg( 'cscache', 6, $href );
870 $href = add_query_arg( 'csrev', (int) get_option( $option . '_rev' ), $href );
871
872 /**
873 * Filter the Custom CSS link enqueued in the head.
874 *
875 * @module custom-css
876 *
877 * @since 1.7.0
878 *
879 * @param string $href Custom CSS link enqueued in the head.
880 * @param string $blog_id Blog ID.
881 */
882 $href = apply_filters( 'safecss_href', $href, $blog_id );
883
884 if ( self::is_preview() ) {
885 $href = add_query_arg( 'csspreview', 'true', $href );
886 }
887
888 ?>
889 <link rel="stylesheet" id="custom-css-css" type="text/css" href="<?php echo esc_url( $href ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet ?>" />
890 <?php
891
892 }
893
894 /**
895 * Fires after creating the <link> in the <head> element for the custom css stylesheet.
896 *
897 * @module custom-css
898 *
899 * @since 2.2.2
900 */
901 do_action( 'safecss_link_tag_post' );
902 }
903
904 /**
905 * Filter the default blank Custom CSS URL.
906 *
907 * @param string $current - the current CSS.
908 *
909 * @return string
910 */
911 public static function style_filter( $current ) {
912 if ( self::is_freetrial() && ( ! self::is_preview() || ! current_user_can( 'switch_themes' ) ) ) {
913 return $current;
914 } elseif ( self::skip_stylesheet() ) {
915 /**
916 * Filter the default blank Custom CSS URL.
917 *
918 * @module custom-css
919 *
920 * @since 2.2.1
921 *
922 * @param string $url Default blank Custom CSS URL.
923 */
924 return apply_filters( 'safecss_style_filter_url', plugins_url( 'custom-css/css/blank.css', __FILE__ ) );
925 }
926
927 return $current;
928 }
929
930 /**
931 * Buffer the HTML.
932 *
933 * @param string $html - the HTML.
934 *
935 * @return string
936 */
937 public static function buffer( $html ) {
938 $html = str_replace( '</body>', self::preview_flag(), $html );
939 return preg_replace_callback( '!href=([\'"])(.*?)\\1!', array( 'Jetpack_Custom_CSS', 'preview_links' ), $html );
940 }
941
942 /**
943 * Preview links.
944 *
945 * @param array $matches - the matches.
946 *
947 * @return string
948 */
949 public static function preview_links( $matches ) {
950 if ( 0 !== strpos( $matches[2], get_option( 'home' ) ) ) {
951 return $matches[0];
952 }
953
954 $link = wp_specialchars_decode( $matches[2] );
955 $link = add_query_arg( 'csspreview', 'true', $link );
956 $link = esc_url( $link );
957 return "href={$matches[1]}$link{$matches[1]}";
958 }
959
960 /**
961 * Places a black bar above every preview page
962 */
963 public static function preview_flag() {
964 if ( is_admin() ) {
965 return;
966 }
967
968 $message = esc_html__( 'Preview: changes must be saved or they will be lost', 'jetpack' );
969 /**
970 * Filter the Preview message displayed on the site when previewing custom CSS, before to save it.
971 *
972 * @module custom-css
973 *
974 * @since 1.7.0
975 *
976 * @param string $message Custom CSS preview message.
977 */
978 $message = apply_filters( 'safecss_preview_message', $message );
979
980 $preview_flag_js = "var flag = document.createElement('div');
981 flag.innerHTML = " . wp_json_encode( $message ) . ";
982 flag.style.background = '#FF6600';
983 flag.style.color = 'white';
984 flag.style.textAlign = 'center';
985 flag.style.fontSize = '15px';
986 flag.style.padding = '2px';
987 flag.style.fontFamily = 'sans-serif';
988 document.body.style.paddingTop = '0px';
989 document.body.insertBefore(flag, document.body.childNodes[0]);
990 ";
991
992 /**
993 * Filter the Custom CSS preview message JS styling.
994 *
995 * @module custom-css
996 *
997 * @since 1.7.0
998 *
999 * @param string $preview_flag_js Custom CSS preview message JS styling.
1000 */
1001 $preview_flag_js = apply_filters( 'safecss_preview_flag_js', $preview_flag_js );
1002 if ( $preview_flag_js ) {
1003 $preview_flag_js = '<script type="text/javascript">
1004 // <![CDATA[
1005 ' . $preview_flag_js . '
1006 // ]]>
1007 </script>';
1008 }
1009
1010 return $preview_flag_js;
1011 }
1012
1013 /**
1014 * Add the additional CSS menu.
1015 */
1016 public static function menu() {
1017 $title = __( 'Additional CSS', 'jetpack' );
1018 $hook = add_theme_page( $title, $title, 'edit_theme_options', 'editcss', array( 'Jetpack_Custom_CSS', 'admin' ) );
1019
1020 add_action( 'load-revision.php', array( 'Jetpack_Custom_CSS', 'prettify_post_revisions' ) );
1021 add_action( "load-$hook", array( 'Jetpack_Custom_CSS', 'update_title' ) );
1022 }
1023
1024 /**
1025 * Adds a menu item in the appearance section for this plugin's administration
1026 * page. Also adds hooks to enqueue the CSS and JS for the admin page.
1027 */
1028 public static function update_title() {
1029 global $title;
1030 $title = __( 'CSS', 'jetpack' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1031 }
1032
1033 /**
1034 * Prettify the post revision.
1035 */
1036 public static function prettify_post_revisions() {
1037 add_filter( 'the_title', array( 'Jetpack_Custom_CSS', 'post_title' ), 10, 2 );
1038 }
1039
1040 /**
1041 * Get the post title.
1042 *
1043 * @param string $title - the post title.
1044 * @param int $post_id - the post ID.
1045 *
1046 * @return string
1047 */
1048 public static function post_title( $title, $post_id ) {
1049 $post_id = (int) $post_id;
1050 if ( ! $post_id ) {
1051 return $title;
1052 }
1053
1054 $post = get_post( $post_id );
1055 if ( ! $post ) {
1056 return $title;
1057 }
1058
1059 if ( 'safecss' !== $post->post_type ) {
1060 return $title;
1061 }
1062
1063 return __( 'Custom CSS Stylesheet', 'jetpack' );
1064 }
1065
1066 /**
1067 * Enqueue scripts.
1068 *
1069 * @param string $hook - the hook.
1070 */
1071 public static function enqueue_scripts( $hook ) {
1072 if ( 'appearance_page_editcss' !== $hook ) {
1073 return;
1074 }
1075
1076 wp_enqueue_script( 'postbox' );
1077 wp_enqueue_script(
1078 'custom-css-editor',
1079 Assets::get_file_url_for_environment(
1080 '_inc/build/custom-css/custom-css/js/css-editor.min.js',
1081 'modules/custom-css/custom-css/js/css-editor.js'
1082 ),
1083 'jquery',
1084 '20130325',
1085 true
1086 );
1087 wp_enqueue_style( 'custom-css-editor', plugins_url( 'custom-css/css/css-editor.css', __FILE__ ) ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
1088
1089 if ( defined( 'SAFECSS_USE_ACE' ) && SAFECSS_USE_ACE ) {
1090 wp_register_style( 'jetpack-css-codemirror', plugins_url( 'custom-css/css/codemirror.css', __FILE__ ), array(), '20120905' );
1091 wp_enqueue_style( 'jetpack-css-use-codemirror', plugins_url( 'custom-css/css/use-codemirror.css', __FILE__ ), array( 'jetpack-css-codemirror' ), '20120905' );
1092
1093 wp_register_script( 'jetpack-css-codemirror', plugins_url( 'custom-css/js/codemirror.min.js', __FILE__ ), array(), '3.16', true );
1094 wp_enqueue_script(
1095 'jetpack-css-use-codemirror',
1096 Assets::get_file_url_for_environment(
1097 '_inc/build/custom-css/custom-css/js/use-codemirror.min.js',
1098 'modules/custom-css/custom-css/js/use-codemirror.js'
1099 ),
1100 array( 'jquery', 'underscore', 'jetpack-css-codemirror' ),
1101 '20131009',
1102 true
1103 );
1104 }
1105 }
1106
1107 /**
1108 * Render the saved message.
1109 */
1110 public static function saved_message() {
1111 echo '<div id="message" class="updated fade"><p><strong>' . esc_html__( 'Stylesheet saved.', 'jetpack' ) . '</strong></p></div>';
1112 }
1113
1114 /**
1115 * Render the admin page.
1116 */
1117 public static function admin() {
1118 add_meta_box( 'submitdiv', __( 'Publish', 'jetpack' ), array( __CLASS__, 'publish_box' ), 'editcss', 'side' );
1119 add_action( 'custom_css_submitbox_misc_actions', array( __CLASS__, 'content_width_settings' ) );
1120
1121 $safecss_post = self::get_post();
1122
1123 if ( ! empty( $safecss_post ) && 0 < $safecss_post['ID'] && wp_get_post_revisions( $safecss_post['ID'], array( 'posts_per_page' => 1 ) ) ) {
1124 add_meta_box( 'revisionsdiv', __( 'CSS Revisions', 'jetpack' ), array( __CLASS__, 'revisions_meta_box' ), 'editcss', 'side' );
1125 }
1126 ?>
1127 <div class="wrap">
1128 <?php
1129
1130 /**
1131 * Fires right before the custom css page begins.
1132 *
1133 * @module custom-css
1134 *
1135 * @since 1.7.0
1136 */
1137 do_action( 'custom_design_header' );
1138
1139 ?>
1140 <h1><?php esc_html_e( 'CSS Stylesheet Editor', 'jetpack' ); ?></h1>
1141 <form id="safecssform" action="" method="post">
1142 <?php wp_nonce_field( 'safecss' ); ?>
1143 <?php wp_nonce_field( 'meta-box-order', 'meta-box-order-nonce', false ); ?>
1144 <?php wp_nonce_field( 'closedpostboxes', 'closedpostboxesnonce', false ); ?>
1145 <input type="hidden" name="action" value="save" />
1146 <div id="poststuff">
1147 <p class="css-support">
1148 <?php
1149 /**
1150 * Filter the intro text appearing above the Custom CSS Editor.
1151 *
1152 * @module custom-css
1153 *
1154 * @since 1.7.0
1155 *
1156 * @param string $str Intro text appearing above the Custom CSS editor.
1157 */
1158 echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1159 'safecss_intro_text',
1160 __(
1161 'New to CSS? Start with a <a href="https://www.htmldog.com/guides/css/beginner/" rel="noopener noreferrer" target="_blank">beginner tutorial</a>. Questions?
1162 Ask in the <a href="https://wordpress.org/support/forum/themes-and-templates" rel="noopener noreferrer" target="_blank">Themes and Templates forum</a>.',
1163 'jetpack'
1164 )
1165 );
1166 ?>
1167 </p>
1168 <p class="css-support"><?php echo esc_html__( 'Note: Custom CSS will be reset when changing themes.', 'jetpack' ); ?></p>
1169
1170 <div id="post-body" class="metabox-holder columns-2">
1171 <div id="post-body-content">
1172 <div class="postarea">
1173 <textarea id="safecss" name="safecss"
1174 <?php
1175 if ( SAFECSS_USE_ACE ) {
1176 echo ' class="hide-if-js"';
1177 }
1178 ?>
1179 ><?php echo esc_textarea( self::get_css() ); ?></textarea>
1180 <div class="clear"></div>
1181 </div>
1182 </div>
1183 <div id="postbox-container-1" class="postbox-container">
1184 <?php do_meta_boxes( 'editcss', 'side', $safecss_post ); ?>
1185 </div>
1186 </div>
1187 <br class="clear" />
1188 </div>
1189 </form>
1190 </div>
1191 <?php
1192 }
1193
1194 /**
1195 * Content width setting callback
1196 */
1197 public static function content_width_settings() {
1198 $safecss_post = self::get_current_revision();
1199
1200 $custom_content_width = get_post_meta( $safecss_post['ID'], 'content_width', true );
1201
1202 // If custom content width hasn't been overridden and the theme has a content_width value, use that as a default.
1203 if ( $custom_content_width <= 0 && ! empty( $GLOBALS['content_width'] ) ) {
1204 $custom_content_width = $GLOBALS['content_width'];
1205 }
1206
1207 if ( ! $custom_content_width || ( isset( $GLOBALS['content_width'] ) && $custom_content_width == $GLOBALS['content_width'] ) ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
1208 $custom_content_width = '';
1209 }
1210
1211 ?>
1212 <div class="misc-pub-section">
1213 <label><?php esc_html_e( 'Media Width:', 'jetpack' ); ?></label>
1214 <span id="content-width-display" data-default-text="<?php esc_attr_e( 'Default', 'jetpack' ); ?>" data-custom-text="
1215 <?php
1216 // translators: the custom content width.
1217 esc_attr_e( '%s px', 'jetpack' );
1218 ?>
1219 ">
1220 <?php
1221 // translators: the custom content width.
1222 echo esc_html( $custom_content_width ? sprintf( __( '%s px', 'jetpack' ), $custom_content_width ) : __( 'Default', 'jetpack' ) );
1223 ?>
1224 </span>
1225 <a class="edit-content-width hide-if-no-js" href="#content-width"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
1226 <div id="content-width-select" class="hide-if-js">
1227 <input type="hidden" name="custom_content_width" id="custom_content_width" value="<?php echo esc_attr( $custom_content_width ); ?>" />
1228 <p>
1229 <?php
1230
1231 printf( /* translators: %1$s is replaced with an input field for numbers. */
1232 wp_kses_post( __( 'Limit width to %1$s pixels for full size images. (<a href="%2$s" rel="noopener noreferrer" target="_blank">More info</a>.)', 'jetpack' ) ),
1233 '<input type="text" id="custom_content_width_visible" value="' . esc_attr( $custom_content_width ) . '" size="4" />',
1234 /**
1235 * Filter the Custom CSS limited width's support doc URL.
1236 *
1237 * @module custom-css
1238 *
1239 * @since 2.2.3
1240 *
1241 * @param string $url Custom CSS limited width's support doc URL.
1242 */
1243 esc_url(
1244 apply_filters( 'safecss_limit_width_link', Redirect::get_url( 'jetpack-support-custom-css', array( 'anchor' => 'limited-width' ) ) )
1245 )
1246 );
1247
1248 ?>
1249 </p>
1250 <?php
1251
1252 if (
1253 ! empty( $GLOBALS['content_width'] )
1254 && $custom_content_width != $GLOBALS['content_width'] // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
1255 ) {
1256 $current_theme = wp_get_theme()->Name;
1257
1258 ?>
1259 <p>
1260 <?php
1261 echo esc_html(
1262 sprintf(
1263 /* translators: %1$s is the theme name, %2$d is an amount of pixels. */
1264 _n(
1265 'The default content width for the %1$s theme is %2$d pixel.',
1266 'The default content width for the %1$s theme is %2$d pixels.',
1267 (int) $GLOBALS['content_width'],
1268 'jetpack'
1269 ),
1270 $current_theme,
1271 (int) $GLOBALS['content_width']
1272 )
1273 );
1274 ?>
1275 </p>
1276 <?php
1277 }
1278
1279 ?>
1280 <a class="save-content-width hide-if-no-js button" href="#content-width"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
1281 <a class="cancel-content-width hide-if-no-js" href="#content-width"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
1282 </div>
1283 <script type="text/javascript">
1284 jQuery( function ( $ ) {
1285 var defaultContentWidth = <?php echo isset( $GLOBALS['content_width'] ) ? wp_json_encode( (int) $GLOBALS['content_width'] ) : 0; ?>;
1286
1287 $( '.edit-content-width' ).bind( 'click', function ( e ) {
1288 e.preventDefault();
1289
1290 $( '#content-width-select' ).slideDown();
1291 $( this ).hide();
1292 } );
1293
1294 $( '.cancel-content-width' ).bind( 'click', function ( e ) {
1295 e.preventDefault();
1296
1297 $( '#content-width-select' ).slideUp( function () {
1298 $( '.edit-content-width' ).show();
1299 $( '#custom_content_width_visible' ).val( $( '#custom_content_width' ).val() );
1300 } );
1301 } );
1302
1303 $( '.save-content-width' ).bind( 'click', function ( e ) {
1304 e.preventDefault();
1305
1306 $( '#content-width-select' ).slideUp();
1307
1308 var newContentWidth = parseInt( $( '#custom_content_width_visible' ).val(), 10 );
1309
1310 if ( newContentWidth && newContentWidth != defaultContentWidth ) {
1311 $( '#content-width-display' ).text(
1312 $( '#content-width-display' )
1313 .data( 'custom-text' )
1314 .replace( '%s', $( '#custom_content_width_visible' ).val() )
1315 );
1316 }
1317 else {
1318 $( '#content-width-display' ).text( $( '#content-width-display' ).data( 'default-text' ) );
1319 }
1320
1321 $( '#custom_content_width' ).val( $( '#custom_content_width_visible' ).val() );
1322 $( '.edit-content-width' ).show();
1323 } );
1324 } );
1325 </script>
1326 </div>
1327 <?php
1328 }
1329
1330 /**
1331 * Render the publish box.
1332 */
1333 public static function publish_box() {
1334 ?>
1335 <div id="minor-publishing">
1336 <div id="misc-publishing-actions">
1337 <?php
1338
1339 /**
1340 * Filter the array of available Custom CSS preprocessors.
1341 *
1342 * @module custom-css
1343 *
1344 * @since 2.0.3
1345 *
1346 * @param array array() Empty by default.
1347 */
1348 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
1349
1350 if ( ! empty( $preprocessors ) ) {
1351 $safecss_post = self::get_current_revision();
1352 $selected_preprocessor_key = get_post_meta( $safecss_post['ID'], 'custom_css_preprocessor', true );
1353 $selected_preprocessor = isset( $preprocessors[ $selected_preprocessor_key ] ) ? $preprocessors[ $selected_preprocessor_key ] : null;
1354
1355 ?>
1356 <div class="misc-pub-section">
1357 <label><?php esc_html_e( 'Preprocessor:', 'jetpack' ); ?></label>
1358 <span id="preprocessor-display"><?php echo esc_html( $selected_preprocessor ? $selected_preprocessor['name'] : __( 'None', 'jetpack' ) ); ?></span>
1359 <a class="edit-preprocessor hide-if-no-js" href="#preprocessor"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
1360 <div id="preprocessor-select" class="hide-if-js">
1361 <input type="hidden" name="custom_css_preprocessor" id="custom_css_preprocessor" value="<?php echo esc_attr( $selected_preprocessor_key ); ?>" />
1362 <select id="preprocessor_choices">
1363 <option value=""><?php esc_html_e( 'None', 'jetpack' ); ?></option>
1364 <?php
1365
1366 foreach ( $preprocessors as $preprocessor_key => $preprocessor ) {
1367 ?>
1368 <option value="<?php echo esc_attr( $preprocessor_key ); ?>" <?php selected( $selected_preprocessor_key, $preprocessor_key ); ?>><?php echo esc_html( $preprocessor['name'] ); ?></option>
1369 <?php
1370 }
1371
1372 ?>
1373 </select>
1374 <a class="save-preprocessor hide-if-no-js button" href="#preprocessor"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
1375 <a class="cancel-preprocessor hide-if-no-js" href="#preprocessor"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
1376 </div>
1377 </div>
1378 <?php
1379 }
1380
1381 $safecss_post = self::get_current_revision();
1382
1383 $add_css = ( get_post_meta( $safecss_post['ID'], 'custom_css_add', true ) !== 'no' );
1384
1385 ?>
1386 <div class="misc-pub-section">
1387 <label><?php esc_html_e( 'Mode:', 'jetpack' ); ?></label>
1388 <span id="css-mode-display"><?php echo esc_html( $add_css ? __( 'Add-on', 'jetpack' ) : __( 'Replacement', 'jetpack' ) ); ?></span>
1389 <a class="edit-css-mode hide-if-no-js" href="#css-mode"><?php echo esc_html_e( 'Edit', 'jetpack' ); ?></a>
1390 <div id="css-mode-select" class="hide-if-js">
1391 <input type="hidden" name="add_to_existing" id="add_to_existing" value="<?php echo $add_css ? 'true' : 'false'; ?>" />
1392 <p>
1393 <label>
1394 <input type="radio" name="add_to_existing_display" value="true" <?php checked( $add_css ); ?>/>
1395 <?php echo wp_kses( __( 'Add-on CSS <b>(Recommended)</b>', 'jetpack' ), array( 'b' => array() ) ); ?>
1396 </label>
1397 <br />
1398 <label>
1399 <input type="radio" name="add_to_existing_display" value="false" <?php checked( ! $add_css ); ?>/>
1400 <?php
1401 printf(
1402 // translators: the theme's stylesheet URL.
1403 wp_kses_post( __( 'Replace <a href="%s">theme\'s CSS</a> <b>(Advanced)</b>', 'jetpack' ) ),
1404 /**
1405 * Filter the theme's stylesheet URL.
1406 *
1407 * @module custom-css
1408 *
1409 * @since 1.7.0
1410 *
1411 * @param string $url Active theme's stylesheet URL. Default to get_stylesheet_uri().
1412 */
1413 esc_url( apply_filters( 'safecss_theme_stylesheet_url', get_stylesheet_uri() ) )
1414 );
1415 ?>
1416 </label>
1417 </p>
1418 <a class="save-css-mode hide-if-no-js button" href="#css-mode"><?php esc_html_e( 'OK', 'jetpack' ); ?></a>
1419 <a class="cancel-css-mode hide-if-no-js" href="#css-mode"><?php esc_html_e( 'Cancel', 'jetpack' ); ?></a>
1420 </div>
1421 </div>
1422 <?php
1423
1424 /**
1425 * Allows addition of elements to the submit box for custom css on the wp-admin side.
1426 *
1427 * @module custom-css
1428 *
1429 * @since 2.0.3
1430 */
1431 do_action( 'custom_css_submitbox_misc_actions' );
1432
1433 ?>
1434 </div>
1435 </div>
1436 <div id="major-publishing-actions">
1437 <input type="button" class="button" id="preview" name="preview" value="<?php esc_attr_e( 'Preview', 'jetpack' ); ?>" />
1438 <div id="publishing-action">
1439 <input type="submit" class="button-primary" id="save" name="save" value="<?php ( self::is_freetrial() ) ? esc_attr_e( 'Save &amp; Buy Upgrade', 'jetpack' ) : esc_attr_e( 'Save Stylesheet', 'jetpack' ); ?>" />
1440 </div>
1441 </div>
1442 <?php
1443 }
1444
1445 /**
1446 * Render metabox listing CSS revisions and the themes that correspond to the revisions.
1447 * Called by safecss_admin
1448 *
1449 * @global $post
1450 * @param array $safecss_post - the safecss array.
1451 * @uses wp_revisions_to_keep
1452 * @uses WP_Query
1453 * @uses wp_post_revision_title
1454 * @uses esc_html
1455 * @uses add_query_arg
1456 * @uses menu_page_url
1457 * @uses wp_reset_query
1458 *
1459 * @todo can this be removed? The revision page seems to work via the customizer now.
1460 */
1461 public static function revisions_meta_box( $safecss_post ) {
1462
1463 $show_all_revisions = isset( $_GET['show_all_rev'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nothing on the site is changing
1464
1465 if ( function_exists( 'wp_revisions_to_keep' ) ) {
1466 $max_revisions = wp_revisions_to_keep( (object) $safecss_post );
1467 } else {
1468 $max_revisions = defined( 'WP_POST_REVISIONS' ) && is_numeric( WP_POST_REVISIONS ) ? (int) WP_POST_REVISIONS : 25;
1469 }
1470
1471 $posts_per_page = $show_all_revisions ? $max_revisions : 6;
1472
1473 $revisions = new WP_Query(
1474 array(
1475 'posts_per_page' => $posts_per_page,
1476 'post_type' => 'revision',
1477 'post_status' => 'inherit',
1478 'post_parent' => $safecss_post['ID'],
1479 'orderby' => 'date',
1480 'order' => 'DESC',
1481 )
1482 );
1483
1484 if ( $revisions->have_posts() ) {
1485 ?>
1486 <ul class="post-revisions">
1487 <?php
1488
1489 global $post;
1490
1491 while ( $revisions->have_posts() ) :
1492 $revisions->the_post();
1493
1494 ?>
1495 <li>
1496 <?php
1497 echo wp_post_revision_title( $post ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1498
1499 if ( ! empty( $post->post_excerpt ) ) {
1500 echo ' (' . esc_html( $post->post_excerpt ) . ')';
1501 }
1502 ?>
1503 </li>
1504 <?php
1505
1506 endwhile;
1507
1508 ?>
1509 </ul>
1510 <?php
1511
1512 if ( $revisions->found_posts > 6 && ! $show_all_revisions ) {
1513 ?>
1514 <br>
1515 <a href="<?php echo esc_url( add_query_arg( 'show_all_rev', 'true', menu_page_url( 'editcss', false ) ) ); ?>"><?php esc_html_e( 'Show all', 'jetpack' ); ?></a>
1516 <?php
1517 }
1518 }
1519
1520 wp_reset_postdata();
1521 }
1522
1523 /**
1524 * Hook in init at priority 11 to disable custom CSS.
1525 */
1526 public static function disable() {
1527 remove_action( 'wp_head', array( 'Jetpack_Custom_CSS', 'link_tag' ), 101 );
1528 remove_filter( 'stylesheet_uri', array( 'Jetpack_Custom_CSS', 'style_filter' ) );
1529 }
1530
1531 /**
1532 * Reset all aspects of Custom CSS on a theme switch so that changing
1533 * themes is a sure-fire way to get a clean start.
1534 */
1535 public static function reset() {
1536 $safecss_post_id = self::save_revision( '' );
1537 $safecss_revision = self::get_current_revision();
1538
1539 update_option( 'safecss_rev', (int) get_option( 'safecss_rev' ) + 1 );
1540
1541 update_post_meta( $safecss_post_id, 'custom_css_add', 'yes' );
1542 update_post_meta( $safecss_post_id, 'content_width', false );
1543 update_post_meta( $safecss_post_id, 'custom_css_preprocessor', '' );
1544
1545 delete_option( 'safecss_add' );
1546 delete_option( 'safecss_content_width' );
1547
1548 update_metadata( 'post', $safecss_revision['ID'], 'custom_css_add', 'yes' );
1549 update_metadata( 'post', $safecss_revision['ID'], 'content_width', false );
1550 update_metadata( 'post', $safecss_revision['ID'], 'custom_css_preprocessor', '' );
1551
1552 delete_option( 'safecss_preview_add' );
1553 }
1554
1555 /**
1556 * Checks of we're in the customizer.
1557 *
1558 * @return bool
1559 */
1560 public static function is_customizer_preview() {
1561 if ( isset( $GLOBALS['wp_customize'] ) ) {
1562 return ! $GLOBALS['wp_customize']->is_theme_active();
1563 }
1564
1565 return false;
1566 }
1567
1568 /**
1569 * Handle minifying CSS.
1570 *
1571 * @param string $css - the CSS.
1572 * @param string $preprocessor - the preprocessor we want to use.
1573 *
1574 * @return string
1575 */
1576 public static function minify( $css, $preprocessor = '' ) {
1577 if ( ! $css ) {
1578 return '';
1579 }
1580
1581 if ( $preprocessor ) {
1582 /** This filter is documented in modules/custom-css/custom-css.php */
1583 $preprocessors = apply_filters( 'jetpack_custom_css_preprocessors', array() );
1584
1585 if ( isset( $preprocessors[ $preprocessor ] ) ) {
1586 $css = call_user_func( $preprocessors[ $preprocessor ]['callback'], $css );
1587 }
1588 }
1589
1590 safecss_class();
1591 $csstidy = new csstidy();
1592 $csstidy->optimise = new safecss( $csstidy );
1593
1594 $csstidy->set_cfg( 'remove_bslash', false );
1595 $csstidy->set_cfg( 'compress_colors', true );
1596 $csstidy->set_cfg( 'compress_font-weight', true );
1597 $csstidy->set_cfg( 'remove_last_;', true );
1598 $csstidy->set_cfg( 'case_properties', true );
1599 $csstidy->set_cfg( 'discard_invalid_properties', true );
1600 $csstidy->set_cfg( 'css_level', 'CSS3.0' );
1601 $csstidy->set_cfg( 'template', 'highest' );
1602 $csstidy->parse( $css );
1603
1604 return $csstidy->print->plain();
1605 }
1606
1607 /**
1608 * When restoring a SafeCSS post revision, also copy over the
1609 * content_width and custom_css_add post metadata.
1610 *
1611 * @param int $_post_id - the post ID.
1612 * @param int $_revision_id - the revision ID.
1613 */
1614 public static function restore_revision( $_post_id, $_revision_id ) {
1615 $_post = get_post( $_post_id );
1616
1617 if ( 'safecss' !== $_post->post_type ) {
1618 return;
1619 }
1620
1621 $safecss_revision = self::get_current_revision();
1622
1623 $content_width = get_post_meta( $_revision_id, 'content_width', true );
1624 $custom_css_add = get_post_meta( $_revision_id, 'custom_css_add', true );
1625 $preprocessor = get_post_meta( $_revision_id, 'custom_css_preprocessor', true );
1626
1627 update_metadata( 'post', $safecss_revision['ID'], 'content_width', $content_width );
1628 update_metadata( 'post', $safecss_revision['ID'], 'custom_css_add', $custom_css_add );
1629 update_metadata( 'post', $safecss_revision['ID'], 'custom_css_preprocessor', $preprocessor );
1630
1631 delete_option( 'safecss_add' );
1632 delete_option( 'safecss_content_width' );
1633
1634 update_post_meta( $_post->ID, 'content_width', $content_width );
1635 update_post_meta( $_post->ID, 'custom_css_add', $custom_css_add );
1636 update_post_meta( $_post->ID, 'custom_css_preprocessor', $preprocessor );
1637
1638 delete_option( 'safecss_preview_add' );
1639 }
1640
1641 /**
1642 * Migration routine for moving safecss from wp_options to wp_posts to support revisions
1643 */
1644 public static function upgrade() {
1645 $css = get_option( 'safecss' );
1646
1647 if ( get_option( 'safecss_revision_migrated' ) ) {
1648 return false;
1649 }
1650
1651 // Check if CSS is stored in wp_options
1652 if ( $css ) {
1653 // Remove the async actions from publish_post
1654 remove_action( 'publish_post', 'queue_publish_post' );
1655
1656 $post = array();
1657 $post['post_content'] = $css;
1658 $post['post_title'] = 'safecss';
1659 $post['post_status'] = 'publish';
1660 $post['post_type'] = 'safecss';
1661
1662 // Insert the CSS into wp_posts
1663 $post_id = wp_insert_post( $post );
1664 // Check for errors
1665 if ( ! $post_id || is_wp_error( $post_id ) ) {
1666 die( $post_id->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1667 }
1668
1669 // Delete safecss option
1670 delete_option( 'safecss' );
1671 }
1672
1673 unset( $css );
1674
1675 // Check if we have already done this
1676 if ( ! get_option( 'safecss_revision_migrated' ) ) {
1677 define( 'DOING_MIGRATE', true );
1678
1679 // Get hashes of safecss post and current revision
1680 $safecss_post = self::get_post();
1681
1682 if ( empty( $safecss_post ) ) {
1683 return;
1684 }
1685
1686 $safecss_post_hash = md5( $safecss_post['post_content'] );
1687 $current_revision = self::get_current_revision();
1688
1689 if ( null === $current_revision ) {
1690 return;
1691 }
1692
1693 $current_revision_hash = md5( $current_revision['post_content'] );
1694
1695 // If hashes are not equal, set safecss post with content from current revision
1696 if ( $safecss_post_hash !== $current_revision_hash ) {
1697 self::save_revision( $current_revision['post_content'] );
1698 // Reset post_content to display the migrated revsion
1699 $safecss_post['post_content'] = $current_revision['post_content'];
1700 }
1701
1702 // Set option so that we dont keep doing this
1703 update_option( 'safecss_revision_migrated', time() );
1704 }
1705
1706 $newest_safecss_post = self::get_current_revision();
1707
1708 if ( $newest_safecss_post ) {
1709 if ( get_option( 'safecss_content_width' ) ) {
1710 // Add the meta to the post and the latest revision.
1711 update_post_meta( $newest_safecss_post['ID'], 'content_width', get_option( 'safecss_content_width' ) );
1712 update_metadata( 'post', $newest_safecss_post['ID'], 'content_width', get_option( 'safecss_content_width' ) );
1713
1714 delete_option( 'safecss_content_width' );
1715 }
1716
1717 if ( get_option( 'safecss_add' ) ) {
1718 update_post_meta( $newest_safecss_post['ID'], 'custom_css_add', get_option( 'safecss_add' ) );
1719 update_metadata( 'post', $newest_safecss_post['ID'], 'custom_css_add', get_option( 'safecss_add' ) );
1720
1721 delete_option( 'safecss_add' );
1722 }
1723 }
1724 }
1725
1726 /**
1727 * Adds a filter to the redirect location in `wp-admin/revisions.php`.
1728 */
1729 public static function add_revision_redirect() {
1730 add_filter( 'wp_redirect', array( __CLASS__, 'revision_redirect' ) );
1731 }
1732
1733 /**
1734 * Filters the redirect location in `wp-admin/revisions.php`.
1735 *
1736 * @param string $location The path to redirect to.
1737 * @return string
1738 */
1739 public static function revision_redirect( $location ) {
1740 $post = get_post();
1741
1742 if ( ! empty( $post->post_type ) && 'safecss' === $post->post_type ) {
1743 $location = 'themes.php?page=editcss';
1744
1745 if ( 'edit.php' === $location ) {
1746 $location = '';
1747 }
1748 }
1749
1750 return $location;
1751 }
1752
1753 /**
1754 * The revision post link.
1755 *
1756 * @param string $post_link - the post link.
1757 * @param int $post_id - the post ID.
1758 * @param string $context - the context.
1759 *
1760 * @return string
1761 */
1762 public static function revision_post_link( $post_link, $post_id, $context ) {
1763 $post_id = (int) $post_id;
1764 if ( ! $post_id ) {
1765 return $post_link;
1766 }
1767
1768 $post = get_post( $post_id );
1769 if ( ! $post ) {
1770 return $post_link;
1771 }
1772
1773 if ( 'safecss' !== $post->post_type ) {
1774 return $post_link;
1775 }
1776
1777 $post_link = admin_url( 'themes.php?page=editcss' );
1778
1779 if ( 'display' === $context ) {
1780 return esc_url( $post_link );
1781 }
1782
1783 return esc_url_raw( $post_link );
1784 }
1785
1786 /**
1787 * When on the edit screen, make sure the custom content width
1788 * setting is applied to the large image size.
1789 *
1790 * @param array $dims - the width and height dimensions.
1791 * @param string $size - the size.
1792 * @param string $context - the context in which we're applying dimensions.
1793 *
1794 * @return array
1795 */
1796 public static function editor_max_image_size( $dims, $size = 'medium', $context = null ) {
1797 list( $width, $height ) = $dims;
1798
1799 if ( 'large' === $size && 'edit' === $context ) {
1800 $width = Jetpack::get_content_width();
1801 }
1802
1803 return array( $width, $height );
1804 }
1805
1806 /**
1807 * Override the content_width with a custom value if one is set.
1808 *
1809 * @param int $content_width - the content width in pixels.
1810 *
1811 * @return int
1812 */
1813 public static function jetpack_content_width( $content_width ) {
1814 $custom_content_width = 0;
1815
1816 if ( self::is_preview() ) {
1817 $safecss_post = self::get_current_revision();
1818 $custom_content_width = (int) get_post_meta( $safecss_post['ID'], 'content_width', true );
1819 } elseif ( ! self::is_freetrial() ) {
1820 $custom_css_post_id = self::post_id();
1821 if ( $custom_css_post_id ) {
1822 $custom_content_width = (int) get_post_meta( $custom_css_post_id, 'content_width', true );
1823 }
1824 }
1825
1826 if ( $custom_content_width > 0 ) {
1827 $content_width = $custom_content_width;
1828 }
1829
1830 return $content_width;
1831 }
1832 }
1833
1834 /**
1835 * The Safe CSS Class.
1836 */
1837 class Jetpack_Safe_CSS { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
1838 /**
1839 * Filter attriburtes.
1840 *
1841 * @param string $css - the CSS.
1842 * @param string $element - the HTML element.
1843 *
1844 * @return string
1845 */
1846 public static function filter_attr( $css, $element = 'div' ) {
1847 safecss_class();
1848
1849 $css = $element . ' {' . $css . '}';
1850
1851 $csstidy = new csstidy();
1852 $csstidy->optimise = new safecss( $csstidy );
1853 $csstidy->set_cfg( 'remove_bslash', false );
1854 $csstidy->set_cfg( 'compress_colors', false );
1855 $csstidy->set_cfg( 'compress_font-weight', false );
1856 $csstidy->set_cfg( 'discard_invalid_properties', true );
1857 $csstidy->set_cfg( 'merge_selectors', false );
1858 $csstidy->set_cfg( 'remove_last_;', false );
1859 $csstidy->set_cfg( 'css_level', 'CSS3.0' );
1860
1861 // Turn off css shorthands and leading zero removal as it breaks block validation.
1862 $csstidy->set_cfg( 'optimise_shorthands', 0 );
1863 $csstidy->set_cfg( 'preserve_leading_zeros', true );
1864
1865 $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $css );
1866 $css = wp_kses_split( $css, array(), array() );
1867 $csstidy->parse( $css );
1868
1869 $css = $csstidy->print->plain();
1870
1871 $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
1872
1873 preg_match( "/^{$element}\s*{(.*)}\s*$/", $css, $matches );
1874
1875 if ( empty( $matches[1] ) ) {
1876 return '';
1877 }
1878
1879 return $matches[1];
1880 }
1881 }
1882
1883 if ( ! function_exists( 'safecss_class' ) ) :
1884 /**
1885 * Setup safecss class.
1886 */
1887 function safecss_class() {
1888 // Wrapped so we don't need the parent class just to load the plugin
1889 if ( class_exists( 'safecss' ) ) {
1890 return;
1891 }
1892
1893 require_once __DIR__ . '/csstidy/class.csstidy.php';
1894
1895 /**
1896 * Safe CSS Class.
1897 */
1898 class safecss extends csstidy_optimise { // phpcs:ignore
1899
1900 /**
1901 * Add action to fire after parsing CSS.
1902 */
1903 public function postparse() { // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
1904
1905 /**
1906 * Fires after parsing the css.
1907 *
1908 * @module custom-css
1909 *
1910 * @since 1.8.0
1911 *
1912 * @param obj $this CSSTidy object.
1913 */
1914 do_action( 'csstidy_optimize_postparse', $this );
1915
1916 return parent::postparse();
1917 }
1918
1919 /**
1920 * Handle subvalue action.
1921 */
1922 public function subvalue() { // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
1923
1924 /**
1925 * Fires before optimizing the Custom CSS subvalue.
1926 *
1927 * @module custom-css
1928 *
1929 * @since 1.8.0
1930 *
1931 * @param obj $this CSSTidy object.
1932 */
1933 do_action( 'csstidy_optimize_subvalue', $this );
1934
1935 return parent::subvalue();
1936 }
1937 }
1938 }
1939 endif;
1940
1941 if ( ! function_exists( 'safecss_filter_attr' ) ) {
1942
1943 /**
1944 * Filter safecss attriburtes.
1945 *
1946 * @param string $css - the CSS.
1947 * @param string $element - the HTML element.
1948 */
1949 function safecss_filter_attr( $css, $element = 'div' ) {
1950 return Jetpack_Safe_CSS::filter_attr( $css, $element );
1951 }
1952 }
1953
1954 require_once __DIR__ . '/custom-css/preprocessors.php';
1955