From c0cbf7057afd4805b63cd2a9a04c7b3587b788d5 Mon Sep 17 00:00:00 2001
From: Mike Pearce <mike@mikepearce.net>
Date: Tue, 10 Dec 2013 17:50:41 +0000
Subject: [PATCH] Updated WP_Codebird::_callApi() to support _buildMultipart()

This fixes issue #6. Added support for _buildMultipart() which posts images to twitter correctly. Until we update Codebird on wpcom, this will allow posting of images. Essentially, it just looks to see if it's a multipart posting and, if so, builds the multipart post body (with the content-disposition tags and boundaries). It also set's the headers correctly.
---
 class-wp-codebird.php | 199 +++++++++++++++++++++++++++++++-----------
 1 file changed, 150 insertions(+), 49 deletions(-)

diff --git a/class-wp-codebird.php b/class-wp-codebird.php
index 3d3fbd2..26d0e61 100644
--- a/class-wp-codebird.php
+++ b/class-wp-codebird.php
@@ -7,10 +7,10 @@
  * @version 1.1.1
  */
 class WP_Codebird extends Codebird {
-    /**
-     * The current singleton instance
-     */
-    private static $_instance = null;
+	/**
+	 * The current singleton instance
+	 */
+	private static $_instance = null;
 
 	/**
 	 * Returns singleton class instance
@@ -34,7 +34,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 ) {
@@ -53,12 +53,15 @@ class WP_Codebird extends Codebird {
 	 * @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
+	 * @param array           $params          The parameters to send along (optional)
+	 * @param bool            $multipart       Whether to use multipart/form-data (optional)
+	 * @param bool		      $app_only_auth
+	 * @throws Exception						If something goes awry with auth or the reply
 	 *
 	 * @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;
@@ -78,26 +81,36 @@ class WP_Codebird extends Codebird {
 		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() );
+			if ($multipart) {
+				$authorization = $this->_sign($httpmethod, $url, array());
+				$params        = $this->_buildMultipart($method_template, $params);
+
+				// Add the boundaries
+				// For the same reason we strip "Authorisation: " below. WP_HTTP_API uses the array key to know what the
+				// header is and Codebird uses a string.
+				$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;
 
-			if ( ! $multipart ) {
-				$authorization 	= $this->_sign( $httpmethod, $url, $params );
+			} 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 )
 				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 )
 				$this->oauth2_token();
@@ -139,31 +152,33 @@ class WP_Codebird extends Codebird {
 		return $reply;
 	}
 
-    /**
-     * Gets the OAuth bearer token
-     *
-     * Overridden to use the WordPress HTTP API
-     *
-     * @return string The OAuth bearer token
-     */
+	/**
+	 * Gets the OAuth bearer token
+	 *
+	 * Overridden to use the WordPress HTTP API
+	 *
+	 * @throws Exception
+	 *
+	 * @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.');
-        }
+	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'
-        );
+		$post_fields = array(
+			'grant_type' => 'client_credentials'
+		);
 
-        $url = self::$_endpoint_oauth . 'oauth2/token';
+		$url = self::$_endpoint_oauth . 'oauth2/token';
 
-        $headers = array(
-        	'Authorization' => 'Basic ' . base64_encode( self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret ),
-        	'Expect'		=> ''
-        );
+		$headers = array(
+			'Authorization' => 'Basic ' . base64_encode( self::$_oauth_consumer_key . ':' . self::$_oauth_consumer_secret ),
+			'Expect'		=> ''
+		);
 
-        $remote_params = array(
+		$remote_params = array(
 			'method' 		=> 'POST',
 			'timeout' 		=> 5,
 			'redirection' 	=> 5,
@@ -175,26 +190,26 @@ class WP_Codebird extends Codebird {
 			'sslverify' 	=> false
 		);
 
-        $reply 		= wp_remote_post( $url, $remote_params );
+		$reply 		= wp_remote_post( $url, $remote_params );
 
-        $httpstatus = wp_remote_retrieve_response_code( $reply );
+		$httpstatus = wp_remote_retrieve_response_code( $reply );
 
-        $reply 		= $this->_parseApiReply( 'oauth2/token', $reply );
+		$reply 		= $this->_parseApiReply( 'oauth2/token', $reply );
 
-        if ( CODEBIRD_RETURNFORMAT_OBJECT == $this->_return_format ) {
-            $reply->httpstatus = $httpstatus;
+		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 );
+		} else {
+			$reply['httpstatus'] = $httpstatus;
 
-            if ( 200 == $httpstatus )
-                self::setBearerToken( $reply['access_token'] );
-        }
+			if ( 200 == $httpstatus )
+				self::setBearerToken( $reply['access_token'] );
+		}
 
-        return $reply;
-    }
+		return $reply;
+	}
 
 	/**
 	 * Parses the API reply to encode it in the set return_format.
@@ -245,4 +260,90 @@ class WP_Codebird extends Codebird {
 		}
 		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
+	 *
+	 * @throws Exception
+	 *
+	 * @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)) {
+				throw new \Exception('Using URL-encoded parameters is not supported for uploading media.');
+				continue;
+			}
+			$multipart_request .=
+				'--' . $multipart_border . "\r\n"
+				. 'Content-Disposition: form-data; name="' . $key . '"';
+
+			// check for filenames
+			if (in_array($key, $possible_files)) {
+				if (// is it a file, a readable one?
+				@file_exists($value)
+				 && @is_readable($value)
+
+					 // is it a valid image?
+					 && $data = @getimagesize($value)
+				) {
+					if (// is it a supported image format?
+					in_array($data[2], $this->_supported_media_files)
+					) {
+						// try to read the file
+						ob_start();
+						readfile($value);
+						$data = ob_get_contents();
+						ob_end_clean();
+						if (strlen($data) == 0) {
+							continue;
+						}
+						$value = $data;
+					}
+				}
+
+				/*
+				$multipart_request .=
+					"\r\nContent-Transfer-Encoding: base64";
+				$value = base64_encode($value);
+				*/
+			}
+
+			$multipart_request .=
+				"\r\n\r\n" . $value . "\r\n";
+		}
+		$multipart_request .= '--' . $multipart_border . '--';
+
+		return $multipart_request;
+	}
 }
-- 
GitLab