Giter Club home page Giter Club logo

blog's People

Contributors

drodata avatar

Watchers

 avatar

blog's Issues

接入指南中验证服务器—— 不能使用 Yii2 beautiful URL?

上午将 bind-request/authorize action 完工。此 action 的其中一个工作是:给申请绑定的微信用户发送一条消息。之前测试消息功能时,自己依照教程,将实例代码上传到服务器上,可以实现简单的发送消息的功能。其实就是一个 PHP 文件 wx_sample.php,现在我想把发送消息功能整合进自己的 Yii2 project 内,使用一个 route 作为 URL,例如 http://a.com/message/index. 在实际操作中发现一个问题:使用这种 beautifil URL 形式的地址作为 URL, 是不管用的,即使在

提交后提示“配置成功“的情况下,照样不能正常发送消息。

temp

temp write pad.

自定义菜单 - 事件推送

📖 http://mp.weixin.qq.com/wiki/19/a037750e2df0261ab0a84899d16abd33.html

用户点击自定义菜单后,微信会把点击事件推送给开发者,

2. 点击菜单拉取消息时的事件推送

推送XML数据包示例:

<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[CLICK]]></Event>
<EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>

参数说明:

参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间
MsgType 消息类型,event
Event 事件类型,CLICK
EventKey 事件KEY值,与自定义菜单接口中KEY值对应

其它的都类似。 📝 这里再次回到被动回复消息那一节的内容——通过 POST 发送 xml 数据至自己的服务器。

从计算 ~4 的值学习原码、反码、补码

从一个题目开始:

~4 的值是多少?

~ 是按位取反操作符,4 用二进制表示为 100, 那么取反后是 011, 即 3. 但是正确的却是 -5. 高不清楚为什么,尤其是那个负号是怎么来的?


二进制基础

  1. 二进制的最高位是符号位: 0表示正数,1表示负数
  2. php没有无符号数,换言之,php中的数都是有符号的
  3. 在计算机运算的时候,都是以补码的方式来运算的.
  4. 正数的原码,反码,补码都一样
  5. 负数的反码=它的原码符号位不变,其它位取反(0->1,1->0)
  6. 负数的补码=它的反码+1
  7. 0 的反码,补码都是0

所以,4 在计算机上的完整二进制形式是

00000000 00000000 00000000 00000100

按位取反后是:

11111111 11111111 11111111 11111011

最高位由 0 变为 1, 表明这是一个负数,也就是答案 -5 中的负号的来历。

上面的结果就是答案的二进制形式,但是是以补码的形式存在。因为负数的补码等于其反码加 1,因此我们将上面的值减 1 即可得到其反码:

11111111 11111111 11111111 11111011
                                 -1
11111111 11111111 11111111 11111010

再根据规则5,我们得出正确答案:

10000000 00000000 00000000 00000101

即 -5.

参考:

不要在 PC 端浏览器内测试微信网页授权页 connect_redirect=1

微信网页登陆授权,只能在微信客户端内打开链接,如果试图在 PC 端浏览器内打开链接,url 内将多出一个名为 connect_redirect 的参数,如下:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_userinfo&connect_redirect=1#wechat_redirect

由于授权操作安全等级较高,所以在发起授权请求时,微信会对授权链接做正则强匹配校验,如果链接的_参数顺序不对_,授权页面将无法正常访问

微信网页授权

自定义菜单 - 创建接口

📖 http://mp.weixin.qq.com/wiki/10/0234e39a2025342c17a7d23595c6b40a.html

Example:

$token = 'your-token';
$url = 'https://api.weixin.qq.com/cgi-bin/menu/create?access_token=' . $token;

$menu = ['button' => [
    [
        'type' => 'click',
        "name" => "订单查询",
        'key' => 'key01',
    ],
    [
        'name' => '自助服务',
        'sub_button' => [
            [
                'type' => 'view',
                'name' => '订单系统',
                'url' => 'http://i.a.com/',
            ],
            [
                'type' => 'click',
                'name' => '点赞',
                'key' => 'key02',
            ],
        ],
    ],
]];

// json_encode 第2个参数确保中文能正确显示
$postFieldsData = json_encode($menu, JSON_UNESCAPED_UNICODE);

$ch = curl_init(); 

curl_setopt($ch, CURLOPT_URL, $url); 
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); 
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFieldsData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 

$info = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'Errno'.curl_error($ch);
}

curl_close($ch);

var_dump($info);

修改自定义菜单

同样调用上面的 munu/create api, 只是讲 menu 内容修改即可。 🔔 修改后的菜单不会立即同步到微信客户端, 有一定时间延迟。

DataProvider 设置 setSort() 时提示 “Invalid argument supplied for foreach()”

$dataProvider->setSort([
    'attributes' => [
        'full_name' => [
            'asc'  => ['CONVERT(full_name USING gbk)' => SORT_ASC],
            'desc' => ['CONVERT(full_name USING gbk)' => SORT_DESC],
        ],
    ],
    'defaultOrder' => [
        'last_deal_time' => SORT_DESC,
    ],
]);

上面的配置将出现错误:

PHP Warning – yii\base\ErrorException

Invalid argument supplied for foreach()

解决方法

必须将 last_deal_time 列入 attributes 内,才能避免此错误。

    'attributes' => [
        'last_deal_time', // add this line
        'full_name' => [
            'asc'  => ['CONVERT(full_name USING gbk)' => SORT_ASC],
            'desc' => ['CONVERT(full_name USING gbk)' => SORT_DESC],
        ],
    ],

Fail to practice web service

根据教程中的例子,当我访问 http://localhost/blog/stock/quote 时按理应该在页面中显示一堆 XML 内容,但是在 Chrome 上显示却是:

This page contains the following errors:

error on line 2 at column 6: XML declaration allowed only at the start of the document

Below is a rendering of the page up to the first error.

使用 Ctrl + U view page source 为:


<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="urn:StockControllerwsdl" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
...

注意首行的空行,问题就出在这里,如果没有首行的空行,下面显示的 XML 内容应该就会正常显示出来。

使用 Firefox 访问,上面的问题会看得更清楚:

webservice-error

提示信息中讲得很清楚:XML 文档的声明没有出现在文档的开头(前面有空行)。

Creating my first controller extension [`CController` or `Controller`]

之前熟悉的 Controller 大多通过 Gii 来自动生成,假设创建一个 XyzController Class, 外加一个 index action, Gii 生成的文件结构如下:

protected/
    controllers/
        XyzController.php
    views/
        xyz/
            index.php

假如想在另一个项目中直接使用这个 controller, 需要同时将文件 XyzController.php 和目录 xyz/ 分别复制到新项目的 protected/views/ 目录下。

其实 Yii 中的 Controller 还有另一种形式(distributed as an extension ):controller extension. 它能将 controller 文件及其 views 文件放在同一个目录下,让代码重用(MVC 的精髓)变得更加简单。下面我们以 blog application 中的 SiteController 为例,将它转换成 controller extension.

先对两种类型的 controller 对应的文件结构进行对比,以便对 controller extension 有一个直观的印象。

传统形式:

protected/
    controllers/
        SiteController.php
    views/
        site/
            login.php
            logout.php

controller extension 形式:

protected/
    extensions/
    site/
        SiteController.php
        views/
            login.php
            logout.php

转换的过程实际上就是移动代码的位置,代码几乎不需要做任何改动,除了一点以外:需要将 SiteController Class 的父类由 CController 改为 CExtController.CExtControllerCController 的子类.

A controller distributed as an extension should extend from CExtController, instead of CController.

extensions 下放了很多插件,为了让结构清晰,我们可以在 extensions/ 下新建一个 controller/ 目录,专门放置 controller extension.

2. Configuring in Application Configuration

In order to use a controller extension, we need to configure the CWebApplication::controllerMap property in the application configuration:

return array(
    'controllerMap'=>array(
        'xyz'=>array(
            'class'=>'ext.xyz.XyzClass',
            'property1'=>'value1',
            'property2'=>'value2',
        ),
        // other controllers
    ),
);

⚠️ controllerMap property 可不是放在 components property 内。

配置如下:

// config/main.php
return array(
    ...
    'controllerMap' => array(
        'site' => array(
            'class' => 'ext.controller.site.SiteController',
        ),
    ),
    ...
    'components' => array(),
);

到此,转换工作已完成。重新访问 blog/site/login 页面,看上去和之前一样,但是 SiteController 已经变成 controller extension.

Question

在更改 SiteController Class 的父类时,原先的声明是:

class SiteController extends Controller {}

我直接将 Controller 改为 ExtControler (不是 CExtController), 访问 site/login 时出现如下错误:

Error 500
include(ExtController.php): failed to open stream: No such file or directory

使用完整的 CExtController 没问题。这里面的疑惑是:

Leran Security: XXS + the `filter` validation rule

读完 Security - Cross-site Scripting Prevention 后,发现之前根本没在意这方面。随便找了一个页面(clip/create ),在 "Content" 栏输入下面的内容并提交:

<script>alert('OMG');</script>

clip/view 页面,弹窗出现,一个最简单的 XSS 实例就这么诞生了。

Resolution

CHtmlPurifier API 中说由于 HTML Purifier Package 体积大,因此会影响到性能。API 中建议的正确使用方法是——在数据存入 DB 前进行 purify 工作,而不是将含有恶意代码的内容存入 DB, 每次读取前进行一次 purify 工作。

