CodeIgniter源碼分析 3 - 輸入輸出

大家有沒有想一下一般我們可以通過$_GET或許$_POST等獲取表單數(shù)據(jù)留夜,但是輸入類提供了post,get等方法,那么這種方式是多此一舉還是有別的需求勘究?如果有區(qū)別勘畔,那么區(qū)別在哪里?

這一節(jié)我們看下CI提供給我們的輸入類和輸出類荧琼,通過研究這兩個(gè)類的源碼學(xué)習(xí)下表單數(shù)據(jù)處理譬胎,安全過濾(xss-跨站腳本,csrf-跨站請求偽造),網(wǎng)頁緩存,輸出控制以及http相關(guān)的一些知識(shí)。


輸入類 CI_Input

先從構(gòu)造方法開始

看下構(gòu)造方法做了哪些初始化

public function __construct()
{
    /*
     * 讀取相關(guān)的配置命锄,分別為:
     * 是否銷毀全局的GET數(shù)組的allow_get_array堰乔,
     * 是否進(jìn)行xss過濾的global_xss_filtering,
     * 是否防御跨站請求偽造的csrf_protection脐恩,
     * 統(tǒng)一換行符的standardize_newlines,
     * */
    $this->_allow_get_array     = (config_item('allow_get_array') === TRUE);
    $this->_enable_xss      = (config_item('global_xss_filtering') === TRUE);
    $this->_enable_csrf     = (config_item('csrf_protection') === TRUE);
    $this->_standardize_newlines    = (bool) config_item('standardize_newlines');


    //security類主要進(jìn)行xss和csrf的操作镐侯,load_class我們在之前的請求處理中分析過
    $this->security =& load_class('Security', 'core');

    //加載utf8類,將對(duì)字符進(jìn)行utf8編碼處理
    if (UTF8_ENABLED === TRUE)
    {
        $this->uni =& load_class('Utf8', 'core');
    }

    //該方法就是根據(jù)讀取到的相關(guān)配置驶冒,進(jìn)而對(duì)全局?jǐn)?shù)組進(jìn)行處理
    $this->_sanitize_globals();

    log_message('info', 'Input Class Initialized');
}

我們一般是從全局?jǐn)?shù)組中獲取表單數(shù)據(jù)苟翻,通過分析很明顯看到是在_sanitize_globals()方法中對(duì)表單數(shù)據(jù)做了處理搭伤,看下此方法

protected function _sanitize_globals()
{
    //還記得構(gòu)造方法中加載的配置allow_get_array嗎?
    //可以看到該配置為false將會(huì)銷毀全局GET數(shù)組
    if ($this->_allow_get_array === FALSE)
    {
        $_GET = array();
    }
    //如果允許全局GET數(shù)組袜瞬,那么對(duì)全局GET數(shù)組的數(shù)據(jù)做清洗
    elseif (is_array($_GET) && count($_GET) > 0)
    {
        foreach ($_GET as $key => $val)
        {
            $_GET[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
        }
    }

    //對(duì)全局POST數(shù)組的數(shù)據(jù)做清洗
    if (is_array($_POST) && count($_POST) > 0)
    {
        foreach ($_POST as $key => $val)
        {
            $_POST[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
        }
    }

    //對(duì)cookie做清洗
    if (is_array($_COOKIE) && count($_COOKIE) > 0)
    {
        unset(
            $_COOKIE['$Version'],
            $_COOKIE['$Path'],
            $_COOKIE['$Domain']
        );

        foreach ($_COOKIE as $key => $val)
        {
            if (($cookie_key = $this->_clean_input_keys($key)) !== FALSE)
            {
                $_COOKIE[$cookie_key] = $this->_clean_input_data($val);
            }
            else
            {
                //不合法的cookie就刪掉
                unset($_COOKIE[$key]);
            }
        }
    }

    //PHP_SELF是除了域名外的url段,這里清除在url中帶標(biāo)簽的惡意腳本
    $_SERVER['PHP_SELF'] = strip_tags($_SERVER['PHP_SELF']);

    //如果配置中的csrf_protection為true身堡,那么就需要調(diào)用security類對(duì)跨站請求偽造進(jìn)行處理
    if ($this->_enable_csrf === TRUE && ! is_cli())
    {
        $this->security->csrf_verify();
    }

    log_message('debug', 'Global POST, GET and COOKIE data sanitized');
}

_sanitize_globals()方法中我們看到對(duì)于全局?jǐn)?shù)組的key和value分別使用了_clean_input_keys()和_clean_input_data()做清洗邓尤,看下這兩個(gè)方法

/*
 * 清洗key
 * */
protected function _clean_input_keys($str, $fatal = TRUE)
{
    //監(jiān)測表單的key,如果key不合法就報(bào)錯(cuò)
    if ( ! preg_match('/^[a-z0-9:_\/|-]+$/i', $str))
    {
        if ($fatal === TRUE)
        {
            return FALSE;
        }
        else
        {
            set_status_header(503);
            echo 'Disallowed Key Characters.';
            exit(7); // EXIT_USER_INPUT
        }
    }

    //如果腳本內(nèi)部編碼支持utf8贴谎,那么對(duì)key進(jìn)行utf8編碼
    if (UTF8_ENABLED === TRUE)
    {
        return $this->uni->clean_string($str);
    }

    return $str;
}
/*
 * 清洗value
 * */
