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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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