'; const REWRITE_BLOCK_END = ''; /** 模板目录(相对插件目录) */ const TEMPLATE_DIR = 'templates'; /** * 获取插件绝对路径 */ public static function getPluginDir() { return dirname(__FILE__); } /** * 获取模板根目录绝对路径 */ public static function getTemplateRoot() { return self::getPluginDir() . DIRECTORY_SEPARATOR . self::TEMPLATE_DIR; } /** * 列出所有文件模板(读取 templates//manifest.json) * * @return array */ public static function listTemplates() { $root = self::getTemplateRoot(); $list = array(); if (!is_dir($root)) { return $list; } $dirs = glob($root . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR); if (!$dirs) { return $list; } foreach ($dirs as $dir) { $manifestFile = $dir . DIRECTORY_SEPARATOR . 'manifest.json'; if (!is_file($manifestFile)) { continue; } $json = @file_get_contents($manifestFile); if (!$json) { continue; } $manifest = @json_decode($json, true); if (!is_array($manifest)) { continue; } $name = isset($manifest['name']) ? (string)$manifest['name'] : basename($dir); if ($name === '') { continue; } $manifest['_dir'] = $dir; $list[$name] = $manifest; } return $list; } /** * 读取模板文件内容 */ public static function readTemplateFile($templateName, $file) { $templateName = trim((string)$templateName); $file = trim((string)$file); if ($templateName === '' || $file === '') { return null; } // 简单防穿越:只允许 [A-Za-z0-9_-] if (!preg_match('/^[A-Za-z0-9_-]+$/', $templateName)) { return null; } $path = self::getTemplateRoot() . DIRECTORY_SEPARATOR . $templateName . DIRECTORY_SEPARATOR . $file; if (!is_file($path)) { return null; } return file_get_contents($path); } /** * 注入模板 CSS/JS(同一模板同一请求只注入一次) */ public static function injectTemplateAssetsOnce($templateName, array $manifest) { static $injected = array(); $key = 'tpl:' . $templateName; if (isset($injected[$key])) { return; } $injected[$key] = true; $inject = isset($manifest['inject']) && is_array($manifest['inject']) ? $manifest['inject'] : array(); $injectCss = !empty($inject['css']); $injectJs = !empty($inject['js']); if ($injectCss) { $css = self::readTemplateFile($templateName, 'style.css'); if ($css && trim($css) !== '') { echo ''; } } if ($injectJs) { $js = self::readTemplateFile($templateName, 'script.js'); if ($js && trim($js) !== '') { echo ''; } } } /** * 激活插件方法,如果激活失败,直接抛出异常 * * @access public * @return string * @throws Typecho_Plugin_Exception */ public static function activate() { $info = Links_Plugin::linksInstall(); try { $menuIndex = Helper::addMenu('Links Plus'); Helper::addPanel($menuIndex, 'Links/manage-links.php', _t('友情链接'), _t('管理友情链接'), 'administrator'); } catch (Exception $e) { Helper::addPanel(3, 'Links/manage-links.php', _t('友情链接'), _t('管理友情链接'), 'administrator'); } catch (Throwable $e) { Helper::addPanel(3, 'Links/manage-links.php', _t('友情链接'), _t('管理友情链接'), 'administrator'); } Helper::addAction('links-edit', 'Links_Action'); // Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('Links_Plugin', 'parse'); // Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('Links_Plugin', 'parse'); // Typecho_Plugin::factory('Widget_Abstract_Comments')->contentEx = array('Links_Plugin', 'parse'); // Typecho_Plugin::factory('Widget_Archive')->callLinks = array('Links_Plugin', 'output_str'); return _t($info); } /** * 禁用插件方法,如果禁用失败,直接抛出异常 * * @static * @access public * @return void * @throws Typecho_Plugin_Exception */ public static function deactivate() { Helper::removeAction('links-edit'); try { $menuIndex = Helper::removeMenu('Links Plus'); if ($menuIndex !== null) { Helper::removePanel($menuIndex, 'Links/manage-links.php'); } } catch (Exception $e) { // ignore } catch (Throwable $e) { // ignore } // 兼容旧注册方式 Helper::removePanel(3, 'Links/manage-links.php'); } /** * 获取插件配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form 配置面板 * @return void */ public static function config(Typecho_Widget_Helper_Form $form) { echo ' ' . << (function(){ var REPO = "lhl77/Typecho-Plugin-LinksPlus"; // 当前版本(按 tag 口径对比) var CURRENT = "v1.3.1"; function normalizeTag(tag){ tag = (tag || "").toString().trim(); if(!tag) return ""; return tag.replace(/^refs\/tags\//, ""); } function tagToVersion(tag){ tag = normalizeTag(tag); return tag.replace(/^[vV]/, ""); } function cmp(a, b){ a = (a || "").toString(); b = (b || "").toString(); var as = a.split('.'); var bs = b.split('.'); var n = Math.max(as.length, bs.length); for(var i=0;i bn) return 1; if(an < bn) return -1; } else { if(ai > bi) return 1; if(ai < bi) return -1; } } return 0; } function fetchJson(url, cb){ var xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.setRequestHeader("Accept", "application/vnd.github+json"); xhr.onreadystatechange = function(){ if(xhr.readyState !== 4) return; if(xhr.status >= 200 && xhr.status < 300){ try { cb(null, JSON.parse(xhr.responseText)); } catch(e){ cb(e); } } else { cb(new Error("HTTP " + xhr.status)); } }; xhr.send(null); } function setBtnText(btn, text){ if(btn){ btn.textContent = text; } } function setBusy(btn, busy){ if(!btn) return; busy = !!busy; btn.setAttribute('aria-disabled', busy ? 'true' : 'false'); if(busy){ btn.classList.add('is-disabled'); btn.dataset.busy = '1'; } else { btn.classList.remove('is-disabled'); btn.dataset.busy = ''; } } function renderNote(host, type, title, html){ if(!host) return; var cls = 'lp-update-note'; if(type === 'ok') cls += ' is-ok'; if(type === 'warn') cls += ' is-warn'; if(type === 'err') cls += ' is-err'; host.innerHTML = '
' + '
' + title + '
' + '
' + html + '
' + '
'; } document.addEventListener("DOMContentLoaded", function(){ var btn = document.getElementById("links-plus-check-update"); if(!btn) return; var card = btn.closest ? btn.closest('.md3-card') : null; var out = card ? card.querySelector('.lp-update-out') : null; btn.addEventListener("click", function(e){ e.preventDefault(); if(btn.dataset && btn.dataset.busy === '1') return; setBusy(btn, true); var api = "https://api.github.com/repos/" + REPO + "/tags?per_page=100"; var oldText = btn.textContent; setBtnText(btn, "检查中..."); renderNote(out, 'ok', '检查更新', '正在查询 GitHub tags…'); fetchJson(api, function(err, tags){ setBtnText(btn, oldText || "检查更新"); setBusy(btn, false); if(err || !Array.isArray(tags)){ renderNote( out, 'err', '检查失败', '原因:' + (err ? err.message : '响应异常') + '
' + '说明:此方案直接从浏览器访问 GitHub API,可能会受网络或 CORS 限制。' ); return; } var latestTag = ""; var latestVer = ""; for(var i=0;i 0){ latestVer = ver; latestTag = tag; } } if(!latestTag){ renderNote(out, 'err', '检查失败', '未发现可用的版本 tag。'); return; } var curVer = tagToVersion(CURRENT); var hasUpdate = cmp(latestVer, curVer) > 0; var url = "https://github.com/" + REPO + "/releases/tag/" + encodeURIComponent(latestTag); if(hasUpdate){ renderNote( out, 'warn', '发现新版本:' + latestTag, '当前版本:' + CURRENT + '
' + '最新版本:' + latestTag + '
' + '打开 GitHub 查看' ); } else { renderNote( out, 'ok', '已是最新版本', '当前版本:' + CURRENT + '
' + '最新版本:' + latestTag + '' ); } }); }); }); })(); LINKS_PLUS_UPDATE_JS . '
友情链接插件 (Links Plus)

