xref: /plugin/acknowledge/cli.php (revision 9bded093d5c0d7867d6c7968fdc20711e40d0698)
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