diff options
-rw-r--r-- | application/Utils.php | 43 | ||||
-rw-r--r-- | inc/shaarli.css | 4 | ||||
-rw-r--r-- | index.php | 31 | ||||
-rw-r--r-- | plugins/markdown/markdown.php | 37 | ||||
-rw-r--r-- | tests/plugins/PluginMarkdownTest.php | 19 | ||||
-rw-r--r-- | tpl/pluginsadmin.html | 16 |
6 files changed, 101 insertions, 49 deletions
diff --git a/application/Utils.php b/application/Utils.php index 10d60698..3d819716 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -62,7 +62,11 @@ function endsWith($haystack, $needle, $case=true) | |||
62 | } | 62 | } |
63 | 63 | ||
64 | /** | 64 | /** |
65 | * htmlspecialchars wrapper | 65 | * Htmlspecialchars wrapper |
66 | * | ||
67 | * @param string $str the string to escape. | ||
68 | * | ||
69 | * @return string escaped. | ||
66 | */ | 70 | */ |
67 | function escape($str) | 71 | function escape($str) |
68 | { | 72 | { |
@@ -70,6 +74,18 @@ function escape($str) | |||
70 | } | 74 | } |
71 | 75 | ||
72 | /** | 76 | /** |
77 | * Reverse the escape function. | ||
78 | * | ||
79 | * @param string $str the string to unescape. | ||
80 | * | ||
81 | * @return string unescaped string. | ||
82 | */ | ||
83 | function unescape($str) | ||
84 | { | ||
85 | return htmlspecialchars_decode($str); | ||
86 | } | ||
87 | |||
88 | /** | ||
73 | * Link sanitization before templating | 89 | * Link sanitization before templating |
74 | */ | 90 | */ |
75 | function sanitizeLink(&$link) | 91 | function sanitizeLink(&$link) |
@@ -213,3 +229,28 @@ function space2nbsp($text) | |||
213 | function format_description($description, $redirector) { | 229 | function format_description($description, $redirector) { |
214 | return nl2br(space2nbsp(text2clickable($description, $redirector))); | 230 | return nl2br(space2nbsp(text2clickable($description, $redirector))); |
215 | } | 231 | } |
232 | |||
233 | /** | ||
234 | * Sniff browser language to set the locale automatically. | ||
235 | * Note that is may not work on your server if the corresponding locale is not installed. | ||
236 | * | ||
237 | * @param string $headerLocale Locale send in HTTP headers (e.g. "fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3"). | ||
238 | **/ | ||
239 | function autoLocale($headerLocale) | ||
240 | { | ||
241 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE | ||
242 | $attempts = array('en_US'); | ||
243 | if (isset($headerLocale)) { | ||
244 | // (It's a bit crude, but it works very well. Preferred language is always presented first.) | ||
245 | if (preg_match('/([a-z]{2})-?([a-z]{2})?/i', $headerLocale, $matches)) { | ||
246 | $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : ''); | ||
247 | $attempts = array( | ||
248 | $loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc), | ||
249 | $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc), | ||
250 | $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8', | ||
251 | $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc | ||
252 | ); | ||
253 | } | ||
254 | } | ||
255 | setlocale(LC_ALL, $attempts); | ||
256 | } \ No newline at end of file | ||
diff --git a/inc/shaarli.css b/inc/shaarli.css index 2e41988e..7a9d9afb 100644 --- a/inc/shaarli.css +++ b/inc/shaarli.css | |||
@@ -1151,6 +1151,10 @@ ul.errors { | |||
1151 | margin: 10px 0; | 1151 | margin: 10px 0; |
1152 | } | 1152 | } |
1153 | 1153 | ||
1154 | #pluginsadmin label { | ||
1155 | cursor: pointer; | ||
1156 | } | ||
1157 | |||
1154 | #pluginsadmin .plugin_parameter { | 1158 | #pluginsadmin .plugin_parameter { |
1155 | padding: 5px 0; | 1159 | padding: 5px 0; |
1156 | border-width: 1px 0; | 1160 | border-width: 1px 0; |
@@ -268,7 +268,7 @@ $GLOBALS['redirector'] = !empty($GLOBALS['redirector']) ? escape($GLOBALS['redir | |||
268 | // a token depending of deployment salt, user password, and the current ip | 268 | // a token depending of deployment salt, user password, and the current ip |
269 | define('STAY_SIGNED_IN_TOKEN', sha1($GLOBALS['hash'].$_SERVER["REMOTE_ADDR"].$GLOBALS['salt'])); | 269 | define('STAY_SIGNED_IN_TOKEN', sha1($GLOBALS['hash'].$_SERVER["REMOTE_ADDR"].$GLOBALS['salt'])); |
270 | 270 | ||
271 | autoLocale(); // Sniff browser language and set date format accordingly. | 271 | autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']); // Sniff browser language and set date format accordingly. |
272 | header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper international characters handling. | 272 | header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper international characters handling. |
273 | 273 | ||
274 | //================================================================================================== | 274 | //================================================================================================== |
@@ -315,26 +315,6 @@ function setup_login_state() { | |||
315 | } | 315 | } |
316 | $userIsLoggedIn = setup_login_state(); | 316 | $userIsLoggedIn = setup_login_state(); |
317 | 317 | ||
318 | |||
319 | // ------------------------------------------------------------------------------------------ | ||
320 | // Sniff browser language to display dates in the right format automatically. | ||
321 | // (Note that is may not work on your server if the corresponding local is not installed.) | ||
322 | function autoLocale() | ||
323 | { | ||
324 | $attempts = array('en_US'); // Default if browser does not send HTTP_ACCEPT_LANGUAGE | ||
325 | if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) // e.g. "fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3" | ||
326 | { // (It's a bit crude, but it works very well. Preferred language is always presented first.) | ||
327 | if (preg_match('/([a-z]{2})-?([a-z]{2})?/i',$_SERVER['HTTP_ACCEPT_LANGUAGE'],$matches)) { | ||
328 | $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : ''); | ||
329 | $attempts = array($loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc), | ||
330 | $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc), | ||
331 | $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8', | ||
332 | $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc); | ||
333 | } | ||
334 | } | ||
335 | setlocale(LC_TIME, $attempts); // LC_TIME = Set local for date/time format only. | ||
336 | } | ||
337 | |||
338 | // ------------------------------------------------------------------------------------------ | 318 | // ------------------------------------------------------------------------------------------ |
339 | // PubSubHubbub protocol support (if enabled) [UNTESTED] | 319 | // PubSubHubbub protocol support (if enabled) [UNTESTED] |
340 | // (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ ) | 320 | // (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ ) |
@@ -1243,11 +1223,12 @@ function renderPage() | |||
1243 | uksort($tags, function($a, $b) { | 1223 | uksort($tags, function($a, $b) { |
1244 | // Collator is part of PHP intl. | 1224 | // Collator is part of PHP intl. |
1245 | if (class_exists('Collator')) { | 1225 | if (class_exists('Collator')) { |
1246 | $c = new Collator(setlocale(LC_ALL, 0)); | 1226 | $c = new Collator(setlocale(LC_COLLATE, 0)); |
1247 | return $c->compare($a, $b); | 1227 | if (!intl_is_failure(intl_get_error_code())) { |
1248 | } else { | 1228 | return $c->compare($a, $b); |
1249 | return strcasecmp($a, $b); | 1229 | } |
1250 | } | 1230 | } |
1231 | return strcasecmp($a, $b); | ||
1251 | }); | 1232 | }); |
1252 | 1233 | ||
1253 | $tagList=array(); | 1234 | $tagList=array(); |
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php index 3630ef14..a45b6574 100644 --- a/plugins/markdown/markdown.php +++ b/plugins/markdown/markdown.php | |||
@@ -117,23 +117,43 @@ function reverse_space2nbsp($description) | |||
117 | } | 117 | } |
118 | 118 | ||
119 | /** | 119 | /** |
120 | * Remove '>' at start of line auto generated by Shaarli core system | 120 | * Remove dangerous HTML tags (tags, iframe, etc.). |
121 | * to allow markdown blockquotes. | 121 | * Doesn't affect <code> content (already escaped by Parsedown). |
122 | * | 122 | * |
123 | * @param string $description input description text. | 123 | * @param string $description input description text. |
124 | * | 124 | * |
125 | * @return string $description without HTML links. | 125 | * @return string given string escaped. |
126 | */ | 126 | */ |
127 | function reset_quote_tags($description) | 127 | function sanitize_html($description) |
128 | { | 128 | { |
129 | return preg_replace('/^( *)> /m', '$1> ', $description); | 129 | $escapeTags = array( |
130 | 'script', | ||
131 | 'style', | ||
132 | 'link', | ||
133 | 'iframe', | ||
134 | 'frameset', | ||
135 | 'frame', | ||
136 | ); | ||
137 | foreach ($escapeTags as $tag) { | ||
138 | $description = preg_replace_callback( | ||
139 | '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', | ||
140 | function ($match) { return escape($match[0]); }, | ||
141 | $description); | ||
142 | } | ||
143 | $description = preg_replace( | ||
144 | '#(<[^>]+)on[a-z]*="[^"]*"#is', | ||
145 | '$1', | ||
146 | $description); | ||
147 | return $description; | ||
130 | } | 148 | } |
131 | 149 | ||
132 | /** | 150 | /** |
133 | * Render shaare contents through Markdown parser. | 151 | * Render shaare contents through Markdown parser. |
134 | * 1. Remove HTML generated by Shaarli core. | 152 | * 1. Remove HTML generated by Shaarli core. |
135 | * 2. Generate markdown descriptions. | 153 | * 2. Reverse the escape function. |
136 | * 3. Wrap description in 'markdown' CSS class. | 154 | * 3. Generate markdown descriptions. |
155 | * 4. Sanitize sensible HTML tags for security. | ||
156 | * 5. Wrap description in 'markdown' CSS class. | ||
137 | * | 157 | * |
138 | * @param string $description input description text. | 158 | * @param string $description input description text. |
139 | * | 159 | * |
@@ -147,11 +167,12 @@ function process_markdown($description) | |||
147 | $processedDescription = reverse_text2clickable($processedDescription); | 167 | $processedDescription = reverse_text2clickable($processedDescription); |
148 | $processedDescription = reverse_nl2br($processedDescription); | 168 | $processedDescription = reverse_nl2br($processedDescription); |
149 | $processedDescription = reverse_space2nbsp($processedDescription); | 169 | $processedDescription = reverse_space2nbsp($processedDescription); |
150 | $processedDescription = reset_quote_tags($processedDescription); | 170 | $processedDescription = unescape($processedDescription); |
151 | $processedDescription = $parsedown | 171 | $processedDescription = $parsedown |
152 | ->setMarkupEscaped(false) | 172 | ->setMarkupEscaped(false) |
153 | ->setBreaksEnabled(true) | 173 | ->setBreaksEnabled(true) |
154 | ->text($processedDescription); | 174 | ->text($processedDescription); |
175 | $processedDescription = sanitize_html($processedDescription); | ||
155 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | 176 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; |
156 | 177 | ||
157 | return $processedDescription; | 178 | return $processedDescription; |
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php index 455f5ba7..8e1a128a 100644 --- a/tests/plugins/PluginMarkdownTest.php +++ b/tests/plugins/PluginMarkdownTest.php | |||
@@ -100,13 +100,18 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase | |||
100 | } | 100 | } |
101 | 101 | ||
102 | /** | 102 | /** |
103 | * Test reset_quote_tags() | 103 | * Test sanitize_html(). |
104 | */ | 104 | */ |
105 | function testResetQuoteTags() | 105 | function testSanitizeHtml() { |
106 | { | 106 | $input = '< script src="js.js"/>'; |
107 | $text = '> quote1'. PHP_EOL . ' > quote2 ' . PHP_EOL . 'noquote'; | 107 | $input .= '< script attr>alert(\'xss\');</script>'; |
108 | $processedText = escape($text); | 108 | $input .= '<style> * { display: none }</style>'; |
109 | $reversedText = reset_quote_tags($processedText); | 109 | $output = escape($input); |
110 | $this->assertEquals($text, $reversedText); | 110 | $input .= '<a href="#" onmouseHover="alert(\'xss\');" attr="tt">link</a>'; |
111 | $output .= '<a href="#" attr="tt">link</a>'; | ||
112 | $this->assertEquals($output, sanitize_html($input)); | ||
113 | // Do not touch escaped HTML. | ||
114 | $input = escape($input); | ||
115 | $this->assertEquals($input, sanitize_html($input)); | ||
111 | } | 116 | } |
112 | } | 117 | } |
diff --git a/tpl/pluginsadmin.html b/tpl/pluginsadmin.html index 4f7d091e..5ddcf061 100644 --- a/tpl/pluginsadmin.html +++ b/tpl/pluginsadmin.html | |||
@@ -36,7 +36,7 @@ | |||
36 | <tbody> | 36 | <tbody> |
37 | {loop="$enabledPlugins"} | 37 | {loop="$enabledPlugins"} |
38 | <tr data-line="{$key}" data-order="{$counter}"> | 38 | <tr data-line="{$key}" data-order="{$counter}"> |
39 | <td class="center"><input type="checkbox" name="{$key}" checked="checked"></td> | 39 | <td class="center"><input type="checkbox" name="{$key}" id="{$key}" checked="checked"></td> |
40 | <td class="center"> | 40 | <td class="center"> |
41 | <a href="#" | 41 | <a href="#" |
42 | onclick="return orderUp(this.parentNode.parentNode.getAttribute('data-order'));"> | 42 | onclick="return orderUp(this.parentNode.parentNode.getAttribute('data-order'));"> |
@@ -48,8 +48,8 @@ | |||
48 | </a> | 48 | </a> |
49 | <input type="hidden" name="order_{$key}" value="{$counter}"> | 49 | <input type="hidden" name="order_{$key}" value="{$counter}"> |
50 | </td> | 50 | </td> |
51 | <td>{$key}</td> | 51 | <td><label for="{$key}">{function="str_replace('_', ' ', $key)"}</label></td> |
52 | <td>{$value.description}</td> | 52 | <td><label for="{$key}">{$value.description}</label></td> |
53 | </tr> | 53 | </tr> |
54 | {/loop} | 54 | {/loop} |
55 | </tbody> | 55 | </tbody> |
@@ -73,9 +73,9 @@ | |||
73 | </tr> | 73 | </tr> |
74 | {loop="$disabledPlugins"} | 74 | {loop="$disabledPlugins"} |
75 | <tr> | 75 | <tr> |
76 | <td class="center"><input type="checkbox" name="{$key}"></td> | 76 | <td class="center"><input type="checkbox" name="{$key}" id="{$key}"></td> |
77 | <td>{$key}</td> | 77 | <td><label for="{$key}">{function="str_replace('_', ' ', $key)"}</label></td> |
78 | <td>{$value.description}</td> | 78 | <td><label for="{$key}">{$value.description}</label></td> |
79 | </tr> | 79 | </tr> |
80 | {/loop} | 80 | {/loop} |
81 | </table> | 81 | </table> |
@@ -99,7 +99,7 @@ | |||
99 | {loop="$enabledPlugins"} | 99 | {loop="$enabledPlugins"} |
100 | {if="count($value.parameters) > 0"} | 100 | {if="count($value.parameters) > 0"} |
101 | <div class="plugin_parameters"> | 101 | <div class="plugin_parameters"> |
102 | <h2>{$key}</h2> | 102 | <h2>{function="str_replace('_', ' ', $key)"}</h2> |
103 | {loop="$value.parameters"} | 103 | {loop="$value.parameters"} |
104 | <div class="plugin_parameter"> | 104 | <div class="plugin_parameter"> |
105 | <div class="float_label"> | 105 | <div class="float_label"> |
@@ -128,4 +128,4 @@ | |||
128 | 128 | ||
129 | <script src="inc/plugin_admin.js#"></script> | 129 | <script src="inc/plugin_admin.js#"></script> |
130 | </body> | 130 | </body> |
131 | </html> \ No newline at end of file | 131 | </html> |