CHtmlPurifier 可以以 validation rule 的形式使用,下面是一个例子:

array('title, content','filter','filter'=>array($obj=new CHtmlPurifier(),'purify')),

套用到其它 Models 时,仅需修改第一个参数即可。


💡 filter 的值类型为 Callback, 如果对 array($obj=new CHtmlPurifier(),'purify') 表示疑惑,表明对 Callback 的基础知识欠缺。Callback 的本质是函数,而函数有多种形式:普通自定义函数、类中的方法等。对于前者,直接用 String 类型的函数名表示即可,后者需要用特殊的数组来表示:Index 0 表示实例化的类,Index 1 表示方法的名称。


此外,filter validation rule 实际上不是 validator, 而是 data processor. 它的作用和函数 trim() 的作用很类似。以上面这个 rule 为例,假设 content 栏输入内容如下:

Hello<script>alert('OMG');</script>

经过 filter validator “验证”(实际上是处理)后,content 的值已经变成:

Hello

本次 commit 以 clip model 为例,演示了如何避免 XSS 攻击,以后凡是使用文本框(<input type="text">, <textarea>)搜集数据的 attributes, 都要加上 filter validation rule.

消息加解密

使用方法

在安全模式或兼容模式下,url上会新增两个参数encrypt_typemsg_signature。encrypt_type表示加密类型,msg_signature:表示对消息体的签名。

实例化对象

使用构造函数,实例化一个对象,传入公众帐号的token, appid, EncodingAESKey。

解密(公众号收到微信服务器发送的密文后)

安全模式下,公众号收到以下带密文消息体:

encrypt_msg = 
<xml>
    <ToUserName><![CDATA[gh_10f6c3c3ac5a]]></ToUserName>
    ……
    <Encrypt><![CDATA[hQM/NS0ujPGbF+/8yVe61E3mUVWVO1izRlZdyv26zrVUSE3zUEBdcXITxjbjiHH38kexVdpQLCnRfbrqny1yGvgqqKTGKxJWWQ9D5WiiUKxavHRNzYVzAjYkp7esNGy7HJcl/P3BGarQF3+AWyNQ5w7xax5GbOwiXD54yri7xmNMHBOHapDzBslbnTFiEy+8sjSl4asNbn2+ZVBpqGsyKDv0ZG+DlSlXlW+gNPVLP+YxeUhJcyfp91qoa0FJagRNlkNul4mGz+sZXJs0WF7lPx6lslDGW3J66crvIIx/klpl0oa/tC6n/9c8OFQ9pp8hrLq7B9EaAGFlIyz5UhVLiWPN97JkL6JCfxVooVMEKcKRrrlRDGe8RWVM3EW/nxk9Ic37lYY5j97YZfq375AoTBdGDtoPFZsvv3Upyut1i6G0JRogUsMPlyZl9B8Pl/wcA7k7i4LYMr2yK4SxNFrBUw==]]></Encrypt>
</xml>

调用 decryptMsg 接口,传入收到的url上的参数:msg_signature(注意:不是signature,而是msg_signature), timestamp, nonce和接收到的encrypt_msg,若调用成功,msg 则为输出结果,其内容为如下的明文的xml消息体:

<xml>
    <ToUserName><![CDATA[gh_10f6c3c3ac5a]]></ToUserName>
    <FromUserName><![CDATA[oyORnuP8q7ou2gfYjqLzSIWZf0rs]]></FromUserName>
    <CreateTime>1411035097</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[this is a test message]]></Content>
    <MsgId>6060349595123187712</MsgId>
</xml>

#2 公众帐号处理消息

生成需要回复给微信公众平台的xml消息体,假设回复以下内容:

res_msg = 
<xml>    
    <ToUserName><![CDATA[oyORnuP8q7ou2gfYjqLzSIWZf0rs]]></ToUserName>
    <FromUserName><![CDATA[gh_10f6c3c3ac5a]]></FromUserName>
    <CreateTime>1411034505</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[Welcome to join us!]]></Content>
    <FuncFlag>0</FuncFlag>
</xml>

加密(公众号发送消息给微信服务器前)

调用 encryptMsg() method,传入需要回复给微信公众平台的res_msg, timestamp, nonce,若加密成功,则encryptMsg 为密文消息体,内容如下:

<xml>
    <Encrypt><![CDATA[LDFAmKFr7U/RMmwRbsR676wjym90byw7+hhh226e8bu6KVYy00HheIsVER4eMgz/VBtofSaeXXQBz6fVdkN2CzBUaTtjJeTCXEIDfTBNxpw/QRLGLqqMZHA3I+JiBxrrSzd2yXuXst7TdkVgY4lZEHQcWk85x1niT79XLaWQog+OnBV31eZbXGPPv8dZciKqGo0meTYi+fkMEJdyS8OE7NjO79vpIyIw7hMBtEXPBK/tJGN5m5SoAS6I4rRZ8Zl8umKxXqgr7N8ZOs6DB9tokpvSl9wT9T3E62rufaKP5EL1imJUd1pngxy09EP24O8Th4bCrdUcZpJio2l11vE6bWK2s5WrLuO0cKY2GP2unQ4fDxh0L4ePmNOVFJwp9Hyvd0BAsleXA4jWeOMw5nH3Vn49/Q/ZAQ2HN3dB0bMA+6KJYLvIzTz/Iz6vEjk8ZkK+AbhW5eldnyRDXP/OWfZH2P3WQZUwc/G/LGmS3ekqMwQThhS2Eg5t4yHv0mAIei07Lknip8nnwgEeF4R9hOGutE9ETsGG4CP1LHTQ4fgYchOMfB3wANOjIt9xendbhHbu51Z4OKnA0F+MlgZomiqweT1v/+LUxcsFAZ1J+Vtt0FQXElDKg+YyQnRCiLl3I+GJ/cxSj86XwClZC3NNhAkVU11SvxcXEYh9smckV/qRP2Acsvdls0UqZVWnPtzgx8hc8QBZaeH+JeiaPQD88frNvA==]]></Encrypt>
    <MsgSignature><![CDATA[8d9521e63f84b2cd2e0daa124eb7eb0c34b6204a]]></MsgSignature>
    <TimeStamp>1411034505</TimeStamp>
    <Nonce><![CDATA[1351554359]]></Nonce>
</xml>

#3 返回码

函数返回码 说明
0 处理成功
-40001 校验签名失败
-40002 解析xml失败
-40003 计算签名失败
-40004 不合法的AESKey
-40005 校验AppID失败
-40006 AES加密失败
-40007 AES解密失败
-40008 公众平台发送的xml不合法
-40009 Base64编码失败
-40010 Base64解码失败
-40011 公众帐号生成回包xml失败

Creating My First Widget Extension

本 commit 是依照 Extending Yii - Creating Extensions - Widget 部分所举的例子进行的实践。

The easiest way of creating a new widget is extending an existing widget and overriding its methods or changing its default property values.

创建自己的第一个 Yii extension 并不是什么难事,先从最简单的做起。继承已有的一个 widget, 然后进行简单的修改(覆盖方法或改变属性值)。我们以 TabView widget 为例来演示一下。

__autoload() 和 spl_autoload_register()

https://github.com/yiisoft/yii2/blob/master/framework/BaseYii.php#L272

Yii 的 class autoloading 使用的是 PHP 的 Autoloading Classes 机制。通常情况下,使用一个类前必须先“引入”(includerequre)它。假设在一个目录下有三个文件:

ClassA.php
ClassB.php
index.php

要想在 index.php 内使用 ClassAClassB, 必须在开头进行引用:

// index.php
include ClassA.php
include ClassB.php

$class_a = new ClassA();
$class_b = new ClassB();

显然这很麻烦。__autoload() 函数让这一切变得简单,借助 __autoload() 函数,上面的代码可以重写:

function __autoload($class_name)
{
    include $class_name . '.php';
}

$class_a = new ClassA();
$class_b = new ClassB();

这个函数在你试图使用一个未定义(即引入)的类时会自动被调用,达到自动引入的目的。通过调用 __autoload(), the script engine 得到一个在报错前引用所需类的最后机会。

更好的方式——spl_autoload_register()

PHP 提供了一个比 __autoload 函数更灵活(more flexible)的函数 —— spl_autoload_register, Yii 正是使用 spl_autoload_register 来实现 class autoloading. 它的声明如下:

bool spl_autoload_register (
    [ callable $autoload_function 
    [, bool $throw = true 
    [, bool $prepend = false ]]] 
)

其中第一个参数是一个 callback. 使用 spl_autoload_register 再次重写上面的代码如下:

function my_autoloader($class)
{
    include $class_name . '.php';
}

spl_autoload_register(my_autoloader);

$class_a = new ClassA();
$class_b = new ClassB();

More Flexible

之所以更灵活,是因为 __autoload() 的参数只能是 string, 即函数的名称。而 spl_autoload_register 中的 callable $autoload_function parameter 的形式就多了,可以是下面任意一种:

  • string function name. 即上面例子中的形式;

  • a method of an instantiated object;

  • static class methods;

    spl_autoload_register(['Yii', 'autoload']);
    
  • anonymous functions;

    spl_autoload_register(function($class){
        include $class_name . '.php';
    });
    

Learn Gii: code generator

