| @@ -100,8 +100,12 @@ | ||
| 100 | 100 | } |
| 101 | 101 | |
| 102 | 102 | // Check temporary access (transient) |
| 103 | 103 | if ($this->has_transient_access()) { |
| 104 | + // Sliding window: renew the access window itself, not just the | |
| 105 | + // decorative activity timestamp, so an admin actively using the | |
| 106 | + // tab is not cut off exactly at grant-time + timeout. | |
| 107 | + set_transient($this->get_transient_key(), 'granted', $this->transient_timeout); | |
| 104 | 108 | // Refresh transient activity |
| 105 | 109 | $this->update_activity(); |
| 106 | 110 | return true; |
| 107 | 111 | } |
| @@ -163,19 +167,19 @@ | ||
| 163 | 167 | * Grant temporary access (transient) |
| 164 | 168 | * |
| 165 | 169 | * @return bool True on success, false on failure |
| 166 | 170 | */ |
| 167 | - public function grant_transient_access() { | |
| 171 | + public function grant_transient_access($epoch = null) { | |
| 168 | 172 | $user_id = $this->get_user_id(); |
| 169 | 173 | if (!$user_id) { |
| 170 | 174 | return false; |
| 171 | 175 | } |
| 172 | 176 | |
| 173 | - $transient_key = $this->get_transient_key(); | |
| 177 | + $transient_key = $this->get_transient_key($epoch); | |
| 174 | 178 | $result = set_transient($transient_key, 'granted', $this->transient_timeout); |
| 175 | 179 | |
| 176 | 180 | // Set activity timestamp |
| 177 | - $this->update_activity(); | |
| 181 | + $this->update_activity($epoch); | |
| 178 | 182 | |
| 179 | 183 | return $result; |
| 180 | 184 | } |
| 181 | 185 | |
| @@ -235,15 +239,15 @@ | ||
| 235 | 239 | * Update last activity timestamp |
| 236 | 240 | * |
| 237 | 241 | * @return bool True on success, false on failure |
| 238 | 242 | */ |
| 239 | - private function update_activity() { | |
| 243 | + private function update_activity($epoch = null) { | |
| 240 | 244 | $user_id = $this->get_user_id(); |
| 241 | 245 | if (!$user_id) { |
| 242 | 246 | return false; |
| 243 | 247 | } |
| 244 | 248 | |
| 245 | - $activity_key = $this->get_activity_transient_key(); | |
| 249 | + $activity_key = $this->get_activity_transient_key($epoch); | |
| 246 | 250 | return set_transient($activity_key, time(), $this->transient_timeout); |
| 247 | 251 | } |
| 248 | 252 | |
| 249 | 253 | /** |
| @@ -281,14 +285,15 @@ | ||
| 281 | 285 | * Get transient key for this user and context |
| 282 | 286 | * |
| 283 | 287 | * @return string|false Returns transient key or false if no user ID available |
| 284 | 288 | */ |
| 285 | - private function get_transient_key() { | |
| 289 | + private function get_transient_key($epoch = null) { | |
| 286 | 290 | $user_id = $this->get_user_id(); |
| 287 | 291 | if (!$user_id) { |
| 288 | 292 | return false; |
| 289 | 293 | } |
| 290 | - return 'metasync_auth_' . $this->context . '_' . $user_id; | |
| 294 | + $epoch = $epoch === null ? self::get_revocation_epoch($this->context) : (int) $epoch; | |
| 295 | + return 'metasync_auth_' . $this->context . '_' . $epoch . '_' . $user_id; | |
| 291 | 296 | } |
| 292 | 297 | |
| 293 | 298 | /** |
| 294 | 299 | * Get activity transient key for this user and context |
| @@ -294,16 +299,29 @@ | ||
| 294 | 299 | * Get activity transient key for this user and context |
| 295 | 300 | * |
| 296 | 301 | * @return string|false Returns transient key or false if no user ID available |
| 297 | 302 | */ |
| 298 | - private function get_activity_transient_key() { | |
| 303 | + private function get_activity_transient_key($epoch = null) { | |
| 299 | 304 | $user_id = $this->get_user_id(); |
| 300 | 305 | if (!$user_id) { |
| 301 | 306 | return false; |
| 302 | 307 | } |
| 303 | - return 'metasync_activity_' . $this->context . '_' . $user_id; | |
| 308 | + $epoch = $epoch === null ? self::get_revocation_epoch($this->context) : (int) $epoch; | |
| 309 | + return 'metasync_activity_' . $this->context . '_' . $epoch . '_' . $user_id; | |
| 304 | 310 | } |
| 305 | 311 | |
| 312 | + private static function get_revocation_epoch($context) { | |
| 313 | + $epoch = (int) get_option('metasync_auth_revoke_epoch_' . sanitize_key($context), 1); | |
| 314 | + return $epoch > 0 ? $epoch : 1; | |
| 315 | + } | |
| 316 | + | |
| 317 | + private function revoke_epoch_transient_access($epoch) { | |
| 318 | + $user_id = $this->get_user_id(); | |
| 319 | + if (!$user_id) return; | |
| 320 | + delete_transient('metasync_auth_' . $this->context . '_' . $epoch . '_' . $user_id); | |
| 321 | + delete_transient('metasync_activity_' . $this->context . '_' . $epoch . '_' . $user_id); | |
| 322 | + } | |
| 323 | + | |
| 306 | 324 | /** |
| 307 | 325 | * Get authentication status information |
| 308 | 326 | * |
| 309 | 327 | * @return array Status information |
| @@ -340,14 +358,22 @@ | ||
| 340 | 358 | * @param bool $persistent Whether to grant persistent access |
| 341 | 359 | * @return bool True if password is valid and access granted, false otherwise |
| 342 | 360 | */ |
| 343 | 361 | public function verify_and_grant($password, $valid_passwords, $persistent = false) { |
| 362 | + if ($this->context === 'whitelabel' && $persistent) { | |
| 363 | + return false; | |
| 364 | + } | |
| 365 | + $epoch_before = $this->context === 'whitelabel' ? self::get_revocation_epoch($this->context) : 0; | |
| 344 | 366 | if (!is_array($valid_passwords)) { |
| 345 | 367 | $valid_passwords = array($valid_passwords); |
| 346 | 368 | } |
| 347 | 369 | |
| 348 | - // Remove empty passwords | |
| 349 | - $valid_passwords = array_filter($valid_passwords); | |
| 370 | + // Remove unset/empty passwords, but keep the string '0' — a | |
| 371 | + // truthiness filter would silently drop a configured password of | |
| 372 | + // "0" and make it impossible to authenticate with. | |
| 373 | + $valid_passwords = array_filter($valid_passwords, function ($p) { | |
| 374 | + return null !== $p && '' !== $p; | |
| 375 | + }); | |
| 350 | 376 | |
| 351 | 377 | if (empty($valid_passwords)) { |
| 352 | 378 | return false; |
| 353 | 379 | } |
| @@ -355,13 +381,17 @@ | ||
| 355 | 381 | // Check if password matches any valid password |
| 356 | 382 | $password_valid = in_array($password, $valid_passwords, true); |
| 357 | 383 | |
| 358 | 384 | if ($password_valid) { |
| 359 | - if ($persistent) { | |
| 360 | - return $this->grant_persistent_access(); | |
| 361 | - } else { | |
| 362 | - return $this->grant_transient_access(); | |
| 385 | + if ($this->context === 'whitelabel' && $epoch_before !== self::get_revocation_epoch($this->context)) { | |
| 386 | + return false; | |
| 363 | 387 | } |
| 388 | + $granted = $persistent ? $this->grant_persistent_access() : $this->grant_transient_access($this->context === 'whitelabel' ? $epoch_before : null); | |
| 389 | + if ($granted && $this->context === 'whitelabel' && $epoch_before !== self::get_revocation_epoch($this->context)) { | |
| 390 | + $this->revoke_epoch_transient_access($epoch_before); | |
| 391 | + return false; | |
| 392 | + } | |
| 393 | + return $granted; | |
| 364 | 394 | } |
| 365 | 395 | |
| 366 | 396 | return false; |
| 367 | 397 | } |
| @@ -389,8 +419,26 @@ | ||
| 389 | 419 | $user_meta_key = 'metasync_' . sanitize_key($context) . '_access'; |
| 390 | 420 | return delete_user_meta($user_id, $user_meta_key); |
| 391 | 421 | } |
| 392 | 422 | |
| 423 | + /** Revoke persistent and transient grants for every user on this site. */ | |
| 424 | + public static function revoke_all_access($context = 'default') { | |
| 425 | + $context = sanitize_key($context); | |
| 426 | + $meta_key = 'metasync_' . $context . '_access'; | |
| 427 | + $users = function_exists('get_users') ? get_users(array('fields' => 'ID', 'meta_key' => $meta_key)) : array(); | |
| 428 | + $success = true; | |
| 429 | + foreach ($users as $user_id) { | |
| 430 | + if (!delete_user_meta($user_id, $meta_key)) { | |
| 431 | + $success = false; | |
| 432 | + } | |
| 433 | + } | |
| 434 | + // Bump the namespace so transient-only grants from every user are invalidated. | |
| 435 | + if (!update_option('metasync_auth_revoke_epoch_' . $context, self::get_revocation_epoch($context) + 1, false)) { | |
| 436 | + $success = false; | |
| 437 | + } | |
| 438 | + return $success; | |
| 439 | + } | |
| 440 | + | |
| 393 | 441 | /** |
| 394 | 442 | * Static helper: Check if user has access |
| 395 | 443 | * |
| 396 | 444 | * @param int $user_id User ID |
| @@ -397,8 +445,14 @@ | ||
| 397 | 445 | * @param string $context Authentication context |
| 398 | 446 | * @return bool True if user has access, false otherwise |
| 399 | 447 | */ |
| 400 | 448 | public static function user_has_access($user_id, $context = 'default') { |
| 449 | + $user_id = absint($user_id); | |
| 450 | + if (!$user_id) { | |
| 451 | + return false; | |
| 452 | + } | |
| 453 | + | |
| 401 | 454 | $auth = new self($context); |
| 455 | + $auth->user_id = $user_id; | |
| 402 | 456 | return $auth->has_access(); |
| 403 | 457 | } |
| 404 | 458 | } |