1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2007-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Protocols\XRDS;
24:
25: use \XMLReader;
26:
27: /**
28: * A simple XRDS parser.
29: *
30: * This parser uses the classic expat functions available in PHP to parse the
31: * XRDS Simple XML document.
32: *
33: * The result is a {@link XRDSServices} object.
34: *
35: * @see https://docs.oasis-open.org/xri/2.0/specs/xri-resolution-V2.0.html
36: */
37: class XRDSParser {
38:
39: /**
40: * The namespace identifier for an XRDS document.
41: */
42: const XRDS_NS = 'xri://$xrds';
43:
44: /**
45: * The namespace identifier for XRDS version 2.
46: */
47: const XRD2_NS = 'xri://$xrd*($v*2.0)';
48:
49: /**
50: * The namespace identifier for XRDS Simple.
51: */
52: const XRDS_SIMPLE_NS = 'http://xrds-simple.net/core/1.0';
53:
54: /**
55: * The type identifier for XRDS Simple.
56: */
57: const XRDS_SIMPLE_TYPE = 'xri://$xrds*simple';
58:
59: /**
60: * The namespace identifier for OpenID services.
61: */
62: const XRD_OPENID_NS = 'http://openid.net/xmlns/1.0';
63:
64: /**
65: * XML reader
66: * @var XMLReader
67: */
68: private $reader;
69:
70: /**
71: * Discovered services
72: * @var XRDSServices
73: */
74: private $services;
75:
76: /**
77: * Creates an instance of the XRDS parser.
78: *
79: * This constructor also initialises the underlying XML parser.
80: */
81: public function __construct() {
82: $this->services = new XRDSServices();
83: $this->reader = new XMLReader();
84: }
85:
86: /**
87: * Frees memory associated with the underlying XML parser.
88: *
89: * Note that only the memory associated with the underlying XML parser is
90: * freed. Memory associated with the class itself is not freed.
91: *
92: * @return void
93: */
94: public function close() {
95: $this->reader->close();
96: }
97:
98: /**
99: * Loads an XRDS document.
100: *
101: * @param string $xml the XML document to load
102: * @return void
103: */
104: public function load($xml) {
105: $this->reader->xml($xml);
106: }
107:
108: /**
109: * Parses an XRDS document and returns the discovered services.
110: *
111: * @return XRDSServices the discovered services
112: */
113: public function parse() {
114: while ($this->reader->read()) {
115: if (($this->reader->nodeType == XMLReader::ELEMENT)
116: && (strtolower($this->reader->namespaceURI) == strtolower(self::XRD2_NS))) {
117: switch ($this->reader->localName) {
118: case 'Service':
119: $this->services->add($this->parseService());
120: break;
121: }
122:
123: }
124: }
125:
126: return $this->services;
127: }
128:
129: /**
130: * @return array<string, mixed>
131: */
132: private function parseService() {
133: $service = [];
134:
135: if ($this->reader->getAttribute('priority')) {
136: $service['#priority'] = $this->reader->getAttribute('priority');
137: }
138: if ($this->reader->getAttribute('id')) {
139: $service['#id'] = $this->reader->getAttribute('id');
140: }
141:
142: if ($this->reader->isEmptyElement) return $service;
143:
144: while ($this->reader->read()) {
145: if (($this->reader->nodeType == XMLReader::END_ELEMENT) &&
146: (strtolower($this->reader->namespaceURI) == strtolower(self::XRD2_NS)) &&
147: ($this->reader->localName == 'Service')) {
148:
149: foreach ([ 'type', 'localid', 'uri' ] as $key) {
150: if (!isset($service[$key])) continue;
151: /** @var array<array<string, mixed>> $service[$key] */
152: $service[$key] = $this->flatten_uris($service[$key]);
153: }
154: break;
155: }
156:
157:
158: if (($this->reader->nodeType == XMLReader::ELEMENT)
159: && (strtolower($this->reader->namespaceURI) == strtolower(self::XRD2_NS))) {
160: switch ($this->reader->localName) {
161: case 'Type':
162: case 'LocalID':
163: case 'URI':
164: $key = strtolower($this->reader->localName);
165: if (!isset($service[$key])) {
166: $service[$key] = [];
167: }
168:
169: $item = [ '#uri' => trim($this->reader->readString()) ];
170: if ($this->reader->getAttribute('priority'))
171: $item['#priority'] = $this->reader->getAttribute('priority');
172:
173: /** @var array<array<string, string>> $service[$key] */
174: $service[$key][] = $item;
175: break;
176: }
177: }
178:
179: if (($this->reader->nodeType == XMLReader::ELEMENT)
180: && (strtolower($this->reader->namespaceURI) == strtolower(self::XRD_OPENID_NS)) &&
181: ($this->reader->localName == 'Delegate')) {
182: $service['delegate'] = trim($this->reader->readString());
183: }
184:
185: }
186:
187: return $service;
188: }
189:
190:
191: /**
192: * Flattens the service array.
193: *
194: * In an XRDS document, child elements of the service element often contains
195: * a list of URIs, with the priority specified in the priority attribute.
196: *
197: * When the document is parsed in this class, the URI and the priority are first
198: * extracted into the #uri and the #priority keys respectively. This function
199: * takes this array, sorts the elements using the #priority keys (if $sort is
200: * true), then collapses the array using the value associated with the #uri key.
201: *
202: * @param array<array<string, mixed>> $array the service array, with URIs and priorities
203: * @param bool $sort whether to sort the service array using the #priority
204: * keys
205: * @return array<array<string, mixed>> the services array with URIs sorted by priority
206: */
207: protected function flatten_uris($array, $sort = TRUE) {
208: $result = [];
209:
210: if ($sort) uasort($array, '\SimpleID\Protocols\XRDS\XRDSServices::sortByPriority');
211:
212: for ($i = 0; $i < count($array); $i++) {
213: $result[] = $array[$i]['#uri'];
214: }
215:
216: return $result;
217: }
218: }
219: ?>
220: