| 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: |