← All changes
|
src/Framework/PaymentGateways/Routes/RouteSignature.php
+118
-4
4.16.7
→
4.17.0
View file →
| @@ -1,8 +1,9 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace Give\Framework\PaymentGateways\Routes; |
| 4 | 4 | |
| 5 | +use Give\Framework\PaymentGateways\DataTransferObjects\GatewayRouteData; | |
| 5 | 6 | use Give\Framework\Shims\Shim; |
| 6 | 7 | |
| 7 | 8 | /** |
| 8 | 9 | * Route signature for creating secure gateway route methods |
| @@ -18,8 +19,15 @@ | ||
| 18 | 19 | /** |
| 19 | 20 | * @var string |
| 20 | 21 | */ |
| 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; | |
| 22 | 30 | |
| 23 | 31 | /** |
| 24 | 32 | * @since 2.19.5 replace wp_create_nonce with wp_hash and timestamp expiration |
| 25 | 33 | * @since 2.19.4 replace RouteSignature args with unique donationId |
| @@ -24,32 +32,138 @@ | ||
| 24 | 32 | * @since 2.19.5 replace wp_create_nonce with wp_hash and timestamp expiration |
| 25 | 33 | * @since 2.19.4 replace RouteSignature args with unique donationId |
| 26 | 34 | * @since 2.19.0 |
| 27 | 35 | * |
| 36 | + * @since 4.16.8 add $args so the route's own query args are covered | |
| 37 | + * | |
| 28 | 38 | * @param int $gatewayId |
| 29 | 39 | * @param string $gatewayMethod |
| 30 | 40 | * @param int $donationId |
| 31 | 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. | |
| 32 | 44 | */ |
| 33 | - public function __construct($gatewayId, $gatewayMethod, $donationId, $expiration = null) | |
| 45 | + public function __construct($gatewayId, $gatewayMethod, $donationId, $expiration = null, array $args = []) | |
| 34 | 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); | |
| 35 | 60 | $this->expiration = $expiration ?: $this->createExpirationTimestamp(); |
| 36 | - $this->signature = $this->generateSignatureString($gatewayId, $gatewayMethod, $donationId, $this->expiration); | |
| 61 | + $this->signature = $this->generateSignatureString( | |
| 62 | + $gatewayId, | |
| 63 | + $gatewayMethod, | |
| 64 | + $donationId, | |
| 65 | + $this->expiration, | |
| 66 | + $args | |
| 67 | + ); | |
| 37 | 68 | } |
| 38 | 69 | |
| 39 | 70 | |
| 40 | 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 | + /** | |
| 41 | 150 | * @since 2.19.5 |
| 42 | 151 | * |
| 43 | 152 | * @param string $gatewayId |
| 44 | 153 | * @param string $gatewayMethod |
| 45 | 154 | * @param int $donationId |
| 155 | + * @since 4.16.8 append the route's query args | |
| 156 | + * | |
| 46 | 157 | * @param string $expiration |
| 47 | 158 | * @return string |
| 48 | 159 | */ |
| 49 | - private function generateSignatureString($gatewayId, $gatewayMethod, $donationId, $expiration) | |
| 160 | + private function generateSignatureString($gatewayId, $gatewayMethod, $donationId, $expiration, array $args = []) | |
| 50 | 161 | { |
| 51 | - return "$gatewayId@$gatewayMethod:$donationId|$expiration"; | |
| 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; | |
| 52 | 166 | } |
| 53 | 167 | |
| 54 | 168 | /** |
| 55 | 169 | * @since 2.19.0 |