5 * 提供存取以 PostgreSQL 資料庫構成的資料結構後端的物件
\r
7 * @package PMCLibrary
\r
8 * @version $Id: pio.pgsql.php 671 2008-09-20 05:54:15Z scribe $
\r
9 * @date $Date: 2008-09-20 13:54:15 +0800 (星期六, 20 九月 2008) $
\r
13 var $ENV, $username, $password, $server, $port, $dbname, $tablename; // Local Constant
\r
14 var $con, $prepared, $useTransaction; // Local Global
\r
16 function PIOpgsql($connstr='', $ENV){
\r
18 $this->prepared = 0;
\r
19 if($connstr) $this->dbConnect($connstr);
\r
22 /* private 攔截SQL錯誤 */
\r
23 function _error_handler($errarray, $query=''){
\r
24 $err = 'Pixmicat! SQL Error: '.$errarray[0].' on line '.$errarray[1];
\r
25 //error_log($err."\n".pg_last_error($this->con)."\n".$query."\n\n", 3, 'error.log');
\r
26 trigger_error($err, E_USER_ERROR);
\r
30 /* private 使用SQL字串和PostgreSQL伺服器要求 */
\r
31 function _pgsql_call($query, $errarray=false){
\r
32 $resource = pg_query($this->con, $query);
\r
33 if(is_array($errarray) && $resource===false) $this->_error_handler($errarray, $query);
\r
34 else return $resource;
\r
37 /* private 由資源輸出陣列 */
\r
38 function _ArrangeArrayStructure($line){
\r
40 while($row = pg_fetch_array($line, null, PGSQL_ASSOC)) $posts[] = $row;
\r
41 pg_free_result($line);
\r
46 function pioVersion(){
\r
47 return '0.6 (v20100404)';
\r
51 function dbConnect($connStr){
\r
52 // 格式: pgsql://帳號:密碼@伺服器位置:埠號(可省略)/資料庫/資料表/
\r
53 // 示例: pgsql://yotsubanome:pass@localhost/yotsubanome/img_loog/
\r
54 if(preg_match('/^pgsql:\/\/(.*)\:(.*)\@([^\:]*)((\:)([0-9]+)){0,1}\/(.*)\/(.*)\/$/i', $connStr, $linkinfos)){
\r
55 $this->username = $linkinfos[1]; // 登入帳號
\r
56 $this->password = $linkinfos[2]; // 登入密碼
\r
57 $this->server = $linkinfos[3]; // 登入伺服器
\r
58 $this->port = ($linkinfos[6] ? $linkinfos[6] : '5432'); // 登入埠號
\r
59 $this->dbname = $linkinfos[7]; // 資料庫名稱
\r
60 $this->tablename = $linkinfos[8]; // 資料表名稱
\r
65 function dbInit($isAddInitData=true){
\r
67 if(pg_num_rows(pg_query($this->con, "SELECT relname FROM pg_class WHERE relname = '".$this->tablename."'"))!=1){ // 資料表不存在
\r
68 $result = "CREATE SEQUENCE ".$this->tablename."_no_seq;
\r
69 CREATE TABLE ".$this->tablename." (
70 \"no\" int NOT NULL DEFAULT nextval('".$this->tablename."_no_seq'),
\r
71 \"resto\" int NOT NULL,
\r
72 \"root\" timestamp NULL DEFAULT '1980-01-01 00:00:00',
\r
73 \"time\" int NOT NULL,
\r
74 \"md5chksum\" varchar(32) NOT NULL,
\r
75 \"category\" varchar(255) NOT NULL,
\r
76 \"tim\" bigint NOT NULL,
\r
77 \"ext\" varchar(4) NOT NULL,
\r
78 \"imgw\" smallint NOT NULL,
\r
79 \"imgh\" smallint NOT NULL,
\r
80 \"imgsize\" varchar(10) NOT NULL,
81 \"filename\" varchar(255) NOT NULL,
\r
82 \"tw\" smallint NOT NULL,
\r
83 \"th\" smallint NOT NULL,
\r
84 \"pwd\" varchar(8) NOT NULL,
\r
85 \"now\" varchar(255) NOT NULL,
\r
86 \"name\" varchar(255) NOT NULL,
\r
87 \"email\" varchar(255) NOT NULL,
\r
88 \"sub\" varchar(255) NOT NULL,
\r
89 \"com\" text NOT NULL,
\r
90 \"host\" varchar(255) NOT NULL,
\r
91 \"status\" varchar(255) NOT NULL,
\r
92 PRIMARY KEY (\"no\"));"; // PIO Structure V4
\r
93 $idxs = array('resto', 'root', 'time');
\r
94 foreach($idxs as $idx) $result .= 'CREATE INDEX '.$this->tablename.'_'.$idx.'_index ON '.$this->tablename.' ('.$idx.');';
\r
95 pg_query($this->con, $result); // 正式新增資料表
\r
96 if($isAddInitData) $this->addPost(0, 0, '', '', 0, '', 0, 0, '', '', 0, 0, '', '08/11/08(土) 10:24:04', '【スパーキー(④ ^ヮ^)】', '', $this->ENV['NOTITLE'], $this->ENV['NOCOMMENT'], ''); // 追加一筆新資料
\r
102 function dbPrepare($transaction=true){
\r
103 if($this->prepared) return true;
\r
105 $this->con = pg_pconnect("host='".$this->server."' port=".$this->port." dbname='".$this->dbname."' user='".$this->username."' password='".$this->password."'");
\r
106 if(!$this->con) $this->_error_handler(array('Open database failed', __LINE__));
\r
107 $this->useTransaction = $transaction;
\r
108 if($transaction) @pg_query($this->con, 'START TRANSACTION;'); // 啟動交易性能模式
\r
110 $this->prepared = 1;
\r
114 function dbCommit(){
\r
115 if(!$this->prepared) return false;
\r
117 if($this->useTransaction) @pg_query($this->con, 'COMMIT;'); // 交易性能模式提交
\r
121 function dbMaintanence($action, $doit=false){
\r
125 $this->dbPrepare(false);
\r
126 if($this->_pgsql_call('VACUUM '.$this->tablename)) return true;
\r
128 }else return true; // 支援最佳化資料表
\r
132 $this->dbPrepare(false);
\r
133 $gp = gzopen('piodata.log.gz', 'w9');
\r
134 gzwrite($gp, $this->dbExport());
\r
136 return '<a href="piodata.log.gz">下載 piodata.log.gz 中介檔案</a>';
\r
137 }else return true; // 支援匯出資料
\r
141 default: return false; // 不支援
\r
146 function dbImport($data){
\r
147 $this->dbInit(false); // 僅新增結構不新增資料
\r
148 $data = explode("\r\n", $data);
\r
149 $data_count = count($data) - 1;
\r
150 $replaceComma = create_function('$txt', 'return str_replace(",", ",", $txt);');
\r
151 for($i = 0; $i < $data_count; $i++){
\r
152 $line = array_map($replaceComma, explode(',', $data[$i])); // 取代 , 為 ,
\r
153 if($line[2]=='0') $line[2] = '1980-01-01 00:00:00'; // 零值
\r
154 $SQL = 'INSERT INTO '.$this->tablename.' (no,resto,root,time,md5chksum,category,tim,ext,imgw,imgh,imgsize,filename,tw,th,pwd,now,name,email,sub,com,host,status) VALUES ('.
\r
158 substr($line[5], 0, 10).',\''.
\r
159 pg_escape_string($line[3]).'\',\''.
\r
160 pg_escape_string($line[4]).'\','.
\r
161 $line[5].',\''.pg_escape_string($line[6]).'\','.
\r
162 $line[7].','.$line[8].',\''.pg_escape_string($line[9]).','.pg_escape_string($line[10]).'\','.$line[11].','.$line[12].',\''.
\r
163 pg_escape_string($line[13]).'\',\''.
\r
164 pg_escape_string($line[14]).'\',\''.
\r
165 pg_escape_string($line[15]).'\',\''.
\r
166 pg_escape_string($line[16]).'\',\''.
\r
167 pg_escape_string($line[17]).'\',\''.
\r
168 pg_escape_string($line[18]).'\',\''.
\r
169 pg_escape_string($line[19]).'\',\''.
\r
170 pg_escape_string($line[20]).'\');';
\r
171 $this->_pgsql_call($SQL, array('Insert a new post failed', __LINE__));
\r
173 $this->dbCommit(); // 送交
\r
178 function dbExport(){
\r
179 if(!$this->prepared) $this->dbPrepare();
\r
180 $line = $this->_pgsql_call('SELECT no,resto,root,md5chksum,category,tim,ext,imgw,imgh,imgsize,filename,tw,th,pwd,now,name,email,sub,com,host,status FROM '.$this->tablename.' ORDER BY no DESC',
\r
181 array('Export posts failed', __LINE__));
\r
183 $replaceComma = create_function('$txt', 'return str_replace(",", ",", $txt);');
\r
184 while($row = pg_fetch_array($line, null, PGSQL_ASSOC)){
\r
185 $row = array_map($replaceComma, $row); // 取代 , 為 ,
\r
186 if($row['root']=='1980-01-01 00:00:00') $row['root'] = '0'; // 初始值設為 0
\r
187 $data .= implode(',', $row).",\r\n";
\r
189 pg_free_result($line);
\r
194 function postCount($resno=0){
\r
195 if(!$this->prepared) $this->dbPrepare();
\r
197 if($resno){ // 回傳討論串總文章數目
\r
198 $line = $this->_pgsql_call('SELECT COUNT(no) FROM '.$this->tablename.' WHERE resto = '.intval($resno),
\r
199 array('Fetch count in thread failed', __LINE__));
\r
200 $countline = pg_fetch_result($line, 0, 0) + 1;
\r
202 $line = $this->_pgsql_call('SELECT COUNT(no) FROM '.$this->tablename, array('Fetch count of posts failed', __LINE__));
\r
203 $countline = pg_fetch_result($line, 0, 0);
\r
205 pg_free_result($line);
\r
210 function threadCount(){
\r
211 if(!$this->prepared) $this->dbPrepare();
\r
213 $tree = $this->_pgsql_call('SELECT COUNT(no) FROM '.$this->tablename.' WHERE resto = 0',
\r
214 array('Fetch count of threads failed', __LINE__));
\r
215 $counttree = pg_fetch_result($tree, 0, 0); pg_free_result($tree); // 計算討論串目前資料筆數
\r
220 function getLastPostNo($state){
\r
221 if(!$this->prepared) $this->dbPrepare();
\r
223 if($state=='afterCommit'){ // 送出後的最後文章編號
\r
224 $tree = $this->_pgsql_call('SELECT MAX(no) FROM '.$this->tablename, array('Get the last No. failed', __LINE__));
\r
225 $lastno = pg_fetch_result($tree, 0, 0); pg_free_result($tree);
\r
227 }else return 0; // 其他狀態沒用
\r
231 function fetchPostList($resno=0, $start=0, $amount=0){
\r
232 if(!$this->prepared) $this->dbPrepare();
\r
235 $resno = intval($resno);
\r
236 if($resno){ // 輸出討論串的結構 (含自己, EX : 1,2,3,4,5,6)
\r
237 $tmpSQL = 'SELECT no FROM '.$this->tablename.' WHERE no = '.$resno.' OR resto = '.$resno.' ORDER BY no';
\r
238 }else{ // 輸出所有文章編號,新的在前
\r
239 $tmpSQL = 'SELECT no FROM '.$this->tablename.' ORDER BY no DESC';
\r
240 $start = intval($start); $amount = intval($amount);
\r
241 if($amount) $tmpSQL .= " LIMIT {$amount} OFFSET {$start}"; // 有指定數量才用 LIMIT
\r
243 $tree = $this->_pgsql_call($tmpSQL, array('Fetch post list failed', __LINE__));
\r
244 while($rows = pg_fetch_array($tree)) $line[] = $rows[0]; // 迴圈
\r
246 pg_free_result($tree);
\r
251 function fetchThreadList($start=0, $amount=0, $isDESC=false){
\r
252 if(!$this->prepared) $this->dbPrepare();
\r
254 $start = intval($start); $amount = intval($amount);
\r
255 $treeline = array();
\r
256 $tmpSQL = 'SELECT no FROM '.$this->tablename.' WHERE resto = 0 ORDER BY '.($isDESC ? 'no' : 'root').' DESC';
\r
257 if($amount) $tmpSQL .= " LIMIT {$amount} OFFSET {$start}"; // 有指定數量才用 LIMIT
\r
258 $tree = $this->_pgsql_call($tmpSQL, array('Fetch thread list failed', __LINE__));
\r
259 while($rows = pg_fetch_array($tree)) $treeline[] = $rows[0]; // 迴圈
\r
261 pg_free_result($tree);
\r
266 function fetchPosts($postlist){
\r
267 if(!$this->prepared) $this->dbPrepare();
\r
269 if(is_array($postlist)){ // 取多串
\r
270 if(!count($postlist)) return array();
\r
271 $pno = implode(', ', $postlist); // ID字串
\r
272 $tmpSQL = 'SELECT * FROM '.$this->tablename.' WHERE no IN ('.$pno.') ORDER BY no';
\r
273 if(count($postlist) > 1){ if($postlist[0] > $postlist[1]) $tmpSQL .= ' DESC'; } // 由大排到小
\r
274 }else $tmpSQL = 'SELECT * FROM '.$this->tablename.' WHERE no = '.intval($postlist); // 取單串
\r
275 $line = $this->_pgsql_call($tmpSQL, array('Fetch the post content failed', __LINE__));
\r
277 return $this->_ArrangeArrayStructure($line); // 輸出陣列結構
\r
280 /* 刪除舊附件 (輸出附件清單) */
\r
281 function delOldAttachments($total_size, $storage_max, $warnOnly=true){
\r
283 if(!$this->prepared) $this->dbPrepare();
\r
285 $arr_warn = $arr_kill = array(); // 警告 / 即將被刪除標記陣列
\r
286 $result = $this->_pgsql_call('SELECT no,ext,tim FROM '.$this->tablename." WHERE ext <> '' ORDER BY no",
\r
287 array('Get the old post failed', __LINE__));
\r
288 while(list($dno, $dext, $dtim) = pg_fetch_array($result)){ // 個別跑舊文迴圈
\r
289 $dfile = $dtim.$dext; // 附加檔案名稱
\r
290 $dthumb = $dtim.'s.jpg'; // 預覽檔案名稱
\r
291 if($FileIO->imageExists($dfile)){ $total_size -= $FileIO->getImageFilesize($dfile) / 1024; $arr_kill[] = $dno; $arr_warn[$dno] = 1; } // 標記刪除
\r
292 if($FileIO->imageExists($dthumb)) $total_size -= $FileIO->getImageFilesize($dthumb) / 1024;
\r
293 if($total_size < $storage_max) break;
\r
295 pg_free_result($result);
\r
296 return $warnOnly ? $arr_warn : $this->removeAttachments($arr_kill);
\r
300 function removePosts($posts){
\r
301 if(!$this->prepared) $this->dbPrepare();
\r
302 if(count($posts)==0) return array();
\r
304 $files = $this->removeAttachments($posts, true); // 先遞迴取得刪除文章及其回應附件清單
\r
305 $pno = implode(', ', $posts); // ID字串
\r
306 $this->_pgsql_call('DELETE FROM '.$this->tablename.' WHERE no IN ('.$pno.') OR resto IN('.$pno.')',
\r
307 array('Delete old posts and replies failed', __LINE__)); // 刪掉文章
\r
311 /* 刪除附件 (輸出附件清單) */
\r
312 function removeAttachments($posts, $recursion=false){
\r
314 if(!$this->prepared) $this->dbPrepare();
\r
315 if(count($posts)==0) return array();
\r
318 $pno = implode(', ', $posts); // ID字串
\r
319 if($recursion) $tmpSQL = 'SELECT ext,tim FROM '.$this->tablename.' WHERE (no IN ('.$pno.') OR resto IN('.$pno.")) AND ext <> ''"; // 遞迴取出 (含回應附件)
\r
320 else $tmpSQL = 'SELECT ext,tim FROM '.$this->tablename.' WHERE no IN ('.$pno.") AND ext <> ''"; // 只有指定的編號
\r
322 $result = $this->_pgsql_call($tmpSQL, array('Get attachments of the post failed', __LINE__));
\r
323 while(list($dext, $dtim) = pg_fetch_array($result)){ // 個別跑迴圈
\r
324 $dfile = $dtim.$dext; // 附加檔案名稱
\r
325 $dthumb = $dtim.'s.jpg'; // 預覽檔案名稱
\r
326 if($FileIO->imageExists($dfile)) $files[] = $dfile;
\r
327 if($FileIO->imageExists($dthumb)) $files[] = $dthumb;
\r
329 pg_free_result($result);
\r
334 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
335 if(!$this->prepared) $this->dbPrepare();
\r
337 $time = (int)substr($tim, 0, -3); // 13位數的數字串是檔名,10位數的才是時間數值
338 $updatetime = gmdate('Y-m-d H:i:s'); // 更動時間 (UTC)
339 $resto = intval($resto);
\r if($resto){ // 新增回應
340 $root = '1980-01-01 00:00:00';
342 $this->_pgsql_call('UPDATE '.$this->tablename.' SET root = \''.$updatetime.'\' WHERE no = '.$resto,
\r array('Push the post failed', __LINE__)); // 將被回應的文章往上移動
344 }else $root = $updatetime; // 新增討論串, 討論串最後被更新時間
346 if($no === 0){ // init post 0
\r
347 $query = 'INSERT INTO '.$this->tablename.' (no,resto,root,time,md5chksum,category,tim,ext,imgw,imgh,imgsize,tw,th,pwd,now,name,email,sub,com,host,status) VALUES ('.
350 $root."',". // 最後更新時間
352 "'$md5chksum',". // 附加檔案md5
353 "'".pg_escape_string($category)."',". // 分類標籤
354 "'$tim', '$ext',". // 附加檔名
355 (int)$imgw.','.(int)$imgh.",'".$imgsize."','".$filename."',".(int)$tw.','.(int)$th.','. // 圖檔長寬及檔案大小;預覽圖長寬
356 "'".pg_escape_string($pwd)."',".
\r
357 "'$now',". // 時間(含ID)字串
358 "'".pg_escape_string($name)."',".
\r
359 "'".pg_escape_string($email)."',".
\r
360 "'".pg_escape_string($sub)."',".
\r
361 "'".pg_escape_string($com)."',".
\r
362 "'".pg_escape_string($host)."', '".pg_escape_string($status)."');";
364 $query = 'INSERT INTO '.$this->tablename.' (resto,root,time,md5chksum,category,tim,ext,imgw,imgh,imgsize,tw,th,pwd,now,name,email,sub,com,host,status) VALUES ('.
\r
366 $root."',". // 最後更新時間
368 "'$md5chksum',". // 附加檔案md5
369 "'".pg_escape_string($category)."',". // 分類標籤
370 "'$tim', '$ext',". // 附加檔名
371 (int)$imgw.','.(int)$imgh.",'".$imgsize."','".$filename."',".(int)$tw.','.(int)$th.','. // 圖檔長寬及檔案大小;預覽圖長寬
372 "'".pg_escape_string($pwd)."',".
\r
373 "'$now',". // 時間(含ID)字串
374 "'".pg_escape_string($name)."',".
\r
375 "'".pg_escape_string($email)."',".
\r
376 "'".pg_escape_string($sub)."',".
\r
377 "'".pg_escape_string($com)."',".
\r
378 "'".pg_escape_string($host)."', '".pg_escape_string($status)."');";
380 $this->_pgsql_call($query, array('Insert a new post failed', __LINE__));
\r
384 function isSuccessivePost($lcount, $com, $timestamp, $pass, $passcookie, $host, $isupload){
\r
386 if(!$this->prepared) $this->dbPrepare();
\r
388 if(!$this->ENV['PERIOD.POST']) return false; // 關閉連續投稿檢查
\r
389 $timestamp = intval($timestamp);
\r
390 $tmpSQL = 'SELECT pwd,host FROM '.$this->tablename.' WHERE time > '.($timestamp - (int)$this->ENV['PERIOD.POST']); // 一般投稿時間檢查
\r
391 if($isupload) $tmpSQL .= ' OR time > '.($timestamp - (int)$this->ENV['PERIOD.IMAGEPOST']); // 附加圖檔的投稿時間檢查 (與下者兩者擇一)
\r
392 else $tmpSQL .= " OR md5(com) = '".md5($com)."'"; // 內文一樣的檢查 (與上者兩者擇一)
\r
394 $result = $this->_pgsql_call($tmpSQL, array('Get the post to check the succession failed', __LINE__));
\r
395 while(list($lpwd, $lhost) = pg_fetch_array($result)){
\r
396 // 判斷為同一人發文且符合連續投稿條件
\r
397 if($host==$lhost || $pass==$lpwd || $passcookie==$lpwd) return true;
\r
403 function isDuplicateAttachment($lcount, $md5hash){
\r
405 if(!$this->prepared) $this->dbPrepare();
\r
407 $result = $this->_pgsql_call('SELECT tim,ext FROM '.$this->tablename." WHERE ext <> '' AND md5chksum = '$md5hash' ORDER BY no DESC",
\r
408 array('Get the post to check the duplicate attachment failed', __LINE__));
\r
409 while(list($ltim, $lext) = pg_fetch_array($result)){
\r
410 if($FileIO->imageExists($ltim.$lext)) return true; // 有相同檔案
\r
416 function isThread($no){
\r
417 if(!$this->prepared) $this->dbPrepare();
\r
419 $result = $this->_pgsql_call('SELECT no FROM '.$this->tablename.' WHERE no = '.intval($no).' AND resto = 0');
\r
420 return pg_fetch_array($result) ? true : false;
\r
424 function searchPost($keyword, $field, $method){
\r
425 if(!$this->prepared) $this->dbPrepare();
\r
427 $keyword_cnt = count($keyword);
\r
428 $SearchQuery = 'SELECT * FROM '.$this->tablename." WHERE {$field} ILIKE '%".pg_escape_string($keyword[0])."%'";
\r
429 if($keyword_cnt > 1){
\r
430 for($i = 1; $i < $keyword_cnt; $i++){
\r
431 $SearchQuery .= " {$method} {$field} ILIKE '%".pg_escape_string($keyword[$i])."%'"; // 多重字串交集 / 聯集搜尋
\r
434 $SearchQuery .= ' ORDER BY no DESC'; // 按照號碼大小排序
\r
435 $line = $this->_pgsql_call($SearchQuery, array('Search the post failed', __LINE__));
\r
436 return $this->_ArrangeArrayStructure($line); // 輸出陣列結構
\r
440 function searchCategory($category){
\r
441 if(!$this->prepared) $this->dbPrepare();
\r
443 $foundPosts = array();
\r
444 $SearchQuery = 'SELECT no FROM '.$this->tablename." WHERE category ~* ',".pg_escape_string($category).",' ORDER BY no DESC";
\r
445 $line = $this->_pgsql_call($SearchQuery, array('Search the category failed', __LINE__));
\r
446 while($rows = pg_fetch_array($line)) $foundPosts[] = $rows[0];
\r
447 pg_free_result($line);
\r
448 return $foundPosts;
\r
452 function getPostStatus($status){
\r
453 return new FlagHelper($status); // 回傳 FlagHelper 物件
\r
457 function updatePost($no, $newValues){
\r
458 if(!$this->prepared) $this->dbPrepare();
\r
461 $chk = array('resto', 'md5chksum', 'category', 'tim', 'ext', 'imgw', 'imgh', 'imgsize', 'filename', 'tw', 'th', 'pwd', 'now', 'name', 'email', 'sub', 'com', 'host', 'status');
\r
462 foreach($chk as $c){
\r
463 if(isset($newValues[$c])){
\r
464 $this->_pgsql_call('UPDATE '.$this->tablename." SET $c = '".pg_escape_string($newValues[$c])."' WHERE no = ".$no,
\r
465 array('Update the field of the post failed', __LINE__)); // 更新討論串屬性
\r
471 function setPostStatus($no, $newStatus){
\r
472 $this->updatePost($no, array('status' => $newStatus));
\r