@automattic/newspack-blocks 4.28.0 → 4.28.1-hotfix-blocks-input-validation.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/includes/class-modal-checkout.php +78 -2
- package/newspack-blocks.php +2 -2
- package/package.json +1 -1
- package/src/blocks/author-list/class-wp-rest-newspack-author-list-controller.php +12 -1
- package/src/blocks/author-list/view.php +1 -1
- package/src/blocks/author-profile/class-wp-rest-newspack-authors-controller.php +44 -0
- package/src/blocks/iframe/class-wp-rest-newspack-iframe-controller.php +138 -7
- package/src/modal-checkout/templates/thankyou.php +14 -1
- package/vendor/composer/installed.php +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## @automattic/newspack-blocks [4.28.1-hotfix-blocks-input-validation.2](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.28.1-hotfix-blocks-input-validation.1...newspack-blocks@4.28.1-hotfix-blocks-input-validation.2) (2026-07-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **blocks:** close after-success dead ends and tighten write checks ([12e6bd8](https://github.com/Automattic/newspack-workspace/commit/12e6bd898d28eaf4fb6a59633f99b297febc308f))
|
|
7
|
+
|
|
1
8
|
# @automattic/newspack-blocks [4.28.0](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.27.1...newspack-blocks@4.28.0) (2026-07-20)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1188,6 +1188,80 @@ final class Modal_Checkout {
|
|
|
1188
1188
|
ob_end_flush();
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* Behaviors that have somewhere to go once checkout finishes.
|
|
1193
|
+
*
|
|
1194
|
+
* Anything else leaves the reader in a modal that won't move and won't close, so it is
|
|
1195
|
+
* treated as "close the modal" instead.
|
|
1196
|
+
*
|
|
1197
|
+
* @var string[]
|
|
1198
|
+
*/
|
|
1199
|
+
const AFTER_SUCCESS_BEHAVIORS = [ 'custom', 'referrer' ];
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Reduce a post-checkout destination to one this site is willing to send readers to.
|
|
1203
|
+
*
|
|
1204
|
+
* The destination reaches the thank-you page through the request, whether it came from
|
|
1205
|
+
* the block's own settings or from the link the reader followed, and by that point the
|
|
1206
|
+
* two are indistinguishable. Core's redirect validation decides, so the destinations
|
|
1207
|
+
* this site accepts are the ones `allowed_redirect_hosts` reports. That's this site's
|
|
1208
|
+
* own host, plus whatever a publisher adds through that filter, plus anything another
|
|
1209
|
+
* plugin has added (`Newspack\NRH` registers one).
|
|
1210
|
+
*
|
|
1211
|
+
* @param string $url The requested destination.
|
|
1212
|
+
*
|
|
1213
|
+
* @return string The destination, or an empty string if it is not allowed.
|
|
1214
|
+
*/
|
|
1215
|
+
public static function sanitize_after_success_url( $url ) {
|
|
1216
|
+
$url = sanitize_url( (string) $url );
|
|
1217
|
+
|
|
1218
|
+
if ( empty( $url ) ) {
|
|
1219
|
+
return '';
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Core compares hosts case-sensitively; host names aren't. Normalise first so this
|
|
1223
|
+
// site's own host typed in capitals isn't read as somewhere else.
|
|
1224
|
+
$host = wp_parse_url( $url, PHP_URL_HOST );
|
|
1225
|
+
if ( $host && strtolower( $host ) !== $host ) {
|
|
1226
|
+
$url = str_replace( '://' . $host, '://' . strtolower( $host ), $url );
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return (string) wp_validate_redirect( $url, '' );
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Drop an after-success behavior the reader can't act on.
|
|
1234
|
+
*
|
|
1235
|
+
* A behavior with nowhere to go renders a modal that neither navigates nor closes, so
|
|
1236
|
+
* the destination, the behavior and its button label all fall away together and the
|
|
1237
|
+
* reader gets the ordinary "close" button instead of one labelled for a page they
|
|
1238
|
+
* won't be taken to.
|
|
1239
|
+
*
|
|
1240
|
+
* @param array $params After-success params.
|
|
1241
|
+
*
|
|
1242
|
+
* @return array The params, less any the reader can't act on.
|
|
1243
|
+
*/
|
|
1244
|
+
public static function filter_after_success_params( $params ) {
|
|
1245
|
+
$behavior = $params['after_success_behavior'] ?? '';
|
|
1246
|
+
|
|
1247
|
+
if ( '' === $behavior ) {
|
|
1248
|
+
return $params;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
$is_unknown = ! in_array( $behavior, self::AFTER_SUCCESS_BEHAVIORS, true );
|
|
1252
|
+
$has_nowhere = 'custom' === $behavior && empty( $params['after_success_url'] );
|
|
1253
|
+
|
|
1254
|
+
if ( $is_unknown || $has_nowhere ) {
|
|
1255
|
+
unset(
|
|
1256
|
+
$params['after_success_behavior'],
|
|
1257
|
+
$params['after_success_url'],
|
|
1258
|
+
$params['after_success_button_label']
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return $params;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1191
1265
|
/**
|
|
1192
1266
|
* Get after success button params.
|
|
1193
1267
|
*/
|
|
@@ -1201,14 +1275,16 @@ final class Modal_Checkout {
|
|
|
1201
1275
|
\wp_parse_str( $referrer_query, $request_params );
|
|
1202
1276
|
}
|
|
1203
1277
|
}
|
|
1204
|
-
|
|
1278
|
+
$params = array_filter(
|
|
1205
1279
|
[
|
|
1206
1280
|
'after_success_behavior' => isset( $request_params['after_success_behavior'] ) ? sanitize_text_field( wp_unslash( $request_params['after_success_behavior'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
1207
|
-
'after_success_url' => isset( $request_params['after_success_url'] ) ?
|
|
1281
|
+
'after_success_url' => isset( $request_params['after_success_url'] ) ? self::sanitize_after_success_url( wp_unslash( $request_params['after_success_url'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
1208
1282
|
'after_success_button_label' => isset( $request_params['after_success_button_label'] ) ? sanitize_text_field( wp_unslash( $request_params['after_success_button_label'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
1209
1283
|
'action_type' => isset( $request_params['action_type'] ) ? sanitize_text_field( wp_unslash( $request_params['action_type'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
1210
1284
|
]
|
|
1211
1285
|
);
|
|
1286
|
+
|
|
1287
|
+
return self::filter_after_success_params( $params );
|
|
1212
1288
|
}
|
|
1213
1289
|
|
|
1214
1290
|
/**
|
package/newspack-blocks.php
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Author URI: https://newspack.com/
|
|
8
8
|
* Text Domain: newspack-blocks
|
|
9
9
|
* Domain Path: /languages
|
|
10
|
-
* Version: 4.28.
|
|
10
|
+
* Version: 4.28.1-hotfix-blocks-input-validation.2
|
|
11
11
|
*
|
|
12
12
|
* @package Newspack_Blocks
|
|
13
13
|
*/
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
define( 'NEWSPACK_BLOCKS__PLUGIN_FILE', __FILE__ );
|
|
16
16
|
define( 'NEWSPACK_BLOCKS__BLOCKS_DIRECTORY', 'dist/' );
|
|
17
17
|
define( 'NEWSPACK_BLOCKS__PLUGIN_DIR', plugin_dir_path( NEWSPACK_BLOCKS__PLUGIN_FILE ) );
|
|
18
|
-
define( 'NEWSPACK_BLOCKS__VERSION', '4.28.
|
|
18
|
+
define( 'NEWSPACK_BLOCKS__VERSION', '4.28.1-hotfix-blocks-input-validation.2' );
|
|
19
19
|
|
|
20
20
|
require_once NEWSPACK_BLOCKS__PLUGIN_DIR . 'includes/class-newspack-blocks.php';
|
|
21
21
|
require_once NEWSPACK_BLOCKS__PLUGIN_DIR . 'includes/class-newspack-blocks-api.php';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/newspack-blocks",
|
|
3
|
-
"version": "4.28.
|
|
3
|
+
"version": "4.28.1-hotfix-blocks-input-validation.2",
|
|
4
4
|
"author": "Automattic",
|
|
5
5
|
"description": "=== Newspack Blocks === Contributors: (this should be a list of wordpress.org userid's) Donate link: https://example.com/ Tags: comments, spam Requires at least: 4.5 Tested up to: 5.1.1 Stable tag: 0.1.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
class WP_REST_Newspack_Author_List_Controller extends WP_REST_Newspack_Authors_Controller {
|
|
13
13
|
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Fields returned when a caller doesn't ask for a specific set.
|
|
17
|
+
*
|
|
18
|
+
* @var string[]
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_FIELDS = [ 'id', 'name', 'bio', 'email', 'social', 'avatar', 'url' ];
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Constructs the controller.
|
|
17
24
|
*
|
|
@@ -136,6 +143,10 @@ class WP_REST_Newspack_Author_List_Controller extends WP_REST_Newspack_Authors_C
|
|
|
136
143
|
$options['fields'] = explode( ',', $request->get_param( 'fields' ) );
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
// Restricted here rather than in get_all_authors(), which also renders the block on
|
|
147
|
+
// the front end for visitors and must keep returning whatever a publisher publishes.
|
|
148
|
+
$options['fields'] = self::restrict_fields( $options['fields'] ?? self::DEFAULT_FIELDS );
|
|
149
|
+
|
|
139
150
|
$combined_authors = $this->get_all_authors( $options );
|
|
140
151
|
$response = new \WP_REST_Response( $combined_authors );
|
|
141
152
|
$response->header( 'x-wp-total', count( $combined_authors ) );
|
|
@@ -157,7 +168,7 @@ class WP_REST_Newspack_Author_List_Controller extends WP_REST_Newspack_Authors_C
|
|
|
157
168
|
'avatar_hide_default' => false,
|
|
158
169
|
'exclude' => [], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
|
|
159
170
|
'exclude_empty' => false,
|
|
160
|
-
'fields' =>
|
|
171
|
+
'fields' => self::DEFAULT_FIELDS,
|
|
161
172
|
'per_page' => 10,
|
|
162
173
|
];
|
|
163
174
|
$options = wp_parse_args( $options, $default_options );
|
|
@@ -57,7 +57,7 @@ function newspack_blocks_render_block_author_list( $attributes ) {
|
|
|
57
57
|
'author_type' => $author_type,
|
|
58
58
|
'author_roles' => $author_roles,
|
|
59
59
|
'exclude' => $attributes['exclude'], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
|
|
60
|
-
'fields' =>
|
|
60
|
+
'fields' => WP_REST_Newspack_Author_List_Controller::DEFAULT_FIELDS,
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
if ( $exclude_empty ) {
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
|
|
13
13
|
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Fields carrying contact details that WordPress reserves for people who manage users.
|
|
17
|
+
*
|
|
18
|
+
* These endpoints are open to anyone who can edit posts, which is a much wider audience
|
|
19
|
+
* than core allows to read an address.
|
|
20
|
+
*/
|
|
21
|
+
const USER_MANAGEMENT_FIELDS = [ 'email' ];
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* Constructs the controller.
|
|
17
25
|
*
|
|
@@ -22,6 +30,41 @@ class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
|
|
|
22
30
|
$this->rest_base = 'authors';
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Whether the current user may read author contact details.
|
|
35
|
+
*
|
|
36
|
+
* Matches the capabilities core requires before exposing a user's address.
|
|
37
|
+
*
|
|
38
|
+
* @return boolean
|
|
39
|
+
*/
|
|
40
|
+
public static function can_read_user_management_fields() {
|
|
41
|
+
// Keep `list_users` first: on multisite, core maps `edit_users` through
|
|
42
|
+
// `manage_network_users`, so a site administrator passes only on `list_users`.
|
|
43
|
+
return current_user_can( 'list_users' ) || current_user_can( 'edit_users' );
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Drop any requested fields the current user is not allowed to read.
|
|
48
|
+
*
|
|
49
|
+
* Applied per request rather than inside the formatting helpers, which also render the
|
|
50
|
+
* front-end author blocks for visitors and must keep returning whatever a publisher has
|
|
51
|
+
* chosen to publish there.
|
|
52
|
+
*
|
|
53
|
+
* `false` is the helpers' "every field" convention, used by the front-end blocks; it is
|
|
54
|
+
* passed through untouched so those renders are unaffected.
|
|
55
|
+
*
|
|
56
|
+
* @param array|false $fields Requested fields, or false for every field.
|
|
57
|
+
*
|
|
58
|
+
* @return array|false Fields the current user may receive.
|
|
59
|
+
*/
|
|
60
|
+
public static function restrict_fields( $fields ) {
|
|
61
|
+
if ( ! is_array( $fields ) || self::can_read_user_management_fields() ) {
|
|
62
|
+
return $fields;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return array_values( array_diff( $fields, self::USER_MANAGEMENT_FIELDS ) );
|
|
66
|
+
}
|
|
67
|
+
|
|
25
68
|
/**
|
|
26
69
|
* Registers the necessary REST API routes.
|
|
27
70
|
*
|
|
@@ -84,6 +127,7 @@ class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
|
|
|
84
127
|
$per_page = ! empty( $request->get_param( 'perPage' ) ) ? $request->get_param( 'perPage' ) : 10; // Number of results to return per page. This is applied to each query, so the actual number of results returned may be up to 2x this number.
|
|
85
128
|
$avatar_hide_default = ! empty( $request->get_param( 'avatar_hide_default' ) ) ? true : false; // Hide the default avatar if the user has no custom avatar.
|
|
86
129
|
$fields = ! empty( $request->get_param( 'fields' ) ) ? explode( ',', $request->get_param( 'fields' ) ) : [ 'id' ]; // Fields to get. Will return at least id.
|
|
130
|
+
$fields = self::restrict_fields( $fields );
|
|
87
131
|
$include = ! empty( $request->get_param( 'include' ) ) ? explode( ',', $request->get_param( 'include' ) ) : null; // Fetch authors by multiple IDs.
|
|
88
132
|
$post_id = ! empty( $request->get_param( 'post_id' ) ) ? $request->get_param( 'post_id' ) : 0; // Fetch authors for a specific post (contextual mode).
|
|
89
133
|
|
|
@@ -246,6 +246,100 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
|
|
|
246
246
|
return in_array( $mime_type, array_keys( self::iframe_archive_accepted_file_mimes() ), true );
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Reduce a caller-supplied name to a single safe path segment.
|
|
251
|
+
*
|
|
252
|
+
* Used for names this plugin generates but builds out of caller-supplied parts, such as
|
|
253
|
+
* the destination folder derived from an attachment title.
|
|
254
|
+
*
|
|
255
|
+
* @param string $segment The name to reduce.
|
|
256
|
+
*
|
|
257
|
+
* @return string|false The safe segment, or false if nothing usable is left.
|
|
258
|
+
*/
|
|
259
|
+
private function sanitize_path_segment( $segment ) {
|
|
260
|
+
$segment = sanitize_file_name( basename( str_replace( '\\', '/', (string) $segment ) ) );
|
|
261
|
+
|
|
262
|
+
if ( '' === $segment || '.' === $segment || '..' === $segment ) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return $segment;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Reduce a caller-supplied relative path to safe segments, keeping subdirectories.
|
|
271
|
+
*
|
|
272
|
+
* Archive entries may legitimately sit in subdirectories and are referenced by name from
|
|
273
|
+
* the archive's own markup, so the segments are checked rather than rewritten: renaming
|
|
274
|
+
* them would break those references. Anything that could point outside the destination —
|
|
275
|
+
* an upward segment, an absolute path, a drive letter or a null byte — is refused
|
|
276
|
+
* outright rather than stripped, since stripping is what makes layered payloads such as
|
|
277
|
+
* `....//` work.
|
|
278
|
+
*
|
|
279
|
+
* @param string $path The relative path to reduce.
|
|
280
|
+
*
|
|
281
|
+
* @return string|false The safe relative path, or false if it is not usable.
|
|
282
|
+
*/
|
|
283
|
+
private function sanitize_relative_path( $path ) {
|
|
284
|
+
$path = str_replace( '\\', '/', (string) $path );
|
|
285
|
+
|
|
286
|
+
if ( '' === $path || false !== strpos( $path, "\0" ) ) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Absolute paths, including Windows drive letters.
|
|
291
|
+
if ( '/' === $path[0] || preg_match( '#^[a-zA-Z]:#', $path ) ) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
$segments = [];
|
|
296
|
+
foreach ( explode( '/', $path ) as $segment ) {
|
|
297
|
+
if ( '' === $segment || '.' === $segment ) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if ( '..' === $segment ) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
$segments[] = $segment;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return empty( $segments ) ? false : implode( '/', $segments );
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Confirm that a destination resolves inside a directory.
|
|
311
|
+
*
|
|
312
|
+
* A last check on the resolved paths, so that a symlink or a gap in the checks above
|
|
313
|
+
* still cannot place a file outside the directory. The destination itself need not
|
|
314
|
+
* exist yet: the deepest part of it that does exist is what gets resolved, which is
|
|
315
|
+
* what lets this run before anything is created.
|
|
316
|
+
*
|
|
317
|
+
* @param string $directory Absolute path to the directory the write must stay inside.
|
|
318
|
+
* @param string $destination Absolute path to the intended destination.
|
|
319
|
+
*
|
|
320
|
+
* @return boolean
|
|
321
|
+
*/
|
|
322
|
+
private function is_inside_directory( $directory, $destination ) {
|
|
323
|
+
$directory = realpath( $directory );
|
|
324
|
+
|
|
325
|
+
if ( ! $directory ) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
$existing = dirname( $destination );
|
|
330
|
+
while ( ! file_exists( $existing ) && dirname( $existing ) !== $existing ) {
|
|
331
|
+
$existing = dirname( $existing );
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
$resolved = realpath( $existing );
|
|
335
|
+
|
|
336
|
+
if ( ! $resolved ) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return 0 === strpos( trailingslashit( $resolved ), trailingslashit( $directory ) );
|
|
341
|
+
}
|
|
342
|
+
|
|
249
343
|
/**
|
|
250
344
|
* Receive the iframe content, handle it, and returns the URL and the folder name.
|
|
251
345
|
*
|
|
@@ -363,7 +457,11 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
|
|
|
363
457
|
* @return WP_REST_Response
|
|
364
458
|
*/
|
|
365
459
|
private function process_iframe_document( $request, $document_filename, $media_source_path ) {
|
|
366
|
-
|
|
460
|
+
// The filename is built from the source attachment's title, so it is only a name once
|
|
461
|
+
// it has been reduced to a single segment.
|
|
462
|
+
$document_filename = $this->sanitize_path_segment( $document_filename );
|
|
463
|
+
|
|
464
|
+
if ( ! $document_filename || ! $this->validate_file_extension( $document_filename, false ) ) {
|
|
367
465
|
return new WP_Error(
|
|
368
466
|
'newspack_blocks',
|
|
369
467
|
sprintf(
|
|
@@ -393,6 +491,14 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
|
|
|
393
491
|
}
|
|
394
492
|
}
|
|
395
493
|
|
|
494
|
+
if ( ! $this->is_inside_directory( $iframe_upload_dir, $iframe_path ) ) {
|
|
495
|
+
return new WP_Error(
|
|
496
|
+
'newspack_blocks',
|
|
497
|
+
__( 'Could not upload your document.', 'newspack-blocks' ),
|
|
498
|
+
[ 'status' => '400' ]
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
396
502
|
// Copy uploaded document file to destination.
|
|
397
503
|
if ( copy( $media_source_path, $iframe_path ) ) {
|
|
398
504
|
return [
|
|
@@ -434,14 +540,25 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
|
|
|
434
540
|
}
|
|
435
541
|
global $wp_filesystem;
|
|
436
542
|
|
|
543
|
+
// The folder is built from the source attachment's title, so it is only a name once it
|
|
544
|
+
// has been reduced to a single segment.
|
|
545
|
+
$iframe_folder = $this->sanitize_path_segment( $iframe_folder );
|
|
546
|
+
if ( ! $iframe_folder ) {
|
|
547
|
+
return $invalid_file_error;
|
|
548
|
+
}
|
|
549
|
+
|
|
437
550
|
$wp_upload_dir = wp_upload_dir();
|
|
438
551
|
$iframe_upload_dir = $wp_upload_dir['path'] . self::IFRAME_UPLOAD_DIR;
|
|
439
552
|
$iframe_path = trailingslashit( $iframe_upload_dir . $iframe_folder );
|
|
440
553
|
$data = $request->get_body_params();
|
|
441
554
|
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
555
|
+
// Create this archive's own directory up front, so each entry's destination can be
|
|
556
|
+
// resolved against it before anything is written.
|
|
557
|
+
if ( ! file_exists( $iframe_path ) ) {
|
|
558
|
+
wp_mkdir_p( $iframe_path );
|
|
559
|
+
}
|
|
560
|
+
if ( ! $this->is_inside_directory( $iframe_upload_dir, $iframe_path ) ) {
|
|
561
|
+
return $invalid_file_error;
|
|
445
562
|
}
|
|
446
563
|
|
|
447
564
|
// Extract files from archive.
|
|
@@ -464,10 +581,24 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
|
|
|
464
581
|
}
|
|
465
582
|
$stat = $zip->statIndex( $file_index );
|
|
466
583
|
if ( $stat && isset( $stat['name'] ) ) {
|
|
467
|
-
|
|
468
|
-
|
|
584
|
+
// An entry names its own destination, so it decides where the write lands.
|
|
585
|
+
$entry_relative_path = $this->sanitize_relative_path( $stat['name'] );
|
|
586
|
+
if ( ! $entry_relative_path ) {
|
|
587
|
+
return $invalid_file_error;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
$destination = $iframe_path . $entry_relative_path;
|
|
591
|
+
|
|
592
|
+
// Checked before anything is created, so a destination outside this archive's
|
|
593
|
+
// own folder never gets a directory made for it.
|
|
594
|
+
if ( ! $this->is_inside_directory( $iframe_path, $destination ) ) {
|
|
595
|
+
return $invalid_file_error;
|
|
469
596
|
}
|
|
470
|
-
|
|
597
|
+
if ( ! file_exists( dirname( $destination ) ) ) {
|
|
598
|
+
wp_mkdir_p( dirname( $destination ) );
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
$put = $wp_filesystem->put_contents( $destination, $contents );
|
|
471
602
|
if ( ! $put ) {
|
|
472
603
|
return $invalid_file_error;
|
|
473
604
|
}
|
|
@@ -47,9 +47,22 @@ if ( ! $is_valid ) {
|
|
|
47
47
|
|
|
48
48
|
$is_success = ! $order->has_status( 'failed' );
|
|
49
49
|
$after_success_behavior = isset( $_GET['after_success_behavior'] ) ? \sanitize_text_field( \wp_unslash( $_GET['after_success_behavior'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
50
|
-
$after_success_url = isset( $_GET['after_success_url'] ) ? esc_url( \sanitize_url( \wp_unslash( $_GET['after_success_url'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
50
|
+
$after_success_url = isset( $_GET['after_success_url'] ) ? esc_url( \Newspack_Blocks\Modal_Checkout::sanitize_after_success_url( \sanitize_url( \wp_unslash( $_GET['after_success_url'] ) ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
51
51
|
$after_success_label = isset( $_GET['after_success_button_label'] ) ? \sanitize_text_field( \wp_unslash( $_GET['after_success_button_label'] ) ) : \Newspack_Blocks\Modal_Checkout::get_modal_checkout_labels( 'after_success' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
52
52
|
$checkout_data = Checkout_Data::get_checkout_data( $order );
|
|
53
|
+
|
|
54
|
+
// A behavior the reader can't act on falls back to closing the modal, so they get a
|
|
55
|
+
// finished checkout rather than a button that goes nowhere.
|
|
56
|
+
$after_success_params = \Newspack_Blocks\Modal_Checkout::filter_after_success_params(
|
|
57
|
+
[
|
|
58
|
+
'after_success_behavior' => $after_success_behavior,
|
|
59
|
+
'after_success_url' => $after_success_url,
|
|
60
|
+
'after_success_button_label' => $after_success_label,
|
|
61
|
+
]
|
|
62
|
+
);
|
|
63
|
+
$after_success_behavior = $after_success_params['after_success_behavior'] ?? '';
|
|
64
|
+
$after_success_url = $after_success_params['after_success_url'] ?? '';
|
|
65
|
+
$after_success_label = $after_success_params['after_success_button_label'] ?? \Newspack_Blocks\Modal_Checkout::get_modal_checkout_labels( 'after_success' );
|
|
53
66
|
?>
|
|
54
67
|
<div class="woocommerce-order">
|
|
55
68
|
<?php if ( $is_success ) : ?>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
'name' => 'automattic/newspack-blocks',
|
|
4
4
|
'pretty_version' => 'dev-main',
|
|
5
5
|
'version' => 'dev-main',
|
|
6
|
-
'reference' => '
|
|
6
|
+
'reference' => '12e6bd898d28eaf4fb6a59633f99b297febc308f',
|
|
7
7
|
'type' => 'wordpress-plugin',
|
|
8
8
|
'install_path' => __DIR__ . '/../../',
|
|
9
9
|
'aliases' => array(),
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
'automattic/newspack-blocks' => array(
|
|
14
14
|
'pretty_version' => 'dev-main',
|
|
15
15
|
'version' => 'dev-main',
|
|
16
|
-
'reference' => '
|
|
16
|
+
'reference' => '12e6bd898d28eaf4fb6a59633f99b297febc308f',
|
|
17
17
|
'type' => 'wordpress-plugin',
|
|
18
18
|
'install_path' => __DIR__ . '/../../',
|
|
19
19
|
'aliases' => array(),
|