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