Completed
Push — add/api-site-endpoint-v1-3 ( 55dbfd...927a28 )
by Kirk
145:40 queued 138:00
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( 'init', array( $this, 'show_error_if_logged_out' ) );
40
			add_action( 'admin_init', array( $this, 'disable_send_frame_options_header' ), 9 );
41
			add_filter( 'admin_body_class', array( $this, 'add_iframed_body_class' ) );
42
		}
43
		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_scripts' ) );
44
		add_filter( 'mce_external_plugins', array( $this, 'add_tinymce_plugins' ) );
45
	}
46
47
	/**
48
	 * Checks if we are embedding the block editor in an iframe in WordPress.com.
49
	 *
50
	 * @return bool Whether the current request is from the iframed block editor.
51
	 */
52
	public function is_iframed_block_editor() {
53
		$is_calypsoify = 1 === (int) get_user_meta( get_current_user_id(), 'calypsoify', true );
54
		global $pagenow;
55
		// phpcs:ignore WordPress.Security.NonceVerification
56
		return ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) && ! empty( $_GET['frame-nonce'] ) && $is_calypsoify;
57
	}
58
59
	/**
60
	 * Shows a custom message if the user is logged out.
61
	 *
62
	 * The iframed block editor can be only embedded in WordPress.com if the user is logged
63
	 * into the Jetpack site. So we abort the default redirection to the login page (which
64
	 * cannot be embedded in a iframe) and instead we explain that we need the user to log
65
	 * into Jetpack.
66
	 */
67
	public function show_error_if_logged_out() {
68
		if ( ! get_current_user_id() ) {
69
			$login_url = wp_login_url();
70
			/* translators: %s: Login URL */
71
			wp_die( sprintf( __( 'In order to use the block editor of this Jetpack site in WordPress.com, you need to <a href="%s" target="_blank" rel="noopener noreferrer">log into</a> the Jetpack site.', 'jetpack' ), $login_url ) ); // // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
72
		}
73
	}
74
75
	/**
76
	 * Prevents frame options header from firing if this is a whitelisted iframe request.
77
	 */
78
	public function disable_send_frame_options_header() {
79
		if ( $this->framing_allowed() ) {
80
			remove_action( 'admin_init', 'send_frame_options_header' );
81
		}
82
	}
83
84
	/**
85
	 * Adds custom admin body class if this is a whitelisted iframe request.
86
	 *
87
	 * @param string $classes Admin body classes.
88
	 * @return string
89
	 */
90
	public function add_iframed_body_class( $classes ) {
91
		if ( $this->framing_allowed() ) {
92
			$classes .= ' is-iframed ';
93
		}
94
95
		return $classes;
96
	}
97
98
	/**
99
	 * Checks whether this is a whitelisted iframe request.
100
	 *
101
	 * @return bool
102
	 */
