<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" /><style type="text/css"><!--
#msg dl { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fc0 solid; padding: 6px; }
#msg ul, pre { overflow: auto; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<title>[17555] trunk: First run of introducing Stream-To-File for the WP_HTTP API.</title>
</head>
<body>
<div id="msg">
<dl>
<dt>Revision</dt> <dd><a href="http://trac.wordpress.org/changeset/17555">17555</a></dd>
<dt>Author</dt> <dd>dd32</dd>
<dt>Date</dt> <dd>2011-03-25 02:42:20 +0000 (Fri, 25 Mar 2011)</dd>
</dl>
<h3>Log Message</h3>
<pre>First run of introducing Stream-To-File for the WP_HTTP API. Reduces memory consumption during file downloads. Implemented in download_url() for upgraders. Props sivel. See <a href="http://trac.wordpress.org/ticket/16236">#16236</a></pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkwpadminincludesfilephp">trunk/wp-admin/includes/file.php</a></li>
<li><a href="#trunkwpincludesclasshttpphp">trunk/wp-includes/class-http.php</a></li>
<li><a href="#trunkwpincludesfunctionsphp">trunk/wp-includes/functions.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkwpadminincludesfilephp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/includes/file.php (17554 => 17555)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/includes/file.php        2011-03-24 16:12:47 UTC (rev 17554)
+++ trunk/wp-admin/includes/file.php        2011-03-25 02:42:20 UTC (rev 17555)
</span><span class="lines">@@ -153,42 +153,6 @@
</span><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> /**
</span><del>- * Determines a writable directory for temporary files.
- * Function's preference is to WP_CONTENT_DIR followed by the return value of <code>sys_get_temp_dir()</code>, before finally defaulting to /tmp/
- *
- * In the event that this function does not find a writable location, It may be overridden by the <code>WP_TEMP_DIR</code> constant in your <code>wp-config.php</code> file.
- *
- * @since 2.5.0
- *
- * @return string Writable temporary directory
- */
-function get_temp_dir() {
-        static $temp;
-        if ( defined('WP_TEMP_DIR') )
-                return trailingslashit(WP_TEMP_DIR);
-
-        if ( $temp )
-                return trailingslashit($temp);
-
-        $temp = WP_CONTENT_DIR . '/';
-        if ( is_dir($temp) && @is_writable($temp) )
-                return $temp;
-
-        if ( function_exists('sys_get_temp_dir') ) {
-                $temp = sys_get_temp_dir();
-                if ( @is_writable($temp) )
-                        return trailingslashit($temp);
-        }
-
-        $temp = ini_get('upload_tmp_dir');
-        if ( is_dir($temp) && @is_writable($temp) )
-                return trailingslashit($temp);
-
-        $temp = '/tmp/';
-        return $temp;
-}
-
-/**
</del><span class="cx"> * Returns a filename of a Temporary unique file.
</span><span class="cx"> * Please note that the calling function must unlink() this itself.
</span><span class="cx"> *
</span><span class="lines">@@ -519,27 +483,18 @@
</span><span class="cx">         if ( ! $tmpfname )
</span><span class="cx">                 return new WP_Error('http_no_file', __('Could not create Temporary file.'));
</span><span class="cx">
</span><del>-        $handle = @fopen($tmpfname, 'wb');
-        if ( ! $handle )
-                return new WP_Error('http_no_file', __('Could not create Temporary file.'));
</del><ins>+        $response = wp_remote_get( $url, array( 'timeout' => $timeout, 'stream' => true, 'filename' => $tmpfname ) );
</ins><span class="cx">
</span><del>-        $response = wp_remote_get($url, array('timeout' => $timeout));
-
-        if ( is_wp_error($response) ) {
-                fclose($handle);
-                unlink($tmpfname);
</del><ins>+        if ( is_wp_error( $response ) ) {
+                unlink( $tmpfname );
</ins><span class="cx">                 return $response;
</span><span class="cx">         }
</span><span class="cx">
</span><span class="cx">         if ( $response['response']['code'] != '200' ){
</span><del>-                fclose($handle);
-                unlink($tmpfname);
-                return new WP_Error('http_404', trim($response['response']['message']));
</del><ins>+                unlink( $tmpfname );
+                return new WP_Error( 'http_404', trim( $response['response']['message'] ) );
</ins><span class="cx">         }
</span><span class="cx">
</span><del>-        fwrite($handle, $response['body']);
-        fclose($handle);
-
</del><span class="cx">         return $tmpfname;
</span><span class="cx"> }
</span><span class="cx">
</span></span></pre></div>
<a id="trunkwpincludesclasshttpphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/class-http.php (17554 => 17555)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/class-http.php        2011-03-24 16:12:47 UTC (rev 17554)
+++ trunk/wp-includes/class-http.php        2011-03-25 02:42:20 UTC (rev 17555)
</span><span class="lines">@@ -97,7 +97,9 @@
</span><span class="cx">                         'body' => null,
</span><span class="cx">                         'compress' => false,
</span><span class="cx">                         'decompress' => true,
</span><del>-                        'sslverify' => true
</del><ins>+                        'sslverify' => true,
+                        'stream' => false,
+                        'filename' => null
</ins><span class="cx">                 );
</span><span class="cx">
</span><span class="cx">                 
</span><span class="lines">@@ -136,6 +138,18 @@
</span><span class="cx">                 $r['local'] = $homeURL['host'] == $arrURL['host'] || 'localhost' == $arrURL['host'];
</span><span class="cx">                 unset( $homeURL );
</span><span class="cx">
</span><ins>+                // If we are streaming to a file but no filename was given drop it in the WP temp dir
+                // and pick it's name using the basename of the $url
+                if ( $r['stream'] && empty( $r['filename'] ) )
+                        $r['filename'] = get_temp_dir() . basename( $url );
+
+                // Force some settings if we are streaming to a file and check for existence and perms of destination directory
+                if ( $r['stream'] ) {
+                        $r['blocking'] = true;
+                        if ( ! is_writable( dirname( $r['filename'] ) ) )
+                                return new WP_Error( 'http_request_failed', __( 'Destination directory for file streaming does not exist or is not writable.' ) );
+                }
+
</ins><span class="cx">                 if ( is_null( $r['headers'] ) )
</span><span class="cx">                         $r['headers'] = array();
</span><span class="cx">
</span><span class="lines">@@ -659,16 +673,49 @@
</span><span class="cx">                 }
</span><span class="cx">
</span><span class="cx">                 $strResponse = '';
</span><del>-                while ( ! feof($handle) )
-                        $strResponse .= fread($handle, 4096);
</del><ins>+                $bodyStarted = false;
</ins><span class="cx">
</span><del>-                fclose($handle);
</del><ins>+                // If streaming to a file setup the file handle
+                if ( $r['stream'] ) {
+                        if ( ! WP_DEBUG )
+                                $stream_handle = @fopen( $r['filename'], 'w+' );
+                        else
+                                $stream_handle = fopen( $r['filename'], 'w+' );
+                        if ( ! $stream_handle )
+                                return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
</ins><span class="cx">
</span><ins>+                        while ( ! feof($handle) ) {
+                                $block = fread( $handle, 4096 );
+                                if ( $bodyStarted ) {
+                                        fwrite( $stream_handle, $block );
+                                } else {
+                                        $strResponse .= $block;
+                                        if ( strpos( $strResponse, "\r\n\r\n" ) ) {
+                                                $process = WP_Http::processResponse( $strResponse );
+                                                $bodyStarted = true;
+                                                fwrite( $stream_handle, $process['body'] );
+                                                unset( $strResponse );
+                                                $process['body'] = '';
+                                        }
+                                }
+                        }
+
+                        fclose( $stream_handle );
+
+                } else {
+                        while ( ! feof($handle) )
+                                $strResponse .= fread( $handle, 4096 );
+
+                        $process = WP_Http::processResponse( $strResponse );
+                        unset( $strResponse );
+                }
+
+                fclose( $handle );
+
</ins><span class="cx">                 if ( true === $secure_transport )
</span><span class="cx">                         error_reporting($error_reporting);
</span><span class="cx">
</span><del>-                $process = WP_Http::processResponse($strResponse);
-                $arrHeaders = WP_Http::processHeaders($process['headers']);
</del><ins>+                $arrHeaders = WP_Http::processHeaders( $process['headers'] );
</ins><span class="cx">
</span><span class="cx">                 // Is the response code within the 400 range?
</span><span class="cx">                 if ( (int) $arrHeaders['response']['code'] >= 400 && (int) $arrHeaders['response']['code'] < 500 )
</span><span class="lines">@@ -690,7 +737,7 @@
</span><span class="cx">                 if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($arrHeaders['headers']) )
</span><span class="cx">                         $process['body'] = WP_Http_Encoding::decompress( $process['body'] );
</span><span class="cx">
</span><del>-                return array('headers' => $arrHeaders['headers'], 'body' => $process['body'], 'response' => $arrHeaders['response'], 'cookies' => $arrHeaders['cookies']);
</del><ins>+                return array( 'headers' => $arrHeaders['headers'], 'body' => $process['body'], 'response' => $arrHeaders['response'], 'cookies' => $arrHeaders['cookies'], 'filename' => $r['filename'] );
</ins><span class="cx">         }
</span><span class="cx">
</span><span class="cx">         /**
</span><span class="lines">@@ -834,11 +881,27 @@
</span><span class="cx">                         return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() );
</span><span class="cx">                 }
</span><span class="cx">
</span><del>-                $strResponse = stream_get_contents($handle);
-                $meta = stream_get_meta_data($handle);
</del><ins>+                if ( $r['stream'] ) {
+                        if ( ! WP_DEBUG )
+                                $stream_handle = @fopen( $r['filename'], 'w+' );
+                        else
+                                $stream_handle = fopen( $r['filename'], 'w+' );
</ins><span class="cx">
</span><del>-                fclose($handle);
</del><ins>+                        if ( ! $stream_handle )
+                                return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
</ins><span class="cx">
</span><ins>+                        stream_copy_to_stream( $handle, $stream_handle );
+
+                        fclose( $stream_handle );
+                        $strResponse = '';
+                } else {
+                        $strResponse = stream_get_contents( $handle );
+                }
+
+                $meta = stream_get_meta_data( $handle );
+
+                fclose( $handle );
+
</ins><span class="cx">                 $processedHeaders = array();
</span><span class="cx">                 if ( isset( $meta['wrapper_data']['headers'] ) )
</span><span class="cx">                         $processedHeaders = WP_Http::processHeaders($meta['wrapper_data']['headers']);
</span><span class="lines">@@ -856,7 +919,7 @@
</span><span class="cx">                 if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($processedHeaders['headers']) )
</span><span class="cx">                         $strResponse = WP_Http_Encoding::decompress( $strResponse );
</span><span class="cx">
</span><del>-                return array('headers' => $processedHeaders['headers'], 'body' => $strResponse, 'response' => $processedHeaders['response'], 'cookies' => $processedHeaders['cookies']);
</del><ins>+                return array( 'headers' => $processedHeaders['headers'], 'body' => $strResponse, 'response' => $processedHeaders['response'], 'cookies' => $processedHeaders['cookies'], 'filename' => $r['filename'] );
</ins><span class="cx">         }
</span><span class="cx">
</span><span class="cx">         /**
</span><span class="lines">@@ -1006,11 +1069,25 @@
</span><span class="cx">                 if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($theHeaders['headers']) )
</span><span class="cx">                         $theBody = http_inflate( $theBody );
</span><span class="cx">
</span><ins>+                if ( $r['stream'] ) {
+                        if ( !WP_DEBUG )
+                                $stream_handle = @fopen( $r['filename'], 'w+' );
+                        else
+                                $stream_handle = fopen( $r['filename'], 'w+' );
+
+                        if ( ! $stream_handle )
+                                return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
+
+                        fwrite( $stream_handle, $theBody );
+                        fclose( $stream_handle );
+                        $theBody = '';
+                }
+
</ins><span class="cx">                 $theResponse = array();
</span><span class="cx">                 $theResponse['code'] = $info['response_code'];
</span><span class="cx">                 $theResponse['message'] = get_status_header_desc($info['response_code']);
</span><span class="cx">
</span><del>-                return array('headers' => $theHeaders['headers'], 'body' => $theBody, 'response' => $theResponse, 'cookies' => $theHeaders['cookies']);
</del><ins>+                return array( 'headers' => $theHeaders['headers'], 'body' => $theBody, 'response' => $theResponse, 'cookies' => $theHeaders['cookies'], 'filename' => $r['filename'] );
</ins><span class="cx">         }
</span><span class="cx">
</span><span class="cx">         /**
</span><span class="lines">@@ -1038,6 +1115,15 @@
</span><span class="cx"> class WP_Http_Curl {
</span><span class="cx">
</span><span class="cx">         /**
</span><ins>+         * Temporary header storage for use with streaming to a file.
+         *
+         * @since 3.2.0
+         * @access private
+         * @var string
+         */
+        private $headers;
+
+        /**
</ins><span class="cx">          * Send a HTTP request to a URI using cURL extension.
</span><span class="cx">          *
</span><span class="cx">          * @access public
</span><span class="lines">@@ -1121,10 +1207,21 @@
</span><span class="cx">                 }
</span><span class="cx">
</span><span class="cx">                 if ( true === $r['blocking'] )
</span><del>-                        curl_setopt( $handle, CURLOPT_HEADER, true );
-                else
-                        curl_setopt( $handle, CURLOPT_HEADER, false );
</del><ins>+                        curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( &$this, 'stream_headers' ) );
</ins><span class="cx">
</span><ins>+                curl_setopt( $handle, CURLOPT_HEADER, false );
+
+                // If streaming to a file open a file handle, and setup our curl streaming handler
+                if ( $r['stream'] ) {
+                        if ( ! WP_DEBUG )
+                                $stream_handle = @fopen( $r['filename'], 'w+' );
+                        else
+                                $stream_handle = fopen( $r['filename'], 'w+' );
+                        if ( ! $stream_handle )
+                                return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
+                        curl_setopt( $handle, CURLOPT_FILE, $stream_handle );
+                }
+
</ins><span class="cx">                 // The option doesn't work with safe mode or when open_basedir is set.
</span><span class="cx">                 if ( !ini_get('safe_mode') && !ini_get('open_basedir') && 0 !== $r['_redirection'] )
</span><span class="cx">                         curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, true );
</span><span class="lines">@@ -1155,51 +1252,61 @@
</span><span class="cx">                 }
</span><span class="cx">
</span><span class="cx">                 $theResponse = curl_exec( $handle );
</span><ins>+                $theBody = '';
+                $theHeaders = WP_Http::processHeaders( $this->headers );
</ins><span class="cx">
</span><del>-                if ( !empty($theResponse) ) {
-                        $headerLength = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
-                        $theHeaders = trim( substr($theResponse, 0, $headerLength) );
-                        if ( strlen($theResponse) > $headerLength )
-                                $theBody = substr( $theResponse, $headerLength );
-                        else
-                                $theBody = '';
-                        if ( false !== strpos($theHeaders, "\r\n\r\n") ) {
-                                $headerParts = explode("\r\n\r\n", $theHeaders);
-                                $theHeaders = $headerParts[ count($headerParts) -1 ];
-                        }
-                        $theHeaders = WP_Http::processHeaders($theHeaders);
-                } else {
</del><ins>+                if ( ! empty($theResponse) && ! is_bool( $theResponse ) ) // is_bool: when using $args['stream'], curl_exec will return (bool)true
+                        $theBody = $theResponse;
+
+                // If no response, and It's not a HEAD request with valid headers returned
+                if ( empty($theResponse) && 'HEAD' != $args['method'] && ! empty($this->headers) ) {
</ins><span class="cx">                         if ( $curl_error = curl_error($handle) )
</span><span class="cx">                                 return new WP_Error('http_request_failed', $curl_error);
</span><span class="cx">                         if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array(301, 302) ) )
</span><span class="cx">                                 return new WP_Error('http_request_failed', __('Too many redirects.'));
</span><del>-
-                        $theHeaders = array( 'headers' => array(), 'cookies' => array() );
-                        $theBody = '';
</del><span class="cx">                 }
</span><span class="cx">
</span><ins>+                unset( $this->headers );
+
</ins><span class="cx">                 $response = array();
</span><span class="cx">                 $response['code'] = curl_getinfo( $handle, CURLINFO_HTTP_CODE );
</span><span class="cx">                 $response['message'] = get_status_header_desc($response['code']);
</span><span class="cx">
</span><span class="cx">                 curl_close( $handle );
</span><span class="cx">
</span><ins>+                if ( $r['stream'] )
+                        fclose( $stream_handle );
+
</ins><span class="cx">                 // See #11305 - When running under safe mode, redirection is disabled above. Handle it manually.
</span><del>-                if ( !empty($theHeaders['headers']['location']) && (ini_get('safe_mode') || ini_get('open_basedir')) && 0 !== $r['_redirection'] ) {
</del><ins>+                if ( ! empty( $theHeaders['headers']['location'] ) && ( ini_get( 'safe_mode' ) || ini_get( 'open_basedir' ) ) && 0 !== $r['_redirection'] ) {
</ins><span class="cx">                         if ( $r['redirection']-- > 0 ) {
</span><del>-                                return $this->request($theHeaders['headers']['location'], $r);
</del><ins>+                                return $this->request( $theHeaders['headers']['location'], $r );
</ins><span class="cx">                         } else {
</span><del>-                                return new WP_Error('http_request_failed', __('Too many redirects.'));
</del><ins>+                                return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) );
</ins><span class="cx">                         }
</span><span class="cx">                 }
</span><span class="cx">
</span><span class="cx">                 if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($theHeaders['headers']) )
</span><span class="cx">                         $theBody = WP_Http_Encoding::decompress( $theBody );
</span><span class="cx">
</span><del>-                return array('headers' => $theHeaders['headers'], 'body' => $theBody, 'response' => $response, 'cookies' => $theHeaders['cookies']);
</del><ins>+                return array( 'headers' => $theHeaders['headers'], 'body' => $theBody, 'response' => $response, 'cookies' => $theHeaders['cookies'], 'filename' => $r['filename'] );
</ins><span class="cx">         }
</span><span class="cx">
</span><span class="cx">         /**
</span><ins>+         * Grab the headers of the cURL request
+         *
+         * Each header is sent individually to this callback, so we append to the $header property for temporary storage
+         *
+         * @since 3.2.0
+         * @access private
+         * @return int
+         */
+        private function stream_headers( $handle, $headers ) {
+                $this->headers .= $headers;
+                return strlen( $headers );
+        }
+
+        /**
</ins><span class="cx">          * Whether this class can be used for retrieving an URL.
</span><span class="cx">          *
</span><span class="cx">          * @static
</span></span></pre></div>
<a id="trunkwpincludesfunctionsphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/functions.php (17554 => 17555)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/functions.php        2011-03-24 16:12:47 UTC (rev 17554)
+++ trunk/wp-includes/functions.php        2011-03-25 02:42:20 UTC (rev 17555)
</span><span class="lines">@@ -2111,6 +2111,42 @@
</span><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> /**
</span><ins>+ * Determines a writable directory for temporary files.
+ * Function's preference is to WP_CONTENT_DIR followed by the return value of <code>sys_get_temp_dir()</code>, before finally defaulting to /tmp/
+ *
+ * In the event that this function does not find a writable location, It may be overridden by the <code>WP_TEMP_DIR</code> constant in your <code>wp-config.php</code> file.
+ *
+ * @since 2.5.0
+ *
+ * @return string Writable temporary directory
+ */
+function get_temp_dir() {
+        static $temp;
+        if ( defined('WP_TEMP_DIR') )
+                return trailingslashit(WP_TEMP_DIR);
+
+        if ( $temp )
+                return trailingslashit($temp);
+
+        $temp = WP_CONTENT_DIR . '/';
+        if ( is_dir($temp) && @is_writable($temp) )
+                return $temp;
+
+        if ( function_exists('sys_get_temp_dir') ) {
+                $temp = sys_get_temp_dir();
+                if ( @is_writable($temp) )
+                        return trailingslashit($temp);
+        }
+
+        $temp = ini_get('upload_tmp_dir');
+        if ( is_dir($temp) && @is_writable($temp) )
+                return trailingslashit($temp);
+
+        $temp = '/tmp/';
+        return $temp;
+}
+
+/**
</ins><span class="cx"> * Get an array containing the current upload directory's path and url.
</span><span class="cx"> *
</span><span class="cx"> * Checks the 'upload_path' option, which should be from the web root folder,
</span></span></pre>
</div>
</div>
</body>
</html>