├── upgrade.lock ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── .gitattributes ├── LICENSE ├── Upgrade.php ├── README.md └── Plugin.php /upgrade.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | .github/* 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: 存在疑问 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: 请求增加特性 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | README.md merge=ignore 2 | 3 | * text=auto 4 | 5 | *.php text eol=lf 6 | *.js text eol=lf 7 | *.css text eol=lf 8 | *.html text eol=lf 9 | *.xml text eol=lf 10 | *.md text eol=lf 11 | .git* text eol=lf 12 | 13 | *.png binary 14 | *.jpg binary 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 上报 Bug 4 | title: '' 5 | labels: bug 6 | assignees: wuxianucw 7 | 8 | --- 9 | 10 | **Bug 描述** 11 | 12 | 13 | **如何触发** 14 | 15 | 16 | **截图** 17 | 18 | 19 | **环境信息** 20 | - PHP 版本:[例如:7.2] 21 | - Typecho 版本:[例如:1.1(20.05.15) 开发版] 22 | 23 | ----- 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wuxianucw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Upgrade.php: -------------------------------------------------------------------------------- 1 | getCode()}): {$e->getMessage()} in {$e->getFile()} on line {$e->getLine()}\n"); 28 | }); 29 | output("Start upgrading...\nDetails will also be written to the log file (./upgrade.log). If the connection lost, you can check it later.\n"); 30 | output("==========\n\n"); 31 | $db = Typecho_Db::get(); 32 | $rows = $db->fetchAll($db->select('cid')->from('table.fields')->where('name = ?', 'pp_isEnabled')->where('str_value = ?', '1')); 33 | output("Scanning posts...\n"); 34 | foreach($rows as $row) { 35 | $cid = $row['cid']; 36 | output("cid = {$cid}, pp_isEnable = 1, checking...\n"); 37 | $sep = $db->fetchRow($db->select('str_value')->from('table.fields')->where('cid = ?', $cid)->where('name = ?', 'pp_sep')); 38 | if (!$sep) { 39 | output(" -> No \"pp_sep\" field, skip.\n"); 40 | continue; 41 | } 42 | $sep = $sep['str_value']; 43 | output(" -> pp_sep = \"{$sep}\"\n"); 44 | $pwds = $db->fetchRow($db->select('str_value')->from('table.fields')->where('cid = ?', $cid)->where('name = ?', 'pp_passwords')); 45 | if (!$pwds) { 46 | output(" -> No \"pp_passwords\" field, what the hell?\n"); 47 | continue; 48 | } 49 | $pwds = $pwds['str_value']; 50 | $before = $pwds; 51 | if ($pwds) { 52 | if (!$sep) $pwds = array($pwds); 53 | else $pwds = explode($sep, $pwds); 54 | $pwds = json_encode($pwds); 55 | } 56 | output(" -> Automatically converting pp_passwords \"{$before}\" to \"{$pwds}\"...\n"); 57 | $db->query( 58 | $db->update('table.fields')->rows(array('str_value' => $pwds))->where('cid = ?', $cid)->where('name = ?', 'pp_passwords'), 59 | Typecho_Db::WRITE 60 | ); 61 | output(" -> Removing \"pp_passwords\" field...\n"); 62 | $db->query( 63 | $db->delete('table.fields')->where('cid = ?', $cid)->where('name = ?', 'pp_sep'), 64 | Typecho_Db::WRITE 65 | ); 66 | output(" -> Done.\n"); 67 | } 68 | output("==========\n\n"); 69 | quit("All works completed successfully. For security reasons, the script will be locked.\n"); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typecho 文章部分加密插件(PartiallyPassword) 2 | 3 | Typecho 文章部分加密插件(PartiallyPassword)支持对某一篇文章的特定部分创建密码,访客需要正确输入密码才能查看内容。 4 | 5 | ## 安装 Installation 6 | 7 | ### A. 直接下载 8 | 9 | 访问本项目的 Release 页面,下载最新的 Release 版本,解压后将其中的文件夹重命名为 `PartiallyPassword`(如果它原本不是这个名字)并移动到 Typecho 插件目录下。 10 | 11 | ### B. 从仓库克隆 12 | 13 | 在 Typecho 插件目录下启动终端,执行命令即可。 14 | 15 | ```bash 16 | git clone -b master --single-branch https://github.com/wuxianucw/PartiallyPassword.git 17 | ``` 18 | 19 | 或下载压缩包(`Download ZIP`)并解压,将其中的文件夹重命名为 `PartiallyPassword` (如果它原本不是这个名字)并移动到 Typecho 插件目录下。 20 | 21 | ## 使用方法 Usage 22 | 23 | ### 初始化 24 | 25 | #### 全新安装 26 | 27 | 启用插件,即完成全部初始化工作。默认配置是一套极简风格的密码输入框,你也可以根据主题特性进行自定义修改。 28 | 29 | #### 从旧版本升级 30 | 31 | 首先备份插件配置(手动将其拷贝到文本文档中等方法),然后禁用旧版本插件。用新版本插件文件覆盖旧版本插件文件,再启用新版本并从备份中恢复配置。 32 | 33 | **注意:** 如果旧版本是 v2.x 版本,请在删除插件目录下的 `upgrade.lock` 文件后运行一次 `Upgrade.php`(在浏览器中打开 `http(s)://你的博客地址/usr/plugins/PartiallyPassword/Upgrade.php`),它将会自动升级 v2.x 的文章自定义字段配置到 v3 配置(JSON 方案)。 34 | 35 | **确认无误后** ,删除 `Upgrade.php`,保留脚本自动创建的 `upgrade.lock` 以防止误操作,并将 `upgrade.log` 拷贝到其他位置备用。如果在执行脚本中遇到问题(`Error`、`Exception` 字样),请提出 issue(类型为 `Bug report`)并附带上 `upgrade.log`。 36 | 37 | ### 调用方法举例 38 | 39 | #### 基础用法 40 | 41 | **在书写加密语法之前,请先将对应文章下方“自定义字段”中“是否开启文章部分加密”一项调整为“开启”状态。** 该项目默认为“关闭”状态,在此情况下,任何加密语法都不会被解析。 42 | 43 | 下面的所有例子都包含一个“文本部分”和一个“配置部分”,其中上方的“文本部分”是需要在 Typecho 编辑器中书写的内容,下方的“配置部分”是需要在文章下方“自定义字段”中“密码组”一项内填入的内容。 44 | 45 | ```text 46 | 不需要密码的内容 47 | 48 | [ppblock] 49 | 输入密码可见的内容 50 | [/ppblock] 51 | 52 | 不需要密码的内容 53 | ``` 54 | 55 | ```json 56 | ["123456"] 57 | ``` 58 | 59 | 这就是一个最简单的例子。你也可以进一步给加密块添加附加信息: 60 | 61 | ```text 62 | [ppblock ex="请输入密码"] 63 | 输入密码可见的内容 64 | [/ppblock] 65 | ``` 66 | 67 | ```json 68 | ["123456"] 69 | ``` 70 | 71 | 附加信息将会在输入密码处显示。 72 | 73 | 如果你想书写一段 `[ppblock]...[/ppblock]` 形式的文本,但不希望它被解析,请使用 `[[ppblock]...[/ppblock]]`,两侧多余的方括号会被自动移除。 74 | 75 | #### 插入多个加密块 76 | 77 | ```text 78 | 不需要密码的内容 79 | 80 | [ppblock] 81 | 需要密码的内容 A,id = 0 82 | [/ppblock] 83 | 84 | 不需要密码的内容 85 | 86 | [ppblock pwd="喵"] 87 | 需要密码的内容 B,id = 1 88 | [/ppblock] 89 | 90 | 不需要密码的内容 91 | 92 | [ppblock ex="给我密码"] 93 | 需要密码的内容 C,id = 2 94 | [/ppblock] 95 | 96 | 不需要密码的内容 97 | ``` 98 | 99 | ```json 100 | { 101 | "0": "123456", 102 | "喵": "miao~", 103 | "2": "000000" 104 | } 105 | ``` 106 | 107 | 每个加密块都会被赋予一个 `id`,它从 0 开始依次增加。加密块密码的寻找逻辑如下: 108 | 109 | 1. 检查 `pwd` 属性 110 | - 若该属性存在,从第 2 步继续后续操作 → 111 | - 若该属性不存在,从第 3 步继续后续操作 → 112 | 2. 寻找 JSON 中是否有索引为 `pwd` 的值的项目 113 | - 是,使用该项目作为当前块的密码,结束 √ 114 | - 否,从第 4 步继续后续操作 → 115 | 3. 寻找 JSON 中是否有索引为 `id` 的值的项目 116 | - 是,使用该项目作为当前块的密码,结束 √ 117 | - 否,从第 4 步继续后续操作 → 118 | 4. 寻找 JSON 中是否有索引为 `fallback` 的项目 119 | - 是,使用该项目作为当前块的密码,结束 √ 120 | - 否,当前块展现为“密码未设置”错误提示 × 121 | 122 | 在上例中,三个加密块的密码依次是 `123456`、`miao~` 和 `000000`。 123 | 124 | 下面的例子演示 `fallback` 的功能: 125 | 126 | ```text 127 | 不需要密码的内容 128 | 129 | [ppblock] 130 | 需要密码的内容 A,id = 0 131 | [/ppblock] 132 | 133 | 不需要密码的内容 134 | 135 | [ppblock pwd="喵" ex="给我密码"] 136 | 需要密码的内容 B,id = 1 137 | [/ppblock] 138 | 139 | 不需要密码的内容 140 | ``` 141 | 142 | ```json 143 | { 144 | "1": "miao~", 145 | "fallback": "123456" 146 | } 147 | ``` 148 | 149 | 这个例子中,两个加密块的密码都是 `123456`。第二个加密块虽然指定了 `pwd` 属性,但密码组中没有对应的项目,因此也不再检查是否有符合 `id` 的项目,而是直接使用 `fallback`。 150 | 151 | 这种使用 `pwd` 属性指定密码的方式称为“命名密码”。当只使用索引时,密码组配置可以简写为一个 `string[]` 类型的 JSON 数组,例如: 152 | 153 | ```text 154 | 不需要密码的内容 155 | 156 | [ppblock] 157 | 需要密码的内容 A,id = 0 158 | [/ppblock] 159 | 160 | 不需要密码的内容 161 | 162 | [ppblock ex="给我密码"] 163 | 需要密码的内容 B,id = 1 164 | [/ppblock] 165 | 166 | 不需要密码的内容 167 | ``` 168 | 169 | ```json 170 | ["123456", "miao~"] 171 | ``` 172 | 173 | 这在加密块比较少或密码不重用时非常方便。 174 | 175 | #### 不同密码对应不同内容 176 | 177 | 自 v3.0.0 起,引入了一种新的标记 `ppswitch`。下面是一个例子: 178 | 179 | ```text 180 | 不需要密码的内容 181 | 182 | [ppswitch] 183 | 公共内容(可选) 184 | [case pwd="p1"] 185 | 输入了 p1 的情况 186 | [/case] 187 | [case pwd="p2"] 188 | 输入了 p2 的情况 189 | [/case] 190 | [/ppswitch] 191 | ``` 192 | 193 | ```json 194 | { 195 | "p1": "111111", 196 | "p2": "222222" 197 | } 198 | ``` 199 | 200 | 当未输入密码时,展现为: 201 | 202 | ```text 203 | 不需要密码的内容 204 | 205 | ``` 206 | 207 | 如果输入 `111111`(即 `p1` 的值),则展现为: 208 | 209 | ```text 210 | 不需要密码的内容 211 | 212 | 公共内容(可选) 213 | 输入了 p1 的情况 214 | ``` 215 | 216 | 输入 `222222`(即 `p2` 的值)后的展现类推。 217 | 218 | 有两点需要特别注意: 219 | 220 | 1. `case` 标记必须指定 `pwd` 属性,否则无论如何都不会显示,而且,除非指定 `pwd="fallback"`,否则不会在找不到密码时自动采用 `fallback` 的值; 221 | 2. `ppswitch` 与 `ppblock` 共用一套 `id` 系统,尽管 `ppswitch` 默认不会寻找密码组中索引为 `id` 的值的项目。 222 | 223 | 关于第二点,下面这个例子可能能够帮助理解: 224 | 225 | ```text 226 | [ppblock] 227 | 这个加密块的 id = 0 228 | [/ppblock] 229 | 230 | [ppswitch] 231 | 这个加密块的 id = 1 232 | [case pwd="0"] 233 | 你输入了上一个加密块的密码 234 | [/case] 235 | [case pwd="2"] 236 | 你输入了下一个加密块的密码 237 | [/case] 238 | [/ppswitch] 239 | 240 | [ppblock] 241 | 这个加密块的 id = 2 242 | [/ppblock] 243 | ``` 244 | 245 | ```json 246 | { 247 | "0": "000000", 248 | "2": "123456" 249 | } 250 | ``` 251 | 252 | 可以看到,中间的 `ppswitch` 占用了一个 `id`,但是无法直接通过它的 `id` 为它指定密码(除非设置 `[case pwd="1"]`,但这时它是一个索引为 `1` 的命名密码)。这种设计是出于多个 `case` 时难以规定默认行为的考虑。 253 | 254 | ### 提示 255 | 256 | - 请勿不成对或嵌套地使用标记,它的展现无法预期。 257 | 258 | ## TODO List 259 | 260 | - [x] 在 `Widget_Abstract_Contents` 的 `excerpt` 下挂接函数,屏蔽所有 `[ppblock]` 以及其中的内容,不判断 Cookie。(Since v1.1.0) 261 | - [x] ~~寻找一个方案可以直接操作 `$widget->text` 取出的内容,实现完美屏蔽。~~ 已经更改为在 `Widget_Abstract_Contents` 的 `filter` 下挂接插件实现方法,这样操作后从 Widget 中取出的数据已经全部进行了过滤,除非直接读取数据库,否则理论上不存在加密区块不解析的情形。(Since v2.0.0) 262 | - [x] ~~现有的鉴权逻辑较为不完善,应增加提交密码时的后端相关处理,并合理优化流程。~~ 已经完全交由后端处理 Cookie,流程变更为直接向文章页面 POST 数据。(Since v2.0.0) 263 | - [x] ~~默认外观需要优化,包括样式和插入位置。~~ 已经完成优化,现在的默认样式是一套极简风格的密码输入框。(Since v1.1.1)公共 HTML 的插入位置变更为页头和页脚。(Since v2.0.0) 264 | - [x] ~~考虑增加加密区块语法支持,后续将可能支持更加复杂的语法。具体方案暂时未定。~~ 新增 `ppswitch` 语法,能够实现不同密码对应不同内容([#2](https://github.com/wuxianucw/PartiallyPassword/issues/2))。(Since v3.0.0) 265 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | filter = array('PartiallyPassword_Plugin', 'render'); 21 | Typecho_Plugin::factory('Widget_Contents_Post_Edit')->getDefaultFieldItems = array('PartiallyPassword_Plugin', 'pluginFields'); 22 | Typecho_Plugin::factory('Widget_Archive')->singleHandle = array('PartiallyPassword_Plugin', 'handleSubmit'); 23 | Typecho_Plugin::factory('Widget_Archive')->header = array('PartiallyPassword_Plugin', 'header'); 24 | Typecho_Plugin::factory('Widget_Archive')->footer = array('PartiallyPassword_Plugin', 'footer'); 25 | } 26 | 27 | /** 28 | * 禁用插件 29 | * 30 | * @static 31 | * @access public 32 | * @return void 33 | * @throws Typecho_Plugin_Exception 34 | */ 35 | public static function deactivate() {} 36 | 37 | /** 38 | * 获取插件配置面板 39 | * 40 | * @access public 41 | * @param Typecho_Widget_Helper_Form $form 配置面板 42 | * @return void 43 | */ 44 | public static function config(Typecho_Widget_Helper_Form $form) { 45 | /** Referer 检查 */ 46 | $form->addInput(new Typecho_Widget_Helper_Form_Element_Select( 47 | 'checkReferer', 48 | array(0 => '关闭', 1 => '开启'), 49 | 1, 50 | _t('Referer 检查'), 51 | '若开启,将对每个密码请求进行 Referer 检查。' 52 | )); 53 | 54 | /** 额外 Markdown 标记 */ 55 | $form->addInput(new Typecho_Widget_Helper_Form_Element_Select( 56 | 'extraMdToken', 57 | array(0 => '开启', 1 => '关闭'), 58 | 0, 59 | _t('额外 Markdown 标记'), 60 | '若开启,对于 Markdown 格式的文章,将在每个加密块首尾插入 !!! 标记。本配置为兼容性配置,' . 61 | '对于 Typecho 1.1 默认的 HyperDown 解析器,需要保持开启以确保 HTML 生效。如果您在使用过程中发现加密块' . 62 | '前后有多余的 !!! 标记,请尝试将本配置项设置为关闭。' 63 | )); 64 | 65 | /** 自定义页头 HTML */ 66 | $default = << 68 | .pp-block {text-align: center; border-radius:3px; background-color: rgba(0, 0, 0, 0.45); padding: 20px 0 20px 0;} 69 | .pp-block>form>input {height: 24px; border: 1px solid #fff; background-color: transparent; width: 50%; border-radius: 3px; color: #fff; text-align: center;} 70 | .pp-block>form>input::placeholder{color: #fff;} 71 | .pp-block>form>input::-webkit-input-placeholder{color: #fff;} 72 | .pp-block>form>input::-moz-placeholder{color: #fff;} 73 | .pp-block>form>input:-moz-placeholder{color: #fff;} 74 | .pp-block>form>input:-ms-input-placeholder{color: #fff;} 75 | 76 | TEXT; 77 | $tips = <<addInput(new Typecho_Widget_Helper_Form_Element_Textarea('header', NULL, $default, _t('自定义页头 HTML'), $tips)); 81 | 82 | /** 自定义页脚 HTML */ 83 | $default = << 85 | // Powered by wuxianucw 86 | console.log('PartiallyPassword is enabled.'); 87 | 88 | TEXT; 89 | $tips = <<addInput(new Typecho_Widget_Helper_Form_Element_Textarea('footer', NULL, $default, _t('自定义页脚 HTML'), $tips)); 93 | 94 | /** 密码区域 HTML */ 95 | $default = << 97 |
98 | 99 | 100 |
101 | 102 | TEXT; 103 | $tips = << 106 |
  • {id}:当前加密块 ID
  • 107 |
  • {additionalContent}:附加信息
  • 108 |
  • {currentPage}:当前页面 URL
  • 109 |
  • {cid}:当前日志 ID
  • 110 |
  • {targetUrl}:POST 提交接口页面URL
  • 111 | 112 | TEXT; 113 | $form->addInput(new Typecho_Widget_Helper_Form_Element_Textarea('placeholder', NULL, $default, _t('密码区域 HTML'), $tips)); 114 | } 115 | 116 | /** 117 | * 个人用户的配置面板 118 | * 119 | * @access public 120 | * @param Typecho_Widget_Helper_Form $form 121 | * @return void 122 | */ 123 | public static function personalConfig(Typecho_Widget_Helper_Form $form) {} 124 | 125 | /** 126 | * 自定义输出header 127 | * 128 | * @access public 129 | * @param string $header 130 | * @param Widget_Archive $archive 131 | * @return void 132 | */ 133 | public static function header($header, Widget_Archive $archive) { 134 | @$header_html = Helper::options()->plugin('PartiallyPassword')->header; 135 | if ($header_html) echo $header_html; 136 | } 137 | 138 | /** 139 | * 自定义输出footer 140 | * 141 | * @access public 142 | * @param Widget_Archive $archive 143 | * @return void 144 | */ 145 | public static function footer(Widget_Archive $archive) { 146 | @$footer_html = Helper::options()->plugin('PartiallyPassword')->footer; 147 | if ($footer_html) echo $footer_html; 148 | } 149 | 150 | /** 151 | * 取得请求发送的密码 152 | * 153 | * @access private 154 | * @param mixed $cid 155 | * @param mixed $pid 156 | * @param mixed $currentCid 157 | * @return string 158 | */ 159 | private static function getRequestPassword($cid, $pid, $currentCid = -1) { 160 | if ($currentCid == -1) $currentCid = $cid; 161 | $request = new Typecho_Request(); 162 | $request_pid = isset($request->pid) ? intval($request->pid) : 0; 163 | if ($request->isPost() && isset($request->partiallyPassword) && $request_pid === $pid) { 164 | if (@Helper::options()->plugin('PartiallyPassword')->checkReferer) 165 | if (stripos($request->getReferer(), Helper::options()->rootUrl) !== 0) return; 166 | return (new PasswordHash(8, true))->HashPassword($request->partiallyPassword); 167 | } 168 | return Typecho_Cookie::get("partiallyPassword_{$cid}_{$pid}", ''); 169 | } 170 | 171 | /** 172 | * 插件实现方法 173 | * 174 | * @access public 175 | * @param array $value 176 | * @param Widget_Abstract_Contents $contents 177 | * @return string 178 | */ 179 | public static function render($value, Widget_Abstract_Contents $contents) { 180 | if ($value['type'] != 'page' && $value['type'] != 'post') return $value; 181 | if (defined('__TYPECHO_ADMIN__')) { 182 | if ($value['authorId'] != $contents->widget('Widget_User')->uid && !$contents->widget('Widget_User')->pass('editor', true)) 183 | $value['hidden'] = true; 184 | return $value; 185 | } 186 | $fields = array(); 187 | $db = Typecho_Db::get(); 188 | $rows = $db->fetchAll($db->select()->from('table.fields')->where('cid = ?', $value['cid'])); 189 | foreach ($rows as $row) { 190 | $fields[$row['name']] = $row[$row['type'] . '_value']; 191 | } 192 | $fields = new Typecho_Config($fields); 193 | if ($fields->pp_isEnabled) { 194 | @$pwds = json_decode($fields->pp_passwords, true); 195 | if (!is_array($pwds)) $pwds = array(); 196 | array_map('strval', $pwds); 197 | $options = Helper::options()->plugin('PartiallyPassword'); 198 | $placeholder = isset($options->placeholder) ? $options->placeholder : ''; 199 | $extraMdToken = isset($options->extraMdToken) ? intval($options->extraMdToken) : 0; 200 | if (!$placeholder) $placeholder = '
    请配置密码区域 HTML!
    '; 201 | if ($value['isMarkdown'] && $extraMdToken === 0) $placeholder = "\n!!!\n{$placeholder}\n!!!\n"; 202 | $hasher = new PasswordHash(8, true); 203 | $value['text'] = preg_replace_callback( 204 | '/' . self::getShortcodeRegex(array('ppblock', 'ppswitch')) . '/', 205 | function($matches) use ($contents, $value, $pwds, $placeholder, $extraMdToken, $hasher) { 206 | static $id = -1; 207 | if ($matches[1] == '[' && $matches[6] == ']') return substr($matches[0], 1, -1); // 不解析类似 [[ppblock]] 双重括号的代码 208 | $id++; 209 | $attrs = self::shortcodeParseAtts($matches[3]); // 获取短代码的参数 210 | $ex = ''; 211 | $pwd_idx = strval($id); 212 | if (is_array($attrs)) { 213 | if (isset($attrs['ex'])) $ex = $attrs['ex']; 214 | if (isset($attrs['pwd'])) $pwd_idx = $attrs['pwd']; 215 | } 216 | $inner = trim($matches[5]); 217 | $input = self::getRequestPassword($value['cid'], $id, $contents->cid); 218 | if ($matches[2] == 'ppswitch') { 219 | if (!$input) { 220 | $placeholder = str_replace( 221 | array('{id}', '{currentPage}', '{cid}', '{additionalContent}', '{targetUrl}'), 222 | array($id, $value['permalink'], $value['cid'], $ex, $value['permalink']), 223 | $placeholder 224 | ); 225 | return $placeholder; 226 | } 227 | $succ = false; 228 | $inner = preg_replace_callback( 229 | '/' . self::getShortcodeRegex(array('case')) . '/', 230 | function($matches) use ($pwds, $input, $hasher, &$succ) { 231 | if ($matches[1] == '[' && $matches[6] == ']') return substr($matches[0], 1, -1); 232 | $attrs = self::shortcodeParseAtts($matches[3]); 233 | if (!isset($attrs['pwd']) || !in_array($attrs['pwd'], array_keys($pwds))) return ''; 234 | if ($hasher->CheckPassword($pwds[$attrs['pwd']], $input)) { 235 | $succ = true; 236 | return trim($matches[5]); 237 | } else return ''; 238 | }, 239 | $inner 240 | ); 241 | if ($succ) return $inner; 242 | else { 243 | $placeholder = str_replace( 244 | array('{id}', '{currentPage}', '{cid}','{additionalContent}', '{targetUrl}'), 245 | array($id, $value['permalink'], $value['cid'], $ex, $value['permalink']), 246 | $placeholder 247 | ); 248 | return $placeholder; 249 | } 250 | } 251 | if (!in_array($pwd_idx, array_keys($pwds))) { 252 | if (isset($pwds['fallback'])) $pwd_idx = 'fallback'; 253 | else { 254 | $err = "
    错误:id = {$id} 的加密块未设置密码!
    "; 255 | if ($value['isMarkdown'] && $extraMdToken === 0) $err = "\n!!!\n{$err}\n!!!\n"; 256 | return $err; 257 | } 258 | } 259 | if ($input && $hasher->CheckPassword($pwds[$pwd_idx], $input)) return $inner; 260 | else { 261 | $placeholder = str_replace( 262 | array('{id}', '{currentPage}', '{cid}', '{additionalContent}', '{targetUrl}'), 263 | array($id, $value['permalink'], $value['cid'], $ex, $value['permalink']), 264 | $placeholder 265 | ); 266 | return $placeholder; 267 | } 268 | }, 269 | $value['text'] 270 | ); 271 | } 272 | return $value; 273 | } 274 | 275 | /** 276 | * 插件自定义字段 277 | * 278 | * @access public 279 | * @param mixed $layout 280 | * @return void 281 | */ 282 | public static function pluginFields($layout) { 283 | $layout->addItem(new Typecho_Widget_Helper_Form_Element_Select( 284 | 'pp_isEnabled', 285 | array(0 => '关闭', 1 => '开启'), 286 | 0, 287 | _t('是否开启文章部分加密'), 288 | '是否对这篇文章启用部分加密功能' 289 | )); 290 | $layout->addItem(new Typecho_Widget_Helper_Form_Element_Textarea( 291 | 'pp_passwords', 292 | NULL, 293 | '', 294 | _t('密码组'), 295 | 'JSON 格式的密码组,参考 README' 296 | )); 297 | } 298 | 299 | /** 300 | * 处理密码提交 301 | * 302 | * @access public 303 | * @param Widget_Archive $archive 304 | * @param Typecho_Db_Query $select 305 | * @return void 306 | */ 307 | public static function handleSubmit(Widget_Archive $archive, Typecho_Db_Query $select) { 308 | if (!$archive->is('page') && !$archive->is('post')) return; 309 | if ($archive->fields->pp_isEnabled && $archive->request->isPost() && isset($archive->request->partiallyPassword)) { 310 | $pid = isset($archive->request->pid) ? intval($archive->request->pid) : 0; 311 | if ($pid < 0) return; 312 | if (@Helper::options()->plugin('PartiallyPassword')->checkReferer) 313 | if (stripos($archive->request->getReferer(), Helper::options()->rootUrl) !== 0) return; 314 | Typecho_Cookie::set( 315 | "partiallyPassword_{$archive->cid}_{$pid}", 316 | (new PasswordHash(8, true))->HashPassword($archive->request->partiallyPassword) 317 | ); 318 | } 319 | } 320 | 321 | /** 322 | * 获取匹配短代码的正则表达式 323 | * 324 | * @access protected 325 | * @param string $tagnames 326 | * @return string 327 | * @link https://github.com/WordPress/WordPress/blob/master/wp-includes/shortcodes.php 328 | */ 329 | protected static function getShortcodeRegex($tagnames) { 330 | $tagregexp = implode('|', array_map('preg_quote', $tagnames)); 331 | // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcode_tag() 332 | // Also, see shortcode_unautop() and shortcode.js. 333 | // phpcs:disable Squiz.Strings.ConcatenationSpacing.PaddingFound -- don't remove regex indentation 334 | return 335 | '\\[' // Opening bracket 336 | . '(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]] 337 | . "($tagregexp)" // 2: Shortcode name 338 | . '(?![\\w-])' // Not followed by word character or hyphen 339 | . '(' // 3: Unroll the loop: Inside the opening shortcode tag 340 | . '[^\\]\\/]*' // Not a closing bracket or forward slash 341 | . '(?:' 342 | . '\\/(?!\\])' // A forward slash not followed by a closing bracket 343 | . '[^\\]\\/]*' // Not a closing bracket or forward slash 344 | . ')*?' 345 | . ')' 346 | . '(?:' 347 | . '(\\/)' // 4: Self closing tag ... 348 | . '\\]' // ... and closing bracket 349 | . '|' 350 | . '\\]' // Closing bracket 351 | . '(?:' 352 | . '(' // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags 353 | . '[^\\[]*+' // Not an opening bracket 354 | . '(?:' 355 | . '\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag 356 | . '[^\\[]*+' // Not an opening bracket 357 | . ')*+' 358 | . ')' 359 | . '\\[\\/\\2\\]' // Closing shortcode tag 360 | . ')?' 361 | . ')' 362 | . '(\\]?)'; // 6: Optional second closing brocket for escaping shortcodes: [[tag]] 363 | // phpcs:enable 364 | } 365 | 366 | /** 367 | * 获取短代码属性数组 368 | * 369 | * @access protected 370 | * @param $text 371 | * @return array|string 372 | * @link https://github.com/WordPress/WordPress/blob/master/wp-includes/shortcodes.php 373 | */ 374 | protected static function shortcodeParseAtts($text) { 375 | $atts = array(); 376 | $pattern = self::getShortcodeAttsRegex(); 377 | $text = preg_replace( "/[\x{00a0}\x{200b}]+/u", ' ', $text ); 378 | if (preg_match_all($pattern, $text, $match, PREG_SET_ORDER)) { 379 | foreach ($match as $m) { 380 | if (!empty($m[1])) { 381 | $atts[strtolower($m[1])] = stripcslashes($m[2]); 382 | } elseif (!empty($m[3])) { 383 | $atts[strtolower($m[3])] = stripcslashes($m[4]); 384 | } elseif (!empty($m[5])) { 385 | $atts[strtolower($m[5])] = stripcslashes($m[6]); 386 | } elseif (isset($m[7]) && strlen($m[7])) { 387 | $atts[] = stripcslashes($m[7]); 388 | } elseif (isset($m[8]) && strlen($m[8])) { 389 | $atts[] = stripcslashes($m[8]); 390 | } elseif (isset($m[9])) { 391 | $atts[] = stripcslashes($m[9]); 392 | } 393 | } 394 | // Reject any unclosed HTML elements 395 | foreach ($atts as &$value) { 396 | if (strpos($value, '<') !== false) { 397 | if (preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value) !== 1) { 398 | $value = ''; 399 | } 400 | } 401 | } 402 | } else { 403 | $atts = ltrim($text); 404 | } 405 | return $atts; 406 | } 407 | 408 | /** 409 | * 获取短代码属性正则表达式 410 | * 411 | * @access private 412 | * @return string 413 | * @link https://github.com/WordPress/WordPress/blob/master/wp-includes/shortcodes.php 414 | */ 415 | private static function getShortcodeAttsRegex() { 416 | return '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; 417 | } 418 | } 419 | --------------------------------------------------------------------------------