protected function _clean_input_data($str)
{
    //考慮到表單的value可能是數(shù)組汞扎,對(duì)value遞歸處理
    if (is_array($str))
    {
        $new_array = array();
        foreach (array_keys($str) as $key)
        {
            $new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($str[$key]);
        }
        return $new_array;
    }

    //5.4版本以下,如果magic_quotes為on擅这,所有的'(單引號(hào))澈魄、"(雙引號(hào))、\(反斜杠)和 NULL會(huì)被一個(gè)反斜杠
   //自動(dòng)轉(zhuǎn)義仲翎;在 PHP 5.4.O 起將始終返回 FALSE 痹扇,關(guān)于magic_quotes見:
    //http://php.net/manual/en/info.configuration.php#ini.magic-quotes-gpc
    if ( ! is_php('5.4') && get_magic_quotes_gpc())
    {
        $str = stripslashes($str);
    }

    //對(duì)value進(jìn)行utf8編碼
    if (UTF8_ENABLED === TRUE)
    {
        $str = $this->uni->clean_string($str);
    }

    //移除非打印字符
    $str = remove_invisible_characters($str, FALSE);

    //由于unix,windows溯香,mac上換行符不一致鲫构,這里根據(jù)配置對(duì)換行符做一致性處理
    if ($this->_standardize_newlines === TRUE)
    {
        return preg_replace('/(?:\r\n|[\r\n])/', PHP_EOL, $str);
    }

    return $str;
}

至此整個(gè)構(gòu)造方法相關(guān)的初始化就分析完了,我們發(fā)現(xiàn)整個(gè)過程本質(zhì)就做了一件事玫坛,讀取相關(guān)配置對(duì)全局?jǐn)?shù)組進(jìn)行清洗结笨。

獲取表單數(shù)據(jù)

前文的構(gòu)造方法的源碼分析中我們看到需要的表單數(shù)據(jù)已經(jīng)被清洗完成,那么這時(shí)候Input類提供的表單數(shù)據(jù)獲取方法get(),post()本質(zhì)上就是從全局?jǐn)?shù)組上拿數(shù)據(jù)就行了湿镀。由于這些方法大同小異炕吸,這里以post方法為例,看下表單數(shù)據(jù)讀取

/*
 * post方法接受兩個(gè)參數(shù)勉痴,第一個(gè)是表單的key赫模,第二個(gè)是是否對(duì)xss(跨站腳本)做處理的可選參數(shù)
 */
public function post($index = NULL, $xss_clean = NULL)
{
    return $this->_fetch_from_array($_POST, $index, $xss_clean);
}

如果仔細(xì)看其他幾個(gè)獲取數(shù)據(jù)的方法,會(huì)發(fā)現(xiàn)它們內(nèi)部都調(diào)用_fetch_from_array()這個(gè)方法蚀腿,該方法用來從全局?jǐn)?shù)組中獲取相應(yīng)的數(shù)據(jù)

protected function _fetch_from_array(&$array, $index = NULL, $xss_clean = NULL)
{
    //讀取是否進(jìn)行跨站腳本清洗的配置
    is_bool($xss_clean) OR $xss_clean = $this->_enable_xss;

    //判斷key是否為空嘴瓤,如果為空就意味著讀取整個(gè)全局?jǐn)?shù)組
    isset($index) OR $index = array_keys($array);

    //如果是一次獲取多個(gè)表單值,那么遞歸讀取這些值
    if (is_array($index))
    {
        $output = array();
        foreach ($index as $key)
        {
            $output[$key] = $this->_fetch_from_array($array, $key, $xss_clean);
        }

        return $output;
    }

    //從全局?jǐn)?shù)組中讀取該值
    if (isset($array[$index]))
    {
        $value = $array[$index];
    }

    /*
     *          key         |     value
     *          favorite[]  |     a
     *          favorite[]  |     b
     * 數(shù)組格式的表單值的讀取其key有兩種風(fēng)格莉钙,例如 : this->input->post('favorite') 和 this->input->post('favorite[]'),
     * 接下來的這段代碼就是處理key為'favorite[]'這種讀取表單值的方式,注意:全局?jǐn)?shù)組是不支持這種帶[]的key的
     * */
    elseif (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1)
    {
        $value = $array;
        for ($i = 0; $i < $count; $i++)
        {
            $key = trim($matches[0][$i], '[]');
            if ($key === '')
            {
                break;
            }

            if (isset($value[$key]))
            {
                $value = $value[$key];
            }
            else
            {
                return NULL;
            }
        }
    }
    else
    {
        return NULL;
    }

    //xss過濾返回表單值
    return ($xss_clean === TRUE)
        ? $this->security->xss_clean($value)
        : $value;
}

