diff --git a/class-wp-codebird.php b/class-wp-codebird.php index 3d3fbd28c41920fd172e7d9e758dbe490949d104..e77a0a44c9c0b436444c23e62baaafcd4ed96445 100644 --- a/class-wp-codebird.php +++ b/class-wp-codebird.php @@ -4,13 +4,19 @@ * An extension of the Codebird class to use Wordpress' HTTP API instead of * cURL. * - * @version 1.1.1 + * @version 1.1.2 */ class WP_Codebird extends Codebird { - /** - * The current singleton instance - */ - private static $_instance = null; + /** + * The current singleton instance + */ + private static $_instance = null; + + /** + * The file formats extensions twitter supports + * @var array + */ + protected $_supported_media_files_extensions = array( 'gif', 'jpg', 'jpeg', 'png' ); /** * Returns singleton class instance @@ -34,7 +40,7 @@ class WP_Codebird extends Codebird { /** * Overload magic __call() to transparently intercept Exceptions * - * Most exceptions encountered in production are API timeouts - this will + * Most exceptions encountered in production are API timeouts - this will * transparently handle these Exceptions to prevent fatal errors */ public function __call( $function, $arguments ) { @@ -49,58 +55,71 @@ class WP_Codebird extends Codebird { * Calls the API using Wordpress' HTTP API. * * @since 0.1.0 - * @see Codebird::_callApi - * @param string $httpmethod The HTTP method to use for making the request - * @param string $method The API method to call - * @param string $method_template The templated API method to call - * @param array optional $params The parameters to send along - * @param bool optional $multipart Whether to use multipart/form-data + * @see Codebird::_callApi + * + * @param string $httpmethod The HTTP method to use for making the request + * @param string $method The API method to call + * @param string $method_template The templated API method to call + * @param array optional $params The parameters to send along + * @param bool optional $multipart Whether to use multipart/form-data * * @return mixed The API reply, encoded in the set return_format. */ protected function _callApi( $httpmethod, $method, $method_template, $params = array(), $multipart = false, $app_only_auth = false ) { - $url = $this->_getEndpoint( $method, $method_template ); - $url_with_params = null; - $authorization = null; + $url = $this->_getEndpoint( $method, $method_template ); + $url_with_params = null; + $authorization = null; $remote_params = array( - 'method' => $httpmethod, - 'timeout' => 5, + 'method' => $httpmethod, + 'timeout' => 5, 'redirection' => 5, 'httpversion' => '1.0', - 'blocking' => true, - 'headers' => array(), - 'body' => null, - 'cookies' => array(), - 'sslverify' => false + 'blocking' => true, + 'headers' => array(), + 'body' => null, + 'cookies' => array(), + 'sslverify' => false ); if ( 'GET' == $httpmethod ) { $url_with_params = $url; if ( count( $params ) > 0 ) { - $url_with_params .= '?' . http_build_query( $params ); - } - + $url_with_params .= '?' . http_build_query( $params ); + } + $authorization = $this->_sign( $httpmethod, $url, $params ); $url = $url_with_params; - } else { - $authorization = $this->_sign( $httpmethod, $url, array() ); + } + else { + if ( $multipart ) { + $authorization = $this->_sign( $httpmethod, $url, array() ); + $params = $this->_buildMultipart( $method_template, $params ); - if ( ! $multipart ) { - $authorization = $this->_sign( $httpmethod, $url, $params ); - } + // Add the boundaries + $first_newline = strpos( $params, "\r\n" ); + $multipart_boundary = substr( $params, 2, $first_newline - 2 ); + $remote_params['headers']['Content-Length'] = strlen( $params ); + $remote_params['headers']['Content-Type'] = 'multipart/form-data; boundary=' . $multipart_boundary; + } + else { + $authorization = $this->_sign( $httpmethod, $url, $params ); + $params = http_build_query( $params ); + } $remote_params['body'] = $params; } - if ( $app_only_auth ){ - if ( null == self::$_oauth_consumer_key ) + if ( $app_only_auth ) { + if ( null == self::$_oauth_consumer_key ) { throw new Exception( 'To make an app-only auth API request, the consumer key must be set' ); - + } + // automatically fetch bearer token, if necessary - if ( null == self::$_oauth_bearer_token ) + if ( null == self::$_oauth_bearer_token ) { $this->oauth2_token(); + } $authorization = 'Authorization: Bearer ' . self::$_oauth_bearer_token; } @@ -109,98 +128,106 @@ class WP_Codebird extends Codebird { $authorization = trim( str_replace( 'Authorization:', '', $authorization ) ); if ( $authorization ) { - $remote_params['headers']['Authorization'] = $authorization; - $remote_params['headers']['Expect'] = ''; + $remote_params['headers']['Authorization'] = $authorization; + $remote_params['headers']['Expect'] = ''; } if ( 'GET' == $httpmethod ) { $reply = wp_remote_get( $url, $remote_params ); - } else { + } + else { $reply = wp_remote_post( $url, $remote_params ); } if ( isset( $reply ) ) { if ( is_wp_error( $reply ) ) { throw new Exception( $reply->get_error_message() ); - } else { - $httpstatus = $reply[ 'response' ][ 'code' ]; - $reply = $this->_parseApiReply( $method_template, $reply ); + } + else { + $httpstatus = $reply['response']['code']; + $reply = $this->_parseApiReply( $method_template, $reply ); if ( $this->_return_format == CODEBIRD_RETURNFORMAT_OBJECT ) { $reply->httpstatus = $httpstatus; - } else { - $reply[ 'httpstatus' ] = $httpstatus; + } + else { + $reply['httpstatus'] = $httpstatus; } } - } else { + } + else { throw new Exception( 'A reply was never generated. Some has gone horribly awry.' ); } return $reply; } - /** - * Gets the OAuth bearer token - * - * Overridden to use the WordPress HTTP API - * - * @return string The OAuth bearer token - */ - - public function oauth2_token() { - if ( null == self::$_oauth_consumer_key ) { - throw new Exception('To obtain a bearer token, the consumer key must be set.'); - } - - $post_fields = array( - 'grant_type' => 'client_credentials' - ); - - $url = self::$_endpoint_oauth . 'oauth2/token'; - - $headers = array( - 'Authorization' => 'Basic ' . base64_encode( self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret ), - 'Expect' => '' - ); - - $remote_params = array( - 'method' => 'POST', - 'timeout' => 5, - 'redirection' => 5, - 'httpversion' => '1.0', - 'blocking' => true, - 'headers' => $headers, - 'body' => $post_fields, - 'cookies' => array(), - 'sslverify' => false + /** + * Gets the OAuth bearer token + * + * Overridden to use the WordPress HTTP API + * + * @return string The OAuth bearer token + */ + + public function oauth2_token() { + if ( null == self::$_oauth_consumer_key ) { + throw new Exception( 'To obtain a bearer token, the consumer key must be set.' ); + } + + $post_fields = array( + 'grant_type' => 'client_credentials' ); - $reply = wp_remote_post( $url, $remote_params ); + $url = self::$_endpoint_oauth . 'oauth2/token'; - $httpstatus = wp_remote_retrieve_response_code( $reply ); + $headers = array( + 'Authorization' => 'Basic ' . base64_encode( self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret ), + 'Expect' => '' + ); - $reply = $this->_parseApiReply( 'oauth2/token', $reply ); + $remote_params = array( + 'method' => 'POST', + 'timeout' => 5, + 'redirection' => 5, + 'httpversion' => '1.0', + 'blocking' => true, + 'headers' => $headers, + 'body' => $post_fields, + 'cookies' => array(), + 'sslverify' => false + ); - if ( CODEBIRD_RETURNFORMAT_OBJECT == $this->_return_format ) { - $reply->httpstatus = $httpstatus; + $reply = wp_remote_post( $url, $remote_params ); - if ( 200 == $httpstatus ) - self::setBearerToken( $reply->access_token ); - } else { - $reply['httpstatus'] = $httpstatus; + $httpstatus = wp_remote_retrieve_response_code( $reply ); - if ( 200 == $httpstatus ) - self::setBearerToken( $reply['access_token'] ); - } + $reply = $this->_parseApiReply( 'oauth2/token', $reply ); - return $reply; - } + if ( CODEBIRD_RETURNFORMAT_OBJECT == $this->_return_format ) { + $reply->httpstatus = $httpstatus; + + if ( 200 == $httpstatus ) { + self::setBearerToken( $reply->access_token ); + } + } + else { + $reply['httpstatus'] = $httpstatus; + + if ( 200 == $httpstatus ) { + self::setBearerToken( $reply['access_token'] ); + } + } + + return $reply; + } /** * Parses the API reply to encode it in the set return_format. * * @since 0.1.0 - * @see Codebird::_parseApiReply + * @see Codebird::_parseApiReply + * * @param string $method The method that has been called * @param string $reply The actual reply, JSON-encoded or URL-encoded * @@ -209,11 +236,11 @@ class WP_Codebird extends Codebird { protected function _parseApiReply( $method, $reply ) { // split headers and body $http_response = $reply; - $headers = $http_response[ 'headers' ]; + $headers = $http_response['headers']; $reply = ''; - if ( isset( $http_response[ 'body' ] ) ) { - $reply = $http_response[ 'body' ]; + if ( isset( $http_response['body'] ) ) { + $reply = $http_response['body']; } $need_array = $this->_return_format == CODEBIRD_RETURNFORMAT_ARRAY; @@ -224,25 +251,106 @@ class WP_Codebird extends Codebird { $parsed = array(); if ( $method == 'users/profile_image/:screen_name' ) { // this method returns a 302 redirect, we need to extract the URL - if ( isset( $headers[ 'Location' ] ) ) { - $parsed = array( 'profile_image_url_https' => $headers[ 'Location' ] ); + if ( isset( $headers['Location'] ) ) { + $parsed = array( 'profile_image_url_https' => $headers['Location'] ); } - } elseif ( !$parsed = json_decode( $reply, $need_array ) ) { + } + elseif ( ! $parsed = json_decode( $reply, $need_array ) ) { if ( $reply ) { $reply = explode( '&', $reply ); foreach ( $reply as $element ) { if ( stristr( $element, '=' ) ) { list( $key, $value ) = explode( '=', $element ); - $parsed[ $key ] = $value; - } else { - $parsed[ 'message' ] = $element; + $parsed[$key] = $value; + } + else { + $parsed['message'] = $element; } } } } - if ( !$need_array ) { + if ( ! $need_array ) { $parsed = ( object ) $parsed; } + return $parsed; } + + /** + * Detect filenames in upload parameters, + * build multipart request from upload params + * + * @param string $method The API method to call + * @param array $params The parameters to send along + * + * @return void + */ + protected function _buildMultipart( $method, $params ) { + // well, files will only work in multipart methods + if ( ! $this->_detectMultipart( $method ) ) { + return; + } + + // only check specific parameters + $possible_files = array( + // Tweets + 'statuses/update_with_media' => 'media[]', + // Accounts + 'account/update_profile_background_image' => 'image', + 'account/update_profile_image' => 'image', + 'account/update_profile_banner' => 'banner' + ); + // method might have files? + if ( ! in_array( $method, array_keys( $possible_files ) ) ) { + return; + } + + $possible_files = explode( ' ', $possible_files[$method] ); + + $multipart_border = '--------------------' . $this->_nonce(); + $multipart_request = ''; + + foreach ( $params as $key => $value ) { + // is it an array? + if ( is_array( $value ) ) { + _doing_it_wrong( + '_buildMultiPart()', + 'Using URL-encoded parameters is not supported for uploading media.', + '3.7.1' + ); + continue; + } + $multipart_request .= + '--' . $multipart_border . "\r\n" + . 'Content-Disposition: form-data; name="' . $key . '"'; + + // check for filenames + if ( in_array( $key, $possible_files ) ) { + if ( + in_array( strtolower( end( explode( ".", $value ) ) ), $this->_supported_media_files_extensions ) + && file_exists( $value ) + && is_readable( $value ) + && $data = getimagesize( $value ) + ) { + if ( // is it a supported image format? + in_array( $data[2], $this->_supported_media_files ) + ) { + // try to read the file + $data = file_get_contents( $value ); + if ( strlen( $data ) == 0 ) { + continue; + } + $value = $data; + } + } + } + + $multipart_request .= + "\r\n\r\n" . $value . "\r\n"; + } + $multipart_request .= '--' . $multipart_border . '--'; + + return $multipart_request; + } + }