<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Hacking at 0300 &#187; PHP</title>
	<atom:link href="http://bililite.com/blog/category/php/feed/" rel="self" type="application/rss+xml" />
	<link>http://bililite.com/blog</link>
	<description>Thoughts on web design and programming from a very occasional volunteer webmaster</description>
	<lastBuildDate>Fri, 23 Jul 2010 09:00:56 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0</generator>
		<item>
		<title>Parsing the HTTP Accept: header</title>
		<link>http://bililite.com/blog/2010/01/06/parsing-the-http-accept-header/</link>
		<comments>http://bililite.com/blog/2010/01/06/parsing-the-http-accept-header/#comments</comments>
		<pubDate>Thu, 07 Jan 2010 03:03:13 +0000</pubDate>
		<dc:creator>Danny</dc:creator>
				<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://bililite.com/blog/?p=1126</guid>
		<description><![CDATA[I wanted the webservices to be as RESTful as possible, so they should use the Accept: header rather than file name extensions to determine the type. As I noted before, this won't work in general, because most browsers don't send the correct Accept: headers. Still, that's no reason not to use the header if no [...]]]></description>
			<content:encoded><![CDATA[<p>I wanted the <a href="/blog/2009/12/31/bililite-com-webservices/">webservices</a> to be as <a href="http://en.wikipedia.org/wiki/Representational_State_Transfer">RESTful</a> as possible, so they should use the <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"><code>Accept:</code> header</a> rather than file name extensions to determine the type.<span id="more-1126"></span> As I noted before, this won't work in general, because <a href="http://www.newmediacampaigns.com/page/browser-rest-http-accept-headers">most browsers don't send the correct <code>Accept:</code> headers</a>. Still, that's no reason not to use the header if no extension is sent.</p>
<p>Parsing the header is straightforward; the mime-types are separated by commas (note that the top-level separation is the comma, not the semicolon) and within each mime-type description the parameters are separated by semicolons. The first field is the mime-type itself (with possible wild card '*'), the second (if present) is the quality factor, "q={number from 0 to 1}" which describes how much the requestor wants that particular mime-type, and the remainder of the fields are other parameters.</p>
<p>As the <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1">specification</a> says, parameters in the <code>Accept:</code> header are rare, and my programs don't use them, so we will ignore them. That means that <code>Accept: text/html; q=0.8; level=2, text/html; q=0.2; level=1</code>
is interpreted the same as <code>Accept: text/html; q=0.2</code>; not exactly the same as the spec but easier to code.</p>
<p>The q parameter technically should be weighted by how much the <em>host</em> wants to send that type. Thus, <code>Accept: image/jpeg; q=1, image/gif; q=0.2</code> means "Send me a <code>png</code>; I'll accept
a <code>jpeg</code>, but I'll be only 20% as happy." If the host feels that the <code>gif</code> is 10 times better than the <code>jpeg</code>, it should rank the <code>gif</code> as 10*0.2 = 2 and the <code>jpeg</code> as 1*1 = 1, and send the <code>gif</code>. My code doesn't have such strong feelings and ignores such niceties.</p>
<p>A q parameter of 0 should be interpreted as "never send me this type" but my code simply ignores it and, if there are no other mime-types in the <code>Accept:</code> header, sends the default. If it's the default type that has the q=0, then the requestor loses.</p>
<p>One subtle trick from <a href="http://github.com/recess/recess/blob/master/recess/recess/http/AcceptsList.class.php">Kris Jordan's Recess</a>: wild card ('text/*' and '*/*') types should rank <em>below</em> more specific types with the same q parameter. We subtract 0.0001 from the stated q to push them lower; the specification says the value cannot have more than 3 decimal places.</p>
<pre><code class="language-php">function parseAccept ($accept){
	$mimetypes = array( // associate types with file extensions
		'*/*' =&gt; 'html',
		'text/*' =&gt; 'txt',
		'text/plain' =&gt; 'txt',
		'text/html' =&gt; 'html',
		'text/csv' =&gt; 'csv',
		'text/javascript' =&gt; 'js',
		'image/*' =&gt; 'png',
		'image/png' =&gt; 'png',
		'image/gif' =&gt; 'gif',
		'image/jpeg' =&gt; 'jpg',
		'application/*' =&gt; 'js',
		'application/json' =&gt; 'js',
		'application/xml' =&gt; 'xml'
	);

	$types = array();
	foreach (explode(',', $accept) as $mediaRange){
		@list ($type, $qparam) = preg_split ('/\s*;\s*/', $mediaRange); // the q parameter must be the first one according to the RFC
		$q = substr ($qparam, 0, 2) == 'q=' ? floatval(substr($qparam,2)) : 1;
		if ($q &lt;= 0) continue;
		if (substr($type, -1) == '*') $q -= 0.0001;
		if (@$type[0] == '*') $q -= 0.0001;
		$types[$type] = $q;
	}
	arsort ($types); // sort from highest to lowest q value
	foreach ($types as $type =&gt; $q){
		if (isset ($mimetypes[$type])) return $mimetypes[$type];
	}
	return 'html';
}</code></pre>
<p>The same sort of code would work for <code>Accept-Encoding:</code> and the other Accept-type headers.</p>]]></content:encoded>
			<wfw:commentRss>http://bililite.com/blog/2010/01/06/parsing-the-http-accept-header/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>bililite.com webservices</title>
		<link>http://bililite.com/blog/2009/12/31/bililite-com-webservices/</link>
		<comments>http://bililite.com/blog/2009/12/31/bililite-com-webservices/#comments</comments>
		<pubDate>Fri, 01 Jan 2010 03:51:48 +0000</pubDate>
		<dc:creator>Danny</dc:creator>
				<category><![CDATA[Medical Informatics]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://bililite.com/blog/?p=1120</guid>
		<description><![CDATA[I've been spending my project time learning how to manipulate images in PHP to let me create custom growth charts and the like, with a RESTful interface that would allow them to be used as the source of &#60;img&#62; elements. I like the way things turned out; they are available at bililite.com/webservices/. It includes height,weight, [...]]]></description>
			<content:encoded><![CDATA[<p>I've been spending my project time learning how to manipulate images in PHP to let me create custom growth charts and the like, with a <a href="http://en.wikipedia.org/wiki/Representational_State_Transfer">RESTful</a> interface that would allow them to be used as the source of &lt;img&gt; elements. I like the way things turned out; they are available at <a href="/webservices/">bililite.com/webservices/</a>. It includes height,weight, head circumference, peak expiratory flow (for managing asthma), blood pressure and bilirubin levels. While it took a fair amount of trial-and-error to get things to work, I don't think there's much that's innovative enough programming-wise to write a tutorial.</p>
<p>I want to get it to parse the Accept: header, so  users can leave out the file extensions, but it looks like <a href="http://www.newmediacampaigns.com/page/browser-rest-http-accept-headers">browsers don't implement that correctly</a>, so I'll have to keep using extensions as well.</p>
<p>I still need to learn more about caching and effectively using eTags and all the headers that go with it. It always makes my brain hurt.</p>]]></content:encoded>
			<wfw:commentRss>http://bililite.com/blog/2009/12/31/bililite-com-webservices/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Moving the Blog</title>
		<link>http://bililite.com/blog/2009/06/05/moving-the-blog/</link>
		<comments>http://bililite.com/blog/2009/06/05/moving-the-blog/#comments</comments>
		<pubDate>Fri, 05 Jun 2009 14:01:38 +0000</pubDate>
		<dc:creator>Danny</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web Design]]></category>

		<guid isPermaLink="false">http://bililite.com/blog/?p=824</guid>
		<description><![CDATA[I decided that this blog is mine, rather than Young Israel's, so I wanted to move it to a domain that I personally own. Thus, it is no longer at youngisrael-stl.org/wordpress, but at bililite.com/blog. Moving the blog intact was non-trivial, so I'm recording how I did it, based on mydigitallife's instructions: Create the new folder, [...]]]></description>
			<content:encoded><![CDATA[<p>I decided that this blog is mine, rather than Young Israel's, so I wanted to move it to a domain that I personally own. Thus, it is no longer at youngisrael-stl.org/wordpress, but at bililite.com/blog. Moving the blog intact was non-trivial, so I'm recording how I did it, based on <a href="http://www.mydigitallife.info/2007/10/01/how-to-move-wordpress-blog-to-new-domain-or-location/">mydigitallife's instructions</a>:</p>
<ol>
<li>Create the new folder, bililite.com/blog/</li>
<li>Create a new mySQL database on bililite.com
<li>Copy all of youngisrael-stl.org/wordpress/ into bililite.com/blog/</li>
<li>Edit the wp-config.php file to reflect the new database name/user/password</li>
<li>Backup the youngisrael-stl.org database into an SQL file with <a href="http://www.phpmyadmin.net/">phpMyAdmin</a> or <a href="/blog/2009/02/10/the-agony-of-unicode-and-backing-up-mysql/">some other tool</a></li>
<li>In a text editor, change all the "http://youngisrael-stl.org/wordpress" to "http://bililite.com/blog" and "/wordpress" to "/blog". note that there are both absolute and relative URL's in there, and I was changing both the domain and the subdirectory, so I had to do two replacements. I couldn't just change "http://youngisrael-stl.org" because I didn't want to change links to the actual site.</li>
<li>Run the edited SQL file on the bililite.com database</li>
<li>In the youngisrael-stl.org/.htaccess , add the line "<a href="http://httpd.apache.org/docs/2.2/mod/mod_alias.html#redirect">Redirect</a> permanent /wordpress http://bililite.com/blog" to let browsers know I've moved</li>
<li>Actually announce the change on mailing lists of <a href="http://groups.google.com/group/jquery-ui-dev/topics?hl=en">people who actually read the site</a></li>
</ol>]]></content:encoded>
			<wfw:commentRss>http://bililite.com/blog/2009/06/05/moving-the-blog/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Scheduling tasks with PHP</title>
		<link>http://bililite.com/blog/2009/02/12/scheduling-tasks-with-php/</link>
		<comments>http://bililite.com/blog/2009/02/12/scheduling-tasks-with-php/#comments</comments>
		<pubDate>Fri, 13 Feb 2009 03:50:11 +0000</pubDate>
		<dc:creator>Danny</dc:creator>
				<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://bililite.com/blog/?p=381</guid>
		<description><![CDATA[I've got a cheap website that doesn't let me use cron to schedule tasks (like database backups), so I had to do it myself. I found pseudo-cron, which looks cool but has some bugs and was more complicated than I wanted, so I wrote a simple PHP script to do what I wanted. They key [...]]]></description>
			<content:encoded><![CDATA[<p>I've got a cheap website that doesn't let me use <a href="http://en.wikipedia.org/wiki/Cron">cron</a> to schedule tasks (like <a href="http://bililite.com/blog/2009/02/10/the-agony-of-unicode-and-backing-up-mysql/">database backups</a>), so I had to do it myself. I found <a href="http://www.bitfolge.de/?l=en&#038;s=pseudocron">pseudo-cron</a>, which looks cool but has <a href="http://phpbb.bitfolge.de/viewtopic.php?t=319">some bugs</a> and was more complicated than I wanted, so I wrote a simple PHP script to do what I wanted.</p>
<span id="more-381"></span>
<p>They key is that while I can't schedule tasks to run, I can count on one event happening at least <a href="http://www.sitemeter.com/?a=stats&#038;s=s41yistl">a couple times a day</a>: hits on the website. So I add a <code class="language-php">require_once('/path/to/scripts/scheduler.php')</code> at the start of every page, and, as long as the scheduled tasks don't take too long, the website user never notices that I'm taking advantage of him.</p>
<p>At each call, the script pulls a single job to run from a mySQL database, runs it and updates the next scheduled time.</p>
<p>The table 'schedule' has 4 fields:</p>
<dl>
<dt>id: Varchar</dt>
<dd>Unique identifier for that job.</dd>
<dt>frequency: Varchar</dt>
<dd>How often to run the job. Will be passed to <a href="http://php.net/strtotime">strtotime</a>. Typical values 'next week', 'next Sunday', 'next month', 'next year'.</dd>
<dd>If it evaluates to a day in the past, the year is incremented until it is in the future. That way 'April 15' will be the coming April 15.</dd>
<dd>Special case: numbers are interpreted as days of the month (at midnight); '28' will run on the next 28th of the month. Use 0 for the last of the month, -1 for the second to last of the month.</dd>
<dd>Note that this uses <a href="http://www.php.net/mktime">mktime</a> which corrects dates forward; so a frequency of 31 will trigger on Jan 31, Mar 2, Mar 31, May 1, May 31, Jul 1, Jul 31, Aug 31, Oct 1, Oct 31 and Dec 1. Again, use 0 for the last day of the month</dd>
<dd>Note: you can do things like '+6 hours' for time scheduling as well, though I haven't tested this.</dd>
<dt>scheduled: Datetime</dt>
<dd>The next scheduled run time; set to NULL or a point in the past to have the job run immediately</dd>
<dt>command: Text</dt>
<dd>Text that will be passed unchanged to <a href="http://www.php.net/eval">eval</a>.</dd>
</dl>

<pre><code class="language-php">
// for debugging, use 'example.com/scheduler.php?scheduledebugdate=2009-02-12' to force the script
// to pretend that the current time is 2009-02-12
	$now = $_GET['scheduledebugdate'] ? strtotime($_GET['scheduledebugdate']) : time();
	$sqlnow = date ('Y-m-d H:i:s', $now);
	$sql = mysql_connect($server, $user, $password); mysql_select_db ($database, $sql);
	$result = mysql_query ("SELECT * FROM schedule WHERE `scheduled` IS NULL OR `scheduled` <= '$sqlnow' LIMIT 1", $sql);
	// note only one job at a time; otherwise the website user who doesn't know he's triggering
	// this code may wait forever. Change the LIMIT term if you want to run more than one job
	while ($row = mysql_fetch_assoc ($result)) {
		eval($row['command']);
		$f = $row['frequency'];
		if (is_numeric($f)){
			$d = getdate($now);
			do {
				$nexttime = mktime($d['hours'], $d['minutes'], $d['seconds'], $d['mon']++, $f, $d['year']);
			}while ($nexttime <= $now);
		}else{
			$nexttime = strtotime($f, $now);
			while ($nexttime <= $now) $nexttime = strtotime('+1 year', $nexttime);
		}
		$nexttime = date ('Y-m-d H:i:s', $nexttime);
		mysql_query ("UPDATE schedule SET `scheduled`='$nexttime' WHERE `id` = '${row['id']}'", $sql);
	}
</code></pre>]]></content:encoded>
			<wfw:commentRss>http://bililite.com/blog/2009/02/12/scheduling-tasks-with-php/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>The Agony of Unicode (and backing up mySQL)</title>
		<link>http://bililite.com/blog/2009/02/10/the-agony-of-unicode-and-backing-up-mysql/</link>
		<comments>http://bililite.com/blog/2009/02/10/the-agony-of-unicode-and-backing-up-mysql/#comments</comments>
		<pubDate>Tue, 10 Feb 2009 07:19:52 +0000</pubDate>
		<dc:creator>Danny</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web Design]]></category>

		<guid isPermaLink="false">http://bililite.com/blog/?p=359</guid>
		<description><![CDATA[All I wanted to do was back up the Young Israel databases, some way more amenable to automation than phpMyAdmin. There are lots of PHP-based solutions on the web, but all seem based on mysqldump. I implemented one and found myself faced with an eyeful of ××¨×™××œ ×ž××™×¨ ×™×¢×§×‘ ×‘×Ÿ ×“×•×“ ××‘×¨×”× where the Hebrew [...]]]></description>
			<content:encoded><![CDATA[<p>All I wanted to do was back up the Young Israel databases, some way more amenable to automation than <a href="http://www.phpmyadmin.net/home_page/index.php">phpMyAdmin</a>. There are <a href="http://www.google.com/search?q=php+mysql+backup">lots of PHP-based solutions</a> on the web, but all seem based on <a href="http://dev.mysql.com/doc/refman/5.1/en/mysqldump.html">mysqldump</a>. I implemented <a href="http://www.dagondesign.com/articles/automatic-mysql-backup-script/">one</a> and found myself faced with an eyeful of ××¨×™××œ ×ž××™×¨ ×™×¢×§×‘ ×‘×Ÿ ×“×•×“ ××‘×¨×”× where the Hebrew names should be. Turns out this a known bug; <a href="http://bugs.mysql.com/bug.php?id=28969">mysqldump can't handle Unicode</a>. There are reports of <a href="http://vandenabeele.com/mysqldump-utf8-bug">workarounds</a>, but I spent 8 hours not getting anything to work.<p>
<p>So I had to write my own backup, going through each database on a server then each table in the database, writing the appropriate <code>INSERT INTO</code> commands (thank goodness the <code>SHOW CREATE DATABASE</code> and <code>SHOW CREATE TABLE</code> commands work correctly). It wasn't terribly miserable (not nearly as miserable it was trying to use someone else's tool that doesn't work), and now I get my&nbsp;
 אריאל מאיר יעקב בן דוד אברהם  just fine, and the backup works. I import the generated file into my local copy of mySQL and it regenerates the databases.</p>
<span id="more-359"></span>
<pre><code class="language-php">
// if this file is called directly from the web, run the backup on the requested databases
if (realpath(__FILE__) == realpath($_SERVER['SCRIPT_FILENAME'])) db_backup($_REQUEST['database']);

function db_backup($databases){
	if (!is_array($databases)) $databases = array($databases);
	$dir = '/backup'; // change this as desired
	if( !is_dir( $dir ) &#038;& !mkdir( $dir )) die('&lt;h1&gt;Could not create backup directory&lt;/h1&gt;'); 
	foreach ($databases as $database){
		list($host, $name, $user, $password) = explode('/', $database);
		$now = date('Y-m-d'); // change this as desired
		@mysql_connect($host, $user, $password) or die ("&lt;h1&gt;Could not connect to $host&lt;/h1&gt;");
		file_put_contents("$dir/$name-$now.sql", dump_dbs());
	}
}

// based on http://davidwalsh.name/backup-mysql-database-php
function dump_dbs(){
	$ret = '';
	$dbs = mysql_list_dbs();
	while ($db_row = mysql_fetch_object($dbs)) {
		$db = $db_row->Database;
		if ($db == 'information_schema') continue;
		mysql_select_db ($db);
		mysql_query("SET NAMES 'utf8'");  // magic command to make mySQL output UTF8 to PHP
		$ret = "DROP DATABASE IF EXISTS `$db`;\n";
		$createdb = mysql_fetch_row(mysql_query("SHOW CREATE DATABASE `$db`"));
		$ret .= $createdb[1].";\nUSE `$db`;\n";
		$tables = mysql_query('SHOW TABLES');
		while($table = mysql_fetch_row($tables)){
			$ret .= dump_table($table[0]);
		}
	} // while
	return $ret;
}

function dump_table($table){
	$ret = "\nDROP TABLE IF EXISTS `$table`;\n";
	$createtable = mysql_fetch_row(mysql_query("SHOW CREATE TABLE `$table`"));
	$ret .= $createtable[1].";\n";
	$dbresult = mysql_query ("SELECT * FROM `$table`");
	while($row = mysql_fetch_assoc($dbresult)) {
		$keys = fixkeys(array_keys($row));
		$values = fixvalues(array_values($row));
		$ret .= "\tINSERT INTO `$table` (".join(',', $keys).') VALUES ('.join(',',$values).");\n";
	}
	return $ret;
}
function fixkeys ($arr){
	foreach ($arr as &#038;$i){
		$i = "`$i`";
	}
	return $arr;
}
function fixvalues($arr){
	foreach ($arr as &#038;$i){
		$i = is_null($i) ? 'NULL' : "'".preg_replace("/\r\n|\n|\r/",'\n',addslashes($i))."'";
	}
	return $arr;
}
</code></pre>
<p>The main function db_backup takes a string or an array of strings that describe the databases to be backed up, of the form <code>'host/name/user/password'</code>. For the databases that <a href="http://1and1.com">my host</a> provides it would look like <code>'db12345.perfora.net/descriptivename/dbo45678/password'</code>, where <code>descriptivename</code> is whatever you want prepended to the backup file, which is named 'descriptivename-2009-02-09.sql' or whatever the date is.</p>
<p>If it is called directly as a web page, the database strings are taken from the query string with key "database". If you want to pass more than one database, don't forget to use <code>database[]=</code> to make it into a PHP array. Thus,</p> <pre><code>http://example.com/db_backup.php?database=db12345.perfora.net/serverdata/dbo45678/password</code></pre>
<p>or</p>
<pre><code>http://example.com/db_backup.php?database[]=db12345.perfora.net/serverdata/dbo45678/password&#038;database[]=localhost/localdata</code></pre>
<p>I hope this saves someone a whole lot of agony!</p>]]></content:encoded>
			<wfw:commentRss>http://bililite.com/blog/2009/02/10/the-agony-of-unicode-and-backing-up-mysql/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
