Support for chunked HTTP messages added, some code encapsulated:
[mailer.git] / inc / functions.php
index bf192a5fb7917c2681401374ed201350e49e8383..79e5d8d5a7541f79bae4a3cc289f8032a9a5e27d 100644 (file)
@@ -819,7 +819,9 @@ function extractHostnameFromUrl (&$script) {
 
        // Extract host name
        $host = str_replace('http://', '', $url);
-       if (isInString('/', $host)) $host = substr($host, 0, strpos($host, '/'));
+       if (isInString('/', $host)) {
+               $host = substr($host, 0, strpos($host, '/'));
+       } // END - if
 
        // Generate relative URL
        //* DEBUG: */ debugOutput('SCRIPT=' . $script);
@@ -832,22 +834,24 @@ function extractHostnameFromUrl (&$script) {
        }
 
        //* DEBUG: */ debugOutput('SCRIPT=' . $script);
-       if (substr($script, 0, 1) == '/') $script = substr($script, 1);
+       if (substr($script, 0, 1) == '/') {
+               $script = substr($script, 1);
+       } // END - if
 
        // Return host name
        return $host;
 }
 
 // Send a GET request
-function sendGetRequest ($script, $data = array()) {
-       // Extract host name from script
+function sendGetRequest ($script, $data = array(), $removeHeader = false) {
+       // Extract hostname and port from script
        $host = extractHostnameFromUrl($script);
 
        // Add data
        $body = http_build_query($data, '', '&');
 
        // There should be data, else we don't need to extend $script with $body
-       if (empty($body)) {
+       if (!empty($body)) {
                // Do we have a question-mark in the script?
                if (strpos($script, '?') === false) {
                        // No, so first char must be question mark
@@ -861,7 +865,9 @@ function sendGetRequest ($script, $data = array()) {
                $script .= $body;
 
                // Remove trailed & to make it more conform
-               if (substr($script, -1, 1) == '&') $script = substr($script, 0, -1);
+               if (substr($script, -1, 1) == '&') {
+                       $script = substr($script, 0, -1);
+               } // END - if
        } // END - if
 
        // Generate GET request header
@@ -882,19 +888,18 @@ function sendGetRequest ($script, $data = array()) {
        // Send the raw request
        $response = sendRawRequest($host, $request);
 
+       // Should we remove header lines?
+       if ($removeHeader === true) {
+               // Okay, remove them
+               $response = removeHttpHeaderFromResponse($response);
+       } // END - if
+
        // Return the result to the caller function
        return $response;
 }
 
 // Send a POST request
-function sendPostRequest ($script, $postData) {
-       // Is postData an array?
-       if (!is_array($postData)) {
-               // Abort here
-               logDebugMessage(__FUNCTION__, __LINE__, sprintf("postData is not an array. Type: %s", gettype($postData)));
-               return array('', '', '');
-       } // END - if
-
+function sendPostRequest ($script, array $postData, $removeHeader = false) {
        // Extract host name from script
        $host = extractHostnameFromUrl($script);
 
@@ -920,6 +925,12 @@ function sendPostRequest ($script, $postData) {
        // Send the raw request
        $response = sendRawRequest($host, $request);
 
+       // Should we remove header lines?
+       if ($removeHeader === true) {
+               // Okay, remove them
+               $response = removeHttpHeaderFromResponse($response);
+       } // END - if
+
        // Return the result to the caller function
        return $response;
 }
@@ -927,7 +938,11 @@ function sendPostRequest ($script, $postData) {
 // Sends a raw request to another host
 function sendRawRequest ($host, $request) {
        // Init errno and errdesc with 'all fine' values
-       $errno = '0'; $errdesc = '';
+       $errno = '0';
+       $errdesc = '';
+
+       // Default port is 80
+       $port = 80;
 
        // Initialize array
        $response = array('', '', '');
@@ -944,6 +959,17 @@ function sendRawRequest ($host, $request) {
        // Load include
        loadIncludeOnce('inc/classes/resolver.class.php');
 
+       // Extract port part from host
+       $portArray = explode(':', $host);
+       if (count($portArray) == 2) {
+               // Extract host and port
+               $host = $portArray[0];
+               $port = $portArray[1];
+       } elseif (count($portArray) > 2) {
+               // This should not happen!
+               debug_report_bug(__FUNCTION__, __LINE__, 'Invalid ' . $host . '. Please report this to the Mailer-Project team.');
+       }
+
        // Get resolver instance
        $resolver = new HostnameResolver();
 
@@ -960,7 +986,7 @@ function sendRawRequest ($host, $request) {
                $ip = $resolver->resolveHostname($host);
 
                // Connect to host directly
-               $fp = fsockopen($ip, 80, $errno, $errdesc, 30);
+               $fp = fsockopen($ip, $port, $errno, $errdesc, 30);
        }
 
        // Is there a link?
@@ -977,7 +1003,7 @@ function sendRawRequest ($host, $request) {
        // Do we use proxy?
        if ($useProxy === true) {
                // Setup proxy tunnel
-               $response = setupProxyTunnel($host, $fp);
+               $response = setupProxyTunnel($host, $port, $fp);
 
                // If the response is invalid, abort
                if ((count($response) == 3) && (empty($response[0])) && (empty($response[1])) && (empty($response[2]))) {
@@ -1020,7 +1046,7 @@ function sendRawRequest ($host, $request) {
                } // END - if
 
                // Add it to response
-               $response[] = trim($line);
+               $response[] = $line;
        } // END - while
 
        // Close socket
@@ -1067,6 +1093,9 @@ function sendRawRequest ($host, $request) {
                // Not found / access forbidden
                logDebugMessage(__FUNCTION__, __LINE__, 'Unexpected status code ' . $response[0] . ' detected. "200 OK" was expected.');
                $response = array('', '', '');
+       } else {
+               // Check array for chuncked encoding
+               $response = unchunkHttpResponse($response);
        } // END - if
 
        // Return response
@@ -1074,12 +1103,12 @@ function sendRawRequest ($host, $request) {
 }
 
 // Sets up a proxy tunnel for given hostname and through resource
-function setupProxyTunnel ($host, $resource) {
+function setupProxyTunnel ($host, $port, $resource) {
        // Initialize array
        $response = array('', '', '');
 
        // Generate CONNECT request header
-       $proxyTunnel  = 'CONNECT ' . $host . ':80 HTTP/1.0' . getConfig('HTTP_EOL');
+       $proxyTunnel  = 'CONNECT ' . $host . ':' . $port . ' HTTP/1.0' . getConfig('HTTP_EOL');
        $proxyTunnel .= 'Host: ' . $host . getConfig('HTTP_EOL');
 
        // Use login data to proxy? (username at least!)
@@ -1114,6 +1143,76 @@ function setupProxyTunnel ($host, $resource) {
        return $respArray;
 }
 
+// Check array for chuncked encoding
+function unchunkHttpResponse (array $response) {
+       // Default is not chunked
+       $isChunked = false;
+
+       // Check if we have chunks
+       foreach ($response as $line) {
+               // Make lower-case and trim it
+               $line = trim(strtolower($line));
+
+               // Entry found?
+               if ((strpos($line, 'transfer-encoding') !== false) && (strpos($line, 'chunked') !== false)) {
+                       // Found!
+                       $isChunked = true;
+                       break;
+               } // END - if
+       } // END - foreach
+
+       // Is it chunked?
+       if ($isChunked === true) {
+               // Good, we still have the HTTP headers in there, so we need to get rid
+               // of them temporarly
+               //* DEBUG: */ die('<pre>'.htmlentities(print_r(removeHttpHeaderFromResponse($response), true)).'</pre>');
+               $tempResponse = http_chunked_decode(implode('', removeHttpHeaderFromResponse($response)));
+
+               // We got a string back from http_chunked_decode(), so we need to convert it back to an array
+               //* DEBUG: */ die('tempResponse['.strlen($tempResponse).']=<pre>'.replaceReturnNewLine(htmlentities($tempResponse)).'</pre>');
+
+               // Re-add the headers
+               $response = merge_array($GLOBALS['http_headers'], stringToArray("\n", $tempResponse));
+       } // END - if
+
+       // Return the unchunked array
+       return $response;
+}
+
+// Removes HTTP header lines from a response array (e.g. output from send<Get|Post>Request() )
+function removeHttpHeaderFromResponse (array $response) {
+       // Save headers for later usage
+       $GLOBALS['http_headers'] = array();
+
+       // The first array element has to contain HTTP
+       if ((isset($response[0])) && (substr(strtoupper($response[0]), 0, 5) == 'HTTP/')) {
+               // Okay, we have headers, now remove them with a second array
+               $response2 = $response;
+               foreach ($response as $line) {
+                       // Remove line
+                       array_shift($response2);
+
+                       // Add full line to temporary global array
+                       $GLOBALS['http_headers'][] = $line;
+
+                       // Trim it for testing
+                       $lineTest = trim($line);
+
+                       // Is this line empty?
+                       if (empty($lineTest)) {
+                               // Then stop here
+                               break;
+                       } // END - if
+               } // END - foreach
+
+               // Write back the array
+               $response = $response2;
+       } // END - if
+
+       // Return the modified response array
+       return $response;
+}
+
 // Taken from www.php.net isInStringIgnoreCase() user comments
 function isEmailValid ($email) {
        // Check first part of email address
@@ -2507,7 +2606,7 @@ function generateAdminMailLinks ($mailType, $mailId) {
                if (SQL_NUMROWS($result) == 1) {
                        // Load the entry
                        $content = SQL_FETCHARRAY($result);
-                       die('<pre>'.print_r($content, true).'</pre>');
+                       die(__FUNCTION__.':<br />content=<pre>'.print_r($content, true).'</pre>');
                } // END - if
 
                // Free result
@@ -2518,10 +2617,55 @@ function generateAdminMailLinks ($mailType, $mailId) {
        return $OUT;
 }
 
+
+/**
+ * determine if a string can represent a number in hexadecimal
+ *
+ * @param      $hex    A string to check if it is hex-encoded
+ * @return     $foo    True if the string is a hex, otherwise false
+ * @author     Marques Johansson
+ * @link       http://php.net/manual/en/function.http-chunked-decode.php#89786
+ */
+function isHexadecimal ($hex) {
+       // Make it lowercase
+       $hex = strtolower(trim(ltrim($hex, '0')));
+
+       // Fix empty strings to zero
+       if (empty($hex)) {
+               $hex = 0;
+       } // END - if
+
+       // Simply compare decode->encode result with original
+       return ($hex == dechex(hexdec($hex)));
+}
+
+// Replace "\r" with "[r]" and "\n" with "[n]" and add a final new-line to make
+// them visible to the developer. Use this function to debug e.g. buggy HTTP
+// response handler functions.
+function replaceReturnNewLine ($str) {
+       return str_replace("\r", '[r]', str_replace("\n", '[n]
+', $str));
+}
+
+// Converts a given string by splitting it up with given delimiter similar to
+// explode(), but appending the delimiter again
+function stringToArray ($delimiter, $string) {
+       // Init array
+       $strArray = array();
+
+       // "Walk" through all entries
+       foreach (explode($delimiter, $string) as $split) {
+               //  Append the delimiter and add it to the array
+               $strArray[] = $split . $delimiter;
+       } // END - foreach
+
+       // Return array
+       return $strArray;
+}
+
 //-----------------------------------------------------------------------------
 // Automatically re-created functions, all taken from user comments on www.php.net
 //-----------------------------------------------------------------------------
-//
 if (!function_exists('html_entity_decode')) {
        // Taken from documentation on www.php.net
        function html_entity_decode ($string) {
@@ -2540,7 +2684,9 @@ if (!function_exists('http_build_query')) {
                                $k = urlencode($prefix . $k);
                        } // END - if
 
-                       if ((!empty($key)) || ($key === 0))  $k = $key . '[' . urlencode($k) . ']';
+                       if ((!empty($key)) || ($key === 0)) {
+                               $k = $key . '[' . urlencode($k) . ']';
+                       } // END - if
 
                        if (is_array($v) || is_object($v)) {
                                array_push($ret, http_build_query($v, '', $sep, $k));
@@ -2549,11 +2695,112 @@ if (!function_exists('http_build_query')) {
                        }
                } // END - foreach
 
-               if (empty($sep)) $sep = ini_get('arg_separator.output');
+               if (empty($sep)) {
+                       $sep = ini_get('arg_separator.output');
+               } // END - if
 
                return implode($sep, $ret);
        }
 } // END - if
 
+if (!function_exists('http_chunked_decode')) {
+       /**
+        * dechunk an http 'transfer-encoding: chunked' message.
+        *
+        * @param       $chunk          The encoded message
+        * @return      $dechunk        The decoded message. If $chunk wasn't encoded properly debug_report_bug() is being called
+        * @author      Marques Johansson
+        * @link        http://php.net/manual/en/function.http-chunked-decode.php#89786
+        */
+       function http_chunked_decode ($chunk) {
+               // Init some variables
+               $offset = 0;
+               $len = mb_strlen($chunk);
+               $dechunk = '';
+
+               // Walk through all chunks
+               while ($offset < $len) {
+                       // Where does the \r\n begin?
+                       $lineEndAt = mb_strpos($chunk, getConfig('HTTP_EOL'), $offset);
+
+                       /* DEBUG: *
+                       print 'lineEndAt[<em>'.__LINE__.'</em>]='.$lineEndAt.'<br />
+offset[<em>'.__LINE__.'</em>]='.$offset.'<br />
+len='.$len.'<br />
+next[offset]=<pre>'.replaceReturnNewLine(htmlentities(mb_substr($chunk, $offset, 10))).'</pre>';
+                       /* DEBUG: */
+
+                       // Get next hex-coded chunk length
+                       $chunkLenHex = mb_substr($chunk, $offset, ($lineEndAt - $offset));
+
+                       /* DEBUG: *
+                       print 'chunkLenHex[<em>'.__LINE__.'</em>]='.replaceReturnNewLine(htmlentities($chunkLenHex)).'<br />
+';
+                       /* DEBUG: */
+
+                       // Validation if it is hexadecimal
+                       if (!isHexadecimal($chunkLenHex)) {
+                               // Please help debugging this
+                               //* DEBUG: */ die('ABORT:chunkLenHex=<pre>'.replaceReturnNewLine(htmlentities($chunkLenHex)).'</pre>');
+                               debug_report_bug(__FUNCTION__, __LINE__, 'Value ' . $chunkLenHex . ' is not properly chunk encoded.');
+
+                               // This won't be reached
+                               return $chunk;
+                       } // END - if
+
+                       // Position of next chunk is right after \r\n
+                       $offset   = $offset + strlen($chunkLenHex) + strlen(getConfig('HTTP_EOL'));
+                       $chunkLen = hexdec(rtrim($chunkLenHex, getConfig('HTTP_EOL')));
+
+                       /* DEBUG: *
+                       print 'chunkLen='.$chunkLen.'<br />
+offset[<em>'.__LINE__.'</em>]='.$offset.'<br />';
+                       /* DEBUG: */
+
+                       // Moved out for debugging
+                       $next  = mb_substr($chunk, $offset, $chunkLen);
+                       //* DEBUG: */ print 'next=<pre>'.replaceReturnNewLine(htmlentities($next)).'</pre>';
+
+                       // Count occurrences of \r\n
+                       $count = mb_substr_count($next, getConfig('HTTP_EOL'));
+
+                       // Correct it because we need to subtract occurrences of \r\n
+                       $chunkLen = hexdec(rtrim($chunkLenHex, getConfig('HTTP_EOL'))) - ($count * strlen(getConfig('HTTP_EOL')));
+
+                       $dechunk .= mb_substr($chunk, $offset, $chunkLen);
+
+                       /* DEBUG: *
+                       print('offset[<em>'.__LINE__.'</em>]='.$offset.'<br />
+lineEndAt[<em>'.__LINE__.'</em>]='.$lineEndAt.'<br />
+len='.$len.'<br />
+count='.$count.'<br />
+chunkLen='.$chunkLen.'<br />
+chunkLenHex='.$chunkLenHex.'<br />
+dechunk=<pre>'.replaceReturnNewLine(htmlentities($dechunk)).'</pre>
+chunk=<pre>'.replaceReturnNewLine(htmlentities($chunk)).'</pre>');
+                       /* DEBUG: */
+
+                       // Is $offset + $chunkLen larger than or equal $len?
+                       if (($offset + $chunkLen) >= $len) {
+                               // Then stop processing here
+                               break;
+                       } // END - if
+
+                       // Calculate next offset of chunk
+                       $offset = mb_strpos($chunk, getConfig('HTTP_EOL'), $offset + $chunkLen) + 2;
+
+                       /* DEBUG: *
+                       print('offset[<em>'.__LINE__.'</em>]='.$offset.'<br />
+next[100]=<pre>'.replaceReturnNewLine(htmlentities(mb_substr($chunk, $offset, 100))).'</pre>
+---:---:---:---:---:---:---:---:---<br />
+');
+                       /* DEBUG: */
+               } // END - while
+
+               // Return de-chunked string
+               return $dechunk;
+       }
+} // END - if
+
 // [EOF]
 ?>