ShaneMcC.co.uk :: World of Warcraft :: Addon Updater Code

 
Notes
The version of the script here comes pre-populated with example addons (The ones that I use), download link below.
Code
You can download this script here
#!/usr/bin/php
<?php
	//============================================================================
	// This script will keep wow addons up to date.
	//
	// You need to edit the $config['wowdir'] variable, and add the required
	// "addAddon()" lines under the "Addons to keep up to date" heading.
	//============================================================================
	// Changes:
	//======================================
	// 04/01/2008 - Added Ability to store addAddon lines in a separate file
	//              rather than having to edit this file every time there is an
	//              update.
	//======================================
	// 24/12/2008 - Added GroupCalendar
	//======================================
	// 17/12/2008 - Added DoTimer
	//            - Added Recount
	//            - Added DeadlyBossMods
	//            - Added Ripped
	//======================================
	// 14/12/2008 - Added Enhanced Flight Map Official download location
	//            - Added EasySerum
	//            - Added X-DF-Date header.
	//======================================
	// 28/11/2008 - Fixed issue where sometimes toc files were corrupted causing
	//              wow to show the addon as incomatible
	//            - Lower the timout when trying to find the remote version
	//            - Dont show nasty PHP error if remote server returned nothing
	//======================================
	// 18/11/2008 - Added Support for "Manual" type downloads (just shows a
	//              message after the rest have downloaded)
	//            - Added Auctioneer to example list (beta and normal)
	//            - Added QuestHelper to example list
	//            - Fixes for adding X-DF-Version to stop it sometimes being added
	//              twice, or blank.
	//            - Fixed WOWUI support
	//            - Added force-update config option (downloadall)
	//===================
	// 15/10/2008 - Updated for Curse url Changes
	//            - Started keeping a record of changes
	//============================================================================

	// WoW Dir - Edit this
	$config['wowdir'] = '/usr/games/wow/';
	// Addon Dir (Can usually leave alone)
	$config['addondir'] = $config['wowdir'].'Interface/AddOns/';
	// Addons list Filename
	$config['addonlist'] = $config['wowdir'].'Interface/AddonList.php';

	// Download addons if remote version isn't obtainable but download url is.
	$config['downloadblank'] = true;
	
	// Force the updating of all addons.
	$config['downloadall'] = false;

	// Addon Types - Leave this alone
	define(CUSTOM, "Custom");
	define(CURSE, "Curse");
	define(WOWINTERFACE, "WoW Interface");
	define(COSMOS, "Cosmos");
	define(MANUAL, "Manual Download"); // These are shown as reminders at the end.
	// WoWUI don't provide a version number on their page which makes it
	// impossible to see what the newest version is, so we guess based on last
	// update date, and file size.
	define(WOWUI, "WoW UI");

	// Used to store the list of addons. - Leave this alone
	$addons = array();

	/**
	 * Add an Addon to the array of addons. (Called below)
	 *
	 * @param $name Name of addon
	 * @param $type Type of addon (CURSE etc)
	 * @param $id ID of addon (used for downloading, or url for manual checking)
	 * @param $tocname (Optional) Name of dir/toc if not the same as addon name,
	 *                 used to check current version.
	 * @param $custom Array used for custom information used for download.
	 */
	function addAddon($name, $type, $id, $tocname='', $custom=array()) {
		global $addons, $config;
		
		$addon['name'] = $name;
		$addon['type'] = $type;
		$addon['id'] = $id;
		$addon['tocname'] = ($tocname == '') ? $name : $tocname;
		$addon['dir'] = $config['addondir'].$addon['tocname'].'/';
		foreach ($custom as $k => $v) {
			if (!isset($addon[$k])) {
				$addon[$k] = $v;
			}
		}
		
		$addons[] = $addon;
	}

	//==========================================================================//
	// Addons to keep up to date, edit these.
	//==========================================================================//
	if (file_exists($config['addonlist'])) {
		include($config['addonlist']);
	} else {
		addAddon("Atlas", CURSE, "Atlas");
		addAddon("AtlasLoot Enhanced", CURSE, "AtlasLoot-Enhanced", "AtlasLoot");
		addAddon("Baud Bag", CURSE, "Baud-Bag", "BaudBag");
		addAddon("Examiner", CURSE, "Examiner");
		addAddon("FreeRefills", CURSE, "Free-Refills");
		addAddon("FloAspectBar", CURSE, "Flo-Aspect-Bar");
		addAddon("MobInfo-2 (includes MobHealth)", CURSE, "mob-info-2-includes-mob-health", "MobInfo2");
		addAddon("MozzFullWorldMap(Fan's Update)", CURSE, "mozz-full-world-map-fans-update", "MozzFullWorldMap");
		addAddon("Omen Threat Meter", CURSE, "Omen-Threat-Meter", "Omen");
		//addAddon("PetInfo", CURSE, "pet-info");
		addAddon("WarlockAlert", CURSE, "warlock-alert");
		addAddon("QuestHelper", CURSE, "quest-helper");
		addAddon("EasySerum", CURSE, "easyserum");
		addAddon("DoTimer", CURSE, "do-timer");
		addAddon("Recount", CURSE, "recount");
		addAddon("Ripped", CURSE, "ripped");
		addAddon("RippedAdvancedWarlockStrategy", CURSE, "arws");
		addAddon("GroupCalendar", CURSE, "group-calendar");
		
		
		addAddon("DoubleWide", WOWINTERFACE, 6887, '', array("FileVersionRegex" => "/## Title: DoubleWide \(v(.*)\)/", "AddToc" => true));
		addAddon("EquipCompare", WOWINTERFACE, 4392);
		addAddon("AuditorFu", WOWINTERFACE, 5429, "FuBar_AuditorFu");
		addAddon("LightHeaded", WOWINTERFACE, 7017);
		addAddon("TomTom", WOWINTERFACE, 7032);
		addAddon("Titan Panel", WOWINTERFACE, 8092, "Titan");
		
		addAddon("CTMod", CUSTOM, 0, "CT_Core", array("downloadurl" => "http://www.ctmod.net/media/packages/Advanced.zip", "custom_url" => "http://www.ctmod.net/downloads/", "custom_versionregex" => "/<b>This package contains:<\/b> CT_Core v(.*?),/", "FileVersionRegex" => '/^## Version: (.*) \(.*$/i'));
		addAddon("FeedOMatic", CUSTOM, 0, "GFW_FeedOMatic", array("custom_url" => "http://fizzwidget.com/notes/feedomatic/feed", "custom_versionregex" => "/<title>Feed-O-Matic (.*?)<\/title>/", "custom_downloadregex" => "/<enclosure url=\"(http:\/\/fizzwidget.com\/downloads\/.*?)\" length/"));
	
		addAddon("Quest Log Level Patch", COSMOS, "QuestLogLevelPatch", "QuestLogLevelPatch");
	
		addAddon("Enhanced Flight Map", CUSTOM, 0, "EnhancedFlightMap", array("custom_preurl" => "http://lysaddons.game-host.org/download/", "custom_url" => "http://lysaddons.game-host.org/efm/index.html", "custom_versionregex" => "/latest version of the addon <A HREF=\"..\/download\/EnhancedFlightMap-(.*?)\.zip\">Here<\/A>/", "custom_downloadregex" => "/latest version of the addon <A HREF=\"..\/download\/(EnhancedFlightMap-.*?\.zip)\">Here<\/A>/"));
	
//		addAddon("Auctioneer", MANUAL, "http://auctioneeraddon.com/dl/");
//		addAddon("Auctioneer", CURSE, "auctioneer", "Auc-Advanced", array("FileVersionRegex" => "/## Version: (.*?) \(/"));
		addAddon("Auctioneer-Beta", CUSTOM, 0, "Auc-Advanced", array("FileVersionRegex" => "/## Version: (.*?) \(/", "custom_preurl" => "http://mirror01.auctioneeraddon.com/dl/", "custom_url" => "http://auctioneeraddon.com/dl/", "custom_versionregex" => "/<td class=\"beta\"><a href=\"\?dl=Packages(?:.*?)\/AuctioneerSuite-(.*?)\.zip\">AuctioneerSuite-(?:.*?)\.zip<\/a>/", "custom_downloadregex" => "/<td class=\"beta\"><a href=\"\?dl=(Packages.*\/AuctioneerSuite-.*?\.zip)\">AuctioneerSuite-(?:.*?)\.zip<\/a>/"));
		
		
		addAddon("Deadly Boss Mods", CUSTOM, 0, "DBM-Core", array("custom_preurl" => "http://www.deadlybossmods.com/", "custom_url" => "http://www.deadlybossmods.com/downloads.php", "custom_versionregex" => "/<tr><td><a href=\".*?\">DBM-Core\+WotLK Mods<\/a><\/td><td>(.*?)<\/td><td>/", "custom_downloadregex" => "/<tr><td><a href=\"(.*?)\">DBM-Core\+WotLK Mods<\/a><\/td><td>.*?<\/td><td>/"));
		
		addAddon('Improved Casting Bar', WOWUI, 3657, "improvedCastingBar");
//		addAddon("AutoBindOnPickup", CURSE, 227);
//		addAddon("Notes", CURSE, 3628);
//		addAddon("SuperMacro", WOWUI, 1325);	
//		addAddon("WeaponQuickSwap", WOWUI, 121);
	}
	
	//==========================================================================//
	// Functions for actual work, no need to edit below here.
	//==========================================================================//
	
	// Used for echoing
	define(CRLF, "\n");
	define(T, "\t");
		
	/**
	 * Check each addon, and do work etc!
	 */
	function checkAddons() {
		global $addons, $config;
		
		foreach ($addons as &$addon) {
			if ($addon['type'] == MANUAL) { continue; }
			echo 'Found addon: ', $addon['name'], CRLF;
			getLocalVersion($addon);
			$tries = 0;
			do {
				echo '.';
				getRemoteData($addon);
				$tries++;
			} while ((!isset($addon['newversion']) || $addon['newversion'] == '') && $tries < 2);
			echo T, ' Local Version: "', trim($addon['myversion']), '"', CRLF;
			echo T, 'Remote Version: "', trim($addon['newversion']), '"', CRLF;
			echo T, '  Download URL: "', trim($addon['downloadurl']), '"', ' [', $addon['type'], ']', CRLF;
			$my = trim($addon['myversion']);
			$new = trim($addon['newversion']);
			if ($my != $new) {
				echo T, '[Needs Update]', CRLF;
			} else if ($config['downloadall']) {
				echo T, '[Forced Update]', CRLF;
			}
		}
		
		echo '=====================================', CRLF;
		echo 'Needing Update:', CRLF;
		
		$downloads = array();
		foreach ($addons as $a) {
			if ($addon['type'] == MANUAL) { continue; }
			$my = trim($a['myversion']);
			$new = trim($a['newversion']);
			if ($new == "" && !$config['downloadblank']) {
				echo T, $a['name'], ' -> Unable to find current version.', CRLF;
			} else if ($a['downloadurl'] == "") {
				echo T, $a['name'], ' -> Unable to find download URL.', CRLF;
			} else if ($my != $new || $config['downloadall']) {
				echo T, $a['name'], ' ("', $my, '" != "', $new, '") [', $a['downloadurl'], ']', CRLF;
				$downloads[] = array("name" => $a['name'].'.zip', "url" => $a['downloadurl'], "version" => $new, "addon" => $a);
			}
		}
		
		echo '=====================================', CRLF;
		echo 'Updating:', CRLF;
		
		// Now download them!
		getAddons($downloads);
		
		// Cleanup
		execAndPrint('rm -Rfv AddOns/*.txt AddOns/*.zip*');
		execAndPrint('chmod -Rf 755 '.$config['addondir']);
		
		// Remind about manual addons.
		$first = true;
		foreach ($addons as &$addon) {
			if ($addon['type'] == MANUAL) {
				if ($first) {
					echo CRLF, 'The following addons need checked manually:', CRLF;
					$first = false;
				}
				echo T, $addon['name'], ' -> '. $addon['id'], CRLF;
			}
		}
	}
	
	/**
	 * Download/Install addons on linux.
	 *
	 * @param $downloads Array from checkAddons() with addons to get
	 */
	function getAddons($downloads) {
		if (PHP_OS == 'Linux' || PHP_OS == 'Darwin' || substr(PHP_OS, -3) == 'BSD') {
			getAddonsUnix($downloads);
		} else {
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			echo 'Error: This script only updates on unix-based OSs', CRLF;
			echo 'Your OS: ', PHP_OS, CRLF;
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			die();
		}
	}
	
	/**
	 * Prepare for download/unzip by finding the applications.
	 * This uses "which"
	 */
	function prepareGet() {
		global $config;
		if (file_exists('/usr/bin/which') || file_exists('/bin/which')) {
			$which = file_exists('/usr/bin/which') ? '/usr/bin/which' : '/bin/which';
		} else {
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			echo 'Error: This script was unable to figure out how to download on your OS.', CRLF;
			echo 'Your OS: ', PHP_OS, CRLF;
			echo 'Unable to find `which` (in either /usr/bin or /bin)', CRLF;
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			die();
		}
		
		$wget = trim(`$which wget`);
		$curl = trim(`$which curl`);
		$fetch = trim(`$which fetch`);
		if ($wget != '') {
			$config['download_method'] = 'wget';
			$config['download_app'] = $wget;
		} else if ($curl != '') {
			$config['download_method'] = 'curl';
			$config['download_app'] = $curl;
		} else if ($fetch != '') {
			$config['download_method'] = 'fetch';
			$config['download_app'] = $fetch;
		} else {
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			echo 'Error: This script was unable to figure out how to download on your OS.', CRLF;
			echo 'Your OS: ', PHP_OS, CRLF;
			echo 'Supported Methods:', CRLF;
			echo T, 'wget, curl, fetch', CRLF;
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			die();
		}
		
		$unzip = trim(`$which unzip`);
		if ($unzip != '') {
			$config['unzip_method'] = 'unzip';
			$config['unzip_app'] = $unzip;
		} else {
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			echo 'Error: This script was unable to figure out how to unzip on your OS.', CRLF;
			echo 'Your OS: ', PHP_OS, CRLF;
			echo 'Supported Methods:', CRLF;
			echo T, 'unzip', CRLF;
			echo '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@', CRLF;
			die();
		}
	}
	
	/**
	 * Download/Install addons on linux.
	 *
	 * @param $downloads Array from checkAddons() with addons to get
	 */
	function getAddonsUnix($downloads) {
		global $config;
		
		// Make sure we have the ability to download/unzip
		prepareGet();
		
		chdir($config['addondir']);
		if (!file_exists($config['addondir'].'zipfiles')) {
			mkdir($config['addondir'].'zipfiles');
		}
		foreach ($downloads as $dl) {
			if ($config['download_method'] == 'wget') {
				execAndPrint($config['download_app'].' "'.$dl['url'].'" -O "'.$dl['name'].'"  2>&1');
			} else if ($config['download_method'] == 'curl') {
				execAndPrint($config['download_app'].' "'.$dl['url'].'" -# -o "'.$dl['name'].'"  2>&1');
			} else if ($config['download_method'] == 'fetch') {
				execAndPrint($config['download_app'].' -o "'.$dl['name'].'" "'.$dl['url'].'" 2>&1');
			}
			
			if ($config['unzip_method'] == 'unzip') {
				execAndPrint($config['unzip_app'].' -o "'.$dl['name'].'" -d "'.$config['addondir'].'"  2>&1');
			}
			execAndPrint('/bin/mv -fv "'.$dl['name'].'" zipfiles  2>&1');
			addNewVersion($dl['addon']);
		}
	}
	
	/**
	 * Adds the verison number we think this addon is to the toc file.
	 * This is used because some addons are crap and don't provide this data!
	 *
	 * We add the X-DF-Version and X-DF-Date params.
	 *
	 * @param $addon to add to!
	 */
	function addNewVersion($addon) {
		echo "Updating Toc for ".$addon." (".$addon['tocfile'].")".CRLF;
		$data = explode("\n", file_get_contents($addon['tocfile']));
		foreach ($data as $line) {
			if (stristr($line, 'X-DF-Version') === False) {
				$newdata[] = trim($line);
			}
			if (stristr($line, '## Interface:') !== False) {
				$newdata[] = '## X-DF-Version: '.trim($addon['newversion']);
				$newdata[] = '## X-DF-Date: '.time().' ['.date('r').']';
			}
		}
		file_put_contents($addon['tocfile'], implode("\n", $newdata));
	}
	
	/**
	 * Execute the given command and print the output from stdout as it happens
	 *
	 * @param $cmd Command to execute
	 */
	function execAndPrint($cmd) {
		echo "execAndPrint: ", $cmd, "\n";
		appexec($cmd, $proc);
		fclose($proc['pipes'][0]);
	
		// Now close the process handle
		while (!feof($proc['pipes'][1])) {
			echo fread($proc['pipes'][1], 512);
			flush();
		}
		
		fclose($proc['pipes'][1]);
		proc_close($proc['process']);
	}
	
	/**
	 * Runs an application and puts the process and the pipes in the given array
	 * $process['pipes'][0] is stdin
	 * $process['pipes'][1] is stdout
	 *
	 * @param $path Patch to exec
	 * @param $process Array where process pipes will be put
	 * @return true/false on successful execution
	 */
	function appexec($path, &$process) {
		$descriptorspec = array(0 => array("pipe", "r"),1 => array("pipe", "w"),2 => array("file", "/dev/null", "a"));
		$process = array();
		$process['process'] = proc_open($path, $descriptorspec, $process['pipes']);
		return is_resource($process['process']);
	}
	
	/**
	 * Get the local version of the given addon.
	 * This sets $addon['myversion'].
	 *    -3 = Local copy of Addon not found
	 *    -2 = TOC not found
	 *    -1 = Version not found.
	 *
	 * @param $addon Array with addon information
	 */
	function getLocalVersion(&$addon) {
		unset($addon['myversion']);
		unset($addon['toc']);
		unset($addon['dfversion']);
		
		if (!file_exists($addon['dir'])) {
			$addon['myversion'] = -3;
			return;
		}
		
		$toc = $addon['dir'].$addon['tocname'].'.toc';
		if (!file_exists($toc)) {
			// Look for toc, it could have a different case -_-
			$found = false;
			if ($handle = opendir($addon['dir'])) {
				while (false !== ($file = readdir($handle))) {
					if (strtolower($file) == strtolower($addon['tocname'].'.toc')) {
						$toc = $addon['dir'].$file;
						$found = true;
						break;
					}
				}
				if (!$found) { $addon['myversion'] = -2; return; }
			}
		}
		
		$addon['tocfile'] = $toc;
		
		$lines = file($toc);
		foreach ($lines as $line) {
			$regex = (isset($addon['FileVersionRegex'])) ? $addon['FileVersionRegex'] : '/^## Version: (.*)$/i';
			$dfversion = '/^## X-DF-Version: (.*)$/i';
			$tocregex = '/^## Interface: (.*)$/i';
			if (!isset($addon['myversion']) && preg_match($regex, $line, $matches)) {
				$addon['myversion'] = $matches[1];
			} else if (!isset($addon['dfversion']) && preg_match($dfversion, $line, $matches)) {
				$addon['dfversion'] = $matches[1];
			} else if (!isset($addon['toc']) && preg_match($tocregex, $line, $matches)) {
				$addon['toc'] = $matches[1];
			}
		}
		
		if (isset($addon['myversion']) || isset($addon['dfversion'])) {
			if (isset($addon['AddToc'])) {
				$addon['myversion'] .= '-'.$addon['toc'];
			}
			if (isset($addon['dfversion'])) { $addon['myversion'] = $addon['dfversion']; }
			return $addon['myversion'];
		}
		
		$addon['myversion'] -1;
	}
	
	/**
	 * Get the remote version/download url of the given addon
	 * This populates $addon['newversion'] and $addon['downloadurl']
	 *
	 * @param $addon Array with addon information
	 */
	function getRemoteData(&$addon) {
		Switch ($addon['type']) {
			case CURSE:
				$url = 'http://wow.curse.com/downloads/wow-addons/details/'.$addon['id'].'.aspx';
				$preurl = 'http://wow.curse.com';
				$mid = (isset($addon['VersionSubRegex'])) ? $addon['VersionSubRegex'] : '(?:'.preg_quote($addon['name']).' )?r?v?(?:ersion)?(.*?)' ;
				$versionregex = '/<a href="\/downloads\/wow-addons\/details\/.*?\/download\/[0-9]+.aspx">'.$mid.'<\/a>/i';
				$downloadregex = '/<a title=".*?" class="button button-download" href="(\/downloads\/wow-addons\/details\/.*?\/download\/[0-9]+.aspx)"/i';
				break;
			case WOWINTERFACE:
				$url = 'http://www.wowinterface.com/downloads/info'.$addon['id'].'.html';
				$preurl = 'http://www.wowinterface.com';
				$versionregex = '/<td class="alt2"><div class="infoboxfont1">Version:<\/div><\/td>.+?<td class="alt2"><div class="smallfont">r?v?(?:ersion)?(.*?)<\/div><\/td>/ims';
				$addon['downloadurl'] = 'http://www.wowinterface.com/downloads/download'.$addon['id'];
				break;
			case WOWUI:
				$url = 'http://wowui.worldofwar.net/?p=mod&m='.$addon['id'];
				$addon['downloadurl'] = 'http://wowui.worldofwar.net/?p=download&m='.$addon['id'];
				$versionregex = '/<b>Download size:<\/b> (.*?)<br \/>Updated <b>(.*?)<\/b>/i';
				break;
			case COSMOS:
				$url = 'http://www.cosmosui.org/addons.php?info='.$addon['id'];
				$preurl = 'http://www.cosmosui.org/';
				$versionregex = '/<li>Revision: (.*)<\/li>/i';
				$downloadregex = '/href="(download\.php\?t=addons&f=.*?)"/i';
				break;
			case CUSTOM:
				$url = $addon['custom_url'];
				if (isset($addon['custom_preurl'])) { $preurl = $addon['custom_preurl']; }
				if (isset($addon['custom_versionregex'])) { $versionregex = $addon['custom_versionregex']; }
				if (isset($addon['custom_downloadregex'])) { $downloadregex = $addon['custom_downloadregex']; }
				break;
			default:
				return -1;
				break;
		}
		
		$context = stream_context_create(array('http' => array('timeout' => 5)));
		$page = @file_get_contents($url, false, $context);
		
		if ($page === false) {
			$addon['newversion'] = "Unknown";
			$addon['downloadurl'] = "";
			return;
		}
		
		if (isset($downloadregex)) {
			preg_match($downloadregex, $page, $matches);
			
			$addon['downloadurl'] = $matches[1];
			if (isset($preurl)) { $addon['downloadurl'] = $preurl.$addon['downloadurl']; }
			Switch ($addon['type']) {
				case CURSE:
					$page2 = file_get_contents($addon['downloadurl']);
					$regex = '/<a class="button button-pop" href="(.*?)"><span>Manual Install<\/span>/';
					preg_match($regex, $page2, $matches);
					
					$addon['downloadurl'] = html_entity_decode($matches[1]);
					if (!empty($addon['downloadurl'])) {
						$addon['downloadurl'] = $preurl.$addon['downloadurl'];
					}
					break;
			}
		}
		
		preg_match($versionregex, $page, $matches);
		Switch ($addon['type']) {
			case WOWUI:
				$addon['newversion'] = preg_replace('/\s/', '', $matches[1].$matches[2]);
				break;
			default:
				$addon['newversion'] = $matches[1];
		}
	}
	
	// And begin!
	checkAddons();
?>