db = new SQLiteDB('approve', DOKU_PLUGIN . 'approve/db/'); $no_apr_namespaces = $this->getConf('no_apr_namespaces'); $this->no_apr_namespaces_array = array_map(function ($namespace) { return ltrim($namespace, ':'); }, preg_split('/\s+/', $no_apr_namespaces,-1,PREG_SPLIT_NO_EMPTY)); $this->initNoApproveNamespaces(); } public function getDbFile(): string { return $this->db->getDbFile(); } protected function initNoApproveNamespaces(): void { $config_key = 'no_apr_namespaces'; $db_value = $this->getDbConf($config_key); $config_value = $this->getConf('no_apr_namespaces'); if ($db_value !== $config_value) { // $db_value might be null. In this case run the commit anyway. $this->db->getPdo()->beginTransaction(); $this->setDbConf($config_key, $config_value); $pages_meta = $this->getPagesMetadata(); foreach ($pages_meta as $page_meta) { $page_id = $page_meta['page']; $hidden = (int) $this->pageInHiddenNamespace($page_id); $this->setPageHiddenStatus($page_id, $hidden); } $this->db->getPdo()->commit(); } } public function getPagesMetadata(): array { $sql = 'SELECT page, approver, hidden FROM page'; return $this->db->queryAll($sql); } public function getPageMetadata(string $page_id): ?array { $sql = 'SELECT approver FROM page WHERE page=? AND hidden != 1'; return $this->db->queryRecord($sql, $page_id); } public function getDbConf(string $key): ?string { $sql = 'SELECT value FROM config WHERE key=?'; return $this->db->queryValue($sql, $key); } public function setDbConf(string $key, string $value): void { $this->db->saveRecord('config', ['key' => $key, 'value' => $value]); } /** * @param string $page_id * @param int $hidden Must be int since SQLite doesn't suport bool type. * @return void */ public function setPageHiddenStatus(string $page_id, int $hidden): void { $sql = 'UPDATE page SET hidden=? WHERE page=?'; $this->db->query($sql, $hidden, $page_id); } public function updatePagesAssignments(): void { $this->db->getPdo()->beginTransaction(); // clean current settings $this->db->query('DELETE FROM page'); $wikiPages = $this->getWikiPages(); foreach ($wikiPages as $id) { // update revision information $this->updatePage($id); } $this->db->getPdo()->commit(); } public function getWikiPages(): array { global $conf; $datadir = realpath($conf['datadir']); // path without ending "/" $directory = new RecursiveDirectoryIterator($datadir, FilesystemIterator::SKIP_DOTS); $iterator = new RecursiveIteratorIterator($directory); $pages = []; /** @var SplFileInfo $fileinfo */ foreach ($iterator as $fileinfo) { if (!$fileinfo->isFile()) continue; $path = $fileinfo->getRealPath(); // it should return "/" both on windows and linux // remove dir part $path = substr($path, strlen($datadir)); // make file a dokuwiki path $id = pathID($path); $pages[] = $id; } return $pages; } public function weightedAssignments(): array { $assignments = $this->db->queryAll('SELECT id, namespace, approver FROM maintainer'); $weighted_assignments = []; foreach ($assignments as $assignment) { $ns = $assignment['namespace']; // more general namespaces are overridden by more specific ones. if (substr($ns, -1) == '*') { $weight = substr_count($ns, ':'); } else { $weight = PHP_INT_MAX; } $assignment['weight'] = $weight; $weighted_assignments[] = $assignment; } array_multisort(array_column($weighted_assignments, 'weight'), $weighted_assignments); return $weighted_assignments; } /** * Returns approver or null if page is not in $weighted_assignments. * Approver can be empty string. * * @param string $page_id * @param array $weighted_assignments * @return string */ public function getPageAssignment(string $page_id, array $weighted_assignments): ?string { $page_approver = null; foreach ($weighted_assignments as $assignment) { $ns = ltrim($assignment['namespace'], ':'); $approver = $assignment['approver']; if (substr($ns, -2) == '**') { //remove '**' $ns = substr($ns, 0, -2); if (substr($page_id, 0, strlen($ns)) == $ns) { $page_approver = $approver; } } elseif (substr($ns, -1) == '*') { //remove '*' $ns = substr($ns, 0, -1); $noNS = substr($page_id, strlen($ns)); if (strpos($noNS, ':') === FALSE && substr($page_id, 0, strlen($ns)) == $ns) { $page_approver = $approver; } } elseif($page_id == $ns) { $page_approver = $approver; } } return $page_approver; } public function pageInHiddenNamespace(string $page_id): bool { $page_id = ltrim($page_id, ':'); foreach ($this->no_apr_namespaces_array as $namespace) { if (substr($page_id, 0, strlen($namespace)) == $namespace) { return true; } } return false; } public function getPages(string $user='', array $states=['approved', 'draft', 'ready_for_approval'], string $namespace='', string $filter=''): array { /* @var AuthPlugin $auth */ global $auth; $sql = 'SELECT page.page AS id, page.approver, revision.rev, revision.approved, revision.approved_by, revision.ready_for_approval, revision.ready_for_approval_by, LENGTH(page.page) - LENGTH(REPLACE(page.page, \':\', \'\')) AS colons FROM page INNER JOIN revision ON page.page = revision.page WHERE page.hidden = 0 AND revision.current=1 AND page.page GLOB ? AND page.page REGEXP ? ORDER BY colons, page.page'; $pages = $this->db->queryAll($sql, $namespace.'*', $filter); // add status to the page $pages = array_map([$this, 'setPageStatus'], $pages); if ($user !== '') { $user_data = $auth->getUserData($user); $user_groups = $user_data['grps']; $pages = array_filter($pages, function ($page) use ($user, $user_groups) { return $page['approver'][0] == '@' && in_array(substr($page['approver'], 1), $user_groups) || $page['approver'] == $user; }); } // filter by status $pages = array_filter($pages, function ($page) use ($states) { return in_array($page['status'], $states); }); return $pages; } public function getPageRevisions(string $page_id): array { $sql = 'SELECT page AS id, rev, approved, approved_by, ready_for_approval, ready_for_approval_by FROM revision WHERE page=?'; $revisions = $this->db->queryAll($sql, $page_id); // add status to the page $revisions = array_map([$this, 'setPageStatus'], $revisions); return $revisions; } public function getPageRevision(string $page_id, int $rev): ?array { $sql = 'SELECT ready_for_approval, ready_for_approval_by, approved, approved_by, version FROM revision WHERE page=? AND rev=?'; $page = $this->db->queryRecord($sql, $page_id, $rev); if ($page == null) { $page = [ 'ready_for_approval' => null, 'ready_for_approval_by' => null, 'approved' => null, 'approved_by' => null ]; } $page['id'] = $page_id; $page['rev'] = $rev; $page = $this->setPageStatus($page); return $page; } protected function setPageStatus(array $page): array { if ($page['approved'] !== null) { $page['status'] = 'approved'; } elseif ($page['ready_for_approval'] !== null) { $page['status'] = 'ready_for_approval'; } else { $page['status'] = 'draft'; } return $page; } public function moveRevisionHistory(string $old_page_id, string $new_page_id): void { $this->db->exec('UPDATE revision SET page=? WHERE page=?', $new_page_id, $old_page_id); } public function getLastDbRev(string $page_id, ?string $status=null): ?int { if ($status == 'approved') { $sql = 'SELECT rev FROM revision WHERE page=? AND approved IS NOT NULL ORDER BY rev DESC LIMIT 1'; return $this->db->queryValue($sql, $page_id); } elseif ($status == 'ready_for_approval') { $sql = 'SELECT rev FROM revision WHERE page=? AND ready_for_approval IS NOT NULL ORDER BY rev DESC LIMIT 1'; return $this->db->queryValue($sql, $page_id); } $sql = 'SELECT rev FROM revision WHERE page=? AND current=1'; return $this->db->queryValue($sql, $page_id); } public function setApprovedStatus(string $page_id): void { global $INFO; // approved IS NULL prevents from overriding already approved page $sql = 'UPDATE revision SET approved=?, approved_by=?, version=(SELECT IFNULL(MAX(version), 0) FROM revision WHERE page=?) + 1 WHERE page=? AND current=1 AND approved IS NULL'; $this->db->exec($sql, date('c'), $INFO['client'], $page_id, $page_id); } public function setReadyForApprovalStatus(string $page_id): void { global $INFO; // approved IS NULL prevents from overriding already approved page $sql = 'UPDATE revision SET ready_for_approval=?, ready_for_approval_by=? WHERE page=? AND current=1 AND ready_for_approval IS NULL'; $this->db->exec($sql, date('c'), $INFO['client'], $page_id); } protected function deletePage($page_id): void { // delete information about availability of a page but keep the history $this->db->exec('DELETE FROM page WHERE page=?', $page_id); $this->db->exec('DELETE FROM revision WHERE page=? AND approved IS NULL AND ready_for_approval IS NULL' , $page_id); $this->db->exec('UPDATE revision SET current=0 WHERE page=? AND current=1', $page_id); } public function handlePageDelete(string $page_id): void { $this->db->getPdo()->beginTransaction(); $this->deletePage($page_id); $this->db->getPdo()->commit(); } protected function updatePage(string $page_id): void { // delete all unimportant revisions $this->db->exec('DELETE FROM revision WHERE page=? AND approved IS NULL AND ready_for_approval IS NULL' , $page_id); $weighted_assignments = $this->weightedAssignments(); $approver = $this->getPageAssignment($page_id, $weighted_assignments); if ($approver !== null) { $data = [ 'page' => $page_id, 'hidden' => (int) $this->pageInHiddenNamespace($page_id), 'approver' => $approver ]; $this->db->saveRecord('page', $data); // insert or replace } $last_change_date = @filemtime(wikiFN($page_id)); // record for current revision exists $sql = 'SELECT 1 FROM revision WHERE page=? AND rev=?'; $exists = $this->db->queryValue($sql, $page_id, $last_change_date); if ($exists === null) { // mark previous revision as old. this may be already deleted by DELETE $this->db->exec('UPDATE revision SET current=0 WHERE page=? AND current=1', $page_id); // create new record $this->db->saveRecord('revision', [ 'page' => $page_id, 'rev' => $last_change_date, 'current' => 1 ]); } } public function handlePageEdit(string $page_id): void { $this->db->getPdo()->beginTransaction(); $this->updatePage($page_id); $this->db->getPdo()->commit(); } public function deleteMaintainer(int $maintainer_id): void { $this->db->getPdo()->beginTransaction(); $this->db->exec('DELETE FROM maintainer WHERE id=?', $maintainer_id); $this->db->getPdo()->commit(); } public function addMaintainer(string $namespace, string $approver): void { $this->db->getPdo()->beginTransaction(); $this->db->saveRecord('maintainer', [ 'namespace' => $namespace, 'approver' => $approver ]); $this->db->getPdo()->commit(); } public function getMaintainers(): ?array { $sql = 'SELECT id, namespace, approver FROM maintainer ORDER BY namespace'; return $this->db->queryAll($sql); } }