<?php

/**
 * Post to a Facebook profile or page timeline
 *
 * @since 1.1
 */
class Facebook_Social_Publisher {
	/**
	 * Initialize social publisher hooks
	 *
	 * @since 1.2
	 *
	 * @global \Facebook_Loader $facebook_loader test if Facebook app access token exists
	 * @return void
	 */
	public static function init() {
		global $facebook_loader;

		if ( ! ( isset( $facebook_loader ) && $facebook_loader->app_access_token_exists() ) )
			return;

		// always load publish and delete hooks
		// post can be published or deleted many different ways
		add_action( 'transition_post_status', array( 'Facebook_Social_Publisher', 'publish' ), 10, 3 );
		add_action( 'before_delete_post', array( 'Facebook_Social_Publisher', 'delete_facebook_post' ) );

		if ( is_admin() ) {
			self::add_save_post_hooks();

			// load meta box hooks on post creation screens
			foreach( array( 'post', 'post-new' ) as $hook ) {
				add_action( 'load-' . $hook . '.php', array( 'Facebook_Social_Publisher', 'load' ), 1, 0 );
			}
		}
	}

	/**
	 * Check for meta box content on post save.
	 *
	 * @since 1.1
	 *
	 * @return void
	 */
	public static function add_save_post_hooks() {
		// verify if this is an auto save routine.
		// If it is exit early without loading additional files
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
			return;

		if ( ! class_exists( 'Facebook_Social_Publisher_Meta_Box_Profile' ) )
			require_once( dirname(__FILE__) . '/publish-box-profile.php' );
		add_action( 'save_post', array( 'Facebook_Social_Publisher_Meta_Box_Profile', 'save' ) );

		if ( ! class_exists( 'Facebook_Social_Publisher_Meta_Box_Page' ) )
			require_once(  dirname(__FILE__) . '/publish-box-page.php' );
		add_action( 'save_post', array( 'Facebook_Social_Publisher_Meta_Box_Page', 'save' ) );
	}

	/**
	 * Add actions to post edit page.
	 *
	 * @since 1.2.3
	 *
	 * @return void
	 */
	public static function load() {
		// on post pages
		add_action( 'admin_notices', array( 'Facebook_Social_Publisher', 'output_post_admin_notices' ) );

		// wait until after post data loaded, then evaluate post
		add_action( 'add_meta_boxes', array( 'Facebook_Social_Publisher', 'load_post_features' ), 1, 0 );
	}

	/**
	 * Can the current user publish to Facebook?
	 *
	 * @since 1.1
	 *
	 * @param int $wordpress_user_id WordPress user identifier
	 * @return bool true if Facebook data stored for user and permissions exist
	 */
	public static function user_can_publish_to_facebook( $wordpress_user_id = null ) {
		global $facebook_loader;

		if ( ! class_exists( 'Facebook_User' ) )
			require_once( $facebook_loader->plugin_directory . 'facebook-user.php' );

		return Facebook_User::can_publish_to_facebook( $wordpress_user_id );
	}

	/**
	 * Can the site possibly publish to Facebook on behalf of a WordPress user?
	 *
	 * Access token stored along with the token may fail, but its existence is an indicator of possible success.
	 *
	 * @since 1.1
	 *
	 * @return array associative array of stored page data or empty array
	 */
	public static function get_publish_page() {
		$page = get_option( 'facebook_publish_page' );
		if ( is_array( $page ) && isset( $page['access_token'] ) && isset( $page['id'] ) && isset( $page['name'] ) )
			return $page;
		return array();
	}

	/**
	 * Test if a post type is intended for use publicly.
	 *
	 * If not explicitly declared as public a post type is considered non-public (default false).
	 *
	 * @since 1.2.3
	 *
	 * @see register_post_type()
	 * @param string $post_type WordPress post type
	 * @return bool true if public else false
	 */
	public static function post_type_is_public( $post_type ) {
		// empty string or a false response from get_post_type()
		if ( ! $post_type )
			return false;

		$post_type_object = get_post_type_object( $post_type );
		if ( isset( $post_type_object->public ) && $post_type_object->public )
			return true;

		return false;
	}

