model = new Model(); $this->payload = $this->readPayload(); $this->assertApiAccess(); } public function ping() { $this->success(array( 'cms_type' => self::CMS_TYPE, 'plugin_version' => self::PLUGIN_VERSION, 'site_url' => $this->siteUrl(), 'site_name' => $this->config('cmsname') ?: 'PBootCMS', 'api_auth' => (bool) $this->config('api_auth'), 'time' => date('Y-m-d H:i:s') )); } public function categories() { $acode = $this->param('acode', $this->defaultAcode()); $mcode = $this->param('mcode', $this->param('model_hash', '2')); if (! $this->isSafeCode($acode) || ! $this->isSafeCode($mcode)) { $this->fail(1400, 'invalid acode or mcode'); } $rows = $this->model->table('ay_content_sort') ->where("acode='" . $this->sqlValue($acode) . "'") ->where("mcode='" . $this->sqlValue($mcode) . "'") ->where('status=1') ->field('scode,pcode,mcode,name,filename,sorting') ->order('sorting ASC,id ASC') ->select(); $items = array(); if ($rows) { foreach ($rows as $row) { $items[] = array( 'id' => (string) $row->scode, 'scode' => (string) $row->scode, 'parent_id' => (string) $row->pcode, 'mcode' => (string) $row->mcode, 'title' => (string) $row->name, 'name' => (string) $row->name, 'filename' => (string) $row->filename, 'sorting' => (int) $row->sorting ); } } $this->success($items); } public function publish() { $data = $this->buildContentData(false); $id = $this->model->table('ay_content')->autoTime()->insertGetId($this->filterContentFields($data)); if (! $id) { $this->fail(1403, 'insert ay_content error', '文章发布失败'); } $this->clearRuntimeCache(); $this->success(array( 'id' => (int) $id, 'cms_type' => self::CMS_TYPE, 'status' => (int) $data['status'], 'url' => $this->contentUrl( $id, $this->param('scode', $this->param('category_id', '')), $this->param('filename', '') ) ), 'published'); } public function update() { $id = (int) $this->param('id', 0); if ($id <= 0) { $this->fail(1404, 'id is empty', '文章 ID 不能为空'); } $current = $this->model->table('ay_content')->where('id=' . $id)->find(); if (! $current) { $this->fail(1404, 'content not found', '文章不存在'); } $data = $this->buildContentData(true, $current); $ok = $this->model->table('ay_content')->where('id=' . $id)->autoTime()->update($this->filterContentFields($data)); if ($ok === false) { $this->fail(1403, 'update ay_content error', '文章更新失败'); } $this->clearRuntimeCache(); $this->success(array( 'id' => $id, 'cms_type' => self::CMS_TYPE, 'status' => (int) $data['status'], 'url' => $this->contentUrl( $id, $this->param('scode', $this->param('category_id', $current->scode)), $this->param('filename', $current->filename) ) ), 'updated'); } public function status() { $id = (int) $this->param('id', 0); if ($id <= 0) { $this->fail(1404, 'id is empty', '文章 ID 不能为空'); } $row = $this->model->table('ay_content')->where('id=' . $id)->find(); if (! $row) { $this->fail(1404, 'content not found', '文章不存在'); } $this->success(array( 'id' => (int) $row->id, 'title' => (string) $row->title, 'scode' => (string) $row->scode, 'status' => (int) $row->status, 'date' => (string) $row->date, 'url' => $this->contentUrl($row->id, $row->scode, $row->filename) )); } // Backward compatible names used by older Aiseo-style plugins. public function categoryLists() { $this->categories(); } public function articleAdd() { $this->publish(); } private function buildContentData($isUpdate = false, $current = null) { $title = $this->param('title', $isUpdate && $current ? $current->title : ''); if (! $title) { $this->fail(1404, 'title is empty', '标题不能为空'); } $content = $this->param('content', $isUpdate && $current ? $current->content : ''); if (! $content) { $this->fail(1404, 'content is empty', '内容不能为空'); } $scode = $this->param('scode', $this->param('category_id', $isUpdate && $current ? $current->scode : '')); if (! $scode) { $this->fail(1404, 'scode is empty', '内容栏目编码不能为空'); } if (! $this->isSafeCode($scode)) { $this->fail(1400, 'invalid scode', '栏目编码不合法'); } $sort = $this->getSort($scode); if (! $sort) { $this->fail(1404, 'sort not found', '栏目不存在或不可用'); } $content = $this->localizeContentImages($content); $ico = $this->param('ico', $isUpdate && $current ? $current->ico : ''); if ($ico && ($this->isRemoteUrl($ico) || $this->isDataImageUrl($ico))) { $ico = $this->localizeImage($ico); } $status = $this->param('status', $isUpdate && $current ? $current->status : 1); if ($status === '' || $status === null) { $status = 1; } return array( 'acode' => $this->sqlValue($this->param('acode', $isUpdate && $current ? $current->acode : $this->defaultAcode())), 'scode' => $this->sqlValue($scode), 'subscode' => $this->sqlValue($this->param('subscode', $isUpdate && $current ? $current->subscode : '')), 'title' => $this->sqlValue($title), 'titlecolor' => $this->sqlValue($this->param('titlecolor', $isUpdate && $current ? $current->titlecolor : '#333333')), 'subtitle' => $this->sqlValue($this->param('subtitle', $isUpdate && $current ? $current->subtitle : '')), 'filename' => $this->sqlValue($this->param('filename', $isUpdate && $current ? $current->filename : '')), 'author' => $this->sqlValue($this->param('author', $isUpdate && $current ? $current->author : 'Geo SaaS')), 'source' => $this->sqlValue($this->param('source', $isUpdate && $current ? $current->source : 'Geo SaaS')), 'outlink' => $this->sqlValue($this->param('outlink', $isUpdate && $current ? $current->outlink : '')), 'date' => $this->sqlValue($this->normalizeDate($this->param('date', $isUpdate && $current ? $current->date : ''))), 'ico' => $this->sqlValue($ico), 'pics' => $this->sqlValue($this->normalizePics($this->param('pics', $isUpdate && $current ? $current->pics : ''))), 'content' => $this->sqlValue($content), 'tags' => $this->sqlValue($this->param('tags', $isUpdate && $current ? $current->tags : '')), 'enclosure' => $this->sqlValue($this->param('enclosure', $isUpdate && $current ? $current->enclosure : '')), 'keywords' => $this->sqlValue($this->param('keywords', $isUpdate && $current ? $current->keywords : '')), 'description' => $this->sqlValue($this->param('description', $isUpdate && $current ? $current->description : $this->summary($content))), 'sorting' => (int) $this->param('sorting', $isUpdate && $current ? $current->sorting : 255), 'status' => (int) $status, 'istop' => (int) $this->param('istop', $isUpdate && $current ? $current->istop : 0), 'isrecommend' => (int) $this->param('isrecommend', $isUpdate && $current ? $current->isrecommend : 0), 'isheadline' => (int) $this->param('isheadline', $isUpdate && $current ? $current->isheadline : 0), 'visits' => $isUpdate && $current ? (int) $current->visits : 0, 'likes' => $isUpdate && $current ? (int) $current->likes : 0, 'oppose' => $isUpdate && $current ? (int) $current->oppose : 0, 'create_user' => $this->sqlValue($isUpdate && $current ? $current->create_user : 'geo_saas'), 'update_user' => $this->sqlValue('geo_saas'), 'gid' => (int) $this->param('gid', $isUpdate && $current ? $current->gid : 0), 'gtype' => (int) $this->param('gtype', $isUpdate && $current ? $current->gtype : 4), 'gnote' => $this->sqlValue($this->param('gnote', $isUpdate && $current ? $current->gnote : '')), 'picstitle' => $this->sqlValue($this->param('picstitle', $isUpdate && $current ? $current->picstitle : '')) ); } private function getSort($scode) { return $this->model->table('ay_content_sort a') ->field('a.*,m.urlname,m.type') ->join(array( array('ay_model m', 'a.mcode=m.mcode', 'LEFT') )) ->where("a.scode='" . $this->sqlValue($scode) . "'") ->where('a.status=1') ->find(); } private function contentUrl($id, $scode, $filename = '') { $sort = $this->getSort($scode); if (! $sort) { return $this->siteUrl(); } try { if (class_exists('\\app\\home\\controller\\ParserController')) { $parserClass = '\\app\\home\\controller\\ParserController'; $parser = new $parserClass(); $link = $parser->parserLink( $sort->type, $sort->urlname, 'content', $scode, $sort->filename, $id, $filename ); return $this->absoluteUrl($link); } } catch (\Exception $e) { } $urlName = $sort->urlname ?: $sort->filename; if (! $urlName) { $urlName = $scode; } return $this->absoluteUrl('/?' . $urlName . '/' . $id . '.html'); } private function filterContentFields($data) { $fields = $this->contentFields(); if (! $fields) { return $data; } $filtered = array(); foreach ($data as $key => $value) { if (isset($fields[$key])) { $filtered[$key] = $value; } } return $filtered; } private function contentFields() { if ($this->contentFields !== null) { return $this->contentFields; } $this->contentFields = array(); try { $fields = $this->model->tableFields('ay_content'); if (is_array($fields)) { foreach ($fields as $field) { $field = trim((string) $field); if ($field !== '') { $this->contentFields[$field] = true; } } } } catch (\Exception $e) { $this->contentFields = array(); } return $this->contentFields; } private function readPayload() { $payload = array(); $raw = file_get_contents('php://input'); if ($raw) { $json = json_decode($raw, true); if (is_array($json)) { $payload = $json; } } foreach ($_REQUEST as $key => $value) { $payload[$key] = $value; } return $payload; } private function param($name, $default = '') { if (array_key_exists($name, $this->payload)) { return $this->payload[$name]; } return $default; } private function assertApiAccess() { $config = $this->config(); if (! isset($config['api_open']) || ! $config['api_open']) { $this->fail(1401, 'api is closed', '系统尚未开启 API 功能'); } if (! empty($config['api_auth'])) { if (empty($config['api_appid']) || empty($config['api_secret'])) { $this->fail(1401, 'api credential is not configured', '后台 API 认证配置不完整'); } $appid = $this->param('appid', ''); $timestamp = $this->param('timestamp', ''); $signature = $this->param('signature', ''); if (! $appid || ! $timestamp || ! $signature) { $this->fail(1401, 'missing appid timestamp or signature', '缺少 API 认证参数'); } if ((string) $appid !== (string) $config['api_appid']) { $this->fail(1401, 'invalid appid', 'API 用户不正确'); } if (! is_numeric($timestamp) || abs(time() - (int) $timestamp) > 300) { $this->fail(1401, 'timestamp expired', 'API 时间戳已过期'); } $expected = md5(md5($config['api_appid'] . $config['api_secret'] . $timestamp)); if (! $this->hashEquals($expected, $signature)) { $this->fail(1401, 'invalid signature', 'API 签名不正确'); } } } private function hashEquals($known, $user) { if (function_exists('hash_equals')) { return hash_equals((string) $known, (string) $user); } return (string) $known === (string) $user; } private function localizeContentImages($content) { return preg_replace_callback('/]*>/i', function ($imgMatch) { $tag = $imgMatch[0]; preg_match_all('/\\b(src|data-src|_src)=[\"\']([^\"\']+)[\"\']/i', $tag, $attrMatches, PREG_SET_ORDER); if (! $attrMatches) { return $tag; } $replacements = array(); foreach ($attrMatches as $attrMatch) { $url = trim($attrMatch[2]); if (! $this->isRemoteUrl($url) && ! $this->isDataImageUrl($url)) { continue; } if (! isset($replacements[$url])) { $replacements[$url] = $this->localizeImage($url); } } foreach ($replacements as $remote => $local) { $tag = str_replace($remote, $local, $tag); } return $tag; }, $content); } private function normalizePics($pics) { if (! $pics) { return ''; } if (is_array($pics)) { $items = $pics; } else { $items = explode(',', $pics); } $result = array(); foreach ($items as $item) { $item = trim((string) $item); if (! $item) { continue; } if ($this->isRemoteUrl($item)) { $item = $this->localizeImage($item); } elseif ($this->isDataImageUrl($item)) { $item = $this->localizeImage($item); } if ($item) { $result[] = $item; } } return implode(',', $result); } private function localizeImage($value) { if ($this->isDataImageUrl($value)) { $decoded = $this->decodeDataImage($value); return $this->saveImageData($decoded['data'], $decoded['ext']); } return $this->downloadImage($value); } private function downloadImage($url) { if (! $this->isRemoteUrl($url)) { return $url; } $data = $this->httpGet($url); if (! $data) { $this->fail(1405, 'download image failed: ' . $url, '图片下载失败'); } $path = parse_url($url, PHP_URL_PATH); if (! $path) { $path = ''; } $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (! in_array($ext, array('jpg', 'jpeg', 'png', 'gif', 'webp'))) { $ext = $this->imageExtensionFromData($data); } if (! $ext) { $ext = 'jpg'; } return $this->saveImageData($data, $ext); } private function saveImageData($data, $ext = '') { if (! $data || ! $this->isImageData($data, '')) { $this->fail(1405, 'invalid image data', '图片数据无效'); } if (! $ext || ! in_array(strtolower($ext), array('jpg', 'jpeg', 'png', 'gif', 'webp'))) { $ext = $this->imageExtensionFromData($data); } if (! $ext) { $ext = 'jpg'; } $relativeDir = '/static/upload/image/' . date('Ymd'); $absoluteDir = $this->rootPath() . $relativeDir; if (! is_dir($absoluteDir) && ! mkdir($absoluteDir, 0777, true)) { $this->fail(1405, 'create upload directory failed', '图片保存目录创建失败'); } $fileName = 'geo_' . date('His') . '_' . mt_rand(1000, 9999) . '.' . $ext; $absoluteFile = $absoluteDir . '/' . $fileName; $relativeFile = $relativeDir . '/' . $fileName; if (file_put_contents($absoluteFile, $data) === false) { $this->fail(1405, 'save image failed', '图片保存失败'); } return $relativeFile; } private function decodeDataImage($value) { if (! preg_match('/^data:image\\/(jpeg|jpg|png|gif|webp);base64,([A-Za-z0-9+\\/\\r\\n=]+)$/i', trim((string) $value), $matches)) { $this->fail(1405, 'invalid data image', '图片数据无效'); } $data = base64_decode(preg_replace('/\\s+/', '', $matches[2]), true); if (! $data) { $this->fail(1405, 'decode data image failed', '图片数据无效'); } $ext = strtolower($matches[1]); if ($ext === 'jpeg') { $ext = 'jpg'; } return array('data' => $data, 'ext' => $ext); } private function httpGet($url) { if (function_exists('curl_init')) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 8); curl_setopt($curl, CURLOPT_TIMEOUT, 20); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_USERAGENT, 'GeoPublisher/' . self::PLUGIN_VERSION); $data = curl_exec($curl); $code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE); $contentType = (string) curl_getinfo($curl, CURLINFO_CONTENT_TYPE); curl_close($curl); if ($code >= 200 && $code < 300 && $data && $this->isImageData($data, $contentType)) { return $data; } return false; } $context = stream_context_create(array( 'http' => array( 'method' => 'GET', 'timeout' => 20, 'header' => "User-Agent: GeoPublisher/" . self::PLUGIN_VERSION . "\r\n" ) )); $data = @file_get_contents($url, false, $context); return $data && $this->isImageData($data, '') ? $data : false; } private function isImageData($data, $contentType = '') { if ($contentType && stripos($contentType, 'image/') !== false) { return true; } if (function_exists('getimagesizefromstring')) { if (@getimagesizefromstring($data) !== false) { return true; } } return $this->isWebPData($data); } private function isWebPData($data) { return strlen($data) >= 12 && substr($data, 0, 4) === 'RIFF' && substr($data, 8, 4) === 'WEBP'; } private function imageExtensionFromData($data) { if ($this->isWebPData($data)) { return 'webp'; } if (! function_exists('getimagesizefromstring')) { return 'jpg'; } $info = @getimagesizefromstring($data); if (! $info || empty($info['mime'])) { return 'jpg'; } switch (strtolower($info['mime'])) { case 'image/jpeg': return 'jpg'; case 'image/png': return 'png'; case 'image/gif': return 'gif'; case 'image/webp': return 'webp'; default: return 'jpg'; } } private function rootPath() { if (defined('ROOT_PATH') && ROOT_PATH) { return rtrim(ROOT_PATH, '/\\'); } return rtrim(dirname(dirname(dirname(dirname(__FILE__)))), '/\\'); } private function normalizeDate($value) { if (! $value) { return date('Y-m-d H:i:s'); } if (is_numeric($value)) { return date('Y-m-d H:i:s', (int) $value); } $time = strtotime($value); return $time ? date('Y-m-d H:i:s', $time) : date('Y-m-d H:i:s'); } private function summary($content) { $text = strip_tags($content); if (function_exists('clear_html_blank')) { $text = clear_html_blank($text); } if (function_exists('substr_both')) { return substr_both($text, 0, 150); } if (function_exists('mb_substr')) { return mb_substr($text, 0, 150, 'UTF-8'); } return substr($text, 0, 150); } private function defaultAcode() { if (function_exists('get_default_lg')) { return get_default_lg(); } return 'cn'; } private function isSafeCode($value) { return (bool) preg_match('/^[A-Za-z0-9_\\-]+$/', (string) $value); } private function isRemoteUrl($url) { return preg_match('/^https?:\\/\\//i', (string) $url); } private function isDataImageUrl($url) { return preg_match('/^data:image\\/(jpeg|jpg|png|gif|webp);base64,/i', trim((string) $url)); } private function sqlValue($value) { if (is_array($value) || is_object($value)) { $value = json_encode($value); } $value = (string) $value; if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { $value = stripslashes($value); } return addslashes($value); } private function absoluteUrl($link) { if (preg_match('/^https?:\\/\\//i', $link)) { return $link; } $base = $this->siteUrl(); if (! $link) { return $base; } return rtrim($base, '/') . '/' . ltrim($link, '/'); } private function siteUrl() { if (function_exists('get_http_url')) { return get_http_url(); } $https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on'; $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; return ($https ? 'https://' : 'http://') . $host; } private function clearRuntimeCache() { if (defined('RUN_PATH') && function_exists('path_delete')) { @path_delete(RUN_PATH . '/cache'); @path_delete(RUN_PATH . '/complile'); @path_delete(RUN_PATH . '/config'); @path_delete(RUN_PATH . '/upgrade'); } if (function_exists('cache_config')) { @cache_config(true); } if (function_exists('opcache_reset') && extension_loaded('Zend OPcache')) { @opcache_reset(); } } private function success($data = array(), $msg = '') { $this->respond(1, $data, $msg); } private function fail($code, $data = '', $msg = '') { $this->respond($code, $data, $msg); } private function respond($code, $data = '', $msg = '') { if (! headers_sent()) { header('Content-Type: application/json; charset=utf-8'); } echo json_encode(array( 'code' => $code, 'data' => $data, 'msg' => $msg )); exit(); } }