<?php
/*
Plugin Name: Cloudfiles CDN
Plugin URI: http://voceconnect.com/
Description: Adds/Deletes uploaded images on CDN and rewrites asset URLs to CDN
Version: 0.1
Author: Chris Scott, Michael Pretty
Author URI: http://voceconnect.com/
*/

require_once('voce-settings.php');

class CloudfilesCdn {

	var $submenu_general;

	//private static $option_group = 'cloudfiles_cdn';
	const OPTION_GENERAL = 'cloudfiles_cdn_general';

	/**
	 * get general setting
	 *
	 * @param string $setting setting name
	 * @return mixed setting value or false if not set
	 */
	public static function get_setting($setting) {
		$settings = get_option(self::OPTION_GENERAL);
		if(!$settings || !is_array($settings)) {
			$settings = array(
				'file_extensions' => 'bmp|bz2|css|gif|ico|gz|jpg|jpeg|js|mp3|pdf|png|rar|rtf|swf|tar|tgz|txt|wav|zip'
			);
		}
		return (isset($settings[$setting])) ? $settings[$setting] : false;
	}

	public function __construct() {}

	public function initialize() {
		// relies on Voce_Settings
		if (!class_exists('Voce_Settings')) {
			return;
		}

		add_action('admin_menu', array($this, 'add_options_page'));

		if (!self::get_setting('username') || !self::get_setting('api_key') || !self::get_setting('container') || !self::get_setting('root_url') || !self::get_setting('file_extensions')) {
			add_action('admin_notices', array($this, 'settings_warning'));
			return;
		}

		add_filter('wp_handle_upload', array($this, 'catch_wp_handle_upload'));
		add_filter('wp_delete_file', array($this, 'catch_wp_delete_file'));
		add_filter('wp_generate_attachment_metadata', array($this, 'catch_wp_generate_attachment_metadata'));
		add_filter('bp_core_avatar_cropstore', array($this, 'catch_bp_core_avatar_cropstore'));
		add_action('bp_core_avatar_save', array($this, 'catch_bp_core_avatar_save'), 10, 2);

	}

	public function settings_warning() {
		echo "<div class='update-nag'>The Cloudfiles CDN plugin is missing some required settings.</div>";
	}

	/**
	 * adds the options page
	 *
	 * @return void
	 */
	public function add_options_page() {
		$this->submenu_general = add_options_page('Cloudfiles CDN', 'Cloudfiles CDN', 'manage_options', self::OPTION_GENERAL, array($this, 'submenu_general'));
		$settings = new Voce_Settings(self::OPTION_GENERAL, self::OPTION_GENERAL);

		$section = $settings->add_section('api', 'Cloudfiles API Settings', $this->submenu_general);
		$section->add_field('username', 'Username (required)', 'field_input');
		$section->add_field('api_key', 'API Key (required)', 'field_input');
		$section->add_field('container', 'Container Name (required)', 'field_input', array('description' => 'The container to store files in.'));
		$section->add_field('root_url', 'Root URL (required)', 'field_input', array('description' => 'The root URL to the container without a trailing slash.'));
		$section->add_field('file_extensions', 'File Extensions (required)', 'field_input');
		$section->add_field('enable_debug', 'Enable Debugging?', 'field_checkbox', array('description' => 'Enable error_log() to log upload/delete actions.'));

	}

	/**
	 * callback to display submenu_external
	 *
	 * @return void
	 */
	function submenu_general() {
		?>
		<div class="wrap">
			<h2>Cloudfiles CDN Settings</h2>
			<form method="post" action="options.php">
				<?php settings_fields(self::OPTION_GENERAL); ?>
				<?php do_settings_sections($this->submenu_general); ?>
				<p class="submit">
					<input name="Submit" type="submit" class="button-primary" value="<?php esc_attr_e('Save Changes'); ?>" />
				</p>
			</form>
		</div>
		<?php
	}

