. * @author ruhllatio * @copyright 2016 nZEDb */ namespace nntmux\http; use nntmux\Category; use nntmux\Utility\Utility; /** * Class XMLReturn. */ class XML_Response { /** * @var string The buffered cData before final write */ protected $cdata; /** * The RSS namespace used for the output. * * @var string */ protected $namespace; /** * The trailing URL parameters on the request. * * @var mixed */ protected $parameters; /** * The release we are adding to the stream. * * @var array */ protected $release; /** * The retrieved releases we are returning from the API call. * * @var mixed */ protected $releases; /** * The various server variables and active categories. * * @var mixed */ protected $server; /** * The XML formatting operation we are returning. * * @var mixed */ protected $type; /** * The XMLWriter Class. * * @var \XMLWriter */ protected $xml; /** * @var mixed */ protected $offset; /** * XMLReturn constructor. * * @param array $options */ public function __construct(array $options = []) { $defaults = [ 'Parameters' => null, 'Data' => null, 'Server' => null, 'Offset' => null, 'Type' => null, ]; $options += $defaults; $this->parameters = $options['Parameters']; $this->releases = $options['Data']; $this->server = $options['Server']; $this->offset = $options['Offset']; $this->type = $options['Type']; $this->xml = new \XMLWriter(); $this->xml->openMemory(); $this->xml->setIndent(true); } /** * @return bool|string */ public function returnXML() { if ($this->xml) { switch ($this->type) { case 'caps': return $this->returnCaps(); break; case 'api': $this->namespace = 'newznab'; return $this->returnApiRss(); break; case 'rss': $this->namespace = 'nntmux'; return $this->returnApiRss(); break; case 'reg': return $this->returnReg(); break; } } return false; } /** * XML writes and returns the API capabilities. * * @return string The XML Formatted string data */ protected function returnCaps(): string { $w = $this->xml; $s = $this->server; $w->startDocument('1.0', 'UTF-8'); $w->startElement('caps'); $this->addNode(['name' => 'server', 'data' => $s['server']]); $this->addNode(['name' => 'limits', 'data' => $s['limits']]); $this->addNode(['name' => 'registration', 'data' => $s['registration']]); $this->addNodes(['name' => 'searching', 'data' => $s['searching']]); $this->writeCategoryListing(); $w->endElement(); $w->endDocument(); return $w->outputMemory(); } /** * XML writes and returns the API data. * * @return string The XML Formatted string data */ protected function returnApiRss(): string { $w = $this->xml; $this->xml->startDocument('1.0', 'UTF-8'); $this->includeRssAtom(); // Open RSS $w->startElement('channel'); // Open channel $this->includeRssAtomLink(); $this->includeMetaInfo(); $this->includeImage(); $this->includeTotalRows(); $this->includeReleases(); $w->endElement(); // End channel $w->endElement(); // End RSS $w->endDocument(); return $w->outputMemory(); } /** * @return string The XML formatted registration information */ protected function returnReg(): string { $this->xml->startDocument('1.0', 'UTF-8'); $this->xml->startElement('register'); $this->xml->writeAttribute('username', $this->parameters['username']); $this->xml->writeAttribute('password', $this->parameters['password']); $this->xml->writeAttribute('apikey', $this->parameters['token']); $this->xml->endElement(); $this->xml->endDocument(); return $this->xml->outputMemory(); } /** * Starts a new element, loops through the attribute data and ends the element. * * @param array $element An array with the name of the element and the attribute data */ protected function addNode($element): void { $this->xml->startElement($element['name']); foreach ($element['data'] as $attr => $val) { $this->xml->writeAttribute($attr, $val); } $this->xml->endElement(); } /** * Starts a new element, loops through the attribute data and ends the element. * * @param array $element An array with the name of the element and the attribute data */ protected function addNodes($element): void { $this->xml->startElement($element['name']); foreach ($element['data'] as $elem => $value) { $subelement['name'] = $elem; $subelement['data'] = $value; $this->addNode($subelement); } $this->xml->endElement(); } /** * Adds the site category listing to the XML feed. */ protected function writeCategoryListing(): void { $this->xml->startElement('categories'); foreach ($this->server['categories'] as $p) { $this->xml->startElement('category'); $this->xml->writeAttribute('id', $p['id']); $this->xml->writeAttribute('name', html_entity_decode($p['title'])); if ($p['description'] !== '') { $this->xml->writeAttribute('description', html_entity_decode($p['description'])); } foreach ($p['subcatlist'] as $c) { $this->xml->startElement('subcat'); $this->xml->writeAttribute('id', $c['id']); $this->xml->writeAttribute('name', html_entity_decode($c['title'])); if ($c['description'] !== '') { $this->xml->writeAttribute('description', html_entity_decode($c['description'])); } $this->xml->endElement(); } $this->xml->endElement(); } } /** * Adds RSS Atom information to the XML. */ protected function includeRssAtom(): void { switch ($this->namespace) { case 'newznab': $url = 'http://www.newznab.com/DTD/2010/feeds/attributes/'; break; case 'nntmux': default: $url = $this->server['server']['url'].'rss-info/'; } $this->xml->startElement('rss'); $this->xml->writeAttribute('version', '2.0'); $this->xml->writeAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom'); $this->xml->writeAttribute("xmlns:{$this->namespace}", $url); $this->xml->writeAttribute('encoding', 'utf-8'); } protected function includeRssAtomLink(): void { $this->xml->startElement('atom:link'); $this->xml->startAttribute('href'); $this->xml->text($this->server['server']['url'].($this->namespace === 'newznab' ? 'api' : 'rss')); $this->xml->endAttribute(); $this->xml->startAttribute('rel'); $this->xml->text('self'); $this->xml->endAttribute(); $this->xml->startAttribute('type'); $this->xml->text('application/rss+xml'); $this->xml->endAttribute(); $this->xml->endElement(); } /** * Writes the channel information for the feed. */ protected function includeMetaInfo(): void { $server = $this->server['server']; switch ($this->namespace) { case 'newznab': $path = 'apihelp/'; $tag = 'API'; break; case 'nntmux': default: $path = 'rss-info/'; $tag = 'RSS'; } $this->xml->writeElement('title', $server['title']); $this->xml->writeElement('description', $server['title']." {$tag} Details"); $this->xml->writeElement('link', $server['url']); $this->xml->writeElement('language', 'en-gb'); $this->xml->writeElement('webMaster', $server['email'].' '.$server['title']); $this->xml->writeElement('category', $server['meta']); $this->xml->writeElement('generator', 'nntmux'); $this->xml->writeElement('ttl', '10'); $this->xml->writeElement('docs', $this->server['server']['url'].$path); } /** * Adds nntmux logo data to the XML. */ protected function includeImage(): void { $this->xml->startElement('image'); $this->xml->writeAttribute('url', $this->server['server']['url'].'themes/shared/images/logo.png'); $this->xml->writeAttribute('title', $this->server['server']['title']); $this->xml->writeAttribute('link', $this->server['server']['url']); $this->xml->writeAttribute( 'description', 'Visit '.$this->server['server']['title'].' - '.$this->server['server']['strapline'] ); $this->xml->endElement(); } public function includeTotalRows(): void { $this->xml->startElement($this->namespace.':response'); $this->xml->writeAttribute('offset', $this->offset); $this->xml->writeAttribute('total', $this->releases[0]['_totalrows'] ?? 0); $this->xml->endElement(); } /** * Loop through the releases and add their info to the XML stream. */ public function includeReleases(): void { if (is_array($this->releases) && ! empty($this->releases)) { foreach ($this->releases as $this->release) { $this->xml->startElement('item'); $this->includeReleaseMain(); $this->setZedAttributes(); $this->xml->endElement(); } } } /** * Writes the primary release information. */ public function includeReleaseMain(): void { $this->xml->writeElement('title', $this->release['searchname']); $this->xml->startElement('guid'); $this->xml->writeAttribute('isPermaLink', 'true'); $this->xml->text("{$this->server['server']['url']}details/{$this->release['guid']}"); $this->xml->endElement(); $this->xml->writeElement( 'link', "{$this->server['server']['url']}getnzb/{$this->release['guid']}.nzb". "&i={$this->parameters['uid']}"."&r={$this->parameters['token']}". ((int) $this->parameters['del'] === 1 ? '&del=1' : '') ); $this->xml->writeElement('comments', "{$this->server['server']['url']}details/{$this->release['guid']}#comments"); $this->xml->writeElement('pubDate', date(DATE_RSS, strtotime($this->release['adddate']))); $this->xml->writeElement('category', $this->release['category_name']); if ($this->namespace === 'newznab') { $this->xml->writeElement('description', $this->release['searchname']); } else { $this->writeRssCdata(); } if (! isset($this->parameters['dl']) || (isset($this->parameters['dl']) && (int) $this->parameters['dl'] === 1)) { $this->xml->startElement('enclosure'); $this->xml->writeAttribute( 'url', "{$this->server['server']['url']}getnzb/{$this->release['guid']}.nzb". "&i={$this->parameters['uid']}"."&r={$this->parameters['token']}". ((int) $this->parameters['del'] === 1 ? '&del=1' : '') ); $this->xml->writeAttribute('length', $this->release['size']); $this->xml->writeAttribute('type', 'application/x-nzb'); $this->xml->endElement(); } } /** * Writes the Zed (newznab) specific attributes. */ protected function setZedAttributes(): void { $this->writeZedAttr('category', $this->release['categories_id']); $this->writeZedAttr('size', $this->release['size']); if (isset($this->release['coverurl']) && ! empty($this->release['coverurl'])) { $this->writeZedAttr( 'coverurl', $this->server['server']['url']."covers/{$this->release['coverurl']}" ); } if ((int) $this->parameters['extended'] === 1) { $this->writeZedAttr('files', $this->release['totalpart']); $this->writeZedAttr('poster', $this->release['fromname']); if (($this->release['videos_id'] > 0 || $this->release['tv_episodes_id'] > 0) && $this->namespace === 'newznab') { $this->setTvAttr(); } if (isset($this->release['imdbid']) && $this->release['imdbid'] > 0) { $this->writeZedAttr('imdb', $this->release['imdbid']); } if (isset($this->release['anidbid']) && $this->release['anidbid'] > 0) { $this->writeZedAttr('anidbid', $this->release['anidbid']); } if (isset($this->release['predb_id']) && $this->release['predb_id'] > 0) { $this->writeZedAttr('prematch', 1); } if (isset($this->release['nfostatus']) && (int) $this->release['nfostatus'] === 1) { $this->writeZedAttr( 'info', $this->server['server']['url']. "api?t=info&id={$this->release['guid']}&r={$this->parameters['token']}" ); } $this->writeZedAttr('grabs', $this->release['grabs']); $this->writeZedAttr('comments', $this->release['comments']); $this->writeZedAttr('password', $this->release['passwordstatus']); $this->writeZedAttr('usenetdate', date_format(date_create($this->release['postdate']), 'D, d M Y H:i:s O')); if (! empty($this->release['group_name'])) { $this->writeZedAttr('group', $this->release['group_name']); } } } /** * Writes the TV Specific attributes. */ protected function setTvAttr(): void { if (! empty($this->release['title'])) { $this->writeZedAttr('title', $this->release['title']); } if (isset($this->release['series']) && $this->release['series'] > 0) { $this->writeZedAttr('season', $this->release['series']); } if (isset($this->release['episode']) && $this->release['episode'] > 0) { $this->writeZedAttr('episode', $this->release['episode']); } if (! empty($this->release['firstaired'])) { $this->writeZedAttr('tvairdate', $this->release['firstaired']); } if (isset($this->release['tvdb']) && $this->release['tvdb'] > 0) { $this->writeZedAttr('tvdbid', $this->release['tvdb']); } if (isset($this->release['trakt']) && $this->release['trakt'] > 0) { $this->writeZedAttr('traktid', $this->release['trakt']); } if (isset($this->release['tvrage']) && $this->release['tvrage'] > 0) { $this->writeZedAttr('tvrageid', $this->release['tvrage']); $this->writeZedAttr('rageid', $this->release['tvrage']); } if (isset($this->release['tvmaze']) && $this->release['tvmaze'] > 0) { $this->writeZedAttr('tvmazeid', $this->release['tvmaze']); } if (isset($this->release['imdb']) && $this->release['imdb'] > 0) { $this->writeZedAttr('imdbid', str_pad($this->release['imdb'], 7, '0', STR_PAD_LEFT)); } if (isset($this->release['tmdb']) && $this->release['tmdb'] > 0) { $this->writeZedAttr('tmdbid', $this->release['tmdb']); } } /** * Writes individual zed (newznab) type attributes. * * @param string $name The namespaced attribute name tag * @param string $value The namespaced attribute value */ protected function writeZedAttr($name, $value): void { $this->xml->startElement($this->namespace.':attr'); $this->xml->writeAttribute('name', $name); $this->xml->writeAttribute('value', $value); $this->xml->endElement(); } /** * Writes the cData (HTML format) for the RSS feed * Also calls supplementary cData writes depending upon post process. */ protected function writeRssCdata(): void { $this->cdata = ''; $w = $this->xml; $r = $this->release; $s = $this->server; $p = $this->parameters; $this->cdata = "\n\t
\n"; switch (1) { case ! empty($r['cover']): $dir = 'movies'; $column = 'imdbid'; break; case ! empty($r['mu_cover']): $dir = 'music'; $column = 'musicinfo_id'; break; case ! empty($r['co_cover']): $dir = 'console'; $column = 'consoleinfo_id'; break; case ! empty($r['bo_cover']): $dir = 'books'; $column = 'bookinfo_id'; break; } if (isset($dir, $column)) { $dcov = ($dir === 'movies' ? '-cover' : ''); $this->cdata .= "\t\"{$r['searchname']}\"\n"; } $size = Utility::bytesToSizeString($r['size']); $this->cdata .= "\t
  • ID: {$r['guid']}
  • \n". "\t
  • Name: {$r['searchname']}
  • \n". "\t
  • Size: {$size}
  • \n". "\t
  • Category: {$r['category_name']}
  • \n". "\t
  • Group: {$r['group_name']}
  • \n". "\t
  • Poster: {$r['fromname']}
  • \n". "\t
  • Posted: {$r['postdate']}
  • \n"; switch ($r['passwordstatus']) { case 0: $pstatus = 'None'; break; case 1: $pstatus = 'Possibly Passworded'; break; case 2: $pstatus = 'Probably not viable'; break; case 10: $pstatus = 'Passworded'; break; default: $pstatus = 'Unknown'; } $this->cdata .= "\t
  • Password: {$pstatus}
  • \n"; if ($r['nfostatus'] === 1) { $this->cdata .= "\t
  • Nfo: ". "". "{$r['searchname']}.nfo
  • \n"; } if ($r['parentid'] === Category::MOVIE_ROOT && $r['imdbid'] !== '') { $this->writeRssMovieInfo(); } elseif ($r['parentid'] === Category::MUSIC_ROOT && $r['musicinfo_id'] > 0) { $this->writeRssMusicInfo(); } elseif ($r['parentid'] === Category::GAME_ROOT && $r['consoleinfo_id'] > 0) { $this->writeRssConsoleInfo(); } $w->startElement('description'); $w->writeCData($this->cdata."\t
    "); $w->endElement(); } /** * Writes the Movie Info for the RSS feed cData. */ protected function writeRssMovieInfo(): void { $r = $this->release; $movieCol = ['rating', 'plot', 'year', 'genre', 'director', 'actors']; $cData = $this->buildCdata($movieCol); $this->cdata .= "\t
  • Imdb Info: \t \t
  • \n"; } /** * Writes the Music Info for the RSS feed cData. */ protected function writeRssMusicInfo(): void { $r = $this->release; $tData = $cDataUrl = ''; $musicCol = ['mu_artist', 'mu_genre', 'mu_publisher', 'mu_releasedate', 'mu_review']; $cData = $this->buildCdata($musicCol); if ($r['mu_url'] !== '') { $cDataUrl = "
  • Amazon: {$r['mu_title']}
  • "; } $this->cdata .= "\t
  • Music Info:
  • \n"; if ($r['mu_tracks'] !== '') { $tracks = explode('|', $r['mu_tracks']); if (count($tracks) > 0) { foreach ($tracks as $track) { $track = trim($track); $tData .= "
  • {$track}
  • "; } } $this->cdata .= "
  • Track Listing:
      {$tData}
  • \n"; } } /** * Writes the Console Info for the RSS feed cData. */ protected function writeRssConsoleInfo(): void { $r = $this->release; $gamesCol = ['co_genre', 'co_publisher', 'year', 'co_review']; $cData = $this->buildCdata($gamesCol); $this->cdata .= "
  • Console Info:
  • \n"; } /** * Accepts an array of values to loop through to build cData from the release info. * * @param array $columns The columns in the release we need to insert * * @return string The HTML format cData */ protected function buildCdata($columns): string { $r = $this->release; $cData = ''; foreach ($columns as $info) { if (! empty($r[$info])) { if ($info === 'mu_releasedate') { $ucInfo = 'Released'; $rDate = date('Y-m-d', strtotime($r[$info])); $cData .= "
  • {$ucInfo}: {$rDate}
  • \n"; } else { $ucInfo = ucfirst(preg_replace('/^[a-z]{2}_/i', '', $info)); $cData .= "
  • {$ucInfo}: {$r[$info]}
  • \n"; } } } return $cData; } }