1<?php
2/**
3 * Implementation of the Yadis Specification 1.0 protocol for service
4 * discovery from an Identity URI/XRI or other.
5 *
6 * PHP version 5
7 *
8 * LICENSE:
9 *
10 * Copyright (c) 2007 P�draic Brady <padraic.brady@yahoo.com>
11 * All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 *
17 *    * Redistributions of source code must retain the above copyright
18 *      notice, this list of conditions and the following disclaimer.
19 *    * Redistributions in binary form must reproduce the above copyright
20 *      notice, this list of conditions and the following disclaimer in the
21 *      documentation and/or other materials provided with the distribution.
22 *    * The name of the author may not be used to endorse or promote products
23 *      derived from this software without specific prior written permission.
24 *
25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 *
37 * @category   Services
38 * @package    Services_Yadis
39 * @author     P�draic Brady (http://blog.astrumfutura.com)
40 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41 * @version    $Id$
42 */
43
44/** Services_Yadis_Xrds_Namespace */
45require_once 'Services/Yadis/Xrds/Namespace.php';
46
47/**
48 * The Services_Yadis_Xrds class is a wrapper for elements of an
49 * XRD document which is parsed using SimpleXML, and contains methods for
50 * retrieving data about the document. The concrete aspects of retrieving
51 * specific data elements is left to a concrete subclass.
52 *
53 * @category   Services
54 * @package    Services_Yadis
55 * @subpackage Yadis
56 * @author     P�draic Brady (http://blog.astrumfutura.com)
57 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
58 */
59class Services_Yadis_Xrds
60{
61
62    /**
63     * Current key/pointer for the Iterator
64     *
65     * @var integer
66     */
67    protected $_currentKey = 0;
68
69    /**
70     * Contains the valid xrd:XRD nodes parsed from the XRD document.
71     *
72     * @var SimpleXMLElement
73     */
74    protected $_xrdNodes = null;
75
76    /**
77     * Instance of Services_Yadis_Xrds_Namespace for managing namespaces
78     * associated with an XRDS document.
79     *
80     * @var Services_Yadis_Xrds_Namespace
81     */
82    protected $_namespace = null;
83
84    /**
85     * Constructor; parses and validates an XRD document. All access to
86     * the data held in the XML is left to a concrete subclass specific to
87     * expected XRD format and data types.
88     * Cannot be directly instantiated; must call from subclass.
89     *
90     * @param   SimpleXMLElement $xrds
91     * @param   Services_Yadis_Xrds_Namespace $namespace
92     */
93    protected function __construct(SimpleXMLElement $xrds, Services_Yadis_Xrds_Namespace $namespace)
94    {
95        $this->_namespace = $namespace;
96        $xrdNodes = $this->_getValidXrdNodes($xrds);
97        if (!$xrdNodes) {
98            require_once 'Services/Yadis/Exception.php';
99            throw new Services_Yadis_Exception('The XRD document was found to be invalid');
100        }
101        $this->_xrdNodes = $xrdNodes;
102    }
103
104    /**
105     * Add a list (array) of additional namespaces to be utilised by the XML
106     * parser when it receives a valid XRD document.
107     *
108     * @param   array $namespaces
109     * @return  Services_Yadis
110     */
111    public function addNamespaces(array $namespaces)
112    {
113        $this->_namespace->addNamespaces($namespaces);
114        return $this;
115    }
116
117    /**
118     * Add a single namespace to be utilised by the XML parser when it receives
119     * a valid XRD document.
120     *
121     * @param   string $namespace
122     * @param   string $namespaceUrl
123     * @return  Services_Yadis
124     */
125    public function addNamespace($namespace, $namespaceUrl)
126    {
127        $this->_namespace->addNamespace($namespace, $namespaceUrl);
128        return $this;
129    }
130
131    /**
132     * Return the value of a specific namespace.
133     *
134     * @return string|boolean
135     */
136    public function getNamespace($namespace)
137    {
138        return $this->_namespace->getNamespace($namespace);
139    }
140
141    /**
142     * Returns an array of all currently set namespaces.
143     *
144     * @return array
145     */
146    public function getNamespaces()
147    {
148        return $this->_namespace->getNamespaces();
149    }
150
151    /**
152     * Returns an array of all xrd elements located in the XRD document.
153     *
154     * @param SimpleXMLElement
155     * @return array
156     */
157    protected function _getValidXrdNodes(SimpleXMLElement $xrds)
158    {
159        /**
160         * Register all namespaces to this SimpleXMLElement.
161         */
162        $this->_namespace->registerXpathNamespaces($xrds);
163
164        /**
165         * Verify the XRDS resource has a root element called "xrds:XRDS".
166         */
167        $root = $xrds->xpath('/xrds:XRDS[1]');
168        if (count($root) == 0) {
169            return null;
170        }
171
172        /**
173         * Check namespace urls of standard xmlns (no suffix) or xmlns:xrd
174         * (if present and of priority) for validity.
175         * No loss if neither exists, but they really should be.
176         */
177        $namespaces = $xrds->getDocNamespaces();
178        if (array_key_exists('xrd', $namespaces) && $namespaces['xrd'] != 'xri://$xrd*($v*2.0)') {
179            return null;
180        } elseif (array_key_exists('', $namespaces) && $namespaces[''] != 'xri://$xrd*($v*2.0)') {
181            // Hack for the namespace declaration in the XRD node, which SimpleXML misses
182            $xrdHack = false;
183            if (!isset($xrds->XRD)) {
184                return null;
185            }
186
187            foreach ($xrds->XRD as $xrd) {
188                $namespaces = $xrd->getNamespaces();
189                if (array_key_exists('', $namespaces)
190                    && $namespaces[''] == 'xri://$xrd*($v*2.0)') {
191
192                    $xrdHack = true;
193                    break;
194                }
195            }
196
197            if (!$xrdHack) {
198                return null;
199            }
200        }
201
202        /**
203         * Grab the XRD elements which contains details of the service provider's
204         * Server url, service types, and other details. Concrete subclass may
205         * have additional requirements concerning node priority or valid position
206         * in relation to other nodes. E.g. Yadis requires only using the *last*
207         * node.
208         */
209        $xrdNodes = $xrds->xpath('/xrds:XRDS[1]/xrd:XRD');
210        if (!$xrdNodes) {
211            return null;
212        }
213        return $xrdNodes;
214    }
215
216    /**
217     * Order an array of elements by priority. This assumes an array form of:
218     *      $array[$priority] = <array of elements with equal priority>
219     * Where multiple elements are assigned to a priority, their order in the
220     * priority array should be made random. After ordering, the array is
221     * flattened to a single array of elements for iteration.
222     *
223     * @param   array $unsorted
224     * @return  array
225     */
226    public static function sortByPriority(array $unsorted)
227    {
228        ksort($unsorted);
229        $flattened = array();
230        foreach ($unsorted as $priority) {
231            if (count($priority) > 1) {
232                shuffle($priority);
233                $flattened = array_merge($flattened, $priority);
234            } else {
235                $flattened[] = $priority[0];
236            }
237        }
238        return $flattened;
239    }
240
241}
242