	/**
	 * delete old BP avatars on save
	 *
	 * @param string $user_id not used
	 * @param string $old the path to the file being deleted
	 * @return void
	 */
	public function catch_bp_core_avatar_save($user_id, $old) {
		$files = array();
		// this will be -avatar2
		$files[] = str_replace(ABSPATH, '', $old);
		// get -avatar1 also
		$files[] = str_replace('-avatar2', '-avatar1', $files[0]);

		foreach ($files as $file) {
			if (self::get_setting('enable_debug'))
				error_log("DELETING OLD BP AVATAR: $file");

			$this->delete_file($file);
		}
	}

	/**
	 * when BP avatars are cropped, catch the cropped sizes and upload
	 *
	 * @param string $files
	 * @return array the original file array
	 */
	public function catch_bp_core_avatar_cropstore($files) {
		foreach ((array) $files as $file) {
			$relative_file_path = str_replace(ABSPATH, '', $file);
			$file_type = wp_check_filetype($file);

			if (self::get_setting('enable_debug'))
				error_log("UPLOADING BP AVATAR: $relative_file_path");
			$this->upload_file($file, $file_type['type'], $relative_file_path);
		}

		return $files;
	}

	/**
	 * go grab the already generated intermediate sizes and upload
	 *
	 * @param string $metadata
	 * @return array updated metadata
	 */
	public function catch_wp_generate_attachment_metadata($metadata) {
		//error_log("WP_GENERATE_ATTACHMENT_METADATA: " . var_export($metadata, true));
		$upload_dir = wp_upload_dir();
		$upload_path = trailingslashit($upload_dir['path']);
		$sizes = $metadata['sizes'];

		foreach ((array) $sizes as $size => $size_data ) {
			$file = $size_data['file'];
			if (is_multisite()) {
				$relative_file_path = self::get_blog_path() . 'files' . trailingslashit($upload_dir['subdir']) . $file;
			} else {
				$relative_file_path = $file;
			}
			$file_type = wp_check_filetype($file);
			if (self::get_setting('enable_debug'))
				error_log("UPLOADING INTERMEDIATE SIZE: $relative_file_path");
			$this->upload_file($upload_path . $file, $file_type['type'], $relative_file_path);
		}

		return $metadata;
	}

	/**
	 * get a local filename from a CDN URL
	 *
	 * @param string $url
	 * @return string filename
	 */
	private function get_local_filename($url) {
		return ABSPATH . str_replace(trailingslashit(self::get_setting('root_url')), '', $url);
	}

	/**
	 * Filter to handle wp_handle_upload for uploaded files. Logs any errors.
	 *
	 * @param string $upload
	 * @return void
	 */
	public function catch_wp_handle_upload($upload) {
		// check for buddypress avatar upload and don't upload since it resizes and deletes this one
		if (function_exists('bp_core_setup_globals') && strpos($upload['file'], '/avatars/') !== false) {
			return $upload;
		}

		$blog_path = $this->get_blog_path();
		$relative_url = $blog_path . $this->remove_site_url($upload['url']);
		if (self::get_setting('enable_debug'))
			error_log("UPLOADING: $relative_url");

		// upload file
		if (!$this->upload_file($upload['file'], $upload['type'], $relative_url)) {
			error_log("[CloudfilesCdn] Error uploading file: $relative_url");
			return $upload;
		}

		return $upload;
	}

	/**
	 * Filter to handle wp_delete_file for deleted files. Deletes the file
	 * from the CDN.
	 *
	 * @param string $file
	 * @return string the original file path
	 */
	public function catch_wp_delete_file($file) {
		//error_log("WP_DELETE_FILE: " . var_export($file, true));
		if (is_multisite()) {
			// not sure if this is needed or not...
			if (strpos($file, 'blogs.dir')) {
				$parts = explode('files/', $file);
				$file = $parts[1];
			}
			$file = $this->get_blog_path() . 'files/' . $file;
		}

		// delete the file from the CDN
		if (self::get_setting('enable_debug'))
			error_log("DELETING FILE: $file");
		$this->delete_file(str_replace(ABSPATH, '', $file));

		return $file;
	}

