分类目录归档:PHP

PHP源码,PHP扩展,PHP程序

wordpress升级到3.3引起的血案

某位哥哥因为看着wordpress3.3的升级提示激动了,一不小心就将它升级了,升级后一切正常,只是之前可用的附件上传功能现在不能用了,显示报错如下:

Warning: touch() [function.touch]: SAFE MODE Restriction in effect.
 The script whose uid is 10041 is not allowed to access /tmp owned by uid 0 in ......

各种纠结……

依据提示我们知道是由于PHP开启了安全模式的原因,最简单的办法:修改php.ini文件,将safe_mode设置为off,将安全模式关闭。

只是这是生产环境,关闭安全模式不太靠谱。只能换方案。
依据刚才的错误提示,我们知道是安全模式在检查执行程序的文件的UID与需要写入的文件夹的UID不一样,这是因为在PHP的安全模式中,对于部分函数是需要检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者),很不幸,touch就是这样的函数之一。当然,有这个规则就应该有跳过这个规则的办法,在php.ini文件中,我们可以设置safe_mode_include_dir参数,多个路径以冒号隔开(Windows是分号),在此参数内设置的目录及其子目录都将会越过 UID/GID 检查。

除此之外,安全模式相关的还有一个open_basedir设置项,PHP 所能打开的文件限制在open_basedir指定的目录树,包括文件本身。本指令不受安全模式打开或者关闭的影响。

safe_mode_include_dir和open_basedir指定的限制实际上是一个前缀,而非一个目录名。这也就是说“safe_mode_include_dir = /tmp”将允许访问“/tmp和“/template”(如果它们存在的话),。如果希望将访问控制在一个指定的目录,那么请在结尾加上一个斜线,例如:“safe_mode_include_dir = /tmp”。

这两个与本次安全问题相关的内容都添加了/tmp和当前操作的目录,发现依旧报错。

追根溯源,从源码中开始吧,如果对于PHP源码不太熟悉,那么我们可以从本次事件的关键点safe_mode_include_dir开始。

在PHP源码中搜索safe_mode_include_dir,除了与此配置项相关的初始化操作,我们可以找到验证此项的函数php_check_safe_mode_include_dir。现在,我们可以用GDB debug的方法查看,给php_check_safe_mode_include_dir函数增加断点,运行一个你认为一定会经过此参数检查的PHP函数调用,如fopen等。运行,发现会在断点处停下,如果我们运行一个只包含了touch函数的代码,会发现程序会执行完,即touch函数根本就没有调用php_check_safe_mode_include_dir函数,即在安全模式下,根本无法跳过UID的检测。

或者我们直接查看touch函数的PHP源码实现,其代码在/ext/standard/filestat.c文件649行。在其代码中我们可以清楚的看到其实现过程在处理了相关参数后,就直接判断完全模式和检测UID了,如下代码:

PHP_FUNCTION(touch)
{
 
...
 
/* Safe-mode */
if (PG(safe_mode) && (!php_checkuid(filename, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
RETURN_FALSE;
}
 
/* Check the basedir */
if (php_check_open_basedir(filename TSRMLS_CC)) {
RETURN_FALSE;
}
 
...
 
}

找到了问题的根源,也许是PHP的BUG,也许是其它考量,待确认。

那么这个问题怎么解决呢?

比较wordpress3.3和wordpress3.2的代码,我们发现wordpress在处理附件上传时最终都会调用wp_handle_upload函数(/wp-admin/include/file.c)。在此函数中wordpress3.3版本多了一句:

$tmp_file = wp_tempnam($filename);

在wp_tempnam函数中,最终会通过touch生成临时文件,从前面我们可以知道我们暂时无法解决在此环境下的touch函数的问题,那么我们只能适应这个函数,也就是说我们需要将新创建的临时文件的目录UID和执行的PHP文件的UID设置为一样,那么这个临时文件的目录是否可以改变?我们查看wp_tempnam函数,发现其最先取的临时目录的地址为常量WP_TEMP_DIR的值,此值默认是没有设置的,如果这个值没有才会取其它值。此时,解决方案就浮出水面了:在wp-config.php文件中设置常量WP_TEMP_DIR的值为网站目录下某个UID和PHP脚本一样UID的目录即可。

附gdb bt显示的内容

#0 php_check_safe_mode_include_dir (path=0x86f4fcc "/tmp/aaa43243.tmp")
at /home/martin/project/c/phpsrc/main/fopen_wrappers.c:319
#1 0x0829d97b in php_plain_files_stream_opener (wrapper=0x85eb624, 
path=0x86f4fcc "/tmp/aaa43243.tmp", mode=0x86f6b38 "a", options=4, 
opened_path=0x0, context=0x86f5560)
at /home/martin/project/c/phpsrc/main/streams/plain_wrapper.c:991
#2 0x0829842d in _php_stream_open_wrapper_ex (
path=0x86f4fcc "/tmp/aaa43243.tmp", mode=0x86f6b38 "a", options=12, 
opened_path=0x0, context=0x86f5560)
at /home/martin/project/c/phpsrc/main/streams/streams.c:1867
#3 0x08229b73 in php_if_fopen (ht=2, return_value=0x86f5544, 
return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/martin/project/c/phpsrc/ext/standard/file.c:909
#4 0x0817b088 in phar_fopen (ht=2, return_value=0x86f5544, 
return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/martin/project/c/phpsrc/ext/phar/func_interceptors.c:418
#5 0x0831b9a1 in zend_do_fcall_common_helper_SPEC (execute_data=0x8727eb8)
at /home/martin/project/c/phpsrc/Zend/zend_vm_execute.h:313
#6 0x082f4bc6 in execute (op_array=0x86f545c)
at /home/martin/project/c/phpsrc/Zend/zend_vm_execute.h:104
#7 0x082d2256 in zend_execute_scripts (type=8, retval=0x0, file_count=3)
---Type <return> to continue, or q <return> to quit---
at /home/martin/project/c/phpsrc/Zend/zend.c:1194
#8 0x08281b70 in php_execute_script (primary_file=0xbffff2a0)
at /home/martin/project/c/phpsrc/main/main.c:2225
#9 0x08351b97 in main (argc=4, argv=0xbffff414)
at /home/martin/project/c/phpsrc/sapi/cli/php_cli.c:1190

Yii框架的组件行为管理机制和Mix-in

Yii框架的组件行为管理机制和Mix-in

本文包括以下内容:

  • Yii框架的组件行为管理机制介绍
  • Ruby、PHP5.4和Mix-in

在Yii框架的官网,我们可以看到关于Behaviors & events的介绍: Behaviors are simply a way of adding methods to an object.

我们看官网上的使用示例:

class SomeClass extends CBehavior{
    public function add($x, $y) { return $x + $y; }
}

class TestComponent extends CComponent {
}

$test_comp = new TestComponent();
$test_comp->attachbehavior('blah', new SomeClass);
$test_comp->add(2, 5);

在TestComponent类的对象创建的后,我们可以通过调用attachbehavior给对象添加新的方法。

通过其源码(在base/CComponent.php)可以知道它是通过在组件类内部以私有变量的方式存储这些添加的方法所在的对象, 通过魔术方法__call,当调用一个未定义的方法时需要调用__call方法的特性,遍历所有通过attachbehavior方法添加进来的对象, 并判断此对象是否禁用并且此对象是否存在需要调用的方法,如果存在则调用。

此种实现方式存在如下一些问题:

  • 如果多个对象存在相同的方法,则程序调用时永远会调用第一次添加进去的方法
  • 如果我们只是需要某个对象中的某个方法,但是在存储上需要将整个对象添加到列表中

也许你会觉得这些都是一些如果,都是一些假设,可能不会出现,这有些像众所周知的goto语句问题,如果用得好,这是一个利器,如果用得不好,可能会给你带来痛苦。 Yii框架中的这种机制实现运行时的方法绑定,虽然类的属性和实例参数仍然归属于其它类和对象。

在官方说明中也提到了这是一种类似于ruby语言的实现方式,如果我们用Ruby实现上面的方法该如何写呢?如下:

module SomeClass
    def add(x, y)
        return x + y
    end
end

class TestComponet
    include SomeClass
end

