final move of files
[web.mtrack] / Auth / OpenID / AX.php
1 <?php
2
3 /**
4  * Implements the OpenID attribute exchange specification, version 1.0
5  * as of svn revision 370 from openid.net svn.
6  *
7  * @package OpenID
8  */
9
10 /**
11  * Require utility classes and functions for the consumer.
12  */
13 require_once "Auth/OpenID/Extension.php";
14 require_once "Auth/OpenID/Message.php";
15 require_once "Auth/OpenID/TrustRoot.php";
16
17 define('Auth_OpenID_AX_NS_URI',
18        'http://openid.net/srv/ax/1.0');
19
20 // Use this as the 'count' value for an attribute in a FetchRequest to
21 // ask for as many values as the OP can provide.
22 define('Auth_OpenID_AX_UNLIMITED_VALUES', 'unlimited');
23
24 // Minimum supported alias length in characters.  Here for
25 // completeness.
26 define('Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH', 32);
27
28 /**
29  * AX utility class.
30  *
31  * @package OpenID
32  */
33 class Auth_OpenID_AX {
34     /**
35      * @param mixed $thing Any object which may be an
36      * Auth_OpenID_AX_Error object.
37      *
38      * @return bool true if $thing is an Auth_OpenID_AX_Error; false
39      * if not.
40      */
41     function isError($thing)
42     {
43         return is_a($thing, 'Auth_OpenID_AX_Error');
44     }
45 }
46
47 /**
48  * Check an alias for invalid characters; raise AXError if any are
49  * found.  Return None if the alias is valid.
50  */
51 function Auth_OpenID_AX_checkAlias($alias)
52 {
53   if (strpos($alias, ',') !== false) {
54       return new Auth_OpenID_AX_Error(sprintf(
55                    "Alias %s must not contain comma", $alias));
56   }
57   if (strpos($alias, '.') !== false) {
58       return new Auth_OpenID_AX_Error(sprintf(
59                    "Alias %s must not contain period", $alias));
60   }
61
62   return true;
63 }
64
65 /**
66  * Results from data that does not meet the attribute exchange 1.0
67  * specification
68  *
69  * @package OpenID
70  */
71 class Auth_OpenID_AX_Error {
72     function Auth_OpenID_AX_Error($message=null)
73     {
74         $this->message = $message;
75     }
76 }
77
78 /**
79  * Abstract class containing common code for attribute exchange
80  * messages.
81  *
82  * @package OpenID
83  */
84 class Auth_OpenID_AX_Message extends Auth_OpenID_Extension {
85     /**
86      * ns_alias: The preferred namespace alias for attribute exchange
87      * messages
88      */
89     var $ns_alias = 'ax';
90
91     /**
92      * mode: The type of this attribute exchange message. This must be
93      * overridden in subclasses.
94      */
95     var $mode = null;
96
97     var $ns_uri = Auth_OpenID_AX_NS_URI;
98
99     /**
100      * Return Auth_OpenID_AX_Error if the mode in the attribute
101      * exchange arguments does not match what is expected for this
102      * class; true otherwise.
103      *
104      * @access private
105      */
106     function _checkMode($ax_args)
107     {
108         $mode = Auth_OpenID::arrayGet($ax_args, 'mode');
109         if ($mode != $this->mode) {
110             return new Auth_OpenID_AX_Error(
111                             sprintf(
112                                     "Expected mode '%s'; got '%s'",
113                                     $this->mode, $mode));
114         }
115
116         return true;
117     }
118
119     /**
120      * Return a set of attribute exchange arguments containing the
121      * basic information that must be in every attribute exchange
122      * message.
123      *
124      * @access private
125      */
126     function _newArgs()
127     {
128         return array('mode' => $this->mode);
129     }
130 }
131
132 /**
133  * Represents a single attribute in an attribute exchange
134  * request. This should be added to an AXRequest object in order to
135  * request the attribute.
136  *
137  * @package OpenID
138  */
139 class Auth_OpenID_AX_AttrInfo {
140     /**
141      * Construct an attribute information object.  Do not call this
142      * directly; call make(...) instead.
143      *
144      * @param string $type_uri The type URI for this attribute.
145      *
146      * @param int $count The number of values of this type to request.
147      *
148      * @param bool $required Whether the attribute will be marked as
149      * required in the request.
150      *
151      * @param string $alias The name that should be given to this
152      * attribute in the request.
153      */
154     function Auth_OpenID_AX_AttrInfo($type_uri, $count, $required,
155                                      $alias)
156     {
157         /**
158          * required: Whether the attribute will be marked as required
159          * when presented to the subject of the attribute exchange
160          * request.
161          */
162         $this->required = $required;
163
164         /**
165          * count: How many values of this type to request from the
166          * subject. Defaults to one.
167          */
168         $this->count = $count;
169
170         /**
171          * type_uri: The identifier that determines what the attribute
172          * represents and how it is serialized. For example, one type
173          * URI representing dates could represent a Unix timestamp in
174          * base 10 and another could represent a human-readable
175          * string.
176          */
177         $this->type_uri = $type_uri;
178
179         /**
180          * alias: The name that should be given to this attribute in
181          * the request. If it is not supplied, a generic name will be
182          * assigned. For example, if you want to call a Unix timestamp
183          * value 'tstamp', set its alias to that value. If two
184          * attributes in the same message request to use the same
185          * alias, the request will fail to be generated.
186          */
187         $this->alias = $alias;
188     }
189
190     /**
191      * Construct an attribute information object.  For parameter
192      * details, see the constructor.
193      */
194     function make($type_uri, $count=1, $required=false,
195                   $alias=null)
196     {
197         if ($alias !== null) {
198             $result = Auth_OpenID_AX_checkAlias($alias);
199
200             if (Auth_OpenID_AX::isError($result)) {
201                 return $result;
202             }
203         }
204
205         return new Auth_OpenID_AX_AttrInfo($type_uri, $count, $required,
206                                            $alias);
207     }
208
209     /**
210      * When processing a request for this attribute, the OP should
211      * call this method to determine whether all available attribute
212      * values were requested.  If self.count == UNLIMITED_VALUES, this
213      * returns True.  Otherwise this returns False, in which case
214      * self.count is an integer.
215     */
216     function wantsUnlimitedValues()
217     {
218         return $this->count === Auth_OpenID_AX_UNLIMITED_VALUES;
219     }
220 }
221
222 /**
223  * Given a namespace mapping and a string containing a comma-separated
224  * list of namespace aliases, return a list of type URIs that
225  * correspond to those aliases.
226  *
227  * @param $namespace_map The mapping from namespace URI to alias
228  * @param $alias_list_s The string containing the comma-separated
229  * list of aliases. May also be None for convenience.
230  *
231  * @return $seq The list of namespace URIs that corresponds to the
232  * supplied list of aliases. If the string was zero-length or None, an
233  * empty list will be returned.
234  *
235  * return null If an alias is present in the list of aliases but
236  * is not present in the namespace map.
237  */
238 function Auth_OpenID_AX_toTypeURIs(&$namespace_map, $alias_list_s)
239 {
240     $uris = array();
241
242     if ($alias_list_s) {
243         foreach (explode(',', $alias_list_s) as $alias) {
244             $type_uri = $namespace_map->getNamespaceURI($alias);
245             if ($type_uri === null) {
246                 // raise KeyError(
247                 // 'No type is defined for attribute name %r' % (alias,))
248                 return new Auth_OpenID_AX_Error(
249                   sprintf('No type is defined for attribute name %s',
250                           $alias)
251                   );
252             } else {
253                 $uris[] = $type_uri;
254             }
255         }
256     }
257
258     return $uris;
259 }
260
261 /**
262  * An attribute exchange 'fetch_request' message. This message is sent
263  * by a relying party when it wishes to obtain attributes about the
264  * subject of an OpenID authentication request.
265  *
266  * @package OpenID
267  */
268 class Auth_OpenID_AX_FetchRequest extends Auth_OpenID_AX_Message {
269
270     var $mode = 'fetch_request';
271
272     function Auth_OpenID_AX_FetchRequest($update_url=null)
273     {
274         /**
275          * requested_attributes: The attributes that have been
276          * requested thus far, indexed by the type URI.
277          */
278         $this->requested_attributes = array();
279
280         /**
281          * update_url: A URL that will accept responses for this
282          * attribute exchange request, even in the absence of the user
283          * who made this request.
284         */
285         $this->update_url = $update_url;
286     }
287
288     /**
289      * Add an attribute to this attribute exchange request.
290      *
291      * @param attribute: The attribute that is being requested
292      * @return true on success, false when the requested attribute is
293      * already present in this fetch request.
294      */
295     function add($attribute)
296     {
297         if ($this->contains($attribute->type_uri)) {
298             return new Auth_OpenID_AX_Error(
299               sprintf("The attribute %s has already been requested",
300                       $attribute->type_uri));
301         }
302
303         $this->requested_attributes[$attribute->type_uri] = $attribute;
304
305         return true;
306     }
307
308     /**
309      * Get the serialized form of this attribute fetch request.
310      *
311      * @returns Auth_OpenID_AX_FetchRequest The fetch request message parameters
312      */
313     function getExtensionArgs()
314     {
315         $aliases = new Auth_OpenID_NamespaceMap();
316
317         $required = array();
318         $if_available = array();
319
320         $ax_args = $this->_newArgs();
321
322         foreach ($this->requested_attributes as $type_uri => $attribute) {
323             if ($attribute->alias === null) {
324                 $alias = $aliases->add($type_uri);
325             } else {
326                 $alias = $aliases->addAlias($type_uri, $attribute->alias);
327
328                 if ($alias === null) {
329                     return new Auth_OpenID_AX_Error(
330                       sprintf("Could not add alias %s for URI %s",
331                               $attribute->alias, $type_uri
332                       ));
333                 }
334             }
335
336             if ($attribute->required) {
337                 $required[] = $alias;
338             } else {
339                 $if_available[] = $alias;
340             }
341
342             if ($attribute->count != 1) {
343                 $ax_args['count.' . $alias] = strval($attribute->count);
344             }
345
346             $ax_args['type.' . $alias] = $type_uri;
347         }
348
349         if ($required) {
350             $ax_args['required'] = implode(',', $required);
351         }
352
353         if ($if_available) {
354             $ax_args['if_available'] = implode(',', $if_available);
355         }
356
357         return $ax_args;
358     }
359
360     /**
361      * Get the type URIs for all attributes that have been marked as
362      * required.
363      *
364      * @return A list of the type URIs for attributes that have been
365      * marked as required.
366      */
367     function getRequiredAttrs()
368     {
369         $required = array();
370         foreach ($this->requested_attributes as $type_uri => $attribute) {
371             if ($attribute->required) {
372                 $required[] = $type_uri;
373             }
374         }
375
376         return $required;
377     }
378
379     /**
380      * Extract a FetchRequest from an OpenID message
381      *
382      * @param request: The OpenID request containing the attribute
383      * fetch request
384      *
385      * @returns mixed An Auth_OpenID_AX_Error or the
386      * Auth_OpenID_AX_FetchRequest extracted from the request message if
387      * successful
388      */
389     function &fromOpenIDRequest($request)
390     {
391         $m = $request->message;
392         $obj = new Auth_OpenID_AX_FetchRequest();
393         $ax_args = $m->getArgs($obj->ns_uri);
394
395         $result = $obj->parseExtensionArgs($ax_args);
396
397         if (Auth_OpenID_AX::isError($result)) {
398             return $result;
399         }
400
401         if ($obj->update_url) {
402             // Update URL must match the openid.realm of the
403             // underlying OpenID 2 message.
404             $realm = $m->getArg(Auth_OpenID_OPENID_NS, 'realm',
405                         $m->getArg(
406                                   Auth_OpenID_OPENID_NS,
407                                   'return_to'));
408
409             if (!$realm) {
410                 $obj = new Auth_OpenID_AX_Error(
411                   sprintf("Cannot validate update_url %s " .
412                           "against absent realm", $obj->update_url));
413             } else if (!Auth_OpenID_TrustRoot::match($realm,
414                                                      $obj->update_url)) {
415                 $obj = new Auth_OpenID_AX_Error(
416                   sprintf("Update URL %s failed validation against realm %s",
417                           $obj->update_url, $realm));
418             }
419         }
420
421         return $obj;
422     }
423
424     /**
425      * Given attribute exchange arguments, populate this FetchRequest.
426      *
427      * @return $result Auth_OpenID_AX_Error if the data to be parsed
428      * does not follow the attribute exchange specification. At least
429      * when 'if_available' or 'required' is not specified for a
430      * particular attribute type.  Returns true otherwise.
431     */
432     function parseExtensionArgs($ax_args)
433     {
434         $result = $this->_checkMode($ax_args);
435         if (Auth_OpenID_AX::isError($result)) {
436             return $result;
437         }
438
439         $aliases = new Auth_OpenID_NamespaceMap();
440
441         foreach ($ax_args as $key => $value) {
442             if (strpos($key, 'type.') === 0) {
443                 $alias = substr($key, 5);
444                 $type_uri = $value;
445
446                 $alias = $aliases->addAlias($type_uri, $alias);
447
448                 if ($alias === null) {
449                     return new Auth_OpenID_AX_Error(
450                       sprintf("Could not add alias %s for URI %s",
451                               $alias, $type_uri)
452                       );
453                 }
454
455                 $count_s = Auth_OpenID::arrayGet($ax_args, 'count.' . $alias);
456                 if ($count_s) {
457                     $count = Auth_OpenID::intval($count_s);
458                     if (($count === false) &&
459                         ($count_s === Auth_OpenID_AX_UNLIMITED_VALUES)) {
460                         $count = $count_s;
461                     }
462                 } else {
463                     $count = 1;
464                 }
465
466                 if ($count === false) {
467                     return new Auth_OpenID_AX_Error(
468                       sprintf("Integer value expected for %s, got %s",
469                               'count.' . $alias, $count_s));
470                 }
471
472                 $attrinfo = Auth_OpenID_AX_AttrInfo::make($type_uri, $count,
473                                                           false, $alias);
474
475                 if (Auth_OpenID_AX::isError($attrinfo)) {
476                     return $attrinfo;
477                 }
478
479                 $this->add($attrinfo);
480             }
481         }
482
483         $required = Auth_OpenID_AX_toTypeURIs($aliases,
484                          Auth_OpenID::arrayGet($ax_args, 'required'));
485
486         foreach ($required as $type_uri) {
487             $attrib =& $this->requested_attributes[$type_uri];
488             $attrib->required = true;
489         }
490
491         $if_available = Auth_OpenID_AX_toTypeURIs($aliases,
492                              Auth_OpenID::arrayGet($ax_args, 'if_available'));
493
494         $all_type_uris = array_merge($required, $if_available);
495
496         foreach ($aliases->iterNamespaceURIs() as $type_uri) {
497             if (!in_array($type_uri, $all_type_uris)) {
498                 return new Auth_OpenID_AX_Error(
499                   sprintf('Type URI %s was in the request but not ' .
500                           'present in "required" or "if_available"',
501                           $type_uri));
502
503             }
504         }
505
506         $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url');
507
508         return true;
509     }
510
511     /**
512      * Iterate over the AttrInfo objects that are contained in this
513      * fetch_request.
514      */
515     function iterAttrs()
516     {
517         return array_values($this->requested_attributes);
518     }
519
520     function iterTypes()
521     {
522         return array_keys($this->requested_attributes);
523     }
524
525     /**
526      * Is the given type URI present in this fetch_request?
527      */
528     function contains($type_uri)
529     {
530         return in_array($type_uri, $this->iterTypes());
531     }
532 }
533
534 /**
535  * An abstract class that implements a message that has attribute keys
536  * and values. It contains the common code between fetch_response and
537  * store_request.
538  *
539  * @package OpenID
540  */
541 class Auth_OpenID_AX_KeyValueMessage extends Auth_OpenID_AX_Message {
542
543     function Auth_OpenID_AX_KeyValueMessage()
544     {
545         $this->data = array();
546     }
547
548     /**
549      * Add a single value for the given attribute type to the
550      * message. If there are already values specified for this type,
551      * this value will be sent in addition to the values already
552      * specified.
553      *
554      * @param type_uri: The URI for the attribute
555      * @param value: The value to add to the response to the relying
556      * party for this attribute
557      * @return null
558      */
559     function addValue($type_uri, $value)
560     {
561         if (!array_key_exists($type_uri, $this->data)) {
562             $this->data[$type_uri] = array();
563         }
564
565         $values =& $this->data[$type_uri];
566         $values[] = $value;
567     }
568
569     /**
570      * Set the values for the given attribute type. This replaces any
571      * values that have already been set for this attribute.
572      *
573      * @param type_uri: The URI for the attribute
574      * @param values: A list of values to send for this attribute.
575      */
576     function setValues($type_uri, &$values)
577     {
578         $this->data[$type_uri] =& $values;
579     }
580
581     /**
582      * Get the extension arguments for the key/value pairs contained
583      * in this message.
584      *
585      * @param aliases: An alias mapping. Set to None if you don't care
586      * about the aliases for this request.
587      *
588      * @access private
589      */
590     function _getExtensionKVArgs(&$aliases)
591     {
592         if ($aliases === null) {
593             $aliases = new Auth_OpenID_NamespaceMap();
594         }
595
596         $ax_args = array();
597
598         foreach ($this->data as $type_uri => $values) {
599             $alias = $aliases->add($type_uri);
600
601             $ax_args['type.' . $alias] = $type_uri;
602             $ax_args['count.' . $alias] = strval(count($values));
603
604             foreach ($values as $i => $value) {
605               $key = sprintf('value.%s.%d', $alias, $i + 1);
606               $ax_args[$key] = $value;
607             }
608         }
609
610         return $ax_args;
611     }
612
613     /**
614      * Parse attribute exchange key/value arguments into this object.
615      *
616      * @param ax_args: The attribute exchange fetch_response
617      * arguments, with namespacing removed.
618      *
619      * @return Auth_OpenID_AX_Error or true
620      */
621     function parseExtensionArgs($ax_args)
622     {
623         $result = $this->_checkMode($ax_args);
624         if (Auth_OpenID_AX::isError($result)) {
625             return $result;
626         }
627
628         $aliases = new Auth_OpenID_NamespaceMap();
629
630         foreach ($ax_args as $key => $value) {
631             if (strpos($key, 'type.') === 0) {
632                 $type_uri = $value;
633                 $alias = substr($key, 5);
634
635                 $result = Auth_OpenID_AX_checkAlias($alias);
636
637                 if (Auth_OpenID_AX::isError($result)) {
638                     return $result;
639                 }
640
641                 $alias = $aliases->addAlias($type_uri, $alias);
642
643                 if ($alias === null) {
644                     return new Auth_OpenID_AX_Error(
645                       sprintf("Could not add alias %s for URI %s",
646                               $alias, $type_uri)
647                       );
648                 }
649             }
650         }
651
652         foreach ($aliases->iteritems() as $pair) {
653             list($type_uri, $alias) = $pair;
654
655             if (array_key_exists('count.' . $alias, $ax_args)) {
656
657                 $count_key = 'count.' . $alias;
658                 $count_s = $ax_args[$count_key];
659
660                 $count = Auth_OpenID::intval($count_s);
661
662                 if ($count === false) {
663                     return new Auth_OpenID_AX_Error(
664                       sprintf("Integer value expected for %s, got %s",
665                               'count. %s' . $alias, $count_s,
666                               Auth_OpenID_AX_UNLIMITED_VALUES)
667                                                     );
668                 }
669
670                 $values = array();
671                 for ($i = 1; $i < $count + 1; $i++) {
672                     $value_key = sprintf('value.%s.%d', $alias, $i);
673
674                     if (!array_key_exists($value_key, $ax_args)) {
675                       return new Auth_OpenID_AX_Error(
676                         sprintf(
677                                 "No value found for key %s",
678                                 $value_key));
679                     }
680
681                     $value = $ax_args[$value_key];
682                     $values[] = $value;
683                 }
684             } else {
685                 $key = 'value.' . $alias;
686
687                 if (!array_key_exists($key, $ax_args)) {
688                   return new Auth_OpenID_AX_Error(
689                     sprintf(
690                             "No value found for key %s",
691                             $key));
692                 }
693
694                 $value = $ax_args['value.' . $alias];
695
696                 if ($value == '') {
697                     $values = array();
698                 } else {
699                     $values = array($value);
700                 }
701             }
702
703             $this->data[$type_uri] = $values;
704         }
705
706         return true;
707     }
708
709     /**
710      * Get a single value for an attribute. If no value was sent for
711      * this attribute, use the supplied default. If there is more than
712      * one value for this attribute, this method will fail.
713      *
714      * @param type_uri: The URI for the attribute
715      * @param default: The value to return if the attribute was not
716      * sent in the fetch_response.
717      *
718      * @return $value Auth_OpenID_AX_Error on failure or the value of
719      * the attribute in the fetch_response message, or the default
720      * supplied
721      */
722     function getSingle($type_uri, $default=null)
723     {
724         $values = Auth_OpenID::arrayGet($this->data, $type_uri);
725         if (!$values) {
726             return $default;
727         } else if (count($values) == 1) {
728             return $values[0];
729         } else {
730             return new Auth_OpenID_AX_Error(
731               sprintf('More than one value present for %s',
732                       $type_uri)
733               );
734         }
735     }
736
737     /**
738      * Get the list of values for this attribute in the
739      * fetch_response.
740      *
741      * XXX: what to do if the values are not present? default
742      * parameter? this is funny because it's always supposed to return
743      * a list, so the default may break that, though it's provided by
744      * the user's code, so it might be okay. If no default is
745      * supplied, should the return be None or []?
746      *
747      * @param type_uri: The URI of the attribute
748      *
749      * @return $values The list of values for this attribute in the
750      * response. May be an empty list.  If the attribute was not sent
751      * in the response, returns Auth_OpenID_AX_Error.
752      */
753     function get($type_uri)
754     {
755         if (array_key_exists($type_uri, $this->data)) {
756             return $this->data[$type_uri];
757         } else {
758             return new Auth_OpenID_AX_Error(
759               sprintf("Type URI %s not found in response",
760                       $type_uri)
761               );
762         }
763     }
764
765     /**
766      * Get the number of responses for a particular attribute in this
767      * fetch_response message.
768      *
769      * @param type_uri: The URI of the attribute
770      *
771      * @returns int The number of values sent for this attribute.  If
772      * the attribute was not sent in the response, returns
773      * Auth_OpenID_AX_Error.
774      */
775     function count($type_uri)
776     {
777         if (array_key_exists($type_uri, $this->data)) {
778             return count($this->get($type_uri));
779         } else {
780             return new Auth_OpenID_AX_Error(
781               sprintf("Type URI %s not found in response",
782                       $type_uri)
783               );
784         }
785     }
786 }
787
788 /**
789  * A fetch_response attribute exchange message.
790  *
791  * @package OpenID
792  */
793 class Auth_OpenID_AX_FetchResponse extends Auth_OpenID_AX_KeyValueMessage {
794     var $mode = 'fetch_response';
795
796     function Auth_OpenID_AX_FetchResponse($update_url=null)
797     {
798         $this->Auth_OpenID_AX_KeyValueMessage();
799         $this->update_url = $update_url;
800     }
801
802     /**
803      * Serialize this object into arguments in the attribute exchange
804      * namespace
805      *
806      * @return $args The dictionary of unqualified attribute exchange
807      * arguments that represent this fetch_response, or
808      * Auth_OpenID_AX_Error on error.
809      */
810     function getExtensionArgs($request=null)
811     {
812         $aliases = new Auth_OpenID_NamespaceMap();
813
814         $zero_value_types = array();
815
816         if ($request !== null) {
817             // Validate the data in the context of the request (the
818             // same attributes should be present in each, and the
819             // counts in the response must be no more than the counts
820             // in the request)
821
822             foreach ($this->data as $type_uri => $unused) {
823                 if (!$request->contains($type_uri)) {
824                     return new Auth_OpenID_AX_Error(
825                       sprintf("Response attribute not present in request: %s",
826                               $type_uri)
827                       );
828                 }
829             }
830
831             foreach ($request->iterAttrs() as $attr_info) {
832                 // Copy the aliases from the request so that reading
833                 // the response in light of the request is easier
834                 if ($attr_info->alias === null) {
835                     $aliases->add($attr_info->type_uri);
836                 } else {
837                     $alias = $aliases->addAlias($attr_info->type_uri,
838                                                 $attr_info->alias);
839
840                     if ($alias === null) {
841                         return new Auth_OpenID_AX_Error(
842                           sprintf("Could not add alias %s for URI %s",
843                                   $attr_info->alias, $attr_info->type_uri)
844                           );
845                     }
846                 }
847
848                 if (array_key_exists($attr_info->type_uri, $this->data)) {
849                     $values = $this->data[$attr_info->type_uri];
850                 } else {
851                     $values = array();
852                     $zero_value_types[] = $attr_info;
853                 }
854
855                 if (($attr_info->count != Auth_OpenID_AX_UNLIMITED_VALUES) &&
856                     ($attr_info->count < count($values))) {
857                     return new Auth_OpenID_AX_Error(
858                       sprintf("More than the number of requested values " .
859                               "were specified for %s",
860                               $attr_info->type_uri)
861                       );
862                 }
863             }
864         }
865
866         $kv_args = $this->_getExtensionKVArgs($aliases);
867
868         // Add the KV args into the response with the args that are
869         // unique to the fetch_response
870         $ax_args = $this->_newArgs();
871
872         // For each requested attribute, put its type/alias and count
873         // into the response even if no data were returned.
874         foreach ($zero_value_types as $attr_info) {
875             $alias = $aliases->getAlias($attr_info->type_uri);
876             $kv_args['type.' . $alias] = $attr_info->type_uri;
877             $kv_args['count.' . $alias] = '0';
878         }
879
880         $update_url = null;
881         if ($request) {
882             $update_url = $request->update_url;
883         } else {
884             $update_url = $this->update_url;
885         }
886
887         if ($update_url) {
888             $ax_args['update_url'] = $update_url;
889         }
890
891         Auth_OpenID::update(&$ax_args, $kv_args);
892
893         return $ax_args;
894     }
895
896     /**
897      * @return $result Auth_OpenID_AX_Error on failure or true on
898      * success.
899      */
900     function parseExtensionArgs($ax_args)
901     {
902         $result = parent::parseExtensionArgs($ax_args);
903
904         if (Auth_OpenID_AX::isError($result)) {
905             return $result;
906         }
907
908         $this->update_url = Auth_OpenID::arrayGet($ax_args, 'update_url');
909
910         return true;
911     }
912
913     /**
914      * Construct a FetchResponse object from an OpenID library
915      * SuccessResponse object.
916      *
917      * @param success_response: A successful id_res response object
918      *
919      * @param signed: Whether non-signed args should be processsed. If
920      * True (the default), only signed arguments will be processsed.
921      *
922      * @return $response A FetchResponse containing the data from the
923      * OpenID message
924      */
925     function fromSuccessResponse($success_response, $signed=true)
926     {
927         $obj = new Auth_OpenID_AX_FetchResponse();
928         if ($signed) {
929             $ax_args = $success_response->getSignedNS($obj->ns_uri);
930         } else {
931             $ax_args = $success_response->message->getArgs($obj->ns_uri);
932         }
933         if ($ax_args === null || Auth_OpenID::isFailure($ax_args) ||
934               sizeof($ax_args) == 0) {
935             return null;
936         }
937
938         $result = $obj->parseExtensionArgs($ax_args);
939         if (Auth_OpenID_AX::isError($result)) {
940             #XXX log me
941             return null;
942         }
943         return $obj;
944     }
945 }
946
947 /**
948  * A store request attribute exchange message representation.
949  *
950  * @package OpenID
951  */
952 class Auth_OpenID_AX_StoreRequest extends Auth_OpenID_AX_KeyValueMessage {
953     var $mode = 'store_request';
954
955     /**
956      * @param array $aliases The namespace aliases to use when making
957      * this store response. Leave as None to use defaults.
958      */
959     function getExtensionArgs($aliases=null)
960     {
961         $ax_args = $this->_newArgs();
962         $kv_args = $this->_getExtensionKVArgs($aliases);
963         Auth_OpenID::update(&$ax_args, $kv_args);
964         return $ax_args;
965     }
966 }
967
968 /**
969  * An indication that the store request was processed along with this
970  * OpenID transaction.  Use make(), NOT the constructor, to create
971  * response objects.
972  *
973  * @package OpenID
974  */
975 class Auth_OpenID_AX_StoreResponse extends Auth_OpenID_AX_Message {
976     var $SUCCESS_MODE = 'store_response_success';
977     var $FAILURE_MODE = 'store_response_failure';
978
979     /**
980      * Returns Auth_OpenID_AX_Error on error or an
981      * Auth_OpenID_AX_StoreResponse object on success.
982      */
983     function &make($succeeded=true, $error_message=null)
984     {
985         if (($succeeded) && ($error_message !== null)) {
986             return new Auth_OpenID_AX_Error('An error message may only be '.
987                                     'included in a failing fetch response');
988         }
989
990         return new Auth_OpenID_AX_StoreResponse($succeeded, $error_message);
991     }
992
993     function Auth_OpenID_AX_StoreResponse($succeeded=true, $error_message=null)
994     {
995         if ($succeeded) {
996             $this->mode = $this->SUCCESS_MODE;
997         } else {
998             $this->mode = $this->FAILURE_MODE;
999         }
1000
1001         $this->error_message = $error_message;
1002     }
1003
1004     /**
1005      * Was this response a success response?
1006      */
1007     function succeeded()
1008     {
1009         return $this->mode == $this->SUCCESS_MODE;
1010     }
1011
1012     function getExtensionArgs()
1013     {
1014         $ax_args = $this->_newArgs();
1015         if ((!$this->succeeded()) && $this->error_message) {
1016             $ax_args['error'] = $this->error_message;
1017         }
1018
1019         return $ax_args;
1020     }
1021 }
1022
1023 ?>