| @@ -11,8 +11,10 @@ | ||
| 11 | 11 | add_filter( 'manage_ultp_custom_font_posts_columns', array( $this, 'templates_table_head' ) ); |
| 12 | 12 | add_action( 'manage_ultp_custom_font_posts_custom_column', array( $this, 'templates_table_content' ), 10, 2 ); |
| 13 | 13 | add_filter( 'upload_mimes', array( $this, 'add_file_types_to_uploads' ) ); |
| 14 | 14 | add_filter( 'wp_check_filetype_and_ext', array( $this, 'font_correct_filetypes' ), 10, 5 ); |
| 15 | + add_filter( 'wp_handle_upload_prefilter', array( $this, 'sanitize_svg_upload' ) ); | |
| 16 | + add_filter( 'wp_handle_sideload_prefilter', array( $this, 'sanitize_svg_upload' ) ); | |
| 15 | 17 | add_filter( 'enter_title_here', array( $this, 'update_custom_font_title' ), 10, 2 ); |
| 16 | 18 | } |
| 17 | 19 | |
| 18 | 20 | public function update_custom_font_title( $title, $post ) { |
| @@ -42,11 +44,14 @@ | ||
| 42 | 44 | $new_filetypes = array(); |
| 43 | 45 | $new_filetypes['woff'] = 'font/woff'; |
| 44 | 46 | $new_filetypes['woff2'] = 'font/woff2'; |
| 45 | 47 | $new_filetypes['ttf'] = 'font/ttf'; |
| 46 | - $new_filetypes['svg'] = 'image/svg+xml'; | |
| 47 | 48 | $new_filetypes['eot'] = 'font/ttf'; |
| 48 | - $file_types = array_merge( $file_types, $new_filetypes ); | |
| 49 | + // SVG only for a custom font, where every upload is sanitized by sanitize_svg_upload(). | |
| 50 | + if ( $this->is_custom_font_upload() ) { | |
| 51 | + $new_filetypes['svg'] = 'image/svg+xml'; | |
| 52 | + } | |
| 53 | + $file_types = array_merge( $file_types, $new_filetypes ); | |
| 49 | 54 | return $file_types; |
| 50 | 55 | } else { |
| 51 | 56 | return $file_types; |
| 52 | 57 | } |
| @@ -51,9 +56,82 @@ | ||
| 51 | 56 | return $file_types; |
| 52 | 57 | } |
| 53 | 58 | } |
| 54 | 59 | |
| 60 | + /** | |
| 61 | + * Whether the current request uploads to, or edits, a custom font the user may edit. | |
| 62 | + * | |
| 63 | + * Uploads from the font screen's media modal send the font as `post_id`; async-upload.php | |
| 64 | + * checks its own nonce and refuses a `post_id` the user cannot edit. The screen itself is | |
| 65 | + * recognised by the global post. | |
| 66 | + * | |
| 67 | + * @return bool | |
| 68 | + */ | |
| 69 | + private function is_custom_font_upload() { | |
| 70 | + if ( ! current_user_can( 'manage_options' ) ) { | |
| 71 | + return false; | |
| 72 | + } | |
| 73 | + $post_id = isset( $_REQUEST['post_id'] ) ? absint( $_REQUEST['post_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read only to choose the context; async-upload.php verifies the media-form nonce. | |
| 74 | + if ( ! $post_id ) { | |
| 75 | + $post_id = (int) get_the_ID(); | |
| 76 | + } | |
| 77 | + return $post_id > 0 && 'ultp_custom_font' === get_post_type( $post_id ) && current_user_can( 'edit_post', $post_id ); | |
| 78 | + } | |
| 55 | 79 | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * Clean an SVG uploaded to a custom font. | |
| 83 | + * | |
| 84 | + * Only runs for custom font uploads, the one place this add-on allows SVG. The file keeps | |
| 85 | + * only its font markup; an SVG that is not a font is refused. SVG uploads anywhere else | |
| 86 | + * are not touched here: WordPress refuses them unless another plugin allows SVG. | |
| 87 | + * | |
| 88 | + * @param array $file Upload data from `$_FILES`. | |
| 89 | + * @return array | |
| 90 | + */ | |
| 91 | + public function sanitize_svg_upload( $file ) { | |
| 92 | + if ( empty( $file['name'] ) || empty( $file['tmp_name'] ) || ! empty( $file['error'] ) ) { | |
| 93 | + return $file; | |
| 94 | + } | |
| 95 | + if ( 'svg' !== strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) ) ) { | |
| 96 | + return $file; | |
| 97 | + } | |
| 98 | + if ( ! $this->is_custom_font_upload() ) { | |
| 99 | + return $file; | |
| 100 | + } | |
| 101 | + | |
| 102 | + require_once __DIR__ . '/Svg_Font_Sanitizer.php'; | |
| 103 | + $sanitized = Svg_Font_Sanitizer::sanitize_file( $file['tmp_name'] ); | |
| 104 | + if ( is_wp_error( $sanitized ) ) { | |
| 105 | + $file['error'] = $sanitized->get_error_message(); | |
| 106 | + } | |
| 107 | + return $file; | |
| 108 | + } | |
| 109 | + | |
| 110 | + /** | |
| 111 | + * Font weight as stored and printed: 100-900 in steps of 100, normal or bold. | |
| 112 | + * | |
| 113 | + * @param string $weight Raw weight. | |
| 114 | + * @return string | |
| 115 | + */ | |
| 116 | + public static function sanitize_font_weight( $weight ) { | |
| 117 | + $weight = strtolower( trim( (string) $weight ) ); | |
| 118 | + if ( in_array( $weight, array( 'normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900' ), true ) ) { | |
| 119 | + return $weight; | |
| 120 | + } | |
| 121 | + return 'normal'; | |
| 122 | + } | |
| 123 | + | |
| 124 | + /** | |
| 125 | + * Font family name safe to print inside a CSS string or a style attribute. | |
| 126 | + * | |
| 127 | + * @param string $font_name Post title of the font. | |
| 128 | + * @return string | |
| 129 | + */ | |
| 130 | + public static function sanitize_font_family( $font_name ) { | |
| 131 | + return str_replace( array( '"', '\\', ';', '{', '}', '<', '>' ), '', (string) $font_name ); | |
| 132 | + } | |
| 133 | + | |
| 56 | 134 | // Template Heading Add |
| 57 | 135 | public function templates_table_head( $defaults ) { |
| 58 | 136 | $type_array = array( |
| 59 | 137 | 'preview' => '<span class="ultp-custom-font-preview-th">' . __( 'Preview', 'ultimate-post' ) . '</span>', |
| @@ -88,10 +166,10 @@ | ||
| 88 | 166 | if ( $settings['eot'] ) { |
| 89 | 167 | array_push( $font_src, 'url(' . esc_url( $settings['eot'] ) . ') format("eot")' ); |
| 90 | 168 | } |
| 91 | 169 | $font_face = '@font-face { |
| 92 | - font-family: "' . $font_name . '"; | |
| 93 | - font-weight: ' . $settings['weight'] . '; | |
| 170 | + font-family: "' . self::sanitize_font_family( $font_name ) . '"; | |
| 171 | + font-weight: ' . self::sanitize_font_weight( isset( $settings['weight'] ) ? $settings['weight'] : '' ) . '; | |
| 94 | 172 | src: ' . implode( ', ', $font_src ) . '; |
| 95 | 173 | }'; |
| 96 | 174 | |
| 97 | 175 | return $font_face; |
| @@ -116,9 +194,9 @@ | ||
| 116 | 194 | if ( $value['eot'] ) { |
| 117 | 195 | $eot = true; } |
| 118 | 196 | } |
| 119 | 197 | $font_face = $this->get_font_face( $settings[0], get_the_title( $post_id ) ); |
| 120 | - echo '<style type="text/css">' . $font_face . '</style>'; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 198 | + echo '<style type="text/css">' . $font_face . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSS from get_font_face(): URLs pass esc_url(), family and weight pass sanitize_font_family()/sanitize_font_weight(). | |
| 121 | 199 | |
| 122 | 200 | switch ( $column_id ) { |
| 123 | 201 | case 0: |
| 124 | 202 | echo '<span class="ultp-custom-font-preview" style="font-family: ' . esc_attr( get_the_title( $post_id ) ) . '">' . esc_html__( 'The quick brown fox jumps over the lazy dog.', 'ultimate-post' ) . '</span>'; |
| @@ -160,9 +238,9 @@ | ||
| 160 | 238 | ?> |
| 161 | 239 | <div class="ultp-custom-font-container ultp-custom-font<?php echo empty( $arr ) ? '-copy' : ''; ?>"> |
| 162 | 240 | <div class="ultp-custom-font-heading"> |
| 163 | 241 | <div> |
| 164 | - <label class="font-label"><?php echo esc_html__( 'Weight: ', 'ultimate-post' ); ?> <span class="ultp-custom-font-weight"> <?php echo esc_html__( isset( $arr['weight'] ) ? $arr['weight'] : '', 'ultimate-post' ); ?> </span></label> | |
| 242 | + <label class="font-label"><?php echo esc_html__( 'Weight: ', 'ultimate-post' ); ?> <span class="ultp-custom-font-weight"> <?php echo esc_html( isset( $arr['weight'] ) ? $arr['weight'] : '' ); ?> </span></label> | |
| 165 | 243 | <select name="weight[]"> |
| 166 | 244 | <?php $weight = isset( $arr['weight'] ) ? $arr['weight'] : ''; ?> |
| 167 | 245 | <option <?php selected( $weight, 'normal' ); ?> value="normal"><?php echo esc_html__( 'Normal', 'ultimate-post' ); ?></option> |
| 168 | 246 | <option <?php selected( $weight, '100' ); ?> value="100"><?php echo esc_html__( '100', 'ultimate-post' ); ?></option> |
| @@ -176,16 +254,16 @@ | ||
| 176 | 254 | <option <?php selected( $weight, '900' ); ?> value="900"><?php echo esc_html__( '900', 'ultimate-post' ); ?></option> |
| 177 | 255 | </select> |
| 178 | 256 | </div> |
| 179 | 257 | <?php |
| 180 | - $styles = ''; | |
| 258 | + $preview_style = ''; | |
| 181 | 259 | if ( ! empty( $arr ) ) { |
| 182 | 260 | $font_face = $this->get_font_face( $arr, $font_name ); |
| 183 | - echo '<style type="text/css">' . $font_face . '</style>'; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 184 | - $styles = 'style="font-family: ' . $font_name . '; font-weight: ' . $arr['weight'] . ' "'; | |
| 261 | + echo '<style type="text/css">' . $font_face . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSS from get_font_face(): URLs pass esc_url(), family and weight pass sanitize_font_family()/sanitize_font_weight(). | |
| 262 | + $preview_style = 'font-family: "' . self::sanitize_font_family( $font_name ) . '"; font-weight: ' . self::sanitize_font_weight( isset( $arr['weight'] ) ? $arr['weight'] : '' ); | |
| 185 | 263 | } |
| 186 | 264 | ?> |
| 187 | - <span <?php echo $styles; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> class="ultp-custom-font-preview"><?php echo esc_html__( 'The quick brown fox jumps over the lazy dog', 'ultimate-post' ); ?></span> | |
| 265 | + <span <?php echo '' !== $preview_style ? 'style="' . esc_attr( $preview_style ) . '"' : ''; ?> class="ultp-custom-font-preview"><?php echo esc_html__( 'The quick brown fox jumps over the lazy dog', 'ultimate-post' ); ?></span> | |
| 188 | 266 | <div class="ultp-custom-font-actions"> |
| 189 | 267 | <span class="ultp-custom-font-edit"><span class="dashicons dashicons-edit"></span><?php echo esc_html__( 'Edit', 'ultimate-post' ); ?></span> |
| 190 | 268 | <span class="ultp-custom-font-close"><span class="dashicons dashicons-no-alt"></span><?php echo esc_html__( 'Close', 'ultimate-post' ); ?></span> |
| 191 | 269 | <span class="ultp-custom-font-delete"><span class="dashicons dashicons-trash"></span><?php echo esc_html__( 'Delete', 'ultimate-post' ); ?></span> |
| @@ -253,14 +331,14 @@ | ||
| 253 | 331 | ! empty( $_POST['ttf'][ $i ] ) || |
| 254 | 332 | ! empty( $_POST['svg'][ $i ] ) || |
| 255 | 333 | ! empty( $_POST['eot'][ $i ] ) ) ) { |
| 256 | 334 | $temp = array(); |
| 257 | - $temp['weight'] = isset( $_POST['weight'][ $i ] ) ? sanitize_text_field( $_POST['weight'][ $i ] ) : ''; | |
| 258 | - $temp['woff'] = isset( $_POST['woff'][ $i ] ) ? sanitize_text_field( $_POST['woff'][ $i ] ) : ''; | |
| 259 | - $temp['woff2'] = isset( $_POST['woff2'][ $i ] ) ? sanitize_text_field( $_POST['woff2'][ $i ] ) : ''; | |
| 260 | - $temp['ttf'] = isset( $_POST['ttf'][ $i ] ) ? sanitize_text_field( $_POST['ttf'][ $i ] ) : ''; | |
| 261 | - $temp['svg'] = isset( $_POST['svg'][ $i ] ) ? sanitize_text_field( $_POST['svg'][ $i ] ) : ''; | |
| 262 | - $temp['eot'] = isset( $_POST['eot'][ $i ] ) ? sanitize_text_field( $_POST['eot'][ $i ] ) : ''; | |
| 335 | + $temp['weight'] = self::sanitize_font_weight( sanitize_text_field( $_POST['weight'][ $i ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- update_post_meta() unslashes; unslashing here would strip backslashes twice. | |
| 336 | + $temp['woff'] = isset( $_POST['woff'][ $i ] ) ? sanitize_text_field( $_POST['woff'][ $i ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- update_post_meta() unslashes; unslashing here would strip backslashes twice. | |
| 337 | + $temp['woff2'] = isset( $_POST['woff2'][ $i ] ) ? sanitize_text_field( $_POST['woff2'][ $i ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- update_post_meta() unslashes; unslashing here would strip backslashes twice. | |
| 338 | + $temp['ttf'] = isset( $_POST['ttf'][ $i ] ) ? sanitize_text_field( $_POST['ttf'][ $i ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- update_post_meta() unslashes; unslashing here would strip backslashes twice. | |
| 339 | + $temp['svg'] = isset( $_POST['svg'][ $i ] ) ? sanitize_text_field( $_POST['svg'][ $i ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- update_post_meta() unslashes; unslashing here would strip backslashes twice. | |
| 340 | + $temp['eot'] = isset( $_POST['eot'][ $i ] ) ? sanitize_text_field( $_POST['eot'][ $i ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- update_post_meta() unslashes; unslashing here would strip backslashes twice. | |
| 263 | 341 | $arr[] = $temp; |
| 264 | 342 | } |
| 265 | 343 | } |
| 266 | 344 | update_post_meta( $post_id, '__font_settings', $arr ); |