Events.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Database;
  4. use PhpMyAdmin\DatabaseInterface;
  5. use PhpMyAdmin\Html\Generator;
  6. use PhpMyAdmin\Message;
  7. use PhpMyAdmin\Response;
  8. use PhpMyAdmin\Template;
  9. use PhpMyAdmin\Util;
  10. use function count;
  11. use function explode;
  12. use function htmlspecialchars;
  13. use function in_array;
  14. use function intval;
  15. use function mb_strpos;
  16. use function mb_strtoupper;
  17. use function sprintf;
  18. use function strtoupper;
  19. use function trim;
  20. /**
  21. * Functions for event management.
  22. */
  23. class Events
  24. {
  25. /** @var array<string, array<int, string>> */
  26. private $status = [
  27. 'query' => ['ENABLE', 'DISABLE', 'DISABLE ON SLAVE'],
  28. 'display' => ['ENABLED', 'DISABLED', 'SLAVESIDE_DISABLED'],
  29. ];
  30. /** @var array<int, string> */
  31. private $type = ['RECURRING', 'ONE TIME'];
  32. /** @var array<int, string> */
  33. private $interval = [
  34. 'YEAR',
  35. 'QUARTER',
  36. 'MONTH',
  37. 'DAY',
  38. 'HOUR',
  39. 'MINUTE',
  40. 'WEEK',
  41. 'SECOND',
  42. 'YEAR_MONTH',
  43. 'DAY_HOUR',
  44. 'DAY_MINUTE',
  45. 'DAY_SECOND',
  46. 'HOUR_MINUTE',
  47. 'HOUR_SECOND',
  48. 'MINUTE_SECOND',
  49. ];
  50. /** @var DatabaseInterface */
  51. private $dbi;
  52. /** @var Template */
  53. private $template;
  54. /** @var Response */
  55. private $response;
  56. /**
  57. * @param DatabaseInterface $dbi DatabaseInterface instance.
  58. * @param Template $template Template instance.
  59. * @param Response $response Response instance.
  60. */
  61. public function __construct(DatabaseInterface $dbi, Template $template, $response)
  62. {
  63. $this->dbi = $dbi;
  64. $this->template = $template;
  65. $this->response = $response;
  66. }
  67. /**
  68. * Handles editor requests for adding or editing an item
  69. *
  70. * @return void
  71. */
  72. public function handleEditor()
  73. {
  74. global $db, $table, $errors, $message;
  75. if (! empty($_POST['editor_process_add'])
  76. || ! empty($_POST['editor_process_edit'])
  77. ) {
  78. $sql_query = '';
  79. $item_query = $this->getQueryFromRequest();
  80. // set by getQueryFromRequest()
  81. if (! count($errors)) {
  82. // Execute the created query
  83. if (! empty($_POST['editor_process_edit'])) {
  84. // Backup the old trigger, in case something goes wrong
  85. $create_item = $this->dbi->getDefinition(
  86. $db,
  87. 'EVENT',
  88. $_POST['item_original_name']
  89. );
  90. $drop_item = 'DROP EVENT IF EXISTS '
  91. . Util::backquote($_POST['item_original_name'])
  92. . ";\n";
  93. $result = $this->dbi->tryQuery($drop_item);
  94. if (! $result) {
  95. $errors[] = sprintf(
  96. __('The following query has failed: "%s"'),
  97. htmlspecialchars($drop_item)
  98. )
  99. . '<br>'
  100. . __('MySQL said: ') . $this->dbi->getError();
  101. } else {
  102. $result = $this->dbi->tryQuery($item_query);
  103. if (! $result) {
  104. $errors[] = sprintf(
  105. __('The following query has failed: "%s"'),
  106. htmlspecialchars($item_query)
  107. )
  108. . '<br>'
  109. . __('MySQL said: ') . $this->dbi->getError();
  110. // We dropped the old item, but were unable to create
  111. // the new one. Try to restore the backup query
  112. $result = $this->dbi->tryQuery($create_item);
  113. $errors = $this->checkResult($result, $create_item, $errors);
  114. } else {
  115. $message = Message::success(
  116. __('Event %1$s has been modified.')
  117. );
  118. $message->addParam(
  119. Util::backquote($_POST['item_name'])
  120. );
  121. $sql_query = $drop_item . $item_query;
  122. }
  123. }
  124. } else {
  125. // 'Add a new item' mode
  126. $result = $this->dbi->tryQuery($item_query);
  127. if (! $result) {
  128. $errors[] = sprintf(
  129. __('The following query has failed: "%s"'),
  130. htmlspecialchars($item_query)
  131. )
  132. . '<br><br>'
  133. . __('MySQL said: ') . $this->dbi->getError();
  134. } else {
  135. $message = Message::success(
  136. __('Event %1$s has been created.')
  137. );
  138. $message->addParam(
  139. Util::backquote($_POST['item_name'])
  140. );
  141. $sql_query = $item_query;
  142. }
  143. }
  144. }
  145. if (count($errors)) {
  146. $message = Message::error(
  147. '<b>'
  148. . __(
  149. 'One or more errors have occurred while processing your request:'
  150. )
  151. . '</b>'
  152. );
  153. $message->addHtml('<ul>');
  154. foreach ($errors as $string) {
  155. $message->addHtml('<li>' . $string . '</li>');
  156. }
  157. $message->addHtml('</ul>');
  158. }
  159. $output = Generator::getMessage($message, $sql_query);
  160. if ($this->response->isAjax()) {
  161. if ($message->isSuccess()) {
  162. $events = $this->dbi->getEvents($db, $_POST['item_name']);
  163. $event = $events[0];
  164. $this->response->addJSON(
  165. 'name',
  166. htmlspecialchars(
  167. mb_strtoupper($_POST['item_name'])
  168. )
  169. );
  170. if (! empty($event)) {
  171. $sqlDrop = sprintf(
  172. 'DROP EVENT IF EXISTS %s',
  173. Util::backquote($event['name'])
  174. );
  175. $this->response->addJSON(
  176. 'new_row',
  177. $this->template->render('database/events/row', [
  178. 'db' => $db,
  179. 'table' => $table,
  180. 'event' => $event,
  181. 'has_privilege' => Util::currentUserHasPrivilege('EVENT', $db),
  182. 'sql_drop' => $sqlDrop,
  183. 'row_class' => '',
  184. ])
  185. );
  186. }
  187. $this->response->addJSON('insert', ! empty($event));
  188. $this->response->addJSON('message', $output);
  189. } else {
  190. $this->response->setRequestStatus(false);
  191. $this->response->addJSON('message', $message);
  192. }
  193. $this->response->addJSON('tableType', 'events');
  194. exit;
  195. }
  196. }
  197. /**
  198. * Display a form used to add/edit a trigger, if necessary
  199. */
  200. if (! count($errors)
  201. && (! empty($_POST['editor_process_add'])
  202. || ! empty($_POST['editor_process_edit'])
  203. || (empty($_REQUEST['add_item'])
  204. && empty($_REQUEST['edit_item'])
  205. && empty($_POST['item_changetype'])))
  206. ) {
  207. return;
  208. }
  209. // FIXME: this must be simpler than that
  210. $operation = '';
  211. $title = '';
  212. $item = null;
  213. $mode = '';
  214. if (! empty($_POST['item_changetype'])) {
  215. $operation = 'change';
  216. }
  217. // Get the data for the form (if any)
  218. if (! empty($_REQUEST['add_item'])) {
  219. $title = __('Add event');
  220. $item = $this->getDataFromRequest();
  221. $mode = 'add';
  222. } elseif (! empty($_REQUEST['edit_item'])) {
  223. $title = __('Edit event');
  224. if (! empty($_REQUEST['item_name'])
  225. && empty($_POST['editor_process_edit'])
  226. && empty($_POST['item_changetype'])
  227. ) {
  228. $item = $this->getDataFromName($_REQUEST['item_name']);
  229. if ($item !== null) {
  230. $item['item_original_name'] = $item['item_name'];
  231. }
  232. } else {
  233. $item = $this->getDataFromRequest();
  234. }
  235. $mode = 'edit';
  236. }
  237. $this->sendEditor($mode, $item, $title, $db, $operation);
  238. }
  239. /**
  240. * This function will generate the values that are required to for the editor
  241. *
  242. * @return array Data necessary to create the editor.
  243. */
  244. public function getDataFromRequest()
  245. {
  246. $retval = [];
  247. $indices = [
  248. 'item_name',
  249. 'item_original_name',
  250. 'item_status',
  251. 'item_execute_at',
  252. 'item_interval_value',
  253. 'item_interval_field',
  254. 'item_starts',
  255. 'item_ends',
  256. 'item_definition',
  257. 'item_preserve',
  258. 'item_comment',
  259. 'item_definer',
  260. ];
  261. foreach ($indices as $index) {
  262. $retval[$index] = $_POST[$index] ?? '';
  263. }
  264. $retval['item_type'] = 'ONE TIME';
  265. $retval['item_type_toggle'] = 'RECURRING';
  266. if (isset($_POST['item_type']) && $_POST['item_type'] === 'RECURRING') {
  267. $retval['item_type'] = 'RECURRING';
  268. $retval['item_type_toggle'] = 'ONE TIME';
  269. }
  270. return $retval;
  271. }
  272. /**
  273. * This function will generate the values that are required to complete
  274. * the "Edit event" form given the name of a event.
  275. *
  276. * @param string $name The name of the event.
  277. *
  278. * @return array|null Data necessary to create the editor.
  279. */
  280. public function getDataFromName($name): ?array
  281. {
  282. global $db;
  283. $retval = [];
  284. $columns = '`EVENT_NAME`, `STATUS`, `EVENT_TYPE`, `EXECUTE_AT`, '
  285. . '`INTERVAL_VALUE`, `INTERVAL_FIELD`, `STARTS`, `ENDS`, '
  286. . '`EVENT_DEFINITION`, `ON_COMPLETION`, `DEFINER`, `EVENT_COMMENT`';
  287. $where = 'EVENT_SCHEMA ' . Util::getCollateForIS() . '='
  288. . "'" . $this->dbi->escapeString($db) . "' "
  289. . "AND EVENT_NAME='" . $this->dbi->escapeString($name) . "'";
  290. $query = 'SELECT ' . $columns . ' FROM `INFORMATION_SCHEMA`.`EVENTS` WHERE ' . $where . ';';
  291. $item = $this->dbi->fetchSingleRow($query);
  292. if (! $item) {
  293. return null;
  294. }
  295. $retval['item_name'] = $item['EVENT_NAME'];
  296. $retval['item_status'] = $item['STATUS'];
  297. $retval['item_type'] = $item['EVENT_TYPE'];
  298. if ($retval['item_type'] === 'RECURRING') {
  299. $retval['item_type_toggle'] = 'ONE TIME';
  300. } else {
  301. $retval['item_type_toggle'] = 'RECURRING';
  302. }
  303. $retval['item_execute_at'] = $item['EXECUTE_AT'];
  304. $retval['item_interval_value'] = $item['INTERVAL_VALUE'];
  305. $retval['item_interval_field'] = $item['INTERVAL_FIELD'];
  306. $retval['item_starts'] = $item['STARTS'];
  307. $retval['item_ends'] = $item['ENDS'];
  308. $retval['item_preserve'] = '';
  309. if ($item['ON_COMPLETION'] === 'PRESERVE') {
  310. $retval['item_preserve'] = " checked='checked'";
  311. }
  312. $retval['item_definition'] = $item['EVENT_DEFINITION'];
  313. $retval['item_definer'] = $item['DEFINER'];
  314. $retval['item_comment'] = $item['EVENT_COMMENT'];
  315. return $retval;
  316. }
  317. /**
  318. * Displays a form used to add/edit an event
  319. *
  320. * @param string $mode If the editor will be used to edit an event
  321. * or add a new one: 'edit' or 'add'.
  322. * @param string $operation If the editor was previously invoked with
  323. * JS turned off, this will hold the name of
  324. * the current operation
  325. * @param array $item Data for the event returned by
  326. * getDataFromRequest() or getDataFromName()
  327. *
  328. * @return string HTML code for the editor.
  329. */
  330. public function getEditorForm($mode, $operation, array $item)
  331. {
  332. global $db;
  333. if ($operation === 'change') {
  334. if ($item['item_type'] === 'RECURRING') {
  335. $item['item_type'] = 'ONE TIME';
  336. $item['item_type_toggle'] = 'RECURRING';
  337. } else {
  338. $item['item_type'] = 'RECURRING';
  339. $item['item_type_toggle'] = 'ONE TIME';
  340. }
  341. }
  342. return $this->template->render('database/events/editor_form', [
  343. 'db' => $db,
  344. 'event' => $item,
  345. 'mode' => $mode,
  346. 'is_ajax' => $this->response->isAjax(),
  347. 'status_display' => $this->status['display'],
  348. 'event_type' => $this->type,
  349. 'event_interval' => $this->interval,
  350. ]);
  351. }
  352. /**
  353. * Composes the query necessary to create an event from an HTTP request.
  354. *
  355. * @return string The CREATE EVENT query.
  356. */
  357. public function getQueryFromRequest()
  358. {
  359. global $errors;
  360. $query = 'CREATE ';
  361. if (! empty($_POST['item_definer'])) {
  362. if (mb_strpos($_POST['item_definer'], '@') !== false
  363. ) {
  364. $arr = explode('@', $_POST['item_definer']);
  365. $query .= 'DEFINER=' . Util::backquote($arr[0]);
  366. $query .= '@' . Util::backquote($arr[1]) . ' ';
  367. } else {
  368. $errors[] = __('The definer must be in the "username@hostname" format!');
  369. }
  370. }
  371. $query .= 'EVENT ';
  372. if (! empty($_POST['item_name'])) {
  373. $query .= Util::backquote($_POST['item_name']) . ' ';
  374. } else {
  375. $errors[] = __('You must provide an event name!');
  376. }
  377. $query .= 'ON SCHEDULE ';
  378. if (! empty($_POST['item_type'])
  379. && in_array($_POST['item_type'], $this->type)
  380. ) {
  381. if ($_POST['item_type'] === 'RECURRING') {
  382. if (! empty($_POST['item_interval_value'])
  383. && ! empty($_POST['item_interval_field'])
  384. && in_array($_POST['item_interval_field'], $this->interval)
  385. ) {
  386. $query .= 'EVERY ' . intval($_POST['item_interval_value']) . ' ';
  387. $query .= $_POST['item_interval_field'] . ' ';
  388. } else {
  389. $errors[]
  390. = __('You must provide a valid interval value for the event.');
  391. }
  392. if (! empty($_POST['item_starts'])) {
  393. $query .= "STARTS '"
  394. . $this->dbi->escapeString($_POST['item_starts'])
  395. . "' ";
  396. }
  397. if (! empty($_POST['item_ends'])) {
  398. $query .= "ENDS '"
  399. . $this->dbi->escapeString($_POST['item_ends'])
  400. . "' ";
  401. }
  402. } else {
  403. if (! empty($_POST['item_execute_at'])) {
  404. $query .= "AT '"
  405. . $this->dbi->escapeString($_POST['item_execute_at'])
  406. . "' ";
  407. } else {
  408. $errors[]
  409. = __('You must provide a valid execution time for the event.');
  410. }
  411. }
  412. } else {
  413. $errors[] = __('You must provide a valid type for the event.');
  414. }
  415. $query .= 'ON COMPLETION ';
  416. if (empty($_POST['item_preserve'])) {
  417. $query .= 'NOT ';
  418. }
  419. $query .= 'PRESERVE ';
  420. if (! empty($_POST['item_status'])) {
  421. foreach ($this->status['display'] as $key => $value) {
  422. if ($value == $_POST['item_status']) {
  423. $query .= $this->status['query'][$key] . ' ';
  424. break;
  425. }
  426. }
  427. }
  428. if (! empty($_POST['item_comment'])) {
  429. $query .= "COMMENT '" . $this->dbi->escapeString(
  430. $_POST['item_comment']
  431. ) . "' ";
  432. }
  433. $query .= 'DO ';
  434. if (! empty($_POST['item_definition'])) {
  435. $query .= $_POST['item_definition'];
  436. } else {
  437. $errors[] = __('You must provide an event definition.');
  438. }
  439. return $query;
  440. }
  441. public function getEventSchedulerStatus(): bool
  442. {
  443. $state = $this->dbi->fetchValue(
  444. 'SHOW GLOBAL VARIABLES LIKE \'event_scheduler\'',
  445. 0,
  446. 1
  447. );
  448. return strtoupper($state) === 'ON' || $state === '1';
  449. }
  450. /**
  451. * @param resource|bool $result Query result
  452. * @param string|null $createStatement Query
  453. * @param array $errors Errors
  454. *
  455. * @return array
  456. */
  457. private function checkResult($result, $createStatement, array $errors)
  458. {
  459. if ($result) {
  460. return $errors;
  461. }
  462. // OMG, this is really bad! We dropped the query,
  463. // failed to create a new one
  464. // and now even the backup query does not execute!
  465. // This should not happen, but we better handle
  466. // this just in case.
  467. $errors[] = __('Sorry, we failed to restore the dropped event.') . '<br>'
  468. . __('The backed up query was:')
  469. . '"' . htmlspecialchars((string) $createStatement) . '"<br>'
  470. . __('MySQL said: ') . $this->dbi->getError();
  471. return $errors;
  472. }
  473. /**
  474. * Send editor via ajax or by echoing.
  475. *
  476. * @param string $mode Editor mode 'add' or 'edit'
  477. * @param array|null $item Data necessary to create the editor
  478. * @param string $title Title of the editor
  479. * @param string $db Database
  480. * @param string $operation Operation 'change' or ''
  481. *
  482. * @return void
  483. */
  484. private function sendEditor($mode, ?array $item, $title, $db, $operation)
  485. {
  486. if ($item !== null) {
  487. $editor = $this->getEditorForm($mode, $operation, $item);
  488. if ($this->response->isAjax()) {
  489. $this->response->addJSON('message', $editor);
  490. $this->response->addJSON('title', $title);
  491. } else {
  492. echo "\n\n<h2>" . $title . "</h2>\n\n" . $editor;
  493. unset($_POST);
  494. }
  495. exit;
  496. }
  497. $message = __('Error in processing request:') . ' ';
  498. $message .= sprintf(
  499. __('No event with name %1$s found in database %2$s.'),
  500. htmlspecialchars(Util::backquote($_REQUEST['item_name'])),
  501. htmlspecialchars(Util::backquote($db))
  502. );
  503. $message = Message::error($message);
  504. if ($this->response->isAjax()) {
  505. $this->response->setRequestStatus(false);
  506. $this->response->addJSON('message', $message);
  507. exit;
  508. }
  509. echo $message->getDisplay();
  510. }
  511. public function export(): void
  512. {
  513. global $db;
  514. if (empty($_GET['export_item']) || empty($_GET['item_name'])) {
  515. return;
  516. }
  517. $itemName = $_GET['item_name'];
  518. $exportData = $this->dbi->getDefinition($db, 'EVENT', $itemName);
  519. if (! $exportData) {
  520. $exportData = false;
  521. }
  522. $itemName = htmlspecialchars(Util::backquote($_GET['item_name']));
  523. if ($exportData !== false) {
  524. $exportData = htmlspecialchars(trim($exportData));
  525. $title = sprintf(__('Export of event %s'), $itemName);
  526. if ($this->response->isAjax()) {
  527. $this->response->addJSON('message', $exportData);
  528. $this->response->addJSON('title', $title);
  529. exit;
  530. }
  531. $exportData = '<textarea cols="40" rows="15" style="width: 100%;">'
  532. . $exportData . '</textarea>';
  533. echo "<fieldset>\n" . '<legend>' . $title . "</legend>\n"
  534. . $exportData . "</fieldset>\n";
  535. return;
  536. }
  537. $message = sprintf(
  538. __('Error in processing request: No event with name %1$s found in database %2$s.'),
  539. $itemName,
  540. htmlspecialchars(Util::backquote($db))
  541. );
  542. $message = Message::error($message);
  543. if ($this->response->isAjax()) {
  544. $this->response->setRequestStatus(false);
  545. $this->response->addJSON('message', $message);
  546. exit;
  547. }
  548. echo $message->getDisplay();
  549. }
  550. }