From 82942b64b9e85766ff3b8d787c7112c6be78bed6 Mon Sep 17 00:00:00 2001 From: THEtheChad Date: Sun, 21 Apr 2013 23:29:25 -0500 Subject: [PATCH] Initial Commit --- facebook.php | 963 +++++++++++++++++++++++++++++++++++++++ readme.txt | 85 ++++ screenshot-1.png | Bin 0 -> 21702 bytes sync-facebook-events.php | 292 ++++++++++++ 4 files changed, 1340 insertions(+) create mode 100755 facebook.php create mode 100644 readme.txt create mode 100644 screenshot-1.png create mode 100644 sync-facebook-events.php diff --git a/facebook.php b/facebook.php new file mode 100755 index 0000000..374a975 --- /dev/null +++ b/facebook.php @@ -0,0 +1,963 @@ + + */ +class FacebookApiException extends Exception +{ + /** + * The result from the API server that represents the exception information. + */ + protected $result; + + /** + * Make a new API Exception with the given result. + * + * @param Array $result the result from the API server + */ + public function __construct($result) { + $this->result = $result; + + $code = isset($result['error_code']) ? $result['error_code'] : 0; + + if (isset($result['error_description'])) { + // OAuth 2.0 Draft 10 style + $msg = $result['error_description']; + } else if (isset($result['error']) && is_array($result['error'])) { + // OAuth 2.0 Draft 00 style + $msg = $result['error']['message']; + } else if (isset($result['error_msg'])) { + // Rest server style + $msg = $result['error_msg']; + } else { + $msg = 'Unknown Error. Check getResult()'; + } + + parent::__construct($msg, $code); + } + + /** + * Return the associated result object returned by the API server. + * + * @returns Array the result from the API server + */ + public function getResult() { + return $this->result; + } + + /** + * Returns the associated type for the error. This will default to + * 'Exception' when a type is not available. + * + * @return String + */ + public function getType() { + if (isset($this->result['error'])) { + $error = $this->result['error']; + if (is_string($error)) { + // OAuth 2.0 Draft 10 style + return $error; + } else if (is_array($error)) { + // OAuth 2.0 Draft 00 style + if (isset($error['type'])) { + return $error['type']; + } + } + } + return 'Exception'; + } + + /** + * To make debugging easier. + * + * @returns String the string representation of the error + */ + public function __toString() { + $str = $this->getType() . ': '; + if ($this->code != 0) { + $str .= $this->code . ': '; + } + return $str . $this->message; + } +} + +/** + * Provides access to the Facebook Platform. + * + * @author Naitik Shah + */ +class Facebook +{ + /** + * Version. + */ + const VERSION = '2.1.2'; + + /** + * Default options for curl. + */ + public static $CURL_OPTS = array( + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_USERAGENT => 'facebook-php-2.0', + ); + + /** + * List of query parameters that get automatically dropped when rebuilding + * the current URL. + */ + protected static $DROP_QUERY_PARAMS = array( + 'session', + 'signed_request', + ); + + /** + * Maps aliases to Facebook domains. + */ + public static $DOMAIN_MAP = array( + 'api' => 'https://api.facebook.com/', + 'api_read' => 'https://api-read.facebook.com/', + 'graph' => 'https://graph.facebook.com/', + 'www' => 'https://www.facebook.com/', + ); + + /** + * The Application ID. + */ + protected $appId; + + /** + * The Application API Secret. + */ + protected $apiSecret; + + /** + * The active user session, if one is available. + */ + protected $session; + + /** + * The data from the signed_request token. + */ + protected $signedRequest; + + /** + * Indicates that we already loaded the session as best as we could. + */ + protected $sessionLoaded = false; + + /** + * Indicates if Cookie support should be enabled. + */ + protected $cookieSupport = false; + + /** + * Base domain for the Cookie. + */ + protected $baseDomain = ''; + + /** + * Indicates if the CURL based @ syntax for file uploads is enabled. + */ + protected $fileUploadSupport = false; + + /** + * Initialize a Facebook Application. + * + * The configuration: + * - appId: the application ID + * - secret: the application secret + * - cookie: (optional) boolean true to enable cookie support + * - domain: (optional) domain for the cookie + * - fileUpload: (optional) boolean indicating if file uploads are enabled + * + * @param Array $config the application configuration + */ + public function __construct($config) { + $this->setAppId($config['appId']); + $this->setApiSecret($config['secret']); + if (isset($config['cookie'])) { + $this->setCookieSupport($config['cookie']); + } + if (isset($config['domain'])) { + $this->setBaseDomain($config['domain']); + } + if (isset($config['fileUpload'])) { + $this->setFileUploadSupport($config['fileUpload']); + } + } + + /** + * Set the Application ID. + * + * @param String $appId the Application ID + */ + public function setAppId($appId) { + $this->appId = $appId; + return $this; + } + + /** + * Get the Application ID. + * + * @return String the Application ID + */ + public function getAppId() { + return $this->appId; + } + + /** + * Set the API Secret. + * + * @param String $appId the API Secret + */ + public function setApiSecret($apiSecret) { + $this->apiSecret = $apiSecret; + return $this; + } + + /** + * Get the API Secret. + * + * @return String the API Secret + */ + public function getApiSecret() { + return $this->apiSecret; + } + + /** + * Set the Cookie Support status. + * + * @param Boolean $cookieSupport the Cookie Support status + */ + public function setCookieSupport($cookieSupport) { + $this->cookieSupport = $cookieSupport; + return $this; + } + + /** + * Get the Cookie Support status. + * + * @return Boolean the Cookie Support status + */ + public function useCookieSupport() { + return $this->cookieSupport; + } + + /** + * Set the base domain for the Cookie. + * + * @param String $domain the base domain + */ + public function setBaseDomain($domain) { + $this->baseDomain = $domain; + return $this; + } + + /** + * Get the base domain for the Cookie. + * + * @return String the base domain + */ + public function getBaseDomain() { + return $this->baseDomain; + } + + /** + * Set the file upload support status. + * + * @param String $domain the base domain + */ + public function setFileUploadSupport($fileUploadSupport) { + $this->fileUploadSupport = $fileUploadSupport; + return $this; + } + + /** + * Get the file upload support status. + * + * @return String the base domain + */ + public function useFileUploadSupport() { + return $this->fileUploadSupport; + } + + /** + * Get the data from a signed_request token + * + * @return String the base domain + */ + public function getSignedRequest() { + if (!$this->signedRequest) { + if (isset($_REQUEST['signed_request'])) { + $this->signedRequest = $this->parseSignedRequest( + $_REQUEST['signed_request']); + } + } + return $this->signedRequest; + } + + /** + * Set the Session. + * + * @param Array $session the session + * @param Boolean $write_cookie indicate if a cookie should be written. this + * value is ignored if cookie support has been disabled. + */ + public function setSession($session=null, $write_cookie=true) { + $session = $this->validateSessionObject($session); + $this->sessionLoaded = true; + $this->session = $session; + if ($write_cookie) { + $this->setCookieFromSession($session); + } + return $this; + } + + /** + * Get the session object. This will automatically look for a signed session + * sent via the signed_request, Cookie or Query Parameters if needed. + * + * @return Array the session + */ + public function getSession() { + if (!$this->sessionLoaded) { + $session = null; + $write_cookie = true; + + // try loading session from signed_request in $_REQUEST + $signedRequest = $this->getSignedRequest(); + if ($signedRequest) { + // sig is good, use the signedRequest + $session = $this->createSessionFromSignedRequest($signedRequest); + } + + // try loading session from $_REQUEST + if (!$session && isset($_REQUEST['session'])) { + $session = json_decode( + get_magic_quotes_gpc() + ? stripslashes($_REQUEST['session']) + : $_REQUEST['session'], + true + ); + $session = $this->validateSessionObject($session); + } + + // try loading session from cookie if necessary + if (!$session && $this->useCookieSupport()) { + $cookieName = $this->getSessionCookieName(); + if (isset($_COOKIE[$cookieName])) { + $session = array(); + parse_str(trim( + get_magic_quotes_gpc() + ? stripslashes($_COOKIE[$cookieName]) + : $_COOKIE[$cookieName], + '"' + ), $session); + $session = $this->validateSessionObject($session); + // write only if we need to delete a invalid session cookie + $write_cookie = empty($session); + } + } + + $this->setSession($session, $write_cookie); + } + + return $this->session; + } + + /** + * Get the UID from the session. + * + * @return String the UID if available + */ + public function getUser() { + $session = $this->getSession(); + return $session ? $session['uid'] : null; + } + + /** + * Gets a OAuth access token. + * + * @return String the access token + */ + public function getAccessToken() { + $session = $this->getSession(); + // either user session signed, or app signed + if ($session) { + return $session['access_token']; + } else { + return $this->getAppId() .'|'. $this->getApiSecret(); + } + } + + /** + * Get a Login URL for use with redirects. By default, full page redirect is + * assumed. If you are using the generated URL with a window.open() call in + * JavaScript, you can pass in display=popup as part of the $params. + * + * The parameters: + * - next: the url to go to after a successful login + * - cancel_url: the url to go to after the user cancels + * - req_perms: comma separated list of requested extended perms + * - display: can be "page" (default, full page) or "popup" + * + * @param Array $params provide custom parameters + * @return String the URL for the login flow + */ + public function getLoginUrl($params=array()) { + $currentUrl = $this->getCurrentUrl(); + return $this->getUrl( + 'www', + 'login.php', + array_merge(array( + 'api_key' => $this->getAppId(), + 'cancel_url' => $currentUrl, + 'display' => 'page', + 'fbconnect' => 1, + 'next' => $currentUrl, + 'return_session' => 1, + 'session_version' => 3, + 'v' => '1.0', + ), $params) + ); + } + + /** + * Get a Logout URL suitable for use with redirects. + * + * The parameters: + * - next: the url to go to after a successful logout + * + * @param Array $params provide custom parameters + * @return String the URL for the logout flow + */ + public function getLogoutUrl($params=array()) { + return $this->getUrl( + 'www', + 'logout.php', + array_merge(array( + 'next' => $this->getCurrentUrl(), + 'access_token' => $this->getAccessToken(), + ), $params) + ); + } + + /** + * Get a login status URL to fetch the status from facebook. + * + * The parameters: + * - ok_session: the URL to go to if a session is found + * - no_session: the URL to go to if the user is not connected + * - no_user: the URL to go to if the user is not signed into facebook + * + * @param Array $params provide custom parameters + * @return String the URL for the logout flow + */ + public function getLoginStatusUrl($params=array()) { + return $this->getUrl( + 'www', + 'extern/login_status.php', + array_merge(array( + 'api_key' => $this->getAppId(), + 'no_session' => $this->getCurrentUrl(), + 'no_user' => $this->getCurrentUrl(), + 'ok_session' => $this->getCurrentUrl(), + 'session_version' => 3, + ), $params) + ); + } + + /** + * Make an API call. + * + * @param Array $params the API call parameters + * @return the decoded response + */ + public function api(/* polymorphic */) { + $args = func_get_args(); + if (is_array($args[0])) { + return $this->_restserver($args[0]); + } else { + return call_user_func_array(array($this, '_graph'), $args); + } + } + + /** + * Invoke the old restserver.php endpoint. + * + * @param Array $params method call object + * @return the decoded response object + * @throws FacebookApiException + */ + protected function _restserver($params) { + // generic application level parameters + $params['api_key'] = $this->getAppId(); + $params['format'] = 'json-strings'; + + $result = json_decode($this->_oauthRequest( + $this->getApiUrl($params['method']), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error_code'])) { + throw new FacebookApiException($result); + } + return $result; + } + + /** + * Invoke the Graph API. + * + * @param String $path the path (required) + * @param String $method the http method (default 'GET') + * @param Array $params the query/post data + * @return the decoded response object + * @throws FacebookApiException + */ + protected function _graph($path, $method='GET', $params=array()) { + if (is_array($method) && empty($params)) { + $params = $method; + $method = 'GET'; + } + $params['method'] = $method; // method override as we always do a POST + + $result = json_decode($this->_oauthRequest( + $this->getUrl('graph', $path), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error'])) { + $e = new FacebookApiException($result); + switch ($e->getType()) { + // OAuth 2.0 Draft 00 style + case 'OAuthException': + // OAuth 2.0 Draft 10 style + case 'invalid_token': + $this->setSession(null); + } + throw $e; + } + return $result; + } + + /** + * Make a OAuth Request + * + * @param String $path the path (required) + * @param Array $params the query/post data + * @return the decoded response object + * @throws FacebookApiException + */ + protected function _oauthRequest($url, $params) { + if (!isset($params['access_token'])) { + $params['access_token'] = $this->getAccessToken(); + } + + // json_encode all params values that are not strings + foreach ($params as $key => $value) { + if (!is_string($value)) { + $params[$key] = json_encode($value); + } + } + return $this->makeRequest($url, $params); + } + + /** + * Makes an HTTP request. This method can be overriden by subclasses if + * developers want to do fancier things or use something other than curl to + * make the request. + * + * @param String $url the URL to make the request to + * @param Array $params the parameters to use for the POST body + * @param CurlHandler $ch optional initialized curl handle + * @return String the response text + */ + protected function makeRequest($url, $params, $ch=null) { + if (!$ch) { + $ch = curl_init(); + } + + $opts = self::$CURL_OPTS; + if ($this->useFileUploadSupport()) { + $opts[CURLOPT_POSTFIELDS] = $params; + } else { + $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); + } + $opts[CURLOPT_URL] = $url; + + // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait + // for 2 seconds if the server does not support this header. + if (isset($opts[CURLOPT_HTTPHEADER])) { + $existing_headers = $opts[CURLOPT_HTTPHEADER]; + $existing_headers[] = 'Expect:'; + $opts[CURLOPT_HTTPHEADER] = $existing_headers; + } else { + $opts[CURLOPT_HTTPHEADER] = array('Expect:'); + } + + curl_setopt_array($ch, $opts); + $result = curl_exec($ch); + + if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT + self::errorLog('Invalid or no certificate authority found, using bundled information'); + curl_setopt($ch, CURLOPT_CAINFO, + dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); + $result = curl_exec($ch); + } + + if ($result === false) { + $e = new FacebookApiException(array( + 'error_code' => curl_errno($ch), + 'error' => array( + 'message' => curl_error($ch), + 'type' => 'CurlException', + ), + )); + curl_close($ch); + throw $e; + } + curl_close($ch); + return $result; + } + + /** + * The name of the Cookie that contains the session. + * + * @return String the cookie name + */ + protected function getSessionCookieName() { + return 'fbs_' . $this->getAppId(); + } + + /** + * Set a JS Cookie based on the _passed in_ session. It does not use the + * currently stored session -- you need to explicitly pass it in. + * + * @param Array $session the session to use for setting the cookie + */ + protected function setCookieFromSession($session=null) { + if (!$this->useCookieSupport()) { + return; + } + + $cookieName = $this->getSessionCookieName(); + $value = 'deleted'; + $expires = time() - 3600; + $domain = $this->getBaseDomain(); + if ($session) { + $value = '"' . http_build_query($session, null, '&') . '"'; + if (isset($session['base_domain'])) { + $domain = $session['base_domain']; + } + $expires = $session['expires']; + } + + // prepend dot if a domain is found + if ($domain) { + $domain = '.' . $domain; + } + + // if an existing cookie is not set, we dont need to delete it + if ($value == 'deleted' && empty($_COOKIE[$cookieName])) { + return; + } + + if (headers_sent()) { + self::errorLog('Could not set cookie. Headers already sent.'); + + // ignore for code coverage as we will never be able to setcookie in a CLI + // environment + // @codeCoverageIgnoreStart + } else { + setcookie($cookieName, $value, $expires, '/', $domain); + } + // @codeCoverageIgnoreEnd + } + + /** + * Validates a session_version=3 style session object. + * + * @param Array $session the session object + * @return Array the session object if it validates, null otherwise + */ + protected function validateSessionObject($session) { + // make sure some essential fields exist + if (is_array($session) && + isset($session['uid']) && + isset($session['access_token']) && + isset($session['sig'])) { + // validate the signature + $session_without_sig = $session; + unset($session_without_sig['sig']); + $expected_sig = self::generateSignature( + $session_without_sig, + $this->getApiSecret() + ); + if ($session['sig'] != $expected_sig) { + self::errorLog('Got invalid session signature in cookie.'); + $session = null; + } + // check expiry time + } else { + $session = null; + } + return $session; + } + + /** + * Returns something that looks like our JS session object from the + * signed token's data + * + * TODO: Nuke this once the login flow uses OAuth2 + * + * @param Array the output of getSignedRequest + * @return Array Something that will work as a session + */ + protected function createSessionFromSignedRequest($data) { + if (!isset($data['oauth_token'])) { + return null; + } + + $session = array( + 'uid' => $data['user_id'], + 'access_token' => $data['oauth_token'], + 'expires' => $data['expires'], + ); + + // put a real sig, so that validateSignature works + $session['sig'] = self::generateSignature( + $session, + $this->getApiSecret() + ); + + return $session; + } + + /** + * Parses a signed_request and validates the signature. + * Then saves it in $this->signed_data + * + * @param String A signed token + * @param Boolean Should we remove the parts of the payload that + * are used by the algorithm? + * @return Array the payload inside it or null if the sig is wrong + */ + protected function parseSignedRequest($signed_request) { + list($encoded_sig, $payload) = explode('.', $signed_request, 2); + + // decode the data + $sig = self::base64UrlDecode($encoded_sig); + $data = json_decode(self::base64UrlDecode($payload), true); + + if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') { + self::errorLog('Unknown algorithm. Expected HMAC-SHA256'); + return null; + } + + // check sig + $expected_sig = hash_hmac('sha256', $payload, + $this->getApiSecret(), $raw = true); + if ($sig !== $expected_sig) { + self::errorLog('Bad Signed JSON signature!'); + return null; + } + + return $data; + } + + /** + * Build the URL for api given parameters. + * + * @param $method String the method name. + * @return String the URL for the given parameters + */ + protected function getApiUrl($method) { + static $READ_ONLY_CALLS = + array('admin.getallocation' => 1, + 'admin.getappproperties' => 1, + 'admin.getbannedusers' => 1, + 'admin.getlivestreamvialink' => 1, + 'admin.getmetrics' => 1, + 'admin.getrestrictioninfo' => 1, + 'application.getpublicinfo' => 1, + 'auth.getapppublickey' => 1, + 'auth.getsession' => 1, + 'auth.getsignedpublicsessiondata' => 1, + 'comments.get' => 1, + 'connect.getunconnectedfriendscount' => 1, + 'dashboard.getactivity' => 1, + 'dashboard.getcount' => 1, + 'dashboard.getglobalnews' => 1, + 'dashboard.getnews' => 1, + 'dashboard.multigetcount' => 1, + 'dashboard.multigetnews' => 1, + 'data.getcookies' => 1, + 'events.get' => 1, + 'events.getmembers' => 1, + 'fbml.getcustomtags' => 1, + 'feed.getappfriendstories' => 1, + 'feed.getregisteredtemplatebundlebyid' => 1, + 'feed.getregisteredtemplatebundles' => 1, + 'fql.multiquery' => 1, + 'fql.query' => 1, + 'friends.arefriends' => 1, + 'friends.get' => 1, + 'friends.getappusers' => 1, + 'friends.getlists' => 1, + 'friends.getmutualfriends' => 1, + 'gifts.get' => 1, + 'groups.get' => 1, + 'groups.getmembers' => 1, + 'intl.gettranslations' => 1, + 'links.get' => 1, + 'notes.get' => 1, + 'notifications.get' => 1, + 'pages.getinfo' => 1, + 'pages.isadmin' => 1, + 'pages.isappadded' => 1, + 'pages.isfan' => 1, + 'permissions.checkavailableapiaccess' => 1, + 'permissions.checkgrantedapiaccess' => 1, + 'photos.get' => 1, + 'photos.getalbums' => 1, + 'photos.gettags' => 1, + 'profile.getinfo' => 1, + 'profile.getinfooptions' => 1, + 'stream.get' => 1, + 'stream.getcomments' => 1, + 'stream.getfilters' => 1, + 'users.getinfo' => 1, + 'users.getloggedinuser' => 1, + 'users.getstandardinfo' => 1, + 'users.hasapppermission' => 1, + 'users.isappuser' => 1, + 'users.isverified' => 1, + 'video.getuploadlimits' => 1); + $name = 'api'; + if (isset($READ_ONLY_CALLS[strtolower($method)])) { + $name = 'api_read'; + } + return self::getUrl($name, 'restserver.php'); + } + + /** + * Build the URL for given domain alias, path and parameters. + * + * @param $name String the name of the domain + * @param $path String optional path (without a leading slash) + * @param $params Array optional query parameters + * @return String the URL for the given parameters + */ + protected function getUrl($name, $path='', $params=array()) { + $url = self::$DOMAIN_MAP[$name]; + if ($path) { + if ($path[0] === '/') { + $path = substr($path, 1); + } + $url .= $path; + } + if ($params) { + $url .= '?' . http_build_query($params, null, '&'); + } + return $url; + } + + /** + * Returns the Current URL, stripping it of known FB parameters that should + * not persist. + * + * @return String the current URL + */ + protected function getCurrentUrl() { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' + ? 'https://' + : 'http://'; + $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $parts = parse_url($currentUrl); + + // drop known fb params + $query = ''; + if (!empty($parts['query'])) { + $params = array(); + parse_str($parts['query'], $params); + foreach(self::$DROP_QUERY_PARAMS as $key) { + unset($params[$key]); + } + if (!empty($params)) { + $query = '?' . http_build_query($params, null, '&'); + } + } + + // use port if non default + $port = + isset($parts['port']) && + (($protocol === 'http://' && $parts['port'] !== 80) || + ($protocol === 'https://' && $parts['port'] !== 443)) + ? ':' . $parts['port'] : ''; + + // rebuild + return $protocol . $parts['host'] . $port . $parts['path'] . $query; + } + + /** + * Generate a signature for the given params and secret. + * + * @param Array $params the parameters to sign + * @param String $secret the secret to sign with + * @return String the generated signature + */ + protected static function generateSignature($params, $secret) { + // work with sorted data + ksort($params); + + // generate the base string + $base_string = ''; + foreach($params as $key => $value) { + $base_string .= $key . '=' . $value; + } + $base_string .= $secret; + + return md5($base_string); + } + + /** + * Prints to the error log if you aren't in command line mode. + * + * @param String log message + */ + protected static function errorLog($msg) { + // disable error log if we are running in a CLI environment + // @codeCoverageIgnoreStart + if (php_sapi_name() != 'cli') { + error_log($msg); + } + // uncomment this if you want to see the errors on the page + // print 'error_log: '.$msg."\n"; + // @codeCoverageIgnoreEnd + } + + /** + * Base64 encoding that doesn't need to be urlencode()ed. + * Exactly the same as base64_encode except it uses + * - instead of + + * _ instead of / + * + * @param String base64UrlEncodeded string + */ + protected static function base64UrlDecode($input) { + return base64_decode(strtr($input, '-_', '+/')); + } +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..cddf63f --- /dev/null +++ b/readme.txt @@ -0,0 +1,85 @@ +=== Sync Facebook Events === +Contributors: markpdxt, scottconnerly +Tags: facebook, events, synchronize, calendar +Requires at least: 3.0 +Tested up to: 3.4.2 +Stable tag: 1.0.8 + +A simple plugin to Sync Facebook events to The Events Calendar plugin by Modern Tribe. + +== Description == + +A simple plugin to Sync Facebook events to The Events Calendar plugin by Modern Tribe. + +Get The Events Calendar plugin: +http://wordpress.org/extend/plugins/the-events-calendar/ + +== Installation == + +1. Download the plugin archive and expand it +2. Upload the sync-facebook-events folder to your /wp-content/plugins/ directory +3. Go to the plugins page and click 'Activate' for Sync FB Events +4. Navigate to the Settings section within Wordpress and enter your Facebook App ID, App Secret & UID. +5. Ensure The Events Calendar plugin is installed and configured - http://wordpress.org/extend/plugins/the-events-calendar/ +5. Press 'Update' to synchronize your current Facebook events for display within The Events Calendar. +6. Synchronization will continue to occur on the schedule you set. You can always update manually if/when needed. + +== Frequently Asked Questions == + +Q: What is the Facebook App ID and App Secret, and why are they required? + +A: The Facebook App ID and App Secret are required by Facebook to access data via the Facebook graph API. +To signup for a developer account or learn more see - http://developers.facebook.com/docs/guides/canvas/ + +Q: How do I find the Facebook UID of the page for which I wish to synchronize events? + +A: Goto the page you're interested in - ex. https://www.facebook.com/webtrends +Copy the URL and replace 'www' with 'graph' - ex. https://graph.facebook.com/webtrends +The UID is the first item in the resulting text. In this example it is "54905721286". + +Q: Do my Facebook events get updated on a schedule? + +A: Yes, You can choose the update interval and also update immediately when you press the 'Update' button from the Sync FB Events section within settings. + +Q: Why do I get a blank screen when running an update? + +A: Check your Facebook App ID, Facebook App Secret and Facebook UID. One of them is probably incorrect. + +Q: Why doesn't Modern Tribe just provide this functionality within their own plugin? + +A: They incorporated this one, actually. + +== Upgrade Notice == + +Upgrade Notice + +== Screenshots == + +1. Facebook Event Sync Configuration + +== Changelog == + += 1.0.7 / 1.0.8 = +* Fixing the duplicate events issue (finally). + += 1.0.6 = +* Adding the correct post_type for modern tribe events + += 1.0.5 = +* Minor compatibility fix for Wordpress 3.1 + += 1.0.4 = +* Added ability to allow event synchronization from multiple Facebook pages + += 1.0.3 = +* Added ability to adjust event sync frequency + += 1.0.2 = +* Added automatic daily event sync +* Added ability to adjust event timezone to WordPress + += 1.0.1 = +* Improved update display + += 1.0 = +* Initial release \ No newline at end of file diff --git a/screenshot-1.png b/screenshot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..37df490124919ea426fbabed0bfdea88838d7cc3 GIT binary patch literal 21702 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 literal 0 HcmV?d00001 diff --git a/sync-facebook-events.php b/sync-facebook-events.php new file mode 100644 index 0000000..c44cc45 --- /dev/null +++ b/sync-facebook-events.php @@ -0,0 +1,292 @@ + $fbes_api_key, + 'secret' => $fbes_api_secret, + 'cookie' => true, + )); + + $ret = array(); + foreach ($fbes_api_uids as $key => $value) { + + if($value!='') { + //https://developers.facebook.com/docs/reference/fql/event/ + $fql = "SELECT eid, name, start_time, end_time, location, description + FROM event WHERE eid IN ( SELECT eid FROM event_member WHERE uid = $value ) + ORDER BY start_time desc"; + + $param = array( + 'method' => 'fql.query', + 'query' => $fql, + 'callback' => '' + ); + + $result = $facebook->api($param); + foreach($result as $k => $v) + $result[$k]['uid'] = $value; + $ret = array_merge($ret, $result); + } + } + + + return $ret; +} + +function fbes_segments($url='') { + $parsed_url = parse_url($url); + $path = trim($parsed_url['path'],'/'); + return explode('/',$path); +} + +function fbes_send_events($events) { + + $query = new WP_Query(array( + 'post_type'=>'tribe_events', + 'posts_per_page'=>'-1' + )); + + foreach($query->posts as $post) { + if(!empty($post->to_ping)) { + $segments = fbes_segments($post->to_ping); + $eid = array_pop($segments); + $eids[$eid] = $post->ID; + } +//if you're reading this and you want to delete all those duplicate events, uncomment this temporarially. Note, it will also delete all manually made events since June 13 +//http://codex.wordpress.org/Version_3.4 - June 13, 2012 +//depending on many duplicates you had, you might end up re-loading this script a bunch of times after it times out. Me, I had 14k duplicates. Had to run the script like 10 times. +/* + else { + $post_date = trim(substr($post->post_date, 0, 10)); + if($post->post_date > '2012-06-12') + wp_delete_post($post->ID); + } +*/ + } + //file_put_contents($_SERVER['DOCUMENT_ROOT'].'/fbevent.log', print_r(array(time(),$events,$eids),1)."\n".str_repeat('=',40)."\n", FILE_APPEND); + + foreach($events as $event) { + + $args['post_title'] = $event['name']; + + $offset = get_option('gmt_offset')*3600; + + $offsetStart = $event['start_time']+$offset; + $offsetEnd = $event['end_time']+$offset; + + //don't update or insert events from the past. + if($offsetEnd > time()) { + + $args['EventStartDate'] = date("m/d/Y", $offsetStart); + $args['EventStartHour'] = date("H", $offsetStart); + $args['EventStartMinute'] = date("i", $offsetStart); + + $args['EventEndDate'] = date("m/d/Y", $offsetEnd); + $args['EventEndHour'] = date("H", $offsetEnd); + $args['EventEndMinute'] = date("i", $offsetEnd); + + $args['post_content'] = $event['description']; + $args['Venue']['Venue'] = $event['location']; + + $args['post_status'] = "Publish"; + $args['post_type'] = "tribe_events"; + //$args['to_ping'] = $event['eid']; //damn you, sanitize_trackback_urls in 3.4 + $args['to_ping'] = 'https://www.facebook.com/events/'.$event['eid'].'/'; + + if($args['EventStartHour'] == '22' && $event['uid'] == '256763181050120') { //why are UT events 2 hours off??? + $args['EventStartHour'] = '20'; + $args['EventEndHour'] = '22'; + $args['EventEndDate'] = date('m/d/Y',strtotime($args['EventEndDate'], '-1 day')); + } + + $inserting = $post_id = false; + if (!array_key_exists($event['eid'], $eids)) { + //double check + $already_exists = false; + foreach($query->posts as $post) { + if($post->to_ping == $args['to_ping'] || trim($post->pinged) == $args['to_ping']) { + $already_exists = true; + } + } + if(!$already_exists) { + file_put_contents($_SERVER['DOCUMENT_ROOT'].'/fbevent.log', print_r(array(time(),'creating', $args, $eids, $query->posts),1)."\n".str_repeat('=',40)."\n", FILE_APPEND); + $post_id = tribe_create_event($args); + echo "
Inserting: ".$post_id; + $inserting = true; + } + } + if(!$inserting) { + $post_id = $eids[$event['eid']]; + tribe_update_event($post_id, $args); + echo "
Updating: ".$eids[$event['eid']]; + } + if($post_id) + update_metadata('post', $post_id, 'fb_event_obj', $event); + //eid, name, start_time, end_time, location, description + } + reset($eids); + } +} + +function fbes_options_page() { + + $fbes_api_uids = array(); + + #Get option values + $fbes_api_key = get_option('fbes_api_key'); + $fbes_api_secret = get_option('fbes_api_secret'); + $fbes_api_uid = get_option('fbes_api_uid'); + $fbes_api_uids = get_option('fbes_api_uids'); + $fbes_frequency = get_option('fbes_frequency'); + + #Get new updated option values, and save them + if( !empty($_POST['update']) ) { + + $fbes_api_key = $_POST['fbes_api_key']; + update_option('fbes_api_key', $fbes_api_key); + + $fbes_api_secret = $_POST['fbes_api_secret']; + update_option('fbes_api_secret', $fbes_api_secret); + + $fbes_api_uid = $_POST['fbes_api_uid']; + update_option('fbes_api_uid', $fbes_api_uid); + + $fbes_frequency = $_POST['fbes_frequency']; + update_option('fbes_frequency', $fbes_frequency); + + $events = fbes_get_events($fbes_api_key, $fbes_api_secret, $fbes_api_uids); + + update_schedule($fbes_frequency); + + $msg = "Syncronization of Events from Facebook Complete."; +?> +

+ $value) + if($fbes_api_uids[$key] == $_GET['r']) + unset($fbes_api_uids[$key]); + + update_option('fbes_api_uids', $fbes_api_uids); + } +?> +
+

+

Sync Facebook Events

+
+ + '; + echo ''; + echo ''; + + echo ''; + + echo ''; + + echo '
Facebook App ID:
Facebook App Secret:
Update Fequency:'; + + echo '
Add Facebook Page UID:'; + echo '
'; + + foreach ($fbes_api_uids as $value) { + if($value!='') + echo '  '.$value.'  remove
'; + } + + echo '

'; + ?> +
+
+ +
+ Updaing all facebook events...
+
+ Events Calendar updated with current Facebook events.

+
+ +