<?php

/**
 * The MIT License
 *
 * Copyright (c) 2009 Dustin Senos http://dustinsenos.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author		Dustin Senos <dustin@dustinsenos.com>
 * @copyright	2009 Dustin Senos
 * @date		March 1st, 2009
 * @version		1.0
 * @license		http://www.opensource.org/licenses/mit-license.php
 * @link		http://dustinsenos.com
 *
 * @usage
 *
 * $linkroll = new DeliciousLinkroll('dsenos');
 * echo $linkroll->getLinkroll();
 *
 */
class DeliciousLinkroll {

	const DEBUG_ACTIVE				= false;
	const LINKROLL_ID				= 'deliciousLinkroll';

	// Caching settings
	const CACHE_ENABLED				= true;
	const CACHE_LOCATION			= 'DeliciousLinkroll.cache';
	const CACHE_LIFETIME			= 3600; // 3600 seconds == 1 hour

	// Delicious constants
	const DELICIOUS_URL				= 'http://delicious.com/';
	const DELICIOUS_FEED_URL		= 'http://feeds.delicious.com/v2/json/';
	const DELICIOUS_COUNT_PARAM		= 'count';
	const DELICIOUS_JSON_URL		= 'u';
	const DELICIOUS_JSON_NAME		= 'd';
	const DELICIOUS_JSON_TAGS		= 't';

	// Curl settings
	const CURL_TIMEOUT				= 2;

	// Errors
	const ERROR_CACHE_DIRECTORY		= "Error: Cache directory doesn't exist";
	const ERROR_CACHE_FILE			= "Error: Cache file doesn't exist";
	const ERROR_CACHE_PERMISSIONS	= "Error: Can't write to the cache file. Please check file permissions";
	const ERROR_CACHE_UNSERIALIZE	= 'Error: Could not unserialize the cache file';
	const ERROR_CURL_FAILED			= 'Error: CURL request failed';
	const ERROR_JSON_DECODE			= 'Error: Could not decode JSON. Possibly malformed';
	const ERROR_HTML_DATA			= 'Error: No Data passed to createXhtml()';
	const ERROR_NO_DATA				= 'Error: No valid data to display';

	// Warnings
	const WARNING_CACHE_DATA		= 'Warning: No data passed to sendToCache(); Not writing to cache file';

	// Instance variables defined in constructor
	private $username;
	private $count;
	private $showTags;
	private $listId;


	/**
	 * Instantiates a new DeliciousLinkroll Object.
	 *
	 * @param $username		The username to fetch the delicious bookmarks for
	 * @param $count		How many bookmarks to show
	 * @param $showTags		True if you want to display the tags for the bookmark
	 *
	 * @return void
	 *
	 */
	public function __construct($username = '', $count = 5, $showTags = true, $listId = DeliciousLinkroll::LINKROLL_ID) {
		$this->username	= $username;
		$this->count	= $count;
		$this->showTags	= $showTags;
		$this->listId	= $listId;
	}



	/**
	 * Fetches the linkroll from either the cache if enabled and not stale or
	 * uses CURL to fetch it from delicious. Returns a xhtml unordered list
	 * containing bookmarks.
	 *
	 * @return void
	 *
	 */
	public function getLinkroll() {

		$linkroll = '';

		// If we are caching the response
		if (DeliciousLinkroll::CACHE_ENABLED) {

			$linkroll = $this->getFromCache();

			if ($linkroll != NULL) {
				return $this->createXhtml($linkroll);
			}
		}

		$linkroll = $this->getFromDelicious();

		if ($linkroll != NULL) {
			$this->sendToCache($linkroll);
			return $this->createXhtml($linkroll);
		}

		$this->sendError(DeliciousLinkroll::ERROR_NO_DATA);
		return NULL;

	}



