Posts Tagged ‘sparkle’

Keeping track of your users hardware / Software with Sparkle and PHP.

Tuesday, April 15th, 2008

Some months ago I started using the amazing Sparkle framework to manage the auto-update features on my MediaInfo Mac application, and it wasn’t until today that I decided to update my Sparkle version to the latest one from the SVN and give it a try, mostly because I’m planning to release upcoming MediaInfo Mac builds as 32 and 64bit Universal Binaries, but that’s a different story.

I have found that with the new Sparkle version, you can kindly ask the user if he/she wants to send anonymous information about the system they are using to run your application, like the MacOS version, amount of RAM, number of processors, language, etc. This is specially useful for many reasons (at least for me), first, I’m starting the process of localizing my application, so that gives me a pretty good idea of what languages are the most used so far, and, finally, it will let me now when I could stop caring about Tiger and start a Leopard only version of my Software.

The idea is pretty simple, you have a PHP script that generates the whole AppCast compatible feed, and all the hardware / software information is passed as url parameters, as in feed.php?osVersion=10.2.5&lang=en … etc.

So, by using good old Google, i found a really good script to generate the AppCast feed, but, i also needed a way to store the tracking data into a DB for later usage, and, i came up with this (really barebones, but working) prototype.

So, i did some minor modifications to the original PHP code; first, i changed the way to parse the version number from the file names, so as long as you use the application_name_1.5.zip pattern it will work, please note that you can put as much revision or subversion numbers as you wish: application_name_1.2.3.4.5.zip should also work.

This is the code snippet as i have it working now on my development server:

<?php
// ----------------------------------------------------------- //
// Script to generate an RSS appcast from folder contents
// Version 1.0.1
//
// (cc) Random Sequence 2007, Some Rights Reserved
//
// Licenced under a creative commons Attribution ShareAlike licence
// http://creativecommons.org/licenses/by-sa/3.0/
// ----------------------------------------------------------- //
// REQUIRES PHP 5 or greater
// Tested with APACHE 1 & 2 on Mac OS X, Debian Linux   

// -------------------- BEGIN CONFIG ------------------------- //

$title = "Downloads";           // Used as feed title in feed readers
$description = "File List";     // Used as feed description in feed readers

// these are the types of files to list in the appcast & their MIME Types. Use lower case.
$fileTypes = array( "zip"=>"application/zip",
                    "tgz"=>"application/x-gtar",
                    "tar"=>"application/x-tar",
                    "dmg"=>"application/octet-stream"
                    );

// -------------------- END OF CONFIG ------------------------ //

$appcastHeader = "<?xml version=\"1.0\" encoding=\"utf-8\"?>
<rss version=\"2.0\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:sparkle=\"http://www.andymatuschak.org/xml-namespaces/sparkle\">
<channel>
    <title>MediaInfo Mac Software Updates Feed</title>
    <link>http://massanti.com/mediainfo</link>
    <description>AppCast compatible feed for MediaInfo Mac Updates</description>
    <language>en</language>
";
$appcastTemplate = "
        <item>
            <guid  isPermaLink=\"false\">*guid*</guid>
            <title>*title*</title>
            <description><![CDATA[*description*]]></description>
            <pubDate>*pubdate*</pubDate>
            <enclosure
                sparkle:version=\"*version*\"
                type=\"*type*\"
                url=\"*url*\"
                length=\"*length*\" />
        </item>";

$appcastFooter = "
</channel>
</rss>";
$dir = getcwd()."/releases";
$files = scandir("$dir");
$etag = sha1(implode("/",$files));
rsort($files);

// support for conditional fetch
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag) {
    header('HTTP/1.1 304 Not Modified');
    exit;
}

$appcast = $appcastHeader;

$link = "http://".$_SERVER["HTTP_HOST"].$_SERVER["PHP_SELF"];

$appcast = str_replace("*link*",$link,$appcast);
$appcast = str_replace("*title*",$title,$appcast);
$appcast = str_replace("*description*",$description,$appcast);

