@automattic/newspack-blocks 4.28.1-hotfix-blocks-input-validation.1 → 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 CHANGED
@@ -1,12 +1,9 @@
1
- ## @automattic/newspack-blocks [4.28.1-hotfix-blocks-input-validation.1](https://github.com/Automattic/newspack-workspace/compare/newspack-blocks@4.28.0...newspack-blocks@4.28.1-hotfix-blocks-input-validation.1) (2026-07-31)
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
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * **blocks:** keep author list and checkout fallbacks intact ([53012be](https://github.com/Automattic/newspack-workspace/commit/53012be987c2001aa8a855dfbf6b857b70c4463b))
7
- * **blocks:** keep iframe imports inside the uploads directory ([a0118cc](https://github.com/Automattic/newspack-workspace/commit/a0118cc501ca0bfbb851c9e0fdb0f005702b99da))
8
- * **blocks:** restrict author contact fields to user managers ([c872eb9](https://github.com/Automattic/newspack-workspace/commit/c872eb90589d056a354228adc0a2a65afa6245d3))
9
- * **blocks:** validate post-checkout redirect destinations ([f418142](https://github.com/Automattic/newspack-workspace/commit/f418142055e14fc6e8fe82726996c7a7028452d5))
6
+ * **blocks:** close after-success dead ends and tighten write checks ([12e6bd8](https://github.com/Automattic/newspack-workspace/commit/12e6bd898d28eaf4fb6a59633f99b297febc308f))
10
7
 
11
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)
12
9
 
@@ -1188,27 +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
+
1191
1201
  /**
1192
1202
  * Reduce a post-checkout destination to one this site is willing to send readers to.
1193
1203
  *
1194
1204
  * The destination reaches the thank-you page through the request, whether it came from
1195
1205
  * the block's own settings or from the link the reader followed, and by that point the
1196
- * two are indistinguishable. Core's redirect validation decides: this site's host is
1197
- * always allowed, and a publisher who wants to send readers somewhere else adds that
1198
- * host through the standard `allowed_redirect_hosts` filter.
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).
1199
1210
  *
1200
1211
  * @param string $url The requested destination.
1201
1212
  *
1202
1213
  * @return string The destination, or an empty string if it is not allowed.
1203
1214
  */
1204
1215
  public static function sanitize_after_success_url( $url ) {
1216
+ $url = sanitize_url( (string) $url );
1217
+
1205
1218
  if ( empty( $url ) ) {
1206
1219
  return '';
1207
1220
  }
1208
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
+
1209
1229
  return (string) wp_validate_redirect( $url, '' );
1210
1230
  }
1211
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
+
1212
1265
  /**
1213
1266
  * Get after success button params.
1214
1267
  */
@@ -1225,19 +1278,13 @@ final class Modal_Checkout {
1225
1278
  $params = array_filter(
1226
1279
  [
1227
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
1228
- 'after_success_url' => isset( $request_params['after_success_url'] ) ? self::sanitize_after_success_url( sanitize_url( wp_unslash( $request_params['after_success_url'] ) ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
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
1229
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
1230
1283
  'action_type' => isset( $request_params['action_type'] ) ? sanitize_text_field( wp_unslash( $request_params['action_type'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1231
1284
  ]
1232
1285
  );
1233
1286
 
1234
- // A custom destination this site won't redirect to falls back to closing the modal, so
1235
- // the reader gets a finished checkout rather than an empty destination.
1236
- if ( 'custom' === ( $params['after_success_behavior'] ?? '' ) && empty( $params['after_success_url'] ) ) {
1237
- unset( $params['after_success_behavior'] );
1238
- }
1239
-
1240
- return $params;
1287
+ return self::filter_after_success_params( $params );
1241
1288
  }
1242
1289
 
1243
1290
  /**
@@ -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.1-hotfix-blocks-input-validation.1
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.1-hotfix-blocks-input-validation.1' );
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.1-hotfix-blocks-input-validation.1",
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": {
@@ -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' => [ 'id', 'name', 'bio', 'email', 'social', 'avatar', 'url' ],
60
+ 'fields' => WP_REST_Newspack_Author_List_Controller::DEFAULT_FIELDS,
61
61
  ];
62
62
 
63
63
  if ( $exclude_empty ) {
@@ -38,6 +38,8 @@ class WP_REST_Newspack_Authors_Controller extends WP_REST_Controller {
38
38
  * @return boolean
39
39
  */
40
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`.
41
43
  return current_user_can( 'list_users' ) || current_user_can( 'edit_users' );
42
44
  }
43
45
 
@@ -309,8 +309,10 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
309
309
  /**
310
310
  * Confirm that a destination resolves inside a directory.
311
311
  *
312
- * A last check on the resolved paths, after the destination's parent exists, so that a
313
- * symlink or a gap in the checks above still cannot place a file outside the directory.
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.
314
316
  *
315
317
  * @param string $directory Absolute path to the directory the write must stay inside.
316
318
  * @param string $destination Absolute path to the intended destination.
@@ -318,14 +320,24 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
318
320
  * @return boolean
319
321
  */
320
322
  private function is_inside_directory( $directory, $destination ) {
321
- $directory = realpath( $directory );
322
- $destination = realpath( dirname( $destination ) );
323
+ $directory = realpath( $directory );
323
324
 
324
- if ( ! $directory || ! $destination ) {
325
+ if ( ! $directory ) {
325
326
  return false;
326
327
  }
327
328
 
328
- return 0 === strpos( trailingslashit( $destination ), trailingslashit( $directory ) );
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 ) );
329
341
  }
330
342
 
331
343
  /**
@@ -540,9 +552,13 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
540
552
  $iframe_path = trailingslashit( $iframe_upload_dir . $iframe_folder );
541
553
  $data = $request->get_body_params();
542
554
 
543
- // create iframe directory if not existing.
544
- if ( ! file_exists( $iframe_upload_dir ) ) {
545
- wp_mkdir_p( $iframe_upload_dir );
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;
546
562
  }
547
563
 
548
564
  // Extract files from archive.
@@ -572,12 +588,15 @@ class WP_REST_Newspack_Iframe_Controller extends WP_REST_Controller {
572
588
  }
573
589
 
574
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;
596
+ }
575
597
  if ( ! file_exists( dirname( $destination ) ) ) {
576
598
  wp_mkdir_p( dirname( $destination ) );
577
599
  }
578
- if ( ! $this->is_inside_directory( $iframe_upload_dir, $destination ) ) {
579
- return $invalid_file_error;
580
- }
581
600
 
582
601
  $put = $wp_filesystem->put_contents( $destination, $contents );
583
602
  if ( ! $put ) {
@@ -51,11 +51,18 @@ $after_success_url = isset( $_GET['after_success_url'] ) ? esc_url( \Newspa
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
53
 
54
- // A custom destination this site won't redirect to falls back to closing the modal, so the
55
- // reader gets a finished checkout rather than an empty destination.
56
- if ( 'custom' === $after_success_behavior && empty( $after_success_url ) ) {
57
- $after_success_behavior = '';
58
- }
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' );
59
66
  ?>
60
67
  <div class="woocommerce-order">
61
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' => '53012be987c2001aa8a855dfbf6b857b70c4463b',
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' => '53012be987c2001aa8a855dfbf6b857b70c4463b',
16
+ 'reference' => '12e6bd898d28eaf4fb6a59633f99b297febc308f',
17
17
  'type' => 'wordpress-plugin',
18
18
  'install_path' => __DIR__ . '/../../',
19
19
  'aliases' => array(),