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