mirror of
https://github.com/rumosky/mp-Blog.git
synced 2026-10-19 23:00:58 +08:00
1014 lines
31 KiB
PHP
1014 lines
31 KiB
PHP
<?php
|
|
if (!defined('__TYPECHO_ROOT_DIR__')) {
|
|
exit;
|
|
}
|
|
|
|
class Restful_Action extends Typecho_Widget implements Widget_Interface_Do
|
|
{
|
|
/**
|
|
* @var Typecho_Config
|
|
*/
|
|
private $config;
|
|
|
|
/**
|
|
* @var Typecho_Db
|
|
*/
|
|
private $db;
|
|
|
|
/**
|
|
* @var Widget_Options
|
|
*/
|
|
private $options;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $httpParams;
|
|
|
|
public function __construct($request, $response, $params = null)
|
|
{
|
|
parent::__construct($request, $response, $params);
|
|
|
|
$this->db = Typecho_Db::get();
|
|
$this->options = $this->widget('Widget_Options');
|
|
$this->config = $this->options->plugin('Restful');
|
|
}
|
|
|
|
/**
|
|
* 获取路由参数
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function getRoutes()
|
|
{
|
|
$routes = array();
|
|
$reflectClass = new ReflectionClass(__CLASS__);
|
|
$prefix = defined('__TYPECHO_RESTFUL_PREFIX__') ? __TYPECHO_RESTFUL_PREFIX__ : '/api/';
|
|
|
|
foreach ($reflectClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectMethod) {
|
|
$methodName = $reflectMethod->getName();
|
|
|
|
preg_match('/(.*)Action$/', $methodName, $matches);
|
|
if (isset($matches[1])) {
|
|
array_push($routes, array(
|
|
'action' => $matches[0],
|
|
'name' => 'rest_' . $matches[1],
|
|
'shortName' => $matches[1],
|
|
'uri' => $prefix . $matches[1],
|
|
'description' => trim(str_replace(
|
|
array('/', '*'),
|
|
'',
|
|
substr($reflectMethod->getDocComment(), 0, strpos($reflectMethod->getDocComment(), '@'))
|
|
)),
|
|
));
|
|
}
|
|
}
|
|
return $routes;
|
|
}
|
|
|
|
public function execute()
|
|
{
|
|
$this->sendCORS();
|
|
$this->parseRequest();
|
|
}
|
|
|
|
public function action()
|
|
{}
|
|
|
|
/**
|
|
* 发送跨域 HEADER
|
|
*
|
|
* @return void
|
|
*/
|
|
private function sendCORS()
|
|
{
|
|
$httpOrigin = $this->request->getServer('HTTP_ORIGIN');
|
|
$allowedHttpOrigins = explode("\n", str_replace("\r", "", $this->config->origin));
|
|
|
|
if (!$httpOrigin) {
|
|
return;
|
|
}
|
|
|
|
if (in_array($httpOrigin, $allowedHttpOrigins)) {
|
|
$this->response->setHeader('Access-Control-Allow-Origin', $httpOrigin);
|
|
}
|
|
|
|
if (strtolower($this->request->getServer('REQUEST_METHOD')) == 'options') {
|
|
Typecho_Response::setStatus(204);
|
|
$this->response->setHeader('Access-Control-Allow-Headers', 'Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type');
|
|
$this->response->setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 解析请求参数
|
|
*
|
|
* @return void
|
|
*/
|
|
private function parseRequest()
|
|
{
|
|
if ($this->request->isPost()) {
|
|
$data = file_get_contents('php://input');
|
|
$data = json_decode($data, true);
|
|
if (json_last_error() != JSON_ERROR_NONE) {
|
|
$this->throwError('Parse JSON error');
|
|
}
|
|
$this->httpParams = $data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取 GET/POST 参数
|
|
*
|
|
* @param string $key 目标参数的 key
|
|
* @param mixed $default 返回的默认值
|
|
* @return mixed
|
|
*/
|
|
private function getParams($key, $default = null)
|
|
{
|
|
if ($this->request->isGet()) {
|
|
return $this->request->get($key, $default);
|
|
}
|
|
if (!isset($this->httpParams[$key])) {
|
|
return $default;
|
|
}
|
|
return $this->httpParams[$key];
|
|
}
|
|
|
|
/**
|
|
* 以 JSON 格式返回错误
|
|
*
|
|
* @param string $message 错误信息
|
|
* @param integer $status HTTP 状态码
|
|
* @return void
|
|
*/
|
|
private function throwError($message = 'unknown', $status = 400)
|
|
{
|
|
Typecho_Response::setStatus($status);
|
|
$this->response->throwJson(array(
|
|
'status' => 'error',
|
|
'message' => $message,
|
|
'data' => null,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 以 JSON 格式响应请求的信息
|
|
*
|
|
* @param mixed $data 要返回给用户的内容
|
|
* @return void
|
|
*/
|
|
private function throwData($data)
|
|
{
|
|
$this->response->throwJson(array(
|
|
'status' => 'success',
|
|
'message' => '',
|
|
'data' => $data,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 锁定 API 请求方式
|
|
*
|
|
* @param string $method 请求方式 (get/post)
|
|
* @return void
|
|
*/
|
|
private function lockMethod($method)
|
|
{
|
|
$method = strtolower($method);
|
|
if (strtolower($this->request->getServer('REQUEST_METHOD')) != $method) {
|
|
$this->throwError('method not allowed', 405);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* show errors when accessing a disabled API
|
|
*
|
|
* @param string $route
|
|
* @return void
|
|
*/
|
|
private function checkState($route)
|
|
{
|
|
$state = $this->config->$route;
|
|
if (!$state) {
|
|
$this->throwError('This API has been disabled.', 403);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取文章自定义字段内容
|
|
*
|
|
* @param int $cid
|
|
* @return array
|
|
*/
|
|
public function getCustomFields($cid)
|
|
{
|
|
$cfg = $this->config->fieldsPrivacy;
|
|
$filters = empty($cfg) ? array() : explode(',', $cfg);
|
|
|
|
$query = $this->db->select('*')->from('table.fields')
|
|
->where('cid = ?', $cid);
|
|
$rows = $this->db->fetchAll($query);
|
|
$result = array();
|
|
if (count($rows) > 0) {
|
|
foreach ($rows as $key => $value) {
|
|
if (in_array($value['name'], $filters)) {
|
|
continue;
|
|
}
|
|
$type = $value['type'];
|
|
$result[$value['name']] = array(
|
|
"name" => $value['name'],
|
|
"type" => $value['type'],
|
|
"value" => $value[$value['type'] . '_value'],
|
|
);
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 获取文章列表、搜索文章的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function postsAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('posts');
|
|
|
|
$pageSize = $this->getParams('pageSize', 5);
|
|
$page = $this->getParams('page', 1);
|
|
$page = is_numeric($page) ? $page : 1;
|
|
$offset = $pageSize * ($page - 1);
|
|
|
|
$filterType = trim($this->getParams('filterType', ''));
|
|
$filterSlug = trim($this->getParams('filterSlug', ''));
|
|
$showContent = trim($this->getParams('showContent', '')) === 'true';
|
|
$showDigest = trim($this->getParams('showDigest', ''));
|
|
|
|
if (in_array($filterType, array('category', 'tag', 'search'))) {
|
|
if ($filterSlug == '') {
|
|
$this->throwError('filter slug is empty');
|
|
}
|
|
|
|
if ($filterType != 'search') {
|
|
$select = $this->db->select('mid')
|
|
->from('table.metas')
|
|
->where('type = ?', $filterType)
|
|
->where('slug = ?', $filterSlug);
|
|
|
|
$row = $this->db->fetchRow($select);
|
|
if (!isset($row['mid'])) {
|
|
$this->throwError('unknown slug name');
|
|
}
|
|
$mid = $row['mid'];
|
|
$select = $this->db->select('cid')->from('table.relationships')
|
|
->where('mid = ?', $mid);
|
|
|
|
$cids = $this->db->fetchAll($select);
|
|
|
|
if (count($cids) == 0) {
|
|
$this->throwData(array(
|
|
'page' => (int) $page,
|
|
'pageSize' => (int) $pageSize,
|
|
'pages' => 0,
|
|
'count' => 0,
|
|
'dataSet' => array(),
|
|
));
|
|
} else {
|
|
foreach ($cids as $key => $cid) {
|
|
$cids[$key] = $cids[$key]['cid'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$select = $this->db
|
|
->select('cid', 'title', 'created', 'modified', 'slug', 'commentsNum', 'text', 'type')
|
|
->from('table.contents')
|
|
->where('type = ?', 'post')
|
|
->where('status = ?', 'publish')
|
|
->where('created < ?', time())
|
|
->where('password IS NULL')
|
|
->order('created', Typecho_Db::SORT_DESC);
|
|
if (isset($cids)) {
|
|
$cidStr = implode(',', $cids);
|
|
$select->where('cid IN (' . $cidStr . ')');
|
|
} elseif ($filterType == 'search') {
|
|
// Widget_Archive::searchHandle()
|
|
$searchQuery = '%' . str_replace(' ', '%', $filterSlug) . '%';
|
|
$select->where('title LIKE ? OR text LIKE ?', $searchQuery, $searchQuery);
|
|
}
|
|
|
|
$count = count($this->db->fetchAll($select));
|
|
$select->offset($offset)
|
|
->limit($pageSize);
|
|
$result = $this->db->fetchAll($select);
|
|
foreach ($result as $key => $value) {
|
|
// digest has two types
|
|
if ($showDigest === 'more') {
|
|
// if you use 'more', plugin will truncate from <!--more-->
|
|
$result[$key]['digest'] = str_replace(
|
|
"<!--markdown-->", "",
|
|
explode("<!--more-->", $result[$key]['text'])[0]
|
|
);
|
|
|
|
$result[$key] = $this->filter($result[$key]);
|
|
} else if ($showDigest === 'excerpt') {
|
|
// if you use 'excerpt', plugin will truncate for certain number of text
|
|
$limit = (int) trim($this->getParams('limit', '200'));
|
|
$result[$key] = $this->filter($result[$key]);
|
|
$result[$key]['digest'] = mb_substr(
|
|
htmlspecialchars_decode(strip_tags($result[$key]['text'])),
|
|
0,
|
|
$limit,
|
|
'utf-8'
|
|
) . "...";
|
|
} else {
|
|
$result[$key] = $this->filter($result[$key]);
|
|
}
|
|
|
|
|
|
if (!$showContent) {
|
|
unset($result[$key]['text']);
|
|
}
|
|
}
|
|
|
|
$this->throwData(array(
|
|
'page' => (int) $page,
|
|
'pageSize' => (int) $pageSize,
|
|
'pages' => ceil($count / $pageSize),
|
|
'count' => $count,
|
|
'dataSet' => $result,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取页面列表的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function pagesAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('pages');
|
|
|
|
$select = $this->db
|
|
->select('cid', 'title', 'created', 'slug')
|
|
->from('table.contents')
|
|
->where('type = ?', 'page')
|
|
->where('status = ?', 'publish')
|
|
->where('created < ?', time())
|
|
->where('password IS NULL')
|
|
->order('order', Typecho_Db::SORT_ASC);
|
|
|
|
$result = $this->db->fetchAll($select);
|
|
$count = count($result);
|
|
|
|
$this->throwData(array(
|
|
'count' => $count,
|
|
'dataSet' => $result,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取分类列表的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function categoriesAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('categories');
|
|
$categories = $this->widget('Widget_Metas_Category_List');
|
|
|
|
if (isset($categories->stack)) {
|
|
$this->throwData($categories->stack);
|
|
} else {
|
|
$reflect = new ReflectionObject($categories);
|
|
$map = $reflect->getProperty('_map');
|
|
$map->setAccessible(true);
|
|
$this->throwData(array_merge($map->getValue($categories)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取标签列表的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function tagsAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('tags');
|
|
|
|
$this->widget('Widget_Metas_Tag_Cloud')->to($tags);
|
|
|
|
if ($tags->have()) {
|
|
while ($tags->next()) {
|
|
$this->throwData($tags->stack);
|
|
}
|
|
}
|
|
|
|
$this->throwError('no tag', 404);
|
|
}
|
|
|
|
/**
|
|
* 获取文章、独立页面详情的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function postAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('post');
|
|
|
|
$slug = $this->getParams('slug', '');
|
|
$cid = $this->getParams('cid', '');
|
|
|
|
$select = $this->db
|
|
->select('title','cid', 'created', 'type', 'slug', 'commentsNum', 'text')
|
|
->from('table.contents')
|
|
->where('password IS NULL');
|
|
|
|
if (is_numeric($cid)) {
|
|
$select->where('cid = ?', $cid);
|
|
} else {
|
|
$select->where('slug = ?', $slug);
|
|
}
|
|
|
|
$result = $this->db->fetchRow($select);
|
|
if (count($result) != 0) {
|
|
$result = $this->filter($result);
|
|
$result['csrfToken'] = $this->generateCsrfToken($result['permalink']);
|
|
$this->throwData($result);
|
|
} else {
|
|
$this->throwError('post not exists', 404);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取最新(最近)评论的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function recentCommentsAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('recentComments');
|
|
|
|
$size = $this->getParams('size', 9);
|
|
$query = $this->db
|
|
->select('coid', 'cid', 'author', 'text')
|
|
->from('table.comments')
|
|
->where('type = ? AND status = ?', 'comment', 'approved')
|
|
->order('created', Typecho_Db::SORT_DESC)
|
|
->limit($size);
|
|
$result = $this->db->fetchAll($query);
|
|
|
|
$this->throwData(array(
|
|
'count' => count($result),
|
|
'dataSet' => $result
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取文章、独立页面评论列表的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function commentsAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('comments');
|
|
|
|
$pageSize = $this->getParams('pageSize', 5);
|
|
$page = $this->getParams('page', 1);
|
|
$page = is_numeric($page) ? $page : 1;
|
|
$offset = $pageSize * ($page - 1);
|
|
$slug = $this->getParams('slug', '');
|
|
$cid = $this->getParams('cid', '');
|
|
$order = strtolower($this->getParams('order', ''));
|
|
|
|
// 为带 cookie 请求的用户显示正在等待审核的评论
|
|
$author = Typecho_Cookie::get('__typecho_remember_author');
|
|
$mail = Typecho_Cookie::get('__typecho_remember_mail');
|
|
|
|
if (empty($cid) && empty($slug)) {
|
|
$this->throwError('No specified posts.', 404);
|
|
}
|
|
|
|
$select = $this->db
|
|
->select('table.comments.coid', 'table.comments.parent', 'table.comments.cid', 'table.comments.created', 'table.comments.author', 'table.comments.mail', 'table.comments.url', 'table.comments.text', 'table.comments.status')
|
|
->from('table.comments')
|
|
->join('table.contents', 'table.comments.cid = table.contents.cid', Typecho_Db::LEFT_JOIN)
|
|
->where('table.comments.type = ?', 'comment')
|
|
->group('table.comments.coid')
|
|
->order('table.comments.created', $order === 'asc' ? Typecho_Db::SORT_ASC : Typecho_Db::SORT_DESC);
|
|
|
|
if (empty($author)) {
|
|
$select->where('table.comments.status = ?', 'approved');
|
|
} else {
|
|
$select
|
|
->where('table.comments.status = ? OR (table.comments.author = ? AND table.comments.mail = ?)', 'approved', $author, $mail);
|
|
}
|
|
|
|
if (is_numeric($cid)) {
|
|
$select->where('table.comments.cid = ?', $cid);
|
|
} else {
|
|
$select->where('table.contents.slug = ?', $slug);
|
|
}
|
|
|
|
$result = $this->db->fetchAll($select);
|
|
|
|
if (count($result) <= 0) {
|
|
$count = 0;
|
|
$finalResult = array();
|
|
} else {
|
|
$newResult = $this->buildNodes($result);
|
|
$count = count($newResult);
|
|
|
|
$finalResult = array_slice($newResult, $offset, $pageSize);
|
|
}
|
|
|
|
$this->throwData(array(
|
|
'page' => (int) $page,
|
|
'pageSize' => (int) $pageSize,
|
|
'pages' => ceil($count / $pageSize),
|
|
'count' => $count,
|
|
'dataSet' => $finalResult,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 发表评论的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function commentAction()
|
|
{
|
|
$this->lockMethod('post');
|
|
$this->checkState('comment');
|
|
|
|
$slug = $this->getParams('slug', '');
|
|
$cid = $this->getParams('cid', '');
|
|
$token = $this->getParams('token', '');
|
|
|
|
// administrator
|
|
$uid = $this->getParams('uid', null);
|
|
$authCode = $this->getParams('authCode', null);
|
|
$cookie = Typecho_Cookie::get('__typecho_uid');
|
|
if (!empty($cookie)) {
|
|
$uid = $cookie;
|
|
$authCode = Typecho_Cookie::get('__typecho_authCode');
|
|
}
|
|
|
|
$select = $this->db->select('cid', 'created', 'type', 'slug', 'commentsNum', 'text')
|
|
->from('table.contents')
|
|
->where('password IS NULL');
|
|
|
|
if (is_numeric($cid)) {
|
|
$select->where('cid = ?', $cid);
|
|
} else {
|
|
$select->where('slug = ?', $slug);
|
|
}
|
|
|
|
$result = $this->db->fetchRow($select);
|
|
if (count($result) != 0) {
|
|
$result = $this->filter($result);
|
|
} else {
|
|
$this->throwError('post not exists', 404);
|
|
}
|
|
|
|
if (!$this->checkCsrfToken($result['permalink'], $token)) {
|
|
$this->throwError('token invalid');
|
|
}
|
|
|
|
$commentUrl = Typecho_Router::url(
|
|
'feedback',
|
|
array('type' => 'comment', 'permalink' => $result['pathinfo']),
|
|
$this->options->index
|
|
);
|
|
if (defined('IN_PHPUNIT_SERVER')) {
|
|
$commentUrl = str_replace(':' . WEB_SERVER_PORT, ':' . FORKED_WEB_SERVER_PORT, $commentUrl);
|
|
}
|
|
|
|
$postData = empty($authCode) ? array(
|
|
'text' => $this->getParams('text', ''),
|
|
'author' => $this->getParams('author', ''),
|
|
'mail' => $this->getParams('mail', ''),
|
|
'url' => $this->getParams('url', ''),
|
|
) : array(
|
|
'text' => $this->getParams('text'),
|
|
);
|
|
|
|
// Typecho 0.9- has no anti-spam security
|
|
if (file_exists(__TYPECHO_ROOT_DIR__ . '/var/Widget/Security.php')) {
|
|
$postData['_'] = $this->widget('Widget_Security')->getToken($result['permalink']);
|
|
}
|
|
|
|
$parent = $this->getParams('parent', '');
|
|
if (is_numeric($parent)) {
|
|
$postData['parent'] = $parent;
|
|
}
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $commentUrl);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
|
'X-TYPECHO-RESTFUL-IP: ' . $this->request->getIp(),
|
|
));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, $this->request->getAgent());
|
|
curl_setopt($ch, CURLOPT_REFERER, $result['permalink']);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // no verify ssl
|
|
|
|
if (!empty($authCode)) {
|
|
$cookiePrefix = Typecho_Cookie::getPrefix();
|
|
$cookieText = $cookiePrefix . '__typecho_uid=' . $uid . '; ' . $cookiePrefix . '__typecho_authCode=' . $authCode . ';';
|
|
curl_setopt($ch, CURLOPT_COOKIE, $cookieText);
|
|
}
|
|
|
|
$data = curl_exec($ch);
|
|
|
|
if (curl_error($ch)) {
|
|
$this->throwError('comment failed');
|
|
}
|
|
|
|
curl_close($ch);
|
|
|
|
preg_match('!(<title[^>]*>)(.*)(</title>)!i', $data, $matches);
|
|
if (isset($matches[2]) && $matches[2] == 'Error') {
|
|
preg_match('/<div class=\"container\">(.*?)<\/div>/s', $data, $matches);
|
|
if (isset($matches[1])) {
|
|
$this->throwError(trim($matches[1]));
|
|
}
|
|
}
|
|
|
|
$query = $this->db->select('coid', 'status')
|
|
->from('table.comments')
|
|
->where('text = ?', $text)
|
|
->order('created', Typecho_Db::SORT_DESC);
|
|
$res = $this->db->fetchRow($query);
|
|
|
|
$this->throwData($res);
|
|
}
|
|
|
|
/**
|
|
* 获取设置项的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function settingsAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('settings');
|
|
|
|
$key = trim($this->getParams('key', ''));
|
|
$allowed = array_merge(explode(',', $this->config->allowedOptions), array(
|
|
'title', 'description', 'keywords', 'timezone',
|
|
));
|
|
|
|
if (!empty($key)) {
|
|
if (in_array($key, $allowed)) {
|
|
$query = $this->db->select('*')
|
|
->from('table.options')
|
|
->where('name = ?', $key);
|
|
$this->throwData($this->db->fetchAll($query));
|
|
} else {
|
|
$this->throwError('The options key you requested is therefore not allowed.', 403);
|
|
}
|
|
}
|
|
|
|
$this->throwData(array(
|
|
'title' => $this->options->title,
|
|
'description' => $this->options->description,
|
|
'keywords' => $this->options->keywords,
|
|
'timezone' => $this->options->timezone,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 获取作者信息和作者文章的接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function usersAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('users');
|
|
|
|
$uid = $this->getParams('uid', '');
|
|
$name = $this->getParams('name', '');
|
|
|
|
$select = $this->db->select('uid', 'mail', 'url', 'screenName')
|
|
->from('table.users');
|
|
if (!empty($uid)) {
|
|
$select->where('uid = ?', $uid);
|
|
} elseif (!empty($name)) {
|
|
$select->where('name = ? OR screenName = ?', $name, $name);
|
|
}
|
|
|
|
$result = $this->db->fetchAll($select);
|
|
$users = array();
|
|
foreach ($result as $key => $value) {
|
|
$postSelector = $this->db->select('cid', 'title', 'slug', 'created', 'modified', 'type')
|
|
->from('table.contents')
|
|
->where('status = ?', 'publish')
|
|
->where('password IS NULL')
|
|
->where('type = ?', 'post')
|
|
->where('authorId = ?', $value['uid']);
|
|
$posts = $this->db->fetchAll($postSelector);
|
|
foreach ($posts as $postNumber => $post) {
|
|
$posts[$postNumber] = $this->filter($post);
|
|
}
|
|
|
|
array_push($users, array(
|
|
"uid" => $value['uid'],
|
|
"name" => $value['screenName'],
|
|
"mailHash" => md5($value['mail']),
|
|
"url" => $value['url'],
|
|
"count" => count($posts),
|
|
"posts" => $posts,
|
|
));
|
|
}
|
|
|
|
$this->throwData(array(
|
|
"count" => count($users),
|
|
"dataSet" => $users,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 归档页面接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function archivesAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
$this->checkState('archives');
|
|
$showContent = trim($this->getParams('showContent', '')) === 'true';
|
|
$order = strtolower($this->getParams('order', 'desc'));
|
|
$showDigest = trim($this->getParams('showDigest', ''));
|
|
|
|
$select = $this->db->select('cid', 'title', 'slug', 'created', 'modified', 'type', 'text')
|
|
->from('table.contents')
|
|
->where('status = ?', 'publish')
|
|
->where('password IS NULL')
|
|
->where('type = ?', 'post')
|
|
->order('created', $order === 'asc' ? Typecho_Db::SORT_ASC : Typecho_Db::SORT_DESC);
|
|
$posts = $this->db->fetchAll($select);
|
|
|
|
$archives = array();
|
|
$created = array();
|
|
foreach ($posts as $key => $post) {
|
|
// digest related
|
|
if ($showDigest === 'more') {
|
|
$post['digest'] = str_replace(
|
|
"<!--markdown-->", "",
|
|
explode("<!--more-->", $post['text'])[0]
|
|
);
|
|
|
|
$post = $this->filter($post);
|
|
}
|
|
else if ($showDigest === 'excerpt') {
|
|
$limit = (int) trim($this->getParams('limit', '200'));
|
|
$post = $this->filter($post);
|
|
$post['digest'] = mb_substr(
|
|
htmlspecialchars_decode(strip_tags($post['text'])),
|
|
0,
|
|
$limit,
|
|
'utf-8'
|
|
) . "...";
|
|
}
|
|
else {
|
|
$post = $this->filter($post);
|
|
}
|
|
|
|
if (!$showContent) {
|
|
unset($post['text']);
|
|
}
|
|
|
|
$date = $post['created'];
|
|
$year = date('Y', $date);
|
|
$month = date('m', $date);
|
|
$archives[$year] = isset($archives[$year]) ? $archives[$year] : array();
|
|
$archives[$year][$month] = isset($archives[$year][$month])
|
|
? $archives[$year][$month]
|
|
: array();
|
|
array_push($archives[$year][$month], $post);
|
|
}
|
|
|
|
// sort by date descend / ascend
|
|
if ($order !== 'asc') {
|
|
krsort($archives, SORT_NUMERIC);
|
|
foreach ($archives as $archive) {
|
|
krsort($archive, SORT_NUMERIC);
|
|
}
|
|
} else {
|
|
ksort($archives, SORT_NUMERIC);
|
|
foreach ($archives as $archive) {
|
|
ksort($archive, SORT_NUMERIC);
|
|
}
|
|
}
|
|
|
|
$this->throwData(array(
|
|
"count" => count($posts),
|
|
"dataSet" => $archives,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 插件更新接口
|
|
*
|
|
* @return void
|
|
*/
|
|
public function upgradeAction()
|
|
{
|
|
$this->lockMethod('get');
|
|
|
|
$isAdmin = call_user_func(function () {
|
|
$hasLogin = $this->widget('Widget_User')->hasLogin();
|
|
$isAdmin = false;
|
|
if (!$hasLogin) {
|
|
return false;
|
|
}
|
|
$isAdmin = $this->widget('Widget_User')->pass('administrator', true);
|
|
return $isAdmin;
|
|
}, $this);
|
|
|
|
if (!$isAdmin) {
|
|
$this->throwError('must be admin');
|
|
}
|
|
|
|
$localPluginPath = __DIR__ . '/Plugin.php';
|
|
$localActionPath = __DIR__ . '/Action.php';
|
|
$localPluginContent = file_get_contents($localPluginPath);
|
|
$localActionContent = file_get_contents($localActionPath);
|
|
|
|
$remotePluginContent = file_get_contents('https://raw.githubusercontent.com/moefront/typecho-plugin-Restful/master/Plugin.php');
|
|
$remoteActionContent = file_get_contents('https://raw.githubusercontent.com/moefront/typecho-plugin-Restful/master/Action.php');
|
|
|
|
if (!$remotePluginContent || !$remoteActionContent) {
|
|
$this->throwError('unable to connect to GitHub');
|
|
}
|
|
|
|
if (md5($localPluginContent) != md5($remotePluginContent)
|
|
|| md5($localActionContent) != md5($remoteActionContent)) {
|
|
if (file_put_contents($localPluginPath, $remotePluginContent)
|
|
&& file_put_contents($localActionPath, $remoteActionContent)) {
|
|
$this->throwData(null);
|
|
} else {
|
|
$this->throwError('upgrade failed');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 构造文章评论关系树
|
|
*
|
|
* @param array $raw 评论的集合
|
|
* @return array 返回构造后的评论关系数组
|
|
*/
|
|
private function buildNodes($comments)
|
|
{
|
|
$childMap = array();
|
|
$parentMap = array();
|
|
$tree = array();
|
|
|
|
foreach ($comments as $index => $comment) {
|
|
$comments[$index]['mailHash'] = md5($comment['mail']);
|
|
unset($comments[$index]['mail']); // avoid exposing users' email to public
|
|
|
|
$parent = (int) $comment['parent'];
|
|
if ($parent !== 0) {
|
|
if (!isset($childMap[$parent])) {
|
|
$childMap[$parent] = array();
|
|
}
|
|
array_push($childMap[$parent], $index);
|
|
} else {
|
|
array_push($parentMap, $index);
|
|
}
|
|
}
|
|
|
|
$tree = $this->recursion($comments, $parentMap, $childMap);
|
|
return $tree;
|
|
}
|
|
|
|
/**
|
|
* 通过递归构建评论父子关系
|
|
*
|
|
* @param array $comments 评论集合
|
|
* @param array $parents 父评论的 key 集合
|
|
* @param array $map 子评论与父评论的映射关系
|
|
* @return array 返回处理后的结果集合
|
|
*/
|
|
private function recursion($comments, $parents, $map)
|
|
{
|
|
$result = array();
|
|
|
|
foreach ($parents as $parent) {
|
|
$item = &$comments[$parent];
|
|
$coid = (int) $item['coid'];
|
|
if (isset($map[$coid])) {
|
|
$item['children'] = $this->recursion($comments, $map[$coid], $map);
|
|
} else {
|
|
$item['children'] = array();
|
|
}
|
|
array_push($result, $item);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Safely parse markdown into HTML
|
|
* @param string|null $content
|
|
* @return string
|
|
*/
|
|
private function safelyParseMarkdown($content) {
|
|
if (is_null($content) || empty($content)) {
|
|
return '';
|
|
}
|
|
$contentWidget = $this->widget('Widget_Abstract_Contents');
|
|
return method_exists($contentWidget, 'markdown')
|
|
? $contentWidget->markdown($content)
|
|
: MarkdownExtraExtended::defaultTransform($content);
|
|
}
|
|
|
|
/**
|
|
* 过滤和补全文章数组
|
|
*
|
|
* @param array $value 文章详细信息数组
|
|
* @return array
|
|
*/
|
|
private function filter($value)
|
|
{
|
|
$contentWidget = $this->widget('Widget_Abstract_Contents');
|
|
$value['text'] = isset($value['text']) ? $value['text'] : null;
|
|
$value['digest'] = isset($value['digest']) ? $value['digest'] : null;
|
|
|
|
$value['password'] = ''; // typecho:#94ddb69 compat
|
|
|
|
$value['type'] = isset($value['type']) ? $value['type'] : null; // Typecho 0.9 compatibility
|
|
$value = $contentWidget->filter($value);
|
|
$value['text'] = $this->safelyParseMarkdown($value['text']);
|
|
$value['digest'] = $this->safelyParseMarkdown($value['text']);
|
|
|
|
if ($value['type'] === null) {
|
|
unset($value['type']);
|
|
}
|
|
if (empty(trim($value['text']))) {
|
|
unset($value['text']);
|
|
}
|
|
if (empty(trim($value['digest']))) {
|
|
unset($value['digest']);
|
|
}
|
|
// Custom fields
|
|
$value['fields'] = $this->getCustomFields($value['cid']);
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* 生成 CSRF Token
|
|
*
|
|
* @param mixed $key
|
|
* @return string
|
|
*/
|
|
private function generateCsrfToken($key)
|
|
{
|
|
return base64_encode(
|
|
hash_hmac(
|
|
'sha256',
|
|
hash_hmac(
|
|
'sha256',
|
|
date('Ymd') . $this->request->getServer('REMOTE_ADDR') . $this->request->getServer('HTTP_USER_AGENT'),
|
|
hash('sha256', $key, true),
|
|
true
|
|
),
|
|
$this->config->csrfSalt,
|
|
true
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 检查 CSRF Token 是否匹配
|
|
*
|
|
* @param mixed $key
|
|
* @param mixed $token
|
|
* @return boolean
|
|
*/
|
|
private function checkCsrfToken($key, $token)
|
|
{
|
|
return hash_equals($token, $this->generateCsrfToken($key));
|
|
}
|
|
}
|