Trouble with regular expression for comments code - php

I am currently making a homepage where logged in users can write comments. The comment string is first run through a function that str_replaces emoticons. After that I want it to exchange
[url=www.whatever.com]linktext[/url]
with:
<a href='www.whatever.com'>linktext</a>
The reason for this is that I want to strip the text for all the html code that isn't controlled by my comment code, in case some users decide to get creative-
and thought it would be best to use preg replace but the code I ended up with (Partially from reading about reg exp from my trusty "O reilly Sql and Php"-book and partially from the web) Is pretty bonkers, and most importantly, doesn't work.
Any help would be appreciated, thanks.
It's probably possible to exchange the entire code, not in 2 segments like I have done. Just decided on that getting 2 smaller parts to work first would be easier, and then merge them afterwards.
code:
function text_format($string)
{
$pattern="/([url=)+[a-zA-Z0-9]+(])+/";
$string=preg_replace($pattern, "/(<a href=\')+[a-zA-Z0-9]+(\'>)+/", $string);
$pattern="/([\/url])+/";
$string=preg_replace($pattern, "/(<\/a>)+/", $string);
return $string;
}

It looks like you're using something similar to BBCode. Why not use a BBCode parser, such as this one?
http://nbbc.sourceforge.net/
It also handles smilies, replacing them with images. If you use their test page, you will still see the text though, because they don't host the images and they set the alt-text to the smily.

I experimented a bit with the following:
function text_format($string)
{
return preg_replace('#\[url=([^\]]+)\]([^\[]*)\[/url\]#', '$2', $string);
}
However, one immediate fault with this is that if linktext is empty, there will be nothing between <a> and </a>. One way around it would be to do another pass with something like this:
preg_replace('##', '$1', $string);
Another option would be to use preg_replace_callback and put this logic inside your callback function.
Finally, this is obviously a common "problem" and has been solved many times by others, and if using a more mature open sourced solution is an option, I'd recommend looking for one.

#Lauri Lehtinen's answer is good for learning the idea behind the technique, but you shouldn't use it in practice because it would make your site extremely vulnerable to XSS attacks. Also, link spammers would appreciate the lack of rel="nofollow" on the generated links.
Instead, use something like:
<?php
// \author Daniel Trebbien
// \date 2010-06-22
// \par License
// Public Domain
$allowed_uri_schemes = array('http', 'https', 'ftp', 'ftps', 'irc', 'mailto');
/**
* Encodes a string in RFC 3986
*
* \see http://tools.ietf.org/html/rfc3986
*/
function encode_uri($str)
{
$str = urlencode('' . $str);
$search = array('%3A', '%2F', '%3F', '%23', '%5B', '%5D', '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', '%3B', '%3D', '%2E', '%7E');
$replace = array(':', '/', '?', '#', '[', ']', '#', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '.', '~'); // gen-delims / sub-delims / unreserved
return str_ireplace($search, $replace, $str);
}
function url_preg_replace_callback($matches)
{
global $allowed_uri_schemes;
if (empty($matches[1]))
return $matches[0];
$href = trim($matches[1]);
if (($i = strpos($href, ':')) !== FALSE) {
if (strrpos($href, '/', $i) === FALSE) {
if (!in_array(strtolower(substr($href, 0, $i)), $allowed_uri_schemes))
return $matches[0];
}
}
// unescape `\]`, `\\\]`, `\\\\\]`, etc.
for ($j = strpos($href, '\\]'); $j !== FALSE; $j = strpos($href, '\\]', $j)) {
for ($i = $j - 2; $i >= 0 && $href[$i] == '\\' && $href[$i + 1] == '\\'; $i -= 2)
/* empty */;
$i += 2;
$h = '';
if ($i > 0)
$h = substr($href, 0, $i);
for ($numBackslashes = floor(($j - $i)/2); $numBackslashes > 0; --$numBackslashes)
$h .= '\\';
$h .= ']';
if (($j + 2) < strlen($href))
$h .= substr($href, $j + 2);
$href = $h;
$j = $i + floor(($j - $i)/2) + 1;
}
if (!empty($matches[2]))
$href .= str_replace('\\\\', '\\', $matches[2]);
if (empty($matches[3]))
$linkText = $href;
else {
$linkText = trim($matches[3]);
if (empty($linkText))
$linkText = $href;
}
$href = htmlspecialchars(encode_uri(htmlspecialchars_decode($href)));
return "$linkText";
}
function render($input)
{
$input = htmlspecialchars(strip_tags('' . $input));
$input = preg_replace_callback('~\[url=((?:[^\]]|(?<!\\\\)(?:\\\\\\\\)*\\\\\])*)((?<!\\\\)(?:\\\\\\\\)*)\]' . '((?:[^[]|\[(?!/)|\[/(?!u)|\[/u(?!r)|\[/ur(?!l)|\[/url(?!\]))*)' . '\[/url\]~i', 'url_preg_replace_callback', $input);
return $input;
}
which I believe is safe against XSS. This version has the added benefit that it is possible to write out links to URLs that contain ']'.
Evaluate this code with the following "test suite":
echo render('[url=http://www.bing.com/][[/[/u[/ur[/urlBing[/url]') . "\n";
echo render('[url=][/url]') . "\n";
echo render('[url=http://www.bing.com/][[/url]') . "\n";
echo render('[url=http://www.bing.com/][/[/url]') . "\n";
echo render('[url=http://www.bing.com/][/u[/url]') . "\n";
echo render('[url=http://www.bing.com/][/ur[/url]') . "\n";
echo render('[url=http://www.bing.com/][/url[/url]') . "\n";
echo render('[url=http://www.bing.com/][/url][/url]') . "\n";
echo render('[url= javascript: window.alert("hi")]click me[/url]') . "\n";
echo render('[url=#" onclick="window.alert(\'hi\')"]click me[/url]') . "\n";
echo render('[url=http://www.bing.com/] [/url]') . "\n";
echo render('[url=/?#[\\]#!$&\'()*+,;=.~] [/url]') . "\n"; // link text should be `/?#[]#!$&'()*+,;=.~`
echo render('[url=http://localhost/\\\\]d]abc[/url]') . "\n"; // href should be `http://localhost/%5C`, link text should be `d]abc`
echo render('[url=\\]][/url]') . "\n"; // link text should be `]`
echo render('[url=\\\\\\]][/url]') . "\n"; // link text should be `\]`
echo render('[url=\\\\\\\\\\]][/url]') . "\n"; // link text should be `\\]`
echo render('[url=a\\\\\\\\\\]bcde\\]fgh\\\\\\]ijklm][/url]') . "\n"; // link text should be `a\\]bcde]fgh\]ijklm`
Or, just look at the Codepad results.
As you can see, it works.

