<?php
!function_exists('readover') && exit('Forbidden');
/**
 * 過濾內容的實體工具，包含過濾的一系列動作及實現
 * 對其他底層類的封裝和調用，每個方法都可以單獨調用，
 * 用以實現外部的需求，比如需要進行編碼轉換，那麼直接掉
 * 本類中的編碼轉換即可獲得數據，其他方法調用類似
 *
 * @package Filter
 */
class PW_FilterUtil {
	/**
	 * 序列化後二進制字典存放的默認路徑
	 * @var string
	 */
	var $dict_bin_path;
	/**
	 * 默認所有字典的存放位置
	 * @var string
	 */
	var $dict_dir;
	/**
	 * 默認文本形式字典存放路徑
	 * @var string
	 */
	var $dict_source_path;
	/**
	 * 當次檢測的敏感詞權重等級
	 * @var int
	 */
	var $filter_weight = 0;
	/**
	 * 當次檢測的敏感詞數據
	 * @var array
	 */
	var $filter_word;

	var $code;
	var $fbwords = null;
	var $replace = null;
	var $_list = array();

	function PW_FilterUtil($file = array()) {
		if ($file) {
			$this->dict_dir = $file['dir'];
			$this->dict_bin_path = $file['bin'];
			$this->dict_source_path = $file['source'];
		} else {
			$this->dict_dir = D_P.'data/bbscache/';
			$this->dict_path = $this->dict_dir . 'wordsfb.php';
			$this->dict_bin_path = $this->dict_dir . 'dict_all.dat';
			$this->dict_source_path = $this->dict_dir . 'dict_all.txt';
		}
		$this->code = $GLOBALS['db_wordsfb'];
	}

	function setFiles($file) {
		$this->dict_dir = $file['dir'];
		$this->dict_bin_path = $file['bin'];
		$this->dict_source_path = $file['source'];
	}

	/**
	 * 檢測字符串是否需要經過詞語替換
	 */
	function ifwordsfb($str) {
		return ($this->comprise($str) === false) ? $this->code : 0;
	}

	function equal($currcode) {
		return ($currcode == $this->code);
	}

	function loadWords() {
		if (!is_array($this->fbwords)) {
			//* include pwCache::getPath(D_P."data/bbscache/wordsfb.php");
			extract(pwCache::getData(D_P."data/bbscache/wordsfb.php", false));
			$this->fbwords	= (array)$wordsfb;
			$this->replace	= (array)$replace;
			$this->alarm	= (array)$alarm;
		}
	}
	/**
	 * 返回一個經過詞語替換的字符串
	 */
	function convert($str, $wdstruct = array()) {
		$msg = $str;
		
		$file = array(
			'bin'    => $this->dict_bin_path,
			'source' => $this->dict_source_path
		);
		$trie = new Trie($file);
		$trie->nodes = $trie->getBinaryDict($trie->default_out_path);
		$msg = $trie->replaces($str);
		
		if ($wdstruct) {
			if ($msg == $str) {
				$this->addList('yes', $wdstruct['type'], $wdstruct['id']);
			} elseif ($wdstruct['code'] > 0) {
				$this->addList('no', $wdstruct['type'], $wdstruct['id']);
			}
		}
		return $msg;
	}
	function addList($key, $type, $id) {
		if (empty($this->_list)) {
			register_shutdown_function(array($this, 'updateWordsfb'));
		}
		$this->_list[$key][$type][] = $id;
	}
	/**
	 * 更新所有內容的詞語系數
	 */
	function updateWordsfb() {
		if ($this->_list['yes']) {
			$this->_update($this->_list['yes'], $this->code);
		}
		if ($this->_list['no']) {
			$this->_update($this->_list['no'], 0);
		}
		$this->_list = array();
	}

	function _update($arr, $val) {//private function
		global $db;
		foreach ($arr as $k => $v) {
			list($table, $field) = $this->tablestruct($k);
			if ($table && $v) {
				/**
				$db->update("UPDATE $table SET ifwordsfb=" . S::sqlEscape($val) . " WHERE $field IN (" . S::sqlImplode($v) . ')');
				**/
				pwQuery::update("{$table}", "$field IN (:$field)", array($v), array('ifwordsfb' => $val));
			}
		}
	}

