1<?php
2
3/**
4 * Simple registration request and response parsing and object
5 * representation.
6 *
7 * This module contains objects representing simple registration
8 * requests and responses that can be used with both OpenID relying
9 * parties and OpenID providers.
10 *
11 * 1. The relying party creates a request object and adds it to the
12 * {@link Auth_OpenID_AuthRequest} object before making the
13 * checkid request to the OpenID provider:
14 *
15 *   $sreg_req = Auth_OpenID_SRegRequest::build(array('email'));
16 *   $auth_request->addExtension($sreg_req);
17 *
18 * 2. The OpenID provider extracts the simple registration request
19 * from the OpenID request using {@link
20 * Auth_OpenID_SRegRequest::fromOpenIDRequest}, gets the user's
21 * approval and data, creates an {@link Auth_OpenID_SRegResponse}
22 * object and adds it to the id_res response:
23 *
24 *   $sreg_req = Auth_OpenID_SRegRequest::fromOpenIDRequest(
25 *                                  $checkid_request);
26 *   // [ get the user's approval and data, informing the user that
27 *   //   the fields in sreg_response were requested ]
28 *   $sreg_resp = Auth_OpenID_SRegResponse::extractResponse(
29 *                                  $sreg_req, $user_data);
30 *   $sreg_resp->toMessage($openid_response->fields);
31 *
32 * 3. The relying party uses {@link
33 * Auth_OpenID_SRegResponse::fromSuccessResponse} to extract the data
34 * from the OpenID response:
35 *
36 *   $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse(
37 *                                  $success_response);
38 *
39 * @package OpenID
40 */
41
42/**
43 * Import message and extension internals.
44 */
45require_once 'Auth/OpenID/Message.php';
46require_once 'Auth/OpenID/Extension.php';
47
48// The data fields that are listed in the sreg spec
49global $Auth_OpenID_sreg_data_fields;
50$Auth_OpenID_sreg_data_fields = [
51    'fullname' => 'Full Name',
52    'nickname' => 'Nickname',
53    'dob' => 'Date of Birth',
54    'email' => 'E-mail Address',
55    'gender' => 'Gender',
56    'postcode' => 'Postal Code',
57    'country' => 'Country',
58    'language' => 'Language',
59    'timezone' => 'Time Zone',
60];
61
62/**
63 * Check to see that the given value is a valid simple registration
64 * data field name.  Return true if so, false if not.
65 *
66 * @param string $field_name
67 * @return bool
68 */
69function Auth_OpenID_checkFieldName($field_name)
70{
71    global $Auth_OpenID_sreg_data_fields;
72
73    if (!in_array($field_name, array_keys($Auth_OpenID_sreg_data_fields))) {
74        return false;
75    }
76    return true;
77}
78
79// URI used in the wild for Yadis documents advertising simple
80// registration support
81define('Auth_OpenID_SREG_NS_URI_1_0', 'http://openid.net/sreg/1.0');
82
83// URI in the draft specification for simple registration 1.1
84// <http://openid.net/specs/openid-simple-registration-extension-1_1-01.html>
85define('Auth_OpenID_SREG_NS_URI_1_1', 'http://openid.net/extensions/sreg/1.1');
86
87// This attribute will always hold the preferred URI to use when
88// adding sreg support to an XRDS file or in an OpenID namespace
89// declaration.
90define('Auth_OpenID_SREG_NS_URI', Auth_OpenID_SREG_NS_URI_1_1);
91
92Auth_OpenID_registerNamespaceAlias(Auth_OpenID_SREG_NS_URI_1_1, 'sreg');
93
94/**
95 * Does the given endpoint advertise support for simple
96 * registration?
97 *
98 * @param Auth_OpenID_ServiceEndpoint $endpoint The endpoint object as returned by OpenID discovery.
99 * returns whether an sreg type was advertised by the endpoint
100 * @return bool
101 */
102function Auth_OpenID_supportsSReg($endpoint)
103{
104    return ($endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_1) ||
105            $endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_0));
106}
107
108/**
109 * A base class for classes dealing with Simple Registration protocol
110 * messages.
111 *
112 * @package OpenID
113 */
114class Auth_OpenID_SRegBase extends Auth_OpenID_Extension {
115    /**
116     * Extract the simple registration namespace URI from the given
117     * OpenID message. Handles OpenID 1 and 2, as well as both sreg
118     * namespace URIs found in the wild, as well as missing namespace
119     * definitions (for OpenID 1)
120     *
121     * $message: The OpenID message from which to parse simple
122     * registration fields. This may be a request or response message.
123     *
124     * Returns the sreg namespace URI for the supplied message. The
125     * message may be modified to define a simple registration
126     * namespace.
127     *
128     * @access private
129     * @param Auth_OpenID_Message $message
130     * @return mixed|null|string
131     */
132    static function _getSRegNS($message)
133    {
134        $alias = null;
135        $found_ns_uri = null;
136
137        // See if there exists an alias for one of the two defined
138        // simple registration types.
139        foreach ([Auth_OpenID_SREG_NS_URI_1_1, Auth_OpenID_SREG_NS_URI_1_0] as $sreg_ns_uri) {
140            $alias = $message->namespaces->getAlias($sreg_ns_uri);
141            if ($alias !== null) {
142                $found_ns_uri = $sreg_ns_uri;
143                break;
144            }
145        }
146
147        if ($alias === null) {
148            // There is no alias for either of the types, so try to
149            // add one. We default to using the modern value (1.1)
150            $found_ns_uri = Auth_OpenID_SREG_NS_URI_1_1;
151            if ($message->namespaces->addAlias(Auth_OpenID_SREG_NS_URI_1_1,
152                                               'sreg') === null) {
153                // An alias for the string 'sreg' already exists, but
154                // it's defined for something other than simple
155                // registration
156                return null;
157            }
158        }
159
160        return $found_ns_uri;
161    }
162}
163
164/**
165 * An object to hold the state of a simple registration request.
166 *
167 * required: A list of the required fields in this simple registration
168 * request
169 *
170 * optional: A list of the optional fields in this simple registration
171 * request
172 *
173 * @package OpenID
174 */
175class Auth_OpenID_SRegRequest extends Auth_OpenID_SRegBase {
176
177    /** @var string  */
178    public $ns_alias = 'sreg';
179    /** @var array  */
180    public $required = [];
181    /** @var array  */
182    public $optional = [];
183    /** @var string  */
184    public $policy_url = '';
185
186    /**
187     * Initialize an empty simple registration request.
188     *
189     * @param null $required
190     * @param null $optional
191     * @param null $policy_url
192     * @param string $sreg_ns_uri
193     * @param string $cls
194     * @return null
195     */
196    static function build($required=null, $optional=null,
197                   $policy_url=null,
198                   $sreg_ns_uri=Auth_OpenID_SREG_NS_URI,
199                   $cls='Auth_OpenID_SRegRequest')
200    {
201        /** @var Auth_OpenID_SRegRequest $obj */
202        $obj = new $cls();
203
204        $obj->required = [];
205        $obj->optional = [];
206        $obj->policy_url = $policy_url;
207        $obj->ns_uri = $sreg_ns_uri;
208
209        if ($required) {
210            if (!$obj->requestFields($required, true, true)) {
211                return null;
212            }
213        }
214
215        if ($optional) {
216            if (!$obj->requestFields($optional, false, true)) {
217                return null;
218            }
219        }
220
221        return $obj;
222    }
223
224    /**
225     * Create a simple registration request that contains the fields
226     * that were requested in the OpenID request with the given
227     * arguments
228     *
229     * $request: The OpenID authentication request from which to
230     * extract an sreg request.
231     *
232     * $cls: name of class to use when creating sreg request object.
233     * Used for testing.
234     *
235     * Returns the newly created simple registration request
236     *
237     * @param Auth_OpenID_Request $request
238     * @param string $cls
239     * @return Auth_OpenID_SRegRequest|null
240     */
241    static function fromOpenIDRequest($request, $cls='Auth_OpenID_SRegRequest')
242    {
243
244        $obj = call_user_func_array([$cls, 'build'],
245                 [null, null, null, Auth_OpenID_SREG_NS_URI, $cls]);
246
247        // Since we're going to mess with namespace URI mapping, don't
248        // mutate the object that was passed in.
249        $m = $request->message;
250
251        $obj->ns_uri = $obj->_getSRegNS($m);
252        $args = $m->getArgs($obj->ns_uri);
253
254        if ($args === null || Auth_OpenID::isFailure($args)) {
255            return null;
256        }
257
258        $obj->parseExtensionArgs($args);
259
260        return $obj;
261    }
262
263    /**
264     * Parse the unqualified simple registration request parameters
265     * and add them to this object.
266     *
267     * This method is essentially the inverse of
268     * getExtensionArgs. This method restores the serialized simple
269     * registration request fields.
270     *
271     * If you are extracting arguments from a standard OpenID
272     * checkid_* request, you probably want to use fromOpenIDRequest,
273     * which will extract the sreg namespace and arguments from the
274     * OpenID request. This method is intended for cases where the
275     * OpenID server needs more control over how the arguments are
276     * parsed than that method provides.
277     *
278     * $args == $message->getArgs($ns_uri);
279     * $request->parseExtensionArgs($args);
280     *
281     * $args: The unqualified simple registration arguments
282     *
283     * strict: Whether requests with fields that are not defined in
284     * the simple registration specification should be tolerated (and
285     * ignored)
286     *
287     * @param array $args
288     * @param bool $strict
289     * @return bool
290     */
291    function parseExtensionArgs($args, $strict=false)
292    {
293        foreach (['required', 'optional'] as $list_name) {
294            $required = ($list_name == 'required');
295            $items = Auth_OpenID::arrayGet($args, $list_name);
296            if ($items) {
297                foreach (explode(',', $items) as $field_name) {
298                    if (!$this->requestField($field_name, $required, $strict)) {
299                        if ($strict) {
300                            return false;
301                        }
302                    }
303                }
304            }
305        }
306
307        $this->policy_url = Auth_OpenID::arrayGet($args, 'policy_url');
308
309        return true;
310    }
311
312    /**
313     * A list of all of the simple registration fields that were
314     * requested, whether they were required or optional.
315     */
316    function allRequestedFields()
317    {
318        return array_merge($this->required, $this->optional);
319    }
320
321    /**
322     * Have any simple registration fields been requested?
323     */
324    function wereFieldsRequested()
325    {
326        return count($this->allRequestedFields());
327    }
328
329    /**
330     * Was this field in the request?
331     *
332     * @param string $field_name
333     * @return bool
334     */
335    function contains($field_name)
336    {
337        return (in_array($field_name, $this->required) ||
338                in_array($field_name, $this->optional));
339    }
340
341    /**
342     * Request the specified field from the OpenID user
343     *
344     * $field_name: the unqualified simple registration field name
345     *
346     * required: whether the given field should be presented to the
347     * user as being a required to successfully complete the request
348     *
349     * strict: whether to raise an exception when a field is added to
350     * a request more than once
351     *
352     * @param string $field_name
353     * @param bool $required
354     * @param bool $strict
355     * @return bool
356     */
357    function requestField($field_name,
358                          $required=false, $strict=false)
359    {
360        if (!Auth_OpenID_checkFieldName($field_name)) {
361            return false;
362        }
363
364        if ($strict) {
365            if ($this->contains($field_name)) {
366                return false;
367            }
368        } else {
369            if (in_array($field_name, $this->required)) {
370                return true;
371            }
372
373            if (in_array($field_name, $this->optional)) {
374                if ($required) {
375                    unset($this->optional[array_search($field_name,
376                                                       $this->optional)]);
377                } else {
378                    return true;
379                }
380            }
381        }
382
383        if ($required) {
384            $this->required[] = $field_name;
385        } else {
386            $this->optional[] = $field_name;
387        }
388
389        return true;
390    }
391
392    /**
393     * Add the given list of fields to the request
394     *
395     * field_names: The simple registration data fields to request
396     *
397     * required: Whether these values should be presented to the user
398     * as required
399     *
400     * strict: whether to raise an exception when a field is added to
401     * a request more than once
402     *
403     * @param string $field_names
404     * @param bool $required
405     * @param bool $strict
406     * @return bool
407     */
408    function requestFields($field_names, $required=false, $strict=false)
409    {
410        if (!is_array($field_names)) {
411            return false;
412        }
413
414        foreach ($field_names as $field_name) {
415            if (!$this->requestField($field_name, $required, $strict)) {
416                return false;
417            }
418        }
419
420        return true;
421    }
422
423    /**
424     * Get a dictionary of unqualified simple registration arguments
425     * representing this request.
426     *
427     * This method is essentially the inverse of
428     * C{L{parseExtensionArgs}}. This method serializes the simple
429     * registration request fields.
430     *
431     * @param Auth_OpenID_Request|null $request
432     * @return array|null
433     */
434    function getExtensionArgs($request = null)
435    {
436        $args = [];
437
438        if ($this->required) {
439            $args['required'] = implode(',', $this->required);
440        }
441
442        if ($this->optional) {
443            $args['optional'] = implode(',', $this->optional);
444        }
445
446        if ($this->policy_url) {
447            $args['policy_url'] = $this->policy_url;
448        }
449
450        return $args;
451    }
452}
453
454/**
455 * Represents the data returned in a simple registration response
456 * inside of an OpenID C{id_res} response. This object will be created
457 * by the OpenID server, added to the C{id_res} response object, and
458 * then extracted from the C{id_res} message by the Consumer.
459 *
460 * @package OpenID
461 */
462class Auth_OpenID_SRegResponse extends Auth_OpenID_SRegBase {
463
464    /** @var string  */
465    public $ns_alias = 'sreg';
466
467    /** @var array */
468    public $data = [];
469
470    function __construct($data=null, $sreg_ns_uri=Auth_OpenID_SREG_NS_URI)
471    {
472        if ($data !== null) {
473            $this->data = $data;
474        }
475
476        $this->ns_uri = $sreg_ns_uri;
477    }
478
479    /**
480     * Take a C{L{SRegRequest}} and a dictionary of simple
481     * registration values and create a C{L{SRegResponse}} object
482     * containing that data.
483     *
484     * request: The simple registration request object
485     *
486     * data: The simple registration data for this response, as a
487     * dictionary from unqualified simple registration field name to
488     * string (unicode) value. For instance, the nickname should be
489     * stored under the key 'nickname'.
490     *
491     * @param Auth_OpenID_SRegRequest $request
492     * @param array $data
493     * @return Auth_OpenID_SRegResponse
494     */
495    static function extractResponse($request, $data)
496    {
497        $obj = new Auth_OpenID_SRegResponse();
498        $obj->ns_uri = $request->ns_uri;
499
500        foreach ($request->allRequestedFields() as $field) {
501            $value = Auth_OpenID::arrayGet($data, $field);
502            if ($value !== null) {
503                $obj->data[$field] = $value;
504            }
505        }
506
507        return $obj;
508    }
509
510    /**
511     * Create a C{L{SRegResponse}} object from a successful OpenID
512     * library response
513     * (C{L{openid.consumer.consumer.SuccessResponse}}) response
514     * message
515     *
516     * success_response: A SuccessResponse from consumer.complete()
517     *
518     * signed_only: Whether to process only data that was
519     * signed in the id_res message from the server.
520     *
521     * Returns a simple registration response containing the data that
522     * was supplied with the C{id_res} response.
523     *
524     * @param Auth_OpenID_SuccessResponse $success_response
525     * @param bool $signed_only
526     * @return Auth_OpenID_SRegResponse|null
527     */
528    static function fromSuccessResponse($success_response, $signed_only=true)
529    {
530        global $Auth_OpenID_sreg_data_fields;
531
532        $obj = new Auth_OpenID_SRegResponse();
533        $obj->ns_uri = $obj->_getSRegNS($success_response->message);
534
535        if ($signed_only) {
536            $args = $success_response->getSignedNS($obj->ns_uri);
537        } else {
538            $args = $success_response->message->getArgs($obj->ns_uri);
539        }
540
541        if ($args === null || Auth_OpenID::isFailure($args)) {
542            return null;
543        }
544
545        foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) {
546            if (in_array($field_name, array_keys($args))) {
547                $obj->data[$field_name] = $args[$field_name];
548            }
549        }
550
551        return $obj;
552    }
553
554    /**
555     * Get the string arguments that should be added to an OpenID
556     * message for this extension.
557     *
558     * @param Auth_OpenID_Request|null $request
559     * @return null
560     */
561    function getExtensionArgs($request = null)
562    {
563        return $this->data;
564    }
565
566    // Read-only dictionary interface
567    function get($field_name, $default=null)
568    {
569        if (!Auth_OpenID_checkFieldName($field_name)) {
570            return null;
571        }
572
573        return Auth_OpenID::arrayGet($this->data, $field_name, $default);
574    }
575
576    function contents()
577    {
578        return $this->data;
579    }
580}
581
582
583