Skip to content
Merged
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
1 change: 0 additions & 1 deletion plugin/waypoints/plan-your-day.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* Requires PHP: 8.2
* Author: acodebeard
* Text Domain: waypoints
* Domain Path: /languages
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/
Expand Down
10 changes: 10 additions & 0 deletions plugin/waypoints/src/Admin/SettingsPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ private function build_google_test_query( string $default_address, array $catego
}

private function render_google_test_notice(): void {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin notice flag from a nonce-protected Google API test redirect.
if ( ! isset( $_GET['plan_your_day_google_tested'] ) || is_array( $_GET['plan_your_day_google_tested'] ) ) {
return;
}
Expand All @@ -847,6 +848,7 @@ private function render_google_test_notice(): void {
)
)
);
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}

private function render_google_test_results_panel(): void {
Expand Down Expand Up @@ -1083,9 +1085,11 @@ private function is_settings_screen( string $hook_suffix ): bool {
return true;
}

// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin page routing check; no state is changed.
$page = isset( $_GET['page'] ) && ! is_array( $_GET['page'] )
? sanitize_key( wp_unslash( $_GET['page'] ) )
: '';
// phpcs:enable WordPress.Security.NonceVerification.Recommended

return Settings::PAGE_SLUG === $page;
}
Expand Down Expand Up @@ -1249,6 +1253,7 @@ private function render_select( string $name, string $id, string $value, array $
}

private function render_cache_notice(): void {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin notice flag from nonce-protected cache-clear redirects.
if ( ! isset( $_GET['plan_your_day_cache_cleared'] ) ) {
$this->render_scope_cache_notice();
$this->render_place_cache_notice();
Expand All @@ -1274,9 +1279,11 @@ private function render_cache_notice(): void {

$this->render_scope_cache_notice();
$this->render_place_cache_notice();
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}

private function render_scope_cache_notice(): void {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin notice flag from a nonce-protected cache-clear redirect.
if ( ! isset( $_GET['plan_your_day_cache_scope_cleared'] ) || is_array( $_GET['plan_your_day_cache_scope_cleared'] ) ) {
return;
}
Expand Down Expand Up @@ -1307,9 +1314,11 @@ private function render_scope_cache_notice(): void {
)
)
);
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}

private function render_place_cache_notice(): void {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin notice flag from a nonce-protected cache-clear redirect.
if ( ! isset( $_GET['plan_your_day_cache_place_cleared'] ) || is_array( $_GET['plan_your_day_cache_place_cleared'] ) ) {
return;
}
Expand Down Expand Up @@ -1340,6 +1349,7 @@ private function render_place_cache_notice(): void {
)
)
);
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}

private function google_cache_scope_choices(): array {
Expand Down
1 change: 1 addition & 0 deletions plugin/waypoints/src/Frontend/PlannerBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public function render( array $attributes = [], string $content = '', mixed $blo

$this->assets->enqueue();

// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public dynamic block reads URL planner state; values are sanitized by RequestStateParser before use.
return $this->renderer->render( $_GET, $action_url );
}
}
1 change: 1 addition & 0 deletions plugin/waypoints/src/Frontend/PlannerShortcode.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function render( array|string $attributes = [], ?string $content = null,

$this->assets->enqueue();

// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public shortcode reads URL planner state; values are sanitized by RequestStateParser before use.
return $this->renderer->render( $_GET, $action_url );
}
}
9 changes: 0 additions & 9 deletions plugin/waypoints/src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ private function __construct() {
}

public function init(): void {
add_action( 'init', [ $this, 'load_textdomain' ], 0 );
add_action( 'init', [ $this->settings, 'maybe_upgrade' ], 1 );
add_action( 'init', [ $this->frontend_assets, 'register' ], 2 );
add_action( 'init', [ $this->planner_shortcode, 'register' ] );
Expand All @@ -121,14 +120,6 @@ public function init(): void {
add_action( 'admin_post_plan_your_day_test_google_api', [ $this->settings_page, 'handle_test_google_api' ] );
}

public function load_textdomain(): void {
load_plugin_textdomain(
PLAN_YOUR_DAY_TEXT_DOMAIN,
false,
dirname( plugin_basename( PLAN_YOUR_DAY_PLUGIN_FILE ) ) . '/languages'
);
}

public function settings(): Settings {
return $this->settings;
}
Expand Down
45 changes: 36 additions & 9 deletions plugin/waypoints/src/Rest/PlannerRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,18 @@ private function guard_request( WP_REST_Request $request, string $scope, array $
'status' => 403,
]
);
$server_snapshot = $this->debug_server_snapshot();

