PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 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 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-comments / src / editor / class-block-editor.php

class-block-editor.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-comments/src/editor/class-block-editor.php

287 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Blocks in comments.
4 *
5 * @package automattic/jetpack-comments
6 */
7
8 namespace Automattic\Jetpack\Comments;
9
10 use Automattic\Jetpack\Assets;
11
12 /**
13 * Keeps the blocks the editor offers, and draws them in the comment.
14 */
15 class Block_Editor {
16
17 /**
18 * Singleton instance.
19 *
20 * @var Block_Editor|null
21 */
22 private static $instance = null;
23
24 /**
25 * Whether the comment being filtered now holds blocks: set at 9, cleared at 11, so the
26 * wider kses rules last for that comment's pass alone.
27 *
28 * @var bool
29 */
30 private $has_blocks = false;
31
32 /**
33 * Register the hooks. Safe to call more than once.
34 *
35 * @return Block_Editor
36 */
37 public static function init() {
38 if ( null === self::$instance ) {
39 self::$instance = new self();
40 }
41
42 return self::$instance;
43 }
44
45 /**
46 * Hook in around core's comment filtering.
47 */
48 private function __construct() {
49 // Either side of kses at 10, which lets the markup of these blocks through.
50 add_filter( 'pre_comment_content', array( $this, 'keep_allowed_blocks' ), 9 );
51 add_filter( 'pre_comment_content', array( $this, 'forget_blocks' ), 11 );
52 add_filter( 'wp_kses_allowed_html', array( $this, 'allowed_html' ), 10, 2 );
53 // Ahead of wpautop at 30.
54 add_filter( 'comment_text', array( __CLASS__, 'render' ), 5 );
55 // The edit-comment screen, for a comment that holds blocks.
56 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin' ) );
57 }
58
59 /**
60 * Core's translations of the strings in strings.php, and the plural rules under '', in one Jed messages set.
61 *
62 * @return object
63 */
64 public static function locale_data() {
65 $locale = determine_locale();
66
67 if ( 'en_US' === $locale ) {
68 return (object) array();
69 }
70
71 // Keyed by the WordPress version and the package version, whose updates bring new strings.
72 $key = 'jetpack_comments_editor_i18n_' . md5( $locale . get_bloginfo( 'version' ) . Comments::PACKAGE_VERSION );
73 $messages = get_transient( $key );
74
75 if ( ! is_array( $messages ) ) {
76 $messages = array();
77 $wanted = array_flip( require __DIR__ . '/strings.php' ) + array( '' => true );
78
79 foreach ( array( 'wp-a11y', 'wp-block-editor', 'wp-block-library', 'wp-blocks', 'wp-components', 'wp-format-library', 'wp-rich-text' ) as $handle ) {
80 $json = load_script_textdomain( $handle, 'default' );
81 $data = is_string( $json ) ? json_decode( $json, true ) : null;
82
83 if ( isset( $data['locale_data']['messages'] ) && is_array( $data['locale_data']['messages'] ) ) {
84 $messages += array_intersect_key( $data['locale_data']['messages'], $wanted );
85 }
86 }
87
88 // Core translates block titles in PHP, so they are in its .mo, not the script files.
89 foreach ( array( 'Paragraph', 'List', 'List Item', 'Quote', 'Code' ) as $title ) {
90 // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.TextDomainMismatch -- Core's own strings.
91 $messages[ "block title\u{0004}$title" ] = array( _x( $title, 'block title', 'default' ) );
92 }
93
94 // A day, so an updated language pack shows soon after.
95 set_transient( $key, $messages, DAY_IN_SECONDS );
96 }
97
98 return (object) $messages;
99 }
100
101 /**
102 * Whether the block editor replaces the comment textarea.
103 *
104 * @return bool
105 */
106 public static function is_enabled() {
107 /**
108 * Offer the block editor in the Jetpack Comments form.
109 *
110 * @since 0.4.0
111 *
112 * @param bool $enabled Whether to offer the block editor. Default true, unless WordPress.com's
113 * "blocks in comments" Discussion setting is off.
114 */
115 return (bool) apply_filters( 'jetpack_comments_block_editor', (bool) get_option( 'enable_blocks_comments', true ) );
116 }
117
118 /**
119 * Drop any block the editor does not offer.
120 *
121 * @param string $content Slashed comment content.
122 * @return string
123 */
124 public function keep_allowed_blocks( $content ) {
125 $this->has_blocks = self::is_enabled() && has_blocks( $content );
126
127 if ( ! $this->has_blocks ) {
128 return $content;
129 }
130
131 return wp_slash( serialize_blocks( self::allowed( parse_blocks( wp_unslash( $content ) ) ) ) );
132 }
133
134 /**
135 * End the wider kses rules with the comment they were for.
136 *
137 * @param string $content Slashed comment content.
138 * @return string
139 */
140 public function forget_blocks( $content ) {
141 $this->has_blocks = false;
142
143 return $content;
144 }
145
146 /**
147 * Let the tags these blocks save through kses, for a comment that holds them.
148 *
149 * @param array $tags Allowed tags.
150 * @param string $context The kses context.
151 * @return array
152 */
153 public function allowed_html( $tags, $context ) {
154 if ( 'pre_comment_content' !== $context || ! $this->has_blocks ) {
155 return $tags;
156 }
157
158 return array_merge(
159 $tags,
160 array(
161 'p' => array(),
162 'br' => array(),
163 'li' => array(),
164 // The one class each block saves, without which the editor reads it as invalid.
165 'blockquote' => array(
166 'cite' => true,
167 'class' => array( 'values' => array( 'wp-block-quote' ) ),
168 ),
169 'pre' => array( 'class' => array( 'values' => array( 'wp-block-code' ) ) ),
170 'ul' => array( 'class' => array( 'values' => array( 'wp-block-list' ) ) ),
171 'ol' => array( 'class' => array( 'values' => array( 'wp-block-list' ) ) ),
172 )
173 );
174 }
175
176 /**
177 * The comment's HTML without the block delimiters, and without any block the editor does not offer.
178 *
179 * @param string $content Comment content.
180 * @return string
181 */
182 public static function render( $content ) {
183 if ( ! has_blocks( $content ) ) {
184 return $content;
185 }
186
187 // With their attributes cleared, what is left of the delimiters is this shape alone.
188 return (string) preg_replace( '#<!-- /?wp:[a-z0-9/-]+ /?-->#', '', serialize_blocks( self::allowed( parse_blocks( $content ) ) ) );
189 }
190
191 /**
192 * Whether the edit-comment screen is open on a comment that holds blocks, and the
193 * block editor is on.
194 *
195 * @return bool
196 */
197 private static function is_editing_blocks() {
198 global $pagenow;
199
200 if ( ! self::is_enabled() ) {
201 return false;
202 }
203
204 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which comment the screen shows, read only.
205 $comment = 'comment.php' === $pagenow && isset( $_GET['c'] ) ? get_comment( absint( $_GET['c'] ) ) : null;
206
207 return $comment instanceof \WP_Comment && has_blocks( $comment->comment_content );
208 }
209
210 /**
211 * Load the editor onto the edit-comment screen.
212 *
213 * @return void
214 */
215 public static function enqueue_admin() {
216 if ( ! self::is_editing_blocks() ) {
217 return;
218 }
219
220 Assets::register_script(
221 'jetpack-comments-admin',
222 '../../build/admin.js',
223 __FILE__,
224 array(
225 'in_footer' => true,
226 'strategy' => 'defer',
227 'enqueue' => true,
228 )
229 );
230
231 $labels = array(
232 'blockTools' => __( 'Block tools', 'jetpack-comments' ),
233 'addBlock' => __( 'Add block', 'jetpack-comments' ),
234 );
235
236 wp_add_inline_script(
237 'jetpack-comments-admin',
238 'window.jetpackCommentsEditorLocale = ' . wp_json_encode( self::locale_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';'
239 . 'window.jetpackCommentsEditorLabels = ' . wp_json_encode( $labels, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
240 'before'
241 );
242 }
243
244 /**
245 * The allowed blocks, at every depth.
246 *
247 * @param array $blocks Parsed blocks.
248 * @return array
249 */
250 private static function allowed( array $blocks ) {
251 $kept = array();
252
253 foreach ( $blocks as $block ) {
254 // A null name is the markup between blocks, which kses sees like any other.
255 if ( null !== $block['blockName'] && ! in_array( $block['blockName'], array( 'core/paragraph', 'core/list', 'core/list-item', 'core/quote', 'core/code' ), true ) ) {
256 continue;
257 }
258
259 // The markup carries everything these blocks draw; attributes only matter to the editor.
260 $block['attrs'] = array();
261
262 $inner = $block['innerBlocks'];
263 $content = $block['innerContent'];
264 $block['innerBlocks'] = array();
265 $block['innerContent'] = array();
266
267 // Each null in innerContent marks where the next inner block goes.
268 foreach ( $content as $chunk ) {
269 if ( null !== $chunk ) {
270 $block['innerContent'][] = $chunk;
271 continue;
272 }
273
274 $child = self::allowed( array( array_shift( $inner ) ) );
275 if ( $child ) {
276 $block['innerBlocks'][] = $child[0];
277 $block['innerContent'][] = null;
278 }
279 }
280
281 $kept[] = $block;
282 }
283
284 return $kept;
285 }
286 }
287