foreach ($files as $file) {
	preg_match("/.*\.([a-z09]{1,3})$/i",$file,$matches);
	if (isset($matches[1]) && isset($fileTypes[strtolower($matches[1])]) !== false) {
		$appcastFile = $appcastTemplate;
		$folderUrl = "http://".$_SERVER["HTTP_HOST"].substr($_SERVER["PHP_SELF"],0,strrpos($_SERVER["PHP_SELF"],"/"))."/releases/";
		$guid = $folderUrl.sha1($file);
		$path_parts = pathinfo($file);
		$title = mb_convert_case( str_replace("_", " ", $path_parts['filename']) , MB_CASE_TITLE, "UTF-8");
		//$description = preg_replace("/^(.*?)([0-9]+[a-z])\.([a-z09]{1,3})$/i","$1",$file);
		$description = getFileDescription("releases/".$file.".html");
		$pubdate = date("D, d M Y H:i:s",filectime("releases/".$file));
		$type = $fileTypes[strtolower($matches[1])];
		$url = $folderUrl.$file;
		$length = filesize("releases/".$file);  
		preg_match("/(\d+\.)+\d+/", $file, $m_version);
		$version = $m_version[0];
		//$version = preg_replace("/^(.*?)([0-9]+[a-z])\.([a-z09]{1,3})$/i","$2",$file);
		$appcastFile = str_replace("*guid*",$guid,$appcastFile);
		$appcastFile = str_replace("*title*",$title,$appcastFile);
		$appcastFile = str_replace("*description*",$description,$appcastFile);
		$appcastFile = str_replace("*version*",$version,$appcastFile);
		$appcastFile = str_replace("*pubdate*",$pubdate,$appcastFile);
		$appcastFile = str_replace("*type*",$type,$appcastFile);
		$appcastFile = str_replace("*url*",$url,$appcastFile);
		$appcastFile = str_replace("*length*",$length,$appcastFile);                                    
		$appcast .= $appcastFile;
	}
}

// Reads a file called filename.html and returns the HTML output

function getFileDescription($fileName) {
	$handle = @fopen("$fileName", "r");
	$rValue = "";
	if ($handle) {
		while (!feof($handle)) {
			$buffer = fgets($handle, 4096);
			$rValue .= $buffer;
		}
		fclose($handle);
	}
	return $rValue;
}

$appcast .= $appcastFooter;
/*"GET /u.php?UUID=F2958FD0-2EC9-40CA-8171-C63D663AAE90&
		osVersion=10.5.4
		cputype=7
		cpu64bit=1
		cpusubtype=4
		model=P5KC
		ncpu=4
		lang=en
		appName=MediaInfo%20Mac&appVersion=0.7.7.50
		cpuFreqMHz=2400
		ramMB=2048 HTTP/1.1" 200 575 "-" "MediaInfo Mac/0.7.7.50 Sparkle/282"
		*/
// Database Functions

$vars = array('osVersion' , 'cputype', 'cpu64bit', 'cpusubtype', 'model', 'ncpu', 'lang', 'appName', 'appVersion', 'cpuFreqMHz', 'ramMB', 'UUID');
$values = array();
$shouldInsert = true;
foreach ($vars as $item) {
	if (isset($_GET[$item])) {
		$values[$item] = html_entity_decode($_GET[$item]);
	} else {
		if ($item != 'cpu64bit') {
			$shouldInsert = false;
		}

	}
}
if (isset($_GET['cpu64bit'])) {
	$values['cpu64bit'] = html_entity_decode($_GET['cpu64bit']);
} else {
	$values['cpu64bit'] = '0';
}

if ($shouldInsert == true) {
	$mysqli = new mysqli("yourDBHost", "yourDBUser", "yourDBPassword", "yourDBName");

	if (mysqli_connect_errno()) {
	    printf("Connect failed: %s\n", mysqli_connect_error());
	    exit();
	}

	$stmt = $mysqli->prepare("INSERT INTO stats (os_version, cpu_type, cpu_64bit, cpu_subtype, model, n_cpu, lang, app_name, app_version, cpu_freq, ram_mb, UUID) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
	$stmt->bind_param('siississsiis', $values['osVersion'], $values['cputype'], $values['cpu64bit'], $values['cpusubtype'], $values['model'], $values['ncpu'], $values['lang'], $values['appName'], $values['appVersion'], $values['cpuFreqMHz'], $values['ramMB'], $values['UUID']);
	$stmt->execute();
	$stmt->close();

}

header("Content-type: application/xml; charset=UTF-8");
header("ETag: $etag");

echo $appcast;
exit;
?>

As usual, suggestions or improvements are welcome ;)