Replace placeholder in XML file with PHP function - php

I have an XML file that contains FAQs, but some of the contents of the answers need to use PHP functions to output appropriate content.
How can I find the placeholders within the answers, and replace them with PHP functions? Ideally, I would like to be able to have the functions set as variables to be changeable across multiple websites this code would be used on.
XML File (placeholders in last block, %LOCAL_NO% and %PREM_NO%)
<?xml version="1.0" encoding="UTF-8"?>
<faqs>
<faq>
<category>General</category>
<q>How old do I have to be to use your service?</q>
<a>You must be at least 18 years of age.</a>
</faq>
<faq>
<category>General</category>
<q>How long is a psychic reading?</q>
<a>The length of a psychic reading is completely up to you. It depends on the number and complexity of the questions you ask. The average length of a reading is 15 to 20 minutes.</a>
</faq>
<faq>
<category>General</category>
<q>Can I choose the psychic I speak with?</q>
<a>Of course! You can choose who you would like to speak to by selecting your desired psychic's profile and following the online prompts via the online booking page, or call us on %PREM_NO% and enter their PIN, or call %LOCAL_NO% and our live receptionists will connect you to a psychic that matches your requirements!</a>
</faq>
</faqs>
PHP output
<?php // General FAQs
$faqGeneral = $xml->xpath("/faqs/faq[category='General']");
echo "<h2>General</h2>";
foreach ($faqGeneral as $faq) { ?>
<h3><?php echo $faq->q; ?></h3>
<p><?php echo $faq->a; ?></p>
<?php } ?>

That looks like a case for preg_replace_callback, of course called before evaluating the XML. Which also ensures that the "PHP-echoed" values do not break XML syntax.
$data = array(
'tags' => array(
'PREM_NO' => '1-800-CSICOP',
)
);
$filledXML = preg_replace_callback(
'#%(\\w+)%#', // A-Z and underscore between %%'s
function ($matches) use ($data) {
switch($matches[1]) {
case 'PREM_NO':
case 'WHATEVER':
return $data['tags'][$matches[1]];
case 'YYYYMMDD':
return date('Y-m-d');
default:
return '';
}
},
$xmlString);
// $xml = loadXML($xmlString);
$xml = loadXML($filledXML);
This allows for special tags such as YYYYMMDD to return runtime-calculated values, as well as externals. You can even include a PDO handle in $data and be able to run SQL queries inside the function.
A simpler version
$tags = array(
'%PREM_NO%' => '12345',
'%YYYYMMDD%' => date('Y-m-d'),
// ... et cetera
);
$filledXML = str_replace(array_keys($tags), array_values($tags), $xmlString);

If you know the strings to match and the values before (i.e., not dynamic) then you can just do a str_replace inline.
If they are dynamic then you can grab the values from your database (or wherever you are storing them) and then loop through and str_replace them.
Or, you could use regex, something like /(\%[a-z_]+\%)/i. For that you can look into preg_match_all.
str_replace in the PHP manual
preg_match_all in the PHP manual
Update: You can use arrays as parametera for str_replace. E.g.,
$find = array('%PREM_NO%', '%LOCAL_NO%');
$replace = array('012345', '67890');
$answer = str_replace($find, $replace, $faq->a);

Related

strip_tags disallow some tags

Based on the strip_tags documentation, the second parameter takes the allowable tags. However in my case, I want to do the reverse. Say I'll accept the tags the script_tags normally (default) accept, but strip only the <script> tag. Any possible way for this?
I don't mean somebody to code it for me, but rather an input of possible ways on how to achieve this (if possible) is greatly appreciated.
EDIT
To use the HTML Purifier HTML.ForbiddenElements config directive, it seems you would do something like:
require_once '/path/to/HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.ForbiddenElements', array('script','style','applet'));
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html);
http://htmlpurifier.org/docs
HTML.ForbiddenElements should be set to an array. What I don't know is what form the array members should take:
array('script','style','applet')
Or:
array('<script>','<style>','<applet>')
Or... Something else?
I think it's the first form, without delimiters; HTML.AllowedElements uses a form of configuration string somewhat common to TinyMCE's valid elements syntax:
tinyMCE.init({
...
valid_elements : "a[href|target=_blank],strong/b,div[align],br",
...
});
So my guess is it's just the term, and no attributes should be provided (since you're banning the element... although there is a HTML.ForbiddenAttributes, too). But that's a guess.
I'll add this note from the HTML.ForbiddenAttributes docs, as well:
Warning: This directive complements %HTML.ForbiddenElements,
accordingly, check out that directive for a discussion of why you
should think twice before using this directive.
Blacklisting is just not as "robust" as whitelisting, but you may have your reasons. Just beware and be careful.
Without testing, I'm not sure what to tell you. I'll keep looking for an answer, but I will likely go to bed first. It is very late. :)
Although I think you really should use HTML Purifier and utilize it's HTML.ForbiddenElements configuration directive, I think a reasonable alternative if you really, really want to use strip_tags() is to derive a whitelist from the blacklist. In other words, remove what you don't want and then use what's left.
For instance:
function blacklistElements($blacklisted = '', &$errors = array()) {
if ((string)$blacklisted == '') {
$errors[] = 'Empty string.';
return array();
}
$html5 = array(
"<menu>","<command>","<summary>","<details>","<meter>","<progress>",
"<output>","<keygen>","<textarea>","<option>","<optgroup>","<datalist>",
"<select>","<button>","<input>","<label>","<legend>","<fieldset>","<form>",
"<th>","<td>","<tr>","<tfoot>","<thead>","<tbody>","<col>","<colgroup>",
"<caption>","<table>","<math>","<svg>","<area>","<map>","<canvas>","<track>",
"<source>","<audio>","<video>","<param>","<object>","<embed>","<iframe>",
"<img>","<del>","<ins>","<wbr>","<br>","<span>","<bdo>","<bdi>","<rp>","<rt>",
"<ruby>","<mark>","<u>","<b>","<i>","<sup>","<sub>","<kbd>","<samp>","<var>",
"<code>","<time>","<data>","<abbr>","<dfn>","<q>","<cite>","<s>","<small>",
"<strong>","<em>","<a>","<div>","<figcaption>","<figure>","<dd>","<dt>",
"<dl>","<li>","<ul>","<ol>","<blockquote>","<pre>","<hr>","<p>","<address>",
"<footer>","<header>","<hgroup>","<aside>","<article>","<nav>","<section>",
"<body>","<noscript>","<script>","<style>","<meta>","<link>","<base>",
"<title>","<head>","<html>"
);
$list = trim(strtolower($blacklisted));
$list = preg_replace('/[^a-z ]/i', '', $list);
$list = '<' . str_replace(' ', '> <', $list) . '>';
$list = array_map('trim', explode(' ', $list));
return array_diff($html5, $list);
}
Then run it:
$blacklisted = '<html> <bogus> <EM> em li ol';
$whitelist = blacklistElements($blacklisted);
if (count($errors)) {
echo "There were errors.\n";
print_r($errors);
echo "\n";
} else {
// Do strip_tags() ...
}
http://codepad.org/LV8ckRjd
So if you pass in what you don't want to allow, it will give you back the HTML5 element list in an array form that you can then feed into strip_tags() after joining it into a string:
$stripped = strip_tags($html, implode('', $whitelist)));
Caveat Emptor
Now, I've kind've hacked this together and I know there are some issues I haven't thought out yet. For instance, from the strip_tags() man page for the $allowable_tags argument:
Note:
This parameter should not contain whitespace. strip_tags() sees a tag
as a case-insensitive string between < and the first whitespace or >.
It means that strip_tags("<br/>", "<br>") returns an empty string.
It's late and for some reason I can't quite figure out what this means for this approach. So I'll have to think about that tomorrow. I also compiled the HTML element list in the function's $html5 element from this MDN documentation page. Sharp-eyed reader's might notice all of the tags are in this form:
<tagName>
I'm not sure how this will effect the outcome, whether I need to take into account variations in the use of a shorttag <tagName/> and some of the, ahem, odder variations. And, of course, there are more tags out there.
So it's probably not production ready. But you get the idea.
First, see what others have said on this topic:
Strip <script> tags and everything in between with PHP?
and
remove script tag from HTML content
It seems you have 2 choices, one is a Regex solution, both the links above give them. The second is to use HTML Purifier.
If you are stripping the script tag for some other reason than sanitation of user content, the Regex could be a good solution. However, as everyone has warned, it is a good idea to use HTML Purifier if you are sanitizing input.
PHP(5 or greater) solution:
If you want to remove <script> tags (or any other), and also you want to remove the content inside tags, you should use:
OPTION 1 (simplest):
preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);
OPTION 2 (more versatile):
<?php
$html = "<p>Your HTML code</p><script>With malicious code</script>"
$dom = new DOMDocument();
$dom->loadHTML($html);
$script = $dom->getElementsByTagName('script');
$remove = [];
foreach($script as $item)
{
$item->parentNode->removeChild($item);
}
$html = $dom->saveHTML();
Then $html will be:
"<p>Your HTML code</p>"
This is what I use to strip out a list of forbidden tags, can do both removing of tags wrapping content and tags including content, Plus trim off leftover white space.
$description = trim(preg_replace([
# Strip tags around content
'/\<(.*)doctype(.*)\>/i',
'/\<(.*)html(.*)\>/i',
'/\<(.*)head(.*)\>/i',
'/\<(.*)body(.*)\>/i',
# Strip tags and content inside
'/\<(.*)script(.*)\>(.*)<\/script>/i',
], '', $description));
Input example:
$description = '<html>
<head>
</head>
<body>
<p>This distinctive Mini Chopper with Desire styling has a powerful wattage and high capacity which makes it a very versatile kitchen accessory. It also comes equipped with a durable glass bowl and lid for easy storage.</p>
<script type="application/javascript">alert('Hello world');</script>
</body>
</html>';
Output result:
<p>This distinctive Mini Chopper with Desire styling has a powerful wattage and high capacity which makes it a very versatile kitchen accessory. It also comes equipped with a durable glass bowl and lid for easy storage.</p>
I use the following:
function strip_tags_with_forbidden_tags($input, $forbidden_tags)
{
foreach (explode(',', $forbidden_tags) as $tag) {
$tag = preg_replace(array('/^</', '/>$/'), array('', ''), $tag);
$input = preg_replace(sprintf('/<%s[^>]*>([^<]+)<\/%s>/', $tag, $tag), '$1', $input);
}
return $input;
}
Then you can do:
echo strip_tags_with_forbidden_tags('<cancel>abc</cancel>xpto<p>def></p><g>xyz</g><t>xpto</t>', 'cancel,g');
Output: 'abcxpto<p>def></p>xyz<t>xpto</t>'
echo strip_tags_with_forbidden_tags('<cancel>abc</cancel> xpto <p>def></p> <g>xyz</g> <t>xpto</t>', 'cancel,g');
Outputs: 'abc xpto <p>def></p> xyz <t>xpto</t>'