通過分析post()獲取表單值的源碼廓脆,發(fā)現(xiàn)post(),get()等方法通過調(diào)用_fetch_from_array(),其本質(zhì)上是從全局?jǐn)?shù)組上讀取已經(jīng)被清洗好了的表單值磁玉,至此讀取表單數(shù)據(jù)的源碼分析就完成了停忿。


安全性處理 CI_Security

我們在源碼分析中多次看到xss_clean和csrf_verify對(duì)數(shù)據(jù)做安全性處理,那么xss和csrf是什么蚊伞?

  • xss((Cross Site Scripting):跨站腳本攻擊席赂,為了不和層疊樣式表(Cascading Style Sheets)的縮寫混淆吮铭,故將跨站腳本攻擊縮寫為XSS,這是一種通過Web頁面里插入惡意Script代碼颅停,當(dāng)用戶瀏覽該頁之時(shí)谓晌,嵌入其中Web里面的Script代碼會(huì)被執(zhí)行,從而達(dá)到惡意攻擊用戶的目的癞揉。
  • CSRF(Cross-site request forgery)跨站請求偽造纸肉,這是一種通過偽造會(huì)話信息,冒充用戶進(jìn)而騙取服務(wù)器信任的一種攻擊手段喊熟。通過描述可以看到xss和csrf是不一樣的柏肪,它們屬于不同維度的攻擊手段。

CI框架提供了CI_Security類來處理xss和csrf芥牌,通過閱讀該類的源碼來學(xué)習(xí)下對(duì)這兩種攻擊手段的處理烦味。

xss_clean()方法

關(guān)于xss_clean要想清楚這幾個(gè)問題:clean哪些東西?或者說腳本會(huì)從哪里注入進(jìn)來壁拉?腳本注入的類型有哪些谬俄?

想清楚上面的這幾個(gè)問題后會(huì)發(fā)現(xiàn)其實(shí)xss_clean的原理從大的方面來說始終圍繞兩點(diǎn)

  • 轉(zhuǎn)換腳本的標(biāo)簽為實(shí)體從而破壞腳本的執(zhí)行環(huán)境
  • 將一些可能為系統(tǒng)命令或函數(shù)的關(guān)鍵字給清除

接下來,我們進(jìn)入xss_clean()方法中看下其實(shí)現(xiàn)

public function xss_clean($str, $is_image = FALSE)
    {
        //如果待被過濾字符是數(shù)組弃理,遞歸處理它
        if (is_array($str))
        {
            while (list($key) = each($str))
            {
                $str[$key] = $this->xss_clean($str[$key]);
            }

            return $str;
        }

        //移除非打印字符凤瘦,這個(gè)我們在之前的源碼分析第二篇中已經(jīng)分析過了
        $str = remove_invisible_characters($str);


        //因?yàn)橛行阂庾址麜?huì)偽裝成url編碼,反正不管怎么樣案铺,先解碼一下蔬芥,讓那些惡意字符現(xiàn)出原形
        do
        {
            $str = rawurldecode($str);
        }
        while (preg_match('/%[0-9a-f]{2,}/i', $str));


        //將標(biāo)簽符號(hào)轉(zhuǎn)義成字符實(shí)體
        $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
        $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str);

        //移除非打印字符
        $str = remove_invisible_characters($str);

        //將正則制表符換成空格,后文中會(huì)去除這些空格
        $str = str_replace("\t", ' ', $str);

        // Capture converted string for later comparison
        $converted_string = $str;

        //清除字符中不被允許的關(guān)鍵字
        $str = $this->_do_never_allowed($str);

        //將字符中的php標(biāo)簽符號(hào)轉(zhuǎn)換成實(shí)體
        if ($is_image === TRUE)
        {
            // Images have a tendency to have the PHP short opening and
            // closing tags every so often so we skip those and only
            // do the long opening tags.
            $str = preg_replace('/<\?(php)/i', '&lt;?\\1', $str);
        }
        else
        {
            $str = str_replace(array('<?', '?'.'>'), array('&lt;?', '?&gt;'), $str);
        }


        //去除關(guān)鍵字中的空格
        $words = array(
            'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
            'vbs', 'script', 'base64', 'applet', 'alert', 'document',
            'write', 'cookie', 'window', 'confirm', 'prompt'
        );

        foreach ($words as $word)
        {
            $word = implode('\s*', str_split($word)).'\s*';

            // We only want to do this when it is followed by a non-word character
            // That way valid stuff like "dealer to" does not become "dealerto"
            $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
        }


        //清除鏈接和圖片路徑中的關(guān)鍵字控汉,并將鏈接中的標(biāo)簽符號(hào)轉(zhuǎn)換成實(shí)體
        do
        {
            $original = $str;

            if (preg_match('/<a/i', $str))
            {
                $str = preg_replace_callback('#<a[^a-z0-9>]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
            }

            if (preg_match('/<img/i', $str))
            {
                $str = preg_replace_callback('#<img[^a-z0-9]+([^>]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str);
            }

            if (preg_match('/script|xss/i', $str))
            {
                $str = preg_replace('#</*(?:script|xss).*?>#si', '[removed]', $str);
            }
        }
        while ($original !== $str);

        unset($original);


        //清除標(biāo)簽屬性中出現(xiàn)的關(guān)鍵字
        $str = $this->_remove_evil_attributes($str, $is_image);


        //有些關(guān)鍵字可能直接以html標(biāo)簽的方式出現(xiàn)笔诵,如果是這樣,將標(biāo)簽符號(hào)轉(zhuǎn)成實(shí)體
        $naughty = 'alert|prompt|confirm|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|button|select|isindex|layer|link|meta|keygen|object|plaintext|style|script|textarea|title|math|video|svg|xml|xss';
        $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str);


        //有些關(guān)鍵字可能會(huì)以函數(shù)或者系統(tǒng)命令的方式出現(xiàn)姑子,如果是這樣乎婿,將函數(shù)調(diào)用的括號(hào)()轉(zhuǎn)換成實(shí)體
        $str = preg_replace('#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si',
            '\\1\\2&#40;\\3&#41;',
            $str);

        //再次清除字符中不被允許的關(guān)鍵字
        $str = $this->_do_never_allowed($str);


        //最終清洗后得到的str和最初做了清洗的str($converted_string)做比較,如果它們是一樣的街佑,
        //就說明圖片是安全的(true)谢翎,如果不一樣就說明該圖片中含有xss代碼,是不安全的(false)
        if ($is_image === TRUE)
        {
            return ($str === $converted_string);
        }

        return $str;
    }

xss_clean的源碼分析到此結(jié)束了,從代碼中不難發(fā)現(xiàn)其本質(zhì)就是我們說的那兩點(diǎn)沐旨,通過轉(zhuǎn)換惡意腳本的標(biāo)簽符號(hào)為實(shí)體從而破壞腳本的執(zhí)行環(huán)境森逮;將惡意的函數(shù)/命令關(guān)鍵字刪除以免在服務(wù)器被執(zhí)行;

csrf_verify()方法

csrf防御一般都是通過token(令牌)實(shí)現(xiàn)的磁携,其原理就是在你的頁面服務(wù)端為你生成一條token褒侧,當(dāng)你發(fā)送請求時(shí)攜帶上該token和服務(wù)端保存的token進(jìn)行比對(duì),從而判斷該請求是否合法。

那么就有個(gè)疑問闷供,跨域的ajax提交表單時(shí)怎么做csrf保護(hù)呢烟央?其實(shí)現(xiàn)在的ajax請求一般都被設(shè)計(jì)成了 REST API 的方式,而 REST API天生帶token歪脏,并且 REST API 原則上是屏蔽會(huì)話信息的疑俭, 也就是它不接受提交所謂的cookie,既然不接受會(huì)話信息婿失,那也就談不上會(huì)話的偽造了怠硼;當(dāng)然token被破解又是另一說了。

CI_Security類在構(gòu)造方法中對(duì)csrf相關(guān)的設(shè)置做了初始化

public function __construct()
    {
        //是否防御csrf
        if (config_item('csrf_protection'))
        {
            //讀取相關(guān)的變量
            foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key)
            {
                if (NULL !== ($val = config_item($key)))
                {
                    $this->{'_'.$key} = $val;
                }
            }

            //如果有需要移怯,可以將該cookie掛在某個(gè)命名空間下面
            if ($cookie_prefix = config_item('cookie_prefix'))
            {
                $this->_csrf_cookie_name = $cookie_prefix.$this->_csrf_cookie_name;
            }

            //生成token,該token同時(shí)會(huì)被存儲(chǔ)在cookie中
            $this->_csrf_set_hash();
        }

        $this->charset = strtoupper(config_item('charset'));

        log_message('info', 'Security Class Initialized');
    }