test = TestComponet.new
puts test.add(10, 20)

非常简单的实现了类的重用,我们知道在PHP中,接口是可以多继承的,但是接口只是形态上的多继承,是一种对于类实现的约束,是一种规格。 如果要实现这种类的重用,Ruby受Lisp的影响引入了Mix-in,在PHP5.4引入了trait关键字。

在Ruby中Mix-in的关键字是module,而在即将推出的PHP5.4,其对应的关键字是trait; 如果要复用这个定义的类,在Ruby中使用include,而在PHP5.4中使用use。如下PHP代码:

<?PHP
trait SomeClass {
    public function add($x, $y) {
        return $x + $y;
    }
}

class TestComponent {
    use SomeClass;
}

$obj = new TestComponent();
echo $obj->add(10, 20);

对于Mix-in类,有两个约束:

  • 不能单独生成实例
  • 不能继承其它的普通类

如果实例这个类程序执行会显示:

Fatal error: Cannot instantiate trait SomeClass...

如果从其它普通类继承会显示:

Fatal error: A trait (SomeClass) cannot extend a class ...

如果要查找这两个约束的源码实现,可以直接在源码中搜索Cannot instantiate trait和cannot extend a class。 从搜索可以看出:

  • 不能单独生成实例的检测是在new关键字的中间代码执行时执行的,在Zend/zend_vm_execture.h文件
  • 不能继承的约束是在编译成中间代码的过程中实现的,在 Zend/zend_compile.c文件

在面向对象编程语言,Mix-in是一个提供了一些被用于继承或在子类中重用的功能的类,它类似于一种多继承, 但是实际上它是一种中小粒度的代码复用单元,而不直接用于实例化。 虽然这不是一种专业的方式进行功能复用,这在实现多继承的同时,在一定程序上避免了多继承的明显问题。 一如Yii的组件行为管理机制,也是另外一种取巧的Mix-in实现。

代理模式(Proxy)和PHP的反射功能

代理模式(Proxy)和PHP的反射功能

本文包括以下内容:

  • 代理模式概述
  • 代理模式常规示例
  • 使用PHP的反射功能实现多代理

正文

模式意图 :为其他对象提供一种代理以控制对这个对象的访问[GOF95]

代理模式是对象的结构模式,代理模式给某一个对象提供一个代理对象,并由此代理对象控制对原代理对象的引用。代理模式不应该让用户感觉到代理的存在,所以代理对象和原对象的对外的调用接口是一致的。

代理模式一般包括三个角色:

  • 抽象主题角色(Subject):它的作用是统一接口。此角色定义了真实主题角色和代理主题角色共用的接口,这样就可以在使用真实主题角色的地方使用代理主题角色。
  • 真实主题角色(RealSubject):隐藏在代理角色后面的真实对象。
  • 代理主题角色(ProxySubject):它的作用是代理真实主题,在其内部保留了对真实主题角色的引用。它与真实主题角色都继承自抽象主题角色,保持接口的统一。它可以控制对真实主题的存取,并可能负责创建和删除真实对象。代理角色并不是简单的转发,通常在将调用传递给真实对象之前或之后执行某些操作,当然你也可以只是简单的转发。 与适配器模式相比:适配器模式是为了改变对象的接口,而代理模式并不能改变所代理对象的接口。

从以上三个角色我们可以得出一个简单的示例:

/**
 * 代理模式简单示例 2011-10-30 sz
 * @author phppan.p#gmail.com  http://www.phppan.com
 * @package design pattern
 */

/**
 * 抽象主题角色
 */
abstract class Subject {
    abstract public function action();
}

/**
 * 真实主题角色
 */
class RealSubject extends Subject {

    public function __construct() {
    }

    public function action() {
        echo "action method in RealSubject<br />\r\n";
    }

}

/**
 * 代理主题角色
 */
class ProxySubject extends Subject {

    private $_real_subject = NULL;

    public function __construct() {
    }

    public function action() {
        $this->_beforeAction();

        if (is_null($this->_real_subject)) {
            $this->_real_subject = new RealSubject();
        }

        $this->_real_subject->action();

        $this->_afterAction();
    }

