Jelajahi Sumber

Tweaks for Vienna RSS (#2093)

* Tweaks for Vienna RSS

https://github.com/FreshRSS/FreshRSS/issues/2091
https://github.com/ViennaRSS/vienna-rss/issues/1197

* Fix get feed by URL

* Fix get item ids returning starred elements

* API add item ids by feed URL

* Add API filter `it`

https://feedhq.readthedocs.io/en/latest/api/reference.html#stream-items-ids

* API add `nt=` filter + refactoring

* No ; prefix for author

https://github.com/FreshRSS/FreshRSS/issues/2091#issuecomment-435562495

* Add id long form prefix and accept short id form

https://github.com/FreshRSS/FreshRSS/issues/2091#issuecomment-435631259

* Fix quote problem

https://github.com/FreshRSS/FreshRSS/issues/2091#issuecomment-435683930

* Isolate bug fix for News+

https://github.com/FreshRSS/FreshRSS/issues/2091#issuecomment-435687041

* Rework encoding conventions

https://github.com/FreshRSS/FreshRSS/issues/2091#issuecomment-437441834

* Unicode escaping alternative

Alternative approach to encode XML special characters and other
problematic characters into their Unicode fullwidth version when we
cannot use HTML-encoding because clients disagree wether they should
HTML-decode or not.
https://github.com/FreshRSS/FreshRSS/issues/2091#issuecomment-436059559
Alexandre Alapetite 7 tahun lalu
induk
melakukan
b672fc190d
6 mengubah file dengan 171 tambahan dan 89 penghapusan
  1. 8 0
      app/Models/BooleanSearch.php
  2. 2 2
      app/Models/EntryDAO.php
  3. 1 1
      app/Models/Feed.php
  4. 8 0
      app/Models/Search.php
  5. 12 0
      lib/lib_rss.php
  6. 140 86
      p/api/greader.php

+ 8 - 0
app/Models/BooleanSearch.php

@@ -45,6 +45,14 @@ class FreshRSS_BooleanSearch {
 		return $this->searches;
 	}
 
+	public function add($search) {
+		if ($search instanceof FreshRSS_Search) {
+			$this->searches[] = $search;
+			return $search;
+		}
+		return null;
+	}
+
 	public function __toString() {
 		return $this->getRawInput();
 	}

+ 2 - 2
app/Models/EntryDAO.php

@@ -921,8 +921,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
-	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {	//For API
-		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null) {	//For API
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
 
 		$stm = $this->bd->prepare($sql);
 		$stm->execute($values);

+ 1 - 1
app/Models/Feed.php

@@ -424,7 +424,7 @@ class FreshRSS_Feed extends Minz_Model {
 			$author_names = '';
 			if (is_array($authors)) {
 				foreach ($authors as $author) {
-					$author_names .= html_only_entity_decode(strip_tags($author->name == '' ? $author->email : $author->name)) . '; ';
+					$author_names .= escapeToUnicodeAlternative(strip_tags($author->name == '' ? $author->email : $author->name)) . '; ';
 				}
 			}
 			$author_names = substr($author_names, 0, -2);

+ 8 - 0
app/Models/Search.php

@@ -73,10 +73,18 @@ class FreshRSS_Search {
 		return $this->min_date;
 	}
 
+	public function setMinDate($value) {
+		return $this->min_date = $value;
+	}
+
 	public function getMaxDate() {
 		return $this->max_date;
 	}
 
+	public function setMaxDate($value) {
+		return $this->max_date = $value;
+	}
+
 	public function getMinPubdate() {
 		return $this->min_pubdate;
 	}

+ 12 - 0
lib/lib_rss.php

@@ -102,6 +102,18 @@ function safe_ascii($text) {
 	return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
 }
 
+function escapeToUnicodeAlternative($text) {
+	$text = htmlspecialchars_decode($text, ENT_QUOTES);
+	// https://raw.githubusercontent.com/mihaip/google-reader-api/master/wiki/StreamId.wiki
+	return trim(str_replace(
+			//Problematic characters
+			array("'", '"', '^', '<', '>', '?', '&', '\\', '/', ',', ';'),
+			//Use their fullwidth Unicode form instead:
+			array("’", '"', '^', '<', '>', '?', '&', '\', '/', ',', ';'),
+			$text
+		));
+}
+
 /**
  * Test if a given server address is publicly accessible.
  *

+ 140 - 86
p/api/greader.php

@@ -19,6 +19,7 @@ Server-side API compatible with Google Reader API layer 2
 * https://github.com/devongovett/reader
 * https://github.com/theoldreader/api
 * https://www.inoreader.com/developers/
+* https://feedhq.readthedocs.io/en/latest/api/index.html
 */
 
 require(__DIR__ . '/../../constants.php');
@@ -198,6 +199,7 @@ function clientLogin($email, $pass) {	//http://web.archive.org/web/2013060409104
 			header('Content-Type: text/plain; charset=UTF-8');
 			$auth = $email . '/' . sha1(FreshRSS_Context::$system_conf->salt . $email . FreshRSS_Context::$user_conf->apiPasswordHash);
 			echo 'SID=', $auth, "\n",
+				'LSID=null', "\n",	//Vienna RSS
 				'Auth=', $auth, "\n";
 			exit();
 		} else {
@@ -258,7 +260,7 @@ function tagList() {
 
 	foreach ($res as $cName) {
 		$tags[] = array(
-			'id' => 'user/-/label/' . $cName,
+			'id' => 'user/-/label/' . htmlspecialchars_decode($cName, ENT_QUOTES),
 			//'sortid' => $cName,
 			'type' => 'folder',	//Inoreader
 		);
@@ -270,7 +272,7 @@ function tagList() {
 	$labels = $tagDAO->listTags(true);
 	foreach ($labels as $label) {
 		$tags[] = array(
-			'id' => 'user/-/label/' . $label->name(),
+			'id' => 'user/-/label/' . htmlspecialchars_decode($label->name(), ENT_QUOTES),
 			//'sortid' => $cName,
 			'type' => 'tag',	//Inoreader
 			'unread_count' => $label->nbUnread(),	//Inoreader
@@ -298,17 +300,17 @@ function subscriptionList() {
 	foreach ($res as $line) {
 		$subscriptions[] = array(
 			'id' => 'feed/' . $line['id'],
-			'title' => $line['name'],
+			'title' => escapeToUnicodeAlternative($line['name']),
 			'categories' => array(
 				array(
-					'id' => 'user/-/label/' . $line['c_name'],
-					'label' => $line['c_name'],
+					'id' => 'user/-/label/' . htmlspecialchars_decode($line['c_name'], ENT_QUOTES),
+					'label' => htmlspecialchars_decode($line['c_name'], ENT_QUOTES),
 				),
 			),
 			//'sortid' => $line['name'],
 			//'firstitemmsec' => 0,
-			'url' => $line['url'],
-			'htmlUrl' => $line['website'],
+			'url' => htmlspecialchars_decode($line['url'], ENT_QUOTES),
+			'htmlUrl' => htmlspecialchars_decode($line['website'], ENT_QUOTES),
 			'iconUrl' => $faviconsUrl . hash('crc32b', $salt . $line['url']),
 		);
 	}
@@ -345,6 +347,7 @@ function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '
 				$c_name = '';
 			}
 		}
+		$c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
 		$cat = $categoryDAO->searchByName($c_name);
 		$addCatId = $cat == null ? 0 : $cat->id();
 	} else if ($remove != '' && strpos($remove, 'user/-/label/')) {
@@ -355,26 +358,28 @@ function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '
 		badRequest();
 	}
 	for ($i = count($streamNames) - 1; $i >= 0; $i--) {
-		$streamName = $streamNames[$i];	//feed/http://example.net/sample.xml	;	feed/338
-		if (strpos($streamName, 'feed/') === 0) {
-			$streamName = substr($streamName, 5);
+		$streamUrl = $streamNames[$i];	//feed/http://example.net/sample.xml	;	feed/338
+		if (strpos($streamUrl, 'feed/') === 0) {
+			$streamUrl = substr($streamUrl, 5);
 			$feedId = 0;
-			if (ctype_digit($streamName)) {
+			if (ctype_digit($streamUrl)) {
 				if ($action === 'subscribe') {
 					continue;
 				}
-				$feedId = $streamName;
+				$feedId = $streamUrl;
 			} else {
-				$feed = $feedDAO->searchByUrl($streamName);
+				$streamUrl = htmlspecialchars($streamUrl, ENT_COMPAT, 'UTF-8');
+				$feed = $feedDAO->searchByUrl($streamUrl);
 				$feedId = $feed == null ? -1 : $feed->id();
 			}
 			$title = isset($titles[$i]) ? $titles[$i] : '';
+			$title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8');
 			switch ($action) {
 				case 'subscribe':
 					if ($feedId <= 0) {
-						$http_auth = '';	//TODO
+						$http_auth = '';
 						try {
-							$feed = FreshRSS_feed_Controller::addFeed($streamName, $title, $addCatId, $c_name, $http_auth);
+							$feed = FreshRSS_feed_Controller::addFeed($streamUrl, $title, $addCatId, $c_name, $http_auth);
 							continue;
 						} catch (Exception $e) {
 							Minz_Log::error('subscriptionEdit error subscribe: ' . $e->getMessage(), API_LOG);
@@ -407,6 +412,7 @@ function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '
 
 function quickadd($url) {
 	try {
+		$url = htmlspecialchars($url, ENT_COMPAT, 'UTF-8');
 		$feed = FreshRSS_feed_Controller::addFeed($url);
 		exit(json_encode(array(
 				'numResults' => 1,
@@ -442,7 +448,7 @@ function unreadCount() {	//http://blog.martindoms.com/2009/10/16/using-the-googl
 			}
 		}
 		$unreadcounts[] = array(
-			'id' => 'user/-/label/' . $cat->name(),
+			'id' => 'user/-/label/' . htmlspecialchars_decode($cat->name(), ENT_QUOTES),
 			'count' => $cat->nbNotRead(),
 			'newestItemTimestampUsec' => $catLastUpdate . '000000',
 		);
@@ -455,7 +461,7 @@ function unreadCount() {	//http://blog.martindoms.com/2009/10/16/using-the-googl
 	$tagDAO = FreshRSS_Factory::createTagDao();
 	foreach ($tagDAO->listTags(true) as $label) {
 		$unreadcounts[] = array(
-			'id' => 'user/-/label/' . $label->name(),
+			'id' => 'user/-/label/' . htmlspecialchars_decode($label->name(), ENT_QUOTES),
 			'count' => $label->nbUnread(),
 		);
 	}
@@ -496,28 +502,29 @@ function entriesToArray($entries) {
 			$f_name = '_';
 		}
 		$item = array(
-			'id' => /*'tag:google.com,2005:reader/item/' .*/ dec2hex($entry->id()),	//64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId
+			'id' => 'tag:google.com,2005:reader/item/' . dec2hex($entry->id()),	//64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId
 			'crawlTimeMsec' => substr($entry->id(), 0, -3),
 			'timestampUsec' => '' . $entry->id(),	//EasyRSS
 			'published' => $entry->date(true),
-			'title' => $entry->title(),
+			'title' => escapeToUnicodeAlternative($entry->title()),
 			'summary' => array('content' => $entry->content()),
 			'alternate' => array(
 				array('href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES)),
 			),
 			'categories' => array(
 				'user/-/state/com.google/reading-list',
-				'user/-/label/' . $c_name,
+				'user/-/label/' . htmlspecialchars_decode($c_name, ENT_QUOTES),
 			),
 			'origin' => array(
 				'streamId' => 'feed/' . $f_id,
-				'title' => $f_name,	//EasyRSS
+				'title' => escapeToUnicodeAlternative($f_name),	//EasyRSS
 				//'htmlUrl' => $line['f_website'],
 			),
 		);
 		$author = $entry->authors(true);
+		$author = trim($author, '; ');
 		if ($author != '') {
-			$item['author'] = $author;
+			$item['author'] = escapeToUnicodeAlternative($author);
 		}
 		if ($entry->isRead()) {
 			$item['categories'][] = 'user/-/state/com.google/read';
@@ -527,69 +534,117 @@ function entriesToArray($entries) {
 		}
 		$tagNames = isset($entryIdsTagNames['e_' . $entry->id()]) ? $entryIdsTagNames['e_' . $entry->id()] : array();
 		foreach ($tagNames as $tagName) {
-			$item['categories'][] = 'user/-/label/' . $tagName;
+			$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($tagName, ENT_QUOTES);
 		}
 		$items[] = $item;
 	}
 	return $items;
 }
 
-function streamContents($path, $include_target, $start_time, $count, $order, $exclude_target, $continuation) {
-//http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
-//http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
-	header('Content-Type: application/json; charset=UTF-8');
-
-	switch ($path) {
-		case 'reading-list':
-			$type = 'A';
-			break;
-		case 'starred':
-			$type = 's';
-			break;
-		case 'feed':
-			$type = 'f';
+function streamContentsFilters($type, $streamId, $filter_target, $exclude_target, $start_time, $stop_time) {
+	switch ($type) {
+		case 'f':	//feed
+			if ($streamId != '' && !ctype_digit($streamId)) {
+				$feedDAO = FreshRSS_Factory::createFeedDao();
+				$streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
+				$feed = $feedDAO->searchByUrl($streamId);
+				$streamId = $feed == null ? -1 : $feed->id();
+			}
 			break;
-		case 'label':
+		case 'c':	//category or label
 			$categoryDAO = FreshRSS_Factory::createCategoryDao();
-			$cat = $categoryDAO->searchByName($include_target);
+			$streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
+			$cat = $categoryDAO->searchByName($streamId);
 			if ($cat != null) {
 				$type = 'c';
-				$include_target = $cat->id();
+				$streamId = $cat->id();
 			} else {
 				$tagDAO = FreshRSS_Factory::createTagDao();
-				$tag = $tagDAO->searchByName($include_target);
+				$tag = $tagDAO->searchByName($streamId);
 				if ($tag != null) {
 					$type = 't';
-					$include_target = $tag->id();
+					$streamId = $tag->id();
 				} else {
 					$type = 'A';
-					$include_target = -1;
+					$streamId = -1;
 				}
 			}
 			break;
+	}
+
+	switch ($filter_target) {
+		case 'user/-/state/com.google/read':
+			$state = FreshRSS_Entry::STATE_READ;
+			break;
+		case 'user/-/state/com.google/unread':
+			$state = FreshRSS_Entry::STATE_NOT_READ;
+			break;
+		case 'user/-/state/com.google/starred':
+			$state = FreshRSS_Entry::STATE_FAVORITE;
+			break;
 		default:
-			$type = 'A';
+			$state = FreshRSS_Entry::STATE_ALL;
 			break;
 	}
 
 	switch ($exclude_target) {
 		case 'user/-/state/com.google/read':
-			$state = FreshRSS_Entry::STATE_NOT_READ;
+			$state &= FreshRSS_Entry::STATE_NOT_READ;
 			break;
 		case 'user/-/state/com.google/unread':
-			$state = FreshRSS_Entry::STATE_READ;
+			$state &= FreshRSS_Entry::STATE_READ;
+			break;
+		case 'user/-/state/com.google/starred':
+			$state &= FreshRSS_Entry::STATE_NOT_FAVORITE;
+			break;
+	}
+
+	$searches = new FreshRSS_BooleanSearch('');
+	if ($start_time != '') {
+		$search = new FreshRSS_Search('');
+		$search->setMinDate($start_time);
+		$searches->add($search);
+	}
+	if ($stop_time != '') {
+		$search = new FreshRSS_Search('');
+		$search->setMaxDate($stop_time);
+		$searches->add($search);
+	}
+
+	return array($type, $streamId, $state, $searches);
+}
+
+function streamContents($path, $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation) {
+//http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
+//http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
+	header('Content-Type: application/json; charset=UTF-8');
+
+	switch ($path) {
+		case 'reading-list':
+			$type = 'A';
+			break;
+		case 'starred':
+			$type = 's';
+			break;
+		case 'feed':
+			$type = 'f';
+			break;
+		case 'label':
+			$type = 'c';
 			break;
 		default:
-			$state = FreshRSS_Entry::STATE_ALL;
+			$type = 'A';
 			break;
 	}
 
+	list($type, $include_target, $state, $searches) = streamContentsFilters($type, $include_target, $filter_target, $exclude_target, $start_time, $stop_time);
+
 	if ($continuation != '') {
 		$count++;	//Shift by one element
 	}
 
 	$entryDAO = FreshRSS_Factory::createEntryDao();
-	$entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_BooleanSearch(''), $start_time);
+	$entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
 
 	$items = entriesToArray($entries);
 
@@ -614,7 +669,7 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex
 	exit();
 }
 
-function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude_target, $continuation) {
+function streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation) {
 //http://code.google.com/p/google-reader-api/wiki/ApiStreamItemsIds
 //http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI
 //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
@@ -622,55 +677,32 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude
 	$id = '';
 	if ($streamId === 'user/-/state/com.google/reading-list') {
 		$type = 'A';
-	} elseif ('user/-/state/com.google/starred') {
+	} elseif ($streamId === 'user/-/state/com.google/starred') {
 		$type = 's';
 	} elseif (strpos($streamId, 'feed/') === 0) {
 		$type = 'f';
-		$id = basename($streamId);
+		$streamId = substr($streamId, 5);
 	} elseif (strpos($streamId, 'user/-/label/') === 0) {
 		$type = 'c';
-		$c_name = substr($streamId, 13);
-		$categoryDAO = FreshRSS_Factory::createCategoryDao();
-		$cat = $categoryDAO->searchByName($c_name);
-		if ($cat != null) {
-			$type = 'c';
-			$id = $cat->id();
-		} else {
-			$tagDAO = FreshRSS_Factory::createTagDao();
-			$tag = $tagDAO->searchByName($c_name);
-			if ($tag != null) {
-				$type = 't';
-				$id = $tag->id();
-			} else {
-				$type = 'A';
-				$id = -1;
-			}
-		}
+		$streamId = substr($streamId, 13);
 	}
 
-	switch ($exclude_target) {
-		case 'user/-/state/com.google/read':
-			$state = FreshRSS_Entry::STATE_NOT_READ;
-			break;
-		default:
-			$state = FreshRSS_Entry::STATE_ALL;
-			break;
-	}
+	list($type, $id, $state, $searches) = streamContentsFilters($type, $streamId, $filter_target, $exclude_target, $start_time, $stop_time);
 
 	if ($continuation != '') {
 		$count++;	//Shift by one element
 	}
 
 	$entryDAO = FreshRSS_Factory::createEntryDao();
-	$ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_BooleanSearch(''), $start_time);
+	$ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
 
 	if ($continuation != '') {
 		array_shift($ids);	//Discard first element that was already sent in the previous response
 		$count--;
 	}
 
-	if (empty($ids)) {	//For News+ bug https://github.com/noinnion/newsplus/issues/84#issuecomment-57834632
-		$ids[] = 0;
+	if (empty($ids) && isset($_GET['client']) && $_GET['client'] === 'newsplus') {
+		$ids[] = 0;	//For News+ bug https://github.com/noinnion/newsplus/issues/84#issuecomment-57834632
 	}
 	$itemRefs = array();
 	foreach ($ids as $id) {
@@ -697,7 +729,10 @@ function streamContentsItems($e_ids, $order) {
 	header('Content-Type: application/json; charset=UTF-8');
 
 	foreach ($e_ids as $i => $e_id) {
-		$e_ids[$i] = hex2dec(basename($e_id));	//Strip prefix 'tag:google.com,2005:reader/item/'
+		if (strpos($e_id, '/') !== null) {
+			$e_id = hex2dec(basename($e_id));	//Strip prefix 'tag:google.com,2005:reader/item/'
+		}
+		$e_ids[$i] = $e_id;
 	}
 
 	$entryDAO = FreshRSS_Factory::createEntryDao();
@@ -717,7 +752,10 @@ function streamContentsItems($e_ids, $order) {
 
 function editTag($e_ids, $a, $r) {
 	foreach ($e_ids as $i => $e_id) {
-		$e_ids[$i] = hex2dec(basename($e_id));	//Strip prefix 'tag:google.com,2005:reader/item/'
+		if (strpos($e_id, '/') !== null) {
+			$e_id = hex2dec(basename($e_id));	//Strip prefix 'tag:google.com,2005:reader/item/'
+		}
+		$e_ids[$i] = $e_id;
 	}
 
 	$entryDAO = FreshRSS_Factory::createEntryDao();
@@ -748,6 +786,7 @@ function editTag($e_ids, $a, $r) {
 				}
 			}
 			if ($tagName != '') {
+				$tagName = htmlspecialchars($tagName, ENT_COMPAT, 'UTF-8');
 				$tag = $tagDAO->searchByName($tagName);
 				if ($tag == null) {
 					$tagDAO->addTag(array('name' => $tagName));
@@ -771,6 +810,7 @@ function editTag($e_ids, $a, $r) {
 		default:
 			if (strpos($r, 'user/-/label/') === 0) {
 				$tagName = substr($r, 13);
+				$tagName = htmlspecialchars($tagName, ENT_COMPAT, 'UTF-8');
 				$tag = $tagDAO->searchByName($tagName);
 				if ($tag != null) {
 					foreach ($e_ids as $e_id) {
@@ -788,7 +828,9 @@ function renameTag($s, $dest) {
 	if ($s != '' && strpos($s, 'user/-/label/') === 0 &&
 		$dest != '' &&  strpos($dest, 'user/-/label/') === 0) {
 		$s = substr($s, 13);
+		$s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
 		$dest = substr($dest, 13);
+		$dest = htmlspecialchars($dest, ENT_COMPAT, 'UTF-8');
 
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$cat = $categoryDAO->searchByName($s);
@@ -810,6 +852,7 @@ function renameTag($s, $dest) {
 function disableTag($s) {
 	if ($s != '' && strpos($s, 'user/-/label/') === 0) {
 		$s = substr($s, 13);
+		$s = htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$cat = $categoryDAO->searchByName($s);
 		if ($cat != null) {
@@ -838,6 +881,7 @@ function markAllAsRead($streamId, $olderThanId) {
 		$entryDAO->markReadFeed($f_id, $olderThanId);
 	} elseif (strpos($streamId, 'user/-/label/') === 0) {
 		$c_name = substr($streamId, 13);
+		$c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
 		$categoryDAO = FreshRSS_Factory::createCategoryDao();
 		$cat = $categoryDAO->searchByName($c_name);
 		if ($cat != null) {
@@ -902,12 +946,14 @@ if (count($pathInfos) < 3) {
 			 * exclude items from a particular feed (obviously not useful in this
 			 * request, but xt appears in other listing requests). */
 			$exclude_target = isset($_GET['xt']) ? $_GET['xt'] : '';
+			$filter_target = isset($_GET['it']) ? $_GET['it'] : '';
 			$count = isset($_GET['n']) ? intval($_GET['n']) : 20;	//n=[integer] : The maximum number of results to return.
 			$order = isset($_GET['r']) ? $_GET['r'] : 'd';	//r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order.
 			/* ot=[unix timestamp] : The time from which you want to retrieve
 			 * items. Only items that have been crawled by Google Reader after
 			 * this time will be returned. */
 			$start_time = isset($_GET['ot']) ? intval($_GET['ot']) : 0;
+			$stop_time = isset($_GET['nt']) ? intval($_GET['nt']) : 0;
 			/* Continuation token. If a StreamContents response does not represent
 			 * all items in a timestamp range, it will have a continuation attribute.
 			 * The same request can be re-issued with the value of that attribute put
@@ -920,23 +966,31 @@ if (count($pathInfos) < 3) {
 				if (isset($pathInfos[7])) {
 					if ($pathInfos[6] === 'feed') {
 						$include_target = $pathInfos[7];
-						StreamContents($pathInfos[6], $include_target, $start_time, $count, $order, $exclude_target, $continuation);
+						if ($include_target != '' && !ctype_digit($include_target)) {
+							$include_target = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI'];
+							if (preg_match('#/reader/api/0/stream/contents/feed/([A-Za-z0-9\'!*()%$_.~+-]+)#', $include_target, $matches) && isset($matches[1])) {
+								$include_target = urldecode($matches[1]);
+							} else {
+								$include_target = '';
+							}
+						}
+						streamContents($pathInfos[6], $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
 					} elseif ($pathInfos[6] === 'user' && isset($pathInfos[8]) && isset($pathInfos[9])) {
 						if ($pathInfos[8] === 'state') {
 							if ($pathInfos[9] === 'com.google' && isset($pathInfos[10])) {
 								if ($pathInfos[10] === 'reading-list' || $pathInfos[10] === 'starred') {
 									$include_target = '';
-									streamContents($pathInfos[10], $include_target, $start_time, $count, $order, $exclude_target, $continuation);
+									streamContents($pathInfos[10], $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
 								}
 							}
 						} elseif ($pathInfos[8] === 'label') {
 							$include_target = $pathInfos[9];
-							streamContents($pathInfos[8], $include_target, $start_time, $count, $order, $exclude_target, $continuation);
+							streamContents($pathInfos[8], $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
 						}
 					}
 				} else {	//EasyRSS
 					$include_target = '';
-					streamContents('reading-list', $include_target, $start_time, $count, $order, $exclude_target, $continuation);
+					streamContents('reading-list', $include_target, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
 				}
 			} elseif ($pathInfos[5] === 'items') {
 				if ($pathInfos[6] === 'ids' && isset($_GET['s'])) {
@@ -944,7 +998,7 @@ if (count($pathInfos) < 3) {
 					 * be repeated to fetch the item IDs from multiple streams at once
 					 * (more efficient from a backend perspective than multiple requests). */
 					$streamId = $_GET['s'];
-					streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude_target, $continuation);
+					streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
 				} else if ($pathInfos[6] === 'contents' && isset($_POST['i'])) {	//FeedMe
 					$e_ids = multiplePosts('i');	//item IDs
 					streamContentsItems($e_ids, $order);