<?php
/*
Plugin Name: Social
Plugin URI: http://mailchimp.com/social-plugin-for-wordpress/
Description: Broadcast newly published posts and pull in discussions using integrations with Twitter and Facebook. Brought to you by <a href="http://mailchimp.com">MailChimp</a>.
Version: 2.6
Author: Crowd Favorite
Author URI: http://crowdfavorite.com/
*/

if (!class_exists('Social')) { // try to avoid double-loading...

/**
 * Social Core
 *
 * @package Social
 */
final class Social {

	/**
	 * @var  string  URL of the API
	 */
	public static $api_url = 'https://sopresto.mailchimp.com/';

	/**
	 * @var  string  version number
	 */
	public static $version = '2.6';

	/**
	 * @var  string  CRON lock directory.
	 */
	public static $cron_lock_dir = null;

	/**
	 * @var  string  plugins URL
	 */
	public static $plugins_url = '';

	/**
	 * @var  string  plugins file path
	 */
	public static $plugins_path = '';

	/**
	 * @var  bool  loaded by theme?
	 */
	public static $loaded_by_theme = false;

	/**
	 * @var  string  duplicate comment message
	 */
	public static $duplicate_comment_message = 'duplicate comment';

	/**
	 * @var  Social_Log  logger
	 */
	private static $log = null;

	/**
	 * @var  array  default options
	 */
	protected static $options = array(
		'debug' => false,
		'install_date' => 0,
		'installed_version' => 0,
		'broadcast_format' => '{title}: {content} {url}',
		'comment_broadcast_format' => '{content} {url}',
		'system_cron_api_key' => null,
		'fetch_comments' => '1',
		'broadcast_by_default' => '0',
		'use_standard_comments' => '0',
	);

	/**
	 * @var  Social  instance of Social
	 */
	public static $instance = null;

	/**
	 * Loads the instance of Social.
	 *
	 * @static
	 * @return Social
	 */
	public static function instance() {
		if (self::$instance === null) {
			self::$instance = new self;
		}

		return self::$instance;
	}

	/**
	 * Handles the auto loading of classes.
	 *
	 * @static
	 *
	 * @param  string  $class
	 *
	 * @return bool
	 */
	public static function auto_load($class) {
		if (substr($class, 0, 7) == 'Social_' or substr($class, 0, 7) == 'Kohana_') {
			try {
				$file = Social::$plugins_path.'lib/'.str_replace('_', '/', strtolower($class)).'.php';
				$file = apply_filters('social_auto_load_file', $file, $class);
				if (file_exists($file)) {
					require $file;

					return true;
				}

				return false;
			}
			catch (Exception $e) {
				Social::log(sprintf(__('Failed to auto load class %s.', 'social'), $class));
			}
		}

		return true;
	}

	/**
	 * Returns the broadcast format tokens.
	 *
	 * Format:
	 *
	 *     {key} => __('Description', 'social')
	 *
	 * @static
	 * @return array
	 */
	public static function broadcast_tokens() {
		$query = new WP_Query(array(
			'posts_per_page' => 1
		));
		if (count($query->posts) and $post = $query->posts[0]) {
			$url = wp_get_shortlink($post->ID);
			$date = get_date_from_gmt($post->post_date_gmt);
		}
		else {
			$url = home_url('?p=123');
			$date = get_date_from_gmt(current_time('mysql', true));
		}

		$defaults = array(
			'{url}' => sprintf(__('Example: %s', 'social'), $url),
			'{title}' => '',
			'{content}' => '',
			'{date}' => sprintf(__('Example: %s', 'social'), $date),
			'{author}' => '',
		);

		return apply_filters('social_broadcast_tokens', $defaults);
	}

	/**
	 * Returns the comment broadcast format tokens.
	 *
	 * Format:
	 *
	 *     {key} => __('Description', 'social')
	 *
	 * @static
	 * @return mixed
	 */
	public static function comment_broadcast_tokens() {
		$defaults = array(
			'{content}' => '',
			'{url}' => '',
		);
		return apply_filters('social_comment_broadcast_tokens', $defaults);
	}

	/**
	 * Sets or gets an option based on the key defined.
	 *
	 * Get Format:
	 *
	 *     Running Social::option('option_name') will load "social_option_name" using get_option()
	 *
	 * Set Format:
	 *
	 *     Running Social::option('option_name', 'new_value') will update "social_option_name" to "new_value"
	 *     using update_option().
	 *
	 * @static
	 * @param  string  $key     option key
	 * @param  mixed   $value   option value
	 * @return bool|mixed
	 * @uses get_option()
	 * @uses update_option()
	 */
	public static function option($key, $value = null) {
		if ($value === null) {
			$default = null;
			if (isset(Social::$options[$key])) {
				$default = Social::$options[$key];
			}

			return get_option('social_'.$key, $default);
		}

		update_option('social_'.$key, $value);
		return false;
	}

	/**
	 * Add a message to the log.
	 *
	 * @static
	 * @param  string  $message    message to add to the log
	 * @param  array   $args       arguments to pass to the writer
	 * @param  string  $context    context of the log message
	 * @param  bool    $backtrace  show the backtrace
	 * @return void
	 */
	public static function log($message, array $args = null, $context = null, $backtrace = false) {
		Social::$log->write($message, $args, $context, $backtrace);
	}

	/**
	 * Sets the loaded by theme.
	 *
	 * @static
	 * @return void
	 */
	public static function social_loaded_by_theme() {
		self::$loaded_by_theme = true;
	}

	/**
	 * Sets the customer die handler.
	 *
	 * @static
	 * @param  string  $handler
	 * @return array
	 */
	public static function wp_die_handler($handler) {
		return array('Social', 'wp_comment_die_handler');
	}

	/**
	 * Don't actually die for aggregation runs.
	 *
	 * @static
	 * @param  string  $message
	 * @param  string  $title
	 * @param  array   $args
	 * @return mixed
	 */
	public static function wp_comment_die_handler($message, $title, $args) {
		if ($message == __('Duplicate comment detected; it looks as though you&#8217;ve already said that!')) {
			// Keep going
			throw new Exception(Social::$duplicate_comment_message);
		}
	}

	/**
	 * @var  bool  is Social enabled?
	 */
	private $_enabled = null;

	/**
	 * Returns an array of all of the services.
	 *
	 * Format of the data returned:
	 *
	 *     $services = array(
	 *         'twitter' => Social_Service_Twitter,
	 *         'facebook' => Social_Service_Facebook,
	 *         // ... any other services registered
	 *     )
	 *
	 * @return array
	 */
	public function services() {
		return $this->load_services();
	}

	/**
	 * Returns a service by access key.
	 *
	 * Loading a service:
	 *
	 *     $twitter = Social::instance()->service('twitter');
	 *
	 * @param  string  $key    service key
	 * @return mixed Social_Service|Social_Service_Twitter|Social_Service_Facebook|false
	 */
	public function service($key) {
		$services = $this->load_services();
		if (!isset($services[$key])) {
			return false;
		}
		return $services[$key];
	}
	
	/**
	 * Returns a service by comment type.
	 *
	 * Loading a service:
	 *
	 *     $twitter = Social::instance()->service_for_comment_type('social-twitter-rt');
	 *
	 * @param  string  $key    service key
	 * @return mixed  Social_Service|Social_Service_Twitter|Social_Service_Facebook|false
	 */
	public function service_for_comment_type($comment_type) {
		$services = $this->load_services();
		foreach ($services as $service) {
			if (in_array($comment_type, $service->comment_types())) {
				return $service;
			}
		}
		return false;
	}

	/**
	 * Initializes Social.
	 *
	 * @wp-action  init
	 * @return void
	 */
	public function init() {
		// Load the language translations
		if (Social::$loaded_by_theme) {
			$path = trailingslashit(Social::$plugins_path).'lang';
			load_theme_textdomain('social', $path);
		}
		else {
			$plugin_dir = basename(dirname(SOCIAL_FILE)).'/lang';
			load_plugin_textdomain('social', false, $plugin_dir);
		}

		if (version_compare(PHP_VERSION, '5.2.4', '<')) {
			deactivate_plugins(basename(__FILE__)); // Deactivate ourself
			wp_die(__("Sorry, Social requires PHP 5.2.4 or higher. Ask your host how to enable PHP 5 as the default on your servers.", 'social'));
		}

		// Just activated?
		if (!Social::option('install_date')) {
			Social::option('install_date', current_time('timestamp', 1));
			Social::option('system_cron_api_key', wp_generate_password(16, false));
		}

		// Plugins URL
		$url = plugins_url('', SOCIAL_FILE);
		Social::$plugins_url = trailingslashit(apply_filters('social_plugins_url', $url));

		Social::$plugins_path = trailingslashit(apply_filters('social_plugins_path', SOCIAL_PATH));

		// Set the logger
		Social::$log = Social_Log::factory();

		// Require Facebook and Twitter by default.
		require Social::$plugins_path.'social-twitter.php';
		require Social::$plugins_path.'social-facebook.php';
	}

	/**
	 * Auth Cookie expiration for API users.
	 *
	 * @return int
	 */
	public function auth_cookie_expiration() {
		return 31536000; // 1 Year
	}

	/**
	 * Enqueues the assets for Social.
	 *
	 * @wp-action  wp_enqueue_scripts
	 * @wp-action  load-post-new.php
	 * @wp-action  load-post.php
	 * @wp-action  load-profile.php
	 * @wp-action  load-settings_page_social
	 * @return void
	 */
	public function enqueue_assets() {
		if (Social::option('use_standard_comments') == '1') {
			return;
		}
		// JS/CSS
		if (!defined('SOCIAL_COMMENTS_JS')) {
			define('SOCIAL_COMMENTS_JS', Social::$plugins_url.'assets/social.js');
		}
		if (SOCIAL_COMMENTS_JS !== false) {
			wp_enqueue_script('jquery');
			wp_enqueue_script('social_js', SOCIAL_COMMENTS_JS, array('jquery'), Social::$version, true);
			wp_localize_script('social_js', 'Sociali18n', array(
				'commentReplyTitle' => __('Post a Reply', 'social'),
			));
		}

		if (!is_admin()) {
			if (!defined('SOCIAL_COMMENTS_CSS')) {
				define('SOCIAL_COMMENTS_CSS', Social::$plugins_url.'assets/comments.css');
			}
			if (SOCIAL_COMMENTS_CSS !== false) {
				wp_enqueue_style('social_comments', SOCIAL_COMMENTS_CSS, array(), Social::$version, 'screen');
			}
		}

	}

	/**
	 * Enqueues the assets for Social.
	 *
	 * @wp-action  admin_enqueue_scripts
	 * @return void
	 */
	public function admin_enqueue_assets() {
		if (!defined('SOCIAL_ADMIN_JS')) {
			define('SOCIAL_ADMIN_JS', Social::$plugins_url.'assets/admin.js');
		}

		if (!defined('SOCIAL_ADMIN_CSS')) {
			define('SOCIAL_ADMIN_CSS', Social::$plugins_url.'assets/admin.css');
		}

		if (SOCIAL_ADMIN_CSS !== false) {
			wp_enqueue_style('social_admin', SOCIAL_ADMIN_CSS, array(), Social::$version, 'screen');
		}

		if (SOCIAL_ADMIN_JS !== false) {
			wp_enqueue_script('social_admin', SOCIAL_ADMIN_JS, array(), Social::$version, true);
			$data = apply_filters('social_admin_js_strings', array(
				'protectedTweet' => __('Protected Tweet', 'social'),
				'invalidUrl' => __('Invalid URL', 'social'),
			));
			wp_localize_script('social_admin', 'socialAdminL10n', $data);
		}
	}

	/**
	 * Loads the services on every page if the user is an admin.
	 *
	 * @wp-action  admin_init
	 * @return void
	 */
	public function admin_init() {
		if (current_user_can('manage_options') or current_user_can('publish_posts')) {
			// Trigger upgrade?
			if (isset($_GET['page']) and $_GET['page'] == basename(SOCIAL_FILE)) {
				global $wpdb;

				// First check for the semaphore options, they need to be added before the upgrade starts.
				$results = $wpdb->get_results("
					SELECT option_id
					  FROM $wpdb->options
					 WHERE option_name IN ('social_locked', 'social_unlocked')
				");
				if (!count($results)) {
					update_option('social_unlocked', '1');
					update_option('social_last_lock_time', current_time('mysql', 1));
					update_option('social_semaphore', '0');
				}

				if (version_compare(Social::option('installed_version'), Social::$version, '<')) {
					$this->_enabled = false;
					$this->upgrade();
				}
			}

			if ($this->_enabled === null) {
				$this->load_services();
			}
		}

		// Redirect to the home_url() if the user is a commenter.
		if (!current_user_can('publish_posts')) {
			$commenter = get_user_meta(get_current_user_id(), 'social_commenter', true);
			if (!empty($commenter) and $commenter == 'true') {
				wp_redirect(trailingslashit(home_url()));
			}
		}
	}

	/**
	 * Checks to see if system crons are disabled.
	 *
	 * @wp-action  load-settings_page_social
	 * @return void
	 */
	public function check_system_cron() {
		Social::log('Checking system CRON');
		// Schedule CRONs
		if (Social::option('fetch_comments') == '1') {
			if (wp_next_scheduled('social_cron_15_init') === false) {
				Social::log('Adding Social 15 CRON schedule');
				wp_schedule_event(time() + 900, 'every15min', 'social_cron_15_init');
			}
			wp_remote_get(
				admin_url('options_general.php?'.http_build_query(array(
					'social_controller' => 'cron',
					'social_action' => 'check_crons',
					'social_api_key' => Social::option('system_cron_api_key')
				), null, '&')),
				array(
					'timeout' => 0.01,
					'blocking' => false,
					'sslverify' => apply_filters('https_local_ssl_verify', true),
				)
			);
		}
	}

	/**
	 * Handlers requests.
	 *
	 * @wp-action  init
	 * @return void
	 */
	public function request_handler() {
		if (isset($_GET['social_controller'])) {
			Social_Request::factory()->execute();
		}
	}

	/**
	 * Adds a link to the "Settings" menu in WP-Admin.
	 *
	 * @wp-action  admin_menu
	 * @return void
	 */
	public function admin_menu() {
		add_options_page(
			__('Social Options', 'social'),
			__('Social', 'social'),
			'manage_options',
			basename(SOCIAL_FILE),
			array(
				$this,
				'admin_options_form'
			)
		);
	}

	/**
	 * Add Settings link to plugins - code from GD Star Ratings
	 *
	 * @wp-filter  plugin_action_links
	 * @param  array   $links
	 * @param  string  $file
	 * @return array
	 */
	public function add_settings_link($links, $file) {
		static $this_plugin;
		if (!$this_plugin) {
			$this_plugin = plugin_basename(__FILE__);
		}

		if ($file == $this_plugin) {
			$settings_link = '<a href="'.esc_url(admin_url('options-general.php?page=social.php')).'">'.__('Settings', 'social').'</a>';
			array_unshift($links, $settings_link);
		}
		return $links;
	}

	/**
	 * Handles the display of different messages for admin notices.
	 *
	 * @wp-action  admin_notices
	 * @action     admin_notices
	 */
	public function admin_notices() {
		if (current_user_can('manage_options') or current_user_can('publish_posts')) {
			// Upgrade notice
			if (version_compare(Social::option('installed_version'), Social::$version, '<')) {
				$message = sprintf(__('Social is shiny and new! Please <a href="%s">verify and save your settings</a> to complete the upgrade.', 'social'), esc_url(Social::settings_url()));
				echo '<div class="error"><p>'.$message.'</p></div>';
			}

			$suppress_no_accounts_notice = get_user_meta(get_current_user_id(), 'social_suppress_no_accounts_notice', true);
			if (!$this->_enabled and (!isset($_GET['page']) or $_GET['page'] != basename(SOCIAL_FILE)) and empty($suppress_no_accounts_notice)) {
				$dismiss = sprintf(__('<a href="%s" class="social_dismiss">[Dismiss]</a>', 'social'), esc_url(admin_url('options-general.php?social_controller=settings&social_action=suppress_no_accounts_notice')));
				$message = sprintf(__('To start using Social, please <a href="%s">add an account</a>.', 'social'), esc_url(Social::settings_url()));
				echo '<div class="error"><p>'.$message.' '.$dismiss.'</p></div>';
			}

			if (isset($_GET['page']) and $_GET['page'] == basename(SOCIAL_FILE)) {
				// CRON Lock
				if (Social::option('cron_lock_error') !== null) {
					$upload_dir = wp_upload_dir();
					if (is_writeable(Social::$plugins_path) or (isset($upload_dir['basedir']) and is_writeable($upload_dir['basedir']))) {
						delete_option('social_cron_lock_error');
					}
					else {
						if (isset($upload_dir['basedir'])) {
							$message = sprintf(__('Social requires that either %s or %s be writable for CRON jobs.', 'social'), esc_html(Social::$plugins_path), esc_html($upload_dir['basedir']));
						}
						else {
							$message = sprintf(__('Social requires that %s is writable for CRON jobs.', 'social'), esc_html(Social::$plugins_path));
						}

						echo '<div class="error"><p>'.esc_html($message).'</p></div>';
					}
				}

				// Enable notice?
				$suppress_enable_notice = get_user_meta(get_current_user_id(), 'social_suppress_enable_notice', true);
				if (empty($suppress_enable_notice)) {
					$message = __('When you enable Social, users will be created when they log in with Facebook or Twitter to comment. These users are created without a role and will be prevented from accessing the admin side of WordPress until an administrator edits the user to give them a role.', 'social');
					$dismiss = sprintf(__('<a href="%s" class="social_dismiss">[Dismiss]</a>', 'social'), esc_url(admin_url('options-general.php?social_controller=settings&social_action=suppress_enable_notice')));
					echo '<div class="updated"><p>'.$message.' '.$dismiss.'</p></div>';
				}
			}

			// Log write error
			$error = Social::option('log_write_error');
			if ($error == '1') {
				echo '<div class="error"><p>'.
					sprintf(__('%s needs to be writable for Social\'s logging. <a href="%" class="social_dismiss">[Dismiss]</a>', 'social'), esc_html(Social::$plugins_path), esc_url(admin_url('options-general.php?social_controller=settings&social_action=clear_log_write_error'))).
					'</p></div>';
			}
		}

		// Deauthed accounts
		$deauthed = Social::option('deauthed');
		if (!empty($deauthed)) {
			foreach ($deauthed as $service => $data) {
				foreach ($data as $id => $message) {
					$dismiss = sprintf(__('<a href="%s" class="%s">[Dismiss]</a>', 'social'), esc_url(admin_url('options-general.php?social_controller=settings&social_action=clear_deauth&id='.$id.'&service='.$service)), 'social_dismiss');
					echo '<div class="error"><p>'.esc_html($message).' '.$dismiss.'</p></div>';
				}
			}
		}

		// Errored broadcasting?
		global $post;
		if (isset($post->ID)) {
			$error_accounts = get_post_meta($post->ID, '_social_broadcast_error', true);
			if (!empty($error_accounts)) {
				$message = Social_View::factory('wp-admin/post/broadcast/error/notice', array(
					'social' => $this,
					'accounts' => $error_accounts,
					'post' => $post,
				));
				echo '<div class="error" id="social-broadcast-error">'.$message.'</div>';

				delete_post_meta($post->ID, '_social_broadcast_error');
			}
		}

		// 2.0 Upgrade?
		$upgrade_2_0 = get_user_meta(get_current_user_id(), 'social_2.0_upgrade', true);
		if (!empty($upgrade_2_0)) {
			if (current_user_can('manage_options')) {
				$output = __('Social needs to re-authorize your Facebook account(s). Please re-connect your <a href="%s">global</a> and <a href="%s">personal</a> accounts.', 'social');
				$output = sprintf($output, esc_url(Social::settings_url()), esc_url(admin_url('profile.php#social-accounts')));
			}
			else {
				$output = __('Social needs to re-authorize your Facebook account(s).. Please re-connect your <a href="%s">personal</a> accounts.', 'social');
				$output = sprintf($output, esc_url(admin_url('profile.php#social-networks')));
			}

			$dismiss = sprintf(__('<a href="%s" class="%s">[Dismiss]</a>', 'social'), esc_url(admin_url('options-general.php?social_controller=settings&social_action=clear_2_0_upgrade')), 'social_dismiss');
			echo '<div class="error"><p>'.$output.' '.$dismiss.'</p></div>';
		}
	}

	/**
	 * Displays the admin options form.
	 *
	 * @return void
	 */
	public function admin_options_form() {
		Social_Request::factory('settings/index')->execute();
	}

	/**
	 * Shows the user's social network accounts.
	 *
	 * @wp-action  show_user_profile
	 * @param  object  $profileuser
	 * @return void
	 */
	public function show_user_profile($profileuser) {
		$default_accounts = get_user_meta($profileuser->ID, 'social_default_accounts', true);
		if (empty($default_accounts)) {
			$default_accounts = array();
		}
		$accounts = array();
		foreach ($this->services() as $key => $service) {
			if (!isset($accounts[$key])) {
				$accounts[$key] = array();
			}
			foreach ($service->accounts() as $account) {
				if ($account->personal()) {
					$accounts[$key][] = $account->id();
				}
			}
		}
		echo Social_View::factory('wp-admin/profile', array(
			'defaults' => $default_accounts,
			'services' => $this->services(),
			'accounts' => $accounts,
		));
	}

	/**
	 * Saves the default accounts for the user.
	 *
	 * @wp-action personal_options_update
	 * @param  int  $user_id
	 * @return void
	 */
	public function personal_options_update($user_id) {
		// Store the default accounts
		$accounts = array();
		if (isset($_POST['social_default_accounts']) and is_array($_POST['social_default_accounts'])) {
			foreach ($_POST['social_default_accounts'] as $account) {
				$account = explode('|', $account);
				$accounts[$account[0]][] = $account[1];
			}
		}

		// TODO abstract this to the facebook plugin
		if (isset($_POST['social_default_pages']) and is_array($_POST['social_default_pages'])) {
			if (!isset($accounts['facebook'])) {
				$accounts['facebook'] = array(
					'pages' => array()
				);
			}
			$accounts['facebook']['pages'] = $_POST['social_default_pages'];
		}

		if (count($accounts)) {
			update_user_meta($user_id, 'social_default_accounts', $accounts);
		}
		else {
			delete_user_meta($user_id, 'social_default_accounts');
		}

		// Save Enabled child accounts
		$is_profile = true;
		$enabled_child_accounts = is_array($_POST['social_enabled_child_accounts']) ? $_POST['social_enabled_child_accounts'] : array();
		foreach ($this->services() as $service_key => $service) {
			$updated_accounts = array();
			foreach ($service->accounts() as $account) {
				//default service to empty array in case it is not set
				$enabled_child_accounts[$service_key] = isset($enabled_child_accounts[$service_key]) ? $enabled_child_accounts[$service_key] : array();

				$account->update_enabled_child_accounts($enabled_child_accounts[$service_key]);
				$updated_accounts[$account->id()] = $account->as_object();
			}
			$service->accounts($updated_accounts)->save($is_profile);
		}
	}

	/**
	 * Return array of enabled social broadcasting post types
	 *
	 * @static
	 * @return array
	 */
	public static function broadcasting_enabled_post_types() {
		return apply_filters('social_broadcasting_enabled_post_types', get_post_types(array(
			'public' => true,
			'hierarchical' => false
		)));
	}
	
	/**
	 * Check if a post type has broadcasting enabled
	 *
	 * @static
	 * @param  string  $post_type  post type to check for
	 * @return bool
	 */
	public static function broadcasting_enabled_for_post_type($post_type = null) {
		return (bool) in_array($post_type, self::broadcasting_enabled_post_types());
	}

	/**
	 * Add Meta Boxes
	 *
	 * @wp-action  do_meta_boxes
	 * @return void
	 */
	public function do_meta_boxes() {
		global $post;

		if ($post !== null && Social::option('disable_broadcasting') != 1) {
			foreach (self::broadcasting_enabled_post_types() as $post_type) {
				add_meta_box('social_meta_broadcast', __('Social Broadcasting', 'social'), array(
					$this,
					'add_meta_box_broadcast'
				), $post_type, 'side', 'high');
	
				$fetch = Social::option('fetch_comments');
				if ($this->_enabled and !empty($fetch)) {
					if ($post->post_status == 'publish') {
						add_meta_box('social_meta_aggregation_log', __('Social Comments', 'social'), array(
							$this,
							'add_meta_box_log'
						), $post_type, 'normal', 'core');
					}
				}
			}
		}
	}

	/**
	 * Adds the broadcasting meta box.
	 *
	 * @return void
	 */
	public function add_meta_box_broadcast() {
		global $post;

		$broadcasted = '';
		$broadcasted_ids = get_post_meta($post->ID, '_social_broadcasted_ids', true);
		if (!empty($broadcasted_ids)) {
			$broadcasted = Social_View::factory('wp-admin/post/meta/broadcast/parts/broadcasted', array(
				'services' => $this->services(),
				'ids' => $broadcasted_ids,
				'post' => $post,
			));
		}

		$show_broadcast = false;
		foreach ($this->services() as $service) {
			if (count($service->accounts())) {
				$show_broadcast = true;
				break;
			}
		}

		// Content
		$button = '';
		$content = '';
		if ($show_broadcast) {
			if ($post->post_status != 'private') {
				switch ($post->post_status) {
					case 'pending':
						$button = 'Edit';
						$accounts = get_post_meta($post->ID, '_social_broadcast_accounts', true);
						$content = Social_View::factory('wp-admin/post/meta/broadcast/pending', array(
							'accounts' => $accounts,
							'services' => $this->services(),
						));
						break;
					case 'future':
						$button = 'Edit';
						$accounts = get_post_meta($post->ID, '_social_broadcast_accounts', true);
						$content = Social_View::factory('wp-admin/post/meta/broadcast/scheduled', array(
							'services' => $this->services(),
							'accounts' => $accounts,
						));
						break;
					case 'publish':
						$button = 'Broadcast';
						break;
					default:
						if ($post->post_status == 'draft' and !empty($broadcasted_ids)) {
							$content = '';
						}
						else {
							$notify = false;
							if (get_post_meta($post->ID, '_social_notify', true) == '1') {
								$notify = true;
							}
							else if (Social::option('broadcast_by_default') == '1') {
								$notify = true;
							}

							$content = Social_View::factory('wp-admin/post/meta/broadcast/default', array(
								'post' => $post,
								'notify' => $notify,
							));
						}
						break;
				}
			}
			else {
				$content = Social_View::factory('wp-admin/post/meta/broadcast/private');
			}

			// Button
			if (!empty($button)) {
				$button = Social_View::factory('wp-admin/post/meta/broadcast/parts/button', array(
					'broadcasted' => $broadcasted,
					'button_text' => $button,
				));
			}
		}

		echo Social_View::factory('wp-admin/post/meta/broadcast/shell', array(
			'post' => $post,
			'content' => $content,
			'broadcasted' => $broadcasted,
			'button' => $button
		));
	}

	/**
	 * Adds the aggregation log meta box.
	 *
	 * @return void
	 */
	public function add_meta_box_log() {
		global $post;

		$next_run = get_post_meta($post->ID, '_social_aggregation_next_run', true);
		if (empty($next_run)) {
			$next_run = __('Not Scheduled', 'social');
		}
		else {
			$next_run = Social_Aggregation_Queue::next_run($next_run);
		}

		echo Social_View::factory('wp-admin/post/meta/log/shell', array(
			'post' => $post,
			'next_run' => $next_run,
		));
	}

	/**
	 * Show the broadcast options if publishing.
	 *
	 * @wp-filter  redirect_post_location
	 * @param  string  $location  default post-publish location
	 * @param  int     $post_id   post ID
	 * @return string|void
	 */
	public function redirect_post_location($location, $post_id) {
		if ((isset($_POST['social_notify']) and $_POST['social_notify'] == '1') and
			(isset($_POST['visibility']) and $_POST['visibility'] !== 'private')
		) {
			update_post_meta($post_id, '_social_notify', '1');
			if (isset($_POST['publish']) or isset($_POST['social_broadcast'])) {
				Social_Request::factory('broadcast/options')->post(array(
					'post_ID' => $post_id,
					'location' => $location,
				))->execute();
			}
		}
		else {
			delete_post_meta($post_id, '_social_notify');
		}
		return $location;
	}

	/**
	 * Removes post meta if the post is going to private.
	 *
	 * @wp-action  transition_post_status
	 * @param  string  $new
	 * @param  string  $old
	 * @param  object  $post
	 * @return void
	 */
	public function transition_post_status($new, $old, $post) {
		if ($new == 'private') {
			delete_post_meta($post->ID, '_social_notify');
			delete_post_meta($post->ID, '_social_broadcast_accounts');

			foreach ($this->services() as $key => $service) {
				delete_post_meta($post->ID, '_social_'.$key.'_content');
			}
		}
		else {
			$xmlrpc = false;
			if ($new == 'publish') {
				if ( ( defined('XMLRPC_REQUEST') or defined('SOCIAL_MAIL_PUBLISH') ) and $old != 'publish') {
					$xmlrpc = true;
					$this->xmlrpc_publish_post($post);
				}
				if (self::broadcasting_enabled_for_post_type($post->post_type)) {
					Social_Aggregation_Queue::factory()->add($post->ID)->save();
				}
			}

			// Sends previously saved broadcast information
			if ($xmlrpc or ($old == 'future' and !in_array($new, array('future', 'draft')))) {
				Social_Request::factory('broadcast/run')->query(array(
					'post_ID' => $post->ID
				))->execute();
			}
		}
	}

	/**
	 * Broadcasts the post on XML RPC requests.
	 *
	 * @param  object  $post
	 * @return void
	 */
	public function xmlrpc_publish_post($post) {
		if ($post and Social::option('broadcast_by_default') == '1') {
			Social::log('Broadcasting triggered by XML-RPC.');

			$broadcast_accounts = array();
			$broadcast_content = array();
			$broadcast_meta = array();

			foreach ($this->default_accounts($post) as $service_key => $accounts) {
				$service = $this->service($service_key);
				if ($service !== false) {
					$broadcast_content[$service_key] = array();
					$broadcast_meta[$service_key] = array();
					foreach ($accounts as $key => $id) {
						// TODO abstract this to the Facebook plugin
						if ($service_key == 'facebook' and $key === 'pages') {
							foreach ($id as $account_id => $pages) {
								$account = $service->account($account_id);

								// TODO This could use some DRY love
								$universal_pages = $account->pages();
								$personal_pages = $account->pages(null, true);

								foreach ($pages as $page_id) {
									if (!isset($broadcast_accounts[$service_key])) {
										$broadcast_accounts[$service_key] = array();
									}

									if (!isset($broadcast_accounts[$service_key][$page_id])) {
										if (isset($universal_pages[$page_id])) {
											$broadcast_accounts[$service_key][$page_id] = (object) array(
												'id' => $page_id,
												'name' => $universal_pages[$page_id]->name,
												'universal' => true,
												'page' => true,
											);
										}
										else if (isset($personal_pages[$page_id])) {
											$broadcast_accounts[$service_key][$page_id] = (object) array(
												'id' => $page_id,
												'name' => $personal_pages[$page_id]->name,
												'universal' => false,
												'page' => true,
											);
										}
									}
									$broadcast_content[$service_key][$page_id] = $service->format_content($post, Social::option('broadcast_format'));
									$broadcast_meta[$service_key][$page_id] = $service->get_broadcast_extras($page_id, $post);
								}
							}
						}
						else {
							$account = $service->account($id);
							if ($account !== false) {
								if (!isset($broadcast_accounts[$service_key])) {
									$broadcast_accounts[$service_key] = array();
								}

								$broadcast_accounts[$service_key][$account->id()] = (object) array(
									'id' => $account->id(),
									'universal' => $account->universal()
								);

								$broadcast_content[$service_key][$account->id()] = $service->format_content($post, Social::option('broadcast_format'));
								$broadcast_meta[$service_key][$account->id()] = $service->get_broadcast_extras($account->id(), $post);
							}
						}
					}
				}
			}

			update_post_meta($post->ID, '_social_broadcast_content', addslashes_deep($broadcast_content));
			update_post_meta($post->ID, '_social_broadcast_meta', addslashes_deep($broadcast_meta));

			if (count($broadcast_accounts)) {
				Social::log('There are default accounts, running broadcast');
				update_post_meta($post->ID, '_social_broadcast_accounts', addslashes_deep($broadcast_accounts));
			}
		}
	}

	/**
	 * Loads the default accounts for the post.
	 *
	 * @param  object  $post
	 * @return array
	 */
	public function default_accounts($post) {
		$default_accounts = Social::option('default_accounts');
		$author_default_accounts = get_user_meta($post->post_author, 'social_default_accounts', true);
		if (is_array($author_default_accounts)) {
			foreach ($author_default_accounts as $service_key => $accounts) {
				if (!isset($default_accounts[$service_key])) {
					$default_accounts[$service_key] = $accounts;
				}
				else {
					foreach ($accounts as $key => $account) {
						if ($key === 'pages') {
							if (!isset($default_accounts[$key]['pages'])) {
								$default_accounts[$key]['pages'] = $account;
							}
							else {
								foreach ($account as $page_id) {
									if (!in_array($page_id, $default_accounts[$key]['pages'])) {
										$default_accounts[$key]['pages'][] = $page_id;
									}
								}
							}
						}
						else {
							$default_accounts[$service_key][] = $account;
						}
					}
				}
			}
		}

		return apply_filters('social_default_accounts', $default_accounts, $post);
	}

	/**
	 * Sets the broadcasted IDs for the post.
	 *
	 * @param  int  $post_id  post id
	 * @param  string  $service  service key
	 * @param  string  $broadcasted_id  broadcasted id
	 * @param  string  $message  broadcasted message
	 * @param  Social_Service_Account  $account  account
	 * @param  Social_Response  $response  response object
	 * @return void
	 */
	public function add_broadcasted_id($post_id, $service, $broadcasted_id, $message, $account, Social_Response $response = null) {
		$broadcasted_ids = get_post_meta($post_id, '_social_broadcasted_ids', true);
		if (empty($broadcasted_ids)) {
			$broadcasted_ids = array();
		}

		if (!isset($broadcasted_ids[$service])) {
			$broadcasted_ids[$service] = array();
		}

		if (!isset($broadcasted_ids[$service][$account->id()])) {
			$broadcasted_ids[$service][$account->id()] = array();
		}

		if (!isset($broadcasted_ids[$service][$account->id()][$broadcasted_id])) {
			$urls = array(
				get_permalink($post_id)
			);

			$shortlink = wp_get_shortlink($post_id);
			if (!in_array($shortlink, $urls)) {
				$urls[] = $shortlink;
			}

			$home_url = home_url('?p='.$post_id);
			if (!in_array($home_url, $urls)) {
				$urls[] = $home_url;
			}

			$data = array(
				'message' => $message,
				'urls' => $urls
			);
			$data = apply_filters('social_save_broadcasted_ids_data', $data, $account, $service, $post_id, $response);
			$broadcasted_ids[$service][$account->id()][$broadcasted_id] = $data;
			update_post_meta($post_id, '_social_broadcasted_ids', $broadcasted_ids);
		}
	}

	/**
	 * Adds the 15 minute interval.
	 *
	 * @wp-filter  cron_schedules
	 * @param  array  $schedules
	 * @return array
	 */
	public function cron_schedules($schedules) {
		$schedules['every15min'] = array(
			'interval' => 900,
			'display' => 'Every 15 minutes'
		);
		return $schedules;
	}

	/**
	 * Sends a request to initialize CRON 15.
	 *
	 * @wp-action  social_cron_15_init
	 * @return void
	 */
	public function cron_15_init() {
		Social::log('Running cron_15_init');
		Social_Request::factory('cron/cron_15')->query('api_key', Social::option('system_cron_api_key'))->execute();
	}

	/**
	 * Runs the aggregation loop.
	 *
	 * @wp-action  social_cron_15
	 * @return void
	 */
	public function run_aggregation() {
		$semaphore = Social_Semaphore::factory();
		$queue = Social_Aggregation_Queue::factory();

		foreach ($queue->runnable() as $timestamp => $posts) {
			foreach ($posts as $id => $interval) {
				$post = get_post($id);
				if ($post !== null) {
					$queue->add($id, $interval)->save();
					$semaphore->increment();
					$this->request(home_url('index.php?social_controller=aggregation&social_action=run&post_id='.$id), 'run');
				}
				else {
					$queue->remove($id, $timestamp)->save();
				}
			}
		}
	}

	/**
	 * Removes the post from the aggregation queue.
	 *
	 * @wp-action  delete_post
	 * @param  int  $post_id
	 * @return void
	 */
	public function delete_post($post_id) {
		Social_Aggregation_Queue::factory()->remove($post_id);
	}

	/**
	 * Hides the Site Admin link for social-based users.
	 *
	 * @wp-filter register
	 * @param  string  $link
	 * @return string
	 */
	public function register($link) {
		if (is_user_logged_in()) {
			$commenter = get_user_meta(get_current_user_id(), 'social_commenter', true);
			if (!empty($commenter)) {
				return '';
			}
		}

		return $link;
	}

	/**
	 * Sets the user role.
	 *
	 * @wp-action set_user_role
	 * @param  int     $user_id
	 * @param  string  $role
	 */
	public function set_user_role($user_id, $role) {
		if (!empty($role)) {
			delete_user_meta($user_id, 'social_commenter');
		}
	}

	/**
	 * Show the disconnect link for social-based users.
	 *
	 * @wp-filter loginout
	 * @param  string  $link
	 * @return string
	 */
	public function loginout($link) {
		if (is_user_logged_in()) {
			$commenter = get_user_meta(get_current_user_id(), 'social_commenter', true);
			if (!empty($commenter)) {
				foreach ($this->services() as $key => $service) {
					$account = reset($service->accounts());
					if ($account) {
						return $service->disconnect_link($account);
					}
				}
			}
		}
		else {
			$link = explode('>'.__('Log in'), $link);
			$link = $link[0].' id="social_login">'.__('Log in').$link[1];
		}

		return $link;
	}

	/**
	 * Increments the service comment counter.
	 *
	 * @static
	 * @param  array  $items
	 * @param  array  $groups
	 */
	public static function add_social_items_count($items, &$groups) {
		foreach ($items as $group => $_items) {
			if ($group == 'parent') {
				self::add_social_items_count($_items, $groups);
			}
			else {
				if (!isset($groups['social-'.$group])) {
					$groups['social-'.$group] = 0;
				}

				$groups['social-'.$group] = $groups['social-'.$group] + count($_items);
			}
		}
	}

	/**
	 * Overrides the default WordPress comments_template function.
	 *
	 * @wp-filter  comments_template
	 * @return string
	 */
	public function comments_template($path) {
		global $post;

		if (!(
			is_singular() and 
			(have_comments() or $post->comment_status == 'open') and 
			Social::option('use_standard_comments') != '1'
		)) {
			return $path;
		}

		if (!defined('SOCIAL_COMMENTS_FILE')) {
			define('SOCIAL_COMMENTS_FILE', trailingslashit(dirname(SOCIAL_FILE)).'views/comments.php');
		}

		return SOCIAL_COMMENTS_FILE;
	}

	/**
	 * Returns an array of comment types that display avatars.
	 *
	 * @wp-filter  get_avatar_comment_types
	 * @param  array  $types  default WordPress types
	 * @return array
	 */
	public function get_avatar_comment_types($types) {
		$types[] = 'wordpress';
		return $types;
	}

	/**
	 * Gets the avatar based on the comment type.
	 *
	 * @wp-filter  get_avatar
	 * @param  string  $avatar
	 * @param  object  $comment
	 * @param  int     $size
	 * @param  string  $default
	 * @param  string  $alt
	 * @return string
	 */
	public function get_avatar($avatar, $comment, $size, $default, $alt) {
		$image = null;
		if (is_object($comment)) {
			$image = get_comment_meta($comment->comment_ID, 'social_profile_image_url', true);
			if (empty($image)) {
				$image = null;
			}
		}
		else {
			// Commenter?
			$social_avatar = get_user_meta($comment, 'social_avatar', true);
			if (!empty($social_avatar)) {
				$image = $social_avatar;
			}
		}

		if ($image !== null) {
			$image = esc_url($image);
			$size = esc_attr($size);
			$type = '';
			if (is_object($comment)) {
				$type = esc_attr($comment->comment_type);
			}

			$image = esc_url($image);
			$image_format = apply_filters('social_get_avatar_image_format', '<img alt="%1$s" src="%2$s" class="avatar avatar-%3$s photo %4$s" height="%3$s" width="%3$s" />');
			return sprintf($image_format, $alt, $image, $size, $type);
		}

		return $avatar;
	}

	/**
	 * Sets the comment type upon being saved.
	 *
	 * @wp-action  comment_post
	 * @param  int  $comment_ID
	 */
	public function comment_post($comment_ID) {
		global $wpdb;

		$comment = get_comment($comment_ID);
		$services = $this->services();
		if (!empty($services)) {
			$account_id = $_POST['social_post_account'];
			foreach ($services as $key => $service) {
				$output = $service->format_comment_content($comment, Social::option('comment_broadcast_format'));
				foreach ($service->accounts() as $account) {
					if ($account_id == $account->id()) {
						if (isset($_POST['post_to_service'])) {
							$in_reply_to_status_id = get_comment_meta($_POST['comment_parent'], 'social_status_id', true);
							if ($comment->comment_approved == '0') {
								update_comment_meta($comment_ID, 'social_to_broadcast', $_POST['social_post_account']);
								if (!empty($in_reply_to_status_id)) {
									update_comment_meta($comment_ID, 'social_in_reply_to_status_id', addslashes_deep($in_reply_to_status_id));
								}
							}
							else {
								$args = array();
								if (!empty($in_reply_to_status_id)) {
									$args['in_reply_to_status_id'] = $in_reply_to_status_id;
									delete_comment_meta($comment_ID, 'social_in_reply_to_status_id');
								}
								Social::log(sprintf(__('Broadcasting comment #%s to %s using account #%s.', 'social'), $comment_ID, $service->title(), $account->id()));
								$response = $service->broadcast($account, $output, $args, null, $comment_ID);
								if ($response === false or $response->id() === '0') {
									wp_delete_comment($comment_ID);
									Social::log(sprintf(__('Error: Broadcast comment #%s to %s using account #%s, please go back and try again.', 'social'), $comment_ID, esc_html($service->title()), esc_html($account->id())));
									wp_die(sprintf(__('Error: Your comment could not be sent to %s, please go back and try again.', 'social'), esc_html($service->title())));
								}

								$wpdb->query($wpdb->prepare("
									UPDATE $wpdb->comments
									   SET comment_type = %s
									 WHERE comment_ID = %s
								", 'social-'.$service->key(), $comment_ID));

								$this->set_comment_aggregated_id($comment_ID, $service->key(), $response->id());
								update_comment_meta($comment_ID, 'social_status_id', addslashes_deep($response->id()));
								update_comment_meta($comment_ID, 'social_raw_data', addslashes_deep(base64_encode(json_encode($response->body()->response))));
								Social::log(sprintf(__('Broadcasting comment #%s to %s using account #%s COMPLETE.', 'social'), $comment_ID, $service->title(), $account->id()));
							}
						}

						update_comment_meta($comment_ID, 'social_account_id', addslashes_deep($account_id));
						update_comment_meta($comment_ID, 'social_profile_image_url', addslashes_deep($account->avatar()));
						update_comment_meta($comment_ID, 'social_comment_type', addslashes_deep('social-'.$service->key()));

						if ($comment->user_id != '0') {
							$comment->comment_author = $account->name();
							$comment->comment_author_url = $account->url();
							wp_update_comment(get_object_vars($comment));
						}
						Social::log(sprintf(__('Comment #%s saved.', 'social'), $comment_ID));
						break;
					}
				}
			}
		}
	}

	/**
	 * Sets the comment to be approved.
	 *
	 * @wp-action  wp_set_comment_status
	 * @param  int     $comment_id
	 * @param  string  $comment_status
	 * @return void
	 */
	public function wp_set_comment_status($comment_id, $comment_status) {
		if ($comment_status == 'approve') {
			global $wpdb;
			$results = $wpdb->get_results($wpdb->prepare("
				SELECT user_id, m.meta_value
				  FROM $wpdb->commentmeta AS m
				  JOIN $wpdb->comments AS c
				    ON m.comment_id = c.comment_ID
				 WHERE m.meta_key = %s
				   AND m.comment_id = %s
			", 'social_to_broadcast', $comment_id));
			if (!empty($results)) {
				$result = reset($results);
				$accounts = get_user_meta($result->user_id, 'social_accounts', true);
				if (!empty($accounts)) {
					foreach ($accounts as $service => $accounts) {
						$service = $this->service($service);
						if ($service !== false) {
							$account = null;
							if (!$service->account_exists($result->meta_value)) {
								foreach ($accounts as $id => $account) {
									if ($id == $result->meta_value) {
										$class = 'Social_Service_'.$service->key().'_Account';
										$account = new $class($account);
										break;
									}
								}
							}
							else {
								$account = $service->account($result->meta_value);
							}

							if ($account !== null) {
								Social::log(sprintf(__('Broadcasting comment #%s to %s using account #%s.', 'social'), $comment_id, $service->title(), $account->id()));
								$comment = get_comment($comment_id);

								$in_reply_to_status_id = get_comment_meta($comment_id, 'social_in_reply_to_status_id', true);
								$args = array();
								if (!empty($in_reply_to_status_id)) {
									$args['in_reply_to_status_id'] = $in_reply_to_status_id;
									delete_comment_meta($comment_id, 'social_in_reply_to_status_id');
								}

								$output = $service->format_comment_content($comment, Social::option('comment_broadcast_format'));
								$response = $service->broadcast($account, $output, $args, null, $comment_id);
								if ($response === false or $response->id() === false) {
									wp_delete_comment($comment_id);
									Social::log(sprintf(__('Error: Broadcast comment #%s to %s using account #%s, please go back and try again.', 'social'), $comment_id, $service->title(), $account->id()));
								}

								$wpdb->query($wpdb->prepare("
									UPDATE $wpdb->comments
									   SET comment_type = %s
									 WHERE comment_ID = %s
								", 'social-'.$service->key(), $comment_id));

								$this->set_comment_aggregated_id($comment_id, $service->key(), $response->id());
								update_comment_meta($comment_id, 'social_status_id', addslashes_deep($response->id()));
								update_comment_meta($comment_id, 'social_raw_data', addslashes_deep(base64_encode(json_encode($response->body()->response))));
								Social::log(sprintf(__('Broadcasting comment #%s to %s using account #%s COMPLETE.', 'social'), $comment_id, $service->title(), $account->id()));
							}
						}
					}
				}

				delete_comment_meta($comment_id, 'social_to_broadcast');
			}
		}
	}

	/**
	 * Sets the comment aggregation ID.
	 *
	 * Format of the stored data (serialized):
	 *
	 *     array(
	 *         'twitter' => array(
	 *             1234567890,
	 *             0987654321,
	 *             // ... Other aggregated IDs
	 *         ),
	 *         'facebook' => array(
	 *             1234567890_1234567890,
	 *             0987654321_0987654321,
	 *             // ... Other aggregated IDs
	 *         )
	 *     )
	 *
	 * @param  int     $comment_id
	 * @param  string  $service
	 * @param  int     $broadcasted_id
	 * @return void
	 */
	private function set_comment_aggregated_id($comment_id, $service, $broadcasted_id) {
		$comment = get_comment($comment_id);
		if (is_object($comment)) {
			$aggregated_ids = get_post_meta($comment->comment_post_ID, '_social_aggregated_ids', true);
			if (empty($aggregated_ids)) {
				$aggregated_ids = array();
			}

			if (!isset($aggregated_ids[$service])) {
				$aggregated_ids[$service] = array();
			}

			if (!in_array($broadcasted_id, $aggregated_ids[$service])) {
				$aggregated_ids[$service][] = $broadcasted_id;
			}

			update_post_meta($comment->comment_post_ID, '_social_aggregated_ids', $aggregated_ids);
		}
	}

	/**
	 * Counts the different types of comments.
	 *
	 * @wp-filter social_comments_array
	 * @static
	 * @param  array  $comments
	 * @param  int    $post_id
	 * @return array
	 */
	public function comments_array(array $comments, $post_id) {
		$groups = array();
		if (isset($comments['social_groups'])) {
			$groups = $comments['social_groups'];
		}

		// count the comment types for output in tab headers
		foreach ($comments as $comment) {
			if (is_object($comment)) {
				if (empty($comment->comment_type)) {
					$comment->comment_type = 'wordpress';
				}

				if (!isset($groups[$comment->comment_type])) {
					$groups[$comment->comment_type] = 1;
				}
				else {
					++$groups[$comment->comment_type];
				}
				if (isset($comment->social_items) and is_array($comment->social_items)) {
					$groups[$comment->comment_type] += count($comment->social_items);
				}
			}
		}

		$comments['social_groups'] = apply_filters('social_comments_array_groups', $groups, $comments);

		return $comments;
	}

	/**
	 * Displays a comment.
	 *
	 * @param  object  $comment  comment object
	 * @param  array   $args
	 * @param  int     $depth
	 */
	public function comment($comment, array $args = array(), $depth = 0) {
		$GLOBALS['comment'] = $comment;

		$social_items = '';
		$status_url = null;
		$comment_type = $comment->comment_type;
		$ignored_types = apply_filters('social_ignored_comment_types', array(
			'wordpress',
			'pingback'
		));
		// set the comment type to WordPress if we can't load the Social service (perhaps it was deactivated)
		// and the type isn't an ignored type
		if (!($service = $this->service_for_comment_type($comment->comment_type)) && !in_array($comment->comment_type, $ignored_types)) {
			$comment_type = 'wordpress';
		}
		// set Social Items for Social comments
		if (!in_array($comment->comment_type, $ignored_types)) {
			$status_id = get_comment_meta($comment->comment_ID, 'social_status_id', true);
			if (!empty($status_id)) {
				$status_url = $service->status_url(get_comment_author(), $status_id);
			}
			// Social items?
			if (!empty($comment->social_items)) {
				if (is_object($service) && method_exists($service, 'key')) {
					$avatar_size = apply_filters('social_items_comment_avatar_size', array(
						'width' => 18,
						'height' => 18,
					));
					$social_items = Social_View::factory('comment/social_item', array(
						'items' => $comment->social_items,
						'service' => $service,
						'avatar_size' => $avatar_size
					));
				}
				else {
					Social::log('service not set for: '.print_r($comment, true));
					ob_start();
					var_dump($service);
					Social::log('$service: '.ob_get_clean());
				}
			}
		}

		echo Social_View::factory('comment/comment', array(
			'comment_type' => $comment_type,
			'comment' => $comment,
			'service' => $service,
			'status_url' => $status_url,
			'depth' => $depth,
			'args' => $args,
			'social_items' => $social_items,
		));
	}

	/**
	 * Adds the Aggregate Comments link to the post row actions.
	 *
	 * @param  array    $actions
	 * @param  WP_Post  $post
	 * @return array
	 */
	public function post_row_actions(array $actions, $post) {
		if ($post->post_status == 'publish' && in_array(Social::option('fetch_comments'), array('1', '2'))) {
			$actions['social_aggregation'] = sprintf(__('<a href="%s" rel="%s">Social Comments</a>', 'social'), esc_url(wp_nonce_url(admin_url('options-general.php?social_controller=aggregation&social_action=run&post_id='.$post->ID), 'run')), $post->ID).
				'<img src="'.esc_url(admin_url('images/wpspin_light.gif')).'" class="social_run_aggregation_loader" />';
		}
		return $actions;
	}

	/**
	 * Adds the aggregation functionality to the admin bar.
	 *
	 * @return void
	 */
	public function admin_bar_menu() {
		global $wp_admin_bar;

		$current_object = get_queried_object();

		if (empty($current_object)) {
			return;
		}

		if (!empty($current_object->post_type)
			and ($post_type_object = get_post_type_object($current_object->post_type))
				and current_user_can($post_type_object->cap->edit_post, $current_object->ID)
					and ($post_type_object->show_ui or 'attachment' == $current_object->post_type)
						and (in_array(Social::option('fetch_comments'), array('1', '2')))
		) {
			$wp_admin_bar->add_menu(array(
				'parent' => 'comments',
				'id' => 'social-find-comments',
				'title' => __('Find Social Comments', 'social')
					.'<span class="social-aggregation-spinner" style="display: none;">&nbsp;(
						<span class="social-dot dot-active">.</span>
						<span class="social-dot">.</span>
						<span class="social-dot">.</span>
					)</span>',
				'href' => esc_url(wp_nonce_url(admin_url('options-general.php?social_controller=aggregation&social_action=run&post_id='.$current_object->ID), 'run')),
			));
			$wp_admin_bar->add_menu(array(
				'parent' => 'comments',
				'id' => 'social-add-tweet-by-url',
				'title' => __('Add Tweet by URL', 'social')
					.'<form class="social-add-tweet" style="display: none;" method="get" action="'.esc_url(wp_nonce_url(admin_url('options-general.php?social_controller=import&social_action=from_url&social_service=twitter&post_id='.$current_object->ID), 'from_url')).'">
						<input type="text" size="20" name="url" value="" autocomplete="off" />
						<input type="submit" name="social-add-tweet-button" name="social-add-tweet-button" value="'.__('Add Tweet by URL', 'social').'" />
					</form>',
				'href' => esc_url(get_edit_post_link($current_object->ID)),
			));
		}
	}
	
	function admin_bar_footer_css() {
?>
<style class="text/css">
#wpadminbar #wp-admin-bar-comments .social-aggregation-spinner {
	background: transparent;
	white-space: nowrap;
}
#wpadminbar .social-aggregation-spinner .dot-active {
	font-weight: bold;
}
#wpadminbar #wp-admin-bar-social-add-tweet-by-url form {
	display: block;
	line-height: 100%;
	margin: 0;
	padding: 5px;
}
#wpadminbar #wp-admin-bar-social-add-tweet-by-url input {
	color: #333;
	font-size: 11px;
	font-weight: normal;
	line-height: 1;
	margin-bottom: 3px;
	padding: 3px;
	text-shadow: none;
	width: 90%;
}
#wpadminbar #wp-admin-bar-social-add-tweet-by-url input[type="submit"] {
	margin: 0;
}
#wpadminbar #wp-admin-bar-social-add-tweet-by-url .loading {
	background: url(<?php echo admin_url('images/wpspin_light.gif'); ?>) center center no-repeat;
}
#wpadminbar #wp-admin-bar-social-add-tweet-by-url p.msg {
	color: #333;
	font-size: 12px;
	font-weight: normal;
	margin: 0;
	padding: 0;
	text-align: center;
	text-shadow: none;
}
#wpadminbar #wp-admin-bar-social-add-tweet-by-url p.error {
	color: #900;
}
</style>
<?php
	}

	function admin_bar_footer_js() {
?>
<script type="text/javascript">
var socialAdminBarMsgs = {
	'protected': '<?php echo esc_js(__('Protected Tweet', 'social')); ?>',
	'invalid': '<?php echo esc_js(__('Invalid URL', 'social')); ?>',
	'success': '<?php echo esc_js(__('Tweet Imported!', 'social')); ?>'
};
</script>
<?php
	}

	/**
	 * Runs the upgrade only if the installed version is older than the current version.
	 *
	 * @return void
	 */
	private function upgrade() {
		define('SOCIAL_UPGRADE', true);
		global $wpdb; // Don't delete, this is used in upgrade files.

		$upgrades = array(
			Social::$plugins_path.'upgrades/2.0.php',
		);
		$upgrades = apply_filters('social_upgrade_files', $upgrades);
		foreach ($upgrades as $file) {
			if (file_exists($file)) {
				include_once $file;
			}
		}

		Social::option('installed_version', Social::$version);
	}

	/**
	 * Are there accounts connected?
	 *
	 * @return bool
	 */
	public function have_accounts() {
		foreach ($this->services() as $service) {
			if (count($service->accounts())) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Removes an account from the default broadcast accounts.
	 *
	 * @param  string  $service
	 * @param  int     $id
	 * @return void
	 */
	public function remove_from_default_accounts($service, $id) {
		Social::log('Removing from default accounts #:id', array('id' => $id));
		if (defined('IS_PROFILE_PAGE')) {
			$defaults = get_user_meta(get_current_user_id(), 'social_default_accounts', true);
		}
		else {
			$defaults = Social::option('default_accounts');
		}

		if (!empty($defaults) and isset($defaults[$service])) {
			Social::log('Old default accounts: :accounts', array('accounts' => print_r($defaults, true)));

			$_ids = array();
			foreach ($defaults[$service] as $key => $_id) {
				if ($_id != $id) {
					$_ids[$key] = $_id;
				}
			}

			// TODO abstract this to the Facebook plugin
			if ($service == 'facebook' and isset($_ids['pages'])) {
				$pages = $_ids['pages'];
				unset($_ids['pages']);
				sort($_ids);
				foreach ($pages as $account_id => $account_pages) {
					if ($account_id != $id) {
						$_ids['pages'][$account_id] = $account_pages;
					}
				}
			}

			$defaults[$service] = $_ids;
			if (!count($defaults[$service])) {
				unset($defaults[$service]);
			}
			Social::log('New default accounts: :accounts', array('accounts' => print_r($defaults, true)));
			if (defined('IS_PROFILE_PAGE')) {
				update_user_meta(get_current_user_id(), 'social_default_accounts', $defaults);
			}
			else {
				Social::option('default_accounts', $defaults);
			}
		}
	}

	/**
	 * Recursively applies wp_kses() to an array/stdClass.
	 *
	 * @param  mixed  $object
	 * @return mixed
	 */
	public function kses($object) {
		if (is_object($object)) {
			$_object = new stdClass;
		}
		else {
			$_object = array();
		}

		foreach ($object as $key => $val) {
			if (is_object($val) or is_array($val)) {
				if (is_object($_object)) {
					$_object->$key = $this->kses($val);
				}
				else if (is_array($_object)) {
					$_object[$key] = $this->kses($val);
				}
			}
			else {
				if (is_object($_object)) {
					$_object->$key = wp_kses($val, array());
				}
				else {
					$_object[$key] = wp_kses($val, array());
				}
			}
		}

		return $_object;
	}

	/**
	 * Handles the remote timeout requests for Social.
	 *
	 * @param  string  $url        url to request
	 * @param  string  $nonce_key  key to use when generating the nonce
	 * @param  bool    $post       set to true to do a wp_remote_post
	 * @return void
	 */
	private function request($url, $nonce_key = null, $post = false) {
		if ($nonce_key !== null) {
			$url = str_replace('&amp;', '&', wp_nonce_url($url, $nonce_key));
		}

		$data = array(
			'timeout' => 0.01,
			'blocking' => false,
			'sslverify' => apply_filters('https_local_ssl_verify', true),
		);

		if ($post) {
			Social::log('POST request to: :url', array(
				'url' => $url
			));
			wp_remote_post($url, $data);
		}
		else {
			Social::log('GET request to: :url', array(
				'url' => $url
			));
			wp_remote_get($url, $data);
		}
	}

	/**
	 * Loads the services.
	 *
	 * @return array
	 */
	private function load_services() {
		if ((isset($_GET['page']) and $_GET['page'] == basename(SOCIAL_FILE)) or defined('IS_PROFILE_PAGE')) {
			$services = false;
		}
		else {
			$services = wp_cache_get('services', 'social');
		}

		if ($services === false) {
			$services = array();
			// Register services
			$registered_services = apply_filters('social_register_service', array());
			if (is_array($registered_services) and count($registered_services)) {
				$accounts = Social::option('accounts');
				foreach ($registered_services as $service) {
					if (!isset($services[$service])) {
						$service_accounts = array();

						if (isset($accounts[$service]) and count($accounts[$service])) {
							// Flag social as enabled, we have at least one account.
							if ($this->_enabled === null) {
								$this->_enabled = true;
							}

							foreach ($accounts[$service] as $account_id => $account) {
								// TODO Shouldn't have to do this. Fix later.
								$account->personal = '0';
								$service_accounts[$account_id] = $account;
							}
						}

						$class = 'Social_Service_'.$service;
						$services[$service] = new $class($service_accounts);
					}
				}
				wp_cache_set('services', $services, 'social');
			}
		}
		else if ($this->_enabled === null and is_array($services)) {
			foreach ($services as $service) {
				if (count($service->accounts())) {
					$this->_enabled = true;
					break;
				}
			}
		}

// don't return global services for commenters
		$commenter = get_user_meta(get_current_user_id(), 'social_commenter', true);
		if ($commenter == 'true' && !current_user_can('publish_posts')) {
			foreach ($services as $key => $accounts) {
				$services[$key]->clear_accounts();
			}
		}

		$personal_accounts = get_user_meta(get_current_user_id(), 'social_accounts', true);
		if (is_array($personal_accounts)) {
			foreach ($personal_accounts as $key => $_accounts) {
				if (count($_accounts) and isset($services[$key])) {
					$class = 'Social_Service_'.$key.'_Account';
					foreach ($_accounts as $account_id => $account) {
						// TODO Shouldn't have to do this. Fix later.
						$account->universal = '0';
						if ($services[$key]->account_exists($account_id) and !defined('IS_PROFILE_PAGE')) {
							$account = $this->merge_accounts($services[$key]->account($account_id)->as_object(), $account, $key);
						}
						$account = new $class((object) $account);
						$services[$key]->account($account);
						// Flag social as enabled, we have at least one account.
						if ($this->_enabled === null) {
							$this->_enabled = true;
						}
					}
				}
			}
		}

		return $services;
	}

	/**
	 * Merges universal with personal account.
	 *
	 * @param  array   $arr1
	 * @param  array   $arr2
	 * @param  string  $service_key
	 * @return object
	 */
	private function merge_accounts($arr1, $arr2, $service_key) {
		$arr1->personal = true;
		return apply_filters('social_merge_accounts', $arr1, $arr2, $service_key);
	}

	/**
	 * Checks to see if the array is an associative array.
	 *
	 * @param  array  $arr
	 * @return bool
	 */
	private function is_assoc($arr) {
		$keys = array_keys($arr);
		return array_keys($keys) !== $keys;
	}

	/**
	 * Builds the settings URL for the plugin.
	 *
	 * @param  array  $params
	 * @param  bool   $personal
	 * @return string
	 */
	public static function settings_url(array $params = null, $personal = false) {
		if ($params !== null) {
			foreach ($params as $key => $value) {
				$params[$key] = urlencode($value);
			}
		}

		if (!current_user_can('manage_options') or $personal) {
			$file = 'profile.php';
		}
		else {
			$file = 'options-general.php';
			$params['page'] = basename(SOCIAL_FILE);
		}

		$url = add_query_arg($params, admin_url($file));

		if (!current_user_can('manage_options') or $personal) {
			$url .= '#social-networks';
		}

		return $url;
	}
	
	/**
	 * Filter the where clause for pulling comments for feeds (to exclude meta comments).
	 *
	 * @param  string  $where
	 * @return string
	 */
	public static function comments_feed_exclusions($where) {
		global $wpdb;
		$meta_types = array();
// get services
		$services = Social::instance()->services();
// ask each service for it's "meta" comment types
		foreach ($services as $service) {
			$meta_types = array_merge($meta_types, $service->comment_types_meta());
		}
		$meta_types = array_unique($meta_types);
		if (count($meta_types)) {
			$where .= " AND comment_type NOT IN ('".implode("', '", array_map('social_wpdb_escape', $meta_types))."') ";
		}
		return $where;
	}
	
	/**
	 * Filter the image tag to implement lazy loading support for meta comments.
	 *
	 * @param  string  $image_format
	 * @return string
	 */
	public static function social_item_output_image_format($image_format) {
		if (class_exists('LazyLoad_Images')) {
			// would be great if the plugin provided an API for this, until then we'll copy the code
			$placeholder_image = apply_filters( 'lazyload_images_placeholder_image', LazyLoad_Images::get_url( 'images/1x1.trans.gif' ) );
			$image_format = '<img src="'.esc_url($placeholder_image).'" data-lazy-src="%1$s" width="%2$s" height="%3$s" alt="%4$s" /><noscript>'.$image_format.'</noscript>';
		}
		return $image_format;
	}

	/**
	 * Filter the image tag to implement lazy loading support for avatars.
	 *
	 * @param  string  $image_format
	 * @return string
	 */
	public static function social_get_avatar_image_format($image_format) {
		if (class_exists('LazyLoad_Images')) {
			// would be great if the plugin provided an API for this, until then we'll copy the code
			$placeholder_image = apply_filters( 'lazyload_images_placeholder_image', LazyLoad_Images::get_url( 'images/1x1.trans.gif' ) );
			$image_format = '<img alt="%1$s" src="'.esc_url($placeholder_image).'" data-lazy-src="%2$s" class="avatar avatar-%3$s photo %4$s" height="%3$s" width="%3$s" /><noscript>'.$image_format.'</noscript>';
		}
		return $image_format;
	}

} // End Social

if (!function_exists('addslashes_deep')) {
/**
 * Navigates through an array and adds slashes to the values.
 *
 * If an array is passed, the array_map() function causes a callback to pass the
 * value back to the function. Slashes will be added to this value.
 *
 * @since 3.4.0?
 *
 * @param array|string $value The array or string to be slashed.
 * @return array|string Slashed array (or string in the callback).
 */
function addslashes_deep($value) {
	if ( is_array($value) ) {
		$value = array_map('addslashes_deep', $value);
	} elseif ( is_object($value) ) {
		$vars = get_object_vars( $value );
		foreach ($vars as $key=>$data) {
			$value->{$key} = addslashes_deep( $data );
		}
	} else {
		$value = addslashes($value);
	}

	return $value;
}
}

function social_wpdb_escape($str) {
	global $wpdb;
	return $wpdb->escape($str);
}

function social_wp_mail_indicator() {
	define('SOCIAL_MAIL_PUBLISH', true);
}

$social_file = __FILE__;
if (isset($plugin)) {
	$social_file = $plugin;
}
else if (isset($mu_plugin)) {
	$social_file = $mu_plugin;
}
else if (isset($network_plugin)) {
	$social_file = $network_plugin;
}

define('SOCIAL_FILE', $social_file);
define('SOCIAL_PATH', WP_PLUGIN_DIR.'/'.basename(dirname($social_file)).'/');

// Register Social's autoloading
spl_autoload_register(array('Social', 'auto_load'));

$social = Social::instance();

// General Actions
add_action('init', array($social, 'init'), 1);
add_action('init', array($social, 'request_handler'), 2);
add_action('admin_init', array($social, 'admin_init'), 1);
add_action('load-settings_page_social', array($social, 'check_system_cron'));
add_action('load-social.php', array($social, 'check_system_cron'));
add_action('comment_post', array($social, 'comment_post'));
add_action('wp_set_comment_status', array($social, 'wp_set_comment_status'), 10, 3);
add_action('admin_notices', array($social, 'admin_notices'));
add_action('transition_post_status', array($social, 'transition_post_status'), 10, 3);
add_action('show_user_profile', array($social, 'show_user_profile'));
add_action('do_meta_boxes', array($social, 'do_meta_boxes'));
add_action('delete_post', array($social, 'delete_post'));
add_action('wp_enqueue_scripts', array($social, 'enqueue_assets'));
add_action('load-post-new.php', array($social, 'enqueue_assets'));
add_action('load-post.php', array($social, 'enqueue_assets'));
add_action('load-profile.php', array($social, 'enqueue_assets'));
add_action('load-settings_page_social', array($social, 'enqueue_assets'));
add_action('admin_enqueue_scripts', array($social, 'admin_enqueue_assets'));
add_action('admin_bar_menu', array($social, 'admin_bar_menu'), 95);
add_action('wp_after_admin_bar_render', array($social, 'admin_bar_footer_css'));
add_action('wp_after_admin_bar_render', array($social, 'admin_bar_footer_js'));
add_action('set_user_role', array($social, 'set_user_role'), 10, 2);
add_action('wp-mail.php', 'social_wp_mail_indicator');
add_action('social_settings_save', array('Social_Service_Facebook', 'social_settings_save'));

// CRON Actions
add_action('social_cron_15_init', array($social, 'cron_15_init'));
add_action('social_cron_15', array($social, 'run_aggregation'));

// Admin Actions
add_action('admin_menu', array($social, 'admin_menu'));
add_action('personal_options_update', array($social, 'personal_options_update'));

// Filters
add_filter('cron_schedules', array($social,'cron_schedules'));
add_filter('plugin_action_links', array($social, 'add_settings_link'), 10, 2);
add_filter('redirect_post_location', array($social, 'redirect_post_location'), 10, 2);
add_filter('comments_template', array($social, 'comments_template'));
add_filter('get_avatar_comment_types', array($social, 'get_avatar_comment_types'));
add_filter('get_avatar', array($social, 'get_avatar'), 10, 5);
add_filter('register', array($social, 'register'));
add_filter('loginout', array($social, 'loginout'));
add_filter('post_row_actions', array($social, 'post_row_actions'), 10, 2);
add_filter('social_comments_array', array($social, 'comments_array'), 100, 2);
add_filter('comment_feed_where', array($social, 'comments_feed_exclusions'));
add_filter('social_settings_default_accounts', array('Social_Service_Facebook', 'social_settings_default_accounts'), 10, 2);
add_filter('social_item_output_image_format', array($social, 'social_item_output_image_format'));
add_filter('social_get_avatar_image_format', array($social, 'social_get_avatar_image_format'));

// Service filters
add_filter('social_auto_load_class', array($social, 'auto_load_class'));

} // End class_exists check
