* Add feed widget * add feed render * Add CommentPage widget * New theme (#1390) * 调整忽略目录 * add theme * fix theme scss build Co-authored-by: fen <f3nb0x@gmail.com> * s/is_writeable/is_writable/g * New upgrade method * merge new fixes from master * add pgsql ssl mode support (ref #1600) (#1623) * Feat/code refactor (#1626) * remove all magic methods, add type for class properties * refactor codes * fix all * refactor code * fix type * fix all * fix request is method * fix all * fix router * fix get page * fix 1.3.0 upgrade * [feat] support high resolution avatar * fix types in i18n component * Implement Ctrl+S or Command+S for save draft (#1628) * Implement Ctrl+S or Command+S for save draft * rename * add Typecho.savePost * fix upload file size * add new uploader * replace new uploader * fix textarea change * fix preview * refactor post edit * fix issue * fix page edit --------- Co-authored-by: joyqi <joyqi@segmentfault.com> Co-authored-by: joyqi <magike.net@gmail.com> * fix #1632 * Add svg to image types * Feat/tree pages (#1646) * add tree trait * finish category tree trait * support select fields * fix select fields * refactor admin trait * fix draft status * Add new contents type "revision" * minor refactor * add more tree view abstracts * add tree trait to pages * get ready for tree view pages * improve page edit * fix revision * fix slug * add router params delegate * fix params delegate * fix * fix * fix all * fix all * fix tree * fix page link * fix feed * fix page * fix permalink * fix permalink input * fix offset query * Support IDN (#1629) * Support IDN * use js * Optimize code * Optimize code * fix URL script * remove unnecessary use --------- Co-authored-by: joyqi <joyqi@segmentfault.com> * fix input element * fix #1651, close #1653 * Use json instead of serialize (#1624) * Use json instead of serialize * Fix Upgrade code * add tree trait * finish category tree trait * support select fields * fix select fields * refactor admin trait * fix draft status * Add new contents type "revision" * minor refactor * add more tree view abstracts * add tree trait to pages * get ready for tree view pages * improve page edit * fix revision * fix slug * add router params delegate * fix params delegate * fix * fix * fix all * fix all * fix tree * fix page link * fix feed * fix page * fix permalink * fix permalink input * fix offset query * Fix typo * remove proxy methods * remove unnecessary useage --------- Co-authored-by: joyqi <joyqi@segmentfault.com> Co-authored-by: joyqi <magike.net@gmail.com> * Fix Prevent XSS vulnerability in default theme (#1654) * Fix Prevent XSS vulnerability in default theme * Update var/Typecho/Db/Adapter/Pdo.php * fix the getter --------- Co-authored-by: joyqi <joyqi@segmentfault.com> * add throwCallback to widget response * fix: cut down fields when selecting recent posts * fix typo errors * fix typo errors * fix http client cookie * add throw finish * fix theme lang * fix default theme * fix query * add open graph and twitter card support add canonical link * fix canonical link meta * fix theme classic-22 * remove unnecessary scss file when packaging * init plugin signal * improve: remove feather-icon js file * fix: typo * improve: post detail layout * fix tags saving * improve: nav search * fix: theme screenshot * fix: theme page layout * remove php 7.2/7.3 env --------- Co-authored-by: fen <f3nb0x@gmail.com> Co-authored-by: Lu Fei <52o@qq52o.cn>
413 lines
12 KiB
PHP
413 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Typecho;
|
|
|
|
/**
|
|
* Feed
|
|
*
|
|
* @package Feed
|
|
*/
|
|
class Feed
|
|
{
|
|
/** 定义RSS 1.0类型 */
|
|
public const RSS1 = 'RSS 1.0';
|
|
|
|
/** 定义RSS 2.0类型 */
|
|
public const RSS2 = 'RSS 2.0';
|
|
|
|
/** 定义ATOM 1.0类型 */
|
|
public const ATOM1 = 'ATOM 1.0';
|
|
|
|
/** 定义RSS时间格式 */
|
|
public const DATE_RFC822 = 'r';
|
|
|
|
/** 定义ATOM时间格式 */
|
|
public const DATE_W3CDTF = 'c';
|
|
|
|
/** 定义行结束符 */
|
|
public const EOL = "\n";
|
|
|
|
/**
|
|
* feed状态
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $type;
|
|
|
|
/**
|
|
* 字符集编码
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $charset;
|
|
|
|
/**
|
|
* 语言状态
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $lang;
|
|
|
|
/**
|
|
* 聚合地址
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $feedUrl;
|
|
|
|
/**
|
|
* 基本地址
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $baseUrl;
|
|
|
|
/**
|
|
* 聚合标题
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $title;
|
|
|
|
/**
|
|
* 聚合副标题
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $subTitle;
|
|
|
|
/**
|
|
* 版本信息
|
|
*
|
|
* @access private
|
|
* @var string
|
|
*/
|
|
private string $version;
|
|
|
|
/**
|
|
* 所有的items
|
|
*
|
|
* @access private
|
|
* @var array
|
|
*/
|
|
private array $items = [];
|
|
|
|
/**
|
|
* 创建Feed对象
|
|
*
|
|
* @param $version
|
|
* @param string $type
|
|
* @param string $charset
|
|
* @param string $lang
|
|
*/
|
|
public function __construct($version, string $type = self::RSS2, string $charset = 'UTF-8', string $lang = 'en')
|
|
{
|
|
$this->version = $version;
|
|
$this->type = $type;
|
|
$this->charset = $charset;
|
|
$this->lang = $lang;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getType(): string
|
|
{
|
|
return $this->type;
|
|
}
|
|
|
|
/**
|
|
* 设置标题
|
|
*
|
|
* @param string $title 标题
|
|
*/
|
|
public function setTitle(string $title)
|
|
{
|
|
$this->title = $title;
|
|
}
|
|
|
|
/**
|
|
* 设置副标题
|
|
*
|
|
* @param string|null $subTitle 副标题
|
|
*/
|
|
public function setSubTitle(?string $subTitle)
|
|
{
|
|
$this->subTitle = $subTitle;
|
|
}
|
|
|
|
/**
|
|
* 设置聚合地址
|
|
*
|
|
* @param string $feedUrl 聚合地址
|
|
*/
|
|
public function setFeedUrl(string $feedUrl)
|
|
{
|
|
$this->feedUrl = $feedUrl;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getFeedUrl(): string
|
|
{
|
|
return $this->feedUrl;
|
|
}
|
|
|
|
/**
|
|
* 设置主页
|
|
*
|
|
* @param string $baseUrl 主页地址
|
|
*/
|
|
public function setBaseUrl(string $baseUrl)
|
|
{
|
|
$this->baseUrl = $baseUrl;
|
|
}
|
|
|
|
/**
|
|
* $item的格式为
|
|
* <code>
|
|
* array (
|
|
* 'title' => 'xxx',
|
|
* 'content' => 'xxx',
|
|
* 'excerpt' => 'xxx',
|
|
* 'date' => 'xxx',
|
|
* 'link' => 'xxx',
|
|
* 'author' => 'xxx',
|
|
* 'comments' => 'xxx',
|
|
* 'commentsUrl'=> 'xxx',
|
|
* 'commentsFeedUrl' => 'xxx',
|
|
* )
|
|
* </code>
|
|
*
|
|
* @param array $item
|
|
*/
|
|
public function addItem(array $item)
|
|
{
|
|
$this->items[] = $item;
|
|
}
|
|
|
|
/**
|
|
* 输出字符串
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString(): string
|
|
{
|
|
$result = '<?xml version="1.0" encoding="' . $this->charset . '"?>' . self::EOL;
|
|
|
|
if (self::RSS1 == $this->type) {
|
|
$result .= '<rdf:RDF
|
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
xmlns="http://purl.org/rss/1.0/"
|
|
xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;
|
|
|
|
$content = '';
|
|
$links = [];
|
|
$lastUpdate = 0;
|
|
|
|
foreach ($this->items as $item) {
|
|
$content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL;
|
|
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
|
|
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
|
|
$content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL;
|
|
$content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL;
|
|
if (!empty($item['suffix'])) {
|
|
$content .= $item['suffix'];
|
|
}
|
|
$content .= '</item>' . self::EOL;
|
|
|
|
$links[] = $item['link'];
|
|
|
|
if ($item['date'] > $lastUpdate) {
|
|
$lastUpdate = $item['date'];
|
|
}
|
|
}
|
|
|
|
$result .= '<channel rdf:about="' . $this->feedUrl . '">
|
|
<title>' . htmlspecialchars($this->title) . '</title>
|
|
<link>' . $this->baseUrl . '</link>
|
|
<description>' . htmlspecialchars($this->subTitle ?? '') . '</description>
|
|
<items>
|
|
<rdf:Seq>' . self::EOL;
|
|
|
|
foreach ($links as $link) {
|
|
$result .= '<rdf:li resource="' . $link . '"/>' . self::EOL;
|
|
}
|
|
|
|
$result .= '</rdf:Seq>
|
|
</items>
|
|
</channel>' . self::EOL;
|
|
|
|
$result .= $content . '</rdf:RDF>';
|
|
} elseif (self::RSS2 == $this->type) {
|
|
$result .= '<rss version="2.0"
|
|
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
|
|
<channel>' . self::EOL;
|
|
|
|
$content = '';
|
|
$lastUpdate = 0;
|
|
|
|
foreach ($this->items as $item) {
|
|
$content .= '<item>' . self::EOL;
|
|
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
|
|
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
|
|
$content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
|
|
$content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
|
|
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName)
|
|
. '</dc:creator>' . self::EOL;
|
|
|
|
if (!empty($item['category']) && is_array($item['category'])) {
|
|
foreach ($item['category'] as $category) {
|
|
$content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . self::EOL;
|
|
}
|
|
}
|
|
|
|
if (!empty($item['excerpt'])) {
|
|
$content .= '<description><![CDATA[' . strip_tags($item['excerpt'])
|
|
. ']]></description>' . self::EOL;
|
|
}
|
|
|
|
if (!empty($item['content'])) {
|
|
$content .= '<content:encoded xml:lang="' . $this->lang . '"><![CDATA['
|
|
. self::EOL .
|
|
$item['content'] . self::EOL .
|
|
']]></content:encoded>' . self::EOL;
|
|
}
|
|
|
|
if (isset($item['comments']) && strlen($item['comments']) > 0) {
|
|
$content .= '<slash:comments>' . $item['comments'] . '</slash:comments>' . self::EOL;
|
|
}
|
|
|
|
$content .= '<comments>' . $item['link'] . '#comments</comments>' . self::EOL;
|
|
if (!empty($item['commentsFeedUrl'])) {
|
|
$content .= '<wfw:commentRss>' . $item['commentsFeedUrl'] . '</wfw:commentRss>' . self::EOL;
|
|
}
|
|
|
|
if (!empty($item['suffix'])) {
|
|
$content .= $item['suffix'];
|
|
}
|
|
|
|
$content .= '</item>' . self::EOL;
|
|
|
|
if ($item['date'] > $lastUpdate) {
|
|
$lastUpdate = $item['date'];
|
|
}
|
|
}
|
|
|
|
$result .= '<title>' . htmlspecialchars($this->title) . '</title>
|
|
<link>' . $this->baseUrl . '</link>
|
|
<atom:link href="' . $this->feedUrl . '" rel="self" type="application/rss+xml" />
|
|
<language>' . $this->lang . '</language>
|
|
<description>' . htmlspecialchars($this->subTitle ?? '') . '</description>
|
|
<lastBuildDate>' . $this->dateFormat($lastUpdate) . '</lastBuildDate>
|
|
<pubDate>' . $this->dateFormat($lastUpdate) . '</pubDate>' . self::EOL;
|
|
|
|
$result .= $content . '</channel>
|
|
</rss>';
|
|
} elseif (self::ATOM1 == $this->type) {
|
|
$result .= '<feed xmlns="http://www.w3.org/2005/Atom"
|
|
xmlns:thr="http://purl.org/syndication/thread/1.0"
|
|
xml:lang="' . $this->lang . '"
|
|
xml:base="' . $this->baseUrl . '"
|
|
>' . self::EOL;
|
|
|
|
$content = '';
|
|
$lastUpdate = 0;
|
|
|
|
foreach ($this->items as $item) {
|
|
$content .= '<entry>' . self::EOL;
|
|
$content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
|
|
$content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
|
|
$content .= '<id>' . $item['link'] . '</id>' . self::EOL;
|
|
$content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
|
|
$content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
|
|
$content .= '<author>
|
|
<name>' . $item['author']->screenName . '</name>
|
|
<uri>' . $item['author']->url . '</uri>
|
|
</author>' . self::EOL;
|
|
|
|
if (!empty($item['category']) && is_array($item['category'])) {
|
|
foreach ($item['category'] as $category) {
|
|
$content .= '<category scheme="' . $category['permalink'] . '" term="'
|
|
. $category['name'] . '" />' . self::EOL;
|
|
}
|
|
}
|
|
|
|
if (!empty($item['excerpt'])) {
|
|
$content .= '<summary type="html"><![CDATA[' . htmlspecialchars($item['excerpt'])
|
|
. ']]></summary>' . self::EOL;
|
|
}
|
|
|
|
if (!empty($item['content'])) {
|
|
$content .= '<content type="html" xml:base="' . $item['link']
|
|
. '" xml:lang="' . $this->lang . '"><![CDATA['
|
|
. self::EOL .
|
|
$item['content'] . self::EOL .
|
|
']]></content>' . self::EOL;
|
|
}
|
|
|
|
if (isset($item['comments']) && strlen($item['comments']) > 0) {
|
|
$content .= '<link rel="replies" type="text/html" href="' . $item['link']
|
|
. '#comments" thr:count="' . $item['comments'] . '" />' . self::EOL;
|
|
|
|
if (!empty($item['commentsFeedUrl'])) {
|
|
$content .= '<link rel="replies" type="application/atom+xml" href="'
|
|
. $item['commentsFeedUrl'] . '" thr:count="' . $item['comments'] . '"/>' . self::EOL;
|
|
}
|
|
}
|
|
|
|
if (!empty($item['suffix'])) {
|
|
$content .= $item['suffix'];
|
|
}
|
|
|
|
$content .= '</entry>' . self::EOL;
|
|
|
|
if ($item['date'] > $lastUpdate) {
|
|
$lastUpdate = $item['date'];
|
|
}
|
|
}
|
|
|
|
$result .= '<title type="text">' . htmlspecialchars($this->title) . '</title>
|
|
<subtitle type="text">' . htmlspecialchars($this->subTitle ?? '') . '</subtitle>
|
|
<updated>' . $this->dateFormat($lastUpdate) . '</updated>
|
|
<generator uri="https://typecho.org/" version="' . $this->version . '">Typecho</generator>
|
|
<link rel="alternate" type="text/html" href="' . $this->baseUrl . '" />
|
|
<id>' . $this->feedUrl . '</id>
|
|
<link rel="self" type="application/atom+xml" href="' . $this->feedUrl . '" />
|
|
';
|
|
$result .= $content . '</feed>';
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 获取Feed时间格式
|
|
*
|
|
* @param integer $stamp 时间戳
|
|
* @return string
|
|
*/
|
|
public function dateFormat(int $stamp): string
|
|
{
|
|
if (self::RSS2 == $this->type) {
|
|
return date(self::DATE_RFC822, $stamp);
|
|
} elseif (self::RSS1 == $this->type || self::ATOM1 == $this->type) {
|
|
return date(self::DATE_W3CDTF, $stamp);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
}
|