	/**
	 * Test if a post's post status is public.
	 *
	 * @since 1.2.3
	 *
	 * @param int $post_id WordPress post identifier
	 * @return bool true if public, else false
	 */
	public static function post_status_is_public( $post_id ) {
		$post_status_object = get_post_status_object( get_post_status( $post_id ) );
		if ( ! $post_status_object )
			return false;

		if ( isset( $post_status_object->public ) && $post_status_object->public )
			return true;

		return false;
	}

	/**
	 * Get post capability singular base to be used when gating access.
	 *
	 * @since 1.1
	 *
	 * @param string $post_type post type
	 * @return string post type object capability type or empty string
	 */
	public static function post_type_capability_base( $post_type ) {
		$post_type_object = get_post_type_object( $post_type );
		if ( ! isset( $post_type_object->capability_type ) )
			return '';

		$capability_singular_base = '';
		if ( is_string( $post_type_object->capability_type ) ) {
			$capability_singular_base = $post_type_object->capability_type;
		} else if ( is_array( $post_type_object->capability_type ) ) {
			if ( isset( $post_type_object->capability_type[0] ) )
				$capability_singular_base = $post_type_object->capability_type[0];
		}

		return $capability_singular_base;
	}

	/**
	 * Load post meta boxes and actions after post data loaded if post matches publisher preferences and capabilities.
	 *
	 * @since 1.2.3
	 *
	 * @global stdClass|WP_Post $post WordPress post object
	 * @return void
	 */
	public static function load_post_features() {
		global $post;

		if ( ! isset( $post ) )
			return;

		$post_type = get_post_type( $post );
		// do not load meta boxes if post type not public or post status is public
		if ( ! self::post_type_is_public( $post_type ) || self::post_status_is_public( $post->ID ) )
			return;

		$capability_singular_base = self::post_type_capability_base( $post_type );

		// unable to verify capability for the post type
		// most likely reason: custom post type incorrectly registered
		if ( ! $capability_singular_base )
			return;

		// only display post meta boxes if current user can edit the current post
		if ( ! current_user_can( 'edit_' . $capability_singular_base, $post->ID ) )
			return;

		// load page meta box if Facebook Page data saved
		$page_to_publish = self::get_publish_page();
		if ( ! empty( $page_to_publish ) ) {
			if ( ! class_exists( 'Facebook_Social_Publisher_Meta_Box_Page' ) )
				require_once(  dirname(__FILE__) . '/publish-box-page.php' );

			Facebook_Social_Publisher_Meta_Box_Page::add_meta_box( $post_type, $page_to_publish );
		}
		unset( $page_to_publish );

		// does the current post support authorship? can the post author post to Facebook Timeline?
		if ( post_type_supports( $post_type, 'author' ) && isset( $post->post_author ) && self::user_can_publish_to_facebook( (int) $post->post_author ) ) {
			if ( ! class_exists( 'Facebook_Social_Publisher_Meta_Box_Profile' ) )
				require_once( dirname(__FILE__) . '/publish-box-profile.php' );

			Facebook_Social_Publisher_Meta_Box_Profile::add_meta_box( $post_type );
		}
	}

	/**
	 * Act on the publish action.
	 *
	 * Attempt to post to author timeline if Facebook data exists for author. Attempt to post to associated site page if page data saved.
	 *
	 * @since 1.1
	 *
	 * @param string $new_status name of the new WordPress post status
	 * @param string $old_status name of the old WordPress post status
	 * @param stdClass|WP_Post $post WordPress post object
	 * @return void
	 */
	public static function publish( $new_status, $old_status, $post ) {
		// content not public even if status public
		if ( ! empty( $post->post_password ) )
			return;

		if ( ! self::post_type_is_public( get_post_type( $post ) ) )
			return;

		// transition from non-public to public?
		$new_status_object = get_post_status_object( $new_status );
		if ( ! ( $new_status_object && isset( $new_status_object->public ) && $new_status_object->public ) )
			return;

		$old_status_object = get_post_status_object( $old_status );
		if ( ! $old_status_object || ( isset( $old_status_object->public ) && $old_status_object->public ) )
			return;

		// transition post status happens before save post
		// wait until the end of the insert / update process to send to Facebook
		if ( isset( $post->post_author ) && self::user_can_publish_to_facebook( (int) $post->post_author ) )
			add_action( 'wp_insert_post', array( 'Facebook_Social_Publisher', 'publish_to_facebook_profile' ), 10, 2 );

		$publish_page = self::get_publish_page();
		if ( ! empty( $publish_page ) )
			add_action( 'wp_insert_post', array( 'Facebook_Social_Publisher', 'publish_to_facebook_page' ), 10, 2 );
	}