欢迎使用 Links Plus 增强版。您可以在“管理”菜单下找到“友情链接”进行日常操作。

本插件支持多种输出模式(文字、图片、图文混合),并支持自定义字段扩展。

模式字符串变量说明
变量占位符 说明
{url}友链的 URL 地址
{name}友链显示的名称
{description}友链的描述
{image}图片地址 (Logo/头像)
{size}在调用中设置的图片尺寸值 (数字)
{sort}分类名称
{user}自定义扩展数据
{lid}数据库内的 ID 编号
'; // 模板选择(文件模板 / 保留高级自定义 textarea 作为兼容) $templates = self::listTemplates(); $tplOptions = array( '' => _t('(不使用模板,沿用下方自定义规则/旧配置)'), ); if (!empty($templates)) { foreach ($templates as $name => $manifest) { $title = isset($manifest['title']) ? (string)$manifest['title'] : $name; $tplOptions[$name] = $title; } } $selectedText = new Typecho_Widget_Helper_Form_Element_Select( 'template_text', $tplOptions, 'default-text', _t('SHOW_TEXT 使用的模板'), _t('选择一个文件模板(templates 目录)。选择后,前台调用 SHOW_TEXT 会优先使用该模板渲染。') ); $form->addInput($selectedText); $selectedImg = new Typecho_Widget_Helper_Form_Element_Select( 'template_img', $tplOptions, 'default-img', _t('SHOW_IMG 使用的模板'), _t('选择一个文件模板(templates 目录)。选择后,前台调用 SHOW_IMG 会优先使用该模板渲染。') ); $form->addInput($selectedImg); $selectedMix = new Typecho_Widget_Helper_Form_Element_Select( 'template_mix', $tplOptions, 'default-mix', _t('SHOW_MIX 使用的模板'), _t('选择一个文件模板(templates 目录)。选择后,前台调用 SHOW_MIX 会优先使用该模板渲染。') ); $form->addInput($selectedMix); $advHelp = new Typecho_Widget_Helper_Layout('div', array('class' => 'md3-card')); $advHelp->html( '
高级:自定义源码规则(兼容旧版本)
' . '
' . '