	function tablestruct($type) {
		$struct = array(
			'topic'		=> array($GLOBALS['pw_tmsgs'], 'tid'),
			'posts'		=> array($GLOBALS['pw_posts'], 'pid'),
			'comments'	=> array('pw_comment', 'id'),
			'oboard'	=> array('pw_oboard','id'),
			'diary'		=> array('pw_diary','did')
		);
		return isset($struct[$type]) ? $struct[$type] : array('','');
	}

	/**
	 * 檢測字符串中是否包含禁用詞語
	 *
	 * @param $str
	 * @param $replace
	 * @param $alarm
	 * @return bool 是否包含禁用詞語，true為包含，false為沒有禁用詞語
	 */
	function comprise($str, $replace = true,$alarm = true) {
		if (empty($str)) {
			return false;
		}
		$this->getFilterResult($str);
		$titlelen = strlen($str);
		$arrWords = array_keys($this->filter_word);
		$title_filter_word = '';
		foreach ($arrWords as $key) {
			if ($key > $titlelen) break;
			$title_filter_word .= $title_filter_word ? ','.$this->filter_word[$key] : $this->filter_word[$key];
		}
		return $title_filter_word ? $this->getTrueBanword($title_filter_word) : false;
	}

	function getTrueBanword($word) {
		$word = stripslashes($word);
		//$word = substr($s_word,1,strlen($word)-3);
		$word = preg_replace('/\.\{0\,(\d+)\}/i', '', $word);
		return $word;
	}
	/**
	 * 檢測內容中是否有報警詞語
	 */
	function alarm($title, $content = '') {
		if ($this->alarm) {
			foreach ($this->alarm as $key => $value) {
				if (preg_match($key,$title) || preg_match($key,$content)) {
					return true;
				}
			}
		}
		return false;
	}

    /**
	* @desc 返回唯一實例,本例為單件

	function getInstance() {
		static $instance = null;
		if (!isset($instance)) {
			$instance = new FilterUtil();
		}
		return $instance;
	}*/

    /**
     * 構建字典
     * @param $path 序列化後字典存放路徑
     * @return $return int 構造成功後返回的行數
     */
    function buildDict($path = null) {
        if($path == null) {
           $path = array(
                'bin'    => $this->dict_bin_path,
                'source' => $this->dict_source_path
            );
        }
        $trie = new Trie($path);
        $return = $trie->build();
        return $return;
    }

    /**
     * 過濾內容
     * 1.過濾HTML代碼 2. 過濾火星文 3.過濾英文標點
     * 4. 繁體轉換 5. 跳詞合並 6. 分詞匹配 7.計算權重
     * 注意:過濾英文標點後過濾全角符號，可能會出現錯誤
     * @param $content string 干淨的待分詞的內容
     * @param $skip int  跳詞距離
     * @param $convert boll  簡繁轉換
     * @param $dic_path string 序列化字典存放路徑
     * @return $weight int 被分析文檔的整體權重
     */
	function paraseContent($content, $skip = 0, $convert = false, $dict_path = null) {

		//過濾用戶輸入文本中所有UBB標簽
		//$content = $this->filterWindCode($content);

		//過濾用戶輸入文本中所有HTML標簽
		$content = $this->filterHtml($content);

		//過濾中文狀態標點符號及火星文,會過濾日文片假名
		//$content = $this->filterChineseCode($content);

		//過濾鍵盤上能敲出來的各種標點符號，包括全角和半角
		// $content = $this->filterSymbol($content);

		if($convert){
			//進行編碼轉換，這里主要用于繁體轉簡體
			$content = $this->convertCode($content);
		}

		if ($skip >= 1) {
			$skip = intval($skip);
			//跳詞處理
			$content = $this->skipWords($skip,$content);
		}
		$file = array(
			'bin'    => $this->dict_bin_path,
			'source' => $this->dict_source_path
		);
		$trie = new Trie($file);

		//用于提供查找關鍵字的方法
		$result = $trie->search($content, $dict_path);
		if (empty($result)) {
            return 0;
        }

		$bayes = new Bayes();
		//獲取文章權重
		$weight = $bayes->getWeight($result);
		return array($weight,$result);
    }