	/**
	 * Creates the xhtml containing the bookmarks.
	 *
	 * @param $data		Array containing the delicious bookmarks
	 *
	 * @return			String containing Xhtml
	 *
	 */
	private function createXhtml(array $data = NULL) {

		if ($data == NULL) { $this->sendError(DeliciousLinkroll::ERROR_HTML_DATA); return NULL; }

		$html = "\n" .'<ul id="' . $this->listId . '">';

		// Loop through the bookmarks
		foreach ($data as $bookmark) {

			$url	= $bookmark->{DeliciousLinkroll::DELICIOUS_JSON_URL};
			$name	= $bookmark->{DeliciousLinkroll::DELICIOUS_JSON_NAME};

			$html .= "\n\t<li>";
			$html .= '<a href="' . $url . '" title="' . $name . '">' . $name . '</a>';

			// If we are showing tags
			if ($this->showTags) {

				$tags = $bookmark->{DeliciousLinkroll::DELICIOUS_JSON_TAGS};

				$html .= "<span>";
				foreach ($tags as $tag) {
					$html .= '<a href="' . DeliciousLinkroll::DELICIOUS_URL . $this->username . '/' . $tag . '" title="' . $this->username . '\'s ' . $tag . ' bookmarks">' . $tag . '</a> ';
				}
				$html .= '</span>';

			}

			$html .= '</li>';
		}

		$html .= '</ul>';

		return $html;

	}



	/**
	 * Fetches the JSON request from the delicious feed via curl. Returns
	 * an array if request was successful, Null otherwise.
	 *
	 * @return		Array containing bookmark data from the delicious feed
	 *
	 */
	private function getFromDelicious() {

		$curl = curl_init();

		curl_setopt($curl, CURLOPT_URL, DeliciousLinkroll::DELICIOUS_FEED_URL . $this->username . '?' . DeliciousLinkroll::DELICIOUS_COUNT_PARAM . '=' . $this->count);
		curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, DeliciousLinkroll::CURL_TIMEOUT);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

		$curlResponse = curl_exec($curl);

		curl_close($curl);

		// If curl failed, return NULL
		if (empty($curlResponse)) { $this->sendError(DeliciousLinkroll::ERROR_CURL_FAILED); return NULL; }

		$decoded = json_decode($curlResponse);

		// Make sure the JSON decoded correctly
		if (!$decoded) { $this->sendError(DeliciousLinkroll::ERROR_JSON_DECODE); return NULL; }

		return $decoded;

	}



	/**
	 * Returns the cached data if it's parsed successfully and not stale.
	 *
	 * @return		Cached array containing bookmark data from the delicious feed
	 *
	 */
	private function getFromCache() {

		$cache = file_get_contents(DeliciousLinkroll::CACHE_LOCATION);

		// Confirm we were able to read the cache
		if (!$cache) { return NULL; }

		$cache = unserialize($cache);

		// Confirm the cache was unserialized correctly
		if (!is_array($cache)) { $this->sendError(DeliciousLinkroll::ERROR_CACHE_UNSERIALIZE); return NULL; }

		$currentTime = time();
		$cachedTime = $cache[0];

		// Confirm the cache isn't stale
		if (($currentTime - $cachedTime) < DeliciousLinkroll::CACHE_LIFETIME) {
			return $cache[1];
		}

		return NULL;

	}



	/**
	 * Stores the passed in data to a cache file.
	 *
	 * @param $data		Array containing the data we want to cache
	 *
	 * @return			True if cached successfully
	 *
	 */
	private function sendToCache(array $data = NULL) {

		if ($data == NULL) { $this->sendError(DeliciousLinkroll::WARNING_CACHE_DATA); return false; }

		$fileReference = fopen(DeliciousLinkroll::CACHE_LOCATION, 'w');

		// Confirm we have a valid file reference
		if (!$fileReference) {
			if (!is_dir(dirname($cache)))	{ $this->sendError(DeliciousLinkroll::ERROR_CACHE_DIRECTORY); }
			if (!is_file($cache))			{ $this->sendError(DeliciousLinkroll::ERROR_CACHE_FILE); }
			if (!is_writable($cache))		{ $this->sendError(DeliciousLinkroll::ERROR_CACHE_PERMISSIONS); }
			return false;
		}

		$dataForCache = serialize(array(time(), $data));
		$writeSuccess = fwrite($fileReference, $dataForCache);

		fclose($fileReference);

		return true;

	}


	/**
	 * Catches all exceptions thrown from this class. If debug mode is active
	 * echos them as they are handled.
	 *
	 * @param $exception	The exception object
	 *
	 * @return Null
	 *
	 */
	private function sendError($error) {
		if (!DeliciousLinkroll::DEBUG_ACTIVE) { return; }
		echo "<br/>" . $error;
	}
}

?>