学会自定义 Gii 的模板。以自定义 Model generator 为例,只需在 protected/ 下新建 gii/model/templates/ 目录,并在该目录下新建一个自己命名的目录,然后在将 Gii 自带的 Model generator 模板(system.gii.generators.model.templates.default 内的 Model.php 文件)拷贝过来,自己按需修改即可。

🔴 创建 一个全新的 code generator 部分不易看懂。另外,本节涉及到的 API 中的类单独摘出来,见 http://naotu.baidu.com/viewshare.html?shareId=ax1d8rzyij9c

ccodemodel

Creating my first behavior extension

What is Behavior

Behavior 本质上是一个对象,含有属性和方法。Behavior 可以附加到 Component 上,使得 Component “继承”了 Behavior 中的属性和方法,因而可以调用它们。

关于 Behavior 最重要的一点是:Behavior 实现了 Component 的“多重继承”。一个 Component 一旦 attach 了某个 Behavior, 它就能使用这个 Behavior 内的所有方法,就像 Component extends from Behavior 一样(实际上不是,因为 Component 往往已经有父类).

自己想了一个不太恰当的例子来说明 Behavior 的特性。假设有个类:GoodHabitBadHabit, 两个类内分别声明了一些好习惯和坏习惯:

class GoodHabit
{
    function readBook() {} // 读书
    function buildBody() {} // 健身
}

class BadHabit
{
    function smoke() {} // 抽烟
    function drink() {} // 酗酒
}

接下来再创建一个表示“学生”的类。学生开始在学校学的都是好习惯,因此它默认继承自 GoodHabit:

class Student extends from GoodHabit
{
}

假设 $xiaoming (小明) 是一个 Student object, 他有两个好习惯:阅读和健身。后来小明进入社会,慢慢染上一些坏习惯,他开始有抽烟的行为。如何在不改变 GoodHabit 的情况下让小明使用 BadHabit 里的方法呢?Behavior 就是干这个的。

我们创建一个坏习惯的行为 BadHabitBehavior, 然后将这个行为附加到小明身上:

$xiaoming->attachBehavior('behavior1',BadHabitBehavior);

现在小明同时有了优点和缺点,他现在能抽烟了:

$xiaoming->smoke();

新建 behavior 时,必须继承 IBehavior interface, 不过更常用的是继承 IBehavior 的实现(子类),如 CBehavior, CActiveRecordBehavior 等。

class CBehavior extends CComponent implements IBehavior {}

Using Behaviors

包含两步:

  1. 为组件附加行为:

    $component->attachBehavior($name,$behavior);

    $name 是行为的 ID.

  2. 在组件中使用行为中的方法:

    $component->test();

通过 attachBahavior 来附加是标准的方法,但是不常用,常用的是下面的方法:

Attaching in Configurative Way

More often, a behavior is attached to a component using a configurative way instead of calling the attachBehavior method. For example, to attach a behavior to an application component, we could use the following application configuration:

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'CDbConnection',
            'behaviors'=>array(
                'xyz'=>array(
                    'class'=>'ext.xyz.XyzBehavior',
                    'property1'=>'value1',
                    'property2'=>'value2',
                ),
            ),
        ),
        //....
    ),
);

The above code attaches the xyz behavior to the db application component. We can do so because CApplicationComponent defines a property named behaviors. By setting this property with a list of behavior configurations, the component will attach the corresponding behaviors when it is being initialized.

3 Configurative Way (For CController, CFormModel and CActiveRecord)

直接覆盖里面的 behaviors() 方法即可。这些类在初始化过程中会自动完成附加工作。

The classes will automatically attach any behaviors declared in this method during initialization. For example,

public function behaviors()
{
    return array(
        'xyz'=>array(
            'class'=>'ext.xyz.XyzBehavior',
            'property1'=>'value1',
            'property2'=>'value2',
        ),
    );
}

Accessing Attached Behavior Like Property of Component

attached behavior 可以像使用 property 那样访问。举个例子,假设名为 tree 的行为被附加到 component $component 上,我们可以通过下面的方法获得该行为对象的引用:

$behavior = $component->tree;

Diable Behavior Temporarily

$component->disableBehavior($name);
// the following statement will throw an exception
$component->test();
$component->enableBehavior($name);
// it works now
$component->test();

Method Precedence

假设有两个 behavior b1b2 都附加到组件 C 上,b1b2 内都用名为 m1() 的方法, 这时 C->m1() 调用的是哪个 behavior 中的 m1() 呢?这就要看 b1b2 谁先附加到组件中,调用的是先附加的行为中的方法。

It is possible that two behaviors attached to the same component have methods of the same name. In this case, the method of the first attached behavior will take precedence.

Relationship Between Behavior and Event

与 Event 配合使用,让 Behavior 变得更加强大。Behavior 是对象,而 event handler 接收的方法。

⭐ A behavior, when being attached to a component, can attach some of its methods to some events of the component. By doing so, the behavior gets a chance to observe or change the normal execution flow of the component.

当某个行为被附加到一个组件时,行为本身又可以将它的方法附加到组件的事件中去。也就是说,行为既可以“被附加”(行为被附加到组件),又可以“附加”(把行为中的方法附加到组件中的事件中)。

Accessing Behavior's Properties

A behavior's properties can also be accessed via the component it is attached to.

属性包括两种:

The properties include both the public member variables and the properties defined via getters and/or setters of the behavior.

For example, if a behavior has a property named xyz and the behavior is attached to a component $a. Then we can use the expression $a->xyz to access the behavior's property.

上面的话很好理解:behavior 的作用实际上和继承差不多,既然是继承,自然包括方法和属性两种。


For example, the CActiveRecordBehavior class implements a set of methods (:one:)to respond to the life cycle events raised in an ActiveRecord object. A child class can thus override these methods to put in customized code which will participate in the AR life cycles.

https://trello.com/c/FYK9jOyf

  • 1️⃣ 处指的是 beforeXXX()afterXXX()

不要把 appsecret 和 access_token 传给客户端

由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起

Read notes milestone

一、开始开发

  • 获取 Access Token #33
  • 获取微信服务器 IP 地址列表 #34
  • 报警排查指引
  • 接口测试号申请
  • 接口在线调试
  • FAQ

二、自定义菜单

  • #27
  • 查询接口 #29
  • 删除接口
  • 事件推送 #30 再次回到被动回复消息那儿。
  • 个性化菜单接口 #31
  • 获取公众号的菜单配置 #32
类别 名称 请求方法
Access Token 获取 GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
获取微信服务器 IP GET getcallbackip
自定义菜单 查询 GET https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN
删除 GET https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN
个性化菜单 创建 POST https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=ACCESS_TOKEN
删除 POST https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=ACCESS_TOKEN
测试匹配结果 POST https://api.weixin.qq.com/cgi-bin/menu/trymatch?access_token=ACCESS_TOKEN
获取自定义菜单配置 GET https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=ACCESS_TOKEN

3. URI Syntax Components

一个通用的 URI 语法由一个层级的组件序列组成,这串组件分别被称为:scheme, authority, path, query, and fragment. 语法如下:

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 

hier-part = "//" authority path-abempty
             / path-absolute
             / path-rootless
             / path-empty
  • The scheme and path components are required, though the path may be empty (no characters).
  • When authority is present, the path must either be empty or begin with a slash ("/") character.
  • When authority is not present, the path cannot begin with two slash characters ("//").

These restrictions result in five different ABNF rules for a path (Section 3.3), only one of which will match any given URI reference.

The following are two example URIs and their component parts:

  foo://example.com:8042/over/there?name=ferret#nose
  \_/   \______________/\_________/ \_________/ \__/
   |           |            |            |        |
scheme     authority       path        query   fragment
   |   _____________________|__
  / \ /                        \
  urn:example:animal:ferret:nose

“该公众号暂时无法提供服务,请稍后再试”

昨天下午,忽然发现公众号异常,具体表现为:所有 click 事件都返回

该公众号暂时无法提供服务,请稍后再试

由于该功能完成于一个月前,现在一点印象也没有。一时间不知从何处下手。

最理想的方式是将回调 URL 设置为订单系统中的一个 route, 但实际上行不通。微信好像要求必须是一个具体的 .php 文件,route 本质是 index.php。

这个消息服务器主要功能是和订单系统对接,因此必须要能访问订单系统中的所有数据。今天早上想到了解决方法。灵感来自 Using Yii in Third-Party Systems.

具体操作方法是:在 @backend/web 目录下新建一个文件(例如 server.php)作为微信服务器和订单系统的桥梁。文件开头内容如下:

require(__DIR__ . '/../../vendor/autoload.php');
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../../common/config/bootstrap.php');

$config = require(__DIR__ . '/../../wechat/config/3rd-party.php');
$application = new yii\web\Application($config);

这和 backend tier's entry script 非常像,最大的不同是没有下面一行:

$application->run();

Yii Guide 这样解释:

This is because by calling run(), Yii will take over the control of the request handling workflow which is not needed in this case and already handled by the existing application.

这里我们主要使用的就是 AR 类和 easywechat SDK (自己已将它集成到 wechat component 内)。因此,我们在 3rd-party.php 内主要配置这两项。

搭建最简单的 server

依照 EasyWechat 快速开始 添加如下内容:

$app = Yii::$app->wechat->app;
$app->server->serve()->send();

至此,带验证功能的 server 已搭建完成(此时将该 url 提交的微信服务器回调 URL 文本框,正常情况下即可通过验证)。

添加消息功能

继续在 server.php 下面追加如下内容:

$app = Yii::$app->wechat->app;

$app->server->setMessageHandler('process');
$app->server->serve()->send();

function process($message)
{
    // put you logic here.
}

Callback 返回值要求

返回值只能是字符串,不得是整数、表达式

function process($message)
{
    //! 该公众号暂时无法提供服务,请稍后再试
    return time();
    return 3;

    // ok
    return 'ok';
}

开始开发 - 获取 Access Token

access_token 是公众号的全局唯一票据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

公众平台的API调用所需的access_token的使用及生成方式说明:

  • 为了保密appsecrect,第三方需要一个access_token获取和刷新的 ❓ 中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则会造成access_token覆盖而影响业务;

    📝 什么是中控服务器?

  • 目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡;

  • access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。

⚠️ 如果第三方不使用中控服务器,而是选择各个业务逻辑点各自去刷新access_token,那么就可能会产生冲突,导致服务不稳定。

获取 Access Token

公众号可以使用AppID和AppSecret调用本接口来获取access_token。发送 GET 请求至:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数说明:
grant_type: 值为 client_credential
返回一个 JSON 数据包,正确时为:{"access_token":"ACCESS_TOKEN","expires_in":7200}
错误时为:{"errcode":40013,"errmsg":"invalid appid"}

User Group

User Group

Example Desc. Note
create('Customers') Create a group named 'Customers'
update(100, 'customer') Update group name, 100 is group id
delete(100) Delete a group whose id is 100
moveUser($openId, $groupId) Move a user to a new group
moveUsers([$openIds, $openIds], $groupId) Move a user in batch mode
lists() List all groups info
userGroup($openId) get a user's group info
#1. create('staff')
$this->app->user_group->create('staff');

will return:

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [group] => Array
                (
                    [id] => 100
                    [name] => staff
                )

        )

)

#2. lists(): Return group lists

$this->app->user_group->lists();

will return:

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [groups] => Array
                (
                    [0] => Array
                        (
                            [id] => 0
                            [name] => 未分组
                            [count] => 2
                        )

                    [1] => Array
                        (
                            [id] => 1
                            [name] => 黑名单
                            [count] => 0
                        )

                    [2] => Array
                        (
                            [id] => 2
                            [name] => 星标组
                            [count] => 0
                        )

                    [3] => Array
                        (
                            [id] => 100
                            [name] => staff
                            [count] => 0
                        )

                )

        )

)

#3. update(100, 'new-name')

$a = $this->app->user_group->update(100, '员工');

print $a:

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [errcode] => 0
            [errmsg] => ok
        )

)

#4. delete($groupId)

$a = $this->app->user_group->delete(100);

print $a:

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
        )

)

⚠️ return type.

userGroup()

$a = $this->app->user_group->userGroup($openId);

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [groupid] => 0
        )

)

moveUser($openId, $groupId)

$a = $this->app->user_group->moveUser($openId, 1);

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [errcode] => 0
            [errmsg] => ok
        )

)

moveUsers(array $openIds, int $groupId)

$a = $this->app->user_group->moveUser($openId, 1);

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [errcode] => 0
            [errmsg] => ok
        )

)

-4 >> 1 的计算过程

1. 获得 -4 的补码(二进制表示方法)

首先,需要得到 -4 的补码,因为计算机都是以补码形式进行计算的。

-4 的原码是( 4 的原码,然后把符号位设为 1):

10000000 00000000 00000000 00000100

-4 的反码计算规则:除了符号位保持不变,剩余位按位取反,可得:

11111111 11111111 11111111 11111011

-4 的补码计算规则:-4 的反码 + 1, 可得:

11111111 11111111 11111111 11111100

2. 进行移位操作

操作步骤:向右移 1 位,最高位复制符号位,可得:

11111111 11111111 11111111 11111110

3. 根据补码倒推原码

第 2 步的结果是答案的补码形式,我们对第 1 步进行逆操作,倒推出答案,步骤如下:

11111111 11111111 11111111 11111101 补码减 1 得反码
10000000 00000000 00000000 00000010 符号位不动,剩余位按位取反,得原码 -2

Issues 搜索 Tips

  • 搜索标题中包含 'how' 关键词的 issues: how in:title,类似的,在正文中搜索就是 how in:body;
  • 在标题或正文中搜索: how in:title,body;

302

在订单系统内新增一个 api end, 用来测试 REST.

  1. 复制 backend 目录为 api
  2. 对 environments 稍作配置,
  3. 在 Apache 内添加虚拟主机,在 /etc/hosts 内追加 api.yalong.com;

测试时出现 302 Found 错误。折腾了一下午,最后发现问题出在第 3 步上。

为什么会有 302 提示?

这个 302 提示页面实际上是 Document Root (/home/ts/www/) 内 index.html 显示的内容,我也不知道何时把这个文件复制过来。赶紧编辑其内容,免得引起歧义。最开始我一直怀疑是 Yii 的配置出现问题。

Creating my first filter extension

1. What is Filter?

a piece of code that is configured to be executed before and / or after a controller action executes.

https://trello.com/c/IGUwxO73

2. Creating a Filter Extension

Filter extension 必须继承自 CFilter 或其子类。实现过程主要涉及两个方法:CFilter::preFilter()CFilter::postFilter().

class MyFilter extends CFilter
{
    protected function preFilter($filterChain)
    {
        // logic being applied before the action is executed
        return true; // false if the action should not be executed
    }

    protected function postFilter($filterChain)
    {
        // logic being applied after the action is executed
    }
}

上面两个方法都携带一个类型为 CFilterChain Class 的参数 $filterChain.

我们可以向 Controller Extension 那样,将 Filter Extension 也放在 extensions/ 目录下,

Given a filter class XyzClass belonging to the xyz extension, we can use it by overriding the CController::filters method in our controller class:

class TestController extends CController
{
    public function filters()
    {
        return array(
            array(
                'ext.xyz.XyzClass',
                'property1'=>'value1',
                'property2'=>'value2',
            ),
            // other filters
        );
    }
}

Practise

读完后立刻想到 commit :在想要进行访问控制的 Controller 中都添加了内置的 accessControl filter, 此 filter 需要在 accessRules() 内配置。这个 filter 功能很强大。为了实践本节学的理论知识,我们创建一个名为 LoginFilter 的 filter extension, 简单实现用户是否登录的 filter.

// in extensions/filter/login/
class LoginFilter extends CFilter
{
    protected function preFilter($filterChain)
    {
        if (Yii::app()->user->id)
            $filterChain->run();
        else
        {
            Yii::app()->request->redirect(Yii::app()->baseUrl.'/site/login');
            return false;
        }
    }
}

我们在 BackController 中不再使用 AccessControl Filter, 改用 LoginFilter 来达到禁止未登录用户访问的目的:

class BackController extends Controller
{
    public function filters()
    {
        return array(
            array('ext.filter.login.LoginFilter'),
        );
    }
}

LoginFilter 适用于 BackController 内所有 actions, 有时我们需要 filter 仅针对部分 actions, 这时我们可以在 filter path alias 后面使用 +, -, 后面跟着 action name, 来实现指定 actions. 例如 #15 中的一个实例:

array('ext.filter.precheck.PostRequestCheckFilter + delete')

挂载阿里云 ECS 独立磁盘

fdisk -l 查找独立磁盘信息

以 root 用户登录 ECS, 执行 fdisk -l 查看磁盘信息如下:

Disk /dev/xvda: 21.5 GB, 21474836480 bytes
255 heads, 63 sectors/track, 2610 cylinders, total 41943040 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00068823

    Device Boot      Start         End      Blocks   Id  System
/dev/xvda1   *        2048    41940991    20969472   83  Linux

Disk /dev/xvdb: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders, total 20971520 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

Disk /dev/xvdb doesn't contain a valid partition table

磁盘 /dev/xvdb 正是自己购买的独立磁盘。

2 fdisk /dev/xvdb 添加分区

Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel with disk identifier 0x6b31cd47.
Changes will remain in memory only, until you decide to write them.
After that, of course, the previous content won't be recoverable.

Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help):

按下 m 显示帮助信息:

Command action
   a   toggle a bootable flag
   b   edit bsd disklabel
   c   toggle the dos compatibility flag
   d   delete a partition
   l   list known partition types
   m   print this menu
   n   add a new partition
   o   create a new empty DOS partition table
   p   print the partition table
   q   quit without saving changes
   s   create a new empty Sun disklabel
   t   change a partition's system id
   u   change display/entry units
   v   verify the partition table
   w   write table to disk and exit
   x   extra functionality (experts only)

按下 n, 添加一个新分区:

Command (m for help): n
Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p):

选择分区类型,我们使用默认的 p:

Select (default p): p
Partition number (1-4, default 1): 

选择分区数量,仍然使用默认值 1, 即用整个磁盘只分一个区:

Partition number (1-4, default 1): 1
First sector (2048-20971519, default 2048): 
Using default value 2048
Last sector, +sectors or +size{K,M,G} (2048-20971519, default 20971519): 
Using default value 20971519

Command (m for help):

输入 wq, 将分区表写入磁盘并退出:

Command (m for help): wq
The partition table has been altered!

Calling ioctl() to re-read partition table.
Syncing disks.

再次 fdisk -l 查看磁盘信息如下:

Disk /dev/xvda: 21.5 GB, 21474836480 bytes
255 heads, 63 sectors/track, 2610 cylinders, total 41943040 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00068823

    Device Boot      Start         End      Blocks   Id  System