	function getFilterResult($content, $skip = 0, $convert = false, $dict_path = null ) {
		//判斷敏感詞
		$result = $this->paraseContent($content);
		$array = array();
		//處理判斷結果結果
		if (is_array($result)) {
			foreach ($result[1] as $key=>$value) {
				$array[$key] = $value[0];
			}
			$array = array_unique($array);

			$this->filter_weight = $result[0] >= 1 ? 1 : ($result[0] >= 0.8 ? 2 : 3);
		}
		$this->filter_word = $array;
	}

	/**
	 * @desc 插入filter記錄
	 *
	 * @param int $tid -- 主題id
	 * @param int $pid -- 回復id
	 * @param string $filter -- 包含敏感詞
	 */
	function insert($tid, $pid, $filter, $state=0) {
    	global $db,$timestamp;

    	//判斷是否重復記錄
    	$sql = "SELECT id,state FROM pw_filter WHERE tid=".S::sqlEscape($tid)." AND pid=".S::sqlEscape($pid);
    	$record = $db->get_one($sql);

	    if (!$record) {
	    	//處理數據
	    	$value = array(
	    	    'tid'    => $tid,
	    	    'pid'    => $pid,
	            'filter' => $filter,
	            'state'  => ($state!=3 ? 0 : 3),
				'assessor'=> ($state!=3 ? '' : 'SYSTEM'),
	            'created_at' => $timestamp,
				'updated_at' => $timestamp,
	        );
	        //插入新記錄
	        $db->update("INSERT INTO pw_filter SET " . S::sqlSingle($value));
    	} else {
    		if ($record['state'] == 2 || $record['state'] == 1) {
    			//處理數據
				$value = array(
					'state'  => 0,
					'filter' => $filter,
					'created_at' => $timestamp,
				);
				$value = S::sqlSingle($value);

    			//更新記錄
				$sql = "UPDATE pw_filter SET {$value} WHERE tid=".S::sqlEscape($tid)." AND pid=" . S::sqlEscape($pid);
				$db->update($sql);
    		}
    	}
    }

	/**
	 * @desc 刪除filter
	 *
	 * @param int $tid 主題id
	 * @param int $pid 回復id
	 */
	function delete($tid, $pid) {
		global $db;
		$db->update("DELETE FROM pw_filter WHERE tid=" . S::sqlEscape($tid) . " AND pid=" . S::sqlEscape($pid));
	}

	/**
	 * 過濾鍵盤上能敲出來的各種標點符號，包括全角和半角
	 * @param $content 經過基本HTML標簽過濾的內容
	 * @return $ret string 返回過濾後的結果
	 */
	function filterSymbol($content) {
		$length = strlen($content);
		$i = 0;
		$ret = '';
		while ($i < $length) {
			$c = ord($content[$i]);
			if($c<48 || ($c>58 && $c <65) || ($c>90 && $c <97) ||($c>122 && $c<127) ) {
				$i++;
				continue;  //ASCII碼中規定的非數字字母符號
			}
			$ret .= chr($c);
			$i++;
		}
		return $ret;
	}

    /**
     * 進行編碼轉換，這里主要用于繁體轉簡體
     * @param $fcode 即from code ，原來的編碼,比如"BIG5"
     * @param $tcode 即to code，目標編碼,比如"GB2312"
     * @param $content 對已經處理過的文本進行轉換
     * @param $dict_dir 轉換字符對照表存放位置
     * @return $ret string 返回轉換後的文本
     */
    function convertCode($content, $fcode = 'CHST', $tcode = 'CHSS', $dict_dir = null) {
        if(is_null($dict_dir)) {
            $dict_dir = $this->dict_dir;
        }
        L::loadClass('Chinese', 'utility/lang', false);
        $ch = new Chinese($fcode, $tcode, true);
        $ret = $ch->Convert($content);
        return $ret;
    }

