1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Protocols\OpenID;
24:
25: /**
26: * Class representing an OpenID response.
27: *
28: * Response parameters are stored *without* the `openid.` prefix. This
29: * prefix is added by the {@link render()} function when it is
30: * required.
31: */
32: class Response extends Message {
33:
34: /** Parameter for {@link $indirect_component} */
35: const OPENID_RESPONSE_QUERY = 0;
36: /** Parameter for {@link $indirect_component} */
37: const OPENID_RESPONSE_FRAGMENT = 1;
38:
39: /** @var array<string> an array of fields to be signed */
40: private $signed_fields = [];
41:
42: /**
43: * @var int the number suffix to use if an extension alias needs
44: * to be automatically generated.
45: */
46: protected $extension_autonum = 1;
47:
48: /** @var int for indirect communication, where in the URL should the
49: * response be encoded. This can be one of OPENID_RESPONSE_QUERY
50: * (always in the query string), OPENID_RESPONSE_FRAGMENT (always in the fragment) */
51: protected $indirect_component = self::OPENID_RESPONSE_QUERY;
52:
53: /**
54: * Creates an OpenID response.
55: *
56: * An OpenID response is created based on an OpenID request. The
57: * response will contain the same OpenID version, as well as the same
58: * extension URI-to-alias mapping as the underlying request.
59: *
60: * @param Request|array<string, string>|null $request the request to which the response will
61: * be made
62: */
63: public function __construct($request = NULL) {
64: if ($request === null) return;
65: if (!$request instanceof Request) $request = new Request($request);
66:
67: $this->setVersion($request->getVersion());
68: $this->extension_map = $request->getExtensionMap();
69:
70: foreach ($request as $key => $value) {
71: $value = strval($value);
72: if (strpos($key, 'openid.ns.') === 0) {
73: $alias = substr($key, 10);
74: $this->extension_map[$value] = $alias;
75: }
76: }
77: }
78:
79: /**
80: * Sets the OpenID version to be used for this response.
81: *
82: * If $version is {@link OPENID_VERSION_2}, the OpenID 2.0
83: * namespace will be added to the response.
84: *
85: * @param int $version the OpenID version
86: * @return void
87: */
88: public function setVersion($version) {
89: if ($version == Message::OPENID_VERSION_2) {
90: $this->set('ns', Message::OPENID_NS_2_0);
91: } else {
92: $this->offsetUnset('ns');
93: }
94: }
95:
96: /**
97: * Sets a field in the response
98: *
99: * @param string $field the field to set
100: * @param string $value the value
101: * @param bool|null $signed whether this field should be included in the
102: * signature
103: * @return void
104: */
105: public function set($field, $value, $signed = NULL) {
106: $this->container[$field] = $value;
107:
108: if ($signed === null) $signed = (!in_array($field, [ 'mode', 'signed', 'sig' ]));
109:
110: if ($signed) $this->signed_fields[] = $field;
111: }
112:
113: /**
114: * Sets multiple fields in the response.
115: *
116: * @param array<string, string> $data the fields and values to set
117: * @param bool|null $signed whether this field should be included in the
118: * signature
119: * @return void
120: */
121: public function setArray($data, $signed = NULL) {
122: foreach ($data as $key => $value) {
123: $this->set($key, $value, $signed);
124: }
125: }
126:
127: /**
128: * Gets the component to be used in indirect responses.
129: *
130: * @return int the component
131: */
132: public function getIndirectComponent() {
133: return $this->indirect_component;
134: }
135:
136: /**
137: * Sets the component to be used in indirect responses. This should
138: * be either OPENID_RESPONSE_QUERY or OPENID_RESPONSE_FRAGMENT
139: *
140: * @param int $indirect_component the component
141: * @return void
142: */
143: public function setIndirectComponent($indirect_component) {
144: $this->indirect_component = $indirect_component;
145: }
146:
147: /**
148: * Sends an OpenID assertion response.
149: *
150: * The OpenID specification version 2.0 provides for the sending of assertions
151: * via indirect communication. However, future versions of the OpenID
152: * specification may provide for sending of assertions via direct communication.
153: *
154: * @param string $indirect_url the URL to which the OpenID response is sent. If
155: * this is null, the response is sent via direct communication
156: * @return void
157: */
158: public function render($indirect_url = NULL) {
159: if ($indirect_url) {
160: $f3 = \Base::instance();
161:
162: $f3->status(303);
163: header('Location: ' . $this->toIndirectURL($indirect_url));
164: } else {
165: header("Content-Type: text/plain");
166: print $this->toDirectMessage();
167: }
168: }
169:
170: /**
171: * Encodes the response in key-value format
172: *
173: * @return string the encoded response
174: */
175: public function toDirectMessage() {
176: return parent::toKeyValueForm($this->container);
177: }
178:
179: /**
180: * Encodes the response in application/x-www-form-urlencoded format.
181: *
182: * @param string $url the URL to which the OpenID response is sent.
183: * @return string the encoded message
184: * @since 0.8
185: */
186: public function toIndirectURL($url) {
187: // 1. Firstly, get the query string
188: $query_array = [];
189: foreach ($this->container as $key => $value) $query_array['openid.' . $key] = $value;
190: $query = str_replace([ '+', '%7E' ], [ '%20', '~' ], http_build_query($query_array));
191:
192: // 2. If there is no query string, then we just return the URL
193: if (!$query) return $url;
194:
195: // 3. The URL may already have a query and a fragment. If this is so, we
196: // need to slot in the new query string properly. We disassemble and
197: // reconstruct the URL.
198: $parts = parse_url($url);
199: if ($parts == false) return $url;
200:
201: $url = $parts['scheme'] . '://';
202: if (isset($parts['user'])) {
203: $url .= $parts['user'];
204: if (isset($parts['pass'])) $url .= ':' . $parts['pass'];
205: $url .= '@';
206: }
207: if (isset($parts['host'])) $url .= $parts['host'];
208: if (isset($parts['port'])) $url .= ':' . $parts['port'];
209: if (isset($parts['path'])) $url .= $parts['path'];
210:
211: if (($this->getIndirectComponent() == self::OPENID_RESPONSE_QUERY) || (strpos($url, '#') === FALSE)) {
212: $url .= '?' . ((isset($parts['query'])) ? $parts['query'] . '&' : '') . $query;
213: if (isset($parts['fragment'])) $url .= '#' . $parts['fragment'];
214: } elseif ($this->getIndirectComponent() == self::OPENID_RESPONSE_FRAGMENT) {
215: // In theory $parts['fragment'] should be an empty string, but the
216: // current draft specification does not prohibit putting other things
217: // in the fragment.
218: if (!isset($parts['fragment'])) $parts['fragment'] = '';
219: if (isset($parts['query'])) {
220: $url .= '?' . $parts['query'] . '#' . $parts['fragment'] . '&' . $query;
221: } else {
222: $url .= '#' . $parts['fragment'] . '?' . $query;
223: }
224: }
225: return $url;
226: }
227:
228: /**
229: * Calculates the base string from which an OpenID signature is generated.
230: *
231: * @return string the signature base string
232: * @link http://openid.net/specs/openid-authentication-2_0.html#anchor11
233: */
234: public function getSignatureBaseString() {
235: // Remove duplicates
236: $this->signed_fields = array_keys(array_flip($this->signed_fields));
237:
238: // Update signed
239: $this->set('signed', implode(',', $this->signed_fields), false);
240:
241: return $this->buildSignatureBaseString($this->signed_fields);
242: }
243:
244: /**
245: * Returns the OpenID alias for an extension, given a Type URI, based on the
246: * alias definitions in the current OpenID request.
247: *
248: * @param string $ns the Type URI
249: * @param bool|string $create whether to create an alias if the Type URI does not already
250: * have an alias in the current OpenID request. If this parameter is a string,
251: * then the string specified is the preferred alias to be created, unless a collision
252: * occurs
253: * @return string|null the alias, or NULL if the Type URI does not already
254: * have an alias in the current OpenID request <i>and</i> $create is false
255: */
256: public function getAliasForExtension($ns, $create = FALSE) {
257: if (isset($this->extension_map[$ns])) return $this->extension_map[$ns];
258: if ($create !== FALSE) {
259: $alias = 'e' . $this->extension_autonum;
260:
261: if ($create === TRUE) {
262: $this->extension_autonum++;
263: } elseif (is_string($create)) {
264: $used_aliases = array_values($this->extension_map);
265:
266: $alias = $create;
267: $i = 0;
268:
269: while (in_array($alias, $used_aliases)) {
270: $i++;
271: $alias = $create . $i;
272: }
273: }
274: $this->extension_map[$ns] = $alias;
275: return $alias;
276: }
277: return NULL;
278: }
279:
280: /**
281: * Returns the prefix to be used for extension searching.
282: *
283: * @return string
284: */
285: protected function getPrefix() {
286: return '';
287: }
288:
289: /**
290: * Convenient function to create an error response.
291: *
292: * @param string $error the error message
293: * @param array<string, string> $additional any additional data to be sent with the error
294: * message
295: * @param Request $request the request
296: * @return Response
297: */
298: static public function createError($error, $additional = [], $request = NULL) {
299: $response = new Response($request);
300: $response->loadData(array_merge([ 'error' => $error ], $additional));
301: return $response;
302: }
303:
304: /** Signed fields*/
305: public function offsetSet($offset, $value): void {
306: if (is_null($offset)) {
307: parent::offsetSet($offset, $value);
308: } else {
309: $this->set($offset, $value);
310: }
311: }
312:
313: /** Signed fields*/
314: public function offsetUnset($offset): void {
315: parent::offsetUnset($offset);
316: $this->signed_fields = array_diff($this->signed_fields, [ $offset ]);
317: }
318: }
319:
320: ?>