Skip to content
Merged

Release #2978

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
".",
"https://downloads.wordpress.org/plugin/ai-provider-for-openai.zip"
],
"themes": [ "./test/emptytheme" ],
"themes": [
"./test/emptytheme",
"https://downloads.wordpress.org/theme/twentytwentyone.zip"
],
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
Expand All @@ -17,7 +20,8 @@
},
"mappings": {
"wp-content/mu-plugins": "./packages/e2e-tests/mu-plugins",
"wp-content/themes/raft": "https://downloads.wordpress.org/theme/raft.zip"
"wp-content/themes/raft": "https://downloads.wordpress.org/theme/raft.zip",
"wp-content/plugins/woocommerce": "./vendor/wp-content/plugins/woocommerce"
},
"lifecycleScripts": {
"afterStart": "bash bin/e2e-tests.sh"
Expand Down
6 changes: 3 additions & 3 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions inc/class-base-css.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@ public function get_animation_css( $blocks ) {
return $style;
}

if ( ! self::has_own_css_parser() ) {
// A foreign php-css-parser release is loaded; parsing now fatals
// uncatchably at class-link time (#2942). The frontend loader serves
// the stock stylesheet instead.
error_log( '[Otter Blocks] A conflicting Sabberworm php-css-parser release is loaded; skipping animation CSS optimization and serving the stock stylesheet instead.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return $style;
}

$prepared_classes = array( ':root' );

foreach ( $classes as $class ) {
Expand Down Expand Up @@ -445,6 +453,70 @@ public function get_animation_css( $blocks ) {
return $style;
}

/**
* Check that every loaded Sabberworm\CSS symbol resolves to this plugin's copy.
*
* Another plugin can ship a different php-css-parser release under the same
* namespace. Once any of its classes loads, loading the bundled counterparts
* fatals at class-link time, and that error is not catchable.
*
* @return bool
*/
public static function has_own_css_parser() {
$own_vendor = wp_normalize_path( OTTER_BLOCKS_PATH . '/vendor/' );
$prefix = 'Sabberworm\\CSS\\';

// Reject any foreign copy already in memory before the sentinel checks
// autoload a bundled class: the parser uses more classes than the
// sentinels, and one preloaded foreign symbol poisons the process.
$declared = array_merge( get_declared_classes(), get_declared_interfaces(), get_declared_traits() );

foreach ( $declared as $declared_name ) {
// PHP class names are case-insensitive; match a foreign copy in any casing.
if ( 0 !== stripos( $declared_name, $prefix ) ) {
continue;
}

if ( ! self::is_bundled_class( $declared_name, $own_vendor ) ) {
return false;
}
}

// Entry points nothing may have loaded yet: whichever autoloader resolves
// them must serve the bundled copy.
$sentinels = array(
'\Sabberworm\CSS\Parser',
'\Sabberworm\CSS\Comment\Commentable',
'\Sabberworm\CSS\Renderable',
);

foreach ( $sentinels as $sentinel ) {
if ( ! class_exists( $sentinel ) && ! interface_exists( $sentinel ) ) {
return false;
}

if ( ! self::is_bundled_class( $sentinel, $own_vendor ) ) {
return false;
}
}

return true;
}

/**
* Check that a class, interface, or trait was loaded from this plugin's vendor directory.
*
* @param string $name Fully qualified name.
* @param string $own_vendor Normalized path of this plugin's vendor directory.
* @return bool
*/
private static function is_bundled_class( $name, $own_vendor ) {
$reflection = new \ReflectionClass( $name );
$file = $reflection->getFileName();

return false !== $file && 0 === strpos( wp_normalize_path( $file ), $own_vendor );
}

/**
* Get Animation Classes
*
Expand Down
8 changes: 7 additions & 1 deletion inc/class-blocks-animation.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,13 @@ public function frontend_load( $block_content, $block ) {
}

if ( ! self::$scripts_loaded['animation'] && strpos( $block_content, 'animated' ) ) {
if ( ! defined( 'OTTER_BLOCKS_VERSION' ) || ( defined( 'OTTER_BLOCKS_VERSION' ) && ! get_option( 'themeisle_blocks_settings_optimize_animations_css', true ) ) ) {
// Foreign-parser pages (#2942) cache post-CSS with no animation rules,
// so deliver the stock stylesheet here rather than via the parser path.
if (
! defined( 'OTTER_BLOCKS_VERSION' ) ||
! get_option( 'themeisle_blocks_settings_optimize_animations_css', true ) ||
! Base_CSS::has_own_css_parser()
) {
wp_enqueue_style( 'otter-animation' );
}

Expand Down
6 changes: 6 additions & 0 deletions inc/class-main.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public function autoload_classes() {
$classnames = apply_filters( 'otter_blocks_autoloader', $classnames );

foreach ( $classnames as $classname ) {
// A stale Composer classmap or a third-party filter can list a class that is not loadable; skip it instead of fataling the request.
if ( ! is_string( $classname ) || ! class_exists( $classname ) ) {
error_log( '[Otter Blocks] Skipped an autoload entry that could not be loaded: ' . ( is_string( $classname ) ? $classname : gettype( $classname ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
continue;
}

$classname = new $classname();

if ( method_exists( $classname, 'instance' ) ) {
Expand Down
9 changes: 4 additions & 5 deletions inc/css/class-css-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -496,18 +496,17 @@ public static function save_widgets_styles() {
public static function is_writable() {
global $wp_filesystem;
include_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();

$wp_upload_dir = wp_upload_dir( null, false );
$upload_dir = $wp_upload_dir['basedir'];

if ( ! function_exists( 'WP_Filesystem' ) ) {
return false;
}

$wp_upload_dir = wp_upload_dir( null, false );
$upload_dir = $wp_upload_dir['basedir'];

$writable = WP_Filesystem( false, $upload_dir );

return $writable && 'direct' === $wp_filesystem->method;
return $writable && $wp_filesystem instanceof \WP_Filesystem_Base && 'direct' === $wp_filesystem->method;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions inc/plugins/class-dynamic-content.php
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ public function get_content( $data ) {
return '';
}

$content = get_the_content( $data['context'] );
$content = get_the_content( null, false, $data['context'] );
$content = apply_filters( 'the_content', str_replace( ']]>', ']]>', $content ) );
return wp_kses_post( $content );
}
Expand Down Expand Up @@ -902,7 +902,7 @@ class_exists( '\Neve_Pro\Modules\Custom_Layouts\Module' )
if ( ! $post instanceof \WP_Post ) {
return $data;
}
$content = get_the_content( $data['context'] );
$content = get_the_content( null, false, $data['context'] );
if ( strpos( $content, 'data-type="postContent"' ) ) {
$key = $this->get_exception_key( $data, $post->ID );
if ( $key ) {
Expand Down
90 changes: 82 additions & 8 deletions inc/render/class-stripe-checkout-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
* Class Stripe_Checkout_Block
*/
class Stripe_Checkout_Block {
/**
* Transient prefix for the cached checkout mode of a price.
*
* @var string
*/
const PRICE_MODE_CACHE_PREFIX = 'otter_stripe_price_mode_';

/**
* Stripe API instance.
*
Expand Down Expand Up @@ -56,8 +63,8 @@ public function watch_checkout() {

$product_id = isset( $_GET['product_id'] ) ? sanitize_text_field( wp_unslash( $_GET['product_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$price_id = isset( $_GET['price_id'] ) ? sanitize_text_field( wp_unslash( $_GET['price_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$url = isset( $_GET['url'] ) ? sanitize_text_field( wp_unslash( $_GET['url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$mode = isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : 'payment'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$url = isset( $_GET['url'] ) ? sanitize_url( wp_unslash( $_GET['url'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

if ( empty( $product_id ) || empty( $price_id ) || empty( $url ) ) {
return sprintf(
Expand All @@ -66,12 +73,19 @@ public function watch_checkout() {
);
}

if ( ! hash_equals( self::get_checkout_token( $product_id, $price_id ), $token ) ) {
return sprintf(
'<div class="wp-block-themeisle-blocks-stripe-checkout"><div class="o-stripe-checkout">%s</div></div>',
__( 'An error occurred! Could not retrieve the product information!', 'otter-blocks' )
);
}

$permalink = add_query_arg(
array(
'stripe_session_id' => '{CHECKOUT_SESSION_ID}',
'product_id' => $product_id,
),
$url
$this->get_return_url( $url )
);

$session = $this->stripe_api->create_request(
Expand All @@ -85,7 +99,7 @@ public function watch_checkout() {
'quantity' => 1,
),
),
'mode' => $mode,
'mode' => $this->get_mode_for_price( $price_id ),
)
);

Expand Down Expand Up @@ -165,17 +179,22 @@ public function render( $attributes ) {
$details_markup .= '<h5>' . $currency . $amount . '</h5>';
$details_markup .= '</div>';

$mode = 'recurring' === $price['type'] ? 'subscription' : 'payment';
// A widget area or an FSE template has no permalink of its own, so fall back to the current URL.
$return_url = get_permalink();

if ( ! is_string( $return_url ) || '' === $return_url ) {
$return_url = home_url( add_query_arg( array() ) );
}

$session_url = add_query_arg(
array(
'action' => 'buy_stripe',
'product_id' => $attributes['product'],
'price_id' => $attributes['price'],
'url' => get_permalink(),
'mode' => $mode,
'url' => $return_url,
'token' => self::get_checkout_token( $attributes['product'], $attributes['price'] ),
),
get_permalink()
$return_url
);

$button_markup = '<a href="' . esc_url( $session_url ) . '">' . __( 'Checkout', 'otter-blocks' ) . '</a>';
Expand All @@ -188,6 +207,61 @@ public function render( $attributes ) {
);
}

/**
* Sign a product/price pair so the checkout can verify it was offered by a block.
*
* @param string $product_id Stripe product ID.
* @param string $price_id Stripe price ID.
* @return string
*/
public static function get_checkout_token( $product_id, $price_id ) {
return hash_hmac( 'sha256', $product_id . '|' . $price_id, wp_salt( 'otter_stripe' ) );
}

/**
* Get the URL Stripe returns the buyer to, restricted to this site.
*
* @param string $url Requested return URL.
* @return string
*/
private function get_return_url( $url ) {
$host = wp_parse_url( $url, PHP_URL_HOST );
$home = wp_parse_url( home_url(), PHP_URL_HOST );

if ( $home !== $host ) {
return home_url( '/' );
}

return $url;
}

/**
* Get the checkout session mode for a price.
*
* @param string $price_id Stripe price ID.
* @return string
*/
private function get_mode_for_price( $price_id ) {
$cache_key = self::PRICE_MODE_CACHE_PREFIX . md5( $price_id );
$cached = get_transient( $cache_key );

if ( 'payment' === $cached || 'subscription' === $cached ) {
return $cached;
}

$price = $this->stripe_api->create_request( 'price', $price_id );

if ( is_wp_error( $price ) || ! isset( $price['type'] ) ) {
return 'payment';
}

$mode = 'recurring' === $price['type'] ? 'subscription' : 'payment';

set_transient( $cache_key, $mode, WEEK_IN_SECONDS );

return $mode;
}

/**
* Format the error message.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/**
* Typed `Commentable` interface as shipped by php-css-parser 9.x.
*
* Lives in a subdirectory so WordPress does not auto-load it as an mu-plugin;
* otter-e2e-bootstrap.php requires it only while the foreign-Sabberworm
* scenario (issue #2942) is armed. Once defined, loading Otter's bundled
* untyped CSSList fatals at class-link time — exactly like a second plugin
* shipping a newer parser release.
*
* @package otter-blocks
*/

// phpcs:ignoreFile -- deliberately mirrors the upstream 9.x signatures.

namespace Sabberworm\CSS\Comment;

interface Commentable {
public function addComments( array $comments ): void;
public function getComments(): array;
public function setComments( array $comments ): void;
}
Loading
Loading