require_once './start.php'; //submitted info for making a new thread //(name / password already handled in 'start.php') define('TITLE', mb_substr(@$_POST['title'], 0, SIZE_TITLE)); define('TEXT', mb_substr(@$_POST['text'], 0, SIZE_TEXT)); //can the current user post new threads in the current forum? //(posting replies is dependent on the the thread -- if locked -- so tested in 'thread.php') define('CAN_POST', FORUM_ENABLED && (IS_MOD || IS_MEMBER || !FORUM_LOCK)); /* ====================================================================================================================== new thread submitted ====================================================================================================================== */ //has the user submitted a new thread? //(`AUTH` will be true if username and password submitted and correct, `TITLE` and `TEXT` are checked to not be blank) if (CAN_POST && AUTH && TITLE && TEXT) { //the file on disk is a simplified version of the title; see 'lib/functions.php' for `safeTransliterate` $translit = safeTransliterate(TITLE); //if a thread already exsits with that name, append a number until an available filename is found. //we also check for directories with the same name so as to avoid problematic Apache behaviour $c = 0; do { $file = $translit . ($c++ ? '_' . ($c - 1) : ''); } while (file_exists("{$file}") || file_exists("{$file}.rss")); //write out the new thread as an RSS file: $post_id = base_convert(microtime(), 10, 36); $rss = new DOMTemplate(file_get_contents(FORUM_LIB . 'rss-template.xml')); $rss->set(array('/rss/channel/title' => TITLE, '/rss/channel/link' => FORUM_URL . url(PATH_URL, $file), '/rss/channel/item/title' => TITLE, '/rss/channel/item/link' => FORUM_URL . url(PATH_URL, $file) . '#' . $post_id, '/rss/channel/item/author' => NAME, '/rss/channel/item/pubDate' => gmdate('r'), '/rss/channel/item/description' => formatText(TEXT, FORUM_URL . url(PATH_URL, $file, 1), $post_id)))->remove('//category'); file_put_contents("{$file}.rss", $rss) or (require FORUM_LIB . 'error_permissions.php'); //regenerate the folder's RSS file indexRSS(); //redirect to newley created thread header('Location: ' . FORUM_URL . url(PATH_URL, $file), true, 303);
function formatText($text, $permalink = '', $post_id = '', $rss = NULL) { //unify carriage returns between Windows / UNIX, and sanitise HTML against injection $text = safeHTML(preg_replace('/\\r\\n?/', "\n", $text)); //these arrays will hold any portions of text that have to be temporarily removed to avoid interference with the //markup processing, i.e code spans / blocks $pre = array(); $code = array(); /* preformatted text (code blocks): -------------------------------------------------------------------------------------------------------------- */ /* example: or: (latex in particular since it uses % as a comment marker) % title $ title ⋮ ⋮ % $ */ while (preg_match('/^(?-s:(\\h*)([%$])(.*?))\\n(.*?)\\n\\h*\\2(["”»]?)$/msu', $text, $m, PREG_OFFSET_CAPTURE)) { //format the code block $pre[] = "<pre><span class=\"ct\">{$m[2][0]}{$m[3][0]}</span>\n" . (strlen($m[1][0]) ? preg_replace("/^\\s{1," . strlen($m[1][0]) . "}/m", '', $m[4][0]) : $m[4][0]) . "\n<span class=\"cb\">{$m[2][0]}</span></pre>"; //replace the code block with a placeholder: //(we will have to remove the code chunks from the source text to avoid the other markup processing from //munging it and then restore the chunks back later) $text = substr_replace($text, "\n&PRE_" . (count($pre) - 1) . ";\n" . $m[5][0], $m[0][1], strlen($m[0][0])); } /* inline code / teletype text: -------------------------------------------------------------------------------------------------------------- */ // example: `code` or ``code`` while (preg_match('/(?<=[\\s\\p{Z}\\p{P}]|^)(`+)(.*?)(?<!`)\\1(?!`)/m', $text, $m, PREG_OFFSET_CAPTURE)) { //format the code block $code[] = '<code>' . $m[1][0] . $m[2][0] . $m[1][0] . '</code>'; //same as with normal code blocks, replace them with a placeholder $text = substr_replace($text, '&CODE_' . (count($code) - 1) . ';', $m[0][1], strlen($m[0][0])); } /* hyperlinks: -------------------------------------------------------------------------------------------------------------- */ //find full URLs and turn into HTML hyperlinks. we also detect e-mail addresses automatically while (preg_match('/(?: ((?:(?:http|ftp)s?|irc)?:\\/\\/) # $1 = protocol | ([a-z0-9\\._%+\\-]+@) # $2 = email name )( # $3 = friendly URL (no protocol) [-\\.\\p{L}\\p{M}\\p{N}]+ # domain (letters, diacritics, numbers & dash only) (?:\\.[\\p{L}\\p{M}\\p{N}]+)+ # TLDs (also letters, diacritics & numbers only) )(?(2)| # email ends here (\\/)? # $4 = slash is excluded from friendly URL (?(4)( # $5 = folders and filename, relative URL (?> # folders and filename "(?!\\/?>|\\s|$)| # ignore the end of an HTML hyperlink \\)(?![:\\.,"”»]?(?:\\s|$))| # ignore brackets on end with punctuation [:\\.,”»](?!\\s|$)| # ignore various characters on the end [^\\s:)\\.,"”»] # the rest, including bookmark )* )?) )/xiu', $text, $m, PREG_OFFSET_CAPTURE, @($m[0][1] + strlen($replace)))) { $text = substr_replace($text, $replace = '<a href="' . ($p = @$m[2][0] ? 'mailto:' . $m[2][0] : ($m[1][0] ? $m[1][0] : 'http://')) . htmlspecialchars($m[3][0] . @$m[4][0] . @$m[5][0], ENT_COMPAT, 'UTF-8', false) . '"' . ($p . $m[3][0] !== FORUM_URL ? ' rel="nofollow external"' : '') . '>' . $m[0][0] . '</a>', $m[0][1], strlen($m[0][0])); } /* inline formatting: -------------------------------------------------------------------------------------------------------------- */ $text = preg_replace(array('/(?<=\\s|^)_(?!_)(.*?)(?<!_)_(?=\\s|$)/m', '/(?<![*\\w])\\*(?!\\*)(.*?)(?<!\\*)\\*(?![*\\w])/'), array('<em>_$1_</em>', '<strong>*$1*</strong>'), $text); /* divider: "---" -------------------------------------------------------------------------------------------------------------- */ $text = preg_replace('/(?:\\n|\\A)\\h*(---+)\\h*(?:\\n?$|\\Z)/m', "\n\n<p class=\"hr\">\$1</p>\n", $text); /* blockquotes: -------------------------------------------------------------------------------------------------------------- */ /* example: “this is the first quote level. “this is the second quote level.” back to the first quote level.” */ do { $text = preg_replace(array('/(?:\\n|\\A)\\h*("(?!\\s+)((?>(?1)|.)*?)\\s*")\\h*(?:\\n?$|\\Z)/msu', '/(?:\\n|\\A)\\h*(“(?!\\s+)((?>(?1)|.)*?)\\s*”)\\h*(?:\\n?$|\\Z)/msu', '/(?:\\n|\\A)\\h*(«(?!\\s+)((?>(?1)|.)*?)\\s*»)\\h*(?:\\n?$|\\Z)/msu'), "\n\n<blockquote>\n\n" . "<span class=\"ql\">“</span>\n\$2\n<span class=\"qr\">”</span>\n\n" . "</blockquote>\n", $text, -1, $c); } while ($c); //remove the extra linebreaks addeded between our theme quotes //(required so that extra `<br />`s don’t get added!) $text = preg_replace(array('/“<\\/span>\\n(?!\\n)/', '/\\n<span class="qr">/'), array('“</span>', '<span class="qr">'), $text); /* name references: -------------------------------------------------------------------------------------------------------------- */ //name references (e.g. "@bob") will link back to the last reply in the thread made by that person. //this requires that the whole RSS thread is passed to this function to refer to if (!is_null($rss)) { //first, produce a list of all authors in the thread $names = array(); foreach ($rss->channel->xpath('./item/author') as $name) { $names[] = $name[0]; } $names = array_unique($names); //remove duplicates $names = array_map('strtolower', $names); //set all to lowercase $names = array_map('safeHTML', $names); //HTML encode names as they will be in the source text //sort the list of names Z-A so that longer names and names with spaces occur first, //this is so that we don’t choose "Bob" over "Bob Monkhouse" when matching names rsort($names); //find all possible name references in the text: //(that is, any "@" followed by text up to the end of a line. note that this means that what might be //matched may include additional text that *isn't* part of the name, e.g. "@bob How are you?") $offset = 0; while (preg_match('/(?:^|\\s+)(@.+)/m', $text, $m, PREG_OFFSET_CAPTURE, $offset)) { //check each of the known names in the thread and see if one fits the source text reference //e.g. does "@bob How are you?" begin with "bob" foreach ($names as $name) { if (stripos($m[1][0], $name) === 1) { //locate the last post made by that author in the thread to link to foreach ($rss->channel->item as $item) { if (safeHTML(strtolower($item->author)) == $name) { //replace the reference with the link to the post $text = substr_replace($text, '<a href="' . safeHTML($item->link) . '"' . (isMod($name) ? ' class="nnf_mod"' : '') . '>' . substr($m[1][0], 0, strlen($name) + 1) . '</a>', $m[1][1], strlen($name) + 1); //move on to the next reference, no need to check any further names for this one $offset = $m[1][1] + strlen($name) + strlen($item->link) + 15 + 1; break 2; } } } } //failing any match, continue searching //(avoid getting stuck in an infinite loop) $offset = $m[1][1] + 1; } } /* titles -------------------------------------------------------------------------------------------------------------- */ //example: :: title $replace = ''; $titles = array(); while (preg_match('/(?:\\n|\\A)(::.*)(?:\\n?$|\\Z)/mu', $text, $m, PREG_OFFSET_CAPTURE, @($m[0][1] + strlen($replace)))) { //generate a unique HTML ID for the title: //flatten the title text into a URL-safe string of [a-z0-9_] $translit = safeTransliterate(strip_tags($m[1][0])); //if a title already exsits with that ID, append a number until an available ID is found. $c = 0; do { $id = $translit . ($c++ ? '_' . ($c - 1) : ''); } while (in_array($id, $titles)); //add the current ID to the list of used IDs $titles[] = $id; //remove hyperlinks in the title (since the title will be a hyperlink too) //if a user-link is present, keep the mod class if present $m[1][0] = preg_replace('/<a href="[^"]+"( class="nnf_mod")?>(.*?)<\\/a>/', "<b\$1>\$2</b>", $m[1][0]); //create the replacement HTML, including an anchor link $text = substr_replace($text, $replace = "\n\n<h2 id=\"{$post_id}::{$id}\">" . "<a href=\"" . safeHTML($permalink) . "#{$post_id}::{$id}\">" . $m[1][0] . "</a>" . "</h2>\n", $m[0][1], strlen($m[0][0])); } /* finalise: -------------------------------------------------------------------------------------------------------------- */ //add paragraph tags between blank lines foreach (preg_split('/\\n{2,}/', safeTrim($text), -1, PREG_SPLIT_NO_EMPTY) as $chunk) { //if not a blockquote, title, hr or pre-block, wrap in a paragraph if (!preg_match('/^<\\/?(?:bl|h2|p)|^&PRE_/', $chunk)) { $chunk = "<p>\n" . str_replace("\n", "<br />\n", $chunk) . "\n</p>"; } $text = @($result .= "\n{$chunk}"); } //restore code spans/blocks foreach ($code as $i => $html) { $text = str_replace("&CODE_{$i};", $html, $text); } foreach ($pre as $i => $html) { $text = str_replace("&PRE_{$i};", $html, $text); } return $text; }