Related

ignore url in PHP regex

I've got a utility where I'm trying to enforce brand standards in an application where the function will wrap brand words in a span with a class.
public function filterBrandWords($text)
{
// look up the brand words from the config settings
$filter_terms = ['brandword1', 'brandword2', 'brandword3'];
$filtered_text = $text;
foreach ($filter_terms as $word) {
$match_count = preg_match_all('/' . $word . '/i', $text, $matches);
for ($i = 0; $i < $match_count; $i++) {
$brand_string = trim($matches[0][$i]);
$lower = strtolower($brand_string);
$new = '<span class="font-semibold">' . substr($lower, 0, 3) . '</span>' . substr($lower, 3);
$filtered_text = preg_replace('/\b' . $brand_string . '\b/', $new, $filtered_text);
}
}
return $filtered_text;
}
This works but noticed that it's also filtering text that contains the brand URL when applied.
I tried amending $match_count = preg_match_all('/' . $word . '/i', $text, $matches); to $match_count = preg_match_all('/' . $word . 'com$' . '/i', $text, $matches); in the hope it would ignore matches with com in them.
What have I gotten wrong here the regex?
If I do
echo filterBrandWords('brandword1');
the output is
<span class="font-semibold">bra</span>ndword1
with a URL, the output is
<span class="font-semibold">bra</span>ndword1.com
In those instances, I want to ignore the filter and just give it straight.
If you want to ignore anything like a URL you can use something like this as your regex:
(?|.*\.(com|net|org))
which is a Negative Lookahead assertion that matches URL's (broadly). Insert that into your function as I have done here:
function filterBrandWords($text)
{
// look up the brand words from the config settings
$filter_terms = ['brandword1', 'brandword2', 'brandword3'];
$filtered_text = $text;
if(!preg_match('/(?|.*\.(com|net|org))/', $filtered_text)) { // if it resembles a URL, skip it
foreach ($filter_terms as $word) {
$match_count = preg_match_all('/' . $word . '/i', $text, $matches);
for ($i = 0; $i < $match_count; $i++) {
$brand_string = trim($matches[0][$i]);
$lower = strtolower($brand_string);
$new = '<span class="font-semibold">' . substr($lower, 0, 3) . '</span>' . substr($lower, 3);
$filtered_text = preg_replace('/\b' . $brand_string . '\b/', $new, $filtered_text);
}
}
}
return $filtered_text;
}
Now call the function with something resembling a URL:
echo filterBrandWords('brandword1.com');
And the entire URL is just returned:
brandword1.com
EXAMPLE

Adding custom masks to phone numbers

