Completed
Push — branch-7.3 ( 8c6d38...9da697 )
by Jeremy
09:20 queued 01:10
created

Jetpack_WPCOM_Block_Editor   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 4
dl 0
loc 323
rs 8.8
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 9 2
A __construct() 0 10 2
A is_iframed_block_editor() 0 6 3
A disable_send_frame_options_header() 0 6 2
A add_iframed_body_class() 0 8 2
B allow_block_editor_login() 0 29 6
A add_login_message() 0 6 1
A add_login_html() 0 11 1
A do_redirect() 0 4 1
A framing_allowed() 0 13 4
B verify_frame_nonce() 0 46 8
A filter_salt() 0 11 3
B enqueue_scripts() 0 55 6
A add_tinymce_plugins() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like Jetpack_WPCOM_Block_Editor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_WPCOM_Block_Editor, and based on these observations, apply Extract Interface, too.

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_action( 'login_init', array( $this, 'allow_block_editor_login' ), 1 );
44
		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_scripts' ) );
45
		add_filter( 'mce_external_plugins', array( $this, 'add_tinymce_plugins' ) );
46
	}
47
48
	/**
49
	 * Checks if we are embedding the block editor in an iframe in WordPress.com.
50
	 *
51
	 * @return bool Whether the current request is from the iframed block editor.
52
	 */
53
	public function is_iframed_block_editor() {
54
		global $pagenow;
55
56
		// phpcs:ignore WordPress.Security.NonceVerification
57
		return ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) && ! empty( $_GET['frame-nonce'] );
58
	}
59
60
	/**
61
	 * Prevents frame options header from firing if this is a whitelisted iframe request.
62
	 */
63
	public function disable_send_frame_options_header() {
64
		// phpcs:ignore WordPress.Security.NonceVerification
65
		if ( $this->framing_allowed( $_GET['frame-nonce'] ) ) {
66
			remove_action( 'admin_init', 'send_frame_options_header' );
67
		}
68
	}
69
70
	/**
71
	 * Adds custom admin body class if this is a whitelisted iframe request.
72
	 *
73
	 * @param string $classes Admin body classes.
74
	 * @return string
75
	 */
76
	public function add_iframed_body_class( $classes ) {
77
		// phpcs:ignore WordPress.Security.NonceVerification
78
		if ( $this->framing_allowed( $_GET['frame-nonce'] ) ) {
79
			$classes .= ' is-iframed ';
80
		}
81
82
		return $classes;
83
	}
84
85
	/**
86
	 * Allows to iframe the login page if a user is logged out
87
	 * while trying to access the block editor from wordpress.com.
88
	 */
89
	public function allow_block_editor_login() {
90
		// phpcs:ignore WordPress.Security.NonceVerification
91
		if ( empty( $_REQUEST['redirect_to'] ) ) {
92
			return;
93
		}
94
95
		// phpcs:ignore WordPress.Security.NonceVerification
96
		$query = wp_parse_url( urldecode( $_REQUEST['redirect_to'] ), PHP_URL_QUERY );
97
		$args  = wp_parse_args( $query );
98
99
		// Check nonce and make sure this is a Gutenframe request.
100
		if ( ! empty( $args['frame-nonce'] ) && $this->framing_allowed( $args['frame-nonce'] ) ) {
101
102
			// If SSO is active, we'll let WordPress.com handle authentication...
103
			if ( Jetpack::is_module_active( 'sso' ) ) {
104
				// ...but only if it's not an Atomic site. They already do that.
105
				if ( ! jetpack_is_atomic_site() ) {
106
					add_filter( 'jetpack_sso_bypass_login_forward_wpcom', '__return_true' );
107
				}
108
			} else {
109
				$_REQUEST['interim-login'] = true;
110
				add_action( 'wp_login', array( $this, 'do_redirect' ) );
111
				add_action( 'login_form', array( $this, 'add_login_html' ) );
112
				add_filter( 'wp_login_errors', array( $this, 'add_login_message' ) );
113
				remove_action( 'login_init', 'send_frame_options_header' );
114
				wp_add_inline_style( 'login', '.interim-login #login{padding-top:8%}' );
115
			}
116
		}
117
	}
