Completed
Push — renovate/slack-web-api-5.x ( f1014a...4f2b74 )
by
unknown
30:35 queued 23:31
created

Jetpack_WPCOM_Block_Editor::init()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * WordPress.com Block Editor
4
 * Allow new block editor posts to be composed on WordPress.com.
5
 * This is auto-loaded as of Jetpack v7.4 for sites connected to WordPress.com only.
6
 *
7
 * @package Jetpack
8
 */
9
10
/**
11
 * WordPress.com Block editor for Jetpack
12
 */
13
class Jetpack_WPCOM_Block_Editor {
14
	/**
15
	 * ID of the user who signed the nonce.
16
	 *
17
	 * @var int
18
	 */
19
	private $nonce_user_id;
20
21
	/**
22
	 * Singleton
23
	 */
24
	public static function init() {
25
		static $instance = false;
26
27
		if ( ! $instance ) {
28
			$instance = new Jetpack_WPCOM_Block_Editor();
29
		}
30
31
		return $instance;
32
	}
33
34
	/**
35
	 * Jetpack_WPCOM_Block_Editor constructor.
36
	 */
37
	private function __construct() {
38
		if ( $this->is_iframed_block_editor() ) {
39
			add_action( 'admin_init', array( $this, 'disable_send_frame_options_header' ), 9 );
40
			add_filter( 'admin_body_class', array( $this, 'add_iframed_body_class' ) );
41
		}
42
43
		add_filter( 'jetpack_auth_cookie_samesite', array( $this, 'set_samesite_auth_cookie' ) );
44
45
		add_action( 'login_init', array( $this, 'allow_block_editor_login' ), 1 );
46
		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ), 9 );
47
		add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
48
		add_filter( 'mce_external_plugins', array( $this, 'add_tinymce_plugins' ) );
49
	}
50
51
	/**
52
	 * Checks if we are embedding the block editor in an iframe in WordPress.com.
53
	 *
54
	 * @return bool Whether the current request is from the iframed block editor.
55
	 */
56
	public function is_iframed_block_editor() {
57
		global $pagenow;
58
59
		// phpcs:ignore WordPress.Security.NonceVerification
60
		return ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) && ! empty( $_GET['frame-nonce'] );
61
	}
62
63
	/**
64
	 * Prevents frame options header from firing if this is a whitelisted iframe request.
65
	 */
66
	public function disable_send_frame_options_header() {
67
		// phpcs:ignore WordPress.Security.NonceVerification
68
		if ( $this->framing_allowed( $_GET['frame-nonce'] ) ) {
69
			remove_action( 'admin_init', 'send_frame_options_header' );
70
		}
71
	}
72
73
	/**
74
	 * Adds custom admin body class if this is a whitelisted iframe request.
75
	 *
76
	 * @param string $classes Admin body classes.
77
	 * @return string
78
	 */
79
	public function add_iframed_body_class( $classes ) {
80
		// phpcs:ignore WordPress.Security.NonceVerification
81
		if ( $this->framing_allowed( $_GET['frame-nonce'] ) ) {
82
			$classes .= ' is-iframed ';
83
		}
84
85
		return $classes;
86
	}
87
88
	/**
89
	 * Allows to iframe the login page if a user is logged out
90
	 * while trying to access the block editor from wordpress.com.
91
	 */