DebugLogger::log(
'rest.guard.invalid_origin',
[
'scope' => $scope,
'host' => $_SERVER['HTTP_HOST'] ?? '',
'origin' => $_SERVER['HTTP_ORIGIN'] ?? '',
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
'sec_fetch_site' => $_SERVER['HTTP_SEC_FETCH_SITE'] ?? '',
'sec_fetch_mode' => $_SERVER['HTTP_SEC_FETCH_MODE'] ?? '',
'sec_fetch_dest' => $_SERVER['HTTP_SEC_FETCH_DEST'] ?? '',
'host' => $server_snapshot['host'],
'origin' => $server_snapshot['origin'],
'referer' => $server_snapshot['referer'],
'sec_fetch_site' => $server_snapshot['sec_fetch_site'],
'sec_fetch_mode' => $server_snapshot['sec_fetch_mode'],
'sec_fetch_dest' => $server_snapshot['sec_fetch_dest'],
]
);

Expand All @@ -271,8 +273,8 @@ private function guard_request( WP_REST_Request $request, string $scope, array $
'scope' => $scope,
'token_present' => '' !== $endpoint_token,
'cookie_present' => isset( $_COOKIE['plan_your_day_visitor'] ),
'origin' => $_SERVER['HTTP_ORIGIN'] ?? '',
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
'origin' => $this->server_value( 'HTTP_ORIGIN' ),
'referer' => $this->server_value( 'HTTP_REFERER' ),
]
);

Expand Down Expand Up @@ -688,7 +690,32 @@ private function debug_request_params( ?WP_REST_Request $request ): array {
'move_waypoint' => $request->get_param( 'move_waypoint' ),
],
'has_token' => '' !== (string) $request->get_param( 'endpoint_token' ),
'content_type' => $_SERVER['CONTENT_TYPE'] ?? '',
'content_type' => $this->server_value( 'CONTENT_TYPE' ),
];
}

/**
* @return array{host:string, origin:string, referer:string, sec_fetch_site:string, sec_fetch_mode:string, sec_fetch_dest:string}
*/
private function debug_server_snapshot(): array {
return [
'host' => $this->server_value( 'HTTP_HOST' ),
'origin' => $this->server_value( 'HTTP_ORIGIN' ),
'referer' => $this->server_value( 'HTTP_REFERER' ),
'sec_fetch_site' => $this->server_value( 'HTTP_SEC_FETCH_SITE' ),
'sec_fetch_mode' => $this->server_value( 'HTTP_SEC_FETCH_MODE' ),
'sec_fetch_dest' => $this->server_value( 'HTTP_SEC_FETCH_DEST' ),
];
}

