wamp中的copy函数bug

查看ecshop的添加商品代码的时候碰到一个奇怪的问题 . 目前为止仍然没有找到解决的办法, 但是挖掘出了一些信息.

如何重现? WAMP环境, 在添加商品中, 商品图片使用外部url. 提交之后就会出现下面的错误:

Warning: copy() [function.copy]: open_basedir restriction in effect. 
File(http://zhangley.com/article/chrome-ajax-redirect/capture0711.png) is not within the allowed path(s): 
(F:\WebProjects\ecshop\upload;C:\Windows\Temp;) in F:\WebProjects\ecshop\upload\admin\goods.php on line 908

如果不想每次都如此麻烦的重现问题, 可以用一个简单的脚本:

<?php

error_reporting (E_ALL);


if(copy("http://zhangley.com/article/chrome-ajax-redirect/capture0711.png""xx.png")){
    echo 'copy is right' 
else {
    $error = error_get_last();
    echo 'copy error' $error['type'];
    print_r($error);
    echo ' copy failed' 
}
phpinfo();
?>

奇怪的地方在于copy的source是一个url, 应该不存在受到open_basedir限制的问题. 我们可以简单将open_basedir注释掉, 这样就不会有问题. 但这不是最好的办法, 有些场合我们必须限制basedir. 而且经过测试, LAMP环境中的open_basedir并不影响copy url的执行.

可以肯定不是PHP版本的问题, 在CentOS上测试了PHP 5.1.6 PHP 5.2.17 PHP5.3均没有问题. 也不是Apache的问题, CentOS上分别用nginx, apache做服务器, 同样没有问题.

一开始用的是PHPNOW , 然后用appserv-win32-2.5.10测试, 问题都一样. 总结一下出现问题的条件: open_basedir不为空, WAMP. 所以实际上copy从url下载文件这个功能是正常的, 但是copy因为判断open_basedir, 条件不成立所以后面的下载动作完全没有执行. 这个判断的逻辑按理应该在各个系统上都是一样的, 但是结果显示却是不一样. 最有可能是操作系统的某个系统调用影响了PHP的某一部分代码, 但是这个很难定位.

copy函数的源码如下, 位于 /php-5.4.5/ext/standard/file.c

/* {{{ proto bool copy(string source_file, string destination_file [, resource context])
   Copy a file */
PHP_FUNCTION(copy)
{
    char *source, *target;
    int source_lentarget_len;
    zval *zcontext NULL;
    php_stream_context *context;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC"pp|r", &source, &source_len, &target, &target_len, &zcontext) == FAILURE) {
        return;
    }

    // 检查openbase dir, wamp平台上返回-1, 从而提前结束
    if (php_check_open_basedir(source TSRMLS_CC)) {
        RETURN_FALSE;
    }

    context php_stream_context_from_zval(zcontext0);

    if (php_copy_file_ctx(sourcetarget0context TSRMLS_CC) == SUCCESS) {
        RETURN_TRUE;
    else {
        RETURN_FALSE;
    }
}
/* }}} */

关键在于php_check_open_basedir函数, 在WAMP平台上如果source是url, 其返回值是-1, 表示没有通过检查, 返回0表示正确.

PHPAPI int php_check_open_basedir_ex(const char *pathint warn TSRMLS_DC)
{
    /* Only check when open_basedir is available */
    if (PG(open_basedir) && *PG(open_basedir)) {
        char *pathbuf;
        char *ptr;
        char *end;

        /* Check if the path is too long so we can give a more useful error
        * message. */
        if (strlen(path) > (MAXPATHLEN 1)) {
            php_error_docref(NULL TSRMLS_CCE_WARNING"File name is longer than the maximum allowed path length on this platform (%d): %s"MAXPATHLENpath);
            errno EINVAL;
            return -1;
        }

        pathbuf estrdup(PG(open_basedir));

        ptr pathbuf;

        while (ptr && *ptr) {
            end strchr(ptrDEFAULT_DIR_SEPARATOR);
            if (end != NULL) {
                *end '\0';
                end++;
            }

            if (php_check_specific_open_basedir(ptrpath TSRMLS_CC) == 0) {
                efree(pathbuf);
                return 0;
            }

            ptr end;
        }
        if (warn) {
            php_error_docref(NULL TSRMLS_CCE_WARNING"open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s)"pathPG(open_basedir));
        }
        efree(pathbuf);
        errno EPERM/* we deny permission to open it */
        return -1;
    }

    /* Nothing to check... */
    return 0;
}

上面的函数可以看出来, 如果open_basedir没有设置, 那么直接返回0 . 否则就进行check. while语句会检查open_basedir中的每一个路径, 在WAMP上, while循环结束, 这表示路径不属于open_basedir中的任何一项, 如果开启了警告, 则输出 open_basedir restriction in effect. 而在LAMP上, 同样的场景, while应该是在某次循环中return 0. 因为开头的if成立的话, 这里是唯一能够正确返回的点.

因此一个合理的猜测是php_check_specific_open_basedir函数在参数条件完全相同的情况下, 其计算结果依赖于操作系统平台.

还有一个可能的因素是Thread Safety, 因为凡是LAMP平台上运行的PHP, Thread Safety几乎都是disabled, 而WAMP平台基本都是enabled. 但是我还没有测试WAMP平台上disabled 或者LAMP平台上enabled的情况.