<?php
/*
Plugin Name: Eclipse Link Cloaker
Plugin URI: http://eclipsecloaker.com/
Description: Automatically cloaks outgoing links, tracks click and conversion statistics, and can turn any keywords you specify into links.   
Version: 1.2
Author: Janis Elsts
Author URI: http://w-shadow.com/
Text Domain: eclipse-link-cloaker
*/

// Pre-2.6 compatibility [likely redundant]
if ( ! defined( 'WP_CONTENT_URL' ) )
	define( 'WP_CONTENT_URL', get_option( 'siteurl' ) . '/wp-content' );
if ( ! defined( 'WP_CONTENT_DIR' ) )
	define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' );
if ( ! defined( 'WP_PLUGIN_URL' ) )
	define( 'WP_PLUGIN_URL', WP_CONTENT_URL. '/plugins' );
if ( ! defined( 'WP_PLUGIN_DIR' ) )
	define( 'WP_PLUGIN_DIR', WP_CONTENT_DIR . '/plugins' );

//A simplified file_put_contents function for pre-PHP 5 systems
//(only works with simple datatypes, e.g. strings and numbers)	
if (!function_exists('file_put_contents')) {
    function file_put_contents($filename, $data) {
        $f = @fopen($filename, 'w');
        if (!$f) {
            return false;
        } else {
            $bytes = fwrite($f, $data);
            fclose($f);
            return $bytes;
        }
    }
}

if ( !function_exists('plugin_dir_url') ){
	function plugin_dir_url( $file ) {
		return trailingslashit( plugins_url( '', $file ) );
	}
}

//An utility script that helps set up the "Screen Options" panel in the "Cloaked Links" page.
if ( is_admin() ){
	include 'screen-options/screen-options.php';
}