    /**
     * 跳詞處理
     * @param $skip 跳躍長度
     * @param $content 處理過的輸入文本
     * @param $dict_dir 字典文本文件位置
     * @return $ret 跳詞處理後的文本
     */
    function skipWords($skip, $content, $dict_dir=null) {
        $ret = $content;
        if(is_null($dict_dir)) {
            $dict_dir = $this->dict_source_path;
        }

        $handle = fopen($dict_dir,"r");
        while (!feof($handle)) {
            $lines = fgets($handle);
			//echo $lines;
			//exit;
			//echo $lines;
			//$lines = "古方迷香 1";
            preg_match('/^(.*?)\s+(.*)/i', $lines, $key);
            $len = strlen($key[1]); //計算關鍵詞長度
            for($i=0; $i<$len;$i++) { //開始拼裝正則
                if($i == 0) {
					if(ord($key[1][$i]) > 127){
						$rgx = substr($key[1], $i,2);
						$i++;
					}else{
						$rgx = substr($key[1], $i,1);
					}
                } else  {
					if(ord($key[1][$i]) > 127){
						$rgx .= "(.{0,".$skip."}?)". substr($key[1], $i,2);
						$i++;
					}else{
						$rgx .=  substr(str_replace(array('/','.'),array('\/','\.'),$key[1]), $i,1);
					}
                    if($i == $len-1) {
                        $rgx ="/" . $rgx ."/";
                    }
                }
            }
			//echo "$rgx, $key[1], $ret";

			//echo $rgx;exit;
            $ret = preg_replace($rgx, $key[1], $ret);
        }
        fclose($handle);
        return $ret;
    }

    /**
     * 過濾用戶輸入文本中所有UBB標簽
     * @param $content string 用戶輸入的文本
     * @return string  返回被過濾後文本
     */
    function filterWindCode($content) {
    	$pattern = array();
    	if (strpos($content,"[post]")!==false && strpos($content,"[/post]")!==false) {
    		$pattern[] = "/\[post\].+?\[\/post\]/is";
    	}
    	if (strpos($content,"[hide=")!==false && strpos($content,"[/hide]")!==false) {
    		$pattern[] = "/\[hide=.+?\].+?\[\/hide\]/is";
    	}
    	if (strpos($content,"[sell")!==false && strpos($content,"[/sell]")!==false) {
    		$pattern[] = "/\[sell=.+?\].+?\[\/sell\]/is";
    	}
    	$pattern[] = "/\[[a-zA-Z]+[^]]*?\]/is";
    	$pattern[] = "/\[\/[a-zA-Z]*[^]]\]/is";

    	$content = preg_replace($pattern,'',$content);
    	return trim($content);
    }

    /**
     * 過濾用戶輸入文本中所有HTML標簽
     * @param $content string 用戶輸入的文本
     * @return $ret string  返回被過濾後文本
     */
    function filterHtml($content) {
        $ret = strip_tags($content);
        return $ret;
    }

    /**
     * 過濾中文狀態標點符號及火星文,會過濾日文片假名
     * @param $content  string  需要過濾的字符串
     * @return $ret string 過濾後的字符串
     */
    function filterChineseCode($content) {
        $ret = "";
        $chars = array();
        //為是否為中文標點做標記
        $is_code = false;
        $length = iconv_strlen($content,"GBK");
        for ($i=0; $i<$length; $i++) {
            $chars[] = iconv_substr($content, $i, 1, "GBK");
        }

        foreach($chars as $char){

            for($byte = 0xA0; $byte<= 0xA9; $byte++) {
                if(strlen($char) == 2 && ord($char[0]) == $byte) {
                    $is_code = true;
                    continue;
                }
            }
            if(!$is_code) {
                $ret .= $char;
            }
            //回溯標記位
            $is_code = false;
        }
        return $ret;
    }
}

class Trie {
    //默認序列化後字典存放路徑
    var $default_out_path ;
    //默認原始字典存放路徑
    var $default_dict_path ;
    //節點數組。每個節點為二元組，依次為是否葉子節點，子節點.
    var $nodes ;

    function Trie($file) {
        $this->default_out_path  = $file['bin'];
        $this->default_dict_path = $file['source'];
    }

    /**
     * 構建樹，存儲序列化文本等操作封裝
     * @param $path  string 字典存放位置
     * @param $out_path  string 序列化後存放位置
     * @return $ret mixed 是否成功，不成功返回false
     */
    function build($path = null, $out_path = null) {
        if(empty($path)) {
            $path = $this->default_dict_path;
        }
        if(empty($out_path)) {
            $out_path = $this->default_out_path;
        }

        $words = $this->getDict($path);
        $tree = $this->getTree($words);
        $ret = $this->putBinaryDict($out_path, $tree);
        $a = true;
        return $ret;
    }