103
	public function framing_allowed() {
104
		$verified = $this->verify_frame_nonce(
105
			$_GET['frame-nonce'], // phpcs:ignore WordPress.Security.NonceVerification
106
			'frame-' . Jetpack_Options::get_option( 'id' )
107
		);
108
109
		if ( is_wp_error( $verified ) ) {
110
			wp_die( $verified ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
111
		}
112
113
		if ( $verified && ! defined( 'IFRAME_REQUEST' ) ) {
114
			define( 'IFRAME_REQUEST', true );
115
		}
116
117
		return (bool) $verified;
118
	}
119
120
	/**
121
	 * Verify that correct nonce was used with time limit.
122
	 *
123
	 * The user is given an amount of time to use the token, so therefore, since the
124
	 * UID and $action remain the same, the independent variable is the time.
125
	 *
126
	 * @param string $nonce Nonce that was used in the form to verify.
127
	 * @param string $action Should give context to what is taking place and be the same when nonce was created.
128
	 * @return boolean|WP_Error Whether the nonce is valid.
129
	 */
130
	public function verify_frame_nonce( $nonce, $action ) {
131
		if ( empty( $nonce ) ) {
132
			return false;
133
		}
134
135
		list( $expiration, $user_id, $hash ) = explode( ':', $nonce, 3 );
136
137
		$this->nonce_user_id = (int) $user_id;
138
		if ( ! $this->nonce_user_id ) {
139
			return false;
140
		}
141
142
		$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...
143
		if ( ! $token ) {
144
			return false;
145
		}
146
147
		/*
148
		 * Failures must return `false` (blocking the iframe) prior to the
149
		 * signature verification.
150
		 */
151
152
		add_filter( 'salt', array( $this, 'filter_salt' ), 10, 2 );
153
		$expected_hash = wp_hash( "$expiration|$action|{$this->nonce_user_id}", 'jetpack_frame_nonce' );
154
		remove_filter( 'salt', array( $this, 'filter_salt' ) );
155
156
		if ( ! hash_equals( $hash, $expected_hash ) ) {
157
			return false;
158
		}
159
160
		/*
161
		 * Failures may return `WP_Error` (showing an error in the iframe) after the
162
		 * signature verification passes.
163
		 */
164
165
		if ( time() > $expiration ) {
166
			return new WP_Error( 'nonce_invalid_expired', 'Expired nonce.', array( 'status' => 401 ) );
167
		}
168
169
		if ( get_current_user_id() !== $this->nonce_user_id ) {
170
			return new WP_Error( 'nonce_invalid_user_mismatch', 'User ID mismatch.', array( 'status' => 401 ) );
171
		}
172
173
		return true;
174
	}
175
176
	/**
177
	 * Filters the WordPress salt.
178
	 *
179
	 * @param string $salt Salt for the given scheme.
180
	 * @param string $scheme Authentication scheme.
181
	 * @return string
182
	 */
183
	public function filter_salt( $salt, $scheme ) {
184
		if ( 'jetpack_frame_nonce' === $scheme ) {
185
			$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...
186
187
			if ( $token ) {
188
				$salt = $token->secret;
189
			}
190
		}
191
192
		return $salt;
193
	}
194
195
	/**
196
	 * Enqueue the scripts for the WordPress.com block editor integration.
197
	 */
198
	public function enqueue_scripts() {
199
		$debug         = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
200
		$version       = gmdate( 'YW' );
201
		$is_calypsoify = 1 === (int) get_user_meta( get_current_user_id(), 'calypsoify', true );
202
203
		$src_common = $debug
204
			? '//widgets.wp.com/wpcom-block-editor/common.js?minify=false'
205
			: '//widgets.wp.com/wpcom-block-editor/common.min.js';
206
		wp_enqueue_script(
207
			'wpcom-block-editor-common',
208
			$src_common,
209
			array( 'lodash', 'wp-compose', 'wp-data', 'wp-editor', 'wp-rich-text' ),
210
			$version,
211
			true
212
		);
213
		wp_localize_script(
214
			'wpcom-block-editor-common',
215
			'wpcomGutenberg',
216
			array(
217
				'switchToClassic' => array(
218
					'isVisible' => false,
219
				),
220
				'isCalypsoify'    => $is_calypsoify,
221
				'richTextToolbar' => array(
222
					'justify'   => __( 'Justify', 'jetpack' ),
223
					'underline' => __( 'Underline', 'jetpack' ),
224
				),
225
			)
226
		);
227
		if ( $this->is_iframed_block_editor() ) {
228
			$src_calypso_iframe_bridge = $debug
229
				? '//widgets.wp.com/wpcom-block-editor/calypso-iframe-bridge-server.js?minify=false'
230
				: '//widgets.wp.com/wpcom-block-editor/calypso-iframe-bridge-server.min.js';
231
			wp_enqueue_script(
232
				'wpcom-block-editor-calypso-iframe-bridge',
233
				$src_calypso_iframe_bridge,
234
				array( 'calypsoify_wpadminmods_js', 'jquery', 'lodash', 'react', 'wp-blocks', 'wp-data', 'wp-hooks', 'wp-tinymce', 'wp-url' ),
235
				$version,
236
				true
237
			);
238
		}
239
	}
240
241
	/**
242
	 * Register the Tiny MCE plugins for the WordPress.com block editor integration.
243
	 *
244
	 * @param array $plugin_array An array of external Tiny MCE plugins.
245
	 * @return array External TinyMCE plugins.
246
	 */
247
	public function add_tinymce_plugins( $plugin_array ) {
248
		if ( $this->is_iframed_block_editor() ) {
249
			$debug               = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
250
			$src_calypso_tinymce = $debug
251
				? '//widgets.wp.com/wpcom-block-editor/calypso-tinymce.js?minify=false'
252
				: '//widgets.wp.com/wpcom-block-editor/calypso-tinymce.min.js';
253
			$plugin_array['gutenberg-wpcom-iframe-media-modal'] = add_query_arg(
254
				'v',
255
				gmdate( 'YW' ),
256
				$src_calypso_tinymce
257
			);
258
		}
259
		return $plugin_array;
260
	}
261
}
262
263
Jetpack_WPCOM_Block_Editor::init();
264