PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.8
GiveWP – Donation Plugin and Fundraising Platform v4.16.8
4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 All 253 releases
give / src / Framework / PaymentGateways / Routes / RouteSignature.php
RouteSignature.php
219 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Framework\PaymentGateways\Routes;
4
5 use Give\Framework\PaymentGateways\DataTransferObjects\GatewayRouteData;
6 use Give\Framework\Shims\Shim;
7
8 /**
9 * Route signature for creating secure gateway route methods
10 *
11 * @since 2.19.0
12 */
13 class RouteSignature
14 {
15 /**
16 * @var string
17 */
18 private $signature;
19 /**
20 * @var string
21 */
22 public $expiration;
23 /**
24 * Keys of the query args covered by this signature, sorted.
25 *
26 * @since 4.16.8
27 * @var string[]
28 */
29 public $argKeys;
30
31 /**
32 * @since 2.19.5 replace wp_create_nonce with wp_hash and timestamp expiration
33 * @since 2.19.4 replace RouteSignature args with unique donationId
34 * @since 2.19.0
35 *
36 * @since 4.16.8 add $args so the route's own query args are covered
37 *
38 * @param int $gatewayId
39 * @param string $gatewayMethod
40 * @param int $donationId
41 * @param string $expiration
42 * @param array $args Query args the route carries, which the signature then covers. Values the
43 * URL cannot carry — false, and null or empty arrays at any depth — are dropped.
44 */
45 public function __construct($gatewayId, $gatewayMethod, $donationId, $expiration = null, array $args = [])
46 {
47 // add_query_arg leaves a false arg off the URL entirely, but only at the top level; a nested
48 // false is serialized as '0' by both query builders and round-trips.
49 $args = array_filter($args, static function ($value) {
50 return $value !== false;
51 });
52
53 // Null and empty array values produce no query parameter at any depth, so signing them would
54 // produce a signature the request coming back could never rebuild.
55 $args = self::pruneArgs($args);
56
57 ksort($args);
58
59 $this->argKeys = array_keys($args);
60 $this->expiration = $expiration ?: $this->createExpirationTimestamp();
61 $this->signature = $this->generateSignatureString(
62 $gatewayId,
63 $gatewayMethod,
64 $donationId,
65 $this->expiration,
66 $args
67 );
68 }
69
70
71 /**
72 * Rebuilds the signature a request claims to carry, from the args that request was signed with.
73 *
74 * Args the gateway appended on the way back are not among them, so they are ignored; one that was
75 * signed and has since been edited or dropped changes the hash.
76 *
77 * @since 4.16.8
78 */
79 public static function fromRouteData(GatewayRouteData $data): self
80 {
81 return new self(
82 $data->gatewayId,
83 $data->gatewayMethod,
84 $data->routeSignatureId,
85 $data->routeSignatureExpiration,
86 array_intersect_key($data->queryParams, array_flip($data->routeSignatureArgKeys))
87 );
88 }
89
90 /**
91 * Drops null and empty array values at any depth, children first, so a parent left holding
92 * nothing goes with them.
93 *
94 * @since 4.16.8
95 */
96 private static function pruneArgs(array $args): array
97 {
98 $args = array_map(static function ($value) {
99 return is_array($value) ? self::pruneArgs($value) : $value;
100 }, $args);
101
102 return array_filter($args, static function ($value) {
103 return $value !== null && $value !== [];
104 });
105 }
106
107 /**
108 * Normalizes args to what the request coming back will carry: PHP urldecodes each query value
109 * once (%XX to its character, + to a space), and GatewayRoute then runs the request through
110 * give_clean(). Signing the raw values instead rejects a genuine return whenever a value
111 * changes shape in transit — a rawurlencoded return URL being the everyday case.
112 *
113 * Only URL generation runs this; values arriving on a request have been through the real thing.
114 *
115 * @since 4.16.8 normalize nested values too, as PHP decodes and give_clean() cleans at every depth
116 * @since 4.16.8
117 */
118 public static function normalizeArgs(array $args): array
119 {
120 return array_map(static function ($value) {
121 if (is_array($value)) {
122 return self::normalizeArgs($value);
123 }
124
125 return is_string($value) ? give_clean(urldecode($value)) : $value;
126 }, $args);
127 }
128
129 /**
130 * Encodes normalized args for the URL. add_query_arg() writes values as given, so a raw value
131 * holding an ampersand — a legacy form's success URL under plain permalinks — is split into
132 * separate parameters on the way back and the signed value never round-trips. Encoding what was
133 * signed means PHP's single decode hands the route exactly that value, however the gateway
134 * shaped it.
135 *
136 * @since 4.16.8
137 */
138 public static function encodeArgs(array $args): array
139 {
140 return array_map(static function ($value) {
141 if (is_array($value)) {
142 return self::encodeArgs($value);
143 }
144
145 return is_string($value) ? rawurlencode($value) : $value;
146 }, $args);
147 }
148
149 /**
150 * @since 2.19.5
151 *
152 * @param string $gatewayId
153 * @param string $gatewayMethod
154 * @param int $donationId
155 * @since 4.16.8 append the route's query args
156 *
157 * @param string $expiration
158 * @return string
159 */
160 private function generateSignatureString($gatewayId, $gatewayMethod, $donationId, $expiration, array $args = [])
161 {
162 $signature = "$gatewayId@$gatewayMethod:$donationId|$expiration";
163
164 // A signature made before args were covered has none, and has to keep hashing to what it did then.
165 return $args ? $signature . '|' . http_build_query($args) : $signature;
166 }
167
168 /**
169 * @since 2.19.0
170 *
171 * @return string
172 */
173 public function toString()
174 {
175 return $this->signature;
176 }
177
178 /**
179 * @since 2.19.5
180 *
181 * @return string
182 */
183 public function toHash()
184 {
185 return wp_hash($this->signature);
186 }
187
188 /**
189 * Create expiration timestamp
190 *
191 * @since 2.19.5
192 *
193 * @return string
194 */
195 public function createExpirationTimestamp()
196 {
197 return (string)current_datetime()->modify('+1 day')->getTimestamp();
198 }
199
200
201 /**
202 * @since 2.19.5
203 *
204 * @param string $suppliedSignature
205 * @return bool
206 */
207 public function isValid($suppliedSignature)
208 {
209 $isSignatureValid = hash_equals(
210 $suppliedSignature,
211 $this->toHash()
212 );
213
214 $isNotExpired = ((int)$this->expiration) >= current_datetime()->getTimestamp();
215
216 return $isSignatureValid && $isNotExpired;
217 }
218 }
219