541 lines
21 KiB
PHP
541 lines
21 KiB
PHP
<?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;
|
||
}
|
||
|
||
}
|