So i'm creating a simple function to mask phone numbers. My phone numbers have a 9 digits and i want preg_replace them with a given mask like 2-2-2-1-2 or 3-2-2-2 and etc.
I tried this:
$mask = explode('-', '3-2-2-2');
$pattern = '';
$replace = '';
foreach ($mask as $key => $value) {
if ($key == 0) {
$pattern = '/\(?(\d{' . $value . '})\)?[- ]';
$replace = '$' . ++$key . '-';
continue;
}
if ($key == count($mask) - 1) {
$pattern .= '?(\d{' . $value . '})/';
$replace .= '$' . ++$key;
break;
}
$pattern .= '?(\d{' . $value . '})[- ]';
$replace .= '$' . ++$key . '-';
}
return preg_replace($pattern, $replace, '902000810');
and the result is 902-00-08-10. Sometimes getting error preg_replace(): No ending delimiter '/' found. How can i refactor this to not getting errors?
Assuming:
$num = '902000810';
$mask = explode('-', '3-2-2-2');
There're other ways than using regex to format a phone number from the mask.
using formatted strings:
$maskPH = array_map(fn($i) => "%{$i}s", $mask);
$formatI = implode('', $maskPH);
$formatO = implode('-', $maskPH);
$result = vsprintf($formatO, sscanf($num, $formatI));
using unpack:
$format = array_reduce($mask, function ($c, $i) {
static $j = 0;
return "{$c}A{$i}_" . $j++ . "/";
});
$result = implode('-', unpack($format, $num));
preg_replace(): No ending delimiter '/' found
means that your pattern does not terminate with a / as last character.
But all three patterns lack proper formatting:
You should modify them accordingly.
From:
$pattern = '/\(?(\d{' . $value . '})\)?[- ]';
$pattern .= '?(\d{' . $value . '})/';
$pattern .= '?(\d{' . $value . '})[- ]';
To:
$pattern = '/\(?(\d{' . $value . '})\)?[- ]/';
$pattern .= '/?(\d{' . $value . '})/';
$pattern .= '/?(\d{' . $value . '})[- ]/';

Wrap Output in Properly Nested Lists - PHP Directory Listing

Okay, I've been searching for a way to list directories and files, which I've figured out and am utilizing code I found here on StackOverflow (Listing all the folders subfolders and files in a directory using php).
So far I've altered code found in one of the answers. I've been able to remove file extensions from both the path and the file name using preg_replace, capitalize the file names using ucwords, and switch out dashes for spaces using str_replace.
What I'm having trouble with now is wrapping the whole thing in a properly nested HTML list. I've managed to set it up so it's wrapped in a list, but it doesn't use nested lists where needed and I can't, for the life of me, figure out how to capitalize the directory names or replace any dashes within the directory name.
So, the questions are, if anyone would be so kind:
How do I wrap the output in properly nested lists?
How do I capitalize directory names while removing the preceding slash and replace dashes or underscores with spaces?
I've left the | within the $ss variable intentionally. I use it as a marker of sorts when I want to throw in characters that will identify where it shows up during trial and error (example $ss = $ss . "<li>workingOrNot").
I'm using:
<?php
$pathLen = 0;
function prePad($level) {
$ss = "";
for ($ii = 0; $ii < $level; $ii++) {
$ss = $ss . "| ";
}
return $ss;
}
function dirScanner($dir, $level, $rootLen) {
global $pathLen;
$filesHidden = array(".", "..", '.htaccess', 'resources', 'browserconfig.xml', 'scripts', 'articles');
if ($handle = opendir($dir)) {
$fileList = array();
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != ".." && !in_array($entry, $filesHidden)) {
if (is_dir($dir . "/" . $entry)) {
$fileList[] = "F: " . $dir . "/" . $entry;
}
else {
$fileList[] = "D: " . $dir . "/" . $entry;
}
}
}
closedir($handle);
natsort($fileList);
foreach($fileList as $value) {
$displayName = ucwords ( str_replace("-", " ", substr(preg_replace('/\\.[^.\\s]{3,5}$/', '', $value), $rootLen + 4)));
$filePath = substr($value, 3);
$linkPath = str_replace(" ", "%20", substr(preg_replace('/\\.[^.\\s]{3,5}$/', '', $value), $pathLen + 3));
if (is_dir($filePath)) {
echo prePad($level) . "<li>" . $linkPath . "</li>\n";
dirScanner($filePath, $level + 1, strlen($filePath));
} else {
echo "<li>" . prePad($level) . "" . $displayName . "</li>\n";
}
}
}
}
I feel like these answers should be simple, so maybe I've been staring at it too much the last two days or maybe it has become Frankenstein code.
I'm about out of trial and error and I need help.
foreach($fileList as $value) {
$displayName = ucwords ( str_replace("-", " ", substr(preg_replace('/\\.[^.\\s]{3,5}$/', '', $value), $rootLen + 4)));
$filePath = substr($value, 3);
$linkPath = str_replace(" ", "%20", substr(preg_replace('/\\.[^.\\s]{3,5}$/', '', $value), $pathLen + 3));
if (is_dir($filePath)) {
// Do not close <li> yet, instead, open an <ul>
echo prePad($level) . "<li>" . $linkPath; . "<ul>\n";
dirScanner($filePath, $level + 1, strlen($filePath));
// Close <li> and <ul>
echo "</li></ul>\n";
} else {
echo "<li>" . prePad($level) . "" . $displayName . "</li>\n";
}
}
I guess you're opening the main before call the function and closing it at the end.