Using preg_replace_callback to identify and manipulate latex code

I have latex + html code somewhere in the following form:
...some text1.... \[latex-code1\]....some text2....\[latex-code2\]....etc
Firstly I want to obtain the latex codes in an array codes[] to be able to send them to a server for rendering, so that
code[0]=latex-code1, code[1]=latex-code2, etc
Secondly, I want to modify this text so that it looks like:
...some text1.... <img src="root/1.png">....some text2....<img src="root/2.png">....etc
i.e, the i-th latex code fragment is replaced by the link to the i-th rendered image.
I have been trying to do this with preg_replace_callback and preg_match_all but being new to PHP haven't been able to make it work. Please advise.
If you're looking for codez:
$html = '...some text1.... \[latex-code1\]....some text2....\[latex-code2\]....etc';
$codes = array();
$count = 0;
$replace = function($matches) use (&$codes, &$count) {
list(, $codes[]) = $matches;
return sprintf('<img src="root/%d.png">', ++$count);
};
$changed = preg_replace_callback('~\\\\\\[(.+?)\\\\\\]~', $replace, $html);
echo "Original: $html\n";
echo "Changed : $changed\n\nLatex Codes: ", print_r($codes, 1), "Count: ", $count;
I don't know at which part you've got the problems, if it's the regex pattern, you use characters inside your markers that needs heavy escaping: For PHP and PCRE, that's why there are so many slashes.
Another tricky part is the callback function because it needs to collect the codes as well as having a counter. It's done in the example with an anonymous function that has variable aliases / references in it's use clause. This makes the variables $codes and $count available inside the callback.

Reading php files with special tags in php

