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