1<?php
2
3
4namespace ComboStrap;
5
6/**
7 * Class Event
8 * @package ComboStrap
9 * Asynchronous pub/sub system
10 *
11 * Dokuwiki allows event but they are synchronous
12 * because php does not live in multiple thread
13 *
14 * With the help of Sqlite, we make them asynchronous
15 */
16class Event
17{
18
19    const EVENT_TABLE_NAME = "EVENTS_QUEUE";
20
21    const CANONICAL = "support";
22    const EVENT_NAME_ATTRIBUTE = "name";
23    const EVENT_DATA_ATTRIBUTE = "data";
24
25    /**
26     * process all replication request, created with {@link Event::createEvent()}
27     *
28     * by default, there is 5 pages in a default dokuwiki installation in the wiki namespace)
29     */
30    public static function dispatchEvent($maxEvent = 10)
31    {
32
33        $sqlite = Sqlite::createOrGetBackendSqlite();
34        if ($sqlite === null) {
35            LogUtility::msg("Sqlite is mandatory for asynchronous event");
36            return;
37        }
38
39        /**
40         * In case of a start or if there is a recursive bug
41         * We don't want to take all the resources
42         */
43        $maxBackgroundEventLow = 10;
44
45        $version = $sqlite->getVersion();
46        $rows = [];
47        if ($version > "3.35.0") {
48
49            // returning clause is available since 3.35 on delete
50            // https://www.sqlite.org/lang_returning.html
51
52            $eventTableName = self::EVENT_TABLE_NAME;
53            $statement = "delete from {$eventTableName} returning *";
54            // https://www.sqlite.org/lang_delete.html#optional_limit_and_order_by_clauses
55            if ($sqlite->hasOption("SQLITE_ENABLE_UPDATE_DELETE_LIMIT")) {
56                $statement .= "order by timestamp limit {$maxBackgroundEventLow}";
57            }
58            $request = $sqlite->createRequest()
59                ->setStatement($statement);
60            try {
61                $rows = $request->execute()
62                    ->getRows();
63                if (sizeof($rows) === 0) {
64                    return;
65                }
66            } catch (ExceptionCombo $e) {
67                LogUtility::msg($e->getMessage(), LogUtility::LVL_MSG_ERROR, $e->getCanonical());
68            } finally {
69                $request->close();
70            }
71
72        }
73
74        /**
75         * Error in the block before or not the good version
76         * We try to get the records with a select/delete
77         */
78        if (sizeof($rows) === 0) {
79
80
81            // technically the lock system of dokuwiki does not allow two process to run on
82            // the indexer, we trust it
83            $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID];
84            $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes);
85            $request = $sqlite->createRequest()
86                ->setQuery($select);
87
88            $rowsSelected = [];
89            try {
90                $rowsSelected = $request->execute()
91                    ->getRows();
92                if (sizeof($rowsSelected) === 0) {
93                    return;
94                }
95            } catch (ExceptionCombo $e) {
96                LogUtility::msg("Error while retrieving the event {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical());
97                return;
98            } finally {
99                $request->close();
100            }
101
102            $eventTableName = self::EVENT_TABLE_NAME;
103            $rows = [];
104            foreach ($rowsSelected as $row) {
105                $request = $sqlite->createRequest()
106                    ->setQueryParametrized("delete from $eventTableName where rowid = ? ", [$row[DatabasePageRow::ROWID]]);
107                try {
108                    $changeCount = $request->execute()
109                        ->getChangeCount();
110                    if ($changeCount !== 1) {
111                        LogUtility::msg("The delete of the event was not successful or it was deleted by another process", LogUtility::LVL_MSG_ERROR);
112                    } else {
113                        $rows[] = $row;
114                    }
115                } catch (ExceptionCombo $e) {
116                    LogUtility::msg("Error while deleting the event. Message {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical());
117                    return;
118                } finally {
119                    $request->close();
120                }
121            }
122
123
124        }
125
126
127        $eventCounter = 0;
128        foreach ($rows as $row) {
129            $eventCounter++;
130            $eventName = $row[self::EVENT_NAME_ATTRIBUTE];
131            $eventData = [];
132            $eventDataJson = $row[self::EVENT_DATA_ATTRIBUTE];
133            if ($eventDataJson !== null) {
134                try {
135                    $eventData = Json::createFromString($eventDataJson)->toArray();
136                } catch (ExceptionCombo $e) {
137                    LogUtility::msg("The stored data for the event $eventName was not in the json format");
138                    continue;
139                }
140            }
141            \dokuwiki\Extension\Event::createAndTrigger($eventName, $eventData);
142
143            if ($eventCounter >= $maxEvent) {
144                break;
145            }
146
147        }
148
149    }
150
151    /**
152     * Ask a replication in the background
153     * @param string $name - a string with the reason
154     * @param array $data
155     */
156    public static
157    function createEvent(string $name, array $data)
158    {
159
160        $sqlite = Sqlite::createOrGetBackendSqlite();
161        if ($sqlite === null) {
162            LogUtility::msg("Unable to create the event $name. Sqlite is not available");
163            return;
164        }
165
166
167        /**
168         * If not present
169         */
170        $entry = array(
171            "name" => $name,
172            "timestamp" => Iso8601Date::createFromNow()->toString()
173        );
174
175        if ($data !== null) {
176            $entry["data"] = Json::createFromArray($data)->toPrettyJsonString();
177        }
178
179        /**
180         * Execute
181         */
182        $request = $sqlite->createRequest()
183            ->setTableRow(self::EVENT_TABLE_NAME, $entry);
184        try {
185            $request->execute();
186        } catch (ExceptionCombo $e) {
187            LogUtility::msg("Unable to create the event $name. Error:" . $e->getMessage(), LogUtility::LVL_MSG_ERROR, $e->getCanonical());
188        } finally {
189            $request->close();
190        }
191
192
193    }
194
195    /**
196     * @param $pageId
197     *
198     * This is equivalent to {@link TaskRunner}
199     *
200     * lib/exe/taskrunner.php?id='.rawurlencode($ID)
201     * $taskRunner = new \dokuwiki\TaskRunner();
202     * $taskRunner->run();
203     *
204     */
205    public static function startTaskRunnerForPage($pageId)
206    {
207        $tmp = []; // No event data
208        $tmp['page'] = $pageId;
209        $evt = new \dokuwiki\Extension\Event('INDEXER_TASKS_RUN', $tmp);
210        $evt->advise_after();
211    }
212
213    /**
214     * @throws ExceptionCombo
215     */
216    public static function getQueue(): array
217    {
218        $sqlite = Sqlite::createOrGetBackendSqlite();
219        if ($sqlite === null) {
220            throw new ExceptionCombo("Sqlite is not available");
221        }
222
223
224        /**
225         * Execute
226         */
227        $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID];
228        $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes);
229        $request = $sqlite->createRequest()
230            ->setQuery($select);
231        try {
232            return $request->execute()
233                ->getRows();
234        } catch (ExceptionCombo $e) {
235            throw new ExceptionCombo("Unable to get the queue. Error:" . $e->getMessage(),self::CANONICAL,0,$e);
236        } finally {
237            $request->close();
238        }
239
240    }
241
242    /**
243     * @throws ExceptionCombo
244     */
245    public static function purgeQueue(): int
246    {
247        $sqlite = Sqlite::createOrGetBackendSqlite();
248        if ($sqlite === null) {
249            throw new ExceptionCombo("Sqlite is not available");
250        }
251
252
253        /**
254         * Execute
255         */
256        $request = $sqlite->createRequest()
257            ->setQuery("delete from " . self::EVENT_TABLE_NAME);
258        try {
259            return $request->execute()
260                ->getChangeCount();
261        } catch (ExceptionCombo $e) {
262            throw new ExceptionCombo("Unable to count the number of event in the queue. Error:" . $e->getMessage(),self::CANONICAL,0,$e);
263        } finally {
264            $request->close();
265        }
266    }
267
268
269}
270