接著我們在csrf_verify()看下csrf的防御

public function csrf_verify()
    {
        //如果是非表單提交而是頁面渲染的話这难,這時(shí)候就需要將token寫入cookie中去
        if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
        {
            return $this->csrf_set_cookie();
        }

        //排除不需要進(jìn)行csrf防御的頁面
        if ($exclude_uris = config_item('csrf_exclude_uris'))
        {
            $uri = load_class('URI', 'core');
            foreach ($exclude_uris as $excluded)
            {
                if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string()))
                {
                    return $this;
                }
            }
        }

        //比對(duì)客戶端提交過來的token和服務(wù)端存儲(chǔ)token舟误,如果二者不一致,那就說明此請求是偽造的
        if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])
            OR $_POST[$this->_csrf_token_name] !== $_COOKIE[$this->_csrf_cookie_name]) // Do the tokens match?
        {
            $this->csrf_show_error();
        }

        //從全局?jǐn)?shù)組中刪除該token姻乓,因?yàn)樵撛刂皇怯脕硇r?yàn)token嵌溢,不應(yīng)該出現(xiàn)在后續(xù)的業(yè)務(wù)邏輯中
        unset($_POST[$this->_csrf_token_name]);

        //根據(jù)配置決定是否每次請求結(jié)束后從新生成令牌,該配置慎用蹋岩,因?yàn)轫撁嫔峡床灰姷漠惒秸埱罂赡軙?huì)讓沒使用的token失效
        if (config_item('csrf_regenerate'))
        {
            // Nothing should last forever
            unset($_COOKIE[$this->_csrf_cookie_name]);
            $this->_csrf_hash = NULL;
        }
        
        //重新生成token赖草,當(dāng)然如果token有存活周期的話,就不需要重新生成了
        $this->_csrf_set_hash();
        $this->csrf_set_cookie();

        log_message('info', 'CSRF token verified');
        return $this;
    }

