PHP: Second-class functions

I needed to hook curl_exec() to add some functionality but ran into a weird issue with some of our customers' code. It seems that curl_exec() has the ability to execute private callbacks, but if I hook the function using runkit, I get errors.

Sample Code

<?php

// Demo of some weird behavior I'm seeing w/ curl_exec() + runkit_function_rename();

function main()
{
    check_prereqs();

    echo "Starting first test:\n";
    $tester = new CurlTester();
    $tester->test() || die("First curl test shouldn't fail.");

    echo "Hooking function:\n";
    hook_curl();

    echo "Starting second test:\n";
    $tester = new CurlTester();
    $success = $tester->test();
    echo $success ? "The second test passed.\n" : "The second test failed\n";

}

class CurlTester
{
    /** Returns true iff the test passed. */
    function test()
    {
        $ch = curl_init("http://www.google.com");

        // Note that header_handler is *private*. 
        // This seems to work with the original curl_exec(), 
        // but doesn't work if I hook curl_exec:
        $callback = array($this, "header_handler");
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, $callback);

        // Don't output transfer to stdout.
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $data = curl_exec($ch);
        $success = ($data !== false && strlen($data) > 0);
        curl_close($ch);
        return $success;
    }    

    private function header_handler($ch, $data)
    {
        $parts = explode(":", $data);
        if (count($parts) > 1) { 
            $header_name = trim($parts[0]);
            echo "Got HTTP header: $header_name: ...\n"; 
        }
        return strlen($data);
    }
}

function check_prereqs()
{
    if (!extension_loaded("runkit")) {
        die("This test requires the runkit extension");
    }
    if (!ini_get("runkit.internal_override")) {
        echo "This test requires runkit.internal_override=On.\n";
        die("try: php -d runkit.internal_override=On\n");
    }
}

function hook_curl()
{

    // Copy the function so that we can delegate to it:
    runkit_function_copy("curl_exec", "original_curl_exec") || die("function_copy failed");

    // Redefine curl_exec to do some more work:
    $code = <<<'CODE'

        echo "Called curl_exec()\n";
        // Delegate to the original function:
        return original_curl_exec($ch);

CODE;

    runkit_function_redefine("curl_exec", '$ch', $code) || die("function_redefine failed");
}



main();

Execution and Output

$ php test.php 
Starting first test:
Got HTTP header: Date: ...
Got HTTP header: Expires: ...
Got HTTP header: Cache-Control: ...
Got HTTP header: Content-Type: ...
Got HTTP header: Set-Cookie: ...
Got HTTP header: Set-Cookie: ...
Got HTTP header: P3P: ...
Got HTTP header: Server: ...
Got HTTP header: X-XSS-Protection: ...
Got HTTP header: X-Frame-Options: ...
Got HTTP header: Alternate-Protocol: ...
Got HTTP header: Transfer-Encoding: ...
Hooking function:
Starting second test:
Called curl_exec()
PHP Warning:  Invalid callback CurlTester::header_handler, cannot access private method CurlTester::header_handler() in /test.php(82) : runkit created function on line 4

Warning: Invalid callback CurlTester::header_handler, cannot access private method CurlTester::header_handler() in test.php(82) : runkit created function on line 4
PHP Warning:  curl_exec(): Could not call the CURLOPT_HEADERFUNCTION in test.php(82) : runkit created function on line 4

Warning: curl_exec(): Could not call the CURLOPT_HEADERFUNCTION in test.php(82) : runkit created function on line 4
The second test failed
comments powered by Disqus