1<?php 2 3/** 4 * @see https://github.com/laminas/laminas-feed for the canonical source repository 5 * @copyright https://github.com/laminas/laminas-feed/blob/master/COPYRIGHT.md 6 * @license https://github.com/laminas/laminas-feed/blob/master/LICENSE.md New BSD License 7 */ 8 9namespace Laminas\Feed\Reader; 10 11use ArrayObject; 12use DOMNodeList; 13use Laminas\Feed\Uri; 14 15class FeedSet extends ArrayObject 16{ 17 public $rss; 18 19 public $rdf; 20 21 public $atom; 22 23 /** 24 * Import a DOMNodeList from any document containing a set of links 25 * for alternate versions of a document, which will normally refer to 26 * RSS/RDF/Atom feeds for the current document. 27 * 28 * All such links are stored internally, however the first instance of 29 * each RSS, RDF or Atom type has its URI stored as a public property 30 * as a shortcut where the use case is simply to get a quick feed ref. 31 * 32 * Note that feeds are not loaded at this point, but will be lazy 33 * loaded automatically when each links 'feed' array key is accessed. 34 * 35 * @param string $uri 36 * @return void 37 */ 38 public function addLinks(DOMNodeList $links, $uri) 39 { 40 foreach ($links as $link) { 41 if (strtolower($link->getAttribute('rel')) !== 'alternate' 42 || ! $link->getAttribute('type') || ! $link->getAttribute('href') 43 ) { 44 continue; 45 } 46 if (! isset($this->rss) && $link->getAttribute('type') === 'application/rss+xml') { 47 $this->rss = $this->absolutiseUri(trim($link->getAttribute('href')), $uri); 48 } elseif (! isset($this->atom) && $link->getAttribute('type') === 'application/atom+xml') { 49 $this->atom = $this->absolutiseUri(trim($link->getAttribute('href')), $uri); 50 } elseif (! isset($this->rdf) && $link->getAttribute('type') === 'application/rdf+xml') { 51 $this->rdf = $this->absolutiseUri(trim($link->getAttribute('href')), $uri); 52 } 53 $this[] = new static([ 54 'rel' => 'alternate', 55 'type' => $link->getAttribute('type'), 56 'href' => $this->absolutiseUri(trim($link->getAttribute('href')), $uri), 57 'title' => $link->getAttribute('title'), 58 ]); 59 } 60 } 61 62 /** 63 * Attempt to turn a relative URI into an absolute URI 64 * 65 * @param string $link 66 * @param null|string $uri OPTIONAL 67 * @return null|string absolutised link or null if invalid 68 */ 69 protected function absolutiseUri($link, $uri = null) 70 { 71 $linkUri = Uri::factory($link); 72 if ($linkUri->isAbsolute()) { 73 // invalid absolute link can not be recovered 74 return $linkUri->isValid() ? $link : null; 75 } 76 77 $scheme = 'http'; 78 if ($uri !== null) { 79 $uri = Uri::factory($uri); 80 $scheme = $uri->getScheme() ?: $scheme; 81 } 82 83 if ($linkUri->getHost()) { 84 $link = $this->resolveSchemeRelativeUri($link, $scheme); 85 } elseif ($uri !== null) { 86 $link = $this->resolveRelativeUri($link, $scheme, $uri->getHost(), $uri->getPath()); 87 } 88 89 if (! Uri::factory($link)->isValid()) { 90 return null; 91 } 92 93 return $link; 94 } 95 96 /** 97 * Resolves scheme relative link to absolute 98 * 99 * @param string $link 100 * @param string $scheme 101 * @return string 102 */ 103 private function resolveSchemeRelativeUri($link, $scheme) 104 { 105 $link = ltrim($link, '/'); 106 return sprintf('%s://%s', $scheme, $link); 107 } 108 109 /** 110 * Resolves relative link to absolute 111 * 112 * @param string $link 113 * @param string $scheme 114 * @param string $host 115 * @param string $uriPath 116 * @return string 117 */ 118 private function resolveRelativeUri($link, $scheme, $host, $uriPath) 119 { 120 if ($link[0] !== '/') { 121 $link = $uriPath . '/' . $link; 122 } 123 return sprintf( 124 '%s://%s/%s', 125 $scheme, 126 $host, 127 $this->canonicalizePath($link) 128 ); 129 } 130 131 /** 132 * Canonicalize relative path 133 * 134 * @param string $path 135 * @return string 136 */ 137 protected function canonicalizePath($path) 138 { 139 $parts = array_filter(explode('/', $path)); 140 $absolutes = []; 141 foreach ($parts as $part) { 142 if ('.' === $part) { 143 continue; 144 } 145 if ('..' === $part) { 146 array_pop($absolutes); 147 } else { 148 $absolutes[] = $part; 149 } 150 } 151 return implode('/', $absolutes); 152 } 153 154 /** 155 * Supports lazy loading of feeds using Reader::import() but 156 * delegates any other operations to the parent class. 157 * 158 * @param string $offset 159 * @return mixed 160 */ 161 public function offsetGet($offset) 162 { 163 if ($offset === 'feed' && ! $this->offsetExists('feed')) { 164 if (! $this->offsetExists('href')) { 165 return; 166 } 167 $feed = Reader::import($this->offsetGet('href')); 168 $this->offsetSet('feed', $feed); 169 return $feed; 170 } 171 return parent::offsetGet($offset); 172 } 173} 174