aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/formatter
diff options
context:
space:
mode:
Diffstat (limited to 'application/formatter')
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php81
-rw-r--r--application/formatter/BookmarkFormatter.php256
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php198
-rw-r--r--application/formatter/BookmarkRawFormatter.php13
-rw-r--r--application/formatter/FormatterFactory.php46
5 files changed, 594 insertions, 0 deletions
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
new file mode 100644
index 00000000..7550c556
--- /dev/null
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -0,0 +1,81 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5/**
6 * Class BookmarkDefaultFormatter
7 *
8 * Default bookmark formatter.
9 * Escape values for HTML display and automatically add link to URL and hashtags.
10 *
11 * @package Shaarli\Formatter
12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter
14{
15 /**
16 * @inheritdoc
17 */
18 public function formatTitle($bookmark)
19 {
20 return escape($bookmark->getTitle());
21 }
22
23 /**
24 * @inheritdoc
25 */
26 public function formatDescription($bookmark)
27 {
28 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
29 return format_description(escape($bookmark->getDescription()), $indexUrl);
30 }
31
32 /**
33 * @inheritdoc
34 */
35 protected function formatTagList($bookmark)
36 {
37 return escape($bookmark->getTags());
38 }
39
40 /**
41 * @inheritdoc
42 */
43 public function formatTagString($bookmark)
44 {
45 return implode(' ', $this->formatTagList($bookmark));
46 }
47
48 /**
49 * @inheritdoc
50 */
51 public function formatUrl($bookmark)
52 {
53 if (! empty($this->contextData['index_url']) && (
54 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
55 )) {
56 return $this->contextData['index_url'] . escape($bookmark->getUrl());
57 }
58 return escape($bookmark->getUrl());
59 }
60
61 /**
62 * @inheritdoc
63 */
64 protected function formatRealUrl($bookmark)
65 {
66 if (! empty($this->contextData['index_url']) && (
67 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
68 )) {
69 return $this->contextData['index_url'] . escape($bookmark->getUrl());
70 }
71 return escape($bookmark->getUrl());
72 }
73
74 /**
75 * @inheritdoc
76 */
77 protected function formatThumbnail($bookmark)
78 {
79 return escape($bookmark->getThumbnail());
80 }
81}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
new file mode 100644
index 00000000..c82c3452
--- /dev/null
+++ b/application/formatter/BookmarkFormatter.php
@@ -0,0 +1,256 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Bookmark\Bookmark;
8
9/**
10 * Class BookmarkFormatter
11 *
12 * Abstract class processing all bookmark attributes through methods designed to be overridden.
13 *
14 * @package Shaarli\Formatter
15 */
16abstract class BookmarkFormatter
17{
18 /**
19 * @var ConfigManager
20 */
21 protected $conf;
22
23 /**
24 * @var array Additional parameters than can be used for specific formatting
25 * e.g. index_url for Feed formatting
26 */
27 protected $contextData = [];
28
29 /**
30 * LinkDefaultFormatter constructor.
31 * @param ConfigManager $conf
32 */
33 public function __construct(ConfigManager $conf)
34 {
35 $this->conf = $conf;
36 }
37
38 /**
39 * Convert a Bookmark into an array usable by templates and plugins.
40 *
41 * All Bookmark attributes are formatted through a format method
42 * that can be overridden in a formatter extending this class.
43 *
44 * @param Bookmark $bookmark instance
45 *
46 * @return array formatted representation of a Bookmark
47 */
48 public function format($bookmark)
49 {
50 $out['id'] = $this->formatId($bookmark);
51 $out['shorturl'] = $this->formatShortUrl($bookmark);
52 $out['url'] = $this->formatUrl($bookmark);
53 $out['real_url'] = $this->formatRealUrl($bookmark);
54 $out['title'] = $this->formatTitle($bookmark);
55 $out['description'] = $this->formatDescription($bookmark);
56 $out['thumbnail'] = $this->formatThumbnail($bookmark);
57 $out['taglist'] = $this->formatTagList($bookmark);
58 $out['tags'] = $this->formatTagString($bookmark);
59 $out['sticky'] = $bookmark->isSticky();
60 $out['private'] = $bookmark->isPrivate();
61 $out['class'] = $this->formatClass($bookmark);
62 $out['created'] = $this->formatCreated($bookmark);
63 $out['updated'] = $this->formatUpdated($bookmark);
64 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
65 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
66 return $out;
67 }
68
69 /**
70 * Add additional data available to formatters.
71 * This is used for example to add `index_url` in description's links.
72 *
73 * @param string $key Context data key
74 * @param string $value Context data value
75 */
76 public function addContextData($key, $value)
77 {
78 $this->contextData[$key] = $value;
79 }
80
81 /**
82 * Format ID
83 *
84 * @param Bookmark $bookmark instance
85 *
86 * @return int formatted ID
87 */
88 protected function formatId($bookmark)
89 {
90 return $bookmark->getId();
91 }
92
93 /**
94 * Format ShortUrl
95 *
96 * @param Bookmark $bookmark instance
97 *
98 * @return string formatted ShortUrl
99 */
100 protected function formatShortUrl($bookmark)
101 {
102 return $bookmark->getShortUrl();
103 }
104
105 /**
106 * Format Url
107 *
108 * @param Bookmark $bookmark instance
109 *
110 * @return string formatted Url
111 */
112 protected function formatUrl($bookmark)
113 {
114 return $bookmark->getUrl();
115 }
116
117 /**
118 * Format RealUrl
119 * Legacy: identical to Url
120 *
121 * @param Bookmark $bookmark instance
122 *
123 * @return string formatted RealUrl
124 */
125 protected function formatRealUrl($bookmark)
126 {
127 return $bookmark->getUrl();
128 }
129
130 /**
131 * Format Title
132 *
133 * @param Bookmark $bookmark instance
134 *
135 * @return string formatted Title
136 */
137 protected function formatTitle($bookmark)
138 {
139 return $bookmark->getTitle();
140 }
141
142 /**
143 * Format Description
144 *
145 * @param Bookmark $bookmark instance
146 *
147 * @return string formatted Description
148 */
149 protected function formatDescription($bookmark)
150 {
151 return $bookmark->getDescription();
152 }
153
154 /**
155 * Format Thumbnail
156 *
157 * @param Bookmark $bookmark instance
158 *
159 * @return string formatted Thumbnail
160 */
161 protected function formatThumbnail($bookmark)
162 {
163 return $bookmark->getThumbnail();
164 }
165
166 /**
167 * Format Tags
168 *
169 * @param Bookmark $bookmark instance
170 *
171 * @return array formatted Tags
172 */
173 protected function formatTagList($bookmark)
174 {
175 return $bookmark->getTags();
176 }
177
178 /**
179 * Format TagString
180 *
181 * @param Bookmark $bookmark instance
182 *
183 * @return string formatted TagString
184 */
185 protected function formatTagString($bookmark)
186 {
187 return implode(' ', $bookmark->getTags());
188 }
189
190 /**
191 * Format Class
192 * Used to add specific CSS class for a link
193 *
194 * @param Bookmark $bookmark instance
195 *
196 * @return string formatted Class
197 */
198 protected function formatClass($bookmark)
199 {
200 return $bookmark->isPrivate() ? 'private' : '';
201 }
202
203 /**
204 * Format Created
205 *
206 * @param Bookmark $bookmark instance
207 *
208 * @return DateTime instance
209 */
210 protected function formatCreated(Bookmark $bookmark)
211 {
212 return $bookmark->getCreated();
213 }
214
215 /**
216 * Format Updated
217 *
218 * @param Bookmark $bookmark instance
219 *
220 * @return DateTime instance
221 */
222 protected function formatUpdated(Bookmark $bookmark)
223 {
224 return $bookmark->getUpdated();
225 }
226
227 /**
228 * Format CreatedTimestamp
229 *
230 * @param Bookmark $bookmark instance
231 *
232 * @return int formatted CreatedTimestamp
233 */
234 protected function formatCreatedTimestamp(Bookmark $bookmark)
235 {
236 if (! empty($bookmark->getCreated())) {
237 return $bookmark->getCreated()->getTimestamp();
238 }
239 return 0;
240 }
241
242 /**
243 * Format UpdatedTimestamp
244 *
245 * @param Bookmark $bookmark instance
246 *
247 * @return int formatted UpdatedTimestamp
248 */
249 protected function formatUpdatedTimestamp(Bookmark $bookmark)
250 {
251 if (! empty($bookmark->getUpdated())) {
252 return $bookmark->getUpdated()->getTimestamp();
253 }
254 return 0;
255 }
256}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
new file mode 100644
index 00000000..f60c61f4
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -0,0 +1,198 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class BookmarkMarkdownFormatter
9 *
10 * Format bookmark description into Markdown format.
11 *
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
15{
16 /**
17 * When this tag is present in a bookmark, its description should not be processed with Markdown
18 */
19 const NO_MD_TAG = 'nomarkdown';
20
21 /** @var \Parsedown instance */
22 protected $parsedown;
23
24 /** @var bool used to escape HTML in Markdown or not.
25 * It MUST be set to true for shared instance as HTML content can
26 * introduce XSS vulnerabilities.
27 */
28 protected $escape;
29
30 /**
31 * @var array List of allowed protocols for links inside bookmark's description.
32 */
33 protected $allowedProtocols;
34
35 /**
36 * LinkMarkdownFormatter constructor.
37 *
38 * @param ConfigManager $conf instance
39 */
40 public function __construct(ConfigManager $conf)
41 {
42 parent::__construct($conf);
43 $this->parsedown = new \Parsedown();
44 $this->escape = $conf->get('security.markdown_escape', true);
45 $this->allowedProtocols = $conf->get('security.allowed_protocols', []);
46 }
47
48 /**
49 * @inheritdoc
50 */
51 public function formatDescription($bookmark)
52 {
53 if (in_array(self::NO_MD_TAG, $bookmark->getTags())) {
54 return parent::formatDescription($bookmark);
55 }
56
57 $processedDescription = $bookmark->getDescription();
58 $processedDescription = $this->filterProtocols($processedDescription);
59 $processedDescription = $this->formatHashTags($processedDescription);
60 $processedDescription = $this->parsedown
61 ->setMarkupEscaped($this->escape)
62 ->setBreaksEnabled(true)
63 ->text($processedDescription);
64 $processedDescription = $this->sanitizeHtml($processedDescription);
65
66 if (!empty($processedDescription)) {
67 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
68 }
69
70 return $processedDescription;
71 }
72
73 /**
74 * Remove the NO markdown tag if it is present
75 *
76 * @inheritdoc
77 */
78 protected function formatTagList($bookmark)
79 {
80 $out = parent::formatTagList($bookmark);
81 if (($pos = array_search(self::NO_MD_TAG, $out)) !== false) {
82 unset($out[$pos]);
83 return array_values($out);
84 }
85 return $out;
86 }
87
88 /**
89 * Replace not whitelisted protocols with http:// in given description.
90 * Also adds `index_url` to relative links if it's specified
91 *
92 * @param string $description input description text.
93 *
94 * @return string $description without malicious link.
95 */
96 protected function filterProtocols($description)
97 {
98 $allowedProtocols = $this->allowedProtocols;
99 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
100
101 return preg_replace_callback(
102 '#]\((.*?)\)#is',
103 function ($match) use ($allowedProtocols, $indexUrl) {
104 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
105 $link .= whitelist_protocols($match[1], $allowedProtocols);
106 return ']('. $link.')';
107 },
108 $description
109 );
110 }
111
112 /**
113 * Replace hashtag in Markdown links format
114 * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)`
115 * It includes the index URL if specified.
116 *
117 * @param string $description
118 *
119 * @return string
120 */
121 protected function formatHashTags($description)
122 {
123 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
124
125 /*
126 * To support unicode: http://stackoverflow.com/a/35498078/1484919
127 * \p{Pc} - to match underscore
128 * \p{N} - numeric character in any script
129 * \p{L} - letter from any language
130 * \p{Mn} - any non marking space (accents, umlauts, etc)
131 */
132 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
133 $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)';
134
135 $descriptionLines = explode(PHP_EOL, $description);
136 $descriptionOut = '';
137 $codeBlockOn = false;
138 $lineCount = 0;
139
140 foreach ($descriptionLines as $descriptionLine) {
141 // Detect line of code: starting with 4 spaces,
142 // except lists which can start with +/*/- or `2.` after spaces.
143 $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
144 // Detect and toggle block of code
145 if (!$codeBlockOn) {
146 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
147 } elseif (preg_match('/^```/', $descriptionLine) > 0) {
148 $codeBlockOn = false;
149 }
150
151 if (!$codeBlockOn && !$codeLineOn) {
152 $descriptionLine = preg_replace($regex, $replacement, $descriptionLine);
153 }
154
155 $descriptionOut .= $descriptionLine;
156 if ($lineCount++ < count($descriptionLines) - 1) {
157 $descriptionOut .= PHP_EOL;
158 }
159 }
160
161 return $descriptionOut;
162 }
163
164 /**
165 * Remove dangerous HTML tags (tags, iframe, etc.).
166 * Doesn't affect <code> content (already escaped by Parsedown).
167 *
168 * @param string $description input description text.
169 *
170 * @return string given string escaped.
171 */
172 protected function sanitizeHtml($description)
173 {
174 $escapeTags = array(
175 'script',
176 'style',
177 'link',
178 'iframe',
179 'frameset',
180 'frame',
181 );
182 foreach ($escapeTags as $tag) {
183 $description = preg_replace_callback(
184 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
185 function ($match) {
186 return escape($match[0]);
187 },
188 $description
189 );
190 }
191 $description = preg_replace(
192 '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
193 '$1',
194 $description
195 );
196 return $description;
197 }
198}
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php
new file mode 100644
index 00000000..bc372273
--- /dev/null
+++ b/application/formatter/BookmarkRawFormatter.php
@@ -0,0 +1,13 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5/**
6 * Class BookmarkRawFormatter
7 *
8 * Used to retrieve bookmarks as array with raw values.
9 * Warning: Do NOT use this for HTML content as it can introduce XSS vulnerabilities.
10 *
11 * @package Shaarli\Formatter
12 */
13class BookmarkRawFormatter extends BookmarkFormatter {}
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
new file mode 100644
index 00000000..0d2c0466
--- /dev/null
+++ b/application/formatter/FormatterFactory.php
@@ -0,0 +1,46 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class FormatterFactory
9 *
10 * Helper class used to instantiate the proper BookmarkFormatter.
11 *
12 * @package Shaarli\Formatter
13 */
14class FormatterFactory
15{
16 /** @var ConfigManager instance */
17 protected $conf;
18
19 /**
20 * FormatterFactory constructor.
21 *
22 * @param ConfigManager $conf
23 */
24 public function __construct(ConfigManager $conf)
25 {
26 $this->conf = $conf;
27 }
28
29 /**
30 * Instanciate a BookmarkFormatter depending on the configuration or provided formatter type.
31 *
32 * @param string|null $type force a specific type regardless of the configuration
33 *
34 * @return BookmarkFormatter instance.
35 */
36 public function getFormatter($type = null)
37 {
38 $type = $type ? $type : $this->conf->get('formatter', 'default');
39 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
40 if (!class_exists($className)) {
41 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
42 }
43
44 return new $className($this->conf);
45 }
46}