plugins/content/classes/yf_bb_codes.class.php
<?php
/**
* BB codes handler.
*
* @author YFix Team <yfix.dev@gmail.com>
* @version 1.0
*/
class yf_bb_codes
{
/** @var bool BB codes Enabled or not */
public $ENABLE_BB_CODES = true;
/** @var bool Smilies in bb codes enabled or not */
public $ENABLE_SMILIES = true;
/** @var bool Smilies in bb codes are like images or only with special CSS class */
public $SMILIES_AS_IMAGES = true;
/** @var string Smilies folder */
public $SMILIES_DIR = 'images/smilies/';
/** @var bool Try to highlight special text (SQL or HTML) */
public $USE_HIGHLIGHT = true;
/** @var array CSS classes names @conf_skip */
public $CSS_CLASSES = [
'show1' => 'forum1',
'show2' => 'forum2',
'quote' => 'forum_quote',
'code' => 'forum_code',
'smile' => 'forum_smile',
'topic_a_1' => 'row1',
'topic_a_2' => 'row2',
'topic_u_1' => 'row2shaded',
'topic_u_2' => 'row4shaded',
'post_a_1' => 'post2',
'post_u_1' => 'post2shaded',
];
/** @var bool Filter 'bad words' or not */
public $FILTER_BAD_WORDS = false;
/** @var bool Check for long words 'hacking' or not */
public $CHECK_WORDS_LENGTH = false;
/** @var bool Enable extra codes */
public $ENABLE_EXTRA_CODES = false;
/** @var array Default codes on/off */
public $DEFAULT_SHOW_CODES = [
'font_family' => 0,
'font_size' => 1,
'font_color' => 1,
'extra_fields' => 0,
'help_box' => 0,
'open_tags' => 0,
'youtube' => 0,
];
/** @var bool Check for unclosed bb codes every time when parsing */
public $CHECK_CODES_IF_CLOSED = false;
/** @var bool */
public $USE_NOFOLLOW_TAG = false;
/** @var bool Use or not custom bb codes (from db) */
public $USE_CUSTOM_BB_CODES = false;
/** @var string Available bb codes pattern (for closing codes check) */
public $_avail_codes = '(b|i|u|code|quote|color|size|url|img|sql|html|media|swf|email|imgurl|sub|sup|csv|li|hr|youtube|spoiler)';
/**
* Catch missing method call.
* @param mixed $name
* @param mixed $args
*/
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function _preload_data()
{
if ($this->_preload_complete) {
return true;
}
if ($this->ENABLE_BB_CODES && $this->ENABLE_SMILIES && ! isset($GLOBALS['_smiles_array'])) {
$GLOBALS['_smiles_array'] = main()->get_data('smilies');
}
$nofollow = $this->USE_NOFOLLOW_TAG ? ' rel="nofollow"' : '';
$this->_preg_bb_codes = [
'/\[url=[\"\']{0,1}([^\]]*?)[\"\']{0,1}\](.*?)\[\/url\]/i' => '<a href="\1" target="blank"' . $nofollow . '>\2</a>',
'/\[url\](.*?)\[\/url\]/i' => '<a href="\1" target="blank"' . $nofollow . '>\1</a>',
'/\[img\]([^\[]*?)\[\/img\]/i' => '<div class="bb_remote_image"><img src="\1"></div>',
'/\[color=[\"\']*([#\w]+)[\"\']*\]/i' => '<span style="color:\1">',
'/\[size=[\"\']*([#\w]+)[\"\']*\]/i' => '<span style="font-size:\1px;">',
'/\[quote[:=\w]*[\"\']*([\w\s&;-]*)[\"\']*\]/i' => '<div>' . t('quote') . ' <b>\1</b> :</div><div class="' . $this->CSS_CLASSES['quote'] . '">',
'/\[code\]/i' => '<pre class="' . $this->CSS_CLASSES['code'] . '">',
'/\[\/(color|size)\]/i' => '</span>',
'/\[\/quote\]/i' => '</div>',
'/\[\/code\]/i' => '</pre>',
'/\[([\/]{0,1})(b|i|u|sub|sup|li)\]/i' => '<\1\2>',
'/\[imgurl=([^\]]*)\]([^\[]*?)\[\/imgurl\]/i' => '<a href="\1" target="blank"' . $nofollow . '><img src="\2" border="0"></a>',
'/\[media\]([^\[]*?)\[\/media\]/i' => '<embed name="RAOCXplayer" src="\1">',
'/\[swf\]([^\[]*?)\[\/swf\]/i' => '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0"><param name="movie" value="\1" /><param name="quality" value="high" /><embed src="\1" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" /></object>',
'/\[email\]([^\[]*?)\[\/email\]/i' => '<a href="mailto:\1"' . $nofollow . '>\1</a>',
'/\[hr\]/i' => '<hr />',
'/\[youtube\]([^\[]*?)\[\/youtube\]/i' => '<object width="425" height="350"><param name="movie" value="\1"></param><param name="wmode" value="transparent"></param><embed src="\1" type="application/x-shockwave-flash" wmode="transparent" width="425" height="350"></embed></object>',
'/\[spoiler[=]{0,1}[\"\']*([^\]]*?)[\"\']*\]([^\[]*?)\[\/spoiler\]/i' => '<div class="spoiler_block"><div class="spoiler_head"><input type="button" class="toggle_button" value="+">\1 </div><div class="spoiler_body">\2</div></div>',
];
if ($this->USE_CUSTOM_BB_CODES) {
$custom_codes = main()->get_data('custom_bbcode');
foreach ((array) $custom_codes as $_custom_tag => $_info) {
$_custom_tag = preg_quote($_custom_tag);
$_regex = '/\[' . $_custom_tag . '[=]{0,1}([a-z0-9_-]+)' . ($_info['useoption'] ? '{0,1}' : '{0}') . '\](.*?)\[\/' . $_custom_tag . '\]/ims';
$this->_preg_bb_codes[$_regex] = str_replace(['{option}', '{content}'], ['\1', '\2'], $_info['replace']);
}
}
// Prepare avail codes string (sort them by name desc)
$tmp_codes = [];
foreach (explode('|', substr($this->_avail_codes, 1, -1)) as $_item) {
$tmp_codes[$_item] = $_item;
}
krsort($tmp_codes);
$this->_avail_codes = '(' . implode('|', $tmp_codes) . ')';
$this->_preload_complete = true;
}
/**
* Process text to show.
* @param mixed $body
* @param mixed $no_smilies
* @param mixed $smilies_as_image
*/
public function _process_text($body = '', $no_smilies = false, $smilies_as_image = false)
{
if (empty($body)) {
return '';
}
$this->_preload_data();
$body = str_replace(['<', '>'], ['<', '>'], $body);
if ($this->FILTER_BAD_WORDS) {
if ( ! isset($GLOBALS['BAD_WORDS_ARRAY'])) {
$Q = db()->query('SELECT word FROM ' . db('badwords') . '');
while ($A = db()->fetch_assoc($Q)) {
$GLOBALS['BAD_WORDS_ARRAY'] = $A['word'];
}
}
$body = str_replace($GLOBALS['BAD_WORDS_ARRAY'], '', $body);
}
if ($this->CHECK_WORDS_LENGTH) {
$body = _check_words_length($body);
}
if ($this->ENABLE_BB_CODES) {
if (false !== strpos($body, '[')) {
if ($this->CHECK_CODES_IF_CLOSED) {
$body = $this->_force_close_bb_codes($body);
}
$body = preg_replace(array_keys($this->_preg_bb_codes), array_values($this->_preg_bb_codes), $body);
if ($this->USE_HIGHLIGHT) {
$body = preg_replace_callback('/\[sql\](.*?)\[\/sql\]/ims', function ($m) {
return _class_safe('text_highlight', 'classes/common/')->_regex_sql_tag(stripslashes($m[1]));
}, $body);
$body = preg_replace_callback('/\[html\](.*?)\[\/html\]/ims', function ($m) {
return _class_safe('text_highlight', 'classes/common/')->_regex_html_tag(stripslashes($m[1]));
}, $body);
}
$_this = $this;
$body = preg_replace_callback('/\[csv\](.*?)\[\/csv\]/ims', function ($m) use ($_this) {
return $_this->_parse_csv_bb_code($m[1]);
}, $body);
}
if ($this->ENABLE_SMILIES && is_array($GLOBALS['_smiles_array']) && empty($no_smilies)) {
if ( ! empty($GLOBALS['_SMILIES_CACHE'])) {
$this->_smilies_replace = &$GLOBALS['_SMILIES_CACHE'];
} else {
$smilies_as_image = 0;
if ($this->SMILIES_AS_IMAGES) {
$smilies_as_image = true;
}
foreach ((array) $GLOBALS['_smiles_array'] as $smile_info) {
$replace = [
'img_src' => WEB_PATH . /*tpl()->TPL_PATH. */$this->SMILIES_DIR . $smile_info['url'],
'img_alt' => _prepare_html($smile_info['emoticon']),
'css_class' => $this->CSS_CLASSES['smile'],
'text' => _prepare_html($smile_info['code']),
'as_image' => (int) $smilies_as_image,
];
$this->_smilies_replace[$smile_info['code']] = tpl()->parse('system/smile_item', $replace);
}
$GLOBALS['_SMILIES_CACHE'] = $this->_smilies_replace;
}
}
if ( ! empty($this->_smilies_replace)) {
$body = str_replace(array_keys($this->_smilies_replace), array_values($this->_smilies_replace), $body);
}
}
$body = nl2br($body);
return $body;
}
/**
* @param mixed $body
*/
public function _cut_bb_codes($body = '')
{
return preg_replace('/\[[^\]]+\]/ims', '', $body);
}
/**
* @param mixed $input
*/
public function _display_buttons($input = [])
{
$this->_preload_data();
$STPL_NAME = isset($input['stpl_name']) ? $input['stpl_name'] : __CLASS__ . '/buttons';
$js_vars_code = '';
if ( ! $GLOBALS['_bb_codes_calls']++) {
$js_vars_code = tpl()->parse(__CLASS__ . '/js_vars', [
'max_length' => isset($input['max_length']) ? (int) ($input['max_length']) : 0,
'bb_codes_js_src' => ! $GLOBALS['_bb_codes_calls'] ? WEB_PATH . 'js/yf_bb_codes.js' : '',
'display_i18n_js_vars' => ! $GLOBALS['_bb_codes_calls'] ? 1 : 0,
'emo_pop_link' => './?object=help&action=display_emo_pop',
'bb_pop_link' => './?object=help&action=display_bb_pop',
]);
}
return tpl()->parse($STPL_NAME, [
'display_font_family' => isset($input['font_family']) ? (int) ((bool) $input['font_family']) : $this->DEFAULT_SHOW_CODES['font_family'],
'display_font_size' => isset($input['font_size']) ? (int) ((bool) $input['font_size']) : $this->DEFAULT_SHOW_CODES['font_size'],
'display_font_color' => isset($input['font_color']) ? (int) ((bool) $input['font_color']) : $this->DEFAULT_SHOW_CODES['font_color'],
'display_extra_fields' => isset($input['extra_fields']) ? (int) ((bool) $input['extra_fields']) : $this->DEFAULT_SHOW_CODES['extra_fields'],
'display_youtube' => isset($input['youtube']) ? (int) ((bool) $input['youtube']) : $this->DEFAULT_SHOW_CODES['youtube'],
'display_help_box' => isset($input['help_box']) ? (int) ((bool) $input['help_box']) : $this->DEFAULT_SHOW_CODES['help_box'],
'display_open_tags' => isset($input['open_tags']) ? (int) ((bool) $input['open_tags']) : $this->DEFAULT_SHOW_CODES['open_tags'],
'unique_id' => isset($input['unique_id']) ? _prepare_html($input['unique_id']) : substr(md5(microtime(true) . rand()), 0, 8),
'js_vars_code' => $js_vars_code,
]);
}
/**
* Force to close bb codes in given text (prevent design bugging).
* @param mixed $text
*/
public function _force_close_bb_codes($text = '')
{
$this->_preload_data();
$add_text = '';
$opened_codes = $closed_codes = [];
// Try to find unclosed codes
if ($num_opening_bb_codes = preg_match_all('/\[' . $this->_avail_codes . '[^\]]*?\]/i', $text, $m_opening)) {
foreach ((array) $m_opening[1] as $cur_code) {
$opened_codes[strtolower($cur_code)]++;
}
}
if ($num_closing_bb_codes = preg_match_all('/\[\/' . $this->_avail_codes . '\]/i', $text, $m_closing)) {
foreach ((array) $m_closing[1] as $cur_code) {
$closed_codes[strtolower($cur_code)]++;
}
}
// Process opening codes
$tmp_opened_codes = $opened_codes;
foreach ((array) $tmp_opened_codes as $cur_code => $num_items) {
if ( ! empty($closed_codes[$cur_code])) {
$opened_codes[$cur_code] -= $closed_codes[$cur_code];
}
}
// Do close codes
foreach ((array) $opened_codes as $cur_code => $num_items) {
if ($num_items <= 0) {
continue;
}
$add_text .= str_repeat('[/' . strtoupper($cur_code) . ']', $num_items);
}
$text .= $add_text;
// Fix youtube URL
$text = preg_replace('/(\[youtube\]http:\/\/www.youtube.com\/)watch\?v=(\w+)(\[\/youtube\])/ims', '\1v/\2\3', $text);
return $text;
}
/**
* Try to parse CSV code.
*
* @example
* [CSV]DocID,ParentID,RefCount,RefDocID,DocCode,DocName
* 2,0,1,0,,Text1
* 3,2,1,0,NULL,Text2
* 4,3,1,0,NULL,Text3[/CSV]
* @param mixed $text
*/
public function _parse_csv_bb_code($text = '')
{
// TODO: user table()->auto()
$output = '';
$c = 0;
$num_cols = 0;
foreach (explode("\n", str_replace(["\r", "<br \/>"], '', $text)) as $cur_line) {
$data = explode(',', $cur_line);
if ($c++ == 0) {
$output .= '<table border="1" cellpadding="2" cellspacing="1" style="font: 11px verdana;border-collapse:collapse;border:1px solid gray;margin:5px;"';
$num_cols = count((array) $data);
}
$output .= '<tr>';
for ($i = 0; $i < $num_cols; $i++) {
$output .= '<td bgcolor="' . ($c == 1 ? '#b9ccdf' : '#ffffff') . '">' . _prepare_html($data[$i]) . '</td>';
}
$output .= '</tr>';
}
$output .= '</table>';
return $output;
}
// TODO: use pure-JS highlighter instead of server-based version
public function _geshi_highlight($text = '', $prog_lang = 'html4strict')
{
require_once dirname(INCLUDE_PATH) . '/yf/__SANDBOX/geshi/geshi.php';
$text = geshi_highlight($text, $prog_lang, '', 1);
return $text;
}
}