PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / services / RequestedRedirectIds.php

RequestedRedirectIds.php in 404 Solution trunk, at includes/services/RequestedRedirectIds.php

145 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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