I have a file which reads as follows
<<row>> 1|test|20110404<</row>>
<<row>> 1|test|20110404<</row>>
<<row>><</row>> indicates start and end of line.I want to read line between this tags and also check whether this tags are present.
The first thing you need to do is locate the position of this "tag". The strpos() function does just that.
$tag_pos=strpos('<> 1|test|20110404<> <> 1|test|20110404<>', '<>');
if ($tag_pos===false) {
//The tag was not found!
} else {
//$tag_pos equals the numeric position of the first character of your tag
}
If these are truly lines, an efficient way to get them all is just to split on <>.
$lines=explode('<>', '<> 1|test|20110404<> <> 1|test|20110404<>');
$lines=array_filter($lines); //Removes blank strings from array
You could improve this by adding a callback function to the array_filter() call that uses trim() to remove any whitespace and then see if it is blank or not.
Edit: Great, I see that your "tags" were missing from your post. Since your start and end tags do not match, the code above will be of little use to you. Let me try again...
function strbetweenstrs($source, $tag1, $tag2, $casesensitive=true) {
$whatsleft=$source;
while ($whatsleft<>'') {
if ($casesensitive) {
$pos1=strpos($whatsleft, $str1);
$pos2=strpos($whatsleft, $str2, $pos1+strlen($str1));
} else {
$pos1=strpos(strtoupper($whatsleft), strtoupper($str1));
$pos2=strpos(strtoupper($whatsleft), strtoupper($str2), $pos1+strlen($str1));
}
if (($pos1===false) || ($pos2===false)) {
break;
}
array_push($results, substr($whatsleft, $pos1+strlen($str1), $pos2-($pos1_strlen($str1))));
$whatsleft=substr($whatsleft, $pos2+strlen($str2));
}
}
Note that I haven't tested this... but you get the generally idea. There is probably a much more efficient way to go about doing it.
Creating your own format is not so hard, but creating a script to read it can be difficult.
The advantage of using standardized formats is that most programming languages has support for them already. For example:
XML: You can use the simplexml_load_string() function and it can make you navigate easily through your content.
$str = "<?xml version="1.0" encoding="utf-8"?>
<data>
<row>1|test|20110404</row>
<row>1|test|20110404</row>
</data>";
$xml = simplexml_load_string($str);
Now you can access your data
echo $xml->row[0];
echo $xml->row[1];
i'm sure you get the idea,
there is also a very good support for JSON (Javascript Object Notation) using the jsondecode() function;
Check it on php.net for more details
i would suggest to use preg_match :-
preg_match( '#<< row>>(.*)<< /row>>#', $line, $matches);
if( ! empty($matches))
{
// line was found
print_r( $matches[1] ); // will contain the content between the start and end row tags
}

Inserting multiple links into text, ignoring matches that happen to be inserted

The site I'm working on has a database table filled with glossary terms. I am building a function that will take some HTML and replace the first instances of the glossary terms with tooltip links.
I am running into a problem though. Since it's not just one replace, the function is replacing text that has been inserted in previous iterations, so the HTML is getting mucked up.
I guess the bottom line is, I need to ignore text if it:
Appears within the < and > of any HTML tag, or
Appears within the text of an <a></a> tag.
Here's what I have so far. I was hoping someone out there would have a clever solution.
function insertGlossaryLinks($html)
{
// Get glossary terms from database, once per request
static $terms;
if (is_null($terms)) {
$query = Doctrine_Query::create()
->select('gt.title, gt.alternate_spellings, gt.description')
->from('GlossaryTerm gt');
$glossaryTerms = $query->rows();
// Create whole list in $terms, including alternate spellings
$terms = array();
foreach ($glossaryTerms as $glossaryTerm) {
// Initialize with title
$term = array(
'wordsHtml' => array(
h(trim($glossaryTerm['title']))
),
'descriptionHtml' => h($glossaryTerm['description'])
);
// Add alternate spellings
foreach (explode(',', $glossaryTerm['alternate_spellings']) as $alternateSpelling) {
$alternateSpelling = h(trim($alternateSpelling));
if (empty($alternateSpelling)) {
continue;
}
$term['wordsHtml'][] = $alternateSpelling;
}
$terms[] = $term;
}
}
// Do replacements on this HTML
$newHtml = $html;
foreach ($terms as $term) {
$callback = create_function('$m', 'return \'<span>\'.$m[0].\'</span>\';');
$term['wordsHtmlPreg'] = array_map('preg_quote', $term['wordsHtml']);
$pattern = '/\b('.implode('|', $term['wordsHtmlPreg']).')\b/i';
$newHtml = preg_replace_callback($pattern, $callback, $newHtml, 1);
}
return $newHtml;
}
Using Regexes to process HTML is always risky business. You will spend a long time fiddling with the greediness and laziness of your Regexes to only capture text that is not in a tag, and not in a tag name itself. My recommendation would be to ditch the method you are currently using and parse your HTML with an HTML parser, like this one: http://simplehtmldom.sourceforge.net/. I have used it before and have recommended it to others. It is a much simpler way of dealing with complex HTML.
I ended up using preg_replace_callback to replace all existing links with placeholders. Then I inserted the new glossary term links. Then I put back the links that I had replaced.
It's working great!

