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 = "event";
22
23    /**
24     * Uppercase mandatory (the column is uppercased when returnd from a *)
25     */
26    const EVENT_NAME_ATTRIBUTE = "NAME";
27
28    /**
29     * Uppercase mandatory (the column is uppercased when returnd from a *)
30     */
31    const EVENT_DATA_ATTRIBUTE = "DATA";
32    /**
33     * Uppercase mandatory (the column is uppercased when returnd from a *)
34     */
35    const TIMESTAMP_ATTRIBUTE = "TIMESTAMP";
36
37    /**
38     * process all replication request, created with {@link Event::createEvent()}
39     *
40     * by default, there is 5 pages in a default dokuwiki installation in the wiki namespace)
41     *
42     * @param int $maxEvent In case of a start or if there is a recursive bug. We don't want to take all the resources
43     *
44     */
45    public static function dispatchEvent(int $maxEvent = 10)
46    {
47        $comboFuntionName = 'ComboDispatchEvent';
48        print "$comboFuntionName(): Trying to get a lock" . NL;
49        $lock = self::getLock();
50        try {
51            $lock->acquire();
52        } catch (ExceptionTimeOut $e) {
53            // process running
54            return;
55        }
56        print "$comboFuntionName(): Locked" . NL;
57
58        try {
59            try {
60                $sqlite = Sqlite::createOrGetBackendSqlite();
61            } catch (ExceptionSqliteNotAvailable $e) {
62                LogUtility::error("Sqlite is mandatory for asynchronous event", self::CANONICAL, $e);
63                return;
64            }
65
66
67            $rows = [];
68            /**
69             * Returning clause
70             * does not work
71             */
72            $version = $sqlite->getVersion();
73            if ($version > "3.35.0") {
74
75                // returning clause is available since 3.35 on delete
76                // https://www.sqlite.org/lang_returning.html
77
78                $eventTableName = self::EVENT_TABLE_NAME;
79                $statement = "delete from {$eventTableName} returning *";
80                // https://www.sqlite.org/lang_delete.html#optional_limit_and_order_by_clauses
81                if ($sqlite->hasOption("SQLITE_ENABLE_UPDATE_DELETE_LIMIT")) {
82                    $statement .= "order by timestamp limit {$maxEvent}";
83                }
84                $request = $sqlite->createRequest()
85                    ->setStatement($statement);
86                try {
87                    $rows = $request->execute()
88                        ->getRows();
89                    if (sizeof($rows) === 0) {
90                        return;
91                    }
92                } catch (ExceptionCompile $e) {
93                    LogUtility::error($e->getMessage(), $e->getCanonical(), $e);
94                } finally {
95                    $request->close();
96                }
97
98            }
99
100            /**
101             * Error in the block before or not the good version
102             * We try to get the records with a select/delete
103             */
104            if (sizeof($rows) === 0) {
105
106
107                // technically the lock system of dokuwiki does not allow two process to run on
108                // the indexer, we trust it
109                $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID];
110                $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes);
111                $select .= " order by " . self::TIMESTAMP_ATTRIBUTE . " limit {$maxEvent}";
112                $request = $sqlite->createRequest()
113                    ->setQuery($select);
114
115                $rowsSelected = [];
116                try {
117                    $rowsSelected = $request->execute()
118                        ->getRows();
119                    if (sizeof($rowsSelected) === 0) {
120                        return;
121                    }
122                } catch (ExceptionCompile $e) {
123                    LogUtility::msg("Error while retrieving the event {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical());
124                    return;
125                } finally {
126                    $request->close();
127                }
128
129                $eventTableName = self::EVENT_TABLE_NAME;
130                $rows = [];
131                foreach ($rowsSelected as $row) {
132                    $request = $sqlite->createRequest()
133                        ->setQueryParametrized("delete from $eventTableName where rowid = ? ", [$row[DatabasePageRow::ROWID]]);
134                    try {
135                        $changeCount = $request->execute()->getChangeCount();
136                        if ($changeCount !== 1) {
137                            LogUtility::msg("The delete of the event was not successful or it was deleted by another process", LogUtility::LVL_MSG_ERROR);
138                        } else {
139                            $rows[] = $row;
140                        }
141                    } catch (ExceptionCompile $e) {
142                        LogUtility::msg("Error while deleting the event. Message {$e->getMessage()}", LogUtility::LVL_MSG_ERROR, $e->getCanonical());
143                        return;
144                    } finally {
145                        $request->close();
146                    }
147                }
148
149
150            }
151
152
153            $eventCounter = 0;
154            foreach ($rows as $row) {
155                $eventCounter++;
156                $eventName = $row[self::EVENT_NAME_ATTRIBUTE];
157                $eventData = [];
158                $eventDataJson = $row[self::EVENT_DATA_ATTRIBUTE];
159                if ($eventDataJson !== null) {
160                    try {
161                        $eventData = Json::createFromString($eventDataJson)->toArray();
162                    } catch (ExceptionCompile $e) {
163                        LogUtility::msg("The stored data for the event $eventName was not in the json format");
164                        continue;
165                    }
166                }
167                \dokuwiki\Extension\Event::createAndTrigger($eventName, $eventData);
168
169                if ($eventCounter >= $maxEvent) {
170                    break;
171                }
172
173            }
174        } catch (\Exception $e) {
175            LogUtility::internalError("An internal error has runned on event. " . $e->getMessage(), self::CANONICAL, $e);
176        } finally {
177            $lock->release();
178            print "$comboFuntionName(): Lock Released" . NL;
179        }
180
181    }
182
183    /**
184     * Ask a replication in the background
185     * @param string $name - a string with the reason
186     * @param array $data
187     */
188    public static
189    function createEvent(string $name, array $data)
190    {
191
192        try {
193            $sqlite = Sqlite::createOrGetBackendSqlite();
194        } catch (ExceptionSqliteNotAvailable $e) {
195            LogUtility::error("Unable to create the event $name. Sqlite is not available");
196            return;
197        }
198
199        /**
200         * If not present
201         */
202        $entry = array(
203            "name" => $name,
204            "timestamp" => Iso8601Date::createFromNow()->toString()
205        );
206
207
208        $entry["data"] = Json::createFromArray($data)->toPrettyJsonString();
209        $entry["data_hash"] = md5($entry["data"]);
210
211        /**
212         * Execute
213         */
214        $request = $sqlite->createRequest()
215            ->setTableRow(self::EVENT_TABLE_NAME, $entry);
216        try {
217            $request->execute();
218        } catch (ExceptionCompile $e) {
219            LogUtility::error("Unable to create the event $name. Error:" . $e->getMessage(), self::CANONICAL, $e);
220        } finally {
221            $request->close();
222        }
223
224
225    }
226
227    /**
228     * @param $pageId
229     *
230     * This is equivalent to {@link TaskRunner}
231     *
232     * lib/exe/taskrunner.php?id='.rawurlencode($ID)
233     * $taskRunner = new \dokuwiki\TaskRunner();
234     * $taskRunner->run();
235     *
236     */
237    public static function startTaskRunnerForPage($pageId)
238    {
239        $tmp = []; // No event data
240        $tmp['page'] = $pageId;
241        $evt = new \dokuwiki\Extension\Event('INDEXER_TASKS_RUN', $tmp);
242        $evt->advise_before();
243        $evt->advise_after();
244    }
245
246
247    public static function getQueue(string $eventName = null): array
248    {
249        try {
250            $sqlite = Sqlite::createOrGetBackendSqlite();
251        } catch (ExceptionSqliteNotAvailable $e) {
252            LogUtility::internalError("Sqlite is not available, no events was returned", self::CANONICAL);
253            return [];
254        }
255
256        /**
257         * Execute
258         */
259        $attributes = [self::EVENT_NAME_ATTRIBUTE, self::EVENT_DATA_ATTRIBUTE, DatabasePageRow::ROWID];
260        $select = Sqlite::createSelectFromTableAndColumns(self::EVENT_TABLE_NAME, $attributes);
261        $request = $sqlite->createRequest();
262        if (empty($eventName)) {
263            $request->setQuery($select);
264        } else {
265            $request->setQueryParametrized($select . " where " . self::EVENT_NAME_ATTRIBUTE . " = ?", [$eventName]);
266        }
267        try {
268            return $request->execute()
269                ->getRows();
270        } catch (ExceptionCompile $e) {
271            LogUtility::internalError("Unable to get the queue. Error:" . $e->getMessage(), self::CANONICAL, $e);
272            return [];
273        } finally {
274            $request->close();
275        }
276
277    }
278
279    /**
280     * @throws ExceptionCompile
281     */
282    public static function purgeQueue(): int
283    {
284        $sqlite = Sqlite::createOrGetBackendSqlite();
285        if ($sqlite === null) {
286            throw new ExceptionCompile("Sqlite is not available");
287        }
288
289
290        /**
291         * Execute
292         */
293        /** @noinspection SqlWithoutWhere */
294        $request = $sqlite->createRequest()
295            ->setQuery("delete from " . self::EVENT_TABLE_NAME);
296        try {
297            return $request->execute()
298                ->getChangeCount();
299        } catch (ExceptionCompile $e) {
300            throw new ExceptionCompile("Unable to count the number of event in the queue. Error:" . $e->getMessage(), self::CANONICAL, 0, $e);
301        } finally {
302            $request->close();
303        }
304    }
305
306    /**
307     * @throws ExceptionCompile
308     */
309    public static function getEvents(string $eventName): array
310    {
311        return Event::getQueue($eventName);
312    }
313
314    public static function getLock(): Lock
315    {
316        return Lock::create("combo-event");
317    }
318
319
320}
321