<?php
declare(strict_types=1);
namespace Laminas\Feed\Reader;
use ArrayObject;
use DOMElement;
use DOMNodeList;
use Laminas\Feed\Uri;
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
use ReturnTypeWillChange;
use function array_filter;
use function array_pop;
use function explode;
use function implode;
use function ltrim;
use function sprintf;
use function strtolower;
use function trim;
/** @template-extends ArrayObject<array-key, FeedSet|Feed\FeedInterface|string|null> */
class FeedSet extends ArrayObject
{
/** @var null|string */
public $rss;
/** @var null|string */
public $rdf;
/** @var null|string */
public $atom;
/**
* Import a DOMNodeList from any document containing a set of links
* for alternate versions of a document, which will normally refer to
* RSS/RDF/Atom feeds for the current document.
*
* All such links are stored internally, however the first instance of
* each RSS, RDF or Atom type has its URI stored as a public property
* as a shortcut where the use case is simply to get a quick feed ref.
*
* Note that feeds are not loaded at this point, but will be lazy
* loaded automatically when each links 'feed' array key is accessed.
*
* @param string $uri
* @return void
*/
public function addLinks(DOMNodeList $links, $uri)
{
foreach ($links as $link) {
/** @var DOMElement $link */
if (
strtolower($link->getAttribute('rel')) !== 'alternate'
|| ! $link->getAttribute('type') || ! $link->getAttribute('href')
) {
continue;
}
if (null === $this->rss && $link->getAttribute('type') === 'application/rss+xml') {
$this->rss = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
} elseif (null === $this->atom && $link->getAttribute('type') === 'application/atom+xml') {
$this->atom = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
} elseif (null === $this->rdf && $link->getAttribute('type') === 'application/rdf+xml') {
$this->rdf = $this->absolutiseUri(trim($link->getAttribute('href')), $uri);
}
$this[] = new static([
'rel' => 'alternate',
'type' => $link->getAttribute('type'),
'href' => $this->absolutiseUri(trim($link->getAttribute('href')), $uri),
'title' => $link->getAttribute('title'),
]);
}
}
/**
* Attempt to turn a relative URI into an absolute URI
*
* @param string $link
* @param null|string $uri OPTIONAL
* @return null|string absolutised link or null if invalid
*/
protected function absolutiseUri($link, $uri = null)
{
$linkUri = Uri::factory($link);
if ($linkUri->isAbsolute()) {
// invalid absolute link can not be recovered
return $linkUri->isValid() ? $link : null;
}
$scheme = 'http';
if ($uri !== null) {
$uri = Uri::factory($uri);
$scheme = $uri->getScheme() ?: $scheme;
}
if ($linkUri->getHost()) {
$link = $this->resolveSchemeRelativeUri($link, $scheme);
} elseif ($uri !== null) {
$link = $this->resolveRelativeUri($link, $scheme, $uri->getHost(), $uri->getPath());
}
if (! Uri::factory($link)->isValid()) {
return null;
}
return $link;
}
/**
* Resolves scheme relative link to absolute
*
* @param string $link
* @param string $scheme
* @return string
*/
private function resolveSchemeRelativeUri($link, $scheme)
{
$link = ltrim($link, '/');
return sprintf('%s://%s', $scheme, $link);
}
/**
* Resolves relative link to absolute
*
* @param string $link
* @param string $scheme
* @param string $host
* @param string $uriPath
* @return string
*/
private function resolveRelativeUri($link, $scheme, $host, $uriPath)
{
if ($link[0] !== '/') {
$link = $uriPath . '/' . $link;
}
return sprintf(
'%s://%s/%s',
$scheme,
$host,
$this->canonicalizePath($link)
);
}
/**
* Canonicalize relative path
*
* @param string $path
* @return string
*/
protected function canonicalizePath($path)
{
$parts = array_filter(explode('/', $path));
$absolutes = [];
foreach ($parts as $part) {
if ('.' === $part) {
continue;
}
if ('..' === $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
return implode('/', $absolutes);
}
/**
* @inheritDoc
*
* Supports lazy loading of feeds using Reader::import() but
* delegates any other operations to the parent class.
*/
#[ReturnTypeWillChange]
public function offsetGet($offset)
{
if ($offset === 'feed' && ! $this->offsetExists('feed')) {
if (! $this->offsetExists('href')) {
return;
}
$feed = Reader::import($this->offsetGet('href'));
$this->offsetSet('feed', $feed);
return $feed;
}
return parent::offsetGet($offset);
}
}
|