mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Add xml helper
This commit is contained in:
@@ -105,6 +105,41 @@ class XML_Response
|
||||
$this->xml->setIndent(false);
|
||||
}
|
||||
|
||||
protected function writeXmlElement(string $name, mixed $content): void
|
||||
{
|
||||
$this->xml->writeElement($name, $this->xmlString($content));
|
||||
}
|
||||
|
||||
protected function writeXmlAttribute(string $name, mixed $value): void
|
||||
{
|
||||
$this->xml->writeAttribute($name, $this->xmlString($value));
|
||||
}
|
||||
|
||||
protected function writeXmlText(mixed $content): void
|
||||
{
|
||||
$this->xml->text($this->xmlString($content));
|
||||
}
|
||||
|
||||
protected function writeXmlCdata(mixed $content): void
|
||||
{
|
||||
$this->xml->writeCdata($this->xmlString($content));
|
||||
}
|
||||
|
||||
protected function xmlString(mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$string = (string) $value;
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// XML 1.0 allows tab, LF, CR, and the non-control Unicode ranges.
|
||||
return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '', $string) ?? '';
|
||||
}
|
||||
|
||||
public function returnXML(): bool|string
|
||||
{
|
||||
switch ($this->type) {
|
||||
@@ -402,9 +437,9 @@ class XML_Response
|
||||
{
|
||||
$this->xml->startDocument('1.0', 'UTF-8');
|
||||
$this->xml->startElement('register');
|
||||
$this->xml->writeAttribute('username', (string) ($this->parameters['username'] ?? ''));
|
||||
$this->xml->writeAttribute('password', (string) ($this->parameters['password'] ?? ''));
|
||||
$this->xml->writeAttribute('apikey', (string) ($this->parameters['token'] ?? ''));
|
||||
$this->writeXmlAttribute('username', $this->parameters['username'] ?? '');
|
||||
$this->writeXmlAttribute('password', $this->parameters['password'] ?? '');
|
||||
$this->writeXmlAttribute('apikey', $this->parameters['token'] ?? '');
|
||||
$this->xml->endElement();
|
||||
$this->xml->endDocument();
|
||||
|
||||
@@ -420,7 +455,7 @@ class XML_Response
|
||||
{
|
||||
$this->xml->startElement($element['name']);
|
||||
foreach ($element['data'] as $attr => $val) {
|
||||
$this->xml->writeAttribute($attr, (string) $val);
|
||||
$this->writeXmlAttribute((string) $attr, $val);
|
||||
}
|
||||
$this->xml->endElement();
|
||||
}
|
||||
@@ -449,17 +484,17 @@ class XML_Response
|
||||
$this->xml->startElement('categories');
|
||||
foreach ($this->server['categories'] as $this->parameters) {
|
||||
$this->xml->startElement('category');
|
||||
$this->xml->writeAttribute('id', (string) $this->parameters['id']);
|
||||
$this->xml->writeAttribute('name', html_entity_decode((string) $this->parameters['title']));
|
||||
$this->writeXmlAttribute('id', $this->parameters['id']);
|
||||
$this->writeXmlAttribute('name', html_entity_decode((string) $this->parameters['title']));
|
||||
if (! empty($this->parameters['description'])) {
|
||||
$this->xml->writeAttribute('description', html_entity_decode((string) $this->parameters['description']));
|
||||
$this->writeXmlAttribute('description', html_entity_decode((string) $this->parameters['description']));
|
||||
}
|
||||
foreach ($this->parameters['categories'] as $c) {
|
||||
$this->xml->startElement('subcat');
|
||||
$this->xml->writeAttribute('id', (string) $c['id']);
|
||||
$this->xml->writeAttribute('name', html_entity_decode((string) $c['title']));
|
||||
$this->writeXmlAttribute('id', $c['id']);
|
||||
$this->writeXmlAttribute('name', html_entity_decode((string) $c['title']));
|
||||
if (! empty($c['description'])) {
|
||||
$this->xml->writeAttribute('description', html_entity_decode((string) $c['description']));
|
||||
$this->writeXmlAttribute('description', html_entity_decode((string) $c['description']));
|
||||
}
|
||||
$this->xml->endElement();
|
||||
}
|
||||
@@ -473,10 +508,10 @@ class XML_Response
|
||||
$this->xml->startElement('groups');
|
||||
foreach (($this->server['groups'] ?? []) as $group) {
|
||||
$this->xml->startElement('group');
|
||||
$this->xml->writeAttribute('name', (string) ($group['name'] ?? ''));
|
||||
$this->xml->writeAttribute('description', (string) ($group['description'] ?? ''));
|
||||
$this->writeXmlAttribute('name', $group['name'] ?? '');
|
||||
$this->writeXmlAttribute('description', $group['description'] ?? '');
|
||||
if (! empty($group['lastupdate'])) {
|
||||
$this->xml->writeAttribute('lastupdate', (string) $group['lastupdate']);
|
||||
$this->writeXmlAttribute('lastupdate', $group['lastupdate']);
|
||||
}
|
||||
$this->xml->endElement();
|
||||
}
|
||||
@@ -488,9 +523,9 @@ class XML_Response
|
||||
$this->xml->startElement('genres');
|
||||
foreach (($this->server['genres'] ?? []) as $genre) {
|
||||
$this->xml->startElement('genre');
|
||||
$this->xml->writeAttribute('id', (string) ($genre['id'] ?? ''));
|
||||
$this->xml->writeAttribute('name', (string) ($genre['name'] ?? ''));
|
||||
$this->xml->writeAttribute('categoryid', (string) ($genre['categoryid'] ?? '0'));
|
||||
$this->writeXmlAttribute('id', $genre['id'] ?? '');
|
||||
$this->writeXmlAttribute('name', $genre['name'] ?? '');
|
||||
$this->writeXmlAttribute('categoryid', $genre['categoryid'] ?? '0');
|
||||
$this->xml->endElement();
|
||||
}
|
||||
$this->xml->endElement();
|
||||
@@ -507,23 +542,23 @@ class XML_Response
|
||||
};
|
||||
|
||||
$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');
|
||||
$this->writeXmlAttribute('version', '2.0');
|
||||
$this->writeXmlAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom');
|
||||
$this->writeXmlAttribute("xmlns:{$this->namespace}", $url);
|
||||
$this->writeXmlAttribute('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/v1/api' : '/rss'));
|
||||
$this->writeXmlText($this->server['server']['url'].($this->namespace === 'newznab' ? '/api/v1/api' : '/rss'));
|
||||
$this->xml->endAttribute();
|
||||
$this->xml->startAttribute('rel');
|
||||
$this->xml->text('self');
|
||||
$this->writeXmlText('self');
|
||||
$this->xml->endAttribute();
|
||||
$this->xml->startAttribute('type');
|
||||
$this->xml->text('application/rss+xml');
|
||||
$this->writeXmlText('application/rss+xml');
|
||||
$this->xml->endAttribute();
|
||||
$this->xml->endElement();
|
||||
}
|
||||
@@ -546,15 +581,15 @@ class XML_Response
|
||||
$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);
|
||||
$this->writeXmlElement('title', $server['title']);
|
||||
$this->writeXmlElement('description', $server['title']." {$tag} Details");
|
||||
$this->writeXmlElement('link', $server['url']);
|
||||
$this->writeXmlElement('language', 'en-gb');
|
||||
$this->writeXmlElement('webMaster', $server['email'].' '.$server['title']);
|
||||
$this->writeXmlElement('category', $server['meta']);
|
||||
$this->writeXmlElement('generator', 'nntmux');
|
||||
$this->writeXmlElement('ttl', '10');
|
||||
$this->writeXmlElement('docs', $this->server['server']['url'].$path);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -563,10 +598,10 @@ class XML_Response
|
||||
protected function includeImage(): void
|
||||
{
|
||||
$this->xml->startElement('image');
|
||||
$this->xml->writeAttribute('url', $this->server['server']['url'].'/assets/images/tmux_logo.png');
|
||||
$this->xml->writeAttribute('title', $this->server['server']['title']);
|
||||
$this->xml->writeAttribute('link', $this->server['server']['url']);
|
||||
$this->xml->writeAttribute(
|
||||
$this->writeXmlAttribute('url', $this->server['server']['url'].'/assets/images/tmux_logo.png');
|
||||
$this->writeXmlAttribute('title', $this->server['server']['title']);
|
||||
$this->writeXmlAttribute('link', $this->server['server']['url']);
|
||||
$this->writeXmlAttribute(
|
||||
'description',
|
||||
'Visit '.$this->server['server']['title'].' - '.$this->server['server']['strapline']
|
||||
);
|
||||
@@ -576,23 +611,23 @@ class XML_Response
|
||||
public function includeTotalRows(): void
|
||||
{
|
||||
$this->xml->startElement($this->namespace.':response');
|
||||
$this->xml->writeAttribute('offset', (string) $this->offset);
|
||||
$this->xml->writeAttribute('total', (string) ($this->releases[0]->_totalrows ?? 0));
|
||||
$this->writeXmlAttribute('offset', $this->offset);
|
||||
$this->writeXmlAttribute('total', $this->releases[0]->_totalrows ?? 0);
|
||||
$this->xml->endElement();
|
||||
}
|
||||
|
||||
public function includeLimits(): void
|
||||
{
|
||||
$this->xml->startElement($this->namespace.':apilimits');
|
||||
$this->xml->writeAttribute('apicurrent', (string) $this->parameters['requests']);
|
||||
$this->xml->writeAttribute('apimax', (string) $this->parameters['apilimit']);
|
||||
$this->xml->writeAttribute('grabcurrent', (string) $this->parameters['grabs']);
|
||||
$this->xml->writeAttribute('grabmax', (string) $this->parameters['downloadlimit']);
|
||||
$this->writeXmlAttribute('apicurrent', $this->parameters['requests']);
|
||||
$this->writeXmlAttribute('apimax', $this->parameters['apilimit']);
|
||||
$this->writeXmlAttribute('grabcurrent', $this->parameters['grabs']);
|
||||
$this->writeXmlAttribute('grabmax', $this->parameters['downloadlimit']);
|
||||
if (! empty($this->parameters['oldestapi'])) {
|
||||
$this->xml->writeAttribute('apioldesttime', (string) $this->parameters['oldestapi']);
|
||||
$this->writeXmlAttribute('apioldesttime', $this->parameters['oldestapi']);
|
||||
}
|
||||
if (! empty($this->parameters['oldestgrab'])) {
|
||||
$this->xml->writeAttribute('graboldesttime', (string) $this->parameters['oldestgrab']);
|
||||
$this->writeXmlAttribute('graboldesttime', $this->parameters['oldestgrab']);
|
||||
}
|
||||
$this->xml->endElement();
|
||||
}
|
||||
@@ -625,35 +660,35 @@ class XML_Response
|
||||
*/
|
||||
public function includeReleaseMain(): void
|
||||
{
|
||||
$this->xml->writeElement('title', $this->release->searchname);
|
||||
$this->writeXmlElement('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->writeXmlAttribute('isPermaLink', 'true');
|
||||
$this->writeXmlText("{$this->server['server']['url']}/details/{$this->release->guid}");
|
||||
$this->xml->endElement();
|
||||
$this->xml->writeElement(
|
||||
$this->writeXmlElement(
|
||||
'link',
|
||||
"{$this->server['server']['url']}/getnzb?id={$this->release->guid}.nzb".
|
||||
"&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((string) $this->release->adddate)));
|
||||
$this->xml->writeElement('category', $this->release->category_name);
|
||||
$this->writeXmlElement('comments', "{$this->server['server']['url']}/details/{$this->release->guid}#comments");
|
||||
$this->writeXmlElement('pubDate', date(DATE_RSS, strtotime((string) $this->release->adddate)));
|
||||
$this->writeXmlElement('category', $this->release->category_name);
|
||||
if ($this->namespace === 'newznab') {
|
||||
$this->xml->writeElement('description', $this->release->searchname);
|
||||
$this->writeXmlElement('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(
|
||||
$this->writeXmlAttribute(
|
||||
'url',
|
||||
"{$this->server['server']['url']}/getnzb?id={$this->release->guid}.nzb".
|
||||
"&r={$this->parameters['token']}".
|
||||
((int) $this->parameters['del'] === 1 ? '&del=1' : '')
|
||||
);
|
||||
$this->xml->writeAttribute('length', (string) $this->release->size);
|
||||
$this->xml->writeAttribute('type', 'application/x-nzb');
|
||||
$this->writeXmlAttribute('length', $this->release->size);
|
||||
$this->writeXmlAttribute('type', 'application/x-nzb');
|
||||
$this->xml->endElement();
|
||||
}
|
||||
}
|
||||
@@ -785,8 +820,8 @@ class XML_Response
|
||||
protected function writeZedAttr(string $name, mixed $value): void
|
||||
{
|
||||
$this->xml->startElement($this->namespace.':attr');
|
||||
$this->xml->writeAttribute('name', $name);
|
||||
$this->xml->writeAttribute('value', $value === null ? '' : (string) $value);
|
||||
$this->writeXmlAttribute('name', $name);
|
||||
$this->writeXmlAttribute('value', $value);
|
||||
$this->xml->endElement();
|
||||
}
|
||||
|
||||
@@ -855,7 +890,7 @@ class XML_Response
|
||||
$this->writeRssConsoleInfo();
|
||||
}
|
||||
$this->xml->startElement('description');
|
||||
$this->xml->writeCdata($this->cdata."\t</div>");
|
||||
$this->writeXmlCdata($this->cdata."\t</div>");
|
||||
$this->xml->endElement();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Api;
|
||||
|
||||
use App\Http\Controllers\Api\XML_Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class XMLResponseTest extends TestCase
|
||||
{
|
||||
public function test_api_xml_removes_invalid_xml_control_characters_from_release_fields(): void
|
||||
{
|
||||
$release = (object) [
|
||||
'searchname' => "Clean\x1fTitle",
|
||||
'guid' => 'release-guid',
|
||||
'adddate' => '2026-05-29 12:00:00',
|
||||
'category_name' => 'Movies > HD',
|
||||
'categories_id' => 2040,
|
||||
'size' => 123456789,
|
||||
'_totalrows' => 1,
|
||||
];
|
||||
|
||||
$response = new XML_Response([
|
||||
'Parameters' => [
|
||||
'extended' => '0',
|
||||
'del' => '0',
|
||||
'token' => 'test-token',
|
||||
'requests' => 1,
|
||||
'apilimit' => 100,
|
||||
'grabs' => 0,
|
||||
'downloadlimit' => 100,
|
||||
'oldestapi' => '',
|
||||
'oldestgrab' => '',
|
||||
],
|
||||
'Data' => [$release],
|
||||
'Server' => [
|
||||
'server' => [
|
||||
'title' => 'NNTmux Tests',
|
||||
'strapline' => 'Testing',
|
||||
'email' => 'noreply@example.test',
|
||||
'meta' => 'usenet',
|
||||
'url' => 'https://indexer.example.test',
|
||||
],
|
||||
],
|
||||
'Offset' => 0,
|
||||
'Type' => 'api',
|
||||
]);
|
||||
|
||||
$xml = $response->returnXML();
|
||||
|
||||
self::assertIsString($xml);
|
||||
self::assertStringNotContainsString("\x1f", $xml);
|
||||
self::assertStringContainsString('CleanTitle', $xml);
|
||||
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
$parsed = simplexml_load_string($xml);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
|
||||
self::assertNotFalse($parsed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user