/dev/xvda1   *        2048    41940991    20969472   83  Linux

Disk /dev/xvdb: 10.7 GB, 10737418240 bytes
107 heads, 17 sectors/track, 11529 cylinders, total 20971520 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x6b31cd47

    Device Boot      Start         End      Blocks   Id  System
/dev/xvdb1            2048    20971519    10484736   83  Linux

这表示分区创建成功。

3 格式化新建的分区

执行如下命令:

mkfs.ext3 /dev/xvdb1 

命令 mkfs.ext3 是创建一个 ext3 格式的文件系统。此命令的输出内容为:

mke2fs 1.42.5 (29-Jul-2012)
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
655360 inodes, 2621184 blocks
131059 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=2684354560
80 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks: 
    32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done 

4 添加分区信息

执行下面命令

echo '/dev/xvdb1 /home ext3 defaults 0 0' >> /etc/fstab

这里我们打算将 /home/ 目录放在独立磁盘上,如果在独立磁盘上存放其它目录的内容,替换 /home/ 即可。

执行上面的命令没有任何动静,我们需要通过执行

cat /etc/fstab

来判断操作是否成功,如果看到如下信息,则表示新分区信息写入工作成功:

# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
UUID=bbbb-xxxx / ext4 errors=remount-ro 0 1
/dev/xvdb1 /home ext3 defaults 0 0

5 mount -a 挂载新分区

-a 参数的意思是,把 /etc/fstab 中提到的所有文件系统都进行挂载。

再次用 df -h 查看,输出如下:

Filesystem                                              Size  Used Avail Use% Mounted on
rootfs                                                   20G  946M   18G   5% /
udev                                                     10M     0   10M   0% /dev
tmpfs                                                    51M  212K   50M   1% /run
/dev/disk/by-uuid/bxxx                                   20G  946M   18G   5% /
tmpfs                                                   5.0M     0  5.0M   0% /run/lock
tmpfs                                                   101M     0  101M   0% /run/shm
/dev/xvdb1                                              9.9G  151M  9.2G   2% /home

从最后一行可以看出,独立磁盘 /dev/xvdb1 已成功挂载到 /home/ 目录上。至此,独立磁盘挂载工作完成。

参考:

Practical Logging - Locate a Slow Execution via Performance Profiling

之前就发现 pk/quotation/create 页面很慢,在学习查看 Performance Profiling 信息后,意外发现这个页面执行的 SQL 语句明显高于其它页面。CDbConnection::getStats() 显示该页竟然执行了六百多条 SQL 语句。而且绝大多数都是下面的查询:

SELECT * FROM `ts_explanation` `t` 
    WHERE vocabulary_id=:ycp49 
    ORDER BY is_main DESC, class ASC

查看 views/quotation/_cu.php 后立刻发现了问题,原来这几百条语句竟然仅仅为了输出一个下拉菜单。

自定义菜单 - 查询接口

📖 http://mp.weixin.qq.com/wiki/5/f287d1a5b78a35a8884326312ac3e4ed.html

使用接口创建自定义菜单后,开发者还可使用接口查询自定义菜单的结构。另外请注意,在设置了个性化菜单后,使用本自定义菜单查询接口可以获取默认菜单和全部个性化菜单信息。

如果没有设置自定义菜单,返回的结果格式如下:

{
    "menu": {
        "button": [
            {
                "type": "click",
                "name": "订单查询",
                "key": "key01",
                "sub_button": []
            },
            {
                "name": "自助服务",
                "sub_button": [
                    {
                        "type": "view",
                        "name": "订单系统",
                        "url": "http://i.a.com/",
                        "sub_button": []
                    },
                    {
                        "type": "click",
                        "name": "点赞",
                        "key": "key02",
                        "sub_button": []
                    }
                ]
            }
        ]
    }
}

可以看到,menu.button 的值与 menu/create 时的信息对应。

如果设置了个性化菜单,将会多出一个 conditionalmenu 的值:

{
    "menu": {
        "button": [
            {
                "type": "click",
                "name": "订单查询",
                "key": "key01",
                "sub_button": []
            },
            {
                "name": "自助服务",
                "sub_button": [
                    // ...
                ]
            }
        ],
        "menuid": 402327447
    },
    "conditionalmenu": [
        {
            "button": [
                {
                    "type": "click",
                    "name": "个性化菜单(男)",
                    "key": "key01",
                    "sub_button": []
                },
            ],
            "matchrule": {
                "sex": "1"
            },
            "menuid": 402332099
        },
    ]
}

在 Yii2 中开发微信公众号注意禁用 csrf validation

在微信开发中,如果使用 Yii2 的某个 route (例如 index.php?r=wechat/server 或者 wechat/server)作为服务器配置地址,那么,在实现被动消息回复功能时,很可能出现

该公众号暂时无法提供服务,请稍后再试

的错误提示。

这个错误之所以让人困惑,在于自己确定被动回复消息的逻辑代码正确无误,但就是不知道问题出在哪里。

单单从微信中提示的这段话,我们是不能排查出错误的。我们可以使用类似 Postman 的浏览器插件来模拟用户在微信中回复消息的过程,借此查看我们的 URL 回复给微信服务器的到底是什么内容。在模拟过程开始前,先声明几个约定:

  • 服务器配置 URL 为 http://mine.com/wechat/server
  • 不管用户回复什么内容,公众号一律回复文本消息”你好“

经过模拟后意外发现,我们的服务器返回的竟是一个 400 status code. 还有

您提交的数据无法被验证

的错误提示。简单了解后发现,这是 Yii2 为了防止 CSRF 的特性导致的。简单的说,Yii2 要求所有提交的表单数据必须出自同一个应用,而微信服务器向我们的服务器发起的 POST 请求(里面包含用户在微信客户端输入的内容)不满足此条件。因此没有正常返回 XML 数据包,而是返回了异常数据。

我们已经知道,导致微信服务器下发“该公众号暂时无法提供服务,请稍后再试”的错误提示有两种情况:

  • 开发者在 5 秒内未回复任何内容
  • 开发者回复的异常数据,例如错误的格式,该是 XML 格式的,回复了 JSON 格式

上面的 Bad Request 400 错误显然属于第二种情况。

解决办法

Controllers 内有个 $enableCsrfValidation 属性用来控制是否开启 CSRF 验证,默认值为 true, 我们只需将其设置为 false 即可:

class WechatController extends Controller
{
    public $enableCsrfValidation = false;

    public function actionServer()
    {
        // logic
    }
    ...

消息加解密

请开发者查看加解密接入指引消息加解密功能开发者FAQ来接入消息体签名及加解密功能。若关注技术实现,可查看技术方案

启用加解密功能(即选择兼容模式或安全模式)后,公众平台服务器在向公众账号服务器配置地址(可在“开发者中心”修改)推送消息时,URL将新增加两个参数加密类型消息体签名),并以此来体现新功能。

  • 明文模式:维持现有模式,没有适配加解密新特性,消息体明文收发,默认设置为明文模式
  • 兼容模式:公众平台发送消息内容将同时包括明文和密文,消息包长度增加到原来的3倍左右;公众号回复明文或密文均可,不影响现有消息收发;开发者可在此模式下进行调试
  • 安全模式(推荐):公众平台发送消息体的内容只含有密文,公众账号回复的消息体也为密文,建议开发者在调试成功后使用此模式收发消息

The position CSS Attribute

Positioned Element

  1. positioned element: 计算过(Computed value)的属性值是:relative,
    absolutefixed;
  2. absolutely positioned element: 包括absolutedfixed两种情况;

语法

position: static; /* default value */
position: relative;
position: absolute; 
position: fixed; 

解释

文档正常的显示顺序是自上而下的,例如下面三个 DIV:

<div id="a" class="block bg-danger"></div>
<div id="b" class="block bg-warning"></div>
<div id="c" class="block bg-success"></div>

应用如下简单样式:

.block {
    width: 100px;
    height: 100px;
}

显示如下:

relative 的含义

现在我们将 #b 添加如下样式:

#b {
    position:relative;
    left: 100px;
}

显示效果如下:

从中我们可以直观地看到relative的作用:

  1. Div b 原来的位置还在(Div c 没有上移填补空缺);
  2. 在 Div b 原有位置的基础上,通过left, right, topbottom 进行相对偏移

absolute 的含义

我们将上面 #b 的样式保持不变,仅仅将position值由relative改成absolute:

#b {
    position:absolute;
    left: 100px;
}

显示效果如下:

通过对比,可以直观地看到relativeabsolute的不同:

  1. Div b 原来的地方没有了(Div c)上移填补其位;
  2. Dib b 的偏移参考坐标变了:由原来的自己改为页面的左上角;

relativeabsolute 的区别

  1. relative 保留元素原有的位置(Gap),absolute 则不保留;
  2. 参考坐标不同:relative 偏移时的参考坐标是元素原来位置的左上角;absolute 偏移是参考的坐标是距离元素最近的、设置 position 属性的父元素(nearest positioned ancestor),如果没有这样的父元素(上面的例子就属这种情况),则参考对象是页面的左上角;

下面再看下 position 值为absolute的元素在有'nearest positioned ancestor' 的情况下的偏移效果。HTML 如下:

<div id="a" class="block bg-danger"></div>
<div id="b" class="block bg-warning">
    <div id="d" class="block bg-info"></div>
