Skip to content
Draft
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
6 changes: 6 additions & 0 deletions environments/plugin-directory/.wp-env.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
"plugins": [
"../wordpress.org/public_html/wp-content/plugins/plugin-directory"
],
"mappings": {
"wp-content/env-bin": "./plugin-directory/bin"
},
"lifecycleScripts": {
"afterStart": "bash plugin-directory/bin/after-start-test.sh"
},
"config": {
"PLUGINS_TABLE_PREFIX": "wp_"
}
}
6 changes: 5 additions & 1 deletion environments/plugin-directory/bin/after-start-test.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
#
# Runs after wp-env start for the test environment.
# Installs PHPUnit 11 and Yoast polyfills in the test container.
# Installs PHPUnit 11 and Yoast polyfills, and creates the stub tables tests read.
#

CONFIG="--config plugin-directory/.wp-env.test.json"
Expand All @@ -10,3 +10,7 @@ RUN="npx wp-env $CONFIG run tests-cli"
echo "Installing PHPUnit 11 and polyfills..."
$RUN composer global require -W phpunit/phpunit:^11.0 2>&1
$RUN composer require --dev yoast/phpunit-polyfills:^4.0 --working-dir=/wordpress-phpunit 2>&1

# Create stub database tables that exist outside WordPress on production.
echo "Creating stub database tables..."
$RUN -- wp db import wp-content/env-bin/database-tables.sql
10 changes: 10 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,14 @@
<exclude name="WordPress.Security.NonceVerification.Missing" />
<exclude name="WordPress.Security.NonceVerification.Recommended" />
</rule>

<!-- Recognise project test-case bases so the FileName sniff exempts their subclasses (whose
names must match their class, not be hyphenated, so PHPUnit can discover them). -->
<rule ref="WordPress.Files.FileName">
<properties>
<property name="custom_test_classes" type="array">
<element value="Gandalf_Callback_Test_Case"/>
</property>
</properties>
</rule>
</ruleset>
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ static function display() {
}

