3 * PIO Log API (flock+)
\r
5 * 提供存取以 Log 檔案構成的資料結構後端的物件
\r
7 * @package PMCLibrary
\r
8 * @version $Id: pio.logflockp.php 671 2008-09-20 05:54:15Z scribe $
\r
9 * @date $Date: 2008-09-20 13:54:15 +0800 (星期六, 20 九月 2008) $
\r
11 * @package PMCLibrary
\r
17 var $ENV, $logfile, $treefile, $porderfile; // Local Constant
\r
18 var $logs, $trees, $LUT, $porder, $torder, $prepared; // Local Global
\r
19 //var $memcached, $mid;
\r
21 /* private 設定 memcached 資料
\r
22 function _memcacheSet($isAnalysis=true){
\r
23 if(!$this->_memcachedEstablish()) return false;
\r
24 $this->memcached->set('pmc'.$this->mid.'_isset', true);
\r
26 $this->memcached->set('pmc'.$this->mid.'_logs', ($isAnalysis ? array_map(array($this, '_AnalysisLogs'), $this->logs) : $this->logs));
\r
27 $this->memcached->set('pmc'.$this->mid.'_trees', $this->trees);
\r
28 $this->memcached->set('pmc'.$this->mid.'_LUT', $this->LUT);
\r
29 $this->memcached->set('pmc'.$this->mid.'_porder', $this->porder);
\r
30 $this->memcached->set('pmc'.$this->mid.'_torder', $this->torder);
\r
33 /* private 取得 memcached 資料
\r
34 function _memcacheGet(){
\r
35 if(!$this->_memcachedEstablish()) return false;
\r
36 if($this->memcached->get('pmc'.$this->mid.'_isset')){ // 有資料
\r
37 $this->logs = $this->memcached->get('pmc'.$this->mid.'_logs');
\r
38 $this->trees = $this->memcached->get('pmc'.$this->mid.'_trees');
\r
39 $this->LUT = $this->memcached->get('pmc'.$this->mid.'_LUT');
\r
40 $this->porder = $this->memcached->get('pmc'.$this->mid.'_porder');
\r
41 $this->torder = $this->memcached->get('pmc'.$this->mid.'_torder');
\r
46 /* private 建立 memcached 實體
\r
47 function _memcachedEstablish(){
\r
48 if(!extension_loaded('memcache')) return ($this->memcached = false);
\r
49 if(is_null($this->memcached)){
\r
50 $this->memcached = new Memcache;
\r
51 if(!$this->memcached->pconnect('localhost')) return ($this->memcached = false);
\r
54 return ($this->memcached===false) ? false : true;
\r
57 function PIOlogflockp($connstr='', $ENV){
\r
59 $this->logs = $this->trees = $this->LUT = $this->porder = $this->torder = array();
\r
60 $this->prepared = 0;
\r
61 //$this->mid = md5($_SERVER['SCRIPT_FILENAME']); // Unique ID
\r
62 //$this->memcached = false; // memcached object (null: use, false: don't use)
\r
64 if($connstr) $this->dbConnect($connstr);
\r
67 /* private 把每一行 Log 解析轉換成陣列資料 */
\r
68 function _AnalysisLogs($line){
\r
70 list($tline['no'], $tline['resto'], $tline['md5chksum'], $tline['category'], $tline['tim'], $tline['ext'], $tline['imgw'], $tline['imgh'], $tline['imgsize'], $tline['filename'], $tline['tw'], $tline['th'], $tline['pwd'], $tline['now'], $tline['name'], $tline['email'], $tline['sub'], $tline['com'], $tline['host'], $tline['status']) = explode(',', $line);
\r
71 return array_reverse($tline); // list()是由右至左代入的
\r
74 /* private 將回文放進陣列 */
\r
75 function _includeReplies($posts){
\r
76 $torder_flip = array_flip($this->torder);
\r
77 foreach($posts as $post){
\r
78 if(array_key_exists($post, $torder_flip)){ // 討論串首篇
\r
79 $posts = array_merge($posts, $this->trees[$post]);
\r
82 return array_merge(array(), array_unique($posts)); // 去除重複值
\r
85 /* private 取代 , 成為 , 避免衝突 */
\r
86 function _replaceComma($txt){
\r
87 return str_replace(',', ',', $txt);
\r
90 /* private 由編號取出資料分析成陣列 */
\r
91 function _ArrangeArrayStructure($line){
\r
92 $line = (array)$line; // 全部視為Arrays
\r
94 foreach($line as $i){
\r
95 if(!isset($this->LUT[$i])) continue;
\r
96 if(!is_array($this->logs[$this->LUT[$i]])){ // 進行分析轉換
\r
97 $line = $this->logs[$this->LUT[$i]]; if($line=='') continue;
\r
98 $this->logs[$this->LUT[$i]] = $this->_AnalysisLogs($line);
\r
100 $posts[] = $this->logs[$this->LUT[$i]];
\r
106 function pioVersion(){
\r
107 return '0.6 (v20100404)';
\r
111 function dbConnect($connStr){
\r
112 if(preg_match('/^logflockp:\/\/(.*)\:(.*)\/$/i', $connStr, $linkinfos)){
\r
113 $this->logfile = $this->ENV['BOARD'].'/'.$linkinfos[1]; // 投稿文字記錄檔檔名
\r
114 $this->treefile = $this->ENV['BOARD'].'/'.$linkinfos[2]; // 樹狀結構記錄檔檔名
\r
115 $this->porderfile = $this->ENV['LUTCACHE']; // LUT索引查找表暫存檔案
\r
121 $chkfile = array($this->logfile, $this->treefile, $this->porderfile);
\r
123 foreach($chkfile as $value){
\r
124 if(!is_file($value)){ // 檔案不存在
\r
125 $fp = fopen($value, 'w');
\r
126 stream_set_write_buffer($fp, 0);
\r
127 if($value==$this->logfile) fwrite($fp, '0,0,,,0,,0,0,,,0,0,,08/11/08(土) 10:24:04,【スパーキー(④ ^ヮ^)】,,'.$this->ENV['NOTITLE'].','.$this->ENV['NOCOMMENT'].',,,'); // PIO Structure V4
\r
128 if($value==$this->treefile) fwrite($fp, '1');
\r
129 if($value==$this->porderfile) fwrite($fp, '1');
\r
132 @chmod($value, 0666);
\r
139 function dbPrepare($reload=false, $transaction=true){
\r
140 if($this->prepared && !$reload) return true;
\r
141 if($reload && $this->prepared) $this->porder = $this->torder = $this->LUT = $this->logs = $this->trees = array();
\r
142 //if($this->_memcacheGet()){ $this->prepared = 1; return true; } // 如果 memcache 有快取則直接使用
\r
144 $this->logs = file($this->logfile); // Log每行原始資料
\r
145 if(!file_exists($this->porderfile)){ // LUT不在,重生成
\r
147 foreach($this->logs as $line){
\r
148 if(!isset($line)) continue;
\r
149 $tmp = explode(',', $line); $lut .= $tmp[0]."\r\n";
\r
151 $this->_lock($this->porderfile);
\r
152 $fp = fopen($this->porderfile, 'w'); // LUT
\r
153 stream_set_write_buffer($fp, 0);
\r
154 // flock($fp, LOCK_EX); // 鎖定檔案
\r
156 // flock($fp, LOCK_UN); // 解鎖
\r
158 $this->_unlock($this->porderfile);
\r
160 $this->porder = array_map('rtrim', file($this->porderfile)); // 文章編號陣列
\r
161 $this->LUT = array_flip($this->porder); // LUT索引查找表
\r
163 $tree = array_map('rtrim', file($this->treefile));
\r
164 foreach($tree as $treeline){ // 解析樹狀結構製成索引
\r
165 if($treeline=='') continue;
\r
166 $tline = explode(',', $treeline);
\r
167 $this->torder[] = $tline[0]; // 討論串首篇編號陣列
\r
168 $this->trees[$tline[0]] = $tline; // 特定編號討論串完整結構陣列
\r
170 //$this->_memcacheSet(); // 把目前資料設定到 memcached 內
\r
171 $this->prepared = 1;
\r
175 function dbCommit(){
\r
176 if(!$this->prepared) return false;
\r
178 $log = $tree = $lut = '';
\r
179 $this->logs = array_merge(array(), $this->logs); // 更新logs鍵值
\r
180 $this->torder = array_merge(array(), $this->torder); // 更新torder鍵值
\r
181 $this->porder = $this->LUT = array(); // 重新生成索引
\r
183 foreach($this->logs as $line){
\r
184 if(!isset($line)) continue;
\r
185 if(is_array($line)){ // 已被分析過
\r
186 $log .= implode(',', $line).",\r\n";
\r
187 $lut .= ($this->porder[] = $line['no'])."\r\n";
\r
190 $tmp = explode(',', $line); $lut .= ($this->porder[] = $tmp[0])."\r\n";
\r
193 $this->LUT = array_flip($this->porder);
\r
194 $tcount = count($this->trees);
\r
195 for($tline = 0; $tline < $tcount; $tline++){
\r
196 $tree .= $this->isThread($this->torder[$tline]) ? implode(',', $this->trees[$this->torder[$tline]])."\r\n" : '';
\r
198 //$this->_memcacheSet(false); // 更新快取 (不需要再分析)
\r
200 $this->_lock($this->logfile);
\r
201 $fp = fopen($this->logfile, 'w'); // Log
\r
202 stream_set_write_buffer($fp, 0);
\r
203 // flock($fp, LOCK_EX); // 鎖定檔案
\r
205 // flock($fp, LOCK_UN); // 解鎖
\r
207 $this->_unlock($this->logfile);
\r
209 $this->_lock($this->treefile);
\r
210 $fp = fopen($this->treefile, 'w'); // tree
\r
211 stream_set_write_buffer($fp, 0);
\r
212 // flock($fp, LOCK_EX); // 鎖定檔案
\r
213 fwrite($fp, $tree);
\r
214 // flock($fp, LOCK_UN); // 解鎖
\r
216 $this->_unlock($this->treefile);
\r
218 $this->_lock($this->porderfile);
\r
219 $fp = fopen($this->porderfile, 'w'); // LUT
\r
220 stream_set_write_buffer($fp, 0);
\r
221 // flock($fp, LOCK_EX); // 鎖定檔案
\r
223 // flock($fp, LOCK_UN); // 解鎖
\r
225 $this->_unlock($this->porderfile);
\r
229 function dbMaintanence($action,$doit=false){
\r
233 $this->dbPrepare(false);
\r
234 $gp = gzopen('piodata.log.gz', 'w9');
\r
235 gzwrite($gp, $this->dbExport());
\r
237 return '<a href="piodata.log.gz">下載 piodata.log.gz 中介檔案</a>';
\r
238 }else return true; // 支援匯出資料
\r
243 default: return false; // 不支援
\r
248 function dbImport($data){
\r
249 $arrData = explode("\r\n", $data);
\r
250 $arrData_cnt = count($arrData) - 1; // 最後一個是空的
\r
251 $arrTree = array();
\r
252 $tree = $logs = $lut = '';
\r
253 for($i = 0; $i < $arrData_cnt; $i++){
\r
254 $line = explode(',', $arrData[$i], 4); // 切成四段
\r
255 $logs .= $line[0].','.$line[1].','.$line[3]."\r\n"; // 重建討論結構
\r
256 $lut .= $line[0]."\r\n"; // 重建 LUT 查找表結構
\r
257 if($line[1]==0){ // 首篇
\r
258 if(!isset($arrTree[$line[0]])) $arrTree[$line[0]] = array($line[0]); // 僅自身一篇
\r
259 else array_unshift($arrTree[$line[0]], $line[0]);
\r
262 if(!isset($arrTree[$line[1]])) $arrTree[$line[1]] = array();
\r
263 array_unshift($arrTree[$line[1]], $line[0]);
\r
265 foreach($arrTree as $t) $tree .= implode(',', $t)."\r\n"; // 重建樹狀結構
\r
266 $chkfile = array($this->logfile, $this->treefile, $this->porderfile);
\r
267 foreach($chkfile as $value){
\r
268 $fp = fopen($value, 'w');
\r
269 stream_set_write_buffer($fp, 0);
\r
270 if($value==$this->logfile) fwrite($fp, $logs);
\r
271 if($value==$this->treefile) fwrite($fp, $tree);
\r
272 if($value==$this->porderfile) fwrite($fp, $lut);
\r
275 @chmod($value, 0666);
\r
281 function dbExport(){
\r
282 if(!$this->prepared) $this->dbPrepare();
\r
283 $f = file($this->logfile);
\r
285 foreach($f as $line){
\r
286 $line = explode(',', $line, 3); // 分成三段 (最後一段特別長)
\r
287 if($line[1]==0 && isset($this->trees[$line[0]])){
\r
288 $lastno = array_pop($this->trees[$line[0]]);
\r
289 $line2 = $this->fetchPosts($lastno);
\r
290 $root = gmdate('Y-m-d H:i:s', substr($line2[0]['tim'], 0, 10)); // UTC 時間
\r
291 unset($this->trees[$line[0]]); // 刪除表示已取過
\r
295 $data .= $line[0].','.$line[1].','.$root.','.$line[2];
\r
301 function postCount($resno=0){
\r
302 if(!$this->prepared) $this->dbPrepare();
\r
304 return $resno ? ($this->isThread($resno) ? count(@$this->trees[$resno]) : 0) : count($this->porder);
\r
308 function threadCount(){
\r
309 if(!$this->prepared) $this->dbPrepare();
\r
311 return count($this->torder);
\r
315 function getLastPostNo($state){
\r
316 if(!$this->prepared) $this->dbPrepare();
\r
319 case 'beforeCommit':
\r
320 case 'afterCommit':
\r
321 return reset($this->porder);
\r
326 function fetchPostList($resno=0, $start=0, $amount=0){
\r
327 if(!$this->prepared) $this->dbPrepare();
\r
331 if($this->isThread($resno)){
\r
332 if($start && $amount){
\r
333 $plist = array_slice($this->trees[$resno], $start, $amount);
\r
334 array_unshift($plist, $resno);
\r
336 if(!$start && $amount) $plist = array_slice($this->trees[$resno], 0, $amount);
\r
337 if(!$start && !$amount) $plist = $this->trees[$resno];
\r
340 $plist = $amount ? array_slice($this->porder, $start, $amount) : $this->porder;
\r
346 function fetchThreadList($start=0, $amount=0, $isDESC=false){
\r
347 if(!$this->prepared) $this->dbPrepare();
\r
348 $tmp_array = $this->torder;
\r
349 if($isDESC) rsort($tmp_array); // 按編號遞減排序 (預設為按最後更新時間排序)
\r
350 return $amount ? array_slice($tmp_array, $start, $amount) : $tmp_array;
\r
354 function fetchPosts($postlist){
\r
355 if(!$this->prepared) $this->dbPrepare();
\r
357 return $this->_ArrangeArrayStructure($postlist); // 輸出陣列結構
\r
360 /* 刪除舊附件 (輸出附件清單) */
\r
361 function delOldAttachments($total_size, $storage_max, $warnOnly=true){
\r
363 if(!$this->prepared) $this->dbPrepare();
\r
365 $rpord = $this->porder; sort($rpord); // 由舊排到新 (小->大)
\r
366 $arr_warn = $arr_kill = array();
\r
367 foreach($rpord as $post){
\r
368 $logsarray = $this->_ArrangeArrayStructure($post); // 分析資料為陣列
\r
369 if($FileIO->imageExists($logsarray[0]['tim'].$logsarray[0]['ext'])){ $total_size -= $FileIO->getImageFilesize($logsarray[0]['tim'].$logsarray[0]['ext']) / 1024; $arr_kill[] = $post; $arr_warn[$post] = 1; } // 標記刪除
\r
370 if($FileIO->imageExists($logsarray[0]['tim'].'s.jpg')) $total_size -= $FileIO->getImageFilesize($logsarray[0]['tim'].'s.jpg') / 1024;
\r
371 if($total_size < $storage_max) break;
\r
373 return $warnOnly ? $arr_warn : $this->removeAttachments($arr_kill);
\r
377 function removePosts($posts){
\r
378 if(!$this->prepared) $this->dbPrepare();
\r
379 if(count($posts)==0) return array();
\r
381 $posts = $this->_includeReplies($posts); // 包含所有回文
\r
382 $filelist = $this->removeAttachments($posts); // 欲刪除附件
\r
383 $torder_flip = array_flip($this->torder);
\r
384 $pcount = count($posts);
\r
385 $logsarray = $this->_ArrangeArrayStructure($posts); // 分析資料為陣列
\r
386 for($p = 0; $p < $pcount; $p++){
\r
387 if(!isset($logsarray[$p])) continue;
\r
388 if($logsarray[$p]['resto']==0){ // 討論串頭
\r
389 unset($this->trees[$logsarray[$p]['no']]); // 刪除樹狀記錄
\r
390 if(array_key_exists($logsarray[$p]['no'], $torder_flip)) unset($this->torder[$torder_flip[$logsarray[$p]['no']]]); // 從討論串首篇陣列中移除
\r
393 if(array_key_exists($logsarray[$p]['resto'], $this->trees)){
\r
394 $tr_flip = array_flip($this->trees[$logsarray[$p]['resto']]);
\r
395 unset($this->trees[$logsarray[$p]['resto']][$tr_flip[$posts[$p]]]);
\r
398 unset($this->logs[$this->LUT[$logsarray[$p]['no']]]);
\r
399 if(array_key_exists($logsarray[$p]['no'], $this->LUT)) unset($this->porder[$this->LUT[$logsarray[$p]['no']]]); // 從討論串編號陣列中移除
\r
401 $this->LUT = array_flip($this->porder);
\r
405 /* 刪除附件 (輸出附件清單) */
\r
406 function removeAttachments($posts){
\r
408 if(!$this->prepared) $this->dbPrepare();
\r
409 if(count($posts)==0) return array();
\r
412 $logsarray = $this->_ArrangeArrayStructure($posts); // 分析資料為陣列
\r
413 $lcount = count($logsarray);
\r
414 for($i = 0; $i < $lcount; $i++){
\r
415 if($logsarray[$i]['ext']){
\r
416 if($FileIO->imageExists($logsarray[$i]['tim'].$logsarray[$i]['ext'])) $files[] = $logsarray[$i]['tim'].$logsarray[$i]['ext'];
\r
417 if($FileIO->imageExists($logsarray[$i]['tim'].'s.jpg')) $files[] = $logsarray[$i]['tim'].'s.jpg';
\r
424 function addPost($no, $resto, $md5chksum, $category, $tim, $ext, $imgw, $imgh, $imgsize, $filename, $tw, $th, $pwd, $now, $name, $email, $sub, $com, $host, $age=false, $status='') {
\r
425 if(!$this->prepared) $this->dbPrepare();
\r
427 $tline = array($no, $resto, $md5chksum, $category, $tim, $ext, $imgw, $imgh, $imgsize, $filename, $tw, $th, $pwd, $now, $name, $email, $sub, $com, $host, $status);
\r
428 $tline = array_map(array($this, '_replaceComma'), $tline); // 將資料內的 , 轉換 (Only Log needed)
\r
429 array_unshift($this->logs, implode(',', $tline).",\r\n"); // 更新logs
\r
430 array_unshift($this->porder, $no); // 更新porder
\r
431 $this->LUT = array_flip($this->porder); // 更新LUT
\r
435 $this->trees[$resto][] = $no;
\r
437 $torder_flip = array_flip($this->torder);
\r
438 unset($this->torder[$torder_flip[$resto]]); // 先刪除舊有位置
\r
439 array_unshift($this->torder, $resto); // 再移到頂端
\r
442 $this->trees[$no][0] = $no;
\r
443 array_unshift($this->torder, $no);
\r
448 function isSuccessivePost($lcount, $com, $timestamp, $pass, $passcookie, $host, $isupload){
\r
449 if(!$this->prepared) $this->dbPrepare();
\r
451 $pcount = $this->postCount();
\r
452 $lcount = ($pcount > $lcount) ? $lcount : $pcount;
\r
453 for($i = 0; $i < $lcount; $i++){
\r
454 $logsarray = $this->_ArrangeArrayStructure($this->porder[$i]); // 分析資料為陣列
\r
455 list($lcom, $lhost, $lpwd, $ltime) = array($logsarray[0]['com'], $logsarray[0]['host'], $logsarray[0]['pwd'], substr($logsarray[0]['tim'],0,-3));
\r
456 if($host==$lhost || $pass==$lpwd || $passcookie==$lpwd) $pchk = 1;
\r
458 if($this->ENV['PERIOD.POST'] && $pchk){ // 密碼比對符合且開啟連續投稿時間限制
\r
459 if($timestamp - $ltime < $this->ENV['PERIOD.POST']) return true; // 投稿時間相距太短
\r
460 if($timestamp - $ltime < $this->ENV['PERIOD.IMAGEPOST'] && $isupload) return true; // 附加圖檔的投稿時間相距太短
\r
461 if($com == $lcom && !$isupload) return true; // 內文一樣
\r
468 function isDuplicateAttachment($lcount, $md5hash){
\r
471 $pcount = $this->postCount();
\r
472 $lcount = ($pcount > $lcount) ? $lcount : $pcount;
\r
473 for($i = 0; $i < $lcount; $i++){
\r
474 $logsarray = $this->_ArrangeArrayStructure($this->porder[$i]); // 分析資料為陣列
\r
475 if(!$logsarray[0]['md5chksum']) continue; // 無附加圖檔
\r
476 if($logsarray[0]['md5chksum']==$md5hash){
\r
477 if($FileIO->imageExists($logsarray[0]['tim'].$logsarray[0]['ext'])) return true; // 存在MD5雜湊相同的檔案
\r
484 function isThread($no){
\r
485 if(!$this->prepared) $this->dbPrepare();
\r
487 return isset($this->trees[$no]);
\r
491 function searchPost($keyword,$field,$method){
\r
492 if(!$this->prepared) $this->dbPrepare();
\r
494 $foundPosts = array();
\r
495 $keyword_cnt = count($keyword);
\r
496 $pcount = $this->postCount();
\r
497 for($i = 0; $i < $pcount; $i++){
\r
498 $logsarray = $this->_ArrangeArrayStructure($this->porder[$i]); // 分析資料為陣列
\r
500 foreach($keyword as $k){
\r
501 if(strpos($logsarray[0][$field], $k)!==FALSE) $found++;
\r
502 if($method=="OR" && $found) break;
\r
504 if($method=="AND" && $found==$keyword_cnt) array_push($foundPosts, $logsarray[0]); // 全部都有找到 (AND交集搜尋)
\r
505 elseif($method=="OR" && $found) array_push($foundPosts, $logsarray[0]); // 有找到 (OR聯集搜尋)
\r
507 return $foundPosts;
\r
511 function searchCategory($category){
\r
512 if(!$this->prepared) $this->dbPrepare();
\r
514 $category = strtolower($category);
\r
515 $foundPosts = array();
\r
516 $pcount = $this->postCount();
\r
517 for($i = 0; $i < $pcount; $i++){
\r
518 $logsarray = $this->_ArrangeArrayStructure($this->porder[$i]); // 分析資料為陣列
\r
519 if(!($ary_category = $logsarray[0]['category'])) continue;
\r
520 if(strpos(strtolower($ary_category), ','.$category.',')!==false) array_push($foundPosts, $logsarray[0]['no']); // 找到標籤,加入名單
\r
522 return $foundPosts;
\r
526 function getPostStatus($status){
\r
527 return new FlagHelper($status); // 回傳 FlagHelper 物件
\r
531 function updatePost($no, $newValues){
\r
532 if(!$this->prepared) $this->dbPrepare();
\r
534 $chk = array('resto', 'md5chksum', 'category', 'tim', 'ext', 'imgw', 'imgh', 'imgsize', 'filename', 'tw', 'th', 'pwd', 'now', 'name', 'email', 'sub', 'com', 'host', 'status');
\r
536 $this->_ArrangeArrayStructure($no); // 將資料變成陣列
\r
537 foreach($chk as $c)
\r
538 if(isset($newValues[$c]))
\r
539 $this->logs[$this->LUT[$no]][$c] = $newValues[$c]; // 修改數值
\r
543 function setPostStatus($no, $newStatus){
\r
544 $this->updatePost($no, array('status' => $newStatus));
\r
548 function _lock($lock, $tries=10) {
\r
549 ignore_user_abort(true);
\r
550 $lock0 = ".{$lock}0";
\r
551 $lock1 = ".{$lock}1";
\r
552 for ($i=0; $i<$tries; $i++) {
\r
553 if (!is_file($lock0)) {
\r
555 if (!is_file($lock1)) {
\r
566 function _unlock($lock) {
\r
567 unlink(".{$lock}1");
\r
568 unlink(".{$lock}0");
\r
569 ignore_user_abort(false);
\r