</div>
<div id="c" class="block bg-success"></div>
.block {
    width: 100px;
    height: 100px;
}
#b {
    position:relative;
    left: 100px;
}
#d {
    position:absolute;
    left: 50px;
    top: 50px;
}

输出:

fixed

这个可根据字面意思帮助理解,顾名思义,fixed positioned elements 是“固定”的,这里的“固定”特指元素不会随着页面的滚动而滚动;另外,和 absolute positioned elements 类似,fixed positioned elements 也不会在页面中预留位置。

top, bottom, 'left,right` 的参考坐标

left为例:

For absolutely positioned elements (those with position: absolute or position: fixed), it specifies the distance between the left margin edge of the element and the left edge of its containing block.

参考

Refresh Access Token 的过程

首先,得搞清楚 refresh token 存在哪里,怎么获取

$objToken->params['refresh_token']

由于access_token拥有较短的有效期(2小时),当access_token超时后,可以使用refresh_token进行刷新, 🔴 refresh_token拥有较长的有效期(7天、30天、60天、90天),当refresh_token失效的后,需要用户重新授权

  • refresh token 的有效期在哪能看出来?

在获取 refresh_token 后,请求以下链接获取access_token:

https://api.weixin.qq.com/sns/oauth2/refresh_token?
    appid=APPID
    &grant_type=refresh_token
    &refresh_token=REFRESH_TOKEN

正确时,返回如下 JSON 数据包:

{
   "access_token":"ACCESS_TOKEN",
   "expires_in":7200,
   "refresh_token":"REFRESH_TOKEN",
   "openid":"OPENID",
   "scope":"SCOPE"
}

可以看到,返回的内容与拿 code 换取 access token 步骤中的内容格式一样。

2.6. Protocol Versioning

The interpretation of a header field does not change between minor versions of the same major HTTP version, though the default behavior of a recipient in the absence of such a field can change.

Unless specified otherwise, header fields defined in HTTP/1.1 are defined for all versions of HTTP/1.x. In particular, the Host and Connection header fields ought to be implemented by all HTTP/1.x implementations whether or not they advertise conformance with HTTP/1.1.

📝

New header fields can be introduced without changing the protocol version if their defined semantics allow them to be safely ignored by recipients that do not recognize them. Header field extensibility is discussed in Section 3.2.1 (Field Extensibility).

在不改变协议版本号的前提下引入新 header fields

🔴 后面实在看不下去了……

获取 access token 后存入 session 的过程

本文通过追踪代码的方式,对 Yii2 Auth clients 存储 token 的过程有一个了解。

🔴 疑问

  • 经过验证,token object 的确存储在 session 内。问题是,存在这里有何用处?存储 token 的 session 的 key 值又 protected method getStateKeyPrefix() 生成。如果需要使用 session 内的 token, 怎么获取该 key 呢?

假如认证完成后,页面跳转至 dashboard/index 页,在该页内,怎么获取 access token 的值呢?换句话说, access token 的值存在哪?如何存储的?

if (!empty($_GET[$this->clientIdGetParamName])) {
    $clientId = $_GET[$this->clientIdGetParamName];
    /* @var $collection \yii\authclient\Collection */
    $collection = Yii::$app->get($this->clientCollection);
    if (!$collection->hasClient($clientId)) {
        throw new NotFoundHttpException("Unknown auth client '{$clientId}'");
    }
    $client = $collection->getClient($clientId);
    return $this->auth($client);
} else {
    throw new NotFoundHttpException();
}

末尾的 $this->auth($client) 实际上调用了 authOAuth2($client):

if (isset($_GET['error'])) {
    if ($_GET['error'] == 'access_denied') {
        // user denied error
        return $this->redirectCancel();
    } else {
        // request error
        if (isset($_GET['error_description'])) {
            $errorMessage = $_GET['error_description'];
        } elseif (isset($_GET['error_message'])) {
            $errorMessage = $_GET['error_message'];
        } else {
            $errorMessage = http_build_query($_GET);
        }
        throw new Exception('Auth error: ' . $errorMessage);
    }
}
// Get the access_token and save them to the session.
if (isset($_GET['code'])) {
    $code = $_GET['code'];
    $token = $client->fetchAccessToken($code);
    if (!empty($token)) {
        return $this->authSuccess($client);
    } else {
        return $this->redirectCancel();
    }
} else {
    $url = $client->buildAuthUrl();
    return Yii::$app->getResponse()->redirect($url);
}

注意看

Get the access_token and save them to the session.

这句话,对应的代码为:

// WechatPage.php
public function fetchAccessToken($authCode, array $params = [])
{
    $defaultParams = [
        'appid' => $this->clientId,
        'secret' => $this->clientSecret,
        'code' => $authCode,
        'grant_type' => 'authorization_code',
    ];
    $response = $this->sendRequest('GET', $this->tokenUrl, array_merge($defaultParams, $params));
    $token = $this->createToken(['params' => $response]);
    $this->setAccessToken($token);
    return $token;
}

$this->setAccessToken($token);

这一行。此方法在 BaseOAuth 内定义:

/**
 * @param array|OAuthToken $token
 */
public function setAccessToken($token)
{
    if (!is_object($token)) {
        $token = $this->createToken($token);
    }
    $this->_accessToken = $token;
    $this->saveAccessToken($token);
}

注意,$this->_accessToken = $token 一行让我们可以通过

$client->getAccessToken()

来获取 token object:

public function getAccessToken()
{
    if (!is_object($this->_accessToken)) {
        $this->_accessToken = $this->restoreAccessToken();
    }

    return $this->_accessToken;
}

定义如下:

/**
 * Saves token as persistent state.
 * @param OAuthToken $token auth token
 * @return $this the object itself.
 */
protected function saveAccessToken(OAuthToken $token)
{
    return $this->setState('token', $token);
}

这是一个 protected method, 因此不能直接调用,还是得使用上面的 getAccessToken().

/**
 * Sets persistent state.
 * @param string $key state key.
 * @param mixed $value state value
 * @return $this the object itself
 */
protected function setState($key, $value)
{
    if (!Yii::$app->has('session')) {
        return $this;
    }
    /* @var \yii\web\Session $session */
    $session = Yii::$app->get('session');
    $key = $this->getStateKeyPrefix() . $key;
    $session->set($key, $value);
    return $this;
}

至此发现, token 被存储到 session 内。

接入指南

接入指南

接入操作分为以下三步:

  1. 填写服务器配置
  2. 通过代码验证服务器地址的有效性
  3. 根据 API 实现业务逻辑

1. 填写服务器配置

1.1 URL

这个 URL 就是我们自己服务器上 URL, 我们可以先把它理解为一个 PHP 文件。说是一个文件,其实可以理解为我们自己的服务器,它可以用来做验证工作、接收微信服务器发送的 XML 数据包,以及根据发送过来的数据包经过处理(即我们的业务逻辑)再回传给微信服务器。

对于熟悉 Yii 的朋友来说,Yii 的 route 也可以作为这里的 URL, 例如 wechat/server-callback. 关于这一点,初学时走了不少弯路,自己曾经固执地认为这个 URL 必须是一个具体的 PHP script 而不能是 Yii 中的 route 格式。

1.2 Token

随便填写一个字符串即可。它用来验证服务器地址的有效性

2. 验证服务器地址的有效性

当我们提交服务器配置时,微信服务器会向我们填写的 URL 发送一个 GET 请求,并携带如下四个参数:

  • signature
  • timestamp
  • nonce
  • echostr

这四个参数的含义稍后细讲。现在,假如我们填写的 URL 为 http://a.com/wechat/server.php,那么微信发送的 GET 请求 URL 就是类似下面的样子:

http://a.com/wechat/server.php?signature=x&timestamp=x&nonce=x&echostr=x

server.php 需要借助这四个参数值,外加前面输入的 Token 值,来验证该请求来自微信服务器。

timestamp 表示提交服务器配置时的时间戳;nonce 和 echostr 是微信服务器生成的两个随机数;signature 是微信服务器利用一定加密算法生成的加密字符,该值的计算方法如下:

  • 将 token, nonce 和 echostr 三个值按字典序排序:

    $tmpArr = array($token, $timestamp, $nonce);
    sort($tmpArr, SORT_STRING);
  • 将排序后的数组元素连接成一个字符串:

    $tmpStr = implode($tmpArr);
  • 对字符串进行 sha1 加密:

    $signature = sha1($tmpStr);

知道了算法,我们可以在 url 内使用相同的算法计算 signature 的值,并和微信服务器传过来的值比对,如果一致,则可以确认前面的 GET 请求的确是微信服务器发起的。至此,我们的服务器(URL)和微信服务器就算是建立了可信连接。接下来,就可以借助 API 来完成我们的业务逻辑了。

自定义菜单 - 个性化菜单接口

📖 http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html

让公众号的不同用户群体看到不一样的自定义菜单。

判断标准包括:

  • 用户分组(开发者的业务需求可以借助用户分组来完成)
  • 性别
  • 手机操作系统
  • 地区(用户在微信客户端设置的地区)
  • 语言(用户在微信客户端设置的语言)

对应的值:

"matchrule":{
    "group_id": "2",
    "sex": "1",
    "country": "**",
    "province": "广东",
    "city": "广州",
    "client_platform_type": "2"
    "language": "zh_CN"
}

注意,这 7 个值都是可选的,但至少设置一个。

