/mvc/helpers/ugc
[return to app]1
<?php
/**
* UGC (User Generated Contend) helper class used to componentize Vork tools in a manner that is safe for public
access
*/
class ugcHelper {
/**
* Tags that are allowed within UGC - anything else will be htmlencoded - FYI: all tag-properties get stripped
out
* @var array
*/
public $allowedTags = array('hr', 'em', 'strong', 'tt', 'dl', 'dt', 'dd');
/**
* Display text with vorkCode-embedded
*
* @param string $content
* @return string
*/
public function text($content) {
if ($content !== '') {
$allowedTags = implode('|', $this->allowedTags);
$content = get::htmlentities($content);
$content = preg_replace('/<(\/?(' . $allowedTags . ')(\s.*)?\/?)>/isU', '<$2>', $content);
$content = preg_replace('/(\r?\n){2,}/', PHP_EOL . PHP_EOL, $content); //compact excessive line
breaks
$content = preg_replace('/(\r?\n){2}=/', PHP_EOL . '=', $content); //reduce line break preceding
header
$content = $this->customTags($content);
$content = $this->tagShortcuts($content);
$content = $this->autolink($content);
$content = get::component('filter')->cleanHtml($content);
}
return $content;
}
/**
* Support method for autolink emails
*
* @param array $array
* @return string
*/
protected function _email(array $array) {
return (isset($array[1]) ? get::helper('html')->email($array[1]) : current($array));
}
/**
* Support method for autolink URLs
*
* @param array $array
* @return string
*/
protected function _autolink(array $array) {
if (!isset($array[1])) {
return current($array);
}
$url = $array[1];
if (!isset($array[2])) { // protocol is not prefixed
$url = get::helper('html')->link('http://' . $url, $url);
} else {
$url = get::helper('html')->link($url);
}
return $url;
}
/**
* Automatically converts URLs to links and emails to spam-resistant email-links
*
* @param string $str
* @return string
*/
public function autoLink($str) {
$end = '[^<>\(\)\s\"\']';
$emailRegex = '/(?:[^mailto:])\b(([\w\.!#$%"*+\/=?`{}|~^-]+)@((?:[-\w]+\.)+[A-Za-z]{2,}))\b/';
//$urlRegex = '/(?<=[^"\'\/])\b(((https?|ftp):\/\/|www\.)([-\w]+\.)+[A-Za-z]{2,}(:\d+)?([\\\\\/]\S'
//. $end . '+)*[\\\\\/]?(\?\S*' . $end . ')?)\b/i';
$urlRegex = '/(?:[^\s])?(?<=[^"\'\/])\b((?:(https?|ftp):\/\/|www\.)'
. '(?:[-\w]+\.)+[A-Za-z]{2,}(?:\:\d+)?(?:[\\\\\/]\S'
. $end . '+)*[\\\\\/]?(\?\S*' . $end . ')?)\b/is';
$str = preg_replace_callback($emailRegex, array($this, '_email'), $str);
return preg_replace_callback($urlRegex, array($this, '_autolink'), $str);
}
/**
* Returns a color-coded code block for code custom-tags
*
* @param array $matches
* @return string
*/
protected function _code($matches) {
//PHP-container-match regex will only catch short-open tags that are well-formatted (sloppy-code may get
missed)
$str = preg_replace('/\<\?([\r\n\s])/', '<?php$1', html_entity_decode($matches[3]));
return str_replace(array("\r", "\n"), '', get::helper('html')->phpcode($str, true));
}
/**
* Returns a link for link custom-tags
* This is only needed for local links since autolink handles external links automatically
*
* @param array $matches
* @return string
*/
protected function _link($matches) {
$link = trim($matches[2] ? $matches[2] : $matches[3]);
return get::helper('html')->link($link, $matches[3]);
}
/**
* Returns a wiki-specific link for wiki custom-tags
*
* @param array $matches
* @return string
*/
protected function _wiki($matches) {
$url = '/' . mvc::$controller . '/';
$wikipage = trim($matches[2] ? $matches[2] : $matches[3]);
if (strtolower($wikipage) != 'index') {
$url .= $wikipage;
}
return get::helper('html')->link($url, $matches[3]);
}
/**
* Returns an image for image custom-tags
*
* @param array $matches
* @return string
*/
protected function _image($matches) {
return ($matches[2] ? get::helper('html')->img(array('src' => $matches[2], 'alt' => trim($matches[3]))) :
'');
}
/**
* Strips out content found in comment custom-tags
*
* @param array $matches
* @return string
*/
protected function _comment($matches) {
return '';
}
/**
* Returns a YouTube video for youtube custom-tags
*
* @param array $matches
* @return string
*/
protected function _youtube($matches) {
return get::helper('tools')->youtube(array('id' => trim($matches[2] ? $matches[2] : $matches[3])));
}
/**
* Returns a QR code for qr custom-tags
*
* @param array $matches
* @return string
*/
protected function _qr($matches) {
return get::helper('tools')->qrCode(trim($matches[3]));
}
/**
* Returns a Google map for map custom-tags
*
* @param array $matches
* @return string
*/
protected function _map($matches) {
return get::helper('tools')->googleMap(trim($matches[3]));
}
/**
* Switchboard action-methods on matches found by $this->customTags()
*
* @param array $matches
* @return string
*/
protected function _customTags($matches) {
return $this->{'_' . $matches[1]}($matches);
}
/**
* Valid custom tags
* @var array
*/
protected $_customTags = array('code', 'link', 'image', 'wiki', 'comment', 'youtube', 'qr', 'map');
/**
* Parses out custom tags
*
* @param string $str
* @return string
*/
public function customTags($str) {
$regex = '/<(' . implode('|', $this->_customTags) . ')(?:\s+(.*))?>(.*)<\/\1>/siU';
return preg_replace_callback($regex, array($this, '_customTags'), $str);
}
/**
* Parses shortcut-tags and does a safe version of nl2br()
*
* @param string $str
* @return string
*/
public function tagShortcuts($str) {
for ($x = 4; $x >= 1; $x--) {
$str = preg_replace('/^={' . $x . '}(.+)/m', '<h' . $x . '>$1</h' . $x . '>', $str);
}
$lines = explode("\n", $str);
$listTypes = array('-' => 'ul', '#' => 'ol');
$regex = '/^(\s*)([' . implode(array_keys($listTypes)) . '])/';
foreach ($lines as $lineNum => $line) {
$match = array();
preg_match($regex, $line, $match);
if ($match) {
$lists[$lineNum] = array('depth' => strlen($match[1]), 'listtype' => $listTypes[$match[2]]);
}
}
$autobreak = true;
$lines[-1] = ''; //removes the need for look-behind isset-checking
foreach ($lines as $lineNum => $line) {
$prefix = '';
if (isset($lists[$lineNum])) {
if (!isset($lists[$lineNum - 1])) { //no list open
$prefix .= '<' . $lists[$lineNum]['listtype'] . '><li>';
} else {
$depthChange = ($lists[$lineNum]['depth'] - $lists[$lineNum - 1]['depth']);
if ($depthChange < 0) { //reducing depth
$prefix .= str_repeat('</li></' . $lists[$lineNum]['listtype'] . '>', abs($depthChange));
}
if ($lists[$lineNum - 1]['listtype'] != $lists[$lineNum]['listtype']) { //switch between list
types
$prefix .= '</' . $lists[$lineNum - 1]['listtype'] . '><' . $lists[$lineNum]['listtype'] .
'>';
}
if ($depthChange > 0) { //increasing nesting depth
$prefix .= str_repeat('<' . $lists[$lineNum]['listtype'] . '><li>', $depthChange);
}
$prefix .= '</li><li>';
}
$lines[$lineNum] = $prefix . substr($lines[$lineNum], ($lists[$lineNum]['depth'] + 1));
} else {
if (isset($lists[$lineNum - 1])) {
$closeList = '</li></' . $lists[$lineNum - 1]['listtype'] . '>';
$lines[$lineNum] = str_repeat($closeList, ($lists[$lineNum - 1]['depth'] + 1)) .
$lines[$lineNum];
}
if ($scriptPosition = strpos($line, '<script type="text/javascript">')) {
$autobreak = false;
}
if (!$autobreak && $scriptEndPosition = strrpos($line, '</script>')) { //not a 100% bulletproof
solution
$autobreak = (!isset($scriptPosition) || $scriptPosition < $scriptEndPosition);
}
if ($autobreak) {
if (!preg_match('/<(\/h\d|hr\s*\/?)>/i', substr($lines[$lineNum], -5))) {
$lines[$lineNum] .= '<br />';
}
} else if ($scriptPosition) {
unset($scriptPosition);
}
}
}
return implode(PHP_EOL, $lines);
}
/**
* UGC formatting guide
* @return string
*/
public function guide() {
return '<h4>Automatic Links</h4>Email addresses will be linked automatically (using spam-resistant
links)<br />'
. 'URLs that begin with http://, https://, ftp:// and www. will be automatically linked.<br /><br
/>'
. '<h4 stye="display: inline;">Tag shortcuts</h4>'
. 'Tag shortcut characters must appear at the beginning of a line<br />'
. '<span>=</span> Header 1<br />'
. '<span>==</span> Header 2<br />'
. '<span>===</span> Header 3<br />'
. '<span>====</span> Header 4<br />'
. '<span>-</span> Unordered List Item<br />'
. '<span> -</span> Unordered list, nested one-level - precede lists with spaces to nest
lists<br />'
. '<span> -</span> Nested two-levels deep<br />'
. '<span>#</span> Ordered List Item (nesting works the same as an unordered list)<br /><br />'
. '<h4>HTML tags enabled:</h4>'
. '<span><' . implode('> <', $this->allowedTags) . '></span><br /><br />'
. '<h4>Custom tags enabled:</h4>'
. '<span><code></span>$anyCode = "can go here";<span></code></span><br />'
. '<span><link></span>/some/page/url<span></link></span><br />'
. '<span><link /another/page></span>Linked Content Here<span></link></span><br />'
. '<span><wiki></span>yourWikiPage<span></wiki></span><br />'
. '<span><wiki someWikiPage></span>Wiki-Linked Content Here<span></wiki></span><br />'
. '<span><image /images/myphoto.jpg></span>Alt-Text Here
(optional)<span></image></span><br />'
. '<span><map></span>101 Park Ave., New York, NY<span></map></span> (Address or
Geolocation'
. ' to be mapped)<br />'
. '<span><youtube></span>QpRUC42ab2M<span></youtube></span> (URL or ID of a YouTube
video)<br />'
. '<span><qr></span>Text to be encoded in a QR code<span></qr></span><br />'
. '<span><comment></span>Content visible only to those who edit this wiki
page<span></comment>'
. '</span><br />';
}
}