    /**
     * 用于提供查找關鍵字的方法
     * @param $content string 需要查找的文本
     * @param $dict_path 序列化字典路徑
     * @return $matchs array 查找到的關鍵字和權重
     */
    function search($content, $dict_path) {
        if(empty($dict_path)) {
            $dict_path = $this->default_out_path;
            $ifUpperCase = 1;
        }else{
        	$ifUpperCase = 0;
        }
        $words = $this->getBinaryDict($dict_path);
		if ($words) {
			$this->nodes = $words;
			$matchs = $this->match($ifUpperCase,$content);
			return $matchs;
		} else {
			return false;
		}
    }

    /**
     * 將文件中的字典逐行放到數組中去
     * @param $path string 字典路徑
     * @return $words array 字典
     */
    function getDict($path) {
        $i = 0;
        $words = array();

        $handle = fopen($path, "r");

        if($handle == false) {
            return $words;
        }
        while(!feof($handle)) {
            $words[$i] = trim(fgets($handle));
            $i++;
        }
        fclose($handle);
        return $words;
    }

    /**
     * 獲取序列化後的字典並反序列化
     * @param $path string 序列化字典存放路徑
     * @return $words array 反序列化後的數組
     */
    function getBinaryDict($path = null) {
        if(empty($path)) {
            $path = $this->default_out_path;
        }
		$words = readover($path);
        if(!$words) {
            return array();
        }
        $words = unserialize ($words);
        return $words;
    }

    /**
     * 將字典序列化後保存到文件中
     * @param $path string 保存路徑
     * @param $words array 數組形式的字典
     * @return $ret mixed 沒有保存成功返回false
     */
    function putBinaryDict($path, $words) {
        if(empty($path)) {
            $path = $this->default_out_path;
        }
        if(!$words) {
            return ;
        }
        $words = serialize($words);
        $handle = fopen($path, 'wb');
        $ret = fwrite($handle, $words);
        if($ret == false) {
            return false;
        }
        fclose($handle);
        return $ret;

    }

    /**
     * 構建樹的過程方法
     * @param $words array 字典和權重數組
     */
    function getTree($words) {
        $this->nodes = array( array(false, array()) ); //初始化，添加根節點
        $p = 1; //下一個要插入的節點號
        foreach ($words as $word) {
			$cur = 0; //當前節點號
			//preg_match('/^(.*?)\s+(.*)/i', $word, $weight); //提取關鍵字和權重
			//$weight = explode("|", $word);
			//$word = trim($weight[0]);
			list($word, $weight, $replace) = $this->split($word);
			for ($len = strlen($word), $i = 0; $i < $len; $i++) {
				$c = ord($word[$i]);
				if (isset($this->nodes[$cur][1][$c])) { //已存在就下移
					$cur = $this->nodes[$cur][1][$c];
					continue;
				}
				$this->nodes[$p]= array(false, array()); //創建新節點
				$this->nodes[$cur][1][$c] = $p; //在父節點記錄子節點號
				$cur = $p; //把當前節點設為新插入的
				$p++; //
			}
			$this->nodes[$cur][0] = true; //一個詞結束，標記葉子節點
			$this->nodes[$cur][2] = trim($weight); //將權重放在葉子節點
			$this->nodes[$cur][3] = trim($replace);
		}
		return $this->nodes;
	}

	function split($str) {
		if (($pos = strrpos($str, '|')) === false) {
			return array($str, 0);
		}
		return explode('|',$str);
	}

