This commit is contained in:
root
2026-03-04 00:23:03 +08:00
commit 6136d791f2
611 changed files with 65539 additions and 0 deletions

78
var/Widget/Action.php Executable file
View File

@@ -0,0 +1,78 @@
<?php
namespace Widget;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 执行模块
*
* @package Widget
*/
class Action extends Widget
{
/**
* 路由映射
*
* @access private
* @var array
*/
private array $map = [
'ajax' => '\Widget\Ajax',
'login' => '\Widget\Login',
'logout' => '\Widget\Logout',
'register' => '\Widget\Register',
'upgrade' => '\Widget\Upgrade',
'upload' => '\Widget\Upload',
'service' => '\Widget\Service',
'xmlrpc' => '\Widget\XmlRpc',
'comments-edit' => '\Widget\Comments\Edit',
'contents-page-edit' => '\Widget\Contents\Page\Edit',
'contents-post-edit' => '\Widget\Contents\Post\Edit',
'contents-attachment-edit' => '\Widget\Contents\Attachment\Edit',
'metas-category-edit' => '\Widget\Metas\Category\Edit',
'metas-tag-edit' => '\Widget\Metas\Tag\Edit',
'options-discussion' => '\Widget\Options\Discussion',
'options-general' => '\Widget\Options\General',
'options-permalink' => '\Widget\Options\Permalink',
'options-reading' => '\Widget\Options\Reading',
'plugins-edit' => '\Widget\Plugins\Edit',
'themes-edit' => '\Widget\Themes\Edit',
'users-edit' => '\Widget\Users\Edit',
'users-profile' => '\Widget\Users\Profile',
'backup' => '\Widget\Backup'
];
/**
* 入口函数,初始化路由器
*
* @throws Widget\Exception
*/
public function execute()
{
/** 验证路由地址 **/
$action = $this->request->get('action');
/** 判断是否为plugin */
$actionTable = array_merge($this->map, Options::alloc()->actionTable);
if (isset($actionTable[$action])) {
$widgetName = $actionTable[$action];
}
if (isset($widgetName) && class_exists($widgetName)) {
$widget = self::widget($widgetName);
if ($widget instanceof ActionInterface) {
$widget->action();
return;
}
}
throw new Widget\Exception(_t('请求的地址不存在'), 404);
}
}

14
var/Widget/ActionInterface.php Executable file
View File

@@ -0,0 +1,14 @@
<?php
namespace Widget;
/**
* 可以被Widget\Action调用的接口
*/
interface ActionInterface
{
/**
* 接口需要实现的入口函数
*/
public function action();
}

166
var/Widget/Ajax.php Executable file
View File

@@ -0,0 +1,166 @@
<?php
namespace Widget;
use Typecho\Http\Client;
use Typecho\Widget\Exception;
use Widget\Base\Options as BaseOptions;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 异步调用组件
*
* @author qining
* @category typecho
* @package Widget
*/
class Ajax extends BaseOptions implements ActionInterface
{
/**
* 针对rewrite验证的请求返回
*
* @access public
* @return void
*/
public function remoteCallback()
{
if ($this->options->generator == $this->request->getAgent()) {
echo 'OK';
}
}
/**
* 获取最新版本
*
* @throws Exception|\Typecho\Db\Exception
*/
public function checkVersion()
{
$this->user->pass('editor');
$client = Client::get();
$result = ['available' => 0];
if ($client) {
$client->setHeader('User-Agent', $this->options->generator)
->setTimeout(10);
try {
$client->send('https://typecho.org/version.json');
/** 匹配内容体 */
$response = $client->getResponseBody();
$json = json_decode($response, true);
if (!empty($json)) {
$version = $this->options->version;
if (
isset($json['release'])
&& preg_match("/^[0-9.]+$/", $json['release'])
&& version_compare($json['release'], $version, '>')
) {
$result = [
'available' => 1,
'latest' => $json['release'],
'current' => $version,
'link' => 'https://typecho.org/download'
];
}
}
} catch (\Exception $e) {
// do nothing
}
}
$this->response->throwJson($result);
}
/**
* 远程请求代理
*
* @throws Exception
* @throws Client\Exception|\Typecho\Db\Exception
*/
public function feed()
{
$this->user->pass('subscriber');
$client = Client::get();
$data = [];
if ($client) {
$client->setHeader('User-Agent', $this->options->generator)
->setTimeout(10)
->send('https://typecho.org/feed/');
/** 匹配内容体 */
$response = $client->getResponseBody();
preg_match_all(
"/<item>\s*<title>([^>]*)<\/title>\s*<link>([^>]*)<\/link>\s*<guid>[^>]*<\/guid>\s*<pubDate>([^>]*)<\/pubDate>/i",
$response,
$matches
);
if ($matches) {
foreach ($matches[0] as $key => $val) {
$data[] = [
'title' => $matches[1][$key],
'link' => $matches[2][$key],
'date' => date('n.j', strtotime($matches[3][$key]))
];
if ($key > 8) {
break;
}
}
}
}
$this->response->throwJson($data);
}
/**
* 自定义编辑器大小
*
* @throws \Typecho\Db\Exception|Exception
*/
public function editorResize()
{
$this->user->pass('contributor');
$size = $this->request->filter('int')->get('size');
if (
$this->db->fetchObject($this->db->select(['COUNT(*)' => 'num'])
->from('table.options')->where('name = ? AND user = ?', 'editorSize', $this->user->uid))->num > 0
) {
parent::update(
['value' => $size],
$this->db->sql()->where('name = ? AND user = ?', 'editorSize', $this->user->uid)
);
} else {
parent::insert([
'name' => 'editorSize',
'value' => $size,
'user' => $this->user->uid
]);
}
}
/**
* 异步请求入口
*
* @access public
* @return void
*/
public function action()
{
if (!$this->request->isAjax()) {
$this->response->goBack();
}
$this->on($this->request->is('do=remoteCallback'))->remoteCallback();
$this->on($this->request->is('do=feed'))->feed();
$this->on($this->request->is('do=checkVersion'))->checkVersion();
$this->on($this->request->is('do=editorResize'))->editorResize();
}
}

2139
var/Widget/Archive.php Executable file

File diff suppressed because it is too large Load Diff

383
var/Widget/Backup.php Executable file
View File

@@ -0,0 +1,383 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Exception;
use Typecho\Plugin;
use Widget\Base\Options as BaseOptions;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 备份工具
*
* @package Widget
*/
class Backup extends BaseOptions implements ActionInterface
{
public const HEADER = '%TYPECHO_BACKUP_XXXX%';
public const HEADER_VERSION = '0001';
/**
* @var array
*/
private array $types = [
'contents' => 1,
'comments' => 2,
'metas' => 3,
'relationships' => 4,
'users' => 5,
'fields' => 6
];
/**
* @var array
*/
private array $fields = [
'contents' => [
'cid', 'title', 'slug', 'created', 'modified', 'text', 'order', 'authorId',
'template', 'type', 'status', 'password', 'commentsNum', 'allowComment', 'allowPing', 'allowFeed', 'parent'
],
'comments' => [
'coid', 'cid', 'created', 'author', 'authorId', 'ownerId',
'mail', 'url', 'ip', 'agent', 'text', 'type', 'status', 'parent'
],
'metas' => [
'mid', 'name', 'slug', 'type', 'description', 'count', 'order', 'parent'
],
'relationships' => ['cid', 'mid'],
'users' => [
'uid', 'name', 'password', 'mail', 'url', 'screenName',
'created', 'activated', 'logged', 'group', 'authCode'
],
'fields' => [
'cid', 'name', 'type', 'str_value', 'int_value', 'float_value'
]
];
/**
* @var array
*/
private array $lastIds = [];
/**
* @var array
*/
private array $cleared = [];
/**
* @var bool
*/
private bool $login = false;
/**
* 列出已有备份文件
*
* @return array
*/
public function listFiles(): array
{
return array_map('basename', glob(__TYPECHO_BACKUP_DIR__ . '/*.dat'));
}
/**
* 绑定动作
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->is('do=export'))->export();
$this->on($this->request->is('do=import'))->import();
}
/**
* 导出数据
*
* @throws \Typecho\Db\Exception
*/
private function export()
{
$backupFile = tempnam(sys_get_temp_dir(), 'backup_');
$fp = fopen($backupFile, 'wb');
$host = parse_url($this->options->siteUrl, PHP_URL_HOST);
$this->response->setContentType('application/octet-stream');
$this->response->setHeader('Content-Disposition', 'attachment; filename="'
. date('Ymd') . '_' . $host . '_' . uniqid() . '.dat"');
$header = str_replace('XXXX', self::HEADER_VERSION, self::HEADER);
fwrite($fp, $header);
$db = $this->db;
foreach ($this->types as $type => $val) {
$page = 1;
do {
$rows = $db->fetchAll($db->select()->from('table.' . $type)->page($page, 20));
$page++;
foreach ($rows as $row) {
fwrite($fp, $this->buildBuffer($val, $this->applyFields($type, $row)));
}
} while (count($rows) == 20);
}
self::pluginHandle()->call('export', $fp);
fwrite($fp, $header);
fclose($fp);
$this->response->throwFile($backupFile, 'application/octet-stream');
}
/**
* @param $type
* @param $data
* @return string
*/
private function buildBuffer($type, $data): string
{
$body = '';
$schema = [];
foreach ($data as $key => $val) {
$schema[$key] = null === $val ? null : strlen($val);
$body .= $val;
}
$header = json_encode($schema);
return Common::buildBackupBuffer($type, $header, $body);
}
/**
* 过滤字段
*
* @param $table
* @param $data
* @return array
*/
private function applyFields($table, $data): array
{
$result = [];
foreach ($data as $key => $val) {
$index = array_search($key, $this->fields[$table]);
if ($index !== false) {
$result[$key] = $val;
if ($index === 0 && !in_array($table, ['relationships', 'fields'])) {
$this->lastIds[$table] = isset($this->lastIds[$table])
? max($this->lastIds[$table], $val) : $val;
}
}
}
return $result;
}
/**
* 导入数据
*/
private function import()
{
$path = null;
if (!empty($_FILES)) {
$file = array_pop($_FILES);
if(UPLOAD_ERR_NO_FILE == $file['error']) {
Notice::alloc()->set(_t('没有选择任何备份文件'), 'error');
$this->response->goBack();
}
if (UPLOAD_ERR_OK == $file['error'] && is_uploaded_file($file['tmp_name'])) {
$path = $file['tmp_name'];
} else {
Notice::alloc()->set(_t('备份文件上传失败'), 'error');
$this->response->goBack();
}
} else {
if (!$this->request->is('file')) {
Notice::alloc()->set(_t('没有选择任何备份文件'), 'error');
$this->response->goBack();
}
$path = __TYPECHO_BACKUP_DIR__ . '/' . $this->request->get('file');
if (!file_exists($path)) {
Notice::alloc()->set(_t('备份文件不存在'), 'error');
$this->response->goBack();
}
}
$this->extractData($path);
}
/**
* 解析数据
*
* @param $file
* @throws \Typecho\Db\Exception
*/
private function extractData($file)
{
$fp = @fopen($file, 'rb');
if (!$fp) {
Notice::alloc()->set(_t('无法读取备份文件'), 'error');
$this->response->goBack();
}
$fileSize = filesize($file);
$headerSize = strlen(self::HEADER);
if ($fileSize < $headerSize) {
@fclose($fp);
Notice::alloc()->set(_t('备份文件格式错误'), 'error');
$this->response->goBack();
}
$fileHeader = @fread($fp, $headerSize);
if (!$this->parseHeader($fileHeader, $version)) {
@fclose($fp);
Notice::alloc()->set(_t('备份文件格式错误'), 'error');
$this->response->goBack();
}
fseek($fp, $fileSize - $headerSize);
$fileFooter = @fread($fp, $headerSize);
if (!$this->parseHeader($fileFooter, $version)) {
@fclose($fp);
Notice::alloc()->set(_t('备份文件格式错误'), 'error');
$this->response->goBack();
}
fseek($fp, $headerSize);
$offset = $headerSize;
while (!feof($fp) && $offset + $headerSize < $fileSize) {
$data = Common::extractBackupBuffer($fp, $offset, $version);
if (!$data) {
@fclose($fp);
Notice::alloc()->set(_t('恢复数据出现错误'), 'error');
$this->response->goBack();
}
[$type, $header, $body] = $data;
$this->processData($type, $header, $body);
}
// 针对PGSQL重置计数
if (false !== strpos(strtolower($this->db->getAdapterName()), 'pgsql')) {
foreach ($this->lastIds as $table => $id) {
$seq = $this->db->getPrefix() . $table . '_seq';
$this->db->query('ALTER SEQUENCE ' . $seq . ' RESTART WITH ' . ($id + 1));
}
}
@fclose($fp);
Notice::alloc()->set(_t('数据恢复完成'), 'success');
$this->response->goBack();
}
/**
* @param $str
* @param $version
* @return bool
*/
private function parseHeader($str, &$version): bool
{
if (!$str || strlen($str) != strlen(self::HEADER)) {
return false;
}
if (!preg_match("/%TYPECHO_BACKUP_[A-Z0-9]{4}%/", $str)) {
return false;
}
$version = substr($str, 16, - 1);
return true;
}
/**
* @param $type
* @param $header
* @param $body
*/
private function processData($type, $header, $body)
{
$table = array_search($type, $this->types);
if (!empty($table)) {
$schema = json_decode($header, true);
$data = [];
$offset = 0;
foreach ($schema as $key => $val) {
$data[$key] = null === $val ? null : substr($body, $offset, $val);
$offset += $val;
}
$this->importData($table, $data);
} else {
self::pluginHandle()->import($type, $header, $body);
}
}
/**
* 导入单条数据
*
* @param $table
* @param $data
*/
private function importData($table, $data)
{
$db = $this->db;
try {
if (empty($this->cleared[$table])) {
// 清除数据
$db->truncate('table.' . $table);
$this->cleared[$table] = true;
}
if (!$this->login && 'users' == $table && $data['group'] == 'administrator') {
// 重新登录
$this->reLogin($data);
}
$db->query($db->insert('table.' . $table)->rows($this->applyFields($table, $data)));
} catch (Exception $e) {
Notice::alloc()->set(_t('恢复过程中遇到如下错误: %s', $e->getMessage()), 'error');
$this->response->goBack();
}
}
/**
* 备份过程会重写用户数据
* 所以需要重新登录当前用户
*
* @param $user
*/
private function reLogin(&$user)
{
if (empty($user['authCode'])) {
$user['authCode'] = function_exists('openssl_random_pseudo_bytes') ?
bin2hex(openssl_random_pseudo_bytes(16)) : sha1(Common::randString(20));
}
$user['activated'] = $this->options->time;
$user['logged'] = $user['activated'];
Cookie::set('__typecho_uid', $user['uid']);
Cookie::set('__typecho_authCode', Common::hash($user['authCode']));
$this->login = true;
}
}

122
var/Widget/Base.php Executable file
View File

@@ -0,0 +1,122 @@
<?php
namespace Widget;
use Typecho\Config;
use Typecho\Db;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 纯数据抽象组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
abstract class Base extends Widget
{
/**
* init db
*/
protected const INIT_DB = 0b0001;
/**
* init user widget
*/
protected const INIT_USER = 0b0010;
/**
* init security widget
*/
protected const INIT_SECURITY = 0b0100;
/**
* init options widget
*/
protected const INIT_OPTIONS = 0b1000;
/**
* init all widgets
*/
protected const INIT_ALL = 0b1111;
/**
* init none widget
*/
protected const INIT_NONE = 0;
/**
* 全局选项
*
* @var Options
*/
protected Options $options;
/**
* 用户对象
*
* @var User
*/
protected User $user;
/**
* 安全模块
*
* @var Security
*/
protected Security $security;
/**
* 数据库对象
*
* @var Db
*/
protected Db $db;
/**
* init method
*/
protected function init()
{
$components = self::INIT_ALL;
$this->initComponents($components);
if ($components != self::INIT_NONE) {
$this->db = Db::get();
}
if ($components & self::INIT_USER) {
$this->user = User::alloc();
}
if ($components & self::INIT_OPTIONS) {
$this->options = Options::alloc();
}
if ($components & self::INIT_SECURITY) {
$this->security = Security::alloc();
}
$this->initParameter($this->parameter);
}
/**
* @param int $components
*/
protected function initComponents(int &$components)
{
}
/**
* @param Config $parameter
*/
protected function initParameter(Config $parameter)
{
}
}

515
var/Widget/Base/Comments.php Executable file
View File

@@ -0,0 +1,515 @@
<?php
namespace Widget\Base;
use Typecho\Common;
use Typecho\Date;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Typecho\Router;
use Typecho\Router\ParamsDelegateInterface;
use Utils\AutoP;
use Utils\Markdown;
use Widget\Base;
use Widget\Contents\From;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 评论基类
*
* @property int $coid
* @property int $cid
* @property int $created
* @property string author
* @property int $authorId
* @property int $ownerId
* @property string $mail
* @property string $url
* @property string $ip
* @property string $agent
* @property string $text
* @property string $type
* @property string status
* @property int $parent
* @property int $commentPage
* @property Date $date
* @property string $dateWord
* @property string $theId
* @property Contents $parentContent
* @property string $title
* @property string $permalink
* @property string $content
*/
class Comments extends Base implements QueryInterface, RowFilterInterface, PrimaryKeyInterface, ParamsDelegateInterface
{
/**
* @return string 获取主键
*/
public function getPrimaryKey(): string
{
return 'coid';
}
/**
* @param string $key
* @return string
*/
public function getRouterParam(string $key): string
{
switch ($key) {
case 'permalink':
return $this->parentContent->path;
case 'commentPage':
return $this->commentPage;
default:
return '{' . $key . '}';
}
}
/**
* 增加评论
*
* @param array $rows 评论结构数组
* @return integer
* @throws Exception
*/
public function insert(array $rows): int
{
/** 构建插入结构 */
$insertStruct = [
'cid' => $rows['cid'],
'created' => empty($rows['created']) ? $this->options->time : $rows['created'],
'author' => Common::strBy($rows['author'] ?? null),
'authorId' => empty($rows['authorId']) ? 0 : $rows['authorId'],
'ownerId' => empty($rows['ownerId']) ? 0 : $rows['ownerId'],
'mail' => Common::strBy($rows['mail'] ?? null),
'url' => Common::strBy($rows['url'] ?? null),
'ip' => Common::strBy($rows['ip'] ?? null, $this->request->getIp()),
'agent' => Common::strBy($rows['agent'] ?? null, $this->request->getAgent()),
'text' => Common::strBy($rows['text'] ?? null),
'type' => Common::strBy($rows['type'] ?? null, 'comment'),
'status' => Common::strBy($rows['status'] ?? null, 'approved'),
'parent' => empty($rows['parent']) ? 0 : $rows['parent'],
];
if (!empty($rows['coid'])) {
$insertStruct['coid'] = $rows['coid'];
}
/** 过长的客户端字符串要截断 */
if (Common::strLen($insertStruct['agent']) > 511) {
$insertStruct['agent'] = Common::subStr($insertStruct['agent'], 0, 511, '');
}
/** 首先插入部分数据 */
$insertId = $this->db->query($this->db->insert('table.comments')->rows($insertStruct));
/** 更新评论数 */
$num = $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])->from('table.comments')
->where('status = ? AND cid = ?', 'approved', $rows['cid']))->num;
$this->db->query($this->db->update('table.contents')->rows(['commentsNum' => $num])
->where('cid = ?', $rows['cid']));
return $insertId;
}
/**
* 更新评论
*
* @param array $rows 评论结构数组
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function update(array $rows, Query $condition): int
{
/** 获取内容主键 */
$updateCondition = clone $condition;
$updateComment = $this->db->fetchObject($condition->select('cid')->from('table.comments')->limit(1));
if ($updateComment) {
$cid = $updateComment->cid;
} else {
return 0;
}
/** 构建插入结构 */
$preUpdateStruct = [
'author' => Common::strBy($rows['author'] ?? null),
'mail' => Common::strBy($rows['mail'] ?? null),
'url' => Common::strBy($rows['url'] ?? null),
'text' => Common::strBy($rows['text'] ?? null),
'status' => Common::strBy($rows['status'] ?? null, 'approved'),
];
$updateStruct = [];
foreach ($rows as $key => $val) {
if ((array_key_exists($key, $preUpdateStruct))) {
$updateStruct[$key] = $preUpdateStruct[$key];
}
}
/** 更新创建时间 */
if (!empty($rows['created'])) {
$updateStruct['created'] = $rows['created'];
}
/** 更新评论数据 */
$updateRows = $this->db->query($updateCondition->update('table.comments')->rows($updateStruct));
/** 更新评论数 */
$num = $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])->from('table.comments')
->where('status = ? AND cid = ?', 'approved', $cid))->num;
$this->db->query($this->db->update('table.contents')->rows(['commentsNum' => $num])
->where('cid = ?', $cid));
return $updateRows;
}
/**
* 删除数据
*
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function delete(Query $condition): int
{
/** 获取内容主键 */
$deleteCondition = clone $condition;
$deleteComment = $this->db->fetchObject($condition->select('cid')->from('table.comments')->limit(1));
if ($deleteComment) {
$cid = $deleteComment->cid;
} else {
return 0;
}
/** 删除评论数据 */
$deleteRows = $this->db->query($deleteCondition->delete('table.comments'));
/** 更新评论数 */
$num = $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])->from('table.comments')
->where('status = ? AND cid = ?', 'approved', $cid))->num;
$this->db->query($this->db->update('table.contents')->rows(['commentsNum' => $num])
->where('cid = ?', $cid));
return $deleteRows;
}
/**
* 按照条件计算评论数量
*
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function size(Query $condition): int
{
return $this->db->fetchObject($condition->select(['COUNT(coid)' => 'num'])->from('table.comments'))->num;
}
/**
* 将每行的值压入堆栈
*
* @param array $value 每行的值
* @return array
*/
public function push(array $value): array
{
$value = $this->filter($value);
return parent::push($value);
}
/**
* 通用过滤器
*
* @param array $row 需要过滤的行数据
* @return array
*/
public function filter(array $row): array
{
/** 处理默认空值 */
$row['author'] = $row['author'] ?? '';
$row['mail'] = $row['mail'] ?? '';
$row['url'] = $row['url'] ?? '';
$row['ip'] = $row['ip'] ?? '';
$row['agent'] = $row['agent'] ?? '';
$row['text'] = $row['text'] ?? '';
$row['date'] = new Date($row['created']);
return Comments::pluginHandle()->filter('filter', $row, $this);
}
/**
* 输出文章发布日期
*
* @param string|null $format 日期格式
*/
public function date(?string $format = null)
{
echo $this->date->format(empty($format) ? $this->options->commentDateFormat : $format);
}
/**
* 输出作者相关
*
* @param boolean|null $autoLink 是否自动加上链接
* @param boolean|null $noFollow 是否加上nofollow标签
*/
public function author(?bool $autoLink = null, ?bool $noFollow = null)
{
$autoLink = (null === $autoLink) ? $this->options->commentsShowUrl : $autoLink;
$noFollow = (null === $noFollow) ? $this->options->commentsUrlNofollow : $noFollow;
if ($this->url && $autoLink) {
echo '<a href="' . Common::safeUrl($this->url) . '"'
. ($noFollow ? ' rel="external nofollow"' : null) . '>' . $this->author . '</a>';
} else {
echo $this->author;
}
}
/**
* 调用gravatar输出用户头像
*
* @param integer $size 头像尺寸
* @param string|null $default 默认输出头像
*/
public function gravatar(int $size = 32, ?string $default = null, $highRes = false)
{
if ($this->options->commentsAvatar && 'comment' == $this->type) {
$rating = $this->options->commentsAvatarRating;
Comments::pluginHandle()->trigger($plugged)->call('gravatar', $size, $rating, $default, $this);
if (!$plugged) {
$url = Common::gravatarUrl($this->mail, $size, $rating, $default, $this->request->isSecure());
$srcset = '';
if ($highRes) {
$url2x = Common::gravatarUrl($this->mail, $size * 2, $rating, $default, $this->request->isSecure());
$url3x = Common::gravatarUrl($this->mail, $size * 3, $rating, $default, $this->request->isSecure());
$srcset = ' srcset="' . $url2x . ' 2x, ' . $url3x . ' 3x"';
}
echo '<img class="avatar" loading="lazy" src="' . $url . '"' . $srcset . ' alt="' .
$this->author . '" width="' . $size . '" height="' . $size . '" />';
}
}
}
/**
* 输出评论摘要
*
* @param integer $length 摘要截取长度
* @param string $trim 摘要后缀
*/
public function excerpt(int $length = 100, string $trim = '...')
{
echo Common::subStr(strip_tags($this->content), 0, $length, $trim);
}
/**
* 输出邮箱地址
*
* @param bool $link
* @return void
*/
public function mail(bool $link = false)
{
$mail = htmlspecialchars($this->mail);
echo $link ? 'mailto:' . $mail : $mail;
}
/**
* 获取查询对象
*
* @param mixed $fields
* @return Query
*/
public function select(...$fields): Query
{
return $this->db->select(...$fields)->from('table.comments');
}
/**
* markdown
*
* @param string|null $text
* @return string|null
*/
public function markdown(?string $text): ?string
{
$html = Comments::pluginHandle()->trigger($parsed)->filter('markdown', $text);
if (!$parsed) {
$html = Markdown::convert($text);
}
return $html;
}
/**
* autoP
*
* @param string|null $text
* @return string|null
*/
public function autoP(?string $text): ?string
{
$html = Comments::pluginHandle()->trigger($parsed)->filter('autoP', $text);
if (!$parsed) {
static $parser;
if (empty($parser)) {
$parser = new AutoP();
}
$html = $parser->parse($text);
}
return $html;
}
/**
* 获取当前内容结构
*
* @return Contents
*/
protected function ___parentContent(): Contents
{
return From::allocWithAlias($this->cid, ['cid' => $this->cid]);
}
/**
* 获取当前评论标题
*
* @return string|null
*/
protected function ___title(): ?string
{
return $this->parentContent->title;
}
/**
* 获取当前评论页码
*
* @return int
*/
protected function ___commentPage(): int
{
if ($this->options->commentsPageBreak) {
$coid = $this->coid;
$parent = $this->parent;
while ($parent > 0 && $this->options->commentsThreaded) {
$parentRows = $this->db->fetchRow($this->db->select('parent')->from('table.comments')
->where('coid = ? AND status = ?', $parent, 'approved')->limit(1));
if (!empty($parentRows)) {
$coid = $parent;
$parent = $parentRows['parent'];
} else {
break;
}
}
$select = $this->db->select('coid', 'parent')
->from('table.comments')
->where(
'cid = ? AND (status = ? OR coid = ?)',
$this->cid,
'approved',
$this->status !== 'approved' ? $this->coid : 0
)
->where('coid ' . ('DESC' == $this->options->commentsOrder ? '>=' : '<=') . ' ?', $coid)
->order('coid');
if ($this->options->commentsShowCommentOnly) {
$select->where('type = ?', 'comment');
}
$comments = $this->db->fetchAll($select);
$commentsMap = [];
$total = 0;
foreach ($comments as $comment) {
$commentsMap[$comment['coid']] = $comment['parent'];
if (0 == $comment['parent'] || !isset($commentsMap[$comment['parent']])) {
$total++;
}
}
return ceil($total / $this->options->commentsPageSize);
}
return 0;
}
/**
* 获取当前评论链接
*
* @return string
* @throws Exception
*/
protected function ___permalink(): string
{
if ($this->options->commentsPageBreak) {
return Router::url(
'comment_page',
$this,
$this->options->index
) . '#' . $this->theId;
}
return $this->parentContent->permalink . '#' . $this->theId;
}
/**
* 获取当前评论内容
*
* @return string|null
*/
protected function ___content(): ?string
{
$text = $this->parentContent->hidden ? _t('内容被隐藏') : $this->text;
$text = Comments::pluginHandle()->trigger($plugged)->filter('content', $text, $this);
if (!$plugged) {
$text = $this->options->commentsMarkdown ? $this->markdown($text)
: $this->autoP($text);
}
$text = Comments::pluginHandle()->filter('contentEx', $text, $this);
return Common::stripTags($text, '<p><br>' . $this->options->commentsHTMLTagAllowed);
}
/**
* 输出词义化日期
*
* @return string
*/
protected function ___dateWord(): string
{
return $this->date->word();
}
/**
* 锚点id
*
* @return string
*/
protected function ___theId(): string
{
return $this->type . '-' . $this->coid;
}
}

961
var/Widget/Base/Contents.php Executable file
View File

@@ -0,0 +1,961 @@
<?php
namespace Widget\Base;
use Typecho\Common;
use Typecho\Config;
use Typecho\Cookie;
use Typecho\Date;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Typecho\Plugin;
use Typecho\Router;
use Typecho\Router\ParamsDelegateInterface;
use Typecho\Widget;
use Utils\AutoP;
use Utils\Markdown;
use Widget\Base;
use Widget\Metas\Category\Rows;
use Widget\Upload;
use Widget\Users\Author;
use Widget\Metas\Category\Related as CategoryRelated;
use Widget\Metas\Tag\Related as TagRelated;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 内容基类
*
* @property int $cid
* @property string $title
* @property string $slug
* @property int $created
* @property int $modified
* @property string $text
* @property int $order
* @property int $authorId
* @property string $template
* @property string $type
* @property string $status
* @property string|null $password
* @property int $commentsNum
* @property bool $allowComment
* @property bool $allowPing
* @property bool $allowFeed
* @property int $parent
* @property-read Users $author
* @property-read string $permalink
* @property-read string $path
* @property-read string $url
* @property-read string $feedUrl
* @property-read string $feedRssUrl
* @property-read string $feedAtomUrl
* @property-read bool $isMarkdown
* @property-read bool $hidden
* @property-read Date $date
* @property-read string $dateWord
* @property-read string[] $directory
* @property-read array[] $tags
* @property-read array[] $categories
* @property-read string $excerpt
* @property-read string $plainExcerpt
* @property-read string $summary
* @property-read string $content
* @property-read Config $fields
* @property-read Config $attachment
* @property-read string $theId
* @property-read string $respondId
* @property-read string $commentUrl
* @property-read string $trackbackUrl
* @property-read string $responseUrl
* @property-read string $year
* @property-read string $month
* @property-read string $day
*/
class Contents extends Base implements QueryInterface, RowFilterInterface, PrimaryKeyInterface, ParamsDelegateInterface
{
/**
* @return string 获取主键
*/
public function getPrimaryKey(): string
{
return 'cid';
}
/**
* @param string $key
* @return string
*/
public function getRouterParam(string $key): string
{
switch ($key) {
case 'cid':
return $this->cid;
case 'slug':
return urlencode($this->slug);
case 'directory':
return implode('/', array_map('urlencode', $this->directory));
case 'category':
return empty($this->categories) ? '' : urlencode($this->categories[0]['slug']);
case 'year':
return $this->date->year;
case 'month':
return $this->date->month;
case 'day':
return $this->date->day;
default:
return '{' . $key . '}';
}
}
/**
* 获取查询对象
*
* @param mixed $fields
* @return Query
*/
public function select(...$fields): Query
{
return $this->db->select(...$fields)->from('table.contents');
}
/**
* 插入内容
*
* @param array $rows 内容数组
* @return integer
* @throws Exception
*/
public function insert(array $rows): int
{
/** 构建插入结构 */
$insertStruct = [
'title' => !isset($rows['title']) || strlen($rows['title']) === 0
? null : htmlspecialchars($rows['title']),
'created' => empty($rows['created']) ? $this->options->time : $rows['created'],
'modified' => $this->options->time,
'text' => Common::strBy($rows['text'] ?? null),
'order' => empty($rows['order']) ? 0 : intval($rows['order']),
'authorId' => $rows['authorId'] ?? $this->user->uid,
'template' => Common::strBy($rows['template'] ?? null),
'type' => Common::strBy($rows['type'] ?? null, 'post'),
'status' => Common::strBy($rows['status'] ?? null, 'publish'),
'password' => Common::strBy($rows['password'] ?? null),
'commentsNum' => empty($rows['commentsNum']) ? 0 : $rows['commentsNum'],
'allowComment' => !empty($rows['allowComment']) && 1 == $rows['allowComment'] ? 1 : 0,
'allowPing' => !empty($rows['allowPing']) && 1 == $rows['allowPing'] ? 1 : 0,
'allowFeed' => !empty($rows['allowFeed']) && 1 == $rows['allowFeed'] ? 1 : 0,
'parent' => empty($rows['parent']) ? 0 : intval($rows['parent'])
];
if (!empty($rows['cid'])) {
$insertStruct['cid'] = $rows['cid'];
}
/** 首先插入部分数据 */
$insertId = $this->db->query($this->db->insert('table.contents')->rows($insertStruct));
/** 更新缩略名 */
if ($insertId > 0) {
$this->applySlug(Common::strBy($rows['slug'] ?? null), $insertId, $insertStruct['title']);
}
return $insertId;
}
/**
* 为内容应用缩略名
*
* @param string|null $slug 缩略名
* @param mixed $cid 内容id
* @param string $title 标题
* @return string
* @throws Exception
*/
public function applySlug(?string $slug, $cid, string $title = ''): string
{
if ($cid instanceof Query) {
$cid = $this->db->fetchObject($cid->select('cid')
->from('table.contents')->limit(1))->cid;
}
/** 生成一个非空的缩略名 */
if ((!isset($slug) || strlen($slug) === 0) && preg_match_all("/\w+/", $title, $matches)) {
$slug = implode('-', $matches[0]);
}
$slug = Common::slugName($slug, $cid);
$result = $slug;
/** 对草稿的slug做特殊处理 */
$draft = $this->db->fetchObject($this->db->select('type', 'parent')
->from('table.contents')->where('cid = ?', $cid));
if ('revision' === $draft->type && $draft->parent) {
$result = '@' . $result;
}
/** 判断是否在数据库中已经存在 */
$count = 1;
while (
$this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')->where('slug = ? AND cid <> ?', $result, $cid))->num > 0
) {
$result = $slug . '-' . $count;
$count++;
}
$this->db->query($this->db->update('table.contents')->rows(['slug' => $result])
->where('cid = ?', $cid));
return $result;
}
/**
* 更新内容
*
* @param array $rows 内容数组
* @param Query $condition 更新条件
* @return integer
* @throws Exception
*/
public function update(array $rows, Query $condition): int
{
/** 首先验证写入权限 */
if (!$this->isWriteable(clone $condition)) {
return 0;
}
/** 构建更新结构 */
$preUpdateStruct = [
'title' => !isset($rows['title']) || strlen($rows['title']) === 0
? null : htmlspecialchars($rows['title']),
'order' => empty($rows['order']) ? 0 : intval($rows['order']),
'text' => Common::strBy($rows['text'] ?? null),
'template' => Common::strBy($rows['template'] ?? null),
'type' => Common::strBy($rows['type'] ?? null, 'post'),
'status' => Common::strBy($rows['status'] ?? null, 'publish'),
'password' => Common::strBy($rows['password'] ?? null),
'allowComment' => !empty($rows['allowComment']) && 1 == $rows['allowComment'] ? 1 : 0,
'allowPing' => !empty($rows['allowPing']) && 1 == $rows['allowPing'] ? 1 : 0,
'allowFeed' => !empty($rows['allowFeed']) && 1 == $rows['allowFeed'] ? 1 : 0,
'parent' => empty($rows['parent']) ? 0 : intval($rows['parent'])
];
$updateStruct = [];
foreach ($rows as $key => $val) {
if (array_key_exists($key, $preUpdateStruct)) {
$updateStruct[$key] = $preUpdateStruct[$key];
}
}
/** 更新创建时间 */
if (isset($rows['created'])) {
$updateStruct['created'] = $rows['created'];
}
$updateStruct['modified'] = $this->options->time;
/** 首先插入部分数据 */
$updateCondition = clone $condition;
$updateRows = $this->db->query($condition->update('table.contents')->rows($updateStruct));
/** 更新缩略名 */
if ($updateRows > 0 && isset($rows['slug'])) {
$this->applySlug(strlen($rows['slug']) === 0 ? null : $rows['slug'], $updateCondition);
}
return $updateRows;
}
/**
* 内容是否可以被修改
*
* @param Query $condition 条件
* @return bool
* @throws Exception
*/
public function isWriteable(Query $condition): bool
{
$post = $this->db->fetchRow($condition->select('authorId')->from('table.contents')->limit(1));
return $post && ($this->user->pass('editor', true) || $post['authorId'] == $this->user->uid);
}
/**
* 删除内容
*
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function delete(Query $condition): int
{
return $this->db->query($condition->delete('table.contents'));
}
/**
* 按照条件计算内容数量
*
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function size(Query $condition): int
{
return $this->db->fetchObject($condition
->select(['COUNT(DISTINCT table.contents.cid)' => 'num'])
->from('table.contents')
->cleanAttribute('group'))->num;
}
/**
* 获取当前所有自定义模板
*
* @return array
*/
public function getTemplates(): array
{
$files = glob($this->options->themeFile($this->options->theme, '*.php'));
$result = [];
foreach ($files as $file) {
$info = Plugin::parseInfo($file);
$file = basename($file);
if ('index.php' != $file && 'custom' == $info['title']) {
$result[$file] = $info['description'];
}
}
return $result;
}
/**
* 将每行的值压入堆栈
*
* @param array $value 每行的值
* @return array
*/
public function push(array $value): array
{
$value = $this->filter($value);
return parent::push($value);
}
/**
* 通用过滤器
*
* @param array $row 需要过滤的行数据
* @return array
*/
public function filter(array $row): array
{
/** 处理默认空值 */
$row['title'] = $row['title'] ?? '';
$row['text'] = $row['text'] ?? '';
$row['slug'] = $row['slug'] ?? '';
$row['password'] = $row['password'] ?? '';
$row['date'] = new Date($row['created']);
return Contents::pluginHandle()->filter('filter', $row, $this);
}
/**
* 输出文章发布日期
*
* @param string|null $format 日期格式
*/
public function date(?string $format = null)
{
echo $this->date->format(empty($format) ? $this->options->postDateFormat : $format);
}
/**
* 输出文章内容
*
* @param mixed $more 文章截取后缀
*/
public function content($more = false)
{
echo false !== $more && false !== strpos($this->text, '<!--more-->') ?
$this->excerpt
. "<p class=\"more\"><a href=\"{$this->permalink}\" title=\"{$this->title}\">{$more}</a></p>"
: $this->content;
}
/**
* 输出文章摘要
*
* @param integer $length 摘要截取长度
* @param string $trim 摘要后缀
*/
public function excerpt(int $length = 100, string $trim = '...')
{
echo Common::subStr(strip_tags($this->excerpt), 0, $length, $trim);
}
/**
* 输出标题
*
* @param integer $length 标题截取长度
* @param string $trim 截取后缀
*/
public function title(int $length = 0, string $trim = '...')
{
$title = Contents::pluginHandle()->trigger($plugged)->filter('title', $this->title, $this);
if (!$plugged) {
echo $length > 0 ? Common::subStr($this->title, 0, $length, $trim) : $this->title;
} else {
echo $title;
}
}
/**
* 输出文章评论数
*
* @param ...$args
*/
public function commentsNum(...$args)
{
if (empty($args)) {
$args[] = '%d';
}
$num = intval($this->commentsNum);
echo sprintf($args[$num] ?? array_pop($args), $num);
}
/**
* 获取文章权限
*
* @param ...$permissions
*/
public function allow(...$permissions): bool
{
$allow = true;
foreach ($permissions as $permission) {
$permission = strtolower($permission);
if ('edit' == $permission) {
$allow &= ($this->user->pass('editor', true) || $this->authorId == $this->user->uid);
} else {
/** 对自动关闭反馈功能的支持 */
if (
('ping' == $permission || 'comment' == $permission) && $this->options->commentsPostTimeout > 0 &&
$this->options->commentsAutoClose
) {
if ($this->options->time - $this->created > $this->options->commentsPostTimeout) {
return false;
}
}
$allow &= ($this->row['allow' . ucfirst($permission)] == 1) && !$this->hidden;
}
}
return $allow;
}
/**
* 输出文章分类
*
* @param string $split 多个分类之间分隔符
* @param boolean $link 是否输出链接
* @param string|null $default 如果没有则输出
*/
public function category(string $split = ',', bool $link = true, ?string $default = null)
{
if (!empty($this->categories)) {
$result = [];
foreach ($this->categories as $category) {
$result[] = $link ? "<a href=\"{$category['permalink']}\">{$category['name']}</a>" : $category['name'];
}
echo implode($split, $result);
} else {
echo $default;
}
}
/**
* 输出文章多级分类
*
* @param string $split 多个分类之间分隔符
* @param boolean $link 是否输出链接
* @param string|null $default 如果没有则输出
* @throws Widget\Exception
*/
public function directory(string $split = '/', bool $link = true, ?string $default = null)
{
$category = $this->categories[0];
$directory = Rows::alloc()->getAllParents($category['mid']);
$directory[] = $category;
if ($directory) {
$result = [];
foreach ($directory as $category) {
$result[] = $link ? '<a href="' . $category['permalink'] . '">'
. $category['name'] . '</a>' : $category['name'];
}
echo implode($split, $result);
} else {
echo $default;
}
}
/**
* 输出文章标签
*
* @param string $split 多个标签之间分隔符
* @param boolean $link 是否输出链接
* @param string|null $default 如果没有则输出
*/
public function tags(string $split = ',', bool $link = true, ?string $default = null)
{
if (!empty($this->tags)) {
$result = [];
foreach ($this->tags as $tag) {
$result[] = $link ? "<a href=\"{$tag['permalink']}\">{$tag['name']}</a>" : $tag['name'];
}
echo implode($split, $result);
} else {
echo $default;
}
}
/**
* 输出当前作者
*
* @param string $item 需要输出的项目
*/
public function author(string $item = 'screenName')
{
if ($this->have()) {
echo $this->author->{$item};
}
}
/**
* @return string
*/
protected function ___title(): string
{
return $this->hidden ? _t('此内容被密码保护') : $this->row['title'];
}
/**
* @return string
*/
protected function ___text(): string
{
if ('attachment' == $this->type) {
if ($this->attachment->isImage) {
return '<img src="' . $this->attachment->url . '" alt="' .
$this->title . '" />';
} else {
return '<a href="' . $this->attachment->url . '" title="' .
$this->title . '">' . $this->title . '</a>';
}
} elseif ($this->hidden) {
return '<form class="protected" action="' . $this->security->getTokenUrl($this->permalink)
. '" method="post">' .
'<p class="word">' . _t('请输入密码访问') . '</p>' .
'<p><input type="password" class="text" name="protectPassword" />
<input type="hidden" name="protectCID" value="' . $this->cid . '" />
<input type="submit" class="submit" value="' . _t('提交') . '" /></p>' .
'</form>';
}
return $this->isMarkdown ? substr($this->row['text'], 15) : $this->row['text'];
}
/**
* @return bool
*/
protected function ___isMarkdown(): bool
{
return 0 === strpos($this->row['text'], '<!--markdown-->');
}
/**
* 是否为隐藏文章
*
* @return bool
*/
protected function ___hidden(): bool
{
if (
strlen($this->password) > 0 &&
$this->password !== Cookie::get('protectPassword_' . $this->cid) &&
$this->authorId != $this->user->uid &&
!$this->user->pass('editor', true)
) {
return true;
}
return false;
}
/**
* @return string
*/
protected function ___path(): string
{
return Router::url($this->type, $this);
}
/**
* @return string
*/
protected function ___permalink(): string
{
return Common::url($this->path, $this->options->index);
}
/**
* @return string
*/
protected function ___url(): string
{
return $this->permalink;
}
/**
* @return string
*/
protected function ___feedUrl(): string
{
return Router::url($this->type, $this, $this->options->feedUrl);
}
/**
* @return string
*/
protected function ___feedRssUrl(): string
{
return Router::url($this->type, $this, $this->options->feedRssUrl);
}
/**
* @return string
*/
protected function ___feedAtomUrl(): string
{
return Router::url($this->type, $this, $this->options->feedAtomUrl);
}
/**
* 多级目录结构
*
* @return array
*/
protected function ___directory(): array
{
$directory = [];
if (!empty($this->categories)) {
$directory = Rows::alloc()->getAllParentsSlug($this->categories[0]['mid']);
$directory[] = $this->categories[0]['slug'];
}
return $directory;
}
/**
* @return string
*/
protected function ___category(): string
{
return empty($this->categories) ? '' : $this->categories[0]['slug'];
}
/**
* @return array
*/
protected function ___categories(): array
{
return CategoryRelated::allocWithAlias($this->cid, ['cid' => $this->cid])
->toArray(['mid', 'name', 'slug', 'description', 'order', 'parent', 'count', 'permalink']);
}
/**
* 将tags取出
*
* @return array
*/
protected function ___tags(): array
{
return TagRelated::allocWithAlias($this->cid, ['cid' => $this->cid])
->toArray(['mid', 'name', 'slug', 'description', 'count', 'permalink']);
}
/**
* 文章作者
*
* @return Users
*/
protected function ___author(): Users
{
return Author::allocWithAlias($this->cid, ['uid' => $this->authorId]);
}
/**
* 获取词义化日期
*
* @return string
*/
protected function ___dateWord(): string
{
return $this->date->word();
}
/**
* 对文章的简短纯文本描述
*
* @deprecated
* @return string|null
*/
protected function ___description(): ?string
{
return $this->plainExcerpt;
}
/**
* @return Config|null
*/
protected function ___attachment(): ?Config
{
if ('attachment' == $this->type) {
$content = json_decode($this->row['text'], true);
//增加数据信息
$attachment = new Config($content);
$attachment->isImage = in_array($content['type'], [
'jpg', 'jpeg', 'gif', 'png', 'tiff', 'bmp', 'webp', 'avif'
]);
$attachment->url = Upload::attachmentHandle($attachment);
return $attachment;
}
return null;
}
/**
* ___fields
*
* @return Config
* @throws Exception
*/
protected function ___fields(): Config
{
$fields = [];
$rows = $this->db->fetchAll($this->db->select()->from('table.fields')
->where('cid = ?', $this->cid));
foreach ($rows as $row) {
$value = 'json' == $row['type'] ? json_decode($row['str_value'], true) : $row[$row['type'] . '_value'];
$fields[$row['name']] = $value;
}
return new Config($fields);
}
/**
* 获取文章内容摘要
*
* @return string|null
*/
protected function ___excerpt(): ?string
{
if ($this->hidden) {
return $this->text;
}
$content = Contents::pluginHandle()->filter('excerpt', $this->content, $this);
[$excerpt] = explode('<!--more-->', $content);
return Common::fixHtml(Contents::pluginHandle()->filter('excerptEx', $excerpt, $this));
}
/**
* 对文章的简短纯文本描述
*
* @return string|null
*/
protected function ___plainExcerpt(): ?string
{
$plainText = str_replace("\n", '', trim(strip_tags($this->excerpt)));
$plainText = $plainText ?: $this->title;
return Common::subStr($plainText, 0, 100);
}
/**
* markdown
*
* @param string|null $text
* @return string|null
*/
protected function markdown(?string $text): ?string
{
$html = Contents::pluginHandle()->trigger($parsed)->filter('markdown', $text);
if (!$parsed) {
$html = Markdown::convert($text);
}
return $html;
}
/**
* autoP
*
* @param string|null $text
* @return string|null
*/
protected function autoP(?string $text): ?string
{
$html = Contents::pluginHandle()->trigger($parsed)->filter('autoP', $text);
if (!$parsed && $text) {
static $parser;
if (empty($parser)) {
$parser = new AutoP();
}
$html = $parser->parse($text);
}
return $html;
}
/**
* 获取文章内容
*
* @return string|null
*/
protected function ___content(): ?string
{
if ($this->hidden) {
return $this->text;
}
$content = Contents::pluginHandle()->trigger($plugged)->filter('content', $this->text, $this);
if (!$plugged) {
$content = $this->isMarkdown ? $this->markdown($content)
: $this->autoP($content);
}
return Contents::pluginHandle()->filter('contentEx', $content, $this);
}
/**
* 输出文章的第一行作为摘要
*
* @return string|null
*/
protected function ___summary(): ?string
{
$content = $this->content;
$parts = preg_split("/(<\/\s*(?:p|blockquote|q|pre|table)\s*>)/i", $content, 2, PREG_SPLIT_DELIM_CAPTURE);
if (!empty($parts)) {
$content = $parts[0] . $parts[1];
}
return $content;
}
/**
* 锚点id
*
* @return string
*/
protected function ___theId(): string
{
return $this->type . '-' . $this->cid;
}
/**
* 回复框id
*
* @return string
*/
protected function ___respondId(): string
{
return 'respond-' . $this->theId;
}
/**
* 评论地址
*
* @return string
*/
protected function ___commentUrl(): string
{
/** 生成反馈地址 */
/** 评论 */
return Router::url(
'feedback',
['type' => 'comment', 'permalink' => $this->path],
$this->options->index
);
}
/**
* trackback地址
*
* @return string
*/
protected function ___trackbackUrl(): string
{
return Router::url(
'feedback',
['type' => 'trackback', 'permalink' => $this->path],
$this->options->index
);
}
/**
* 回复地址
*
* @return string
*/
protected function ___responseUrl(): string
{
return $this->permalink . '#' . $this->respondId;
}
/**
* @return string
*/
protected function ___year(): string
{
return $this->date->year;
}
/**
* @return string
*/
protected function ___month(): string
{
return $this->date->month;
}
/**
* @return string
*/
protected function ___day(): string
{
return $this->date->day;
}
}

255
var/Widget/Base/Metas.php Executable file
View File

@@ -0,0 +1,255 @@
<?php
namespace Widget\Base;
use Typecho\Common;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Typecho\Router;
use Typecho\Router\ParamsDelegateInterface;
use Widget\Base;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 描述性数据组件
*
* @property int $mid
* @property string $name
* @property string $title
* @property string $slug
* @property string $type
* @property string $description
* @property int $count
* @property int $order
* @property int $parent
* @property-read string $theId
* @property-read string $url
* @property-read string $permalink
* @property-read string[] $directory
* @property-read string $feedUrl
* @property-read string $feedRssUrl
* @property-read string $feedAtomUrl
*/
class Metas extends Base implements QueryInterface, RowFilterInterface, PrimaryKeyInterface, ParamsDelegateInterface
{
/**
* @return string 获取主键
*/
public function getPrimaryKey(): string
{
return 'mid';
}
/**
* @param string $key
* @return string
*/
public function getRouterParam(string $key): string
{
switch ($key) {
case 'mid':
return (string)$this->mid;
case 'slug':
return urlencode($this->slug);
case 'directory':
return implode('/', array_map('urlencode', $this->directory));
default:
return '{' . $key . '}';
}
}
/**
* 获取记录总数
*
* @param Query $condition 计算条件
* @return integer
* @throws Exception
*/
public function size(Query $condition): int
{
return $this->db->fetchObject($condition->select(['COUNT(mid)' => 'num'])->from('table.metas'))->num;
}
/**
* 将每行的值压入堆栈
*
* @param array $value 每行的值
* @return array
*/
public function push(array $value): array
{
$value = $this->filter($value);
return parent::push($value);
}
/**
* 通用过滤器
*
* @param array $row 需要过滤的行数据
* @return array
*/
public function filter(array $row): array
{
return Metas::pluginHandle()->filter('filter', $row, $this);
}
/**
* 更新记录
*
* @param array $rows 记录更新值
* @param Query $condition 更新条件
* @return integer
* @throws Exception
*/
public function update(array $rows, Query $condition): int
{
return $this->db->query($condition->update('table.metas')->rows($rows));
}
/**
* 获取原始查询对象
*
* @param mixed $fields
* @return Query
* @throws Exception
*/
public function select(...$fields): Query
{
return $this->db->select(...$fields)->from('table.metas');
}
/**
* 删除记录
*
* @param Query $condition 删除条件
* @return integer
* @throws Exception
*/
public function delete(Query $condition): int
{
return $this->db->query($condition->delete('table.metas'));
}
/**
* 插入一条记录
*
* @param array $rows 记录插入值
* @return integer
* @throws Exception
*/
public function insert(array $rows): int
{
return $this->db->query($this->db->insert('table.metas')->rows($rows));
}
/**
* 根据tag获取ID
*
* @param mixed $inputTags 标签名
* @return array|int
* @throws Exception
*/
public function scanTags($inputTags)
{
$tags = is_array($inputTags) ? $inputTags : [$inputTags];
$result = [];
foreach ($tags as $tag) {
if (empty($tag)) {
continue;
}
$row = $this->db->fetchRow($this->select()
->where('type = ?', 'tag')
->where('name = ?', $tag)->limit(1));
if ($row) {
$result[] = $row['mid'];
} else {
$slug = Common::slugName($tag);
if ($slug) {
$result[] = $this->insert([
'name' => $tag,
'slug' => $slug,
'type' => 'tag',
'count' => 0,
'order' => 0,
]);
}
}
}
return is_array($inputTags) ? $result : current($result);
}
/**
* 锚点id
*
* @access protected
* @return string
*/
protected function ___theId(): string
{
return $this->type . '-' . $this->mid;
}
/**
* @return string
*/
protected function ___title(): string
{
return $this->name;
}
/**
* @return array
*/
protected function ___directory(): array
{
return [];
}
/**
* @return string
*/
protected function ___permalink(): string
{
return Router::url($this->type, $this, $this->options->index);
}
/**
* @return string
*/
protected function ___url(): string
{
return $this->permalink;
}
/**
* @return string
*/
protected function ___feedUrl(): string
{
return Router::url($this->type, $this, $this->options->feedUrl);
}
/**
* @return string
*/
protected function ___feedRssUrl(): string
{
return Router::url($this->type, $this, $this->options->feedRssUrl);
}
/**
* @return string
*/
protected function ___feedAtomUrl(): string
{
return Router::url($this->type, $this, $this->options->feedAtomUrl);
}
}

83
var/Widget/Base/Options.php Executable file
View File

@@ -0,0 +1,83 @@
<?php
namespace Widget\Base;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Widget\Base;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 全局选项组件
*
* @link typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Options extends Base implements QueryInterface
{
/**
* 获取原始查询对象
*
* @param mixed ...$fields
* @return Query
* @throws Exception
*/
public function select(...$fields): Query
{
return $this->db->select(...$fields)->from('table.options');
}
/**
* 插入一条记录
*
* @param array $rows 记录插入值
* @return integer
* @throws Exception
*/
public function insert(array $rows): int
{
return $this->db->query($this->db->insert('table.options')->rows($rows));
}
/**
* 更新记录
*
* @param array $rows 记录更新值
* @param Query $condition 更新条件
* @return integer
* @throws Exception
*/
public function update(array $rows, Query $condition): int
{
return $this->db->query($condition->update('table.options')->rows($rows));
}
/**
* 删除记录
*
* @param Query $condition 删除条件
* @return integer
* @throws Exception
*/
public function delete(Query $condition): int
{
return $this->db->query($condition->delete('table.options'));
}
/**
* 获取记录总数
*
* @param Query $condition 计算条件
* @return integer
* @throws Exception
*/
public function size(Query $condition): int
{
return $this->db->fetchObject($condition->select(['COUNT(name)' => 'num'])->from('table.options'))->num;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Widget\Base;
interface PrimaryKeyInterface
{
/**
* 获取主键
*
* @return string
*/
public function getPrimaryKey(): string;
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Widget\Base;
use Typecho\Db\Query;
/**
* Base Query Interface
*/
interface QueryInterface
{
/**
* 查询方法
*
* @param mixed $fields 字段
* @return Query
*/
public function select(...$fields): Query;
/**
* 获得所有记录数
*
* @access public
* @param Query $condition 查询对象
* @return integer
*/
public function size(Query $condition): int;
/**
* 增加记录方法
*
* @access public
* @param array $rows 字段对应值
* @return integer
*/
public function insert(array $rows): int;
/**
* 更新记录方法
*
* @access public
* @param array $rows 字段对应值
* @param Query $condition 查询对象
* @return integer
*/
public function update(array $rows, Query $condition): int;
/**
* 删除记录方法
*
* @access public
* @param Query $condition 查询对象
* @return integer
*/
public function delete(Query $condition): int;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Widget\Base;
/**
* 行过滤器接口
*/
interface RowFilterInterface
{
/**
* 过滤行
*
* @param array $row
* @return array
*/
public function filter(array $row): array;
}

279
var/Widget/Base/TreeTrait.php Executable file
View File

@@ -0,0 +1,279 @@
<?php
namespace Widget\Base;
use Typecho\Config;
use Typecho\Db\Exception;
/**
* 处理树状数据结构
*/
trait TreeTrait
{
/**
* 树状数据结构
*
* @var array
* @access private
*/
private array $treeRows = [];
/**
* 顶层节点
*
* @var array
* @access private
*/
private array $top = [];
/**
* 所有节点哈希表
*
* @var array
* @access private
*/
private array $map = [];
/**
* 顺序流
*
* @var array
* @access private
*/
private array $orders = [];
/**
* 所有子节点列表
*
* @var array
* @access private
*/
private array $childNodes = [];
/**
* 所有父节点列表
*
* @var array
* @access private
*/
private array $parents = [];
/**
* 根据深度余数输出
*
* @param ...$args
*/
public function levelsAlt(...$args)
{
$this->altBy($this->levels, ...$args);
}
/**
* 获取某个节点所有父级节点缩略名
*
* @param int $id
* @return array
*/
public function getAllParentsSlug(int $id): array
{
$parents = [];
if (isset($this->parents[$id])) {
foreach ($this->parents[$id] as $parent) {
$parents[] = $this->map[$parent]['slug'];
}
}
return $parents;
}
/**
* 获取某个节点下的所有子节点
*
* @param int $id
* @return array
*/
public function getAllChildIds(int $id): array
{
return $this->childNodes[$id] ?? [];
}
/**
* 获取某个节点下的子节点
*
* @param int $id
* @return array
*/
public function getChildIds(int $id): array
{
return $id > 0 ? ($this->treeRows[$id] ?? []) : $this->top;
}
/**
* 获取某个节点所有父级节点
*
* @param int $id
* @return array
*/
public function getAllParents(int $id): array
{
$parents = [];
if (isset($this->parents[$id])) {
foreach ($this->parents[$id] as $parent) {
$parents[] = $this->map[$parent];
}
}
return $parents;
}
/**
* 获取多个节点
*
* @param array $ids
* @param integer $ignore
* @return array
*/
public function getRows(array $ids, int $ignore = 0): array
{
$result = [];
if (!empty($ids)) {
foreach ($ids as $id) {
if (!$ignore || ($ignore != $id && !$this->hasParent($id, $ignore))) {
$result[] = $this->map[$id];
}
}
}
return $result;
}
/**
* @param int $id
* @return array|null
*/
public function getRow(int $id): ?array
{
return $this->map[$id] ?? null;
}
/**
* 是否拥有某个父级节点
*
* @param mixed $id
* @param mixed $parentId
* @return bool
*/
public function hasParent($id, $parentId): bool
{
if (isset($this->parents[$id])) {
foreach ($this->parents[$id] as $parent) {
if ($parent == $parentId) {
return true;
}
}
}
return false;
}
/**
* @return array
*/
abstract protected function initTreeRows(): array;
/**
* @param Config $parameter
* @throws Exception
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault('ignore=0&current=');
$rows = $this->initTreeRows();
$pk = $this->getPrimaryKey();
// Sort by order asc
usort($rows, function ($a, $b) {
return $a['order'] <=> $b['order'];
});
foreach ($rows as $row) {
$row['levels'] = 0;
$this->map[$row[$pk]] = $row;
}
// 读取数据
foreach ($this->map as $id => $row) {
$parent = $row['parent'];
if (0 != $parent && isset($this->map[$parent])) {
$this->treeRows[$parent][] = $id;
} else {
$this->top[] = $id;
}
}
// 预处理深度
$this->levelWalkCallback($this->top);
$this->map = array_map([$this, 'filter'], $this->map);
}
/**
* @return array
*/
protected function ___directory(): array
{
$directory = $this->getAllParentsSlug($this->{$this->getPrimaryKey()});
$directory[] = $this->slug;
return $directory;
}
/**
* 获取所有子节点
*
* @return array
*/
protected function ___children(): array
{
$id = $this->{$this->getPrimaryKey()};
return $this->getRows($this->getChildIds($id));
}
/**
* 预处理节点迭代
*
* @param array $rows
* @param array $parents
*/
private function levelWalkCallback(array $rows, array $parents = [])
{
foreach ($parents as $parent) {
if (!isset($this->childNodes[$parent])) {
$this->childNodes[$parent] = [];
}
$this->childNodes[$parent] = array_merge($this->childNodes[$parent], $rows);
}
foreach ($rows as $id) {
$this->orders[] = $id;
$parent = $this->map[$id]['parent'];
if (0 != $parent && isset($this->map[$parent])) {
$levels = $this->map[$parent]['levels'] + 1;
$this->map[$id]['levels'] = $levels;
}
$this->parents[$id] = $parents;
if (!empty($this->treeRows[$id])) {
$new = $parents;
$new[] = $id;
$this->levelWalkCallback($this->treeRows[$id], $new);
}
}
}
}

126
var/Widget/Base/TreeViewTrait.php Executable file
View File

@@ -0,0 +1,126 @@
<?php
namespace Widget\Base;
use Typecho\Config;
trait TreeViewTrait
{
use TreeTrait;
/**
* treeViewRows
*
* @param mixed $rowOptions 输出选项
* @param string $type 类型
* @param string $func 回调函数
* @param int $current 当前项
*/
protected function listRows(Config $rowOptions, string $type, string $func, int $current = 0)
{
$this->stack = $this->getRows($this->top);
if ($this->have()) {
echo '<' . $rowOptions->wrapTag . (empty($rowOptions->wrapClass)
? '' : ' class="' . $rowOptions->wrapClass . '"') . '>';
while ($this->next()) {
$this->treeViewRowsCallback($rowOptions, $type, $func, $current);
}
echo '</' . $rowOptions->wrapTag . '>';
}
$this->stack = $this->map;
}
/**
* 列出分类回调
*
* @param Config $rowOptions 输出选项
* @param string $type 类型
* @param string $func 回调函数
* @param int $current 当前项
*/
private function treeViewRowsCallback(Config $rowOptions, string $type, string $func, int $current): void
{
if (function_exists($func)) {
call_user_func($func, $this, $rowOptions);
return;
}
$id = $this->{$this->getPrimaryKey()};
$classes = [];
if ($rowOptions->itemClass) {
$classes[] = $rowOptions->itemClass;
}
$classes[] = $type . '-level-' . $this->levels;
echo '<' . $rowOptions->itemTag . ' class="'
. implode(' ', $classes);
if ($this->levels > 0) {
echo " {$type}-child";
$this->levelsAlt(" {$type}-level-odd", " {$type}-level-even");
} else {
echo " {$type}-parent";
}
if ($id == $current) {
echo " {$type}-active";
} elseif (
isset($this->childNodes[$id]) && in_array($current, $this->childNodes[$id])
) {
echo " {$type}-parent-active";
}
echo '"><a href="' . $this->permalink . '">' . $this->title . '</a>';
if ($rowOptions->showCount) {
printf($rowOptions->countTemplate, intval($this->count));
}
if ($rowOptions->showFeed) {
printf($rowOptions->feedTemplate, $this->feedUrl);
}
if ($this->children) {
$this->treeViewRows($rowOptions, $type, $func, $current);
}
echo '</' . $rowOptions->itemTag . '>';
}
/**
* treeViewRows
*
* @param Config $rowOptions 输出选项
* @param string $type 类型
* @param string $func 回调函数
* @param int $current 当前项
*/
private function treeViewRows(Config $rowOptions, string $type, string $func, int $current)
{
$children = $this->children;
if ($children) {
//缓存变量便于还原
$tmp = $this->row;
$this->sequence++;
//在子评论之前输出
echo '<' . $rowOptions->wrapTag . (empty($rowOptions->wrapClass)
? '' : ' class="' . $rowOptions->wrapClass . '"') . '>';
foreach ($children as $child) {
$this->row = $child;
$this->treeViewRowsCallback($rowOptions, $type, $func, $current);
$this->row = $tmp;
}
//在子评论之后输出
echo '</' . $rowOptions->wrapTag . '>';
$this->sequence--;
}
}
}

209
var/Widget/Base/Users.php Executable file
View File

@@ -0,0 +1,209 @@
<?php
namespace Widget\Base;
use Typecho\Common;
use Typecho\Config;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Typecho\Router;
use Typecho\Router\ParamsDelegateInterface;
use Widget\Base;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 用户抽象类
*
* @property int $uid
* @property string $name
* @property string $password
* @property string $mail
* @property string $url
* @property string $screenName
* @property int $created
* @property int $activated
* @property int $logged
* @property string $group
* @property string $authCode
* @property-read Config $personalOptions
* @property-read string $permalink
* @property-read string $feedUrl
* @property-read string $feedRssUrl
* @property-read string $feedAtomUrl
*/
class Users extends Base implements QueryInterface, RowFilterInterface, PrimaryKeyInterface, ParamsDelegateInterface
{
/**
* @return string 获取主键
*/
public function getPrimaryKey(): string
{
return 'uid';
}
/**
* 将每行的值压入堆栈
*
* @param array $value 每行的值
* @return array
*/
public function push(array $value): array
{
$value = $this->filter($value);
return parent::push($value);
}
/**
* 通用过滤器
*
* @param array $row 需要过滤的行数据
* @return array
*/
public function filter(array $row): array
{
return Users::pluginHandle()->filter('filter', $row, $this);
}
/**
* @param string $key
* @return string
*/
public function getRouterParam(string $key): string
{
switch ($key) {
case 'uid':
return $this->uid;
default:
return '{' . $key . '}';
}
}
/**
* 查询方法
*
* @param mixed $fields
* @return Query
* @throws Exception
*/
public function select(...$fields): Query
{
return $this->db->select(...$fields)->from('table.users');
}
/**
* 获得所有记录数
*
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function size(Query $condition): int
{
return $this->db->fetchObject($condition->select(['COUNT(uid)' => 'num'])->from('table.users'))->num;
}
/**
* 增加记录方法
*
* @param array $rows 字段对应值
* @return integer
* @throws Exception
*/
public function insert(array $rows): int
{
return $this->db->query($this->db->insert('table.users')->rows($rows));
}
/**
* 更新记录方法
*
* @param array $rows 字段对应值
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function update(array $rows, Query $condition): int
{
return $this->db->query($condition->update('table.users')->rows($rows));
}
/**
* 删除记录方法
*
* @param Query $condition 查询对象
* @return integer
* @throws Exception
*/
public function delete(Query $condition): int
{
return $this->db->query($condition->delete('table.users'));
}
/**
* 调用gravatar输出用户头像
*
* @param integer $size 头像尺寸
* @param string $rating 头像评级
* @param string|null $default 默认输出头像
* @param string|null $class 默认css class
*/
public function gravatar(int $size = 40, string $rating = 'X', ?string $default = null, ?string $class = null)
{
$url = Common::gravatarUrl($this->mail, $size, $rating, $default, $this->request->isSecure());
echo '<img' . (empty($class) ? '' : ' class="' . $class . '"') . ' src="' . $url . '" alt="' .
$this->screenName . '" width="' . $size . '" height="' . $size . '" />';
}
/**
* @return string
*/
protected function ___permalink(): string
{
return Router::url('author', $this, $this->options->index);
}
/**
* @return string
*/
protected function ___feedUrl(): string
{
return Router::url('author', $this, $this->options->feedUrl);
}
/**
* @return string
*/
protected function ___feedRssUrl(): string
{
return Router::url('author', $this, $this->options->feedRssUrl);
}
/**
* @return string
*/
protected function ___feedAtomUrl(): string
{
return Router::url('author', $this, $this->options->feedAtomUrl);
}
/**
* personalOptions
*
* @return Config
* @throws Exception
*/
protected function ___personalOptions(): Config
{
$rows = $this->db->fetchAll($this->db->select()
->from('table.options')->where('user = ?', $this->uid));
$options = [];
foreach ($rows as $row) {
$options[$row['name']] = $row['value'];
}
return new Config($options);
}
}

50
var/Widget/CommentPage.php Executable file
View File

@@ -0,0 +1,50 @@
<?php
namespace Widget;
use Exception;
use Typecho\Router;
use Typecho\Widget\Exception as WidgetException;
/**
* Comment Page Widget
*/
class CommentPage extends Base implements ActionInterface
{
/**
* Perform comment page action
*
* @throws Exception
*/
public function action()
{
$page = abs($this->request->filter('int')->get('commentPage'));
$archive = Router::match($this->request->get('permalink'), [
'checkPermalink' => false,
'commentPage' => $page
]);
if (!($archive instanceof Archive) || !$archive->is('single')) {
throw new WidgetException(_t('请求的地址不存在'), 404);
}
$currentCommentUrl = Router::url('comment_page', [
'permalink' => $archive->path,
'commentPage' => $page
], $this->options->index);
$this->checkPermalink($currentCommentUrl);
$archive->render();
}
/**
* @param string $commentUrl
*/
private function checkPermalink(string $commentUrl)
{
if ($commentUrl != $this->request->getRequestUrl()) {
$this->response->redirect($commentUrl, true);
}
}
}

166
var/Widget/Comments/Admin.php Executable file
View File

@@ -0,0 +1,166 @@
<?php
namespace Widget\Comments;
use Typecho\Cookie;
use Typecho\Db;
use Typecho\Db\Query;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\PageNavigator\Box;
use Widget\Base\Comments;
use Widget\Base\Contents;
use Widget\Contents\From;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 后台评论输出组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Admin extends Comments
{
/**
* 分页计算对象
*
* @access private
* @var Query
*/
private Query $countSql;
/**
* 当前页
*
* @access private
* @var integer
*/
private int $currentPage;
/**
* 所有文章个数
*
* @access private
* @var integer|null
*/
private ?int $total;
/**
* 获取菜单标题
*
* @return string
* @throws Exception
*/
public function getMenuTitle(): string
{
$content = $this->parentContent;
if ($content) {
return _t('%s的评论', $content->title);
}
throw new Exception(_t('内容不存在'), 404);
}
/**
* 执行函数
*
* @throws Db\Exception|Exception
*/
public function execute()
{
$select = $this->select();
$this->parameter->setDefault('pageSize=20');
$this->currentPage = $this->request->filter('int')->get('page', 1);
/** 过滤标题 */
if (null != ($keywords = $this->request->filter('search')->get('keywords'))) {
$select->where('table.comments.text LIKE ?', '%' . $keywords . '%');
}
/** 如果具有贡献者以上权限,可以查看所有评论,反之只能查看自己的评论 */
if (!$this->user->pass('editor', true)) {
$select->where('table.comments.ownerId = ?', $this->user->uid);
} elseif (!$this->request->is('cid')) {
if ($this->request->is('__typecho_all_comments=on')) {
Cookie::set('__typecho_all_comments', 'on');
} else {
if ($this->request->is('__typecho_all_comments=off')) {
Cookie::set('__typecho_all_comments', 'off');
}
if ('on' != Cookie::get('__typecho_all_comments')) {
$select->where('table.comments.ownerId = ?', $this->user->uid);
}
}
}
if (in_array($this->request->get('status'), ['approved', 'waiting', 'spam'])) {
$select->where('table.comments.status = ?', $this->request->get('status'));
} elseif ('hold' == $this->request->get('status')) {
$select->where('table.comments.status <> ?', 'approved');
} else {
$select->where('table.comments.status = ?', 'approved');
}
//增加按文章归档功能
if ($this->request->is('cid')) {
$select->where('table.comments.cid = ?', $this->request->filter('int')->get('cid'));
}
$this->countSql = clone $select;
$select->order('table.comments.coid', Db::SORT_DESC)
->page($this->currentPage, $this->parameter->pageSize);
$this->db->fetchAll($select, [$this, 'push']);
}
/**
* 输出分页
*
* @throws Exception|Db\Exception
*/
public function pageNav()
{
$query = $this->request->makeUriByRequest('page={page}');
/** 使用盒状分页 */
$nav = new Box(
!isset($this->total) ? $this->total = $this->size($this->countSql) : $this->total,
$this->currentPage,
$this->parameter->pageSize,
$query
);
$nav->render(_t('&laquo;'), _t('&raquo;'));
}
/**
* 获取当前内容结构
*
* @return Contents
* @throws Db\Exception
*/
protected function ___parentContent(): Contents
{
$cid = $this->request->is('cid') ? $this->request->filter('int')->get('cid') : $this->cid;
return From::allocWithAlias($cid, ['cid' => $cid]);
}
/**
* @return string
*/
protected function ___permalink(): string
{
if ('approved' === $this->status) {
return parent::___permalink();
}
return '#' . $this->theId;
}
}

507
var/Widget/Comments/Archive.php Executable file
View File

@@ -0,0 +1,507 @@
<?php
namespace Widget\Comments;
use Typecho\Config;
use Typecho\Cookie;
use Typecho\Router;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\PageNavigator\Box;
use Widget\Base\Comments;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 评论归档组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Archive extends Comments
{
/**
* 当前页
*
* @access private
* @var integer
*/
private int $currentPage;
/**
* 所有文章个数
*
* @access private
* @var integer
*/
private int $total = 0;
/**
* 子父级评论关系
*
* @access private
* @var array
*/
private array $threadedComments = [];
/**
* _singleCommentOptions
*
* @var Config|null
* @access private
*/
private ?Config $singleCommentOptions = null;
/**
* @param Config $parameter
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault([
'parentId' => 0,
'respondId' => '',
'commentPage' => 0,
'commentsNum' => 0,
'allowComment' => 1,
'parentContent' => null,
]);
}
/**
* 输出文章评论数
*
* @param ...$args
*/
public function num(...$args)
{
if (empty($args)) {
$args[] = '%d';
}
$num = $this->total;
echo sprintf($args[$num] ?? array_pop($args), $num);
}
/**
* 执行函数
*
* @access public
* @return void
*/
public function execute()
{
if (!$this->parameter->parentId) {
return;
}
$unapprovedCommentId = intval(Cookie::get('__typecho_unapproved_comment', 0));
$select = $this->select()->where('cid = ?', $this->parameter->parentId)
->where(
'status = ? OR (coid = ? AND status <> ?)',
'approved',
$unapprovedCommentId,
'approved'
);
if ($this->options->commentsShowCommentOnly) {
$select->where('table.comments.type = ?', 'comment');
}
$select->order('table.comments.coid', 'ASC');
$this->db->fetchAll($select, [$this, 'push']);
/** 需要输出的评论列表 */
$outputComments = [];
/** 如果开启评论回复 */
if ($this->options->commentsThreaded) {
foreach ($this->stack as $coid => &$comment) {
/** 取出父节点 */
$parent = $comment['parent'];
/** 如果存在父节点 */
if (0 != $parent && isset($this->stack[$parent])) {
/** 如果当前节点深度大于最大深度, 则将其挂接在父节点上 */
if ($comment['levels'] >= $this->options->commentsMaxNestingLevels) {
$comment['levels'] = $this->stack[$parent]['levels'];
$parent = $this->stack[$parent]['parent']; // 上上层节点
$comment['parent'] = $parent;
}
/** 计算子节点顺序 */
$comment['order'] = isset($this->threadedComments[$parent])
? count($this->threadedComments[$parent]) + 1 : 1;
/** 如果是子节点 */
$this->threadedComments[$parent][$coid] = $comment;
} else {
$outputComments[$coid] = $comment;
}
}
$this->stack = $outputComments;
}
/** 评论排序 */
if ('DESC' == $this->options->commentsOrder) {
$this->stack = array_reverse($this->stack, true);
$this->threadedComments = array_map('array_reverse', $this->threadedComments);
}
/** 评论总数 */
$this->total = count($this->stack);
/** 对评论进行分页 */
if ($this->options->commentsPageBreak) {
if ('last' == $this->options->commentsPageDisplay && !$this->parameter->commentPage) {
$this->currentPage = ceil($this->total / $this->options->commentsPageSize);
} else {
$this->currentPage = $this->parameter->commentPage ? $this->parameter->commentPage : 1;
}
/** 截取评论 */
$this->stack = array_slice(
$this->stack,
($this->currentPage - 1) * $this->options->commentsPageSize,
$this->options->commentsPageSize
);
}
/** 评论置位 */
$this->length = count($this->stack);
$this->row = $this->length > 0 ? current($this->stack) : [];
reset($this->stack);
}
/**
* 将每行的值压入堆栈
*
* @param array $value 每行的值
* @return array
*/
public function push(array $value): array
{
$value = $this->filter($value);
/** 计算深度 */
if (0 != $value['parent'] && isset($this->stack[$value['parent']]['levels'])) {
$value['levels'] = $this->stack[$value['parent']]['levels'] + 1;
} else {
$value['levels'] = 0;
}
/** 重载push函数,使用coid作为数组键值,便于索引 */
$this->stack[$value['coid']] = $value;
$this->length ++;
return $value;
}
/**
* 输出分页
*
* @access public
* @param string $prev 上一页文字
* @param string $next 下一页文字
* @param int $splitPage 分割范围
* @param string $splitWord 分割字符
* @param string|array $template 展现配置信息
* @return void
* @throws Exception
*/
public function pageNav(
string $prev = '&laquo;',
string $next = '&raquo;',
int $splitPage = 3,
string $splitWord = '...',
$template = ''
) {
if ($this->options->commentsPageBreak) {
$default = [
'wrapTag' => 'ol',
'wrapClass' => 'page-navigator'
];
if (is_string($template)) {
parse_str($template, $config);
} else {
$config = $template ?: [];
}
$template = array_merge($default, $config);
$query = Router::url('comment_page', [
'permalink' => $this->parameter->parentContent->path,
'commentPage' => '{commentPage}'
], $this->options->index);
self::pluginHandle()->trigger($hasNav)->call(
'pageNav',
$this->currentPage,
$this->total,
$this->options->commentsPageSize,
$prev,
$next,
$splitPage,
$splitWord,
$template,
$query
);
if (!$hasNav && $this->total > $this->options->commentsPageSize) {
/** 使用盒状分页 */
$nav = new Box($this->total, $this->currentPage, $this->options->commentsPageSize, $query);
$nav->setPageHolder('commentPage');
$nav->setAnchor('comments');
echo '<' . $template['wrapTag'] . (empty($template['wrapClass'])
? '' : ' class="' . $template['wrapClass'] . '"') . '>';
$nav->render($prev, $next, $splitPage, $splitWord, $template);
echo '</' . $template['wrapTag'] . '>';
}
}
}
/**
* 列出评论
*
* @param mixed $singleCommentOptions 单个评论自定义选项
*/
public function listComments($singleCommentOptions = null)
{
//初始化一些变量
$this->singleCommentOptions = Config::factory($singleCommentOptions);
$this->singleCommentOptions->setDefault([
'before' => '<ol class="comment-list">',
'after' => '</ol>',
'beforeAuthor' => '',
'afterAuthor' => '',
'beforeDate' => '',
'afterDate' => '',
'dateFormat' => $this->options->commentDateFormat,
'replyWord' => _t('回复'),
'commentStatus' => _t('您的评论正等待审核!'),
'avatarSize' => 32,
'defaultAvatar' => null,
'avatarHighRes' => false
]);
self::pluginHandle()->trigger($plugged)->call('listComments', $this->singleCommentOptions, $this);
if (!$plugged) {
if ($this->have()) {
echo $this->singleCommentOptions->before;
while ($this->next()) {
$this->threadedCommentsCallback();
}
echo $this->singleCommentOptions->after;
}
}
}
/**
* 评论回调函数
*/
private function threadedCommentsCallback(): void
{
$singleCommentOptions = $this->singleCommentOptions;
if (function_exists('threadedComments')) {
threadedComments($this, $singleCommentOptions);
return;
}
$commentClass = '';
if ($this->authorId) {
if ($this->authorId == $this->ownerId) {
$commentClass .= ' comment-by-author';
} else {
$commentClass .= ' comment-by-user';
}
}
?>
<li itemscope itemtype="http://schema.org/UserComments" id="<?php $this->theId(); ?>" class="comment-body<?php
if ($this->levels > 0) {
echo ' comment-child';
$this->levelsAlt(' comment-level-odd', ' comment-level-even');
} else {
echo ' comment-parent';
}
$this->alt(' comment-odd', ' comment-even');
echo $commentClass;
?>">
<div class="comment-author" itemprop="creator" itemscope itemtype="http://schema.org/Person">
<span
itemprop="image">
<?php $this->gravatar(
$singleCommentOptions->avatarSize,
$singleCommentOptions->defaultAvatar,
$singleCommentOptions->avatarHighRes
); ?>
</span>
<cite class="fn" itemprop="name"><?php $singleCommentOptions->beforeAuthor();
$this->author();
$singleCommentOptions->afterAuthor(); ?></cite>
</div>
<div class="comment-meta">
<a href="<?php $this->permalink(); ?>">
<time itemprop="commentTime"
datetime="<?php $this->date('c'); ?>"><?php
$singleCommentOptions->beforeDate();
$this->date($singleCommentOptions->dateFormat);
$singleCommentOptions->afterDate();
?></time>
</a>
<?php if ('approved' !== $this->status) { ?>
<em class="comment-awaiting-moderation"><?php $singleCommentOptions->commentStatus(); ?></em>
<?php } ?>
</div>
<div class="comment-content" itemprop="commentText">
<?php $this->content(); ?>
</div>
<div class="comment-reply">
<?php $this->reply($singleCommentOptions->replyWord); ?>
</div>
<?php if ($this->children) { ?>
<div class="comment-children" itemprop="discusses">
<?php $this->threadedComments(); ?>
</div>
<?php } ?>
</li>
<?php
}
/**
* 根据深度余数输出
*
* @param mixed ...$args 需要输出的值
*/
public function levelsAlt(...$args)
{
$this->altBy($this->levels, ...$args);
}
/**
* 重载alt函数,以适应多级评论
*
* @param ...$args
*/
public function alt(...$args)
{
$sequence = $this->levels <= 0 ? $this->sequence : $this->order;
$this->altBy($sequence, ...$args);
}
/**
* 评论回复链接
*
* @param string $word 回复链接文字
*/
public function reply(string $word = '')
{
if ($this->options->commentsThreaded && !$this->isTopLevel && $this->parameter->allowComment) {
$word = empty($word) ? _t('回复') : $word;
self::pluginHandle()->trigger($plugged)->call('reply', $word, $this);
if (!$plugged) {
echo '<a href="' . substr($this->permalink, 0, - strlen($this->theId) - 1) . '?replyTo=' . $this->coid .
'#' . $this->parameter->respondId . '" rel="nofollow" onclick="return TypechoComment.reply(\'' .
$this->theId . '\', ' . $this->coid . ', this);">' . $word . '</a>';
}
}
}
/**
* 递归输出评论
*/
public function threadedComments()
{
$children = $this->children;
if ($children) {
//缓存变量便于还原
$tmp = $this->row;
$this->sequence ++;
//在子评论之前输出
echo $this->singleCommentOptions->before;
foreach ($children as $child) {
$this->row = $child;
$this->threadedCommentsCallback();
$this->row = $tmp;
}
//在子评论之后输出
echo $this->singleCommentOptions->after;
$this->sequence --;
}
}
/**
* 取消评论回复链接
*
* @param string $word 取消回复链接文字
*/
public function cancelReply(string $word = '')
{
if ($this->options->commentsThreaded) {
$word = empty($word) ? _t('取消回复') : $word;
self::pluginHandle()->trigger($plugged)->call('cancelReply', $word, $this);
if (!$plugged) {
$replyId = $this->request->filter('int')->get('replyTo');
echo '<a id="cancel-comment-reply-link" href="' . $this->parameter->parentContent->permalink . '#' . $this->parameter->respondId .
'" rel="nofollow"' . ($replyId ? '' : ' style="display:none"') . ' onclick="return TypechoComment.cancelReply();">' . $word . '</a>';
}
}
}
/**
* 子评论
*
* @return array
*/
protected function ___children(): array
{
return $this->options->commentsThreaded && !$this->isTopLevel && isset($this->threadedComments[$this->coid])
? $this->threadedComments[$this->coid] : [];
}
/**
* 是否到达顶层
*
* @return boolean
*/
protected function ___isTopLevel(): bool
{
return $this->levels > $this->options->commentsMaxNestingLevels - 2;
}
/**
* 重载评论页码获取
*
* @return int
*/
protected function ___commentPage(): int
{
return $this->currentPage;
}
/**
* 重载内容获取
*
* @return Contents
*/
protected function ___parentContent(): Contents
{
return $this->parameter->parentContent;
}
}

401
var/Widget/Comments/Edit.php Executable file
View File

@@ -0,0 +1,401 @@
<?php
namespace Widget\Comments;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Widget\Base\Comments;
use Widget\ActionInterface;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 评论编辑组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Comments implements ActionInterface
{
/**
* 标记为待审核
*/
public function waitingComment()
{
$comments = $this->request->filter('int')->getArray('coid');
$updateRows = 0;
foreach ($comments as $comment) {
if ($this->mark($comment, 'waiting')) {
$updateRows++;
}
}
/** 设置提示信息 */
Notice::alloc()
->set(
$updateRows > 0 ? _t('评论已经被标记为待审核') : _t('没有评论被标记为待审核'),
$updateRows > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 评论是否可以被修改
*
* @param Query|null $condition 条件
* @return bool
* @throws Exception|\Typecho\Widget\Exception
*/
public function commentIsWriteable(?Query $condition = null): bool
{
if (empty($condition)) {
if ($this->have() && ($this->user->pass('editor', true) || $this->ownerId == $this->user->uid)) {
return true;
}
} else {
$post = $this->db->fetchRow($condition->select('ownerId')->from('table.comments')->limit(1));
if ($post && ($this->user->pass('editor', true) || $post['ownerId'] == $this->user->uid)) {
return true;
}
}
return false;
}
/**
* 标记评论状态
*
* @param integer $coid 评论主键
* @param string $status 状态
* @return boolean
* @throws Exception
*/
private function mark(int $coid, string $status): bool
{
$comment = $this->db->fetchRow($this->select()
->where('coid = ?', $coid)->limit(1), [$this, 'push']);
if ($comment && $this->commentIsWriteable()) {
/** 增加评论编辑插件接口 */
self::pluginHandle()->call('mark', $comment, $this, $status);
/** 不必更新的情况 */
if ($status == $comment['status']) {
return false;
}
/** 更新评论 */
$this->db->query($this->db->update('table.comments')
->rows(['status' => $status])->where('coid = ?', $coid));
/** 更新相关内容的评论数 */
if ('approved' == $comment['status'] && 'approved' != $status) {
$this->db->query($this->db->update('table.contents')
->expression('commentsNum', 'commentsNum - 1')
->where('cid = ? AND commentsNum > 0', $comment['cid']));
} elseif ('approved' != $comment['status'] && 'approved' == $status) {
$this->db->query($this->db->update('table.contents')
->expression('commentsNum', 'commentsNum + 1')->where('cid = ?', $comment['cid']));
}
return true;
}
return false;
}
/**
* 标记为垃圾
*
* @throws Exception
*/
public function spamComment()
{
$comments = $this->request->filter('int')->getArray('coid');
$updateRows = 0;
foreach ($comments as $comment) {
if ($this->mark($comment, 'spam')) {
$updateRows++;
}
}
/** 设置提示信息 */
Notice::alloc()
->set(
$updateRows > 0 ? _t('评论已经被标记为垃圾') : _t('没有评论被标记为垃圾'),
$updateRows > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 标记为展现
*
* @throws Exception
*/
public function approvedComment()
{
$comments = $this->request->filter('int')->getArray('coid');
$updateRows = 0;
foreach ($comments as $comment) {
if ($this->mark($comment, 'approved')) {
$updateRows++;
}
}
/** 设置提示信息 */
Notice::alloc()
->set(
$updateRows > 0 ? _t('评论已经被通过') : _t('没有评论被通过'),
$updateRows > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 删除评论
*
* @throws Exception
*/
public function deleteComment()
{
$comments = $this->request->filter('int')->getArray('coid');
$deleteRows = 0;
foreach ($comments as $coid) {
$comment = $this->db->fetchRow($this->select()
->where('coid = ?', $coid)->limit(1), [$this, 'push']);
if ($comment && $this->commentIsWriteable()) {
self::pluginHandle()->call('delete', $comment, $this);
/** 删除评论 */
$this->db->query($this->db->delete('table.comments')->where('coid = ?', $coid));
/** 更新相关内容的评论数 */
if ('approved' == $comment['status']) {
$this->db->query($this->db->update('table.contents')
->expression('commentsNum', 'commentsNum - 1')->where('cid = ?', $comment['cid']));
}
self::pluginHandle()->call('finishDelete', $comment, $this);
$deleteRows++;
}
}
if ($this->request->isAjax()) {
if ($deleteRows > 0) {
$this->response->throwJson([
'success' => 1,
'message' => _t('删除评论成功')
]);
} else {
$this->response->throwJson([
'success' => 0,
'message' => _t('删除评论失败')
]);
}
} else {
/** 设置提示信息 */
Notice::alloc()
->set(
$deleteRows > 0 ? _t('评论已经被删除') : _t('没有评论被删除'),
$deleteRows > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
}
/**
* 删除所有垃圾评论
*
* @throws Exception
*/
public function deleteSpamComment()
{
$deleteQuery = $this->db->delete('table.comments')->where('status = ?', 'spam');
if (!$this->request->is('__typecho_all_comments=on') || !$this->user->pass('editor', true)) {
$deleteQuery->where('ownerId = ?', $this->user->uid);
}
if ($this->request->is('cid')) {
$deleteQuery->where('cid = ?', $this->request->get('cid'));
}
$deleteRows = $this->db->query($deleteQuery);
/** 设置提示信息 */
Notice::alloc()->set(
$deleteRows > 0 ? _t('所有垃圾评论已经被删除') : _t('没有垃圾评论被删除'),
$deleteRows > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 获取可编辑的评论
*
* @throws Exception
*/
public function getComment()
{
$coid = $this->request->filter('int')->get('coid');
$comment = $this->db->fetchRow($this->select()
->where('coid = ?', $coid)->limit(1), [$this, 'push']);
if ($comment && $this->commentIsWriteable()) {
$this->response->throwJson([
'success' => 1,
'comment' => $comment
]);
} else {
$this->response->throwJson([
'success' => 0,
'message' => _t('获取评论失败')
]);
}
}
/**
* 编辑评论
*
* @return bool
* @throws Exception
*/
public function editComment(): bool
{
$coid = $this->request->filter('int')->get('coid');
$commentSelect = $this->db->fetchRow($this->select()
->where('coid = ?', $coid)->limit(1), [$this, 'push']);
if ($commentSelect && $this->commentIsWriteable()) {
$comment['text'] = $this->request->get('text');
$comment['author'] = $this->request->filter('strip_tags', 'trim', 'xss')->get('author');
$comment['mail'] = $this->request->filter('strip_tags', 'trim', 'xss')->get('mail');
$comment['url'] = $this->request->filter('url')->get('url');
if ($this->request->is('created')) {
$comment['created'] = $this->request->filter('int')->get('created');
}
/** 评论插件接口 */
$comment = self::pluginHandle()->filter('edit', $comment, $this);
/** 更新评论 */
$this->update($comment, $this->db->sql()->where('coid = ?', $coid));
$updatedComment = $this->db->fetchRow($this->select()
->where('coid = ?', $coid)->limit(1), [$this, 'push']);
$updatedComment['content'] = $this->content;
/** 评论插件接口 */
self::pluginHandle()->call('finishEdit', $this);
$this->response->throwJson([
'success' => 1,
'comment' => $updatedComment
]);
}
$this->response->throwJson([
'success' => 0,
'message' => _t('修评论失败')
]);
}
/**
* 回复评论
*
* @throws Exception
*/
public function replyComment()
{
$coid = $this->request->filter('int')->get('coid');
$commentSelect = $this->db->fetchRow($this->select()
->where('coid = ?', $coid)->limit(1), [$this, 'push']);
if ($commentSelect && $this->commentIsWriteable()) {
$comment = [
'cid' => $commentSelect['cid'],
'created' => $this->options->time,
'agent' => $this->request->getAgent(),
'ip' => $this->request->getIp(),
'ownerId' => $commentSelect['ownerId'],
'authorId' => $this->user->uid,
'type' => 'comment',
'author' => $this->user->screenName,
'mail' => $this->user->mail,
'url' => $this->user->url,
'parent' => $coid,
'text' => $this->request->get('text'),
'status' => 'approved'
];
/** 评论插件接口 */
self::pluginHandle()->call('comment', $comment, $this);
/** 回复评论 */
$commentId = $this->insert($comment);
$insertComment = $this->db->fetchRow($this->select()
->where('coid = ?', $commentId)->limit(1), [$this, 'push']);
$insertComment['content'] = $this->content;
/** 评论完成接口 */
self::pluginHandle()->call('finishComment', $this);
$this->response->throwJson([
'success' => 1,
'comment' => $insertComment
]);
}
$this->response->throwJson([
'success' => 0,
'message' => _t('回复评论失败')
]);
}
/**
* 初始化函数
*
* @access public
* @return void
*/
public function action()
{
$this->user->pass('contributor');
$this->security->protect();
$this->on($this->request->is('do=waiting'))->waitingComment();
$this->on($this->request->is('do=spam'))->spamComment();
$this->on($this->request->is('do=approved'))->approvedComment();
$this->on($this->request->is('do=delete'))->deleteComment();
$this->on($this->request->is('do=delete-spam'))->deleteSpamComment();
$this->on($this->request->is('do=get&coid'))->getComment();
$this->on($this->request->is('do=edit&coid'))->editComment();
$this->on($this->request->is('do=reply&coid'))->replyComment();
$this->response->redirect($this->options->adminUrl);
}
}

149
var/Widget/Comments/Ping.php Executable file
View File

@@ -0,0 +1,149 @@
<?php
namespace Widget\Comments;
use Typecho\Config;
use Typecho\Db\Exception;
use Widget\Base\Comments;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 回响归档组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Ping extends Comments
{
/**
* _customSinglePingCallback
*
* @var boolean
* @access private
*/
private bool $customSinglePingCallback = false;
/**
* @param Config $parameter
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault('parentId=0');
/** 初始化回调函数 */
if (function_exists('singlePing')) {
$this->customSinglePingCallback = true;
}
}
/**
* 输出文章回响数
*
* @param mixed ...$args 评论数格式化数据
*/
public function num(...$args)
{
if (empty($args)) {
$args[] = '%d';
}
echo sprintf($args[$this->length] ?? array_pop($args), $this->length);
}
/**
* execute
*
* @access public
* @return void
* @throws Exception
*/
public function execute()
{
if (!$this->parameter->parentId) {
return;
}
$select = $this->select()->where('table.comments.status = ?', 'approved')
->where('table.comments.cid = ?', $this->parameter->parentId)
->where('table.comments.type <> ?', 'comment')
->order('table.comments.coid', 'ASC');
$this->db->fetchAll($select, [$this, 'push']);
}
/**
* 列出回响
*
* @param mixed $singlePingOptions 单个回响自定义选项
*/
public function listPings($singlePingOptions = null)
{
if ($this->have()) {
//初始化一些变量
$parsedSinglePingOptions = Config::factory($singlePingOptions);
$parsedSinglePingOptions->setDefault([
'before' => '<ol class="ping-list">',
'after' => '</ol>',
'beforeTitle' => '',
'afterTitle' => '',
'beforeDate' => '',
'afterDate' => '',
'dateFormat' => $this->options->commentDateFormat
]);
echo $parsedSinglePingOptions->before;
while ($this->next()) {
$this->singlePingCallback($parsedSinglePingOptions);
}
echo $parsedSinglePingOptions->after;
}
}
/**
* 回响回调函数
*
* @param string $singlePingOptions 单个回响自定义选项
*/
private function singlePingCallback(string $singlePingOptions): void
{
if ($this->customSinglePingCallback) {
singlePing($this, $singlePingOptions);
return;
}
?>
<li id="<?php $this->theId(); ?>" class="ping-body">
<div class="ping-title">
<cite class="fn"><?php
$singlePingOptions->beforeTitle();
$this->author(true);
$singlePingOptions->afterTitle();
?></cite>
</div>
<div class="ping-meta">
<a href="<?php $this->permalink(); ?>"><?php $singlePingOptions->beforeDate();
$this->date($singlePingOptions->dateFormat);
$singlePingOptions->afterDate(); ?></a>
</div>
<?php $this->content(); ?>
</li>
<?php
}
/**
* 重载内容获取
*
* @return array|null
*/
protected function ___parentContent(): ?array
{
return $this->parameter->parentContent;
}
}

60
var/Widget/Comments/Recent.php Executable file
View File

@@ -0,0 +1,60 @@
<?php
namespace Widget\Comments;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Exception;
use Widget\Base\Comments;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 最近评论组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Recent extends Comments
{
/**
* @param Config $parameter
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault(
['pageSize' => $this->options->commentsListSize, 'parentId' => 0, 'ignoreAuthor' => false]
);
}
/**
* 执行函数
*
* @throws Exception
*/
public function execute()
{
$select = $this->select()->limit($this->parameter->pageSize)
->where('table.comments.status = ?', 'approved')
->order('table.comments.coid', Db::SORT_DESC);
if ($this->parameter->parentId) {
$select->where('cid = ?', $this->parameter->parentId);
}
if ($this->options->commentsShowCommentOnly) {
$select->where('type = ?', 'comment');
}
/** 忽略作者评论 */
if ($this->parameter->ignoreAuthor) {
$select->where('ownerId <> authorId');
}
$this->db->fetchAll($select, [$this, 'push']);
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Widget\Contents;
use Typecho\Config;
use Typecho\Db\Exception as DbException;
use Typecho\Db\Query;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\PageNavigator\Box;
/**
* 文章管理列表组件
*
* @property-read array? $revision
*/
trait AdminTrait
{
/**
* 所有文章个数
*
* @var integer|null
*/
private ?int $total;
/**
* 当前页
*
* @var integer
*/
private int $currentPage;
/**
* @return void
*/
protected function initPage()
{
$this->parameter->setDefault('pageSize=20');
$this->currentPage = $this->request->filter('int')->get('page', 1);
}
/**
* @param Query $select
* @return void
*/
protected function searchQuery(Query $select)
{
if ($this->request->is('keywords')) {
$keywords = $this->request->filter('search')->get('keywords');
$args = [];
$keywordsList = explode(' ', $keywords);
$args[] = implode(' OR ', array_fill(0, count($keywordsList), 'table.contents.title LIKE ?'));
foreach ($keywordsList as $keyword) {
$args[] = '%' . $keyword . '%';
}
$select->where(...$args);
}
}
/**
* @param Query $select
* @return void
*/
protected function countTotal(Query $select)
{
$countSql = clone $select;
$this->total = $this->size($countSql);
}
/**
* 输出分页
*
* @throws Exception
* @throws DbException
*/
public function pageNav()
{
$query = $this->request->makeUriByRequest('page={page}');
/** 使用盒状分页 */
$nav = new Box(
$this->total,
$this->currentPage,
$this->parameter->pageSize,
$query
);
$nav->render('&laquo;', '&raquo;');
}
/**
* @return array|null
* @throws DbException
*/
protected function ___revision(): ?array
{
return $this->db->fetchRow(
$this->select('cid', 'modified')
->where(
'table.contents.parent = ? AND table.contents.type = ?',
$this->cid,
'revision'
)
->limit(1)
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Widget\Contents\Attachment;
use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Exception;
use Widget\Base\Contents;
use Widget\Contents\AdminTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 文件管理列表组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Admin extends Contents
{
use AdminTrait;
/**
* 执行函数
*
* @return void
* @throws Exception|\Typecho\Widget\Exception
*/
public function execute()
{
$this->initPage();
/** 构建基础查询 */
$select = $this->select()->where('table.contents.type = ?', 'attachment');
/** 如果具有编辑以上权限,可以查看所有文件,反之只能查看自己的文件 */
if (!$this->user->pass('editor', true)) {
$select->where('table.contents.authorId = ?', $this->user->uid);
}
/** 过滤标题 */
$this->searchQuery($select);
$this->countTotal($select);
/** 提交查询 */
$select->order('table.contents.created', Db::SORT_DESC)
->page($this->currentPage, $this->parameter->pageSize);
$this->db->fetchAll($select, [$this, 'push']);
}
/**
* 所属文章
*
* @return Config
* @throws Exception
*/
protected function ___parentPost(): Config
{
return new Config($this->db->fetchRow(
$this->select()->where('table.contents.cid = ?', $this->parent)->limit(1)
));
}
}

View File

@@ -0,0 +1,331 @@
<?php
namespace Widget\Contents\Attachment;
use Typecho\Common;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Layout;
use Widget\ActionInterface;
use Widget\Base\Contents;
use Widget\Contents\PrepareEditTrait;
use Widget\Notice;
use Widget\Upload;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑文章组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Contents implements ActionInterface
{
use PrepareEditTrait;
/**
* 执行函数
*
* @throws Exception|\Typecho\Db\Exception
*/
public function execute()
{
/** 必须为贡献者以上权限 */
$this->user->pass('contributor');
}
/**
* 判断文件名转换到缩略名后是否合法
*
* @param string $name 文件名
* @return boolean
*/
public function nameToSlug(string $name): bool
{
if (empty($this->request->slug)) {
$slug = Common::slugName($name);
if (empty($slug) || !$this->slugExists($name)) {
return false;
}
}
return true;
}
/**
* 判断文件缩略名是否存在
*
* @param string $slug 缩略名
* @return boolean
* @throws \Typecho\Db\Exception
*/
public function slugExists(string $slug): bool
{
$select = $this->db->select()
->from('table.contents')
->where('type = ?', 'attachment')
->where('slug = ?', Common::slugName($slug))
->limit(1);
if ($this->request->is('cid')) {
$select->where('cid <> ?', $this->request->get('cid'));
}
$attachment = $this->db->fetchRow($select);
return !$attachment;
}
/**
* 更新文件
*
* @throws \Typecho\Db\Exception
* @throws Exception
*/
public function updateAttachment()
{
if ($this->form()->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$input = $this->request->from('name', 'slug', 'description');
$input['slug'] = Common::slugName(Common::strBy($input['slug'] ?? null, $input['name']));
$attachment['title'] = $input['name'];
$attachment['slug'] = $input['slug'];
$content = $this->attachment->toArray();
$content['description'] = $input['description'];
$attachment['text'] = json_encode($content);
$cid = $this->request->filter('int')->get('cid');
/** 更新数据 */
$updateRows = $this->update($attachment, $this->db->sql()->where('cid = ?', $cid));
if ($updateRows > 0) {
$this->db->fetchRow($this->select()
->where('table.contents.type = ?', 'attachment')
->where('table.contents.cid = ?', $cid)
->limit(1), [$this, 'push']);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 提示信息 */
Notice::alloc()->set('publish' == $this->status ?
_t('文件 <a href="%s">%s</a> 已经被更新', $this->permalink, $this->title) :
_t('未归档文件 %s 已经被更新', $this->title), 'success');
}
/** 转向原页 */
$this->response->redirect(Common::url('manage-medias.php?' .
$this->getPageOffsetQuery($cid, $this->status), $this->options->adminUrl));
}
/**
* 生成表单
*
* @return Form
*/
public function form(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/contents-attachment-edit'), Form::POST_METHOD);
/** 文件名称 */
$name = new Form\Element\Text('name', null, $this->title, _t('标题') . ' *');
$form->addInput($name);
/** 文件缩略名 */
$slug = new Form\Element\Text(
'slug',
null,
$this->slug,
_t('缩略名'),
_t('文件缩略名用于创建友好的链接形式,建议使用字母,数字,下划线和横杠.')
);
$form->addInput($slug);
/** 文件描述 */
$description = new Form\Element\Textarea(
'description',
null,
$this->attachment->description,
_t('描述'),
_t('此文字用于描述文件,在有的主题中它会被显示.')
);
$form->addInput($description);
/** 分类动作 */
$do = new Form\Element\Hidden('do', null, 'update');
$form->addInput($do);
/** 分类主键 */
$cid = new Form\Element\Hidden('cid', null, $this->cid);
$form->addInput($cid);
/** 提交按钮 */
$submit = new Form\Element\Submit(null, null, _t('提交修改'));
$submit->input->setAttribute('class', 'btn primary');
$delete = new Layout('a', [
'href' => $this->security->getIndex('/action/contents-attachment-edit?do=delete&cid=' . $this->cid),
'class' => 'operate-delete',
'lang' => _t('你确认删除文件 %s 吗?', $this->attachment->name)
]);
$submit->container($delete->html(_t('删除文件')));
$form->addItem($submit);
$name->addRule('required', _t('必须填写文件标题'));
$name->addRule([$this, 'nameToSlug'], _t('文件标题无法被转换为缩略名'));
$slug->addRule([$this, 'slugExists'], _t('缩略名已经存在'));
return $form;
}
/**
* 获取页面偏移的URL Query
*
* @param integer $cid 文件id
* @param string|null $status 状态
* @return string
* @throws \Typecho\Db\Exception|Exception
*/
protected function getPageOffsetQuery(int $cid, string $status = null): string
{
return 'page=' . $this->getPageOffset(
'cid',
$cid,
'attachment',
$status,
$this->user->pass('editor', true) ? 0 : $this->user->uid
);
}
/**
* 删除文章
*
* @throws \Typecho\Db\Exception
*/
public function deleteAttachment()
{
$posts = $this->request->filter('int')->getArray('cid');
$deleteCount = 0;
$this->deleteByIds($posts, $deleteCount);
if ($this->request->isAjax()) {
$this->response->throwJson($deleteCount > 0 ? ['code' => 200, 'message' => _t('文件已经被删除')]
: ['code' => 500, 'message' => _t('没有文件被删除')]);
} else {
/** 设置提示信息 */
Notice::alloc()
->set(
$deleteCount > 0 ? _t('文件已经被删除') : _t('没有文件被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->redirect(Common::url('manage-medias.php', $this->options->adminUrl));
}
}
/**
* clearAttachment
*
* @access public
* @return void
* @throws \Typecho\Db\Exception
*/
public function clearAttachment()
{
$page = 1;
$deleteCount = 0;
do {
$posts = array_column($this->db->fetchAll($this->db->select('cid')
->from('table.contents')
->where('type = ? AND parent = ?', 'attachment', 0)
->page($page, 100)), 'cid');
$page++;
$this->deleteByIds($posts, $deleteCount);
} while (count($posts) == 100);
/** 设置提示信息 */
Notice::alloc()->set(
$deleteCount > 0 ? _t('未归档文件已经被清理') : _t('没有未归档文件被清理'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->redirect(Common::url('manage-medias.php', $this->options->adminUrl));
}
/**
* @return $this
* @throws Exception
* @throws \Typecho\Db\Exception
*/
public function prepare(): self
{
return $this->prepareEdit('attachment', false, _t('文件不存在'));
}
/**
* 绑定动作
*
* @access public
* @return void
*/
public function action()
{
$this->security->protect();
$this->on($this->request->is('do=delete'))->deleteAttachment();
$this->on($this->request->is('do=update'))
->prepare()->updateAttachment();
$this->on($this->request->is('do=clear'))->clearAttachment();
$this->response->redirect($this->options->adminUrl);
}
/**
* @param array $posts
* @param int $deleteCount
* @return void
*/
protected function deleteByIds(array $posts, int &$deleteCount): void
{
foreach ($posts as $post) {
// 删除插件接口
self::pluginHandle()->call('delete', $post, $this);
$condition = $this->db->sql()->where('cid = ?', $post);
$row = $this->db->fetchRow($this->select()
->where('table.contents.type = ?', 'attachment')
->where('table.contents.cid = ?', $post)
->limit(1), [$this, 'push']);
if ($this->isWriteable(clone $condition) && $this->delete($condition)) {
/** 删除文件 */
Upload::deleteHandle($this->toColumn(['cid', 'attachment', 'parent']));
/** 删除评论 */
$this->db->query($this->db->delete('table.comments')
->where('cid = ?', $post));
// 完成删除插件接口
self::pluginHandle()->call('finishDelete', $post, $this);
$deleteCount++;
}
unset($condition);
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Widget\Contents\Attachment;
use Typecho\Db;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 文章相关文件组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Related extends Contents
{
/**
* 执行函数
*
* @access public
* @return void
* @throws Db\Exception
*/
public function execute()
{
$this->parameter->setDefault('parentId=0&limit=0');
//如果没有cid值
if (!$this->parameter->parentId) {
return;
}
/** 构建基础查询 */
$select = $this->select()->where('table.contents.type = ?', 'attachment');
//order字段在文件里代表所属文章
$select->where('table.contents.parent = ?', $this->parameter->parentId);
/** 提交查询 */
$select->order('table.contents.created');
if ($this->parameter->limit > 0) {
$select->limit($this->parameter->limit);
}
if ($this->parameter->offset > 0) {
$select->offset($this->parameter->offset);
}
$this->db->fetchAll($select, [$this, 'push']);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Widget\Contents\Attachment;
use Typecho\Db;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 没有关联的文件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
* @version $Id$
*/
/**
* 没有关联的文件组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Unattached extends Contents
{
/**
* 执行函数
*
* @access public
* @return void
* @throws Db\Exception
*/
public function execute()
{
/** 构建基础查询 */
$select = $this->select()->where('table.contents.type = ? AND
(table.contents.parent = 0 OR table.contents.parent IS NULL)', 'attachment');
/** 加上对用户的判断 */
$select->where('table.contents.authorId = ?', $this->user->uid);
/** 提交查询 */
$select->order('table.contents.created', Db::SORT_DESC);
$this->db->fetchAll($select, [$this, 'push']);
}
}

775
var/Widget/Contents/EditTrait.php Executable file
View File

@@ -0,0 +1,775 @@
<?php
namespace Widget\Contents;
use Typecho\Config;
use Typecho\Db\Exception as DbException;
use Typecho\Validate;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form\Element;
use Typecho\Widget\Helper\Layout;
use Widget\Base\Contents;
use Widget\Base\Metas;
/**
* 内容编辑组件
*/
trait EditTrait
{
/**
* 删除自定义字段
*
* @param integer $cid
* @return integer
* @throws DbException
*/
public function deleteFields(int $cid): int
{
return $this->db->query($this->db->delete('table.fields')
->where('cid = ?', $cid));
}
/**
* 保存自定义字段
*
* @param array $fields
* @param mixed $cid
* @return void
* @throws Exception
*/
public function applyFields(array $fields, $cid)
{
$exists = array_flip(array_column($this->db->fetchAll($this->db->select('name')
->from('table.fields')->where('cid = ?', $cid)), 'name'));
foreach ($fields as $name => $value) {
$type = 'str';
if (is_array($value) && 2 == count($value)) {
$type = $value[0];
$value = $value[1];
} elseif (strpos($name, ':') > 0) {
[$type, $name] = explode(':', $name, 2);
}
if (!$this->checkFieldName($name)) {
continue;
}
$isFieldReadOnly = Contents::pluginHandle()->trigger($plugged)->call('isFieldReadOnly', $name);
if ($plugged && $isFieldReadOnly) {
continue;
}
if (isset($exists[$name])) {
unset($exists[$name]);
}
$this->setField($name, $type, $value, $cid);
}
foreach ($exists as $name => $value) {
$this->db->query($this->db->delete('table.fields')
->where('cid = ? AND name = ?', $cid, $name));
}
}
/**
* 检查字段名是否符合要求
*
* @param string $name
* @return boolean
*/
public function checkFieldName(string $name): bool
{
return preg_match("/^[_a-z][_a-z0-9]*$/i", $name);
}
/**
* 设置单个字段
*
* @param string $name
* @param string $type
* @param mixed $value
* @param integer $cid
* @return integer|bool
* @throws Exception
*/
public function setField(string $name, string $type, $value, int $cid)
{
if (
empty($name) || !$this->checkFieldName($name)
|| !in_array($type, ['str', 'int', 'float', 'json'])
) {
return false;
}
if ($type === 'json') {
$value = json_encode($value);
}
$exist = $this->db->fetchRow($this->db->select('cid')->from('table.fields')
->where('cid = ? AND name = ?', $cid, $name));
$rows = [
'type' => $type,
'str_value' => 'str' == $type || 'json' == $type ? $value : null,
'int_value' => 'int' == $type ? intval($value) : 0,
'float_value' => 'float' == $type ? floatval($value) : 0
];
if (empty($exist)) {
$rows['cid'] = $cid;
$rows['name'] = $name;
return $this->db->query($this->db->insert('table.fields')->rows($rows));
} else {
return $this->db->query($this->db->update('table.fields')
->rows($rows)
->where('cid = ? AND name = ?', $cid, $name));
}
}
/**
* 自增一个整形字段
*
* @param string $name
* @param integer $value
* @param integer $cid
* @return integer
* @throws Exception
*/
public function incrIntField(string $name, int $value, int $cid)
{
if (!$this->checkFieldName($name)) {
return false;
}
$exist = $this->db->fetchRow($this->db->select('type')->from('table.fields')
->where('cid = ? AND name = ?', $cid, $name));
if (empty($exist)) {
return $this->db->query($this->db->insert('table.fields')
->rows([
'cid' => $cid,
'name' => $name,
'type' => 'int',
'str_value' => null,
'int_value' => $value,
'float_value' => 0
]));
} else {
$struct = [
'str_value' => null,
'float_value' => null
];
if ('int' != $exist['type']) {
$struct['type'] = 'int';
}
return $this->db->query($this->db->update('table.fields')
->rows($struct)
->expression('int_value', 'int_value ' . ($value >= 0 ? '+' : '') . $value)
->where('cid = ? AND name = ?', $cid, $name));
}
}
/**
* getFieldItems
*
* @throws DbException
*/
public function getFieldItems(): array
{
$fields = [];
if ($this->have()) {
$defaultFields = $this->getDefaultFieldItems();
$rows = $this->db->fetchAll($this->db->select()->from('table.fields')
->where('cid = ?', isset($this->draft) ? $this->draft['cid'] : $this->cid));
foreach ($rows as $row) {
$isFieldReadOnly = Contents::pluginHandle()
->trigger($plugged)->call('isFieldReadOnly', $row['name']);
if ($plugged && $isFieldReadOnly) {
continue;
}
$isFieldReadOnly = static::pluginHandle()
->trigger($plugged)->call('isFieldReadOnly', $row['name']);
if ($plugged && $isFieldReadOnly) {
continue;
}
if (!isset($defaultFields[$row['name']])) {
$fields[] = $row;
}
}
}
return $fields;
}
/**
* @return array
*/
public function getDefaultFieldItems(): array
{
$defaultFields = [];
$configFile = $this->options->themeFile($this->options->theme, 'functions.php');
$layout = new Layout();
$fields = new Config();
if ($this->have()) {
$fields = $this->fields;
}
Contents::pluginHandle()->call('getDefaultFieldItems', $layout);
static::pluginHandle()->call('getDefaultFieldItems', $layout);
if (file_exists($configFile)) {
require_once $configFile;
if (function_exists('themeFields')) {
themeFields($layout);
}
$func = $this->getThemeFieldsHook();
if (function_exists($func)) {
call_user_func($func, $layout);
}
}
$items = $layout->getItems();
foreach ($items as $item) {
if ($item instanceof Element) {
$name = $item->input->getAttribute('name');
$isFieldReadOnly = Contents::pluginHandle()
->trigger($plugged)->call('isFieldReadOnly', $name);
if ($plugged && $isFieldReadOnly) {
continue;
}
if (preg_match("/^fields\[(.+)\]$/", $name, $matches)) {
$name = $matches[1];
} else {
$inputName = 'fields[' . $name . ']';
if (preg_match("/^(.+)\[\]$/", $name, $matches)) {
$name = $matches[1];
$inputName = 'fields[' . $name . '][]';
}
foreach ($item->inputs as $input) {
$input->setAttribute('name', $inputName);
}
}
if (isset($fields->{$name})) {
$item->value($fields->{$name});
}
$elements = $item->container->getItems();
array_shift($elements);
$div = new Layout('div');
foreach ($elements as $el) {
$div->addItem($el);
}
$defaultFields[$name] = [$item->label, $div];
}
}
return $defaultFields;
}
/**
* 获取自定义字段的hook名称
*
* @return string
*/
abstract protected function getThemeFieldsHook(): string;
/**
* getFields
*
* @return array
*/
protected function getFields(): array
{
$fields = [];
$fieldNames = $this->request->getArray('fieldNames');
if (!empty($fieldNames)) {
$data = [
'fieldNames' => $this->request->getArray('fieldNames'),
'fieldTypes' => $this->request->getArray('fieldTypes'),
'fieldValues' => $this->request->getArray('fieldValues')
];
foreach ($data['fieldNames'] as $key => $val) {
$val = trim($val);
if (0 == strlen($val)) {
continue;
}
$fields[$val] = [$data['fieldTypes'][$key], $data['fieldValues'][$key]];
}
}
$customFields = $this->request->getArray('fields');
foreach ($customFields as $key => $val) {
$fields[$key] = [is_array($val) ? 'json' : 'str', $val];
}
return $fields;
}
/**
* 删除内容
*
* @param integer $cid 草稿id
* @throws DbException
*/
protected function deleteContent(int $cid, bool $hasMetas = true)
{
$this->delete($this->db->sql()->where('cid = ?', $cid));
if ($hasMetas) {
/** 删除草稿分类 */
$this->setCategories($cid, [], false, false);
/** 删除标签 */
$this->setTags($cid, null, false, false);
}
}
/**
* 根据提交值获取created字段值
*
* @return integer
*/
protected function getCreated(): int
{
$created = $this->options->time;
if ($this->request->is('created')) {
$created = $this->request->get('created');
} elseif ($this->request->is('date')) {
$dstOffset = $this->request->get('dst', 0);
$timezoneSymbol = $this->options->timezone >= 0 ? '+' : '-';
$timezoneOffset = abs($this->options->timezone);
$timezone = $timezoneSymbol . str_pad($timezoneOffset / 3600, 2, '0', STR_PAD_LEFT) . ':00';
[$date, $time] = explode(' ', $this->request->get('date'));
$created = strtotime("{$date}T{$time}{$timezone}") - $dstOffset;
} elseif ($this->request->is('year&month&day')) {
$second = $this->request->filter('int')->get('sec', date('s'));
$min = $this->request->filter('int')->get('min', date('i'));
$hour = $this->request->filter('int')->get('hour', date('H'));
$year = $this->request->filter('int')->get('year');
$month = $this->request->filter('int')->get('month');
$day = $this->request->filter('int')->get('day');
$created = mktime($hour, $min, $second, $month, $day, $year)
- $this->options->timezone + $this->options->serverTimezone;
} elseif ($this->have() && $this->created > 0) {
//如果是修改文章
$created = $this->created;
} elseif ($this->request->is('do=save')) {
// 如果是草稿而且没有任何输入则保持原状
$created = 0;
}
return $created;
}
/**
* 设置分类
*
* @param integer $cid 内容id
* @param array $categories 分类id的集合数组
* @param boolean $beforeCount 是否参与计数
* @param boolean $afterCount 是否参与计数
* @throws DbException
*/
protected function setCategories(int $cid, array $categories, bool $beforeCount = true, bool $afterCount = true)
{
$categories = array_unique(array_map('trim', $categories));
/** 取出已有category */
$existCategories = array_column(
$this->db->fetchAll(
$this->db->select('table.metas.mid')
->from('table.metas')
->join('table.relationships', 'table.relationships.mid = table.metas.mid')
->where('table.relationships.cid = ?', $cid)
->where('table.metas.type = ?', 'category')
),
'mid'
);
/** 删除已有category */
if ($existCategories) {
foreach ($existCategories as $category) {
$this->db->query($this->db->delete('table.relationships')
->where('cid = ?', $cid)
->where('mid = ?', $category));
if ($beforeCount) {
$this->db->query($this->db->update('table.metas')
->expression('count', 'count - 1')
->where('mid = ?', $category));
}
}
}
/** 插入category */
if ($categories) {
foreach ($categories as $category) {
/** 如果分类不存在 */
if (
!$this->db->fetchRow(
$this->db->select('mid')
->from('table.metas')
->where('mid = ?', $category)
->limit(1)
)
) {
continue;
}
$this->db->query($this->db->insert('table.relationships')
->rows([
'mid' => $category,
'cid' => $cid
]));
if ($afterCount) {
$this->db->query($this->db->update('table.metas')
->expression('count', 'count + 1')
->where('mid = ?', $category));
}
}
}
}
/**
* 设置内容标签
*
* @param integer $cid
* @param string|null $tags
* @param boolean $beforeCount 是否参与计数
* @param boolean $afterCount 是否参与计数
* @throws DbException
*/
protected function setTags(int $cid, ?string $tags, bool $beforeCount = true, bool $afterCount = true)
{
$tags = str_replace('', ',', $tags ?? '');
$tags = array_unique(array_map('trim', explode(',', $tags)));
$tags = array_filter($tags, [Validate::class, 'xssCheck']);
/** 取出已有tag */
$existTags = array_column(
$this->db->fetchAll(
$this->db->select('table.metas.mid')
->from('table.metas')
->join('table.relationships', 'table.relationships.mid = table.metas.mid')
->where('table.relationships.cid = ?', $cid)
->where('table.metas.type = ?', 'tag')
),
'mid'
);
/** 删除已有tag */
if ($existTags) {
foreach ($existTags as $tag) {
if (0 == strlen($tag)) {
continue;
}
$this->db->query($this->db->delete('table.relationships')
->where('cid = ?', $cid)
->where('mid = ?', $tag));
if ($beforeCount) {
$this->db->query($this->db->update('table.metas')
->expression('count', 'count - 1')
->where('mid = ?', $tag));
}
}
}
/** 取出插入tag */
$insertTags = Metas::alloc()->scanTags($tags);
/** 插入tag */
if ($insertTags) {
foreach ($insertTags as $tag) {
if (0 == strlen($tag)) {
continue;
}
$this->db->query($this->db->insert('table.relationships')
->rows([
'mid' => $tag,
'cid' => $cid
]));
if ($afterCount) {
$this->db->query($this->db->update('table.metas')
->expression('count', 'count + 1')
->where('mid = ?', $tag));
}
}
}
}
/**
* 同步附件
*
* @param integer $cid 内容id
* @throws DbException
*/
protected function attach(int $cid)
{
$attachments = $this->request->getArray('attachment');
if (!empty($attachments)) {
foreach ($attachments as $key => $attachment) {
$this->db->query($this->db->update('table.contents')->rows([
'parent' => $cid,
'status' => 'publish',
'order' => $key + 1
])->where('cid = ? AND type = ?', $attachment, 'attachment'));
}
}
}
/**
* 取消附件关联
*
* @param integer $cid 内容id
* @throws DbException
*/
protected function unAttach(int $cid)
{
$this->db->query($this->db->update('table.contents')->rows(['parent' => 0, 'status' => 'publish'])
->where('parent = ? AND type = ?', $cid, 'attachment'));
}
/**
* 发布内容
*
* @param array $contents 内容结构
* @param boolean $hasMetas 是否有metas
* @throws DbException|Exception
*/
protected function publish(array $contents, bool $hasMetas = true)
{
/** 发布内容, 检查是否具有直接发布的权限 */
$this->checkStatus($contents);
/** 真实的内容id */
$realId = 0;
/** 是否是从草稿状态发布 */
$isDraftToPublish = false;
$isBeforePublish = false;
$isAfterPublish = 'publish' === $contents['status'];
/** 重新发布现有内容 */
if ($this->have()) {
$isDraftToPublish = preg_match("/_draft$/", $this->type);
$isBeforePublish = 'publish' === $this->status;
/** 如果它本身不是草稿, 需要删除其草稿 */
if (!$isDraftToPublish && $this->draft) {
$cid = $this->draft['cid'];
$this->deleteContent($cid);
$this->deleteFields($cid);
}
/** 直接将草稿状态更改 */
if ($this->update($contents, $this->db->sql()->where('cid = ?', $this->cid))) {
$realId = $this->cid;
}
} else {
/** 发布一个新内容 */
$realId = $this->insert($contents);
}
if ($realId > 0) {
if ($hasMetas) {
/** 插入分类 */
if (array_key_exists('category', $contents)) {
$this->setCategories(
$realId,
!empty($contents['category']) && is_array($contents['category'])
? $contents['category'] : [$this->options->defaultCategory],
!$isDraftToPublish && $isBeforePublish,
$isAfterPublish
);
}
/** 插入标签 */
if (array_key_exists('tags', $contents)) {
$this->setTags($realId, $contents['tags'], !$isDraftToPublish && $isBeforePublish, $isAfterPublish);
}
}
/** 同步附件 */
$this->attach($realId);
/** 保存自定义字段 */
$this->applyFields($this->getFields(), $realId);
$this->db->fetchRow($this->select()
->where('table.contents.cid = ?', $realId)->limit(1), [$this, 'push']);
}
}
/**
* 保存内容
*
* @param array $contents 内容结构
* @param boolean $hasMetas 是否有metas
* @return integer
* @throws DbException|Exception
*/
protected function save(array $contents, bool $hasMetas = true): int
{
/** 发布内容, 检查是否具有直接发布的权限 */
$this->checkStatus($contents);
/** 真实的内容id */
$realId = 0;
/** 如果草稿已经存在 */
if ($this->draft) {
$isRevision = !preg_match("/_draft$/", $this->type);
if ($isRevision) {
$contents['parent'] = $this->cid;
$contents['type'] = 'revision';
}
/** 直接将草稿状态更改 */
if ($this->update($contents, $this->db->sql()->where('cid = ?', $this->draft['cid']))) {
$realId = $this->draft['cid'];
}
} else {
if ($this->have()) {
$contents['parent'] = $this->cid;
$contents['type'] = 'revision';
}
/** 发布一个新内容 */
$realId = $this->insert($contents);
if (!$this->have()) {
$this->db->fetchRow(
$this->select()->where('table.contents.cid = ?', $realId)->limit(1),
[$this, 'push']
);
}
}
if ($realId > 0) {
if ($hasMetas) {
/** 插入分类 */
if (array_key_exists('category', $contents)) {
$this->setCategories($realId, !empty($contents['category']) && is_array($contents['category']) ?
$contents['category'] : [$this->options->defaultCategory], false, false);
}
/** 插入标签 */
if (array_key_exists('tags', $contents)) {
$this->setTags($realId, $contents['tags'], false, false);
}
}
/** 同步附件 */
$this->attach($this->cid);
/** 保存自定义字段 */
$this->applyFields($this->getFields(), $realId);
return $realId;
}
return $this->draft['cid'];
}
/**
* 获取页面偏移
*
* @param string $column 字段名
* @param integer $offset 偏移值
* @param string $type 类型
* @param string|null $status 状态值
* @param integer $authorId 作者
* @param integer $pageSize 分页值
* @return integer
* @throws DbException
*/
protected function getPageOffset(
string $column,
int $offset,
string $type,
?string $status = null,
int $authorId = 0,
int $pageSize = 20
): int {
$select = $this->db->select(['COUNT(table.contents.cid)' => 'num'])->from('table.contents')
->where("table.contents.{$column} > {$offset}")
->where(
"table.contents.type = ? OR (table.contents.type = ? AND table.contents.parent = ?)",
$type,
$type . '_draft',
0
);
if (!empty($status)) {
$select->where("table.contents.status = ?", $status);
}
if ($authorId > 0) {
$select->where('table.contents.authorId = ?', $authorId);
}
$count = $this->db->fetchObject($select)->num + 1;
return ceil($count / $pageSize);
}
/**
* @param array $contents
* @return void
* @throws DbException
* @throws Exception
*/
private function checkStatus(array &$contents)
{
if ($this->user->pass('editor', true)) {
if (empty($contents['visibility'])) {
$contents['status'] = 'publish';
} elseif (
!in_array($contents['visibility'], ['private', 'waiting', 'publish', 'hidden'])
) {
if (empty($contents['password']) || 'password' != $contents['visibility']) {
$contents['password'] = '';
}
$contents['status'] = 'publish';
} else {
$contents['status'] = $contents['visibility'];
$contents['password'] = '';
}
} else {
$contents['status'] = 'waiting';
$contents['password'] = '';
}
}
}

92
var/Widget/Contents/From.php Executable file
View File

@@ -0,0 +1,92 @@
<?php
namespace Widget\Contents;
use Typecho\Config;
use Typecho\Db\Exception;
use Widget\Base\Contents;
use Widget\Base\TreeTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 单个内容组件
*/
class From extends Contents
{
use TreeTrait {
initParameter as initTreeParameter;
___directory as ___treeDirectory;
}
/**
* @param Config $parameter
* @return void
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault([
'cid' => null,
'query' => null,
]);
}
/**
* @return void
* @throws Exception
*/
public function execute()
{
$query = null;
if (isset($this->parameter->cid)) {
$query = $this->select()->where('cid = ?', $this->parameter->cid);
} elseif (isset($this->parameter->query)) {
$query = $this->parameter->query;
}
if ($query) {
$this->db->fetchAll($query, [$this, 'push']);
if ($this->type == 'page') {
$this->initTreeParameter($this->parameter);
}
}
}
/**
* @return array
*/
protected function ___directory(): array
{
return $this->type == 'page' ? $this->___treeDirectory() : parent::___directory();
}
/**
* @return array
* @throws Exception
*/
protected function initTreeRows(): array
{
return $this->db->fetchAll($this->select(
'table.contents.cid',
'table.contents.title',
'table.contents.slug',
'table.contents.created',
'table.contents.authorId',
'table.contents.modified',
'table.contents.type',
'table.contents.status',
'table.contents.commentsNum',
'table.contents.order',
'table.contents.parent',
'table.contents.template',
'table.contents.password',
'table.contents.allowComment',
'table.contents.allowPing',
'table.contents.allowFeed'
)->where('table.contents.type = ?', 'page'));
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Widget\Contents\Page;
use Typecho\Common;
use Typecho\Db;
use Typecho\Widget\Exception;
use Widget\Base\Contents;
use Widget\Base\TreeTrait;
use Widget\Contents\AdminTrait;
use Widget\Contents\From;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 独立页面管理列表组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Admin extends Contents
{
use AdminTrait;
use TreeTrait;
/**
* @var int 父级页面
*/
private int $parentId = 0;
/**
* 执行函数
*
* @access public
* @return void
* @throws Db\Exception
*/
public function execute()
{
$this->parameter->setDefault('ignoreRequest=0');
if ($this->parameter->ignoreRequest) {
$this->pushAll($this->getRows($this->orders, $this->parameter->ignore));
} elseif ($this->request->is('keywords')) {
$select = $this->select('table.contents.cid')
->where('table.contents.type = ? OR table.contents.type = ?', 'page', 'page_draft');
$this->searchQuery($select);
$ids = array_column($this->db->fetchAll($select), 'cid');
$this->pushAll($this->getRows($ids));
} else {
$this->parentId = $this->request->filter('int')->get('parent', 0);
$this->pushAll($this->getRows($this->getChildIds($this->parentId)));
}
}
/**
* 向上的返回链接
*
* @throws Db\Exception
*/
public function backLink()
{
if ($this->parentId) {
$page = $this->getRow($this->parentId);
if (!empty($page)) {
$parent = $this->getRow($page['parent']);
if ($parent) {
echo '<a href="'
. Common::url('manage-pages.php?parent=' . $parent['mid'], $this->options->adminUrl)
. '">';
} else {
echo '<a href="' . Common::url('manage-pages.php', $this->options->adminUrl) . '">';
}
echo '&laquo; ';
_e('返回父级页面');
echo '</a>';
}
}
}
/**
* 获取菜单标题
*
* @return string|null
* @throws Db\Exception|Exception
*/
public function getMenuTitle(): ?string
{
if ($this->parentId) {
$page = $this->getRow($this->parentId);
if (!empty($page)) {
return _t('管理 %s 的子页面', $page['title']);
}
} else {
return null;
}
throw new Exception(_t('页面不存在'), 404);
}
/**
* 获取菜单标题
*
* @return string
*/
public function getAddLink(): string
{
return 'write-page.php' . ($this->parentId ? '?parent=' . $this->parentId : '');
}
/**
* @return array
* @throws Db\Exception
*/
protected function initTreeRows(): array
{
$select = $this->select(
'table.contents.cid',
'table.contents.title',
'table.contents.slug',
'table.contents.created',
'table.contents.authorId',
'table.contents.modified',
'table.contents.type',
'table.contents.status',
'table.contents.commentsNum',
'table.contents.order',
'table.contents.parent',
'table.contents.template',
'table.contents.password',
)->where('table.contents.type = ? OR table.contents.type = ?', 'page', 'page_draft');
return $this->db->fetchAll($select);
}
}

393
var/Widget/Contents/Page/Edit.php Executable file
View File

@@ -0,0 +1,393 @@
<?php
namespace Widget\Contents\Page;
use Typecho\Common;
use Typecho\Date;
use Typecho\Db\Exception as DbException;
use Typecho\Widget\Exception;
use Widget\Base\Contents;
use Widget\Contents\EditTrait;
use Widget\ActionInterface;
use Widget\Contents\PrepareEditTrait;
use Widget\Notice;
use Widget\Service;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑页面组件
*
* @property-read array $draft
*/
class Edit extends Contents implements ActionInterface
{
use PrepareEditTrait;
use EditTrait;
/**
* 执行函数
*
* @access public
* @return void
* @throws Exception
* @throws DbException
*/
public function execute()
{
/** 必须为编辑以上权限 */
$this->user->pass('editor');
}
/**
* 发布文章
*/
public function writePage()
{
$contents = $this->request->from(
'text',
'template',
'allowComment',
'allowPing',
'allowFeed',
'slug',
'order',
'visibility'
);
$contents['title'] = $this->request->get('title', _t('未命名页面'));
$contents['created'] = $this->getCreated();
$contents['visibility'] = ('hidden' == $contents['visibility'] ? 'hidden' : 'publish');
$contents['parent'] = $this->getParent();
if ($this->request->is('markdown=1') && $this->options->markdown) {
$contents['text'] = '<!--markdown-->' . $contents['text'];
}
$contents = self::pluginHandle()->filter('write', $contents, $this);
if ($this->request->is('do=publish')) {
/** 重新发布已经存在的文章 */
$contents['type'] = 'page';
$this->publish($contents, false);
// 完成发布插件接口
self::pluginHandle()->call('finishPublish', $contents, $this);
/** 发送ping */
Service::alloc()->sendPing($this);
/** 设置提示信息 */
Notice::alloc()->set(
_t('页面 "<a href="%s">%s</a>" 已经发布', $this->permalink, $this->title),
'success'
);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 页面跳转 */
$this->response->redirect(Common::url('manage-pages.php'
. ($this->parent ? '?parent=' . $this->parent : ''), $this->options->adminUrl));
} else {
/** 保存文章 */
$contents['type'] = 'page_draft';
$draftId = $this->save($contents, false);
// 完成发布插件接口
self::pluginHandle()->call('finishSave', $contents, $this);
/** 设置高亮 */
Notice::alloc()->highlight($this->cid);
if ($this->request->isAjax()) {
$created = new Date($this->options->time);
$this->response->throwJson([
'success' => 1,
'time' => $created->format('H:i:s A'),
'cid' => $this->cid,
'draftId' => $draftId
]);
} else {
/** 设置提示信息 */
Notice::alloc()->set(_t('草稿 "%s" 已经被保存', $this->title), 'success');
/** 返回原页面 */
$this->response->redirect(Common::url('write-page.php?cid=' . $this->cid, $this->options->adminUrl));
}
}
}
/**
* 标记页面
*
* @throws DbException
*/
public function markPage()
{
$status = $this->request->get('status');
$statusList = [
'publish' => _t('公开'),
'hidden' => _t('隐藏')
];
if (!isset($statusList[$status])) {
$this->response->goBack();
}
$pages = $this->request->filter('int')->getArray('cid');
$markCount = 0;
foreach ($pages as $page) {
// 标记插件接口
self::pluginHandle()->call('mark', $status, $page, $this);
$condition = $this->db->sql()->where('cid = ?', $page);
if ($this->db->query($condition->update('table.contents')->rows(['status' => $status]))) {
// 处理草稿
$draft = $this->db->fetchRow($this->db->select('cid')
->from('table.contents')
->where('table.contents.parent = ? AND table.contents.type = ?', $page, 'revision')
->limit(1));
if (!empty($draft)) {
$this->db->query($this->db->update('table.contents')->rows(['status' => $status])
->where('cid = ?', $draft['cid']));
}
// 完成标记插件接口
self::pluginHandle()->call('finishMark', $status, $page, $this);
$markCount++;
}
unset($condition);
}
/** 设置提示信息 */
Notice::alloc()
->set(
$markCount > 0 ? _t('页面已经被标记为<strong>%s</strong>', $statusList[$status]) : _t('没有页面被标记'),
$markCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 删除页面
*
* @throws DbException
*/
public function deletePage()
{
$pages = $this->request->filter('int')->getArray('cid');
$deleteCount = 0;
foreach ($pages as $page) {
// 删除插件接口
self::pluginHandle()->call('delete', $page, $this);
$parent = $this->db->fetchObject($this->select()->where('cid = ?', $page))->parent;
if ($this->delete($this->db->sql()->where('cid = ?', $page))) {
/** 删除评论 */
$this->db->query($this->db->delete('table.comments')
->where('cid = ?', $page));
/** 解除附件关联 */
$this->unAttach($page);
/** 解除首页关联 */
if ($this->options->frontPage == 'page:' . $page) {
$this->db->query($this->db->update('table.options')
->rows(['value' => 'recent'])
->where('name = ?', 'frontPage'));
}
/** 删除草稿 */
$draft = $this->db->fetchRow($this->db->select('cid')
->from('table.contents')
->where('table.contents.parent = ? AND table.contents.type = ?', $page, 'revision')
->limit(1));
/** 删除自定义字段 */
$this->deleteFields($page);
if ($draft) {
$this->deleteContent($draft['cid'], false);
$this->deleteFields($draft['cid']);
}
// update parent
$this->update(
['parent' => $parent],
$this->db->sql()->where('parent = ?', $page)
->where('type = ? OR type = ?', 'page', 'page_draft')
);
// 完成删除插件接口
self::pluginHandle()->call('finishDelete', $page, $this);
$deleteCount++;
}
}
/** 设置提示信息 */
Notice::alloc()
->set(
$deleteCount > 0 ? _t('页面已经被删除') : _t('没有页面被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 删除页面所属草稿
*
* @throws DbException
*/
public function deletePageDraft()
{
$pages = $this->request->filter('int')->getArray('cid');
$deleteCount = 0;
foreach ($pages as $page) {
/** 删除草稿 */
$draft = $this->db->fetchRow($this->db->select('cid')
->from('table.contents')
->where('table.contents.parent = ? AND table.contents.type = ?', $page, 'revision')
->limit(1));
if ($draft) {
$this->deleteContent($draft['cid'], false);
$this->deleteFields($draft['cid']);
$deleteCount++;
}
}
/** 设置提示信息 */
Notice::alloc()
->set(
$deleteCount > 0 ? _t('草稿已经被删除') : _t('没有草稿被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 页面排序
*
* @throws DbException
*/
public function sortPage()
{
$pages = $this->request->filter('int')->getArray('cid');
if ($pages) {
foreach ($pages as $sort => $cid) {
$this->db->query($this->db->update('table.contents')->rows(['order' => $sort + 1])
->where('cid = ?', $cid));
}
}
if (!$this->request->isAjax()) {
/** 转向原页 */
$this->response->goBack();
} else {
$this->response->throwJson(['success' => 1, 'message' => _t('页面排序已经完成')]);
}
}
/**
* @return $this
* @throws DbException
* @throws Exception
*/
public function prepare(): self
{
return $this->prepareEdit('page', true, _t('页面不存在'));
}
/**
* 绑定动作
*
* @return void
* @throws DbException
* @throws Exception
*/
public function action()
{
$this->security->protect();
$this->on($this->request->is('do=publish') || $this->request->is('do=save'))
->prepare()->writePage();
$this->on($this->request->is('do=delete'))->deletePage();
$this->on($this->request->is('do=mark'))->markPage();
$this->on($this->request->is('do=deleteDraft'))->deletePageDraft();
$this->on($this->request->is('do=sort'))->sortPage();
$this->response->redirect($this->options->adminUrl);
}
/**
* 获取网页标题
*
* @return string
*/
public function getMenuTitle(): string
{
$this->prepare();
if ($this->have()) {
return _t('编辑 %s', $this->title);
}
if ($this->request->is('parent')) {
$page = $this->db->fetchRow($this->select()
->where('table.contents.type = ? OR table.contents.type', 'page', 'page_draft')
->where('table.contents.cid = ?', $this->request->filter('int')->get('parent')));
if (!empty($page)) {
return _t('新增 %s 的子页面', $page['title']);
}
}
throw new Exception(_t('页面不存在'), 404);
}
/**
* @return int
*/
public function getParent(): int
{
if ($this->request->is('parent')) {
$parent = $this->request->filter('int')->get('parent');
if (!$this->have() || $this->cid != $parent) {
$parentPage = $this->db->fetchRow($this->select()
->where('table.contents.type = ? OR table.contents.type = ?', 'page', 'page_draft')
->where('table.contents.cid = ?', $parent));
if (!empty($parentPage)) {
return $parent;
}
}
}
return 0;
}
/**
* @return string
*/
protected function getThemeFieldsHook(): string
{
return 'themePageFields';
}
}

101
var/Widget/Contents/Page/Rows.php Executable file
View File

@@ -0,0 +1,101 @@
<?php
namespace Widget\Contents\Page;
use Typecho\Config;
use Typecho\Db\Exception;
use Widget\Base\Contents;
use Widget\Base\TreeViewTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 独立页面列表组件
*
* @author qining
* @page typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Rows extends Contents
{
use TreeViewTrait;
/**
* 执行函数
*
* @return void
* @throws Exception
*/
public function execute()
{
$this->pushAll($this->getRows($this->orders, $this->parameter->ignore));
}
/**
* @return array
* @throws Exception
*/
protected function initTreeRows(): array
{
$select = $this->select(
'table.contents.cid',
'table.contents.title',
'table.contents.slug',
'table.contents.created',
'table.contents.authorId',
'table.contents.modified',
'table.contents.type',
'table.contents.status',
'table.contents.commentsNum',
'table.contents.order',
'table.contents.parent',
'table.contents.template',
'table.contents.password',
'table.contents.allowComment',
'table.contents.allowPing',
'table.contents.allowFeed'
)->where('table.contents.type = ?', 'page')
->where('table.contents.status = ?', 'publish')
->where('table.contents.created < ?', $this->options->time);
//去掉自定义首页
$frontPage = explode(':', $this->options->frontPage);
if (2 == count($frontPage) && 'page' == $frontPage[0]) {
$select->where('table.contents.cid <> ?', $frontPage[1]);
}
return $this->db->fetchAll($select);
}
/**
* treeViewPages
*
* @param mixed $pageOptions 输出选项
*/
public function listPages($pageOptions = null)
{
//初始化一些变量
$pageOptions = Config::factory($pageOptions);
$pageOptions->setDefault([
'wrapTag' => 'ul',
'wrapClass' => '',
'itemTag' => 'li',
'itemClass' => '',
'showCount' => false,
'showFeed' => false,
'countTemplate' => '(%d)',
'feedTemplate' => '<a href="%s">RSS</a>'
]);
// 插件插件接口
self::pluginHandle()->trigger($plugged)->call('listPages', $pageOptions, $this);
if (!$plugged) {
$this->listRows($pageOptions, 'treeViewPagesCallback', intval($this->parameter->current));
}
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Widget\Contents\Post;
use Typecho\Cookie;
use Typecho\Db;
use Typecho\Db\Exception as DbException;
use Typecho\Widget\Exception;
use Widget\Base\Contents;
use Widget\Contents\AdminTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 文章管理列表组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Admin extends Contents
{
use AdminTrait;
/**
* 获取菜单标题
*
* @return string
* @throws Exception|DbException
*/
public function getMenuTitle(): string
{
if ($this->request->is('uid')) {
return _t('%s的文章', $this->db->fetchObject($this->db->select('screenName')->from('table.users')
->where('uid = ?', $this->request->filter('int')->get('uid')))->screenName);
}
throw new Exception(_t('用户不存在'), 404);
}
/**
* 执行函数
*
* @throws DbException
*/
public function execute()
{
$this->initPage();
/** 构建基础查询 */
$select = $this->select();
/** 如果具有编辑以上权限,可以查看所有文章,反之只能查看自己的文章 */
if (!$this->user->pass('editor', true)) {
$select->where('table.contents.authorId = ?', $this->user->uid);
} else {
if ($this->request->is('__typecho_all_posts=on')) {
Cookie::set('__typecho_all_posts', 'on');
} else {
if ($this->request->is('__typecho_all_posts=off')) {
Cookie::set('__typecho_all_posts', 'off');
}
if ('on' != Cookie::get('__typecho_all_posts')) {
$select->where(
'table.contents.authorId = ?',
$this->request->filter('int')->get('uid', $this->user->uid)
);
}
}
}
/** 按状态查询 */
if ($this->request->is('status=draft')) {
$select->where('table.contents.type = ?', 'post_draft');
} elseif ($this->request->is('status=waiting')) {
$select->where(
'(table.contents.type = ? OR table.contents.type = ?) AND table.contents.status = ?',
'post',
'post_draft',
'waiting'
);
} else {
$select->where(
'table.contents.type = ? OR table.contents.type = ?',
'post',
'post_draft'
);
}
/** 过滤分类 */
if (null != ($category = $this->request->get('category'))) {
$select->join('table.relationships', 'table.contents.cid = table.relationships.cid')
->where('table.relationships.mid = ?', $category);
}
$this->searchQuery($select);
$this->countTotal($select);
/** 提交查询 */
$select->order('table.contents.cid', Db::SORT_DESC)
->page($this->currentPage, $this->parameter->pageSize);
$this->db->fetchAll($select, [$this, 'push']);
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Widget\Contents\Post;
use Typecho\Config;
use Typecho\Db;
use Typecho\Router;
use Widget\Base;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 按日期归档列表组件
*
* @author qining
* @category typecho
* @package Widget
*/
class Date extends Base
{
/**
* @param Config $parameter
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault('format=Y-m&type=month&limit=0');
}
/**
* 初始化函数
*
* @return void
*/
public function execute()
{
/** 设置参数默认值 */
$this->parameter->setDefault('format=Y-m&type=month&limit=0');
$resource = $this->db->query($this->db->select('created')->from('table.contents')
->where('type = ?', 'post')
->where('table.contents.status = ?', 'publish')
->where('table.contents.created < ?', $this->options->time)
->order('table.contents.created', Db::SORT_DESC));
$offset = $this->options->timezone - $this->options->serverTimezone;
$result = [];
while ($post = $this->db->fetchRow($resource)) {
$timeStamp = $post['created'] + $offset;
$date = date($this->parameter->format, $timeStamp);
if (isset($result[$date])) {
$result[$date]['count'] ++;
} else {
$result[$date]['year'] = date('Y', $timeStamp);
$result[$date]['month'] = date('m', $timeStamp);
$result[$date]['day'] = date('d', $timeStamp);
$result[$date]['date'] = $date;
$result[$date]['count'] = 1;
}
}
if ($this->parameter->limit > 0) {
$result = array_slice($result, 0, $this->parameter->limit);
}
foreach ($result as $row) {
$row['permalink'] = Router::url(
'archive_' . $this->parameter->type,
$row,
$this->options->index
);
$this->push($row);
}
}
}

373
var/Widget/Contents/Post/Edit.php Executable file
View File

@@ -0,0 +1,373 @@
<?php
namespace Widget\Contents\Post;
use Typecho\Common;
use Typecho\Widget\Exception;
use Widget\Base\Contents;
use Widget\Base\Metas;
use Widget\ActionInterface;
use Typecho\Db\Exception as DbException;
use Typecho\Date as TypechoDate;
use Widget\Contents\EditTrait;
use Widget\Contents\PrepareEditTrait;
use Widget\Notice;
use Widget\Service;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑文章组件
*
* @property-read array $draft
*/
class Edit extends Contents implements ActionInterface
{
use PrepareEditTrait;
use EditTrait;
/**
* 执行函数
*
* @throws Exception|DbException
*/
public function execute()
{
/** 必须为贡献者以上权限 */
$this->user->pass('contributor');
}
/**
* 发布文章
*/
public function writePost()
{
$contents = $this->request->from(
'password',
'allowComment',
'allowPing',
'allowFeed',
'slug',
'tags',
'text',
'visibility'
);
$contents['category'] = $this->request->getArray('category');
$contents['title'] = $this->request->get('title', _t('未命名文档'));
$contents['created'] = $this->getCreated();
if ($this->request->is('markdown=1') && $this->options->markdown) {
$contents['text'] = '<!--markdown-->' . $contents['text'];
}
$contents = self::pluginHandle()->filter('write', $contents, $this);
if ($this->request->is('do=publish')) {
/** 重新发布已经存在的文章 */
$contents['type'] = 'post';
$this->publish($contents);
// 完成发布插件接口
self::pluginHandle()->call('finishPublish', $contents, $this);
/** 发送ping */
$trackback = array_filter(
array_unique(preg_split("/(\r|\n|\r\n)/", trim($this->request->get('trackback', ''))))
);
Service::alloc()->sendPing($this, $trackback);
/** 设置提示信息 */
Notice::alloc()->set('post' == $this->type ?
_t('文章 "<a href="%s">%s</a>" 已经发布', $this->permalink, $this->title) :
_t('文章 "%s" 等待审核', $this->title), 'success');
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 获取页面偏移 */
$pageQuery = $this->getPageOffsetQuery($this->cid);
/** 页面跳转 */
$this->response->redirect(Common::url('manage-posts.php?' . $pageQuery, $this->options->adminUrl));
} else {
/** 保存文章 */
$contents['type'] = 'post_draft';
$draftId = $this->save($contents);
// 完成保存插件接口
self::pluginHandle()->call('finishSave', $contents, $this);
/** 设置高亮 */
Notice::alloc()->highlight($this->cid);
if ($this->request->isAjax()) {
$created = new TypechoDate();
$this->response->throwJson([
'success' => 1,
'time' => $created->format('H:i:s A'),
'cid' => $this->cid,
'draftId' => $draftId
]);
} else {
/** 设置提示信息 */
Notice::alloc()->set(_t('草稿 "%s" 已经被保存', $this->title), 'success');
/** 返回原页面 */
$this->response->redirect(Common::url('write-post.php?cid=' . $this->cid, $this->options->adminUrl));
}
}
}
/**
* 获取页面偏移的URL Query
*
* @param integer $cid 文章id
* @param string|null $status 状态
* @return string
* @throws DbException
*/
protected function getPageOffsetQuery(int $cid, ?string $status = null): string
{
return 'page=' . $this->getPageOffset(
'cid',
$cid,
'post',
$status,
$this->request->is('__typecho_all_posts=on') ? 0 : $this->user->uid
);
}
/**
* 标记文章
*
* @throws DbException
*/
public function markPost()
{
$status = $this->request->get('status');
$statusList = [
'publish' => _t('公开'),
'private' => _t('私密'),
'hidden' => _t('隐藏'),
'waiting' => _t('待审核')
];
if (!isset($statusList[$status])) {
$this->response->goBack();
}
$posts = $this->request->filter('int')->getArray('cid');
$markCount = 0;
foreach ($posts as $post) {
// 标记插件接口
self::pluginHandle()->call('mark', $status, $post, $this);
$condition = $this->db->sql()->where('cid = ?', $post);
$postObject = $this->db->fetchObject($this->db->select('status', 'type')
->from('table.contents')->where('cid = ? AND (type = ? OR type = ?)', $post, 'post', 'post_draft'));
if ($this->isWriteable(clone $condition) && count((array)$postObject)) {
/** 标记状态 */
$this->db->query($condition->update('table.contents')->rows(['status' => $status]));
// 刷新Metas
if ($postObject->type == 'post') {
$op = null;
if ($status == 'publish' && $postObject->status != 'publish') {
$op = '+';
} elseif ($status != 'publish' && $postObject->status == 'publish') {
$op = '-';
}
if (!empty($op)) {
$metas = $this->db->fetchAll(
$this->db->select()->from('table.relationships')->where('cid = ?', $post)
);
foreach ($metas as $meta) {
$this->db->query($this->db->update('table.metas')
->expression('count', 'count ' . $op . ' 1')
->where('mid = ? AND (type = ? OR type = ?)', $meta['mid'], 'category', 'tag'));
}
}
}
// 处理草稿
$draft = $this->db->fetchRow($this->db->select('cid')
->from('table.contents')
->where('table.contents.parent = ? AND table.contents.type = ?', $post, 'revision')
->limit(1));
if (!empty($draft)) {
$this->db->query($this->db->update('table.contents')->rows(['status' => $status])
->where('cid = ?', $draft['cid']));
}
// 完成标记插件接口
self::pluginHandle()->call('finishMark', $status, $post, $this);
$markCount++;
}
unset($condition);
}
/** 设置提示信息 */
Notice::alloc()
->set(
$markCount > 0 ? _t('文章已经被标记为<strong>%s</strong>', $statusList[$status]) : _t('没有文章被标记'),
$markCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 删除文章
*
* @throws DbException
*/
public function deletePost()
{
$posts = $this->request->filter('int')->getArray('cid');
$deleteCount = 0;
foreach ($posts as $post) {
// 删除插件接口
self::pluginHandle()->call('delete', $post, $this);
$condition = $this->db->sql()->where('cid = ?', $post);
$postObject = $this->db->fetchObject($this->db->select('status', 'type')
->from('table.contents')->where('cid = ? AND (type = ? OR type = ?)', $post, 'post', 'post_draft'));
if ($this->isWriteable(clone $condition) && count((array)$postObject) && $this->delete($condition)) {
/** 删除分类 */
$this->setCategories($post, [], 'publish' == $postObject->status
&& 'post' == $postObject->type);
/** 删除标签 */
$this->setTags($post, null, 'publish' == $postObject->status
&& 'post' == $postObject->type);
/** 删除评论 */
$this->db->query($this->db->delete('table.comments')
->where('cid = ?', $post));
/** 解除附件关联 */
$this->unAttach($post);
/** 删除草稿 */
$draft = $this->db->fetchRow($this->db->select('cid')
->from('table.contents')
->where('table.contents.parent = ? AND table.contents.type = ?', $post, 'revision')
->limit(1));
/** 删除自定义字段 */
$this->deleteFields($post);
if ($draft) {
$this->deleteContent($draft['cid']);
$this->deleteFields($draft['cid']);
}
// 完成删除插件接口
self::pluginHandle()->call('finishDelete', $post, $this);
$deleteCount++;
}
unset($condition);
}
// 清理标签
if ($deleteCount > 0) {
Metas::alloc()->clearTags();
}
/** 设置提示信息 */
Notice::alloc()->set(
$deleteCount > 0 ? _t('文章已经被删除') : _t('没有文章被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* 删除文章所属草稿
*
* @throws DbException
*/
public function deletePostDraft()
{
$posts = $this->request->filter('int')->getArray('cid');
$deleteCount = 0;
foreach ($posts as $post) {
/** 删除草稿 */
$draft = $this->db->fetchRow($this->db->select('cid')
->from('table.contents')
->where('table.contents.parent = ? AND table.contents.type = ?', $post, 'revision')
->limit(1));
if ($draft) {
$this->deleteContent($draft['cid']);
$this->deleteFields($draft['cid']);
$deleteCount++;
}
}
/** 设置提示信息 */
Notice::alloc()
->set(
$deleteCount > 0 ? _t('草稿已经被删除') : _t('没有草稿被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 返回原网页 */
$this->response->goBack();
}
/**
* @return $this
* @throws DbException
* @throws Exception
*/
public function prepare(): self
{
return $this->prepareEdit('post', true, _t('文章不存在'));
}
/**
* 绑定动作
*
* @throws Exception|DbException
*/
public function action()
{
$this->security->protect();
$this->on($this->request->is('do=publish') || $this->request->is('do=save'))
->prepare()->writePost();
$this->on($this->request->is('do=delete'))->deletePost();
$this->on($this->request->is('do=mark'))->markPost();
$this->on($this->request->is('do=deleteDraft'))->deletePostDraft();
$this->response->redirect($this->options->adminUrl);
}
/**
* @return string
*/
protected function getThemeFieldsHook(): string
{
return 'themePostFields';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Widget\Contents\Post;
use Typecho\Db;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 最新评论组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Recent extends Contents
{
/**
* 执行函数
*
* @throws Db\Exception
*/
public function execute()
{
$this->parameter->setDefault(['pageSize' => $this->options->postsListSize]);
$this->db->fetchAll($this->select(
'table.contents.cid',
'table.contents.title',
'table.contents.slug',
'table.contents.created',
'table.contents.modified',
'table.contents.type',
'table.contents.status',
'table.contents.commentsNum',
'table.contents.allowComment',
'table.contents.allowPing',
'table.contents.allowFeed',
'table.contents.template',
'table.contents.password',
'table.contents.authorId',
'table.contents.parent',
)
->where('table.contents.status = ?', 'publish')
->where('table.contents.created < ?', $this->options->time)
->where('table.contents.type = ?', 'post')
->order('table.contents.created', Db::SORT_DESC)
->limit($this->parameter->pageSize), [$this, 'push']);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Widget\Contents;
use Typecho\Db\Exception as DbException;
use Typecho\Widget\Exception;
use Widget\Base\Metas;
use Widget\Metas\From as MetasFrom;
/**
* 编辑准备组件
*/
trait PrepareEditTrait
{
/**
* 准备编辑
*
* @param string $type
* @param bool $hasDraft
* @param string $notFoundMessage
* @return $this
* @throws Exception|DbException
*/
protected function prepareEdit(string $type, bool $hasDraft, string $notFoundMessage): self
{
if ($this->request->is('cid')) {
$contentTypes = [$type];
if ($hasDraft) {
$contentTypes[] = $type . '_draft';
}
$this->db->fetchRow($this->select()
->where('table.contents.type IN ?', $contentTypes)
->where('table.contents.cid = ?', $this->request->filter('int')->get('cid'))
->limit(1), [$this, 'push']);
if (!$this->have()) {
throw new Exception($notFoundMessage, 404);
}
if ($hasDraft) {
$draft = $this->type === $type . '_draft' ? $this->row : $this->db->fetchRow($this->select()
->where('table.contents.parent = ? AND table.contents.type = ?', $this->cid, 'revision')
->limit(1), [$this, 'filter']);
if (isset($draft)) {
$draft['parent'] = $this->row['parent']; // keep parent
$draft['slug'] = ltrim($draft['slug'], '@');
$draft['type'] = $this->type;
$draft['draft'] = $draft;
$draft['cid'] = $this->cid;
$draft['tags'] = $this->db->fetchAll($this->db
->select()->from('table.metas')
->join('table.relationships', 'table.relationships.mid = table.metas.mid')
->where('table.relationships.cid = ?', $draft['cid'])
->where('table.metas.type = ?', 'tag'), [Metas::alloc(), 'filter']);
$this->row = $draft;
}
}
if (!$this->allow('edit')) {
throw new Exception(_t('没有编辑权限'), 403);
}
}
return $this;
}
/**
* @return $this
*/
abstract public function prepare(): self;
/**
* 获取网页标题
*
* @return string
*/
public function getMenuTitle(): string
{
return _t('编辑 %s', $this->prepare()->title);
}
/**
* 获取权限
*
* @param mixed ...$permissions
* @return bool
* @throws Exception|DbException
*/
public function allow(...$permissions): bool
{
$allow = true;
foreach ($permissions as $permission) {
$permission = strtolower($permission);
if ('edit' == $permission) {
$allow &= ($this->user->pass('editor', true) || $this->authorId == $this->user->uid);
} else {
$permission = 'allow' . ucfirst(strtolower($permission));
$optionPermission = 'default' . ucfirst($permission);
$allow &= ($this->{$permission} ?? $this->options->{$optionPermission});
}
}
return $allow;
}
/**
* @return string
*/
protected function ___title(): string
{
return $this->have() ? $this->row['title'] : '';
}
/**
* @return string
*/
protected function ___text(): string
{
return $this->have() ? ($this->isMarkdown ? substr($this->row['text'], 15) : $this->row['text']) : '';
}
/**
* @return array
*/
protected function ___categories(): array
{
return $this->have() ? parent::___categories()
: MetasFrom::allocWithAlias(
'category:' . $this->options->defaultCategory,
['mid' => $this->options->defaultCategory]
)->toArray(['mid', 'name', 'slug']);
}
}

64
var/Widget/Contents/Related.php Executable file
View File

@@ -0,0 +1,64 @@
<?php
namespace Widget\Contents;
use Typecho\Db;
use Typecho\Db\Exception;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 相关内容组件(根据标签关联)
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Related extends Contents
{
/**
* 执行函数,初始化数据
*
* @throws Exception
*/
public function execute()
{
$this->parameter->setDefault('limit=5');
if ($this->parameter->tags) {
$tagsGroup = implode(',', array_column($this->parameter->tags, 'mid'));
$this->db->fetchAll($this->select(
'DISTINCT table.contents.cid',
'table.contents.title',
'table.contents.slug',
'table.contents.created',
'table.contents.authorId',
'table.contents.modified',
'table.contents.type',
'table.contents.status',
'table.contents.text',
'table.contents.commentsNum',
'table.contents.order',
'table.contents.template',
'table.contents.password',
'table.contents.allowComment',
'table.contents.allowPing',
'table.contents.allowFeed'
)
->join('table.relationships', 'table.contents.cid = table.relationships.cid')
->where('table.relationships.mid IN (' . $tagsGroup . ')')
->where('table.contents.cid <> ?', $this->parameter->cid)
->where('table.contents.status = ?', 'publish')
->where('table.contents.password IS NULL')
->where('table.contents.created < ?', $this->options->time)
->where('table.contents.type = ?', $this->parameter->type)
->order('table.contents.created', Db::SORT_DESC)
->limit($this->parameter->limit), [$this, 'push']);
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Widget\Contents\Related;
use Typecho\Db;
use Typecho\Db\Exception;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 相关内容组件(根据作者关联)
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Author extends Contents
{
/**
* 执行函数,初始化数据
*
* @throws Exception
*/
public function execute()
{
$this->parameter->setDefault('limit=5');
if ($this->parameter->author) {
$this->db->fetchAll($this->select()
->where('table.contents.authorId = ?', $this->parameter->author)
->where('table.contents.cid <> ?', $this->parameter->cid)
->where('table.contents.status = ?', 'publish')
->where('table.contents.password IS NULL')
->where('table.contents.created < ?', $this->options->time)
->where('table.contents.type = ?', $this->parameter->type)
->order('table.contents.created', Db::SORT_DESC)
->limit($this->parameter->limit), [$this, 'push']);
}
}
}

27
var/Widget/ExceptionHandle.php Executable file
View File

@@ -0,0 +1,27 @@
<?php
namespace Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 异常处理组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class ExceptionHandle extends Base
{
/**
* 重载构造函数
*/
public function execute()
{
Archive::allocWithAlias('404', 'type=404')->render();
}
}

212
var/Widget/Feed.php Executable file
View File

@@ -0,0 +1,212 @@
<?php
namespace Widget;
use Exception;
use Typecho\Common;
use Typecho\Config;
use Typecho\Router;
use Typecho\Widget\Exception as WidgetException;
use Widget\Base\Contents;
use Typecho\Feed as FeedGenerator;
use Widget\Comments\Recent;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* Feed handler
*/
class Feed extends Contents
{
/**
* @var FeedGenerator
*/
private FeedGenerator $feed;
/**
* @param Config $parameter
* @throws Exception
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault([
'pageSize' => 10,
]);
}
/**
* @throws Exception
*/
public function execute()
{
$feedPath = $this->request->get('feed', '/');
$feedType = FeedGenerator::RSS2;
$feedContentType = 'application/rss+xml';
$currentFeedUrl = $this->options->feedUrl;
$isComments = false;
/** 判断聚合类型 */
switch (true) {
case preg_match("/^\/rss(\/|$)/", $feedPath):
/** 如果是RSS1标准 */
$feedPath = substr($feedPath, 4);
$feedType = FeedGenerator::RSS1;
$currentFeedUrl = $this->options->feedRssUrl;
$feedContentType = 'application/rdf+xml';
break;
case preg_match("/^\/atom(\/|$)/", $feedPath):
/** 如果是ATOM标准 */
$feedPath = substr($feedPath, 5);
$feedType = FeedGenerator::ATOM1;
$currentFeedUrl = $this->options->feedAtomUrl;
$feedContentType = 'application/atom+xml';
break;
default:
break;
}
$feed = new FeedGenerator(
Common::VERSION,
$feedType,
$this->options->charset,
_t('zh-CN')
);
if (preg_match("/^\/comments\/?$/", $feedPath)) {
$isComments = true;
$currentFeedUrl = Common::url('/comments/', $currentFeedUrl);
$feed->setBaseUrl($this->options->siteUrl);
$feed->setSubTitle($this->options->description);
} else {
$archive = Router::match($feedPath, [
'pageSize' => $this->parameter->pageSize,
'isFeed' => true
]);
if (!($archive instanceof Archive)) {
throw new WidgetException(_t('聚合页不存在'), 404);
}
switch ($feedType) {
case FeedGenerator::RSS1:
$currentFeedUrl = $archive->getArchiveFeedRssUrl();
break;
case FeedGenerator::ATOM1:
$currentFeedUrl = $archive->getArchiveFeedAtomUrl();
break;
default:
$currentFeedUrl = $archive->getArchiveFeedUrl();
break;
}
$feed->setBaseUrl($archive->getArchiveUrl());
$feed->setSubTitle($archive->getArchiveDescription());
}
$this->checkPermalink($currentFeedUrl);
$feed->setFeedUrl($currentFeedUrl);
$this->feed($feed, $feedContentType, $isComments, $archive ?? null);
$this->feed = $feed;
}
/**
* @param FeedGenerator $feed
* @param string $contentType
* @param bool $isComments
* @param Archive|null $archive
*/
public function feed(
FeedGenerator $feed,
string $contentType,
bool $isComments,
?Archive $archive
) {
if ($isComments || $archive->is('single')) {
$feed->setTitle(_t(
'%s 的评论',
$this->options->title . ($isComments ? '' : ' - ' . $archive->getArchiveTitle())
));
if ($isComments) {
$comments = Recent::alloc('pageSize=10');
} else {
$comments = Recent::alloc('pageSize=10&parentId=' . $archive->cid);
}
while ($comments->next()) {
$suffix = self::pluginHandle()->trigger($plugged)->call(
'commentFeedItem',
$feed->getType(),
$comments
);
if (!$plugged) {
$suffix = null;
}
$feed->addItem([
'title' => $comments->author,
'content' => $comments->content,
'date' => $comments->created,
'link' => $comments->permalink,
'author' => (object)[
'screenName' => $comments->author,
'url' => $comments->url,
'mail' => $comments->mail
],
'excerpt' => strip_tags($comments->content),
'suffix' => $suffix
]);
}
} else {
$feed->setTitle($this->options->title
. ($archive->getArchiveTitle() ? ' - ' . $archive->getArchiveTitle() : ''));
while ($archive->next()) {
$suffix = self::pluginHandle()->trigger($plugged)->call('feedItem', $feed->getType(), $archive);
if (!$plugged) {
$suffix = null;
}
$feed->addItem([
'title' => $archive->title,
'content' => $this->options->feedFullText ? $archive->content
: (false !== strpos($archive->text, '<!--more-->') ? $archive->excerpt .
"<p class=\"more\"><a href=\"{$archive->permalink}\" title=\"{$archive->title}\">[...]</a></p>"
: $archive->content),
'date' => $archive->created,
'link' => $archive->permalink,
'author' => $archive->author,
'excerpt' => $archive->plainExcerpt,
'category' => $archive->categories,
'comments' => $archive->commentsNum,
'commentsFeedUrl' => Common::url($archive->path, $feed->getFeedUrl()),
'suffix' => $suffix
]);
}
}
$this->response->setContentType($contentType);
}
/**
* @return void
*/
public function render()
{
echo $this->feed;
}
/**
* @param string $feedUrl
*/
private function checkPermalink(string $feedUrl)
{
if ($feedUrl != $this->request->getRequestUrl()) {
$this->response->redirect($feedUrl, true);
}
}
}

355
var/Widget/Feedback.php Executable file
View File

@@ -0,0 +1,355 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Db;
use Typecho\Router;
use Typecho\Validate;
use Typecho\Widget\Exception;
use Widget\Base\Comments;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 反馈提交组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Feedback extends Comments implements ActionInterface
{
/**
* 内容对象
*
* @access private
* @var Archive
*/
private $content;
/**
* 对已注册用户的保护性检测
*
* @param string $userName 用户名
* @return bool
* @throws Db\Exception
*/
public function requireUserLogin(string $userName): bool
{
if ($this->user->hasLogin() && $this->user->screenName != $userName) {
/** 当前用户名与提交者不匹配 */
return false;
} elseif (
!$this->user->hasLogin() && $this->db->fetchRow($this->db->select('uid')
->from('table.users')->where('screenName = ? OR name = ?', $userName, $userName)->limit(1))
) {
/** 此用户名已经被注册 */
return false;
}
return true;
}
/**
* 初始化函数
*
* @throws \Exception
*/
public function action()
{
/** 回调方法 */
$callback = $this->request->get('type');
$this->content = Router::match($this->request->get('permalink'));
/** 判断内容是否存在 */
if (
$this->content instanceof Archive &&
$this->content->have() && $this->content->is('single') &&
in_array($callback, ['comment', 'trackback'])
) {
/** 如果文章不允许反馈 */
if ('comment' == $callback) {
/** 评论关闭 */
if (!$this->content->allow('comment')) {
throw new Exception(_t('对不起,此内容的反馈被禁止.'), 403);
}
/** 检查来源 */
if ($this->options->commentsCheckReferer && 'false' != $this->parameter->checkReferer) {
$referer = $this->request->getReferer();
if (empty($referer)) {
throw new Exception(_t('评论来源页错误.'), 403);
}
$refererPart = parse_url($referer);
$currentPart = parse_url($this->content->permalink);
if (
$refererPart['host'] != $currentPart['host'] ||
0 !== strpos($refererPart['path'], $currentPart['path'])
) {
//自定义首页支持
if ('page:' . $this->content->cid == $this->options->frontPage) {
$currentPart = parse_url(rtrim($this->options->siteUrl, '/') . '/');
if (
$refererPart['host'] != $currentPart['host'] ||
0 !== strpos($refererPart['path'], $currentPart['path'])
) {
throw new Exception(_t('评论来源页错误.'), 403);
}
} else {
throw new Exception(_t('评论来源页错误.'), 403);
}
}
}
/** 检查ip评论间隔 */
if (
!$this->user->pass('editor', true) && $this->content->authorId != $this->user->uid &&
$this->options->commentsPostIntervalEnable
) {
$latestComment = $this->db->fetchRow($this->db->select('created')->from('table.comments')
->where('cid = ? AND ip = ?', $this->content->cid, $this->request->getIp())
->order('created', Db::SORT_DESC)
->limit(1));
if (
$latestComment && ($this->options->time - $latestComment['created'] > 0 &&
$this->options->time - $latestComment['created'] < $this->options->commentsPostInterval)
) {
throw new Exception(_t('对不起, 您的发言过于频繁, 请稍候再次发布.'), 403);
}
}
}
/** 如果文章不允许引用 */
if ('trackback' == $callback && !$this->content->allow('ping')) {
throw new Exception(_t('对不起,此内容的引用被禁止.'), 403);
}
/** 调用函数 */
$this->$callback();
} else {
throw new Exception(_t('找不到内容'), 404);
}
}
/**
* 评论处理函数
*
* @throws \Exception
*/
private function comment()
{
// 使用安全模块保护
$this->security->enable($this->options->commentsAntiSpam);
$this->security->protect();
$comment = [
'cid' => $this->content->cid,
'created' => $this->options->time,
'agent' => $this->request->getAgent(),
'ip' => $this->request->getIp(),
'ownerId' => $this->content->author->uid,
'type' => 'comment',
'status' => !$this->content->allow('edit')
&& $this->options->commentsRequireModeration ? 'waiting' : 'approved'
];
/** 判断父节点 */
if ($parentId = $this->request->filter('int')->get('parent')) {
if (
$this->options->commentsThreaded
&& ($parent = $this->db->fetchRow($this->db->select('coid', 'cid')->from('table.comments')
->where('coid = ?', $parentId))) && $this->content->cid == $parent['cid']
) {
$comment['parent'] = $parentId;
} else {
throw new Exception(_t('父级评论不存在'));
}
}
//检验格式
$validator = new Validate();
$validator->addRule('author', 'required', _t('必须填写用户名'));
$validator->addRule('author', 'xssCheck', _t('请不要在用户名中使用特殊字符'));
$validator->addRule('author', [$this, 'requireUserLogin'], _t('您所使用的用户名已经被注册,请登录后再次提交'));
$validator->addRule('author', 'maxLength', _t('用户名最多包含150个字符'), 150);
if ($this->options->commentsRequireMail && !$this->user->hasLogin()) {
$validator->addRule('mail', 'required', _t('必须填写电子邮箱地址'));
}
$validator->addRule('mail', 'email', _t('邮箱地址不合法'));
$validator->addRule('mail', 'maxLength', _t('电子邮箱最多包含150个字符'), 150);
if ($this->options->commentsRequireUrl && !$this->user->hasLogin()) {
$validator->addRule('url', 'required', _t('必须填写个人主页'));
}
$validator->addRule('url', 'url', _t('个人主页地址格式错误'));
$validator->addRule('url', 'maxLength', _t('个人主页地址最多包含255个字符'), 255);
$validator->addRule('text', 'required', _t('必须填写评论内容'));
$comment['text'] = $this->request->get('text');
/** 对一般匿名访问者,将用户数据保存一个月 */
if (!$this->user->hasLogin()) {
/** Anti-XSS */
$comment['author'] = $this->request->filter('trim')->get('author');
$comment['mail'] = $this->request->filter('trim')->get('mail');
$comment['url'] = $this->request->filter('trim', 'url')->get('url');
/** 修正用户提交的url */
if (!empty($comment['url'])) {
$urlParams = parse_url($comment['url']);
if (!isset($urlParams['scheme'])) {
$comment['url'] = 'https://' . $comment['url'];
}
}
$expire = 30 * 24 * 3600;
Cookie::set('__typecho_remember_author', $comment['author'], $expire);
Cookie::set('__typecho_remember_mail', $comment['mail'], $expire);
Cookie::set('__typecho_remember_url', $comment['url'], $expire);
} else {
$comment['author'] = $this->user->screenName;
$comment['mail'] = $this->user->mail;
$comment['url'] = $this->user->url;
/** 记录登录用户的id */
$comment['authorId'] = $this->user->uid;
}
/** 评论者之前须有评论通过了审核 */
if (!$this->options->commentsRequireModeration && $this->options->commentsWhitelist) {
if (
$this->size(
$this->select()->where(
'author = ? AND mail = ? AND status = ?',
$comment['author'],
$comment['mail'],
'approved'
)
)
) {
$comment['status'] = 'approved';
} else {
$comment['status'] = 'waiting';
}
}
if ($error = $validator->run($comment)) {
/** 记录文字 */
Cookie::set('__typecho_remember_text', $comment['text']);
throw new Exception(implode("\n", $error));
}
/** 生成过滤器 */
try {
$comment = self::pluginHandle()->filter('comment', $comment, $this->content);
} catch (\Typecho\Exception $e) {
Cookie::set('__typecho_remember_text', $comment['text']);
throw $e;
}
/** 添加评论 */
$commentId = $this->insert($comment);
Cookie::delete('__typecho_remember_text');
$this->db->fetchRow($this->select()->where('coid = ?', $commentId)
->limit(1), [$this, 'push']);
/** 评论完成接口 */
self::pluginHandle()->call('finishComment', $this);
if ($this->status !== 'approved') {
Cookie::set('__typecho_unapproved_comment', $commentId);
}
$this->response->redirect($this->permalink);
}
/**
* 引用处理函数
*
* @throws Exception|Db\Exception
*/
private function trackback()
{
/** 如果不是POST方法 */
if (!$this->request->isPost() || $this->request->getReferer()) {
$this->response->redirect($this->content->permalink);
}
/** 如果库中已经存在当前ip为spam的trackback则直接拒绝 */
if (
$this->size($this->select()
->where('status = ? AND ip = ?', 'spam', $this->request->getIp())) > 0
) {
/** 使用404告诉机器人 */
throw new Exception(_t('找不到内容'), 404);
}
$trackback = [
'cid' => $this->content->cid,
'created' => $this->options->time,
'agent' => $this->request->getAgent(),
'ip' => $this->request->getIp(),
'ownerId' => $this->content->author->uid,
'type' => 'trackback',
'status' => $this->options->commentsRequireModeration ? 'waiting' : 'approved'
];
$trackback['author'] = $this->request->filter('trim')->get('blog_name');
$trackback['url'] = $this->request->filter('trim', 'url')->get('url');
$trackback['text'] = $this->request->get('excerpt');
//检验格式
$validator = new Validate();
$validator->addRule('url', 'required', 'We require all Trackbacks to provide an url.')
->addRule('url', 'url', 'Your url is not valid.')
->addRule('url', 'maxLength', 'Your url is not valid.', 255)
->addRule('text', 'required', 'We require all Trackbacks to provide an excerption.')
->addRule('author', 'required', 'We require all Trackbacks to provide an blog name.')
->addRule('author', 'xssCheck', 'Your blog name is not valid.')
->addRule('author', 'maxLength', 'Your blog name is not valid.', 150);
$validator->setBreak();
if ($error = $validator->run($trackback)) {
$message = ['success' => 1, 'message' => current($error)];
$this->response->throwXml($message);
}
/** 截取长度 */
$trackback['text'] = Common::subStr($trackback['text'], 0, 100, '[...]');
/** 如果库中已经存在重复url则直接拒绝 */
if (
$this->size($this->select()
->where('cid = ? AND url = ? AND type <> ?', $this->content->cid, $trackback['url'], 'comment')) > 0
) {
/** 使用403告诉机器人 */
throw new Exception(_t('禁止重复提交'), 403);
}
/** 生成过滤器 */
$trackback = self::pluginHandle()->filter('trackback', $trackback, $this->content);
/** 添加引用 */
$this->insert($trackback);
/** 评论完成接口 */
self::pluginHandle()->call('finishTrackback', $this);
/** 返回正确 */
$this->response->throwXml(['success' => 0, 'message' => 'Trackback has registered.']);
}
}

119
var/Widget/Init.php Executable file
View File

@@ -0,0 +1,119 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Date;
use Typecho\Db;
use Typecho\I18n;
use Typecho\Plugin;
use Typecho\Response;
use Typecho\Router;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 初始化模块
*
* @package Widget
*/
class Init extends Widget
{
/**
* 入口函数,初始化路由器
*
* @access public
* @return void
* @throws Db\Exception
*/
public function execute()
{
/** 初始化exception */
if (!defined('__TYPECHO_DEBUG__') || !__TYPECHO_DEBUG__) {
set_exception_handler(function (\Throwable $exception) {
Response::getInstance()->clean();
ob_end_clean();
ob_start(function ($content) {
Response::getInstance()->sendHeaders();
return $content;
});
if (404 == $exception->getCode()) {
ExceptionHandle::alloc();
} else {
Common::error($exception);
}
exit;
});
}
// init class
define('__TYPECHO_CLASS_ALIASES__', [
'Typecho_Plugin_Interface' => '\Typecho\Plugin\PluginInterface',
'Typecho_Widget_Helper_Empty' => '\Typecho\Widget\Helper\EmptyClass',
'Typecho_Db_Adapter_Mysql' => '\Typecho\Db\Adapter\Mysqli',
'Widget_Abstract' => '\Widget\Base',
'Widget_Abstract_Contents' => '\Widget\Base\Contents',
'Widget_Abstract_Comments' => '\Widget\Base\Comments',
'Widget_Abstract_Metas' => '\Widget\Base\Metas',
'Widget_Abstract_Options' => '\Widget\Base\Options',
'Widget_Abstract_Users' => '\Widget\Base\Users',
'Widget_Metas_Category_List' => '\Widget\Metas\Category\Rows',
'Widget_Contents_Page_List' => '\Widget\Contents\Page\Rows',
'Widget_Plugins_List' => '\Widget\Plugins\Rows',
'Widget_Themes_List' => '\Widget\Themes\Rows',
'Widget_Interface_Do' => '\Widget\ActionInterface',
'Widget_Do' => '\Widget\Action',
'AutoP' => '\Utils\AutoP',
'PasswordHash' => '\Utils\PasswordHash',
'Markdown' => '\Utils\Markdown',
'HyperDown' => '\Utils\HyperDown',
'Helper' => '\Utils\Helper',
'Upgrade' => '\Utils\Upgrade'
]);
/** 对变量赋值 */
$options = Options::alloc();
/** 语言包初始化 */
if ($options->lang && $options->lang != 'zh_CN') {
$dir = defined('__TYPECHO_LANG_DIR__') ? __TYPECHO_LANG_DIR__ : __TYPECHO_ROOT_DIR__ . '/usr/langs';
I18n::setLang($dir . '/' . $options->lang . '.mo');
}
/** 备份文件目录初始化 */
if (!defined('__TYPECHO_BACKUP_DIR__')) {
define('__TYPECHO_BACKUP_DIR__', __TYPECHO_ROOT_DIR__ . '/usr/backups');
}
/** cookie初始化 */
Cookie::setPrefix($options->rootUrl);
if (defined('__TYPECHO_COOKIE_OPTIONS__')) {
Cookie::setOptions(__TYPECHO_COOKIE_OPTIONS__);
}
/** 初始化路由器 */
Router::setRoutes($options->routingTable);
/** 初始化插件 */
Plugin::init($options->plugins);
/** 初始化回执 */
$this->response->setCharset($options->charset);
$this->response->setContentType($options->contentType);
/** 初始化时区 */
Date::setTimezoneOffset($options->timezone);
/** 开始会话, 减小负载只针对后台打开session支持 */
if ($options->installed && User::alloc()->hasLogin()) {
@session_start();
}
}
}

112
var/Widget/Login.php Executable file
View File

@@ -0,0 +1,112 @@
<?php
namespace Widget;
use Typecho\Cookie;
use Typecho\Validate;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 登录组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Login extends Users implements ActionInterface
{
/**
* 初始化函数
*
* @access public
* @return void
*/
public function action()
{
// protect
$this->security->protect();
/** 如果已经登录 */
if ($this->user->hasLogin()) {
/** 直接返回 */
$this->response->redirect($this->options->index);
}
/** 初始化验证类 */
$validator = new Validate();
$validator->addRule('name', 'required', _t('请输入用户名'));
$validator->addRule('password', 'required', _t('请输入密码'));
$expire = 30 * 24 * 3600;
/** 记住密码状态 */
if ($this->request->is('remember=1')) {
Cookie::set('__typecho_remember_remember', 1, $expire);
} elseif (Cookie::get('__typecho_remember_remember')) {
Cookie::delete('__typecho_remember_remember');
}
/** 截获验证异常 */
if ($error = $validator->run($this->request->from('name', 'password'))) {
Cookie::set('__typecho_remember_name', $this->request->get('name'));
/** 设置提示信息 */
Notice::alloc()->set($error);
$this->response->goBack();
}
/** 开始验证用户 **/
$valid = $this->user->login(
$this->request->get('name'),
$this->request->get('password'),
false,
$this->request->is('remember=1') ? $expire : 0
);
/** 比对密码 */
if (!$valid) {
/** 防止穷举,休眠3秒 */
sleep(3);
self::pluginHandle()->call(
'loginFailure',
$this->user,
$this->request->get('name'),
$this->request->get('password'),
$this->request->is('remember=1')
);
Cookie::set('__typecho_remember_name', $this->request->get('name'));
Notice::alloc()->set(_t('用户名或密码无效'), 'error');
$this->response->goBack('?referer=' . urlencode($this->request->get('referer')));
}
self::pluginHandle()->call(
'loginSuccess',
$this->user,
$this->request->get('name'),
$this->request->get('password'),
$this->request->is('remember=1')
);
/** 跳转验证后地址 */
if (!empty($this->request->referer)) {
/** fix #952 & validate redirect url */
if (
0 === strpos($this->request->referer, $this->options->adminUrl)
|| 0 === strpos($this->request->referer, $this->options->siteUrl)
) {
$this->response->redirect($this->request->referer);
}
} elseif (!$this->user->pass('contributor', true)) {
/** 不允许普通用户直接跳转后台 */
$this->response->redirect($this->options->profileUrl);
}
$this->response->redirect($this->options->adminUrl);
}
}

37
var/Widget/Logout.php Executable file
View File

@@ -0,0 +1,37 @@
<?php
namespace Widget;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 登出组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Logout extends Users implements ActionInterface
{
/**
* 初始化函数
*
* @access public
* @return void
*/
public function action()
{
// protect
$this->security->protect();
$this->user->logout();
self::pluginHandle()->call('logout');
@session_destroy();
$this->response->goBack(null, $this->options->index);
}
}

327
var/Widget/Menu.php Executable file
View File

@@ -0,0 +1,327 @@
<?php
namespace Widget;
use Typecho\Common;
use Widget\Plugins\Config;
use Widget\Themes\Files;
use Widget\Users\Edit as UsersEdit;
use Widget\Contents\Attachment\Edit as AttachmentEdit;
use Widget\Contents\Post\Edit as PostEdit;
use Widget\Contents\Page\Edit as PageEdit;
use Widget\Contents\Post\Admin as PostAdmin;
use Widget\Contents\Page\Admin as PageAdmin;
use Widget\Comments\Admin as CommentsAdmin;
use Widget\Metas\Category\Admin as CategoryAdmin;
use Widget\Metas\Category\Edit as CategoryEdit;
use Widget\Metas\Tag\Admin as TagAdmin;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 后台菜单显示
*
* @package Widget
*/
class Menu extends Base
{
/**
* 当前菜单标题
* @var string
*/
public string $title;
/**
* 当前增加项目链接
* @var string|null
*/
public ?string $addLink;
/**
* 父菜单列表
*
* @var array
*/
private array $menu = [];
/**
* 当前父菜单
*
* @var integer
*/
private int $currentParent = 1;
/**
* 当前子菜单
*
* @var integer
*/
private int $currentChild = 0;
/**
* 当前页面
*
* @var string
*/
private string $currentUrl;
/**
* 当前菜单URL
*
* @var string
*/
private string $currentMenuUrl;
/**
* 执行函数,初始化菜单
*/
public function execute()
{
$parentNodes = [null, _t('控制台'), _t('撰写'), _t('管理'), _t('设置')];
$childNodes = [
[
[_t('登录'), _t('登录到%s', $this->options->title), 'login.php', 'visitor'],
[_t('注册'), _t('注册到%s', $this->options->title), 'register.php', 'visitor']
],
[
[_t('概要'), _t('网站概要'), 'index.php', 'subscriber'],
[_t('个人设置'), _t('个人设置'), 'profile.php', 'subscriber'],
[_t('插件'), _t('插件管理'), 'plugins.php', 'administrator'],
[[Config::class, 'getMenuTitle'], [Config::class, 'getMenuTitle'], 'options-plugin.php?config=', 'administrator', true],
[_t('外观'), _t('网站外观'), 'themes.php', 'administrator'],
[[Files::class, 'getMenuTitle'], [Files::class, 'getMenuTitle'], 'theme-editor.php', 'administrator', true],
[_t('设置外观'), _t('设置外观'), 'options-theme.php', 'administrator', true],
[_t('备份'), _t('备份'), 'backup.php', 'administrator'],
[_t('升级'), _t('升级程序'), 'upgrade.php', 'administrator', true],
[_t('欢迎'), _t('欢迎使用'), 'welcome.php', 'subscriber', true]
],
[
[_t('撰写文章'), _t('撰写新文章'), 'write-post.php', 'contributor'],
[[PostEdit::class, 'getMenuTitle'], [PostEdit::class, 'getMenuTitle'], 'write-post.php?cid=', 'contributor', true],
[_t('创建页面'), _t('创建新页面'), 'write-page.php', 'editor'],
[[PageEdit::class, 'getMenuTitle'], [PageEdit::class, 'getMenuTitle'], 'write-page.php?cid=', 'editor', true],
[[PageEdit::class, 'getMenuTitle'], [PageEdit::class, 'getMenuTitle'], 'write-page.php?parent=', 'editor', true],
],
[
[_t('文章'), _t('管理文章'), 'manage-posts.php', 'contributor', false, 'write-post.php'],
[[PostAdmin::class, 'getMenuTitle'], [PostAdmin::class, 'getMenuTitle'], 'manage-posts.php?uid=', 'contributor', true],
[_t('独立页面'), _t('管理独立页面'), 'manage-pages.php', 'editor', false, 'write-page.php'],
[[PageAdmin::class, 'getMenuTitle'], [PageAdmin::class, 'getMenuTitle'], 'manage-pages.php?parent=', 'editor', true, [PageAdmin::class, 'getAddLink']],
[_t('评论'), _t('管理评论'), 'manage-comments.php', 'contributor'],
[[CommentsAdmin::class, 'getMenuTitle'], [CommentsAdmin::class, 'getMenuTitle'], 'manage-comments.php?cid=', 'contributor', true],
[_t('分类'), _t('管理分类'), 'manage-categories.php', 'editor', false, 'category.php'],
[_t('新增分类'), _t('新增分类'), 'category.php', 'editor', true],
[[CategoryAdmin::class, 'getMenuTitle'], [CategoryAdmin::class, 'getMenuTitle'], 'manage-categories.php?parent=', 'editor', true, [CategoryAdmin::class, 'getAddLink']],
[[CategoryEdit::class, 'getMenuTitle'], [CategoryEdit::class, 'getMenuTitle'], 'category.php?mid=', 'editor', true],
[[CategoryEdit::class, 'getMenuTitle'], [CategoryEdit::class, 'getMenuTitle'], 'category.php?parent=', 'editor', true],
[_t('标签'), _t('管理标签'), 'manage-tags.php', 'editor'],
[[TagAdmin::class, 'getMenuTitle'], [TagAdmin::class, 'getMenuTitle'], 'manage-tags.php?mid=', 'editor', true],
[_t('文件'), _t('管理文件'), 'manage-medias.php', 'editor'],
[[AttachmentEdit::class, 'getMenuTitle'], [AttachmentEdit::class, 'getMenuTitle'], 'media.php?cid=', 'contributor', true],
[_t('用户'), _t('管理用户'), 'manage-users.php', 'administrator', false, 'user.php'],
[_t('新增用户'), _t('新增用户'), 'user.php', 'administrator', true],
[[UsersEdit::class, 'getMenuTitle'], [UsersEdit::class, 'getMenuTitle'], 'user.php?uid=', 'administrator', true],
],
[
[_t('基本'), _t('基本设置'), 'options-general.php', 'administrator'],
[_t('评论'), _t('评论设置'), 'options-discussion.php', 'administrator'],
[_t('阅读'), _t('阅读设置'), 'options-reading.php', 'administrator'],
[_t('永久链接'), _t('永久链接设置'), 'options-permalink.php', 'administrator'],
]
];
/** 获取扩展菜单 */
$panelTable = $this->options->panelTable;
$extendingParentMenu = empty($panelTable['parent']) ? [] : $panelTable['parent'];
$extendingChildMenu = empty($panelTable['child']) ? [] : $panelTable['child'];
$currentUrl = $this->request->getRequestUrl();
$adminUrl = $this->options->adminUrl;
$menu = [];
$defaultChildNode = [null, null, null, 'administrator', false, null];
$currentUrlParts = parse_url($currentUrl);
$currentUrlParams = [];
if (!empty($currentUrlParts['query'])) {
parse_str($currentUrlParts['query'], $currentUrlParams);
}
if ('/' == $currentUrlParts['path'][strlen($currentUrlParts['path']) - 1]) {
$currentUrlParts['path'] .= 'index.php';
}
foreach ($extendingParentMenu as $key => $val) {
$parentNodes[10 + $key] = $val;
}
foreach ($extendingChildMenu as $key => $val) {
$childNodes[$key] = array_merge($childNodes[$key] ?? [], $val);
}
foreach ($parentNodes as $key => $parentNode) {
// this is a simple struct than before
$children = [];
$showedChildrenCount = 0;
$firstUrl = null;
foreach ($childNodes[$key] as $inKey => $childNode) {
// magic merge
$childNode += $defaultChildNode;
[$name, $title, $url, $access] = $childNode;
$hidden = $childNode[4] ?? false;
$addLink = $childNode[5] ?? null;
// 保存最原始的hidden信息
$orgHidden = $hidden;
// parse url
$menuUrl = $url;
$url = Common::url($url, $adminUrl);
// compare url
$urlParts = parse_url($url);
$urlParams = [];
if (!empty($urlParts['query'])) {
parse_str($urlParts['query'], $urlParams);
}
$validate = true;
if ($urlParts['path'] != $currentUrlParts['path']) {
$validate = false;
} else {
foreach ($urlParams as $paramName => $paramValue) {
if (!isset($currentUrlParams[$paramName])) {
$validate = false;
break;
}
}
}
if (
$validate
&& basename($urlParts['path']) == 'extending.php'
&& !empty($currentUrlParams['panel']) && !empty($urlParams['panel'])
&& $urlParams['panel'] != $currentUrlParams['panel']
) {
$validate = false;
}
if ($hidden && $validate) {
$hidden = false;
}
if (!$hidden && !$this->user->pass($access, true)) {
$hidden = true;
}
if (!$hidden) {
$showedChildrenCount++;
if (empty($firstUrl)) {
$firstUrl = $url;
}
if (is_array($name)) {
[$widget, $method] = $name;
$name = self::widget($widget)->$method();
}
if (is_array($title)) {
[$widget, $method] = $title;
$title = self::widget($widget)->$method();
}
if (is_array($addLink)) {
[$widget, $method] = $addLink;
$addLink = self::widget($widget)->$method();
}
}
if ($validate) {
if ('visitor' != $access) {
$this->user->pass($access);
}
$this->currentParent = $key;
$this->currentChild = $inKey;
$this->title = $title;
$this->addLink = $addLink ? Common::url($addLink, $adminUrl) : null;
$this->currentMenuUrl = $menuUrl;
}
$children[$inKey] = [
$name,
$title,
$url,
$access,
$hidden,
$addLink,
$orgHidden
];
}
$menu[$key] = [$parentNode, $showedChildrenCount > 0, $firstUrl, $children];
}
$this->menu = $menu;
$this->currentUrl = Common::safeUrl($currentUrl);
}
/**
* 获取当前菜单
*
* @return array
*/
public function getCurrentMenu(): ?array
{
return $this->currentParent > 0 ? $this->menu[$this->currentParent][3][$this->currentChild] : null;
}
/**
* 获取当前菜单URL
*
* @return string
*/
public function getCurrentMenuUrl(): string
{
return $this->currentMenuUrl;
}
/**
* 输出父级菜单
*/
public function output($class = 'focus', $childClass = 'focus')
{
foreach ($this->menu as $key => $node) {
if (!$node[1] || !$key) {
continue;
}
echo "<li" . ($key == $this->currentParent ? " class=\"{$class}\"" : '')
. "><a href=\"{$node[2]}\">{$node[0]}</a>"
. "<menu>";
foreach ($node[3] as $inKey => $inNode) {
if ($inNode[4]) {
continue;
}
$focus = false;
if ($key == $this->currentParent && $inKey == $this->currentChild) {
$focus = true;
} elseif ($inNode[6]) {
continue;
}
echo "<li" . ($focus ? " class=\"{$childClass}\"" : '') . "><a href=\""
. ($key == $this->currentParent && $inKey == $this->currentChild ? $this->currentUrl : $inNode[2])
. "\">{$inNode[0]}</a></li>";
}
echo '</menu></li>';
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Widget\Metas\Category;
use Typecho\Common;
use Typecho\Db;
use Typecho\Widget\Exception;
use Widget\Base\Metas;
use Widget\Base\TreeTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* Category Admin
*/
class Admin extends Metas
{
use InitTreeRowsTrait;
use TreeTrait;
/**
* @var int Parent category
*/
private int $parentId = 0;
/**
* 执行函数
*/
public function execute()
{
$this->parentId = $this->request->filter('int')->get('parent', 0);
$this->pushAll($this->getRows($this->getChildIds($this->parentId)));
}
/**
* 向上的返回链接
*
* @throws Db\Exception
*/
public function backLink()
{
if ($this->parentId) {
$category = $this->getRow($this->parentId);
if (!empty($category)) {
$parent = $this->getRow($category['parent']);
if ($parent) {
echo '<a href="'
. Common::url('manage-categories.php?parent=' . $parent['mid'], $this->options->adminUrl)
. '">';
} else {
echo '<a href="' . Common::url('manage-categories.php', $this->options->adminUrl) . '">';
}
echo '&laquo; ';
_e('返回父级分类');
echo '</a>';
}
}
}
/**
* 获取菜单标题
*
* @return string|null
* @throws Db\Exception|Exception
*/
public function getMenuTitle(): ?string
{
if ($this->parentId) {
$category = $this->getRow($this->parentId);
if (!empty($category)) {
return _t('管理 %s 的子分类', $category['name']);
}
} else {
return null;
}
throw new Exception(_t('分类不存在'), 404);
}
/**
* 获取菜单标题
*
* @return string
*/
public function getAddLink(): string
{
return 'category.php' . ($this->parentId ? '?parent=' . $this->parentId : '');
}
}

View File

@@ -0,0 +1,514 @@
<?php
namespace Widget\Metas\Category;
use Typecho\Common;
use Typecho\Db\Exception;
use Typecho\Validate;
use Typecho\Widget\Helper\Form;
use Widget\Base\Metas;
use Widget\ActionInterface;
use Widget\Metas\EditTrait;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑分类组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Metas implements ActionInterface
{
use EditTrait;
/**
* 入口函数
* @throws \Exception
*/
public function execute()
{
/** 编辑以上权限 */
$this->user->pass('editor');
}
/**
* 判断分类是否存在
*
* @param integer $mid 分类主键
* @return boolean
* @throws Exception
*/
public function categoryExists(int $mid): bool
{
$category = $this->db->fetchRow($this->db->select()
->from('table.metas')
->where('type = ?', 'category')
->where('mid = ?', $mid)->limit(1));
return isset($category);
}
/**
* 判断分类名称是否存在
* fix #1843 将重复性判断限制在同一父分类下
*
* @param string $name 分类名称
* @return boolean
* @throws Exception
*/
public function nameExists(string $name): bool
{
$select = $this->db->select()
->from('table.metas')
->where('type = ?', 'category')
->where('name = ?', $name)
->limit(1);
if ($this->request->is('mid')) {
$select->where('mid <> ?', $this->request->get('mid'));
}
// 只在同一父分类下判断重复性
$select->where('parent = ?', $this->request->filter('int')->get('parent', 0));
$category = $this->db->fetchRow($select);
return !$category;
}
/**
* 判断分类名转换到缩略名后是否合法
*
* @param string $name 分类名
* @return boolean
* @throws Exception
*/
public function nameToSlug(string $name): bool
{
if (empty($this->request->slug)) {
$slug = Common::slugName($name);
if (empty($slug) || !$this->slugExists($name)) {
return false;
}
}
return true;
}
/**
* 判断分类缩略名是否存在
*
* @param string $slug 缩略名
* @return boolean
* @throws Exception
*/
public function slugExists(string $slug): bool
{
$select = $this->db->select()
->from('table.metas')
->where('type = ?', 'category')
->where('slug = ?', Common::slugName($slug))
->limit(1);
if ($this->request->is('mid')) {
$select->where('mid <> ?', $this->request->get('mid'));
}
$category = $this->db->fetchRow($select);
return !$category;
}
/**
* 增加分类
*
* @throws Exception
*/
public function insertCategory()
{
if ($this->form('insert')->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$category = $this->request->from('name', 'slug', 'description', 'parent');
$category['slug'] = Common::slugName(Common::strBy($category['slug'] ?? null, $category['name']));
$category['type'] = 'category';
$category['order'] = $this->getMaxOrder('category', $category['parent']) + 1;
/** 插入数据 */
$category['mid'] = $this->insert($category);
$this->push($category);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 提示信息 */
Notice::alloc()->set(
_t('分类 <a href="%s">%s</a> 已经被增加', $this->permalink, $this->name),
'success'
);
/** 转向原页 */
$this->response->redirect(Common::url('manage-categories.php'
. ($category['parent'] ? '?parent=' . $category['parent'] : ''), $this->options->adminUrl));
}
/**
* 生成表单
*
* @param string|null $action 表单动作
* @return Form
* @throws Exception
*/
public function form(?string $action = null): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/metas-category-edit'), Form::POST_METHOD);
/** 分类名称 */
$name = new Form\Element\Text('name', null, null, _t('分类名称') . ' *');
$form->addInput($name);
/** 分类缩略名 */
$slug = new Form\Element\Text(
'slug',
null,
null,
_t('分类缩略名'),
_t('分类缩略名用于创建友好的链接形式, 建议使用字母, 数字, 下划线和横杠.')
);
$form->addInput($slug);
/** 父级分类 */
$options = [0 => _t('不选择')];
$parents = Rows::allocWithAlias(
'options',
($this->request->is('mid') ? 'ignore=' . $this->request->get('mid') : '')
);
while ($parents->next()) {
$options[$parents->mid] = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $parents->levels) . $parents->name;
}
$parent = new Form\Element\Select(
'parent',
$options,
$this->request->get('parent'),
_t('父级分类'),
_t('此分类将归档在您选择的父级分类下.')
);
$form->addInput($parent);
/** 分类描述 */
$description = new Form\Element\Textarea(
'description',
null,
null,
_t('分类描述'),
_t('此文字用于描述分类, 在有的主题中它会被显示.')
);
$form->addInput($description);
/** 分类动作 */
$do = new Form\Element\Hidden('do');
$form->addInput($do);
/** 分类主键 */
$mid = new Form\Element\Hidden('mid');
$form->addInput($mid);
/** 提交按钮 */
$submit = new Form\Element\Submit();
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
if (isset($this->request->mid) && 'insert' != $action) {
/** 更新模式 */
$meta = $this->db->fetchRow($this->select()
->where('mid = ?', $this->request->mid)
->where('type = ?', 'category')->limit(1));
if (!$meta) {
$this->response->redirect(Common::url('manage-categories.php', $this->options->adminUrl));
}
$name->value($meta['name']);
$slug->value($meta['slug']);
$parent->value($meta['parent']);
$description->value($meta['description']);
$do->value('update');
$mid->value($meta['mid']);
$submit->value(_t('编辑分类'));
$_action = 'update';
} else {
$do->value('insert');
$submit->value(_t('增加分类'));
$_action = 'insert';
}
if (empty($action)) {
$action = $_action;
}
/** 给表单增加规则 */
if ('insert' == $action || 'update' == $action) {
$name->addRule('required', _t('必须填写分类名称'));
$name->addRule([$this, 'nameExists'], _t('分类名称已经存在'));
$name->addRule([$this, 'nameToSlug'], _t('分类名称无法被转换为缩略名'));
$name->addRule('xssCheck', _t('请不要在分类名称中使用特殊字符'));
$slug->addRule([$this, 'slugExists'], _t('缩略名已经存在'));
$slug->addRule('xssCheck', _t('请不要在缩略名中使用特殊字符'));
}
if ('update' == $action) {
$mid->addRule('required', _t('分类主键不存在'));
$mid->addRule([$this, 'categoryExists'], _t('分类不存在'));
}
return $form;
}
/**
* 更新分类
*
* @throws Exception
*/
public function updateCategory()
{
if ($this->form('update')->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$category = $this->request->from('name', 'slug', 'description', 'parent');
$category['mid'] = $this->request->get('mid');
$category['slug'] = Common::slugName(Common::strBy($category['slug'] ?? null, $category['name']));
$category['type'] = 'category';
$current = $this->db->fetchRow($this->select()->where('mid = ?', $category['mid']));
if ($current['parent'] != $category['parent']) {
$parent = $this->db->fetchRow($this->select()->where('mid = ?', $category['parent']));
if ($parent['mid'] == $category['mid']) {
$category['order'] = $parent['order'];
$this->update([
'parent' => $current['parent'],
'order' => $current['order']
], $this->db->sql()->where('mid = ?', $parent['mid']));
} else {
$category['order'] = $this->getMaxOrder('category', $category['parent']) + 1;
}
}
/** 更新数据 */
$this->update($category, $this->db->sql()->where('mid = ?', $this->request->filter('int')->get('mid')));
$this->push($category);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 提示信息 */
Notice::alloc()
->set(_t('分类 <a href="%s">%s</a> 已经被更新', $this->permalink, $this->name), 'success');
/** 转向原页 */
$this->response->redirect(Common::url('manage-categories.php'
. ($category['parent'] ? '?parent=' . $category['parent'] : ''), $this->options->adminUrl));
}
/**
* 删除分类
*
* @access public
* @return void
* @throws Exception
*/
public function deleteCategory()
{
$categories = $this->request->filter('int')->getArray('mid');
$deleteCount = 0;
foreach ($categories as $category) {
$parent = $this->db->fetchObject($this->select()->where('mid = ?', $category))->parent;
if ($this->delete($this->db->sql()->where('mid = ?', $category))) {
$this->db->query($this->db->delete('table.relationships')->where('mid = ?', $category));
$this->update(['parent' => $parent], $this->db->sql()->where('parent = ?', $category));
$deleteCount++;
}
}
/** 提示信息 */
Notice::alloc()
->set($deleteCount > 0 ? _t('分类已经删除') : _t('没有分类被删除'), $deleteCount > 0 ? 'success' : 'notice');
/** 转向原页 */
$this->response->goBack();
}
/**
* 合并分类
* @throws Exception
*/
public function mergeCategory()
{
/** 验证数据 */
$validator = new Validate();
$validator->addRule('merge', 'required', _t('分类主键不存在'));
$validator->addRule('merge', [$this, 'categoryExists'], _t('请选择需要合并的分类'));
if ($error = $validator->run($this->request->from('merge'))) {
Notice::alloc()->set($error, 'error');
$this->response->goBack();
}
$merge = $this->request->get('merge');
$categories = $this->request->filter('int')->getArray('mid');
if ($categories) {
$this->merge($merge, 'category', $categories);
/** 提示信息 */
Notice::alloc()->set(_t('分类已经合并'), 'success');
} else {
Notice::alloc()->set(_t('没有选择任何分类'));
}
/** 转向原页 */
$this->response->goBack();
}
/**
* 分类排序
* @throws Exception
*/
public function sortCategory()
{
$categories = $this->request->filter('int')->getArray('mid');
if ($categories) {
$this->sort($categories, 'category');
}
if (!$this->request->isAjax()) {
/** 转向原页 */
$this->response->redirect(Common::url('manage-categories.php', $this->options->adminUrl));
} else {
$this->response->throwJson(['success' => 1, 'message' => _t('分类排序已经完成')]);
}
}
/**
* 刷新分类
*
* @throws Exception
*/
public function refreshCategory()
{
$categories = $this->request->filter('int')->getArray('mid');
if ($categories) {
foreach ($categories as $category) {
$this->refreshCountByTypeAndStatus($category, 'post');
}
Notice::alloc()->set(_t('分类刷新已经完成'), 'success');
} else {
Notice::alloc()->set(_t('没有选择任何分类'));
}
/** 转向原页 */
$this->response->goBack();
}
/**
* 设置默认分类
*
* @throws Exception
*/
public function defaultCategory()
{
/** 验证数据 */
$validator = new Validate();
$validator->addRule('mid', 'required', _t('分类主键不存在'));
$validator->addRule('mid', [$this, 'categoryExists'], _t('分类不存在'));
if ($error = $validator->run($this->request->from('mid'))) {
Notice::alloc()->set($error, 'error');
} else {
$this->db->query($this->db->update('table.options')
->rows(['value' => $this->request->get('mid')])
->where('name = ?', 'defaultCategory'));
$this->db->fetchRow($this->select()->where('mid = ?', $this->request->get('mid'))
->where('type = ?', 'category')->limit(1), [$this, 'push']);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 提示信息 */
Notice::alloc()->set(
_t('<a href="%s">%s</a> 已经被设为默认分类', $this->permalink, $this->name),
'success'
);
}
/** 转向原页 */
$this->response->redirect(Common::url('manage-categories.php', $this->options->adminUrl));
}
/**
* 获取菜单标题
*
* @return string|null
* @throws \Typecho\Widget\Exception|Exception
*/
public function getMenuTitle(): ?string
{
if ($this->request->is('mid')) {
$category = $this->db->fetchRow($this->select()
->where('type = ? AND mid = ?', 'category', $this->request->filter('int')->get('mid')));
if (!empty($category)) {
return _t('编辑分类 %s', $category['name']);
}
}
if ($this->request->is('parent')) {
$category = $this->db->fetchRow($this->select()
->where('type = ? AND mid = ?', 'category', $this->request->filter('int')->get('parent')));
if (!empty($category)) {
return _t('新增 %s 的子分类', $category['name']);
}
} else {
return null;
}
throw new \Typecho\Widget\Exception(_t('分类不存在'), 404);
}
/**
* 入口函数
*
* @access public
* @return void
* @throws Exception
*/
public function action()
{
$this->security->protect();
$this->on($this->request->is('do=insert'))->insertCategory();
$this->on($this->request->is('do=update'))->updateCategory();
$this->on($this->request->is('do=delete'))->deleteCategory();
$this->on($this->request->is('do=merge'))->mergeCategory();
$this->on($this->request->is('do=sort'))->sortCategory();
$this->on($this->request->is('do=refresh'))->refreshCategory();
$this->on($this->request->is('do=default'))->defaultCategory();
$this->response->redirect($this->options->adminUrl);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Widget\Metas\Category;
use Typecho\Db\Exception;
/**
* Trait InitTreeRowsTrait
*/
trait InitTreeRowsTrait
{
/**
* @return array
* @throws Exception
*/
protected function initTreeRows(): array
{
return $this->db->fetchAll($this->select()
->where('type = ?', 'category'));
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Widget\Metas\Category;
use Typecho\Db\Exception;
use Widget\Base\Metas;
use Widget\Base\TreeTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
class Related extends Metas
{
use InitTreeRowsTrait;
use TreeTrait;
/**
* @return void
* @throws Exception
*/
public function execute()
{
$ids = array_column($this->db->fetchAll($this->select('table.metas.mid')
->join('table.relationships', 'table.relationships.mid = table.metas.mid')
->where('table.relationships.cid = ?', $this->parameter->cid)
->where('table.metas.type = ?', 'category')), 'mid');
usort($ids, function ($a, $b) {
$orderA = array_search($a, $this->orders);
$orderB = array_search($b, $this->orders);
return $orderA <=> $orderB;
});
$this->pushAll($this->getRows($ids));
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Widget\Metas\Category;
use Typecho\Config;
use Widget\Base\Metas;
use Widget\Base\TreeViewTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 分类输出组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
* @property-read int $levels
* @property-read array $children
*/
class Rows extends Metas
{
use InitTreeRowsTrait;
use TreeViewTrait;
/**
* 执行函数
*
* @return void
*/
public function execute()
{
$this->pushAll($this->getRows($this->orders, $this->parameter->ignore));
}
/**
* treeViewCategories
*
* @param mixed $categoryOptions 输出选项
*/
public function listCategories($categoryOptions = null)
{
//初始化一些变量
$categoryOptions = Config::factory($categoryOptions);
$categoryOptions->setDefault([
'wrapTag' => 'ul',
'wrapClass' => '',
'itemTag' => 'li',
'itemClass' => '',
'showCount' => false,
'showFeed' => false,
'countTemplate' => '(%d)',
'feedTemplate' => '<a href="%s">RSS</a>'
]);
// 插件插件接口
self::pluginHandle()->trigger($plugged)->call('listCategories', $categoryOptions, $this);
if (!$plugged) {
$this->listRows(
$categoryOptions,
'category',
'treeViewCategoriesCallback',
intval($this->parameter->current)
);
}
}
}

104
var/Widget/Metas/EditTrait.php Executable file
View File

@@ -0,0 +1,104 @@
<?php
namespace Widget\Metas;
use Typecho\Db\Exception;
trait EditTrait
{
/**
* 获取最大排序
*
* @param string $type
* @param int $parent
* @return integer
* @throws Exception
*/
public function getMaxOrder(string $type, int $parent = 0): int
{
return $this->db->fetchObject($this->select(['MAX(order)' => 'maxOrder'])
->where('type = ? AND parent = ?', $type, $parent))->maxOrder ?? 0;
}
/**
* 对数据按照sort字段排序
*
* @param array $metas
* @param string $type
* @throws Exception
*/
public function sort(array $metas, string $type)
{
foreach ($metas as $sort => $mid) {
$this->update(
['order' => $sort + 1],
$this->db->sql()->where('mid = ?', $mid)->where('type = ?', $type)
);
}
}
/**
* 合并数据
*
* @param integer $mid 数据主键
* @param string $type 数据类型
* @param array $metas 需要合并的数据集
* @throws Exception
*/
public function merge(int $mid, string $type, array $metas)
{
$contents = array_column($this->db->fetchAll($this->db->select('cid')
->from('table.relationships')
->where('mid = ?', $mid)), 'cid');
foreach ($metas as $meta) {
if ($mid != $meta) {
$existsContents = array_column($this->db->fetchAll($this->db
->select('cid')->from('table.relationships')
->where('mid = ?', $meta)), 'cid');
$where = $this->db->sql()->where('mid = ? AND type = ?', $meta, $type);
$this->delete($where);
$diffContents = array_diff($existsContents, $contents);
$this->db->query($this->db->delete('table.relationships')->where('mid = ?', $meta));
foreach ($diffContents as $content) {
$this->db->query($this->db->insert('table.relationships')
->rows(['mid' => $mid, 'cid' => $content]));
$contents[] = $content;
}
$this->update(['parent' => $mid], $this->db->sql()->where('parent = ?', $meta));
unset($existsContents);
}
}
$num = $this->db->fetchObject($this->db
->select(['COUNT(mid)' => 'num'])->from('table.relationships')
->where('table.relationships.mid = ?', $mid))->num;
$this->update(['count' => $num], $this->db->sql()->where('mid = ?', $mid));
}
/**
* 根据内容的指定类别和状态更新相关meta的计数信息
*
* @param int $mid meta id
* @param string $type 类别
* @param string $status 状态
* @throws Exception
*/
protected function refreshCountByTypeAndStatus(int $mid, string $type, string $status = 'publish')
{
$num = $this->db->fetchObject($this->db->select(['COUNT(table.contents.cid)' => 'num'])->from('table.contents')
->join('table.relationships', 'table.contents.cid = table.relationships.cid')
->where('table.relationships.mid = ?', $mid)
->where('table.contents.type = ?', $type)
->where('table.contents.status = ?', $status))->num;
$this->db->query($this->db->update('table.metas')->rows(['count' => $num])
->where('mid = ?', $mid));
}
}

52
var/Widget/Metas/From.php Executable file
View File

@@ -0,0 +1,52 @@
<?php
namespace Widget\Metas;
use Typecho\Config;
use Typecho\Db\Exception;
use Widget\Base\Metas;
use Widget\Base\TreeTrait;
use Widget\Metas\Category\InitTreeRowsTrait;
class From extends Metas
{
use InitTreeRowsTrait;
use TreeTrait {
initParameter as initTreeParameter;
}
/**
* @param Config $parameter
* @return void
*/
protected function initParameter(Config $parameter)
{
$parameter->setDefault([
'mid' => null,
'query' => null,
]);
}
/**
* @return void
* @throws Exception
*/
public function execute()
{
$query = null;
if (isset($this->parameter->mid)) {
$query = $this->select()->where('mid = ?', $this->parameter->mid);
} elseif (isset($this->parameter->query)) {
$query = $this->parameter->query;
}
if ($query) {
$this->db->fetchAll($query, [$this, 'push']);
if ($this->type == 'category') {
$this->initTreeParameter($this->parameter);
}
}
}
}

54
var/Widget/Metas/Tag/Admin.php Executable file
View File

@@ -0,0 +1,54 @@
<?php
namespace Widget\Metas\Tag;
use Typecho\Db;
use Typecho\Widget\Exception;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 标签云组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Admin extends Cloud
{
/**
* 入口函数
*
* @throws Db\Exception
*/
public function execute()
{
$select = $this->select()->where('type = ?', 'tag')->order('mid', Db::SORT_DESC);
$this->db->fetchAll($select, [$this, 'push']);
}
/**
* 获取菜单标题
*
* @return string|null
* @throws Exception|Db\Exception
*/
public function getMenuTitle(): ?string
{
if ($this->request->is('mid')) {
$tag = $this->db->fetchRow($this->select()
->where('type = ? AND mid = ?', 'tag', $this->request->get('mid')));
if (!empty($tag)) {
return _t('编辑标签 %s', $tag['name']);
}
} else {
return null;
}
throw new Exception(_t('标签不存在'), 404);
}
}

57
var/Widget/Metas/Tag/Cloud.php Executable file
View File

@@ -0,0 +1,57 @@
<?php
namespace Widget\Metas\Tag;
use Typecho\Common;
use Typecho\Db;
use Widget\Base\Metas;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 标签云组件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Cloud extends Metas
{
/**
* 入口函数
*
* @throws Db\Exception
*/
public function execute()
{
$this->parameter->setDefault(['sort' => 'count', 'ignoreZeroCount' => false, 'desc' => true, 'limit' => 0]);
$select = $this->select()->where('type = ?', 'tag')
->order($this->parameter->sort, $this->parameter->desc ? Db::SORT_DESC : Db::SORT_ASC);
/** 忽略零数量 */
if ($this->parameter->ignoreZeroCount) {
$select->where('count > 0');
}
/** 总数限制 */
if ($this->parameter->limit) {
$select->limit($this->parameter->limit);
}
$this->db->fetchAll($select, [$this, 'push']);
}
/**
* 按分割数输出字符串
*
* @param mixed ...$args 需要输出的值
*/
public function split(...$args)
{
array_unshift($args, $this->count);
echo call_user_func_array([Common::class, 'splitByCount'], $args);
}
}

407
var/Widget/Metas/Tag/Edit.php Executable file
View File

@@ -0,0 +1,407 @@
<?php
namespace Widget\Metas\Tag;
use Typecho\Common;
use Typecho\Db\Exception;
use Typecho\Widget\Helper\Form;
use Widget\Base\Metas;
use Widget\ActionInterface;
use Widget\Metas\EditTrait;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 标签编辑组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Metas implements ActionInterface
{
use EditTrait;
/**
* 入口函数
*/
public function execute()
{
/** 编辑以上权限 */
$this->user->pass('editor');
}
/**
* 判断标签是否存在
*
* @param integer $mid 标签主键
* @return boolean
* @throws Exception
*/
public function tagExists(int $mid): bool
{
$tag = $this->db->fetchRow($this->db->select()
->from('table.metas')
->where('type = ?', 'tag')
->where('mid = ?', $mid)->limit(1));
return isset($tag);
}
/**
* 判断标签名称是否存在
*
* @param string $name 标签名称
* @return boolean
* @throws Exception
*/
public function nameExists(string $name): bool
{
$select = $this->db->select()
->from('table.metas')
->where('type = ?', 'tag')
->where('name = ?', $name)
->limit(1);
if ($this->request->is('mid')) {
$select->where('mid <> ?', $this->request->filter('int')->get('mid'));
}
$tag = $this->db->fetchRow($select);
return !$tag;
}
/**
* 判断标签名转换到缩略名后是否合法
*
* @param string $name 标签名
* @return boolean
* @throws Exception
*/
public function nameToSlug(string $name): bool
{
if (empty($this->request->slug)) {
$slug = Common::slugName($name);
if (empty($slug) || !$this->slugExists($name)) {
return false;
}
}
return true;
}
/**
* 判断标签缩略名是否存在
*
* @param string $slug 缩略名
* @return boolean
* @throws Exception
*/
public function slugExists(string $slug): bool
{
$select = $this->db->select()
->from('table.metas')
->where('type = ?', 'tag')
->where('slug = ?', Common::slugName($slug))
->limit(1);
if ($this->request->is('mid')) {
$select->where('mid <> ?', $this->request->get('mid'));
}
$tag = $this->db->fetchRow($select);
return !$tag;
}
/**
* 插入标签
*
* @throws Exception
*/
public function insertTag()
{
if ($this->form('insert')->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$tag = $this->request->from('name', 'slug');
$tag['type'] = 'tag';
$tag['slug'] = Common::slugName(Common::strBy($tag['slug'] ?? null, $tag['name']));
/** 插入数据 */
$tag['mid'] = $this->insert($tag);
$this->push($tag);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 提示信息 */
Notice::alloc()->set(
_t('标签 <a href="%s">%s</a> 已经被增加', $this->permalink, $this->name),
'success'
);
/** 转向原页 */
$this->response->redirect(Common::url('manage-tags.php', $this->options->adminUrl));
}
/**
* 生成表单
*
* @param string|null $action 表单动作
* @return Form
* @throws Exception
*/
public function form(?string $action = null): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/metas-tag-edit'), Form::POST_METHOD);
/** 标签名称 */
$name = new Form\Element\Text(
'name',
null,
null,
_t('标签名称') . ' *',
_t('这是标签在站点中显示的名称.可以使用中文,如 "地球".')
);
$form->addInput($name);
/** 标签缩略名 */
$slug = new Form\Element\Text(
'slug',
null,
null,
_t('标签缩略名'),
_t('标签缩略名用于创建友好的链接形式, 如果留空则默认使用标签名称.')
);
$form->addInput($slug);
/** 标签动作 */
$do = new Form\Element\Hidden('do');
$form->addInput($do);
/** 标签主键 */
$mid = new Form\Element\Hidden('mid');
$form->addInput($mid);
/** 提交按钮 */
$submit = new Form\Element\Submit();
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
if ($this->request->is('mid') && 'insert' != $action) {
/** 更新模式 */
$meta = $this->db->fetchRow($this->select()
->where('mid = ?', $this->request->get('mid'))
->where('type = ?', 'tag')->limit(1));
if (!$meta) {
$this->response->redirect(Common::url('manage-tags.php', $this->options->adminUrl));
}
$name->value($meta['name']);
$slug->value($meta['slug']);
$do->value('update');
$mid->value($meta['mid']);
$submit->value(_t('编辑标签'));
$_action = 'update';
} else {
$do->value('insert');
$submit->value(_t('增加标签'));
$_action = 'insert';
}
if (empty($action)) {
$action = $_action;
}
/** 给表单增加规则 */
if ('insert' == $action || 'update' == $action) {
$name->addRule('required', _t('必须填写标签名称'));
$name->addRule([$this, 'nameExists'], _t('标签名称已经存在'));
$name->addRule([$this, 'nameToSlug'], _t('标签名称无法被转换为缩略名'));
$name->addRule('xssCheck', _t('请不要标签名称中使用特殊字符'));
$slug->addRule([$this, 'slugExists'], _t('缩略名已经存在'));
$slug->addRule('xssCheck', _t('请不要在缩略名中使用特殊字符'));
}
if ('update' == $action) {
$mid->addRule('required', _t('标签主键不存在'));
$mid->addRule([$this, 'tagExists'], _t('标签不存在'));
}
return $form;
}
/**
* 更新标签
*
* @throws Exception
*/
public function updateTag()
{
if ($this->form('update')->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$tag = $this->request->from('name', 'slug', 'mid');
$tag['type'] = 'tag';
$tag['slug'] = Common::slugName(Common::strBy($tag['slug'] ?? null, $tag['name']));
/** 更新数据 */
$this->update($tag, $this->db->sql()->where('mid = ?', $this->request->filter('int')->get('mid')));
$this->push($tag);
/** 设置高亮 */
Notice::alloc()->highlight($this->theId);
/** 提示信息 */
Notice::alloc()->set(
_t('标签 <a href="%s">%s</a> 已经被更新', $this->permalink, $this->name),
'success'
);
/** 转向原页 */
$this->response->redirect(Common::url('manage-tags.php', $this->options->adminUrl));
}
/**
* 删除标签
*
* @throws Exception
*/
public function deleteTag()
{
$tags = $this->request->filter('int')->getArray('mid');
$deleteCount = 0;
if ($tags) {
foreach ($tags as $tag) {
if ($this->delete($this->db->sql()->where('mid = ?', $tag))) {
$this->db->query($this->db->delete('table.relationships')->where('mid = ?', $tag));
$deleteCount++;
}
}
}
/** 提示信息 */
Notice::alloc()->set(
$deleteCount > 0 ? _t('标签已经删除') : _t('没有标签被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 转向原页 */
$this->response->redirect(Common::url('manage-tags.php', $this->options->adminUrl));
}
/**
* 合并标签
*
* @throws Exception
*/
public function mergeTag()
{
if (empty($this->request->merge)) {
Notice::alloc()->set(_t('请填写需要合并到的标签'));
$this->response->goBack();
}
$merge = $this->scanTags($this->request->get('merge'));
if (empty($merge)) {
Notice::alloc()->set(_t('合并到的标签名不合法'), 'error');
$this->response->goBack();
}
$tags = $this->request->filter('int')->getArray('mid');
if ($tags) {
$this->merge($merge, 'tag', $tags);
/** 提示信息 */
Notice::alloc()->set(_t('标签已经合并'), 'success');
} else {
Notice::alloc()->set(_t('没有选择任何标签'));
}
/** 转向原页 */
$this->response->redirect(Common::url('manage-tags.php', $this->options->adminUrl));
}
/**
* 刷新标签
*
* @access public
* @return void
* @throws Exception
*/
public function refreshTag()
{
$tags = $this->request->filter('int')->getArray('mid');
if ($tags) {
foreach ($tags as $tag) {
$this->refreshCountByTypeAndStatus($tag, 'post');
}
// 自动清理标签
$this->clearTags();
Notice::alloc()->set(_t('标签刷新已经完成'), 'success');
} else {
Notice::alloc()->set(_t('没有选择任何标签'));
}
/** 转向原页 */
$this->response->goBack();
}
/**
* 清理没有任何内容的标签
*
* @throws Exception
*/
public function clearTags()
{
// 取出count为0的标签
$tags = array_column($this->db->fetchAll($this->select('mid')
->where('type = ? AND count = ?', 'tags', 0)), 'mid');
foreach ($tags as $tag) {
// 确认是否已经没有关联了
$content = $this->db->fetchRow($this->db->select('cid')
->from('table.relationships')->where('mid = ?', $tag)
->limit(1));
if (empty($content)) {
$this->db->query($this->db->delete('table.metas')
->where('mid = ?', $tag));
}
}
}
/**
* 入口函数,绑定事件
*
* @access public
* @return void
* @throws Exception
*/
public function action()
{
$this->security->protect();
$this->on($this->request->is('do=insert'))->insertTag();
$this->on($this->request->is('do=update'))->updateTag();
$this->on($this->request->is('do=delete'))->deleteTag();
$this->on($this->request->is('do=merge'))->mergeTag();
$this->on($this->request->is('do=refresh'))->refreshTag();
$this->response->redirect($this->options->adminUrl);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Widget\Metas\Tag;
use Widget\Base\Metas;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 相关信息输出组件
*/
class Related extends Metas
{
/**
* @return void
*/
public function execute()
{
$this->db->fetchAll($this->select()
->join('table.relationships', 'table.relationships.mid = table.metas.mid')
->where('table.relationships.cid = ?', $this->parameter->cid)
->where('table.metas.type = ?', 'tag'), [$this, 'push']);
}
}

73
var/Widget/Notice.php Executable file
View File

@@ -0,0 +1,73 @@
<?php
namespace Widget;
use Typecho\Cookie;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 提示框组件
*
* @package Widget
*/
class Notice extends Widget
{
/**
* 提示高亮
*
* @var string
*/
public string $highlight;
/**
* 高亮相关元素
*
* @param string $theId 需要高亮元素的id
*/
public function highlight(string $theId)
{
$this->highlight = $theId;
Cookie::set(
'__typecho_notice_highlight',
$theId
);
}
/**
* 获取高亮的id
*
* @return integer
*/
public function getHighlightId(): int
{
return preg_match("/[0-9]+/", $this->highlight, $matches) ? $matches[0] : 0;
}
/**
* 设定堆栈每一行的值
*
* @param string|array $value 值对应的键值
* @param string|null $type 提示类型
* @param string $typeFix 兼容老插件
*/
public function set($value, ?string $type = 'notice', string $typeFix = 'notice')
{
$notice = is_array($value) ? array_values($value) : [$value];
if (empty($type) && $typeFix) {
$type = $typeFix;
}
Cookie::set(
'__typecho_notice',
json_encode($notice)
);
Cookie::set(
'__typecho_notice_type',
$type
);
}
}

771
var/Widget/Options.php Executable file
View File

@@ -0,0 +1,771 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Config;
use Typecho\Db;
use Typecho\Router;
use Typecho\Router\Parser;
use Typecho\Widget;
use Typecho\Plugin\Exception as PluginException;
use Typecho\Db\Exception as DbException;
use Typecho\Date;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 全局选项组件
*
* @property string $feedUrl
* @property string $feedRssUrl
* @property string $feedAtomUrl
* @property string $commentsFeedUrl
* @property string $commentsFeedRssUrl
* @property string $commentsFeedAtomUrl
* @property string $themeUrl
* @property string $xmlRpcUrl
* @property string $index
* @property string $siteUrl
* @property string $siteDomain
* @property array $routingTable
* @property string $rootUrl
* @property string $pluginUrl
* @property string $pluginDir
* @property string $adminUrl
* @property string $loginUrl
* @property string $originalSiteUrl
* @property string $loginAction
* @property string $registerUrl
* @property string $registerAction
* @property string $profileUrl
* @property string $logoutUrl
* @property string $title
* @property string $description
* @property string $keywords
* @property string $lang
* @property string $theme
* @property string|null $missingTheme
* @property int $pageSize
* @property int $serverTimezone
* @property int $timezone
* @property string $charset
* @property string $contentType
* @property string $generator
* @property string $software
* @property string $version
* @property bool $markdown
* @property bool $xmlrpcMarkdown
* @property array $allowedAttachmentTypes
* @property string $attachmentTypes
* @property int $time
* @property string $frontPage
* @property int $commentsListSize
* @property bool $commentsShowCommentOnly
* @property array $actionTable
* @property array $panelTable
* @property bool $commentsThreaded
* @property bool $defaultAllowComment
* @property bool $defaultAllowPing
* @property bool $defaultAllowFeed
* @property string $commentDateFormat
* @property string $commentsAvatarRating
* @property string $commentsPageDisplay
* @property int $commentsPageSize
* @property string $commentsOrder
* @property bool $commentsMarkdown
* @property bool $commentsShowUrl
* @property bool $commentsUrlNofollow
* @property bool $commentsAvatar
* @property bool $commentsPageBreak
* @property bool $commentsRequireModeration
* @property bool $commentsWhitelist
* @property bool $commentsRequireMail
* @property bool $commentsRequireUrl
* @property bool $commentsCheckReferer
* @property bool $commentsAntiSpam
* @property bool $commentsAutoClose
* @property bool $commentsPostIntervalEnable
* @property int $commentsMaxNestingLevels
* @property int $commentsPostTimeout
* @property int $commentsPostInterval
* @property string $commentsHTMLTagAllowed
* @property bool $allowRegister
* @property int $allowXmlRpc
* @property int $postsListSize
* @property bool $feedFullText
* @property int $defaultCategory
* @property bool $frontArchive
* @property array $plugins
* @property string $secret
* @property bool $installed
* @property bool $rewrite
* @property string $postDateFormat
*/
class Options extends Base
{
/**
* 缓存的插件配置
*
* @access private
* @var array
*/
private array $pluginConfig = [];
/**
* 缓存的个人插件配置
*
* @access private
* @var array
*/
private array $personalPluginConfig = [];
/**
* @param int $components
*/
protected function initComponents(int &$components)
{
$components = self::INIT_NONE;
}
/**
* @param Config $parameter
*/
protected function initParameter(Config $parameter)
{
if (!$parameter->isEmpty()) {
$this->row = $this->parameter->toArray();
} else {
$this->db = Db::get();
}
}
/**
* 执行函数
*
* @throws DbException
*/
public function execute()
{
$options = [];
if (isset($this->db)) {
$values = $this->db->fetchAll($this->db->select()->from('table.options')
->where('user = 0'));
// finish install
if (empty($values)) {
$this->response->redirect(defined('__TYPECHO_ADMIN__')
? '../install.php?step=3' : 'install.php?step=3');
}
$options = array_column($values, 'value', 'name');
/** 支持皮肤变量重载 */
$themeOptionsKey = 'theme:' . $options['theme'];
if (!empty($options[$themeOptionsKey])) {
$themeOptions = $this->tryDeserialize($options[$themeOptionsKey]);
$options = array_merge($options, $themeOptions);
}
} elseif (function_exists('install_get_default_options')) {
$defaultOptions = install_get_default_options();
$initOptionKeys = ['routingTable', 'plugins', 'charset', 'contentType', 'timezone', 'installed', 'generator', 'siteUrl', 'lang', 'secret'];
foreach ($initOptionKeys as $option) {
$options[$option] = $defaultOptions[$option];
}
}
$this->push($options);
}
/**
* 获取皮肤文件
*
* @param string $theme
* @param string $file
* @return string
*/
public function themeFile(string $theme, string $file = ''): string
{
return __TYPECHO_ROOT_DIR__ . __TYPECHO_THEME_DIR__ . '/' . trim($theme, './') . '/' . trim($file, './');
}
/**
* 输出网站路径
*
* @param string|null $path 子路径
*/
public function siteUrl(?string $path = null)
{
echo Common::url($path, $this->siteUrl);
}
/**
* 输出解析地址
*
* @param string|null $path 子路径
*/
public function index(?string $path = null)
{
echo Common::url($path, $this->index);
}
/**
* 输出模板路径
*
* @param string|null $path 子路径
* @param string|null $theme 模版名称
* @return string | void
*/
public function themeUrl(?string $path = null, ?string $theme = null)
{
if (!isset($theme)) {
echo Common::url($path, $this->themeUrl);
} else {
$url = defined('__TYPECHO_THEME_URL__') ? __TYPECHO_THEME_URL__ :
Common::url(__TYPECHO_THEME_DIR__ . '/' . $theme, $this->siteUrl);
return isset($path) ? Common::url($path, $url) : $url;
}
}
/**
* 输出插件路径
*
* @param string|null $path 子路径
*/
public function pluginUrl(?string $path = null)
{
echo Common::url($path, $this->pluginUrl);
}
/**
* 获取插件目录
*
* @param string|null $plugin
* @return string
*/
public function pluginDir(?string $plugin = null): string
{
return Common::url($plugin, $this->pluginDir);
}
/**
* 输出后台路径
*
* @param string|null $path 子路径
* @param bool $return
* @return void|string
*/
public function adminUrl(?string $path = null, bool $return = false)
{
$url = Common::url($path, $this->adminUrl);
if ($return) {
return $url;
}
echo $url;
}
/**
* 获取或输出后台静态文件路径
*
* @param string $type
* @param string|null $file
* @param bool $return
* @return void|string
*/
public function adminStaticUrl(string $type, ?string $file = null, bool $return = false)
{
$url = Common::url($type, $this->adminUrl);
if (empty($file)) {
return $url;
}
$url = Common::url($file, $url) . '?v=' . $this->version;
if ($return) {
return $url;
}
echo $url;
}
/**
* 编码输出允许出现在评论中的html标签
*/
public function commentsHTMLTagAllowed()
{
echo htmlspecialchars($this->commentsHTMLTagAllowed);
}
/**
* 获取插件系统参数
*
* @param mixed $pluginName 插件名称
* @return mixed
* @throws PluginException
*/
public function plugin($pluginName)
{
if (!isset($this->pluginConfig[$pluginName])) {
if (
!empty($this->row['plugin:' . $pluginName])
&& false !== ($options = $this->tryDeserialize($this->row['plugin:' . $pluginName]))
) {
$this->pluginConfig[$pluginName] = new Config($options);
} else {
throw new PluginException(_t('插件%s的配置信息没有找到', $pluginName), 500);
}
}
return $this->pluginConfig[$pluginName];
}
/**
* 获取个人插件系统参数
*
* @param mixed $pluginName 插件名称
*
* @return mixed
* @throws PluginException
*/
public function personalPlugin($pluginName)
{
if (!isset($this->personalPluginConfig[$pluginName])) {
if (
!empty($this->row['_plugin:' . $pluginName])
&& false !== ($options = $this->tryDeserialize($this->row['_plugin:' . $pluginName]))
) {
$this->personalPluginConfig[$pluginName] = new Config($options);
} else {
throw new PluginException(_t('插件%s的配置信息没有找到', $pluginName), 500);
}
}
return $this->personalPluginConfig[$pluginName];
}
/**
* @return array
*/
protected function ___routingTable(): array
{
$routingTable = $this->tryDeserialize($this->row['routingTable']);
if (isset($this->db) && !isset($routingTable[0])) {
/** 解析路由并缓存 */
$parser = new Parser($routingTable);
$parsedRoutingTable = $parser->parse();
$routingTable = array_merge([$parsedRoutingTable], $routingTable);
$this->db->query($this->db->update('table.options')->rows(['value' => json_encode($routingTable)])
->where('name = ?', 'routingTable'));
}
return $routingTable;
}
/**
* @return array
*/
protected function ___actionTable(): array
{
return $this->tryDeserialize($this->row['actionTable']);
}
/**
* @return array
*/
protected function ___panelTable(): array
{
return $this->tryDeserialize($this->row['panelTable']);
}
/**
* @return array
*/
protected function ___plugins(): array
{
return $this->tryDeserialize($this->row['plugins']);
}
/**
* 动态判断皮肤目录
*
* @return string|null
*/
protected function ___missingTheme(): ?string
{
return !is_dir($this->themeFile($this->row['theme'])) ? $this->row['theme'] : null;
}
/**
* @return string
*/
protected function ___theme(): string
{
return $this->missingTheme ? 'default' : $this->row['theme'];
}
/**
* 动态获取根目录
*
* @return string
*/
protected function ___rootUrl(): string
{
$rootUrl = defined('__TYPECHO_ROOT_URL__') ? __TYPECHO_ROOT_URL__ : $this->request->getRequestRoot();
if (defined('__TYPECHO_ADMIN__')) {
/** 识别在admin目录中的情况 */
$adminDir = '/' . trim(defined('__TYPECHO_ADMIN_DIR__') ? __TYPECHO_ADMIN_DIR__ : '/admin/', '/');
$rootUrl = substr($rootUrl, 0, - strlen($adminDir));
}
return $rootUrl;
}
/**
* @return string
*/
protected function ___originalSiteUrl(): string
{
$siteUrl = $this->row['siteUrl'];
if (defined('__TYPECHO_SITE_URL__')) {
$siteUrl = __TYPECHO_SITE_URL__;
} elseif (defined('__TYPECHO_DYNAMIC_SITE_URL__') && __TYPECHO_DYNAMIC_SITE_URL__) {
$siteUrl = $this->rootUrl;
}
return $siteUrl;
}
/**
* @return string
*/
protected function ___siteUrl(): string
{
$siteUrl = Common::url(null, $this->originalSiteUrl);
/** 增加对SSL连接的支持 */
if ($this->request->isSecure() && 0 === strpos($siteUrl, 'http://')) {
$siteUrl = substr_replace($siteUrl, 'https', 0, 4);
}
return $siteUrl;
}
/**
* @return string
*/
protected function ___siteDomain(): string
{
return parse_url($this->siteUrl, PHP_URL_HOST);
}
/**
* RSS2.0
*
* @return string
*/
protected function ___feedUrl(): string
{
return Router::url('feed', ['feed' => '/'], $this->index);
}
/**
* RSS1.0
*
* @return string
*/
protected function ___feedRssUrl(): string
{
return Router::url('feed', ['feed' => '/rss/'], $this->index);
}
/**
* ATOM1.O
*
* @return string
*/
protected function ___feedAtomUrl(): string
{
return Router::url('feed', ['feed' => '/atom/'], $this->index);
}
/**
* 评论RSS2.0聚合
*
* @return string
*/
protected function ___commentsFeedUrl(): string
{
return Router::url('feed', ['feed' => '/comments/'], $this->index);
}
/**
* 评论RSS1.0聚合
*
* @return string
*/
protected function ___commentsFeedRssUrl(): string
{
return Router::url('feed', ['feed' => '/rss/comments/'], $this->index);
}
/**
* 评论ATOM1.0聚合
*
* @return string
*/
protected function ___commentsFeedAtomUrl(): string
{
return Router::url('feed', ['feed' => '/atom/comments/'], $this->index);
}
/**
* xmlrpc api地址
*
* @return string
*/
protected function ___xmlRpcUrl(): string
{
return Router::url('do', ['action' => 'xmlrpc'], $this->index);
}
/**
* 获取解析路径前缀
*
* @return string
*/
protected function ___index(): string
{
return ($this->rewrite || (defined('__TYPECHO_REWRITE__') && __TYPECHO_REWRITE__))
? $this->rootUrl : Common::url('index.php', $this->rootUrl);
}
/**
* 获取模板路径
*
* @return string
*/
protected function ___themeUrl(): string
{
return $this->themeUrl(null, $this->theme);
}
/**
* 获取插件路径
*
* @return string
*/
protected function ___pluginUrl(): string
{
return defined('__TYPECHO_PLUGIN_URL__') ? __TYPECHO_PLUGIN_URL__ :
Common::url(__TYPECHO_PLUGIN_DIR__, $this->siteUrl);
}
/**
* @return string
*/
protected function ___pluginDir(): string
{
return Common::url(__TYPECHO_PLUGIN_DIR__, __TYPECHO_ROOT_DIR__);
}
/**
* 获取后台路径
*
* @return string
*/
protected function ___adminUrl(): string
{
return Common::url(defined('__TYPECHO_ADMIN_DIR__') ?
__TYPECHO_ADMIN_DIR__ : '/admin/', $this->rootUrl);
}
/**
* 获取登录地址
*
* @return string
*/
protected function ___loginUrl(): string
{
return Common::url('login.php', $this->adminUrl);
}
/**
* 获取登录提交地址
*
* @return string
*/
protected function ___loginAction(): string
{
return Security::alloc()->getTokenUrl(
Router::url(
'do',
['action' => 'login', 'widget' => 'Login'],
Common::url('index.php', $this->rootUrl)
)
);
}
/**
* 获取注册地址
*
* @return string
*/
protected function ___registerUrl(): string
{
return Common::url('register.php', $this->adminUrl);
}
/**
* 获取登录提交地址
*
* @return string
* @throws Widget\Exception
*/
protected function ___registerAction(): string
{
return Security::alloc()->getTokenUrl(
Router::url('do', ['action' => 'register', 'widget' => 'Register'], $this->index)
);
}
/**
* 获取个人档案地址
*
* @return string
*/
protected function ___profileUrl(): string
{
return Common::url('profile.php', $this->adminUrl);
}
/**
* 获取登出地址
*
* @return string
*/
protected function ___logoutUrl(): string
{
return Security::alloc()->getTokenUrl(
Common::url('/action/logout', $this->index)
);
}
/**
* 获取系统时区
*
* @return integer
*/
protected function ___serverTimezone(): int
{
return Date::$serverTimezoneOffset;
}
/**
* 获取GMT标准时间
*
* @return integer
* @deprecated
*/
protected function ___gmtTime(): int
{
return Date::gmtTime();
}
/**
* 获取时间
*
* @return integer
* @deprecated
*/
protected function ___time(): int
{
return Date::time();
}
/**
* 获取格式
*
* @return string
*/
protected function ___contentType(): string
{
return $this->contentType ?? 'text/html';
}
/**
* 软件名称
*
* @return string
*/
protected function ___software(): string
{
[$software] = explode(' ', $this->generator);
return $software;
}
/**
* 软件版本
*
* @return string
*/
protected function ___version(): string
{
[, $version] = explode(' ', $this->generator);
$pos = strpos($version, '/');
// fix for old version
if ($pos !== false) {
$version = substr($version, 0, $pos) . '.0';
}
return $version;
}
/**
* 允许上传的文件类型
*
* @return array
*/
protected function ___allowedAttachmentTypes(): array
{
$attachmentTypesResult = [];
if (null != $this->attachmentTypes) {
$attachmentTypes = str_replace(
['@image@', '@media@', '@doc@'],
[
'gif,jpg,jpeg,png,tiff,bmp,webp', 'mp3,mp4,mov,wmv,wma,rmvb,rm,avi,flv,ogg,oga,ogv',
'txt,doc,docx,xls,xlsx,ppt,pptx,zip,rar,pdf'
],
$this->attachmentTypes
);
$attachmentTypesResult = array_unique(array_map('trim', preg_split("/([,.])/", $attachmentTypes)));
}
return $attachmentTypesResult;
}
/**
* Try to deserialize a value
*
* @param string $value
* @return mixed
*/
private function tryDeserialize(string $value)
{
$isSerialized = strpos($value, 'a:') === 0 || $value === 'b:0;';
return $isSerialized ? @unserialize($value) : json_decode($value, true);
}
}

302
var/Widget/Options/Discussion.php Executable file
View File

@@ -0,0 +1,302 @@
<?php
namespace Widget\Options;
use Typecho\Db\Exception;
use Typecho\Widget\Helper\Form;
use Widget\ActionInterface;
use Widget\Base\Options;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 评论设置组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Discussion extends Options implements ActionInterface
{
use EditTrait;
/**
* 执行更新动作
*
* @throws Exception
*/
public function updateDiscussionSettings()
{
/** 验证格式 */
if ($this->form()->validate()) {
$this->response->goBack();
}
$settings = $this->request->from(
'commentDateFormat',
'commentsListSize',
'commentsPageSize',
'commentsPageDisplay',
'commentsAvatar',
'commentsOrder',
'commentsMaxNestingLevels',
'commentsUrlNofollow',
'commentsPostTimeout',
'commentsUniqueIpInterval',
'commentsWhitelist',
'commentsRequireMail',
'commentsAvatarRating',
'commentsPostTimeout',
'commentsPostInterval',
'commentsRequireModeration',
'commentsRequireUrl',
'commentsHTMLTagAllowed',
'commentsStopWords',
'commentsIpBlackList'
);
$settings['commentsShow'] = $this->request->getArray('commentsShow');
$settings['commentsPost'] = $this->request->getArray('commentsPost');
$settings['commentsShowCommentOnly'] = $this->isEnableByCheckbox(
$settings['commentsShow'],
'commentsShowCommentOnly'
);
$settings['commentsMarkdown'] = $this->isEnableByCheckbox($settings['commentsShow'], 'commentsMarkdown');
$settings['commentsShowUrl'] = $this->isEnableByCheckbox($settings['commentsShow'], 'commentsShowUrl');
$settings['commentsUrlNofollow'] = $this->isEnableByCheckbox($settings['commentsShow'], 'commentsUrlNofollow');
$settings['commentsAvatar'] = $this->isEnableByCheckbox($settings['commentsShow'], 'commentsAvatar');
$settings['commentsPageBreak'] = $this->isEnableByCheckbox($settings['commentsShow'], 'commentsPageBreak');
$settings['commentsThreaded'] = $this->isEnableByCheckbox($settings['commentsShow'], 'commentsThreaded');
$settings['commentsPageSize'] = intval($settings['commentsPageSize']);
$settings['commentsMaxNestingLevels'] = min(7, max(2, intval($settings['commentsMaxNestingLevels'])));
$settings['commentsPageDisplay'] = ('first' == $settings['commentsPageDisplay']) ? 'first' : 'last';
$settings['commentsOrder'] = ('DESC' == $settings['commentsOrder']) ? 'DESC' : 'ASC';
$settings['commentsAvatarRating'] = in_array($settings['commentsAvatarRating'], ['G', 'PG', 'R', 'X'])
? $settings['commentsAvatarRating'] : 'G';
$settings['commentsRequireModeration'] = $this->isEnableByCheckbox(
$settings['commentsPost'],
'commentsRequireModeration'
);
$settings['commentsWhitelist'] = $this->isEnableByCheckbox($settings['commentsPost'], 'commentsWhitelist');
$settings['commentsRequireMail'] = $this->isEnableByCheckbox($settings['commentsPost'], 'commentsRequireMail');
$settings['commentsRequireUrl'] = $this->isEnableByCheckbox($settings['commentsPost'], 'commentsRequireUrl');
$settings['commentsCheckReferer'] = $this->isEnableByCheckbox(
$settings['commentsPost'],
'commentsCheckReferer'
);
$settings['commentsAntiSpam'] = $this->isEnableByCheckbox($settings['commentsPost'], 'commentsAntiSpam');
$settings['commentsAutoClose'] = $this->isEnableByCheckbox($settings['commentsPost'], 'commentsAutoClose');
$settings['commentsPostIntervalEnable'] = $this->isEnableByCheckbox(
$settings['commentsPost'],
'commentsPostIntervalEnable'
);
$settings['commentsPostTimeout'] = intval($settings['commentsPostTimeout']) * 24 * 3600;
$settings['commentsPostInterval'] = round($settings['commentsPostInterval'], 1) * 60;
unset($settings['commentsShow']);
unset($settings['commentsPost']);
foreach ($settings as $name => $value) {
$this->update(['value' => $value], $this->db->sql()->where('name = ?', $name));
}
Notice::alloc()->set(_t("设置已经保存"), 'success');
$this->response->goBack();
}
/**
* 输出表单结构
*
* @return Form
*/
public function form(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/options-discussion'), Form::POST_METHOD);
/** 评论日期格式 */
$commentDateFormat = new Form\Element\Text(
'commentDateFormat',
null,
$this->options->commentDateFormat,
_t('评论日期格式'),
_t('这是一个默认的格式,当你在模板中调用显示评论日期方法时, 如果没有指定日期格式, 将按照此格式输出.') . '<br />'
. _t('具体写法请参考 <a href="https://www.php.net/manual/zh/function.date.php">PHP 日期格式写法</a>.')
);
$commentDateFormat->input->setAttribute('class', 'w-40 mono');
$form->addInput($commentDateFormat);
/** 评论列表数目 */
$commentsListSize = new Form\Element\Number(
'commentsListSize',
null,
$this->options->commentsListSize,
_t('评论列表数目'),
_t('此数目用于指定显示在侧边栏中的评论列表数目.')
);
$commentsListSize->input->setAttribute('class', 'w-20');
$form->addInput($commentsListSize->addRule('isInteger', _t('请填入一个数字')));
$commentsShowOptions = [
'commentsShowCommentOnly' => _t('仅显示评论, 不显示 Pingback 和 Trackback'),
'commentsMarkdown' => _t('在评论中使用 Markdown 语法'),
'commentsShowUrl' => _t('评论者名称显示时自动加上其个人主页链接'),
'commentsUrlNofollow' => _t('对评论者个人主页链接使用 <a href="https://en.wikipedia.org/wiki/Nofollow">nofollow 属性</a>'),
'commentsAvatar' => _t('启用 <a href="https://gravatar.com">Gravatar</a> 头像服务, 最高显示评级为 %s 的头像',
'</label><select id="commentsShow-commentsAvatarRating" name="commentsAvatarRating">
<option value="G"' . ('G' == $this->options->commentsAvatarRating ? ' selected="true"' : '') . '>' . _t('G - 普通') . '</option>
<option value="PG"' . ('PG' == $this->options->commentsAvatarRating ? ' selected="true"' : '') . '>' . _t('PG - 13岁以上') . '</option>
<option value="R"' . ('R' == $this->options->commentsAvatarRating ? ' selected="true"' : '') . '>' . _t('R - 17岁以上成人') . '</option>
<option value="X"' . ('X' == $this->options->commentsAvatarRating ? ' selected="true"' : '') . '>' . _t('X - 限制级') . '</option></select>
<label for="commentsShow-commentsAvatarRating">'),
'commentsPageBreak' => _t('启用分页, 并且每页显示 %s 篇评论, 在列出时将 %s 作为默认显示',
'</label><input type="number" value="' . $this->options->commentsPageSize
. '" class="text num text-s" id="commentsShow-commentsPageSize" name="commentsPageSize" /><label for="commentsShow-commentsPageSize">',
'</label><select id="commentsShow-commentsPageDisplay" name="commentsPageDisplay">
<option value="first"' . ('first' == $this->options->commentsPageDisplay ? ' selected="true"' : '') . '>' . _t('第一页') . '</option>
<option value="last"' . ('last' == $this->options->commentsPageDisplay ? ' selected="true"' : '') . '>' . _t('最后一页') . '</option></select>'
. '<label for="commentsShow-commentsPageDisplay">'),
'commentsThreaded' => _t('启用评论回复, 以 %s 层作为每个评论最多的回复层数',
'</label><input name="commentsMaxNestingLevels" type="number" class="text num text-s" value="' . $this->options->commentsMaxNestingLevels . '" id="commentsShow-commentsMaxNestingLevels" />
<label for="commentsShow-commentsMaxNestingLevels">') . '</label></span><span class="multiline">'
. _t('将 %s 的评论显示在前面', '<select id="commentsShow-commentsOrder" name="commentsOrder">
<option value="DESC"' . ('DESC' == $this->options->commentsOrder ? ' selected="true"' : '') . '>' . _t('较新的') . '</option>
<option value="ASC"' . ('ASC' == $this->options->commentsOrder ? ' selected="true"' : '') . '>' . _t('较旧的') . '</option></select><label for="commentsShow-commentsOrder">')
];
$commentsShowOptionsValue = [];
if ($this->options->commentsShowCommentOnly) {
$commentsShowOptionsValue[] = 'commentsShowCommentOnly';
}
if ($this->options->commentsMarkdown) {
$commentsShowOptionsValue[] = 'commentsMarkdown';
}
if ($this->options->commentsShowUrl) {
$commentsShowOptionsValue[] = 'commentsShowUrl';
}
if ($this->options->commentsUrlNofollow) {
$commentsShowOptionsValue[] = 'commentsUrlNofollow';
}
if ($this->options->commentsAvatar) {
$commentsShowOptionsValue[] = 'commentsAvatar';
}
if ($this->options->commentsPageBreak) {
$commentsShowOptionsValue[] = 'commentsPageBreak';
}
if ($this->options->commentsThreaded) {
$commentsShowOptionsValue[] = 'commentsThreaded';
}
$commentsShow = new Form\Element\Checkbox(
'commentsShow',
$commentsShowOptions,
$commentsShowOptionsValue,
_t('评论显示')
);
$form->addInput($commentsShow->multiMode());
/** 评论提交 */
$commentsPostOptions = [
'commentsRequireModeration' => _t('所有评论必须经过审核'),
'commentsWhitelist' => _t('评论者之前须有评论通过了审核'),
'commentsRequireMail' => _t('必须填写邮箱'),
'commentsRequireUrl' => _t('必须填写网址'),
'commentsCheckReferer' => _t('检查评论来源页 URL 是否与文章链接一致'),
'commentsAntiSpam' => _t('开启反垃圾保护'),
'commentsAutoClose' => _t('在文章发布 %s 天以后自动关闭评论',
'</label><input name="commentsPostTimeout" type="number" class="text num text-s" value="' . intval($this->options->commentsPostTimeout / (24 * 3600)) . '" id="commentsPost-commentsPostTimeout" />
<label for="commentsPost-commentsPostTimeout">'),
'commentsPostIntervalEnable' => _t('同一 IP 发布评论的时间间隔限制为 %s 分钟',
'</label><input name="commentsPostInterval" type="number" class="text num text-s" value="' . round($this->options->commentsPostInterval / (60), 1) . '" id="commentsPost-commentsPostInterval" />
<label for="commentsPost-commentsPostInterval">')
];
$commentsPostOptionsValue = [];
if ($this->options->commentsRequireModeration) {
$commentsPostOptionsValue[] = 'commentsRequireModeration';
}
if ($this->options->commentsWhitelist) {
$commentsPostOptionsValue[] = 'commentsWhitelist';
}
if ($this->options->commentsRequireMail) {
$commentsPostOptionsValue[] = 'commentsRequireMail';
}
if ($this->options->commentsRequireUrl) {
$commentsPostOptionsValue[] = 'commentsRequireUrl';
}
if ($this->options->commentsCheckReferer) {
$commentsPostOptionsValue[] = 'commentsCheckReferer';
}
if ($this->options->commentsAntiSpam) {
$commentsPostOptionsValue[] = 'commentsAntiSpam';
}
if ($this->options->commentsAutoClose) {
$commentsPostOptionsValue[] = 'commentsAutoClose';
}
if ($this->options->commentsPostIntervalEnable) {
$commentsPostOptionsValue[] = 'commentsPostIntervalEnable';
}
$commentsPost = new Form\Element\Checkbox(
'commentsPost',
$commentsPostOptions,
$commentsPostOptionsValue,
_t('评论提交')
);
$form->addInput($commentsPost->multiMode());
/** 允许使用的HTML标签和属性 */
$commentsHTMLTagAllowed = new Form\Element\Textarea(
'commentsHTMLTagAllowed',
null,
$this->options->commentsHTMLTagAllowed,
_t('允许使用的HTML标签和属性'),
_t('默认的用户评论不允许填写任何的HTML标签, 你可以在这里填写允许使用的HTML标签.') . '<br />'
. _t('比如: %s', '<code>&lt;a href=&quot;&quot;&gt; &lt;img src=&quot;&quot;&gt; &lt;blockquote&gt;</code>')
);
$commentsHTMLTagAllowed->input->setAttribute('class', 'mono');
$form->addInput($commentsHTMLTagAllowed);
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
/**
* 绑定动作
*
* @access public
* @return void
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->isPost())->updateDiscussionSettings();
$this->response->redirect($this->options->adminUrl);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Widget\Options;
/**
* 编辑选项组件
*/
trait EditTrait
{
/**
* 以checkbox选项判断是否某个值被启用
*
* @param mixed $settings 选项集合
* @param string $name 选项名称
* @return integer
*/
protected function isEnableByCheckbox($settings, string $name): int
{
return is_array($settings) && in_array($name, $settings) ? 1 : 0;
}
}

314
var/Widget/Options/General.php Executable file
View File

@@ -0,0 +1,314 @@
<?php
namespace Widget\Options;
use Typecho\Db\Exception;
use Typecho\I18n\GetText;
use Typecho\Widget\Helper\Form;
use Widget\ActionInterface;
use Widget\Base\Options;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 基本设置组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class General extends Options implements ActionInterface
{
use EditTrait;
/**
* 检查是否在语言列表中
*
* @param string $lang
* @return bool
*/
public function checkLang(string $lang): bool
{
$langs = self::getLangs();
return isset($langs[$lang]);
}
/**
* 获取语言列表
*
* @return array
*/
public static function getLangs(): array
{
$dir = defined('__TYPECHO_LANG_DIR__') ? __TYPECHO_LANG_DIR__ : __TYPECHO_ROOT_DIR__ . '/usr/langs';
$files = glob($dir . '/*.mo');
$langs = ['zh_CN' => '简体中文'];
if (!empty($files)) {
foreach ($files as $file) {
$getText = new GetText($file, false);
[$name] = explode('.', basename($file));
$title = $getText->translate('lang', $count);
$langs[$name] = $count > - 1 ? $title : $name;
}
ksort($langs);
}
return $langs;
}
/**
* 过滤掉可执行的后缀名
*
* @param string $ext
* @return boolean
*/
public function removeShell(string $ext): bool
{
return !preg_match("/^(php|php4|php5|sh|asp|jsp|rb|py|pl|dll|exe|bat)$/i", $ext);
}
/**
* 执行更新动作
*
* @throws Exception
*/
public function updateGeneralSettings()
{
/** 验证格式 */
if ($this->form()->validate()) {
$this->response->goBack();
}
$settings = $this->request->from(
'title',
'description',
'keywords',
'allowRegister',
'allowXmlRpc',
'lang',
'timezone'
);
$settings['attachmentTypes'] = $this->request->getArray('attachmentTypes');
if (!defined('__TYPECHO_SITE_URL__')) {
$settings['siteUrl'] = rtrim($this->request->get('siteUrl'), '/');
}
$attachmentTypes = [];
if ($this->isEnableByCheckbox($settings['attachmentTypes'], '@image@')) {
$attachmentTypes[] = '@image@';
}
if ($this->isEnableByCheckbox($settings['attachmentTypes'], '@media@')) {
$attachmentTypes[] = '@media@';
}
if ($this->isEnableByCheckbox($settings['attachmentTypes'], '@doc@')) {
$attachmentTypes[] = '@doc@';
}
$attachmentTypesOther = $this->request->filter('trim', 'strtolower')->get('attachmentTypesOther');
if ($this->isEnableByCheckbox($settings['attachmentTypes'], '@other@') && !empty($attachmentTypesOther)) {
$types = implode(
',',
array_filter(array_map('trim', explode(',', $attachmentTypesOther)), [$this, 'removeShell'])
);
if (!empty($types)) {
$attachmentTypes[] = $types;
}
}
$settings['attachmentTypes'] = implode(',', $attachmentTypes);
foreach ($settings as $name => $value) {
$this->update(['value' => $value], $this->db->sql()->where('name = ?', $name));
}
Notice::alloc()->set(_t("设置已经保存"), 'success');
$this->response->goBack();
}
/**
* 输出表单结构
*
* @return Form
*/
public function form(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/options-general'), Form::POST_METHOD);
/** 站点名称 */
$title = new Form\Element\Text('title', null, $this->options->title, _t('站点名称'), _t('站点的名称将显示在网页的标题处.'));
$title->input->setAttribute('class', 'w-100');
$form->addInput($title->addRule('required', _t('请填写站点名称'))
->addRule('xssCheck', _t('请不要在站点名称中使用特殊字符')));
/** 站点地址 */
if (!defined('__TYPECHO_SITE_URL__')) {
$siteUrl = new Form\Element\Url(
'siteUrl',
null,
$this->options->originalSiteUrl,
_t('站点地址'),
_t('站点地址主要用于生成内容的永久链接.') . ($this->options->originalSiteUrl == $this->options->rootUrl ?
'' : '</p><p class="message notice mono">'
. _t('当前地址 <strong>%s</strong> 与上述设定值不一致', $this->options->rootUrl))
);
$siteUrl->input->setAttribute('class', 'w-100 mono');
$form->addInput($siteUrl->addRule('required', _t('请填写站点地址'))
->addRule('url', _t('请填写一个合法的URL地址')));
}
/** 站点描述 */
$description = new Form\Element\Text(
'description',
null,
$this->options->description,
_t('站点描述'),
_t('站点描述将显示在网页代码的头部.')
);
$form->addInput($description->addRule('xssCheck', _t('请不要在站点描述中使用特殊字符')));
/** 关键词 */
$keywords = new Form\Element\Text(
'keywords',
null,
$this->options->keywords,
_t('关键词'),
_t('请以半角逗号 "," 分割多个关键字.')
);
$form->addInput($keywords->addRule('xssCheck', _t('请不要在关键词中使用特殊字符')));
/** 注册 */
$allowRegister = new Form\Element\Radio(
'allowRegister',
['0' => _t('不允许'), '1' => _t('允许')],
$this->options->allowRegister,
_t('是否允许注册'),
_t('允许访问者注册到你的网站, 默认的注册用户不享有任何写入权限.')
);
$form->addInput($allowRegister);
/** XMLRPC */
$allowXmlRpc = new Form\Element\Radio(
'allowXmlRpc',
['0' => _t('关闭'), '1' => _t('仅关闭 Pingback 接口'), '2' => _t('打开')],
$this->options->allowXmlRpc,
_t('XMLRPC 接口')
);
$form->addInput($allowXmlRpc);
/** 语言项 */
// hack 语言扫描
_t('lang');
$langs = self::getLangs();
if (count($langs) > 1) {
$lang = new Form\Element\Select('lang', $langs, $this->options->lang, _t('语言'));
$form->addInput($lang->addRule([$this, 'checkLang'], _t('所选择的语言包不存在')));
}
/** 时区 */
$timezoneList = [
"0" => _t('格林威治(子午线)标准时间 (GMT)'),
"3600" => _t('中欧标准时间 阿姆斯特丹,荷兰,法国 (GMT +1)'),
"7200" => _t('东欧标准时间 布加勒斯特,塞浦路斯,希腊 (GMT +2)'),
"10800" => _t('莫斯科时间 伊拉克,埃塞俄比亚,马达加斯加 (GMT +3)'),
"14400" => _t('第比利斯时间 阿曼,毛里塔尼亚,留尼汪岛 (GMT +4)'),
"18000" => _t('新德里时间 巴基斯坦,马尔代夫 (GMT +5)'),
"21600" => _t('科伦坡时间 孟加拉 (GMT +6)'),
"25200" => _t('曼谷雅加达 柬埔寨,苏门答腊,老挝 (GMT +7)'),
"28800" => _t('北京时间 香港,新加坡,越南 (GMT +8)'),
"32400" => _t('东京平壤时间 西伊里安,摩鹿加群岛 (GMT +9)'),
"36000" => _t('悉尼关岛时间 塔斯马尼亚岛,新几内亚 (GMT +10)'),
"39600" => _t('所罗门群岛 库页岛 (GMT +11)'),
"43200" => _t('惠灵顿时间 新西兰,斐济群岛 (GMT +12)'),
"-3600" => _t('佛德尔群岛 亚速尔群岛,葡属几内亚 (GMT -1)'),
"-7200" => _t('大西洋中部时间 格陵兰 (GMT -2)'),
"-10800" => _t('布宜诺斯艾利斯 乌拉圭,法属圭亚那 (GMT -3)'),
"-14400" => _t('智利巴西 委内瑞拉,玻利维亚 (GMT -4)'),
"-18000" => _t('纽约渥太华 古巴,哥伦比亚,牙买加 (GMT -5)'),
"-21600" => _t('墨西哥城时间 洪都拉斯,危地马拉,哥斯达黎加 (GMT -6)'),
"-25200" => _t('美国丹佛时间 (GMT -7)'),
"-28800" => _t('美国旧金山时间 (GMT -8)'),
"-32400" => _t('阿拉斯加时间 (GMT -9)'),
"-36000" => _t('夏威夷群岛 (GMT -10)'),
"-39600" => _t('东萨摩亚群岛 (GMT -11)'),
"-43200" => _t('艾尼威托克岛 (GMT -12)')
];
$timezone = new Form\Element\Select('timezone', $timezoneList, $this->options->timezone, _t('时区'));
$form->addInput($timezone);
/** 扩展名 */
$attachmentTypesOptionsResult = (null != trim($this->options->attachmentTypes)) ?
array_map('trim', explode(',', $this->options->attachmentTypes)) : [];
$attachmentTypesOptionsValue = [];
if (in_array('@image@', $attachmentTypesOptionsResult)) {
$attachmentTypesOptionsValue[] = '@image@';
}
if (in_array('@media@', $attachmentTypesOptionsResult)) {
$attachmentTypesOptionsValue[] = '@media@';
}
if (in_array('@doc@', $attachmentTypesOptionsResult)) {
$attachmentTypesOptionsValue[] = '@doc@';
}
$attachmentTypesOther = array_diff($attachmentTypesOptionsResult, $attachmentTypesOptionsValue);
$attachmentTypesOtherValue = '';
if (!empty($attachmentTypesOther)) {
$attachmentTypesOptionsValue[] = '@other@';
$attachmentTypesOtherValue = implode(',', $attachmentTypesOther);
}
$attachmentTypesOptions = [
'@image@' => _t('图片文件') . ' <code>(gif jpg jpeg png tiff bmp webp avif)</code>',
'@media@' => _t('多媒体文件') . ' <code>(mp3 mp4 mov wmv wma rmvb rm avi flv ogg oga ogv)</code>',
'@doc@' => _t('常用档案文件') . ' <code>(txt doc docx xls xlsx ppt pptx zip rar pdf)</code>',
'@other@' => _t(
'其他格式 %s',
' <input type="text" class="w-50 text-s mono" name="attachmentTypesOther" value="'
. htmlspecialchars($attachmentTypesOtherValue) . '" />'
),
];
$attachmentTypes = new Form\Element\Checkbox(
'attachmentTypes',
$attachmentTypesOptions,
$attachmentTypesOptionsValue,
_t('允许上传的文件类型'),
_t('用逗号 "," 将后缀名隔开, 例如: %s', '<code>cpp, h, mak</code>')
);
$form->addInput($attachmentTypes->multiMode());
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
/**
* 绑定动作
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->isPost())->updateGeneralSettings();
$this->response->redirect($this->options->adminUrl);
}
}

387
var/Widget/Options/Permalink.php Executable file
View File

@@ -0,0 +1,387 @@
<?php
namespace Widget\Options;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Db\Exception;
use Typecho\Http\Client;
use Typecho\Router\Parser;
use Typecho\Widget\Helper\Form;
use Widget\ActionInterface;
use Widget\Base\Options;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 基本设置组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Permalink extends Options implements ActionInterface
{
/**
* 检查pagePattern里是否含有必要参数
*
* @param mixed $value
* @return bool
*/
public function checkPagePattern($value): bool
{
return strpos($value, '{slug}') !== false
|| strpos($value, '{cid}') !== false
|| strpos($value, '{directory}') !== false;
}
/**
* 检查categoryPattern里是否含有必要参数
*
* @param mixed $value
* @return bool
*/
public function checkCategoryPattern($value): bool
{
return strpos($value, '{slug}') !== false
|| strpos($value, '{mid}') !== false
|| strpos($value, '{directory}') !== false;
}
/**
* 检测是否可以rewrite
*
* @param string $value 是否打开rewrite
* @return bool
*/
public function checkRewrite(string $value): bool
{
if ($value) {
$this->user->pass('administrator');
/** 首先直接请求远程地址验证 */
$client = Client::get();
$hasWrote = false;
if (!file_exists(__TYPECHO_ROOT_DIR__ . '/.htaccess') && strpos(php_sapi_name(), 'apache') !== false) {
if (is_writable(__TYPECHO_ROOT_DIR__)) {
$parsed = parse_url($this->options->siteUrl);
$basePath = empty($parsed['path']) ? '/' : $parsed['path'];
$basePath = rtrim($basePath, '/') . '/';
$hasWrote = file_put_contents(__TYPECHO_ROOT_DIR__ . '/.htaccess', "<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase {$basePath}
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ {$basePath}index.php/$1 [L]
</IfModule>");
}
}
try {
if ($client) {
/** 发送一个rewrite地址请求 */
$client->setData(['do' => 'remoteCallback'])
->setHeader('User-Agent', $this->options->generator)
->setHeader('X-Requested-With', 'XMLHttpRequest')
->send(Common::url('/action/ajax', $this->options->siteUrl));
if (200 == $client->getResponseStatus() && 'OK' == $client->getResponseBody()) {
return true;
}
}
if (false !== $hasWrote) {
@unlink(__TYPECHO_ROOT_DIR__ . '/.htaccess');
//增强兼容性,使用wordpress的redirect式rewrite规则,虽然效率有点地下,但是对fastcgi模式兼容性较好
$hasWrote = file_put_contents(__TYPECHO_ROOT_DIR__ . '/.htaccess', "<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase {$basePath}
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . {$basePath}index.php [L]
</IfModule>");
//再次进行验证
$client = Client::get();
if ($client) {
/** 发送一个rewrite地址请求 */
$client->setData(['do' => 'remoteCallback'])
->setHeader('User-Agent', $this->options->generator)
->setHeader('X-Requested-With', 'XMLHttpRequest')
->send(Common::url('/action/ajax', $this->options->siteUrl));
if (200 == $client->getResponseStatus() && 'OK' == $client->getResponseBody()) {
return true;
}
}
unlink(__TYPECHO_ROOT_DIR__ . '/.htaccess');
}
} catch (Client\Exception $e) {
if ($hasWrote) {
@unlink(__TYPECHO_ROOT_DIR__ . '/.htaccess');
}
return false;
}
return false;
} elseif (file_exists(__TYPECHO_ROOT_DIR__ . '/.htaccess')) {
@unlink(__TYPECHO_ROOT_DIR__ . '/.htaccess');
}
return true;
}
/**
* 执行更新动作
*
* @throws Exception
*/
public function updatePermalinkSettings()
{
$customPattern = $this->request->get('customPattern');
$postPattern = $this->request->get('postPattern');
/** 验证格式 */
if ($this->form()->validate()) {
Cookie::set('__typecho_form_item_postPattern', $customPattern);
$this->response->goBack();
}
$patternValid = $this->checkRule($postPattern);
/** 解析url pattern */
if ('custom' == $postPattern) {
$postPattern = '/' . ltrim($this->encodeRule($customPattern), '/');
}
$settings = defined('__TYPECHO_REWRITE__') ? [] : $this->request->from('rewrite');
if (isset($postPattern) && $this->request->is('pagePattern')) {
$routingTable = $this->options->routingTable;
$routingTable['post']['url'] = $postPattern;
$routingTable['page']['url'] = '/' . ltrim($this->encodeRule($this->request->get('pagePattern')), '/');
$routingTable['category']['url'] = '/' . ltrim($this->encodeRule($this->request->get('categoryPattern')), '/');
$routingTable['category_page']['url'] = rtrim($routingTable['category']['url'], '/') . '/[page:digital]/';
if (isset($routingTable[0])) {
unset($routingTable[0]);
}
$settings['routingTable'] = json_encode($routingTable);
}
foreach ($settings as $name => $value) {
$this->update(['value' => $value], $this->db->sql()->where('name = ?', $name));
}
if ($patternValid) {
Notice::alloc()->set(_t("设置已经保存"), 'success');
} else {
Notice::alloc()->set(_t("自定义链接与现有规则存在冲突! 它可能影响解析效率, 建议你重新分配一个规则."));
}
$this->response->goBack();
}
/**
* 输出表单结构
*
* @return Form
*/
public function form(): Form
{
/** 构建表格 */
$form = new Form($this->security->getRootUrl('index.php/action/options-permalink'), Form::POST_METHOD);
if (!defined('__TYPECHO_REWRITE__')) {
/** 是否使用地址重写功能 */
$rewrite = new Form\Element\Radio(
'rewrite',
['0' => _t('不启用'), '1' => _t('启用')],
$this->options->rewrite,
_t('是否使用地址重写功能'),
_t('地址重写即 rewrite 功能是某些服务器软件提供的优化内部连接的功能.') . '<br />'
. _t('打开此功能可以让你的链接看上去完全是静态地址.')
);
// disable rewrite check when rewrite opened
if (!$this->options->rewrite && !$this->request->is('enableRewriteAnyway=1')) {
$errorStr = _t('重写功能检测失败, 请检查你的服务器设置');
/** 如果是apache服务器, 可能存在无法写入.htaccess文件的现象 */
if (
strpos(php_sapi_name(), 'apache') !== false
&& !file_exists(__TYPECHO_ROOT_DIR__ . '/.htaccess')
&& !is_writable(__TYPECHO_ROOT_DIR__)
) {
$errorStr .= '<br /><strong>' . _t('我们检测到你使用了apache服务器, 但是程序无法在根目录创建.htaccess文件, 这可能是产生这个错误的原因.')
. _t('请调整你的目录权限, 或者手动创建一个.htaccess文件.') . '</strong>';
}
$errorStr .=
'<br /><input type="checkbox" name="enableRewriteAnyway" id="enableRewriteAnyway" value="1" />'
. ' <label for="enableRewriteAnyway">' . _t('如果你仍然想启用此功能, 请勾选这里') . '</label>';
$rewrite->addRule([$this, 'checkRewrite'], $errorStr);
}
$form->addInput($rewrite);
}
$patterns = [
'/archives/[cid:digital]/' => _t('默认风格')
. ' <code>/archives/{cid}/</code>',
'/archives/[slug].html' => _t('wordpress风格')
. ' <code>/archives/{slug}.html</code>',
'/[year:digital:4]/[month:digital:2]/[day:digital:2]/[slug].html' => _t('按日期归档')
. ' <code>/{year}/{month}/{day}/{slug}.html</code>',
'/[category]/[slug].html' => _t('按分类归档')
. ' <code>/{category}/{slug}.html</code>'
];
/** 自定义文章路径 */
$postPatternValue = $this->options->routingTable['post']['url'];
/** 增加个性化路径 */
$customPatternValue = null;
if ($this->request->is('__typecho_form_item_postPattern')) {
$customPatternValue = $this->request->get('__typecho_form_item_postPattern');
Cookie::delete('__typecho_form_item_postPattern');
} elseif (!isset($patterns[$postPatternValue])) {
$customPatternValue = $this->decodeRule($postPatternValue);
}
$patterns['custom'] = _t('个性化定义') .
' <input type="text" class="w-50 text-s mono" name="customPattern" value="' . $customPatternValue . '" />';
$postPattern = new Form\Element\Radio(
'postPattern',
$patterns,
$postPatternValue,
_t('自定义文章路径'),
_t('可用参数: <code>{cid}</code> 日志 ID, <code>{slug}</code> 日志缩略名, <code>{category}</code> 分类, <code>{directory}</code> 多级分类, <code>{year}</code> 年, <code>{month}</code> 月, <code>{day}</code> 日')
. '<br />' . _t('选择一种合适的文章静态路径风格, 使得你的网站链接更加友好.')
. '<br />' . _t('一旦你选择了某种链接风格请不要轻易修改它.')
);
if ($customPatternValue) {
$postPattern->value('custom');
}
$form->addInput($postPattern->multiMode());
/** 独立页面后缀名 */
$pagePattern = new Form\Element\Text(
'pagePattern',
null,
$this->decodeRule($this->options->routingTable['page']['url']),
_t('独立页面路径'),
_t('可用参数: <code>{cid}</code> 页面 ID, <code>{slug}</code> 页面缩略名, <code>{directory}</code> 多级页面')
. '<br />' . _t('请在路径中至少包含上述的一项参数.')
);
$pagePattern->input->setAttribute('class', 'mono w-60');
$form->addInput($pagePattern->addRule([$this, 'checkPagePattern'], _t('独立页面路径中没有包含 {cid} 或者 {slug} ')));
/** 分类页面 */
$categoryPattern = new Form\Element\Text(
'categoryPattern',
null,
$this->decodeRule($this->options->routingTable['category']['url']),
_t('分类路径'),
_t('可用参数: <code>{mid}</code> 分类 ID, <code>{slug}</code> 分类缩略名, <code>{directory}</code> 多级分类')
. '<br />' . _t('请在路径中至少包含上述的一项参数.')
);
$categoryPattern->input->setAttribute('class', 'mono w-60');
$form->addInput($categoryPattern->addRule([$this, 'checkCategoryPattern'], _t('分类路径中没有包含 {mid} 或者 {slug} ')));
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
/**
* 解析自定义的路径
*
* @param string $rule 待解码的路径
* @return string
*/
protected function decodeRule(string $rule): string
{
return preg_replace("/\[([_a-z0-9-]+)[^\]]*\]/i", "{\\1}", $rule);
}
/**
* 检验规则是否冲突
*
* @param string $value 路由规则
* @return boolean
*/
public function checkRule(string $value): bool
{
if ('custom' != $value) {
return true;
}
$routingTable = $this->options->routingTable;
$currentTable = ['custom' => ['url' => $this->encodeRule($this->request->get('customPattern'))]];
$parser = new Parser($currentTable);
$currentTable = $parser->parse();
$regx = $currentTable['custom']['regx'];
foreach ($routingTable as $key => $val) {
if ('post' != $key && 'page' != $key) {
$pathInfo = preg_replace("/\[([_a-z0-9-]+)[^\]]*\]/i", "{\\1}", $val['url']);
$pathInfo = str_replace(
['{cid}', '{slug}', '{category}', '{year}', '{month}', '{day}', '{', '}'],
['123', 'hello', 'default', '2008', '08', '08', '', ''],
$pathInfo
);
if (preg_match($regx, $pathInfo)) {
return false;
}
}
}
return true;
}
/**
* 编码自定义的路径
*
* @param string $rule 待编码的路径
* @return string
*/
protected function encodeRule(string $rule): string
{
return str_replace(
['{cid}', '{slug}', '{category}', '{directory}', '{year}', '{month}', '{day}', '{mid}'],
[
'[cid:digital]', '[slug]', '[category]', '[directory:split:0]',
'[year:digital:4]', '[month:digital:2]', '[day:digital:2]', '[mid:digital]'
],
$rule
);
}
/**
* 绑定动作
*
* @access public
* @return void
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->isPost())->updatePermalinkSettings();
$this->response->redirect($this->options->adminUrl);
}
}

251
var/Widget/Options/Reading.php Executable file
View File

@@ -0,0 +1,251 @@
<?php
namespace Widget\Options;
use Typecho\Db\Exception;
use Typecho\Plugin;
use Typecho\Widget\Helper\Form;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 文章阅读设置组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Reading extends Permalink
{
/**
* 执行更新动作
*
* @throws Exception
*/
public function updateReadingSettings()
{
/** 验证格式 */
if ($this->form()->validate()) {
$this->response->goBack();
}
$settings = $this->request->from(
'postDateFormat',
'frontPage',
'frontArchive',
'pageSize',
'postsListSize',
'feedFullText'
);
if (
'page' == $settings['frontPage'] && $this->request->is('frontPagePage') &&
$this->db->fetchRow($this->db->select('cid')
->from('table.contents')->where('type = ?', 'page')
->where('status = ?', 'publish')->where('created < ?', $this->options->time)
->where('cid = ?', $pageId = intval($this->request->get('frontPagePage'))))
) {
$settings['frontPage'] = 'page:' . $pageId;
} elseif (
'file' == $settings['frontPage'] && $this->request->is('frontPageFile') &&
file_exists(__TYPECHO_ROOT_DIR__ . '/' . __TYPECHO_THEME_DIR__ . '/' . $this->options->theme . '/' .
($file = trim($this->request->get('frontPageFile'), " ./\\")))
) {
$settings['frontPage'] = 'file:' . $file;
} else {
$settings['frontPage'] = 'recent';
}
if ('recent' != $settings['frontPage']) {
$settings['frontArchive'] = empty($settings['frontArchive']) ? 0 : 1;
if ($settings['frontArchive']) {
$routingTable = $this->options->routingTable;
$routingTable['archive']['url'] = '/' . ltrim($this->encodeRule($this->request->get('archivePattern')), '/');
$routingTable['archive_page']['url'] = rtrim($routingTable['archive']['url'], '/')
. '/page/[page:digital]/';
if (isset($routingTable[0])) {
unset($routingTable[0]);
}
$settings['routingTable'] = json_encode($routingTable);
}
} else {
$settings['frontArchive'] = 0;
}
foreach ($settings as $name => $value) {
$this->update(['value' => $value], $this->db->sql()->where('name = ?', $name));
}
Notice::alloc()->set(_t("设置已经保存"), 'success');
$this->response->goBack();
}
/**
* 输出表单结构
*
* @return Form
*/
public function form(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/options-reading'), Form::POST_METHOD);
/** 文章日期格式 */
$postDateFormat = new Form\Element\Text(
'postDateFormat',
null,
$this->options->postDateFormat,
_t('文章日期格式'),
_t('此格式用于指定显示在文章归档中的日期默认显示格式.') . '<br />'
. _t('在某些主题中这个格式可能不会生效, 因为主题作者可以自定义日期格式.') . '<br />'
. _t('请参考 <a href="https://www.php.net/manual/zh/function.date.php">PHP 日期格式写法</a>.')
);
$postDateFormat->input->setAttribute('class', 'w-40 mono');
$form->addInput($postDateFormat->addRule('xssCheck', _t('请不要在日期格式中使用特殊字符')));
//首页显示
$frontPageParts = explode(':', $this->options->frontPage);
$frontPageType = $frontPageParts[0];
$frontPageValue = count($frontPageParts) > 1 ? $frontPageParts[1] : '';
$frontPageOptions = [
'recent' => _t('显示最新发布的文章')
];
$frontPattern = '</label></span><span class="multiline front-archive%class%">'
. '<input type="checkbox" id="frontArchive" name="frontArchive" value="1"'
. ($this->options->frontArchive && 'recent' != $frontPageType ? ' checked' : '') . ' />
<label for="frontArchive">' . _t(
'同时将文章列表页路径更改为 %s',
'<input type="text" name="archivePattern" class="w-20 mono" value="'
. htmlspecialchars($this->decodeRule($this->options->routingTable['archive']['url'])) . '" />'
)
. '</label>';
// 页面列表
$pages = $this->db->fetchAll($this->db->select('cid', 'title')
->from('table.contents')->where('type = ?', 'page')
->where('status = ?', 'publish')->where('created < ?', $this->options->time));
if (!empty($pages)) {
$pagesSelect = '<select name="frontPagePage" id="frontPage-frontPagePage">';
foreach ($pages as $page) {
$selected = '';
if ('page' == $frontPageType && $page['cid'] == $frontPageValue) {
$selected = ' selected="true"';
}
$pagesSelect .= '<option value="' . $page['cid'] . '"' . $selected
. '>' . $page['title'] . '</option>';
}
$pagesSelect .= '</select>';
$frontPageOptions['page'] = _t(
'使用 %s 页面作为首页',
'</label>' . $pagesSelect . '<label for="frontPage-frontPagePage">'
);
$selectedFrontPageType = 'page';
}
// 自定义文件列表
$files = glob($this->options->themeFile($this->options->theme, '*.php'));
$filesSelect = '';
foreach ($files as $file) {
$info = Plugin::parseInfo($file);
$file = basename($file);
if ('index.php' != $file && 'index' == $info['title']) {
$selected = '';
if ('file' == $frontPageType && $file == $frontPageValue) {
$selected = ' selected="true"';
}
$filesSelect .= '<option value="' . $file . '"' . $selected
. '>' . $file . '</option>';
}
}
if (!empty($filesSelect)) {
$frontPageOptions['file'] = _t(
'直接调用 %s 模板文件',
'</label><select name="frontPageFile" id="frontPage-frontPageFile">'
. $filesSelect . '</select><label for="frontPage-frontPageFile">'
);
$selectedFrontPageType = 'file';
}
if (isset($frontPageOptions[$frontPageType]) && 'recent' != $frontPageType && isset($selectedFrontPageType)) {
$selectedFrontPageType = $frontPageType;
$frontPattern = str_replace('%class%', '', $frontPattern);
}
if (isset($selectedFrontPageType)) {
$frontPattern = str_replace('%class%', ' hidden', $frontPattern);
$frontPageOptions[$selectedFrontPageType] .= $frontPattern;
}
$frontPage = new Form\Element\Radio('frontPage', $frontPageOptions, $frontPageType, _t('站点首页'));
$form->addInput($frontPage->multiMode());
/** 文章列表数目 */
$postsListSize = new Form\Element\Number(
'postsListSize',
null,
$this->options->postsListSize,
_t('文章列表数目'),
_t('此数目用于指定显示在侧边栏中的文章列表数目.')
);
$postsListSize->input->setAttribute('class', 'w-20');
$form->addInput($postsListSize->addRule('isInteger', _t('请填入一个数字')));
/** 每页文章数目 */
$pageSize = new Form\Element\Number(
'pageSize',
null,
$this->options->pageSize,
_t('每页文章数目'),
_t('此数目用于指定文章归档输出时每页显示的文章数目.')
);
$pageSize->input->setAttribute('class', 'w-20');
$form->addInput($pageSize->addRule('isInteger', _t('请填入一个数字')));
/** FEED全文输出 */
$feedFullText = new Form\Element\Radio(
'feedFullText',
['0' => _t('仅输出摘要'), '1' => _t('全文输出')],
$this->options->feedFullText,
_t('聚合全文输出'),
_t('如果你不希望在聚合中输出文章全文,请使用仅输出摘要选项.') . '<br />'
. _t('摘要的文字取决于你在文章中使用分隔符的位置.')
);
$form->addInput($feedFullText);
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
/**
* 绑定动作
*
* @access public
* @return void
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->isPost())->updateReadingSettings();
$this->response->redirect($this->options->adminUrl);
}
}

113
var/Widget/Plugins/Config.php Executable file
View File

@@ -0,0 +1,113 @@
<?php
namespace Widget\Plugins;
use Typecho\Plugin;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form;
use Widget\Base\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 插件配置组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Config extends Options
{
/**
* 获取插件信息
*
* @var array
*/
public array $info;
/**
* 插件文件路径
*
* @var string
*/
private string $pluginFileName;
/**
* 插件类
*
* @var string
*/
private string $className;
/**
* 绑定动作
*
* @throws Plugin\Exception
* @throws Exception|\Typecho\Db\Exception
*/
public function execute()
{
$this->user->pass('administrator');
$config = $this->request->filter('slug')->get('config');
if (empty($config)) {
throw new Exception(_t('插件不存在'), 404);
}
/** 获取插件入口 */
[$this->pluginFileName, $this->className] = Plugin::portal($config, $this->options->pluginDir);
$this->info = Plugin::parseInfo($this->pluginFileName);
}
/**
* 获取菜单标题
*
* @return string
*/
public function getMenuTitle(): string
{
return _t('设置插件 %s', $this->info['title']);
}
/**
* 配置插件
*
* @return Form
* @throws Exception|Plugin\Exception
*/
public function config(): Form
{
/** 获取插件名称 */
$pluginName = $this->request->filter('slug')->get('config');
/** 获取已启用插件 */
$plugins = Plugin::export();
$activatedPlugins = $plugins['activated'];
/** 判断实例化是否成功 */
if (!$this->info['config'] || !isset($activatedPlugins[$pluginName])) {
throw new Exception(_t('无法配置插件'), 500);
}
/** 载入插件 */
require_once $this->pluginFileName;
$form = new Form($this->security->getIndex('/action/plugins-edit?config=' . $pluginName), Form::POST_METHOD);
call_user_func([$this->className, 'config'], $form);
$options = $this->options->plugin($pluginName);
if (!empty($options)) {
foreach ($options as $key => $val) {
$form->getInput($key)->value($val);
}
}
$submit = new Form\Element\Submit(null, null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
}

318
var/Widget/Plugins/Edit.php Executable file
View File

@@ -0,0 +1,318 @@
<?php
namespace Widget\Plugins;
use Typecho\Common;
use Typecho\Db;
use Typecho\Plugin;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form;
use Widget\ActionInterface;
use Widget\Base\Options;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 插件管理组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Options implements ActionInterface
{
/**
* @var bool
*/
private bool $configNoticed = false;
/**
* 启用插件
*
* @param $pluginName
* @throws Exception|Db\Exception|Plugin\Exception
*/
public function activate($pluginName)
{
/** 获取插件入口 */
[$pluginFileName, $className] = Plugin::portal($pluginName, $this->options->pluginDir);
$info = Plugin::parseInfo($pluginFileName);
/** 检测依赖信息 */
if (Plugin::checkDependence($info['since'])) {
/** 获取已启用插件 */
$plugins = Plugin::export();
$activatedPlugins = $plugins['activated'];
/** 载入插件 */
require_once $pluginFileName;
/** 判断实例化是否成功 */
if (
isset($activatedPlugins[$pluginName]) || !class_exists($className)
|| !method_exists($className, 'activate')
) {
throw new Exception(_t('无法启用插件'), 500);
}
try {
$result = call_user_func([$className, 'activate']);
Plugin::activate($pluginName);
$this->update(
['value' => json_encode(Plugin::export())],
$this->db->sql()->where('name = ?', 'plugins')
);
} catch (Plugin\Exception $e) {
/** 截获异常 */
Notice::alloc()->set($e->getMessage(), 'error');
$this->response->goBack();
}
$form = new Form();
call_user_func([$className, 'config'], $form);
$personalForm = new Form();
call_user_func([$className, 'personalConfig'], $personalForm);
$options = $form->getValues();
$personalOptions = $personalForm->getValues();
if ($options && !$this->configHandle($pluginName, $options, true)) {
self::configPlugin($pluginName, $options);
}
if ($personalOptions && !$this->personalConfigHandle($className, $personalOptions)) {
self::configPlugin($pluginName, $personalOptions, true);
}
} else {
$result = _t('<a href="%s">%s</a> 无法在此版本的typecho下正常工作', $info['homepage'], $info['title']);
}
/** 设置高亮 */
Notice::alloc()->highlight('plugin-' . $pluginName);
if (isset($result) && is_string($result)) {
Notice::alloc()->set($result, 'notice');
} else {
Notice::alloc()->set(_t('插件已经被启用'), 'success');
}
$this->response->goBack();
}
/**
* 用自有函数处理配置信息
*
* @access public
* @param string $pluginName 插件名称
* @param array $settings 配置值
* @param boolean $isInit 是否为初始化
* @return boolean
* @throws Plugin\Exception
*/
public function configHandle(string $pluginName, array $settings, bool $isInit): bool
{
/** 获取插件入口 */
[$pluginFileName, $className] = Plugin::portal($pluginName, $this->options->pluginDir);
if (!$isInit && method_exists($className, 'configCheck')) {
$result = call_user_func([$className, 'configCheck'], $settings);
if (!empty($result) && is_string($result)) {
Notice::alloc()->set($result);
$this->configNoticed = true;
}
}
if (method_exists($className, 'configHandle')) {
call_user_func([$className, 'configHandle'], $settings, $isInit);
return true;
}
return false;
}
/**
* 手动配置插件变量
*
* @param string $pluginName 插件名称
* @param array $settings 变量键值对
* @param bool $isPersonal 是否为私人变量
* @throws Db\Exception
*/
public static function configPlugin(string $pluginName, array $settings, bool $isPersonal = false)
{
$db = Db::get();
$pluginName = ($isPersonal ? '_' : '') . 'plugin:' . $pluginName;
$select = $db->select()->from('table.options')
->where('name = ?', $pluginName);
$options = $db->fetchAll($select);
if (empty($settings)) {
if (!empty($options)) {
$db->query($db->delete('table.options')->where('name = ?', $pluginName));
}
} else {
if (empty($options)) {
$db->query($db->insert('table.options')
->rows([
'name' => $pluginName,
'value' => json_encode($settings),
'user' => 0
]));
} else {
foreach ($options as $option) {
$value = json_decode($option['value'], true);
$value = array_merge($value, $settings);
$db->query($db->update('table.options')
->rows(['value' => json_encode($value)])
->where('name = ?', $pluginName)
->where('user = ?', $option['user']));
}
}
}
}
/**
* 用自有函数处理自定义配置信息
*
* @param string $className 类名
* @param array $settings 配置值
* @return boolean
*/
public function personalConfigHandle(string $className, array $settings): bool
{
if (method_exists($className, 'personalConfigHandle')) {
call_user_func([$className, 'personalConfigHandle'], $settings, true);
return true;
}
return false;
}
/**
* 禁用插件
*
* @param string $pluginName
* @throws Db\Exception
* @throws Exception
* @throws Plugin\Exception
*/
public function deactivate(string $pluginName)
{
/** 获取已启用插件 */
$plugins = Plugin::export();
$activatedPlugins = $plugins['activated'];
$pluginFileExist = true;
try {
/** 获取插件入口 */
[$pluginFileName, $className] = Plugin::portal($pluginName, $this->options->pluginDir);
} catch (Plugin\Exception $e) {
$pluginFileExist = false;
if (!isset($activatedPlugins[$pluginName])) {
throw $e;
}
}
/** 判断实例化是否成功 */
if (!isset($activatedPlugins[$pluginName])) {
throw new Exception(_t('无法禁用插件'), 500);
}
if ($pluginFileExist) {
/** 载入插件 */
require_once $pluginFileName;
/** 判断实例化是否成功 */
if (
!isset($activatedPlugins[$pluginName]) || !class_exists($className)
|| !method_exists($className, 'deactivate')
) {
throw new Exception(_t('无法禁用插件'), 500);
}
try {
$result = call_user_func([$className, 'deactivate']);
} catch (Plugin\Exception $e) {
/** 截获异常 */
Notice::alloc()->set($e->getMessage(), 'error');
$this->response->goBack();
}
/** 设置高亮 */
Notice::alloc()->highlight('plugin-' . $pluginName);
}
Plugin::deactivate($pluginName);
$this->update(['value' => json_encode(Plugin::export())], $this->db->sql()->where('name = ?', 'plugins'));
$this->delete($this->db->sql()->where('name = ?', 'plugin:' . $pluginName));
$this->delete($this->db->sql()->where('name = ?', '_plugin:' . $pluginName));
if (isset($result) && is_string($result)) {
Notice::alloc()->set($result);
} else {
Notice::alloc()->set(_t('插件已经被禁用'), 'success');
}
$this->response->goBack();
}
/**
* 配置插件
*
* @param string $pluginName
* @throws Db\Exception
* @throws Exception
* @throws Plugin\Exception
*/
public function config(string $pluginName)
{
$form = Config::alloc()->config();
/** 验证表单 */
if ($form->validate()) {
$this->response->goBack();
}
$settings = $form->getAllRequest();
if (!$this->configHandle($pluginName, $settings, false)) {
self::configPlugin($pluginName, $settings);
}
/** 设置高亮 */
Notice::alloc()->highlight('plugin-' . $pluginName);
if (!$this->configNoticed) {
/** 提示信息 */
Notice::alloc()->set(_t("插件设置已经保存"), 'success');
}
/** 转向原页 */
$this->response->redirect(Common::url('plugins.php', $this->options->adminUrl));
}
/**
* 绑定动作
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->is('activate'))->activate($this->request->filter('slug')->get('activate'));
$this->on($this->request->is('deactivate'))->deactivate($this->request->filter('slug')->get('deactivate'));
$this->on($this->request->is('config'))->config($this->request->filter('slug')->get('config'));
$this->response->redirect($this->options->adminUrl);
}
}

115
var/Widget/Plugins/Rows.php Executable file
View File

@@ -0,0 +1,115 @@
<?php
namespace Widget\Plugins;
use Typecho\Plugin;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 插件列表组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Rows extends Widget
{
/**
* 已启用插件
*
* @access public
* @var array
*/
public array $activatedPlugins = [];
/**
* 执行函数
*
* @access public
* @return void
*/
public function execute()
{
/** 列出插件目录 */
$pluginDirs = $this->getPlugins();
$this->parameter->setDefault(['activated' => null]);
/** 获取已启用插件 */
$plugins = Plugin::export();
$this->activatedPlugins = $plugins['activated'];
if (!empty($pluginDirs)) {
foreach ($pluginDirs as $key => $pluginDir) {
$parts = $this->getPlugin($pluginDir, $key);
if (empty($parts)) {
continue;
}
[$pluginName, $pluginFileName] = $parts;
if (file_exists($pluginFileName)) {
$info = Plugin::parseInfo($pluginFileName);
$info['name'] = $pluginName;
$info['dependence'] = Plugin::checkDependence($info['since']);
/** 默认即插即用 */
$info['activated'] = true;
if ($info['activate'] || $info['deactivate'] || $info['config'] || $info['personalConfig']) {
$info['activated'] = isset($this->activatedPlugins[$pluginName]);
if (isset($this->activatedPlugins[$pluginName])) {
unset($this->activatedPlugins[$pluginName]);
}
}
if ($info['activated'] == $this->parameter->activated) {
$this->push($info);
}
}
}
}
}
/**
* @return array
*/
protected function getPlugins(): array
{
return glob(__TYPECHO_ROOT_DIR__ . '/' . __TYPECHO_PLUGIN_DIR__ . '/*');
}
/**
* @param string $plugin
* @return array|null
*/
protected function getPlugin(string $plugin): ?array
{
if (is_dir($plugin)) {
/** 获取插件名称 */
$pluginName = basename($plugin);
/** 获取插件主文件 */
$pluginFileName = $plugin . '/Plugin.php';
} elseif (file_exists($plugin) && 'index.php' != basename($plugin)) {
$pluginFileName = $plugin;
$part = explode('.', basename($plugin));
if (2 == count($part) && 'php' == $part[1]) {
$pluginName = $part[0];
} else {
return null;
}
} else {
return null;
}
return [$pluginName, $pluginFileName];
}
}

110
var/Widget/Register.php Executable file
View File

@@ -0,0 +1,110 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Db\Exception;
use Typecho\Validate;
use Utils\PasswordHash;
use Widget\Base\Users;
use Widget\Users\EditTrait;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 注册组件
*
* @author qining
* @category typecho
* @package Widget
*/
class Register extends Users implements ActionInterface
{
use EditTrait;
/**
* 初始化函数
*
* @throws Exception
*/
public function action()
{
// protect
$this->security->protect();
/** 如果已经登录 */
if ($this->user->hasLogin() || !$this->options->allowRegister) {
/** 直接返回 */
$this->response->redirect($this->options->index);
}
/** 初始化验证类 */
$validator = new Validate();
$validator->addRule('name', 'required', _t('必须填写用户名称'));
$validator->addRule('name', 'minLength', _t('用户名至少包含2个字符'), 2);
$validator->addRule('name', 'maxLength', _t('用户名最多包含32个字符'), 32);
$validator->addRule('name', 'xssCheck', _t('请不要在用户名中使用特殊字符'));
$validator->addRule('name', [$this, 'nameExists'], _t('用户名已经存在'));
$validator->addRule('mail', 'required', _t('必须填写电子邮箱'));
$validator->addRule('mail', [$this, 'mailExists'], _t('电子邮箱地址已经存在'));
$validator->addRule('mail', 'email', _t('电子邮箱格式错误'));
$validator->addRule('mail', 'maxLength', _t('电子邮箱最多包含64个字符'), 64);
/** 如果请求中有password */
if (array_key_exists('password', $_REQUEST)) {
$validator->addRule('password', 'required', _t('必须填写密码'));
$validator->addRule('password', 'minLength', _t('为了保证账户安全, 请输入至少六位的密码'), 6);
$validator->addRule('password', 'maxLength', _t('为了便于记忆, 密码长度请不要超过十八位'), 18);
$validator->addRule('confirm', 'confirm', _t('两次输入的密码不一致'), 'password');
}
/** 截获验证异常 */
if ($error = $validator->run($this->request->from('name', 'password', 'mail', 'confirm'))) {
Cookie::set('__typecho_remember_name', $this->request->get('name'));
Cookie::set('__typecho_remember_mail', $this->request->get('mail'));
/** 设置提示信息 */
Notice::alloc()->set($error);
$this->response->goBack();
}
$hasher = new PasswordHash(8, true);
$generatedPassword = Common::randString(7);
$dataStruct = [
'name' => $this->request->get('name'),
'mail' => $this->request->get('mail'),
'screenName' => $this->request->get('name'),
'password' => $hasher->hashPassword($generatedPassword),
'created' => $this->options->time,
'group' => 'subscriber'
];
$dataStruct = self::pluginHandle()->filter('register', $dataStruct);
$insertId = $this->insert($dataStruct);
$this->db->fetchRow($this->select()->where('uid = ?', $insertId)
->limit(1), [$this, 'push']);
self::pluginHandle()->call('finishRegister', $this);
$this->user->login($this->request->get('name'), $generatedPassword);
Cookie::delete('__typecho_first_run');
Cookie::delete('__typecho_remember_name');
Cookie::delete('__typecho_remember_mail');
Notice::alloc()->set(
_t(
'用户 <strong>%s</strong> 已经成功注册, 密码为 <strong>%s</strong>',
$this->screenName,
$generatedPassword
),
'success'
);
$this->response->redirect($this->options->adminUrl);
}
}

156
var/Widget/Security.php Executable file
View File

@@ -0,0 +1,156 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Response;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 安全选项组件
*
* @link typecho
* @package Widget
* @copyright Copyright (c) 2014 Typecho team (http://typecho.org)
* @license GNU General Public License 2.0
*/
class Security extends Base
{
/**
* @var string
*/
private string $token;
/**
* @var boolean
*/
private bool $enabled = true;
/**
* @param int $components
*/
public function initComponents(int &$components)
{
$components = self::INIT_OPTIONS | self::INIT_USER;
}
/**
* 初始化函数
*/
public function execute()
{
$this->token = $this->options->secret;
if ($this->user->hasLogin()) {
$this->token .= '&' . $this->user->authCode . '&' . $this->user->uid;
}
}
/**
* @param bool $enabled
*/
public function enable(bool $enabled = true)
{
$this->enabled = $enabled;
}
/**
* 保护提交数据
*/
public function protect()
{
if ($this->enabled && $this->request->get('_') != $this->getToken($this->request->getReferer())) {
$this->response->goBack();
}
}
/**
* 获取token
*
* @param string|null $suffix 后缀
* @return string
*/
public function getToken(?string $suffix): string
{
return md5($this->token . '&' . $suffix);
}
/**
* 获取绝对路由路径
*
* @param string|null $path
* @return string
*/
public function getRootUrl(?string $path): string
{
return Common::url($this->getTokenUrl($path), $this->options->rootUrl);
}
/**
* 生成带token的路径
*
* @param $path
* @param string|null $url
* @return string
*/
public function getTokenUrl($path, ?string $url = null): string
{
$parts = parse_url($path);
$params = [];
if (!empty($parts['query'])) {
parse_str($parts['query'], $params);
}
$params['_'] = $this->getToken($url ?: $this->request->getRequestUrl());
$parts['query'] = http_build_query($params);
return Common::buildUrl($parts);
}
/**
* 输出后台安全路径
*
* @param $path
*/
public function adminUrl($path)
{
echo $this->getAdminUrl($path);
}
/**
* 获取安全的后台路径
*
* @param string $path
* @return string
*/
public function getAdminUrl(string $path): string
{
return Common::url($this->getTokenUrl($path), $this->options->adminUrl);
}
/**
* 输出安全的路由路径
*
* @param $path
*/
public function index($path)
{
echo $this->getIndex($path);
}
/**
* 获取安全的路由路径
*
* @param $path
* @return string
*/
public function getIndex($path): string
{
return Common::url($this->getTokenUrl($path), $this->options->index);
}
}

301
var/Widget/Service.php Executable file
View File

@@ -0,0 +1,301 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Http\Client;
use Typecho\Response;
use Typecho\Widget\Exception;
use Widget\Base\Contents;
use Widget\Base\Options as BaseOptions;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 通用异步服务组件
*
* @author qining
* @category typecho
* @package Widget
*/
class Service extends BaseOptions implements ActionInterface
{
/**
* 异步请求
*
* @var array
*/
public array $asyncRequests = [];
/**
* 发送pingback实现
*
* @throws Exception|Client\Exception
*/
public function sendPingHandle()
{
/** 验证权限 */
$data = $this->request->get('@json');
$token = $data['token'] ?? '';
$permalink = $data['permalink'] ?? '';
$title = $data['title'] ?? '';
$excerpt = $data['excerpt'] ?? '';
$response = ['trackback' => [], 'pingback' => []];
if (!Common::timeTokenValidate($token, $this->options->secret, 3) || empty($permalink)) {
throw new Exception(_t('禁止访问'), 403);
}
$this->response->throwFinish();
/** 忽略超时 */
if (function_exists('ignore_user_abort')) {
ignore_user_abort(true);
}
if (function_exists('set_time_limit')) {
set_time_limit(30);
}
if (!empty($data['pingback'])) {
$links = $data['pingback'];
$permalinkPart = parse_url($permalink);
/** 发送pingback */
foreach ($links as $url) {
$urlPart = parse_url($url);
if (isset($urlPart['scheme'])) {
if ('http' != $urlPart['scheme'] && 'https' != $urlPart['scheme']) {
continue;
}
} else {
$urlPart['scheme'] = 'http';
$url = Common::buildUrl($urlPart);
}
if ($permalinkPart['host'] == $urlPart['host']) {
continue;
}
$spider = Client::get();
if ($spider) {
$spider->setTimeout(10)
->send($url);
if (!($xmlrpcUrl = $spider->getResponseHeader('x-pingback'))) {
if (
preg_match(
"/<link[^>]*rel=[\"']pingback[\"'][^>]*href=[\"']([^\"']+)[\"'][^>]*>/i",
$spider->getResponseBody(),
$out
)
) {
$xmlrpcUrl = $out[1];
}
}
if (!empty($xmlrpcUrl)) {
$response['pingback'][] = $url;
try {
$xmlrpc = new \IXR\Client($xmlrpcUrl);
$xmlrpc->pingback->ping($permalink, $url);
unset($xmlrpc);
} catch (\IXR\Exception $e) {
continue;
}
}
}
unset($spider);
}
}
/** 发送trackback */
if (!empty($data['trackback'])) {
$links = $data['trackback'];
foreach ($links as $url) {
$client = Client::get();
$response['trackback'][] = $url;
if ($client) {
try {
$client->setTimeout(5)
->setData([
'blog_name' => $this->options->title . ' &raquo ' . $title,
'url' => $permalink,
'excerpt' => $excerpt
])
->send($url);
unset($client);
} catch (Client\Exception $e) {
continue;
}
}
}
}
$this->response->throwJson($response);
}
/**
* 发送pingback
* <code>
* $this->sendPing($post);
* </code>
*
* @param Contents $content 内容url
* @param array|null $trackback
*/
public function sendPing(Contents $content, ?array $trackback = null)
{
$this->user->pass('contributor');
if ($client = Client::get()) {
try {
$input = [
'do' => 'ping',
'permalink' => $content->permalink,
'excerpt' => $content->excerpt,
'title' => $content->title,
'token' => Common::timeToken($this->options->secret)
];
if (preg_match_all("|<a[^>]*href=[\"'](.*?)[\"'][^>]*>(.*?)</a>|", $content->content, $matches)) {
$pingback = array_unique($matches[1]);
if (!empty($pingback)) {
$input['pingback'] = $pingback;
}
}
if (!empty($trackback)) {
$input['trackback'] = $trackback;
}
$client->setHeader('User-Agent', $this->options->generator)
->setTimeout(2)
->setJson($input)
->send($this->getServiceUrl('ping'));
} catch (Client\Exception $e) {
return;
}
}
}
/**
* 获取真实的 URL
*
* @param string $do 动作名
* @return string
*/
private function getServiceUrl(string $do): string
{
$url = Common::url('/action/service', $this->options->index);
if (defined('__TYPECHO_SERVICE_URL__')) {
$rootPath = rtrim(parse_url($this->options->rootUrl, PHP_URL_PATH), '/');
$path = parse_url($url, PHP_URL_PATH);
$parts = parse_url(__TYPECHO_SERVICE_URL__);
if (
!empty($parts['path'])
&& $parts['path'] != '/'
&& rtrim($parts['path'], '/') != $rootPath
) {
$path = Common::url($path, $parts['path']);
}
$parts['path'] = $path;
$url = Common::buildUrl($parts);
}
return $url . '?do=' . $do;
}
/**
* 请求异步服务
*
* @param $method
* @param mixed $params
*/
public function requestService($method, ...$params)
{
static $called;
if (!$called) {
Response::getInstance()->addResponder(function () {
if (!empty($this->asyncRequests) && $client = Client::get()) {
try {
$client->setHeader('User-Agent', $this->options->generator)
->setTimeout(2)
->setJson([
'requests' => $this->asyncRequests,
'token' => Common::timeToken($this->options->secret)
])
->send($this->getServiceUrl('async'));
} catch (Client\Exception $e) {
return;
}
}
});
$called = true;
}
$this->asyncRequests[] = [$method, $params];
}
/**
* 执行回调
*
* @throws Exception
*/
public function asyncHandle()
{
/** 验证权限 */
$data = $this->request->get('@json');
$token = $data['token'] ?? '';
if (!Common::timeTokenValidate($token, $this->options->secret, 3)) {
throw new Exception(_t('禁止访问'), 403);
}
$this->response->throwFinish();
/** 忽略超时 */
if (function_exists('ignore_user_abort')) {
ignore_user_abort(true);
}
if (function_exists('set_time_limit')) {
set_time_limit(30);
}
$requests = $data['requests'] ?? null;
$plugin = self::pluginHandle();
if (!empty($requests)) {
foreach ($requests as $request) {
[$method, $params] = $request;
$plugin->call($method, ... $params);
}
}
}
/**
* 异步请求入口
*/
public function action()
{
$this->on($this->request->isPost() && $this->request->is('do=ping'))->sendPingHandle();
$this->on($this->request->isPost() && $this->request->is('do=async'))->asyncHandle();
}
}

340
var/Widget/Stat.php Executable file
View File

@@ -0,0 +1,340 @@
<?php
namespace Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 全局统计组件
*
* @property-read int $publishedPostsNum
* @property-read int $waitingPostsNum
* @property-read int $draftPostsNum
* @property-read int $myPublishedPostsNum
* @property-read int $myWaitingPostsNum
* @property-read int $myDraftPostsNum
* @property-read int $currentPublishedPostsNum
* @property-read int $currentWaitingPostsNum
* @property-read int $currentDraftPostsNum
* @property-read int $publishedPagesNum
* @property-read int $draftPagesNum
* @property-read int $publishedCommentsNum
* @property-read int $waitingCommentsNum
* @property-read int $spamCommentsNum
* @property-read int $myPublishedCommentsNum
* @property-read int $myWaitingCommentsNum
* @property-read int $mySpamCommentsNum
* @property-read int $currentCommentsNum
* @property-read int $currentPublishedCommentsNum
* @property-read int $currentWaitingCommentsNum
* @property-read int $currentSpamCommentsNum
* @property-read int $categoriesNum
* @property-read int $tagsNum
*/
class Stat extends Base
{
/**
* @param int $components
*/
protected function initComponents(int &$components)
{
$components = self::INIT_USER;
}
/**
* 获取已发布的文章数目
*
* @return integer
*/
protected function ___publishedPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post')
->where('table.contents.status = ?', 'publish'))->num;
}
/**
* 获取待审核的文章数目
*
* @return integer
*/
protected function ___waitingPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ? OR table.contents.type = ?', 'post', 'post_draft')
->where('table.contents.status = ?', 'waiting'))->num;
}
/**
* 获取草稿文章数目
*
* @return integer
*/
protected function ___draftPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post_draft'))->num;
}
/**
* 获取当前用户已发布的文章数目
*
* @return integer
*/
protected function ___myPublishedPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post')
->where('table.contents.status = ?', 'publish')
->where('table.contents.authorId = ?', $this->user->uid))->num;
}
/**
* 获取当前用户待审核文章数目
*
* @return integer
*/
protected function ___myWaitingPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ? OR table.contents.type = ?', 'post', 'post_draft')
->where('table.contents.status = ?', 'waiting')
->where('table.contents.authorId = ?', $this->user->uid))->num;
}
/**
* 获取当前用户草稿文章数目
*
* @return integer
*/
protected function ___myDraftPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post_draft')
->where('table.contents.authorId = ?', $this->user->uid))->num;
}
/**
* 获取当前用户已发布的文章数目
*
* @return integer
*/
protected function ___currentPublishedPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post')
->where('table.contents.status = ?', 'publish')
->where('table.contents.authorId = ?', $this->request->filter('int')->get('uid')))->num;
}
/**
* 获取当前用户待审核文章数目
*
* @return integer
*/
protected function ___currentWaitingPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ? OR table.contents.type = ?', 'post', 'post_draft')
->where('table.contents.status = ?', 'waiting')
->where('table.contents.authorId = ?', $this->request->filter('int')->get('uid')))->num;
}
/**
* 获取当前用户草稿文章数目
*
* @return integer
*/
protected function ___currentDraftPostsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post_draft')
->where('table.contents.authorId = ?', $this->request->filter('int')->get('uid')))->num;
}
/**
* 获取已发布页面数目
*
* @return integer
*/
protected function ___publishedPagesNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'page')
->where('table.contents.status = ?', 'publish'))->num;
}
/**
* 获取草稿页面数目
*
* @return integer
*/
protected function ___draftPagesNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'page_draft'))->num;
}
/**
* 获取当前显示的评论数目
*
* @return integer
*/
protected function ___publishedCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'approved'))->num;
}
/**
* 获取当前待审核的评论数目
*
* @return integer
*/
protected function ___waitingCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'waiting'))->num;
}
/**
* 获取当前垃圾评论数目
*
* @return integer
*/
protected function ___spamCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'spam'))->num;
}
/**
* 获取当前用户显示的评论数目
*
* @return integer
*/
protected function ___myPublishedCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'approved')
->where('table.comments.ownerId = ?', $this->user->uid))->num;
}
/**
* 获取当前用户待审核的评论数目
*
* @return integer
*/
protected function ___myWaitingCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'waiting')
->where('table.comments.ownerId = ?', $this->user->uid))->num;
}
/**
* 获取当前用户垃圾评论数目
*
* @return integer
*/
protected function ___mySpamCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'spam')
->where('table.comments.ownerId = ?', $this->user->uid))->num;
}
/**
* 获取当前文章的评论数目
*
* @return integer
*/
protected function ___currentCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.cid = ?', $this->request->filter('int')->get('cid')))->num;
}
/**
* 获取当前文章显示的评论数目
*
* @return integer
*/
protected function ___currentPublishedCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'approved')
->where('table.comments.cid = ?', $this->request->filter('int')->get('cid')))->num;
}
/**
* 获取当前文章待审核的评论数目
*
* @return integer
*/
protected function ___currentWaitingCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'waiting')
->where('table.comments.cid = ?', $this->request->filter('int')->get('cid')))->num;
}
/**
* 获取当前文章垃圾评论数目
*
* @return integer
*/
protected function ___currentSpamCommentsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(coid)' => 'num'])
->from('table.comments')
->where('table.comments.status = ?', 'spam')
->where('table.comments.cid = ?', $this->request->filter('int')->get('cid')))->num;
}
/**
* 获取分类数目
*
* @return integer
*/
protected function ___categoriesNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(mid)' => 'num'])
->from('table.metas')
->where('table.metas.type = ?', 'category'))->num;
}
/**
* 获取标签数目
*
* @return integer
*/
protected function ___tagsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(mid)' => 'num'])
->from('table.metas')
->where('table.metas.type = ?', 'tag'))->num;
}
}

90
var/Widget/Themes/Config.php Executable file
View File

@@ -0,0 +1,90 @@
<?php
namespace Widget\Themes;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Submit;
use Widget\Base\Options as BaseOptions;
use Widget\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 皮肤配置组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Config extends BaseOptions
{
/**
* 绑定动作
*
* @throws Exception|\Typecho\Db\Exception
*/
public function execute()
{
$this->user->pass('administrator');
if (!self::isExists()) {
throw new Exception(_t('外观配置功能不存在'), 404);
}
}
/**
* 配置功能是否存在
*
* @param string|null $theme
* @return boolean
*/
public static function isExists(?string $theme = null): bool
{
$options = Options::alloc();
$theme = $theme ?? $options->theme;
$configFile = $options->themeFile($theme, 'functions.php');
if (!$options->missingTheme && file_exists($configFile)) {
require_once $configFile;
if (function_exists('themeConfig')) {
return true;
}
}
return false;
}
/**
* 配置外观
*
* @return Form
*/
public function config(): Form
{
$form = new Form(
$this->security->getIndex('/action/themes-edit?config=' . Options::alloc()->theme),
Form::POST_METHOD
);
themeConfig($form);
$inputs = $form->getInputs();
if (!empty($inputs)) {
foreach ($inputs as $key => $val) {
if (isset($this->options->{$key})) {
$form->getInput($key)->value($this->options->{$key});
}
}
}
$submit = new Submit(null, null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
}

183
var/Widget/Themes/Edit.php Executable file
View File

@@ -0,0 +1,183 @@
<?php
namespace Widget\Themes;
use Typecho\Common;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form;
use Widget\ActionInterface;
use Widget\Base\Options;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑风格组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Options implements ActionInterface
{
/**
* 更换外观
*
* @param string $theme 外观名称
* @throws Exception
* @throws \Typecho\Db\Exception
*/
public function changeTheme(string $theme)
{
$theme = trim($theme, './');
if (is_dir($this->options->themeFile($theme))) {
/** 删除原外观设置信息 */
$oldTheme = $this->options->missingTheme ?: $this->options->theme;
$this->delete($this->db->sql()->where('name = ?', 'theme:' . $oldTheme));
$this->update(['value' => $theme], $this->db->sql()->where('name = ?', 'theme'));
/** 解除首页关联 */
if (0 === strpos($this->options->frontPage, 'file:')) {
$this->update(['value' => 'recent'], $this->db->sql()->where('name = ?', 'frontPage'));
}
$this->options->themeUrl = $this->options->themeUrl(null, $theme);
$configFile = $this->options->themeFile($theme, 'functions.php');
if (file_exists($configFile)) {
require_once $configFile;
if (function_exists('themeConfig')) {
$form = new Form();
themeConfig($form);
$options = $form->getValues();
if ($options && !$this->configHandle($options, true)) {
$this->insert([
'name' => 'theme:' . $theme,
'value' => json_encode($options),
'user' => 0
]);
}
}
}
Notice::alloc()->highlight('theme-' . $theme);
Notice::alloc()->set(_t("外观已经改变"), 'success');
$this->response->goBack();
} else {
throw new Exception(_t('您选择的风格不存在'));
}
}
/**
* 用自有函数处理配置信息
*
* @param array $settings 配置值
* @param boolean $isInit 是否为初始化
* @return boolean
*/
public function configHandle(array $settings, bool $isInit): bool
{
if (function_exists('themeConfigHandle')) {
themeConfigHandle($settings, $isInit);
return true;
}
return false;
}
/**
* 编辑外观文件
*
* @param string $theme 外观名称
* @param string $file 文件名
* @throws Exception
*/
public function editThemeFile(string $theme, string $file)
{
$path = $this->options->themeFile($theme, $file);
if (
file_exists($path) && is_writable($path)
&& (!defined('__TYPECHO_THEME_WRITEABLE__') || __TYPECHO_THEME_WRITEABLE__)
) {
$handle = fopen($path, 'wb');
if ($handle && fwrite($handle, $this->request->get('content'))) {
fclose($handle);
Notice::alloc()->set(_t("文件 %s 的更改已经保存", $file), 'success');
} else {
Notice::alloc()->set(_t("文件 %s 无法被写入", $file), 'error');
}
$this->response->goBack();
} else {
throw new Exception(_t('您编辑的文件不存在'));
}
}
/**
* 配置外观
*
* @param string $theme 外观名
* @throws \Typecho\Db\Exception
*/
public function config(string $theme)
{
// 已经载入了外观函数
$form = Config::alloc()->config();
/** 验证表单 */
if (!Config::isExists($theme) || $form->validate()) {
$this->response->goBack();
}
$settings = $form->getAllRequest();
if (!$this->configHandle($settings, false)) {
if ($this->options->__get('theme:' . $theme)) {
$this->update(
['value' => json_encode($settings)],
$this->db->sql()->where('name = ?', 'theme:' . $theme)
);
} else {
$this->insert([
'name' => 'theme:' . $theme,
'value' => json_encode($settings),
'user' => 0
]);
}
}
/** 设置高亮 */
Notice::alloc()->highlight('theme-' . $theme);
/** 提示信息 */
Notice::alloc()->set(_t("外观设置已经保存"), 'success');
/** 转向原页 */
$this->response->redirect(Common::url('options-theme.php', $this->options->adminUrl));
}
/**
* 绑定动作
*
* @throws Exception|\Typecho\Db\Exception
*/
public function action()
{
/** 需要管理员权限 */
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->is('change'))->changeTheme($this->request->filter('slug')->get('change'));
$this->on($this->request->is('edit&theme'))
->editThemeFile($this->request->filter('slug')->get('theme'), $this->request->get('edit'));
$this->on($this->request->is('config'))->config($this->request->filter('slug')->get('config'));
$this->response->redirect($this->options->adminUrl);
}
}

147
var/Widget/Themes/Files.php Executable file
View File

@@ -0,0 +1,147 @@
<?php
namespace Widget\Themes;
use Typecho\Widget;
use Widget\Base;
use Widget\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 风格文件列表组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Files extends Base
{
/**
* 当前风格
*
* @access private
* @var string
*/
private string $currentTheme;
/**
* 当前文件
*
* @access private
* @var string
*/
private string $currentFile;
/**
* 执行函数
*
* @throws Widget\Exception
*/
public function execute()
{
/** 管理员权限 */
$this->user->pass('administrator');
$this->currentTheme = $this->request->filter('slug')->get('theme', Options::alloc()->theme);
if (
preg_match("/^([_0-9a-z-. ])+$/i", $this->currentTheme)
&& is_dir($dir = Options::alloc()->themeFile($this->currentTheme))
&& (!defined('__TYPECHO_THEME_WRITEABLE__') || __TYPECHO_THEME_WRITEABLE__)
) {
$files = array_filter(glob($dir . '/*'), function ($path) {
return preg_match("/\.(php|js|css|vbs)$/i", $path);
});
$this->currentFile = $this->request->get('file', 'index.php');
if (
preg_match("/^([_0-9a-z-. ])+$/i", $this->currentFile)
&& file_exists($dir . '/' . $this->currentFile)
) {
foreach ($files as $file) {
if (file_exists($file)) {
$file = basename($file);
$this->push([
'file' => $file,
'theme' => $this->currentTheme,
'current' => ($file == $this->currentFile)
]);
}
}
return;
}
}
throw new Widget\Exception('风格文件不存在', 404);
}
/**
* 判断是否拥有写入权限
*
* @return bool
*/
public static function isWriteable(): bool
{
return (!defined('__TYPECHO_THEME_WRITEABLE__') || __TYPECHO_THEME_WRITEABLE__)
&& !Options::alloc()->missingTheme;
}
/**
* 获取菜单标题
*
* @return string
*/
public function getMenuTitle(): string
{
return _t('编辑文件 %s', $this->currentFile);
}
/**
* 获取文件内容
*
* @return string
*/
public function currentContent(): string
{
return htmlspecialchars(file_get_contents(Options::alloc()
->themeFile($this->currentTheme, $this->currentFile)));
}
/**
* 获取文件是否可读
*
* @return bool
*/
public function currentIsWriteable(): bool
{
return is_writable(Options::alloc()
->themeFile($this->currentTheme, $this->currentFile))
&& self::isWriteable();
}
/**
* 获取当前文件
*
* @return string
*/
public function currentFile(): string
{
return $this->currentFile;
}
/**
* 获取当前风格
*
* @return string
*/
public function currentTheme(): string
{
return $this->currentTheme;
}
}

86
var/Widget/Themes/Rows.php Executable file
View File

@@ -0,0 +1,86 @@
<?php
namespace Widget\Themes;
use Typecho\Common;
use Typecho\Plugin;
use Typecho\Widget;
use Widget\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 风格列表组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Rows extends Widget
{
/**
* 执行函数
*/
public function execute()
{
$themes = $this->getThemes();
if ($themes) {
$options = Options::alloc();
$activated = 0;
$result = [];
foreach ($themes as $key => $theme) {
$themeFile = $theme . '/index.php';
if (file_exists($themeFile)) {
$info = Plugin::parseInfo($themeFile);
$info['name'] = $this->getTheme($theme);
if ($info['activated'] = ($options->theme == $info['name'])) {
$activated = $key;
}
$screen = array_filter(glob($theme . '/*'), function ($path) {
return preg_match("/screenshot\.(jpg|png|gif|bmp|jpeg|webp|avif)$/i", $path);
});
if ($screen) {
$info['screen'] = $options->themeUrl(basename(current($screen)), $info['name']);
} else {
$info['screen'] = Common::url('noscreen.png', $options->adminStaticUrl('img'));
}
$result[$key] = $info;
}
}
$clone = $result[$activated];
unset($result[$activated]);
array_unshift($result, $clone);
array_filter($result, [$this, 'push']);
}
}
/**
* @return array
*/
protected function getThemes(): array
{
return glob(__TYPECHO_ROOT_DIR__ . __TYPECHO_THEME_DIR__ . '/*', GLOB_ONLYDIR);
}
/**
* get theme
*
* @param string $theme
* @return string
*/
protected function getTheme(string $theme): string
{
return basename($theme);
}
}

104
var/Widget/Upgrade.php Executable file
View File

@@ -0,0 +1,104 @@
<?php
namespace Widget;
use Typecho\Common;
use Exception;
use Widget\Base\Options as BaseOptions;
use Utils\Upgrade as UpgradeAction;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 升级组件
*
* @author qining
* @category typecho
* @package Widget
*/
class Upgrade extends BaseOptions implements ActionInterface
{
/**
* minimum supported version
*/
public const MIN_VERSION = '1.1.0';
/**
* 执行升级程序
*
* @throws \Typecho\Db\Exception
*/
public function upgrade()
{
$currentVersion = $this->options->version;
if (version_compare($currentVersion, self::MIN_VERSION, '<')) {
Notice::alloc()->set(
_t('请先升级至版本 %s', self::MIN_VERSION),
'error'
);
$this->response->goBack();
}
$ref = new \ReflectionClass(UpgradeAction::class);
$message = [];
foreach ($ref->getMethods() as $method) {
preg_match("/^v([_0-9]+)$/", $method->getName(), $matches);
$version = str_replace('_', '.', $matches[1]);
if (version_compare($currentVersion, $version, '>=')) {
continue;
}
$options = Options::allocWithAlias($version);
/** 执行升级脚本 */
try {
$result = $method->invoke(null, $this->db, $options);
if (!empty($result)) {
$message[] = $result;
}
} catch (Exception $e) {
Notice::alloc()->set($e->getMessage(), 'error');
$this->response->goBack();
}
/** 更新版本号 */
$this->update(
['value' => 'Typecho ' . $version],
$this->db->sql()->where('name = ?', 'generator')
);
Options::destroy($version);
}
/** 更新版本号 */
$this->update(
['value' => 'Typecho ' . Common::VERSION],
$this->db->sql()->where('name = ?', 'generator')
);
Notice::alloc()->set(
empty($message) ? _t("升级已经完成") : $message,
empty($message) ? 'success' : 'notice'
);
}
/**
* 初始化函数
*
* @throws \Typecho\Db\Exception
* @throws \Typecho\Widget\Exception
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->isPost())->upgrade();
$this->response->redirect($this->options->adminUrl);
}
}

438
var/Widget/Upload.php Executable file
View File

@@ -0,0 +1,438 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Config;
use Typecho\Date;
use Typecho\Db\Exception;
use Typecho\Plugin;
use Widget\Base\Contents;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 上传组件
*
* @author qining
* @category typecho
* @package Widget
*/
class Upload extends Contents implements ActionInterface
{
//上传文件目录
public const UPLOAD_DIR = '/usr/uploads';
/**
* 删除文件
*
* @param array $content 文件相关信息
* @return bool
*/
public static function deleteHandle(array $content): bool
{
$result = Plugin::factory(Upload::class)->trigger($hasDeleted)->call('deleteHandle', $content);
if ($hasDeleted) {
return $result;
}
return @unlink(__TYPECHO_ROOT_DIR__ . '/' . $content['attachment']->path);
}
/**
* 获取实际文件绝对访问路径
*
* @param Config $attachment 文件相关信息
* @return string
*/
public static function attachmentHandle(Config $attachment): string
{
$result = Plugin::factory(Upload::class)->trigger($hasPlugged)->call('attachmentHandle', $attachment);
if ($hasPlugged) {
return $result;
}
$options = Options::alloc();
return Common::url(
$attachment->path,
defined('__TYPECHO_UPLOAD_URL__') ? __TYPECHO_UPLOAD_URL__ : $options->siteUrl
);
}
/**
* 获取实际文件数据
*
* @param array $content
* @return string
*/
public static function attachmentDataHandle(array $content): string
{
$result = Plugin::factory(Upload::class)->trigger($hasPlugged)->call('attachmentDataHandle', $content);
if ($hasPlugged) {
return $result;
}
return file_get_contents(
Common::url(
$content['attachment']->path,
defined('__TYPECHO_UPLOAD_ROOT_DIR__') ? __TYPECHO_UPLOAD_ROOT_DIR__ : __TYPECHO_ROOT_DIR__
)
);
}
/**
* 初始化函数
*/
public function action()
{
if ($this->user->pass('contributor', true) && $this->request->isPost()) {
$this->security->protect();
if ($this->request->is('do=modify&cid')) {
$this->modify();
} else {
$this->upload();
}
} else {
$this->response->setStatus(403);
}
}
/**
* 执行升级程序
*
* @throws Exception
*/
public function modify()
{
if (!empty($_FILES)) {
$file = array_pop($_FILES);
if (0 == $file['error'] && is_uploaded_file($file['tmp_name'])) {
$this->db->fetchRow(
$this->select()->where(
'table.contents.cid = ?',
$this->request->filter('int')->get('cid')
)
->where('table.contents.type = ?', 'attachment'),
[$this, 'push']
);
if (!$this->have()) {
$this->response->setStatus(404);
exit;
}
if (!$this->allow('edit')) {
$this->response->setStatus(403);
exit;
}
// xhr的send无法支持utf8
if ($this->request->isAjax()) {
$file['name'] = urldecode($file['name']);
}
$result = self::modifyHandle($this->toColumn(['cid', 'attachment', 'parent']), $file);
if (false !== $result) {
self::pluginHandle()->call('beforeModify', $result);
$this->update([
'text' => json_encode($result)
], $this->db->sql()->where('cid = ?', $this->cid));
$this->db->fetchRow($this->select()->where('table.contents.cid = ?', $this->cid)
->where('table.contents.type = ?', 'attachment'), [$this, 'push']);
/** 增加插件接口 */
self::pluginHandle()->call('modify', $this);
$this->response->throwJson([$this->attachment->url, [
'cid' => $this->cid,
'title' => $this->attachment->name,
'type' => $this->attachment->type,
'size' => $this->attachment->size,
'bytes' => number_format(ceil($this->attachment->size / 1024)) . ' Kb',
'isImage' => $this->attachment->isImage,
'url' => $this->attachment->url,
'permalink' => $this->permalink
]]);
}
}
}
$this->response->throwJson(false);
}
/**
* 修改文件处理函数,如果需要实现自己的文件哈希或者特殊的文件系统,请在options表里把modifyHandle改成自己的函数
*
* @param array $content 老文件
* @param array $file 新上传的文件
* @return mixed
*/
public static function modifyHandle(array $content, array $file)
{
if (empty($file['name'])) {
return false;
}
$result = self::pluginHandle()->trigger($hasModified)->call('modifyHandle', $content, $file);
if ($hasModified) {
return $result;
}
$ext = self::getSafeName($file['name']);
if ($content['attachment']->type != $ext) {
return false;
}
$path = Common::url(
$content['attachment']->path,
defined('__TYPECHO_UPLOAD_ROOT_DIR__') ? __TYPECHO_UPLOAD_ROOT_DIR__ : __TYPECHO_ROOT_DIR__
);
$dir = dirname($path);
//创建上传目录
if (!is_dir($dir)) {
if (!self::makeUploadDir($dir)) {
return false;
}
}
if (isset($file['tmp_name'])) {
@unlink($path);
//移动上传文件
if (!@move_uploaded_file($file['tmp_name'], $path)) {
return false;
}
} elseif (isset($file['bytes'])) {
@unlink($path);
//直接写入文件
if (!file_put_contents($path, $file['bytes'])) {
return false;
}
} elseif (isset($file['bits'])) {
@unlink($path);
//直接写入文件
if (!file_put_contents($path, $file['bits'])) {
return false;
}
} else {
return false;
}
if (!isset($file['size'])) {
$file['size'] = filesize($path);
}
//返回相对存储路径
return [
'name' => $content['attachment']->name,
'path' => $content['attachment']->path,
'size' => $file['size'],
'type' => $content['attachment']->type,
'mime' => $content['attachment']->mime
];
}
/**
* 获取安全的文件名
*
* @param string $name
* @return string
*/
private static function getSafeName(string &$name): string
{
$name = str_replace(['"', '<', '>'], '', $name);
$name = str_replace('\\', '/', $name);
$name = false === strpos($name, '/') ? ('a' . $name) : str_replace('/', '/a', $name);
$info = pathinfo($name);
$name = substr($info['basename'], 1);
return isset($info['extension']) ? strtolower($info['extension']) : '';
}
/**
* 创建上传路径
*
* @param string $path 路径
* @return boolean
*/
private static function makeUploadDir(string $path): bool
{
$path = preg_replace("/\\\+/", '/', $path);
$current = rtrim($path, '/');
$last = $current;
while (!is_dir($current) && false !== strpos($path, '/')) {
$last = $current;
$current = dirname($current);
}
if ($last == $current) {
return true;
}
if (!@mkdir($last, 0755)) {
return false;
}
return self::makeUploadDir($path);
}
/**
* 执行升级程序
*
* @throws Exception
*/
public function upload()
{
if (!empty($_FILES)) {
$file = array_pop($_FILES);
if (0 == $file['error'] && is_uploaded_file($file['tmp_name'])) {
// xhr的send无法支持utf8
if ($this->request->isAjax()) {
$file['name'] = urldecode($file['name']);
}
$result = self::uploadHandle($file);
if (false !== $result) {
self::pluginHandle()->call('beforeUpload', $result);
$struct = [
'title' => $result['name'],
'slug' => $result['name'],
'type' => 'attachment',
'status' => 'publish',
'text' => json_encode($result),
'allowComment' => 1,
'allowPing' => 0,
'allowFeed' => 1
];
if (isset($this->request->cid)) {
$cid = $this->request->filter('int')->get('cid');
if ($this->isWriteable($this->db->sql()->where('cid = ?', $cid))) {
$struct['parent'] = $cid;
}
}
$insertId = $this->insert($struct);
$this->db->fetchRow($this->select()->where('table.contents.cid = ?', $insertId)
->where('table.contents.type = ?', 'attachment'), [$this, 'push']);
/** 增加插件接口 */
self::pluginHandle()->call('upload', $this);
$this->response->throwJson([$this->attachment->url, [
'cid' => $insertId,
'title' => $this->attachment->name,
'type' => $this->attachment->type,
'size' => $this->attachment->size,
'bytes' => number_format(ceil($this->attachment->size / 1024)) . ' Kb',
'isImage' => $this->attachment->isImage,
'url' => $this->attachment->url,
'permalink' => $this->permalink
]]);
}
}
}
$this->response->throwJson(false);
}
/**
* 上传文件处理函数,如果需要实现自己的文件哈希或者特殊的文件系统,请在options表里把uploadHandle改成自己的函数
*
* @param array $file 上传的文件
* @return mixed
*/
public static function uploadHandle(array $file)
{
if (empty($file['name'])) {
return false;
}
$result = self::pluginHandle()->trigger($hasUploaded)->call('uploadHandle', $file);
if ($hasUploaded) {
return $result;
}
$ext = self::getSafeName($file['name']);
if (!self::checkFileType($ext)) {
return false;
}
$date = new Date();
$path = Common::url(
defined('__TYPECHO_UPLOAD_DIR__') ? __TYPECHO_UPLOAD_DIR__ : self::UPLOAD_DIR,
defined('__TYPECHO_UPLOAD_ROOT_DIR__') ? __TYPECHO_UPLOAD_ROOT_DIR__ : __TYPECHO_ROOT_DIR__
) . '/' . $date->year . '/' . $date->month;
//创建上传目录
if (!is_dir($path)) {
if (!self::makeUploadDir($path)) {
return false;
}
}
//获取文件名
$fileName = sprintf('%u', crc32(uniqid())) . '.' . $ext;
$path = $path . '/' . $fileName;
if (isset($file['tmp_name'])) {
//移动上传文件
if (!@move_uploaded_file($file['tmp_name'], $path)) {
return false;
}
} elseif (isset($file['bytes'])) {
//直接写入文件
if (!file_put_contents($path, $file['bytes'])) {
return false;
}
} elseif (isset($file['bits'])) {
//直接写入文件
if (!file_put_contents($path, $file['bits'])) {
return false;
}
} else {
return false;
}
if (!isset($file['size'])) {
$file['size'] = filesize($path);
}
//返回相对存储路径
return [
'name' => $file['name'],
'path' => (defined('__TYPECHO_UPLOAD_DIR__') ? __TYPECHO_UPLOAD_DIR__ : self::UPLOAD_DIR)
. '/' . $date->year . '/' . $date->month . '/' . $fileName,
'size' => $file['size'],
'type' => $ext,
'mime' => Common::mimeContentType($path)
];
}
/**
* 检查文件名
*
* @access private
* @param string $ext 扩展名
* @return boolean
*/
public static function checkFileType(string $ext): bool
{
$options = Options::alloc();
return in_array($ext, $options->allowedAttachmentTypes);
}
}

289
var/Widget/User.php Executable file
View File

@@ -0,0 +1,289 @@
<?php
namespace Widget;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Db\Exception as DbException;
use Typecho\Widget;
use Utils\PasswordHash;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 当前登录用户
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class User extends Users
{
/**
* 用户组
*
* @var array
*/
public array $groups = [
'administrator' => 0,
'editor' => 1,
'contributor' => 2,
'subscriber' => 3,
'visitor' => 4
];
/**
* 用户
*
* @var array
*/
private array $currentUser;
/**
* 是否已经登录
*
* @var boolean|null
*/
private ?bool $hasLogin = null;
/**
* @param int $components
*/
protected function initComponents(int &$components)
{
$components = self::INIT_OPTIONS;
}
/**
* 执行函数
*
* @throws DbException
*/
public function execute()
{
if ($this->hasLogin()) {
$this->push($this->currentUser);
// update last activated time
$this->db->query($this->db
->update('table.users')
->rows(['activated' => $this->options->time])
->where('uid = ?', $this->currentUser['uid']));
// merge personal options
$options = $this->personalOptions->toArray();
foreach ($options as $key => $val) {
$this->options->{$key} = $val;
}
}
}
/**
* 判断用户是否已经登录
*
* @return boolean
* @throws DbException
*/
public function hasLogin(): ?bool
{
if (null !== $this->hasLogin) {
return $this->hasLogin;
} else {
$cookieUid = Cookie::get('__typecho_uid');
if (null !== $cookieUid) {
/** 验证登录 */
$user = $this->db->fetchRow($this->db->select()->from('table.users')
->where('uid = ?', intval($cookieUid))
->limit(1));
$cookieAuthCode = Cookie::get('__typecho_authCode');
if ($user && Common::hashValidate($user['authCode'], $cookieAuthCode)) {
$this->currentUser = $user;
return ($this->hasLogin = true);
}
$this->logout();
}
return ($this->hasLogin = false);
}
}
/**
* 用户登出函数
*
* @access public
* @return void
*/
public function logout()
{
self::pluginHandle()->trigger($logoutPluggable)->call('logout');
if ($logoutPluggable) {
return;
}
Cookie::delete('__typecho_uid');
Cookie::delete('__typecho_authCode');
}
/**
* 以用户名和密码登录
*
* @access public
* @param string $name 用户名
* @param string $password 密码
* @param boolean $temporarily 是否为临时登录
* @param integer $expire 过期时间
* @return boolean
* @throws DbException
*/
public function login(string $name, string $password, bool $temporarily = false, int $expire = 0): bool
{
//插件接口
$result = self::pluginHandle()->trigger($loginPluggable)->call('login', $name, $password, $temporarily, $expire);
if ($loginPluggable) {
return $result;
}
/** 开始验证用户 **/
$user = $this->db->fetchRow($this->db->select()
->from('table.users')
->where('name = ?', $name)
->limit(1));
if (empty($user) && strpos($name, '@') !== false) {
$user = $this->db->fetchRow($this->db->select()
->from('table.users')
->where('mail = ?', $name)
->limit(1));
}
if (empty($user)) {
return false;
}
$hashValidate = self::pluginHandle()->trigger($hashPluggable)->call('hashValidate', $password, $user['password']);
if (!$hashPluggable) {
if ('$P$' == substr($user['password'], 0, 3)) {
$hasher = new PasswordHash(8, true);
$hashValidate = $hasher->checkPassword($password, $user['password']);
} else {
$hashValidate = Common::hashValidate($password, $user['password']);
}
}
if ($hashValidate) {
if (!$temporarily) {
$this->commitLogin($user, $expire);
}
/** 压入数据 */
$this->push($user);
$this->currentUser = $user;
$this->hasLogin = true;
self::pluginHandle()->call('loginSucceed', $this, $name, $password, $temporarily, $expire);
return true;
}
self::pluginHandle()->call('loginFail', $this, $name, $password, $temporarily, $expire);
return false;
}
/**
* @param $user
* @param int $expire
* @throws DbException
*/
public function commitLogin(&$user, int $expire = 0)
{
$authCode = function_exists('openssl_random_pseudo_bytes') ?
bin2hex(openssl_random_pseudo_bytes(16)) : sha1(Common::randString(20));
$user['authCode'] = $authCode;
Cookie::set('__typecho_uid', $user['uid'], $expire);
Cookie::set('__typecho_authCode', Common::hash($authCode), $expire);
//更新最后登录时间以及验证码
$this->db->query($this->db
->update('table.users')
->expression('logged', 'activated')
->rows(['authCode' => $authCode])
->where('uid = ?', $user['uid']));
}
/**
* 只需要提供uid或者完整user数组即可登录的方法, 多用于插件等特殊场合
*
* @param int | array $uid 用户id或者用户数据数组
* @param boolean $temporarily 是否为临时登录,默认为临时登录以兼容以前的方法
* @param integer $expire 过期时间
* @return boolean
* @throws DbException
*/
public function simpleLogin($uid, bool $temporarily = true, int $expire = 0): bool
{
if (is_array($uid)) {
$user = $uid;
} else {
$user = $this->db->fetchRow($this->db->select()
->from('table.users')
->where('uid = ?', $uid)
->limit(1));
}
if (empty($user)) {
self::pluginHandle()->call('simpleLoginFail', $this);
return false;
}
if (!$temporarily) {
$this->commitLogin($user, $expire);
}
$this->push($user);
$this->currentUser = $user;
$this->hasLogin = true;
self::pluginHandle()->call('simpleLoginSucceed', $this, $user);
return true;
}
/**
* 判断用户权限
*
* @access public
* @param string $group 用户组
* @param boolean $return 是否为返回模式
* @return boolean
* @throws DbException|Widget\Exception
*/
public function pass(string $group, bool $return = false): bool
{
if ($this->hasLogin()) {
if (array_key_exists($group, $this->groups) && $this->groups[$this->group] <= $this->groups[$group]) {
return true;
}
} else {
if ($return) {
return false;
} else {
//防止循环重定向
$this->response->redirect(defined('__TYPECHO_ADMIN__') ? $this->options->loginUrl .
(0 === strpos($this->request->getReferer() ?? '', $this->options->loginUrl) ? '' :
'?referer=' . urlencode($this->request->makeUriByRequest())) : $this->options->siteUrl);
}
}
if ($return) {
return false;
} else {
throw new Widget\Exception(_t('禁止访问'), 403);
}
}
}

120
var/Widget/Users/Admin.php Executable file
View File

@@ -0,0 +1,120 @@
<?php
namespace Widget\Users;
use Typecho\Common;
use Typecho\Db;
use Typecho\Db\Query;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\PageNavigator\Box;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 后台成员列表组件
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Admin extends Users
{
/**
* 分页计算对象
*
* @var Query
*/
private Query $countSql;
/**
* 所有文章个数
*
* @var integer
*/
private int $total;
/**
* 当前页
*
* @var integer
*/
private int $currentPage;
/**
* 执行函数
*
* @throws Db\Exception
*/
public function execute()
{
$this->parameter->setDefault('pageSize=20');
$select = $this->select();
$this->currentPage = $this->request->filter('int')->get('page', 1);
/** 过滤标题 */
if (null != ($keywords = $this->request->get('keywords'))) {
$select->where(
'name LIKE ? OR screenName LIKE ?',
'%' . Common::filterSearchQuery($keywords) . '%',
'%' . Common::filterSearchQuery($keywords) . '%'
);
}
$this->countSql = clone $select;
$select->order('table.users.uid')
->page($this->currentPage, $this->parameter->pageSize);
$this->db->fetchAll($select, [$this, 'push']);
}
/**
* 输出分页
*
* @throws Exception|Db\Exception
*/
public function pageNav()
{
$query = $this->request->makeUriByRequest('page={page}');
/** 使用盒状分页 */
$nav = new Box(
!isset($this->total) ? $this->total = $this->size($this->countSql) : $this->total,
$this->currentPage,
$this->parameter->pageSize,
$query
);
$nav->render('&laquo;', '&raquo;');
}
/**
* 仅仅输出域名和路径
*
* @return string
*/
protected function ___domainPath(): string
{
$parts = parse_url($this->url);
return $parts['host'] . ($parts['path'] ?? null);
}
/**
* 发布文章数
*
* @return integer
* @throws Db\Exception
*/
protected function ___postsNum(): int
{
return $this->db->fetchObject($this->db->select(['COUNT(cid)' => 'num'])
->from('table.contents')
->where('table.contents.type = ?', 'post')
->where('table.contents.status = ?', 'publish')
->where('table.contents.authorId = ?', $this->uid))->num;
}
}

35
var/Widget/Users/Author.php Executable file
View File

@@ -0,0 +1,35 @@
<?php
namespace Widget\Users;
use Typecho\Db\Exception;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 相关内容组件(根据标签关联)
*
* @author qining
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Author extends Users
{
/**
* 执行函数,初始化数据
*
* @throws Exception
*/
public function execute()
{
if (isset($this->parameter->uid)) {
$this->db->fetchRow($this->select()
->where('uid = ?', $this->parameter->uid), [$this, 'push']);
}
}
}

320
var/Widget/Users/Edit.php Executable file
View File

@@ -0,0 +1,320 @@
<?php
namespace Widget\Users;
use Typecho\Common;
use Typecho\Widget\Exception;
use Typecho\Widget\Helper\Form;
use Utils\PasswordHash;
use Widget\ActionInterface;
use Widget\Base\Users;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑用户组件
*
* @link typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Edit extends Users implements ActionInterface
{
use EditTrait;
/**
* 执行函数
*
* @return void
* @throws Exception|\Typecho\Db\Exception
*/
public function execute()
{
/** 管理员以上权限 */
$this->user->pass('administrator');
/** 更新模式 */
if (($this->request->is('uid') && 'delete' != $this->request->get('do')) || $this->request->is('do=update')) {
$this->db->fetchRow($this->select()
->where('uid = ?', $this->request->get('uid'))->limit(1), [$this, 'push']);
if (!$this->have()) {
throw new Exception(_t('用户不存在'), 404);
}
}
}
/**
* 获取菜单标题
*
* @return string
*/
public function getMenuTitle(): string
{
return _t('编辑用户 %s', $this->name);
}
/**
* 判断用户是否存在
*
* @param integer $uid 用户主键
* @return boolean
* @throws \Typecho\Db\Exception
*/
public function userExists(int $uid): bool
{
$user = $this->db->fetchRow($this->db->select()
->from('table.users')
->where('uid = ?', $uid)->limit(1));
return !empty($user);
}
/**
* 增加用户
*
* @throws \Typecho\Db\Exception
*/
public function insertUser()
{
if ($this->form('insert')->validate()) {
$this->response->goBack();
}
$hasher = new PasswordHash(8, true);
/** 取出数据 */
$user = $this->request->from('name', 'mail', 'screenName', 'password', 'url', 'group');
$user['screenName'] = empty($user['screenName']) ? $user['name'] : $user['screenName'];
$user['password'] = $hasher->hashPassword($user['password']);
$user['created'] = $this->options->time;
/** 插入数据 */
$user['uid'] = $this->insert($user);
/** 设置高亮 */
Notice::alloc()->highlight('user-' . $user['uid']);
/** 提示信息 */
Notice::alloc()->set(_t('用户 %s 已经被增加', $user['screenName']), 'success');
/** 转向原页 */
$this->response->redirect(Common::url('manage-users.php', $this->options->adminUrl));
}
/**
* 生成表单
*
* @access public
* @param string|null $action 表单动作
* @return Form
*/
public function form(?string $action = null): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/users-edit'), Form::POST_METHOD);
/** 用户名称 */
$name = new Form\Element\Text('name', null, null, _t('用户名') . ' *', _t('此用户名将作为用户登录时所用的名称.')
. '<br />' . _t('请不要与系统中现有的用户名重复.'));
$form->addInput($name);
/** 电子邮箱地址 */
$mail = new Form\Element\Text('mail', null, null, _t('邮件地址') . ' *', _t('电子邮箱地址将作为此用户的主要联系方式.')
. '<br />' . _t('请不要与系统中现有的电子邮箱地址重复.'));
$form->addInput($mail);
/** 用户昵称 */
$screenName = new Form\Element\Text('screenName', null, null, _t('用户昵称'), _t('用户昵称可以与用户名不同, 用于前台显示.')
. '<br />' . _t('如果你将此项留空, 将默认使用用户名.'));
$form->addInput($screenName);
/** 用户密码 */
$password = new Form\Element\Password('password', null, null, _t('用户密码'), _t('为此用户分配一个密码.')
. '<br />' . _t('建议使用特殊字符与字母、数字的混编样式,以增加系统安全性.'));
$password->input->setAttribute('class', 'w-60');
$form->addInput($password);
/** 用户密码确认 */
$confirm = new Form\Element\Password('confirm', null, null, _t('用户密码确认'), _t('请确认你的密码, 与上面输入的密码保持一致.'));
$confirm->input->setAttribute('class', 'w-60');
$form->addInput($confirm);
/** 个人主页地址 */
$url = new Form\Element\Text('url', null, null, _t('个人主页地址'), _t('此用户的个人主页地址, 请用 <code>https://</code> 开头.'));
$form->addInput($url);
/** 用户组 */
$group = new Form\Element\Select(
'group',
[
'subscriber' => _t('关注者'),
'contributor' => _t('贡献者'), 'editor' => _t('编辑'), 'administrator' => _t('管理员')
],
null,
_t('用户组'),
_t('不同的用户组拥有不同的权限.') . '<br />' . _t('具体的权限分配表请<a href="https://docs.typecho.org/develop/acl">参考这里</a>.')
);
$form->addInput($group);
/** 用户动作 */
$do = new Form\Element\Hidden('do');
$form->addInput($do);
/** 用户主键 */
$uid = new Form\Element\Hidden('uid');
$form->addInput($uid);
/** 提交按钮 */
$submit = new Form\Element\Submit();
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
if ($this->request->is('uid')) {
$submit->value(_t('编辑用户'));
$name->value($this->name);
$screenName->value($this->screenName);
$url->value($this->url);
$mail->value($this->mail);
$group->value($this->group);
$do->value('update');
$uid->value($this->uid);
$_action = 'update';
} else {
$submit->value(_t('增加用户'));
$do->value('insert');
$_action = 'insert';
}
if (empty($action)) {
$action = $_action;
}
/** 给表单增加规则 */
if ('insert' == $action || 'update' == $action) {
$screenName->addRule([$this, 'screenNameExists'], _t('昵称已经存在'));
$screenName->addRule('xssCheck', _t('请不要在昵称中使用特殊字符'));
$url->addRule('url', _t('个人主页地址格式错误'));
$mail->addRule('required', _t('必须填写电子邮箱'));
$mail->addRule([$this, 'mailExists'], _t('电子邮箱地址已经存在'));
$mail->addRule('email', _t('电子邮箱格式错误'));
$password->addRule('minLength', _t('为了保证账户安全, 请输入至少六位的密码'), 6);
$confirm->addRule('confirm', _t('两次输入的密码不一致'), 'password');
}
if ('insert' == $action) {
$name->addRule('required', _t('必须填写用户名称'));
$name->addRule('xssCheck', _t('请不要在用户名中使用特殊字符'));
$name->addRule([$this, 'nameExists'], _t('用户名已经存在'));
$password->label(_t('用户密码') . ' *');
$confirm->label(_t('用户密码确认') . ' *');
$password->addRule('required', _t('必须填写密码'));
}
if ('update' == $action) {
$name->input->setAttribute('disabled', 'disabled');
$uid->addRule('required', _t('用户主键不存在'));
$uid->addRule([$this, 'userExists'], _t('用户不存在'));
}
return $form;
}
/**
* 更新用户
*
* @throws \Typecho\Db\Exception
*/
public function updateUser()
{
if ($this->form('update')->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$user = $this->request->from('mail', 'screenName', 'password', 'url', 'group');
$user['screenName'] = empty($user['screenName']) ? $user['name'] : $user['screenName'];
if (empty($user['password'])) {
unset($user['password']);
} else {
$hasher = new PasswordHash(8, true);
$user['password'] = $hasher->hashPassword($user['password']);
}
/** 更新数据 */
$this->update($user, $this->db->sql()->where('uid = ?', $this->request->get('uid')));
/** 设置高亮 */
Notice::alloc()->highlight('user-' . $this->request->get('uid'));
/** 提示信息 */
Notice::alloc()->set(_t('用户 %s 已经被更新', $user['screenName']), 'success');
/** 转向原页 */
$this->response->redirect(Common::url('manage-users.php?' .
$this->getPageOffsetQuery($this->request->get('uid')), $this->options->adminUrl));
}
/**
* 获取页面偏移的URL Query
*
* @param integer $uid 用户id
* @return string
* @throws \Typecho\Db\Exception
*/
protected function getPageOffsetQuery(int $uid): string
{
return 'page=' . $this->getPageOffset('uid', $uid);
}
/**
* 删除用户
*
* @throws \Typecho\Db\Exception
*/
public function deleteUser()
{
$users = $this->request->filter('int')->getArray('uid');
$masterUserId = $this->db->fetchObject($this->db->select(['MIN(uid)' => 'num'])->from('table.users'))->num;
$deleteCount = 0;
foreach ($users as $user) {
if ($masterUserId == $user || $user == $this->user->uid) {
continue;
}
if ($this->delete($this->db->sql()->where('uid = ?', $user))) {
$deleteCount++;
}
}
/** 提示信息 */
Notice::alloc()->set(
$deleteCount > 0 ? _t('用户已经删除') : _t('没有用户被删除'),
$deleteCount > 0 ? 'success' : 'notice'
);
/** 转向原页 */
$this->response->redirect(Common::url('manage-users.php', $this->options->adminUrl));
}
/**
* 入口函数
*
* @access public
* @return void
*/
public function action()
{
$this->user->pass('administrator');
$this->security->protect();
$this->on($this->request->is('do=insert'))->insertUser();
$this->on($this->request->is('do=update'))->updateUser();
$this->on($this->request->is('do=delete'))->deleteUser();
$this->response->redirect($this->options->adminUrl);
}
}

100
var/Widget/Users/EditTrait.php Executable file
View File

@@ -0,0 +1,100 @@
<?php
namespace Widget\Users;
use Typecho\Db\Exception;
/**
* 编辑用户组件
*/
trait EditTrait
{
/**
* 判断用户名称是否存在
*
* @param string $name 用户名称
* @return boolean
* @throws Exception
*/
public function nameExists(string $name): bool
{
$select = $this->db->select()
->from('table.users')
->where('name = ?', $name)
->limit(1);
if ($this->request->is('uid')) {
$select->where('uid <> ?', $this->request->get('uid'));
}
$user = $this->db->fetchRow($select);
return !$user;
}
/**
* 判断电子邮件是否存在
*
* @param string $mail 电子邮件
* @return boolean
* @throws Exception
*/
public function mailExists(string $mail): bool
{
$select = $this->db->select()
->from('table.users')
->where('mail = ?', $mail)
->limit(1);
if ($this->request->is('uid')) {
$select->where('uid <> ?', $this->request->get('uid'));
}
$user = $this->db->fetchRow($select);
return !$user;
}
/**
* 判断用户昵称是否存在
*
* @param string $screenName 昵称
* @return boolean
* @throws Exception
*/
public function screenNameExists(string $screenName): bool
{
$select = $this->db->select()
->from('table.users')
->where('screenName = ?', $screenName)
->limit(1);
if ($this->request->is('uid')) {
$select->where('uid <> ?', $this->request->get('uid'));
}
$user = $this->db->fetchRow($select);
return !$user;
}
/**
* 获取页面偏移
*
* @param string $column 字段名
* @param integer $offset 偏移值
* @param string|null $group 用户组
* @param integer $pageSize 分页值
* @return integer
* @throws Exception
*/
protected function getPageOffset(string $column, int $offset, ?string $group = null, int $pageSize = 20): int
{
$select = $this->db->select(['COUNT(uid)' => 'num'])->from('table.users')
->where("table.users.{$column} > {$offset}");
if (!empty($group)) {
$select->where('table.users.group = ?', $group);
}
$count = $this->db->fetchObject($select)->num + 1;
return ceil($count / $pageSize);
}
}

460
var/Widget/Users/Profile.php Executable file
View File

@@ -0,0 +1,460 @@
<?php
namespace Widget\Users;
use Typecho\Common;
use Typecho\Db\Exception;
use Typecho\Plugin;
use Typecho\Widget\Helper\Form;
use Utils\PasswordHash;
use Widget\ActionInterface;
use Widget\Base\Options;
use Widget\Base\Users;
use Widget\Notice;
use Widget\Plugins\Rows;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 编辑用户组件
*
* @link typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Profile extends Users implements ActionInterface
{
use EditTrait;
/**
* 执行函数
*/
public function execute()
{
/** 注册用户以上权限 */
$this->user->pass('subscriber');
$this->request->setParam('uid', $this->user->uid);
}
/**
* 输出表单结构
*
* @access public
* @return Form
*/
public function optionsForm(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/users-profile'), Form::POST_METHOD);
/** 撰写设置 */
$markdown = new Form\Element\Radio(
'markdown',
['0' => _t('关闭'), '1' => _t('打开')],
$this->options->markdown,
_t('使用 Markdown 语法编辑和解析内容'),
_t('使用 <a href="https://daringfireball.net/projects/markdown/">Markdown</a> 语法能够使您的撰写过程更加简便直观.')
. '<br />' . _t('此功能开启不会影响以前没有使用 Markdown 语法编辑的内容.')
);
$form->addInput($markdown);
$xmlrpcMarkdown = new Form\Element\Radio(
'xmlrpcMarkdown',
['0' => _t('关闭'), '1' => _t('打开')],
$this->options->xmlrpcMarkdown,
_t('在 XMLRPC 接口中使用 Markdown 语法'),
_t('对于完全支持 <a href="https://daringfireball.net/projects/markdown/">Markdown</a> 语法写作的离线编辑器, 打开此选项后将避免内容被转换为 HTML.')
);
$form->addInput($xmlrpcMarkdown);
/** 自动保存 */
$autoSave = new Form\Element\Radio(
'autoSave',
['0' => _t('关闭'), '1' => _t('打开')],
$this->options->autoSave,
_t('自动保存'),
_t('自动保存功能可以更好地保护你的文章不会丢失.')
);
$form->addInput($autoSave);
/** 默认允许 */
$allow = [];
if ($this->options->defaultAllowComment) {
$allow[] = 'comment';
}
if ($this->options->defaultAllowPing) {
$allow[] = 'ping';
}
if ($this->options->defaultAllowFeed) {
$allow[] = 'feed';
}
$defaultAllow = new Form\Element\Checkbox(
'defaultAllow',
['comment' => _t('可以被评论'), 'ping' => _t('可以被引用'), 'feed' => _t('出现在聚合中')],
$allow,
_t('默认允许'),
_t('设置你经常使用的默认允许权限')
);
$form->addInput($defaultAllow);
/** 用户动作 */
$do = new Form\Element\Hidden('do', null, 'options');
$form->addInput($do);
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
/**
* 自定义设置列表
*
* @throws Plugin\Exception
*/
public function personalFormList()
{
$plugins = Rows::alloc('activated=1');
while ($plugins->next()) {
if ($plugins->personalConfig) {
[$pluginFileName, $className] = Plugin::portal($plugins->name, $this->options->pluginDir);
$form = $this->personalForm($plugins->name, $className, $pluginFileName, $group);
if ($this->user->pass($group, true)) {
echo '<br><section id="personal-' . $plugins->name . '">';
echo '<h3>' . $plugins->title . '</h3>';
$form->render();
echo '</section>';
}
}
}
}
/**
* 输出自定义设置选项
*
* @access public
* @param string $pluginName 插件名称
* @param string $className 类名称
* @param string $pluginFileName 插件文件名
* @param string|null $group 用户组
* @throws Plugin\Exception
*/
public function personalForm(string $pluginName, string $className, string $pluginFileName, ?string &$group): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/users-profile'), Form::POST_METHOD);
$form->setAttribute('name', $pluginName);
$form->setAttribute('id', $pluginName);
require_once $pluginFileName;
$group = call_user_func([$className, 'personalConfig'], $form);
$group = $group ?: 'subscriber';
$options = $this->options->personalPlugin($pluginName);
if (!empty($options)) {
foreach ($options as $key => $val) {
$form->getInput($key)->value($val);
}
}
$form->addItem(new Form\Element\Hidden('do', null, 'personal'));
$form->addItem(new Form\Element\Hidden('plugin', null, $pluginName));
$submit = new Form\Element\Submit('submit', null, _t('保存设置'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
return $form;
}
/**
* 更新用户
*
* @throws Exception
*/
public function updateProfile()
{
if ($this->profileForm()->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$user = $this->request->from('mail', 'screenName', 'url');
$user['screenName'] = empty($user['screenName']) ? $user['name'] : $user['screenName'];
/** 更新数据 */
$this->update($user, $this->db->sql()->where('uid = ?', $this->user->uid));
/** 设置高亮 */
Notice::alloc()->highlight('user-' . $this->user->uid);
/** 提示信息 */
Notice::alloc()->set(_t('您的档案已经更新'), 'success');
/** 转向原页 */
$this->response->goBack();
}
/**
* 生成表单
*
* @return Form
*/
public function profileForm(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/users-profile'), Form::POST_METHOD);
/** 用户昵称 */
$screenName = new Form\Element\Text('screenName', null, null, _t('昵称'), _t('用户昵称可以与用户名不同, 用于前台显示.')
. '<br />' . _t('如果你将此项留空, 将默认使用用户名.'));
$form->addInput($screenName);
/** 个人主页地址 */
$url = new Form\Element\Url('url', null, null, _t('个人主页地址'), _t('此用户的个人主页地址, 请用 <code>https://</code> 开头.'));
$form->addInput($url);
/** 电子邮箱地址 */
$mail = new Form\Element\Text('mail', null, null, _t('邮件地址') . ' *', _t('电子邮箱地址将作为此用户的主要联系方式.')
. '<br />' . _t('请不要与系统中现有的电子邮箱地址重复.'));
$form->addInput($mail);
/** 用户动作 */
$do = new Form\Element\Hidden('do', null, 'profile');
$form->addInput($do);
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('更新我的档案'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
$screenName->value($this->user->screenName);
$url->value($this->user->url);
$mail->value($this->user->mail);
/** 给表单增加规则 */
$screenName->addRule([$this, 'screenNameExists'], _t('昵称已经存在'));
$screenName->addRule('xssCheck', _t('请不要在昵称中使用特殊字符'));
$url->addRule('url', _t('个人主页地址格式错误'));
$mail->addRule('required', _t('必须填写电子邮箱'));
$mail->addRule([$this, 'mailExists'], _t('电子邮箱地址已经存在'));
$mail->addRule('email', _t('电子邮箱格式错误'));
return $form;
}
/**
* 执行更新动作
*
* @throws Exception
*/
public function updateOptions()
{
$settings['autoSave'] = $this->request->is('autoSave=1') ? 1 : 0;
$settings['markdown'] = $this->request->is('markdown=1') ? 1 : 0;
$settings['xmlrpcMarkdown'] = $this->request->is('xmlrpcMarkdown=1') ? 1 : 0;
$defaultAllow = $this->request->getArray('defaultAllow');
$settings['defaultAllowComment'] = in_array('comment', $defaultAllow) ? 1 : 0;
$settings['defaultAllowPing'] = in_array('ping', $defaultAllow) ? 1 : 0;
$settings['defaultAllowFeed'] = in_array('feed', $defaultAllow) ? 1 : 0;
foreach ($settings as $name => $value) {
if (
$this->db->fetchObject($this->db->select(['COUNT(*)' => 'num'])
->from('table.options')->where('name = ? AND user = ?', $name, $this->user->uid))->num > 0
) {
Options::alloc()
->update(
['value' => $value],
$this->db->sql()->where('name = ? AND user = ?', $name, $this->user->uid)
);
} else {
Options::alloc()->insert([
'name' => $name,
'value' => $value,
'user' => $this->user->uid
]);
}
}
Notice::alloc()->set(_t("设置已经保存"), 'success');
$this->response->goBack();
}
/**
* 更新密码
*
* @throws Exception
*/
public function updatePassword()
{
/** 验证格式 */
if ($this->passwordForm()->validate()) {
$this->response->goBack();
}
$hasher = new PasswordHash(8, true);
$password = $hasher->hashPassword($this->request->password);
/** 更新数据 */
$this->update(
['password' => $password],
$this->db->sql()->where('uid = ?', $this->user->uid)
);
/** 设置高亮 */
Notice::alloc()->highlight('user-' . $this->user->uid);
/** 提示信息 */
Notice::alloc()->set(_t('密码已经成功修改'), 'success');
/** 转向原页 */
$this->response->goBack();
}
/**
* 生成表单
*
* @return Form
*/
public function passwordForm(): Form
{
/** 构建表格 */
$form = new Form($this->security->getIndex('/action/users-profile'), Form::POST_METHOD);
/** 用户密码 */
$password = new Form\Element\Password('password', null, null, _t('用户密码'), _t('为此用户分配一个密码.')
. '<br />' . _t('建议使用特殊字符与字母、数字的混编样式,以增加系统安全性.'));
$password->input->setAttribute('class', 'w-60');
$form->addInput($password);
/** 用户密码确认 */
$confirm = new Form\Element\Password('confirm', null, null, _t('用户密码确认'), _t('请确认你的密码, 与上面输入的密码保持一致.'));
$confirm->input->setAttribute('class', 'w-60');
$form->addInput($confirm);
/** 用户动作 */
$do = new Form\Element\Hidden('do', null, 'password');
$form->addInput($do);
/** 提交按钮 */
$submit = new Form\Element\Submit('submit', null, _t('更新密码'));
$submit->input->setAttribute('class', 'btn primary');
$form->addItem($submit);
$password->addRule('required', _t('必须填写密码'));
$password->addRule('minLength', _t('为了保证账户安全, 请输入至少六位的密码'), 6);
$confirm->addRule('confirm', _t('两次输入的密码不一致'), 'password');
return $form;
}
/**
* 更新个人设置
*
* @throws \Typecho\Widget\Exception
*/
public function updatePersonal()
{
/** 获取插件名称 */
$pluginName = $this->request->get('plugin');
/** 获取已启用插件 */
$plugins = Plugin::export();
$activatedPlugins = $plugins['activated'];
/** 获取插件入口 */
[$pluginFileName, $className] = Plugin::portal(
$pluginName,
__TYPECHO_ROOT_DIR__ . '/' . __TYPECHO_PLUGIN_DIR__
);
$info = Plugin::parseInfo($pluginFileName);
if (!$info['personalConfig'] || !isset($activatedPlugins[$pluginName])) {
throw new \Typecho\Widget\Exception(_t('无法配置插件'), 500);
}
$form = $this->personalForm($pluginName, $className, $pluginFileName, $group);
$this->user->pass($group);
/** 验证表单 */
if ($form->validate()) {
$this->response->goBack();
}
$settings = $form->getAllRequest();
unset($settings['do'], $settings['plugin']);
$name = '_plugin:' . $pluginName;
if (!$this->personalConfigHandle($className, $settings)) {
if (
$this->db->fetchObject($this->db->select(['COUNT(*)' => 'num'])
->from('table.options')->where('name = ? AND user = ?', $name, $this->user->uid))->num > 0
) {
Options::alloc()
->update(
['value' => json_encode($settings)],
$this->db->sql()->where('name = ? AND user = ?', $name, $this->user->uid)
);
} else {
Options::alloc()->insert([
'name' => $name,
'value' => json_encode($settings),
'user' => $this->user->uid
]);
}
}
/** 提示信息 */
Notice::alloc()->set(_t("%s 设置已经保存", $info['title']), 'success');
/** 转向原页 */
$this->response->redirect(Common::url('profile.php', $this->options->adminUrl));
}
/**
* 用自有函数处理自定义配置信息
*
* @access public
* @param string $className 类名
* @param array $settings 配置值
* @return boolean
*/
public function personalConfigHandle(string $className, array $settings): bool
{
if (method_exists($className, 'personalConfigHandle')) {
call_user_func([$className, 'personalConfigHandle'], $settings, false);
return true;
}
return false;
}
/**
* 入口函数
*
* @access public
* @return void
*/
public function action()
{
$this->security->protect();
$this->on($this->request->is('do=profile'))->updateProfile();
$this->on($this->request->is('do=options'))->updateOptions();
$this->on($this->request->is('do=password'))->updatePassword();
$this->on($this->request->is('do=personal&plugin'))->updatePersonal();
$this->response->redirect($this->options->siteUrl);
}
}

2022
var/Widget/XmlRpc.php Executable file

File diff suppressed because it is too large Load Diff