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