    /**
     * 请求前的操作
     */
    private function _beforeAction() {
        echo "Before action in ProxySubject<br />\r\n";
    }

    /**
     * 请求后的操作
     */
    private function _afterAction() {
        echo "After action in ProxySubject<br />\r\n";
    }

}

/**
 * 客户端
 */
class Client {

    public static function main() {
        $subject = new ProxySubject();
        $subject->action();
    }
}

Client::main();
?>

以上的示例适用于代理模式为一个对象提供一个代理对象。当为多个对象提供代理对象时,是否我们需要创建多个代理对象呢? 如此按这种实现确实是,但是我们也可以将一个代理对象给多个对象用,或者可以作为一个代理工厂,此时一个对象会保留多个对象的引用, 并且在执行操作时需要判断所代理的是哪个对象,那么此时这种判断如何进行呢? 以多个instanceof判断对象的归属?但是这样,代理对象也必须知道真实对象的方法, 如果我们现在的需求是实现一个代理对象和真实对象的松耦合,但是在代理对象中,对于每个真实对象都有一个前置操作和后转操作, 也许我们可以使用反射,那么反射是什么呢?

反射一般来说是指在程序执行过程中获取程序相关的信息或者修改程序信息。 如获取类、方法、函数等的详细信息,或删除类方法定义。ruby的反射功能实现了类、对象、常量、变更等的获取和修改,但是在PHP中只有获取功能而无修改。 在PHP中我们可以使用PHP5以后的Reflection扩展, 此扩展是PHP的核心扩展,在PHP安装时就已经自动加载,它的作用是分析PHP程序, 导出或提取出关于类、方法、函数、属性、参数等的详细信息,包括注释。 在这里我们经常可以此扩展来实现对PHP程序内部关于类、方法等的信息检测,并做作出处理。

<?PHP
/**
 * 使用反射实现代理工厂 2011-10-30 sz
 * @author phppan.p#gmail.com  http://www.phppan.com
 * @package design pattern
 */

/**
 * 真实主题角色 A
 */
final class RealSubjectA {

    public function __construct() {
    }

    public function actionA() {
        echo "actionA method in RealSubject A <br />\r\n";
    }

}

/**
 * 真实主题角色 B
 */
final class RealSubjectB {

    public function __construct() {
    }

    public function actionB() {
        echo "actionB method in RealSubject B <br />\r\n";
    }

}

/**
 * 代理主题角色
 */
final class ProxySubject {

    private $_real_subjects = NULL;

    public function __construct() {
        $this->_real_subjects = array();
    }

    /**
     * 动态添加真实主题
     * @param type $subject
     */
    public function addSubject($subject) {
        $this->_real_subjects[] = $subject;
    }

    public function __call($name, $args) {
        foreach ($this->_real_subjects as $real_subject) {

            /* 使用反射获取类及方法相关信息  */
            $reflection = new ReflectionClass($real_subject);

            /* 如果不存在此方法,下一元素 */
            if (!$reflection->hasMethod($name)) {
                continue;
            }

            $method = $reflection->getMethod($name);

            /* 判断方法是否为公用方法并且是否不为抽象方法 */
            if ($method && $method->isPublic() && !$method->isAbstract()) {
                $this->_beforeAction();

                $method->invoke($real_subject, $args);

                $this->_afterAction();

                break;
            }
        }
    }

    /**
     * 请求前的操作
     */
    private function _beforeAction() {
        echo "Before action in ProxySubject<br />\r\n";
    }

    /**
     * 请求后的操作
     */
    private function _afterAction() {
        echo "After action in ProxySubject<br />\r\n";
    }

}

/**
 * 客户端
 */
class Client {

    public static function main() {
        $subject = new ProxySubject();

        $subject->addSubject(new RealSubjectA());
        $subject->addSubject(new RealSubjectB());

        $subject->actionA();
        $subject->actionB();
    }

}

Client::main();
?>

以上示例使用反射机制实现了对不同真实对象的代理,但是还存在一些问题:

  • 对象的显式添加,不能对用户透明
  • 真实对象的重复问题,如果存在多个相同的对象添加到对象列表中,如何处理?Flyweigh?