注意事项:

  • 客户端版本要求:iPhone6.2.2,Android 6.2.4以上。
  • 菜单的刷新策略:在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。 📌 测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
  • 接口调用频率限制:普通公众号的个性化菜单的新增接口每日限制次数为2000次,删除接口也是2000次,测试个性化菜单匹配结果接口为20000次
  • 出于安全考虑,一个公众号的所有个性化菜单,最多只能设置为跳转到3个域名下的链接
  • 创建个性化菜单之前必须先创建默认菜单(默认菜单是指使用普通自定义菜单创建接口创建的菜单)。如果删除默认菜单,个性化菜单也会全部删除

0.1 匹配规则说明

当公众号创建多个个性化菜单时,将按照发布顺序,由新到旧逐一匹配,直到用户信息与 matchrule 相符合。如果全部个性化菜单都没有匹配成功,则返回默认菜单。

📝 此处和 Yii2 的 url manager 匹配规则很像,区别是,个性化菜单按照发布顺序,由新到旧匹配;Yii2 url manager 则按照声明顺序,从前往后匹配。

例如公众号先后发布了默认菜单,个性化菜单1,个性化菜单2,个性化菜单3。那么当用户进入公众号页面时,将从个性化菜单3开始匹配,如果个性化菜单3匹配成功,则直接返回个性化菜单3,否则继续尝试匹配个性化菜单2,直到成功匹配到一个菜单。

根据上述匹配规则,为了避免菜单生效时间的混淆,决定不予提供个性化菜单编辑API,开发者需要更新菜单时,需将完整配置重新发布一轮。

2.1 创建个性化菜单

例如,新建一个针对女性用户的个性化菜单:

$menu = [
    'button' => [
        [
            'type' => 'click',
            "name" => "个性化菜单(女)",
            'key' => 'key01',
        ],
        [
            'name' => '自助服务',
            'sub_button' => [
                // ...
            ],
        ],
    ],
    'matchrule' => [
        'sex' => 2,
    ],
];

如果提交成功,将返回类似

{
    "menuid":"208379533"
}

的 JSON 数据。

2.2 删除

POST 数据格式:

{
    "menuid":"208379533"
}

menuid 为菜单id,可以通过自定义菜单查询接口(#29)获取。

正确时的返回JSON数据包如下:

{"errcode":0,"errmsg":"ok"}

2.3 测试匹配结果

POST 数据格式:

{
    "user_id":"drodata"
}

user_id 既可以是粉丝的OpenID,也可以是粉丝的微信号。

该接口将返回菜单的配置信息。

2.4 查询

同自定义菜单,#29

2.5 删除

同自定义菜单

User

All methods' return type is Collection.

get($openid) 获取单个用户

返回值实例:

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [subscribe] => 1
            [openid] => o47YtwKuh6YNSWOpJE3vpCmP17wg
            [nickname] => 瑾砚
            [sex] => 2
            [language] => zh_CN
            [city] => 郑州
            [province] => 河南
            [country] => **
            [headimgurl] => http://wx.qlogo.cn/mmopen/.../0
            [subscribe_time] => 1457843401
            [unionid] => oHgecwfYjTD8Y_HTVTyHB8rrUl3Y
            [remark] => 
            [groupid] => 0
        )

)

lists()

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [total] => 2
            [count] => 2
            [data] => Array
                (
                    [openid] => Array
                        (
                            [0] => o47YtwDqCQikQ2RBtjPM_JhmcR2Y
                            [1] => o47YtwKuh6YNSWOpJE3vpCmP17wg
                        )

                )

            [next_openid] => o47YtwKuh6YNSWOpJE3vpCmP17wg
        )

)

remark($openid, 'hello') 设置备注

$app->user->remark('o47YtwKuh6YNSWOpJE3vpCmP17wg', '小宝');

返回值:

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [errcode] => 0
            [errmsg] => ok
        )

)

group($openid) 查看 user's group id

$a = $this->app->user->getGroup('o47YtwKuh6YNSWOpJE3vpCmP17wg');

returns

EasyWeChat\Support\Collection Object
(
    [items:protected] => Array
        (
            [groupid] => 0
        )

)

Values:

  • 0, “未分组”。
  • 1, Blacklist。
  • 2, star。
  • ❓ how to distinguish: gourp() and getGroup()

配置 YL ECS

  1. 新建一般用户 ts,并设置密码

    useradd -d /home/chen -s /bin/bash -m chen
    passwd chen
  2. 让普通用户 ts 具有 sudo 权限

    1. 安装 sudo package

      # apt-get install sudo
    2. 临时修改 /etc/sudoers 文件权限,使得 root 可以直接使用 vim 编辑:

      # chmod +w /etc/sudoers
    3. 编辑 /etc/sudoers, 在里面找到如下一行:

      root    ALL=(ALL:ALL) ALL

      在后面追加一行:

      ts      ALL=(ALL) NOPASSWD: ALL

      保存,退出。以后 ts 就能使用 sudo 了。

  3. 设置 ts .vimrc, .bashrc 文件

  4. 安装常用软件包(以下 packages 均使用 sudo apt-get install <package-name> 安装)

    1. 安装 PHP, Apache php5, php5-curl, php5-gd. 安装 php5 时,package apache2 作为其依赖包也一并安装;

    2. 测试。我需要把默认的 Document Root 指向 /home/ts/www/ 目录。

      vi /etc/apache2/sites-aviable/000-default
      # 将 DocumentRoot 的值由 /var/www 改成 /home/ts/www, 保存并退出
      # 重启 Apache
      sudo /etc/init.d/apache restart
      # 在 /home/ts/www/ 下新建 index.php, 里面调用 phpinfo()

      这时访问 ip, 提示 403 错误,查找后发现,还需要再修改一处:

      vi /etc/apache/apache2.conf

      找到:

      <Directory /var/www/>
          Options Indexes FollowSymLinks
          AllowOverride None
          Require all granted
      </Directory>

      /var/www/ 替换为 /home/ts/www/. 再次重启 Apache, 这次访问首页,成功出现 phpinfo 内容.

    3. 安装 MySQL

      sudo apt-get install mysql-server mysql-client php5-mysql

自定义菜单 - 获取公众号的菜单配置

http://mp.weixin.qq.com/wiki/14/293d0cb8de95e916d1216a33fcb81fd6.html

本接口提供公众号当前使用的自定义菜单的配置,如果公众号是通过API调用设置的菜单,则返回菜单的开发配置,而如果公众号是在公众平台官网通过网站功能发布菜单,则本接口返回运营者设置的菜单配置

📝 通过不同途径设置的自定义菜单,返回值也不同。

注意事项:

  1. 🔴 第三方平台开发者可以通过本接口,在旗下公众号将业务授权给你后,立即通过本接口检测公众号的自定义菜单配置,并通过接口再次给公众号设置好自动回复规则,以提升公众号运营者的业务体验。
  2. 本接口与自定义菜单查询接口的不同之处在于,本接口无论公众号的接口是如何设置的,都能查询到接口,而自定义菜单查询接口则仅能查询到使用API设置的菜单配置
  3. 认证/未认证的服务号/订阅号,以及接口测试号,均拥有该接口权限
  4. 🔴 从第三方平台的公众号登录授权机制上来说,该接口从属于消息与菜单权限集
  5. 本接口中返回的图片/语音/视频为临时素材(临时素材每次获取都不同,3天内有效,通过素材管理-获取临时素材接口来获取这些素材),本接口返回的图文消息为永久素材素材(通过素材管理-获取永久素材接口来获取这些素材)。

📝 1 和 4 看不懂。

调用测试结果实例:

{
    "is_menu_open": 1,
    "selfmenu_info": {
        "button": [
            {
                "type": "click",
                "name": "订单查询",
                "key": "key01"
            },
            {
                "name": "自助服务",
                "sub_button": {
                    "list": [
                        {
                            "type": "view",
                            "name": "订单系统",
                            "url": "http://i.a.com/"
                        },
                        {
                            "type": "click",
                            "name": "点赞",
                            "key": "key02"
                        }
                    ]
                }
            }
        ]
    }
}

Create Widget Extension FlashMessage

Create A New Widget From Scratch

We mainly need to implement two methods:

  • CWidget::init

  • CWidget::run(): 若 widget 使用的是

    $this->widget();

    的格式,run() 方法可以留空;

Publishing Assets

In order to make these files Web accessible, we need to publish them using CWebApplication::assetManager, as shown in the above code snippet.

Besides, if we want to include a CSS or JavaScript file in the current page, we need to register it using CClientScript:

class MyWidget extends CWidget
{
    protected function registerClientScript()
    {
        // ...publish CSS or JavaScript file here...
        $cs=Yii::app()->clientScript;
        $cs->registerCssFile($cssFile);
        $cs->registerScriptFile($jsFile);
    }
}

Views

注意 views 目录和 Widget Class 文件位于同一个目录下。

A widget may also have its own view files. If so, create a directory named views under the directory containing the widget class file, and put all the view files there.

In the widget class, in order to render a widget view, use

$this->render('ViewName');

It is similar to what we do in a controller.

开始开发 - 获取微信服务器 IP 地址列表

公众号基于消息接收安全上的考虑,需要获知微信服务器的IP地址列表,以便识别出哪些消息是微信官方推送给你的,哪些消息可能是他人伪造的,可以通过该接口获得微信服务器IP地址列表。

