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