mirror of
https://git.beihong.wang/wangbeihong/blog-source.git
synced 2026-04-23 14:13:04 +08:00
initial
This commit is contained in:
78
var/Widget/Action.php
Executable file
78
var/Widget/Action.php
Executable 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
14
var/Widget/ActionInterface.php
Executable file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Widget;
|
||||
|
||||
/**
|
||||
* 可以被Widget\Action调用的接口
|
||||
*/
|
||||
interface ActionInterface
|
||||
{
|
||||
/**
|
||||
* 接口需要实现的入口函数
|
||||
*/
|
||||
public function action();
|
||||
}
|
||||
166
var/Widget/Ajax.php
Executable file
166
var/Widget/Ajax.php
Executable 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
2139
var/Widget/Archive.php
Executable file
File diff suppressed because it is too large
Load Diff
383
var/Widget/Backup.php
Executable file
383
var/Widget/Backup.php
Executable 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
122
var/Widget/Base.php
Executable 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
515
var/Widget/Base/Comments.php
Executable 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
961
var/Widget/Base/Contents.php
Executable 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
255
var/Widget/Base/Metas.php
Executable 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
83
var/Widget/Base/Options.php
Executable 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;
|
||||
}
|
||||
}
|
||||
13
var/Widget/Base/PrimaryKeyInterface.php
Executable file
13
var/Widget/Base/PrimaryKeyInterface.php
Executable file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Widget\Base;
|
||||
|
||||
interface PrimaryKeyInterface
|
||||
{
|
||||
/**
|
||||
* 获取主键
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrimaryKey(): string;
|
||||
}
|
||||
56
var/Widget/Base/QueryInterface.php
Executable file
56
var/Widget/Base/QueryInterface.php
Executable 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;
|
||||
}
|
||||
17
var/Widget/Base/RowFilterInterface.php
Executable file
17
var/Widget/Base/RowFilterInterface.php
Executable 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
279
var/Widget/Base/TreeTrait.php
Executable 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¤t=');
|
||||
|
||||
$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
126
var/Widget/Base/TreeViewTrait.php
Executable 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
209
var/Widget/Base/Users.php
Executable 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
50
var/Widget/CommentPage.php
Executable 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
166
var/Widget/Comments/Admin.php
Executable 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('«'), _t('»'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前内容结构
|
||||
*
|
||||
* @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
507
var/Widget/Comments/Archive.php
Executable 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 = '«',
|
||||
string $next = '»',
|
||||
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
401
var/Widget/Comments/Edit.php
Executable 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
149
var/Widget/Comments/Ping.php
Executable 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
60
var/Widget/Comments/Recent.php
Executable 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']);
|
||||
}
|
||||
}
|
||||
108
var/Widget/Contents/AdminTrait.php
Executable file
108
var/Widget/Contents/AdminTrait.php
Executable 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('«', '»');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
);
|
||||
}
|
||||
}
|
||||
68
var/Widget/Contents/Attachment/Admin.php
Executable file
68
var/Widget/Contents/Attachment/Admin.php
Executable 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)
|
||||
));
|
||||
}
|
||||
}
|
||||
331
var/Widget/Contents/Attachment/Edit.php
Executable file
331
var/Widget/Contents/Attachment/Edit.php
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
var/Widget/Contents/Attachment/Related.php
Executable file
57
var/Widget/Contents/Attachment/Related.php
Executable 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']);
|
||||
}
|
||||
}
|
||||
52
var/Widget/Contents/Attachment/Unattached.php
Executable file
52
var/Widget/Contents/Attachment/Unattached.php
Executable 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
775
var/Widget/Contents/EditTrait.php
Executable 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
92
var/Widget/Contents/From.php
Executable 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'));
|
||||
}
|
||||
}
|
||||
144
var/Widget/Contents/Page/Admin.php
Executable file
144
var/Widget/Contents/Page/Admin.php
Executable 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 '« ';
|
||||
_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
393
var/Widget/Contents/Page/Edit.php
Executable 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
101
var/Widget/Contents/Page/Rows.php
Executable 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
109
var/Widget/Contents/Post/Admin.php
Executable file
109
var/Widget/Contents/Post/Admin.php
Executable 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']);
|
||||
}
|
||||
}
|
||||
77
var/Widget/Contents/Post/Date.php
Executable file
77
var/Widget/Contents/Post/Date.php
Executable 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
373
var/Widget/Contents/Post/Edit.php
Executable 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';
|
||||
}
|
||||
}
|
||||
54
var/Widget/Contents/Post/Recent.php
Executable file
54
var/Widget/Contents/Post/Recent.php
Executable 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']);
|
||||
}
|
||||
}
|
||||
139
var/Widget/Contents/PrepareEditTrait.php
Executable file
139
var/Widget/Contents/PrepareEditTrait.php
Executable 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
64
var/Widget/Contents/Related.php
Executable 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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
var/Widget/Contents/Related/Author.php
Executable file
45
var/Widget/Contents/Related/Author.php
Executable 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
27
var/Widget/ExceptionHandle.php
Executable 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
212
var/Widget/Feed.php
Executable 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
355
var/Widget/Feedback.php
Executable 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
119
var/Widget/Init.php
Executable 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
112
var/Widget/Login.php
Executable 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
37
var/Widget/Logout.php
Executable 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
327
var/Widget/Menu.php
Executable 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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
95
var/Widget/Metas/Category/Admin.php
Executable file
95
var/Widget/Metas/Category/Admin.php
Executable 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 '« ';
|
||||
_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 : '');
|
||||
}
|
||||
}
|
||||
514
var/Widget/Metas/Category/Edit.php
Executable file
514
var/Widget/Metas/Category/Edit.php
Executable 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(' ', $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);
|
||||
}
|
||||
}
|
||||
21
var/Widget/Metas/Category/InitTreeRowsTrait.php
Executable file
21
var/Widget/Metas/Category/InitTreeRowsTrait.php
Executable 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'));
|
||||
}
|
||||
}
|
||||
38
var/Widget/Metas/Category/Related.php
Executable file
38
var/Widget/Metas/Category/Related.php
Executable 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));
|
||||
}
|
||||
}
|
||||
70
var/Widget/Metas/Category/Rows.php
Executable file
70
var/Widget/Metas/Category/Rows.php
Executable 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
104
var/Widget/Metas/EditTrait.php
Executable 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
52
var/Widget/Metas/From.php
Executable 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
54
var/Widget/Metas/Tag/Admin.php
Executable 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
57
var/Widget/Metas/Tag/Cloud.php
Executable 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
407
var/Widget/Metas/Tag/Edit.php
Executable 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);
|
||||
}
|
||||
}
|
||||
26
var/Widget/Metas/Tag/Related.php
Executable file
26
var/Widget/Metas/Tag/Related.php
Executable 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
73
var/Widget/Notice.php
Executable 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
771
var/Widget/Options.php
Executable 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
302
var/Widget/Options/Discussion.php
Executable 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><a href=""> <img src=""> <blockquote></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);
|
||||
}
|
||||
}
|
||||
21
var/Widget/Options/EditTrait.php
Executable file
21
var/Widget/Options/EditTrait.php
Executable 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
314
var/Widget/Options/General.php
Executable 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
387
var/Widget/Options/Permalink.php
Executable 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
251
var/Widget/Options/Reading.php
Executable 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
113
var/Widget/Plugins/Config.php
Executable 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
318
var/Widget/Plugins/Edit.php
Executable 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
115
var/Widget/Plugins/Rows.php
Executable 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
110
var/Widget/Register.php
Executable 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
156
var/Widget/Security.php
Executable 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
301
var/Widget/Service.php
Executable 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 . ' » ' . $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
340
var/Widget/Stat.php
Executable 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
90
var/Widget/Themes/Config.php
Executable 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
183
var/Widget/Themes/Edit.php
Executable 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
147
var/Widget/Themes/Files.php
Executable 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
86
var/Widget/Themes/Rows.php
Executable 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
104
var/Widget/Upgrade.php
Executable 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
438
var/Widget/Upload.php
Executable 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
289
var/Widget/User.php
Executable 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
120
var/Widget/Users/Admin.php
Executable 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('«', '»');
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅仅输出域名和路径
|
||||
*
|
||||
* @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
35
var/Widget/Users/Author.php
Executable 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
320
var/Widget/Users/Edit.php
Executable 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
100
var/Widget/Users/EditTrait.php
Executable 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
460
var/Widget/Users/Profile.php
Executable 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
2022
var/Widget/XmlRpc.php
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user