接下來我們分析下輸出類剪个。


輸出類 CI_Output

輸出類就像文檔中說的

發(fā)送Web 頁面內(nèi)容到請求的瀏覽器秧骑,并且還能對(duì)視圖進(jìn)行緩存。

輸出內(nèi)容到瀏覽器

我們在控制器中定義一個(gè)方法扣囊,然后在這個(gè)方法中加載一個(gè)視圖乎折,視圖中寫入一段文字,并在瀏覽器訪問侵歇,發(fā)現(xiàn)該視圖確實(shí)被加載成功了

image.png

一般情況下骂澄,我們要想向?yàn)g覽器輸出內(nèi)容,必須靠 echo惕虑,print_r坟冲,var_dump之類的輸出函數(shù),可是在視圖加載的過程中溃蔫,并沒有看見這些函數(shù)健提,那視圖中的hello world到底是如何輸出的

如果大家仔細(xì)看文檔的話,輸出類中有個(gè)_display()方法伟叛,文檔中是這樣描述該方法的(注意圖中圈出的文字)

image.png

那么_display()方法是在哪里被調(diào)用矩桂?是在引導(dǎo)文件CodeIgniter.php中調(diào)用的

_display()被調(diào)用的地方

那現(xiàn)在我們就設(shè)想下是不是在該方法中最終輸出視圖中的hello world的?

進(jìn)入_display()方法一探究竟

/**
 *
 * 仔細(xì)看下該方法的注釋其實(shí)已經(jīng)說的很明白了:
 * 發(fā)送最終的數(shù)據(jù)以及頭信息和分析數(shù)據(jù)到瀏覽器,并結(jié)束基準(zhǔn)測試侄榴,并且注釋中也提到了所有視圖
 * 數(shù)據(jù)會(huì)被自動(dòng)的添加到final_output這個(gè)變量中去雹锣,那么很清楚了,我們視圖中的 hello world
 * 就是在這被輸出的
 *
 *
 * Display Output
 *
 * Processes and sends finalized output data to the browser along
 * with any server headers and profile data. It also stops benchmark
 * timers so the page rendering speed and memory usage can be shown.
 *
 * Note: All "view" data is automatically put into $this->final_output
 *   by controller class.
 *
 * @uses    CI_Output::$final_output
 * @param   string  $output Output data override
 * @return  void
 */
