├── 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 |
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 |
--------------------------------------------------------------------------------