| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Which redirects one Edit Redirect request named, as one of three states. |
| 9 |
* |
| 10 |
* The states are mutually exclusive and were previously carried as five |
| 11 |
* independent array keys -- a nullable single id, a list, a source string, a |
| 12 |
* `truncated` flag and a count -- which made incoherent combinations |
| 13 |
* expressible: a single id AND a populated list, a bulk answer labelled with a |
| 14 |
* single-id source, a refusal that still carried ids to edit. Nothing built |
| 15 |
* those combinations, but nothing stopped the next edit from doing so either, |
| 16 |
* and a consumer branching on the wrong key would act on the wrong redirects. |
| 17 |
* |
| 18 |
* Three named constructors and a private one, so the only reachable shapes are: |
| 19 |
* |
| 20 |
* - SINGLE: exactly one id, from the GET or POST `id` parameter. |
| 21 |
* - BULK: one or more ids from `idnum`, within the plugin's ceiling. |
| 22 |
* - REFUSED: the request named more than the ceiling, so it names NO ids at |
| 23 |
* all. Deliberately not "the first N of them": editing a subset |
| 24 |
* of what the admin selected, with no way to see which was |
| 25 |
* dropped, is worse than refusing (see |
| 26 |
* ABJ_404_Solution_RedirectEditRequest::MAX_SELECTED_IDS). |
| 27 |
* |
| 28 |
* `requestedCount` survives on every state, including REFUSED, because the |
| 29 |
* refusal has to name the real number back to the admin. |
| 30 |
*/ |
| 31 |
final class ABJ_404_Solution_RequestedRedirectIds { |
| 32 |
|
| 33 |
/** Exactly one redirect, named through the GET or POST `id` parameter. */ |
| 34 |
const KIND_SINGLE = 'single'; |
| 35 |
|
| 36 |
/** A set named through `idnum`, within the plugin's ceiling. */ |
| 37 |
const KIND_BULK = 'bulk'; |
| 38 |
|
| 39 |
/** More than the ceiling, so no ids at all. */ |
| 40 |
const KIND_REFUSED_TOO_MANY = 'refused_too_many'; |
| 41 |
|
| 42 |
/** |
| 43 |
* @var string Which of the three states this is. |
| 44 |
* |
| 45 |
* Explicit rather than inferred from the id count. A bulk selection of one |
| 46 |
* (`idnum[]=5`) and a single-id request both hold one id, and they render |
| 47 |
* differently -- deducing the state from count() would route the former |
| 48 |
* into the single-record form. |
| 49 |
*/ |
| 50 |
private $kind; |
| 51 |
|
| 52 |
/** @var string One of ABJ_404_Solution_RedirectEditRequest's SOURCE_* values. */ |
| 53 |
private $source; |
| 54 |
|
| 55 |
/** @var array<int, int> The ids to act on. Always empty when refused. */ |
| 56 |
private $ids; |
| 57 |
|
| 58 |
/** @var int How many the request named, before any ceiling was applied. */ |
| 59 |
private $requestedCount; |
| 60 |
|
| 61 |
/** |
| 62 |
* Keyed, because `kind` and `source` are both strings: positionally they |
| 63 |
* transpose into a type-correct call that labels a bulk request as a |
| 64 |
* single one, which is the exact confusion this type exists to end. PHP 7.4 |
| 65 |
* is the floor here, so named arguments are not available. |
| 66 |
* |
| 67 |
* @param array{kind: string, source: string, ids: array<int, int>, requested_count: int} $state |
| 68 |
*/ |
| 69 |
private function __construct(array $state) { |
| 70 |
$this->kind = $state['kind']; |
| 71 |
$this->source = $state['source']; |
| 72 |
$this->ids = $state['ids']; |
| 73 |
$this->requestedCount = $state['requested_count']; |
| 74 |
} |
| 75 |
|
| 76 |
/** One redirect, named through the GET or POST `id` parameter. */ |
| 77 |
public static function single(string $source, int $id): self { |
| 78 |
return new self(array('kind' => self::KIND_SINGLE, 'source' => $source, |
| 79 |
'ids' => array($id), 'requested_count' => 1)); |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* A bulk selection within the ceiling, or null when it names no ids. |
| 84 |
* |
| 85 |
* @param array<int, int> $ids A set that sanitizes to nothing yields null |
| 86 |
* rather than an empty bulk request. |
| 87 |
* @param int $requestedCount How many the request NAMED, which can exceed |
| 88 |
* count($ids) when some entries sanitized away to nothing. |
| 89 |
*/ |
| 90 |
public static function bulk(string $source, array $ids, int $requestedCount): ?self { |
| 91 |
// An empty set is not a bulk request with nothing in it; it is "the |
| 92 |
// request named no usable id", which callers already represent as null. |
| 93 |
// Deciding that here rather than at the call site is what keeps a |
| 94 |
// KIND_BULK holding zero ids unrepresentable instead of merely |
| 95 |
// undocumented. |
| 96 |
if ($ids === array()) { |
| 97 |
return null; |
| 98 |
} |
| 99 |
return new self(array('kind' => self::KIND_BULK, 'source' => $source, |
| 100 |
'ids' => array_values($ids), 'requested_count' => $requestedCount)); |
| 101 |
} |
| 102 |
|
| 103 |
/** A selection larger than the plugin will carry. Names no ids on purpose. */ |
| 104 |
public static function refusedAsTooMany(string $source, int $requestedCount): self { |
| 105 |
return new self(array('kind' => self::KIND_REFUSED_TOO_MANY, 'source' => $source, |
| 106 |
'ids' => array(), 'requested_count' => $requestedCount)); |
| 107 |
} |
| 108 |
|
| 109 |
/** Which request parameter the answer came from. */ |
| 110 |
public function source(): string { |
| 111 |
return $this->source; |
| 112 |
} |
| 113 |
|
| 114 |
/** Whether this names exactly one redirect through the `id` parameter. */ |
| 115 |
public function isSingle(): bool { |
| 116 |
return $this->kind === self::KIND_SINGLE; |
| 117 |
} |
| 118 |
|
| 119 |
/** The single id, or null when this is not a single-id request. */ |
| 120 |
public function singleId(): ?int { |
| 121 |
return $this->isSingle() ? $this->ids[0] : null; |
| 122 |
} |
| 123 |
|
| 124 |
/** |
| 125 |
* Every id to act on. Empty when the request was refused, which is why |
| 126 |
* callers must check wasRefusedAsTooMany() first: an empty list here means |
| 127 |
* "nothing to edit", and the reason matters to the admin. |
| 128 |
* |
| 129 |
* @return array<int, int> |
| 130 |
*/ |
| 131 |
public function ids(): array { |
| 132 |
return $this->ids; |
| 133 |
} |
| 134 |
|
| 135 |
/** How many redirects the request named, before the ceiling was applied. */ |
| 136 |
public function requestedCount(): int { |
| 137 |
return $this->requestedCount; |
| 138 |
} |
| 139 |
|
| 140 |
/** Whether the request named more redirects than one screen may carry. */ |
| 141 |
public function wasRefusedAsTooMany(): bool { |
| 142 |
return $this->kind === self::KIND_REFUSED_TOO_MANY; |
| 143 |
} |
| 144 |
} |
| 145 |
|