1*9bded093SAnna Dabrowska<?php 2*9bded093SAnna Dabrowska 3*9bded093SAnna Dabrowskause dokuwiki\Extension\CLIPlugin; 4*9bded093SAnna Dabrowskause splitbrain\phpcli\Options; 5*9bded093SAnna Dabrowska 6*9bded093SAnna Dabrowska/** 7*9bded093SAnna Dabrowska * DokuWiki Plugin acknowledge (CLI Component) 8*9bded093SAnna Dabrowska * 9*9bded093SAnna Dabrowska * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10*9bded093SAnna Dabrowska * @author Anna Dabrowska <dokuwiki@cosmocode.de> 11*9bded093SAnna Dabrowska */ 12*9bded093SAnna Dabrowskaclass cli_plugin_acknowledge extends CLIPlugin 13*9bded093SAnna Dabrowska{ 14*9bded093SAnna Dabrowska /** @var helper_plugin_acknowledge */ 15*9bded093SAnna Dabrowska protected $helper; 16*9bded093SAnna Dabrowska 17*9bded093SAnna Dabrowska /** 18*9bded093SAnna Dabrowska * Initialize helper 19*9bded093SAnna Dabrowska */ 20*9bded093SAnna Dabrowska public function __construct() 21*9bded093SAnna Dabrowska { 22*9bded093SAnna Dabrowska parent::__construct(); 23*9bded093SAnna Dabrowska $this->helper = plugin_load('helper', 'acknowledge'); 24*9bded093SAnna Dabrowska } 25*9bded093SAnna Dabrowska 26*9bded093SAnna Dabrowska /** 27*9bded093SAnna Dabrowska * @inheritDoc 28*9bded093SAnna Dabrowska */ 29*9bded093SAnna Dabrowska protected function setup(Options $options) 30*9bded093SAnna Dabrowska { 31*9bded093SAnna Dabrowska $options->setHelp('Maintenance and import tools for the acknowledge plugin'); 32*9bded093SAnna Dabrowska 33*9bded093SAnna Dabrowska $options->registerCommand( 34*9bded093SAnna Dabrowska 'import-ireadit', 35*9bded093SAnna Dabrowska 'Import read records and ~~IREADIT~~ assignments from the ireadit plugin' 36*9bded093SAnna Dabrowska ); 37*9bded093SAnna Dabrowska $options->registerOption( 38*9bded093SAnna Dabrowska 'dry-run', 39*9bded093SAnna Dabrowska 'Only report what would be imported, without writing anything', 40*9bded093SAnna Dabrowska 'd', 41*9bded093SAnna Dabrowska false, 42*9bded093SAnna Dabrowska 'import-ireadit' 43*9bded093SAnna Dabrowska ); 44*9bded093SAnna Dabrowska } 45*9bded093SAnna Dabrowska 46*9bded093SAnna Dabrowska /** 47*9bded093SAnna Dabrowska * @inheritDoc 48*9bded093SAnna Dabrowska */ 49*9bded093SAnna Dabrowska protected function main(Options $options) 50*9bded093SAnna Dabrowska { 51*9bded093SAnna Dabrowska $cmd = $options->getCmd(); 52*9bded093SAnna Dabrowska switch ($cmd) { 53*9bded093SAnna Dabrowska case 'import-ireadit': 54*9bded093SAnna Dabrowska try { 55*9bded093SAnna Dabrowska $this->importIreadit((bool)$options->getOpt('dry-run')); 56*9bded093SAnna Dabrowska } catch (\Exception $e) { 57*9bded093SAnna Dabrowska $this->fatal($e); 58*9bded093SAnna Dabrowska } 59*9bded093SAnna Dabrowska break; 60*9bded093SAnna Dabrowska default: 61*9bded093SAnna Dabrowska $this->error('No command provided'); 62*9bded093SAnna Dabrowska echo $options->help(); 63*9bded093SAnna Dabrowska exit(1); 64*9bded093SAnna Dabrowska } 65*9bded093SAnna Dabrowska } 66*9bded093SAnna Dabrowska 67*9bded093SAnna Dabrowska /** 68*9bded093SAnna Dabrowska * Import acknowledgements and assignments from the ireadit plugin 69*9bded093SAnna Dabrowska * 70*9bded093SAnna Dabrowska * @param bool $dryRun whether to only report instead of writing 71*9bded093SAnna Dabrowska * @return void 72*9bded093SAnna Dabrowska * @throws Exception 73*9bded093SAnna Dabrowska */ 74*9bded093SAnna Dabrowska protected function importIreadit(bool $dryRun): void 75*9bded093SAnna Dabrowska { 76*9bded093SAnna Dabrowska /** @var helper_plugin_ireadit_db $ireaditDb */ 77*9bded093SAnna Dabrowska $ireaditDb = plugin_load('helper', 'ireadit_db'); 78*9bded093SAnna Dabrowska if ($ireaditDb === null) { 79*9bded093SAnna Dabrowska throw new \RuntimeException( 80*9bded093SAnna Dabrowska 'The ireaditDb plugin is required but could not be loaded.' 81*9bded093SAnna Dabrowska ); 82*9bded093SAnna Dabrowska } 83*9bded093SAnna Dabrowska 84*9bded093SAnna Dabrowska if ($dryRun) { 85*9bded093SAnna Dabrowska $this->notice('Running in dry-run mode, no changes will be written'); 86*9bded093SAnna Dabrowska } 87*9bded093SAnna Dabrowska 88*9bded093SAnna Dabrowska // make sure the pages table is populated 89*9bded093SAnna Dabrowska if (!$dryRun) { 90*9bded093SAnna Dabrowska $this->helper->updatePageIndex(); 91*9bded093SAnna Dabrowska } 92*9bded093SAnna Dabrowska 93*9bded093SAnna Dabrowska [$assignmentCount, $patternCount] = $this->importAssignments($dryRun); 94*9bded093SAnna Dabrowska $acksCount = $this->importIreaditRecords($ireaditDb, $dryRun); 95*9bded093SAnna Dabrowska 96*9bded093SAnna Dabrowska $this->success(sprintf( 97*9bded093SAnna Dabrowska 'Done: imported %d ireadit record(s), %d assignment pattern(s) (%d assignee entries)', 98*9bded093SAnna Dabrowska $acksCount, 99*9bded093SAnna Dabrowska $patternCount, 100*9bded093SAnna Dabrowska $assignmentCount 101*9bded093SAnna Dabrowska )); 102*9bded093SAnna Dabrowska } 103*9bded093SAnna Dabrowska 104*9bded093SAnna Dabrowska /** 105*9bded093SAnna Dabrowska * Import ireadit records into the acks table, keeping only the latest per page+user 106*9bded093SAnna Dabrowska * 107*9bded093SAnna Dabrowska * @param helper_plugin_ireadit_db $ireaditDb 108*9bded093SAnna Dabrowska * @param bool $dryRun 109*9bded093SAnna Dabrowska * @return int number of records imported (or that would be imported if on dry-run) 110*9bded093SAnna Dabrowska * @throws Exception 111*9bded093SAnna Dabrowska */ 112*9bded093SAnna Dabrowska protected function importIreaditRecords(\helper_plugin_ireadit_db $ireaditDb, bool $dryRun): int 113*9bded093SAnna Dabrowska { 114*9bded093SAnna Dabrowska $sqlite = $ireaditDb->getDB(); 115*9bded093SAnna Dabrowska $res = $sqlite->query('SELECT page, user, rev, tim§estamp FROM ireadit'); 116*9bded093SAnna Dabrowska if ($res === false) { 117*9bded093SAnna Dabrowska throw new \RuntimeException('Failed to read records from the ireadit database'); 118*9bded093SAnna Dabrowska } 119*9bded093SAnna Dabrowska $rows = $sqlite->res2arr($res); 120*9bded093SAnna Dabrowska 121*9bded093SAnna Dabrowska // keep only the latest acknowledgement per page+user 122*9bded093SAnna Dabrowska $latest = []; 123*9bded093SAnna Dabrowska foreach ($rows as $row) { 124*9bded093SAnna Dabrowska $time = $row['timestamp'] ? strtotime($row['timestamp']) : (int)$row['rev']; 125*9bded093SAnna Dabrowska if (!$time) continue; 126*9bded093SAnna Dabrowska 127*9bded093SAnna Dabrowska // index by page+user; the NUL byte is a safe separator because it can never 128*9bded093SAnna Dabrowska // appear in a page id or user name, so the two parts can't collide 129*9bded093SAnna Dabrowska $key = $row['page'] . "\0" . $row['user']; 130*9bded093SAnna Dabrowska if (!isset($latest[$key]) || $time > $latest[$key]['time']) { 131*9bded093SAnna Dabrowska $latest[$key] = ['page' => $row['page'], 'user' => $row['user'], 'time' => $time]; 132*9bded093SAnna Dabrowska } 133*9bded093SAnna Dabrowska } 134*9bded093SAnna Dabrowska 135*9bded093SAnna Dabrowska $imported = 0; 136*9bded093SAnna Dabrowska $failed = 0; 137*9bded093SAnna Dabrowska foreach ($latest as $entry) { 138*9bded093SAnna Dabrowska $this->info(sprintf( 139*9bded093SAnna Dabrowska 'ireadit record: %s by %s at %s', 140*9bded093SAnna Dabrowska $entry['page'], 141*9bded093SAnna Dabrowska $entry['user'], 142*9bded093SAnna Dabrowska dformat($entry['time']) 143*9bded093SAnna Dabrowska )); 144*9bded093SAnna Dabrowska 145*9bded093SAnna Dabrowska if ($dryRun) { 146*9bded093SAnna Dabrowska $imported++; 147*9bded093SAnna Dabrowska continue; 148*9bded093SAnna Dabrowska } 149*9bded093SAnna Dabrowska 150*9bded093SAnna Dabrowska // a single bad record should not abort the whole import 151*9bded093SAnna Dabrowska try { 152*9bded093SAnna Dabrowska $this->helper->importAcknowledgement($entry['page'], $entry['user'], $entry['time']); 153*9bded093SAnna Dabrowska $imported++; 154*9bded093SAnna Dabrowska } catch (\Exception $e) { 155*9bded093SAnna Dabrowska $failed++; 156*9bded093SAnna Dabrowska $this->error(sprintf( 157*9bded093SAnna Dabrowska 'Failed to import read record for %s by %s: %s', 158*9bded093SAnna Dabrowska $entry['page'], 159*9bded093SAnna Dabrowska $entry['user'], 160*9bded093SAnna Dabrowska $e->getMessage() 161*9bded093SAnna Dabrowska )); 162*9bded093SAnna Dabrowska } 163*9bded093SAnna Dabrowska } 164*9bded093SAnna Dabrowska 165*9bded093SAnna Dabrowska if ($failed) { 166*9bded093SAnna Dabrowska $this->warning(sprintf('%d read record(s) could not be imported', $failed)); 167*9bded093SAnna Dabrowska } 168*9bded093SAnna Dabrowska 169*9bded093SAnna Dabrowska return $imported; 170*9bded093SAnna Dabrowska } 171*9bded093SAnna Dabrowska 172*9bded093SAnna Dabrowska /** 173*9bded093SAnna Dabrowska * Create assignment patterns for every page containing ~~IREADIT...~~ syntax 174*9bded093SAnna Dabrowska * 175*9bded093SAnna Dabrowska * @param bool $dryRun 176*9bded093SAnna Dabrowska * @return array{0:int,1:int} number of assignee entries and number of patterns created 177*9bded093SAnna Dabrowska * @throws RuntimeException if the assignment patterns cannot be saved 178*9bded093SAnna Dabrowska */ 179*9bded093SAnna Dabrowska protected function importAssignments(bool $dryRun): array 180*9bded093SAnna Dabrowska { 181*9bded093SAnna Dabrowska // legacy indexer, starting with Mort 182*9bded093SAnna Dabrowska $pages = idx_getIndex('page', ''); 183*9bded093SAnna Dabrowska 184*9bded093SAnna Dabrowska $newPatterns = []; 185*9bded093SAnna Dabrowska foreach ($pages as $page) { 186*9bded093SAnna Dabrowska $page = trim($page); 187*9bded093SAnna Dabrowska if ($page === '') continue; 188*9bded093SAnna Dabrowska 189*9bded093SAnna Dabrowska $source = rawWiki($page); 190*9bded093SAnna Dabrowska if (!preg_match('/~~IREADIT(.*?)~~/', $source, $match)) { 191*9bded093SAnna Dabrowska continue; 192*9bded093SAnna Dabrowska } 193*9bded093SAnna Dabrowska 194*9bded093SAnna Dabrowska $assignees = $this->parseIreaditAssignees($match[1]); 195*9bded093SAnna Dabrowska $newPatterns[$page] = $assignees; 196*9bded093SAnna Dabrowska $this->info(sprintf('Pattern: %s -> %s', $page, $assignees)); 197*9bded093SAnna Dabrowska } 198*9bded093SAnna Dabrowska 199*9bded093SAnna Dabrowska if (!$dryRun && $newPatterns) { 200*9bded093SAnna Dabrowska // saveAssignmentPatterns() rewrites the whole assignments_patterns table, so we must merge 201*9bded093SAnna Dabrowska $patterns = $this->helper->getAssignmentPatterns(); 202*9bded093SAnna Dabrowska foreach ($newPatterns as $newPattern => $assignees) { 203*9bded093SAnna Dabrowska // when a page already has our pattern, append the imported assignees 204*9bded093SAnna Dabrowska if (!empty($patterns[$newPattern])) { 205*9bded093SAnna Dabrowska $assignees = $patterns[$newPattern] . ',' . $assignees; 206*9bded093SAnna Dabrowska } 207*9bded093SAnna Dabrowska // clean up combined assignees 208*9bded093SAnna Dabrowska $entries = array_map('trim', explode(',', $assignees)); 209*9bded093SAnna Dabrowska $entries = array_filter($entries); 210*9bded093SAnna Dabrowska $patterns[$newPattern] = implode(',', array_unique($entries)); 211*9bded093SAnna Dabrowska } 212*9bded093SAnna Dabrowska try { 213*9bded093SAnna Dabrowska $this->helper->saveAssignmentPatterns($patterns); 214*9bded093SAnna Dabrowska } catch (\Exception $e) { 215*9bded093SAnna Dabrowska throw new \RuntimeException('Failed to save assignment patterns: ' . $e->getMessage(), 0, $e); 216*9bded093SAnna Dabrowska } 217*9bded093SAnna Dabrowska } 218*9bded093SAnna Dabrowska 219*9bded093SAnna Dabrowska $assigneeCount = 0; 220*9bded093SAnna Dabrowska foreach ($newPatterns as $assignees) { 221*9bded093SAnna Dabrowska $assigneeCount += count(array_filter(explode(',', $assignees))); 222*9bded093SAnna Dabrowska } 223*9bded093SAnna Dabrowska 224*9bded093SAnna Dabrowska return [$assigneeCount, count($newPatterns)]; 225*9bded093SAnna Dabrowska } 226*9bded093SAnna Dabrowska 227*9bded093SAnna Dabrowska /** 228*9bded093SAnna Dabrowska * Parse the ~~IREADIT...~~ syntax into an acknowledge assignee string 229*9bded093SAnna Dabrowska * 230*9bded093SAnna Dabrowska * Mirrors the parsing in syntax_plugin_ireadit_ireadit::handle(). Empty syntax means 231*9bded093SAnna Dabrowska * "everyone" in ireadit, which is mapped to the global default group. 232*9bded093SAnna Dabrowska * 233*9bded093SAnna Dabrowska * @param string $inner the text between ~~IREADIT and ~~ 234*9bded093SAnna Dabrowska * @return string comma-separated list of users and @groups 235*9bded093SAnna Dabrowska */ 236*9bded093SAnna Dabrowska protected function parseIreaditAssignees(string $inner): string 237*9bded093SAnna Dabrowska { 238*9bded093SAnna Dabrowska global $conf; 239*9bded093SAnna Dabrowska 240*9bded093SAnna Dabrowska $splits = preg_split('/[\s:]+/', trim($inner), -1, PREG_SPLIT_NO_EMPTY); 241*9bded093SAnna Dabrowska 242*9bded093SAnna Dabrowska $users = []; 243*9bded093SAnna Dabrowska $groups = []; 244*9bded093SAnna Dabrowska foreach ($splits as $split) { 245*9bded093SAnna Dabrowska if ($split[0] === '@') { 246*9bded093SAnna Dabrowska $groups[] = $split; 247*9bded093SAnna Dabrowska } else { 248*9bded093SAnna Dabrowska $users[] = $split; 249*9bded093SAnna Dabrowska } 250*9bded093SAnna Dabrowska } 251*9bded093SAnna Dabrowska 252*9bded093SAnna Dabrowska // empty ~~IREADIT~~ means everyone, so use default group 253*9bded093SAnna Dabrowska if (!$users && !$groups) { 254*9bded093SAnna Dabrowska return '@' . $conf['defaultgroup']; 255*9bded093SAnna Dabrowska } 256*9bded093SAnna Dabrowska 257*9bded093SAnna Dabrowska return implode(',', array_merge($users, $groups)); 258*9bded093SAnna Dabrowska } 259*9bded093SAnna Dabrowska} 260