	/**
	 * get the blog's path without the leading slash
	 *
	 * @return string
	 */
	private function get_blog_path() {
		global $blog_id;
		$blog_path = '';
		if ((int) $blog_id !== 1) {
			$blog_details = get_blog_details($blog_id);
			$blog_path = $blog_details->path;
		}

		return ltrim($blog_path, '/');
	}

	/**
	 * Get the relative filename. The filename from the wp_delete_file filter
	 * will either have the file path to the WP_UPLOAD_DIR prepended to the cdn'd file
	 * url or it will be the path relative to the WP_UPLOAD_DIR
	 *
	 * @param string $file
	 * @return void
	 */
	private function get_relative_file($file) {
		$cont_url = self::get_setting('root_url');
		if (strpos($file, $cont_url) !== false) {
			if ($file_parts = explode(trailingslashit(self::get_setting('root_url')), $file)) {
				// prepended w/bad upload path (e.g. http://c0002127.cdn1.cloudfiles.rackspacecloud.com/wp-content/uploads/2010/07/http://c0002127.cdn1.cloudfiles.rackspacecloud.com/wp-content/uploads/2010/07/653106995_338e53fb1416-150x150.jpg)
				if (isset($file_parts[2])) {
					return $file_parts[2];
				} elseif (isset($file_parts[1])) {
					// just regular CDN url
					return $file_parts[1];
				}
			}
		}

		return false;
	}

	/**
	 * delete a file from the CDN
	 *
	 * @param string $file relative filename
	 * @return bool true on success, false on failure
	 */
	private function delete_file($file) {
		require_once(trailingslashit(dirname(__FILE__)) . 'cloudfiles/cloudfiles.php');
		$auth = new CF_Authentication(self::get_setting('username'), self::get_setting('api_key'));

		try {
			$auth->authenticate();
		} catch (Exception $e) {
			error_log(sprintf("[CloudfilesCdn] Error authenticating to Cloudfiles: %s", $e->getMessage()));
			return false;
		}

		$conn = new CF_Connection($auth);
		$container = $conn->get_container(self::get_setting('container'));

		try {
			$obj = $container->get_object($file);
			$container->delete_object($obj);
		} catch (Exception $e) {
			error_log(sprintf("[CloudfilesCdn] Error deleting file '%s' from Cloudfiles: %s", $file, $e->getMessage()));
		}

		if (self::get_setting('enable_debug'))
			error_log("DELETED FILE: $file");

		return true;
	}



	/**
	 * Update the attachement URL if the attached file is on the CDN
	 *
	 * @param string $url
	 * @param string $post_id
	 * @return string original or updated URL
	 */
	public function cdn_attachment_url($url, $post_id) {
		if ($file = get_post_meta($post_id, '_wp_attached_file', true)) {
			if (strpos($file, self::get_setting('root_url')) !== false) {
				return $file;
			}
		}

		return $url;
	}

	/**
	 * Upload a file to cloudfiles
	 *
	 * @param string $file the file's path
	 * @param string $file_type the file's mime type
	 * @param string $file_url the file's site-relative URL
	 * @return bool true on succes, false on fail
	 */
	private function upload_file($file, $file_type, $file_url) {
		require_once(trailingslashit(dirname(__FILE__)) . 'cloudfiles/cloudfiles.php');
		$auth = new CF_Authentication(self::get_setting('username'), self::get_setting('api_key'));

		try {
			$auth->authenticate();
		} catch (Exception $e) {
			error_log(sprintf("[CloudfilesCdn] Error authenticating to Cloudfiles: %s", $e->getMessage()));
			return false;
		}

		$conn = new CF_Connection($auth);
		$container = $conn->get_container(self::get_setting('container'));

		$obj = $container->create_object($file_url);
		$obj->content_type = $file_type;

		try {
			$obj->load_from_filename($file);
		} catch (Exception $e) {
			error_log(sprintf("[CloudfilesCdn] Error uploading '%s' to Cloudfiles: %s", $file, $e->getMessage()));
			return false;
		}

		if (self::get_setting('enable_debug'))
			error_log("UPLOADED: $file, $file_type, $file_url");

		return true;
	}

