diff options
Diffstat (limited to 'plugins/markdown/markdown.php')
-rw-r--r-- | plugins/markdown/markdown.php | 365 |
1 files changed, 0 insertions, 365 deletions
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php deleted file mode 100644 index f6f66cc5..00000000 --- a/plugins/markdown/markdown.php +++ /dev/null | |||
@@ -1,365 +0,0 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Plugin Markdown. | ||
5 | * | ||
6 | * Shaare's descriptions are parsed with Markdown. | ||
7 | */ | ||
8 | |||
9 | use Shaarli\Config\ConfigManager; | ||
10 | use Shaarli\Plugin\PluginManager; | ||
11 | use Shaarli\Router; | ||
12 | |||
13 | /* | ||
14 | * If this tag is used on a shaare, the description won't be processed by Parsedown. | ||
15 | */ | ||
16 | define('NO_MD_TAG', 'nomarkdown'); | ||
17 | |||
18 | /** | ||
19 | * Parse linklist descriptions. | ||
20 | * | ||
21 | * @param array $data linklist data. | ||
22 | * @param ConfigManager $conf instance. | ||
23 | * | ||
24 | * @return mixed linklist data parsed in markdown (and converted to HTML). | ||
25 | */ | ||
26 | function hook_markdown_render_linklist($data, $conf) | ||
27 | { | ||
28 | foreach ($data['links'] as &$value) { | ||
29 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { | ||
30 | $value = stripNoMarkdownTag($value); | ||
31 | continue; | ||
32 | } | ||
33 | $value['description_src'] = $value['description']; | ||
34 | $value['description'] = process_markdown( | ||
35 | $value['description'], | ||
36 | $conf->get('security.markdown_escape', true), | ||
37 | $conf->get('security.allowed_protocols') | ||
38 | ); | ||
39 | } | ||
40 | return $data; | ||
41 | } | ||
42 | |||
43 | /** | ||
44 | * Parse feed linklist descriptions. | ||
45 | * | ||
46 | * @param array $data linklist data. | ||
47 | * @param ConfigManager $conf instance. | ||
48 | * | ||
49 | * @return mixed linklist data parsed in markdown (and converted to HTML). | ||
50 | */ | ||
51 | function hook_markdown_render_feed($data, $conf) | ||
52 | { | ||
53 | foreach ($data['links'] as &$value) { | ||
54 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { | ||
55 | $value = stripNoMarkdownTag($value); | ||
56 | continue; | ||
57 | } | ||
58 | $value['description'] = reverse_feed_permalink($value['description']); | ||
59 | $value['description'] = process_markdown( | ||
60 | $value['description'], | ||
61 | $conf->get('security.markdown_escape', true), | ||
62 | $conf->get('security.allowed_protocols') | ||
63 | ); | ||
64 | } | ||
65 | |||
66 | return $data; | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * Parse daily descriptions. | ||
71 | * | ||
72 | * @param array $data daily data. | ||
73 | * @param ConfigManager $conf instance. | ||
74 | * | ||
75 | * @return mixed daily data parsed in markdown (and converted to HTML). | ||
76 | */ | ||
77 | function hook_markdown_render_daily($data, $conf) | ||
78 | { | ||
79 | //var_dump($data);die; | ||
80 | // Manipulate columns data | ||
81 | foreach ($data['linksToDisplay'] as &$value) { | ||
82 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { | ||
83 | $value = stripNoMarkdownTag($value); | ||
84 | continue; | ||
85 | } | ||
86 | $value['formatedDescription'] = process_markdown( | ||
87 | $value['formatedDescription'], | ||
88 | $conf->get('security.markdown_escape', true), | ||
89 | $conf->get('security.allowed_protocols') | ||
90 | ); | ||
91 | } | ||
92 | |||
93 | return $data; | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Check if noMarkdown is set in tags. | ||
98 | * | ||
99 | * @param string $tags tag list | ||
100 | * | ||
101 | * @return bool true if markdown should be disabled on this link. | ||
102 | */ | ||
103 | function noMarkdownTag($tags) | ||
104 | { | ||
105 | return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags); | ||
106 | } | ||
107 | |||
108 | /** | ||
109 | * Remove the no-markdown meta tag so it won't be displayed. | ||
110 | * | ||
111 | * @param array $link Link data. | ||
112 | * | ||
113 | * @return array Updated link without no markdown tag. | ||
114 | */ | ||
115 | function stripNoMarkdownTag($link) | ||
116 | { | ||
117 | if (! empty($link['taglist'])) { | ||
118 | $offset = array_search(NO_MD_TAG, $link['taglist']); | ||
119 | if ($offset !== false) { | ||
120 | unset($link['taglist'][$offset]); | ||
121 | } | ||
122 | } | ||
123 | |||
124 | if (!empty($link['tags'])) { | ||
125 | str_replace(NO_MD_TAG, '', $link['tags']); | ||
126 | } | ||
127 | |||
128 | return $link; | ||
129 | } | ||
130 | |||
131 | /** | ||
132 | * When link list is displayed, include markdown CSS. | ||
133 | * | ||
134 | * @param array $data includes data. | ||
135 | * | ||
136 | * @return mixed - includes data with markdown CSS file added. | ||
137 | */ | ||
138 | function hook_markdown_render_includes($data) | ||
139 | { | ||
140 | if ($data['_PAGE_'] == Router::$PAGE_LINKLIST | ||
141 | || $data['_PAGE_'] == Router::$PAGE_DAILY | ||
142 | || $data['_PAGE_'] == Router::$PAGE_EDITLINK | ||
143 | ) { | ||
144 | $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css'; | ||
145 | } | ||
146 | |||
147 | return $data; | ||
148 | } | ||
149 | |||
150 | /** | ||
151 | * Hook render_editlink. | ||
152 | * Adds an help link to markdown syntax. | ||
153 | * | ||
154 | * @param array $data data passed to plugin | ||
155 | * | ||
156 | * @return array altered $data. | ||
157 | */ | ||
158 | function hook_markdown_render_editlink($data) | ||
159 | { | ||
160 | // Load help HTML into a string | ||
161 | $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); | ||
162 | $translations = [ | ||
163 | t('Description will be rendered with'), | ||
164 | t('Markdown syntax documentation'), | ||
165 | t('Markdown syntax'), | ||
166 | ]; | ||
167 | $data['edit_link_plugin'][] = vsprintf($txt, $translations); | ||
168 | // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion. | ||
169 | if (! in_array(NO_MD_TAG, $data['tags'])) { | ||
170 | $data['tags'][NO_MD_TAG] = 0; | ||
171 | } | ||
172 | |||
173 | return $data; | ||
174 | } | ||
175 | |||
176 | |||
177 | /** | ||
178 | * Remove HTML links auto generated by Shaarli core system. | ||
179 | * Keeps HREF attributes. | ||
180 | * | ||
181 | * @param string $description input description text. | ||
182 | * | ||
183 | * @return string $description without HTML links. | ||
184 | */ | ||
185 | function reverse_text2clickable($description) | ||
186 | { | ||
187 | $descriptionLines = explode(PHP_EOL, $description); | ||
188 | $descriptionOut = ''; | ||
189 | $codeBlockOn = false; | ||
190 | $lineCount = 0; | ||
191 | |||
192 | foreach ($descriptionLines as $descriptionLine) { | ||
193 | // Detect line of code: starting with 4 spaces, | ||
194 | // except lists which can start with +/*/- or `2.` after spaces. | ||
195 | $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0; | ||
196 | // Detect and toggle block of code | ||
197 | if (!$codeBlockOn) { | ||
198 | $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0; | ||
199 | } elseif (preg_match('/^```/', $descriptionLine) > 0) { | ||
200 | $codeBlockOn = false; | ||
201 | } | ||
202 | |||
203 | $hashtagTitle = ' title="Hashtag [^"]+"'; | ||
204 | // Reverse `inline code` hashtags. | ||
205 | $descriptionLine = preg_replace( | ||
206 | '!(`[^`\n]*)<a href="[^ ]*"'. $hashtagTitle .'>([^<]+)</a>([^`\n]*`)!m', | ||
207 | '$1$2$3', | ||
208 | $descriptionLine | ||
209 | ); | ||
210 | |||
211 | // Reverse all links in code blocks, only non hashtag elsewhere. | ||
212 | $hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?'; | ||
213 | $descriptionLine = preg_replace( | ||
214 | '#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m', | ||
215 | '$1', | ||
216 | $descriptionLine | ||
217 | ); | ||
218 | |||
219 | // Make hashtag links markdown ready, otherwise the links will be ignored with escape set to true | ||
220 | if (!$codeBlockOn && !$codeLineOn) { | ||
221 | $descriptionLine = preg_replace( | ||
222 | '#<a href="([^ ]*)"'. $hashtagTitle .'>([^<]+)</a>#m', | ||
223 | '[$2]($1)', | ||
224 | $descriptionLine | ||
225 | ); | ||
226 | } | ||
227 | |||
228 | $descriptionOut .= $descriptionLine; | ||
229 | if ($lineCount++ < count($descriptionLines) - 1) { | ||
230 | $descriptionOut .= PHP_EOL; | ||
231 | } | ||
232 | } | ||
233 | return $descriptionOut; | ||
234 | } | ||
235 | |||
236 | /** | ||
237 | * Remove <br> tag to let markdown handle it. | ||
238 | * | ||
239 | * @param string $description input description text. | ||
240 | * | ||
241 | * @return string $description without <br> tags. | ||
242 | */ | ||
243 | function reverse_nl2br($description) | ||
244 | { | ||
245 | return preg_replace('!<br */?>!im', '', $description); | ||
246 | } | ||
247 | |||
248 | /** | ||
249 | * Remove HTML spaces ' ' auto generated by Shaarli core system. | ||
250 | * | ||
251 | * @param string $description input description text. | ||
252 | * | ||
253 | * @return string $description without HTML links. | ||
254 | */ | ||
255 | function reverse_space2nbsp($description) | ||
256 | { | ||
257 | return preg_replace('/(^| ) /m', '$1 ', $description); | ||
258 | } | ||
259 | |||
260 | function reverse_feed_permalink($description) | ||
261 | { | ||
262 | return preg_replace('@— <a href="([^"]+)" title="[^"]+">([^<]+)</a>$@im', '— [$2]($1)', $description); | ||
263 | } | ||
264 | |||
265 | /** | ||
266 | * Replace not whitelisted protocols with http:// in given description. | ||
267 | * | ||
268 | * @param string $description input description text. | ||
269 | * @param array $allowedProtocols list of allowed protocols. | ||
270 | * | ||
271 | * @return string $description without malicious link. | ||
272 | */ | ||
273 | function filter_protocols($description, $allowedProtocols) | ||
274 | { | ||
275 | return preg_replace_callback( | ||
276 | '#]\((.*?)\)#is', | ||
277 | function ($match) use ($allowedProtocols) { | ||
278 | return ']('. whitelist_protocols($match[1], $allowedProtocols) .')'; | ||
279 | }, | ||
280 | $description | ||
281 | ); | ||
282 | } | ||
283 | |||
284 | /** | ||
285 | * Remove dangerous HTML tags (tags, iframe, etc.). | ||
286 | * Doesn't affect <code> content (already escaped by Parsedown). | ||
287 | * | ||
288 | * @param string $description input description text. | ||
289 | * | ||
290 | * @return string given string escaped. | ||
291 | */ | ||
292 | function sanitize_html($description) | ||
293 | { | ||
294 | $escapeTags = array( | ||
295 | 'script', | ||
296 | 'style', | ||
297 | 'link', | ||
298 | 'iframe', | ||
299 | 'frameset', | ||
300 | 'frame', | ||
301 | ); | ||
302 | foreach ($escapeTags as $tag) { | ||
303 | $description = preg_replace_callback( | ||
304 | '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', | ||
305 | function ($match) { | ||
306 | return escape($match[0]); | ||
307 | }, | ||
308 | $description | ||
309 | ); | ||
310 | } | ||
311 | $description = preg_replace( | ||
312 | '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is', | ||
313 | '$1', | ||
314 | $description | ||
315 | ); | ||
316 | return $description; | ||
317 | } | ||
318 | |||
319 | /** | ||
320 | * Render shaare contents through Markdown parser. | ||
321 | * 1. Remove HTML generated by Shaarli core. | ||
322 | * 2. Reverse the escape function. | ||
323 | * 3. Generate markdown descriptions. | ||
324 | * 4. Sanitize sensible HTML tags for security. | ||
325 | * 5. Wrap description in 'markdown' CSS class. | ||
326 | * | ||
327 | * @param string $description input description text. | ||
328 | * @param bool $escape escape HTML entities | ||
329 | * | ||
330 | * @return string HTML processed $description. | ||
331 | */ | ||
332 | function process_markdown($description, $escape = true, $allowedProtocols = []) | ||
333 | { | ||
334 | $parsedown = new Parsedown(); | ||
335 | |||
336 | $processedDescription = $description; | ||
337 | $processedDescription = reverse_nl2br($processedDescription); | ||
338 | $processedDescription = reverse_space2nbsp($processedDescription); | ||
339 | $processedDescription = reverse_text2clickable($processedDescription); | ||
340 | $processedDescription = filter_protocols($processedDescription, $allowedProtocols); | ||
341 | $processedDescription = unescape($processedDescription); | ||
342 | $processedDescription = $parsedown | ||
343 | ->setMarkupEscaped($escape) | ||
344 | ->setBreaksEnabled(true) | ||
345 | ->text($processedDescription); | ||
346 | $processedDescription = sanitize_html($processedDescription); | ||
347 | |||
348 | if (!empty($processedDescription)) { | ||
349 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | ||
350 | } | ||
351 | |||
352 | return $processedDescription; | ||
353 | } | ||
354 | |||
355 | /** | ||
356 | * This function is never called, but contains translation calls for GNU gettext extraction. | ||
357 | */ | ||
358 | function markdown_dummy_translation() | ||
359 | { | ||
360 | // meta | ||
361 | t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>: | ||
362 | If your shaared descriptions contained HTML tags before enabling the markdown plugin, | ||
363 | enabling it might break your page. | ||
364 | See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.'); | ||
365 | } | ||