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