/mvc/helpers/ugc
[return to app]1
<?php
2 /**
3 * UGC (User Generated Contend) helper class used to componentize Vork tools in a manner that is safe for public
access
4 */
5 class ugcHelper {
6 /**
7 * Tags that are allowed within UGC - anything else will be htmlencoded - FYI: all tag-properties get stripped
out
8 * @var array
9 */
10 public $allowedTags = array('hr', 'em', 'strong', 'tt', 'dl', 'dt', 'dd');
11
12 /**
13 * Display text with vorkCode-embedded
14 *
15 * @param string $content
16 * @return string
17 */
18 public function text($content) {
19 if ($content !== '') {
20 $allowedTags = implode('|', $this->allowedTags);
21 $content = get::htmlentities($content);
22 $content = preg_replace('/<(\/?(' . $allowedTags . ')(\s.*)?\/?)>/isU', '<$2>', $content);
23 $content = preg_replace('/(\r?\n){2,}/', PHP_EOL . PHP_EOL, $content); //compact excessive line
breaks
24 $content = preg_replace('/(\r?\n){2}=/', PHP_EOL . '=', $content); //reduce line break preceding
header
25 $content = $this->customTags($content);
26 $content = $this->tagShortcuts($content);
27 $content = $this->autolink($content);
28 $content = get::component('filter')->cleanHtml($content);
29 }
30 return $content;
31 }
32
33 /**
34 * Support method for autolink emails
35 *
36 * @param array $array
37 * @return string
38 */
39 protected function _email(array $array) {
40 return (isset($array[1]) ? get::helper('html')->email($array[1]) : current($array));
41 }
42
43 /**
44 * Support method for autolink URLs
45 *
46 * @param array $array
47 * @return string
48 */
49 protected function _autolink(array $array) {
50 if (!isset($array[1])) {
51 return current($array);
52 }
53 $url = $array[1];
54 if (!isset($array[2])) { // protocol is not prefixed
55 $url = get::helper('html')->link('http://' . $url, $url);
56 } else {
57 $url = get::helper('html')->link($url);
58 }
59 return $url;
60 }
61
62 /**
63 * Automatically converts URLs to links and emails to spam-resistant email-links
64 *
65 * @param string $str
66 * @return string
67 */
68 public function autoLink($str) {
69 $end = '[^<>\(\)\s\"\']';
70 $emailRegex = '/(?:[^mailto:])\b(([\w\.!#$%"*+\/=?`{}|~^-]+)@((?:[-\w]+\.)+[A-Za-z]{2,}))\b/';
71 //$urlRegex = '/(?<=[^"\'\/])\b(((https?|ftp):\/\/|www\.)([-\w]+\.)+[A-Za-z]{2,}(:\d+)?([\\\\\/]\S'
72 //. $end . '+)*[\\\\\/]?(\?\S*' . $end . ')?)\b/i';
73 $urlRegex = '/(?:[^\s])?(?<=[^"\'\/])\b((?:(https?|ftp):\/\/|www\.)'
74 . '(?:[-\w]+\.)+[A-Za-z]{2,}(?:\:\d+)?(?:[\\\\\/]\S'
75 . $end . '+)*[\\\\\/]?(\?\S*' . $end . ')?)\b/is';
76 $str = preg_replace_callback($emailRegex, array($this, '_email'), $str);
77 return preg_replace_callback($urlRegex, array($this, '_autolink'), $str);
78 }
79
80 /**
81 * Returns a color-coded code block for code custom-tags
82 *
83 * @param array $matches
84 * @return string
85 */
86 protected function _code($matches) {
87 //PHP-container-match regex will only catch short-open tags that are well-formatted (sloppy-code may get
missed)
88 $str = preg_replace('/\<\?([\r\n\s])/', '<?php$1', html_entity_decode($matches[3]));
89 return str_replace(array("\r", "\n"), '', get::helper('html')->phpcode($str, true));
90 }
91
92 /**
93 * Returns a link for link custom-tags
94 * This is only needed for local links since autolink handles external links automatically
95 *
96 * @param array $matches
97 * @return string
98 */
99 protected function _link($matches) {
100 $link = trim($matches[2] ? $matches[2] : $matches[3]);
101 return get::helper('html')->link($link, $matches[3]);
102 }
103
104 /**
105 * Returns a wiki-specific link for wiki custom-tags
106 *
107 * @param array $matches
108 * @return string
109 */
110 protected function _wiki($matches) {
111 $url = '/' . mvc::$controller . '/';
112 $wikipage = trim($matches[2] ? $matches[2] : $matches[3]);
113 if (strtolower($wikipage) != 'index') {
114 $url .= $wikipage;
115 }
116 return get::helper('html')->link($url, $matches[3]);
117 }
118
119 /**
120 * Returns an image for image custom-tags
121 *
122 * @param array $matches
123 * @return string
124 */
125 protected function _image($matches) {
126 return ($matches[2] ? get::helper('html')->img(array('src' => $matches[2], 'alt' => trim($matches[3]))) :
'');
127 }
128
129 /**
130 * Strips out content found in comment custom-tags
131 *
132 * @param array $matches
133 * @return string
134 */
135 protected function _comment($matches) {
136 return '';
137 }
138
139 /**
140 * Returns a YouTube video for youtube custom-tags
141 *
142 * @param array $matches
143 * @return string
144 */
145 protected function _youtube($matches) {
146 return get::helper('tools')->youtube(array('id' => trim($matches[2] ? $matches[2] : $matches[3])));
147 }
148
149 /**
150 * Returns a QR code for qr custom-tags
151 *
152 * @param array $matches
153 * @return string
154 */
155 protected function _qr($matches) {
156 return get::helper('tools')->qrCode(trim($matches[3]));
157 }
158
159 /**
160 * Returns a Google map for map custom-tags
161 *
162 * @param array $matches
163 * @return string
164 */
165 protected function _map($matches) {
166 return get::helper('tools')->googleMap(trim($matches[3]));
167 }
168
169 /**
170 * Switchboard action-methods on matches found by $this->customTags()
171 *
172 * @param array $matches
173 * @return string
174 */
175 protected function _customTags($matches) {
176 return $this->{'_' . $matches[1]}($matches);
177 }
178
179 /**
180 * Valid custom tags
181 * @var array
182 */
183 protected $_customTags = array('code', 'link', 'image', 'wiki', 'comment', 'youtube', 'qr', 'map');
184
185 /**
186 * Parses out custom tags
187 *
188 * @param string $str
189 * @return string
190 */
191 public function customTags($str) {
192 $regex = '/<(' . implode('|', $this->_customTags) . ')(?:\s+(.*))?>(.*)<\/\1>/siU';
193 return preg_replace_callback($regex, array($this, '_customTags'), $str);
194 }
195
196 /**
197 * Parses shortcut-tags and does a safe version of nl2br()
198 *
199 * @param string $str
200 * @return string
201 */
202 public function tagShortcuts($str) {
203 for ($x = 4; $x >= 1; $x--) {
204 $str = preg_replace('/^={' . $x . '}(.+)/m', '<h' . $x . '>$1</h' . $x . '>', $str);
205 }
206 $lines = explode("\n", $str);
207 $listTypes = array('-' => 'ul', '#' => 'ol');
208 $regex = '/^(\s*)([' . implode(array_keys($listTypes)) . '])/';
209 foreach ($lines as $lineNum => $line) {
210 $match = array();
211 preg_match($regex, $line, $match);
212 if ($match) {
213 $lists[$lineNum] = array('depth' => strlen($match[1]), 'listtype' => $listTypes[$match[2]]);
214 }
215 }
216
217 $autobreak = true;
218 $lines[-1] = ''; //removes the need for look-behind isset-checking
219 foreach ($lines as $lineNum => $line) {
220 $prefix = '';
221 if (isset($lists[$lineNum])) {
222 if (!isset($lists[$lineNum - 1])) { //no list open
223 $prefix .= '<' . $lists[$lineNum]['listtype'] . '><li>';
224 } else {
225 $depthChange = ($lists[$lineNum]['depth'] - $lists[$lineNum - 1]['depth']);
226 if ($depthChange < 0) { //reducing depth
227 $prefix .= str_repeat('</li></' . $lists[$lineNum]['listtype'] . '>', abs($depthChange));
228 }
229 if ($lists[$lineNum - 1]['listtype'] != $lists[$lineNum]['listtype']) { //switch between list
types
230 $prefix .= '</' . $lists[$lineNum - 1]['listtype'] . '><' . $lists[$lineNum]['listtype'] .
'>';
231 }
232 if ($depthChange > 0) { //increasing nesting depth
233 $prefix .= str_repeat('<' . $lists[$lineNum]['listtype'] . '><li>', $depthChange);
234 }
235 $prefix .= '</li><li>';
236 }
237
238 $lines[$lineNum] = $prefix . substr($lines[$lineNum], ($lists[$lineNum]['depth'] + 1));
239 } else {
240 if (isset($lists[$lineNum - 1])) {
241 $closeList = '</li></' . $lists[$lineNum - 1]['listtype'] . '>';
242 $lines[$lineNum] = str_repeat($closeList, ($lists[$lineNum - 1]['depth'] + 1)) .
$lines[$lineNum];
243 }
244 if ($scriptPosition = strpos($line, '<script type="text/javascript">')) {
245 $autobreak = false;
246 }
247 if (!$autobreak && $scriptEndPosition = strrpos($line, '</script>')) { //not a 100% bulletproof
solution
248 $autobreak = (!isset($scriptPosition) || $scriptPosition < $scriptEndPosition);
249 }
250 if ($autobreak) {
251 if (!preg_match('/<(\/h\d|hr\s*\/?)>/i', substr($lines[$lineNum], -5))) {
252 $lines[$lineNum] .= '<br />';
253 }
254 } else if ($scriptPosition) {
255 unset($scriptPosition);
256 }
257 }
258 }
259 return implode(PHP_EOL, $lines);
260 }
261
262 /**
263 * UGC formatting guide
264 * @return string
265 */
266 public function guide() {
267 return '<h4>Automatic Links</h4>Email addresses will be linked automatically (using spam-resistant
links)<br />'
268 . 'URLs that begin with http://, https://, ftp:// and www. will be automatically linked.<br /><br
/>'
269 . '<h4 stye="display: inline;">Tag shortcuts</h4>'
270 . 'Tag shortcut characters must appear at the beginning of a line<br />'
271 . '<span>=</span> Header 1<br />'
272 . '<span>==</span> Header 2<br />'
273 . '<span>===</span> Header 3<br />'
274 . '<span>====</span> Header 4<br />'
275 . '<span>-</span> Unordered List Item<br />'
276 . '<span> -</span> Unordered list, nested one-level - precede lists with spaces to nest
lists<br />'
277 . '<span> -</span> Nested two-levels deep<br />'
278 . '<span>#</span> Ordered List Item (nesting works the same as an unordered list)<br /><br />'
279 . '<h4>HTML tags enabled:</h4>'
280 . '<span><' . implode('> <', $this->allowedTags) . '></span><br /><br />'
281 . '<h4>Custom tags enabled:</h4>'
282 . '<span><code></span>$anyCode = "can go here";<span></code></span><br />'
283 . '<span><link></span>/some/page/url<span></link></span><br />'
284 . '<span><link /another/page></span>Linked Content Here<span></link></span><br />'
285 . '<span><wiki></span>yourWikiPage<span></wiki></span><br />'
286 . '<span><wiki someWikiPage></span>Wiki-Linked Content Here<span></wiki></span><br />'
287 . '<span><image /images/myphoto.jpg></span>Alt-Text Here
(optional)<span></image></span><br />'
288 . '<span><map></span>101 Park Ave., New York, NY<span></map></span> (Address or
Geolocation'
289 . ' to be mapped)<br />'
290 . '<span><youtube></span>QpRUC42ab2M<span></youtube></span> (URL or ID of a YouTube
video)<br />'
291 . '<span><qr></span>Text to be encoded in a QR code<span></qr></span><br />'
292 . '<span><comment></span>Content visible only to those who edit this wiki
page<span></comment>'
293 . '</span><br />';
294 }
295 }