'MyFunction', # MyFunction(&$page) returns a string (query) to be executed # if k<100: before content/history, could change the $page array # if k>100: after content/history SDVA($HandleActions, array('vacuum'=>'HandleVacuum', 'undelete'=>'HandleUndelete')); SDVA($HandleAuth, array('vacuum' => 'admin', 'undelete' => 'admin')); SDV($RedirectGroup, ""); # disabled // SDVA($SearchPatterns['all'], array('deleted'=>"!-deleted-\\d{9,}$!")); SDVA($SearchPatterns['normal'], array('deleted'=>"!-deleted-\\d{9,}$!")); SDVA($SearchPatterns['default'], array('deleted'=>"!-deleted-\\d{9,}$!")); SDVA($SearchPatterns['deleted'], array('deleted'=>"/-deleted-\\d{9,}$/")); if(function_exists('sqlite_escape_string')) # deprecated since PHP 5.4, TODO rewrite all with prepared statements SDVA($PageListFilters, array('PageListDiffSearch' => 110)); SDV($HandleDiffFmt,array(&$PageStartFmt, &$PageDiffFmt,"
", 'function:PrintDiffTrail', 'function:PrintDiff', 'function:PrintDiffTrail', '
', &$PageEndFmt)); SDV($DiffCountPerPage, 10); SDV($DiffKeepDays,3650); $FmtPV['$oFullName'] = 'preg_replace($GLOBALS["SearchPatterns"][\'deleted\'][\'deleted\'], "", "$group.$name")'; $FmtPV['$LastModifiedMajor'] = 'strftime($GLOBALS["TimeFmt"], ($page["timemajor"]? $page["timemajor"]: $page["time"]))'; $FmtPV['$page_id'] = '@$page["page_id"]'; $FmtPV['$ShortPage'] = 'shortpage($page["name"], @$page["page_id"])'; $FmtPV['$ShortURL'] = 'shortpage($page["name"], @$page["page_id"], 1)'; $FmtPV['$ShortName'] = 'shortpage($page["name"], @$page["page_id"], 2)'; $FmtPV['$RevMatches'] = '$GLOBALS["DiffSearchMatches"]["$group.$name"]'; SDV($SQLiteCreateQuery,"BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS pages ( page_id integer NOT NULL PRIMARY KEY, name varchar(128) NOT NULL UNIQUE, version varchar(128) NOT NULL default '', agent varchar(255) NOT NULL default '', author varchar(128) NOT NULL default '', charset varchar(40) NOT NULL default '', csum varchar(255) NOT NULL default '', bytes integer NOT NULL default 0, ctime integer NOT NULL default 0, time integer NOT NULL default 0, timemajor integer NOT NULL default 0, host varchar(128) NOT NULL default '', rev integer NOT NULL default 0, targets text NOT NULL default '', passwdattr varchar(255) NOT NULL default '', passwdedit varchar(255) NOT NULL default '', passwdread varchar(255) NOT NULL default '', passwdupload varchar(255) NOT NULL default '', passwdpublish varchar(255) NOT NULL default '', title varchar(255) NOT NULL default '', keywords varchar(255) NOT NULL default '', description varchar(255) NOT NULL default '', text text NOT NULL default '', addattrib text NOT NULL default '' ); CREATE INDEX IF NOT EXISTS ix_pages_fullname ON pages(name); CREATE INDEX IF NOT EXISTS ix_pages_time ON pages(time); CREATE TABLE IF NOT EXISTS revisions ( revision_id integer NOT NULL PRIMARY KEY, fullname varchar(128) NOT NULL default '', currtime integer NOT NULL default 0, prevtime integer NOT NULL default 0, author varchar(128) NOT NULL default '', host varchar(128) NOT NULL default '', csum varchar(255) NOT NULL default '', minor tinyint NOT NULL default 0, bytes integer NOT NULL default 0, deltabytes integer NOT NULL default 0, diff text NOT NULL default '' ); CREATE INDEX IF NOT EXISTS ix_revisions_fullname ON revisions(fullname); CREATE INDEX IF NOT EXISTS ix_revisions_time ON revisions(currtime); CREATE TABLE IF NOT EXISTS wikilinks ( link_id integer NOT NULL PRIMARY KEY, fullname varchar(128) NOT NULL default '', target varchar(128) NOT NULL default '' ); CREATE UNIQUE INDEX IF NOT EXISTS ix_wikilinks ON wikilinks(fullname,target); CREATE TABLE IF NOT EXISTS PTVs ( ptv_id integer NOT NULL PRIMARY KEY, fullname varchar(128) NOT NULL default '', ptv_name varchar(128) NOT NULL default '', ptv_value text NOT NULL default '' ); CREATE UNIQUE INDEX IF NOT EXISTS ix_ptvs ON PTVs(fullname,ptv_name); COMMIT;"); ## class PageStore holds objects that store pages via the native ## filesystem. PageStoreSQLite replaces all functions in order ## to use an SQLite database instead. class PageStoreSQLite extends PageStore { var $DB; var $type; function __construct($dbf='$WorkDir/pmwiki.sqlite.db', $w=0) { global $SQLiteCreateQuery, $pagename; $this->iswrite = $w; $this->type = 'SQLite'; $dbf = FmtPageName($dbf, $pagename); mkdirp(dirname($dbf)); $exists = file_exists($dbf); $this->DB = new PDO("sqlite:$dbf"); if(! $exists)$this->ex($SQLiteCreateQuery); $GLOBALS['PageExistsCache'] = array(); } function read($pagename, $since=0, $uksort=0) { global $SQLiteTrimHistoryActions, $action, $RedirectGroup, $DiffCountPerPage, $EnableDrafts, $WikiDir; if(! $WikiDir->iswrite) $WikiDir->iswrite = 1; $this->fixpagename($pagename); if(preg_match(@"/^$RedirectGroup\\.(.*)$/", $pagename, $m)) { $id = base_convert($m[1], 36, 10); $page = $this->q1("SELECT name FROM pages WHERE page_id='$id' LIMIT 1"); if(!$page) return null; $page['text'] = "(:sqliteredirect {$page['name']}:)"; return $page; } // else $page = $this->q1("SELECT * FROM pages WHERE name='$pagename' LIMIT 1"); if(!$page) return null; if($page['addattrib']>'') { $aatt = unserialize($page['addattrib']); foreach($aatt as $k=>$v) $page[$k] = $v; } unset($page['addattrib']); if(!$this->iswrite) unset($page['page_id']); if(($page['time']>$since && !preg_match($SQLiteTrimHistoryActions, $action)) || @$_REQUEST['restore'] || @$EnableDrafts ) { $hideminor = @$_GET['minor']=='n' ? ' AND minor=0':''; $diffpage = intval(@$_GET['diffpage']); $sqlsince = $limit = ''; if($since || @$_REQUEST['restore'] || $diffpage<0)$sqlsince = " AND currtime>=$since"; else { $start = $DiffCountPerPage * $diffpage; $limit = "LIMIT $DiffCountPerPage OFFSET $start"; } $q = "SELECT * FROM revisions WHERE ( fullname='$pagename'$hideminor$sqlsince ) ORDER BY currtime DESC $limit;"; $r = $this->q9( $q, PDO::FETCH_ASSOC ); foreach($r as $row) { $page["author:{$row['currtime']}"] = $row['author']; $page["csum:{$row['currtime']}"] = $row['csum']; if($row['minor']) $page["diff:{$row['currtime']}:{$row['prevtime']}:minor"] = $row['diff']; else $page["diff:{$row['currtime']}:{$row['prevtime']}"] = $row['diff']; $page["host:{$row['currtime']}"] = $row['host']; } } if($uksort) uksort($page, 'CmpPageAttr'); return $page; } function write($pagename,$page) { global $Now, $Version, $DiffKeepDays, $SQLiteWriteFunctions,$RedirectGroup, $EnableSQLiteEmptyDiffs, $SQLiteAutoVacuum; $this->fixpagename($pagename); if(preg_match(@"/^$RedirectGroup\\.(.*)$/", $pagename, $m)) { Abort("$[?Editing pages in \$RedirectGroup is disabled.]"); } $prevtime = $page['time']; $page["bytes:$Now"] = $page["bytes"] = strlen($page['text']); $page["deltabytes:$Now"] = $page["bytes:$Now"] - intval(@$page['bytes']); $page['name'] = $pagename; $page['time'] = $Now; if(@$_REQUEST['diffclass']!='minor')$page['timemajor'] = $Now; $page['host'] = @$_SERVER['REMOTE_ADDR']; $page['agent'] = @$_SERVER['HTTP_USER_AGENT']; $page['rev'] = @$page['rev']+1; $page['version'] = $Version; $fieldscontent=explode(" ", 'name version agent author charset csum ctime time timemajor host rev targets passwdattr passwdedit passwdupload passwdread passwdpublish title keywords description text bytes addattrib'); $fieldshistory=explode(" ", "prevtime author host csum bytes deltabytes minor diff"); # Additional page attributes, added eventually by recipes $aatt = array(); foreach($page as $k=>$v) { if($k=='' || $k[0]=='=' || in_array($k, $fieldscontent ) || in_array($k, array('page_id') ) || preg_match('/^\\w+:\\d+/', $k) ) continue; $aatt[$k] =$v; } if(count($aatt))$page['addattrib'] = serialize($aatt); $q = "BEGIN TRANSACTION;\n"; if(count(@$SQLiteWriteFunctions) ) { ksort($SQLiteWriteFunctions); foreach($SQLiteWriteFunctions as $k=>$f) { if($k>100 || !function_exists($f)) continue; $q .= $f($page) . "\n"; } } # Drafts preserve 'page_id' but alter 'name' $current_id = $this->exists($page['name']); # Save page content if($current_id) { # page existed in database $qq = array(); foreach($fieldscontent as $field) { $qq[] = " $field=".$this->DB->quote(@$page[$field]); } $q .= "UPDATE pages SET "; $q .= implode(",\n ", $qq); $q .= " WHERE page_id=$current_id;\n"; } else { # either new page, or imported from wiki.d/wikilib.d $ff = $vv = array(); foreach($fieldscontent as $field) { if(isset($page[$field])) { $ff[] = $field; $vv[] = $this->DB->quote($page[$field]); } } $q .= "INSERT INTO pages (".implode(",\n ", $ff).") VALUES (".implode(",\n ", $vv).");\n"; } # get last history timestamp $qhist = "SELECT max(currtime) as max FROM revisions WHERE (fullname='$pagename');"; $maxh = intval($this->q1( $qhist, 'max' )); # Save page history $drophist = $Now-$DiffKeepDays*86400; $allhist = array(); foreach($page as $k=>$v) { if($k=='' || $k[0]=='=') continue; list($key, $currtime, $prevtime, $minor) = explode(":", $k); if($currtime<=$maxh || $currtime<$drophist) continue; # if page was in database, store only latest diff, else import all history if( !$current_id || $Now==$currtime) { $allhist[$currtime][$key] = $v; if(@$prevtime) $allhist[$currtime]['prevtime'] = $prevtime; if(@$minor) $allhist[$currtime]['minor'] = 1; } } if(count($allhist)) { foreach($allhist as $t=>$a) { if(!isset($a['diff']) or ($EnableSQLiteEmptyDiffs==0 and $a['diff']=='')) continue; $ff = $vv = array(); foreach($fieldshistory as $field) { if(isset($a[$field])) { $ff[] = $field; $vv[] = $this->DB->quote($a[$field]); } } $q .= "INSERT INTO revisions (fullname, currtime, ".implode(', ', $ff).") VALUES ('$pagename', $t, ".implode(', ', $vv).");\n"; } } # drop history older than $DiffKeepDays $q .= "DELETE FROM revisions WHERE fullname='$pagename' AND currtime<$drophist;\n"; if(count(@$SQLiteWriteFunctions) ) { foreach((array)$SQLiteWriteFunctions as $k=>$f) { if($k<=100 || !function_exists($f) ) continue; $q .= $f($page) . "\n"; } } # increment edits counter if($SQLiteAutoVacuum>0) { $x = $this->q1("SELECT ptv_value FROM PTVs WHERE fullname='.SQLITE' AND ptv_name='editcount' LIMIT 1;\n", 'ptv_value'); if(! $x) $q.= "INSERT INTO PTVs (ptv_id, fullname, ptv_name, ptv_value) VALUES (1, '.SQLITE', 'editcount', 1);\n"; else $q .= "UPDATE PTVs SET ptv_value='".($x+1)."' WHERE fullname='.SQLITE' AND ptv_name='editcount';\n"; } $q .= "COMMIT;\n"; $this->ex($q) ; if($x % $SQLiteAutoVacuum == 0) { $this->ex("VACUUM;");} PCache($pagename, $page); } function ls($pats=NULL) { global $GroupPattern, $NamePattern; StopWatch("PageStore::ls begin SQLite"); $pats=(array)$pats; array_push($pats, "/^$GroupPattern\\.$NamePattern$/"); $o = $this->q9("SELECT name FROM pages WHERE 1;"); StopWatch("PageStore::ls merge SQLite"); $out = MatchPageNames($o, $pats); StopWatch("PageStore::ls end SQLite"); return $out; } function exists($pagename) { global $RedirectGroup; if (!$pagename) return false; $this->fixpagename($pagename); if(preg_match("/^$RedirectGroup\\.(.*)$/", $pagename, $m) ) { $id = base_convert($m[1], 36, 10); $page = $this->q1("SELECT name FROM pages WHERE page_id='$id' LIMIT 1;"); if(!$page) return null; return true; } return $this->q1("SELECT page_id FROM pages WHERE name=".$this->DB->quote($pagename). " LIMIT 1;", 'page_id'); } function delete($pagename) { global $Now, $SearchPatterns, $Author, $ChangeSummary; $this->fixpagename($pagename); if(preg_match($SearchPatterns['deleted']['deleted'], $pagename)) { //REALLY DELETE PAGE AND DIFFS $q = "BEGIN TRANSACTION; DELETE FROM pages WHERE name=".$this->DB->quote($pagename)."; DELETE FROM revisions WHERE fullname=".$this->DB->quote($pagename)."; COMMIT;"; $this->ex($q); } else { // Move to Group.Name-deleted-123456stamp $this->sqlrename($pagename, "$pagename-deleted-$Now", "[".XL('Page deleted')."] $ChangeSummary", 0, 1); } } function sqlrename($oldname, $newname, $csum='', $redirect=1, $lock=0) { global $Now, $SearchPatterns, $Author, $SQLiteDeletedPagesAttr; $attr = ($lock==0 || $SQLiteDeletedPagesAttr=='') ? '': ", passwdattr='$SQLiteDeletedPagesAttr', passwdedit='$SQLiteDeletedPagesAttr', passwdread='$SQLiteDeletedPagesAttr', passwdpublish='$SQLiteDeletedPagesAttr', passwdupload='$SQLiteDeletedPagesAttr'"; $q = "BEGIN TRANSACTION; UPDATE pages SET name='$newname'$attr WHERE name='$oldname';\n"; # Log deleting author/ip/csum $q .= "INSERT INTO revisions (fullname, currtime, prevtime, author, host, csum) VALUES ('$oldname', $Now, $Now, ".$this->DB->quote(@$Author) .", ".$this->DB->quote(@$_SERVER['REMOTE_ADDR']) .", ".$this->DB->quote($csum).");\n"; $q .= "UPDATE revisions SET fullname='$newname' WHERE fullname='$oldname';\n"; $q .= "COMMIT;\n"; $this->ex($q); if($redirect) { $this->write($oldname, array('text'=>"(:redirect $newname:)")); } } function undelete($pagename, $publicname) { $this->sqlrename($pagename, $publicname, XL("Page undeleted"), 0, 0); } function ex($q) { # execute return $this->DB->exec($q); } function q1($q, $key=null, $fetch=PDO::FETCH_ASSOC) { # fetch 1 record or value $x = $this->DB->query($q)->fetch($fetch); return ($key) ? $x[$key] : $x; } function q9($q, $fetch=PDO::FETCH_COLUMN) { # fetch many return $this->DB->query($q)->fetchAll($fetch); } function fixpagename(&$pn) { $pn = str_replace(array('/', "'", '"'), array('.', '', ''), MakePageName($pn, $pn)); } } function HandleUndelete($pagename, $auth='admin') { global $WikiLibDirs,$WikiDir, $SearchPatterns, $Author, $ChangeSummary; if(! (boolean)RetrieveAuthPage($pagename, $auth, true, READPAGE_CURRENT) ) exit; $publicname = preg_replace($SearchPatterns['deleted']['deleted'], '', $pagename); if($publicname == $pagename) { Abort("? [[$publicname]] : $[Pagename not recognized].\n"); exit; } else { $WikiDir->iswrite = 1; $done = 0; for($i=0; $itype=='SQLite' && $wd->iswrite && $wd->exists($pagename) ) { if($wd->exists($publicname)) { Abort("? [[$publicname]] : $[Page exists. You need to either delete it first, or merge the content manually.]"); exit; } else { $wd->undelete($pagename, $publicname); Redirect("$publicname?action=attr"); exit; } } } Abort("Not done! Page $pagename doesn't exist in any writable SQLite database.\n"); } } function HandleVacuum($pagename, $auth='admin') { global $WikiLibDirs,$WikiDir,$PageStartFmt, $PageEndFmt; if(! (boolean)RetrieveAuthPage($pagename, $auth, true, READPAGE_CURRENT) ) exit; PrintFmt($pagename,$HTMLStartFmt); echo "Optimizing PmWiki SQLite database... "; $WikiDir->iswrite = 1; $done = 0; for($i=0; $itype=='SQLite' && $wd->iswrite) { $wd->ex("VACUUM;"); $done++; } } if($done>0) echo "Done! (Optimized $done databases.)\n"; else echo "Not done! No writable SQLite databases found.\n"; PrintFmt($pagename,$HTMLEndFmt); } function shortpage($pagename, $page_id=0, $url=0) { global $RedirectGroup, $EnablePathInfo, $ScriptUrl; $id = intval($page_id); if($id>0 && $RedirectGroup>'') $p= MakePageName($pagename,"$RedirectGroup.".base_convert($id,10,36)); else $p = $pagename; if($url==0) return $p; elseif($url==2) return preg_replace("/^.*\\./", '', $p); return PUE(($EnablePathInfo) ? "$ScriptUrl/". str_replace(".", "/", $p) : "$ScriptUrl?n=$p"); } function PrintDiffTrail($pagename) { global $DiffCountPerPage, $WikiLibDirs, $DiffShow; static $ok=0; static $out=''; if($ok){ if($out>'') PrintFmt($pagename,$out); return; } $ok=1; for($i=0; $itype=='SQLite') break; } $minor = $DiffShow['minor'];# @$_GET['minor']=='n'? 'n':'y'; $source = $DiffShow['source'];# @$_GET['source']=='y'? 'y':'n'; $hideminor = $minor=='y' ?'' : ' AND minor=0'; $current = intval(@$_GET['diffpage']); $tmp = $wd->q1("SELECT COUNT() as n FROM revisions WHERE fullname='$pagename'$hideminor", 'n'); $pages = ceil($tmp/$DiffCountPerPage); if($pages<=1) return; $out = "markup:$[History page] : "; $pp = array(); for($i=0; $i<$pages; $i++) { $j = $i+1; if($current == $i) $pp[] = "'''$j'''"; else $pp[] = "[[$pagename?action=diff&source=$source&minor=$minor&diffpage=$i | $j]]"; } if($current>=0) $pp[] = "[[$pagename?action=diff&source=$source&minor=$minor&diffpage=-1 | $[Show all] ]]"; $out .= implode(" | ", $pp); PrintFmt($pagename,$out); } function PageListDiffSearch(&$list, &$opt, $pn, &$page) { global $DiffSearchMatches, $Now, $WikiLibDirs; $ors = $ands = $DiffSearchMatches = array(); if(@$opt['ip'].@$opt['user'].@$opt['days']=='')return; if(@$opt['ip']){ $ors[]=SQLiteMultiSelect('host', $opt['ip']); } if(@$opt['user']){ $ors[]=SQLiteMultiSelect('author', $opt['user']); } $or = count($ors)? "(". implode(" OR ", $ors).")" : ''; if(@$opt['group']){ $ands[]=SQLiteMultiSelect('fullname', preg_replace('/(,|$)/','.*$1',$opt['group'])); } return; if(@$opt['days']) $ands[]="currtime>=".($Now - floatval($opt['days'])*86400 ); $and = count($ands)? "(". implode(" AND ", $ands).")" : ''; if("$and$or"==''){return;} if($or && $and) {$and .= ' AND ';} $q = "SELECT fullname FROM revisions WHERE ($and $or);"; for($i=0; $itype=='SQLite') break; } $tmp = $wd->q9($q); foreach($tmp as $v) { $DiffSearchMatches[$v]++; } $matches = array_keys($DiffSearchMatches); $list = array_intersect($matches, $list); return 0; } function SQLiteMultiSelect($colname, $values) { if($values=='') return; $values = str_replace(array('*', '?'), array('%', '_'), $values); $val = explode(',', $values); $ors=$excl=array();$or=$ex=''; return; foreach((array)$val as $v) { if($v[0]!='-') $ors[] = "`$colname` LIKE '".sqlite_escape_string($v)."'"; else $excl[] = "`$colname` NOT LIKE '".sqlite_escape_string(substr($v,1))."'"; } if(count($ors))$or = "(".implode(' OR ', $ors).")"; if(count($excl))$ex = "(".implode(' AND ', $excl).")"; if($or && $ex) $ex .= " AND "; return "$ex$or"; } function SQLiteRedirect($m) { list($markup, $pagename) = $m; global $IncludeHappened; if(@$IncludeHappened==1) return Keep($markup); header("HTTP/1.0 301 Moved Permanently"); Redirect($pagename); exit(); } function SQLiteIncludeHappened() { $GLOBALS['IncludeHappened']=1; } ## (:sqliteredirect:) Markup('include-happened', '