<?php
/**
* Facebook implementation for the service.
*
* @package Social
* @subpackage services
*/
final class Social_Service_Facebook extends Social_Service implements Social_Interface_Service {
/**
* @var string service key
*/
protected $_key = 'facebook';
/**
* The max length a post can be when broadcasted.
*
* @return int
*/
public function max_broadcast_length() {
return 420;
}
/**
* Handles the requests to the proxy.
*
* @param Social_Service_Account|int $account
* @param string $api
* @param array $args
* @param string $method
* @return Social_Response|bool
*/
public function request($account, $api, array $args = array(), $method = 'GET') {
$api = urlencode($api);
return parent::request($account, $api, $args, $method);
}
/**
* Any additional parameters that should be passed with a broadcast.
*
* @static
* @return array
*/
public function get_broadcast_extras($account_id, $post, $args = array()) {
if (get_post_format($post->ID) !== 'status') {
setup_postdata($post);
$link_args = array(
'link' => get_post_permalink($post->ID),
'title' => get_the_title($post->ID),
'description' => get_the_excerpt(),
);
if (function_exists('has_post_thumbnail') and has_post_thumbnail($post->ID)) {
$image = wp_get_attachment_image_src(get_post_thumbnail_id($post->ID), 'single-post-thumbnail');
$link_args = $link_args + array(
'picture' => $image[0],
);
}
wp_reset_postdata();
$args = $args + $link_args;
}
return parent::get_broadcast_extras($account_id, $post, $args);
}
/**
* Broadcasts the message to the specified account. Returns the broadcasted ID.
*
* @param Social_Service_Facebook_Account|object $account account to broadcast to
* @param string $message message to broadcast
* @param array $args extra arguments to pass to the request
* @param int $post_id post ID being broadcasted
* @param int $comment_id comment ID being broadcasted
*
* @return Social_Response
*/
public function broadcast($account, $message, array $args = array(), $post_id = null, $comment_id = null) {
global $post;
// if post ID is set, this is a broadcast of a post,
// if the comment ID is set it is a broadcast of a comment
// TODO - add wrapper functions that abstract these actions out to separate methods
// check comment being replied to, if it is a facebook comment on a post then
// send the comment as a reply on the same post.
// If that fails, then send as posting a link with a comment.
$args = $args + array(
'message' => $message,
);
// first try to send comment to an existing Fb post
if (!is_null($comment_id)) {
$comment = get_comment($comment_id);
if (!empty($comment->comment_parent)) {
$parent_comment = get_comment($comment->comment_parent);
if (!is_null($parent_comment) && in_array($parent_comment->comment_type, self::comment_types())) {
$status_id = get_comment_meta($parent_comment->comment_ID, 'social_status_id', true);
if (!empty($status_id)) {
// we have a Facebook post to reply to
$parts = explode('_', $status_id);
if (count($parts) == 3) {
$status_id = $parts[0].'_'.$parts[1];
}
$args = apply_filters($this->key().'_broadcast_args', $args, $post_id, $comment_id);
$response = $this->request($account, $status_id.'/comments', $args, 'POST');
if ($response !== false && $response->id() !== '0') {
// post succeeded, return response
return $response;
}
// ...broadcast failed, continue and send as post to feed
}
}
}
// posting with a link, do not include URL in comment.
$format = trim(str_replace('{url}', '', Social::option('comment_broadcast_format')));
$message = $this->format_comment_content($comment, $format);
$args['message'] = $message;
// prep data
$post = get_post($comment->comment_post_ID);
setup_postdata($post);
$link_args = array(
'link' => get_post_permalink($post->ID),
'title' => get_the_title($post->ID),
'description' => get_the_excerpt(),
);
if (function_exists('has_post_thumbnail') and has_post_thumbnail($post->ID)) {
$image = wp_get_attachment_image_src(get_post_thumbnail_id($post->ID), 'single-post-thumbnail');
$link_args = $link_args + array(
'picture' => $image[0],
);
}
wp_reset_postdata();
$args = $args + $link_args;
}
// Set access token?
$broadcast_account = $account->broadcast_page();
if ($broadcast_account !== null) {
$args = $args + array(
'access_token' => $broadcast_account->access_token,
'page_id' => $broadcast_account->id,
);
}
$args = apply_filters($this->key().'_broadcast_args', $args, $post_id, $comment_id);
$endpoint = isset($args['link']) ? 'links' : 'feed';
return $this->request($account, 'me/'.$endpoint, $args, 'POST');
}
/**
* Aggregates comments by URL.
*
* @param object $post
* @param array $urls
*
* @return void
*/
public function aggregate_by_url(&$post, array $urls) {
foreach ($urls as $url) {
if (!empty($url)) {
$url = 'https://graph.facebook.com/search?type=post&q='.$url;
Social::log('Searching by URL(s) for post #:post_id. (Query: :url)', array(
'post_id' => $post->ID,
'url' => $url,
));
$response = wp_remote_get($url);
if (!is_wp_error($response)) {
$response = json_decode($response['body']);
if (isset($response->data) and is_array($response->data) and count($response->data)) {
foreach ($response->data as $result) {
if (in_array($result->id, $post->aggregated_ids[$this->_key])) {
Social_Aggregation_Log::instance($post->ID)->add($this->_key, $result->id, 'url', true);
continue;
}
else {
if ($this->is_original_broadcast($post, $result->id)) {
continue;
}
}
Social_Aggregation_Log::instance($post->ID)->add($this->_key, $result->id, 'url');
$post->aggregated_ids[$this->_key][] = $result->id;
$post->results[$this->_key][$result->id] = $result;
}
}
}
else {
Social::log('URL search failed for post #:post_id.', array(
'post_id' => $post->ID,
));
}
}
}
}
/**
* Aggregates comments by the service's API.
*
* @param object $post
*
* @return array
*/
public function aggregate_by_api(&$post) {
// find broadcasts for service
$accounts = $this->get_aggregation_accounts($post);
if (isset($accounts[$this->_key]) and count($accounts[$this->_key])) {
$like_count = 0;
foreach ($accounts[$this->_key] as $account) {
if (isset($post->broadcasted_ids[$this->_key][$account->id()])) {
foreach ($post->broadcasted_ids[$this->_key][$account->id()] as $broadcasted_id => $data) {
$id = explode('_', $broadcasted_id);
$request = $this->request($account, $broadcasted_id.'/comments');
if ($request !== false && isset($request->body()->response)) {
$response = $request->body()->response;
if (isset($response->data) and is_array($response->data) and count($response->data)) {
foreach ($response->data as $result) {
$data = array(
'parent_id' => $broadcasted_id,
);
if (in_array($result->id, $post->aggregated_ids[$this->_key])) {
Social_Aggregation_Log::instance($post->ID)->add($this->_key, $result->id, 'reply', true, $data);
continue;
}
else {
if ($this->is_original_broadcast($post, $result->id)) {
continue;
}
}
Social_Aggregation_Log::instance($post->ID)->add($this->_key, $result->id, 'reply', false, $data);
$result->status_id = $broadcasted_id;
$post->results[$this->_key][$result->id] = $result;
}
}
}
$this->search_for_likes($account, $broadcasted_id, $id[0], $post, $like_count);
}
}
}
if (count($like_count)) {
Social_Aggregation_Log::instance($post->ID)->add($this->_key, $post->ID.time(), 'like', !$like_count, array('total' => $like_count));
}
}
}
/**
* Searches for likes on the post.
*
* @param object $account
* @param string $id
* @param int $parent_id
* @param WP_Post $post
* @param int $like_count
* @param bool|string $next
* @return void
*/
private function search_for_likes(&$account, $id, $parent_id, &$post, &$like_count, $next = false) {
$url = $id.'/likes';
$args = array(
'limit' => '100'
);
if ($next !== false) {
$args['offset'] = $next;
}
$request = $this->request($account, $url, $args);
if ($request !== false && isset($request->body()->response)) {
$response = $request->body()->response;
if (isset($response->data) && is_array($response->data) && count($response->data)) {
foreach ($response->data as $result) {
if ((isset($post->results) && isset($post->results[$this->_key]) && isset($post->results[$this->_key][$result->id])) ||
(in_array($result->id, $post->aggregated_ids[$this->_key]))
) {
continue;
}
$post->aggregated_ids[$this->_key][] = $result->id;
$post->results[$this->_key][$result->id] = (object) array_merge(array(
'like' => true,
'status_id' => $id,
'raw' => $result,
), (array) $result);
++$like_count;
}
}
if (isset($response->paging) && isset($response->paging->next)) {
$url = parse_url($response->paging->next);
if (!empty($url['query'])) {
parse_str($url['query'], $query);
if (!empty($query['offset'])) {
$this->search_for_likes($account, $id, $parent_id, $post, $like_count, $query['offset']);
}
}
}
}
}
/**
* Saves the aggregated comments.
*
* @param object $post
* @return void
*/
public function save_aggregated_comments(&$post) {
if (isset($post->results[$this->_key])) {
global $wpdb;
foreach ($post->results[$this->_key] as $result) {
$commentdata = array(
'comment_post_ID' => $post->ID,
'comment_author_email' => $wpdb->escape($this->_key.'.'.$result->id.'@example.com'),
'comment_author_IP' => $_SERVER['SERVER_ADDR'],
'comment_agent' => 'Social Aggregator'
);
if (!isset($result->like)) {
$url = 'http://graph.facebook.com/'.$result->from->id;
$request = wp_remote_get($url);
if (!is_wp_error($request)) {
$response = json_decode($request['body']);
$account = (object) array(
'user' => $response
);
$class = 'Social_Service_'.$this->_key.'_Account';
$account = new $class($account);
$commentdata = array_merge($commentdata, array(
'comment_type' => 'social-facebook',
'comment_author' => $wpdb->escape($result->from->name),
'comment_author_url' => $account->url(),
'comment_content' => $wpdb->escape($result->message),
'comment_date' => date('Y-m-d H:i:s', strtotime($result->created_time) + (get_option('gmt_offset') * 3600)),
'comment_date_gmt' => gmdate('Y-m-d H:i:s', strtotime($result->created_time)),
));
}
}
else {
$url = 'http://facebook.com/profile.php?id='.$result->id;
$commentdata = array_merge($commentdata, array(
'comment_type' => 'social-facebook-like',
'comment_author' => $wpdb->escape($result->name),
'comment_author_url' => $url,
'comment_content' => $wpdb->escape('<a href="'.$url.'" target="_blank">'.$result->name.'</a> liked this on Facebook.'),
'comment_date' => current_time('mysql'),
'comment_date_gmt' => current_time('mysql', 1),
));
}
$user_id = (isset($result->like) ? $result->id : $result->from->id);
$commentdata = array_merge($commentdata, array(
'comment_post_ID' => $post->ID,
'comment_author_email' => $this->_key.'.'.$user_id.'@example.com',
));
$result_id = (isset($result->status_id) ? $result->status_id : $result->id);
if (apply_filters('social_approve_likes_and_retweets', true) && isset($result->like)) {
$commentdata['comment_approved'] = 1;
}
else if (($commentdata = $this->allow_comment($commentdata, $result_id, $post)) === false) {
continue;
}
// sanity check to make sure this comment is not a duplicate
if ($this->is_duplicate_comment($post, $result->id)) {
Social::log('Result #:result_id already exists, skipping.', array(
'result_id' => $result->id
), 'duplicate-comment');
continue;
}
Social::log('Saving #:result_id.', array(
'result_id' => (isset($result->status_id) ? $result->status_id : $result->id)
));
$comment_id = 0;
try
{
Social::Log('Attempting to save commentdata: :commentdata', array(
'commentdata' => print_r($commentdata, true)
));
$comment_id = wp_insert_comment($commentdata);
update_comment_meta($comment_id, 'social_account_id', addslashes_deep($user_id));
update_comment_meta($comment_id, 'social_profile_image_url', addslashes_deep('http://graph.facebook.com/'.$user_id.'/picture'));
update_comment_meta($comment_id, 'social_status_id', addslashes_deep((isset($result->status_id) ? $result->status_id : $result->id)));
if (!isset($result->raw)) {
$result = (object) array_merge((array) $result, array('raw' => $result));
}
update_comment_meta($comment_id, 'social_raw_data', addslashes_deep(base64_encode(json_encode($result->raw))));
if ($commentdata['comment_approved'] !== 'spam') {
if ($commentdata['comment_approved'] == '0') {
wp_notify_moderator($comment_id);
}
if (get_option('comments_notify') and $commentdata['comment_approved'] and (!isset($commentdata['user_id']) or $post->post_author != $commentdata['user_id'])) {
wp_notify_postauthor($comment_id, 'comment');
}
}
}
catch (Exception $e) {
// Something went wrong, remove the aggregated ID.
if (($key = array_search((isset($result->status_id) ? $result->status_id : $result->id), $post->aggregated_ids['facebook'])) !== false) {
unset($post->aggregated_ids['facebook'][$key]);
}
if ((int) $comment_id) {
// Delete the comment in case it wasn't the insert that failed.
wp_delete_comment($comment_id);
}
}
}
}
}
/**
* Hook to allow services to define their aggregation row items based on the passed in type.
*
* @param string $type
* @param object $item
* @param string $username
* @param int $id
* @return string
*/
public function aggregation_row($type, $item, $username, $id) {
if ($type == 'like') {
return sprintf(__('Found %s additional likes.', 'social'), $item->data['total']);
}
return '';
}
/**
* Checks the response to see if the broadcast limit has been reached.
*
* @param string $response
* @return bool
*/
public function limit_reached($response) {
return ($response == '(#341) Feed action request limit reached');
}
/**
* Checks the response to see if the broadcast is a duplicate.
*
* @param string $response
* @return bool
*/
public function duplicate_status($response) {
return ($response == '(#506) Duplicate status message');
}
/**
* Checks the response to see if the account has been deauthorized.
*
* @param string $response
* @param bool $check_invalid_key
* @return bool
*/
public function deauthorized($response, $check_invalid_key = false) {
if (($check_invalid_key and $response == 'invalid key') or $response == 'Error validating access token') {
return true;
}
return false;
}
/**
* Returns the key to use on the request response to pull the ID.
*
* @return string
*/
public function response_id_key() {
return 'id';
}
/**
* Returns the response message.
*
* @param object $body
* @param string $default
*
* @return mixed
*/
public function response_message($body, $default) {
if (isset($body->response) and isset($body->response->message)) {
return $body->response->message;
}
return $default;
}
/**
* Returns the status URL to a broadcasted item.
*
* @param string $username
* @param string|int $id
* @return string|null
*/
public function status_url($username, $id) {
if (strpos($id, '_') === false) {
return null;
}
$ids = explode('_', $id);
return 'http://facebook.com/permalink.php?story_fbid='.$ids[1].'&id='.$ids[0];
}
/**
* Loads the pages for the account.
*
* @param Social_Service_Account $account
* @param bool $is_profile
* @param bool $save
* @return array
*/
public function get_pages(Social_Service_Account $account, $is_profile = false, $save = true) {
$pages = array();
if ($account->use_pages() or $account->use_pages(true)) {
$response = $this->request($account, $account->id().'/accounts');
if ($response !== false and isset($response->body()->response)) {
if (isset($response->body()->response->data)) {
foreach ($response->body()->response->data as $item) {
if ($item->category != 'Application') {
$pages[$item->id] = $item;
}
}
}
else if ($response->body()->response == 'incorrect method') {
// Account no longer has page permissions.
$service = Social::instance()->service('facebook');
$accounts = $service->accounts();
foreach ($accounts as $account_id => $_account) {
if ($account_id == $account->id()) {
$_account->use_pages(false, false);
$_account->use_pages(true, false);
$_account->pages(array(), $is_profile);
}
$accounts[$account_id] = $account->as_object();
}
if ($save) {
$service->accounts($accounts)->save($is_profile);
}
}
}
}
return $pages;
}
/**
* Builds the page's image URL.
*
* @param object $account
* @return string
*/
public function page_image_url($account) {
return apply_filters('social_facebook_page_image_url', 'http://graph.facebook.com/'.$account->id.'/picture', $account);
}
/**
* Comment types for this service.
*
* @static
* @return array
*/
public static function comment_types() {
return array(
'social-facebook',
'social-facebook-like',
);
}
/**
* Comment types that are "meta". In this case, Likes (and perhaps Shares in the future).
*
* @static
* @return array
*/
public static function comment_types_meta() {
return array(
'social-facebook-like',
);
}
public static function social_settings_save($controller) {
// Save Facebook pages
$is_profile = ($controller->request()->post('social_profile') == 'true');
if ($is_profile and !defined('IS_PROFILE_PAGE')) {
define('IS_PROFILE_PAGE', true);
}
$enabled_child_accounts = $controller->request()->post('social_enabled_child_accounts');
if (!is_array($enabled_child_accounts)) {
$enabled_child_accounts = array();
}
$service = $controller->social()->service('facebook');
if ($service !== false) {
foreach ($service->accounts() as $account) {
$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);
}
}
}
public static function social_settings_default_accounts($accounts, $controller) {
if (is_array($controller->request()->post('social_default_pages'))) {
if (!isset($accounts['facebook'])) {
$accounts['facebook'] = array(
'pages' => array()
);
}
$accounts['facebook']['pages'] = $controller->request()->post('social_default_pages');
}
else {
$accounts['facebook']['pages'] = array();
}
return $accounts;
}
} // End Social_Service_Facebook