public function _display($output = '')
{

    // 這里為什么不使用加載器$CI->load('Class')去加載這兩個(gè)類的原因癞蚕,是因?yàn)樵摲椒赡軙?huì)在輸出緩存的邏輯中被調(diào)用蕊爵,
    // 輸出緩存的邏輯是在引導(dǎo)文件Codeigter.php 323-326 行處,該處一旦發(fā)現(xiàn)緩存有效就直接向?yàn)g覽器輸出桦山,這就會(huì)導(dǎo)致
    // 代碼執(zhí)行不到 & get_instance() 這個(gè)方法處攒射,也就生成不了CI_Controller的示例對(duì)象$CI,那自然也不能用加載器loader去加載類庫了

    $BM =& load_class('Benchmark', 'core');
    $CFG =& load_class('Config', 'core');


    // 如果CI_Controller類存在實(shí)例化該對(duì)象恒水,那就說明這是正常的輸出会放,而非緩存輸出,
    // 該 & get_instance()調(diào)用是Codeigter.php的钉凌,而Codeigter.php中的& get_instance()
    // 是對(duì)CI_Controller中 & get_instance()方法的引用
    if (class_exists('CI_Controller', FALSE))
    {
        $CI =& get_instance();
    }

    // --------------------------------------------------------------------

    //這里不管你輸出的是啥咧最,都把它看作是buffer,這里的final_output是在我們
    //$this->load->view('test/test')時(shí)在view函數(shù)中使用php的內(nèi)置的緩存機(jī)制
    //oupt_buffering生成的御雕,關(guān)注php的內(nèi)置緩存機(jī)制見:
    //
    if ($output === '')
    {
        $output =& $this->final_output;
    }

    // --------------------------------------------------------------------

    //在輸出的同時(shí)判斷是不是還需要進(jìn)行緩存
    if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
    {
        $this->_write_cache($output);
    }

    // --------------------------------------------------------------------


    // 由于該函數(shù)是終極輸出函數(shù)矢沿,那自然相關(guān)的數(shù)據(jù)采集就得在這做了,包括調(diào)用基準(zhǔn)測試類采集腳本的執(zhí)行時(shí)間酸纲,
    // 收集內(nèi)存的使用情況等
    $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');

    if ($this->parse_exec_vars === TRUE)
    {
        $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
        $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
    }

    // --------------------------------------------------------------------

    // 是否對(duì)輸出內(nèi)容壓縮捣鲸,注意:輸出壓縮依賴zlib擴(kuò)展
    if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
        && $this->_compress_output === TRUE
        && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
    {
        ob_start('ob_gzhandler');
    }

    // --------------------------------------------------------------------

    //我們之前說了由于final_output本質(zhì)上就是個(gè)一段buffer,至于讓客戶端怎么解析闽坡,我們只需要指定相應(yīng)的mime頭信息就行了
    if (count($this->headers) > 0)
    {
        foreach ($this->headers as $header)
        {
            @header($header[0], $header[1]);
        }
    }

    // --------------------------------------------------------------------

    // 這段邏輯是為緩存讀取服務(wù)的栽惶,正常的輸出是不走這段邏輯的,注意:如果_compress_output為true疾嗅,
    // 也就意味著讀取的緩存是被壓縮了的媒役,如果客戶端不支持壓縮則必須解壓后才能輸出
    if ( ! isset($CI))
    {
        if ($this->_compress_output === TRUE)
        {
            if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
            {
                header('Content-Encoding: gzip');
                header('Content-Length: '.strlen($output));
            }
            else
            {
                // User agent doesn't support gzip compression,
                // so we'll have to decompress our cache
                $output = gzinflate(substr($output, 10, -8));
            }
        }

        echo $output;
        log_message('info', 'Final output sent to browser');
        log_message('debug', 'Total execution time: '.$elapsed);
        return;
    }

    // --------------------------------------------------------------------

    //這里就是將程序運(yùn)行過程中采集的數(shù)據(jù)通過profiler生成報(bào)表,不過感覺profiler生成報(bào)表的方式不雅觀宪迟,
    //在代碼中拼html的方式導(dǎo)致生成的報(bào)表樣式很難看酣衷,其實(shí)我們自己拿到這些數(shù)據(jù)可以通過js制作一個(gè)好看的調(diào)試控制臺(tái)
    if ($this->enable_profiler === TRUE)
    {
        $CI->load->library('profiler');
        if ( ! empty($this->_profiler_sections))
        {
            $CI->profiler->set_sections($this->_profiler_sections);
        }

        // If the output data contains closing </body> and </html> tags
        // we will remove them and add them back after we insert the profile data
        $output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
        if ($count > 0)
        {
            $output .= '</body></html>';
        }
    }



    //如果CI_Controller自身帶有一個(gè)_output()的方法,將數(shù)據(jù)扔給它輸出次泽,否則就做最終的輸出
    if (method_exists($CI, '_output'))
    {
        $CI->_output($output);
    }
    else
    {
        echo $output; // Send it to the browser!
    }

    log_message('info', 'Final output sent to browser');
    log_message('debug', 'Total execution time: '.$elapsed);
}

在上述的源碼分析中我們說的輸出給瀏覽器的數(shù)據(jù) final_output穿仪,在輸出類來看本質(zhì)就是buffer,我們也說到 final_output 是 $this->load->view() 函數(shù)中通過php內(nèi)置的緩存機(jī)制oupt_buffering生成的意荤,其實(shí)也就是很簡單的幾行代碼啊片,在Loader.php 中936-944行處打開一個(gè)緩沖區(qū),讀取其中的內(nèi)容通過調(diào)用輸出類 Outputappend_output() 方法將內(nèi)容扔給final_output玖像,從而讀取到了視圖中的hello world

image.png

對(duì)于加載視圖紫谷,并沒有顯式的調(diào)用輸出函數(shù)但卻輸出了視圖 hello world 的疑惑就解答完畢了,可以看到CI對(duì)于內(nèi)容輸出的處理很 "粗暴",管你是網(wǎng)頁還是json文本或圖片笤昨,都把你們看成是buffer祖驱,直接輸出該buffer并指定一個(gè)mime頭讓瀏覽器自己解析,這不就是對(duì)http協(xié)議的封裝么瞒窒?

接下來看下捺僻,輸出類中另一個(gè)很重要的功能:網(wǎng)頁緩存。

網(wǎng)頁緩存

網(wǎng)頁緩存包含兩部分崇裁,緩存讀取與生成緩存匕坯。

緩存生成

輸出類中提供了一個(gè)方法cache()來生成緩存,我們可以在控制器或視圖頁面中通過調(diào)用$this->output->cache($minute) 實(shí)現(xiàn)網(wǎng)頁緩存拔稳。該方法在內(nèi)部設(shè)置一個(gè)保存緩存過期時(shí)間的變量葛峻,該變量會(huì)作為生成緩存的一個(gè)條件

public function cache($time)
{
    $this->cache_expiration = is_numeric($time) ? $time : 0;
    return $this;
}

伴隨著輸出的同時(shí)生成了緩存,在_display()方法中通過調(diào)用_write_cache()函數(shù)生成緩存

image.png

我們分析下_write_cache()方法巴比,看下具體的緩存生成的邏輯

