Prechádzať zdrojové kódy

Refactor entry-to-GReader API format (#4490)

* Refactor entry to GReader API format
Some code was copied in two locations and not completely uniform.
Cleaning of related variables and functions (e.g. better types for entries and categories as objects vs. as IDs).
Usecase: I need to call the same GReader-compatible serialization from an extension

* Fixed some edge cases

* Keep summary instead of content
`summary` and `content` seems to be used interchangeably in the Google Reader API. We have been using `summary` for our client API and  `content` in our export/import, so stick to that.
Alexandre Alapetite 3 rokov pred
rodič
commit
82ac1d1e67

+ 3 - 2
app/Controllers/feedController.php

@@ -74,7 +74,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		$feed->_kind($kind);
 		$feed->_attributes('', $attributes);
 		$feed->_httpAuth($http_auth);
-		$feed->_category($cat_id);
+		$feed->_categoryId($cat_id);
 		switch ($kind) {
 			case FreshRSS_Feed::KIND_RSS:
 			case FreshRSS_Feed::KIND_RSS_FORCED:
@@ -425,6 +425,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 							//Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->url(false) .
 								//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
 							$entry->_isRead($mark_updated_article_unread ? false : null);	//Change is_read according to policy.
+							$entry->_isFavorite(null);	// Do not change favourite state
 
 							/** @var FreshRSS_Entry|null */
 							$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
@@ -908,7 +909,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		}
 
 		//Get feed.
-		$feed = $entry->feed(true);
+		$feed = $entry->feed();
 
 		if (!$feed) {
 			$this->view->fatalError = _t('feedback.sub.feed.selector_preview.no_feed');

+ 16 - 9
app/Controllers/importExportController.php

@@ -249,6 +249,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 					'feedUrl' => isset($item['feed_url']) ? $item['feed_url'] : '',
 				);
 			$item['id'] = isset($item['guid']) ? $item['guid'] : (isset($item['feed_url']) ? $item['feed_url'] : $item['published']);
+			$item['guid'] = $item['id'];
 			$table['items'][$i] = $item;
 		}
 		return json_encode($table);
@@ -284,7 +285,10 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 
 		// First, we check feeds of articles are in DB (and add them if needed).
 		foreach ($items as $item) {
-			if (empty($item['id'])) {
+			if (!isset($item['guid']) && isset($item['id'])) {
+				$item['guid'] = $item['id'];
+			}
+			if (empty($item['guid'])) {
 				continue;
 			}
 			if (empty($item['origin'])) {
@@ -326,11 +330,11 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 			}
 
 			if ($feed != null) {
-				$article_to_feed[$item['id']] = $feed->id();
+				$article_to_feed[$item['guid']] = $feed->id();
 				if (!isset($newFeedGuids['f_' . $feed->id()])) {
 					$newFeedGuids['f_' . $feed->id()] = array();
 				}
-				$newFeedGuids['f_' . $feed->id()][] = safe_ascii($item['id']);
+				$newFeedGuids['f_' . $feed->id()][] = safe_ascii($item['guid']);
 			}
 		}
 
@@ -354,14 +358,14 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		$newGuids = array();
 		$this->entryDAO->beginTransaction();
 		foreach ($items as $item) {
-			if (empty($item['id']) || empty($article_to_feed[$item['id']])) {
+			if (empty($item['guid']) || empty($article_to_feed[$item['guid']])) {
 				// Related feed does not exist for this entry, do nothing.
 				continue;
 			}
 
-			$feed_id = $article_to_feed[$item['id']];
+			$feed_id = $article_to_feed[$item['guid']];
 			$author = isset($item['author']) ? $item['author'] : '';
-			$is_starred = false;
+			$is_starred = null; // null is used to preserve the current state if that item exists and is already starred
 			$is_read = null;
 			$tags = empty($item['categories']) ? array() : $item['categories'];
 			$labels = array();
@@ -429,7 +433,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 			}
 
 			$entry = new FreshRSS_Entry(
-				$feed_id, $item['id'], $title, $author,
+				$feed_id, $item['guid'], $title, $author,
 				$content, $url, $published, $is_read, $is_starred
 			);
 			$entry->_id(uTimeString());
@@ -463,7 +467,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 				}
 				$knownLabels[$labelName]['articles'][] = array(
 						//'id' => $entry->id(),	//ID changes after commitNewEntries()
-						'id_feed' => $entry->feed(),
+						'id_feed' => $entry->feedId(),
 						'guid' => $entry->guid(),
 					);
 			}