92
	public function allow_block_editor_login() {
93
		// phpcs:ignore WordPress.Security.NonceVerification
94
		if ( empty( $_REQUEST['redirect_to'] ) ) {
95
			return;
96
		}
97
98
		// phpcs:ignore WordPress.Security.NonceVerification
99
		$query = wp_parse_url( urldecode( $_REQUEST['redirect_to'] ), PHP_URL_QUERY );
100
		$args  = wp_parse_args( $query );
101
102
		// Check nonce and make sure this is a Gutenframe request.
103
		if ( ! empty( $args['frame-nonce'] ) && $this->framing_allowed( $args['frame-nonce'] ) ) {
104
105
			// If SSO is active, we'll let WordPress.com handle authentication...
106
			if ( Jetpack::is_module_active( 'sso' ) ) {
107
				// ...but only if it's not an Atomic site. They already do that.
108
				if ( ! jetpack_is_atomic_site() ) {
109
					add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true' );
110
				}
111
			} else {
112
				$_REQUEST['interim-login'] = true;
113
				add_action( 'wp_login', array( $this, 'do_redirect' ) );
114
				add_action( 'login_form', array( $this, 'add_login_html' ) );
115
				add_filter( 'wp_login_errors', array( $this, 'add_login_message' ) );
116
				remove_action( 'login_init', 'send_frame_options_header' );
117
				wp_add_inline_style( 'login', '.interim-login #login{padding-top:8%}' );
118
			}
119
		}
120
	}
121
122
	/**
123
	 * Adds a login message.
124
	 *
125
	 * Intended to soften the expectation mismatch of ending up with a login screen rather than the editor.
126
	 *
127
	 * @param WP_Error $errors WP Error object.
128
	 * @return \WP_Error
129
	 */
130
	public function add_login_message( $errors ) {
131
		$errors->remove( 'expired' );
0 ignored issues
show
Bug introduced by
The method remove() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
132
		$errors->add( 'info', __( 'Before we continue, please log in to your Jetpack site.', 'jetpack' ), 'message' );
0 ignored issues
show
Bug introduced by
The method add() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
133
134
		return $errors;
135
	}
136
137
	/**
138
	 * Maintains the `redirect_to` parameter in login form links.
139
	 * Adds visual feedback of login in progress.
140
	 */
141
	public function add_login_html() {
142
		?>
143
		<input type="hidden" name="redirect_to" value="<?php echo esc_url( $_REQUEST['redirect_to'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>" />
144
		<script type="application/javascript">
145
			document.getElementById( 'loginform' ).addEventListener( 'submit' , function() {
146
				document.getElementById( 'wp-submit' ).setAttribute( 'disabled', 'disabled' );
147
				document.getElementById( 'wp-submit' ).value = '<?php echo esc_js( __( 'Logging In...', 'jetpack' ) ); ?>';
148
			} );
149
		</script>
150
		<?php
151
	}
152
153
	/**
154
	 * Does the redirect to the block editor.
155
	 */
156
	public function do_redirect() {
157
		wp_safe_redirect( $GLOBALS['redirect_to'] );
158
		exit;
159
	}
160
161
	/**
162
	 * Checks whether this is a whitelisted iframe request.
163
	 *
164
	 * @param string $nonce Nonce to verify.
165
	 * @return bool
166
	 */