	/**
	 * Publish a post to a Facebook Page.
	 *
	 * @since 1.0
	 *
	 * @link https://developers.facebook.com/docs/graph-api/reference/page/feed/ Facebook Graph API create page post
	 * @param int $post_id WordPress post identifier
	 * @param stdClass|WP_Post $post WordPress post object
	 * @return void
	 */
	public static function publish_to_facebook_page( $post_id, $post ) {
		global $facebook_loader;

		$post_id = absint( $post_id );
		if ( ! ( $post && $post_id ) )
			return;

		// check if this post has previously been posted to the Facebook page
		// no need to publish again
		if ( get_post_meta( $post_id, 'fb_fan_page_post_id', true ) )
			return;

		if ( ! class_exists( 'Facebook_Social_Publisher_Meta_Box_Page' ) )
			require_once( dirname(__FILE__) . '/publish-box-page.php' );

		$meta_box_present = true;
		if ( defined('XMLRPC_REQUEST') && XMLRPC_REQUEST )
			$meta_box_present = false;

		if ( $meta_box_present && get_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Page::POST_META_KEY_FEATURE_ENABLED, true ) === '0' )
			return;

		setup_postdata( $post );

		$post_type = get_post_type( $post );
		if ( ! self::post_type_is_public( $post_type ) )
			return;

		// do not publish a protected post
		if ( ! empty( $post->post_password ) )
			return;

		$facebook_page = self::get_publish_page();
		if ( ! $facebook_page )
			$facebook_page = get_option( 'facebook_publish_page' );
		if ( ! ( is_array( $facebook_page ) && ! empty( $facebook_page['access_token'] ) && ! empty( $facebook_page['id'] ) && isset( $facebook_page['name'] ) ) )
			return;

		// check our assumptions about a valid link in place
		// fail if a piece of the filter process killed our response
		$link = apply_filters( 'facebook_rel_canonical', get_permalink( $post_id ) );
		if ( ! $link )
			return;

		$args = array(
			'access_token' => $facebook_page['access_token'],
			'link' => $link
		);
		if ( isset( $facebook_page['appsecret_proof'] ) && $facebook_page['appsecret_proof'] )
			$args['appsecret_proof'] = $facebook_page['appsecret_proof'];

		if ( $meta_box_present )
			$args['fb:explicitly_shared'] = 'true';

		// either message or link is required
		// confident we have link, making message optional
		$fan_page_message = get_post_meta( $post_id, 'fb_fan_page_message', true );
		if ( ! empty( $fan_page_message ) )
			$args['message'] = $fan_page_message;
		unset( $fan_page_message );