@@ -480,6 +484,9 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		$this->entryDAO->beginTransaction();
 		foreach ($knownLabels as $labelName => $knownLabel) {
 			$labelId = $knownLabel['id'];
+			if (!$labelId) {
+				continue;
+			}
 			foreach ($knownLabel['articles'] as $article) {
 				$entryId = $this->entryDAO->searchIdByGuid($article['id_feed'], $article['guid']);
 				if ($entryId != null) {
@@ -521,7 +528,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		try {
 			// Create a Feed object and add it in database.
 			$feed = new FreshRSS_Feed($url);
-			$feed->_category(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
+			$feed->_categoryId(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
 			$feed->_name($name);
 			$feed->_website($website);
 			if (!empty($origin['disable'])) {

+ 1 - 1
app/Controllers/subscriptionController.php

@@ -244,7 +244,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 			}
 
 			if ($feedDAO->updateFeed($id, $values) !== false) {
-				$feed->_category($cat);
+				$feed->_categoryId($cat);
 				$feed->faviconPrepare();
 
 				Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);

+ 1 - 1
app/Models/Category.php

@@ -219,7 +219,7 @@ class FreshRSS_Category extends Minz_Model {
 				foreach ($dryRunCategory->feeds() as $dryRunFeed) {
 					if (empty($existingFeeds[$dryRunFeed->url()])) {
 						// The feed does not exist in the current category, so add that feed
-						$dryRunFeed->_category($this->id());
+						$dryRunFeed->_categoryId($this->id());
 						$ok &= ($feedDAO->addFeedObject($dryRunFeed) !== false);
 					} else {
 						$existingFeed = $existingFeeds[$dryRunFeed->url()];

+ 1 - 1
app/Models/CategoryDAO.php

@@ -108,7 +108,7 @@ SQL;
 			$valuesTmp['name'],
 		);
 
-		if ($stm && $stm->execute($values)) {
+		if ($stm && $stm->execute($values) && $stm->rowCount() > 0) {
 			return $this->pdo->lastInsertId('`_category_id_seq`');
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();

+ 1 - 1
app/Models/Context.php

@@ -299,7 +299,7 @@ class FreshRSS_Context {
 				}
 			}
 			self::$current_get['feed'] = $id;
-			self::$current_get['category'] = $feed->category();
+			self::$current_get['category'] = $feed->categoryId();
 			self::$name = $feed->name();
 			self::$description = $feed->description();
 			self::$get_unread = $feed->nbNotRead();

+ 129 - 19
app/Models/Entry.php

@@ -31,6 +31,7 @@ class FreshRSS_Entry extends Minz_Model {
 	 * @var bool|null
 	 */
 	private $is_read;
+	/** @var bool|null */
 	private $is_favorite;
 
 	/**
@@ -213,17 +214,22 @@ class FreshRSS_Entry extends Minz_Model {
 	public function isFavorite() {
 		return $this->is_favorite;
 	}
-	public function feed($object = false) {
-		if ($object) {
-			if ($this->feed == null) {
-				$feedDAO = FreshRSS_Factory::createFeedDao();
-				$this->feed = $feedDAO->searchById($this->feedId);
-			}
-			return $this->feed;
-		} else {
-			return $this->feedId;
+
+	/**
+	 * @return FreshRSS_Feed|null|false
+	 */
+	public function feed() {
+		if ($this->feed === null) {
+			$feedDAO = FreshRSS_Factory::createFeedDao();
+			$this->feed = $feedDAO->searchById($this->feedId);
 		}
+		return $this->feed;
 	}
+
+	public function feedId(): int {
+		return $this->feedId;
+	}
+
 	public function tags($asString = false) {
 		if ($asString) {
 			return $this->tags == null ? '' : '#' . implode(' #', $this->tags);
@@ -331,18 +337,21 @@ class FreshRSS_Entry extends Minz_Model {
 		$this->is_read = $value === null ? null : (bool)$value;
 	}
 	public function _isFavorite($value) {
-		$this->is_favorite = $value;
+		$this->is_favorite = $value === null ? null : (bool)$value;
 	}
-	public function _feed($value) {
-		if ($value != null) {
-			$this->feed = $value;
-			$this->feedId = $this->feed->id();
-		}
+
+	/** @param FreshRSS_Feed|null $feed */
+	public function _feed($feed) {
+		$this->feed = $feed;
+		$this->feedId = $this->feed == null ? 0 : $this->feed->id();
 	}
-	private function _feedId($value) {
+
+	/** @param int|string $id */
+	private function _feedId($id) {
 		$this->feed = null;
-		$this->feedId = intval($value);
+		$this->feedId = intval($id);
 	}
+
 	public function _tags($value) {
 		$this->hash = '';
 		if (!is_array($value)) {
@@ -558,7 +567,7 @@ class FreshRSS_Entry extends Minz_Model {
 	public function loadCompleteContent(bool $force = false): bool {
 		// Gestion du contenu
 		// Trying to fetch full article content even when feeds do not propose it
-		$feed = $this->feed(true);
+		$feed = $this->feed();
 		if ($feed != null && trim($feed->pathEntries()) != '') {
 			$entryDAO = FreshRSS_Factory::createEntryDao();
 			$entry = $force ? null : $entryDAO->searchByGuid($this->feedId, $this->guid);
@@ -613,9 +622,110 @@ class FreshRSS_Entry extends Minz_Model {
 			'hash' => $this->hash(),
 			'is_read' => $this->isRead(),
 			'is_favorite' => $this->isFavorite(),
-			'id_feed' => $this->feed(),
+			'id_feed' => $this->feedId(),
 			'tags' => $this->tags(true),
 			'attributes' => $this->attributes(),
 		);
 	}
+
+	/**
+	 * Integer format conversion for Google Reader API format
+	 * @param string|int $dec Decimal number
+	 * @return string 64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId
+	 */
+	private static function dec2hex($dec): string {
+		return PHP_INT_SIZE < 8 ? // 32-bit ?
+			str_pad(gmp_strval(gmp_init($dec, 10), 16), 16, '0', STR_PAD_LEFT) :
+			str_pad(dechex($dec), 16, '0', STR_PAD_LEFT);
+	}
+
+	/**
+	 * N.B.: To avoid expensive lookups, ensure to set `$entry->_feed($feed)` before calling this function.
+	 * N.B.: You might have to populate `$entry->_tags()` prior to calling this function.
+	 * @param string $mode Set to `'compat'` to use an alternative Unicode representation for problematic HTML special characters not decoded by some clients;
+	 * 	set to `'freshrss'` for using FreshRSS additions for internal use (e.g. export/import).
+	 * @return array<string,mixed> A representation of this entry in a format compatible with Google Reader API
+	 */
+	public function toGReader(string $mode = ''): array {
+
+		$feed = $this->feed();
+		$category = $feed == null ? null : $feed->category();
+
+		$item = [
+			'id' => 'tag:google.com,2005:reader/item/' . self::dec2hex($this->id()),
+			'crawlTimeMsec' => substr($this->dateAdded(true, true), 0, -3),
+			'timestampUsec' => '' . $this->dateAdded(true, true), //EasyRSS & Reeder
+			'published' => $this->date(true),
+			// 'updated' => $this->date(true),
+			'title' => $this->title(),
+			'summary' => ['content' => $this->content()],
+			'canonical' => [
+				['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)],
+			],
+			'alternate' => [
+				[
+					'href' => htmlspecialchars_decode($this->link(), ENT_QUOTES),
+					'type' => 'text/html',
+				],
+			],
+			'categories' => [
+				'user/-/state/com.google/reading-list',
+			],
+			'origin' => [
+				'streamId' => 'feed/' . $this->feedId,
+			],
+		];
+		if ($mode === 'compat') {
+			$item['title'] = escapeToUnicodeAlternative($this->title(), false);
+		} elseif ($mode === 'freshrss') {
+			$item['guid'] = $this->guid();
+			unset($item['summary']);
+			$item['content'] = ['content' => $this->content()];
+		}
+		if ($category != null && $mode !== 'freshrss') {
+			$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES);
+		}
+		if ($feed != null) {
+			$item['origin']['htmlUrl'] = htmlspecialchars_decode($feed->website());
+			$item['origin']['title'] = $feed->name();	//EasyRSS
+			if ($mode === 'compat') {
+				$item['origin']['title'] = escapeToUnicodeAlternative($feed->name(), true);
+			} elseif ($mode === 'freshrss') {
+				$item['origin']['feedUrl'] = htmlspecialchars_decode($feed->url());
+			}
+		}
+		foreach ($this->enclosures() as $enclosure) {
+			if (!empty($enclosure['url']) && !empty($enclosure['type'])) {
+				$media = [
+						'href' => $enclosure['url'],
+						'type' => $enclosure['type'],
+					];
+				if (!empty($enclosure['length'])) {
+					$media['length'] = intval($enclosure['length']);
+				}
+				$item['enclosure'][] = $media;
+			}
+		}
+		$author = $this->authors(true);
+		$author = trim($author, '; ');
+		if ($author != '') {
+			if ($mode === 'compat') {
+				$item['author'] = escapeToUnicodeAlternative($author, false);
+			} else {
+				$item['author'] = $author;
+			}
+		}
+		if ($this->isRead()) {
+			$item['categories'][] = 'user/-/state/com.google/read';
+		} elseif ($mode === 'freshrss') {
+			$item['categories'][] = 'user/-/state/com.google/unread';
+		}
+		if ($this->isFavorite()) {
+			$item['categories'][] = 'user/-/state/com.google/starred';
+		}
+		foreach ($this->tags() as $tagName) {
+			$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($tagName, ENT_QUOTES);
+		}
+		return $item;
+	}
 }

+ 10 - 1
app/Models/EntryDAO.php

@@ -218,6 +218,9 @@ SQL;
 		if (!isset($valuesTmp['is_read'])) {
 			$valuesTmp['is_read'] = null;
 		}
+		if (!isset($valuesTmp['is_favorite'])) {
+			$valuesTmp['is_favorite'] = null;
+		}
 
 		if ($this->updateEntryPrepared === null) {
 			$sql = 'UPDATE `_entry` '
@@ -226,6 +229,7 @@ SQL;
 				. ', link=:link, date=:date, `lastSeen`=:last_seen'
 				. ', hash=' . static::sqlHexDecode(':hash')
 				. ', is_read=COALESCE(:is_read, is_read)'
+				. ', is_favorite=COALESCE(:is_favorite, is_favorite)'
 				. ', tags=:tags, attributes=:attributes '
 				. 'WHERE id_feed=:id_feed AND guid=:guid';
 			$this->updateEntryPrepared = $this->pdo->prepare($sql);
@@ -254,6 +258,11 @@ SQL;
 			} else {
 				$this->updateEntryPrepared->bindValue(':is_read', $valuesTmp['is_read'] ? 1 : 0, PDO::PARAM_INT);
 			}
+			if ($valuesTmp['is_favorite'] === null) {
+				$this->updateEntryPrepared->bindValue(':is_favorite', null, PDO::PARAM_NULL);
+			} else {
+				$this->updateEntryPrepared->bindValue(':is_favorite', $valuesTmp['is_favorite'] ? 1 : 0, PDO::PARAM_INT);
+			}
 			$this->updateEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
 			$valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 1023, 'UTF-8');
 			$valuesTmp['tags'] = safe_utf8($valuesTmp['tags']);
@@ -1102,7 +1111,7 @@ SQL;
 			. ($limit > 0 ? ' LIMIT ' . intval($limit) : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
 	}
 
-	public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
+	private function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
 			$order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
 		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 

+ 28 - 7
app/Models/Feed.php

@@ -39,7 +39,9 @@ class FreshRSS_Feed extends Minz_Model {
 	/** @var int */
 	private $kind = 0;
 	/** @var int */
-	private $category = 1;
+	private $categoryId = 1;
+	/** @var FreshRSS_Category|null */
+	private $category;
 	/** @var int */
 	private $nbEntries = -1;
 	/** @var int */
@@ -119,9 +121,22 @@ class FreshRSS_Feed extends Minz_Model {
 	public function hubUrl(): string {
 		return $this->hubUrl;
 	}
-	public function category(): int {
+
+	/**
+	 * @return FreshRSS_Category|null|false
+	 */
+	public function category() {
+		if ($this->category === null) {
+			$catDAO = FreshRSS_Factory::createCategoryDao();
+			$this->category = $catDAO->searchById($this->categoryId);
+		}
 		return $this->category;
 	}
+
+	public function categoryId(): int {
+		return $this->categoryId;
+	}
+
 	public function entries() {
 		Minz_Log::warning(__method__ . ' is deprecated since FreshRSS 1.16.1!');
 		$simplePie = $this->load(false, true);
@@ -253,10 +268,16 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->kind = $value;
 	}
 
-	/** @param int $value */
-	public function _category($value) {
-		$value = intval($value);
-		$this->category = $value >= 0 ? $value : 0;
+	/** @param FreshRSS_Category|null $cat */
+	public function _category($cat) {
+		$this->category = $cat;
+		$this->categoryId = $this->category == null ? 0 : $this->category->id();
+	}
+
+	/** @param int|string $id */
+	public function _categoryId($id) {
+		$this->category = null;
+		$this->categoryId = intval($id);
 	}
 
 	public function _name(string $value) {
@@ -700,7 +721,7 @@ class FreshRSS_Feed extends Minz_Model {
 		$archiving = $this->attributes('archiving');
 		if ($archiving == null) {
 			$catDAO = FreshRSS_Factory::createCategoryDao();
-			$category = $catDAO->searchById($this->category());
+			$category = $catDAO->searchById($this->categoryId);
 			$archiving = $category == null ? null : $category->attributes('archiving');
 			if ($archiving == null) {
 				$archiving = FreshRSS_Context::$user_conf->archiving;

+ 2 - 22
app/Models/FeedDAO.php

@@ -82,7 +82,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				'id' => $feed->id(),
 				'url' => $feed->url(),
 				'kind' => $feed->kind(),
-				'category' => $feed->category(),
+				'category' => $feed->categoryId(),
 				'name' => $feed->name(),
 				'website' => $feed->website(),
 				'description' => $feed->description(),
@@ -340,26 +340,6 @@ SQL;
 		return $newestItemUsec;
 	}
 
-	/**
-	 * For API
-	 */
-	public function arrayFeedCategoryNames(): array {
-		$sql = <<<'SQL'
-SELECT f.id, f.name, c.name as c_name FROM `_feed` f
-INNER JOIN `_category` c ON c.id = f.category
-SQL;
-		$stm = $this->pdo->query($sql);
-		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
-		$feedCategoryNames = array();
-		foreach ($res as $line) {
-			$feedCategoryNames[$line['id']] = array(
-				'name' => $line['name'],
-				'c_name' => $line['c_name'],
-			);
-		}
-		return $feedCategoryNames;
-	}
-
 	/**
 	 * Use $defaultCacheDuration == -1 to return all feeds, without filtering them by TTL.
 	 * @return array<FreshRSS_Feed>
@@ -596,7 +576,7 @@ SQL;
 
 			$myFeed = new FreshRSS_Feed($dao['url'] ?? '', false);
 			$myFeed->_kind($dao['kind'] ?? FreshRSS_Feed::KIND_RSS);
-			$myFeed->_category($category);
+			$myFeed->_categoryId($category);
 			$myFeed->_name($dao['name']);
 			$myFeed->_website($dao['website'] ?? '', false);
 			$myFeed->_description($dao['description'] ?? '');

+ 1 - 1
app/Models/TagDAO.php

@@ -61,7 +61,7 @@ SQL;
 			$valuesTmp['name'],
 		);
 
-		if ($stm && $stm->execute($values)) {
+		if ($stm && $stm->execute($values) && $stm->rowCount() > 0) {
 			return $this->pdo->lastInsertId('`_tag_id_seq`');
 		} else {
 			$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();

+ 0 - 2
app/Models/View.php

@@ -72,8 +72,6 @@ class FreshRSS_View extends Minz_View {
 
 	// Export / Import
 	public $content;
-	public $entriesRaw;
-	public $entriesId;
 	public $entryIdsTagNames;
 	public $list_title;
 	public $queryId;

+ 9 - 9
app/Services/ExportService.php

@@ -71,17 +71,17 @@ class FreshRSS_Export_Service {
 	 */
 	public function generateStarredEntries($type) {
 		$view = new FreshRSS_View();
-		$view->categories = $this->category_dao->listCategories();
+		$view->categories = $this->category_dao->listCategories(true);
 		$day = date('Y-m-d');
 
 		$view->list_title = _t('sub.import_export.starred_list');
 		$view->type = 'starred';
-		$view->entriesId = $this->entry_dao->listIdsWhere(
+		$entriesId = $this->entry_dao->listIdsWhere(
 			$type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1
 		);
-		$view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($view->entriesId);
+		$view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($entriesId);
 		// The following is a streamable query, i.e. must be last
-		$view->entriesRaw = $this->entry_dao->listWhereRaw(
+		$view->entries = $this->entry_dao->listWhere(
 			$type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1
 		);
 
@@ -107,19 +107,19 @@ class FreshRSS_Export_Service {
 		}
 
 		$view = new FreshRSS_View();
-		$view->categories = $this->category_dao->listCategories();
+		$view->categories = $this->category_dao->listCategories(true);
 		$view->feed = $feed;
 		$day = date('Y-m-d');
-		$filename = "feed_{$day}_" . $feed->category() . '_' . $feed->id() . '.json';
+		$filename = "feed_{$day}_" . $feed->categoryId() . '_' . $feed->id() . '.json';
 
 		$view->list_title = _t('sub.import_export.feed_list', $feed->name());
 		$view->type = 'feed/' . $feed->id();
-		$view->entriesId = $this->entry_dao->listIdsWhere(
+		$entriesId = $this->entry_dao->listIdsWhere(
 			'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $max_number_entries
 		);
-		$view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($view->entriesId);
+		$view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($entriesId);
 		// The following is a streamable query, i.e. must be last
-		$view->entriesRaw = $this->entry_dao->listWhereRaw(
+		$view->entries = $this->entry_dao->listWhere(
 			'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $max_number_entries
 		);
 

+ 1 - 1
app/Services/ImportService.php

@@ -148,7 +148,7 @@ class FreshRSS_Import_Service {
 		try {
 			// Create a Feed object and add it in DB
 			$feed = new FreshRSS_Feed($url);
-			$feed->_category($parent_cat->id());
+			$feed->_categoryId($parent_cat->id());
 			$parent_cat->addFeed($feed);
 			$feed->_name($name);
 			$feed->_website($website);

+ 9 - 40
app/views/helpers/export/articles.phtml

@@ -18,51 +18,20 @@ if (empty($this->entryIdsTagNames)) {
 	$this->entryIdsTagNames = array();
 }
 
-foreach ($this->entriesRaw as $entryRaw) {
-	if ($entryRaw == null) {
+foreach ($this->entries as $entry) {
+	if ($entry == null) {
 		continue;
 	}
-	$entry = FreshRSS_Entry::fromArray($entryRaw);
-	if (!isset($this->feed)) {
-		$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed());
-		if ($feed === null) {
-			$feed = $entry->feed(true);
-		}
-	} else {
-		$feed = $this->feed;
-	}
 
-	$article = array(
-		'id' => $entry->guid(),
-		'timestampUsec' => '' . $entry->id(),
-		'categories' => array_values($entry->tags()),
-		'title' => $entry->title(),
-		'author' => $entry->authors(true),
-		'published' => $entry->date(true),
-		'updated' => $entry->date(true),
-		'alternate' => array(array(
-			'href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES),
-			'type' => 'text/html',
-		)),
-		'content' => array(
-			'content' => $entry->content(),
-		),
-		'origin' => array(
-			'streamId' => $feed == null ? '' : $feed->id(),
-			'title' => $feed == null ? '' : $feed->name(),
-			'htmlUrl' => $feed == null ? '' : $feed->website(),
-			'feedUrl' => $feed == null ? '' : $feed->url(),
-		)
-	);
-	$article['categories'][] = $entry->isRead() ? 'user/-/state/com.google/read' : 'user/-/state/com.google/unread';
-	if ($entry->isFavorite()) {
-		$article['categories'][] = 'user/-/state/com.google/starred';
-	}
-	$tagNames = isset($this->entryIdsTagNames['e_' . $entry->id()]) ? $this->entryIdsTagNames['e_' . $entry->id()] : array();
-	foreach ($tagNames as $tagName) {
-		$article['categories'][] = 'user/-/label/' . $tagName;
+	$feed = $this->feed ?? FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feedId());
+	$entry->_feed($feed);
+
+	if (isset($this->entryIdsTagNames['e_' . $entry->id()])) {
+		$entry->_tags($this->entryIdsTagNames['e_' . $entry->id()]);
 	}
 
+	$article = $entry->toGReader('freshrss');
+
 	$line = json_encode($article, $options);
 	if ($line != '') {
 		if ($first) {

+ 1 - 1
app/views/helpers/feed/update.phtml

@@ -69,7 +69,7 @@
 			<div class="group-controls">
 				<select name="category" id="category" class="w100">
 				<?php foreach ($this->categories as $cat) { ?>
-				<option value="<?= $cat->id() ?>"<?= $cat->id() == $this->feed->category() ? ' selected="selected"' : '' ?>>
+				<option value="<?= $cat->id() ?>"<?= $cat->id() == $this->feed->categoryId() ? ' selected="selected"' : '' ?>>
 					<?= $cat->name() ?>
 				</option>
 				<?php } ?>

+ 2 - 2
app/views/index/normal.phtml

@@ -37,9 +37,9 @@ $today = @strtotime('today');
 		$this->entry = $item;
 
 		// We most likely already have the feed object in cache
-		$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feed());
+		$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feedId());
 		if ($this->feed == null) {
-			$this->feed = $this->entry->feed(true);
+			$this->feed = $this->entry->feed();
 			if ($this->feed == null) {
 				$this->feed = FreshRSS_Feed::example();
 			}

+ 4 - 4
app/views/index/reader.phtml

@@ -47,12 +47,12 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
 		}
 	?><div class="flux<?= !$item->isRead() ? ' not_read' : '' ?><?= $item->isFavorite() ? ' favorite' : '' ?>" id="flux_<?= $item->id() ?>">
 		<article class="flux_content" dir="auto">
-		
+
 			<div class="content <?= $content_width ?>">
 				<header>
 					<?php
-						$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feed());	//We most likely already have the feed object in cache
-						if (empty($feed)) $feed = $item->feed(true);
+						$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feedId());	//We most likely already have the feed object in cache
+						if ($feed == null) $feed = $item->feed();
 						$favoriteUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id()));
 						if ($item->isFavorite()) {
 							$favoriteUrl['params']['is_favorite'] = 0;
@@ -134,7 +134,7 @@ $MAX_TAGS_DISPLAYED = FreshRSS_Context::$user_conf->show_tags_max;
 						</div>
 					<?php } ?>
 				</header>
-				
+
 				<div class="text">
 					<?= $item->content() ?>
 				</div>

+ 2 - 2
p/api/fever.php

@@ -365,7 +365,7 @@ class FeverAPI
 
 		/** @var FreshRSS_Feed $feed */
 		foreach ($myFeeds as $feed) {
-			$ids[$feed->category()][] = $feed->id();
+			$ids[$feed->categoryId()][] = $feed->id();
 		}
 
 		foreach($ids as $category => $feedIds) {
@@ -493,7 +493,7 @@ class FeverAPI
 			}
 			$items[] = array(
 				'id' => '' . $entry->id(),
-				'feed_id' => $entry->feed(false),
+				'feed_id' => $entry->feedId(),
 				'title' => escapeToUnicodeAlternative($entry->title(), false),
 				'author' => escapeToUnicodeAlternative(trim($entry->authors(true), '; '), false),
 				'html' => $entry->content(),

+ 11 - 75
p/api/greader.php

@@ -29,13 +29,6 @@ require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
 $ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
 
 if (PHP_INT_SIZE < 8) {	//32-bit
-	/**
-	 * @param string|int $dec
-	 * @return string
-	 */
-	function dec2hex($dec) {
-		return str_pad(gmp_strval(gmp_init($dec, 10), 16), 16, '0', STR_PAD_LEFT);
-	}
 	/**
 	 * @param string $hex
 	 * @return string
@@ -45,14 +38,6 @@ if (PHP_INT_SIZE < 8) {	//32-bit
 		return gmp_strval(gmp_init($hex, 16), 10);
 	}
 } else {	//64-bit
-	/**
-	 * @param string|int $dec
-	 * @return string
-	 */
-	function dec2hex($dec) {
-		//http://code.google.com/p/google-reader-api/wiki/ItemId
-		return str_pad(dechex($dec), 16, '0', STR_PAD_LEFT);
-	}
 	/**
 	 * @param string $hex
 	 * @return string
@@ -526,8 +511,9 @@ function entriesToArray($entries) {
 	if (empty($entries)) {
 		return array();
 	}
-	$feedDAO = FreshRSS_Factory::createFeedDao();
-	$arrayFeedCategoryNames = $feedDAO->arrayFeedCategoryNames();
+	$catDAO = FreshRSS_Factory::createCategoryDao();
+	$categories = $catDAO->listCategories(true);
+
 	$tagDAO = FreshRSS_Factory::createTagDao();
 	$entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries);
 	if ($entryIdsTagNames == false) {
@@ -541,65 +527,15 @@ function entriesToArray($entries) {
 		if ($entry == null) {
 			continue;
 		}
-		$f_id = $entry->feed();
-		if (isset($arrayFeedCategoryNames[$f_id])) {
-			$c_name = $arrayFeedCategoryNames[$f_id]['c_name'];
-			$f_name = $arrayFeedCategoryNames[$f_id]['name'];
-		} else {
-			$c_name = '_';
-			$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
-			'crawlTimeMsec' => substr($entry->dateAdded(true, true), 0, -3),
-			'timestampUsec' => '' . $entry->dateAdded(true, true), //EasyRSS & Reeder
-			'published' => $entry->date(true),
-			'title' => escapeToUnicodeAlternative($entry->title(), false),
-			'summary' => array('content' => $entry->content()),
-			'canonical' => array(
-				array('href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES)),
-			),
-			'alternate' => array(
-				array('href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES)),
-			),
-			'categories' => array(
-				'user/-/state/com.google/reading-list',
-				'user/-/label/' . htmlspecialchars_decode($c_name, ENT_QUOTES),
-			),
-			'origin' => array(
-				'streamId' => 'feed/' . $f_id,
-				'title' => escapeToUnicodeAlternative($f_name, true),	//EasyRSS
-				//'htmlUrl' => $line['f_website'],
-			),
-		);
-		foreach ($entry->enclosures() as $enclosure) {
-			if (!empty($enclosure['url']) && !empty($enclosure['type'])) {
-				$media = [
-						'href' => $enclosure['url'],
-						'type' => $enclosure['type'],
-					];
-				if (!empty($enclosure['length'])) {
-					$media['length'] = intval($enclosure['length']);
-				}
-				$item['enclosure'][] = $media;
-			}
-		}
-		$author = $entry->authors(true);
-		$author = trim($author, '; ');
-		if ($author != '') {
-			$item['author'] = escapeToUnicodeAlternative($author, false);
-		}
-		if ($entry->isRead()) {
-			$item['categories'][] = 'user/-/state/com.google/read';
-		}
-		if ($entry->isFavorite()) {
-			$item['categories'][] = 'user/-/state/com.google/starred';
-		}
-		$tagNames = isset($entryIdsTagNames['e_' . $entry->id()]) ? $entryIdsTagNames['e_' . $entry->id()] : array();
-		foreach ($tagNames as $tagName) {
-			$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($tagName, ENT_QUOTES);
+
+		$feed = FreshRSS_CategoryDAO::findFeed($categories, $entry->feedId());
+		$entry->_feed($feed);
+
+		if (isset($entryIdsTagNames['e_' . $entry->id()])) {
+			$entry->_tags($entryIdsTagNames['e_' . $entry->id()]);
 		}
-		$items[] = $item;
+
+		$items[] = $entry->toGReader('compat');
 	}
 	return $items;
 }