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
7 changes: 6 additions & 1 deletion inc/helpers.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?php

use SUPV\Helpers\Request as RequestHelper;

/**
* Gets the asset URL.
*
Expand Down Expand Up @@ -29,7 +32,9 @@ function supv_get_asset_url( $name, $type = 'images' ) {
*/
function supv_is_supervisor_screen() {

return is_admin() && ! empty( $_GET['page'] ) && 'supervisor' === sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = RequestHelper::get_arg( 'page', null, 'sanitize_key' );

return is_admin() && $page === 'supervisor';
}

/**
Expand Down
65 changes: 44 additions & 21 deletions src/Admin/AJAX.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use SUPV\Admin\Views\Cards\SecureLoginCardView;
use SUPV\Admin\Views\Cards\TransientsCardView;
use SUPV\Admin\Views\Cards\WordPressCardView;
use SUPV\Helpers\Request as RequestHelper;

/**
* The AJAX class.
Expand Down Expand Up @@ -88,16 +89,18 @@ public function is_doing_ajax() {
*/
public function hide_admin_notice() {

check_ajax_referer( 'supv_hide_admin_notice' );
$this->verify_ajax_request( 'hide_admin_notice' );

if ( ! empty( $_POST['software'] ) && preg_match( '/(?:ssl|https)/', sanitize_key( wp_unslash( $_POST['software'] ) ) ) ) {
$software = RequestHelper::get_post_arg( 'software', null, 'sanitize_key' );

if ( ! empty( $software ) && preg_match( '/(?:ssl|https)/', $software ) ) {
$notices_transient = get_transient( Dashboard::HIDE_NOTICES_TRANSIENT );

if ( $notices_transient === false ) {
$notices_transient = [];
}

$notices_transient[ sanitize_key( wp_unslash( $_POST['software'] ) ) ] = 1;
$notices_transient[ $software ] = 1;

set_transient( Dashboard::HIDE_NOTICES_TRANSIENT, $notices_transient, DAY_IN_SECONDS );
}
Expand All @@ -112,9 +115,11 @@ public function hide_admin_notice() {
*/
public function transients_cleanup() {

check_ajax_referer( 'supv_transients_cleanup' );
$this->verify_ajax_request( 'transients_cleanup' );

$expired = RequestHelper::get_post_arg( 'expired', false );

supv()->core()->transients()->cleanup( isset( $_POST['expired'] ) );
supv()->core()->transients()->cleanup( (bool) $expired );

( new TransientsCardView() )->output_stats( true );

Expand All @@ -128,7 +133,7 @@ public function transients_cleanup() {
*/
public function autoload_options_list() {

check_ajax_referer( 'supv_autoload_options_list' );
$this->verify_ajax_request( 'autoload_options_list' );

( new AutoloadCardView() )->output_options();

Expand All @@ -142,7 +147,7 @@ public function autoload_options_list() {
*/
public function autoload_options_history() {

check_ajax_referer( 'supv_autoload_options_history' );
$this->verify_ajax_request( 'autoload_options_history' );

( new AutoloadCardView() )->output_history();

Expand All @@ -156,7 +161,7 @@ public function autoload_options_history() {
*/
public function autoload_update_option() {

check_ajax_referer( 'supv_autoload_update_option' );
$this->verify_ajax_request( 'autoload_update_option' );

$data = $this->extract_form_data();

Expand Down Expand Up @@ -184,9 +189,9 @@ public function autoload_update_option() {
*/
public function wordpress_auto_update_policy() {

check_ajax_referer( 'supv_wordpress_auto_update_policy' );
$this->verify_ajax_request( 'wordpress_auto_update_policy' );

$policy = ! empty( $_POST['wp_auto_update_policy'] ) ? sanitize_key( wp_unslash( $_POST['wp_auto_update_policy'] ) ) : false;
$policy = RequestHelper::get_post_arg( 'wp_auto_update_policy', null, 'sanitize_key' );

if ( ! empty( $policy ) && preg_match( '/^(?:minor|major|disabled|dev)$/', $policy ) ) {
supv()->core()->wordpress()->set_auto_update_policy( $policy );
Expand All @@ -204,7 +209,7 @@ public function wordpress_auto_update_policy() {
*/
public function secure_login_settings_output() {

check_ajax_referer( 'supv_secure_login_settings_output' );
$this->verify_ajax_request( 'secure_login_settings_output' );

supv()->core()->secure_login()->update_settings(
[
Expand All @@ -224,7 +229,7 @@ public function secure_login_settings_output() {
*/
public function secure_login_settings_save() {

check_ajax_referer( 'supv_secure_login_settings_save' );
$this->verify_ajax_request( 'secure_login_settings_save' );

$settings = array_map( 'intval', $this->extract_form_data() ); // Converts all the values to int.

Expand All @@ -244,27 +249,45 @@ public function secure_login_settings_save() {
*/
private function extract_form_data() {

// phpcs:disable WordPress.Security.NonceVerification.Missing
$data = [];
$prefix = 'supv-field-';

$data = [];
foreach ( $_POST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
// Decode and sanitize the field name.
$sanitized_key = sanitize_key( urldecode( $key ) );

foreach ( array_keys( $_POST ) as $key ) {
if ( ! preg_match( '/^supv-field-/', sanitize_key( $key ) ) ) {
// Skip if not a supervisor field.
if ( strpos( $sanitized_key, $prefix ) !== 0 ) {
continue;
}

$value = ! empty( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
$field = preg_replace( '/^supv-field-/', '', urldecode( sanitize_key( $key ) ) );
// Extract field name by removing prefix.
$field_name = substr( $sanitized_key, strlen( $prefix ) );

if ( empty( $field ) ) {
if ( empty( $field_name ) ) {
continue;
}

$data[ $field ] = $value;
// Sanitize and store the value.
$data[ $field_name ] = sanitize_text_field( wp_unslash( $value ) );
}

return $data;
}

/**
* Verifies AJAX request security (nonce and capability).
*
* @since {VERSION}
*
* @param string $action The action name for nonce verification.
*/
private function verify_ajax_request( $action ) {

// phpcs:enable WordPress.Security.NonceVerification.Missing
check_ajax_referer( 'supv_' . $action );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( __( 'You do not have permission to perform this action.', 'supervisor' ) );
}
}
}
2 changes: 1 addition & 1 deletion src/Core/WordPress.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function apply_wp_auto_update_policy() { // phpcs:ignore WPForms.PHP.Hook
*/
public function set_auto_update_policy( $policy ) {

if ( $this->get_auto_update_policy() ) {
if ( $this->get_auto_update_policy() !== false ) {
update_option( self::CORE_AUTO_UPDATE_OPTION, $policy );
}
}
Expand Down
81 changes: 81 additions & 0 deletions src/Helpers/Request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
namespace SUPV\Helpers;

/**
* The Request class.
*
* @package supervisor
* @since {VERSION}
*/
final class Request {

/**
* Allowed sanitization callbacks.
*
* @since {VERSION}
*
* @var array
*/
const ALLOWED_SANITIZERS = [
'sanitize_text_field',
'sanitize_key',
];

/**
* Gets a GET parameter.
*
* @since {VERSION}
*
* @param string $key The parameter key.
* @param mixed $default_value Optional. Default value if not found. Default null.
* @param callable $sanitizer Optional. Sanitization callback. Default 'sanitize_text_field'.
*
* @return mixed The sanitized value, or default if not found.
*/
public static function get_arg( $key, $default_value = null, $sanitizer = 'sanitize_text_field' ) {

// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET[ $key ] ) ) {
return $default_value;
}

// Validate sanitizer against allowlist.
if ( ! in_array( $sanitizer, self::ALLOWED_SANITIZERS, true ) ) {
$sanitizer = 'sanitize_text_field';
}

// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$value = wp_unslash( $_GET[ $key ] );

return $sanitizer( $value );
}

/**
* Gets a POST parameter.
*
* @since {VERSION}
*
* @param string $key The parameter key.
* @param mixed $default_value Optional. Default value if not found. Default null.
* @param callable $sanitizer Optional. Sanitization callback. Default 'sanitize_text_field'.
*
* @return mixed The sanitized value, or default if not found.
*/
public static function get_post_arg( $key, $default_value = null, $sanitizer = 'sanitize_text_field' ) {

// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! isset( $_POST[ $key ] ) ) {
return $default_value;
}

// Validate sanitizer against allowlist.
if ( ! in_array( $sanitizer, self::ALLOWED_SANITIZERS, true ) ) {
$sanitizer = 'sanitize_text_field';
}

// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$value = wp_unslash( $_POST[ $key ] );

return $sanitizer( $value );
}
}