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 $result[$key]['digest'] = str_replace( "", "", explode("", $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('!(