167
	public function framing_allowed( $nonce ) {
168
		$verified = $this->verify_frame_nonce( $nonce, 'frame-' . Jetpack_Options::get_option( 'id' ) );
169
170
		if ( is_wp_error( $verified ) ) {
171
			wp_die( $verified ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
172
		}
173
174
		if ( $verified && ! defined( 'IFRAME_REQUEST' ) ) {
175
			define( 'IFRAME_REQUEST', true );
176
		}
177
178
		return (bool) $verified;
179
	}
180
181
	/**
182
	 * Verify that correct nonce was used with time limit.
183
	 *
184
	 * The user is given an amount of time to use the token, so therefore, since the
185
	 * UID and $action remain the same, the independent variable is the time.
186
	 *
187
	 * @param string $nonce Nonce that was used in the form to verify.
188
	 * @param string $action Should give context to what is taking place and be the same when nonce was created.
189
	 * @return boolean|WP_Error Whether the nonce is valid.
190
	 */
191
	public function verify_frame_nonce( $nonce, $action ) {
192
		if ( empty( $nonce ) ) {
193
			return false;
194
		}
195
196
		list( $expiration, $user_id, $hash ) = explode( ':', $nonce, 3 );
197
198
		$this->nonce_user_id = (int) $user_id;
199
		if ( ! $this->nonce_user_id ) {
200
			return false;
201
		}
202
203
		$token = Jetpack_Data::get_access_token( $this->nonce_user_id );
0 ignored issues
show
Documentation introduced by
$this->nonce_user_id is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The method Jetpack_Data::get_access_token() has been deprecated with message: 7.5 Use Connection_Manager instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
204
		if ( ! $token ) {
205
			return false;
206
		}
207
208
		/*
209
		 * Failures must return `false` (blocking the iframe) prior to the
210
		 * signature verification.
211
		 */
212
213
		add_filter( 'salt', array( $this, 'filter_salt' ), 10, 2 );
214
		$expected_hash = wp_hash( "$expiration|$action|{$this->nonce_user_id}", 'jetpack_frame_nonce' );
215
		remove_filter( 'salt', array( $this, 'filter_salt' ) );
216
217
		if ( ! hash_equals( $hash, $expected_hash ) ) {
218
			return false;
219
		}
220
221
		/*
222
		 * Failures may return `WP_Error` (showing an error in the iframe) after the
223
		 * signature verification passes.
224
		 */
225
226
		if ( time() > $expiration ) {
227
			return new WP_Error( 'nonce_invalid_expired', 'Expired nonce.', array( 'status' => 401 ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'nonce_invalid_expired'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
228
		}
229
230
		// Check if it matches the current user, unless they're trying to log in.
231
		if ( get_current_user_id() !== $this->nonce_user_id && ! doing_action( 'login_init' ) ) {
232
			return new WP_Error( 'nonce_invalid_user_mismatch', 'User ID mismatch.', array( 'status' => 401 ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'nonce_invalid_user_mismatch'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
233
		}
234
235
		return true;
236
	}
237
238
	/**
239
	 * Filters the WordPress salt.
240
	 *
241
	 * @param string $salt Salt for the given scheme.
242
	 * @param string $scheme Authentication scheme.
243
	 * @return string
244
	 */
245
	public function filter_salt( $salt, $scheme ) {
246
		if ( 'jetpack_frame_nonce' === $scheme ) {
247
			$token = Jetpack_Data::get_access_token( $this->nonce_user_id );
0 ignored issues
show
Documentation introduced by
$this->nonce_user_id is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The method Jetpack_Data::get_access_token() has been deprecated with message: 7.5 Use Connection_Manager instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
248
249
			if ( $token ) {
250
				$salt = $token->secret;
251
			}
252
		}
253
254
		return $salt;
255
	}
256
257
	/**
258
	 * Enqueues the WordPress.com block editor integration assets for the editor.
259
	 */
260
	public function enqueue_block_editor_assets() {
261
		$debug   = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
262
		$version = gmdate( 'Ymd' );
263
264
		wp_enqueue_script(
265
			'wpcom-block-editor-default-editor-script',
266
			$debug
267
				? '//widgets.wp.com/wpcom-block-editor/default.editor.js?minify=false'
268
				: '//widgets.wp.com/wpcom-block-editor/default.editor.min.js',
269
			array(
270
				'jquery',
271
				'lodash',
272
				'wp-compose',
273
				'wp-data',
274
				'wp-editor',
275
				'wp-element',
276
				'wp-rich-text',
277
			),
278
			$version,
279
			true
280
		);
281
282
		wp_localize_script(
283
			'wpcom-block-editor-default-editor-script',
284
			'wpcomGutenberg',
285
			array(
286
				'switchToClassic' => array(
287
					'isVisible' => $this->is_iframed_block_editor(),
288
					'label'     => __( 'Switch to Classic Editor', 'jetpack' ),
289
					'url'       => Jetpack_Calypsoify::getInstance()->get_switch_to_classic_editor_url(),
290
				),
291
				'richTextToolbar' => array(
292
					'justify'   => __( 'Justify', 'jetpack' ),
293
					'underline' => __( 'Underline', 'jetpack' ),
294
				),
295
			)
296
		);
297
298
		if ( jetpack_is_atomic_site() ) {
299
			wp_enqueue_script(
300
				'wpcom-block-editor-wpcom-editor-script',
301
				$debug
302
					? '//widgets.wp.com/wpcom-block-editor/wpcom.editor.js?minify=false'
303
					: '//widgets.wp.com/wpcom-block-editor/wpcom.editor.min.js',
304
				array(
305
					'lodash',
306
					'wp-blocks',
307
					'wp-data',
308
					'wp-dom-ready',
309
					'wp-plugins',
310
				),
311
				$version,
312
				true
313
			);
314
		}
315
316
		if ( $this->is_iframed_block_editor() ) {
317
			wp_enqueue_script(
318
				'wpcom-block-editor-calypso-editor-script',
319
				$debug
320
					? '//widgets.wp.com/wpcom-block-editor/calypso.editor.js?minify=false'
321
					: '//widgets.wp.com/wpcom-block-editor/calypso.editor.min.js',
322
				array(
323
					'calypsoify_wpadminmods_js',
324
					'jquery',
325
					'lodash',
326
					'react',
327
					'wp-blocks',
328
					'wp-data',
329
					'wp-hooks',
330
					'wp-tinymce',
331
					'wp-url',
332
				),
333
				$version,
334
				true
335
			);
336
337
			wp_enqueue_style(
338
				'wpcom-block-editor-calypso-editor-styles',
339
				$debug
340
					? '//widgets.wp.com/wpcom-block-editor/calypso.editor.css?minify=false'
341
					: '//widgets.wp.com/wpcom-block-editor/calypso.editor.min.css',
342
				array(),
343
				$version
344
			);
345
		}
346
	}
347
348
	/**
349
	 * Enqueues the WordPress.com block editor integration assets for both editor and front-end.
350
	 */
351
	public function enqueue_block_assets() {
352
		// These styles are manually copied from //widgets.wp.com/wpcom-block-editor/default.view.css in order to
353
		// improve the performance by avoiding an extra network request to download the CSS file on every page.
354
		wp_add_inline_style( 'wp-block-library', '.has-text-align-justify{text-align:justify;}' );
355
	}
356
357
	/**
358
	 * Determines if the current $post contains a justified paragraph block.
359
	 *
360
	 * @return boolean true if justified paragraph is found, false otherwise.
361
	 */
362
	public function has_justified_block() {
363
		global $post;
364
		if ( ! $post instanceof WP_Post ) {
0 ignored issues
show
Bug introduced by
The class WP_Post does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
365
			return false;
366
		};
367
368
		if ( ! has_blocks( $post ) ) {
369
			return false;
370
		}
371
372
		return false !== strpos( $post->post_content, '<!-- wp:paragraph {"align":"justify"' );
373
	}
374
375
	/**
376
	 * Register the Tiny MCE plugins for the WordPress.com block editor integration.
377
	 *
378
	 * @param array $plugin_array An array of external Tiny MCE plugins.
379
	 * @return array External TinyMCE plugins.
380
	 */
381
	public function add_tinymce_plugins( $plugin_array ) {
382
		if ( $this->is_iframed_block_editor() ) {
383
			$debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
384
385
			$plugin_array['gutenberg-wpcom-iframe-media-modal'] = add_query_arg(
386
				'v',
387
				gmdate( 'YW' ),
388
				$debug
389
					? '//widgets.wp.com/wpcom-block-editor/calypso.tinymce.js?minify=false'
390
					: '//widgets.wp.com/wpcom-block-editor/calypso.tinymce.min.js'
391
			);
392
		}
393
394
		return $plugin_array;
395
	}
396
397
	/**
398
	 * Designates cookies for cross-site access.
399
	 *
400
	 * @return string SameSite attribute to use on auth cookies.
401
	 */
402
	public function set_samesite_auth_cookie() {
403
		return 'None';
404
	}
405
}
406
407
Jetpack_WPCOM_Block_Editor::init();
408