Replace 'keys' in file with DB data

Currently I have a the following way of retrieving data from my DB:
$school->get('studentCount');
I required a shortcut to access these fields within the page and so came up with a format like this:
<p>Blah blah blah [[studentCount]]</p>
I have output buffering turned on but just need an easy way of replacing that key ('[[field-name]]') with its corresponding data from the DB.
If it was just one field I could do a str_replace on the output like this:
str_replace($output, '[[studentCount]]', $school->get('studentCount'))
Unfortunately that's not suitable. My ideal solution would grab whatever is between '[[' and ']]' and then run the 'get' method and replace the entire key ('[[...]]') with whatever is returned.
Well you could create two arrays, one with the field-name strings [[field-name]] and one with the responses $school->get('field-name'). Then throw those in str_replace as it supports arrays.
Example from PHP Manual:
$phrase = "You should eat fruits, vegetables, and fiber every day.";
$healthy = array("fruits", "vegetables", "fiber");
$yummy = array("pizza", "beer", "ice cream");
$newphrase = str_replace($healthy, $yummy, $phrase);
// Resulting String: "You should eat pizza, beer, and ice cream every day."
If you still wanted to implement your suggestion (finding all [[]]s and replacing them), I'll try to write up a quick function.
Edit: Here are two methods of doing it via your request:
$html = "Hello, [[FirstName]]! Welcome to [[SiteName]].";
$count = preg_match_all("/\[\[([\w]+)\]\]/", $html, $matches);
for ($x = 0; $x < $count; $x++)
$html = str_replace($matches[0][$x], $school->get($matches[1][$x]), $html);
Or using arrays:
$html = "Hello, [[FirstName]]! Welcome to [[SiteName]].";
$count = preg_match_all("/\[\[([\w]+)\]\]/", $html, $matches);
for ($x = 0; $x < $count; $x++)
$matches[1][$x] = $school->get($matches[1][$x]);
$html = str_replace($matches[0], $matches[1], $html);
I'm pretty sure this will work. :)
<?php
// $output contains the string
preg_match_all('/\[{2}([^\[]+)\]{2}/', $output, $matches);
$replaces = $matches['1'];
foreach($replaces as $replace) $str = str_replace('[['.$replace.']]', $school->get($replace), $output);
?>
You will need to use regex to find things inside of two [[ and ]] and take that an insert what is in between into your ->get() function.
Function would be preg_replace
http://us2.php.net/preg-replace
Assuming you can cache the result, a regex and file cache is a great method to do this. First you convert the file:
function cache_it($filename, $tablvar) {
$tmplt = file_get_contents($filename);
$tmplt = preg_replace('/\[\[(.+)\]\]/',
'<?php echo $' . $tablevar . '->get(\1);?>',
$tmplt);
file_put_contents($filename . '.php', $tmplt);
}
Then whenever you need to access the file.
function print_it($filename, $tablevar, $table) {
$_GLOBAL[$tablevar] = $table;
include $filename . '.php';
unset($_GLOBAL[$tablevar]);
}
You probably want to check that the cached file's create date is greater than the last modify date of the source file. Wrapping that and the two functions above in class helps avoid a lot of little pitfalls. But the general idea is sound. There are also some security issues with the cache file being a .php file that you would need to address.
I added this style template caching to the OSS CMS I work on. By caching the regex results, we sped up the original code by over 50%. The real benefit is the templates are PHP files. So anything that speeds the interpreting of PHP files (APC, eAccelerator, etc) speeds up your templates too.
You need to write a parser to look through the $output to find what is between your delimiters and then call a function if it is defined.
I assume you want to do it this way to save the calls until they are needed.
I wrote a template parser that effectively worked this way. Unfortunately it wasn't in PHP.
Thanks for the responses so far. I did think about using str/preg _replace with arrays but I wanted the key ('[[...]]') to directly tie in with the 'get' method, so it's fully expandable. I don't want to have to add to two different arrays every time a new field is added to the DB.
In JavaScript, for example, I would achieve it like this: (JavaScript allows you to pass an anonymous function as the 'replacement' parameter in its equivalent of preg_replace):
('this is the output blah blah [[studentCount]]').replace(/\[{2}([^\[]+)\]{2}/g, function($0, $1) {
get($1);
})

Categories