private function server_value( string $key ): string {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Dynamic server header key is scalar-checked, unslashed, and sanitized before return.
$value = $_SERVER[ $key ] ?? '';

if ( ! is_scalar( $value ) ) {
return '';
}

return sanitize_text_field( wp_unslash( (string) $value ) );
}
}
2 changes: 2 additions & 0 deletions plugin/waypoints/src/Security/RateLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ private function can_use_database_advisory_locks(): bool {
private function get_database_advisory_lock_result( string $key ): mixed {
global $wpdb;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- MySQL advisory locks are connection-level primitives and cannot be represented through WordPress object caching.
return $wpdb->get_var(
$wpdb->prepare(
'SELECT GET_LOCK(%s, %d)',
Expand All @@ -235,6 +236,7 @@ private function get_database_advisory_lock_result( string $key ): mixed {
private function release_database_advisory_lock( string $key ): void {
global $wpdb;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Releases the matching MySQL advisory lock; there is no cacheable WordPress API equivalent.
$wpdb->get_var(
$wpdb->prepare(
'SELECT RELEASE_LOCK(%s)',
Expand Down
2 changes: 2 additions & 0 deletions plugin/waypoints/src/Security/VisitorTokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,14 @@ private function get_or_create_visitor_token(): string {
}

private function get_cookie_token(): string {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw cookie is scalar-checked, unslashed, sanitized, and regex-validated before use.
$cookie_value = $_COOKIE[ self::COOKIE_NAME ] ?? '';

if ( ! is_scalar( $cookie_value ) ) {
return '';
}

$cookie_value = wp_unslash( $cookie_value );
$cookie_value = strtolower( trim( sanitize_text_field( (string) $cookie_value ) ) );

return 1 === preg_match( '/\A[a-f0-9]{48}\z/', $cookie_value ) ? $cookie_value : '';
Expand Down
1 change: 1 addition & 0 deletions plugin/waypoints/src/Support/DebugLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static function log( string $event, array $context = [] ): void {
$encoded_context = '{"encoding":"failed"}';
}

// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is opt-in through WP_DEBUG or an explicit filter.
error_log( sprintf( '[plan-your-day] %s %s', $event, $encoded_context ) );
}

Expand Down
8 changes: 8 additions & 0 deletions plugin/waypoints/tests/PluginDisplayNameTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ final class PluginDisplayNameTest extends TestCase {
public function test_front_facing_metadata_uses_waypoints_display_name(): void {
self::assertStringContainsString( 'Plugin Name: Waypoints', $this->fixture( 'plan-your-day.php' ) );
self::assertStringContainsString( 'Text Domain: waypoints', $this->fixture( 'plan-your-day.php' ) );
self::assertStringNotContainsString( 'Domain Path:', $this->fixture( 'plan-your-day.php' ) );
self::assertStringContainsString( "define( 'PLAN_YOUR_DAY_TEXT_DOMAIN', 'waypoints' );", $this->fixture( 'plan-your-day.php' ) );

$release = json_decode( $this->fixture( 'release.json' ), true );
Expand Down Expand Up @@ -51,6 +52,13 @@ public function test_shortcode_exposes_waypoints_tag_with_legacy_alias(): void {
self::assertStringContainsString( 'add_shortcode( self::LEGACY_TAG', $source );
}

public function test_wordpress_org_translation_loading_is_left_to_core(): void {
$source = $this->fixture( 'src/Plugin.php' );

self::assertStringNotContainsString( 'load_plugin_textdomain', $source );
self::assertStringNotContainsString( 'load_textdomain', $source );
}

private function fixture( string $path ): string {
$content = file_get_contents( dirname( __DIR__ ) . '/' . $path );

Expand Down
7 changes: 4 additions & 3 deletions plugin/waypoints/tests/SubmissionReadinessToolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ public function test_rejects_not_production_ready_readme_copy(): void {
self::assertStringContainsString( 'readme.txt must not describe the plugin as not production ready.', $result['output'] );
}

public function test_rejects_composer_metadata_in_submission_artifact(): void {
public function test_rejects_missing_composer_json_when_vendor_ships_in_submission_artifact(): void {
$workspace = $this->make_workspace();
$plugin_dir = $workspace . '/waypoints';
$artifact = $workspace . '/waypoints-1.0.zip';

$this->write_plugin_fixture( $plugin_dir, 'waypoints', '1.0' );
file_put_contents( $plugin_dir . '/composer.json', "{}\n" );
unlink( $plugin_dir . '/composer.json' );
$this->zip_fixture( $workspace, 'waypoints', $artifact );

$result = $this->run_tool(
Expand All @@ -96,7 +96,7 @@ public function test_rejects_composer_metadata_in_submission_artifact(): void {
);

self::assertSame( 1, $result['status'], $result['output'] );
self::assertStringContainsString( 'Artifact must not contain waypoints/composer.json.', $result['output'] );
self::assertStringContainsString( 'Artifact must contain waypoints/composer.json.', $result['output'] );
}

/**
Expand Down Expand Up @@ -207,6 +207,7 @@ private function write_plugin_fixture( string $plugin_dir, string $slug, string
)
);

file_put_contents( $plugin_dir . '/composer.json', "{}\n" );
file_put_contents( $plugin_dir . '/vendor/autoload.php', "<?php\n" );
}

Expand Down
2 changes: 1 addition & 1 deletion plugin/waypoints/tools/build-release-zip.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ if [[ ! -f "${STAGE_PLUGIN_DIR}/vendor/autoload.php" ]]; then
exit 1
fi

rm -f "${STAGE_PLUGIN_DIR}/composer.json" "${STAGE_PLUGIN_DIR}/composer.lock"
rm -f "${STAGE_PLUGIN_DIR}/composer.lock"
find "${STAGE_PLUGIN_DIR}" -type d -exec chmod 755 {} +
find "${STAGE_PLUGIN_DIR}" -type f -exec chmod 644 {} +
rm -f "${ARTIFACT_PATH}"
Expand Down
3 changes: 2 additions & 1 deletion plugin/waypoints/tools/check-wp-submission-readiness.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ function check_artifact( string $artifact, string $expected_slug, array &$errors

$required_paths = [
$expected_slug . '/readme.txt',
$expected_slug . '/composer.json',
$expected_slug . '/vendor/autoload.php',
];

Expand All @@ -363,7 +364,7 @@ function check_artifact( string $artifact, string $expected_slug, array &$errors
$errors[] = sprintf( 'Artifact must not contain %s.', $path );
}

if ( preg_match( '#^' . preg_quote( $expected_slug, '#' ) . '/(?:composer\.(?:json|lock)|phpstan\.neon|phpcs\.xml\.dist|phpunit\.xml(?:\.dist)?|DECISIONS\.md|\.distignore)$#', $path ) ) {
if ( preg_match( '#^' . preg_quote( $expected_slug, '#' ) . '/(?:composer\.lock|phpstan\.neon|phpcs\.xml\.dist|phpunit\.xml(?:\.dist)?|DECISIONS\.md|\.distignore)$#', $path ) ) {
$errors[] = sprintf( 'Artifact must not contain %s.', $path );
}
}
Expand Down
1 change: 1 addition & 0 deletions plugin/waypoints/uninstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
global $wpdb;

if ( isset( $wpdb ) && isset( $wpdb->options ) && method_exists( $wpdb, 'esc_like' ) && method_exists( $wpdb, 'prepare' ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup removes plugin-scoped legacy rate-limit rows and does not need caching.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
Expand Down