当你不想用模板,或需要更细粒度的自定义时,再使用下面三段规则(旧版配置项)。

' . '
' ); $form->addItem($advHelp); $pattern_text = new Typecho_Widget_Helper_Form_Element_Textarea( 'pattern_text', null, '
  • {name}
  • ', _t('SHOW_TEXT 模式源码规则(高级)'), _t('当未选择模板时生效。使用 SHOW_TEXT(仅文字) 模式输出时的源码,可按上表规则替换其中字段') ); $form->addInput($pattern_text); $pattern_img = new Typecho_Widget_Helper_Form_Element_Textarea( 'pattern_img', null, '
  • {name}
  • ', _t('SHOW_IMG 模式源码规则(高级)'), _t('当未选择模板时生效。使用 SHOW_IMG(仅图片) 模式输出时的源码,可按上表规则替换其中字段') ); $form->addInput($pattern_img); $pattern_mix = new Typecho_Widget_Helper_Form_Element_Textarea( 'pattern_mix', null, '
  • {name}{name}
  • ', _t('SHOW_MIX 模式源码规则(高级)'), _t('当未选择模板时生效。使用 SHOW_MIX(图文混合) 模式输出时的源码,可按上表规则替换其中字段') ); $form->addInput($pattern_mix); $dsize = new Typecho_Widget_Helper_Form_Element_Text( 'dsize', NULL, '32', _t('默认输出图片尺寸'), _t('调用时如果未指定尺寸参数默认输出的图片大小(单位px不用填写)') ); $dsize->input->setAttribute('class', 'w-10'); $form->addInput($dsize->addRule('isInteger', _t('请填写整数数字'))); $temHelp = new Typecho_Widget_Helper_Layout('div', array('class' => 'md3-card')); $temHelp->html( '
    正文重写

    当主题没有通过 $this->content() 输出正文,导致 <links>...</links> 不解析时,可用“正文重写工具”把正文中的占位符替换为友链 HTML。

    固定占位符:' . self::REWRITE_PLACEHOLDER . '

    建议 优先使用文件模板(templates/)来管理输出结构;旧版“源码规则”保留兼容。

    获取最新主题 查看全部主题/开发文档
    ' ); $form->addItem($temHelp); // 前端脚本:获取最新主题按钮行为 — 调用后由服务器端执行下载并覆盖 templates(会备份原有 templates) $script = <<<'SCRIPT' SCRIPT; echo $script; /** * 按 cid 重写正文输出(绕过主题不走 contentEx 的情况) */ // 说明已在上方 intro card 给出,这里不再重复插入大段说明卡片,避免页面过长。 $rewriteCid = new Typecho_Widget_Helper_Form_Element_Text( 'rewrite_cids', null, '', _t('需要重写的 cid(可多个)'), _t('填写文章/页面 cid,多个用英文逗号分隔,例如:12,34,56') ); $rewriteCid->input->setAttribute('class', 'w-50'); $form->addInput($rewriteCid); $rewriteModeOptions = array( ); if (!empty($templates)) { foreach ($templates as $name => $manifest) { $title = isset($manifest['title']) ? (string)$manifest['title'] : $name; $rewriteModeOptions['TPL:' . $name] = $title ; } } $rewritePattern = new Typecho_Widget_Helper_Form_Element_Select( 'rewrite_pattern', $rewriteModeOptions, 'SHOW_TEXT', _t('重写输出主题'), _t('把占位符替换成哪种模式输出,也可以直接选择某个文件模板。') ); $form->addInput($rewritePattern); $rewriteNum = new Typecho_Widget_Helper_Form_Element_Text( 'rewrite_num', null, '0', _t('重写输出数量'), _t('0 表示全部') ); $rewriteNum->input->setAttribute('class', 'w-10'); $form->addInput($rewriteNum->addRule('isInteger', _t('请填写整数数字'))); // 开发中,小子别看了,一起开发提PR吧 // 是否启用短代码 [links_plus] // $enableShortcode = new Typecho_Widget_Helper_Form_Element_Checkbox( // 'enable_shortcodes', // array('links_plus' => _t('启用短代码 [links_plus](前端调用,免重写)')), // array(), // _t('短代码支持'), // _t('开启后,文章正文中使用 [links_plus] 将被动态替换为友链 HTML(不写回正文)。') // ); // $form->addInput($enableShortcode); $rewriteSort = new Typecho_Widget_Helper_Form_Element_Text( 'rewrite_sort', null, '', _t('重写分类(可选)'), _t('只输出指定分类 sort;留空为全部') ); $rewriteSort->input->setAttribute('class', 'w-20'); $form->addInput($rewriteSort); $rewriteSize = new Typecho_Widget_Helper_Form_Element_Text( 'rewrite_size', null, '0', _t('重写图片尺寸(可选)'), _t('0 表示使用插件默认尺寸') ); $rewriteSize->input->setAttribute('class', 'w-10'); $form->addInput($rewriteSize->addRule('isInteger', _t('请填写整数数字'))); $rewriteWrapBang = new Typecho_Widget_Helper_Form_Element_Radio( 'rewrite_wrap_bang', array( '0' => _t('不包裹'), '1' => _t('使用 !!! !!! 包裹(部分主题需要)'), ), '0', _t('重写输出 HTML'), _t('有些主题/渲染器不支持直接输出 HTML,需要用 “!!!” 包裹整段 HTML 才会被当作原始 HTML 渲染。') ); $form->addInput($rewriteWrapBang); // //重写按钮(GET,走 Action,带 CSRF),这里是测试的时候用的 // $sec = Helper::security(); // $rewriteUrl = $sec->getIndex('/action/links-edit?do=rewrite'); // $rewriteBtn = new Typecho_Widget_Helper_Layout('p', array('class' => 'typecho-option')); // $rewriteBtn->html( // '' . // '执行重写' // ); // $form->addItem($rewriteBtn); } /** * 生成用于“重写正文”的 HTML 字符串 */ public static function buildRewriteHtml() { $options = Typecho_Widget::widget('Widget_Options'); $settings = $options->plugin('Links'); $pattern = isset($settings->rewrite_pattern) && $settings->rewrite_pattern ? $settings->rewrite_pattern : 'SHOW_TEXT'; $num = isset($settings->rewrite_num) ? (int)$settings->rewrite_num : 0; $sort = isset($settings->rewrite_sort) && $settings->rewrite_sort !== '' ? (string)$settings->rewrite_sort : null; // size=0 表示使用插件默认尺寸(dsize) $size = isset($settings->rewrite_size) ? (int)$settings->rewrite_size : 0; if ($size <= 0) { $size = (int)$settings->dsize; } // 如果重写模式选择了文件模板(TPL:xxx),则把模板的 CSS/JS 一并内联写入正文, // 以适配“主题不加载 head 注入 / 仅渲染正文”的场景。 $assetCss = ''; $assetJs = ''; if (is_string($pattern) && stripos($pattern, 'TPL:') === 0) { $tplName = trim(substr($pattern, 4)); $templates = self::listTemplates(); if ($tplName !== '' && isset($templates[$tplName])) { $manifest = $templates[$tplName]; $inject = isset($manifest['inject']) && is_array($manifest['inject']) ? $manifest['inject'] : array(); if (!empty($inject['css'])) { $css = self::readTemplateFile($tplName, 'style.css'); if ($css && trim($css) !== '') { $assetCss = "\n"; } } if (!empty($inject['js'])) { $js = self::readTemplateFile($tplName, 'script.js'); if ($js && trim($js) !== '') { $assetJs = "\n"; } } } } $html = Links_Plugin::output_str('', array($pattern, $num, $sort, $size, 'HTML')); // 资产写在正文前,避免部分主题/解析器只截取首段导致样式丢失 if ($assetCss !== '' || $assetJs !== '') { $html = $assetCss . $assetJs . (string)$html; } $wrap = isset($settings->rewrite_wrap_bang) ? (string)$settings->rewrite_wrap_bang : '0'; if ($wrap === '1') { // Trim 只去两端空白,避免破坏内部格式 $html = trim((string)$html); if ($html !== '') { $html = "!!!\n" . $html . "\n!!!"; } } return $html; } /** * 个人用户的配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form * @return void */ public static function personalConfig(Typecho_Widget_Helper_Form $form) { } public static function linksInstall() { $installDb = Typecho_Db::get(); $type = explode('_', $installDb->getAdapterName()); $type = array_pop($type); $prefix = $installDb->getPrefix(); $scripts = file_get_contents('usr/plugins/Links/' . $type . '.sql'); $scripts = str_replace('typecho_', $prefix, $scripts); $scripts = str_replace('%charset%', 'utf8', $scripts); $scripts = explode(';', $scripts); try { foreach ($scripts as $script) { $script = trim($script); if ($script) { $installDb->query($script, Typecho_Db::WRITE); } } return _t('建立友情链接数据表,插件启用成功'); } catch (Typecho_Db_Exception $e) { $code = $e->getCode(); if (('Mysql' == $type && (1050 == $code || '42S01' == $code)) || ('SQLite' == $type && ('HY000' == $code || 1 == $code)) ) { try { $script = 'SELECT `lid`, `name`, `url`, `sort`, `email`, `image`, `description`, `user`, `state`, `order` from `' . $prefix . 'links`'; $installDb->query($script, Typecho_Db::READ); return _t('检测到友情链接数据表,友情链接插件启用成功'); } catch (Typecho_Db_Exception $e) { $code = $e->getCode(); if (('Mysql' == $type && (1054 == $code || '42S22' == $code)) || ('SQLite' == $type && ('HY000' == $code || 1 == $code)) ) { return Links_Plugin::linksUpdate($installDb, $type, $prefix); } throw new Typecho_Plugin_Exception(_t('数据表检测失败,友情链接插件启用失败。错误号:') . $code); } } else { throw new Typecho_Plugin_Exception(_t('数据表建立失败,友情链接插件启用失败。错误号:') . $code); } } } public static function linksUpdate($installDb, $type, $prefix) { $scripts = file_get_contents('usr/plugins/Links/Update_' . $type . '.sql'); $scripts = str_replace('typecho_', $prefix, $scripts); $scripts = str_replace('%charset%', 'utf8', $scripts); $scripts = explode(';', $scripts); try { foreach ($scripts as $script) { $script = trim($script); if ($script) { $installDb->query($script, Typecho_Db::WRITE); } } return _t('检测到旧版本友情链接数据表,升级成功'); } catch (Typecho_Db_Exception $e) { $code = $e->getCode(); if (('Mysql' == $type && (1060 == $code || '42S21' == $code))) { return _t('友情链接数据表已经存在,插件启用成功'); } throw new Typecho_Plugin_Exception(_t('友情链接插件启用失败。错误号:') . $code); } } public static function form($action = null) { /** 构建表格 */ $options = Typecho_Widget::widget('Widget_Options'); $form = new Typecho_Widget_Helper_Form( Helper::security()->getIndex('/action/links-edit'), Typecho_Widget_Helper_Form::POST_METHOD ); /** 友链名称 */ $name = new Typecho_Widget_Helper_Form_Element_Text('name', null, null, _t('友链名称*')); $form->addInput($name); /** 友链地址 */ $url = new Typecho_Widget_Helper_Form_Element_Text('url', null, "http://", _t('友链地址*')); $form->addInput($url); /** 友链分类 */ $sort = new Typecho_Widget_Helper_Form_Element_Text('sort', null, null, _t('友链分类'), _t('建议以英文字母开头,只包含字母与数字')); $form->addInput($sort); /** 友链邮箱 */ $email = new Typecho_Widget_Helper_Form_Element_Text('email', null, null, _t('友链邮箱'), _t('填写友链邮箱')); $form->addInput($email); /** 友链图片 */ $image = new Typecho_Widget_Helper_Form_Element_Text('image', null, null, _t('友链图片'), _t('需要以http://或https://开头,留空表示没有友链图片')); $form->addInput($image); /** 友链描述 */ $description = new Typecho_Widget_Helper_Form_Element_Textarea('description', null, null, _t('友链描述')); $form->addInput($description); /** 自定义数据 */ $user = new Typecho_Widget_Helper_Form_Element_Text('user', null, null, _t('自定义数据'), _t('该项用于用户自定义数据扩展')); $form->addInput($user); /** 友链状态 */ $list = array('0' => '禁用', '1' => '启用'); $state = new Typecho_Widget_Helper_Form_Element_Radio('state', $list, '1', '友链状态'); $form->addInput($state); /** 友链动作 */ $do = new Typecho_Widget_Helper_Form_Element_Hidden('do'); $form->addInput($do); /** 友链主键 */ $lid = new Typecho_Widget_Helper_Form_Element_Hidden('lid'); $form->addInput($lid); /** 提交按钮 */ $submit = new Typecho_Widget_Helper_Form_Element_Submit(); $submit->input->setAttribute('class', 'btn primary'); $form->addItem($submit); $request = Typecho_Request::getInstance(); if (isset($request->lid) && 'insert' != $action) { /** 更新模式 */ $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $link = $db->fetchRow($db->select()->from($prefix . 'links')->where('lid = ?', $request->lid)); if (!$link) { throw new Typecho_Widget_Exception(_t('友链不存在'), 404); } $name->value($link['name']); $url->value($link['url']); $sort->value($link['sort']); $email->value($link['email']); $image->value($link['image']); $description->value($link['description']); $user->value($link['user']); $state->value($link['state']); $do->value('update'); $lid->value($link['lid']); $submit->value(_t('编辑友链')); $_action = 'update'; } else { $do->value('insert'); $submit->value(_t('增加友链')); $_action = 'insert'; } if (empty($action)) { $action = $_action; } /** 给表单增加规则 */ if ('insert' == $action || 'update' == $action) { $name->addRule('required', _t('必须填写友链名称')); $url->addRule('required', _t('必须填写友链地址')); $url->addRule('url', _t('不是一个合法的链接地址')); $email->addRule('email', _t('不是一个合法的邮箱地址')); $image->addRule('url', _t('不是一个合法的图片地址')); $name->addRule('maxLength', _t('友链名称最多包含50个字符'), 50); $url->addRule('maxLength', _t('友链地址最多包含200个字符'), 200); $sort->addRule('maxLength', _t('友链分类最多包含50个字符'), 50); $email->addRule('maxLength', _t('友链邮箱最多包含50个字符'), 50); $image->addRule('maxLength', _t('友链图片最多包含200个字符'), 200); $description->addRule('maxLength', _t('友链描述最多包含200个字符'), 200); $user->addRule('maxLength', _t('自定义数据最多包含200个字符'), 200); } if ('update' == $action) { $lid->addRule('required', _t('友链主键不存在')); $lid->addRule(array(new Links_Plugin, 'LinkExists'), _t('友链不存在')); } return $form; } public static function LinkExists($lid) { $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $link = $db->fetchRow($db->select()->from($prefix . 'links')->where('lid = ?', $lid)->limit(1)); return $link ? true : false; } /** * 控制输出格式 */ public static function output_str($widget, array $params) { $options = Typecho_Widget::widget('Widget_Options'); $settings = $options->plugin('Links'); if (!isset($options->plugins['activated']['Links'])) { return _t('友情链接插件未激活'); } //验证默认参数 $pattern = !empty($params[0]) && is_string($params[0]) ? $params[0] : 'SHOW_TEXT'; $links_num = !empty($params[1]) && is_numeric($params[1]) ? $params[1] : 0; $sort = !empty($params[2]) && is_string($params[2]) ? $params[2] : null; $size = !empty($params[3]) && is_numeric($params[3]) ? $params[3] : $settings->dsize; $mode = isset($params[4]) ? $params[4] : 'FUNC'; // 文件模板调用:TPL:template-name $tplManifest = null; $tplName = null; if (is_string($pattern) && stripos($pattern, 'TPL:') === 0) { $tplName = trim(substr($pattern, 4)); $templates = self::listTemplates(); if ($tplName !== '' && isset($templates[$tplName])) { $tplManifest = $templates[$tplName]; $tplHtml = self::readTemplateFile($tplName, 'template.html'); if ($tplHtml !== null && trim($tplHtml) !== '') { $pattern = $tplHtml . "\n"; } } } // 兼容旧模式字符串(优先模板选择,其次旧 textarea 规则) if ($pattern == 'SHOW_TEXT') { $tpl = isset($settings->template_text) ? trim((string)$settings->template_text) : ''; if ($tpl !== '') { $tplName = $tpl; $templates = self::listTemplates(); if (isset($templates[$tplName])) { $tplManifest = $templates[$tplName]; $tplHtml = self::readTemplateFile($tplName, 'template.html'); if ($tplHtml !== null && trim($tplHtml) !== '') { $pattern = $tplHtml . "\n"; } else { $pattern = $settings->pattern_text . "\n"; } } else { $pattern = $settings->pattern_text . "\n"; } } else { $pattern = $settings->pattern_text . "\n"; } } elseif ($pattern == 'SHOW_IMG') { $tpl = isset($settings->template_img) ? trim((string)$settings->template_img) : ''; if ($tpl !== '') { $tplName = $tpl; $templates = self::listTemplates(); if (isset($templates[$tplName])) { $tplManifest = $templates[$tplName]; $tplHtml = self::readTemplateFile($tplName, 'template.html'); if ($tplHtml !== null && trim($tplHtml) !== '') { $pattern = $tplHtml . "\n"; } else { $pattern = $settings->pattern_img . "\n"; } } else { $pattern = $settings->pattern_img . "\n"; } } else { $pattern = $settings->pattern_img . "\n"; } } elseif ($pattern == 'SHOW_MIX') { $tpl = isset($settings->template_mix) ? trim((string)$settings->template_mix) : ''; if ($tpl !== '') { $tplName = $tpl; $templates = self::listTemplates(); if (isset($templates[$tplName])) { $tplManifest = $templates[$tplName]; $tplHtml = self::readTemplateFile($tplName, 'template.html'); if ($tplHtml !== null && trim($tplHtml) !== '') { $pattern = $tplHtml . "\n"; } else { $pattern = $settings->pattern_mix . "\n"; } } else { $pattern = $settings->pattern_mix . "\n"; } } else { $pattern = $settings->pattern_mix . "\n"; } } $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $nopic_url = Typecho_Common::url('usr/plugins/Links/nopic.png', $options->siteUrl); $sql = $db->select()->from($prefix . 'links'); if ($sort) { $sql = $sql->where('sort=?', $sort); } $sql = $sql->order($prefix . 'links.order', Typecho_Db::SORT_ASC); $links_num = intval($links_num); if ($links_num > 0) { $sql = $sql->limit($links_num); } $links = $db->fetchAll($sql); $str = ""; foreach ($links as $link) { if ($link['image'] == null) { $link['image'] = $nopic_url; if ($link['email'] != null) { $link['image'] = 'https://gravatar.helingqi.com/wavatar/' . md5($link['email']) . '?s=' . $size . '&d=mm'; } } if ($link['state'] == 1) { $str .= str_replace( array('{lid}', '{name}', '{url}', '{sort}', '{title}', '{description}', '{image}', '{user}', '{size}'), array($link['lid'], $link['name'], $link['url'], $link['sort'], $link['description'], $link['description'], $link['image'], $link['user'], $size), $pattern ); } } // 注入模板资源: // - pattern = TPL:xxx // - 或 SHOW_* 映射到 template_text/img/mix 时同样需要注入 if (!empty($tplName) && !empty($tplManifest) && is_array($tplManifest)) { self::injectTemplateAssetsOnce($tplName, $tplManifest); } if ($mode == 'HTML') { return $str; } else { echo $str; } } //输出 public static function output($pattern = 'SHOW_TEXT', $links_num = 0, $sort = null, $size = 32, $mode = '') { return Links_Plugin::output_str('', array($pattern, $links_num, $sort, $size, $mode)); } /** * 解析 * * @access public * @param array $matches 解析值 * @return string */ public static function parseCallback($matches) { // 兼容 这种空参数用法: // - 数量/分类/尺寸为空时使用默认值 // - 标签内容为空时默认使用 SHOW_TEXT $linksNum = (isset($matches[1]) && $matches[1] !== '') ? $matches[1] : 0; $sort = (isset($matches[2]) && $matches[2] !== '') ? $matches[2] : null; $size = (isset($matches[3]) && $matches[3] !== '') ? $matches[3] : 0; $pattern = (isset($matches[4]) && trim($matches[4]) !== '') ? trim($matches[4]) : 'SHOW_TEXT'; return Links_Plugin::output_str('', array($pattern, $linksNum, $sort, $size, 'HTML')); } public static function parse($text, $widget, $lastResult) { $text = empty($lastResult) ? $text : $lastResult; // Shortcode: [links_plus] 支持(仅当在插件配置中启用) try { $options = Typecho_Widget::widget('Widget_Options'); $settings = $options->plugin('Links'); $shortcodes = isset($settings->enable_shortcodes) ? $settings->enable_shortcodes : array(); } catch (Exception $e) { $shortcodes = array(); } // 开发中,短代码 if (is_array($shortcodes) && in_array('links_plus', $shortcodes)) { // 简单匹配 [links_plus] 或带参数形式 [links_plus num=5 sort=friends size=48 template=SHOW_IMG] $text = preg_replace_callback( '/\[links_plus(?:\s+([^\]]+))?\]/i', function ($m) { $args = array(); if (!empty($m[1])) { // 解析 key=value 或 单纯数字的简写(作为数量) $str = trim($m[1]); // 支持 num=5 sort=friends size=48 template=SHOW_IMG preg_match_all('/(\w+)\s*=\s*"([^"]*)"|(\w+)\s*=\s*\'([^\']*)\'|(\w+)\s*=\s*([^\s"]+)/', $str, $ms, PREG_SET_ORDER); foreach ($ms as $row) { foreach ($row as $r) { // noop } } // 另外也支持仅数字形式 if (preg_match('/^\d+$/', $str)) { $args['num'] = (int)$str; } else { // 尝试解析常见的 key=value,容错简单实现 $parts = preg_split('/\s+/', $str); foreach ($parts as $p) { if (strpos($p, '=') !== false) { list($k, $v) = explode('=', $p, 2); $k = trim($k); $v = trim($v, " \t\n\r\0\x0B\"'"); $args[$k] = $v; } } } } $num = isset($args['num']) ? (int)$args['num'] : 0; $sort = isset($args['sort']) ? $args['sort'] : null; $size = isset($args['size']) ? (int)$args['size'] : 0; $pattern = isset($args['template']) ? $args['template'] : 'SHOW_TEXT'; return Links_Plugin::output_str('', array($pattern, $num, $sort, $size, 'HTML')); }, $text ); } if ($widget instanceof Widget_Archive || $widget instanceof Widget_Abstract_Comments) { // 支持: // // // // SHOW_IMG // 分类允许使用常见 slug(字母/数字/下划线/连字符) // 且允许标签的 " > " 前存在空白 // 更健壮的短标签解析: // 1) 允许 带任意 HTML 属性(比如 ) // 2) 允许参数之间/标签两侧出现任意空白与换行 // 3) 参数定义:数量(数字) 分类(非 < > 空白) 尺寸(数字) // - 分类允许中文/连字符/下划线等,只要不包含空白与尖括号 // 4) 标签内容为 pattern(SHOW_TEXT/SHOW_IMG/SHOW_MIX 或自定义模板名) $regex = "/]*)?\\s*(\\d*)\\s*([^\\s<>]*)\\s*(\\d*)\\s*>\\s*(.*?)\\s*<\\/links>/is"; return preg_replace_callback( $regex, array('Links_Plugin', 'parseCallback'), $text ? $text : '' ); } else { return $text; } } }