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 */
13require_once "Auth/OpenID/Extension.php";
14require_once "Auth/OpenID/Message.php";
15require_once "Auth/OpenID/TrustRoot.php";
16
17define('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.
22define('Auth_OpenID_AX_UNLIMITED_VALUES', 'unlimited');
23
24// Minimum supported alias length in characters.  Here for
25// completeness.
26define('Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH', 32);
27
28/**
29 * AX utility class.
30 *
31 * @package OpenID
32 */
33class 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    static 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 */
51function 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 */
71class 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 */
84class 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 */
139class 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    static 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 */
238function 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 */
268class 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    static 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 */
541class 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) && ($ax_args['count.' . $alias] !== Auth_OpenID_AX_UNLIMITED_VALUES)) {
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 */
793class 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    static 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 */
952class 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 */
975class 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