	/**
	 * Get a site-relative url without a leading / from an absolute URL containing the siteurl
	 *
	 * @param string $absolute_url
	 * @return string relative url
	 */
	private function remove_site_url($absolute_url) {
		return str_replace(trailingslashit(get_option('siteurl')), '', $absolute_url);
	}

	private function remove_cdn_url($url) {
		return str_replace(trailingslashit(self::get_setting('root_url')), '', $url);
	}

}
add_action('init', array(new CloudfilesCdn(), 'initialize'));

class CDN_Rewrite {

	private $file_extensions;
	private $blog_details;
	private $cdn_root_url;

	public function __construct() {
		$this->file_extensions = CloudfilesCdn::get_setting('file_extensions');
		$this->cdn_root_url = untrailingslashit(CloudfilesCdn::get_setting('root_url'));
	}

	/**
	 * Initializes class and registers hooks
	 *
	 */
	public function initialize() {
		if('/' != $this->cdn_root_url) {
			add_action('template_redirect', array($this, 'start_buffer'), 1);
		}
	}

	/**
	 * start output buffering.
	 *
	 */
	public function start_buffer() {
		ob_start(array($this, 'filter_urls'));
	}

	/**
	 * Callback for output buffering.  Search content for urls to replace
	 *
	 * @param string $content
	 * @return string
	 */
	public function filter_urls($content) {
		$root_url = $this->get_site_root_url();
		$regex = '#(?<=[(\"\'])'.quotemeta($root_url).'(?:(/[^\"\')]+\.('.$this->file_extensions.')))#';

		$content = preg_replace_callback($regex, array($this, 'url_rewrite'), $content);

		return $content;
	}

	/**
	 * Returns the root url of the current site
	 *
	 * @return string
	 */
	public function get_site_root_url() {
		if(is_multisite() && !is_subdomain_install()) {
			$root_blog = get_blog_details(1);
			$root_url = $root_blog->siteurl;
		} else {
			$root_url = site_url();
		}
		return $root_url;
	}

	/**
	 * Returns the details for the current blog
	 *
	 * @return object
	 */
	public function get_this_blog_details() {
		if(!isset($this->blog_details)) {
			global $blog_id;
			$this->blog_details = get_blog_details($blog_id);
		}
		return $this->blog_details;
	}

	/**
	 * Callback for url preg_replace_callback.  Returns corrected URL
	 *
	 * @param array $match
	 * @return string
	 */
	public function url_rewrite($match) {
		global $blog_id;
		$path = $match[1];
		//if is subfolder install and isn't root blog and path starts with site_url and isnt uploads dir
		if(is_multisite() && !is_subdomain_install() && $blog_id !== 1) {
			$bloginfo = $this->get_this_blog_details();
			if((0 === strpos($path, $bloginfo->path)) && (0 !== strpos($path, $bloginfo->path.'files/'))) {
				$path = '/'.substr($path, strlen($bloginfo->path));
			}
		}
		return $this->cdn_root_url . $path;
	}
}
add_action('init', array(new CDN_Rewrite(), 'initialize'));

class CDN_VersionAssets {

	private $default_version = '';
	private $root_url;

	public function __construct() {
		$this->root_url = site_url();
	}

	public function initialize() {
		add_filter('style_loader_src', array($this, 'replace_version'), 10);
		add_filter('script_loader_src', array($this, 'replace_version'), 10);
		add_filter('style_loader_src', array($this, 'replace_version'), 10);
	}

	public function on_template_redirect() {
		$this->default_version = @filemtime(get_stylesheet_directory().'/style.css');
	}

	private function get_version($url) {
		if(0 === strpos($url, $this->root_url)) {
			$parts = parse_url($url);
			$file_path = str_replace(site_url('/'), ABSPATH, $parts['scheme'].'://'.$parts['host'].$parts['path']);
			if(	!($version = @filemtime($file_path)) ) {
				$version = $this->default_version;
			}
			return $version;
		}
		return false;
	}

	public function replace_version($src) {
		if( $new_version = $this->get_version($src) ) {
			return add_query_arg('ver', $new_version, $src);
		}
		return $src;
	}
}
add_action('init', array(new CDN_VersionAssets(), 'initialize'));