118
119
	/**
120
	 * Adds a login message.
121
	 *
122
	 * Intended to soften the expectation mismatch of ending up with a login screen rather than the editor.
123
	 *
124
	 * @param WP_Error $errors WP Error object.
125
	 * @return \WP_Error
126
	 */
127
	public function add_login_message( $errors ) {
128
		$errors->remove( 'expired' );
129
		$errors->add( 'info', __( 'Before we continue, please log in to your Jetpack site.', 'jetpack' ), 'message' );
130
131
		return $errors;
132
	}
133
134
	/**
135
	 * Maintains the `redirect_to` parameter in login form links.
136
	 * Adds visual feedback of login in progress.
137
	 */
138
	public function add_login_html() {
139
		?>
140
		<input type="hidden" name="redirect_to" value="<?php echo esc_url( $_REQUEST['redirect_to'] ); ?>" />
141
		<script type="application/javascript">
142
			document.getElementById( 'loginform' ).addEventListener( 'submit' , function() {
143
				document.getElementById( 'wp-submit' ).setAttribute( 'disabled', 'disabled' );
144
				document.getElementById( 'wp-submit' ).value = '<?php echo esc_js( __( 'Logging In...', 'jetpack' ) ); ?>';
145
			} );
146
		</script>
147
		<?php
148
	}
149
150
	/**
151
	 * Does the redirect to the block editor.
152
	 */
153
	public function do_redirect() {
154
		wp_redirect( $GLOBALS['redirect_to'] );
155
		exit;
156
	}
157
158
	/**
159
	 * Checks whether this is a whitelisted iframe request.
160
	 *
161
	 * @param string $nonce Nonce to verify.
162
	 * @return bool
163
	 */