public function _write_cache($output)
{
    //判斷緩存文件所在的目錄是否存在并且有寫入的文件的權(quán)限
    $CI =& get_instance();
    $path = $CI->config->item('cache_path');
    $cache_path = ($path === '') ? APPPATH.'cache/' : $path;

    if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
    {
        log_message('error', 'Unable to write cache file: '.$cache_path);
        return;
    }

    //生成緩存文件名术奖,如果想緩存參數(shù),就把參數(shù)寫入到文件名中去匿辩,這里要注意下,
    //帶參數(shù)的緩存和不帶參數(shù)的緩存雖然緩存內(nèi)容一樣榛丢,但由于文件名的原因铲球,它們是不同的緩存
    $uri = $CI->config->item('base_url')
        .$CI->config->item('index_page')
        .$CI->uri->uri_string();

    if ($CI->config->item('cache_query_string') && ! empty($_SERVER['QUERY_STRING']))
    {
        $uri .= '?'.$_SERVER['QUERY_STRING'];
    }

    //對(duì)文件名加密
    $cache_path .= md5($uri);

    //如果生成或者打開文件失敗,就返回晰赞,ps:fopen當(dāng)文件不存在時(shí)會(huì)生成文件
    if ( ! $fp = @fopen($cache_path, 'w+b'))
    {
        log_message('error', 'Unable to write cache file: '.$cache_path);
        return;
    }

    //寫入緩存稼病,注意:由于同一時(shí)間對(duì)于一個(gè)文件來說只能有一個(gè)寫入者,所以打開文件的方式應(yīng)該使用排它鎖lock_ex,
    if (flock($fp, LOCK_EX))
    {
        //接著是根據(jù)輸出時(shí)是否指定了壓縮來決定生成的緩存是否也應(yīng)該壓縮掖鱼,
        //同時(shí)記錄設(shè)置緩存將來輸出時(shí)的mime類型然走,默認(rèn)將緩存內(nèi)容輸出成網(wǎng)頁
        if ($this->_compress_output === TRUE)
        {
            $output = gzencode($output);

            if ($this->get_header('content-type') === NULL)
            {
                $this->set_content_type($this->mime_type);
            }
        }

        //得到緩存的過期時(shí)間
        $expire = time() + ($this->cache_expiration * 60);

        //將過期時(shí)間和頭信息序列化,并做些手腳戏挡,方便將來讀取緩存時(shí)通過正則獲取到這些信息
        $cache_info = serialize(array(
            'expire'    => $expire,
            'headers'   => $this->headers
        ));

        $output = $cache_info.'ENDCI--->'.$output;

        //將數(shù)據(jù)寫入緩存文件中芍瑞,思考下為什么不直接file_put_content()一次性寫入?
        for ($written = 0, $length = strlen($output); $written < $length; $written += $result)
        {
            if (($result = fwrite($fp, substr($output, $written))) === FALSE)
            {
                break;
            }
        }

        //解除文件鎖
        flock($fp, LOCK_UN);
    }
    else
    {
        log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
        return;
    }

    fclose($fp);

    //如果寫入成功褐墅,修改緩存文件的權(quán)限拆檬,并對(duì)客戶端的緩存做一些設(shè)置,失敗的話就刪除緩存文件
    if (is_int($result))
    {
        chmod($cache_path, 0640);
        log_message('debug', 'Cache file written: '.$cache_path);
      
        //對(duì)客戶端緩存也做一些設(shè)置妥凳,
        //set_cache_header()方法第一個(gè)參數(shù)是$last_modified竟贯,其實(shí)就是文件的最后一次修改時(shí)間filemtime(),
        //由于緩存寫入的時(shí)間就是當(dāng)前服務(wù)器的時(shí)間逝钥,所以這里的$last_modified就是$_SERVER['REQUEST_TIME']
        $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
    }
    else
    {
        @unlink($cache_path);
        log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
    }
}

緩存生成的邏輯大概就這樣屑那,我們還在代碼中看到在生成服務(wù)端緩存的同時(shí)通過調(diào)用set_cache_header()對(duì)客戶端的緩存也做了設(shè)置。

/*
 *  該函數(shù)的意思如果我發(fā)現(xiàn)你客戶段的緩存還沒失效的話,我返回一個(gè)304持际,這樣客戶端你讀你本地緩存的就行了沃琅,
    如果你客戶端的緩存失效了,那么你讀取了服務(wù)端的緩存后麻煩你客戶端也緩存一下选酗,免得你每次跑來請求服務(wù)端浪費(fèi)帶寬
*/
public function set_cache_header($last_modified, $expiration)
{
    $max_age = $expiration - $_SERVER['REQUEST_TIME'];

    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
    {
        $this->set_status_header(304);
        exit;
    }
    else
    {
        header('Pragma: public');
        header('Cache-Control: max-age='.$max_age.', public');
        header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
        header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
    }
}

緩存生成的源碼就結(jié)束了阵难,接下來看下緩存讀取的源碼。

緩存讀取

