Sfoglia il codice sorgente

feat(import): accept .txt URL lists alongside OPML/JSON/ZIP (#8818)

* feat(import): accept .txt URL lists alongside OPML/JSON/ZIP

Detects .txt by extension and wraps the URL list into a minimal OPML
document so the existing import pipeline handles dedup, categories and
feed limits unchanged. Blank lines, `#` comments and a UTF-8 BOM are
skipped; lines that don't parse as URLs are logged and dropped without
aborting the batch.

Works through both `cli/import-for-user.php` and the web import form.

* utf8BOM

* ENT_COMPAT

---------

Co-authored-by: Bjørn A. Andersen <polybjorn@users.noreply.github.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
polybjorn 1 settimana fa
parent
commit
8863cdcaf8
2 ha cambiato i file con 39 aggiunte e 1 eliminazioni
  1. 38 0
      app/Controllers/importExportController.php
  2. 1 1
      cli/import-for-user.php

+ 38 - 0
app/Controllers/importExportController.php

@@ -98,6 +98,11 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		} elseif ('zip' === $type_file) {
 			// ZIP extension is not loaded
 			throw new FreshRSS_ZipMissing_Exception();
+		} elseif ('txt' === $type_file) {
+			$contents = file_get_contents($path);
+			if (is_string($contents)) {
+				$list_files['opml'][] = self::txtToOpml($contents);
+			}
 		} elseif ('unknown' !== $type_file) {
 			$list_files[$type_file][] = file_get_contents($path);
 		}
@@ -219,6 +224,8 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 	private static function guessFileType(string $filename): string {
 		if (str_ends_with($filename, '.zip')) {
 			return 'zip';
+		} elseif (str_ends_with($filename, '.txt')) {
+			return 'txt';
 		} elseif (stripos($filename, 'opml') !== false) {
 			return 'opml';
 		} elseif (str_ends_with($filename, '.json')) {
@@ -237,6 +244,37 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		return 'unknown';
 	}
 
+	/**
+	 * Wraps a newline-separated list of feed URLs into a minimal OPML document
+	 * so it can be imported through the existing OPML pipeline.
+	 */
+	private static function txtToOpml(string $contents): string {
+		$utf8BOM = "\xEF\xBB\xBF";
+		$contents = preg_replace('/^' . $utf8BOM . '/', '', $contents) ?? $contents;
+		$outlines = '';
+		foreach (preg_split('/\R/', $contents) ?: [] as $line) {
+			$url = trim($line);
+			if ($url === '' || str_starts_with($url, '#')) {
+				continue;
+			}
+			if (filter_var($url, FILTER_VALIDATE_URL) === false) {
+				$message = 'TXT import: skipping invalid URL “' . $url . '”';
+				if (FreshRSS_Context::$isCli) {
+					fwrite(STDERR, $message . "\n");
+				} else {
+					Minz_Log::warning($message);
+				}
+				continue;
+			}
+			$escaped = htmlspecialchars($url, ENT_COMPAT | ENT_XML1, 'UTF-8');
+			$outlines .= '<outline type="rss" text="' . $escaped . '" xmlUrl="' . $escaped . '" />' . "\n";
+		}
+		return '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
+			. '<opml version="2.0"><body>' . "\n"
+			. $outlines
+			. '</body></opml>' . "\n";
+	}
+
 	private function ttrssXmlToJson(string $xml): string|false {
 		$table = (array)simplexml_load_string($xml, options: LIBXML_NOBLANKS | LIBXML_NOCDATA);
 		$table['items'] = $table['article'] ?? [];

+ 1 - 1
cli/import-for-user.php

@@ -27,7 +27,7 @@ if (!is_readable($filename)) {
 	fail('FreshRSS error: file is not readable “' . $filename . '”');
 }
 
-echo 'FreshRSS importing ZIP/OPML/JSON for user “', $username, "”…\n";
+echo 'FreshRSS importing ZIP/OPML/JSON/TXT for user “', $username, "”…\n";
 
 $importController = new FreshRSS_importExport_Controller();