    /**
     * 用于搜索關鍵字的方法
     * @param $s string 需要查找的文本
     * @return $ret array 查找到的關鍵詞及權重
     */
    function match($ifUppCase,$s) {
    	$ifUppCase == 1 && $s = strtolower($s);
        $isUTF8 = strtoupper(substr($GLOBALS['db_charset'],0,3)) === 'UTF' ? true : false;
        $ret = array();
        $cur = 0; //當前節點，初始為根節點
        $i = 0; //字符串當前偏移
        $p = 0; //字符串回溯位置
        $len = strlen($s);
        while($i < $len) {
            $c = ord($s[$i]);
            if (isset($this->nodes[$cur][1][$c])) { //如果存在
                $cur = $this->nodes[$cur][1][$c]; //下移當前節點
                if ($this->nodes[$cur][0]) { //是葉子節點，單詞匹配！
                    $ret[$p] = array(substr($s, $p, $i - $p + 1), $this->nodes[$cur][2]); //取出匹配位置和匹配的詞以及詞的權重
                    $p = $i + 1; //設置下一個回溯位置
                    $cur = 0; //重置當前節點為根節點
                }
				$i++; //下一個字符
            } else { //不匹配
				$cur = 0; //重置當前節點為根節點
                if (!$isUTF8 && ord($s[$p]) > 127 && ord($s[$p+1]) > 127) {
					$p += 2; //設置下一個回溯位置
				} else {
					$p += 1; //設置下一個回溯位置
				}
				$i = $p; //把當前偏移設為回溯位置
            }
        }
        return $ret;    
    }

 	function replaces($s,$ifUppCase = 0) {
    	$ifUppCase && $s = strtolower($s);
        $isUTF8 = strtoupper(substr($GLOBALS['db_charset'],0,3)) === 'UTF' ? true : false;
        $ret = array();
        $cur = 0; //當前節點，初始為根節點
        $i = 0; //字符串當前偏移
        $p = 0; //字符串回溯位置
        $len = strlen($s);
        while($i < $len) {
            $c = ord($s[$i]);
            if (isset($this->nodes[$cur][1][$c])) { //如果存在
                $cur = $this->nodes[$cur][1][$c]; //下移當前節點
                if ($this->nodes[$cur][0]) { //是葉子節點，單詞匹配！
                    $s = ($this->nodes[$cur][2] == 0.6 && isset($this->nodes[$cur][3])) ? substr_replace($s, $this->nodes[$cur][3], $p, $i - $p + 1) : $s; //取出匹配位置和匹配的詞以及詞的權重
                    $p = $i + 1; //設置下一個回溯位置
                    $cur = 0; //重置當前節點為根節點
                }
				$i++; //下一個字符
            } else { //不匹配
				$cur = 0; //重置當前節點為根節點
                if (!$isUTF8 && ord($s[$p]) > 127 && ord($s[$p+1]) > 127) {
					$p += 2; //設置下一個回溯位置
				} else {
					$p += 1; //設置下一個回溯位置
				}
				$i = $p; //把當前偏移設為回溯位置
            }
        }
        return $s;    
    }
}

/**
 * 根據給定詞語權重對文檔進行評分，目前使用Bayes算法，不考慮詞頻影響
 * 算法如下︰
 * 假設文檔中有分詞t1,t2,t3,……tn,其權重分別為w1,w2,w3,……,wn
 * 則根據Bayes算法，文檔權重為︰
 * 設p1 = w1*w2*w3*……*wn
 * 設p2 = (1-w1)*(1-w2)*(1-w3)*……*(1-wn)
 * 則文檔權重 w = p1/(p1+p2)
 * 如果p1+p2=0,文檔權重為1
 * 權重低于0.5的關鍵詞會降低整體權重，大于0.5則會提高整體權重
 * 如0.9, 0.8, 0.5, 0.6 經過Bayes計算後權重為0.98，
 * 而0.9, 0.8, 0.5, 0.1 經過計算後權重僅為0.8
 */
class Bayes {

    /**
     * 獲取文章權重
     * @param $keys 文檔中匹配的關鍵詞數組及權重信息
     * @return  $weight 經過Bayes算法處理過的權重
     */
    function getWeight($keys) {
		//print_r($keys);
        $p1 = 1;
        $p2 = 1;
        foreach($keys as $key) {
            if( empty($key[1]) ) {
                continue;
            }
            $weight = floatval($key[1]);
            $p1 *= $weight;
            $p2 *= (1- $weight);
        }
        if( ($p1 + $p2) == 0 ) {
            $weight = 1;
            return $weight;
        }

        $weight = $p1 / ($p1 + $p2);
        return $weight;
    }
}
?>