> $conditions */ public function __construct( public int $id = 0, public string $source = '', public string $match_type = self::EXACT, public string $target = '', public string $target_type = 'url', public int $target_id = 0, public int $status_code = 301, public bool $case_sensitive = false, public bool $pass_query = true, public array $conditions = [], public ?string $starts_at = null, public ?string $ends_at = null, public bool $enabled = true, public int $priority = 0, public string $group_name = '', public string $note = '', public string $origin = 'manual', public int $post_id = 0, public int $hits = 0, public ?string $last_hit = null, public string $created_at = '', public string $updated_at = '' ) {} /** * @param object|array $row */ public static function from_row( object|array $row ): self { $row = (array) $row; $conditions = json_decode( (string) ( $row['conditions'] ?? '' ), true ); return new self( id: (int) ( $row['id'] ?? 0 ), source: (string) ( $row['source'] ?? '' ), match_type: (string) ( $row['match_type'] ?? self::EXACT ), target: (string) ( $row['target'] ?? '' ), target_type: (string) ( $row['target_type'] ?? 'url' ), target_id: (int) ( $row['target_id'] ?? 0 ), status_code: (int) ( $row['status_code'] ?? 301 ), case_sensitive: (bool) ( $row['case_sensitive'] ?? false ), pass_query: (bool) ( $row['pass_query'] ?? true ), conditions: is_array( $conditions ) ? array_values( $conditions ) : [], starts_at: empty( $row['starts_at'] ) ? null : (string) $row['starts_at'], ends_at: empty( $row['ends_at'] ) ? null : (string) $row['ends_at'], enabled: (bool) ( $row['enabled'] ?? true ), priority: (int) ( $row['priority'] ?? 0 ), group_name: (string) ( $row['group_name'] ?? '' ), note: (string) ( $row['note'] ?? '' ), origin: (string) ( $row['origin'] ?? 'manual' ), post_id: (int) ( $row['post_id'] ?? 0 ), hits: (int) ( $row['hits'] ?? 0 ), last_hit: empty( $row['last_hit'] ) ? null : (string) $row['last_hit'], created_at: (string) ( $row['created_at'] ?? '' ), updated_at: (string) ( $row['updated_at'] ?? '' ) ); } /** * The columns, ready for $wpdb->insert(). * * @return array */ public function to_row(): array { return [ 'source' => $this->source, 'source_hash' => $this->source_hash(), 'match_type' => $this->match_type, 'target' => $this->target, 'target_type' => $this->target_type, 'target_id' => $this->target_id, 'status_code' => $this->status_code, 'case_sensitive' => (int) $this->case_sensitive, 'pass_query' => (int) $this->pass_query, 'conditions' => [] === $this->conditions ? null : wp_json_encode( $this->conditions ), 'starts_at' => $this->starts_at, 'ends_at' => $this->ends_at, 'enabled' => (int) $this->enabled, 'priority' => $this->priority, 'group_name' => $this->group_name, 'note' => $this->note, 'origin' => $this->origin, 'post_id' => $this->post_id, 'hits' => $this->hits, 'last_hit' => $this->last_hit, ]; } /** * Only exact rules are looked up by hash. Patterns get a fixed value so that an * address that happens to look like one can never collide with them. */ public function source_hash(): string { if ( self::EXACT !== $this->match_type ) { return str_repeat( '0', 32 ); } return Address::parse( $this->source )->hash(); } /** * Text as text: control characters and line breaks out, runs of spaces collapsed, * cut to length. Nothing that merely looks like HTML is touched — it is escaped where * it is shown, which is the only place escaping belongs. */ public static function plain_text( string $text, int $max ): string { $text = (string) preg_replace( '/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/u', ' ', $text ); $text = trim( (string) preg_replace( '/\s{2,}/u', ' ', $text ) ); return mb_substr( $text, 0, $max ); } public function moves(): bool { return in_array( $this->status_code, self::MOVES, true ); } public function is_permanent(): bool { return in_array( $this->status_code, [ 301, 308 ], true ); } public function has_condition( string $type ): bool { foreach ( $this->conditions as $condition ) { if ( ( $condition['type'] ?? '' ) === $type ) { return true; } } return false; } /** Whether the rule is in its scheduled window right now. Times are stored in UTC. */ public function in_schedule( ?int $now = null ): bool { $now ??= time(); if ( null !== $this->starts_at && strtotime( $this->starts_at . ' UTC' ) > $now ) { return false; } if ( null !== $this->ends_at && strtotime( $this->ends_at . ' UTC' ) <= $now ) { return false; } return true; } /** * For the REST API and the admin screen. * * @return array */ public function to_array(): array { $out = get_object_vars( $this ); $out['target_label'] = $this->target_label(); return $out; } /** * What the list shows for "To". A linked page is named by its title, because * "/?p=4471" tells nobody anything and the title is how people think of the page. */ public function target_label(): string { if ( 'post' === $this->target_type && $this->target_id > 0 ) { $title = get_the_title( $this->target_id ); return '' === $title ? '#' . $this->target_id : html_entity_decode( $title, ENT_QUOTES, 'UTF-8' ); } if ( 'term' === $this->target_type && $this->target_id > 0 ) { $term = get_term( $this->target_id ); return $term instanceof \WP_Term ? $term->name : '#' . $this->target_id; } return ''; } }