/**
* Display the release cooldown status and (for reviewers) a force-release control.
* Display the release hold status and (for reviewers) force-release and block controls.
*
* Bails when there's no current release to gate, when the release has no cooldown
* delay (feature off at release-creation, or already force-released), or when the
* cooldown window has elapsed.
* Shows one of two messages: a countdown while a release is cooling down, or a block
* notice when the release is being held (which outlasts the cooldown window). Reviewers
* can force-release either way, and can block a version that's still cooling down. Bails
* when there's no current release, or when it's neither held nor still cooling down.
*/
protected static function display_release_cooldown() {
$post = get_post();
Expand All @@ -65,35 +66,53 @@ protected static function display_release_cooldown() {
return;
}

$release_delay = (int) ( $release['release_delay'] ?? 0 );
if ( ! $release_delay ) {
return;
}

$blocked = API_Update_Updater::is_release_blocked( $release );
$release_delay = (int) ( $release['release_delay'] ?? 0 );
$cooldown_until = API_Update_Updater::compute_release_time( $post, $release ) + $release_delay;
if ( $cooldown_until <= time() ) {
$in_cooldown = $release_delay && $cooldown_until > time();

// Nothing to surface unless the release is held or still cooling down.
if ( ! $blocked && ! $in_cooldown ) {
return;
}

?>
<div class="misc-pub-section misc-pub-release-cooldown">
<p>
<?php
printf(
/* translators: 1: version, 2: relative time until cooldown expires, 3: absolute UTC timestamp */
esc_html__( 'Version %1$s is in the release cooldown — it will be served to sites in %2$s (at %3$s UTC).', 'wporg-plugins' ),
esc_html( $version ),
esc_html( human_time_diff( time(), $cooldown_until ) ),
esc_html( gmdate( 'Y-m-d H:i', $cooldown_until ) )
);
if ( $blocked ) {
$block = $release['release_block'];
if ( isset( $block['risk_score'] ) ) {
printf(
/* translators: 1: version, 2: security scan risk score */
esc_html__( 'Version %1$s is blocked by a security scan (risk score %2$s) and is being held from sites. Force-releasing overrides the block.', 'wporg-plugins' ),
esc_html( $version ),
esc_html( $block['risk_score'] )
);
} else {
printf(
/* translators: %s: version */
esc_html__( 'Version %s is blocked and is being held from sites. Force-releasing overrides the block.', 'wporg-plugins' ),
esc_html( $version )
);
}
} else {
printf(
/* translators: 1: version, 2: relative time until cooldown expires, 3: absolute UTC timestamp */
esc_html__( 'Version %1$s is in the release cooldown — it will be served to sites in %2$s (at %3$s UTC).', 'wporg-plugins' ),
esc_html( $version ),
esc_html( human_time_diff( time(), $cooldown_until ) ),
esc_html( gmdate( 'Y-m-d H:i', $cooldown_until ) )
);
}
?>
</p>
<?php if ( current_user_can( 'plugin_review', $post ) ) : ?>
<p>
<label for="force_release_reason"><?php esc_html_e( 'Force-release reason (required):', 'wporg-plugins' ); ?></label>
<label for="release_action_reason"><?php esc_html_e( 'Reason (required):', 'wporg-plugins' ); ?></label>
<textarea
id="force_release_reason"
name="force_release_reason"
id="release_action_reason"
name="release_action_reason"
rows="2"
style="width: 100%;"
placeholder="<?php esc_attr_e( 'e.g. urgent security fix for CVE-…', 'wporg-plugins' ); ?>"
Expand All @@ -109,19 +128,32 @@ protected static function display_release_cooldown() {
);
?>
</button>
<?php if ( $in_cooldown && ! $blocked ) : ?>
<button type="submit" name="block_release_version" value="<?php echo esc_attr( $version ); ?>" class="button">
<?php
printf(
/* translators: %s: version */
esc_html__( 'Block %s from sites', 'wporg-plugins' ),
esc_html( $version )
);
?>
</button>
<?php endif; ?>
</p>
<?php endif; ?>
</div>
<?php
}

/**
* Save handler for reviewer force-release submissions from the Controls metabox.
* Save handler for reviewer force-release and block submissions from the Controls metabox.
*
* @param int $post_id The post being saved.
*/
public static function save_post( $post_id ) {
if ( empty( $_POST['force_release_version'] ) ) {
$is_force_release = ! empty( $_POST['force_release_version'] );
$is_block = ! empty( $_POST['block_release_version'] );
if ( ! $is_force_release && ! $is_block ) {
return;
}

Expand All @@ -139,20 +171,30 @@ public static function save_post( $post_id ) {
check_admin_referer( 'update-post_' . $post_id );

$version = get_post_meta( $post->ID, 'version', true );
$submitted_version = sanitize_text_field( wp_unslash( $_POST['force_release_version'] ) );
$submitted_version = sanitize_text_field( wp_unslash( $is_force_release ? $_POST['force_release_version'] : $_POST['block_release_version'] ) );
if ( $submitted_version !== $version ) {
// Submitted version doesn't match current — a newer commit landed since the form was rendered.
return;
}

$reason = isset( $_POST['force_release_reason'] )
? trim( sanitize_textarea_field( wp_unslash( $_POST['force_release_reason'] ) ) )
$reason = isset( $_POST['release_action_reason'] )
? trim( sanitize_textarea_field( wp_unslash( $_POST['release_action_reason'] ) ) )
: '';
if ( ! $reason ) {
return;
}

API_Update_Updater::force_release( $post->post_name, $reason );
if ( $is_force_release ) {
API_Update_Updater::force_release( $post->post_name, $reason );
} else {
API_Update_Updater::block_release(
$post->post_name,
array(
'reason' => $reason,
'blocked_by' => wp_get_current_user()->user_login,
)
);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1754,6 +1754,15 @@ public static function add_release( $plugin, $data ) {
unset( $release['discarded'] );
}

/*
* Clear a high-risk Gandalf block so the release can be served.
* See Jobs\API_Update_Updater::force_release().
*/
if ( ! empty( $data['unblock'] ) ) {
unset( $release['release_block'] );
}
unset( $release['unblock'] );

$releases = self::get_releases( $plugin );

// Find any other releases using this slug (as in the case of updates) and remove it.
Expand Down
Loading
Loading