if (!class_exists('EclipseLinkCloaker')) {

class EclipseLinkCloaker {
	var $version = '1.2';
	var $api = 'http://w-shadow.com/pluginbase/api.php'; //The API that handles key verification and updates
	var $plugin_slug = 'eclipse-link-cloaker';
			
	var $options_name='wplc_cloaker_pro'; //The plugin's settings are stored in this WP option
	var $options;
	
	var	$link_pattern =  '#<a\s+(?P<attributes>[^>]+)>(?P<text>.*?)</a>#si';
	var $attribute_pattern = '#(?P<name>\w+)\s*=\s*([\"\'])(?P<value>.*?)\2#s';
	
	var $conversion_tracker_cookie = 'ec_last_link_id';
	var $conversion_tracker_param = 'ec-conversion-tracker';

	function EclipseLinkCloaker(){
		
		//The current hostname should be excluded from cloaking by default
		$parts = @parse_url(get_option('siteurl'));
		$default_exclusions = array();
		if( $parts && isset($parts['host']) ){
			$default_exclusions[] = $parts['host'];
		}
		
		//Set up the defaults
		$this->defaults = array(
			'exclusion' => $default_exclusions,//When cloaking all links, don't cloak those where the domain is in the exclusion list
			'inclusion' => array(),				//When in selective mode, cloak links that match this domain list  
				
			'prefix' => 'recommends',			//e.g. http://example.com/recommends/MyCoolProduct/
			
			'filter_mode' => 'everything',		//Cloak only links found in post content ('content'), or 'everything'?
			'filter_feed' => true,  			//Cloak links in the RSS feed? (works in either filter_mode)
			
			'link_mode' => 'everything',		//Link selection mode : 'selective' = cloak only specific links, 
												//'everything' = cloak all links (except those in the exclusion list).
												
			'autocloak_urls' => 'fancy',		//'fancy' - shorter cloaked URLs, slower;
												//'encoded' - longer cloaked URLs, faster. Disabled in this release.
												 
			'redirect_type' => '302redirect',	//'301redirect' - 301 redirect (Permanent)
												//'302redirect' - 302 redirect (Found)
												//'307redirect' - 307 redirect (Temporary)
												//'meta'		- use the double META refresh trick
												//'frame'		- display the target site in a frame
												//'hideref'		- redirect through a redirect hiding service
			
			'tweak_nofollow' => false,			//Nofollow cloaked links
			'tweak_new_window' => false,		//Show cloaked links in a new window/tab(?)
            'tweak_google_analytics' => false,	//If this is checked, track clicks on cloaked links via Google Analytics
            
            'tracker_prefix' => 'cloaked',		//The prefix to use for the tracking URL when tracking clicks with GA
            
            'autolink_enable' => true,			//Enable/disable automatic keyword linking globally
            
            'autolink_max_links' => 3,			//Max number of auto-inserted links for a single keyword. Can be 
												//overriden on a per-link basis by setting the max_links field.
            'autolink_max_total_links' => 5,	//Max autolinked keywords per post
												
			'autolink_enable_css' => false,		//Add a specific CSS class to all autolinked keywords
			'autolink_css_class' => 'autolinked', //The class name to add
			'autolink_css_rules' => '',			//CSS rule(s) for the aforementioned class
			
			'autolink_only_single' => false,	//Only autolink keywords in single posts/pages, not archive listings
			'autolink_ignore_posts' => array(),	//Disable autolinking for these posts/pages
			'autolink_exclude_tags' => array('a','code','h1','h2','h3','h4','h5','h6'), //Don't autolink keywords inside these tags
			
			'autolink_cloak' => false, 			//Cloak autolinked kw's (even if other cloaking settings would prevent it)
			'autolink_nofollow' => false, 		//Nofollow autolinked kw's
			'autolink_new_window' => false,		//Open automatically inserted links in new window
			
			'enable_conversion_tracking' => false, //Enable/disable the simple conversion tracker
			
			'default_link_text' => 'link',		//The text to use in an auto-generated cloaked URL if 
												//the link has no usable anchor text (applies to image links, etc).
												
			'links_per_page' => 30,             //How many links per page to display in Tools -> Cloaked Links. 
				    
			'url_pass' => '',					//The password used for encoding target URLs when autocloak_urls is set to "encoded" 			
			'max_db_batch'	=> 100,				//How many links to load at once when looking up their names
						
			'bots' => array('bot','ia_archive','slurp','crawl','spider'), //Link hits aren't counted for these UAs
			
			'key' => '', 						//The license key. Must be entered for the plugin to run.
			'verification' => array(
				'success' => false,				//The result of the last verification
				'error_code' => '',				//If the verification failed this will contain the error code
												/*	Possible values : 
														network_error
														empty_key
														invalid_key
														blocked_key
														Other values typically indicate a bug.
												*/
				'error_message' => 	'',			//If the verification failed this will contain the error message
			),
			'last_verification_time' => 0,		//When the key was last verified by checking with the keyserver.
			
			'chart_period' => '30 days',
			'chart_width' => 500,
			'chart_height' => 200,
			
			'update' => array(
				'last_check' => 0,				//When was the last update check
				'checked_version' => '',		//What version was installed at that time
				'update_info' => array(),		//Information about the available update, if any
				'check_interval' => 60*60*12,	//Check for updates every X seconds
			),
			
			'db_cleanup_interval' => 60*60*24,	//How often old stats are deleted 
			'last_db_cleanup' => 0,				//When was the last cleanup
		);
		
		$this->load_options();
		
		add_action('activate_'.plugin_basename(__FILE__), array(&$this,'activation'));
		add_action('admin_menu', array(&$this,'admin_menu'));
		//Init. i18n stuff. Runs after the redirect handler (hook_init) for slightly better performance.
		add_action('init', array(&$this,'load_language'), 11); 
		
		//The rewrite_rules_array hook is before the "if registered" check because WP regenerates 
		//the rewrite rules at awkward times. It would be hard to ensure we get our rule(s) in if 
		//we only set this filter when the plugin is registered & conversion tracking is enabled.
		add_filter( 'rewrite_rules_array', array(&$this, 'insert_tracker_rules')) ;
		
		//Check if the current installation is registered
		if ( !$this->registered() ){
			add_action( 'admin_notices', array( &$this, 'registration_nag') );
			return;
		}
        
        //Add the filter that finds & cloaks links (except on admin pages)
        if ( !is_admin() ){
			if ( $this->options['filter_mode'] == 'content' ){
				add_filter('the_content', array(&$this,'process_content'), 50);
			} else if ( $this->options['filter_mode'] == 'everything' ){
				add_action( 'init', array(&$this,'init_buffer'), 100 );
			}
		}
		
		//Attach the redirect handler
		add_action( 'init', array(&$this,'hook_init') );
		
		//Check for updates when any dashboard page is loaded 
		add_action( 'admin_init', array(&$this,'check_for_updates') );
		
		//Insert our update info into the update array maintained by WP
		if ( function_exists('get_site_transient') || function_exists('get_transient') ){
			//WP 3.0+
			add_filter( 'site_transient_update_plugins', array(&$this,'inject_update') );
			//WP 2.8+
			add_filter( 'transient_update_plugins', array(&$this,'inject_update') );
		} else {
			//WP < 2.8
			add_filter( 'option_update_plugins', array(&$this,'inject_update') );
		}
		add_filter('plugins_api', array(&$this, 'inject_plugin_information'), 10, 3);
		
		//Register AJAX handlers
		add_action( 'wp_ajax_lcp_update_link', array(&$this,'ajax_update_link') );
		add_action( 'wp_ajax_lcp_show_stats', array(&$this,'ajax_show_stats') );
		
		//Add the filter that converts keywords into links
		if ( $this->options['autolink_enable'] ){
			add_filter('the_content', array(&$this, 'autolink_keywords'), 30); //Runs after shortcodes have already been processed
		}
		
		//Output the custom CSS for autolinked keywords
		if ( $this->options['autolink_enable_css'] && !empty($this->options['autolink_css_rules']) ){
			add_action( 'wp_head', array(&$this, 'print_autolink_css') );
		}
		
		//Output the JS that calls the GA tracker
		if ( $this->options['tweak_google_analytics'] ){
			add_action( 'wp_head', array(&$this, 'print_ga_helper') );
		}
		
		//Set hooks for our simple conversion tracker based on the tracking pixel technique.
		if ( $this->options['enable_conversion_tracking'] ){
			add_filter( 'query_vars', array(&$this, 'insert_query_vars') );
			add_action( 'parse_request', array(&$this, 'handle_tracker') );
		}
	}
 
	function load_options(){
	    $this->options = get_option($this->options_name);
	    if( !is_array( $this->options ) ){
	    	//Import Link Cloaking Plugin settings, if any
	    	$old_settings = $this->import_lc_settings();
	        $this->options = array_merge( $this->defaults, $old_settings );
	    } else {
	        $this->options = array_merge( $this->defaults, $this->options );
	    }
	}
	
	function save_options(){
		update_option($this->options_name,$this->options);
	}
	
  /**
   * EclipseLinkCloaker::import_lc_settings()
   * Import the settings from Link Cloaking Plugin (if present) and return them in an array  
   *
   * @return array
   */
	function import_lc_settings(){
		$old_options = get_option( 'wplc_cloaker_options' );
		if ( empty($old_options) || !is_array($old_options) ) return array();
		
		$rez = array();
		if ( isset($old_options['exlusions']) && is_array($old_options['exlusions']) )
			$rez['exlusion'] = $old_options['exlusions'];
		
		if ( isset($old_options['prefix']) ) 
			$rez['prefix']  = $old_options['prefix'];
		
		if ( isset($old_options['mode']) && in_array( $old_options['mode'], array('everything', 'selective') ) ){
			$rez['link_mode'] = $old_options['mode']; 
		}
		
		if ( isset($old_options['nofollow']) ){
			$rez['tweak_nofollow'] = !empty($old_options['nofollow']);
		}
		
		return $rez;
	}
	
	function process_content( $html ){
		global $wpdb;
		
		//A bit of special logic if we're in the RSS feed 
		if ( is_feed() && !$this->options['filter_feed'] ) return $html;
		
		//TODO: Ignore links inside <pre> tags 
		
		//Find all links
		if ( !preg_match_all($this->link_pattern, $html, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ) ){ 
			return $html;
		}
		
		//Figure out which links need to be cloaked and store their info for later use 
		$links = array();
		foreach ($matches as $match){
			
			//Extract link attributes
			if ( !preg_match_all( $this->attribute_pattern, $match['attributes'][0], $attribute_data, PREG_SET_ORDER ) ){
				continue;
			}
				
			//Turn the attribute data into a name->value array
			$attributes = array();
			foreach($attribute_data as $attr){
				$attributes[ strtolower($attr['name']) ] = $this->unhtmlentities( $attr['value'] );
				/* Yes, it would be much more straightforward to just use html_entity_decode here,  
				but it fails to decode entities like &#038; on some PHP 4.x systems. */
			}
			
			//Skip links that have no href (URL)
			if ( empty( $attributes['href'] ) ){
				continue;
			} else {
				$attributes['href'] = trim($attributes['href']); //sometimes users put spaces around the URL accidentally
			}
			
			$link = array(
				'original' => $match[0][0], 
				'offset' => $match[0][1], 
				'text' => $match['text'][0],
				'attributes' => $attributes, 
			);
			
			if ( $this->should_cloak( $link ) ){
				$links[] = $link;
			}
		}
		
		//Fetch the data for these links from the DB
		if ( $this->options['autocloak_urls'] == 'fancy' ){
			$loaded_links = $this->get_link_data( $links );
		} else {
			$loaded_links = array();
		}
		
		$offset = 0;
		foreach ($links as $key => $link){
			$info = array();
			
			//Figure out the cloaked URL for each link
			$cloaked_url = $link['attributes']['href'];
			if ( $this->options['autocloak_urls'] == 'fancy' ){
				
				if ( isset( $loaded_links[ $link['attributes']['href'] ] ) ){
					$info = $loaded_links[ $link['attributes']['href'] ];
					//Does the link have a permanent name?
					if ( $info['name'] ){
						//Yes
						$cloaked_url = $this->make_cloaked_url( $info['name'] );
					} else {
						//No, so use the current link text instead
						$cloaked_url = $this->make_cloaked_url( $this->escapize($link['text']), $info['id'] );
					}
				}
								
			} else if ( $this->options['autocloak_urls'] == 'encoded' ){
				
				$code = base64_encode( $this->encrypt( $link['attributes']['href'] ) );
				$cloaked_url = $this->make_cloaked_url( $this->escapize($link['text']), null, $code );
												
			}
			
			//Apply tweaks
			
			//Add nofolow
			if ($this->options['tweak_nofollow']){
				
				if ( isset( $link['attributes']['rel'] ) ){
					if ( stripos( $link['attributes']['rel'], 'nofollow' ) === false ){
						$link['attributes']['rel'] .= ' nofollow';
					}
				} else {
					$link['attributes']['rel'] = 'nofollow';
				}
			}
			
			//Open cloaked links in a new window
			if ($this->options['tweak_new_window']){
				$link['attributes']['target'] = '_blank';
			}
			
			//Track clicks with Google Analytics
			if ($this->options['tweak_google_analytics']){
				
				//Build the tracker URL. The format is "/tracker_prefix/[link_name/]target_url_without_protocol" 
				$tracker = '/'.$this->options['tracker_prefix'].'/';
				//Remove the protocol from the URL
				$tracked_url = preg_replace('#^([a-z]+)://#i', '', $link['attributes']['href']);
				
				if ( isset($info['name']) ){
					//Track only by name if the link has one
					$tracker .= $info['name'] . '/';
				} else {
					//For other links, use the URL
					$tracker .= $tracked_url;
				}
				
				//Add or modify the onclick event handler
				if ( isset( $link['attributes']['onclick'] ) ){
					$link['attributes']['onclick'] .= "; elcTrackPageview('$tracker');";
				} else {
					$link['attributes']['onclick'] = "javascript:elcTrackPageview('$tracker');";
				}
			}
			
			//Remove the <!--cloak--> & <!--nocloak--> tags from link text, if present
			$text = preg_replace( '/<!--\s*(no)?cloak\s*-->/i', '', $link['text'] );
			
			//Replace the old URL with the cloaked one
			$link['attributes']['href'] = $cloaked_url;
			
			//Assemble the new link tag
			$new_html = '<a';
			foreach ( $link['attributes'] as $name => $value ){
				$new_html .= sprintf(' %s="%s"', $name, htmlentities( $value, ENT_COMPAT )); 
			}
			$new_html .= '>' . $text . '</a>';
			
			//Replace the old link with the cloaked one
			$html = substr_replace($html, $new_html, $link['offset'] + $offset, strlen($link['original']));
			//Update the replacement offset
			$offset += ( strlen($new_html) - strlen($link['original']) );
			
			$links[$key]['cloaked_url'] = $cloaked_url;			
		}
		
		return $html;
	}
	
	/**
	 * Check if a link should be cloaked.
	 * 
	 * @param array $link
	 * @return bool
	 */
	function should_cloak( $link ){
		//Check if the URL is fully qualified 
		$parts = @parse_url( $link['attributes']['href'] );
		if ( !$parts || !isset($parts['scheme']) || !isset($parts['host']) ) return false;
		
		//Cloak only http and https links
		if ( ($parts['scheme'] != 'http') && ($parts['scheme'] != 'https') ) return false; 		
		
		if ( $this->options['link_mode'] == 'selective' ){
			//In selective mode
			
			//First check if the link contains the "<!--cloak-->" tag.
			if ( preg_match('/<!--\s*cloak\s*-->/i', $link['text']) ){
				return true;
			} 
			//Next, check the URL against the inclusion list
			if ( $this->is_included($link['attributes']['href']) ){
				return true;
			}
			
			//Nothing matched; don't cloak the link.
			return false;
		} else {
			//"Cloak everything" mode
			
			//Check if the link contains the "<!--nocloak-->" tag 
			if ( preg_match('/<!--\s*nocloak\s*-->/i', $link['text']) ){
				return false;
			} 
			//Check the URL against the exclusion list
			if ( $this->is_excluded($link['attributes']['href']) ) {
				return false;
			}
			
			return true;
		}
	}
	
	/**
	 * Check if an URL matches any of the entries on the exclusion list.
	 * 
	 * @param string $url
	 * @return bool
	 */
	function is_excluded($url){
		if (isset($this->options['exclusion'])) {
			foreach($this->options['exclusion'] as $keyword){
				if (stripos($url, $keyword) !== false){
					return true;
				}
			}
		}
		return false;
	}
	
	/**
	 * Check if an URL matches any of the entries on the inclusion list
	 * 
	 * @param string $url
	 * @return bool
	 */
	function is_included($url){
		if( isset($this->options['inclusion']) ){
			foreach($this->options['inclusion'] as $keyword){
				if (stripos($url, $keyword) !== false){
					return true;
				}
			}
		}
		return false;
	}
	
	function make_cloaked_url ( $name, $id = null, $code = null ){
		global $wp_rewrite;
		
		$url = untrailingslashit( get_option('home') );
		
		if ( $wp_rewrite->using_permalinks() ) {
			
			$token1 = $token2 = null;
			
			if ( !empty($name) ){
				//If name is set, the first token is the name and the second is ID or code (if present)
				$token1 = $name;
				$token2 = !empty($id)?$id:$code;
			} else {
				//If name is not set then the first and only token is the ID
				if ( !empty( $id ) ){
					$token1 = $id;
				} else {
					//An impossible situation - at least one of $name or $id must be set
					return false;
				}
			}
			
			//Construct the cloaked URL according to permalink settings
			if ( $wp_rewrite->using_mod_rewrite_permalinks() ) {
				$url = $url . '/' . $this->options['prefix'] . '/' . urlencode($token1) . '/';
				if ( !is_null($token2) ){
					$url .= urlencode($token2) . '/';
				}
			} else if ( $wp_rewrite->using_index_permalinks() ){
				$url = $url . '/index.php/' . $this->options['prefix'] . '/' . urlencode($token1) . '/';
				if ( !is_null($token2) ){
					$url .= urlencode($token2) . '/';
				}
			}
			
		} else {
			//Not using permalinks at all, so pass the params via the query string
			if ( !empty($name) ){
				
				$url .= '/?' . $this->options['prefix'] . '=' . urlencode($name);
				if ( !is_null($id) ){
					$url .= '&id=' . urlencode($id);
				}
				if ( !is_null($code) ){
					$url .= '&code=' . urlencode($code);
				}
				
			} else {
				
				if ( !is_null($id) ){
					$url .= '/?' . $this->options['prefix'] . '=' . urlencode($id);
				} else {
					return false;
				}
			}
			
			
		}
		
		return $url;
	}
	
	function init_buffer(){
		if ( is_admin() ) return;
		//Start output buffering. Note that we won't need to stop it explicitly - 
		//WordPress does that automatically in the "shutdown" action.
		ob_start( array(&$this, 'buffer_callback') );
	}
	
	function buffer_callback( $buffer ){
		//Cloak links in the buffered HTML 
		return $this->process_content( $buffer );
	}
	
  /**
   * EclipseLinkCloaker::get_link_data()
   *
   * Returns the database records for multiple links based on a list of target URLs. 
   * URLs that don't match any cloaked link(s) will be saved as new links and their records also returned.
   *
   * @param array $links An array containing one or more URLs
   * @return array Asociative array in the form link_url -> link_data 
   */
	function get_link_data ( $links ){
		global $wpdb;
		if (!is_array($links) || ( count($links) < 1 )) return array();
		
		//Load the URL info from the database
		$loaded_links = array();
		$q = "
			SELECT * 
			FROM 
				{$wpdb->prefix}lcp_cloaked_links AS links
			WHERE 
				url_hash IN (%s)
			ORDER BY name DESC";
			
		$i = 0;
		while ( $i < count($links) ){
			
			//Load records in batches (otherwise we might exceed the query size limit ) 
			$hashes = array_slice( $links, $i, $this->options['max_db_batch'] );
			$i += count($hashes); 
			
			//Calculate the MD5 hash of each URL
			$hashes = array_map( 
				create_function(
					'$a',
					'return md5( $a[\'attributes\'][\'href\'] );'
				),
				$hashes 
			);
			$s = "'" . implode("','", $hashes) . "'";
			
			//Fetch the links
			$batch = $wpdb->get_results( sprintf($q, $s), ARRAY_A);
			
			if (is_array($batch)){
				//Store them in the loaded_links hashtable, indexed by URL
				foreach($batch as $record){
					$record['from_db'] = true;
					if ( !isset($loaded_links[ $record['url'] ]) ){
						$loaded_links[ $record['url'] ] = $record;
					}
				}
			}				
		}
		
		//Create DB records for those links that don't have them. 
		foreach( $links as $link ){
			if ( isset( $loaded_links[ $link['attributes']['href'] ] ) ) continue;
			
            $info = $this->save_link( $link['attributes']['href'], null );
			if ( $info ){
				$loaded_links[ $info['url'] ] = $info;
			}
		}
		
		return $loaded_links;
	}
	
  /**
   * EclipseLinkCloaker::save_link()
   * Adds a new cloaked link to the database
   *
   * @param string $url A fully qualified URL
   * @param string $name The user-defined link name (can be null if auto-saving)
   * @param string|array $keywords Automatically link these keywords to this URL. This should be a comma-separated list or an array.
   * @param int $max_links The max number of keywords that may be autolinked in one post. 0 = use the default value.
   *
   * @return mixed An associative array containing the ID and other properties of the new link, or FALSE on failure 
   */
	function save_link ( $url, $name, $keywords = '', $max_links = 0 ){
		global $wpdb;
		
		if ( strlen($name) > 100 ) $name = substr($name, 0, 100);
		$name = $this->escapize($name, '');
		
		if ( empty($name) ){
			$q = "INSERT INTO {$wpdb->prefix}lcp_cloaked_links(url, url_hash)
			  VALUES(%s, %s)";
			  
			$q = $wpdb->prepare($q,
				$url, 
				md5($url)
			);
		} else {
			$q = "INSERT INTO {$wpdb->prefix}lcp_cloaked_links(name, url, url_hash)
			  VALUES(%s, %s, %s)";
			
			$q = $wpdb->prepare($q,
			 	$name, 
				$url, 
				md5($url) 
			);
		}
		
		if ( $wpdb->query($q) ) {
			$link_id = $wpdb->insert_id;
			
			//If the link has keywords, save those too.
			if ( !empty($keywords) ){
				//If it's an array, convert it to a comma-separated list
				if ( is_array($keywords) ){
					$keywords = array_filter( array_map('trim', $keywords));
					$keywords = implode(',', $keywords);
				}
				
				$q = "INSERT INTO {$wpdb->prefix}lcp_keywords(link_id, keyword, max_links) VALUES(%d, %s, %d)";
				
				$q = $wpdb->prepare($q,
					$link_id,
					$keywords,
					$max_links
				);
				
				if ( !$wpdb->query($q) ){
					return false;
				}
			}
			
			return array(
				'id' => $wpdb->insert_id,
				'name' => $name, 
				'url' => $url,
				'url_hash' => md5($url),
				'hits' => 0,
				'keywords' => $keywords,
				'max_links' => $max_links,
			);
		} else {
			return false;
		}
	}
	
  /**
   * Delete one or more cloaked links and their stats
   *
   * @param int|array $id Link IDs
   * @return int Number of links actually deleted
   */
	function delete_link( $id ){
		global $wpdb;
		if ( !is_array($id) ){
			$id = array( intval($id) );
		}
		$q = "DELETE FROM {$wpdb->prefix}lcp_stats WHERE link_id IN (". implode(', ', $id) .")";
		$wpdb->query( $q );
		$q = "DELETE FROM {$wpdb->prefix}lcp_keywords WHERE link_id IN (". implode(', ', $id) .")";
		$wpdb->query( $q );
		$q = "DELETE FROM {$wpdb->prefix}lcp_cloaked_links WHERE id IN (". implode(', ', $id) .")";
		return $wpdb->query($q);
	}
		
  /**
   * EclipseLinkCloaker::escapize()
   * Sanitize anchor text for use in a cloaked URL. For image links, try to use the ALT/TITLE text 
   * in place of the anchor text. Removes tags and replaces special characters with underscores.
   *
   * @param string $text Link text (AKA anchor text).
   * @param string $def The default link text. This will be used if $text is empty or unusable. Defaults to the text set in options.   
   * @return string Sanitized link text. Contains only alphanumerics and underscores.
   */
	function escapize( $text, $def = null ){
		$new_text = trim(strip_tags($text));  
		$new_text = preg_replace('/[^a-z0-9_.\-]+/i', '_', $new_text);
		
		if( (strlen($new_text) < 1) || ($new_text == '_') ) {
			if ( !is_null($def) ){
				$new_text = $def;
			} else {
				//If the link contains an image, try extracting the alt or title attribute
				//to use in place of the anchor text.
				if ( preg_match('@<img[^>]+(?:alt|title)\s*=\s*(\"|\')(?P<alt_text>[^\"\'>]+?)\1@i', $text, $matches) ){
					$new_text = $this->escapize( $this->unhtmlentities($matches['alt_text']) );
				} else {
					$new_text = $this->options['default_link_text'];
				}
			}
		};
		
		return $new_text;
	}
	
  /**
   * EclipseLinkCloaker::encrypt()
   * Encrypt/decrypt a string using XOR encryption
   *
   * @param string $str The string the encrypt
   * @param string $password (Optional) The password. If this parameter is omitted then the password set in options['url_pass'] will be used.
   * @return string Encrypted/decrypted string.
   */
	function encrypt($str, $password = ''){
		if ( empty($password) ){
			$password = $this->options['url_pass'];
		}
		$keylen = strlen($password);
 
	    // Loop trough input string
	    for ($i = 0; $i < strlen($str); $i++){
	 
	        // Get key phrase character position
	        $rPos = $i % $keylen;
	 
	        // Magic happens here:
	        $r = ord($str[$i]) ^ ord($password[$rPos]);
	 
	        // Replace characters
	        $str[$i] = chr($r);
    	}
 
    	return $str;
	}
	
  /**
   * EclipseLinkCloaker::decrypt()
   * An alias for EclipseLinkCloaker::encrypt()	
   *
   * @param string $str
   * @param string $password
   * @return string
   */
	function decrypt($str, $password = ''){
		return $this->encrypt($str, $password);
	}
	
  /**
   * EclipseLinkCloaker::hook_init()
   * Parses the current request and handles cloaked redirects.
   * AKA "I've had it with those rewrite rules on this WordPress!"
   *
   * @return void
   */
	function hook_init(){
		//We're only interested in GET requests
		if ( $_SERVER['REQUEST_METHOD'] != 'GET' ) return;
		//Fail early if the URI doesn't contain the prefix 
		if ( !isset($_GET[$this->options['prefix']]) && (strpos( $_SERVER['REQUEST_URI'], '/' . $this->options['prefix'] . '/' ) === false)	) {
			return false;
		}
		
		$req_uri = $_SERVER['REQUEST_URI'];
		$req_uri_array = explode('?', $req_uri);
		$req_uri = $req_uri_array[0];
		
		$home_path = parse_url(get_option('home'));
		if ( isset($home_path['path']) )
			$home_path = $home_path['path'];
		else
			$home_path = '';
		$home_path = trim($home_path, '/');

		$req_uri = trim($req_uri, '/');
		$req_uri = preg_replace("|^$home_path|", '', $req_uri);
		$req_uri = trim($req_uri, '/');
		
		$name = $code = '';
		$id = 0;
		
		/*
			Some valid URI formats : 
			
			index.php/prefix/link_name/
			index.php/prefix/link_text/123
			/prefix/link_name/
			/prefix/123
			index.php?prefix=link_name
			?prefix=link_text&code=encrypted_url
		*/
		
		$pattern = "
		 |^(?:index\.php/)?					# match either start of the URI, or index.php   
			{$this->options['prefix']}/		# match the user-defined prefix followed by a slash
			(?P<token1>(?>[^/?]+))			# followed by up to two tokens separated by a slash 
			(?:/(?P<token2>(?>[^/]+)))?		#  
		 |x";

		if ( preg_match( $pattern, $req_uri, $matches ) ){
			if ( isset($matches['token2'] ) ){
				//Both tokens are present.
				$name = urldecode($matches['token1']);
				//We only need the second one to find the target URL.
				$token2 = urldecode($matches['token2']);
				 
				if ( is_numeric( $token2 ) ){
					//If the second token is numeric then it is a link ID
					$id = intval($token2);
				} else {
					//Otherwise it's an encrypted URL
					$code = $token2;
				}
				
			} else {
				//Only the first token is present. 
				$token1 = urldecode( $matches['token1'] );
				//If it's numeric, it's an ID. Otherwise it's a link name.
				
				if ( is_numeric( $token1 ) ){
					//If the first token is numeric then it is a link ID
					$id = intval($token1);
				} else {
					//Otherwise it's a unique link name
					$name = $token1;
				}
				
			}
			
		} else if ( isset( $_GET[$this->options['prefix']] ) && !empty( $_GET[$this->options['prefix']] )  ){
			//If the pattern doesn't match then the link data should be inside query params
			$name = $_GET[$this->options['prefix']];
			if ( isset( $_GET['code'] ) ) $code = $_GET['code'];
			if ( isset( $_GET['id'] ) ) $id = intval($_GET['id']);
			
			//Again, if the name is numeric then it's actually an ID
			if ( is_numeric($name) ){
				$id = intval($name);
				$name = '';
			}
		} else {
			//False alarm. The current URL doesn't really identify a cloaked link.
			return;
		}

		//echo "name = $name, id = $id, code = $code\n";		
		
		//Get the target link based on the name and/or code
		$link = $this->get_target_link( array('name' => $name, 'id' => $id, 'code' => $code) );
		if ( $link ){
			$title = str_replace(array('_', '-'), ' ', !empty($link['name'])?$link['name']:$name);
			
			//Pass along any GET parameters except the prefix, 'id' and 'code'. Sometimes we also 
			//need to exclude the 'step' params that's automatically tacked on when using the 
			//META refresh trick (see handle_redirect).
			$strip_params = array($this->options['prefix'] => '', 'id' => '', 'code' => '');
			if ( $this->options['redirect_type'] == 'meta' ){ $strip_params['step'] = ''; }
			$params = array_merge( $_GET, $strip_params );
			$params = array_filter( $params );
			$params = http_build_query( $params, null, '&' );
			if ( $params ) {
				$params = '?' . $params;
			}
			
			$url = $link['url'] . $params;
			
			//Set the conversion tracking cookie
			if ( $this->options['enable_conversion_tracking'] ) {
				setcookie($this->conversion_tracker_cookie, $link['id'], time() + 60*60*24*7, '/');
			}
			
			//Perform the redirect & record the hit
			$this->record_hit( $link['id'] );
			$this->handle_redirect( $url, $title );
			
			die();
		} else {
			//No such link
			_e("Error : Invalid link! Please check the URL to make sure it is correct.", 'eclipse-link-cloaker');
			die();
		}
	}
	
  /**
   * EclipseLinkCloaker::handle_redirect()
   * Redirects to a new page using the algorithm set in $options['cloaking_method']   
   *
   * @param string $url The URL to redirect to
   * @param string $title (Optional) The page title. Applies only to the meta refresh and frame algorithms.  
   * @return void
   */
	function handle_redirect( $url, $title = '' ){
		
		switch ( $this->options['redirect_type'] ){
			
			case '301redirect' : //Do a simple 301 redirect (passes SEO value)
				header("HTTP/1.1 301 Moved Permanently");
				header('P3P: CP="DSP NON ADM PUBi BUS NAV COM CNT INT ONL PHY DEM"');
				$this->send_nocache_headers();
				header("Location: $url", true, 301);
				header("X-Redirect-Src: EclipseLinkCloaker", true);
				break;
				
			case '302redirect' : //Do a 302 redirect (probably doesn't pass SEO value)
				header("HTTP/1.1 302 Found");
				header('P3P: CP="DSP NON ADM PUBi BUS NAV COM CNT INT ONL PHY DEM"');
				$this->send_nocache_headers();
				header("Location: $url", true, 302);
				header("X-Redirect-Src: EclipseLinkCloaker", true);
				break;
				
			case '307redirect' : //Do a 307 redirect (Temporary)
				header("HTTP/1.1 302 Temporary Redirect");
				header('P3P: CP="DSP NON ADM PUBi BUS NAV COM CNT INT ONL PHY DEM"');
				$this->send_nocache_headers();
				header("Location: $url", true, 307);
				header("X-Redirect-Src: EclipseLinkCloaker", true);
				break;
				
			case 'meta' :		 //Use the double META refresh trick to hide the referer
				
				//The number of refreshes must be limited because the trick doesn't work with Opera & Chrome
				//and we'd get an infinite loop if we didn't give up after X steps.
				$max_steps = 2; 
			    $referer = $_SERVER['HTTP_REFERER'];
			    $step = isset($_GET['step'])?intval($_GET['step']):1;
			    
			    //If the referer hasn't been cleared yet, redirect to the current URL again (and count redirects/steps)
			    if( ($referer != '') && ( $step < $max_steps ) ) {
			    	$step++;
			    	$url = $_SERVER['REQUEST_URI'];
					if ( strpos($_SERVER['REQUEST_URI'], '?') !== false ){
						$url .= '&';
					} else {
						$url .= '?';
					}
					$url .= "step=$step";
			    }
			    
			    header('P3P: CP="DSP NON ADM PUBi BUS NAV COM CNT INT ONL PHY DEM"');
			    $this->send_nocache_headers();
			    
				echo "<html><head><title>" . htmlspecialchars($title) . "</title>",
				     "<meta http-equiv='refresh' content='0;url=".htmlspecialchars($url)."'></head>",
					 "<body></body></html>";
				break;
				
			case 'frame' : 		//Show the target page inside a frame
				header('P3P: CP="DSP NON ADM PUBi BUS NAV COM CNT INT ONL PHY DEM"');
				$this->send_nocache_headers();
				echo "<html><head><title>", htmlspecialchars($title), "</title></head>",
					 "<frameset rows='100%'><frame src='", htmlspecialchars($url), "'></frameset></html>";
				break;
				
			case 'hideref' :	//Redirect through hideref.com, hiding the referer
				header("HTTP/1.1 302 Found");
				header('P3P: CP="DSP NON ADM PUBi BUS NAV COM CNT INT ONL PHY DEM"');
				$this->send_nocache_headers();
				header("Location: http://hiderefer.com/?$url", true, 302);
				header("X-Redirect-Src: EclipseLinkCloaker", true);
				break;
				
			default :
				printf(__('Configuration error : unrecognized or unsupported redirect type "%s"', 'eclipse-link-cloaker'),$this->options['redirect_type']); 
				
		}
	}
	
  /**
   * EclipseLinkCloaker::get_target_link()
   * Get info about a cloaked link. The parameter must be an associative array with at least
   * one of these keys set : 
   *	id - a database ID
   *	code - an encrypted URL
   *	name - a unique, user-defined name associated with a link
   *
   * @param array $query An associative array.  
   * @return array The link record as an associative array, or Null on failure
   */
	function get_target_link( $query ){
		global $wpdb;
		
		if ( !empty( $query['id'] ) ) {
			//Load the link record with this ID from the DB.
			$q = "SELECT * FROM {$wpdb->prefix}lcp_cloaked_links WHERE id=%d LIMIT 1";
			$link = $wpdb->get_row( $wpdb->prepare($q, intval($query['id'])), ARRAY_A);
			return $link;
		} else if ( !empty( $query['code'] ) ){
			//The code should be an encrypted URL. Decrypt it.
			$code = $this->decrypt( base64_decode( $query['code'] ) );
			return array('id' => 0, 'url' => $code, 'name' => $name);
		} else if ( !empty( $query['name'] ) ){
			//Load the link that has this user-defined name
			$q = "SELECT * FROM {$wpdb->prefix}lcp_cloaked_links WHERE name=%s LIMIT 1";
			$link = $wpdb->get_row( $wpdb->prepare($q, $query['name']), ARRAY_A);
			return $link;			
		} else {
			return null;
		}
	}
	
  /**
   * EclipseLinkCloaker::record_hit()
   * Record the current visit for the cloaked link identified by $id.  
   *
   * @param int $id Link ID. 
   * @return bool True if the visit was successfully recorded.
   */
	function record_hit( $id ){
		global $wpdb;
		
		//Check if the passed value is a valid ID
		if ( !is_numeric($id) ) return false;  
		$id = intval($id);
		if ($id <= 0) return false;
		
		//Don't record bots and web crawlers
		/*//TODO: Re-enable bot filtering
		if( array_search(strtolower($_SERVER['HTTP_USER_AGENT']), $this->options['bots'])!==false ) {
			return false;
		}
		//*/
		
		//First update the sum total
		$q = "UPDATE {$wpdb->prefix}lcp_cloaked_links SET hits = hits + 1 WHERE id = ".$id;
		$wpdb->query($q);
		//Then update the daily stats
		$q = "INSERT INTO {$wpdb->prefix}lcp_stats(link_id, period, hits) VALUES(%d, %s, 1)
			  ON DUPLICATE KEY UPDATE hits = hits +1";
		$q = $wpdb->prepare( $q, $id, current_time('mysql') );
		
		return $wpdb->query( $q ) !== false;
	}
	
  /**
   * EclipseLinkCloaker::clean_database()
   * Removes old statistics from the DB
   *
   * @return void
   */
	function clean_database(){
		global $wpdb;
		
		if ( time() - $this->options['last_db_cleanup'] > $this->options['db_cleanup_interval'] ){
		
			$q = "DELETE FROM {$wpdb->prefix}lcp_stats WHERE period < %s";
			$q = $wpdb->prepare( $q, date('Y-m-d', strtotime('-33 days')) );
			
			$wpdb->query( $q );
			
			$this->options['last_db_cleanup'] = time();
			$this->save_options();
		}
	}

  /**
   * EclipseLinkCloaker::activation()
   * Plugin activation hook.
   *
   * @return void
   */
	function activation(){
		$this->deactivate_free_version();
		
		$this->upgrade_database();
		
		//Generate a random URL encryption key if it hasn't been set yet
		if ( empty($this->options['url_pass']) ){
			$this->options['url_pass'] = md5( mt_rand() . '|' . get_option('siteurl') );
		}
		
		$this->save_options();
		
		$this->flush_rewrite_rules();
	}
	
  /**
   * EclipseLinkCloaker::deactivate_free_version()
   * Deactivate the free version of the plugin, if present, and remove it's 
   * .htaccess code (as it would cause conflicts otherwise). 
   *
   * @return void
   */
	function deactivate_free_version(){
		global $ws_link_cloaker;
		
		//Deactivate the Link Cloaking Plugin
		deactivate_plugins( 'link-cloaking-plugin/wplc_link_cloaking_plugin.php' );
		
		//Explicitly remove the old plugin's hook that modifies .htaccess rules. This is neccessary
		//because even after a plugin has been deactivated its hooks still stay active for the duration
		//of the current script's execution. However, we need to get rid of this particular hook
		//pronto because it would interfere with our own .htaccess updates.
		if ( isset($ws_link_cloaker) ){
			remove_filter('mod_rewrite_rules', array(&$ws_link_cloaker, 'rewrite_rules'));
		}
		
		//Find the .htaccess, if present
		$filename = get_home_path() . '.htaccess';
		if ( !file_exists( $filename ) || !is_writeable( $filename ) ) 
			return;
		
		//Remove the Link Cloaking Plugin's conflicting .htaccess rules
		$htaccess = file_get_contents($filename);
		$htaccess = preg_replace( '/\s*#\s*Link\s*Cloaker\s*Plugin\s*BEGINS?.*?#\s*Link\s*Cloaker\s*Plugin\s*ENDS?[\r\n]+\s*/si', "\n\n", $htaccess );
		file_put_contents($filename, $htaccess);
	}
 
 
	function admin_menu(){
		$options_page_hook = add_options_page(
			'Eclipse Link Cloaker Settings', 'Link Cloaker', 'manage_options',
			'eclipse_cloaker_settings',array(&$this,'page_options')
		);
		add_action( 'admin_print_scripts-' . $options_page_hook, array(&$this, 'load_admin_options_scripts'));
		add_action( 'admin_print_styles-' . $options_page_hook, array(&$this, 'load_admin_css') );		
			
		//Only show the "Cloaked Links" menu if this installation of the plugin is registered.
		if ( $this->registered() ){
			$links_page_hook = add_management_page(
				__('Cloaked Links', 'eclipse-link-cloaker'), 
				__('Cloaked Links', 'eclipse-link-cloaker'), 
				'manage_links',
				'eclipse_cloaked_links', 
				array(&$this, 'page_cloaked_links')
			);
			add_action( 'admin_print_styles-' . $links_page_hook, array(&$this, 'load_admin_css') );
	        add_action( 'admin_print_scripts-' . $links_page_hook, array(&$this, 'load_admin_scripts') );
	        
	        if ( is_callable('add_screen_options_panel') ){
	        	add_screen_options_panel(
					'elc-screen-options', 
					'',
					array(&$this, 'screen_options'),
					$links_page_hook 
				);
	        }
		}
	}
 
	function mytruncate($str, $max_length = 50){
		if(strlen($str)<=$max_length) return $str;
		return (substr($str, 0, $max_length-3).'...');
	}

 	function page_options(){
		if(isset($_POST['submit']) && current_user_can('manage_options')) {
			check_admin_referer('link-cloaker-settings');
			
			//If the license key was changed, or a new key entered, verify it. Also re-verify the key
			//if the plugin still isn't registered (this lets the user try the same key several times).
			if ( isset($_POST['key']) && ( ($_POST['key'] != $this->options['key']) || !$this->registered() ) ){
				 
				$this->options['key'] = $_POST['key'];
				$this->options['verification'] = $this->verify_key( $this->options['key'] );
				$this->options['last_verification_time'] = time();
				
				if ( $this->options['verification']['success'] ){
					printf('<div class="updated"><p><strong>%s</strong></p></div>', __('Success! The link cloaker is now enabled.', 'eclipse-link-cloaker'));
				} else {
					//Something untoward happened... Do nothing; the appropriate error message 
					//will be displayed when the key input box is shown again. 
				}
				
				$this->save_options();
				
			} else if ( $this->registered() ) {
				//Just save the settings
				
				//Cloaking settings
				$this->options['filter_mode'] = $_POST['filter_mode'];
				$this->options['filter_feed'] = isset($_POST['filter_feed']) && $_POST['filter_feed'];
				$this->options['link_mode'] = $_POST['link_mode'];
				$this->options['redirect_type'] = $_POST['redirect_type'];
				
				if ( !empty($_POST['prefix']) ){
					$this->options['prefix'] = $this->escapize($_POST['prefix'], $this->options['prefix']);
				} else {
					$this->options['prefix'] = 'goto';
				}
				
				$this->options['default_link_text'] = $this->escapize($_POST['default_link_text'], 'link');
				
				$this->options['exclusion'] = array_filter( preg_split( '/[\s,\r\n]+/', $_POST['exclusion'] ) );
				$this->options['inclusion'] = array_filter( preg_split( '/[\s,\r\n]+/', $_POST['inclusion'] ) );
				
				$this->options['tweak_nofollow'] = isset($_POST['tweak_nofollow']) && $_POST['tweak_nofollow'];
				$this->options['tweak_new_window'] = isset($_POST['tweak_new_window']) && $_POST['tweak_new_window'];
				$this->options['tweak_google_analytics'] = isset($_POST['tweak_google_analytics']) && $_POST['tweak_google_analytics'];
				
				$this->options['tracker_prefix'] = !empty($_POST['tracker_prefix'])? $this->escapize($_POST['tracker_prefix'],'cloaked'):'cloaked';
				
				//Autolinking settings
				$this->options['autolink_enable'] = isset($_POST['autolink_enable']) && $_POST['autolink_enable'];
				$this->options['autolink_only_single'] = isset($_POST['autolink_only_single']) && $_POST['autolink_only_single'];
				
				$this->options['autolink_max_total_links'] = intval($_POST['autolink_max_total_links']);
				if ( $this->options['autolink_max_total_links'] < 0 ) $this->options['autolink_max_total_links'] = 0;
				$this->options['autolink_max_links'] = intval($_POST['autolink_max_links']);
				if ( $this->options['autolink_max_links'] < 0 ) $this->options['autolink_max_links'] = 0;
				
				//Turn the comma-separated post list into an array; trim whitespace and remove empty items
				$this->options['autolink_ignore_posts'] = array_filter( 
					array_map( 'trim', explode(',', $_POST['autolink_ignore_posts']) ) 
				);
				//Do the same thing to the tag list
				$this->options['autolink_exclude_tags'] = array_filter( 
					array_map( 'trim', explode(',', $_POST['autolink_exclude_tags']) ) 
				);
				
				$this->options['autolink_enable_css'] = isset($_POST['autolink_enable_css']) && $_POST['autolink_enable_css'];
				$this->options['autolink_css_rules'] = $_POST['autolink_css_rules'];
				
				$this->options['autolink_cloak'] = isset($_POST['autolink_cloak']) && $_POST['autolink_cloak'];
				$this->options['autolink_nofollow'] = isset($_POST['autolink_nofollow']) && $_POST['autolink_nofollow'];
				$this->options['autolink_new_window'] = isset($_POST['autolink_new_window']) && $_POST['autolink_new_window'];
				
				$this->options['enable_conversion_tracking'] = isset($_POST['enable_conversion_tracking']) && $_POST['enable_conversion_tracking'];
				
				$this->save_options();
				
				echo '<div id="message" class="updated fade"><p><strong>',__('Settings saved.', 'eclipse-link-cloaker'), '</strong></p></div>';
			}
		}
		
?>
<div class="wrap"><h2><?php _e('Eclipse Link Cloaker Settings', 'eclipse-link-cloaker'); ?></h2>
<form name="cloaking_options" method="post" action="<?php echo esc_attr(admin_url('options-general.php?page=eclipse_cloaker_settings')); ?>"> 

<?php
	wp_nonce_field('link-cloaker-settings');
?>

<div id='lcp-tabs'>

<ul><?php
	if ( $this->registered() ) {  
?> 
	<li><a href="#lcp-tab-1"><?php _e('Link Cloaking', 'eclipse-link-cloaker'); ?></a></li>
	<li><a href="#lcp-tab-2"><?php _e('Keyword Autolinking', 'eclipse-link-cloaker'); ?></a></li>
	<li><a href="#lcp-tab-3"><?php _e('Conversion Tracking', 'eclipse-link-cloaker'); ?></a></li><?php
	}
?>	
	<li><a href="#lcp-tab-4" <?php if ( $this->registered() ) echo 'class="lcp-registered"'; ?>><?php _e('Registration', 'eclipse-link-cloaker'); ?></a></li>
</ul>

<?php 
	//Only show the settings when the plugin is registered 
	if ( $this->registered() ) {
?>

<div id="lcp-tab-1">
<h3><?php _e('Link Cloaking', 'eclipse-link-cloaker'); ?></h3>

<table class="form-table"> 

<tr valign="top"> 
<th scope="row"><?php _e('General', 'eclipse-link-cloaker'); ?></th> 
<td>

<p>
<label for="filter_mode_content"><input type="radio" name="filter_mode" id="filter_mode_content" value="content"
<?php checked($this->options['filter_mode'], 'content'); ?>/> <?php _e('Cloak links in post/page content', 'eclipse-link-cloaker'); ?></label>
</p>
<p>
<label for="filter_mode_everything"><input type="radio" name="filter_mode" id="filter_mode_everything" value="everything"
<?php if($this->options['filter_mode'] == 'everything') echo ' checked' ?>/> <?php _e('Cloak links in any part of the site', 'eclipse-link-cloaker'); ?></label>
</p>
<p>
<label for="filter_feed"><input type="checkbox" name="filter_feed" id="filter_feed"
<?php if($this->options['filter_feed']) echo ' checked' ?>/> <?php _e('Also cloak links in the RSS feed', 'eclipse-link-cloaker'); ?></label>
</p>

</td> 
</tr> 

<tr valign="top"> 
<th scope="row"><?php _e('Cloaking mode', 'eclipse-link-cloaker'); ?></th> 
<td>

<p>
<label><input type="radio" name="link_mode" id="mode-everything" value="everything"
<?php if($this->options['link_mode']=='everything') echo ' checked' ?>/> <?php _e('Cloak All Links', 'eclipse-link-cloaker'); ?></label><br>
<?php _e('All links will be cloaked, except links that are tagged with <code>&lt;!--nocloak--&gt;</code> or match the exception list.', 'eclipse-link-cloaker'); ?>
</p>

<p>
<label><input type="radio" name="link_mode" id="mode-selective" value="selective" 
<?php if($this->options['link_mode']=='selective') echo ' checked' ?>/> <?php _e('Selective Cloaking', 'eclipse-link-cloaker'); ?></label><br/>
<?php _e('Only links tagged with <code>&lt;!--cloak--&gt;</code> (e.g. <code>&lt;a href="http://domain.com/"&gt;&lt;!--cloak--&gt;Visit This Site&lt;/a&gt;</code>) and links that match the inclusion list will be cloaked.', 'eclipse-link-cloaker'); ?>
</p>

</td> 
</tr>

<tr valign="top" id="row-exclusion-list"<?php 
	if($this->options['link_mode'] != 'everything') echo ' style="display:none;"'; 
?>> 
<th scope="row"><?php _e('Exception list', 'eclipse-link-cloaker'); ?></th> 
<td>

<textarea name='exclusion' id='exclusion' cols='50' rows='6'>
<?php echo implode("\n", $this->options['exclusion']); ?>
</textarea>

<br/>

<?php _e('List one domain or URL per row. Links that match any of the entered values will not be cloaked when cloaking mode is set to "Cloak All Links".', 'eclipse-link-cloaker'); ?>

</td> 
</tr> 

<tr valign="top" id="row-inclusion-list"<?php 
	if($this->options['link_mode'] != 'selective') echo ' style="display:none;"'; 
?>> 
<th scope="row"><?php _e('Inclusion list', 'eclipse-link-cloaker'); ?></th> 
<td>

<textarea name='inclusion' id='inclusion' cols='50' rows='6'>
<?php echo implode("\n", $this->options['inclusion']); ?>
</textarea>

<br/>

<?php _e('List one domain or URL per row. Links that match any of the entered values will be automatically cloaked in selective cloaking mode.', 'eclipse-link-cloaker'); ?>

</td> 
</tr>

<tr valign="top"> 
<th scope="row"><?php _e('Link prefix', 'eclipse-link-cloaker'); ?></th> 
<td><input type='text' name='prefix' id='prefix' value='<?php echo $this->options['prefix']; ?>' size='25' />
<br />
<?php 
	printf( 
		__('Your cloaked links will look similar to this : <code>%s</code>', 'eclipse-link-cloaker'),
		$this->make_cloaked_url('LinkName')
	);
?>
</td></tr>


<tr valign="top"> 
<th scope="row"><?php _e('Cloaking type', 'eclipse-link-cloaker'); ?></th> 
<td>

<p>
<?php
	$redirect_types = array(
		'301redirect'=> __('301 Permanent Redirect', 'eclipse-link-cloaker'),
		'302redirect' => __('302 Found (default)', 'eclipse-link-cloaker'),
		'307redirect' => __('307 Temporary Redirect', 'eclipse-link-cloaker'),	
		'meta' => __('Double META refresh (Hides the referer)', 'eclipse-link-cloaker'),
		'frame' => __('Show the target site in a frame (Cloaks the address bar URL)', 'eclipse-link-cloaker'),
		//'hideref'=>'Redirect through hiderefer.com', //Disabled as unnecessary
	);
	foreach ($redirect_types as $type => $description){
		echo '<p><label for="redirect_'.$type.'">';
		echo '<input type="radio" name="redirect_type" id="redirect_'.$type.'" value="'.$type.'"';
		if ($this->options['redirect_type'] == $type){
			echo ' checked="checked"';
		}
		echo ' /> '.$description.'</label></p>'; 		
	} 
?>
</p>

</td> 
</tr>  

<tr valign="top"> 
<th scope="row"><?php _e('Tweaks', 'eclipse-link-cloaker'); ?></th> 
<td>

<p>
<label for="tweak_nofollow"><input type="checkbox" name="tweak_nofollow" id="tweak_nofollow"
<?php if($this->options['tweak_nofollow'] ) echo ' checked' ?>/> <?php 
	_e('Prevent search engines from following cloaked links by adding rel="nofollow"', 'eclipse-link-cloaker'); 
?></label>
</p>

<p>
<label for="tweak_new_window"><input type="checkbox" name="tweak_new_window" id="tweak_new_window"
<?php if($this->options['tweak_new_window'] ) echo ' checked' ?>/> <?php
	_e('Open cloaked links in a new window', 'eclipse-link-cloaker'); 
?></label>
</p>

<p>
<label for="tweak_google_analytics"><input type="checkbox" name="tweak_google_analytics" id="tweak_google_analytics"
<?php if($this->options['tweak_google_analytics']) echo ' checked' ?>/> <?php 
	_e('Track clicks with Google Analytics (you need to have the GA code already set up on your site for this to work).', 'eclipse-link-cloaker'); 
?></label>

</p>

<p style='margin-left: 3em;'>
<?php _e('Tracker URL prefix :', 'eclipse-link-cloaker'); ?>  
<input type="text" name="tracker_prefix" id="tracker_prefix" value="<?php 
	echo htmlspecialchars($this->options['tracker_prefix']); 
?>" <?php if ( !$this->options['tweak_google_analytics'] ) echo 'disabled="disabled"'; ?>>
</p>

</td> 
</tr> 

<tr valign="top"> 
<th scope="row"><?php _e('Default link text', 'eclipse-link-cloaker'); ?></th> 
<td><input type='text' name='default_link_text' id='default_link_text' value='<?php echo $this->options['default_link_text']; ?>' size='25' />
<br />
<?php 
	echo __("To make automatically cloaked links look better, the plugin uses the link's anchor text or alt text in the cloaked URL. Here you can specify the text it should use for links that have no valid anchor text.", 'eclipse-link-cloaker');
?>
</td></tr>

</table>
</div>

<div id="lcp-tab-2">
<h3><?php _e('Keyword Autolinking', 'eclipse-link-cloaker'); ?></h3>

<p><?php 
	_e('In addition to cloaking links, you can also specify one or more keywords for each cloaked link and have Eclipse Cloaker automatically parse your posts and turn those keywords into links.', 'eclipse-link-cloaker'); 
?></p>

<table class="form-table">

<tr valign="top"> 
<th scope="row"><?php _e('General', 'eclipse-link-cloaker'); ?></th> 
<td>

	<p>
	<label for="autolink_enable"><input type="checkbox" name="autolink_enable" id="autolink_enable"
	<?php if($this->options['autolink_enable']) echo ' checked' ?>/> <?php _e('Enable autolinking', 'eclipse-link-cloaker'); ?></label>
	</p> 

	<p>
	<label for="autolink_only_single"><input type="checkbox" name="autolink_only_single" id="autolink_only_single"
	<?php if($this->options['autolink_only_single']) echo ' checked' ?>/> <?php 
		_e('Process only single posts and pages', 'eclipse-link-cloaker'); 
	?></label>
	</p>

</td> 
</tr> 

<tr valign="top"> 
<th scope="row"><?php _e('Max links per post', 'eclipse-link-cloaker'); ?></th> 
<td><input type='text' name='autolink_max_total_links' id='autolink_max_total_links' value='<?php echo $this->options['autolink_max_total_links']; ?>' size='4' /> 
<?php _e('links', 'eclipse-link-cloaker'); ?>
</td></tr>

<tr valign="top"> 
	<th scope="row"><?php _e('Max links per keyword', 'eclipse-link-cloaker'); ?></th> 
	<td>
		<input type='text' name='autolink_max_links' id='autolink_max_links' 
			value='<?php echo $this->options['autolink_max_links']; ?>' size='4' /> <?php _e('links', 'eclipse-link-cloaker'); ?>
	</td>
</tr>

<tr valign="top"> 
	<th scope="row"><?php _e('Ignore Posts and Pages', 'eclipse-link-cloaker'); ?></th> 
	<td>
		<input type='text' name='autolink_ignore_posts' id='autolink_ignore_posts' 
			value='<?php 
				if ( is_array($this->options['autolink_ignore_posts']) ) {
					echo esc_attr( implode(', ', $this->options['autolink_ignore_posts']) );
				} 
			?>' size='80' />
		<br />
		<?php _e('To disable autolinking for a certain post or page, add its  ID, slug or name to this box. Use a comma to separate multiple entries.', 'eclipse-link-cloaker'); ?>
	</td>
</tr>

<tr valign="top"> 
	<th scope="row"><?php _e('Ignore Tags', 'eclipse-link-cloaker'); ?></th> 
	<td>
		<input type='text' name='autolink_exclude_tags' id='autolink_exclude_tags' 
			value='<?php 
				echo esc_attr( implode(', ', $this->options['autolink_exclude_tags']) );
			 ?>' size='80' />
		<br />
		<?php _e("The plugin won't create links inside these tags.", 'eclipse-link-cloaker'); ?>
	</td>
</tr>

<tr valign="top">
    <th scope="row"><?php _e('Custom CSS', 'eclipse-link-cloaker'); ?></th>
    <td>
    	<p>
    	<label for='autolink_enable_css'>
    		<input type="checkbox" name="autolink_enable_css" id="autolink_enable_css"
        	<?php if ($this->options['autolink_enable_css']) echo ' checked="checked"'; ?>/>
        	<?php
        		printf( 
					__('Add <code>class="%s"</code> to all automatically inserted links', 'eclipse-link-cloaker'),
					htmlentities($this->options['autolink_css_class'])
				);        	
        	?>
		</label>
		
		</p>
		
		<?php _e("Enter the custom CSS code for automatically inserted links here. Alternatively, you can add it to your theme's stylesheet instead.", 'eclipse-link-cloaker'); ?><br>
	    <textarea name="autolink_css_rules" id="autolink_css_rules" cols='45' rows='4' <?php 
			if ( !$this->options['autolink_enable_css'] ) echo 'disabled="disabled"'; 
		?>/><?php
	        if( isset($this->options['autolink_css_rules']) )
	            echo htmlentities($this->options['autolink_css_rules']);
	    ?></textarea>

    </td>
</tr>

<tr valign="top"> 
	<th scope="row"><?php _e('Tweaks', 'eclipse-link-cloaker'); ?></th> 
	<td>
	
		<p>
		<label for="autolink_nofollow"><input type="checkbox" name="autolink_nofollow" id="autolink_nofollow"
		<?php if($this->options['autolink_nofollow'] ) echo ' checked' ?>/> 
		<?php _e('Prevent search engines from following automatically inserted links', 'eclipse-link-cloaker'); ?></label>
		</p>
		
		<p>
		<label for="autolink_new_window"><input type="checkbox" name="autolink_new_window" id="autolink_new_window"
			<?php if($this->options['autolink_new_window'] ) echo ' checked' ?>/> 
		<?php _e('Open automatically inserted links in a new window', 'eclipse-link-cloaker'); ?></label>
		</p>
		
		<p>
		<label for="autolink_cloak"><input type="checkbox" name="autolink_cloak" id="autolink_cloak"
			<?php if($this->options['autolink_cloak']) echo ' checked' ?>/>
			<?php _e('Always cloak automatically inserted links (overrides cloaking settings)', 'eclipse-link-cloaker'); ?> 
		</label>
		
		</p>
	
	</td> 
</tr> 

</table>
</div>

<div id="lcp-tab-3">
<h3><?php _e('Conversion Tracking', 'eclipse-link-cloaker'); ?></h3>

<p><?php _e("Some affiliate programs offer an option to insert custom tracking code on the order confirmation page. With Eclipse Cloaker, you can use this feature to keep track of how many of the people that click your cloaked links actually buy the product you linked to. Just place the tracker code (below) on the merchant's confirmation page, and the conversion stats will start showing up in the <em>Tools -&gt; Cloaked Links</em> tab.", 'eclipse-link-cloaker'); ?> 
</p>

<table class="form-table">

<tr valign="top"> 
<th scope="row"><?php _e('General', 'eclipse-link-cloaker'); ?></th> 
<td>

	<p>
	<label for="enable_conversion_tracking"><input type="checkbox" name="enable_conversion_tracking" id="enable_conversion_tracking"
	<?php if($this->options['enable_conversion_tracking']) echo ' checked' ?>/> <?php _e('Enable conversion tracking', 'eclipse-link-cloaker'); ?></label>
	</p> 

</td> 
</tr>

<tr valign="top"> 
<th scope="row"><?php _e('Tracker code', 'eclipse-link-cloaker'); ?></th> 
<td>

 
<textarea name='conversion-tracker-code' id='conversion-tracker-code' cols='50' rows='6' readonly="true"><?php 
	echo htmlentities($this->get_pixeltracker_code()); 
?></textarea>
<br /><?php _e('Place this HTML code on the merchant\'s confirmation/"Thank You" page.', 'eclipse-link-cloaker'); ?>

</td> 
</tr>

</table> 
</div> 
 
<?php
	} //end settings only available to registered users
?> 

<div id="lcp-tab-4">
<h3><?php _e('Registration', 'eclipse-link-cloaker'); ?></h3>

<table class="form-table">
<tr valign="top"> 
<th scope="row"><?php _e('License key', 'eclipse-link-cloaker'); ?></th> 
<td>
<input type='text' name='key' id='license-key' value='<?php echo htmlspecialchars($this->options['key']); ?>' size='32' maxlength='16' />

<?php 
	if ( empty($this->options['key']) ){
		echo '<span style="font-weight: bold; padding: 0.4em; background-color:#fffb82;">',
				__('Please enter the license key.', 'eclipse-link-cloaker'),
			 '</span>';
	} else {
		if ( $this->options['verification']['success'] ){
			echo '<span style="font-weight: bold; padding: 0.4em;">', 
					__('This key is valid.', 'eclipse-link-cloaker'), 
				 '</span>';
		} else {
			echo '<span class="error" style="font-weight: bold; padding: 0.4em;">',
			 		sprintf(__('Error : %s', 'eclipse-link-cloaker'), $this->options['verification']['error_message']),
				 '</span>';
		}
	}
?>
</td></tr> 

</table> 
</div>

</div> <!--/lcp-tabs-->

<p class="submit"><input type="submit" name="submit" class='button-primary' value="<?php
	if ( $this->registered() ){ 
		_e('Save Changes'); 
	} else {
		_e('Check Key', 'eclipse-link-cloaker'); 
	}
?>" /></p>
</form>
</div>

<?php 

	}
 
	function output_link_row ( $link, $rowclass = '', $base_url = '' ){
	 	if ( empty($base_url) ){
			if ( !empty($_SERVER['HTTP_REFERER']) ){
				$base_url = $_SERVER['HTTP_REFERER'];
			} else {
				$base_url = 'tools.php?page=eclipse_cloaked_links';
			}
		}
	 	?>
		<tr id='<?php echo "lcp-link-", $link['id']; ?>' class='lcp-row <?php echo $rowclass; ?>'>
            	<th scope="row" class="check-column">
            		<input type="checkbox" name="links[]" value="<?php echo $link['id']; ?>" />
				</th>
            	
				<td class='post-title column-title'>
                	<span class='lcp-link-id' style='display:none;'><?php echo $link['id']; ?></span>
                <?php 
					if ( !empty( $link['name'] ) ) {
						echo htmlspecialchars($link['name']);
					} else {
						echo '<span class="lcp-unnamed">', __('(None)','eclipse-link-cloaker'),'</span>';
					} 
					
					//Output inline action links (copied from edit-post-rows.php)                  	
                  	$actions = array();
					$actions['edit'] = '<span><a href="#" class="lcp-editinline" title="' . esc_attr(__('Edit this link', 'eclipse-link-cloaker')) . '">' . __('Edit', 'eclipse-link-cloaker') . '</a>';
					$actions['stats'] = '<span class="view"><a href="#" class="lcp-stats-button">' . __('Stats', 'eclipse-link-cloaker') . '</a>';
					
					$delete_url = wp_nonce_url(
						add_query_arg(array(
							'action' => 'delete-cloaked-link',
							'id' => $link['id'],
							'noheader' => 1,
						), $base_url),
						'delete-link_' . $link['id']
					);
					
					if (!empty($link['name']))
						$delete_confirmation = sprintf(__("You are about to delete the cloaked link '%s'\n  'Cancel' to stop, 'OK' to delete.", 'eclipse-link-cloaker'), $link['name']);
					else {
						$delete_confirmation = __("You are about to delete this cloaked link\n  'Cancel' to stop, 'OK' to delete.", 'eclipse-link-cloaker');
					} 
					
					$actions['delete'] = "<span class='delete'><a class='submitdelete' title='" . esc_attr(__('Delete this cloaked link','eclipse-link-cloaker')) . "' href='" . $delete_url . "' onclick=\"if ( confirm('" . esc_js( $delete_confirmation ). "') ) { return true;}return false;\">" . __('Delete', 'eclipse-link-cloaker') . "</a>";
					echo '<div class="row-actions">', implode(' | </span>', $actions), '</div>';
					
					echo '<div class="hidden" id="inline_', $link['id'] ,'">',
						 '<div class="link_name">', $link['name'] , '</div>',
						 '<div class="link_url">', $link['url'] , '</div>',
						 '<div class="link_keywords">', $link['keyword'] , '</div>',
						 '<div class="max_links">', !empty($link['max_links'])?$link['max_links']:'' , '</div>',
						 '</div>';
				?>
                </td>
                
				<td class='column-url'>
                	<a href='<?php print $link['url']; ?>' target='_blank' class='lcp-link-url'>
                    	<?php print $this->mytruncate($link['url']); ?></a>
                    <?php
                    	//Output the cloaked URL here for easy copy-pasting (is this really useful?)
                    	if ( !empty( $link['name'] ) ){
             				$cloaked = $this->make_cloaked_url( $link['name'] );
                    	} else {
							$cloaked = $this->make_cloaked_url('', $link['id']);
						}
                
						echo '<div class="row-actions">',
							 '<input type="text" value="'. $cloaked .'" class="lcp-cloaked-url-box" readonly="readonly" />',
							 '</div>';
                    ?>
				</td>
				
				<td>
                    <?php
                    	echo $this->mytruncate($link['keyword']);
                    ?>
				</td>
                
				<td><a href="#" class="lcp-stats-button"><?php 
						echo $link['hits'];
					?></a>
				</td>
				
				<?php if ($this->options['enable_conversion_tracking']) { ?>
				<td><a href="#" class="lcp-stats-button"><?php 
						echo $link['conversions'];
						if ( $link['hits'] > 0 ){
							printf(' (%.2f%%)', $link['conversions'] / $link['hits'] * 100);
						} else {
							echo ' (0.00%)';
						}
					?></a>
				</td>
				<?php } ?>
				
            </tr>
            
            <?php
            
            $cols = $this->options['enable_conversion_tracking'] ? 6 : 5;
            printf(
				'<tr id="lcp-stats-row-%d" style="display:none;"><td colspan="%d" id="lcp-link-stats-%d"> </td></tr>',
				$link['id'], 
				$cols,
				$link['id']
			);
 	}
 
	function page_cloaked_links(){
		global $wpdb;
		
		//Permission check (this really should be handled by WP core (And it probably is in newer WP versions))
		if ( !current_user_can('manage_links')) return;
		
		//Remove stale stats from the DB, if necessary
		$this->clean_database();
		
		$message = '';
		$msgclass = '';
		
		$messages = array(
			1 => __('Link deleted.','eclipse-link-cloaker'),
			2 => __('An unexpected database error occured while trying to delete the link.', 'eclipse-link-cloaker'),
		);
		
		if ( isset($_GET['message']) && isset($messages[$_GET['message']]) ){
			$message = $messages[$_GET['message']];
		}
		if ( isset($_GET['deleted']) && ($_GET['deleted'] > 0) ){
			$deleted = intval($_GET['deleted']);
			$message = sprintf(
				_n(
					'%d link deleted.',
					'%d links deleted.',
					$deleted,
					'eclipse-link-cloaker'
				),
				$deleted
			);
			$msgclass = 'updated';
		}
		
		if ( isset($_GET['updated']) ) $msgclass .= ' updated ';
		if ( isset($_GET['error']) ) $msgclass .= ' error ';
		$msgclass = trim($msgclass);
		
		//Figure out the current URL sans params with special meanings
		$base_url = remove_query_arg( array('id', 'ids', '_wpnonce', 'noheader', 'updated', 'error', 'deleted', 'action', 'action2', 'message') );

		//Should we show the "Add New Link" form?
		$add_link_form = isset($_GET['add_link_form']);
		//Default values for the form's fields
		$link_name = $link_url = $link_keywords = '';
		$max_autolinked_keywords = null;
		$case_sensitive = false;  
		
		$action = isset($_GET['action'])?$_GET['action']:(isset($_POST['action'])?$_POST['action']:'');
		if ( (empty($action) || ($action == '-1')) && !empty($_POST['action2']) ){
			$action = $_POST['action2'];
		}
		
		if ( $action == 'add-cloaked-link' ){
			check_admin_referer('add-cloaked-link') or die("Your request failed the security check");
		
			//Always show the form when adding a new link. This lets the user quickly
			//fix any mistakes he might have made when adding the link, or add another new link
			//right away.
			$add_link_form = true;
		
			//Store the field values in local vars and do some first-pass sanitization
			$link_name = isset($_POST['link_name'])?$_POST['link_name']:'';
			$link_url = isset($_POST['link_url'])?$_POST['link_url']:'';
			
			$link_keywords = isset($_POST['link_keywords'])?$_POST['link_keywords']:'';
			//Remove empty keywords and trim whitespace
			$link_keywords = array_filter( array_map('trim', explode(",", $link_keywords) ));
			$link_keywords = implode(",", $link_keywords);
			
			$max_autolinked_keywords = isset($_POST['max_links'])?intval($_POST['max_links']):0;
			if ( $max_autolinked_keywords < 0 ) $max_autolinked_keywords = 0;
			
			//The name or the keyword must be set when adding links manually
			if ( empty($link_name) && empty($link_keywords) ){
				$message = __("You must enter a name or at least one keyword for the new link", 'eclipse-link-cloaker');
				$msgclass = 'error';
			} 
			else if	( 
				( ( $check = $this->is_valid_name($link_name) ) === true ) &&
				( ( $check = $this->is_valid_url($link_url) ) === true ) ) 
			{
				//All good, save the link
				if ( $this->save_link($link_url, $link_name, $link_keywords, $max_autolinked_keywords ) != false ){
					$message = __("Link added", 'eclipse-link-cloaker');
					$msgclass = 'updated';
					$link_name = $link_url = $link_keywords = '';
					$max_autolinked_keywords = 0;
				} else {
					$message = sprintf(__("An unexpected error occured while adding the link to the database : %s", 'eclipse-link-cloaker'), $wpdb->last_error);
					$msgclass = 'error';
				}
			} else {
				//The link failed validation. 
				$message = $check;
				$msgclass = 'error';
			}
			
		} else if ( $action == 'delete-cloaked-link' ){
			
			$link_id = $_GET['id'];
			check_admin_referer('delete-link_'.$link_id) or die("Your request failed the security check");
			
			if ( $this->delete_link( $link_id ) ){
				wp_redirect( add_query_arg( array('message' => 1, 'updated' => 1), $base_url ) );
			} else {
				wp_redirect( add_query_arg( array('message' => 2, 'error' => 1), $base_url ) );
			}
			die();
			
		} else if ( $action == 'import-static-links' ){
			check_admin_referer('import-static-links') or die("Your request failed the security check");
			
			$imported_links = $this->import_cloaked_links();
			
			if ( $imported_links !== false ){
				$message = sprintf( __('Successfully imported %d cloaked links.', 'eclipse-link-cloaker'), $imported_links );
				$msgclass = 'updated';
			} else {
				$message = __('Link import failed.', 'eclipse-link-cloaker');
				if ( $wpdb->last_error ){
					$message .= ' ' . sprintf(__('Database error : %s', 'eclipse-link-cloaker'), $wpdb->last_error);
				}
				$msgclass = 'error';
			}
		} else if ( $action == 'bulk-delete' ){
			check_admin_referer('bulk-action') or die("Your request failed the security check");
			
			$selected_links = array();
			if ( !empty($_POST['links']) && is_array($_POST['links']) ){
				$selected_links = array_map('intval', $_POST['links']); //Converts non-numeric stuff to zeroes
				$selected_links = array_filter($selected_links);		//Eliminates zeroes
			}
			
			if ( count($selected_links) > 0 ){
				$deleted = $this->delete_link($selected_links);
				wp_redirect( add_query_arg( array('deleted' => $deleted, 'ids' => implode(',', $selected_links)), $base_url ) );
				die();
			}
		}
		
		//If a request with 'noheader' got this far without being handled, it is probably
		//unknown or invalid. So we ignore it and redirect back to the link listing. 
		if ( !empty($_GET['noheader']) ){
			wp_redirect($base_url);
			die();
		}
		
		//Available filters by link type + the appropriate WHERE expressions
		$filters = array(
			'all' => array(
				'where_expr' => '1',
				'name' => __('All', 'eclipse-link-cloaker'),
				'heading' => __('Cloaked Links', 'eclipse-link-cloaker'),
				'heading_zero' => __('No links found', 'eclipse-link-cloaker'),
			 ), 
			 'named' => array(
				'where_expr' => 'name IS NOT NULL',
				'name' => __('Named', 'eclipse-link-cloaker'),
				'heading' => __('Cloaked Links', 'eclipse-link-cloaker'),
				'heading_zero' => __('Cloaked Links', 'eclipse-link-cloaker'),
			 ), 
			 
			'unnamed' => array(
				'where_expr' => 'name IS NULL',
				'name' => __('Unnamed', 'eclipse-link-cloaker'),
				'heading' => __('Cloaked Links', 'eclipse-link-cloaker'),
				'heading_zero' => __('Cloaked Links', 'eclipse-link-cloaker'),
			 ), 
			 
			 'with_keywords' => array(
			 	'where_expr' => 'keyword IS NOT NULL AND keyword <> ""',
				'name' => __('With keywords', 'eclipse-link-cloaker'),
				'heading' => __('Cloaked Links', 'eclipse-link-cloaker'),
				'heading_zero' => __('Cloaked Links', 'eclipse-link-cloaker'),
			 ),
		);	
		
		$link_type = isset($_GET['link_type'])?$_GET['link_type']:'all';
		if ( !isset($filters[$link_type]) ){
			$link_type = 'all';
		}
		
		//Construct the WHERE expression for the search query (if present)
		$search_expr = '';
		if ( !empty($_GET['s']) ){
			$search_query = like_escape($wpdb->escape($_GET['s']));
			$search_query = str_replace('*', '%', $search_query);
			$search_expr = 'AND 
				( 
					(url LIKE "%'.$search_query.'%") OR 
					(keyword LIKE "%'.$search_query.'%") OR
					(name LIKE "%'.$search_query.'%")
				)';
		}
		
		//Get the desired page number (must be > 0) 
		$page = isset($_GET['paged'])?intval($_GET['paged']):1;
		if ($page < 1) $page = 1;
		
		//Links per page
		$per_page = $this->options['links_per_page'];
		
		//Define table columns and how they can be sorted
		$columns = array( 
			'name' => array(
				'caption' => __('Name', 'eclipse-link-cloaker'),
				'sort_title' => __('Click to sort by this column', 'eclipse-link-cloaker'),
				'order_by' => 'name %s, url ASC',
				'url' => '',
				'icon' => '',
			  ), 
			'url' => array(
				'caption' => __('Destination URL', 'eclipse-link-cloaker'),
				'sort_title' => __('Click to sort by this column', 'eclipse-link-cloaker'),
				'order_by' => 'url %s',
				'url' => '',
				'icon' => '',
			  ),
			'keywords' => array(
				'caption' => __('Keywords', 'eclipse-link-cloaker'),
				'sort_title' => __('Click to sort by this column', 'eclipse-link-cloaker'),
				'order_by' => 'keyword %s, url ASC',
				'url' => '',
				'icon' => '',
			  ), 
			'hits' => array(
				'caption' => __('Hits', 'eclipse-link-cloaker'),
				'sort_title' => __('Click to sort by this column', 'eclipse-link-cloaker'),
				'order_by' => 'hits %s, name ASC, url ASC',
				'url' => '',
				'icon' => '',
			  ),
			'conversions' => array(
				'caption' => __('Conv.', 'eclipse-link-cloaker'),
				'sort_title' => __('Click to sort by this column', 'eclipse-link-cloaker'),
				'order_by' => 'conversions %s, name ASC, url ASC',
				'url' => '',
				'icon' => '',
			  ),
		);
		
		//Define the valid sort directions
		$sort_directions = array( 'asc', 'desc' );
		
		if ( isset($_GET['sort_direction']) && in_array( $_GET['sort_direction'], $sort_directions ) ){
			$sort_direction = $_GET['sort_direction'];
		} else {
			$sort_direction = 'asc';
		}
		
		//Get the desired sort column (default is by hits (descending) then by name (ascending))
		if ( isset($_GET['sort_column']) && isset( $columns[ $_GET['sort_column'] ] ) ){
			$sort_column = $_GET['sort_column'];
		} else {
			$sort_column = 'hits';
			$sort_direction = 'desc';
		}
		
		//Construct the ODER BY clause
		$order_by = sprintf("ORDER BY {$columns[$sort_column]['order_by']}", $sort_direction);
		
		//Construct the links for sorting results in other ways
		$sort_links = array();
		$icon_path = WP_PLUGIN_URL . '/' . basename(dirname(__FILE__)) . '/images/';
		foreach ( $columns as $column_name => $column_info ){
			
			$p = array(
				'sort_column' => $column_name, 
				'sort_direction' => 'asc',
			);
			$icon = '';
			
			//special case : when sorting by hits, sort the links in a descending order by default
			if ( 'hits' == $column_name ){
				$p['sort_direction'] = 'desc';
			}
			//If already sorted by this column, reverse the order
			if ( $sort_column == $column_name ) {
				$p['sort_direction'] = ( 'asc' == $sort_direction ?'desc':'asc' );
				$icon = ( 'asc' == $sort_direction ?'down.png':'up.png' );
			}
			if ( !empty($icon) ){
				$icon = '<img src="' . $icon_path . $icon . '" class="lcp-sort-icon" />';
			}
			
			$url = add_query_arg( $p, $base_url );
			
			$columns[$column_name]['url'] = $url;
			$columns[$column_name]['icon'] = $icon;
		}
		
		//Calculate the number of various links (Note : this doesn't take the search query into account)
		foreach ($filters as $filter => $data){
			$filters[$filter]['count'] = $wpdb->get_var( 
				"SELECT COUNT(*)
				FROM 
					{$wpdb->prefix}lcp_cloaked_links links LEFT JOIN {$wpdb->prefix}lcp_keywords keywords
						ON links.id = keywords.link_id
				WHERE ".$data['where_expr'] );	
		}
		$current_filter = $filters[$link_type];
		
		//Show the "Add New Link" form if this page has no links
		$add_link_form = $add_link_form || ($current_filter['count'] == 0);
		
		//Check if we need to show the "Import links from the old version of this plugin" form
		$old_link_count = 0;
		if ( $filters['named']['count'] == 0 ){
			$oldtable = $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}cloaked_links'" );
			if ( !empty($oldtable) ){
				$old_link_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}cloaked_links" );
			}			
		}
		
		//Fetch the links that we will display
		$q = "SELECT SQL_CALC_FOUND_ROWS
				links.*, keywords.keyword, keywords.max_links, keywords.case_sensitive
		 
			  FROM 
			  	{$wpdb->prefix}lcp_cloaked_links links LEFT JOIN {$wpdb->prefix}lcp_keywords keywords 
				ON links.id = keywords.link_id  
			  	
			  WHERE 
			  	{$current_filter['where_expr']}
			  	$search_expr
			  	
			  $order_by
			  
			  LIMIT ".( ($page-1) * $per_page ).", $per_page";
			  
		$links = $wpdb->get_results($q, ARRAY_A);
		
		//Get the number of found links
		$total_results = intval( $wpdb->get_var("SELECT FOUND_ROWS()") );
		$max_pages = ceil( $total_results / $per_page );
		
		if ( $wpdb->last_error ){
			$message .= '<p>'. sprintf(__('Unable to list cloaked links. Database error : %s', 'eclipse-link-cloaker'), $wpdb->last_error) . '</p>';
			$msgclass = 'error';
		}
		?>
<script type='text/javascript'>
	var lcp_current_filter = '<?php echo esc_js($link_type); ?>';
	var lcp_update_link_nonce = '<?php echo esc_js(wp_create_nonce('lcp_update_link')); ?>';
	var lcp_show_stats_nonce = '<?php echo esc_js(wp_create_nonce('lcp_show_stats')); ?>';
	var lcp_optional_field_text = '<?php _e('(optional)', 'eclipse-link-cloaker'); ?>';
	var lcp_bulk_delete_warning = '<?php echo esc_js(__("You are about to delete the selected links.\n'Cancel' to stop, 'OK' to delete.", 'eclipse-link-cloaker')); ?>';
</script>
<div class="wrap">
<h2><?php
	//Output a header matching the current filter
	if ( $current_filter['count'] > 0 ){
		echo $current_filter['heading'];
	} else {
		echo $current_filter['heading_zero'];
	}
	
?><a href="#" class="button add-new-h2" id="lcp-add-new"><?php _e('Add New', 'eclipse-link-cloaker'); ?></a>
<?php
if ( !empty($_GET['s']) )
	printf( '<span class="subtitle">' . __('Search results for &#8220;%s&#8221;', 'eclipse-link-cloaker') . '</span>', htmlentities( $_GET['s'] ) ); ?>
</h2>

<?php

	//Display an (error) message, if any
	if ( !empty($message) ){
		echo '<div id="message" class="', $msgclass ,' fade"><p>',$message,'</p></div>';
	}

	//Display a form for adding links
	if (current_user_can('manage_links')) {
	
	/*
	The form should be hidden by default. It should be shown when 
		* The user clicks the "Add New" button;
		* There are no cloaked links;
		* The user has just added a link (they might want to add another one)
		* There was an error saving a link. In this case, the form should also contain 
		  the values previously entered by the user. 
	*/ 
?>
<form method="POST" name="form-add-cloaked-link" id="form-add-cloaked-link"
	class="stuffbox lcp-form <?php if ( !$add_link_form ) echo ' hidden'; ?>">
<?php
	wp_nonce_field('add-cloaked-link');
?>
<input type="hidden" name="action" id="action" value="add-cloaked-link" />
<table width="100%"><tbody>

	<tr class="inline-edit-row"><td colspan="2">
	<div>
	<fieldset>
	
	<h4><?php _e('Add Cloaked Link', 'eclipse-link-cloaker'); ?></h4>
	
	<label>
		<span class="title lcp-title"><?php _e('Name', 'eclipse-link-cloaker'); ?></span>
		<span class="input-text-wrap"><input type="text" name="link_name" class="ptitle" value="<?php
			echo esc_attr($link_name);
		?>" /></span>
	</label>
	
	<label>
		<span class="title lcp-title"><?php _e('URL', 'eclipse-link-cloaker'); ?></span>
		<span class="input-text-wrap"><input type="text" name="link_url" value="<?php
			echo esc_attr($link_url);
		?>" /></span>
	</label>
	
	<label>
		<span class="title lcp-title"><?php _e('Keywords', 'eclipse-link-cloaker'); ?></span>
		<span class="input-text-wrap"><input type="text" name="link_keywords" class="lcp-optional" value="<?php
			echo esc_attr($link_keywords);
		?>" /></span>
	</label>
	
	<label>
		<span class="title lcp-title"><?php _e('Max. links', 'eclipse-link-cloaker'); ?></span>
		<span class="input-text-wrap"><input type="text" name="max_links" class="lcp-optional" value="<?php
			if( !empty($max_autolinked_keywords) ) echo esc_attr($max_autolinked_keywords);
		?>" /></span>
	</label>
	
	</fieldset>
	
	<p class="submit">
		<a accesskey="c" href="#" title="<?php echo esc_attr(__('Cancel', 'eclipse-link-cloaker')); ?>" class="button-secondary cancel alignleft"><?php _e('Cancel', 'eclipse-link-cloaker'); ?></a>
		<a accesskey="s" href="#" title="<?php echo esc_attr(__('Save', 'eclipse-link-cloaker')); ?>" class="button-primary save alignright"><?php _e('Add Link', 'eclipse-link-cloaker'); ?></a>
		<br class="clear" />
	</p>
	</div>
	</td></tr>
</tbody></table></form>

<?php
	} //Form for adding links
	
	if ( ( $current_filter['count'] == 0 ) && ( $old_link_count > 0 ) ) {
		//Offer the option to import the "static" cloaked links from the old Link Cloaking Plugin
?>
<form method="POST" name="form-import-static-links" id="form-import-static-links"
	class="stuffbox">
<?php
	wp_nonce_field('import-static-links');
?>
<input type="hidden" name="action" id="action" value="import-static-links" />
<table width="100%"><tbody>

	<tr class="inline-edit-row"><td colspan="2">
	<div>
	<fieldset>
	
	<h4><?php _e('Import Cloaked Links', 'eclipse-link-cloaker'); ?></h4>
	
	<span class="title lcp-title">
	<?php 
		printf(
			_n(
				'The plugin has detected that your database contains %d "static" cloaked link created by the Link Cloaking Plugin. You can import them into the Eclipse Link Cloaker database by clicking the button below.', 
				'The plugin has detected that your database contains %d "static" cloaked links created by the Link Cloaking Plugin. You can import them into the Eclipse Link Cloaker database by clicking the button below.',
				$old_link_count,
				'eclipse-link-cloaker'
			),
			$old_link_count
		);
	?>	 
	</span>
	
	<p class="submit" style="text-align:center;">
		<a href="#" title="<?php echo esc_attr(__('Import Cloaked Links', 'eclipse-link-cloaker')); ?>" class="button-secondary save"><?php _e('Import Links', 'eclipse-link-cloaker'); ?></a>
		<br class="clear" />
	</p>
	</div>
	</td></tr>
</tbody></table></form>
	
<?php
	}
?>
	<ul class="subsubsub">
    	<?php
    		//Construct a submenu of filter types
    		$items = array();
			foreach ($filters as $filter => $data){
				$class = $number_class = '';
				
				if ( $link_type == $filter ) $class = 'class="current"';
				
				$items[] = "<li><a href='tools.php?page=eclipse_cloaked_links&link_type=$filter' $class>
					{$data['name']}</a> <span class='count'>({$data['count']})</span>";
			}
			echo implode(' |</li>', $items);
			unset($items);
		?>
	</ul>
	
	<form name="form-search-links" id="form-search-links" method="get" action="<?php 
		echo esc_attr( admin_url('tools.php?page=eclipse_cloaked_links') ); 
	?>">
	<?php
		//Add hidden fields for the parameters that should be passed along with the search query.
		//Seems fragile, but presently I can't think of a better way to do it.
		$important_params = array('page', 'link_type', 'per_page', 'sort_direction', 'sort_column');
		foreach($important_params as $param_name){
			if ( isset($_GET[$param_name]) ){
				printf('<input type="hidden" name="%s" value="%s" />', esc_attr($param_name), esc_attr($_GET[$param_name]));
			}
		}		
	?>
	<p class="search-box">
		<label class="screen-reader-text" for="lcp-link-search-input"><?php _e('Search Links:', 'eclipse-link-cloaker'); ?></label>
		<input type="text" id="lcp-link-search-input" name="s" value="<?php
			if ( !empty($_GET['s']) ) echo htmlentities($_GET['s']); 
		?>" />
		<input type="submit" value="<?php _e('Search Links', 'eclipse-link-cloaker'); ?>" class="button" />
	</p>
	</form>

<form action="<?php echo esc_url( add_query_arg('noheader', 1, $base_url) ); ?>" method="post" id="elc-bulk-action-form">
	<?php 
	wp_nonce_field('bulk-action');
	
	if ( count($links) > 0 ) { 
	?>

	<div class='tablenav'>
		<div class="alignleft actions">
			<select name="action" id="bulk-action">
				<option value="-1" selected="selected"><?php _e('Bulk Actions'); ?></option>
				<option value="bulk-delete"><?php _e('Delete', 'eclipse-link-cloaker'); ?></option>
			</select>
			<input type="submit" name="doaction" id="doaction" value="<?php echo esc_attr(__('Apply')); ?>" class="button-secondary action" />
		</div>
		<?php
			//Display pagination links 
			$page_links = paginate_links( array(
				'base' => add_query_arg( 'paged', '%#%', $base_url ),
				'format' => '',
				'prev_text' => __('&laquo;', 'eclipse-link-cloaker'),
				'next_text' => __('&raquo;', 'eclipse-link-cloaker'),
				'total' => $max_pages,
				'current' => $page
			));
			
			if ( $page_links ) { 
				echo '<div class="tablenav-pages">';
				$page_links_text = sprintf( '<span class="displaying-num">' . __( 'Displaying %s&#8211;%s of %s', 'eclipse-link-cloaker' ) . '</span>%s',
					number_format_i18n( ( $page - 1 ) * $per_page + 1 ),
					number_format_i18n( min( $page * $per_page, $total_results ) ),
					number_format_i18n( $total_results ),
					$page_links
				); 
				echo $page_links_text; 
				echo '</div>';
			}
		?>
	
	</div>
		
<table class="widefat" id='wplc_links'>
	<thead>
	<tr>
		<th scope="col" id="cb" class="column-cb check-column">
			<input type="checkbox" />
		</th>
	<?php
		$visible_columns = array('name', 'url', 'keywords', 'hits');
		if ( $this->options['enable_conversion_tracking'] ){
			$visible_columns[] = 'conversions';
		}
		
		foreach($visible_columns as $column_name){
			printf(
				'<th scope="col" class="column-%s"><a href="%s" title="%s">%s</a>%s</th>',
				$column_name,
				$columns[$column_name]['url'],
				$columns[$column_name]['sort_title'],
				$columns[$column_name]['caption'],
				$columns[$column_name]['icon']
			);
		}
	?>
	
	</tr>
	</thead>
	<tbody id="the-list">		
<?php
		$rowclass = ''; 
        foreach ($links as $link) {
            $rowclass = 'alternate' == $rowclass ? '' : 'alternate';
            $this->output_link_row( $link, $rowclass, $base_url );
        }
?>
	</tbody>
</table>

	<div class='tablenav'>
		<div class="alignleft actions">
			<select name="action2" id="bulk-action2">
				<option value="-1" selected="selected"><?php _e('Bulk Actions'); ?></option>
				<option value="bulk-delete"><?php _e('Delete', 'eclipse-link-cloaker'); ?></option>
			</select>
			<input type="submit" name="doaction2" id="doaction2" value="<?php echo esc_attr(__('Apply')); ?>" class="button-secondary action" />
		</div>
	
<?php 
		//Also display pagination links at the bottom
        if ( $page_links ) { 
			echo '<div class="tablenav-pages">';
			$page_links_text = sprintf( '<span class="displaying-num">' . __( 'Displaying %s&#8211;%s of %s', 'eclipse-link-cloaker' ) . '</span>%s',
				number_format_i18n( ( $page - 1 ) * $per_page + 1 ),
				number_format_i18n( min( $page * $per_page, $total_results ) ),
				number_format_i18n( $total_results ),
				$page_links
			); 
			echo $page_links_text; 
			echo '</div>';
		}
	}
?>
	</div>

</form>

<form method="get" action=""><table style="display: none" width='50%'><tbody id="lcp-inlineedit">

	<tr id="lcp-inline-edit" class="inline-edit-row quick-edit-row" style="display: none">
	<td></td>
	<td class="editor-container-cell">
	<div style='max-width:540px;'>
	<fieldset>
		
	<h4><?php _e('Edit Link', 'eclipse-link-cloaker'); ?></h4>
	
	<label>
		<span class="title"><?php _e('Name', 'eclipse-link-cloaker'); ?></span>
		<span class="input-text-wrap"><input type="text" name="link_name" class="ptitle" value="" /></span>
	</label>
	
	<label>
			<span class="title"><?php _e('URL', 'eclipse-link-cloaker'); ?></span>
			<span class="input-text-wrap"><input type="text" name="link_url" value="" /></span>
	</label>
	
	<label>
			<span class="title"><?php _e('Keywords', 'eclipse-link-cloaker'); ?></span>
			<span class="input-text-wrap"><input type="text" name="link_keywords" class="lcp-optional" value="" /></span>
	</label>
	
	<label>
			<span class="title"><?php _e('Max. links', 'eclipse-link-cloaker'); ?></span>
			<span class="input-text-wrap"><input type="text" name="max_links" class="lcp-optional" value="" /></span>
	</label>
	
	</fieldset>
	
	<p class="submit inline-edit-save">
		<a accesskey="c" href="#lcp-inline-edit" title="<?php echo esc_attr(__('Cancel', 'eclipse-link-cloaker')); ?>" class="button-secondary cancel alignleft"><?php 
			_e('Cancel', 'eclipse-link-cloaker'); 
		?></a>
		<a accesskey="s" href="#lcp-inline-edit" title="<?php echo esc_attr(__('Update', 'eclipse-link-cloaker')); ?>" class="button-primary save alignright"><?php 
			_e('Update Link', 'eclipse-link-cloaker'); 
		?></a>
			<img class="waiting" style="display:none;" src="images/wpspin_light.gif" alt="" />
		<br class="clear" />
	</p>
	</div>
	</td></tr>
	
</table></form>

</div>
		<?php
	}
	
	/**
	 * Generate the contents of the "Screen Options" panel on the "Cloaked Links" page.
	 * 
	 * @return string
	 */
	function screen_options(){
		//Links per page [1 - 500]
		$new_links_per_page = isset($_GET['per_page'])?$_GET['per_page']:(isset($_POST['elc_per_page'])?$_POST['elc_per_page']:0); 
		if ( $new_links_per_page ){
			$this->options['links_per_page'] = min(max(intval($new_links_per_page), 1), 500);
			$this->save_options();
		}
		
		$panel = '<h5>%s</h5>
			<div class="screen-options">
				<input type="text" class="screen-per-page" name="elc_per_page" id="elc_per_page" value="%d" />
				<label for="elc_per_page">%s</label>
				<input type="submit" class="button" value="%s">
			</div>';
		$panel = sprintf(
			$panel,
			__('Show on screen'),
			$this->options['links_per_page'],
			__('links', 'eclipse-link-cloaker'),
			__('Apply')
		);			
		
		return $panel;
	}
	
	function ajax_update_link(){
		global $wpdb;
		
		if ( !current_user_can('manage_links')) {
			_e("You don't have sufficient permissions to edit links!", 'eclipse-link-cloaker');
			die();
		}
		
		check_ajax_referer('lcp_update_link') or die("Your request failed the security check!");
		
		if ( empty($_POST['link_id']) || ( intval($_POST['link_id']) <= 0 ) ){
			_e("Link ID not specified or invalid.", 'eclipse-link-cloaker');
			die();
		}
		$id = intval($_POST['link_id']);
		
		$name = isset($_POST['link_name'])?trim($_POST['link_name']):'';
		$url = isset($_POST['link_url'])?$_POST['link_url']:'';
		
		$link_keywords = isset($_POST['link_keywords'])?$_POST['link_keywords']:'';
		//Remove empty keywords and trim whitespace
		$link_keywords = array_filter( array_map('trim', explode(",", $link_keywords) ));
		$link_keywords = implode(",", $link_keywords);
		
		$max_autolinked_keywords = isset($_POST['max_links'])?intval($_POST['max_links']):0;
		if ( $max_autolinked_keywords < 0 ) $max_autolinked_keywords = 0;
		
		//Validate the URL
		if ( ( $msg = $this->is_valid_url($url) ) !== true ){
			die($msg);
		}
		
		//Validate the name
		if ( ( $msg = $this->is_valid_name($name, $id) ) !== true ){
			die($msg);
		}
		
		//Everything checks out, save the link.
		$q = "UPDATE {$wpdb->prefix}lcp_cloaked_links SET url = '".$wpdb->escape($url)."', ";
		//If the name is blank then set it to NULL
		if (empty($name)) { 
			$q .= "name = NULL ";			
		} else {
			$q .= "name = '" . $wpdb->escape($name) . "' ";
		}
		$q .= "WHERE id = $id";
		
		if ( $wpdb->query( $q ) !== false ){
			
			//Update or remove link keywords
			if ( !empty($link_keywords) ){
				$q = $wpdb->prepare(
					"REPLACE INTO {$wpdb->prefix}lcp_keywords(link_id, keyword, max_links) VALUES(%d, %s, %d)",
					$id,
					$link_keywords,
					$max_autolinked_keywords
				);
			} else {
				$q = $wpdb->prepare("DELETE FROM {$wpdb->prefix}lcp_keywords WHERE link_id = %d", $id);
			}
			
			if ( $wpdb->query( $q ) !== false ){
				
				//Output the updated link row
				$q = $wpdb->prepare(
					"SELECT links.*, keywords.keyword, keywords.max_links 
					 FROM {$wpdb->prefix}lcp_cloaked_links links LEFT JOIN {$wpdb->prefix}lcp_keywords keywords
					 	ON links.id = keywords.link_id
					 WHERE links.id = %d ", 
					$id
				);
				$this->output_link_row( $wpdb->get_row( $q, ARRAY_A ) );
				
			} else {
				
				printf( __("Database error : %s", 'eclipse-link-cloaker'), $wpdb->last_error);
				
			}					
			
		} else {
			printf( __("Database error : %s", 'eclipse-link-cloaker'), $wpdb->last_error);
		};
				
		die();
	}
	
  /**
   * EclipseLinkCloaker::ajax_show_stats()
   * Output the hit & conversion stats chart for the link specified in $_POST['link_id'].
   * The current implementation uses the Google Charts API to generate the chart.
   *
   * @return void
   */
	function ajax_show_stats(){
		global $wpdb;
		check_ajax_referer('lcp_show_stats') or die("Your request failed the security check!");
		
		if ( empty($_POST['link_id']) || ( intval($_POST['link_id']) <= 0 ) ){
			_e("Link ID not specified or invalid.", 'eclipse-link-cloaker');
			die();
		}
		$id = intval($_POST['link_id']);
		
		//Figure out the time period that will be charted
		$period_start = date('Y-m-d', strtotime('-' . $this->options['chart_period']));
		$period_end = date('Y-m-d');
		$min_time = strtotime($period_start);
		$max_time = strtotime($period_end);
		
		//Fetch the stats for this link
		$q = "SELECT period, hits, conversions
			 FROM {$wpdb->prefix}lcp_stats 
			 WHERE 
			 	link_id = %d AND 
			 	(period BETWEEN %s AND %s)
			 ORDER BY period ASC";
		$q = $wpdb->prepare($q, $id, $period_start, $period_end);
		$rows = $wpdb->get_results($q, ARRAY_A);


		$chart = 'http://chart.apis.google.com/chart?';
		
		$values = array();
		$conversion_values = array();
		$max_value = 0; 
		
		$one_day = 60*60*24;
		$last_time = $min_time - $one_day;
		
		//used to output date labels
		$mondays = array(); 
		$monday_pos = array();
		$cnt = 0;
		
		//We may not have statistics data available for the entire charted period,
		//so we need to fill in the blanks with zeroes. To do so, we first fill the 
		//entire value array with the default value of 0... 
		while( $last_time < $max_time ){
			$last_time += $one_day;
			//echo "Emit ",date("Y-m-d H:i:s", $last_time)," : 0<br>";
			$date_str = date('Y-m-d', $last_time);
			$values[ $date_str ] = 0;
			$conversion_values[ $date_str ] = 0;
			
			//Also populate the label array with Mondays while we're at it. 
			if (date('D', $last_time) == 'Mon'){
				$mondays[] = $date_str;
				$monday_pos[] = $cnt;
			}
			
			$cnt++;
		}
		
		//...then merge in the data from the days that do we have stats for. 
		if (is_array($rows)){
			foreach ($rows as $row){
				//Google API requires all values to be rounded to no more than 4 significant digits 
				$v = $this->siground(intval($row['hits']), 3);
				$values[ $row['period'] ] = $v;
				if ( $v > $max_value ) $max_value = $v;
				
				$v = $this->siground(intval($row['conversions']), 3);
				$conversion_values[ $row['period'] ] = $v;
			}
		}
		
		$axis_max = max( 10, $max_value, round($this->siground( ($max_value)* 1.05, 3)) );
		
		$chart .= 'cht=lc'; //line chart
		$chart .= '&chs=' . $this->options['chart_width'] . 'x' . $this->options['chart_height']; //chart size
		
		$chart .= '&chd=t:' . implode(',', $values); //data; text encoding with scaling
		if ( $this->options['enable_conversion_tracking'] ){
			$chart .= '|' . implode(',', $conversion_values); //also append the conversion data
		}
		$chart .= '&chds=0,'. $axis_max; //data range
		
		$chart .= '&chxt=x,y'; //enable axis labels
		//axis range and value axis labels
		$chart .= '&chxr=0,0,30|1,0,'. $axis_max;
		
		$chart .= '&chxl=0:|' . implode('|', $mondays) . '|'; //Date axis labels
		$chart .= '&chxp=0,' . implode(',', $monday_pos); //Date axis label positions
		
		$chart .= '&chco=ff9900'; //line colors
		if ( $this->options['enable_conversion_tracking'] ){
			$chart .= ',ffa51f'; //line color for conversion stats
		}
		
		$chart .= '&chm=B,ffe566,0,0,0'; //fill the area under the chart (original : ffc269)
		if ( $this->options['enable_conversion_tracking'] ){
			$chart .= '|B,fcc453,1,1,0'; //ffb547, a different fill for the conversion data (original : ffb547)
		}
		
		$chart .= '&chls=1.5,0,0|1.5,0,0'; //line style and thickness
		
		//echo $chart, '<br>';
		printf(
			'<img src="%s" width="%d" height="%d" />', 
			$chart, 
			$this->options['chart_width'], 
			$this->options['chart_height']
		);
		
		die();
	}
	
  /**
   * EclipseLinkCloaker::siground()
   * Round a number to a certain number of significant digits
   *
   * @param float $n
   * @param int $sigdigits
   * @return int
   */
	function siground($n, $sigdigits = 4){
		return round($n, ceil(0 - log10($n)) + $sigdigits);
	}
	
  /**
   * EclipseLinkCloaker::is_valid_url()
   * Simple URL validator. Checks that the URL isn't blank and that it contains at least the protocol
   * (e.g. http://) and host components.
   *
   * @param string $url
   * @return TRUE on sucess, or an error message explaining the problem with the URL.
   */
	function is_valid_url( $url ){
		//"Validate" the URL
		if ( empty($url) || ( !is_string($url) ) ){
			return __("Please enter an URL", 'eclipse-link-cloaker');
		}
		
		$parts = parse_url($url);
		if ( !$parts || empty($parts['host']) || empty($parts['scheme']) ){
			return __('Please provide a valid URL, e.g "http://example.com/" (sans quotes)', 'eclipse-link-cloaker');
		}
		
		return true;
	}
	
  /**
   * EclipseLinkCloaker::is_valid_name()
   * Validates a link name. The name can contain only alphanumeric characters, underscores and dashes, 
   * and may not consist entirely of numbers. The name can be empty (a zero-length string).
   *
   * @param string $name
   * @param int $id (optional) The database ID of the link to be checked 
   * @return TRUE if the name is valid, or an error message otherwise.
   */
	function is_valid_name( $name, $id = 0 ){
		//The name can't be all-numeric
		if ( is_numeric( $name ) ){
			return __("The name must contain at least one letter or underscore!", 'eclipse-link-cloaker');
		}
				
		//Allowed characters - alphanumerics, underscores and dots
		if ( !preg_match( '/^[a-zA-Z0-9_.\-]*$/', $name ) ){
			return __("The name can only contain letters, digits, dots, dashes and underscores.", 'eclipse-link-cloaker');
		}
		
		//No two links can have the same name
		if ( $this->is_name_conflict( $name, $id ) ){
			return sprintf(
				__("There is already another link with the name '%s'! Please enter a unique name.", 'eclipse-link-cloaker'),
				$name
			);
		}
		
		return true;
	}
	
  /**
   * EclipseLinkCloaker::is_name_conflict()
   * Checks if another link alreday has the same user-defined name. Blank names are never treated as a conflict.
   *
   * @param string $new_name The name to check
   * @param integer $current_id (Optional) Ignore the link with this ID
   * @return boolean True is there is a conflict, false otherwise
   */
	function is_name_conflict( $new_name, $current_id = 0){
		global $wpdb;
		
		if ( empty($new_name) ) return false;
		
		$q = $wpdb->prepare("SELECT id FROM {$wpdb->prefix}lcp_cloaked_links WHERE name=%s AND id<>%d LIMIT 1", $new_name, $current_id);
		if ( $row = $wpdb->get_row( $q, ARRAY_A )  ){
			return true;
		} else {
			return false;
		}
	}
	
  /**
   * EclipseLinkCloaker::registered()
   * Checks if the current installation is registered
   *
   * @return bool
   */
	function registered(){
		if ( empty($this->options['key']) ) return false;
		return $this->options['verification']['success'];
	}
	
  /**
   * EclipseLinkCloaker::verify_key()
   * Verifies a license key by contacting the remote keyserver. Returns an associative array
   * that contains these values :
   *	success - boolean, TRUE if verification succeeded
   *	error_code - empty, or an error code in case of failure
   *	error_message - emtpy, or an error message in case of failure 	 
   *
   * @param string $key
   * @return array
   */
	function verify_key( $key ){
		if ( empty($key) ){
			return array (
				'success' => false, 
				'error_code' => 'empty_key', 
				'error_message' => __("You didn't enter a key.", 'eclipse-link-cloaker'),
			);
		}
		
		$params = array(
			'action' => 'verify_key',
			'plugin' => plugin_basename(__FILE__),
			'plugin_slug' => $this->plugin_slug,
			'version' => $this->version,
			'key' => $key,
			'blog_url' => get_option('siteurl'),
		);
		$rez = wp_remote_post( $this->api, array('body' => $params) );
		
		if ( !is_wp_error($rez) && ($rez['response']['code'] == 200) ){
			$response = unserialize($rez['body']);
			if ( $response ){
				return $response;
			} else {
				return array (
					'success' => false, 
					'error_code' => 'network_error', 
					'error_message' => __("Got an invalid response from the keyserver. Please try again later, or contact the plugin's author if this problem persists.", 'eclipse-link-cloaker'),
				);
			}
		} else {
			if ( is_wp_error($rez) ){
				$error_msg = $rez->get_error_message();
			} elseif (isset($rez['response'])) {
				$error_msg = implode(' ', $rez['response']);
			}
			
			return array (
				'success' => false, 
				'error_code' => 'network_error', 
				'error_message' =>
				 	sprintf(
						__("Couldn't contact the keyserver. Please try again later, or contact the plugin's author if this problem persists. (Network error : %s)", 'eclipse-link-cloaker'),
						$error_msg
					),
			);
		}
	}
	
	function registration_nag(){
		if ( isset($_GET['page']) && ( $_GET['page'] == 'eclipse_cloaker_settings' ) ) return;
		
		if ( empty($this->options['key']) ){
			
			echo "<div id='lcp-registration-nag' class='updated fade'><p>";
		 	printf(
			 	__('<strong>Eclipse Link Cloaker is almost ready.</strong> You must <a href="%s">enter your license key</a> to enable link cloaking.', 'eclipse-link-cloaker'),
			 	'options-general.php?page=eclipse_cloaker_settings'
			); 
			echo "</p></div>";
			
		} else {
			
			$messages = array(
				'network_error' => __('Please <a href="%s">try entering your key</a> again.', 'eclipse-link-cloaker'),
				'invalid_key' => __('Your current license key is invalid. Please <a href="%s">enter a valid license key</a> to enable link cloaking.', 'eclipse-link-cloaker'),
				'blocked_key' => __('Your current license key has been blocked. Please <a href="%s">enter a valid license key</a> to enable link cloaking.', 'eclipse-link-cloaker'),
				'other' => __('Please <a href="%s">enter your license key</a> to enable link cloaking.', 'eclipse-link-cloaker'),
			);
			
			if ( isset( $messages[ $this->options['verification']['error_code'] ] ) ) {
				$msg = $messages[ $this->options['verification']['error_code'] ];
			} else {
				$msg = $messages['other'];
			}
			
			$msg = sprintf( $msg, 'options-general.php?page=eclipse_cloaker_settings');
			
			echo "<div id='lcp-registration-nag' class='error fade'><p><strong>";
			_e('Eclipse Link Cloaker has been disabled.','eclipse-link-cloaker');
			echo "</strong> $msg</p></div>";
		}
	}

    function check_for_updates($forced_check = false){
    	if ( !class_exists('WP_Http') ) return; //Fail if not WP 2.7+
    	
		//Check for updates when the last checked version is different from the current one, 
		//or every X seconds.
		$should_check = ( time() - $this->options['update']['last_check'] >= $this->options['update']['check_interval'] ) ||
			 ( $this->version <> $this->options['update']['checked_version'] );
		$should_check = $should_check || $forced_check;
			
		if ( $should_check ) {
			
			$this->options['update']['last_check'] = time();
			$this->options['update']['checked_version'] = $this->version;
			$this->options['update']['update_info'] = array();
			
			$params = array(
				'action' => 'check_updates',
				'plugin' => plugin_basename(__FILE__),
				'plugin_slug' => $this->plugin_slug,
				'version' => $this->version,
				'key' => $this->options['key'],
				'blog_url' => get_option('siteurl'),
			);
			$rez = wp_remote_post( $this->api, array('body' => $params) );
			
			if ( is_wp_error($rez) ){
				//Fail silently if there's an error (the API server is probably down temporarily)
				return; 
			}
			
			if ( $rez['response']['code'] == 200 ){
				$response = unserialize($rez['body']);
				if ( $response ){
					//Is there a new version available?
					if ( $response['success'] ){
						//Yep; save its info for later use.
						$this->options['update']['update_info'] = $response['update_info'];
					}			
					//The API also verifies the license key, so save that response as well.
					$this->options['verification'] = $response['verification'];
				}
			}
			
			$this->save_options();
		}
    }

    function inject_update( $updates ){
        $plugin_name = plugin_basename(__FILE__);
        
        //Is there a valid update available?
        if ( !empty($this->options['update']['update_info']) && ( $this->options['update']['checked_version'] == $this->version ) ){
        	
        	//Ensure our entry exists in the response array
        	if ( !isset( $updates->response[ $plugin_name] ) || !is_object( $updates->response[ $plugin_name] ) ){
				$data = new stdClass();
				$updates->response[ $plugin_name ] = $data; 
			}
			$data = $updates->response[ $plugin_name ]; 
        	$info = $this->options['update']['update_info'];
        	
        	//Transmogrify the update data into the form expected by WP
        	$data->id = $info['id'];
        	$data->slug = $info['slug']; 
        	$data->new_version = $info['new_version'];
        	$data->url = $info['url'];
        	//The download URL must include the license key for verification
        	$data->package = sprintf( $info['package'], $this->options['key'] );
		} else {
			//Remove stale update info from the response array		
			if ( isset( $updates->response[ $plugin_name] ) ){
				unset( $updates->response[ $plugin_name] );
			}
		}
        
        return $updates;
    }
    
    function inject_plugin_information($res, $action, $args){
    	//Check if the request concerns this plugin
		$relevant = ($action == 'plugin_information') && isset($args->slug) && ($args->slug == $this->plugin_slug);
		if ( !$relevant ){
			return $res;
		}
		
		//Construct a "plugin information" object in the format that WP expects
		$res = new stdClass;
		$res->name = __('Eclipse Link Cloaker', 'eclipse-link-cloaker');
		$res->slug = $this->plugin_slug;
		$res->version = $this->version;
		$res->author = '<a href="http://eclipsecloaker.com/">Janis Elsts</a>';
		$res->author_profile = 'http://eclipsecloaker.com/';
		$res->contributors = array('whiteshadow' => 'http://eclipsecloaker.com/');
		$res->homepage = 'http://eclipsecloaker.com/';
		
		$res->requires = '2.8';
		$res->tested = '2.9.2';
		
		$res->sections = array();
		
		//Insert information from the latest available update (if any)
		if ( !empty($this->options['update']['update_info']) && ( $this->options['update']['checked_version'] == $this->version ) ){
			$info = $this->options['update']['update_info'];
       	
        	$res->slug = $info['slug'];
        	$res->version = $info['new_version'];
        	$res->download_link = sprintf( $info['package'], $this->options['key'] );
        	
        	if ( isset($info['tested']) ){
				$res->tested = $info['tested'];
			}
			if ( isset($info['requires']) ){
				$res->requires = $info['requires'];
			}
        	if ( isset($info['last_updated']) ){
				$res->last_updated = $info['last_updated'];
			}
			
			if ( isset($info['sections']) && is_array($info['sections']) ){
				$res->sections= array_merge($res->sections, $info['sections']);
			}
		}
		
		return $res;
	}

  /**
   * EclipseLinkCloaker::upgrade_database()
   * Create or upgrade the plugin's database tables
   *
   * @return void
   */
	function upgrade_database(){
		global $wpdb;
		
		require_once (ABSPATH . 'wp-admin/includes/upgrade.php');
		
		//The main table
		$q = "
			CREATE TABLE IF NOT EXISTS {$wpdb->prefix}lcp_cloaked_links (
			  id int(10) unsigned NOT NULL auto_increment,
			  `name` varchar(100) default NULL,
			  url text character set latin1 collate latin1_general_cs NOT NULL,
			  url_hash char(32) NOT NULL,
			  hits int(10) unsigned NOT NULL default '0',
			  conversions int(10) unsigned NOT NULL default '0',
			  PRIMARY KEY  (id),
			  KEY url_hash (url_hash),
			  KEY `name` (`name`)
			) DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;
		";
		//dbDelta( $q ); //Just can't rely on this function, it doesn't support collations.
		if ( $wpdb->query( $q ) === false ){
			return false;
		};
		
		$q = " 
			ALTER TABLE {$wpdb->prefix}lcp_cloaked_links
				ADD COLUMN conversions int(10) unsigned NOT NULL default '0' 
		";
		$wpdb->query( $q ); //This will fail harmlessly & silently if the column already exists. 
		
		$q = "
			CREATE TABLE {$wpdb->prefix}lcp_stats (
			  link_id int(10) unsigned NOT NULL,
			  period date NOT NULL,
			  hits int(10) unsigned NOT NULL default '0',
			  conversions int(10) unsigned NOT NULL default '0',
			  PRIMARY KEY  (link_id,period),
			  KEY link_id (link_id)
			)
		";
		dbDelta( $q );
		
		$q = "
			CREATE TABLE {$wpdb->prefix}lcp_keywords (
			  keyword varchar(255) COLLATE latin1_general_ci NOT NULL,
			  link_id int(10) unsigned NOT NULL,
			  max_links tinyint(4) NOT NULL DEFAULT '0',
			  case_sensitive tinyint(1) NOT NULL,
			  PRIMARY KEY (link_id)
			);
		";
		dbDelta( $q );
	}
	
  /**
   * EclipseLinkCloaker::import_cloaked_links()
   * Imports the "static" cloaked links from the free Link Cloaking Plugin
   *
   * @return int The number of links imported
   */
	function import_cloaked_links(){
		global $wpdb;
		
		$q = "
			INSERT IGNORE INTO {$wpdb->prefix}lcp_cloaked_links(name, url, url_hash, hits)
			SELECT name, url, md5(url), hits FROM {$wpdb->prefix}cloaked_links WHERE 1
		 ";
		 
		return $wpdb->query( $q );
	}
	
  /**
   * EclipseLinkCloaker::unhtmlentities()
   * Convert all HTML entities to their applicable characters. 
   * This function is more reliable than html_entity_decode() on PHP 4 systems.
   *
   * @param string $string
   * @return string
   */
	function unhtmlentities($string) {
	    // replace numeric entities
	    $string = preg_replace('~&#x([0-9a-f]+);~ei', 'chr(hexdec("\\1"))', $string);
	    $string = preg_replace('~&#([0-9]+);~e', 'chr("\\1")', $string);
	    // replace literal entities
	    $trans_tbl = get_html_translation_table(HTML_ENTITIES, ENT_QUOTES);
	    $trans_tbl = array_flip($trans_tbl);
	    return strtr($string, $trans_tbl);
	}
	
  /**
   * EclipseLinkCloaker::load_admin_css()
   * Load the CSS files used on the plugin's Dashboard pages
   *
   * @return void
   */
	function load_admin_css(){
		echo '<link type="text/css" rel="stylesheet" href="', plugin_dir_url(__FILE__) , 'css/admin.css" />',"\n";
	}
	
  /**
   * EclipseLinkCloaker::load_admin_scripts()
   * Enqueue JS libraries and scripts used on the "Cloaked Links" page
   *
   * @return void
   */
	function load_admin_scripts(){
		//jQuery Example plugin
		wp_enqueue_script('jquery-example', plugin_dir_url(__FILE__) . 'js/jquery.example.js', array('jquery'));
		//Various event handlers for the forms on the "Cloaked Links" page
		wp_enqueue_script('lcp-cloaked-links-js', plugin_dir_url(__FILE__) . 'js/cloaked_links.js', array('jquery'), '1.2');
	}
	
  /**
   * EclipseLinkCloaker::load_admin_options_scripts()
   * Enqueue JS libraries and scripts used on the "Link Cloaker" settings page
   *
   * @return void
   */
	function load_admin_options_scripts(){
		//jQuery Tabs plugin
		wp_enqueue_script('jquery-ui-tabs');
		//Various UI scripts for the forms on the "Link Cloaker" page
		wp_enqueue_script('lcp-cloaked-links-js', plugin_dir_url(__FILE__) . 'js/settings.js', array('jquery'));
	}
	
  /**
   * EclipseLinkCloaker::autolink_keywords()
   * A hook for the_content. Looks for certain user-specified keywords and turns them into links. 
   *
   * @param string $content Post content. May contain HTML, but not shortcodes.
   * @return string Post content with keywords converted into links.
   */
	function autolink_keywords( $content ){
		global $post, $wpdb;
		
		if ( $this->options['autolink_only_single'] && !(is_single() || is_page()) ){
			return $content;
		}
		
		if ( !empty($this->options['autolink_ignore_posts']) ) {
			if ( is_single($this->options['autolink_ignore_posts']) || is_page($this->options['autolink_ignore_posts']) ){
				return $content;
			}
		}
		
		//TODO: Keyword caching
		
		//Fetch all keywords
		$q = "SELECT 
				keywords.keyword, keywords.max_links, keywords.link_id, 
				links.name, links.url
				
			  FROM 
			  	{$wpdb->prefix}lcp_keywords AS keywords JOIN {$wpdb->prefix}lcp_cloaked_links AS links
			  		ON keywords.link_id = links.id";
		$keywords = $wpdb->get_results( $q, ARRAY_A );
		
		if ( empty($keywords) || !is_array($keywords) ){
			return $content;
		}
		
		//Prepare the keyword-replacing regexp template
		$exclusions = array_map( 'preg_quote', $this->options['autolink_exclude_tags'] );
		$exclusions = implode('|', $exclusions);
		$regex = "@
		\b (%keyword%) \b  # This is a placeholder. It will be replaced with the real kw later.
		(?! # Don't match keywords inside...
			(?:
				 [^<]*?[>]                 # tags, e.g. <tag b='kw'>
				 |
				 [^>]*?</(?:$exclusions)> # tag content, e.g. <tag>kw</tag> (doesn't work for nested tags)
			)
		)  
		@xsi";
		
		
		//Prepare the regexps and replacements for all keywords
		foreach( $keywords as $index => $keyword ){
			//Each keyword may have several variants, e.g. "green widget" and "green widgets".
			//Transform the variant list to a format suitable for regexps.
			$variants = implode('|', array_map('preg_quote', explode(',', $keyword['keyword'])));
			$variants = str_replace(' ', '\s', $variants);
			
			//TODO: Allow for per-keyword case sensitivity settings
			$keywords[$index]['regex'] = str_replace('%keyword%', $variants, $regex);
			
			//Generate the replacement link
			$link = array();
			
			if ( $this->options['autolink_cloak'] ){
				$link['href'] = $this->make_cloaked_url( 
					$keyword['name'], 
					empty($keyword['name'])?intval($keyword['link_id']):null 
				); 
			} else {
				$link['href'] = $keyword['url'];
			}
			
			if ( $this->options['autolink_nofollow'] ){
				$link['rel'] = 'nofollow';
			}
			
			if ( $this->options['autolink_new_window'] ){
				$link['target'] = '_blank';
			}
			
			if ( $this->options['autolink_enable_css'] && !empty($this->options['autolink_css_class']) ){
				$link['class'] = $this->options['autolink_css_class'];
			}
			
			//Assemble the link
			$link_html = '<a';
			foreach ( $link as $name => $value ){
				$link_html .= sprintf(' %s="%s"', $name, esc_attr( $value )); 
			}
			$link_html .= '>\1</a>';
			
			$keywords[$index]['replacement'] = $link_html;
			$keywords[$index]['max_links'] = (intval($keyword['max_links']) != 0)?intval($keyword['max_links']):$this->options['autolink_max_links'];
		}
		
		/* 
		Replace keywords with links.
		
		Here's how this works : We repeatedly loop through the entire list of keywords and replace 
		(at most) one occurence of each keyword in the post with a link to the user-specified URL.
		Continue until the max number of links per post is reached, or we run out of eligible 
		keywords.
		
		For example, say we have three keywords - "foo", "bar" and "baz"" - and the post content is 
		something silly like "foo the foo baz for the bar in the foo foo!". After the first pass the 
		first "foo", and "bar" and "baz", will become links. After the second pass the second "foo" 
		will also become a link. And so on. This way we get a nice variety of keywords used, which I 
		think is better than autolinking the max. number of foo's first and potentially leaving other 
		keywords out completely.  	  		
		*/ 
		$replacement_limit = $this->options['autolink_max_total_links'];
		do {
			$did_replace = false;
			
			foreach( $keywords as $index => $keyword ){
				if ( $keyword['max_links'] <= 0 ) continue;
				
				$new_content = preg_replace( $keyword['regex'], $keyword['replacement'], $content, 1 );
				if ( $new_content != $content ){
					$content = $new_content;
					$keywords[$index]['max_links']--;
					$did_replace = true;
					$replacement_limit--;
					
					if ( $replacement_limit == 0 ) break;
				} else {
					$keyword['max_links'] = 0; //There are no more matches for this keywords, so don't try it again.
				}
			}
						
		} while ( ($replacement_limit > 0) && $did_replace );
		
		return $content;
	}
	
	function print_autolink_css(){
		echo '<style type="text/css">',$this->options['autolink_css_rules'],'</style>';
	}
	
  /**
   * EclipseLinkCloaker::record_conversion()
   * Record a conversion. Called when someone hits the pixeltracker's URL.
   *
   * @param int $id Link ID. 
   * @return bool True on success, False on failue.
   */
	function record_conversion($id){
		global $wpdb;
		
		//Check if the passed value is a valid ID
		if ( !is_numeric($id) ) return false; 
		$id = intval($id);
		if ($id <= 0) return false;
		
		//Don't record bots and web crawlers
		if( array_search(strtolower($_SERVER['HTTP_USER_AGENT']), $this->options['bots'])!==false ) {
			return false;
		}
		
		//First update the sum total
		$q = "UPDATE {$wpdb->prefix}lcp_cloaked_links SET conversions = conversions + 1 WHERE id = ".$id;
		$wpdb->query($q);
		
		//Then update the daily stats
		$q = "INSERT INTO {$wpdb->prefix}lcp_stats(link_id, period, conversions) VALUES(%d, %s, 1)
			  ON DUPLICATE KEY UPDATE conversions = conversions + 1";
		$q = $wpdb->prepare( $q, $id, current_time('mysql') );
		
		return $wpdb->query( $q ) !== false;
	}
	
  /**
   * EclipseLinkCloaker::get_pixeltracker_url()
   * Get the appropriate tracking pixel URL based on the current permalink settings. 
   *
   * @return string
   */
	function get_pixeltracker_url(){
		global $wp_rewrite;
		
		$url = get_option('home');
		
		if ( $wp_rewrite->using_permalinks() ) {
			
			//Construct the tracker URL according to permalink settings
			if ( $wp_rewrite->using_mod_rewrite_permalinks() ) {
				$url = $url . '/' . $this->conversion_tracker_param . '/pixel.gif';
			} else if ( $wp_rewrite->using_index_permalinks() ){
				$url = $url . '/index.php/' . $this->conversion_tracker_param . '/pixel.gif';
			}
			
		} else {
			
			//Not using permalinks at all, so pass the params via the query string
			$url .= '?' . $this->conversion_tracker_param . '=1';
		}
		
		return $url;
	}
	
  /**
   * EclipseLinkCloaker::get_pixeltracker_code()
   *
   * @return string
   */
	function get_pixeltracker_code(){
		//TODO: Add HTTPS support to the conversion tracker 
		$code = sprintf('<img src="%s" width="1" height="1">', $this->get_pixeltracker_url());
		return $code;
	}
	
  /**
   * EclipseLinkCloaker::insert_tracker_rules()
   * A filter for rewrite_rules_array. Adds the plugin's custom rewrite rules to the WP's own.
   *
   * @param array $rewrite_rules
   * @return array
   */
	function insert_tracker_rules( $rewrite_rules ){
		$my_rules = array(
			$this->conversion_tracker_param . '/pixel.gif' => 'index.php?' . $this->conversion_tracker_param . '=1',
		);
		$rewrite_rules = $my_rules + $rewrite_rules;
		return $rewrite_rules;
	}
	
  /**
   * EclipseLinkCloaker::insert_query_vars()
   * A filter for query_vars. Registers the plugin's custom query variables. 
   *
   * @param array $vars
   * @return array
   */
	function insert_query_vars( $vars ){
		$vars[] = $this->conversion_tracker_param;
		return $vars;
	}
	
  /**
   * EclipseLinkCloaker::handle_tracker()
   * A hook for parse_request. Checks if the current request is for the conversion tracker URL
   * and outputs the tracking pixel (a trasparent GIF). Also records conversions. 
   *
   * @param object $wp
   * @return void
   */
	function handle_tracker( $wp ){
		
		if ( !empty($wp->query_vars[ $this->conversion_tracker_param ]) ){
			if ( !empty($_COOKIE[$this->conversion_tracker_cookie]) ){
				//Record the conversion and output some debug info in a HTTP header
				if ( $this->record_conversion( $_COOKIE[$this->conversion_tracker_cookie] ) ){
					header("X-ELC-Debug: Conversion recorded");
				} else {
					header("X-ELC-Debug: Conversion not recorded");
				};
				//Clear the cookie so that we don't get a duplicate record if the user refreshes the page
				setcookie($this->conversion_tracker_cookie, '', time() - 60*60*24*30, '/');
			} else {
				header("X-ELC-Debug: No cookie");
			}
			
			//Output the transparent tracking pixel (and ensure it isn't cached). 
			$this->send_nocache_headers();
			header("Content-Type: image/gif"); 
			readfile( dirname(__FILE__) . '/images/pixel.gif');
			die();
		}
	}
	
  /**
   * EclipseLinkCloaker::print_ga_helper()
   * Prints a JavaScript helper function that can be later used to call _trackPageview.
   *
   * @return void
   */
	function print_ga_helper(){
		?>
		<script type="text/javascript">
			function elcTrackPageview(url){
				if ( typeof(window['_trackPageview']) != 'undefined' ){
					pageTracker._trackPageview(url);
				} else if (typeof(window['_gaq']) != 'undefined') {
					_gaq.push(['_trackPageview', url]);
				}
			}
		</script>
		<?php
	}
	
  /**
   * EclipseLinkCloaker::send_nocache_headers()
   * Sends a set of HTTP headers that (strive to) ensure the browser will not 
   * cache the result of the current request.
   *
   * @return void
   */
	function send_nocache_headers(){
		header("Pragma: no-cache"); 
		header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); //Just a random date in the past.
		header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); 
		header("Cache-control: no-store, no-cache, must-revalidate");
	}
	
  /**
   * EclipseLinkCloaker::flush_rewrite_rules()
   * Flush the WP rewrite rules to the database & .htaccess. Used when adding custom rewrite rules.
   *
   * @return void
   */
	function flush_rewrite_rules(){
		global $wp_rewrite;
   		$wp_rewrite->flush_rules();
	}
	
  /**
   * EclipseLinkCloaker::load_language()
   * Load the plugin's textdomain that mnatches the current locale.
   *
   * @return void
   */
	function load_language(){
		load_plugin_textdomain( 'eclipse-link-cloaker', false, basename(dirname(__FILE__)) . '/languages' );
	}

} //end class

} //if class_exists()...

$link_cloaker_pro = new EclipseLinkCloaker();

?>