| 1 |
<?php |
| 2 |
/** |
| 3 |
* The nonce handler. |
| 4 |
* |
| 5 |
* @package automattic/jetpack-connection |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace Automattic\Jetpack\Connection; |
| 9 |
|
| 10 |
/** |
| 11 |
* The nonce handler. |
| 12 |
*/ |
| 13 |
class Nonce_Handler { |
| 14 |
|
| 15 |
/** |
| 16 |
* How long the scheduled cleanup can run (in seconds). |
| 17 |
* Can be modified using the filter `jetpack_connection_nonce_scheduled_cleanup_limit`. |
| 18 |
*/ |
| 19 |
const SCHEDULED_CLEANUP_TIME_LIMIT = 5; |
| 20 |
|
| 21 |
/** |
| 22 |
* How many nonces should be removed per batch during the `clean_all()` run. |
| 23 |
*/ |
| 24 |
const CLEAN_ALL_LIMIT_PER_BATCH = 1000; |
| 25 |
|
| 26 |
/** |
| 27 |
* Nonce lifetime in seconds. |
| 28 |
*/ |
| 29 |
const LIFETIME = HOUR_IN_SECONDS; |
| 30 |
|
| 31 |
/** |
| 32 |
* The nonces used during the request are stored here to keep them valid. |
| 33 |
* The property is static to keep the nonces accessible between the `Nonce_Handler` instances. |
| 34 |
* |
| 35 |
* @var array |
| 36 |
*/ |
| 37 |
private static $nonces_used_this_request = array(); |
| 38 |
|
| 39 |
/** |
| 40 |
* The database object. |
| 41 |
* |
| 42 |
* @var \wpdb |
| 43 |
*/ |
| 44 |
private $db; |
| 45 |
|
| 46 |
/** |
| 47 |
* Initializing the object. |
| 48 |
*/ |
| 49 |
public function __construct() { |
| 50 |
global $wpdb; |
| 51 |
|
| 52 |
$this->db = $wpdb; |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Scheduling the WP-cron cleanup event. |
| 57 |
*/ |
| 58 |
public function init_schedule() { |
| 59 |
add_action( 'jetpack_clean_nonces', array( __CLASS__, 'clean_scheduled' ) ); |
| 60 |
if ( ! wp_next_scheduled( 'jetpack_clean_nonces' ) ) { |
| 61 |
wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); |
| 62 |
} |
| 63 |
} |
| 64 |
|
| 65 |
/** |
| 66 |
* Reschedule the WP-cron cleanup event to make it start sooner. |
| 67 |
*/ |
| 68 |
public function reschedule() { |
| 69 |
wp_clear_scheduled_hook( 'jetpack_clean_nonces' ); |
| 70 |
wp_schedule_event( time(), 'hourly', 'jetpack_clean_nonces' ); |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Adds a used nonce to a list of known nonces. |
| 75 |
* |
| 76 |
* @param int $timestamp the current request timestamp. |
| 77 |
* @param string $nonce the nonce value. |
| 78 |
* |
| 79 |
* @return bool whether the nonce is unique or not. |
| 80 |
*/ |
| 81 |
public function add( $timestamp, $nonce ) { |
| 82 |
if ( isset( static::$nonces_used_this_request[ "$timestamp:$nonce" ] ) ) { |
| 83 |
return static::$nonces_used_this_request[ "$timestamp:$nonce" ]; |
| 84 |
} |
| 85 |
|
| 86 |
// This should always have gone through Jetpack_Signature::sign_request() first to check $timestamp and $nonce. |
| 87 |
$timestamp = (int) $timestamp; |
| 88 |
$nonce = esc_sql( $nonce ); |
| 89 |
|
| 90 |
// Raw query so we can avoid races: add_option will also update. |
| 91 |
$show_errors = $this->db->hide_errors(); |
| 92 |
|
| 93 |
$return = false; |
| 94 |
|
| 95 |
// Running `try...finally` to make sure that we re-enable errors in case of an exception. |
| 96 |
try { |
| 97 |
$old_nonce = $this->db->get_row( |
| 98 |
$this->db->prepare( "SELECT 1 FROM `{$this->db->options}` WHERE option_name = %s", "jetpack_nonce_{$timestamp}_{$nonce}" ) |
| 99 |
); |
| 100 |
|
| 101 |
if ( $old_nonce === null ) { |
| 102 |
$return = (bool) $this->db->query( |
| 103 |
$this->db->prepare( |
| 104 |
"INSERT INTO `{$this->db->options}` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, %s)", |
| 105 |
"jetpack_nonce_{$timestamp}_{$nonce}", |
| 106 |
time(), |
| 107 |
'no' |
| 108 |
) |
| 109 |
); |
| 110 |
} |
| 111 |
} finally { |
| 112 |
$this->db->show_errors( $show_errors ); |
| 113 |
} |
| 114 |
|
| 115 |
static::$nonces_used_this_request[ "$timestamp:$nonce" ] = $return; |
| 116 |
|
| 117 |
return $return; |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* Removing all existing nonces, or at least as many as possible. |
| 122 |
* Capped at 20 seconds to avoid breaking the site. |
| 123 |
* |
| 124 |
* @param int $cutoff_timestamp All nonces added before this timestamp will be removed. |
| 125 |
* @param int $time_limit How long the cleanup can run (in seconds). |
| 126 |
* |
| 127 |
* @return true |
| 128 |
*/ |
| 129 |
public function clean_all( $cutoff_timestamp = PHP_INT_MAX, $time_limit = 20 ) { |
| 130 |
// phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed |
| 131 |
for ( $end_time = time() + $time_limit; time() < $end_time; ) { |
| 132 |
$result = $this->delete( static::CLEAN_ALL_LIMIT_PER_BATCH, $cutoff_timestamp ); |
| 133 |
|
| 134 |
if ( ! $result ) { |
| 135 |
break; |
| 136 |
} |
| 137 |
} |
| 138 |
|
| 139 |
return true; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* Scheduled clean up of the expired nonces. |
| 144 |
*/ |
| 145 |
public static function clean_scheduled() { |
| 146 |
/** |
| 147 |
* Adjust the time limit for the scheduled cleanup. |
| 148 |
* |
| 149 |
* @since 9.5.0 |
| 150 |
* |
| 151 |
* @param int $time_limit How long the cleanup can run (in seconds). |
| 152 |
*/ |
| 153 |
$time_limit = apply_filters( 'jetpack_connection_nonce_cleanup_runtime_limit', static::SCHEDULED_CLEANUP_TIME_LIMIT ); |
| 154 |
|
| 155 |
( new static() )->clean_all( time() - static::LIFETIME, $time_limit ); |
| 156 |
} |
| 157 |
|
| 158 |
/** |
| 159 |
* Delete the nonces. |
| 160 |
* |
| 161 |
* @param int $limit How many nonces to delete. |
| 162 |
* @param null|int $cutoff_timestamp All nonces added before this timestamp will be removed. |
| 163 |
* |
| 164 |
* @return int|false Number of removed nonces, or `false` if nothing to remove (or in case of a database error). |
| 165 |
*/ |
| 166 |
public function delete( $limit = 10, $cutoff_timestamp = null ) { |
| 167 |
global $wpdb; |
| 168 |
|
| 169 |
$ids = $wpdb->get_col( |
| 170 |
$wpdb->prepare( |
| 171 |
"SELECT option_id FROM `{$wpdb->options}`" |
| 172 |
. " WHERE `option_name` >= 'jetpack_nonce_' AND `option_name` < %s" |
| 173 |
. ' LIMIT %d', |
| 174 |
'jetpack_nonce_' . $cutoff_timestamp, |
| 175 |
$limit |
| 176 |
) |
| 177 |
); |
| 178 |
|
| 179 |
if ( ! is_array( $ids ) ) { |
| 180 |
// There's an error and we can't proceed. |
| 181 |
return false; |
| 182 |
} |
| 183 |
|
| 184 |
// Removing zeroes in case AUTO_INCREMENT of the options table is broken, and all ID's are zeroes. |
| 185 |
$ids = array_filter( $ids ); |
| 186 |
|
| 187 |
if ( array() === $ids ) { |
| 188 |
// There's nothing to remove. |
| 189 |
return false; |
| 190 |
} |
| 191 |
|
| 192 |
$ids_fill = implode( ', ', array_fill( 0, count( $ids ), '%d' ) ); |
| 193 |
|
| 194 |
$args = $ids; |
| 195 |
$args[] = 'jetpack_nonce_%'; |
| 196 |
|
| 197 |
// The Code Sniffer is unable to understand what's going on... |
| 198 |
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber |
| 199 |
return $wpdb->query( $wpdb->prepare( "DELETE FROM `{$wpdb->options}` WHERE `option_id` IN ( {$ids_fill} ) AND option_name LIKE %s", $args ) ); |
| 200 |
} |
| 201 |
|
| 202 |
/** |
| 203 |
* Clean the cached nonces valid during the current request, therefore making them invalid. |
| 204 |
* |
| 205 |
* @return bool |
| 206 |
*/ |
| 207 |
public static function invalidate_request_nonces() { |
| 208 |
static::$nonces_used_this_request = array(); |
| 209 |
|
| 210 |
return true; |
| 211 |
} |
| 212 |
} |
| 213 |
|