		$status_messages = array();
		try {
			if ( ! class_exists( 'Facebook_WP_Extend' ) )
				require_once( $facebook_loader->plugin_directory . 'includes/facebook-php-sdk/class-facebook-wp.php' );

			$publish_result = Facebook_WP_Extend::graph_api( $facebook_page['id'] . '/feed', 'POST', $args );

			if ( isset( $publish_result['id'] ) ) {
				update_post_meta( $post_id, 'fb_fan_page_post_id', sanitize_text_field( $publish_result['id'] ) );
				delete_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Page::POST_META_KEY_FEATURE_ENABLED );
				delete_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Page::POST_META_KEY );
			}
		} catch (WP_FacebookApiException $e) {
			$error_result = $e->getResult();

			// error validating access token
			if ( $e->getCode() == 190 ) {
				delete_option( 'facebook_publish_page' );

				$status_messages[] = array( 'message' => esc_html( sprintf( __( 'Failed posting to %s Timeline because the access token expired.', 'facebook' ), $facebook_page['name'] ) ) . ' ' . esc_html( __( 'To reactivate publishing, visit Facebook Social Publisher settings page and associate a Page through the "Allow new posts to a Facebook Page" link.', 'facebook' ) ) . ' ' . esc_html( __( 'Error', 'facebook' ) ) . ': ' . esc_html( json_encode( $error_result['error'] ) ), 'error' => true );
			} else {
				$status_messages[] = array( 'message' => esc_html( sprintf( __( 'Failed posting to %s Timeline.', 'facebook' ), $facebook_page['name'] ) ) . ' ' . esc_html( __( 'Error', 'facebook' ) ) . ': ' . esc_html( json_encode( $error_result['error'] ) ), 'error' => true );
			}
			unset( $error_result );
		}

		if ( isset( $publish_result ) && isset( $publish_result['id'] ) ) {
			$link = '<a href="' . esc_url( self::get_permalink_from_feed_publish_id( sanitize_text_field( $publish_result['id'] ) ), array( 'http', 'https' ) ) . '" target="_blank">' . esc_html( $facebook_page['name'] ) . '</a>';
			if ( isset( $args['message'] ) )
				$message = sprintf( esc_html( __( 'Posted to %1$s with message "%2$s"', 'facebook' ) ), $link, esc_html( $args['message'] ) );
			else
				$message = sprintf( esc_html( __( 'Posted to %s', 'facebook' ) ), $link );
			unset( $link );
			$status_messages[] = array( 'message' => $message, 'error' => false );
			unset( $message );
		}
		unset( $publish_result );

		if ( ! empty( $status_messages ) ) {
			// allow author and page messages on the same post
			$existing_status_messages = get_post_meta( $post_id, 'facebook_status_messages', true );
			if ( is_array( $existing_status_messages ) && ! empty( $existing_status_messages ) )
				$status_messages = array_merge( $existing_status_messages, $status_messages );
			unset( $existing_status_messages );

			update_post_meta( $post_id, 'facebook_status_messages', $status_messages );
			add_filter( 'redirect_post_location', array( 'Facebook_Social_Publisher', 'add_new_post_location' ) );
		}
	}

	/**
	 * Publish a post to a Facebook User Timeline.
	 *
	 * @since 1.0
	 *
	 * @global \Facebook_Loader $facebook_loader Access Facebook application credentials
	 * @param int $post_id WordPress post identifier
	 * @param stdClass|WP_Post $post WordPress post object
	 * @return void
	 */
	public static function publish_to_facebook_profile( $post_id, $post ) {
		global $facebook_loader;

		$post_id = absint( $post_id );
		if ( ! ( isset( $facebook_loader ) && $facebook_loader->app_access_token_exists() && $post && $post_id ) )
			return;

		// does the current post have an existing Facebook post id stored? no need to publish again
		if ( get_post_meta( $post_id, 'fb_author_post_id', true ) )
			return;

		$meta_box_present = true;
		if ( defined('XMLRPC_REQUEST') && XMLRPC_REQUEST )
			$meta_box_present = false;

		if ( ! class_exists( 'Facebook_Social_Publisher_Meta_Box_Profile' ) )
			require_once( dirname(__FILE__) . '/publish-box-profile.php' );
		if ( $meta_box_present && get_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Profile::POST_META_KEY_FEATURE_ENABLED, true ) === '0' )
			return;

		setup_postdata( $post );

		$post_type = get_post_type( $post );
		if ( ! ( self::post_type_is_public( $post_type ) && post_type_supports( $post_type, 'author' ) && isset( $post->post_author ) ) )
			return;

		$post_author = (int) $post->post_author;
		if ( ! $post_author )
			return;

		// test the author, not the current actor
		if ( ! self::user_can_publish_to_facebook( $post_author ) )
			return;

		if ( ! class_exists( 'Facebook_User' ) )
			require_once( $facebook_loader->plugin_directory . 'facebook-user.php' );
		$author_facebook_id = Facebook_User::get_facebook_profile_id( $post_author );
		if ( ! $author_facebook_id )
			return;

		// check our assumptions about a valid link in place
		// fail if a piece of the filter process killed our response
		$link = apply_filters( 'facebook_rel_canonical', get_permalink( $post_id ) );
		if ( ! $link )
			return;

		$og_action = false;
		if ( ! class_exists( 'Facebook_Social_Publisher_Settings' ) )
			require_once( $facebook_loader->plugin_directory . 'admin/settings-social-publisher.php' );
		if ( get_option( Facebook_Social_Publisher_Settings::OPTION_OG_ACTION ) )
			$og_action = true;

		if ( ! class_exists( 'Facebook_Open_Graph_Protocol' ) )
			require_once( $facebook_loader->plugin_directory . 'open-graph-protocol.php' );

		$path = $author_facebook_id . '/';
		if ( $og_action && Facebook_Open_Graph_Protocol::get_post_og_type( $post ) === 'article' ) {
			$story = array( 'article' => $link );
			$path .= 'news.publishes';
			if ( $meta_box_present )
				$story['fb:explicitly_shared'] = 'true';
		} else {
			$story = array( 'link' => $link );
			$path .= 'feed';
		}

		$message = get_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Profile::POST_META_KEY_MESSAGE, true );
		if ( is_string( $message ) && $message )
			$story['message'] = trim( $message );

		if ( ! class_exists( 'Facebook_WP_Extend' ) )
			require_once( $facebook_loader->plugin_directory . 'includes/facebook-php-sdk/class-facebook-wp.php' );

		$status_messages = array();
		try {
			$publish_result = Facebook_WP_Extend::graph_api_with_app_access_token( $path, 'POST', $story );

			if ( isset( $publish_result['id'] ) ) {
				update_post_meta( $post_id, 'fb_author_post_id', sanitize_text_field( $publish_result['id'] ) );
				delete_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Profile::POST_META_KEY_MESSAGE );
				delete_post_meta( $post_id, Facebook_Social_Publisher_Meta_Box_Profile::POST_META_KEY_FEATURE_ENABLED );
			}
		} catch (WP_FacebookApiException $e) {
			$error_result = $e->getResult();

			$status_messages[] = array( 'message' => esc_html( __( 'Failed posting to your Facebook Timeline.', 'facebook' ) ) . ' ' . esc_html( __( 'Error', 'facebook' ) ) . ': ' . esc_html( json_encode( $error_result['error'] ) ), 'error' => true );
		}

		if ( isset( $publish_result ) && isset( $publish_result['id'] ) ) {
			$link = '<a href="' . esc_url( 'https://www.facebook.com/' . $publish_result['id'], array( 'http', 'https' ) ) . '" target="_blank">' . esc_html( __( 'Facebook Timeline', 'facebook' ) ) . '</a>';
			if ( empty( $message ) )
				$message = sprintf( esc_html( __( 'Posted to %s', 'facebook' ) ), $link );
			else
				$message = sprintf( esc_html( __( 'Posted to %1$s with message "%2$s"', 'facebook' ) ), $link, esc_html( $message ) );
			$status_messages[] = array( 'message' => $message, 'error' => false );
		}

		// add new status messages
		if ( ! empty( $status_messages ) ) {
			$existing_status_messages = get_post_meta( $post_id, 'fb_status_messages', true );

			if ( is_array( $existing_status_messages ) && ! empty( $existing_status_messages ) )
				$status_messages = array_merge( $existing_status_messages, $status_messages );

			update_post_meta( $post_id, 'facebook_status_messages', $status_messages );
			add_filter( 'redirect_post_location', array( 'Facebook_Social_Publisher', 'add_new_post_location' ) );
		}
	}

	/**
	 * Parse the unique post id from a feed.
	 *
	 * @since 1.0
	 *
	 * @param string $id feed publish identifier
	 * @return string Facebook URL
	 */
	public static function get_permalink_from_feed_publish_id( $id ) {
		preg_match_all( "/(.*?)_(.*?)$/su", $id, $ids, PREG_SET_ORDER );

		return 'https://www.facebook.com/' . $ids[0][2];
	}

	/**
	 * Add a query argument to trigger displaying admin messages on the front-end.
	 *
	 * @since 1.0
	 *
	 * @param string $loc URL
	 * $return string URL with facebook_message query parameter appended
	 */
	public static function add_new_post_location( $loc ) {
		return add_query_arg( 'facebook_message', 1, $loc );
	}

	/**
	 * Output admin notices saved to post data during the Facebook publish process.
	 *
	 * Triggers if our GET argument is present from redirecting the post location.
	 *
	 * @since 1.1
	 *
	 * @global stdClass|WP_Post WordPress post object
	 * @return void
	 */
	public static function output_post_admin_notices() {
		global $post;

		if ( empty( $_GET['facebook_message'] ) || ! isset( $post ) || ! isset( $post->ID ) )
			return;

		$post_id = absint( $post->ID );
		if ( ! $post_id )
			return;

		$post_meta_key = 'facebook_status_messages';
		$messages = get_post_meta( $post_id, $post_meta_key, true );
		if ( ! is_array( $messages ) )
			return;

		foreach ( $messages as $message ) {
			if ( ! isset( $message['message'] ) )
				continue;

			$div = '<div class="fade ';
			if ( isset( $message['error'] ) && $message['error'] )
				$div .= 'error';
			else
				$div .= 'updated';
			$div .= '"><p>';
			$div .= $message['message']; // escaped when generated. may contain links
			$div .= '</p></div>';
			echo $div;
			unset( $div );
		}

		// display once
		delete_post_meta( $post_id, $post_meta_key );
	}

	/**
	 * Delete post data from Facebook when deleted in WordPress
	 *
	 * @since 1.0
	 *
	 * @global \Facebook_Loader $facebook_loader Reference plugin directory
	 * @param int $post_id WordPress post identifer
	 * @return void
	 */
	public static function delete_facebook_post( $post_id ) {
		global $facebook_loader;

		$post_id = absint( $post_id );
		if ( ! $post_id )
			return;

		$fb_page_post_id = get_post_meta( $post_id, 'fb_fan_page_post_id', true );
		if ( $fb_page_post_id ) {
			$page_to_publish = self::get_publish_page();
			if ( isset( $page_to_publish['access_token'] ) ) {
				if ( ! class_exists( 'Facebook_WP_Extend' ) )
					require_once( $facebook_loader->plugin_directory . 'includes/facebook-php-sdk/class-facebook-wp.php' );

				// act as the saved credential, not current user
				try {
					Facebook_WP_Extend::graph_api( $fb_page_post_id, 'DELETE', array( 'access_token' => $page_to_publish['access_token'] ) );
				} catch (WP_FacebookApiException $e) {}
			}
			unset( $page_to_publish );
		}
		unset( $fb_page_post_id );

		$post = get_post( $post_id );
		if ( isset( $post->post_author ) && self::user_can_publish_to_facebook( (int) $post->post_author ) ) {

			if ( ! class_exists( 'Facebook_WP_Extend' ) )
				require_once( $facebook_loader->plugin_directory . 'includes/facebook-php-sdk/class-facebook-wp.php' );

			$fb_author_post_id = get_post_meta( $post_id, 'fb_author_post_id', true );
			if ( $fb_author_post_id ) {
				try {
					Facebook_WP_Extend::graph_api_with_app_access_token( $fb_author_post_id, 'DELETE' );
				} catch (WP_FacebookApiException $e) {}
			}
			unset( $fb_author_post_id );

			// support old post mentions
			$fb_mentioned_pages_post_ids = get_post_meta( $post_id, 'fb_mentioned_pages_post_ids', true );
			if ( $fb_mentioned_pages_post_ids ) {
				foreach( $fb_mentioned_pages_post_ids as $page_post_id ) {
					try {
						Facebook_WP_Extend::graph_api_with_app_access_token( $page_post_id, 'DELETE' );
					} catch (WP_FacebookApiException $e) {}
				}
			}
			unset( $fb_mentioned_pages_post_ids );

			$fb_mentioned_friends_post_ids = get_post_meta( $post_id, 'fb_mentioned_friends_post_ids', true );
			if ( $fb_mentioned_friends_post_ids ) {
				foreach( $fb_mentioned_friends_post_ids as $page_post_id ) {
					try {
						Facebook_WP_Extend::graph_api_with_app_access_token( $page_post_id, 'DELETE' );
					} catch (WP_FacebookApiException $e) {}
				}
			}
			unset( $fb_mentioned_friends_post_ids );
		}
	}
}

?>