发送一个 GET 请求至 getcallbackip api:

https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN

返回结果大致如下:

{
    "ip_list": [
        "101.226.62.77",
        "101.226.62.78",
        "101.226.62.79",
        ...
    ]
}

消息管理 - 发送消息 - 客服接口

📖 http://mp.weixin.qq.com/wiki/11/c88c270ae8935291626538f9c64bd123.html

当用户和公众号产生特定动作的交互时(具体动作列表请见下方说明),微信将会把消息数据推送给开发者,开发者可以在一段时间内(目前修改为48小时)调用客服接口,通过POST一个JSON数据包来发送消息给普通用户此接口主要用于客服等有人工消息处理环节的功能,方便开发者为用户提供更加优质的服务。

目前允许的动作列表如下(公众平台会根据运营情况更新该列表,不同动作触发后,允许的客服接口下发消息条数不同,下发条数达到上限后,会遇到错误返回码,具体请见返回码说明页):

  1. 用户发送信息
  2. 点击自定义菜单(仅有点击推事件、扫码推事件、扫码推事件且弹出“消息接收中”提示框这3种菜单类型是会触发客服接口的)
  3. 关注公众号
  4. 扫描二维码
  5. 支付成功
  6. 用户维权

这表明,通过订单系统中发货来触发发送发货消息的想法不现实。而要通过以下方式实现:

  • 用户回复“单号”;
  • 用户点击菜单中的“订单查询”按钮;

🔴

为了帮助公众号使用不同的客服身份服务不同的用户群体,客服接口进行了升级,开发者可以管理客服账号,并设置客服账号的头像和昵称。该能力针对所有拥有客服接口权限的公众号开放。

不对测试帐号开放

测试添加客服帐号时, 提示如下错误信息:

"{"errcode":61451,"errmsg":"invalid parameter"} "

从网上了解到,客服接口不对测试帐号开放,遂作罢。

Learn Security: XSRF

本节举了一个 CSRF 攻击的简单例子。我照例在 blog application 上模拟了一下,结果又中招(第一次是 XSS 攻击)了。我在其它应用下新建一个 a.php 页面,内容如下:

<img src="http://localhost/blog/pk/clip/delete?id=777" />

blog Session 有效的前提下,访问 a.php 后,blog 内 ID 为 777 的 Clip 已经被删除。

这种 bug 之所以会发生,是因为没有遵循 CSRF 攻击防范措施的第一条—— GET 请求只用于获取数据,不能用于修改数据。现在终于体会到为什么 Yii 内置的 blog demo 中删除 post (source code) 时要加上下面这样一个判断了:

// PostController.php
public function actionDelete()
{
    if (Yii::app()->request->isPostRequest)
    {
        // delete
    }
    else
        throw new CHttpException(400,"Invalid request.");
}

就是为了防止 CSRF 攻击。

Resolution

修改每一个 Controllers 中的 actionDelete() methods 未免太麻烦,我立刻想到 #5 中新建的 LoginFilter, 完全可以依葫芦画瓢,再创建一个判断是不是 POST request 的 filter 嘛。

不同于 LoginFilter 应用于 controller 内的所有 actions, PostRequestCheckFilter 仅针对 actionDelete, 我们需要在 filters() 内配置 filter 时,用到一些特殊符号:

public function filters()
{
    return array( 
        'accessControl',
        array('ext.filter.precheck.PostRequestCheckFilter + delete'), // only to 'delete' action
    );

}

这样我们就杜绝了通过 GET 请求修改数据的可能。

Debian Commands Cheat-sheet

User Group

标记 命令 说明
# useradd -d /home/chen -s /bin/bash -m chen 新增用户 'chen',
$ passwd 修改当前登录用户的密码
# passwd chen root 用户修改用户 chen 的密码
# addgroup admin 新增名为'admin'的用户组
# delgroup admin 删除 Group 'admin', 注:只能删除 Secondary Group, 删除 Primary Group 将会出现错误提示
# usermod -a -G www-data chen 将用户 chen 添加至 www-data group. 重新登录(假设添加的是 www-data 用户,则需要重启 Apache )才能看到变化。另:-G www-data 是一个整体
# deluser chen www-data 将 user chen 从 group www-data中移除。重新登录才能看到变化
$ groups [chen] 查看 User chen 所在的 groups, groups 不带参数则查看当前用户
$ cat /etc/group | grep www-data 查看 Group www-data 下的所有用户,查看某组下所有用户没有直接的命令。Ref.
# chown -R www-data:www-data framework/ 改变文件(夹)的所有者属性

说明:标记列中'#'表示用 root 用户执行;'$'表示用一般用户。

http://www.ruanyifeng.com/blog/2014/03/server_setup.html

5.5

  • 可用的权限字符有:rwxXst
  • x 对目录的含义:可以搜索目录

Constructors and Destructors 构造函数与析构函数

所谓构造函数,就是类内名为 _construct 的方法,语法如下:

void __construct ([ mixed $args = "" [, $... ]] )

每当新建一个对象时,就会调用该方法。因此,构造函数特别适合用来做初始化操作。

构造函数和析构函数的相同点:

  • 若一个子类定义了构造/析构函数,它并不会隐形调用(call implicitly)父类的构造/析构函数。如果需要调用父类的构造/析构函数,必须在子类构造/析构函数内显性调用(parent::__construct())
  • 继承: 如果一个子类内没有定义构造/析构函数,那么,它会从父类中继承。

上面两点用一个代码实例来表示如下:

class BaseClass {
   function __construct() {
       print "In BaseClass constructor\n";
   }
}

class SubClass extends BaseClass {
   function __construct() {
       parent::__construct();
       print "In SubClass constructor\n";
   }
}

class OtherSubClass extends BaseClass {
    // inherits BaseClass's constructor
}

// In BaseClass constructor
$obj = new BaseClass();

// In BaseClass constructor
// In SubClass constructor
$obj = new SubClass();

// In BaseClass constructor
$obj = new OtherSubClass();

析构函数

析构函数的概念在 PHP5 中被引入进来。此函数在何时被调用呢?

The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

对于第 1 种情况,请看下面一个 script:

class Test
{
    function __destruct()
    {
        echo '当对象销毁时调用';
    }
}

$a = $b = $c = new Test();

$a = null;
unset($b);
echo '<hr>';

一开始,三个变量引用指向 Test 对象,$a 赋值成 null 后,剩两个。unset 一个,script 执行完毕后再减一个,最终导致析构函数调用。

question 后一种情况没明白。啥是 shutdown sequence 呢?

找到一个 register_shutdown_function() 函数。当脚本执行结束或调用 exit() 时。

Registers a callback to be executed after script execution finishes or exit() is called.


Pseudo-types and variables used in this documentation

Pseudo-types:

  • mixed:
  • number: integer 或 float
  • callback: 在 PHP 5.4 引入 callable type hint 之前,使用 callback, callback 和 callable 完全等价;
  • void: 用在返回值类型时,表示返回值没有用;用在参数列表内表示函数不需要任何参数(void __destruct (void))。

Pseudo-variables:

  • ...: $... in function prototype 表示 and so on

例子:

void __construct ([ mixed $args = "" [, $... ]] )
void __destruct (void)

exit() = die(): Output a message and terminate the current script

void exit ([ string $status ] )
void exit ( int $status )

Language Construct

exit is a language construct and it can be called without parentheses if no status is passed.

类似的还有:echo, print, unset(), isset(), empty(), include, and require 等等。



Visibility

注意 protected 关键词。protected properties 只能在类内部,或者其子类或父类内访问。

Members declared protected can be accessed only within the class itself and by inherited and parent classes.

  1. Property Visibility

声明属性必须定义 visibility, 这点和声明方法不同。

  1. Method Visibility

如果一个方法没有显性指明 visibility keyword, 表示 public, e.g. __construct().

  1. Visibility from other objects

Objects of the same type will have access to each others private and protected members even though they are not the same instances. This is because the implementation specific details are already known when inside those objects.

看不懂


Temp

2015年1月29日

爸爸下班一进家门,我就让他用胡萝卜刻一个飞机图案的“章”。“章”刻好后没有颜料不能玩,以前我都是把彩笔里的芯取出来涂抹,最后弄得满手都是颜料。这次爸爸到商店里买了一盒印油,我在纸上玩了一会儿就不玩了。


2015年1月30日

早上起床后我突然喊“奶奶”,爸爸说奶奶没回来。我说奶奶今天就回来了,爸爸问我怎么知道的,我说三姑说的。

春节一天天临近。今天老师让我们把这学期所学的课本带回家。

晚上妈妈带我到超市买了一盒威化饼干,妈妈打算每天让我吃一个。

睡觉前我让爸爸把今天从学校带回来的书拿给我,我在床上和妈妈一起看课本。

2.7. Uniform Resource Identifiers

URI references are used to target requests, indicate redirects, and define relationships.

The definitions of "URI-reference", "absolute-URI", "relative-part", "scheme", "authority", "port", "host", "path-abempty", "segment", "query", and "fragment" are adopted from the URI generic syntax. An "absolute-path" rule is defined for protocol elements that can contain a non-empty path component. (This rule differs slightly from the path-abempty rule of RFC 3986, which allows for an empty path to be used in references, and path-absolute rule, which does not allow paths that begin with "//".) A "partial-URI" rule is defined for protocol elements that can contain a relative URI but not a fragment component.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.