validate()) {
$this->response->goBack();
}
/** 取出数据 */
$link = $this->request->from('email', 'image', 'url', 'state');
/** 过滤XSS */
$link['name'] = $this->request->filter('xss')->name;
$link['sort'] = $this->request->filter('xss')->sort;
$link['description'] = $this->request->filter('xss')->description;
$link['user'] = $this->request->filter('xss')->user;
$link['order'] = $this->db->fetchObject($this->db->select(array('MAX(order)' => 'maxOrder'))->from($this->prefix . 'links'))->maxOrder + 1;
/** 插入数据 */
$link_lid = $this->db->query($this->db->insert($this->prefix . 'links')->rows($link));
/** 设置高亮 */
$this->widget('Widget_Notice')->highlight('link-' . $link_lid);
/** 提示信息 */
$this->widget('Widget_Notice')->set(_t(
'友链 %s 已经被增加',
$link['url'],
$link['name']
), null, 'success');
/** 转向原页 */
$this->response->redirect(Typecho_Common::url('extending.php?panel=Links%2Fmanage-links.php', $this->options->adminUrl));
}
public function updateLink()
{
if (Links_Plugin::form('update')->validate()) {
$this->response->goBack();
}
/** 取出数据 */
$link = $this->request->from('email', 'image', 'url', 'state');
$link_lid = $this->request->from('lid');
/** 过滤XSS */
$link['name'] = $this->request->filter('xss')->name;
$link['sort'] = $this->request->filter('xss')->sort;
$link['description'] = $this->request->filter('xss')->description;
$link['user'] = $this->request->filter('xss')->user;
/** 更新数据 */
$this->db->query($this->db->update($this->prefix . 'links')->rows($link)->where('lid = ?', $link_lid));
/** 设置高亮 */
$this->widget('Widget_Notice')->highlight('link-' . $link_lid);
/** 提示信息 */
$this->widget('Widget_Notice')->set(_t(
'友链 %s 已经被更新',
$link['url'],
$link['name']
), null, 'success');
/** 转向原页 */
$this->response->redirect(Typecho_Common::url('extending.php?panel=Links%2Fmanage-links.php', $this->options->adminUrl));
}
public function deleteLink()
{
$lids = $this->request->filter('int')->getArray('lid');
$deleteCount = 0;
if ($lids && is_array($lids)) {
foreach ($lids as $lid) {
if ($this->db->query($this->db->delete($this->prefix . 'links')->where('lid = ?', $lid))) {
$deleteCount++;
}
}
}
/** 提示信息 */
$this->widget('Widget_Notice')->set(
$deleteCount > 0 ? _t('友链已经删除') : _t('没有友链被删除'),
null,
$deleteCount > 0 ? 'success' : 'notice'
);
/** 转向原页 */
$this->response->redirect(Typecho_Common::url('extending.php?panel=Links%2Fmanage-links.php', $this->options->adminUrl));
}
public function enableLink()
{
$lids = $this->request->filter('int')->getArray('lid');
$enableCount = 0;
if ($lids && is_array($lids)) {
foreach ($lids as $lid) {
if ($this->db->query($this->db->update($this->prefix . 'links')->rows(array('state' => '1'))->where('lid = ?', $lid))) {
$enableCount++;
}
}
}
/** 提示信息 */
$this->widget('Widget_Notice')->set(
$enableCount > 0 ? _t('友链已经启用') : _t('没有友链被启用'),
null,
$enableCount > 0 ? 'success' : 'notice'
);
/** 转向原页 */
$this->response->redirect(Typecho_Common::url('extending.php?panel=Links%2Fmanage-links.php', $this->options->adminUrl));
}
public function prohibitLink()
{
$lids = $this->request->filter('int')->getArray('lid');
$prohibitCount = 0;
if ($lids && is_array($lids)) {
foreach ($lids as $lid) {
if ($this->db->query($this->db->update($this->prefix . 'links')->rows(array('state' => '0'))->where('lid = ?', $lid))) {
$prohibitCount++;
}
}
}
/** 提示信息 */
$this->widget('Widget_Notice')->set(
$prohibitCount > 0 ? _t('友链已经禁用') : _t('没有友链被禁用'),
null,
$prohibitCount > 0 ? 'success' : 'notice'
);
/** 转向原页 */
$this->response->redirect(Typecho_Common::url('extending.php?panel=Links%2Fmanage-links.php', $this->options->adminUrl));
}
public function sortLink()
{
$links = $this->request->filter('int')->getArray('lid');
if ($links && is_array($links)) {
foreach ($links as $sort => $lid) {
$this->db->query($this->db->update($this->prefix . 'links')->rows(array('order' => $sort + 1))->where('lid = ?', $lid));
}
}
}
public function emailLogo()
{
/* 邮箱头像解API接口 by 懵仙兔兔 */
$type = $this->request->type;
$email = $this->request->email;
if ($email == null || $email == '') {
$this->response->throwJson('请提交邮箱链接 [email=abc@abc.com]');
exit;
} else if ($type == null || $type == '' || ($type != 'txt' && $type != 'json')) {
$this->response->throwJson('请提交type类型 [type=txt, type=json]');
exit;
} else {
$f = str_replace('@qq.com', '', $email);
$email = $f . '@qq.com';
if (is_numeric($f) && strlen($f) < 11 && strlen($f) > 4) {
stream_context_set_default([
'ssl' => [
'verify_host' => false,
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$geturl = 'https://s.p.qq.com/pub/get_face?img_type=3&uin=' . $f;
$headers = get_headers($geturl, TRUE);
if ($headers) {
$g = $headers['Location'];
$g = str_replace("http:", "https:", $g);
} else {
$g = 'https://q.qlogo.cn/g?b=qq&nk=' . $f . '&s=100';
}
} else {
$g = 'https://cdn.helingqi.com/wavatar/' . md5($email) . '?d=mm';
}
$r = array('url' => $g);
if ($type == 'txt') {
$this->response->throwJson($g);
exit;
} else if ($type == 'json') {
$this->response->throwJson(json_encode($r));
exit;
}
}
}
/**
* 按插件设置的 cid 列表,重写文章/页面正文:
* - 用 {{links_plus}} 占位符替换为插件生成的友链 HTML
*/
public function rewriteContents()
{
$options = Typecho_Widget::widget('Widget_Options');
$settings = $options->plugin('Links');
$raw = isset($settings->rewrite_cids) ? trim((string)$settings->rewrite_cids) : '';
if ($raw === '') {
$this->widget('Widget_Notice')->set(_t('请先在插件设置中填写需要重写的 cid'), null, 'notice');
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
}
$cids = array_filter(array_map('trim', explode(',', $raw)), function ($v) {
return $v !== '';
});
$cids = array_values(array_unique(array_map('intval', $cids)));
$cids = array_filter($cids, function ($v) {
return $v > 0;
});
if (!$cids) {
$this->widget('Widget_Notice')->set(_t('cid 格式不正确,请使用英文逗号分隔的数字'), null, 'notice');
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
}
$placeholder = Links_Plugin::REWRITE_PLACEHOLDER;
$blockStart = Links_Plugin::REWRITE_BLOCK_START;
$blockEnd = Links_Plugin::REWRITE_BLOCK_END;
$html = Links_Plugin::buildRewriteHtml();
if ($html === null || trim($html) === '') {
$this->widget('Widget_Notice')->set(_t('重写已取消:未生成任何友链输出(可能没有启用的友链,或输出模板为空)'), null, 'notice');
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
}
$db = Typecho_Db::get();
$prefix = $db->getPrefix();
$table = $prefix . 'contents';
$total = 0;
$hit = 0;
$miss = 0;
$fail = 0;
foreach ($cids as $cid) {
$total++;
try {
$row = $db->fetchRow($db->select()->from($table)->where('cid = ?', $cid)->limit(1));
if (!$row) {
$fail++;
continue;
}
$text = (string)($row['text'] ?? '');
if ($text === '') {
$miss++;
continue;
}
$wrappedHtml = $blockStart . "\n" . $html . "\n" . $blockEnd;
$newText = null;
// 若存在历史重写块,则直接替换块内容(支持重复重写)
if (strpos($text, $blockStart) !== false && strpos($text, $blockEnd) !== false) {
$pattern = '/' . preg_quote($blockStart, '/') . '.*?' . preg_quote($blockEnd, '/') . '/s';
$newText = preg_replace($pattern, $wrappedHtml, $text, 1);
}
// 否则用占位符替换
if ($newText === null) {
// 一些编辑器/主题会把占位符包裹在行内标签里,或做 HTML 转义,
// 例如: / {{links_plus}}
// 为了让“重写”更稳,这里把几种常见等价写法都当作占位符处理。
$placeholderCandidates = array_values(array_unique(array_filter(array(
$placeholder,
// HTML 实体转义后的样式(部分编辑器会这么存)
htmlspecialchars($placeholder, ENT_QUOTES, 'UTF-8'),
// 全角花括号(中文输入法常见)
str_replace(array('{', '}'), array('{', '}'), $placeholder),
// 被代码标记包裹
'`' . $placeholder . '`',
'' . $placeholder . '',
// 兼容用户把占位符写成 {{ links_plus }}
'{{ links_plus }}',
'{{ links_plus}}',
'{{links_plus }}',
), function ($v) {
return is_string($v) && $v !== '';
})));
$foundCandidate = null;
foreach ($placeholderCandidates as $cand) {
if (strpos($text, $cand) !== false) {
$foundCandidate = $cand;
break;
}
}
if ($foundCandidate === null) {
// 兼容历史:如果正文已被替换成“裸 HTML”(没有占位符,也没有标记块),
// 仍允许通过查找一次生成的 html 片段来包裹标记块。
// 这里采用宽松策略:只要正文包含当前生成的 html(trim 后),则包裹它。
$plain = trim($html);
if ($plain !== '' && strpos($text, $plain) !== false) {
$newText = str_replace($plain, $wrappedHtml, $text);
} else {
$miss++;
continue;
}
}
// 命中占位符(或等价写法)则替换
if ($newText === null) {
$newText = str_replace($foundCandidate, $wrappedHtml, $text);
}
}
if ($newText === null || $newText === $text) {
$miss++;
continue;
}
$db->query($db->update($table)->rows(array('text' => $newText))->where('cid = ?', $cid));
$hit++;
} catch (Exception $e) {
$fail++;
}
}
$this->widget('Widget_Notice')->set(
_t('重写完成:目标 %d 篇,命中替换 %d 篇,未发现占位符 %d 篇,失败 %d 篇。', $total, $hit, $miss, $fail),
null,
$hit > 0 ? 'success' : 'notice'
);
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
}
public function action()
{
Helper::security()->protect();
$user = Typecho_Widget::widget('Widget_User');
$user->pass('administrator');
$this->db = Typecho_Db::get();
$this->prefix = $this->db->getPrefix();
$this->options = Typecho_Widget::widget('Widget_Options');
$this->on($this->request->is('do=insert'))->insertLink();
$this->on($this->request->is('do=update'))->updateLink();
$this->on($this->request->is('do=delete'))->deleteLink();
$this->on($this->request->is('do=enable'))->enableLink();
$this->on($this->request->is('do=prohibit'))->prohibitLink();
$this->on($this->request->is('do=sort'))->sortLink();
$this->on($this->request->is('do=email-logo'))->emailLogo();
$this->on($this->request->is('do=rewrite'))->rewriteContents();
$this->on($this->request->is('do=update_templates'))->updateTemplates();
$this->response->redirect($this->options->adminUrl);
}
/**
* 从 GitHub 下载 templates 并覆盖本地 templates 目录
*/
public function updateTemplates()
{
// 仅管理员可操作(action() 已授权)
$zipUrl = 'https://github.com/lhl77/Typecho-Plugin-LinksPlus/archive/refs/heads/main.zip';
$tmpZip = sys_get_temp_dir() . '/links_templates_' . time() . '.zip';
$tmpDir = sys_get_temp_dir() . '/links_templates_dir_' . time();
// 下载 ZIP
$context = stream_context_create(array('http' => array('timeout' => 30)));
$data = @file_get_contents($zipUrl, false, $context);
if (!$data) {
$this->widget('Widget_Notice')->set(_t('下载失败:无法从 GitHub 获取模板压缩包。'), null, 'notice');
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
return;
}
file_put_contents($tmpZip, $data);
// 解压到临时目录
$zip = new ZipArchive();
if ($zip->open($tmpZip) !== true) {
@unlink($tmpZip);
$this->widget('Widget_Notice')->set(_t('解压失败:无法打开下载的压缩包。'), null, 'notice');
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
return;
}
@mkdir($tmpDir, 0755, true);
$zip->extractTo($tmpDir);
$zip->close();
// 源模板目录(zip 中的路径)
$srcTemplates = $tmpDir . '/Typecho-Plugin-LinksPlus-main/templates';
$dstTemplates = dirname(__FILE__) . '/templates';
if (!is_dir($srcTemplates)) {
// 清理
@unlink($tmpZip);
// 递归删除 tmpDir
$this->rrmdir($tmpDir);
$this->widget('Widget_Notice')->set(_t('模板包中未找到 templates 目录,操作已取消。'), null, 'notice');
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
return;
}
// 备份现有 templates(如存在)
if (is_dir($dstTemplates)) {
$backup = dirname(__FILE__) . '/templates_backup_' . date('YmdHis');
@rename($dstTemplates, $backup);
}
// 复制新模板到目标
$ok = $this->rcopy($srcTemplates, $dstTemplates);
// 清理临时文件
@unlink($tmpZip);
$this->rrmdir($tmpDir);
if ($ok) {
$this->widget('Widget_Notice')->set(_t('模板已成功更新并覆盖到 plugins/Links/templates(旧模板已备份)。'), null, 'success');
} else {
$this->widget('Widget_Notice')->set(_t('模板更新失败,请检查文件权限。'), null, 'notice');
}
$this->response->redirect(Typecho_Common::url('options-plugin.php?config=Links', $this->options->adminUrl));
}
// 递归复制目录
private function rcopy($src, $dst)
{
if (!is_dir($src)) return false;
@mkdir($dst, 0755, true);
$dir = opendir($src);
if (!$dir) return false;
while (false !== ($file = readdir($dir))) {
if ($file == '.' || $file == '..') continue;
$s = $src . DIRECTORY_SEPARATOR . $file;
$d = $dst . DIRECTORY_SEPARATOR . $file;
if (is_dir($s)) {
$this->rcopy($s, $d);
} else {
@copy($s, $d);
}
}
closedir($dir);
return true;
}
// 递归删除目录
private function rrmdir($dir)
{
if (!is_dir($dir)) return;
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $fileinfo) {
$todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
@$todo($fileinfo->getRealPath());
}
@rmdir($dir);
}
}