大家有沒有想一下一般我們可以通過$_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', '<?\\1', $str);
}
else
{
$str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $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(\\3)',
$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í)被加載成功了
一般情況下骂澄,我們要想向?yàn)g覽器輸出內(nèi)容,必須靠 echo惕虑,print_r坟冲,var_dump之類的輸出函數(shù),可是在視圖加載的過程中溃蔫,并沒有看見這些函數(shù)健提,那視圖中的hello world到底是如何輸出的?
如果大家仔細(xì)看文檔的話,輸出類中有個(gè)_display()方法伟叛,文檔中是這樣描述該方法的(注意圖中圈出的文字)
那么_display()方法是在哪里被調(diào)用矩桂?是在引導(dǎo)文件CodeIgniter.php中調(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)用輸出類 Output 的 append_output() 方法將內(nèi)容扔給final_output玖像,從而讀取到了視圖中的hello world
對(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ù)生成緩存
我們分析下_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í)行了殿衰,直接輸出
看下緩存讀取方法_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ù)的高度包裝来氧。