164
	public function framing_allowed( $nonce ) {
165
		$verified = $this->verify_frame_nonce( $nonce, 'frame-' . Jetpack_Options::get_option( 'id' ) );
166
167
		if ( is_wp_error( $verified ) ) {
168
			wp_die( $verified ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
169
		}
170
171
		if ( $verified && ! defined( 'IFRAME_REQUEST' ) ) {
172
			define( 'IFRAME_REQUEST', true );
173
		}
174
175
		return (bool) $verified;
176
	}
177
178
	/**
179
	 * Verify that correct nonce was used with time limit.
180
	 *
181
	 * The user is given an amount of time to use the token, so therefore, since the
182
	 * UID and $action remain the same, the independent variable is the time.
183
	 *
184
	 * @param string $nonce Nonce that was used in the form to verify.
185
	 * @param string $action Should give context to what is taking place and be the same when nonce was created.
186
	 * @return boolean|WP_Error Whether the nonce is valid.
187
	 */
188
	public function verify_frame_nonce( $nonce, $action ) {
189
		if ( empty( $nonce ) ) {
190
			return false;
191
		}
192
193
		list( $expiration, $user_id, $hash ) = explode( ':', $nonce, 3 );
194
195
		$this->nonce_user_id = (int) $user_id;
196
		if ( ! $this->nonce_user_id ) {
197
			return false;
198
		}
199
200
		$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...
201
		if ( ! $token ) {
202
			return false;
203
		}
204
205
		/*
206
		 * Failures must return `false` (blocking the iframe) prior to the
207
		 * signature verification.
208
		 */
209
210
		add_filter( 'salt', array( $this, 'filter_salt' ), 10, 2 );
211
		$expected_hash = wp_hash( "$expiration|$action|{$this->nonce_user_id}", 'jetpack_frame_nonce' );
212
		remove_filter( 'salt', array( $this, 'filter_salt' ) );
213
214
		if ( ! hash_equals( $hash, $expected_hash ) ) {
215
			return false;
216
		}
217
218
		/*
219
		 * Failures may return `WP_Error` (showing an error in the iframe) after the
220
		 * signature verification passes.
221
		 */
222
223
		if ( time() > $expiration ) {
224
			return new WP_Error( 'nonce_invalid_expired', 'Expired nonce.', array( 'status' => 401 ) );
225
		}
226
227
		// Check if it matches the current user, unless they're trying to log in.
228
		if ( get_current_user_id() !== $this->nonce_user_id && ! doing_action( 'login_init' ) ) {
229
			return new WP_Error( 'nonce_invalid_user_mismatch', 'User ID mismatch.', array( 'status' => 401 ) );
230
		}
231
232
		return true;
233
	}
234
235
	/**
236
	 * Filters the WordPress salt.
237
	 *
238
	 * @param string $salt Salt for the given scheme.
239
	 * @param string $scheme Authentication scheme.
240
	 * @return string
241
	 */
242
	public function filter_salt( $salt, $scheme ) {
243
		if ( 'jetpack_frame_nonce' === $scheme ) {
244
			$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...
245
246
			if ( $token ) {
247
				$salt = $token->secret;
248
			}
249
		}
250
251
		return $salt;
252
	}
253
254
	/**
255
	 * Enqueue the scripts for the WordPress.com block editor integration.
256
	 */
257
	public function enqueue_scripts() {
258
		$debug   = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
259
		$version = gmdate( 'Ymd' );
260
261
		$src_common = $debug
262
			? '//widgets.wp.com/wpcom-block-editor/common.js?minify=false'
263
			: '//widgets.wp.com/wpcom-block-editor/common.min.js';
264
265
		wp_enqueue_script(
266
			'wpcom-block-editor-common',
267
			$src_common,
268
			array( 'lodash', 'wp-compose', 'wp-data', 'wp-editor', 'wp-rich-text' ),
269
			$version,
270
			true
271
		);
272
		wp_localize_script(
273
			'wpcom-block-editor-common',
274
			'wpcomGutenberg',
275
			array(
276
				'switchToClassic' => array(
277
					'isVisible' => $this->is_iframed_block_editor(),
278
					'label'     => __( 'Switch to Classic Editor', 'jetpack' ),
279
					'url'       => Jetpack_Calypsoify::getInstance()->get_switch_to_classic_editor_url(),
280
				),
281
				'richTextToolbar' => array(
282
					'justify'   => __( 'Justify', 'jetpack' ),
283
					'underline' => __( 'Underline', 'jetpack' ),
284
				),
285
			)
286
		);
287
288
		$src_styles = $debug
289
			? '//widgets.wp.com/wpcom-block-editor/common.css?minify=false'
290
			: '//widgets.wp.com/wpcom-block-editor/common.min.css';
291
		wp_enqueue_style(
292
			'wpcom-block-editor-styles',
293
			$src_styles,
294
			array(),
295
			$version
296
		);
297
298
		if ( $this->is_iframed_block_editor() ) {
299
			$src_calypso_iframe_bridge = $debug
300
				? '//widgets.wp.com/wpcom-block-editor/calypso-iframe-bridge-server.js?minify=false'
301
				: '//widgets.wp.com/wpcom-block-editor/calypso-iframe-bridge-server.min.js';
302
303
			wp_enqueue_script(
304
				'wpcom-block-editor-calypso-iframe-bridge',
305
				$src_calypso_iframe_bridge,
306
				array( 'calypsoify_wpadminmods_js', 'jquery', 'lodash', 'react', 'wp-blocks', 'wp-data', 'wp-hooks', 'wp-tinymce', 'wp-url' ),
307
				$version,
308
				true
309
			);
310
		}
311
	}
312
313
	/**
314
	 * Register the Tiny MCE plugins for the WordPress.com block editor integration.
315
	 *
316
	 * @param array $plugin_array An array of external Tiny MCE plugins.
317
	 * @return array External TinyMCE plugins.
318
	 */
319
	public function add_tinymce_plugins( $plugin_array ) {
320
		if ( $this->is_iframed_block_editor() ) {
321
			$debug               = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
322
			$src_calypso_tinymce = $debug
323
				? '//widgets.wp.com/wpcom-block-editor/calypso-tinymce.js?minify=false'
324
				: '//widgets.wp.com/wpcom-block-editor/calypso-tinymce.min.js';
325
326
			$plugin_array['gutenberg-wpcom-iframe-media-modal'] = add_query_arg(
327
				'v',
328
				gmdate( 'YW' ),
329
				$src_calypso_tinymce
330
			);
331
		}
332
333
		return $plugin_array;
334
	}
335
}
336
337
Jetpack_WPCOM_Block_Editor::init();
338