| @@ -6,8 +6,10 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace Booktics\Abstracts; |
| 9 | 9 | |
| 10 | +if ( ! defined( 'ABSPATH' ) ) exit; | |
| 11 | + | |
| 10 | 12 | use JsonSerializable; |
| 11 | 13 | use WP_User_Query; |
| 12 | 14 | |
| 13 | 15 | abstract class User_Model implements JsonSerializable { |
| @@ -301,17 +303,16 @@ | ||
| 301 | 303 | * @return boolean |
| 302 | 304 | */ |
| 303 | 305 | public function delete(): bool { |
| 304 | 306 | $user = get_userdata( $this->id ); |
| 305 | - | |
| 307 | + | |
| 306 | 308 | if ( ! $user ) { |
| 307 | 309 | return false; |
| 308 | 310 | } |
| 309 | 311 | |
| 310 | - if ( count( $user->roles ) == 1 && in_array( $this->role, $user->roles ) ) { | |
| 312 | + if ( count( $user->roles ) == 1 && in_array( $this->role, $user->roles ) ) { | |
| 311 | 313 | return wp_delete_user( $this->id ); |
| 312 | 314 | } |
| 313 | - | |
| 314 | 315 | $user->remove_role( $this->role ); |
| 315 | 316 | |
| 316 | 317 | return true; |
| 317 | 318 | } |
| @@ -415,8 +416,9 @@ | ||
| 415 | 416 | $relation = 'OR'; |
| 416 | 417 | } |
| 417 | 418 | } |
| 418 | 419 | if ( ! empty( $meta_query ) ) { |
| 420 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering users by meta fields | |
| 419 | 421 | $args['meta_query'] = [ |
| 420 | 422 | 'relation' => $relation, |
| 421 | 423 | ...$meta_query |
| 422 | 424 | ]; |
| @@ -533,23 +535,23 @@ | ||
| 533 | 535 | $data = []; |
| 534 | 536 | |
| 535 | 537 | foreach ( $this->fillable as $key => $default ) { |
| 536 | 538 | if ( $key === 'user_email' ) { |
| 537 | - $data['email'] = $this->$key; | |
| 539 | + $data['email'] = $this->fillable[$key]; | |
| 538 | 540 | continue; |
| 539 | 541 | } |
| 540 | 542 | |
| 541 | 543 | $value = $this->$key ?? $this->fillable[ $key ]; |
| 542 | 544 | |
| 543 | - // Unserialize if necessary | |
| 545 | + // Unserialize if necessary (object instantiation disallowed — see helper). | |
| 544 | 546 | if ( is_string( $value ) && $this->is_serialized( $value ) ) { |
| 545 | - $value = maybe_unserialize( $value ); | |
| 547 | + $value = \booktics_safe_maybe_unserialize( $value ); | |
| 546 | 548 | } |
| 547 | 549 | |
| 548 | 550 | // Normalize value to default type |
| 549 | 551 | if ( gettype( $value ) !== gettype( $default ) ) { |
| 550 | 552 | if ( gettype( $default ) === 'array' && is_string( $value ) && $this->is_serialized( $value ) ) { |
| 551 | - $value = maybe_unserialize( $value ); | |
| 553 | + $value = \booktics_safe_maybe_unserialize( $value ); | |
| 552 | 554 | } elseif ( gettype( $default ) === 'array' ) { |
| 553 | 555 | $value = $default; |
| 554 | 556 | } |
| 555 | 557 | } |
| @@ -554,9 +556,9 @@ | ||
| 554 | 556 | } |
| 555 | 557 | } |
| 556 | 558 | |
| 557 | 559 | // Cast specific keys to objects |
| 558 | - if ( $key == 'schedule' ) { | |
| 560 | + if ( 'schedule' == $key ) { | |
| 559 | 561 | $value = (object) $value; |
| 560 | 562 | } |
| 561 | 563 | |
| 562 | 564 | // Cast specific keys to boolean |
| @@ -561,14 +563,15 @@ | ||
| 561 | 563 | |
| 562 | 564 | // Cast specific keys to boolean |
| 563 | 565 | if ( in_array( $key, [ |
| 564 | 566 | 'enable_custom_schedule', |
| 565 | - 'enable_custom_holiday' | |
| 567 | + 'enable_custom_holiday', | |
| 568 | + 'enable_custom_availability', | |
| 566 | 569 | ] ) ) { |
| 567 | 570 | $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN ); |
| 568 | 571 | } |
| 569 | 572 | |
| 570 | - if ( $key == 'user_registered' ) { | |
| 573 | + if ( 'user_registered' == $key ) { | |
| 571 | 574 | $value = $default; |
| 572 | 575 | } |
| 573 | 576 | |
| 574 | 577 | $data[ $key ] = $value ?? $default; |
| @@ -586,9 +589,11 @@ | ||
| 586 | 589 | * |
| 587 | 590 | * @return bool True if data is serialized, false otherwise |
| 588 | 591 | */ |
| 589 | 592 | function is_serialized( $data ) { |
| 590 | - return is_string( $data ) && @unserialize( $data ) !== false; | |
| 593 | + // Structural check only (WordPress core) — never calls unserialize(), so it | |
| 594 | + // cannot trigger PHP Object Injection on attacker-controlled user meta. | |
| 595 | + return is_string( $data ) && \is_serialized( $data ); | |
| 591 | 596 | } |
| 592 | 597 | |
| 593 | 598 | /** |
| 594 | 599 | * Specify data which should be serialized to JSON. |
| @@ -596,6 +601,22 @@ | ||
| 596 | 601 | * @return array |
| 597 | 602 | */ |
| 598 | 603 | public function jsonSerialize(): array { |
| 599 | 604 | return $this->to_array(); |
| 605 | + } | |
| 606 | + | |
| 607 | + /** | |
| 608 | + * Get the first user matching the current filters. | |
| 609 | + * | |
| 610 | + * @param array $args | |
| 611 | + * @return self|null | |
| 612 | + */ | |
| 613 | + public function first(array $args = []): ?self { | |
| 614 | + $args['number'] = 1; | |
| 615 | + $args['paged'] = 1; | |
| 616 | + $result = $this->all($args); | |
| 617 | + if (!empty($result['items'][0])) { | |
| 618 | + return $this->hydrate($result['items'][0]['ID'], $result['items'][0]); | |
| 619 | + } | |
| 620 | + return null; | |
| 600 | 621 | } |
| 601 | 622 | } |