= 5.3.0. Requires curl. * * Author: David Wilkinson * Web: http://dopiaza.org/ * * Copyright (c) 2012 David Wilkinson * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of * the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ class WpFlickrEmbed_Flickr { const VERSION = '1.2.3'; /** * Session variable name used to store authentication data */ const SESSION_OAUTH_DATA = 'FlickrSessionOauthData'; /** * Key names for various authentication data items */ const OAUTH_REQUEST_TOKEN = 'oauth_request_token'; const OAUTH_REQUEST_TOKEN_SECRET = 'oauth_request_token_secret'; const OAUTH_VERIFIER = 'oauth_verifier'; const OAUTH_ACCESS_TOKEN = 'oauth_access_token'; const OAUTH_ACCESS_TOKEN_SECRET = 'oauth_access_token_secret'; const USER_NSID = 'user_nsid'; const USER_NAME = 'user_name'; const USER_FULL_NAME = 'user_full_name'; const PERMISSIONS = 'permissions'; const IS_AUTHENTICATING = 'is_authenticating'; /** * Default timeout in seconds for HTTP requests */ const DEFAULT_HTTP_TIMEOUT = 30; /** * Various API endpoints */ const REQUEST_TOKEN_ENDPOINT = 'https://www.flickr.com/services/oauth/request_token'; const AUTH_ENDPOINT = 'https://www.flickr.com/services/oauth/authorize'; const ACCESS_TOKEN_ENDPOINT = 'https://www.flickr.com/services/oauth/access_token'; const API_ENDPOINT = 'https://www.flickr.com/services/rest'; const SECURE_API_ENDPOINT = 'https://secure.flickr.com/services/rest'; const UPLOAD_ENDPOINT = 'https://www.flickr.com/services/upload/'; const REPLACE_ENDPOINT = 'https://www.flickr.com/services/replace/'; /** * @var string Flickr API key */ private $consumerKey; /** * @var string Flickr API secret */ private $consumerSecret; /** * @var string Callback URI for authentication */ private $callback; /** * @var string HTTP Method to use for API calls */ private $method = 'POST'; /** * @var int HTTP Response code for last call made */ private $lastHttpResponseCode; /** * @var int Timeout in seconds for HTTP calls */ private $httpTimeout; /** * Create a new Flickr object * * @param string $key The Flickr API key * @param string $secret The Flickr API secret * @param string $callback The callback URL for authentication */ public function __construct($key, $secret = NULL, $callback = NULL) { session_start(); $this->consumerKey = $key; $this->consumerSecret = $secret; $this->callback = $callback; $this->httpTimeout = self::DEFAULT_HTTP_TIMEOUT; } /** * Call a Flickr API method * * @param string $method The FLickr API method name * @param array $parameters The method parameters * @return mixed|null The response object */ public function call($method, $parameters = NULL) { $signed = $this->getSignedUrlParams($method, $parameters); $response = $this->httpRequest($signed['url'], $signed['params']); return empty($response) ? NULL : unserialize($response); } /** * Get the signed URL and params needed to call a Flickr API method * * If not authenticated then unsigned version is returned. * * @param string $method The FLickr API method name * @param array $parameters The method parameters * @param array $options additional options: * use_secure_api = true|false (default) * * @return array('url' => string, 'params' => array) */ public function getSignedUrlParams($method, $parameters = NULL, $options = array()) { if (!empty($options['use_secure_api'])) { $api_url = self::SECURE_API_ENDPOINT; } else { $api_url = self::API_ENDPOINT; } $requestParams = ($parameters == NULL ? array() : $parameters); $requestParams['method'] = $method; if (empty($requestParams['format'])) $requestParams['format'] = 'php_serial'; // if authenticated then add in authentication if ($this->isAuthenticated()) { $requestParams = array_merge($requestParams, $this->getOauthParams()); $requestParams['oauth_token'] = $this->getOauthData(self::OAUTH_ACCESS_TOKEN); $this->sign($api_url, $requestParams); } return array( 'url' => $api_url, 'params' => $requestParams ); } /** * Upload a photo * @param $parameters * @return mixed|null */ public function upload($parameters) { $requestParams = ($parameters == NULL ? array() : $parameters); $requestParams = array_merge($requestParams, $this->getOauthParams()); $requestParams['oauth_token'] = $this->getOauthData(self::OAUTH_ACCESS_TOKEN); // We don't want to include the photo when signing the request // so temporarily remove it whilst we sign $photo = $requestParams['photo']; unset($requestParams['photo']); $this->sign(self::UPLOAD_ENDPOINT, $requestParams); $requestParams['photo'] = $photo; $xml = $this->httpRequest(self::UPLOAD_ENDPOINT, $requestParams); $response = $this->getResponseFromXML($xml); return empty($response) ? NULL : $response; } /** * Replace a photo * @param $parameters * @return mixed|null */ public function replace($parameters) { $requestParams = ($parameters == NULL ? array() : $parameters); $requestParams = array_merge($requestParams, $this->getOauthParams()); $requestParams['oauth_token'] = $this->getOauthData(self::OAUTH_ACCESS_TOKEN); // We don't want to include the photo when signing the request // so temporarily remove it whilst we sign $photo = $requestParams['photo']; unset($requestParams['photo']); $this->sign(self::REPLACE_ENDPOINT, $requestParams); $requestParams['photo'] = $photo; $xml = $this->httpRequest(self::REPLACE_ENDPOINT, $requestParams); $response = $this->getResponseFromXML($xml); return empty($response) ? NULL : $response; } /** * Initiate the authentication process. Note that this method might not return - the user may get redirected to * Flickr to approve the request. * * @param string $permissions The permissions requested: read, write or delete */ public function authenticate($permissions = 'read') { $ok = false; // First of all, check to see if we're part way through the authentication process if ($this->getOauthData(self::IS_AUTHENTICATING)) { $oauthToken = @$_GET['oauth_token']; $oauthVerifier = @$_GET['oauth_verifier']; if (!empty($oauthToken) && !empty($oauthVerifier)) { // Looks like we're in the callback $this->setOauthData(self::OAUTH_REQUEST_TOKEN, $oauthToken); $this->setOauthData(self::OAUTH_VERIFIER, $oauthVerifier); $ok = $this->obtainAccessToken(); // if we had problems getting the access token then quit now if (!$ok) return false; } $this->setOauthData(self::IS_AUTHENTICATING, false); } $ok = ($this->isAuthenticated() && $this->doWeHaveGoodEnoughPermissions($permissions)); if (!$ok) { // We're authenticating afresh, clear out the session just in case there are remnants of a // previous authentication in there $this->signout(); if ($this->obtainRequestToken()) { // We've got the request token, redirect to Flickr for authentication/authorisation // Make a note in the session of where we are first $this->setOauthData(self::IS_AUTHENTICATING, true); $this->setOauthData(self::PERMISSIONS, $permissions); header(sprintf('Location: %s?oauth_token=%s&perms=%s', self::AUTH_ENDPOINT, $this->getOauthData(self::OAUTH_REQUEST_TOKEN), $permissions )); exit(0); } } return $ok; } /** * Sign the current user out of the current Flickr session. Note this doesn't affect the user's state on the * Flickr web site itself, it merely removes the current request/access tokens from the session. * */ public function signout() { unset($_SESSION[self::SESSION_OAUTH_DATA]); } /** * Is the current session authenticated on Flickr * * @return bool the current authentication status */ public function isAuthenticated() { $authNSID = $this->getOauthData(self::USER_NSID); return !empty($authNSID); } /** * Return a value from the OAuth session data * * @param string $key * @return string value */ public function getOauthData($key) { $val = NULL; $data = @$_SESSION[self::SESSION_OAUTH_DATA]; if (is_array($data)) { $val = @$data[$key]; } return $val; } /** * Return the HTTP Response code for the last HTTP call made * * @return int */ public function getLastHttpResponseCode() { return $this->lastHttpResponseCode; } /** * Set the timeout for HTTP requests * * @param int $timeout */ public function setHttpTimeout($timeout) { $this->httpTimeout = $timeout; } /** * Convert an old authentication token into an OAuth access token * * @param string $token */ public function convertOldToken($token) { $param = array( 'method' => 'flickr.auth.oauth.getAccessToken', 'format' => 'php_serial', 'api_key' => $this->consumerKey, 'auth_token' => $token ); $this->signUsingOldStyleAuth($param); $rsp = $this->httpRequest(self::API_ENDPOINT, $param); $response = unserialize($rsp); if (@$response['stat'] == 'ok') { $accessToken = @$response['auth']['access_token']['oauth_token']; $accessTokenSecret = @$response['auth']['access_token']['oauth_token_secret']; $this->setOauthData(self::OAUTH_ACCESS_TOKEN, $accessToken); $this->setOauthData(self::OAUTH_ACCESS_TOKEN_SECRET, $accessTokenSecret); $response = $this->call('flickr.auth.oauth.checkToken'); if (@$response['stat'] == 'ok') { $this->setOauthData(self::USER_NSID, @$response['oauth']['user']['nsid']); $this->setOauthData(self::USER_NAME, @$response['oauth']['user']['username']); $this->setOauthData(self::USER_FULL_NAME, @$response['oauth']['user']['fullname']); } } } /** * Sign an array of parameters using the old-style auth method * * @param array $parameters */ private function signUsingOldStyleAuth(&$parameters) { $keys = array_keys($parameters); sort($keys, SORT_STRING); $s = $this->consumerSecret; foreach ($keys as $k) { $s .= $k . $parameters[$k]; } $parameters['api_sig'] = md5($s); } private function setOauthData($key, $value) { $data = @$_SESSION[self::SESSION_OAUTH_DATA]; if (!is_array($data)) { $data = array(); } $data[$key] = $value; $_SESSION[self::SESSION_OAUTH_DATA] = $data; } /** * Override the current OAuth API access credentials with the given ones. * * @param $credentials */ public function useOAuthAccessCredentials($credentials) { $this->setOauthData(self::USER_FULL_NAME, $credentials[self::USER_FULL_NAME]); $this->setOauthData(self::USER_NAME, $credentials[self::USER_NAME]); $this->setOauthData(self::USER_NSID, $credentials[self::USER_NSID]); $this->setOauthData(self::OAUTH_ACCESS_TOKEN, $credentials[self::OAUTH_ACCESS_TOKEN]); $this->setOauthData(self::OAUTH_ACCESS_TOKEN_SECRET, $credentials[self::OAUTH_ACCESS_TOKEN_SECRET]); } /** * Check whether the current permission satisfy those requested * * @param string $permissionsRequired * @return bool */ private function doWeHaveGoodEnoughPermissions($permissionsRequired) { $ok = false; $currentPermissions = $this->getOauthData(self::PERMISSIONS); switch($permissionsRequired) { case 'read': $ok = preg_match('/^(read|write|delete)$/', $currentPermissions); break; case 'write': $ok = preg_match('/^(write|delete)$/', $currentPermissions); break; case 'delete': $ok = ($currentPermissions == 'delete'); break; } return $ok; } /** * Get a request token from Flickr * * @return bool */ private function obtainRequestToken() { $params = $this->getOauthParams(); $params['oauth_callback'] = $this->callback; $this->sign(self::REQUEST_TOKEN_ENDPOINT, $params); $rsp = $this->httpRequest(self::REQUEST_TOKEN_ENDPOINT, $params); $responseParameters = $this->splitParameters($rsp); $callbackOK = (@$responseParameters['oauth_callback_confirmed'] == 'true'); if ($callbackOK) { $this->setOauthData(self::OAUTH_REQUEST_TOKEN, @$responseParameters['oauth_token']); $this->setOauthData(self::OAUTH_REQUEST_TOKEN_SECRET, @$responseParameters['oauth_token_secret']); } return $callbackOK; } /** * Get an access token from Flickr * * @return bool */ private function obtainAccessToken() { $params = $this->getOauthParams(); $params['oauth_token'] = $this->getOauthData(self::OAUTH_REQUEST_TOKEN); $params['oauth_verifier'] = $this->getOauthData(self::OAUTH_VERIFIER); $this->sign(self::ACCESS_TOKEN_ENDPOINT, $params); $rsp = $this->httpRequest(self::ACCESS_TOKEN_ENDPOINT, $params); $responseParameters = $this->splitParameters($rsp); $ok = !empty($responseParameters['oauth_token']); if ($ok) { $this->setOauthData(self::OAUTH_ACCESS_TOKEN, @$responseParameters['oauth_token']); $this->setOauthData(self::OAUTH_ACCESS_TOKEN_SECRET, @$responseParameters['oauth_token_secret']); $this->setOauthData(self::USER_NSID, @$responseParameters['user_nsid']); $this->setOauthData(self::USER_NAME, @$responseParameters['username']); $this->setOauthData(self::USER_FULL_NAME, @$responseParameters['fullname']); } return $ok; } /** * Split a string into an array of key-value pairs * * @param string $string * @return array */ private function splitParameters($string) { $parameters = array(); $keyValuePairs = explode('&', $string); foreach ($keyValuePairs as $kvp) { $pieces = explode('=', $kvp); if (count($pieces) == 2) { $parameters[rawurldecode($pieces[0])] = rawurldecode($pieces[1]); } } return $parameters; } /** * Join an array of parameters together into a URL-encoded string * * @param array $parameters * @return string */ private function joinParameters($parameters) { $keys = array_keys($parameters); sort($keys, SORT_STRING); $keyValuePairs = array(); foreach ($keys as $k) { array_push($keyValuePairs, rawurlencode($k) . "=" . rawurlencode($parameters[$k])); } return implode("&", $keyValuePairs); } /** * Get the base string for creating an OAuth signature * * @param string $method * @param string $url * @param array $parameters * @return string */ private function getBaseString($method, $url, $parameters) { $components = array( rawurlencode($method), rawurlencode($url), rawurlencode($this->joinParameters($parameters)) ); $baseString = implode("&", $components); return $baseString; } /** * Sign an array of parameters with an OAuth signature * * @param string $url * @param array $parameters */ private function sign($url, &$parameters) { $baseString = $this->getBaseString($this->method, $url, $parameters); $signature = $this->getSignature($baseString); $parameters['oauth_signature'] = $signature; } /** * Calculate the signature for a string * * @param string $string * @return string */ private function getSignature($string) { $keyPart1 = $this->consumerSecret; $keyPart2 = $this->getOauthData(self::OAUTH_ACCESS_TOKEN_SECRET); if (empty($keyPart2)) { $keyPart2 = $this->getOauthData(self::OAUTH_REQUEST_TOKEN_SECRET); } if (empty($keyPart2)) { $keyPart2 = ''; } $key = "$keyPart1&$keyPart2"; return base64_encode(hash_hmac('sha1', $string, $key, true)); } /** * Get the standard OAuth parameters * * @return array */ private function getOauthParams() { $params = array ( 'oauth_nonce' => $this->makeNonce(), 'oauth_timestamp' => time(), 'oauth_consumer_key' => $this->consumerKey, 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_version' => '1.0', ); return $params; } /** * Create a nonce * * @return string */ private function makeNonce() { // Create a string that will be unique for this app and this user at this time $reasonablyDistinctiveString = implode(':', array( $this->consumerKey, $this->getOauthData(self::USER_NSID), microtime() ) ); return md5($reasonablyDistinctiveString); } /** * Get the response structure from an XML response. * Annoyingly, upload and replace returns XML rather than serialised PHP. * The responses are pretty simple, so rather than depend on an XML parser we'll fake it and * decode using regexps * @param $xml * @return mixed */ private function getResponseFromXML($xml) { $rsp = array(); $stat = 'fail'; $matches = array(); preg_match('//s', $xml, $matches); if (count($matches) > 0) { $stat = $matches[1]; } if ($stat == 'ok') { // do this in individual steps in case the order of the attributes ever changes $rsp['stat'] = $stat; $photoid = array(); $matches = array(); preg_match('/(\d+)<\/photoid>/s', $xml, $matches); if (count($matches) > 0) { $photoid['_content'] = $matches[1]; } $matches = array(); preg_match('//s', $xml, $matches); if (count($matches) > 0) { $photoid['secret'] = $matches[1]; } $matches = array(); preg_match('//s', $xml, $matches); if (count($matches) > 0) { $photoid['originalsecret'] = $matches[1]; } $rsp['photoid'] = $photoid; } else { $rsp['stat'] = 'fail'; $err = array(); $matches = array(); preg_match('//s', $xml, $matches); if (count($matches) > 0) { $err['code'] = $matches[1]; } $matches = array(); preg_match('//s', $xml, $matches); if (count($matches) > 0) { $err['msg'] = $matches[1]; } $rsp['err'] = $err; } return $rsp; } /** * Make an HTTP request * * @param string $url * @param array $parameters * @return mixed */ private function httpRequest($url, $parameters) { $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($curl, CURLOPT_TIMEOUT, $this->httpTimeout); if ($this->method == 'POST') { curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_POST, TRUE); curl_setopt($curl, CURLOPT_POSTFIELDS, $parameters); } else { // Assume GET curl_setopt($curl, CURLOPT_URL, "$url?" . $this->joinParameters($parameters)); } $response = curl_exec($curl); $headers = curl_getinfo($curl); curl_close($curl); $this->lastHttpResponseCode = $headers['http_code']; return $response; } }