緩存一般就應(yīng)該在系統(tǒng)做了最基本的初始化后訪問資源時(shí)讀取芒填,所以我們發(fā)現(xiàn)CI框架的讀取緩存的入口在引導(dǎo)文件CodeInigter.php中呜叫,通過調(diào)用_display_cache()方法發(fā)現(xiàn)一旦發(fā)現(xiàn)發(fā)現(xiàn)緩存有效,就不用再往下執(zhí)行了殿衰,直接輸出

image.png

看下緩存讀取方法_display_cache()的源碼

public function _display_cache(&$CFG, &$URI)
{

    //----------------------------采用和生成緩存一樣的方式得到文件名------------------------

    $cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');

    $uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;

    if ($CFG->item('cache_query_string') && ! empty($_SERVER['QUERY_STRING']))
    {
        $uri .= '?'.$_SERVER['QUERY_STRING'];
    }

    $filepath = $cache_path.md5($uri);


    //緩存文件不存在或者打開失敗朱庆,就返回
    if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb'))
    {
        return FALSE;
    }

    //由于文件讀取時(shí)并不拒絕他人在同一時(shí)間也讀,所以這里用共享鎖LOCK_SH
    flock($fp, LOCK_SH);

    //讀取緩存內(nèi)容
    $cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';

    flock($fp, LOCK_UN);
    fclose($fp);

    //對(duì)內(nèi)容處理下闷祥,我們之前在生成緩存時(shí)說過娱颊,對(duì)序列化信息動(dòng)手腳的原因就是方便這里用正則匹配出它們來
    if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
    {
        return FALSE;
    }

    //讀取保存在緩存文件中的過期時(shí)間
    $cache_info = unserialize($match[1]);
    $expire = $cache_info['expire'];

    $last_modified = filemtime($cache_path);

    // 如果緩存失效就刪除該緩存文件
    if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
    {
        // If so we'll delete it.
        @unlink($filepath);
        log_message('debug', 'Cache file has expired. File deleted.');
        return FALSE;
    }
    else
    {
        //如果沒失效,對(duì)客戶斷的緩存也做下設(shè)置
        $this->set_cache_header($last_modified, $expire);
    }

    //讀取緩存文件的mime類型后設(shè)置凯砍,以便輸出到瀏覽器解析成功解析它
    foreach ($cache_info['headers'] as $header)
    {
        $this->set_header($header[0], $header[1]);
    }

    //緩存也是buffer箱硕,通過調(diào)用_display輸出
    $this->_display(substr($cache, strlen($match[0])));
    log_message('debug', 'Cache file is current. Sending it to browser.');
    return TRUE;
}

至此整個(gè)CI框架對(duì)于輸入輸出的源碼分析就結(jié)束,通過分析源碼悟衩,我們學(xué)習(xí)了表單處理剧罩,安全過濾,網(wǎng)頁緩存座泳,php的輸出控制等惠昔,雖說有點(diǎn)繁瑣,但這一路下來還是收獲滿滿挑势。

我們也看到事物的萬變不離其宗镇防,所謂的輸入盡管兜轉(zhuǎn)迂回其本質(zhì)歸根結(jié)底是對(duì)全局?jǐn)?shù)組的操作。而所謂的輸出盡管像幽靈一樣躲在暗處其本質(zhì)還是對(duì) echo潮饱,print_r 這些輸出函數(shù)的高度包裝来氧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市香拉,隨后出現(xiàn)的幾起案子饲漾,更是在濱河造成了極大的恐慌,老刑警劉巖缕溉,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件考传,死亡現(xiàn)場離奇詭異,居然都是意外死亡证鸥,警方通過查閱死者的電腦和手機(jī)僚楞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門勤晚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泉褐,你說我怎么就攤上這事赐写。” “怎么了膜赃?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵挺邀,是天一觀的道長。 經(jīng)常有香客問我跳座,道長端铛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任疲眷,我火速辦了婚禮禾蚕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狂丝。我一直安慰自己换淆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布几颜。 她就那樣靜靜地躺著倍试,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛋哭。 梳的紋絲不亂的頭發(fā)上县习,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天准颓,我揣著相機(jī)與錄音哈蝇,去河邊找鬼。 笑死炮赦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的性芬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼植锉,長吁一口氣:“原來是場噩夢啊……” “哼峭拘!你這毒婦竟也來了狮暑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤辉饱,失蹤者是張志新(化名)和其女友劉穎搬男,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彭沼,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缔逛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了姓惑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褐奴。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挺益,靈堂內(nèi)的尸體忽然破棺而出歉糜,到底是詐尸還是另有隱情,我是刑警寧澤望众,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布匪补,位于F島的核電站,受9級(jí)特大地震影響烂翰,放射性物質(zhì)發(fā)生泄漏夯缺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一甘耿、第九天 我趴在偏房一處隱蔽的房頂上張望踊兜。 院中可真熱鬧,春花似錦佳恬、人聲如沸捏境。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垫言。三九已至,卻和暖如春倾剿,著一層夾襖步出監(jiān)牢的瞬間筷频,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工前痘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凛捏,地道東北人坯癣。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓示罗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帆锋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锯厢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容