Changes to this site
Category: Architecture, PHP, XML/XSL
Published: 2009-06-02
Updated: 2009-06-03
As explained in one of the first posts on this blog this site is basically just one big Atom feed that gets transformed into this blog by using a bit of Apache content negotiation and client side XSLT. Besides some issues with browsers ignoring client side XSLT in a feed and forcing their own rendition of my feed which was fixed by inserting 512 bytes of crud to throw of the feed sniffing this approach has worked fine for the last four years.
Alas these happy years are now over thanks too Internet Explorer 8. For reasons unknown (and undocumented) IE8 no longer respects the 512 bytes of crud workaround and insists on deploying its "friendly" feed view on everything it perceives as being a feed, client side XSLT and 512 bytes workaround be damned.
Given the potential audience for IE8 I can't go on with my preferred method and I have decided to switch to a server side XSLT rendering. Given the high level of standardisation available within XSLT I didn't have to change a single item in my stylesheet.
Whilst on the experimental track I took some time to play around with typeface.js and Cufón. I had already used SIFR on this site and I was interested in how it would compare against these newcomers. Both typeface.js and Cufón are completely javascript based solutions that embed their fonts as javascript files in the page. Getting both libraries to work required a bit of tinkering to acquire a feel for their approach but basically all examples and code where up and running in minutes. There's not much difference in how both libraries work, they both use javascript with a combination of <canvas> or VML elements. From an aesthetic viewpoint I like Cufón better because the fonts seem crisper than those of typeface.js. This might have something to do with the extensive font rendering process that was created for Cufón. I set the <h1> and <h2> in Cufón and I liked the results enough to abandon SIFR. I even tried to set an entire page in Cufón but that led to all kinds of amazing behaviour within FireFox (even crashing it) so I decided to let that approach rest. It did however get me started on looking in/back to CSS 3 font-face adoption which seems to be imminent for the large browsers with Safari 3.1 already implementing it. For those of you that use Safari 3.1 (or are using a browser that supports font-face now) I have set the paragraphs in Delicious Roman, a free font from Jos Buivenga.
Oh by the way the server side XSLT rendering is done with PHP. All requests for XML files are forwarded by Apache based on a specific AddHandler and Action.
# Handler atom_xslt is associated with xml files
AddHandler atom_xslt xml
# Subsequently handler atom_xslt is associated with atom.php
# for actual processing
Action atom_xslt /atom.php
# Force everything to the feed
DirectoryIndex index.xml
A simple caching mechanism is applied by the PHP script to render to HTML once and only update the rendered file after either the XML feed or the XSLT stylesheet have been update. Now that I don't apply a client side XSLT anymore there's no reason to stick with XHTML and I have switched to HTML 4.01 Strict.
A Query Engine for PHP
Category: PHP, Security, Web Technology, XML/XSL
Published: 2009-05-23
Updated: 2009-05-29
Looking at the code in the previous entry wasn't exactly a pleasant aesthetic experience (sorry for that, bit of a botched job) so for my new project, an implementation of the NIST RBAC model in PHP, I decided to code a nice generic PHP query engine. The Query Engine takes a number of arguments like the SQL query, the arguments for the query (to be passed into prepared statements), the types of the arguments and whether the query is part of an overall transaction. The nice thing is that the QueryEngine function returns the results as an associative array using the database column names as the key value.
/**
* Generic query execution engine for the RBAC data functions. This is a support
* function that is not part of the standard API. This function uses the mysqli
* interface and makes use of mysqli bound parameters and bound results to
* lower the risk of SQL injection attacks.
*
* @param string $sql
* @param array $param
* @param string $type
* @param integer $transaction
* @return array
*/
function QueryEngine($sql='', $param='', $type='', $transaction=0) {
$field = $meta = $params = $key = $val = $c = $results = '';
/* Yes I know, a global, gasp */
$link = $GLOBALS["db_connection"];
/* Check whether the transaction flag has been set, the transaction
needs to be comitted or rolled back */
switch ($transaction) {
case 1:
/* Set autocommit to off */
mysqli_autocommit($link, FALSE);
break;
case 2:
/* Commit transaction */
mysqli_autocommit($link, TRUE);
break;
case 3:
/* Rollback transaction */
mysqli_rollback($link);
break;
}
if (!empty($sql)) {
/* Prepare SQL statement */
$stmt = mysqli_prepare($link, $sql);
/* Dynamically bind arguments via the $param array */
if ($stmt) {
if (($param) && ($type)) {
/* A custom function is constructed that calls
mysqli_stmt_bind_param */
call_user_func_array('mysqli_stmt_bind_param', array_merge(array($stmt, $type), $param));
}
mysqli_stmt_execute($stmt);
/* Get the column names of the retrieved rows by querying the schema
meta-data. The column names are returned as the keys of the
multidimensional result array and the rows are returned as the
values of the array. */
$meta = mysqli_stmt_result_metadata($stmt);
if (!empty($meta)) {
while ($field = mysqli_fetch_field($meta)) {
$params[] = &$row[$field->name];
}
/* A custom function is constructed that uses the retrieved
column names as bound result parameters */
call_user_func_array(array($stmt, 'bind_result'), $params);
while (mysqli_stmt_fetch($stmt)) {
/* The results are put into an associative array */
foreach($row as $key => $val) {
$c[$key] = $val;
}
$result[] = $c;
}
}
} else {
trigger_error('query failed: ' . $sql . ' ' . mysqli_error($link), E_USER_ERROR);
}
mysqli_stmt_close($stmt);
}
return $results;
}
There is some helper code to initialize the database connection which you can find below. You just have to make the call to DatabaseConnection() at the start of your script.
/**
* Database Server IP address
*/
define ("DATABASE_SERVER", "<insert database server>");
/**
* Database username
*/
define ("DATABASE_USER", "<insert database user>");
/**
* Database password
*/
define ("DATABASE_PASSWORD", "<insert database password>");
/**
* Database name
*/
define ("DATABASE_NAME", "<insert database name>");
/**
* Open connection to the database using the connection data defined in the
* constants
*
*/
function DatabaseConnection() {
/* Connect to the database using the predefined constants */
$link = mysqli_connect(DATABASE_SERVER, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME);
if (!$link) {
trigger_error('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error(), E_USER_ERROR);
}
/* force the UTF-8 character set */
if (!mysqli_set_charset($link, "utf8")) {
trigger_error('Error loading character set utf8: ' . mysqli_error($link), E_USER_ERROR);
}
/* Configure the database link as a global variable to ensure database
session state throughout script execution */
$GLOBALS["db_connection"] = $link;
}
Subsequently you use the code as follows:
$sql = 'INSERT INTO role_permission (role_id, permission_id) VALUES (?, ?)';
$args = array($role_id, $permission_id);
$results = QueryEngine($sql, $args, 'ii', 0);
if (!empty($results)) {
$all_query_ok = FALSE;
}
As indicated above the database column names are used as the key values of the associative database, they can be used as follows (the $table array contains the return result of the QueryEngine function):
<table>
<caption>Test</caption>
<thead
<tr>
<?php
$column_headers = array_keys($table[0]);
$number_of_columns = count($column_headers);
for ($counter = 0; $counter < $number_of_columns; $counter++) {
print '<th scope="col">' . $column_headers[$counter] . "</th>\n";
}
?>
</tr>
</thead>
<tbody>
<?php
foreach ($table as $key => $val) {
print "<tr>\n";
for ($counter = 0; $counter < $number_of_columns; $counter++) {
print '<td class="usertable">' . $val[$column_headers[$counter]] . "</td>\n";
}
print "</tr>\n";
}
?>
</tbody>
</table>
And lastly using transactions:
/* Start transaction */
$all_query_ok = TRUE;
QueryEngine('', '', '', 1);
/* Get user id and role ids from the user table*/
$sql = 'SELECT user.user_id, role.role_id
FROM user
INNER JOIN user_role USING (user_id)
INNER JOIN role USING (role_id)
WHERE user.username = ?';
$args = array($username);
$results = QueryEngine($sql, $args, 's', 0);
if (is_array($results)) {
foreach ($results as $key => $val) {
$user_id = (int) $val['user_id'];
$role_id_collection[] = (int) $val['role_id'];
}
} else {
$all_query_ok = FALSE;
}
/* Insert the session based on the unique identifier */
$sql = 'INSERT INTO session (identifier, created) VALUES (?, NOW())';
$args = array($identifier);
$results = QueryEngine($sql, $args, 's', 0);
if (!empty($results)) {
$all_query_ok = FALSE;
}
/* Create the link between the session and the active user */
$sql = 'INSERT INTO user_session (user_id, session_id) VALUES (?, LAST_INSERT_ID())';
$args = array($user_id);
$results = QueryEngine($sql, $args, 'i', 0);
if (!empty($results)) {
$all_query_ok = FALSE;
}
/* Assign the users' roles to the session */
$sql = 'INSERT INTO session_role (role_id, session_id) VALUES (?, LAST_INSERT_ID())';
foreach($role_id_collection as $role_id) {
$args = array($role_id);
$results = QueryEngine($sql, $args, 'i', 0);
if (!empty($results)) {
$all_query_ok = FALSE;
}
}
/* Commit or rollback transaction based on the value of $all_query_ok */
$all_query_ok ? QueryEngine('', '', '', 2) : QueryEngine('', '', '', 3);
That's it, I hope it's of use to you. Any questions please drop me a line at meint dot post squiggly bit bigfoot dot com.
Transactions, prepared statements and PHP mysqli
Category: PHP, Web Technology
Published: 2008-01-21
Updated: 2009-05-29
While working with my good friend Arnold Consten on his new PHP application we came across some nice learning points for dealing with mysqli transactions and prepared statements. It turns out that the order of events is very specific for transactions and prepared statements to work correctly together:
- Make a connection with the database server
- Disable auto commit
- Initialize all prepared statements
- Initialize all query templates
- Prepare all statements
- Assign all bind parameters
- Execute
- Do a rollback if an error occurs in any of the situations here above
- If no errors commit the transaction
- Close the prepared statements
- Done
In code this boils down to the following example:
$season_ID = $_POST[frmhiddenseason_ID];
$class_ID = $_POST[frmhiddenclass_ID];
$link = mysqli_connect("localhost", "my_user", "my_password", "world");
/* check connection */
if (!$link) {
printf("Connect failed: %s\n", mysqli_connect_error());
exit();
}
mysqli_autocommit($link, FALSE);
$stmt1 = mysqli_stmt_init($link);
$stmt2 = mysqli_stmt_init($link);
$stmt3 = mysqli_stmt_init($link);
$sql1 = "INSERT INTO tbl_person (firstname, lastname) VALUES (?, ?)";
$sql2 = "INSERT INTO tbl_person_group (season_ID, class_ID, student_ID) VALUES (?,?,?)";
$sql3 = "UPDATE tbl_person SET firstname = ?, lastname = ? WHERE tbl_person.student_ID = ?";
if (mysqli_stmt_prepare($stmt1, $sql1) && mysqli_stmt_prepare($stmt2, $sql2) && mysqli_stmt_prepare($stmt3, $sql3)) {
mysqli_stmt_bind_param($stmt1, "ss", $firstname, $lastname);
mysqli_stmt_bind_param($stmt2, "ssi", $season_ID, $class_ID, $newstudent_ID);
mysqli_stmt_bind_param($stmt3, "ssi", $firstname, $lastname, $student_ID);
for ($counter = 0; $counter <= 10; $counter++) {
$frmhiddenstudent_ID = "hidden" . $counter;
$student_ID = $_POST[$frmhiddenstudent_ID];
$frmfirstname = "firstname" . $counter;
$frmlastname = "lastname" . $counter;
if ($_POST[$frmfirstname] <> "" && $_POST[$frmlastname] <> "") {
$firstname = $_POST[$frmfirstname];
$lastname = $_POST[$frmlastname];
}
if (empty($student_ID)) {
mysqli_stmt_execute($stmt1);
$newstudent_ID = mysqli_insert_id($link);
mysqli_stmt_execute($stmt2);
} else {
mysqli_stmt_execute($stmt3);
}
}
}
} else {
echo "Error";
mysqli_rollback($link);
}
mysqli_commit($link);
mysqli_stmt_close($stmt1);
mysqli_stmt_close($stmt2);
mysqli_stmt_close($stmt3);
And voila, transactions and prepared statements working nicely together making for a robust and safe database handling solution.
Templates, template engines and PHP
Category: PHP, Web Technology, XML/XSL
Published: 2007-06-05
Updated: 2009-05-30
In the process of developing Lilliput CMS I had to think about how to do templating with PHP. There's a lot of material available regarding PHP and templating and most of it is really weird. Having had a look at the Top 25 PHP template engines I can't for the life of me understand why I would want to use something like Smarty, Savant or phptal. Obviously a lot of love and attention has been poured into these solutions but I can't escape the feeling that these template engines are recreating PHP and its innate templating function. This feeling was confirmed when reading the "Templates and template engines" article on the php patterns website. The article inspired the following train of thought:
- Don't try to recreate PHP and its innate templating functions, it's fine as it is. This doesn't necessarily mean you should only use PHP for templating, rather that you shouldn't rebuild PHP
- There's no sense in separating logic and content as they are intertwined and dependent on each other; focus on separating content and presentation (with the help of clean XHTML and CSS)
- Use XHTML for templates instead of HTML 4 Strict so you can benefit from the strictness introduced by XML parsers
- Templates are created by designers, they shouldn't need to learn a new language to accomplish their goals so the templates should use no frills XHTML
- Because the templates use XHTML and the output format is XHTML there is no need for a template processing/transformation language, i.e. XSLT
- There's only one standardized API for manipulating content and that's the DOM so use this for interaction with template and content
- It should be simple to the point of sacrificing functionality to keep it simple, it won't be all things to all people
- It needs to be extensible, you can't predict all use cases and you need to offer a way for people to introduce different behavior whilst maintaining forward compatibility
So what has this resulted in? The template needs to be valid XHTML and the designer doesn't need to learn new language constructs (so no phptal like solution). The simplest solution is to use content elements in the template that are replaced with the generated content resulting from business logic processing. Because I'm treating the XHTML templates as real XML files we need to manipulate the content via a formal API for XML, i.e. the DOM. The ground rules for this are laid out in two succinct articles "On Postel, Again" and "Are You an XML Bozo?". The replaceable elements are identified by XHTML id's, found via XPath queries and replaced via a DOM manipulation as demonstrated by the following example code.
Template code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Lilliput CMS</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="/lilliput/favicon.ico" />
<link rel="stylesheet" href="/lilliput/template/lilliput/css/style.css" type="text/css" media="screen, projection" />
<script src="/lilliput/template/lilliput/javascript/script.js" type="text/javascript"></script>
</head>
<body>
<div id="wrapper">
<div id="header">
<p class="description">Helping you eat boiled eggs since 2006.</p>
<h1><a href="http://feedme.mind-it.info#">Lilliput CMS</a></h1>
<ul id="menu">
</ul>
</div>
<div id="content" />
<div id="footer">
<p>This text to be replaced.</p>
</div>
</div>
</body>
</html>
The PHP controller code (invoking model actions for retrieving data) that generates the content and passes it on to the view logic:
function controller_addUser() {
// invoke model logic to get the data
$results = model_addUser();
// start new DOM instance for document creation
$dom = new DomDocument('1.0', 'UTF-8');
$selectElement = $dom->createElement('select');
$selectElement = $dom->appendChild($selectElement);
$selectElement->setAttribute('id', 'fm-role');
$selectElement->setAttribute('name', 'fm-role');
$resultsCount = count($results);
for ($row = 0; $row < $resultsCount; $row++) {
$role = $results[$row]["role"];
$optionElement = $dom->createElement('option');
$optionElement = $selectElement->appendChild($optionElement);
$optionElement->setAttribute('value', $role);
$elementValue = $dom->createTextNode($role);
$elementValue = $optionElement->appendChild($elementValue);
}
// pass string data to view and get rendered XHTML structure back
$content["html"] = view_addUser($dom->saveXML($selectElement));
// return rendered XHTML structure to index.php dispatcher for inclusion in page template
return $content;
}
PHP template handling code using PHP 5 XML and DOM functions:
function processTemplate($templateFile, $content) {
// Initialize DOM document
$dom = new DomDocument('1.0', 'utf-8');
// load the template file into the DOM document
$returnValue = $dom->load($templateFile);
$xml = explode("\n", file_get_contents($templateFile));
// call the replaceDomContent function that substitutes <div id="content"/> with the generated content
replaceDomContent($dom, '//xhtml:div[@id="content"]', $content["html"]);
// call the getTitle function to retrieve the title element and replace the content with the generated title
$title = getTitle($dom);
replaceNodeContent($dom, "//xhtml:title", $title);
// export the DOM structure into an XML format
return $dom->saveXML();
}
function getTitle($dom) {
$xpath = new DOMXPath($dom);
$resultNode = $xpath->query("//h2");
$title = $resultNode->item(0)->nodeValue;
return $title;
}
function replaceDomContent($dom, $query, $content) {
$xpath = new DOMXPath($dom);
// register the xhtml namespace otherwise xpath queries will fail
$xpath->registerNamespace("xhtml", "http://www.w3.org/1999/xhtml");
$nodelist = $xpath->query($query);
$oldnode = $nodelist->item(0);
libxml_use_internal_errors(true);
$contentImport = new DOMDocument();
$returnValue = $contentImport->loadXML($content);
$xml = explode("\n", $content);
// import the converted XML content into the DOM structure
$newnode = $dom->importNode($contentImport->documentElement, true);
// Replace the old content with the new content
$oldnode->parentNode->replaceChild($newnode, $oldnode);
return $dom;
}
function replaceNodeContent($dom, $query, $content) {
$xpath = new DOMXPath($dom);
// register the xhtml namespace otherwise xpath queries will fail
$xpath->registerNamespace("xhtml", "http://www.w3.org/1999/xhtml");
$nodelistContent = $xpath->query($query);
$nodelistContent->item(0)->nodeValue = $content;
return $dom;
}
Lies, damned lies and caching
Category: Web Technology
Published: 2007-02-20
Updated: 2009-05-29
One of the most excellent aspects of HTTP and the underpinning REST architecture is the aspect of idempotence and non-idempotence. Idempotence roughly means that some operation yields the same result whether it is done only once or several times. This behaviour forms the basis of caching because if the result is the same you can work with a copy of the result for all subsequent requests. This excellent property of the web also has some downsides as the Law of Preservation of Complexity demands that for any goodness in technology there always is a trade off in increased complexity somewhere else. In the case of caching it is determining when to cache and when not to cache.
The website that is the topic of this entry is one of the largest banking websites of Europe and it is based on IIS 5 which doesn't do a lot out-of- the -box caching wise. Problem is that if your webserver doesn't supply any information about the validity of the objects it serves you will find out fairly quickly that many parties to the conversation will cache each object quite agressively. The browser is one of these parties but also many caching proxies that are used by the target infrastructure or your ISP without you knowing it. The only way of forcing all intermediate parties to relinquish their hold on the page objects is to change the names of the objects (filenames for clarity).
A better aproach is to use cache control to specify the time-to-live (TTL) for each page object. This way the browser and all intermediate parties know how to treat the page objects. When the TTL for a given object has expired the browser (or intermediate proxy) will inquire at the target server if the object is still valid. If so the web server will respond with a 304 Not Modified instead of retransmitting the page object saving valuable bandwidth.
However as indicated the IIS 5 webserver doesn't have a lot of easily accessible cache settings available to it. There are some third party modules that you can use for this stuff but we're talking about a website with upwards of 2 million page views per day and you can't just slap in any ISAPI filter you'd like and trust that everything will turn out right (not to speak of all the test work involved when introducing an additional component in your infrastructure). One of the few components that I did use on the website (totally unrelated to the subject) was ISAPI Rewrite from Helicon Tech and it never failed us through the most extreme loads. I'm usually loath to promote vendors but these guys offer amazing value for money and offered first class support even though we paid a bargain price for their software.
Ok enough with the product talk, back to the subject of applying a caching policy to IIS. After reading up on the subject I decided to create a VBScript script for injecting the proper caching instructions in the IIS Metabase. This way the support team could apply the same policy to all machines without the risk of manual error. The script is based on various scrapings found around the web:
Option Explicit
Call Main
Sub Main
Dim RootDir, CSSDir, JSDir, ImagesDir, JavascriptDir, CSSFile, JSFile, ServerName, SiteNumber, oArgs, iArgNum,
' ServerName is default at localhost but can be changed
' via the -s "SERVERNAME" switch
ServerName = "localhost"
' SiteNumber is set at 1, the default website in IIS
' this can be changed via the -i "SITENUMBER" switch
SiteNumber = "1"
Set oArgs = WScript.Arguments
iArgNum = 0
While iArgNum < oArgs.Count
Select Case LCase(oArgs(iArgNum))
Case "-s","--ServerName":
iArgNum = iArgNum + 1
ServerName = oArgs(iArgNum)
Case "-i","--SiteNumber":
iArgNum = iArgNum + 1
SiteNumber = oArgs(iArgNum)
Case "-?","--help":
Call DisplayUsage
WScript.Quit(1)
Case Else:
WScript.Echo "Unknown argument " & oArgs(iArgNum)
Call DisplayUsage
WScript.Quit(1)
End Select
iArgNum = iArgNum + 1
Wend
WScript.Echo "ServerName: " & ServerName
WScript.Echo "SiteNumber: " & SiteNumber
WScript.Echo "Default caching policy for the entire site is caching for 2 hours (7200 seconds)"
Set RootDir = GetRootDir(ServerName, SiteNumber)
Call SetVDirCacheability(RootDir, 7200)
WScript.Echo "Caching policy for /images is caching for 1 month (2592000 seconds)"
Call GetVDirAndSetCacheability("images", ServerName, SiteNumber, 2592000)
WScript.Echo "Caching policy for /javascript is caching for 1 week (604800 seconds)"
Call GetVDirAndSetCacheability("javascript", ServerName, SiteNumber, 604800)
WScript.Echo "Caching policy for /css is caching for 1 week (604800 seconds)"
Set CSSDir = GetVDirAndSetCacheability("css", ServerName, SiteNumber, 604800)
WScript.Echo "Caching policy for /javascript is caching for 1 week (604800 seconds)"
Set JSDir = GetVDirAndSetCacheability("javascript", ServerName, SiteNumber, 604800)
WScript.Echo "/css/super.css must not be cached! Exception policy will be applied ..."
On Error Resume Next
Set CSSFile = GetObject("IIS://" & ServerName & "/w3svc/" & SiteNumber & "/root/css/super.css")
If (Err.Number <> 0) Then
WScript.Echo "/css/super.css file does not exist yet in metabase, status code " & Err.Number
Err.Clear
Set CSSFile = CSSDir.Create("IIsWebFile", "super.css")
If (Err.Number <> 0) Then
Wscript.Echo "/css/super.css object in metabase could not be created, error code " & Err.Number
Wscript.Quit
Else
WScript.Echo "/css/super.css object created in metabase and cache policy correctly applied"
End If
CSSFile.SetInfo
Else
WScript.Echo "/css/super.css object already exists in metabase"
End If
Call SetCacheability(CSSFile, 3600, "must-revalidate")
WScript.Echo "/javascript/include.js must not be cached! Exception policy will be applied ..."
On Error Resume Next
Set JSFile = GetObject("IIS://" & ServerName & "/w3svc/" & SiteNumber & "/root/javascript/include.js")
If (Err.Number <> 0) Then
WScript.Echo "/javascript/include.js file does not exist yet in metabase, status code " & Err.Number
Err.Clear
Set JSFile = JSDir.Create("IIsWebFile", "include.js")
If (Err.Number <> 0) Then
Wscript.Echo "/javascript/include.js object in metabase could not be created, error code " & Err.Number
Wscript.Quit
Else
WScript.Echo "/javascript/include.js object created in metabase and cache policy correctly applied"
End If
JSFile.SetInfo
Else
WScript.Echo "/javascript/include.js object already exists in metabase"
End If
Call SetCacheability(JSFile, 3600, "must-revalidate")
WScript.Echo ""
WScript.Echo ""
Call SetHTMLCharSet(ServerName, SiteNumber)
End Sub
Function GetRootDir(ServerName, SiteNumber)
Dim RootDir
On Error Resume Next
Set RootDir = GetObject("IIS://" & ServerName & "/w3svc/" & SiteNumber & "/root")
If (Err.Number <> 0) Then
WScript.Echo "Root dir does not exist for site index " & SiteNumber & ", error code " & Err.Number
WScript.Quit
Else
WScript.Echo "Root dir found for site index " & SiteNumber
End If
Set GetRootDir = RootDir
End Function
Function SetVDirCacheability(IISObject, CacheTimeInSeconds)
Call SetCacheability(IISObject, CacheTimeInSeconds, "must-revalidate")
End Function
Function SetCacheability(IISObject, CacheTimeInSeconds, CacheDirective)
On Error Resume Next
IISObject.CacheControlCustom = "max-age=" & CacheTimeInSeconds & "," & CacheDirective
If (Err.Number <> 0) Then
WScript.Echo "CacheControlCustom property set failed, error code: " & Err.Number
WScript.Quit
Else
WScript.Echo "CacheControlCustom property set: max-age=" & CacheTimeInSeconds & "," & CacheDirective
End If
IISObject.SetInfo
End Function
Function GetVDirEvenIfNotExists(DirName, ServerName, SiteNumber)
Dim Dir
Dim RootDir
On Error Resume Next
Set Dir = GetObject("IIS://" & ServerName & "/w3svc/" & SiteNumber & "/root/" & DirName)
If (Err.Number <> 0) Then
WScript.Echo "VDir " & DirName & " does not exist yet"
Set RootDir = GetRootDir(ServerName, SiteNumber)
Set Dir = RootDir.Create("IIsWebVirtualDir", DirName)
If (Err.Number <> 0) Then
WScript.Echo "VDir " & DirName & " could not be created, error code " & Err.Number
WScript.Quit
Else
WScript.Echo "VDir " & DirName & " created"
End If
Dir.SetInfo
Else
WScript.Echo "VDir " & DirName & " already exists"
End If
Set GetVDirEvenIfNotExists = Dir
End Function
Function GetVDirAndSetCacheability(DirName, ServerName, SiteNumber, CacheTimeInSeconds)
Dim Dir
Set Dir = GetVDirEvenIfNotExists(DirName, ServerName, SiteNumber)
Call SetVDirCacheability(Dir, CacheTimeInSeconds)
Set GetVDirAndSetCacheability = Dir
End Function
Sub SetHTMLCharSet(ServerName, SiteNumber)
Dim strExtension, strMimeType
strExtension = ".htm"
strMimeType = "text/html; charset=utf-8"
AddTypeToIIS ServerName, strExtension, strMimeType
strExtension = ".html"
strMimeType = "text/html; charset=utf-8"
AddTypeToIIS ServerName, strExtension, strMimeType
End Sub
Sub AddTypeToIIS(ServerName, varMimeExt, varMimeTyp)
Dim boolFound, intCount, intMimeMap, objMimeMap, varMimeMap, i, MMItem, aMimeMapNew()
Const ADS_PROPERTY_UPDATE = 2
' create the ADSI object & current MIME map at that path
Set objMimeMap = GetObject("IIS://" & ServerName & "/MimeMap")
varMimeMap = objMimeMap.GetEx("MimeMap")
'Delete a mapping by copying to a new map array.
i = 0
For Each MMItem in varMimeMap
If MMItem.Extension <> varMimeExt Then
Redim Preserve aMimeMapNew(i)
Set aMimeMapNew(i) = CreateObject("MimeMap")
aMimeMapNew(i).Extension = MMItem.Extension
aMimeMapNew(i).MimeType = MMItem.MimeType
i = i + 1
Else
WScript.Echo "Mime type " & MMItem.Extension & " excluded"
End If
Next
objMimeMap.PutEx ADS_PROPERTY_UPDATE, "MimeMap", aMimeMapNew
objMimeMap.SetInfo
' get the MIME map count
intMimeMap = UBound(aMimeMapNew) + 1
' if no extension information is found, create the new mapping
ReDim Preserve aMimeMapNew(intMimeMap)
Set aMimeMapNew(intMimeMap) = CreateObject("MimeMap")
aMimeMapNew(intMimeMap).Extension = varMimeExt
' store the new information in the MIME map
aMimeMapNew(intMimeMap).MimeType = varMimeTyp
objMimeMap.PutEx ADS_PROPERTY_UPDATE, "MimeMap", aMimeMapNew
objMimeMap.SetInfo
End Sub
Sub DisplayUsage
WScript.Echo "Usage: iis_caching_policy.vbs <-s|--ServerName ""SERVERNAME""> <-i|--SiteNumber ""SITENUMBER""> [-?|--Help]"
WScript.Echo ""
WScript.Echo "SERVERNAME Optional - The name of the server, default is localhost"
WScript.Echo "SITENUMBER Optional - The sitenumber of the affected site, default is 1"
WScript.Echo ""
WScript.Echo "Example: iis_caching_policy.vbs -s tracker -i 3"
WScript.Quit (1)
End Sub
If you look closely at the script (not too close, it's not a major work of art) you can see there's an exception for two files: super.css and include.js. The reason for this is that all pages are published as static pages from a Content Management System. Static pages deliver the optimum trade off in speed versus processing power. The problem with static pages, idempotent behavior and caching however is that if you do your work very well hardly any updates will get over to the customer unless you change filenames. This is rather unwieldy with a large website as this would require continuous republication of the website when you want to deliver new javascript or css files. The trick I applied was to have a "super" CSS file that includes all other CSS files. The super CSS file has a very limited time span (1 hour), as demonstrated by the VBScript, so you can update the included CSS files by changing their filenames and the client will notice the updated CSS files after the super CSS file has expired after one hour without having to republish all web pages. The same trick applies to the Javascript files.
Oh, by the way this stuff is a breeze to setup in Apache but sometimes you have to work with the stuff at hand.
PHP, DOM and XPath query
Category: PHP, XML/XSL
Published: 2007-01-28
Updated: 2009-05-29
My work for the Lilliput CMS has lead to some interesting new findings about PHP 5's new DOM functions, particularly the XPath part of it. Whilst working on the templating structure for Lilliput I couldn't get the DOM XPath queries to work on the file at hand, a normal XHTML 1.0 Strict document. With an external tool called XPath Explorer (XPE) every query evaluated correctly but as soon as I tried the same XPath expression in PHP it failed. After searching long and hard I came across a code snippet that contained the solution namely to explicitly declare the xhtml namespace for the DOM document you're working on:
<?php
$templateFile = 'template/lilliput/index.xhtml';
$dom = new DomDocument('1.0', 'utf-8');
$returnValue = $dom->load($templateFile);
$xpath = new DOMXPath($dom);
$xpath->registerNamespace("xhtml", "http://www.w3.org/1999/xhtml");
$nodelist = $xpath->query('//xhtml:div[@id="content"]');
$noderesult = $nodelist->item(0);
?>
After this no problems any more but very strange that this isn't mentioned in the PHP DOM documentation anywhere. Even stranger that my user contributed note on this on the XPath query function manual page doesn't seem to have been accepted. Hopefully somebody will gain some help from this blog entry.
Atom feeds, client-side XSLT and crappy browsers
Category: XML/XSL
Published: 2007-01-09
Updated: 2009-05-29
After providing feedback on an article on 24 ways called "Making XML Beautiful Again: Introducing Client-Side XSL" that never got published I decided to write a short entry that explains how this feedsite escapes the feed sniffing and subsequent browser applied default XSLT stylesheet that has been implemented by Safari 2, FireFox 2 and Internet Explorer 7. As soon as these browsers see content they perceive as a feed they ignore all instructions like an XSL style sheet inclusion and render the contents to their liking instead of the authors liking.
Luckily enough the method these browsers employ for feed sniffing is crude enough to trick them into believing they're not dealing with a genuine feed by eating up the first 512 bytes of the feed with unrecognizable crud. So basically the only thing you need to do is insert a comment before the feed element that is larger than 512 bytes. An excerpt from the source code of this page:
<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="atom" type="text/xsl"?>
<!-- This is a comment that has been inserted because of the arrogance of IE7 and FireFox 2 developers that have decided that they don't need to honour a xml stylesheet instruction. Luckily the designers of these browsers use very brittle sniffing techniques that can be overridden by consuming the first 512 bytes of an xml file. This comment provides these essential 512 bytes of crud and destroys the nice simplicity and cleanliness of my Atom feed. This is a comment that has been inserted because of the arrogance of IE7 and FireFox 2 developers that have decided that they don't need to honour a xml stylesheet instruction. Luckily the designers of these browsers use very brittle sniffing techniques that can be overridden by consuming the first 512 bytes of an xml file. This comment provides these essential 512 bytes of crud and destroys the nice simplicity and cleanliness of my Atom feed. This is a comment that has been inserted because of the arrogance of IE7 and FireFox 2 developers that have decided that they don't need to honour a xml stylesheet instruction. Luckily the designers of these browsers use very brittle sniffing techniques that can be overridden by consuming the first 512 bytes of an xml file. This comment provides these essential 512 bytes of crud and destroys the nice simplicity and cleanliness of my Atom feed. -->
<feed
xmlns:xforms="http://www.w3.org/2002/xforms"
xml:lang="e>
<title>FeedMind</title>
And presto you can apply your own client-side XSLT to your hearts content.
Building this site
Category: XML/XSL
Published: 2006-02-14
Updated: 2009-06-03
This blog uses a novel approach (I think) in that it is entirely Atom 1.0 based. There is no underlying (X)HTML, everything is Atom (with a hint of XSL). I came to this approach after I figured out that it is silly to serve the same content in multiple formats when one format suffices. Modern browser are perfectly capable of executing XML/XSL that allow for the transformation of the Atom 1.0 feed into XHTML. There's some nice stuff going, let's dig into the details (hey I'm a technologist at heart so why not busy myself with the stuff I love).
The main engine is Pivot, http://www.pivotlog.net. I like Pivot more and more, it doesn't get in the way and I haven't had to tweak a single line of code to get where I'm at right now which is frankly amazing to me and a tribute to the design of Pivot.
Pivot creates an Atom output file, index.xml (the filename is configurable in Pivot).
This file is configured as default document via an Apache .htaccess instruction:
DirectoryIndex index.xml
This means your browser gets XML (Atom) served directly when you access http://feedme.mind-it.info/ instead of HTML.
Pivot has several templates available to apply to content output, one of them being an Atom 1.0 template. Due to Pivot's ingenious templating structure I was able to add an XSL stylesheet to the XML output without a problem:
<?xml-stylesheet type="text/xsl" href="atom"?>
The XSL stylesheet is called atom, I use Apache content negotiation via Multiviews to deliver the correct XSL stylesheet with either application/xhtml+xml or text/html (Internet Explorer) as media format:
Options +Multiviews
AddType application/xhtml+xml;qs=0.9 .xsl+xhtml
AddType text/html;qs=0.9 .xsl
AddType application/xhtml+xml .xsl+xhtml
AddType text/html .xsl+html
The code above basically selects the right atom XSL file based on the capabilities of the browser. If the browser is application/xhtml+xml capable and prefers this media type (via the qs quality factor) the atom.xsl+xhtml file will be served. If on the other hand the browser isn't willing or capable (think Internet Explorer) it will be served the atom.xsl+html file.
atom.xsl+html:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns="http://www.w3.org/1999/xhtml">
<xsl:output encoding="utf-8" indent="yes" method="xml"
omit-xml-declaration="yes"
media-type="text/html"
doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
doctype-system="DTD/xhtml1-strict.dtd"
cdata-section-elements="script style"/>
<xsl:include href="main.xml" />
</xsl:stylesheet>
atom.xsl+xhtml:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns="http://www.w3.org/1999/xhtml">
<xsl:output encoding="utf-8" indent="yes" method="xml"
omit-xml-declaration="no"
media-type="application/xhtml+xml"
doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
doctype-system="DTD/xhtml1-strict.dtd"
cdata-section-elements="script style"/>
<xsl:include href="main.xml" />
</xsl:stylesheet>
The XSL file itself uses an xsl:include to import the main part of the XSL transformation so I can have two relatively small XSL files to support the content negotiation (atom.xsl+html for text/html and atom.xsl+xhtml for application/xhtml+xml).
All looked well but somehow the HTML formatting wasn't getting through in Mozilla FireFox. No problems in Internet Explorer though, strange ... This is the kind of conundrum that can take all day to solve and it did (take a whole day). On the positive side I learned a lot about XSL, CDATA, namespaces and local names but man what an annoying problem. It turns out that Internet Explorer is quite forgiving but Firefox is more strict in copying portions of XML in a different namespace in the result XML tree. The source document is in the Atom 1.0 namespace (http://www.w3.org/2005/Atom) whilst the result document is in the XHTML namespace (http://www.w3.org/1999/xhtml). Firefox will not allow you to do this (as per XSL specification). The solution took ages to find with various sidesteps involving trying to replace the Atom namespace via XSL but the solution turned out to be pretty simple. I just have to add a <div> element with the XHTML namespace to the content element and all is well.
<content type="xhtml" xml:lang="en" xml:base="http://feedme.mind-it.info/pivot/entry.php?id=8">
<div xmlns="http://www.w3.org/1999/xhtml">
..
</div>
</content>
Well that's basically it. I need to change and tweak a lot but the technique described here is basically what drives this feed/site.
BTW if you're wondering about the layout of the site, I'm not much of a designer so I used a ready made layout from Andreas Viklund. The photo at the top is mine though, I shot it in 2003 in Gyongju (South-Korea).
Welcome to FeedMind
Category: default
Published: 2006-02-14
Updated: 2009-05-29
Welcome to FeedMind, my personal blogging space and ongoing experiment in web technology. I've been thinking about doing a blog for years, started several times but never finished anything. This time it looks like I'm going to succeed :-) I'll be mainly blogging about ICT Architecture, Information Security and Web Technology in the broadest sense. Once in a while something personal may crop up, it can't be helped.
I'm quite a practical guy although I do like the saying that "there's nothing more practical than a good theory". There's a lot of opinion floating around that's propagated by people who obviously haven't carried any large projects in the real world. There's a limit to the amount of new technology you can cram into any project and believe you me that five year old technology still counts as new. I hope to add a voice to the chorus that tries to explain that there's more than one way to do it (although certain problems can have only a limited amount of correct solutions). You see, I'm already being reasonable on the first posting, it's stronger than myself.
Anyway, hope you like the blog.
Cheers,
Meint