Files
plugins/TeStore/Action.php
chorblack e75f275ef4
Some checks failed
定时更新GitHub源插件 / 自动更新GitHub插件 (push) Has been cancelled
Initial commit
2026-03-07 11:19:25 +08:00

541 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}