How to show first few lines of a large paragraph?

Please look at the picture:
This is my community forum page. I use at the first row #, Title, Replies & Post Date. Now I want to add another column called Description that will show the first few lines of the whole topic. For example, here the second Title goes as "Components of Computer", but I want show it like "Components of .......". You got the point?
Anyway I store all my data in database. To more assistance here is my code of this page.
echo "<table border='0' width='100%' id='table-3'>
<tr><td>#</td><td>Title</td><td>Replies</td><td>Post Date</td></tr>";
$sql = mysql_query("SELECT * FROM topics ORDER BY id DESC");
while ($row = mysql_fetch_array($sql)) {
$replies = mysql_num_rows(mysql_query("SELECT * FROM replies WHERE
topic_id='" . $row['id'] . "'"));
echo "<tr><td>" . $row['id'] . "</td><td><a href='show_topic.php?id=" . $row['id']
. "'> <b>" . $row['title'] . "</b></a></td><td>" . $replies . "</td><td>" .
date("d M Y", $row['date']) . " </td></tr>";
}
If your looking for full words this is a simple function that will put "..." if the content is longer than the number of words specified:
function excerpt($content,$numberOfWords = 10){
$contentWords = substr_count($content," ") + 1;
$words = explode(" ",$content,($numberOfWords+1));
if( $contentWords > $numberOfWords ){
$words[count($words) - 1] = '...';
}
$excerpt = join(" ",$words);
return $excerpt;
}
then you only need to call the function using:
excerpt($row['description'],10)
This function will trim the text without cutting any words if there are spaces. Otherwise it cuts it right off after the length limit.
function trim_text($input, $length, $ellipses = true)
{
if (strlen($input) <= $length) {
return $input;
}
// find last space within length
$last_space = strrpos(substr($input, 0, $length), ' ');
if ($last_space === FALSE) {
$last_space = $length;
}
$trimmed_text = substr($input, 0, $last_space);
// add ellipses
if ($ellipses) {
$trimmed_text .= '...';
}
return $trimmed_text;
}
use substr
substr($row['description '], 0, $length_you_want);
Try something like this:
<?php
$news = strip_tags($row['apal_news']);
$snews = substr($news,0,50);
echo $news;
?>

Optimizing PHP function

My language bar generation function looks like that. It works, but, feels like, it's not optimal way and this function has bunch of extra lines that can be removed. How you'd minify it?
public function generateLangs($url, $curlang, $langs) {
$i = 0;
$countlng = count($langs);
foreach ($langs as $lang) {
if (strstr($url, '?')) {
if (strstr($url, 'lang')) {
$newurl = preg_replace('&lang=(\w+)&', 'lang=' . $lang, $url);
} else {
$newurl = $url . '&lang=' . $lang;
}
}
else {
$newurl = $url . '?lang=' . $lang;
}
$result = '<a ';
if ($curlang == $lang) {
$result .= 'class="active" ';
}
$result .= 'href="' . $newurl . '">' . $lang . '</a>' . "\n";
if ($i != $countlng - 1)
$result .= ' | ';
echo $result;
$i++;
}
}
First of all you could make the language value a parameter of the URL by using a simple placeholder, like %s or %lang%:
$url = 'http://example.com/site/?lang=%lang%';
$newurl = str_replace('%lang%', $lang, $url);
You can either do this or you should encapsulate the logic to replace some query parameter of an URI into a function of it's own and use built-in functions in there instead rolling your own (e.g. parse_url, parse_str, ...).
Similar to that, same applies to your output, you could use the existing index (0 based I guess) and therefore streamline the whole:
public function generateLangs($url, $currentLanguage, $languages)
{
$urlPattern = preg_replace('~^(.*[?&]lang=)([a-z]+)((?:&.*)?)$~', '\1%lang%\3', $url, 1, $count);
$count || $urlPattern .= '?lang=%lang%';
unset($count);
foreach ($languages as $i => $lang)
{
$newUrl = str_replace('%lang%', $lang, $urlPattern);
printf("%s<a href=\"%s\"%s>%s</a>\n", $i ? ' | ' : '', $newUrl,
$curlang === $lang ? ' class="active"' : '', $lang);
}
}
The key point more or less is that you group code next to each other that belongs to each other. This more or less automatically reduces the complexity of the code and therefore often as well it's length. But take care that length is not that crucial, it's more important that you can read it cleanly.

Categories