This commit is contained in:
540
TeStore/Action.php
Normal file
540
TeStore/Action.php
Normal file
@@ -0,0 +1,540 @@
|
||||
<?php
|
||||
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
||||
|
||||
class TeStore_Action extends Typecho_Widget
|
||||
{
|
||||
private $options;
|
||||
private $settings;
|
||||
private $security;
|
||||
private $user;
|
||||
private $useCurl;
|
||||
private $pluginRoot;
|
||||
|
||||
/**
|
||||
* 构造函数与初始化
|
||||
*
|
||||
* @access public
|
||||
* @param Typecho_Request $request
|
||||
* @param Typecho_Response $response
|
||||
* @param mixed $params
|
||||
*/
|
||||
public function __construct($request, $response, $params = NULL)
|
||||
{
|
||||
parent::__construct($request, $response, $params);
|
||||
|
||||
$this->options = $this->widget('Widget_Options');
|
||||
$this->settings = $this->options->plugin('TeStore');
|
||||
$this->security = $this->widget('Widget_Security');
|
||||
$this->user = $this->widget('Widget_User');
|
||||
$this->useCurl = $this->settings->curl;
|
||||
$this->pluginRoot = __TYPECHO_ROOT_DIR__ . __TYPECHO_PLUGIN_DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已启用插件名称
|
||||
*
|
||||
* @access private
|
||||
* @return array
|
||||
*/
|
||||
private function getActivePlugins()
|
||||
{
|
||||
$activatedPlugins = Typecho_Plugin::export();
|
||||
return array_keys($activatedPlugins['activated']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装插件信息
|
||||
*
|
||||
* @access private
|
||||
* @param string $name 插件名称
|
||||
* @return array
|
||||
*/
|
||||
private function getLocalInfos($name)
|
||||
{
|
||||
$infos = array();
|
||||
$pluginDir = $this->pluginRoot . '/' . $name;
|
||||
$pluginFile = is_dir($pluginDir) ? $pluginDir . '/Plugin.php' : $pluginDir . '.php';
|
||||
if (is_file($pluginFile)) {
|
||||
$parse = Typecho_Plugin::parseInfo($pluginFile);
|
||||
$infos = array(strip_tags($parse['author']), strip_tags($parse['version'])); //兼容 html 混写
|
||||
}
|
||||
return $infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并整理插件信息
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getPluginData()
|
||||
{
|
||||
$pluginInfo = array();
|
||||
$cacheDir = $this->pluginRoot . '/TeStore/data/';
|
||||
$cacheFile = $cacheDir . 'list.json';
|
||||
$cacheTime = $this->settings->cache_time;
|
||||
|
||||
//读取缓存文件
|
||||
if ($cacheTime && is_file($cacheFile) && (time() - filemtime($cacheFile)) <= $cacheTime * 3600) {
|
||||
$data = file_get_contents($cacheFile);
|
||||
$pluginInfo = Json::decode($data, true);
|
||||
//读取表格地址
|
||||
} else {
|
||||
$html = '';
|
||||
$isRaw = false;
|
||||
$pages = array_filter(preg_split('/(\r|\n|\r\n)/', strip_tags($this->settings->source)));
|
||||
foreach ($pages as $page) {
|
||||
$page = trim($page);
|
||||
if ($page) {
|
||||
$proxy = $this->settings->proxy;
|
||||
$isRaw = strpos($page,'raw.githubusercontent.com') || strpos($page,'raw/master');
|
||||
//替换加速地址
|
||||
if ($proxy || $isRaw) {
|
||||
if (substr($proxy, -3) === "/gh") {
|
||||
$page = str_replace(array('github.com', 'raw.githubusercontent.com'), $proxy, $page);
|
||||
$page = str_replace(array('blob/', 'raw/', 'master/'), '', $page);
|
||||
} else {
|
||||
$page = Typecho_Common::url($page, $proxy);
|
||||
}
|
||||
}
|
||||
$html .= $this->useCurl ? $this->curlGet($page) : @file_get_contents($page,0,
|
||||
stream_context_create(array('http'=>array('timeout'=>20)))); //设 20 秒超时
|
||||
//转码 MD 格式
|
||||
if ($proxy || $isRaw) {
|
||||
$html = htmlspecialchars_decode(Markdown::convert($html)); //fix 17.10.30 Markdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//解析表格内容
|
||||
if ($html) {
|
||||
$dom = new DOMDocument('1.0', 'utf-8');
|
||||
$html = function_exists('mb_convert_encoding') ? mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') : $html;
|
||||
@$dom->loadHTML($html);
|
||||
$trs = $dom->getElementsByTagName('tr');
|
||||
$tdVal = '';
|
||||
$texts = array();
|
||||
$tds = array();
|
||||
$a = (object)array();
|
||||
$href = '';
|
||||
$urls = array();
|
||||
foreach ($trs as $trKey => $trVal) {
|
||||
if ($trVal->parentNode->tagName == 'tbody') {
|
||||
//获取 td 纯文本
|
||||
foreach ($trVal->childNodes as $tdKey => $td) {
|
||||
$tdVal = $td->nodeValue;
|
||||
if ($tdVal) {
|
||||
$texts[$trKey][] = htmlspecialchars(trim($tdVal));
|
||||
}
|
||||
}
|
||||
$tds = $trs->item($trKey)->getElementsByTagName('td');
|
||||
$rowUrls = array(); // 每行单独收集 URL
|
||||
//获取 td 元数据
|
||||
foreach ($tds as $tdKey => $tdVal) {
|
||||
if ($tdKey !== 1 && $tdKey !== 2) {
|
||||
$a = $tds->item($tdKey)->getElementsByTagName('a');
|
||||
$href = $a->item(0) ? $a->item(0)->getAttribute('href') : '';
|
||||
if ($tdKey == 3) {
|
||||
// 获取 td 内部的 HTML 内容(作者栏)
|
||||
$innerHTML = '';
|
||||
$tdNode = $tds->item($tdKey);
|
||||
foreach ($tdNode->childNodes as $child) {
|
||||
if ($child->nodeType == XML_ELEMENT_NODE) {
|
||||
// 元素节点:保留完整标签
|
||||
$innerHTML .= $dom->saveHTML($child);
|
||||
} else if ($child->nodeType == XML_TEXT_NODE) {
|
||||
// 文本节点:直接使用文本内容
|
||||
$innerHTML .= $child->nodeValue;
|
||||
}
|
||||
}
|
||||
$href = $innerHTML;
|
||||
}
|
||||
$rowUrls[] = trim($href);
|
||||
}
|
||||
}
|
||||
// 确保每行都有 3 个 URL 元素,不足的补空字符串
|
||||
while (count($rowUrls) < 3) {
|
||||
$rowUrls[] = '';
|
||||
}
|
||||
// 只取前 3 个元素,防止多余
|
||||
$rowUrls = array_slice($rowUrls, 0, 3);
|
||||
$urls = array_merge($urls, $rowUrls);
|
||||
}
|
||||
}
|
||||
$texts = array_values($texts);
|
||||
$urls = array_chunk($urls, 3);
|
||||
|
||||
//合并关联键名
|
||||
$keys = array('pluginName', 'desc', 'version', 'mark', 'pluginUrl', 'authorHtml', 'zipFile');
|
||||
$names = array();
|
||||
$vals = array();
|
||||
$datas = array();
|
||||
$i = 0;
|
||||
foreach ($texts as $key => $val) {
|
||||
$names[] = isset($val[0]) ? $val[0] : $val[1]; //fix for PHP 7.0+
|
||||
$vals = array_values(array_filter($val));
|
||||
// 表格有 5 列:[0:名称,1:简介,2:版本,3:作者,4:zip 标记]
|
||||
// 需要保留:[0:名称,1:简介,2:版本,4:zip 标记],删除 3:作者
|
||||
// array_filter 后重新索引,所以作者在索引 3 的位置
|
||||
if (isset($vals[3])) {
|
||||
unset($vals[3]); //去除作者栏 text
|
||||
$vals = array_values($vals); //重新索引,确保连续
|
||||
}
|
||||
$datas[] = array_combine($keys, array_merge($vals, $urls[$key]));
|
||||
}
|
||||
//按插件名排序
|
||||
array_multisort($names, SORT_ASC, $datas);
|
||||
|
||||
$pluginInfo = $datas;
|
||||
}
|
||||
|
||||
//生成缓存文件
|
||||
if ($pluginInfo && $cacheTime) {
|
||||
if (!is_dir($cacheDir)) {
|
||||
$this->makedir($cacheDir);
|
||||
}
|
||||
file_put_contents($cacheFile, Json::encode($pluginInfo));
|
||||
}
|
||||
}
|
||||
|
||||
return $pluginInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出插件列表页面
|
||||
*
|
||||
* @access private
|
||||
* @return void
|
||||
*/
|
||||
public function market()
|
||||
{
|
||||
//禁止非管理员访问
|
||||
$this->user->pass('administrator');
|
||||
|
||||
include_once 'views/market.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行安装插件步骤
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$this->security->protect();
|
||||
//禁止非管理员访问
|
||||
$this->user->pass('administrator');
|
||||
|
||||
$plugin = $this->request->plugin;
|
||||
$author = $this->request->author;
|
||||
$zip = $this->request->zip;
|
||||
$result = array(
|
||||
'status' => false,
|
||||
'error' => _t('没有找到插件文件')
|
||||
);
|
||||
|
||||
if ($zip) {
|
||||
//检测是否已启用
|
||||
$activated = $this->getActivePlugins();
|
||||
if (!empty($activated) && in_array($plugin, $activated)) {
|
||||
$result['error'] = _t('请先禁用此插件');
|
||||
} else {
|
||||
$tempDir = $this->pluginRoot . '/TeStore/.tmp';
|
||||
$tempFile = $tempDir . '/' . $plugin . '.zip';
|
||||
if (is_dir($tempDir)) {
|
||||
@$this->delTree($tempDir, true); //清理临时目录
|
||||
} else {
|
||||
$this->makedir($tempDir); //创建临时目录
|
||||
}
|
||||
$proxy = $this->settings->proxy;
|
||||
$isRaw = strpos($zip, 'raw.githubusercontent.com') || strpos($zip, 'raw/master');
|
||||
//替换为加速地址
|
||||
if ($proxy || $isRaw) {
|
||||
if (substr($proxy, -3) === "/gh") {
|
||||
$cdn = $this->ZIP_CDN($plugin, $author);
|
||||
$zip = $cdn ? $cdn : $zip;
|
||||
$proxy = $proxy ? $proxy : 'cdn.jsdelivr.net/gh';
|
||||
$zip = str_replace(array('github.com', 'raw.githubusercontent.com'), $proxy, $zip);
|
||||
$zip = substr($proxy, -3) === "/gh" ? str_replace(array('blob/', 'raw/', 'master/'), '', $zip) : str_replace(array('blob/', 'raw/'), '', $zip);
|
||||
} else {
|
||||
$zip = Typecho_Common::url($zip, $proxy);
|
||||
}
|
||||
}
|
||||
|
||||
//下载至临时目录
|
||||
$zipFile = $this->useCurl ? $this->curlGet($zip) : @file_get_contents($zip, 0,
|
||||
stream_context_create(array('http' => array('timeout' => 20)))); //设 20 秒超时
|
||||
|
||||
if (!$zipFile) {
|
||||
$result['error'] = _t('下载压缩包出错');
|
||||
} else {
|
||||
if (strpos($zipFile, '404') === 0 || strpos($zipFile, 'Couldn\'t find') === 0) {
|
||||
$result['error'] = _t('未找到下载文件');
|
||||
@unlink($tempFile);
|
||||
} else {
|
||||
file_put_contents($tempFile, $zipFile);
|
||||
$phpZip = new ZipArchive();
|
||||
$open = $phpZip->open($tempFile, ZipArchive::CHECKCONS);
|
||||
if ($open !== true) {
|
||||
$result['error'] = _t('压缩包校验错误');
|
||||
@unlink($tempFile);
|
||||
} else {
|
||||
//解压至临时目录
|
||||
if (!$phpZip->extractTo($tempDir)) {
|
||||
$error = error_get_last();
|
||||
$result['error'] = $error['message'];
|
||||
} else {
|
||||
$phpZip->close();
|
||||
@unlink($tempFile); //删除已解压包
|
||||
|
||||
//遍历各文件层级
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tempDir)) as $fileName) {
|
||||
if (!is_dir($fileName)) {
|
||||
$tmpRoutes[] = $fileName;
|
||||
}
|
||||
}
|
||||
|
||||
//定位 Plugin.php
|
||||
$trueDir = '';
|
||||
$parentDir = '';
|
||||
foreach ($tmpRoutes as $tmpRoute) {
|
||||
if (!strcasecmp(basename($tmpRoute), 'Plugin.php')) {
|
||||
$trueDir = dirname($tmpRoute);
|
||||
$parentDir = dirname($trueDir);
|
||||
}
|
||||
}
|
||||
|
||||
//处理目录型插件
|
||||
if ($trueDir) {
|
||||
$pluginDir = $this->pluginRoot . '/' . $plugin;
|
||||
if (is_dir($pluginDir)) {
|
||||
@$this->delTree($pluginDir, true); //清理旧版残留
|
||||
}
|
||||
foreach ($tmpRoutes as $tmpRoute) {
|
||||
//按文件路径创建目录
|
||||
$fileDir = $parentDir == $tempDir ? $tempDir : $parentDir;
|
||||
$tarRoute = str_replace((strpos($tmpRoute, $trueDir) === 0 ? $trueDir : $fileDir),
|
||||
$pluginDir, $tmpRoute);
|
||||
$tarDir = dirname($tarRoute);
|
||||
if (!is_dir($tarDir)) {
|
||||
$this->makedir($tarDir);
|
||||
}
|
||||
//移动文件到各层目录
|
||||
if (!rename($tmpRoute, $tarRoute)) {
|
||||
$error = error_get_last();
|
||||
$result['error'] = $error['message'];
|
||||
}
|
||||
}
|
||||
$result['status'] = true;
|
||||
|
||||
//处理单文件型插件
|
||||
} elseif (count($tmpRoutes) <= 2) {
|
||||
foreach ($tmpRoutes as $tmpRoute) {
|
||||
$name = basename($tmpRoute);
|
||||
if ($name == $plugin . '.php') {
|
||||
//移动文件到根目录
|
||||
if (!rename($tmpRoute, $this->pluginRoot . '/' . $name)) {
|
||||
$result['error'] = _t('移动文件出错');
|
||||
} else {
|
||||
$result['status'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//清空临时目录
|
||||
@$this->delTree($tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//返回提示信息
|
||||
if ($result['status']) {
|
||||
$this->widget('Widget_Notice')->highlight('plugin-' . $plugin);
|
||||
$this->widget('Widget_Notice')->set(_t('安装插件 %s 成功, 可以在下方启用', $plugin), 'success');
|
||||
$this->response->redirect($this->options->adminUrl . 'plugins.php#plugin-' . end($activated));
|
||||
} else {
|
||||
$this->widget('Widget_Notice')->set(_t('安装插件 %s 失败: %s', $plugin, $result['error']), 'error');
|
||||
$this->response->goBack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行卸载插件步骤
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
$this->security->protect();
|
||||
//禁止非管理员访问
|
||||
$this->user->pass('administrator');
|
||||
|
||||
$plugin = $this->request->plugin;
|
||||
$result = array(
|
||||
'status' => false,
|
||||
'error' => _t('移除文件出错')
|
||||
);
|
||||
|
||||
if ($this->getLocalInfos($plugin)) {
|
||||
$activated = $this->getActivePlugins();
|
||||
//已启用则自动禁用
|
||||
if (!empty($activated) && in_array($plugin, $activated)) {
|
||||
Helper::removePlugin($plugin);
|
||||
}
|
||||
|
||||
$pluginDir = $this->pluginRoot . '/' . $plugin;
|
||||
//清空目录型插件
|
||||
if (is_dir($pluginDir)) {
|
||||
if (!@$this->delTree($pluginDir)) {
|
||||
$error = error_get_last();
|
||||
$result['error'] = $error['message'];
|
||||
} else {
|
||||
$result['status'] = true;
|
||||
}
|
||||
//删除单文件插件
|
||||
} else {
|
||||
@unlink($pluginDir . '.php');
|
||||
$result['status'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
//返回提示信息
|
||||
if ($result['status']) {
|
||||
$this->widget('Widget_Notice')->set(_t('删除插件 %s 成功', $plugin), 'success');
|
||||
} else {
|
||||
$this->widget('Widget_Notice')->set(_t('删除插件 %s 失败: %s', $plugin, $result['error']), 'error');
|
||||
}
|
||||
$this->response->goBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测可加速 zip 地址
|
||||
*
|
||||
* @access public
|
||||
* @param string $name 插件名称
|
||||
* @param string $author 作者名称
|
||||
* @return string
|
||||
*/
|
||||
public function ZIP_CDN($name = '', $author = '')
|
||||
{
|
||||
$datas = array();
|
||||
$cacheDir = $this->pluginRoot . '/TeStore/data/';
|
||||
$cacheFile = $cacheDir . 'zip_cdn.json';
|
||||
$cacheTime = $this->settings->cache_time;
|
||||
|
||||
//读取缓存文件
|
||||
if ($cacheTime && is_file($cacheFile) && (time() - filemtime($cacheFile)) <= $cacheTime * 3600) {
|
||||
$data = file_get_contents($cacheFile);
|
||||
$datas = Json::decode($data, true);
|
||||
//读取 API 数据
|
||||
} else {
|
||||
$api = 'https://api.github.com/repositories/14101953/contents/ZIP_CDN';
|
||||
$data = $this->useCurl ? $this->curlGet($api) : @file_get_contents($api, 0,
|
||||
stream_context_create(array('http' => array('header' => array('User-Agent: PHP'), 'timeout' => 20)))); //API 要求 header
|
||||
if ($data) {
|
||||
$datas = Json::decode($data, true);
|
||||
//生成缓存文件
|
||||
if ($cacheTime) {
|
||||
if (!is_dir($cacheDir)) {
|
||||
$this->makedir($cacheDir);
|
||||
}
|
||||
file_put_contents($cacheFile, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$zip = '';
|
||||
if ($name && $author) {
|
||||
foreach ($datas as $data) {
|
||||
if ($data['name'] == $name . '_' . $author . '.zip') { //带作者名优先
|
||||
$zip = $data['download_url'];
|
||||
} elseif ($data['name'] == $name . '.zip') {
|
||||
$zip = $data['download_url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $zip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归创建本地目录
|
||||
*
|
||||
* @access private
|
||||
* @param string $path 目录路径
|
||||
* @return boolean
|
||||
*/
|
||||
private function makedir($path)
|
||||
{
|
||||
$path = preg_replace('/\\\+/', '/', $path);
|
||||
$current = rtrim($path, '/');
|
||||
$last = $current;
|
||||
|
||||
while (!is_dir($current) && false !== strpos($path, '/')) {
|
||||
$last = $current;
|
||||
$current = dirname($current);
|
||||
}
|
||||
if ($last == $current) {
|
||||
return true;
|
||||
}
|
||||
if (!@mkdir($last)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stat = @stat($last);
|
||||
$perms = $stat['mode'] & 0007777;
|
||||
@chmod($last, $perms);
|
||||
|
||||
return $this->makedir($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空目录内文件
|
||||
*
|
||||
* @access private
|
||||
* @param string $folder 目录路径
|
||||
* @param boolean $keep 保留目录
|
||||
* @return boolean
|
||||
*/
|
||||
private function delTree($folder, $keep = false)
|
||||
{
|
||||
$files = array_diff(scandir($folder), array('.', '..'));
|
||||
foreach ($files as $file) {
|
||||
$path = $folder . '/' . $file;
|
||||
is_dir($path) ? $this->delTree($path) : unlink($path);
|
||||
}
|
||||
return $keep ? true : rmdir($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 cURL 方法下载
|
||||
*
|
||||
* @access private
|
||||
* @return string
|
||||
*/
|
||||
private function curlGet($url)
|
||||
{
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_HEADER, 0);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($curl, CURLOPT_CAINFO, 'usr/plugins/TeStore/data/cacert.pem'); //证书识别库
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30); //设 30 秒超时
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
$result = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user