diff options
| author | Charles <sircharlesaze@gmail.com> | 2020-01-09 10:55:03 +0100 |
|---|---|---|
| committer | Charles <sircharlesaze@gmail.com> | 2020-01-09 13:09:38 +0100 |
| commit | 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa (patch) | |
| tree | 5c691241355c943a3c68ddb06b8cf8c60aa11319 /srcs/phpmyadmin/libraries/classes/Rte | |
| parent | 7e0d85db834d6351ed85d01e5126ac31dc510b86 (diff) | |
| download | ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.tar.gz ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.tar.bz2 ft_server-04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa.zip | |
phpmyadmin working
Diffstat (limited to 'srcs/phpmyadmin/libraries/classes/Rte')
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/Events.php | 680 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/Export.php | 168 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/Footer.php | 160 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/General.php | 118 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/Routines.php | 1743 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/RteList.php | 518 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/Triggers.php | 527 | ||||
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Rte/Words.php | 89 |
8 files changed, 4003 insertions, 0 deletions
diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Events.php b/srcs/phpmyadmin/libraries/classes/Rte/Events.php new file mode 100644 index 0000000..bb0d52b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Events.php @@ -0,0 +1,680 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Functions for event management. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Message; +use PhpMyAdmin\Response; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\Events class + * + * @package PhpMyAdmin + */ +class Events +{ + /** + * @var Export + */ + private $export; + + /** + * @var Footer + */ + private $footer; + + /** + * @var General + */ + private $general; + + /** + * @var RteList + */ + private $rteList; + + /** + * @var Words + */ + private $words; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * Events constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + $this->export = new Export($this->dbi); + $this->footer = new Footer($this->dbi); + $this->general = new General($this->dbi); + $this->rteList = new RteList($this->dbi); + $this->words = new Words(); + } + + /** + * Sets required globals + * + * @return void + */ + public function setGlobals() + { + global $event_status, $event_type, $event_interval; + + $event_status = [ + 'query' => [ + 'ENABLE', + 'DISABLE', + 'DISABLE ON SLAVE', + ], + 'display' => [ + 'ENABLED', + 'DISABLED', + 'SLAVESIDE_DISABLED', + ], + ]; + $event_type = [ + 'RECURRING', + 'ONE TIME', + ]; + $event_interval = [ + 'YEAR', + 'QUARTER', + 'MONTH', + 'DAY', + 'HOUR', + 'MINUTE', + 'WEEK', + 'SECOND', + 'YEAR_MONTH', + 'DAY_HOUR', + 'DAY_MINUTE', + 'DAY_SECOND', + 'HOUR_MINUTE', + 'HOUR_SECOND', + 'MINUTE_SECOND', + ]; + } + + /** + * Main function for the events functionality + * + * @return void + */ + public function main() + { + global $db; + + $this->setGlobals(); + /** + * Process all requests + */ + $this->handleEditor(); + $this->export->events(); + /** + * Display a list of available events + */ + $items = $this->dbi->getEvents($db); + echo $this->rteList->get('event', $items); + /** + * Display a link for adding a new event, if + * the user has the privileges and a link to + * toggle the state of the event scheduler. + */ + echo $this->footer->events(); + } + + /** + * Handles editor requests for adding or editing an item + * + * @return void + */ + public function handleEditor() + { + global $errors, $db; + + if (! empty($_POST['editor_process_add']) + || ! empty($_POST['editor_process_edit']) + ) { + $sql_query = ''; + + $item_query = $this->getQueryFromRequest(); + + if (! count($errors)) { // set by PhpMyAdmin\Rte\Routines::getQueryFromRequest() + // Execute the created query + if (! empty($_POST['editor_process_edit'])) { + // Backup the old trigger, in case something goes wrong + $create_item = $this->dbi->getDefinition( + $db, + 'EVENT', + $_POST['item_original_name'] + ); + $drop_item = "DROP EVENT " + . Util::backquote($_POST['item_original_name']) + . ";\n"; + $result = $this->dbi->tryQuery($drop_item); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($drop_item) + ) + . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + // We dropped the old item, but were unable to create + // the new one. Try to restore the backup query + $result = $this->dbi->tryQuery($create_item); + $errors = $this->general->checkResult( + $result, + __( + 'Sorry, we failed to restore the dropped event.' + ), + $create_item, + $errors + ); + } else { + $message = Message::success( + __('Event %1$s has been modified.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $drop_item . $item_query; + } + } + } else { + // 'Add a new item' mode + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '<br><br>' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $message = Message::success( + __('Event %1$s has been created.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $item_query; + } + } + } + + if (count($errors)) { + $message = Message::error( + '<b>' + . __( + 'One or more errors have occurred while processing your request:' + ) + . '</b>' + ); + $message->addHtml('<ul>'); + foreach ($errors as $string) { + $message->addHtml('<li>' . $string . '</li>'); + } + $message->addHtml('</ul>'); + } + + $output = Util::getMessage($message, $sql_query); + $response = Response::getInstance(); + if ($response->isAjax()) { + if ($message->isSuccess()) { + $events = $this->dbi->getEvents($db, $_POST['item_name']); + $event = $events[0]; + $response->addJSON( + 'name', + htmlspecialchars( + mb_strtoupper($_POST['item_name']) + ) + ); + if (! empty($event)) { + $response->addJSON('new_row', $this->rteList->getEventRow($event)); + } + $response->addJSON('insert', ! empty($event)); + $response->addJSON('message', $output); + } else { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + } + exit; + } + } + /** + * Display a form used to add/edit a trigger, if necessary + */ + if (count($errors) + || (empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + && (! empty($_REQUEST['add_item']) + || ! empty($_REQUEST['edit_item']) + || ! empty($_POST['item_changetype']))) + ) { // FIXME: this must be simpler than that + $operation = ''; + if (! empty($_POST['item_changetype'])) { + $operation = 'change'; + } + // Get the data for the form (if any) + if (! empty($_REQUEST['add_item'])) { + $title = $this->words->get('add'); + $item = $this->getDataFromRequest(); + $mode = 'add'; + } elseif (! empty($_REQUEST['edit_item'])) { + $title = __("Edit event"); + if (! empty($_REQUEST['item_name']) + && empty($_POST['editor_process_edit']) + && empty($_POST['item_changetype']) + ) { + $item = $this->getDataFromName($_REQUEST['item_name']); + if ($item !== false) { + $item['item_original_name'] = $item['item_name']; + } + } else { + $item = $this->getDataFromRequest(); + } + $mode = 'edit'; + } + $this->general->sendEditor('EVN', $mode, $item, $title, $db, $operation); + } + } + + /** + * This function will generate the values that are required to for the editor + * + * @return array Data necessary to create the editor. + */ + public function getDataFromRequest() + { + $retval = []; + $indices = [ + 'item_name', + 'item_original_name', + 'item_status', + 'item_execute_at', + 'item_interval_value', + 'item_interval_field', + 'item_starts', + 'item_ends', + 'item_definition', + 'item_preserve', + 'item_comment', + 'item_definer', + ]; + foreach ($indices as $index) { + $retval[$index] = isset($_POST[$index]) ? $_POST[$index] : ''; + } + $retval['item_type'] = 'ONE TIME'; + $retval['item_type_toggle'] = 'RECURRING'; + if (isset($_POST['item_type']) && $_POST['item_type'] == 'RECURRING') { + $retval['item_type'] = 'RECURRING'; + $retval['item_type_toggle'] = 'ONE TIME'; + } + return $retval; + } + + /** + * This function will generate the values that are required to complete + * the "Edit event" form given the name of a event. + * + * @param string $name The name of the event. + * + * @return array|bool Data necessary to create the editor. + */ + public function getDataFromName($name) + { + global $db; + + $retval = []; + $columns = "`EVENT_NAME`, `STATUS`, `EVENT_TYPE`, `EXECUTE_AT`, " + . "`INTERVAL_VALUE`, `INTERVAL_FIELD`, `STARTS`, `ENDS`, " + . "`EVENT_DEFINITION`, `ON_COMPLETION`, `DEFINER`, `EVENT_COMMENT`"; + $where = "EVENT_SCHEMA " . Util::getCollateForIS() . "=" + . "'" . $this->dbi->escapeString($db) . "' " + . "AND EVENT_NAME='" . $this->dbi->escapeString($name) . "'"; + $query = "SELECT $columns FROM `INFORMATION_SCHEMA`.`EVENTS` WHERE $where;"; + $item = $this->dbi->fetchSingleRow($query); + if (! $item) { + return false; + } + $retval['item_name'] = $item['EVENT_NAME']; + $retval['item_status'] = $item['STATUS']; + $retval['item_type'] = $item['EVENT_TYPE']; + if ($retval['item_type'] == 'RECURRING') { + $retval['item_type_toggle'] = 'ONE TIME'; + } else { + $retval['item_type_toggle'] = 'RECURRING'; + } + $retval['item_execute_at'] = $item['EXECUTE_AT']; + $retval['item_interval_value'] = $item['INTERVAL_VALUE']; + $retval['item_interval_field'] = $item['INTERVAL_FIELD']; + $retval['item_starts'] = $item['STARTS']; + $retval['item_ends'] = $item['ENDS']; + $retval['item_preserve'] = ''; + if ($item['ON_COMPLETION'] == 'PRESERVE') { + $retval['item_preserve'] = " checked='checked'"; + } + $retval['item_definition'] = $item['EVENT_DEFINITION']; + $retval['item_definer'] = $item['DEFINER']; + $retval['item_comment'] = $item['EVENT_COMMENT']; + + return $retval; + } + + /** + * Displays a form used to add/edit an event + * + * @param string $mode If the editor will be used to edit an event + * or add a new one: 'edit' or 'add'. + * @param string $operation If the editor was previously invoked with + * JS turned off, this will hold the name of + * the current operation + * @param array $item Data for the event returned by + * getDataFromRequest() or getDataFromName() + * + * @return string HTML code for the editor. + */ + public function getEditorForm($mode, $operation, array $item) + { + global $db, $table, $event_status, $event_type, $event_interval; + + $modeToUpper = mb_strtoupper($mode); + + $response = Response::getInstance(); + + // Escape special characters + $need_escape = [ + 'item_original_name', + 'item_name', + 'item_type', + 'item_execute_at', + 'item_interval_value', + 'item_starts', + 'item_ends', + 'item_definition', + 'item_definer', + 'item_comment', + ]; + foreach ($need_escape as $index) { + $item[$index] = htmlentities((string) $item[$index], ENT_QUOTES); + } + $original_data = ''; + if ($mode == 'edit') { + $original_data = "<input name='item_original_name' " + . "type='hidden' value='{$item['item_original_name']}'>\n"; + } + // Handle some logic first + if ($operation == 'change') { + if ($item['item_type'] == 'RECURRING') { + $item['item_type'] = 'ONE TIME'; + $item['item_type_toggle'] = 'RECURRING'; + } else { + $item['item_type'] = 'RECURRING'; + $item['item_type_toggle'] = 'ONE TIME'; + } + } + if ($item['item_type'] == 'ONE TIME') { + $isrecurring_class = ' hide'; + $isonetime_class = ''; + } else { + $isrecurring_class = ''; + $isonetime_class = ' hide'; + } + // Create the output + $retval = ""; + $retval .= "<!-- START " . $modeToUpper . " EVENT FORM -->\n\n"; + $retval .= "<form class='rte_form' action='db_events.php' method='post'>\n"; + $retval .= "<input name='{$mode}_item' type='hidden' value='1'>\n"; + $retval .= $original_data; + $retval .= Url::getHiddenInputs($db, $table) . "\n"; + $retval .= "<fieldset>\n"; + $retval .= "<legend>" . __('Details') . "</legend>\n"; + $retval .= "<table class='rte_table'>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Event name') . "</td>\n"; + $retval .= " <td><input type='text' name='item_name' \n"; + $retval .= " value='{$item['item_name']}'\n"; + $retval .= " maxlength='64'></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Status') . "</td>\n"; + $retval .= " <td>\n"; + $retval .= " <select name='item_status'>\n"; + foreach ($event_status['display'] as $key => $value) { + $selected = ""; + if (! empty($item['item_status']) && $item['item_status'] == $value) { + $selected = " selected='selected'"; + } + $retval .= "<option$selected>$value</option>"; + } + $retval .= " </select>\n"; + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Event type') . "</td>\n"; + $retval .= " <td>\n"; + if ($response->isAjax()) { + $retval .= " <select name='item_type'>"; + foreach ($event_type as $key => $value) { + $selected = ""; + if (! empty($item['item_type']) && $item['item_type'] == $value) { + $selected = " selected='selected'"; + } + $retval .= "<option$selected>$value</option>"; + } + $retval .= " </select>\n"; + } else { + $retval .= " <input name='item_type' type='hidden' \n"; + $retval .= " value='{$item['item_type']}'>\n"; + $retval .= " <div class='font_weight_bold center half_width'>\n"; + $retval .= " {$item['item_type']}\n"; + $retval .= " </div>\n"; + $retval .= " <input type='submit'\n"; + $retval .= " name='item_changetype' class='half_width'\n"; + $retval .= " value='"; + $retval .= sprintf(__('Change to %s'), $item['item_type_toggle']); + $retval .= "'>\n"; + } + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr class='onetime_event_row $isonetime_class'>\n"; + $retval .= " <td>" . __('Execute at') . "</td>\n"; + $retval .= " <td class='nowrap'>\n"; + $retval .= " <input type='text' name='item_execute_at'\n"; + $retval .= " value='{$item['item_execute_at']}'\n"; + $retval .= " class='datetimefield'>\n"; + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr class='recurring_event_row $isrecurring_class'>\n"; + $retval .= " <td>" . __('Execute every') . "</td>\n"; + $retval .= " <td>\n"; + $retval .= " <input class='half_width' type='text'\n"; + $retval .= " name='item_interval_value'\n"; + $retval .= " value='{$item['item_interval_value']}'>\n"; + $retval .= " <select class='half_width' name='item_interval_field'>"; + foreach ($event_interval as $key => $value) { + $selected = ""; + if (! empty($item['item_interval_field']) + && $item['item_interval_field'] == $value + ) { + $selected = " selected='selected'"; + } + $retval .= "<option$selected>$value</option>"; + } + $retval .= " </select>\n"; + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr class='recurring_event_row$isrecurring_class'>\n"; + $retval .= " <td>" . _pgettext('Start of recurring event', 'Start'); + $retval .= " </td>\n"; + $retval .= " <td class='nowrap'>\n"; + $retval .= " <input type='text'\n name='item_starts'\n"; + $retval .= " value='{$item['item_starts']}'\n"; + $retval .= " class='datetimefield'>\n"; + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr class='recurring_event_row$isrecurring_class'>\n"; + $retval .= " <td>" . _pgettext('End of recurring event', 'End') . "</td>\n"; + $retval .= " <td class='nowrap'>\n"; + $retval .= " <input type='text' name='item_ends'\n"; + $retval .= " value='{$item['item_ends']}'\n"; + $retval .= " class='datetimefield'>\n"; + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Definition') . "</td>\n"; + $retval .= " <td><textarea name='item_definition' rows='15' cols='40'>"; + $retval .= $item['item_definition']; + $retval .= "</textarea></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('On completion preserve') . "</td>\n"; + $retval .= " <td><input type='checkbox'\n"; + $retval .= " name='item_preserve'{$item['item_preserve']}></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Definer') . "</td>\n"; + $retval .= " <td><input type='text' name='item_definer'\n"; + $retval .= " value='{$item['item_definer']}'></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Comment') . "</td>\n"; + $retval .= " <td><input type='text' name='item_comment' maxlength='64'\n"; + $retval .= " value='{$item['item_comment']}'></td>\n"; + $retval .= "</tr>\n"; + $retval .= "</table>\n"; + $retval .= "</fieldset>\n"; + if ($response->isAjax()) { + $retval .= "<input type='hidden' name='editor_process_{$mode}'\n"; + $retval .= " value='true'>\n"; + $retval .= "<input type='hidden' name='ajax_request' value='true'>\n"; + } else { + $retval .= "<fieldset class='tblFooters'>\n"; + $retval .= " <input type='submit' name='editor_process_{$mode}'\n"; + $retval .= " value='" . __('Go') . "'>\n"; + $retval .= "</fieldset>\n"; + } + $retval .= "</form>\n\n"; + $retval .= "<!-- END " . $modeToUpper . " EVENT FORM -->\n\n"; + + return $retval; + } + + /** + * Composes the query necessary to create an event from an HTTP request. + * + * @return string The CREATE EVENT query. + */ + public function getQueryFromRequest() + { + global $errors, $event_status, $event_type, $event_interval; + + $query = 'CREATE '; + if (! empty($_POST['item_definer'])) { + if (mb_strpos($_POST['item_definer'], '@') !== false + ) { + $arr = explode('@', $_POST['item_definer']); + $query .= 'DEFINER=' . Util::backquote($arr[0]); + $query .= '@' . Util::backquote($arr[1]) . ' '; + } else { + $errors[] = __('The definer must be in the "username@hostname" format!'); + } + } + $query .= 'EVENT '; + if (! empty($_POST['item_name'])) { + $query .= Util::backquote($_POST['item_name']) . ' '; + } else { + $errors[] = __('You must provide an event name!'); + } + $query .= 'ON SCHEDULE '; + if (! empty($_POST['item_type']) + && in_array($_POST['item_type'], $event_type) + ) { + if ($_POST['item_type'] == 'RECURRING') { + if (! empty($_POST['item_interval_value']) + && ! empty($_POST['item_interval_field']) + && in_array($_POST['item_interval_field'], $event_interval) + ) { + $query .= 'EVERY ' . intval($_POST['item_interval_value']) . ' '; + $query .= $_POST['item_interval_field'] . ' '; + } else { + $errors[] + = __('You must provide a valid interval value for the event.'); + } + if (! empty($_POST['item_starts'])) { + $query .= "STARTS '" + . $this->dbi->escapeString($_POST['item_starts']) + . "' "; + } + if (! empty($_POST['item_ends'])) { + $query .= "ENDS '" + . $this->dbi->escapeString($_POST['item_ends']) + . "' "; + } + } else { + if (! empty($_POST['item_execute_at'])) { + $query .= "AT '" + . $this->dbi->escapeString($_POST['item_execute_at']) + . "' "; + } else { + $errors[] + = __('You must provide a valid execution time for the event.'); + } + } + } else { + $errors[] = __('You must provide a valid type for the event.'); + } + $query .= 'ON COMPLETION '; + if (empty($_POST['item_preserve'])) { + $query .= 'NOT '; + } + $query .= 'PRESERVE '; + if (! empty($_POST['item_status'])) { + foreach ($event_status['display'] as $key => $value) { + if ($value == $_POST['item_status']) { + $query .= $event_status['query'][$key] . ' '; + break; + } + } + } + if (! empty($_POST['item_comment'])) { + $query .= "COMMENT '" . $this->dbi->escapeString( + $_POST['item_comment'] + ) . "' "; + } + $query .= 'DO '; + if (! empty($_POST['item_definition'])) { + $query .= $_POST['item_definition']; + } else { + $errors[] = __('You must provide an event definition.'); + } + + return $query; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Export.php b/srcs/phpmyadmin/libraries/classes/Rte/Export.php new file mode 100644 index 0000000..2ae19e6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Export.php @@ -0,0 +1,168 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Common functions for the export functionality for Routines, Triggers and Events. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Message; +use PhpMyAdmin\Response; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\Export class + * + * @package PhpMyAdmin + */ +class Export +{ + /** + * @var Words + */ + private $words; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * Export constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + $this->words = new Words(); + } + + /** + * This function is called from one of the other functions in this file + * and it completes the handling of the export functionality. + * + * @param string $export_data The SQL query to create the requested item + * + * @return void + */ + private function handle($export_data) + { + global $db; + + $response = Response::getInstance(); + + $item_name = htmlspecialchars(Util::backquote($_GET['item_name'])); + if ($export_data !== false) { + $export_data = htmlspecialchars(trim($export_data)); + $title = sprintf($this->words->get('export'), $item_name); + if ($response->isAjax()) { + $response->addJSON('message', $export_data); + $response->addJSON('title', $title); + exit; + } else { + $export_data = '<textarea cols="40" rows="15" style="width: 100%;">' + . $export_data . '</textarea>'; + echo "<fieldset>\n" + , "<legend>$title</legend>\n" + , $export_data + , "</fieldset>\n"; + } + } else { + $_db = htmlspecialchars(Util::backquote($db)); + $message = __('Error in processing request:') . ' ' + . sprintf($this->words->get('no_view'), $item_name, $_db); + $message = Message::error($message); + + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + $message->display(); + } + } + } + + /** + * If necessary, prepares event information and passes + * it to handle() for the actual export. + * + * @return void + */ + public function events() + { + global $db; + + if (! empty($_GET['export_item']) && ! empty($_GET['item_name'])) { + $item_name = $_GET['item_name']; + $export_data = $this->dbi->getDefinition($db, 'EVENT', $item_name); + if (! $export_data) { + $export_data = false; + } + $this->handle($export_data); + } + } + + /** + * If necessary, prepares routine information and passes + * it to handle() for the actual export. + * + * @return void + */ + public function routines() + { + global $db; + + if (! empty($_GET['export_item']) + && ! empty($_GET['item_name']) + && ! empty($_GET['item_type']) + ) { + if ($_GET['item_type'] == 'FUNCTION' || $_GET['item_type'] == 'PROCEDURE') { + $rtn_definition + = $this->dbi->getDefinition( + $db, + $_GET['item_type'], + $_GET['item_name'] + ); + if ($rtn_definition === null) { + $export_data = false; + } else { + $export_data = "DELIMITER $$\n" + . $rtn_definition + . "$$\nDELIMITER ;\n"; + } + + $this->handle($export_data); + } + } + } + + /** + * If necessary, prepares trigger information and passes + * it to handle() for the actual export. + * + * @return void + */ + public function triggers() + { + global $db, $table; + + if (! empty($_GET['export_item']) && ! empty($_GET['item_name'])) { + $item_name = $_GET['item_name']; + $triggers = $this->dbi->getTriggers($db, $table, ''); + $export_data = false; + foreach ($triggers as $trigger) { + if ($trigger['name'] === $item_name) { + $export_data = $trigger['create']; + break; + } + } + $this->handle($export_data); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Footer.php b/srcs/phpmyadmin/libraries/classes/Rte/Footer.php new file mode 100644 index 0000000..5181b00 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Footer.php @@ -0,0 +1,160 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Common functions for generating the footer for Routines, Triggers and Events. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\Footer class + * + * @package PhpMyAdmin + */ +class Footer +{ + /** + * @var Words + */ + private $words; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * Footer constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + $this->words = new Words(); + } + + /** + * Creates a fieldset for adding a new item, if the user has the privileges. + * + * @param string $docu String used to create a link to the MySQL docs + * @param string $priv Privilege to check for adding a new item + * @param string $name MySQL name of the item + * + * @return string An HTML snippet with the link to add a new item + */ + private function getLinks($docu, $priv, $name) + { + global $db, $table, $url_query; + + $icon = mb_strtolower($name) . '_add'; + $retval = ""; + $retval .= "<!-- ADD " . $name . " FORM START -->\n"; + $retval .= "<fieldset class='left'>\n"; + $retval .= "<legend>" . _pgettext('Create new procedure', 'New') . "</legend>\n"; + $retval .= " <div class='wrap'>\n"; + if (Util::currentUserHasPrivilege($priv, $db, $table)) { + $retval .= ' <a class="ajax add_anchor" '; + $retval .= "href='db_" . mb_strtolower($name) . "s.php"; + $retval .= "$url_query&add_item=1' "; + $retval .= "onclick='$.datepicker.initialized = false;'>"; + $icon = 'b_' . $icon; + $retval .= Util::getIcon($icon); + $retval .= $this->words->get('add') . "</a>\n"; + } else { + $icon = 'bd_' . $icon; + $retval .= Util::getIcon($icon); + $retval .= $this->words->get('add') . "\n"; + } + $retval .= " " . Util::showMySQLDocu($docu) . "\n"; + $retval .= " </div>\n"; + $retval .= "</fieldset>\n"; + $retval .= "<!-- ADD " . $name . " FORM END -->\n\n"; + + return $retval; + } + + /** + * Creates a fieldset for adding a new routine, if the user has the privileges. + * + * @return string HTML code with containing the footer fieldset + */ + public function routines() + { + return $this->getLinks('CREATE_PROCEDURE', 'CREATE ROUTINE', 'ROUTINE'); + } + + /** + * Creates a fieldset for adding a new trigger, if the user has the privileges. + * + * @return string HTML code with containing the footer fieldset + */ + public function triggers() + { + return $this->getLinks('CREATE_TRIGGER', 'TRIGGER', 'TRIGGER'); + } + + /** + * Creates a fieldset for adding a new event, if the user has the privileges. + * + * @return string HTML code with containing the footer fieldset + */ + public function events() + { + global $db, $url_query; + + /** + * For events, we show the usual 'Add event' form and also + * a form for toggling the state of the event scheduler + */ + // Init options for the event scheduler toggle functionality + $es_state = $this->dbi->fetchValue( + "SHOW GLOBAL VARIABLES LIKE 'event_scheduler'", + 0, + 1 + ); + $es_state = mb_strtolower($es_state); + $options = [ + 0 => [ + 'label' => __('OFF'), + 'value' => "SET GLOBAL event_scheduler=\"OFF\"", + 'selected' => $es_state != 'on', + ], + 1 => [ + 'label' => __('ON'), + 'value' => "SET GLOBAL event_scheduler=\"ON\"", + 'selected' => $es_state == 'on', + ], + ]; + // Generate output + $retval = "<!-- FOOTER LINKS START -->\n"; + $retval .= "<div class='doubleFieldset'>\n"; + // show the usual footer + $retval .= $this->getLinks('CREATE_EVENT', 'EVENT', 'EVENT'); + $retval .= " <fieldset class='right'>\n"; + $retval .= " <legend>\n"; + $retval .= " " . __('Event scheduler status') . "\n"; + $retval .= " </legend>\n"; + $retval .= " <div class='wrap'>\n"; + // show the toggle button + $retval .= Util::toggleButton( + "sql.php$url_query&goto=db_events.php" . urlencode("?db=$db"), + 'sql_query', + $options, + 'Functions.slidingMessage(data.sql_query);' + ); + $retval .= " </div>\n"; + $retval .= " </fieldset>\n"; + $retval .= " <div class='clearfloat'></div>\n"; + $retval .= "</div>"; + $retval .= "<!-- FOOTER LINKS END -->\n"; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/General.php b/srcs/phpmyadmin/libraries/classes/Rte/General.php new file mode 100644 index 0000000..37962b5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/General.php @@ -0,0 +1,118 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * General functions. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Message; +use PhpMyAdmin\Response; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\General class + * + * @package PhpMyAdmin + */ +class General +{ + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * General constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + } + + /** + * Check result + * + * @param resource|bool $result Query result + * @param string $error Error to add + * @param string $createStatement Query + * @param array $errors Errors + * + * @return array + */ + public function checkResult($result, $error, $createStatement, array $errors) + { + if ($result) { + return $errors; + } + + // OMG, this is really bad! We dropped the query, + // failed to create a new one + // and now even the backup query does not execute! + // This should not happen, but we better handle + // this just in case. + $errors[] = $error . '<br>' + . __('The backed up query was:') + . "\"" . htmlspecialchars($createStatement) . "\"" . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + + return $errors; + } + + /** + * Send TRI or EVN editor via ajax or by echoing. + * + * @param string $type TRI or EVN + * @param string $mode Editor mode 'add' or 'edit' + * @param array $item Data necessary to create the editor + * @param string $title Title of the editor + * @param string $db Database + * @param string $operation Operation 'change' or '' + * + * @return void + */ + public function sendEditor($type, $mode, array $item, $title, $db, $operation = null) + { + $events = new Events($this->dbi); + $triggers = new Triggers($this->dbi); + $words = new Words(); + $response = Response::getInstance(); + if ($item !== false) { + // Show form + if ($type == 'TRI') { + $editor = $triggers->getEditorForm($mode, $item); + } else { // EVN + $editor = $events->getEditorForm($mode, $operation, $item); + } + if ($response->isAjax()) { + $response->addJSON('message', $editor); + $response->addJSON('title', $title); + } else { + echo "\n\n<h2>$title</h2>\n\n$editor"; + unset($_POST); + } + exit; + } else { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $words->get('not_found'), + htmlspecialchars(Util::backquote($_REQUEST['item_name'])), + htmlspecialchars(Util::backquote($db)) + ); + $message = Message::error($message); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + $message->display(); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Routines.php b/srcs/phpmyadmin/libraries/classes/Rte/Routines.php new file mode 100644 index 0000000..24b0dd5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Routines.php @@ -0,0 +1,1743 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Functions for routine management. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\Charsets; +use PhpMyAdmin\Charsets\Charset; +use PhpMyAdmin\Core; +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Message; +use PhpMyAdmin\Response; +use PhpMyAdmin\SqlParser\Parser; +use PhpMyAdmin\SqlParser\Statements\CreateStatement; +use PhpMyAdmin\SqlParser\Utils\Routine; +use PhpMyAdmin\Template; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\Routines class + * + * @package PhpMyAdmin + */ +class Routines +{ + /** + * @var Export + */ + private $export; + + /** + * @var Footer + */ + private $footer; + + /** + * @var General + */ + private $general; + + /** + * @var RteList + */ + private $rteList; + + /** + * @var Words + */ + private $words; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * Routines constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + $this->export = new Export($this->dbi); + $this->footer = new Footer($this->dbi); + $this->general = new General($this->dbi); + $this->rteList = new RteList($this->dbi); + $this->words = new Words(); + } + + /** + * Sets required globals + * + * @return void + */ + public function setGlobals() + { + global $param_directions, $param_opts_num, $param_sqldataaccess; + + $param_directions = [ + 'IN', + 'OUT', + 'INOUT', + ]; + $param_opts_num = [ + 'UNSIGNED', + 'ZEROFILL', + 'UNSIGNED ZEROFILL', + ]; + $param_sqldataaccess = [ + 'NO SQL', + 'CONTAINS SQL', + 'READS SQL DATA', + 'MODIFIES SQL DATA', + ]; + } + + /** + * Main function for the routines functionality + * + * @param string $type 'FUNCTION' for functions, + * 'PROCEDURE' for procedures, + * null for both + * + * @return void + */ + public function main($type) + { + global $db; + + $this->setGlobals(); + /** + * Process all requests + */ + $this->handleEditor(); + $this->handleExecute(); + $this->export->routines(); + /** + * Display a list of available routines + */ + if (! Core::isValid($type, ['FUNCTION', 'PROCEDURE'])) { + $type = null; + } + $items = $this->dbi->getRoutines($db, $type); + echo $this->rteList->get('routine', $items); + /** + * Display the form for adding a new routine, if the user has the privileges. + */ + echo $this->footer->routines(); + /** + * Display a warning for users with PHP's old "mysql" extension. + */ + if (! DatabaseInterface::checkDbExtension('mysqli')) { + trigger_error( + __( + 'You are using PHP\'s deprecated \'mysql\' extension, ' + . 'which is not capable of handling multi queries. ' + . '[strong]The execution of some stored routines may fail![/strong] ' + . 'Please use the improved \'mysqli\' extension to ' + . 'avoid any problems.' + ), + E_USER_WARNING + ); + } + } + + /** + * Handles editor requests for adding or editing an item + * + * @return void + */ + public function handleEditor() + { + global $db, $errors; + + $errors = $this->handleRequestCreateOrEdit($errors, $db); + $response = Response::getInstance(); + + /** + * Display a form used to add/edit a routine, if necessary + */ + // FIXME: this must be simpler than that + if (count($errors) + || ( empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + && (! empty($_REQUEST['add_item']) || ! empty($_REQUEST['edit_item']) + || ! empty($_POST['routine_addparameter']) + || ! empty($_POST['routine_removeparameter']) + || ! empty($_POST['routine_changetype']))) + ) { + // Handle requests to add/remove parameters and changing routine type + // This is necessary when JS is disabled + $operation = ''; + if (! empty($_POST['routine_addparameter'])) { + $operation = 'add'; + } elseif (! empty($_POST['routine_removeparameter'])) { + $operation = 'remove'; + } elseif (! empty($_POST['routine_changetype'])) { + $operation = 'change'; + } + // Get the data for the form (if any) + if (! empty($_REQUEST['add_item'])) { + $title = $this->words->get('add'); + $routine = $this->getDataFromRequest(); + $mode = 'add'; + } elseif (! empty($_REQUEST['edit_item'])) { + $title = __("Edit routine"); + if (! $operation && ! empty($_GET['item_name']) + && empty($_POST['editor_process_edit']) + ) { + $routine = $this->getDataFromName( + $_GET['item_name'], + $_GET['item_type'] + ); + if ($routine !== false) { + $routine['item_original_name'] = $routine['item_name']; + $routine['item_original_type'] = $routine['item_type']; + } + } else { + $routine = $this->getDataFromRequest(); + } + $mode = 'edit'; + } + if ($routine !== false) { + // Show form + $editor = $this->getEditorForm($mode, $operation, $routine); + if ($response->isAjax()) { + $response->addJSON('message', $editor); + $response->addJSON('title', $title); + $response->addJSON('paramTemplate', $this->getParameterRow()); + $response->addJSON('type', $routine['item_type']); + } else { + echo "\n\n<h2>$title</h2>\n\n$editor"; + } + exit; + } else { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $this->words->get('no_edit'), + htmlspecialchars( + Util::backquote($_REQUEST['item_name']) + ), + htmlspecialchars(Util::backquote($db)) + ); + + $message = Message::error($message); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + $message->display(); + } + } + } + } + + /** + * Handle request to create or edit a routine + * + * @param array $errors Errors + * @param string $db DB name + * + * @return array + */ + public function handleRequestCreateOrEdit(array $errors, $db) + { + if (empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + ) { + return $errors; + } + + $sql_query = ''; + $routine_query = $this->getQueryFromRequest(); + if (! count($errors)) { + // Execute the created query + if (! empty($_POST['editor_process_edit'])) { + $isProcOrFunc = in_array( + $_POST['item_original_type'], + [ + 'PROCEDURE', + 'FUNCTION', + ] + ); + + if (! $isProcOrFunc) { + $errors[] = sprintf( + __('Invalid routine type: "%s"'), + htmlspecialchars($_POST['item_original_type']) + ); + } else { + // Backup the old routine, in case something goes wrong + $create_routine = $this->dbi->getDefinition( + $db, + $_POST['item_original_type'], + $_POST['item_original_name'] + ); + + $privilegesBackup = $this->backupPrivileges(); + + $drop_routine = "DROP {$_POST['item_original_type']} " + . Util::backquote($_POST['item_original_name']) + . ";\n"; + $result = $this->dbi->tryQuery($drop_routine); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($drop_routine) + ) + . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + list($newErrors, $message) = $this->create( + $routine_query, + $create_routine, + $privilegesBackup + ); + if (empty($newErrors)) { + $sql_query = $drop_routine . $routine_query; + } else { + $errors = array_merge($errors, $newErrors); + } + unset($newErrors); + if (null === $message) { + unset($message); + } + } + } + } else { + // 'Add a new routine' mode + $result = $this->dbi->tryQuery($routine_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($routine_query) + ) + . '<br><br>' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $message = Message::success( + __('Routine %1$s has been created.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $routine_query; + } + } + } + + if (count($errors)) { + $message = Message::error( + __( + 'One or more errors have occurred while' + . ' processing your request:' + ) + ); + $message->addHtml('<ul>'); + foreach ($errors as $string) { + $message->addHtml('<li>' . $string . '</li>'); + } + $message->addHtml('</ul>'); + } + + $output = Util::getMessage($message, $sql_query); + $response = Response::getInstance(); + if (! $response->isAjax()) { + return $errors; + } + + if (! $message->isSuccess()) { + $response->setRequestStatus(false); + $response->addJSON('message', $output); + exit; + } + + $routines = $this->dbi->getRoutines( + $db, + $_POST['item_type'], + $_POST['item_name'] + ); + $routine = $routines[0]; + $response->addJSON( + 'name', + htmlspecialchars( + mb_strtoupper($_POST['item_name']) + ) + ); + $response->addJSON('new_row', $this->rteList->getRoutineRow($routine)); + $response->addJSON('insert', ! empty($routine)); + $response->addJSON('message', $output); + exit; + } + + /** + * Backup the privileges + * + * @return array + */ + public function backupPrivileges() + { + if (! $GLOBALS['proc_priv'] || ! $GLOBALS['is_reload_priv']) { + return []; + } + + // Backup the Old Privileges before dropping + // if $_POST['item_adjust_privileges'] set + if (! isset($_POST['item_adjust_privileges']) + || empty($_POST['item_adjust_privileges']) + ) { + return []; + } + + $privilegesBackupQuery = 'SELECT * FROM ' . Util::backquote( + 'mysql' + ) + . '.' . Util::backquote('procs_priv') + . ' where Routine_name = "' . $_POST['item_original_name'] + . '" AND Routine_type = "' . $_POST['item_original_type'] + . '";'; + + $privilegesBackup = $this->dbi->fetchResult( + $privilegesBackupQuery, + 0 + ); + + return $privilegesBackup; + } + + /** + * Create the routine + * + * @param string $routine_query Query to create routine + * @param string $create_routine Query to restore routine + * @param array $privilegesBackup Privileges backup + * + * @return array + */ + public function create( + $routine_query, + $create_routine, + array $privilegesBackup + ) { + $result = $this->dbi->tryQuery($routine_query); + if (! $result) { + $errors = []; + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($routine_query) + ) + . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + // We dropped the old routine, + // but were unable to create the new one + // Try to restore the backup query + $result = $this->dbi->tryQuery($create_routine); + $errors = $this->general->checkResult( + $result, + __( + 'Sorry, we failed to restore' + . ' the dropped routine.' + ), + $create_routine, + $errors + ); + + return [ + $errors, + null, + ]; + } + + // Default value + $resultAdjust = false; + + if ($GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + // Insert all the previous privileges + // but with the new name and the new type + foreach ($privilegesBackup as $priv) { + $adjustProcPrivilege = 'INSERT INTO ' + . Util::backquote('mysql') . '.' + . Util::backquote('procs_priv') + . ' VALUES("' . $priv[0] . '", "' + . $priv[1] . '", "' . $priv[2] . '", "' + . $_POST['item_name'] . '", "' + . $_POST['item_type'] . '", "' + . $priv[5] . '", "' + . $priv[6] . '", "' + . $priv[7] . '");'; + $resultAdjust = $this->dbi->query( + $adjustProcPrivilege + ); + } + } + + $message = $this->flushPrivileges($resultAdjust); + + return [ + [], + $message, + ]; + } + + /** + * Flush privileges and get message + * + * @param bool $flushPrivileges Flush privileges + * + * @return Message + */ + public function flushPrivileges($flushPrivileges) + { + if ($flushPrivileges) { + // Flush the Privileges + $flushPrivQuery = 'FLUSH PRIVILEGES;'; + $this->dbi->query($flushPrivQuery); + + $message = Message::success( + __( + 'Routine %1$s has been modified. Privileges have been adjusted.' + ) + ); + } else { + $message = Message::success( + __('Routine %1$s has been modified.') + ); + } + $message->addParam( + Util::backquote($_POST['item_name']) + ); + + return $message; + } + + /** + * This function will generate the values that are required to + * complete the editor form. It is especially necessary to handle + * the 'Add another parameter', 'Remove last parameter' and + * 'Change routine type' functionalities when JS is disabled. + * + * @return array Data necessary to create the routine editor. + */ + public function getDataFromRequest() + { + global $param_directions, $param_sqldataaccess; + + $retval = []; + $indices = [ + 'item_name', + 'item_original_name', + 'item_returnlength', + 'item_returnopts_num', + 'item_returnopts_text', + 'item_definition', + 'item_comment', + 'item_definer', + ]; + foreach ($indices as $index) { + $retval[$index] = isset($_POST[$index]) ? $_POST[$index] : ''; + } + + $retval['item_type'] = 'PROCEDURE'; + $retval['item_type_toggle'] = 'FUNCTION'; + if (isset($_REQUEST['item_type']) && $_REQUEST['item_type'] == 'FUNCTION') { + $retval['item_type'] = 'FUNCTION'; + $retval['item_type_toggle'] = 'PROCEDURE'; + } + $retval['item_original_type'] = 'PROCEDURE'; + if (isset($_POST['item_original_type']) + && $_POST['item_original_type'] == 'FUNCTION' + ) { + $retval['item_original_type'] = 'FUNCTION'; + } + $retval['item_num_params'] = 0; + $retval['item_param_dir'] = []; + $retval['item_param_name'] = []; + $retval['item_param_type'] = []; + $retval['item_param_length'] = []; + $retval['item_param_opts_num'] = []; + $retval['item_param_opts_text'] = []; + if (isset($_POST['item_param_name']) + && isset($_POST['item_param_type']) + && isset($_POST['item_param_length']) + && isset($_POST['item_param_opts_num']) + && isset($_POST['item_param_opts_text']) + && is_array($_POST['item_param_name']) + && is_array($_POST['item_param_type']) + && is_array($_POST['item_param_length']) + && is_array($_POST['item_param_opts_num']) + && is_array($_POST['item_param_opts_text']) + ) { + if ($_POST['item_type'] == 'PROCEDURE') { + $retval['item_param_dir'] = $_POST['item_param_dir']; + foreach ($retval['item_param_dir'] as $key => $value) { + if (! in_array($value, $param_directions, true)) { + $retval['item_param_dir'][$key] = ''; + } + } + } + $retval['item_param_name'] = $_POST['item_param_name']; + $retval['item_param_type'] = $_POST['item_param_type']; + foreach ($retval['item_param_type'] as $key => $value) { + if (! in_array($value, Util::getSupportedDatatypes(), true)) { + $retval['item_param_type'][$key] = ''; + } + } + $retval['item_param_length'] = $_POST['item_param_length']; + $retval['item_param_opts_num'] = $_POST['item_param_opts_num']; + $retval['item_param_opts_text'] = $_POST['item_param_opts_text']; + $retval['item_num_params'] = max( + count($retval['item_param_name']), + count($retval['item_param_type']), + count($retval['item_param_length']), + count($retval['item_param_opts_num']), + count($retval['item_param_opts_text']) + ); + } + $retval['item_returntype'] = ''; + if (isset($_POST['item_returntype']) + && in_array($_POST['item_returntype'], Util::getSupportedDatatypes()) + ) { + $retval['item_returntype'] = $_POST['item_returntype']; + } + + $retval['item_isdeterministic'] = ''; + if (isset($_POST['item_isdeterministic']) + && mb_strtolower($_POST['item_isdeterministic']) == 'on' + ) { + $retval['item_isdeterministic'] = " checked='checked'"; + } + $retval['item_securitytype_definer'] = ''; + $retval['item_securitytype_invoker'] = ''; + if (isset($_POST['item_securitytype'])) { + if ($_POST['item_securitytype'] === 'DEFINER') { + $retval['item_securitytype_definer'] = " selected='selected'"; + } elseif ($_POST['item_securitytype'] === 'INVOKER') { + $retval['item_securitytype_invoker'] = " selected='selected'"; + } + } + $retval['item_sqldataaccess'] = ''; + if (isset($_POST['item_sqldataaccess']) + && in_array($_POST['item_sqldataaccess'], $param_sqldataaccess, true) + ) { + $retval['item_sqldataaccess'] = $_POST['item_sqldataaccess']; + } + + return $retval; + } + + /** + * This function will generate the values that are required to complete + * the "Edit routine" form given the name of a routine. + * + * @param string $name The name of the routine. + * @param string $type Type of routine (ROUTINE|PROCEDURE) + * @param bool $all Whether to return all data or just the info about parameters. + * + * @return array|bool Data necessary to create the routine editor. + */ + public function getDataFromName($name, $type, $all = true) + { + global $db; + + $retval = []; + + // Build and execute the query + $fields = "SPECIFIC_NAME, ROUTINE_TYPE, DTD_IDENTIFIER, " + . "ROUTINE_DEFINITION, IS_DETERMINISTIC, SQL_DATA_ACCESS, " + . "ROUTINE_COMMENT, SECURITY_TYPE"; + $where = "ROUTINE_SCHEMA " . Util::getCollateForIS() . "=" + . "'" . $this->dbi->escapeString($db) . "' " + . "AND SPECIFIC_NAME='" . $this->dbi->escapeString($name) . "'" + . "AND ROUTINE_TYPE='" . $this->dbi->escapeString($type) . "'"; + $query = "SELECT $fields FROM INFORMATION_SCHEMA.ROUTINES WHERE $where;"; + + $routine = $this->dbi->fetchSingleRow($query, 'ASSOC'); + + if (! $routine) { + return false; + } + + // Get required data + $retval['item_name'] = $routine['SPECIFIC_NAME']; + $retval['item_type'] = $routine['ROUTINE_TYPE']; + + $definition + = $this->dbi->getDefinition( + $db, + $routine['ROUTINE_TYPE'], + $routine['SPECIFIC_NAME'] + ); + + if ($definition === null) { + return false; + } + + $parser = new Parser($definition); + + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + + $params = Routine::getParameters($stmt); + $retval['item_num_params'] = $params['num']; + $retval['item_param_dir'] = $params['dir']; + $retval['item_param_name'] = $params['name']; + $retval['item_param_type'] = $params['type']; + $retval['item_param_length'] = $params['length']; + $retval['item_param_length_arr'] = $params['length_arr']; + $retval['item_param_opts_num'] = $params['opts']; + $retval['item_param_opts_text'] = $params['opts']; + + // Get extra data + if (! $all) { + return $retval; + } + + if ($retval['item_type'] == 'FUNCTION') { + $retval['item_type_toggle'] = 'PROCEDURE'; + } else { + $retval['item_type_toggle'] = 'FUNCTION'; + } + $retval['item_returntype'] = ''; + $retval['item_returnlength'] = ''; + $retval['item_returnopts_num'] = ''; + $retval['item_returnopts_text'] = ''; + + if (! empty($routine['DTD_IDENTIFIER'])) { + $options = []; + foreach ($stmt->return->options->options as $opt) { + $options[] = is_string($opt) ? $opt : $opt['value']; + } + + $retval['item_returntype'] = $stmt->return->name; + $retval['item_returnlength'] = implode(',', $stmt->return->parameters); + $retval['item_returnopts_num'] = implode(' ', $options); + $retval['item_returnopts_text'] = implode(' ', $options); + } + + $retval['item_definer'] = $stmt->options->has('DEFINER'); + $retval['item_definition'] = $routine['ROUTINE_DEFINITION']; + $retval['item_isdeterministic'] = ''; + if ($routine['IS_DETERMINISTIC'] == 'YES') { + $retval['item_isdeterministic'] = " checked='checked'"; + } + $retval['item_securitytype_definer'] = ''; + $retval['item_securitytype_invoker'] = ''; + if ($routine['SECURITY_TYPE'] == 'DEFINER') { + $retval['item_securitytype_definer'] = " selected='selected'"; + } elseif ($routine['SECURITY_TYPE'] == 'INVOKER') { + $retval['item_securitytype_invoker'] = " selected='selected'"; + } + $retval['item_sqldataaccess'] = $routine['SQL_DATA_ACCESS']; + $retval['item_comment'] = $routine['ROUTINE_COMMENT']; + + return $retval; + } + + /** + * Creates one row for the parameter table used in the routine editor. + * + * @param array $routine Data for the routine returned by + * getDataFromRequest() or getDataFromName() + * @param mixed $index Either a numeric index of the row being processed + * or NULL to create a template row for AJAX request + * @param string $class Class used to hide the direction column, if the + * row is for a stored function. + * + * @return string HTML code of one row of parameter table for the editor. + */ + public function getParameterRow(array $routine = [], $index = null, $class = '') + { + global $param_directions, $param_opts_num; + + if ($index === null) { + // template row for AJAX request + $i = 0; + $index = '%s'; + $drop_class = ''; + $routine = [ + 'item_param_dir' => [0 => ''], + 'item_param_name' => [0 => ''], + 'item_param_type' => [0 => ''], + 'item_param_length' => [0 => ''], + 'item_param_opts_num' => [0 => ''], + 'item_param_opts_text' => [0 => ''], + ]; + } elseif (! empty($routine)) { + // regular row for routine editor + $drop_class = ' hide'; + $i = $index; + } else { + // No input data. This shouldn't happen, + // but better be safe than sorry. + return ''; + } + + $allCharsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + $charsets = []; + /** @var Charset $charset */ + foreach ($allCharsets as $charset) { + $charsets[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'is_selected' => $charset->getName() === $routine['item_param_opts_text'][$i], + ]; + } + + $template = new Template(); + return $template->render('rte/routines/parameter_row', [ + 'class' => $class, + 'index' => $index, + 'param_directions' => $param_directions, + 'param_opts_num' => $param_opts_num, + 'item_param_dir' => $routine['item_param_dir'][$i] ?? '', + 'item_param_name' => $routine['item_param_name'][$i] ?? '', + 'item_param_length' => $routine['item_param_length'][$i] ?? '', + 'item_param_opts_num' => $routine['item_param_opts_num'][$i] ?? '', + 'supported_datatypes' => Util::getSupportedDatatypes( + true, + $routine['item_param_type'][$i] + ), + 'charsets' => $charsets, + 'drop_class' => $drop_class, + ]); + } + + /** + * Displays a form used to add/edit a routine + * + * @param string $mode If the editor will be used to edit a routine + * or add a new one: 'edit' or 'add'. + * @param string $operation If the editor was previously invoked with + * JS turned off, this will hold the name of + * the current operation + * @param array $routine Data for the routine returned by + * getDataFromRequest() or getDataFromName() + * + * @return string HTML code for the editor. + */ + public function getEditorForm($mode, $operation, array $routine) + { + global $db, $errors, $param_sqldataaccess, $param_opts_num; + + $response = Response::getInstance(); + + // Escape special characters + $need_escape = [ + 'item_original_name', + 'item_name', + 'item_returnlength', + 'item_definition', + 'item_definer', + 'item_comment', + ]; + foreach ($need_escape as $key => $index) { + $routine[$index] = htmlentities($routine[$index], ENT_QUOTES, 'UTF-8'); + } + for ($i = 0; $i < $routine['item_num_params']; $i++) { + $routine['item_param_name'][$i] = htmlentities( + $routine['item_param_name'][$i], + ENT_QUOTES + ); + $routine['item_param_length'][$i] = htmlentities( + $routine['item_param_length'][$i], + ENT_QUOTES + ); + } + + // Handle some logic first + if ($operation == 'change') { + if ($routine['item_type'] == 'PROCEDURE') { + $routine['item_type'] = 'FUNCTION'; + $routine['item_type_toggle'] = 'PROCEDURE'; + } else { + $routine['item_type'] = 'PROCEDURE'; + $routine['item_type_toggle'] = 'FUNCTION'; + } + } elseif ($operation == 'add' + || ($routine['item_num_params'] == 0 && $mode == 'add' && ! $errors) + ) { + $routine['item_param_dir'][] = ''; + $routine['item_param_name'][] = ''; + $routine['item_param_type'][] = ''; + $routine['item_param_length'][] = ''; + $routine['item_param_opts_num'][] = ''; + $routine['item_param_opts_text'][] = ''; + $routine['item_num_params']++; + } elseif ($operation == 'remove') { + unset($routine['item_param_dir'][$routine['item_num_params'] - 1]); + unset($routine['item_param_name'][$routine['item_num_params'] - 1]); + unset($routine['item_param_type'][$routine['item_num_params'] - 1]); + unset($routine['item_param_length'][$routine['item_num_params'] - 1]); + unset($routine['item_param_opts_num'][$routine['item_num_params'] - 1]); + unset($routine['item_param_opts_text'][$routine['item_num_params'] - 1]); + $routine['item_num_params']--; + } + $disableRemoveParam = ''; + if (! $routine['item_num_params']) { + $disableRemoveParam = " class='isdisableremoveparam_class' disabled=disabled"; + } + $original_routine = ''; + if ($mode == 'edit') { + $original_routine = "<input name='item_original_name' " + . "type='hidden' " + . "value='{$routine['item_original_name']}'>\n" + . "<input name='item_original_type' " + . "type='hidden' " + . "value='{$routine['item_original_type']}'>\n"; + } + $isfunction_class = ''; + $isprocedure_class = ''; + $isfunction_select = ''; + $isprocedure_select = ''; + if ($routine['item_type'] == 'PROCEDURE') { + $isfunction_class = ' hide'; + $isprocedure_select = " selected='selected'"; + } else { + $isprocedure_class = ' hide'; + $isfunction_select = " selected='selected'"; + } + + // Create the output + $retval = ""; + $retval .= "<!-- START " . mb_strtoupper($mode) + . " ROUTINE FORM -->\n\n"; + $retval .= "<form class='rte_form' action='db_routines.php' method='post'>\n"; + $retval .= "<input name='{$mode}_item' type='hidden' value='1'>\n"; + $retval .= $original_routine; + $retval .= Url::getHiddenInputs($db) . "\n"; + $retval .= "<fieldset>\n"; + $retval .= "<legend>" . __('Details') . "</legend>\n"; + $retval .= "<table class='rte_table'>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Routine name') . "</td>\n"; + $retval .= " <td><input type='text' name='item_name' maxlength='64'\n"; + $retval .= " value='{$routine['item_name']}'></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Type') . "</td>\n"; + $retval .= " <td>\n"; + if ($response->isAjax()) { + $retval .= " <select name='item_type'>\n" + . "<option value='PROCEDURE'$isprocedure_select>PROCEDURE</option>\n" + . "<option value='FUNCTION'$isfunction_select>FUNCTION</option>\n" + . "</select>\n"; + } else { + $retval .= "<input name='item_type' type='hidden'" + . " value='{$routine['item_type']}'>\n" + . "<div class='font_weight_bold center half_width'>\n" + . $routine['item_type'] . "\n" + . "</div>\n" + . "<input type='submit' name='routine_changetype'\n" + . " value='" . sprintf(__('Change to %s'), $routine['item_type_toggle']) + . "'>\n"; + } + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Parameters') . "</td>\n"; + $retval .= " <td>\n"; + // parameter handling start + $retval .= " <table class='routine_params_table'>\n"; + $retval .= " <thead>\n"; + $retval .= " <tr>\n"; + $retval .= " <td></td>\n"; + $retval .= " <th class='routine_direction_cell$isprocedure_class'>" + . __('Direction') . "</th>\n"; + $retval .= " <th>" . __('Name') . "</th>\n"; + $retval .= " <th>" . __('Type') . "</th>\n"; + $retval .= " <th>" . __('Length/Values') . "</th>\n"; + $retval .= " <th colspan='2'>" . __('Options') . "</th>\n"; + $retval .= " <th class='routine_param_remove hide'> </th>\n"; + $retval .= " </tr>"; + $retval .= " </thead>\n"; + $retval .= " <tbody>\n"; + for ($i = 0; $i < $routine['item_num_params']; $i++) { // each parameter + $retval .= $this->getParameterRow($routine, $i, $isprocedure_class); + } + $retval .= " </tbody>\n"; + $retval .= " </table>"; + $retval .= " </td>"; + $retval .= "</tr>"; + $retval .= "<tr>"; + $retval .= " <td> </td>"; + $retval .= " <td>"; + $retval .= " <input type='button'"; + $retval .= " name='routine_addparameter'"; + $retval .= " value='" . __('Add parameter') . "'>"; + $retval .= " <input " . $disableRemoveParam . ""; + $retval .= " type='submit' "; + $retval .= " name='routine_removeparameter'"; + $retval .= " value='" . __('Remove last parameter') . "'>"; + $retval .= " </td>"; + $retval .= "</tr>"; + // parameter handling end + $retval .= "<tr class='routine_return_row" . $isfunction_class . "'>"; + $retval .= " <td>" . __('Return type') . "</td>"; + $retval .= " <td><select name='item_returntype'>"; + $retval .= Util::getSupportedDatatypes(true, $routine['item_returntype']); + $retval .= " </select></td>"; + $retval .= "</tr>"; + $retval .= "<tr class='routine_return_row" . $isfunction_class . "'>"; + $retval .= " <td>" . __('Return length/values') . "</td>"; + $retval .= " <td><input type='text' name='item_returnlength'"; + $retval .= " value='" . $routine['item_returnlength'] . "'></td>"; + $retval .= " <td class='hide no_len'>---</td>"; + $retval .= "</tr>"; + $retval .= "<tr class='routine_return_row" . $isfunction_class . "'>"; + $retval .= " <td>" . __('Return options') . "</td>"; + $retval .= " <td><div>"; + $retval .= '<select lang="en" dir="ltr" name="item_returnopts_text">' . "\n"; + $retval .= '<option value="">' . __('Charset') . '</option>' . "\n"; + $retval .= '<option value=""></option>' . "\n"; + + $charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $retval .= '<option value="' . $charset->getName() + . '" title="' . $charset->getDescription() . '"' + . ($routine['item_returnopts_text'] == $charset->getName() ? ' selected' : '') . '>' + . $charset->getName() . '</option>' . "\n"; + } + + $retval .= '</select>' . "\n"; + $retval .= " </div>"; + $retval .= " <div><select name='item_returnopts_num'>"; + $retval .= " <option value=''></option>"; + foreach ($param_opts_num as $key => $value) { + $selected = ""; + if (! empty($routine['item_returnopts_num']) + && $routine['item_returnopts_num'] == $value + ) { + $selected = " selected='selected'"; + } + $retval .= "<option" . $selected . ">" . $value . "</option>"; + } + $retval .= " </select></div>"; + $retval .= " <div class='hide no_opts'>---</div>"; + $retval .= "</td>"; + $retval .= "</tr>"; + $retval .= "<tr>"; + $retval .= " <td>" . __('Definition') . "</td>"; + $retval .= " <td><textarea name='item_definition' rows='15' cols='40'>"; + $retval .= $routine['item_definition']; + $retval .= "</textarea></td>"; + $retval .= "</tr>"; + $retval .= "<tr>"; + $retval .= " <td>" . __('Is deterministic') . "</td>"; + $retval .= " <td><input type='checkbox' name='item_isdeterministic'" + . $routine['item_isdeterministic'] . "></td>"; + $retval .= "</tr>"; + if (isset($_REQUEST['edit_item']) + && ! empty($_REQUEST['edit_item']) + ) { + $retval .= "<tr>"; + $retval .= " <td>" . __('Adjust privileges'); + $retval .= Util::showDocu('faq', 'faq6-39'); + $retval .= "</td>"; + if ($GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $retval .= " <td><input type='checkbox' " + . "name='item_adjust_privileges' value='1' checked></td>"; + } else { + $retval .= " <td><input type='checkbox' " + . "name='item_adjust_privileges' value='1' title='" . __( + "You do not have sufficient privileges to perform this " + . "operation; Please refer to the documentation for more " + . "details" + ) + . "' disabled></td>"; + } + $retval .= "</tr>"; + } + + $retval .= "<tr>"; + $retval .= " <td>" . __('Definer') . "</td>"; + $retval .= " <td><input type='text' name='item_definer'"; + $retval .= " value='" . $routine['item_definer'] . "'></td>"; + $retval .= "</tr>"; + $retval .= "<tr>"; + $retval .= " <td>" . __('Security type') . "</td>"; + $retval .= " <td><select name='item_securitytype'>"; + $retval .= " <option value='DEFINER'" + . $routine['item_securitytype_definer'] . ">DEFINER</option>"; + $retval .= " <option value='INVOKER'" + . $routine['item_securitytype_invoker'] . ">INVOKER</option>"; + $retval .= " </select></td>"; + $retval .= "</tr>"; + $retval .= "<tr>"; + $retval .= " <td>" . __('SQL data access') . "</td>"; + $retval .= " <td><select name='item_sqldataaccess'>"; + foreach ($param_sqldataaccess as $key => $value) { + $selected = ""; + if ($routine['item_sqldataaccess'] == $value) { + $selected = " selected='selected'"; + } + $retval .= " <option" . $selected . ">" . $value . "</option>"; + } + $retval .= " </select></td>"; + $retval .= "</tr>"; + $retval .= "<tr>"; + $retval .= " <td>" . __('Comment') . "</td>"; + $retval .= " <td><input type='text' name='item_comment' maxlength='64'"; + $retval .= " value='" . $routine['item_comment'] . "'></td>"; + $retval .= "</tr>"; + $retval .= "</table>"; + $retval .= "</fieldset>"; + if ($response->isAjax()) { + $retval .= "<input type='hidden' name='editor_process_" . $mode . "'"; + $retval .= " value='true'>"; + $retval .= "<input type='hidden' name='ajax_request' value='true'>"; + } else { + $retval .= "<fieldset class='tblFooters'>"; + $retval .= " <input type='submit' name='editor_process_" . $mode . "'"; + $retval .= " value='" . __('Go') . "'>"; + $retval .= "</fieldset>"; + } + $retval .= "</form>"; + $retval .= "<!-- END " . mb_strtoupper($mode) . " ROUTINE FORM -->"; + + return $retval; + } + + /** + * Composes the query necessary to create a routine from an HTTP request. + * + * @return string The CREATE [ROUTINE | PROCEDURE] query. + */ + public function getQueryFromRequest() + { + global $errors, $param_sqldataaccess, $param_directions, $dbi; + + $_POST['item_type'] = isset($_POST['item_type']) + ? $_POST['item_type'] : ''; + + $query = 'CREATE '; + if (! empty($_POST['item_definer'])) { + if (mb_strpos($_POST['item_definer'], '@') !== false) { + $arr = explode('@', $_POST['item_definer']); + + $do_backquote = true; + if (substr($arr[0], 0, 1) === "`" + && substr($arr[0], -1) === "`" + ) { + $do_backquote = false; + } + $query .= 'DEFINER=' . Util::backquote($arr[0], $do_backquote); + + $do_backquote = true; + if (substr($arr[1], 0, 1) === "`" + && substr($arr[1], -1) === "`" + ) { + $do_backquote = false; + } + $query .= '@' . Util::backquote($arr[1], $do_backquote) . ' '; + } else { + $errors[] = __('The definer must be in the "username@hostname" format!'); + } + } + if ($_POST['item_type'] == 'FUNCTION' + || $_POST['item_type'] == 'PROCEDURE' + ) { + $query .= $_POST['item_type'] . ' '; + } else { + $errors[] = sprintf( + __('Invalid routine type: "%s"'), + htmlspecialchars($_POST['item_type']) + ); + } + if (! empty($_POST['item_name'])) { + $query .= Util::backquote($_POST['item_name']); + } else { + $errors[] = __('You must provide a routine name!'); + } + $params = ''; + $warned_about_dir = false; + $warned_about_length = false; + + if (! empty($_POST['item_param_name']) + && ! empty($_POST['item_param_type']) + && ! empty($_POST['item_param_length']) + && is_array($_POST['item_param_name']) + && is_array($_POST['item_param_type']) + && is_array($_POST['item_param_length']) + ) { + $item_param_name = $_POST['item_param_name']; + $item_param_type = $_POST['item_param_type']; + $item_param_length = $_POST['item_param_length']; + + for ($i = 0, $nb = count($item_param_name); $i < $nb; $i++) { + if (! empty($item_param_name[$i]) + && ! empty($item_param_type[$i]) + ) { + if ($_POST['item_type'] == 'PROCEDURE' + && ! empty($_POST['item_param_dir'][$i]) + && in_array($_POST['item_param_dir'][$i], $param_directions) + ) { + $params .= $_POST['item_param_dir'][$i] . " " + . Util::backquote($item_param_name[$i]) + . " " . $item_param_type[$i]; + } elseif ($_POST['item_type'] == 'FUNCTION') { + $params .= Util::backquote($item_param_name[$i]) + . " " . $item_param_type[$i]; + } elseif (! $warned_about_dir) { + $warned_about_dir = true; + $errors[] = sprintf( + __('Invalid direction "%s" given for parameter.'), + htmlspecialchars($_POST['item_param_dir'][$i]) + ); + } + if ($item_param_length[$i] != '' + && ! preg_match( + '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|' + . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|' + . 'SERIAL|BOOLEAN)$@i', + $item_param_type[$i] + ) + ) { + $params .= "(" . $item_param_length[$i] . ")"; + } elseif ($item_param_length[$i] == '' + && preg_match( + '@^(ENUM|SET|VARCHAR|VARBINARY)$@i', + $item_param_type[$i] + ) + ) { + if (! $warned_about_length) { + $warned_about_length = true; + $errors[] = __( + 'You must provide length/values for routine parameters' + . ' of type ENUM, SET, VARCHAR and VARBINARY.' + ); + } + } + if (! empty($_POST['item_param_opts_text'][$i])) { + if ($dbi->types->getTypeClass($item_param_type[$i]) == 'CHAR') { + if (! in_array($item_param_type[$i], ['VARBINARY', 'BINARY'])) { + $params .= ' CHARSET ' + . mb_strtolower( + $_POST['item_param_opts_text'][$i] + ); + } + } + } + if (! empty($_POST['item_param_opts_num'][$i])) { + if ($dbi->types->getTypeClass($item_param_type[$i]) == 'NUMBER') { + $params .= ' ' + . mb_strtoupper( + $_POST['item_param_opts_num'][$i] + ); + } + } + if ($i != (count($item_param_name) - 1)) { + $params .= ", "; + } + } else { + $errors[] = __( + 'You must provide a name and a type for each routine parameter.' + ); + break; + } + } + } + $query .= "(" . $params . ") "; + if ($_POST['item_type'] == 'FUNCTION') { + $item_returntype = isset($_POST['item_returntype']) + ? $_POST['item_returntype'] + : null; + + if (! empty($item_returntype) + && in_array( + $item_returntype, + Util::getSupportedDatatypes() + ) + ) { + $query .= "RETURNS " . $item_returntype; + } else { + $errors[] = __('You must provide a valid return type for the routine.'); + } + if (! empty($_POST['item_returnlength']) + && ! preg_match( + '@^(DATE|DATETIME|TIME|TINYBLOB|TINYTEXT|BLOB|TEXT|' + . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i', + $item_returntype + ) + ) { + $query .= "(" . $_POST['item_returnlength'] . ")"; + } elseif (empty($_POST['item_returnlength']) + && preg_match( + '@^(ENUM|SET|VARCHAR|VARBINARY)$@i', + $item_returntype + ) + ) { + if (! $warned_about_length) { + $errors[] = __( + 'You must provide length/values for routine parameters' + . ' of type ENUM, SET, VARCHAR and VARBINARY.' + ); + } + } + if (! empty($_POST['item_returnopts_text'])) { + if ($dbi->types->getTypeClass($item_returntype) == 'CHAR') { + $query .= ' CHARSET ' + . mb_strtolower($_POST['item_returnopts_text']); + } + } + if (! empty($_POST['item_returnopts_num'])) { + if ($dbi->types->getTypeClass($item_returntype) == 'NUMBER') { + $query .= ' ' + . mb_strtoupper($_POST['item_returnopts_num']); + } + } + $query .= ' '; + } + if (! empty($_POST['item_comment'])) { + $query .= "COMMENT '" . $this->dbi->escapeString($_POST['item_comment']) + . "' "; + } + if (isset($_POST['item_isdeterministic'])) { + $query .= 'DETERMINISTIC '; + } else { + $query .= 'NOT DETERMINISTIC '; + } + if (! empty($_POST['item_sqldataaccess']) + && in_array($_POST['item_sqldataaccess'], $param_sqldataaccess) + ) { + $query .= $_POST['item_sqldataaccess'] . ' '; + } + if (! empty($_POST['item_securitytype'])) { + if ($_POST['item_securitytype'] == 'DEFINER' + || $_POST['item_securitytype'] == 'INVOKER' + ) { + $query .= 'SQL SECURITY ' . $_POST['item_securitytype'] . ' '; + } + } + if (! empty($_POST['item_definition'])) { + $query .= $_POST['item_definition']; + } else { + $errors[] = __('You must provide a routine definition.'); + } + + return $query; + } + + /** + * Handles requests for executing a routine + * + * @return void + */ + public function handleExecute() + { + global $db; + + $response = Response::getInstance(); + + /** + * Handle all user requests other than the default of listing routines + */ + if (! empty($_POST['execute_routine']) && ! empty($_POST['item_name'])) { + // Build the queries + $routine = $this->getDataFromName( + $_POST['item_name'], + $_POST['item_type'], + false + ); + if ($routine === false) { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $this->words->get('not_found'), + htmlspecialchars(Util::backquote($_POST['item_name'])), + htmlspecialchars(Util::backquote($db)) + ); + $message = Message::error($message); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + echo $message->getDisplay(); + unset($_POST); + } + } + + $queries = []; + $end_query = []; + $args = []; + $all_functions = $this->dbi->types->getAllFunctions(); + for ($i = 0; $i < $routine['item_num_params']; $i++) { + if (isset($_POST['params'][$routine['item_param_name'][$i]])) { + $value = $_POST['params'][$routine['item_param_name'][$i]]; + if (is_array($value)) { // is SET type + $value = implode(',', $value); + } + $value = $this->dbi->escapeString($value); + if (! empty($_POST['funcs'][$routine['item_param_name'][$i]]) + && in_array( + $_POST['funcs'][$routine['item_param_name'][$i]], + $all_functions + ) + ) { + $queries[] = "SET @p$i=" + . $_POST['funcs'][$routine['item_param_name'][$i]] + . "('$value');\n"; + } else { + $queries[] = "SET @p$i='$value';\n"; + } + $args[] = "@p$i"; + } else { + $args[] = "@p$i"; + } + if ($routine['item_type'] == 'PROCEDURE') { + if ($routine['item_param_dir'][$i] == 'OUT' + || $routine['item_param_dir'][$i] == 'INOUT' + ) { + $end_query[] = "@p$i AS " + . Util::backquote($routine['item_param_name'][$i]); + } + } + } + if ($routine['item_type'] == 'PROCEDURE') { + $queries[] = "CALL " . Util::backquote($routine['item_name']) + . "(" . implode(', ', $args) . ");\n"; + if (count($end_query)) { + $queries[] = "SELECT " . implode(', ', $end_query) . ";\n"; + } + } else { + $queries[] = "SELECT " . Util::backquote($routine['item_name']) + . "(" . implode(', ', $args) . ") " + . "AS " . Util::backquote($routine['item_name']) + . ";\n"; + } + + // Get all the queries as one SQL statement + $multiple_query = implode("", $queries); + + $outcome = true; + $affected = 0; + + // Execute query + if (! $this->dbi->tryMultiQuery($multiple_query)) { + $outcome = false; + } + + // Generate output + if ($outcome) { + // Pass the SQL queries through the "pretty printer" + $output = Util::formatSql(implode($queries, "\n")); + + // Display results + $output .= "<fieldset><legend>"; + $output .= sprintf( + __('Execution results of routine %s'), + Util::backquote(htmlspecialchars($routine['item_name'])) + ); + $output .= "</legend>"; + + $nbResultsetToDisplay = 0; + + do { + $result = $this->dbi->storeResult(); + $num_rows = $this->dbi->numRows($result); + + if (($result !== false) && ($num_rows > 0)) { + $output .= "<table><tr>"; + foreach ($this->dbi->getFieldsMeta($result) as $field) { + $output .= "<th>"; + $output .= htmlspecialchars($field->name); + $output .= "</th>"; + } + $output .= "</tr>"; + + while ($row = $this->dbi->fetchAssoc($result)) { + $output .= "<tr>" . $this->browseRow($row) . "</tr>"; + } + + $output .= "</table>"; + $nbResultsetToDisplay++; + $affected = $num_rows; + } + + if (! $this->dbi->moreResults()) { + break; + } + + $output .= "<br>"; + + $this->dbi->freeResult($result); + } while ($outcome = $this->dbi->nextResult()); + } + + if ($outcome) { + $output .= "</fieldset>"; + + $message = __('Your SQL query has been executed successfully.'); + if ($routine['item_type'] == 'PROCEDURE') { + $message .= '<br>'; + + // TODO : message need to be modified according to the + // output from the routine + $message .= sprintf( + _ngettext( + '%d row affected by the last statement inside the ' + . 'procedure.', + '%d rows affected by the last statement inside the ' + . 'procedure.', + $affected + ), + $affected + ); + } + $message = Message::success($message); + + if ($nbResultsetToDisplay == 0) { + $notice = __( + 'MySQL returned an empty result set (i.e. zero rows).' + ); + $output .= Message::notice($notice)->getDisplay(); + } + } else { + $output = ''; + $message = Message::error( + sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($multiple_query) + ) + . '<br><br>' + . __('MySQL said: ') . $this->dbi->getError() + ); + } + + // Print/send output + if ($response->isAjax()) { + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('message', $message->getDisplay() . $output); + $response->addJSON('dialog', false); + exit; + } else { + echo $message->getDisplay() , $output; + if ($message->isError()) { + // At least one query has failed, so shouldn't + // execute any more queries, so we quit. + exit; + } + unset($_POST); + // Now deliberately fall through to displaying the routines list + } + return; + } elseif (! empty($_GET['execute_dialog']) && ! empty($_GET['item_name'])) { + /** + * Display the execute form for a routine. + */ + $routine = $this->getDataFromName( + $_GET['item_name'], + $_GET['item_type'], + true + ); + if ($routine !== false) { + $form = $this->getExecuteForm($routine); + if ($response->isAjax()) { + $title = __("Execute routine") . " " . Util::backquote( + htmlentities($_GET['item_name'], ENT_QUOTES) + ); + $response->addJSON('message', $form); + $response->addJSON('title', $title); + $response->addJSON('dialog', true); + } else { + echo "\n\n<h2>" . __("Execute routine") . "</h2>\n\n"; + echo $form; + } + exit; + } elseif ($response->isAjax()) { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $this->words->get('not_found'), + htmlspecialchars(Util::backquote($_GET['item_name'])), + htmlspecialchars(Util::backquote($db)) + ); + $message = Message::error($message); + + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } + } + } + + /** + * Browse row array + * + * @param array $row Columns + * + * @return string + */ + private function browseRow(array $row) + { + $output = null; + foreach ($row as $value) { + if ($value === null) { + $value = '<i>NULL</i>'; + } else { + $value = htmlspecialchars($value); + } + $output .= "<td>" . $value . "</td>"; + } + return $output; + } + + /** + * Creates the HTML code that shows the routine execution dialog. + * + * @param array $routine Data for the routine returned by + * getDataFromName() + * + * @return string HTML code for the routine execution dialog. + */ + public function getExecuteForm(array $routine) + { + global $db, $cfg; + + $response = Response::getInstance(); + + // Escape special characters + $routine['item_name'] = htmlentities($routine['item_name'], ENT_QUOTES); + for ($i = 0; $i < $routine['item_num_params']; $i++) { + $routine['item_param_name'][$i] = htmlentities( + $routine['item_param_name'][$i], + ENT_QUOTES + ); + } + + // Create the output + $retval = ""; + $retval .= "<!-- START ROUTINE EXECUTE FORM -->\n\n"; + $retval .= "<form action='db_routines.php' method='post'\n"; + $retval .= " class='rte_form ajax' onsubmit='return false'>\n"; + $retval .= "<input type='hidden' name='item_name'\n"; + $retval .= " value='{$routine['item_name']}'>\n"; + $retval .= "<input type='hidden' name='item_type'\n"; + $retval .= " value='{$routine['item_type']}'>\n"; + $retval .= Url::getHiddenInputs($db) . "\n"; + $retval .= "<fieldset>\n"; + if (! $response->isAjax()) { + $retval .= "<legend>{$routine['item_name']}</legend>\n"; + $retval .= "<table class='rte_table'>\n"; + $retval .= "<caption class='tblHeaders'>\n"; + $retval .= __('Routine parameters'); + $retval .= "</caption>\n"; + } else { + $retval .= "<legend>" . __('Routine parameters') . "</legend>\n"; + $retval .= "<table class='rte_table'>\n"; + } + $retval .= "<tr>\n"; + $retval .= "<th>" . __('Name') . "</th>\n"; + $retval .= "<th>" . __('Type') . "</th>\n"; + if ($cfg['ShowFunctionFields']) { + $retval .= "<th>" . __('Function') . "</th>\n"; + } + $retval .= "<th>" . __('Value') . "</th>\n"; + $retval .= "</tr>\n"; + // Get a list of data types that are not yet supported. + $no_support_types = Util::unsupportedDatatypes(); + for ($i = 0; $i < $routine['item_num_params']; $i++) { // Each parameter + if ($routine['item_type'] == 'PROCEDURE' + && $routine['item_param_dir'][$i] == 'OUT' + ) { + continue; + } + $retval .= "\n<tr>\n"; + $retval .= "<td>{$routine['item_param_name'][$i]}</td>\n"; + $retval .= "<td>{$routine['item_param_type'][$i]}</td>\n"; + if ($cfg['ShowFunctionFields']) { + $retval .= "<td>\n"; + if (false !== stripos($routine['item_param_type'][$i], 'enum') + || false !== stripos($routine['item_param_type'][$i], 'set') + || in_array( + mb_strtolower($routine['item_param_type'][$i]), + $no_support_types + ) + ) { + $retval .= "--\n"; + } else { + $field = [ + 'True_Type' => mb_strtolower( + $routine['item_param_type'][$i] + ), + 'Type' => '', + 'Key' => '', + 'Field' => '', + 'Default' => '', + 'first_timestamp' => false, + ]; + $retval .= "<select name='funcs[" + . $routine['item_param_name'][$i] . "]'>"; + $retval .= Util::getFunctionsForField($field, false, []); + $retval .= "</select>"; + } + $retval .= "</td>\n"; + } + // Append a class to date/time fields so that + // jQuery can attach a datepicker to them + $class = ''; + if ($routine['item_param_type'][$i] == 'DATETIME' + || $routine['item_param_type'][$i] == 'TIMESTAMP' + ) { + $class = 'datetimefield'; + } elseif ($routine['item_param_type'][$i] == 'DATE') { + $class = 'datefield'; + } + $retval .= "<td class='nowrap'>\n"; + if (in_array($routine['item_param_type'][$i], ['ENUM', 'SET'])) { + if ($routine['item_param_type'][$i] == 'ENUM') { + $input_type = 'radio'; + } else { + $input_type = 'checkbox'; + } + foreach ($routine['item_param_length_arr'][$i] as $value) { + $value = htmlentities(Util::unQuote($value), ENT_QUOTES); + $retval .= "<input name='params[" + . $routine['item_param_name'][$i] . "][]' " + . "value='" . $value . "' type='" + . $input_type . "'>" + . $value . "<br>\n"; + } + } elseif (in_array( + mb_strtolower($routine['item_param_type'][$i]), + $no_support_types + )) { + $retval .= "\n"; + } else { + $retval .= "<input class='$class' type='text' name='params[" + . $routine['item_param_name'][$i] . "]'>\n"; + } + $retval .= "</td>\n"; + $retval .= "</tr>\n"; + } + $retval .= "\n</table>\n"; + if (! $response->isAjax()) { + $retval .= "</fieldset>\n\n"; + $retval .= "<fieldset class='tblFooters'>\n"; + $retval .= " <input type='submit' name='execute_routine'\n"; + $retval .= " value='" . __('Go') . "'>\n"; + $retval .= "</fieldset>\n"; + } else { + $retval .= "<input type='hidden' name='execute_routine' value='true'>"; + $retval .= "<input type='hidden' name='ajax_request' value='true'>"; + } + $retval .= "</form>\n\n"; + $retval .= "<!-- END ROUTINE EXECUTE FORM -->\n\n"; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/RteList.php b/srcs/phpmyadmin/libraries/classes/Rte/RteList.php new file mode 100644 index 0000000..0e87c6c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/RteList.php @@ -0,0 +1,518 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Common functions for generating lists of Routines, Triggers and Events. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Response; +use PhpMyAdmin\SqlParser\Parser; +use PhpMyAdmin\SqlParser\Statements\CreateStatement; +use PhpMyAdmin\SqlParser\Utils\Routine; +use PhpMyAdmin\Template; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\RteList class + * + * @package PhpMyAdmin + */ +class RteList +{ + /** + * @var Words + */ + private $words; + + /** + * @var Template + */ + public $template; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * RteList constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + $this->words = new Words(); + $this->template = new Template(); + } + + /** + * Creates a list of items containing the relevant + * information and some action links. + * + * @param string $type One of ['routine'|'trigger'|'event'] + * @param array $items An array of items + * + * @return string HTML code of the list of items + */ + public function get($type, array $items) + { + global $table; + + /** + * Conditional classes switch the list on or off + */ + $class1 = 'hide'; + $class2 = ''; + if (! $items) { + $class1 = ''; + $class2 = ' hide'; + } + /** + * Generate output + */ + $retval = "<!-- LIST OF " . $this->words->get('docu') . " START -->\n"; + $retval .= '<form id="rteListForm" class="ajax" action="'; + switch ($type) { + case 'routine': + $retval .= 'db_routines.php'; + break; + case 'trigger': + if (! empty($table)) { + $retval .= 'tbl_triggers.php'; + } else { + $retval .= 'db_triggers.php'; + } + break; + case 'event': + $retval .= 'db_events.php'; + break; + default: + break; + } + $retval .= '">'; + $retval .= Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']); + $retval .= "<fieldset>\n"; + $retval .= " <legend>\n"; + $retval .= " " . $this->words->get('title') . "\n"; + $retval .= " " + . Util::showMySQLDocu($this->words->get('docu')) . "\n"; + $retval .= " </legend>\n"; + $retval .= " <div class='$class1' id='nothing2display'>\n"; + $retval .= " " . $this->words->get('nothing') . "\n"; + $retval .= " </div>\n"; + $retval .= " <table class='data$class2'>\n"; + $retval .= " <!-- TABLE HEADERS -->\n"; + $retval .= " <tr>\n"; + // th cells with a colspan need corresponding td cells, according to W3C + switch ($type) { + case 'routine': + $retval .= " <th></th>\n"; + $retval .= " <th>" . __('Name') . "</th>\n"; + $retval .= " <th colspan='4'>" . __('Action') . "</th>\n"; + $retval .= " <th>" . __('Type') . "</th>\n"; + $retval .= " <th>" . __('Returns') . "</th>\n"; + $retval .= " </tr>\n"; + $retval .= " <tr class='hide'>\n"; // see comment above + for ($i = 0; $i < 7; $i++) { + $retval .= " <td></td>\n"; + } + break; + case 'trigger': + $retval .= " <th></th>\n"; + $retval .= " <th>" . __('Name') . "</th>\n"; + if (empty($table)) { + $retval .= " <th>" . __('Table') . "</th>\n"; + } + $retval .= " <th colspan='3'>" . __('Action') . "</th>\n"; + $retval .= " <th>" . __('Time') . "</th>\n"; + $retval .= " <th>" . __('Event') . "</th>\n"; + $retval .= " </tr>\n"; + $retval .= " <tr class='hide'>\n"; // see comment above + for ($i = 0; $i < (empty($table) ? 7 : 6); $i++) { + $retval .= " <td></td>\n"; + } + break; + case 'event': + $retval .= " <th></th>\n"; + $retval .= " <th>" . __('Name') . "</th>\n"; + $retval .= " <th>" . __('Status') . "</th>\n"; + $retval .= " <th colspan='3'>" . __('Action') . "</th>\n"; + $retval .= " <th>" . __('Type') . "</th>\n"; + $retval .= " </tr>\n"; + $retval .= " <tr class='hide'>\n"; // see comment above + for ($i = 0; $i < 6; $i++) { + $retval .= " <td></td>\n"; + } + break; + default: + break; + } + $retval .= " </tr>\n"; + $retval .= " <!-- TABLE DATA -->\n"; + $response = Response::getInstance(); + foreach ($items as $item) { + if ($response->isAjax() && empty($_REQUEST['ajax_page_request'])) { + $rowclass = 'ajaxInsert hide'; + } else { + $rowclass = ''; + } + // Get each row from the correct function + switch ($type) { + case 'routine': + $retval .= $this->getRoutineRow($item, $rowclass); + break; + case 'trigger': + $retval .= $this->getTriggerRow($item, $rowclass); + break; + case 'event': + $retval .= $this->getEventRow($item, $rowclass); + break; + default: + break; + } + } + $retval .= " </table>\n"; + + if (count($items)) { + $retval .= '<div class="withSelected">'; + $retval .= $this->template->render('select_all', [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'form_name' => 'rteListForm', + ]); + $retval .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Export'), + 'b_export', + 'export' + ); + $retval .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Drop'), + 'b_drop', + 'drop' + ); + $retval .= '</div>'; + } + + $retval .= "</fieldset>\n"; + $retval .= "</form>\n"; + $retval .= "<!-- LIST OF " . $this->words->get('docu') . " END -->\n"; + + return $retval; + } + + /** + * Creates the contents for a row in the list of routines + * + * @param array $routine An array of routine data + * @param string $rowclass Additional class + * + * @return string HTML code of a row for the list of routines + */ + public function getRoutineRow(array $routine, $rowclass = '') + { + global $url_query, $db, $titles; + + $sql_drop = sprintf( + 'DROP %s IF EXISTS %s', + $routine['type'], + Util::backquote($routine['name']) + ); + $type_link = "item_type={$routine['type']}"; + + $retval = " <tr class='$rowclass'>\n"; + $retval .= " <td>\n"; + $retval .= ' <input type="checkbox"' + . ' class="checkall" name="item_name[]"' + . ' value="' . htmlspecialchars($routine['name']) . '">'; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " <span class='drop_sql hide'>" + . htmlspecialchars($sql_drop) . "</span>\n"; + $retval .= " <strong>\n"; + $retval .= " " + . htmlspecialchars($routine['name']) . "\n"; + $retval .= " </strong>\n"; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + + // this is for our purpose to decide whether to + // show the edit link or not, so we need the DEFINER for the routine + $where = "ROUTINE_SCHEMA " . Util::getCollateForIS() . "=" + . "'" . $this->dbi->escapeString($db) . "' " + . "AND SPECIFIC_NAME='" . $this->dbi->escapeString($routine['name']) . "'" + . "AND ROUTINE_TYPE='" . $this->dbi->escapeString($routine['type']) . "'"; + $query = "SELECT `DEFINER` FROM INFORMATION_SCHEMA.ROUTINES WHERE $where;"; + $routine_definer = $this->dbi->fetchValue($query); + + $curr_user = $this->dbi->getCurrentUser(); + + // Since editing a procedure involved dropping and recreating, check also for + // CREATE ROUTINE privilege to avoid lost procedures. + if ((Util::currentUserHasPrivilege('CREATE ROUTINE', $db) + && $curr_user == $routine_definer) + || $this->dbi->isSuperuser() + ) { + $retval .= ' <a class="ajax edit_anchor"' + . ' href="db_routines.php' + . $url_query + . '&edit_item=1' + . '&item_name=' + . urlencode($routine['name']) + . '&' . $type_link + . '">' . $titles['Edit'] . "</a>\n"; + } else { + $retval .= " {$titles['NoEdit']}\n"; + } + $retval .= " </td>\n"; + $retval .= " <td>\n"; + + // There is a problem with Util::currentUserHasPrivilege(): + // it does not detect all kinds of privileges, for example + // a direct privilege on a specific routine. So, at this point, + // we show the Execute link, hoping that the user has the correct rights. + // Also, information_schema might be hiding the ROUTINE_DEFINITION + // but a routine with no input parameters can be nonetheless executed. + + // Check if the routine has any input parameters. If it does, + // we will show a dialog to get values for these parameters, + // otherwise we can execute it directly. + + $definition = $this->dbi->getDefinition( + $db, + $routine['type'], + $routine['name'] + ); + if ($definition !== null) { + $parser = new Parser($definition); + + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + + $params = Routine::getParameters($stmt); + + if (Util::currentUserHasPrivilege('EXECUTE', $db)) { + $execute_action = 'execute_routine'; + for ($i = 0; $i < $params['num']; $i++) { + if ($routine['type'] == 'PROCEDURE' + && $params['dir'][$i] == 'OUT' + ) { + continue; + } + $execute_action = 'execute_dialog'; + break; + } + $query_part = $execute_action . '=1&item_name=' + . urlencode($routine['name']) . '&' . $type_link; + $retval .= ' <a class="ajax exec_anchor"' + . ' href="db_routines.php' + . $url_query + . ($execute_action == 'execute_routine' + ? '" data-post="' . $query_part + : '&' . $query_part) + . '">' . $titles['Execute'] . "</a>\n"; + } else { + $retval .= " {$titles['NoExecute']}\n"; + } + } + + $retval .= " </td>\n"; + $retval .= " <td>\n"; + if ((Util::currentUserHasPrivilege('CREATE ROUTINE', $db) + && $curr_user == $routine_definer) + || $this->dbi->isSuperuser() + ) { + $retval .= ' <a class="ajax export_anchor"' + . ' href="db_routines.php' + . $url_query + . '&export_item=1' + . '&item_name=' + . urlencode($routine['name']) + . '&' . $type_link + . '">' . $titles['Export'] . "</a>\n"; + } else { + $retval .= " {$titles['NoExport']}\n"; + } + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= Util::linkOrButton( + 'sql.php' . $url_query . '&sql_query=' . urlencode($sql_drop) . '&goto=db_routines.php' . urlencode("?db={$db}"), + $titles['Drop'], + ['class' => 'ajax drop_anchor'] + ); + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " {$routine['type']}\n"; + $retval .= " </td>\n"; + $retval .= " <td dir=\"ltr\">\n"; + $retval .= " " + . htmlspecialchars($routine['returns']) . "\n"; + $retval .= " </td>\n"; + $retval .= " </tr>\n"; + + return $retval; + } + + /** + * Creates the contents for a row in the list of triggers + * + * @param array $trigger An array of routine data + * @param string $rowclass Additional class + * + * @return string HTML code of a cell for the list of triggers + */ + public function getTriggerRow(array $trigger, $rowclass = '') + { + global $url_query, $db, $table, $titles; + + $retval = " <tr class='$rowclass'>\n"; + $retval .= " <td>\n"; + $retval .= ' <input type="checkbox"' + . ' class="checkall" name="item_name[]"' + . ' value="' . htmlspecialchars($trigger['name']) . '">'; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " <span class='drop_sql hide'>" + . htmlspecialchars($trigger['drop']) . "</span>\n"; + $retval .= " <strong>\n"; + $retval .= " " . htmlspecialchars($trigger['name']) . "\n"; + $retval .= " </strong>\n"; + $retval .= " </td>\n"; + if (empty($table)) { + $retval .= " <td>\n"; + $retval .= "<a href='db_triggers.php{$url_query}" + . "&table=" . urlencode($trigger['table']) . "'>" + . htmlspecialchars($trigger['table']) . "</a>"; + $retval .= " </td>\n"; + } + $retval .= " <td>\n"; + if (Util::currentUserHasPrivilege('TRIGGER', $db, $table)) { + $retval .= ' <a class="ajax edit_anchor"' + . ' href="db_triggers.php' + . $url_query + . '&edit_item=1' + . '&item_name=' + . urlencode($trigger['name']) + . '">' . $titles['Edit'] . "</a>\n"; + } else { + $retval .= " {$titles['NoEdit']}\n"; + } + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= ' <a class="ajax export_anchor"' + . ' href="db_triggers.php' + . $url_query + . '&export_item=1' + . '&item_name=' + . urlencode($trigger['name']) + . '">' . $titles['Export'] . "</a>\n"; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + if (Util::currentUserHasPrivilege('TRIGGER', $db)) { + $retval .= Util::linkOrButton( + 'sql.php' . $url_query . '&sql_query=' . urlencode($trigger['drop']) . '&goto=db_triggers.php' . urlencode("?db={$db}"), + $titles['Drop'], + ['class' => 'ajax drop_anchor'] + ); + } else { + $retval .= " {$titles['NoDrop']}\n"; + } + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " {$trigger['action_timing']}\n"; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " {$trigger['event_manipulation']}\n"; + $retval .= " </td>\n"; + $retval .= " </tr>\n"; + + return $retval; + } + + /** + * Creates the contents for a row in the list of events + * + * @param array $event An array of routine data + * @param string $rowclass Additional class + * + * @return string HTML code of a cell for the list of events + */ + public function getEventRow(array $event, $rowclass = '') + { + global $url_query, $db, $titles; + + $sql_drop = sprintf( + 'DROP EVENT IF EXISTS %s', + Util::backquote($event['name']) + ); + + $retval = " <tr class='$rowclass'>\n"; + $retval .= " <td>\n"; + $retval .= ' <input type="checkbox"' + . ' class="checkall" name="item_name[]"' + . ' value="' . htmlspecialchars($event['name']) . '">'; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " <span class='drop_sql hide'>" + . htmlspecialchars($sql_drop) . "</span>\n"; + $retval .= " <strong>\n"; + $retval .= " " + . htmlspecialchars($event['name']) . "\n"; + $retval .= " </strong>\n"; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " {$event['status']}\n"; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + if (Util::currentUserHasPrivilege('EVENT', $db)) { + $retval .= ' <a class="ajax edit_anchor"' + . ' href="db_events.php' + . $url_query + . '&edit_item=1' + . '&item_name=' + . urlencode($event['name']) + . '">' . $titles['Edit'] . "</a>\n"; + } else { + $retval .= " {$titles['NoEdit']}\n"; + } + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= ' <a class="ajax export_anchor"' + . ' href="db_events.php' + . $url_query + . '&export_item=1' + . '&item_name=' + . urlencode($event['name']) + . '">' . $titles['Export'] . "</a>\n"; + $retval .= " </td>\n"; + $retval .= " <td>\n"; + if (Util::currentUserHasPrivilege('EVENT', $db)) { + $retval .= Util::linkOrButton( + 'sql.php' . $url_query . '&sql_query=' . urlencode($sql_drop) . '&goto=db_events.php' . urlencode("?db={$db}"), + $titles['Drop'], + ['class' => 'ajax drop_anchor'] + ); + } else { + $retval .= " {$titles['NoDrop']}\n"; + } + $retval .= " </td>\n"; + $retval .= " <td>\n"; + $retval .= " {$event['type']}\n"; + $retval .= " </td>\n"; + $retval .= " </tr>\n"; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Triggers.php b/srcs/phpmyadmin/libraries/classes/Rte/Triggers.php new file mode 100644 index 0000000..45ce02f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Triggers.php @@ -0,0 +1,527 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Functions for trigger management. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Message; +use PhpMyAdmin\Response; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * PhpMyAdmin\Rte\Triggers class + * + * @package PhpMyAdmin + */ +class Triggers +{ + /** + * @var Export + */ + private $export; + + /** + * @var Footer + */ + private $footer; + + /** + * @var General + */ + private $general; + + /** + * @var RteList + */ + private $rteList; + + /** + * @var Words + */ + private $words; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * Triggers constructor. + * + * @param DatabaseInterface $dbi DatabaseInterface object + */ + public function __construct(DatabaseInterface $dbi) + { + $this->dbi = $dbi; + $this->export = new Export($this->dbi); + $this->footer = new Footer($this->dbi); + $this->general = new General($this->dbi); + $this->rteList = new RteList($this->dbi); + $this->words = new Words(); + } + + /** + * Sets required globals + * + * @return void + */ + public function setGlobals() + { + global $action_timings, $event_manipulations; + + // Some definitions for triggers + $action_timings = [ + 'BEFORE', + 'AFTER', + ]; + $event_manipulations = [ + 'INSERT', + 'UPDATE', + 'DELETE', + ]; + } + + /** + * Main function for the triggers functionality + * + * @return void + */ + public function main() + { + global $db, $table; + + $this->setGlobals(); + /** + * Process all requests + */ + $this->handleEditor(); + $this->export->triggers(); + /** + * Display a list of available triggers + */ + $items = $this->dbi->getTriggers($db, $table); + echo $this->rteList->get('trigger', $items); + /** + * Display a link for adding a new trigger, + * if the user has the necessary privileges + */ + echo $this->footer->triggers(); + } + + /** + * Handles editor requests for adding or editing an item + * + * @return void + */ + public function handleEditor() + { + global $errors, $db, $table; + + if (! empty($_POST['editor_process_add']) + || ! empty($_POST['editor_process_edit']) + ) { + $sql_query = ''; + + $item_query = $this->getQueryFromRequest(); + + if (! count($errors)) { // set by PhpMyAdmin\Rte\Routines::getQueryFromRequest() + // Execute the created query + if (! empty($_POST['editor_process_edit'])) { + // Backup the old trigger, in case something goes wrong + $trigger = $this->getDataFromName($_POST['item_original_name']); + $create_item = $trigger['create']; + $drop_item = $trigger['drop'] . ';'; + $result = $this->dbi->tryQuery($drop_item); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($drop_item) + ) + . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '<br>' + . __('MySQL said: ') . $this->dbi->getError(); + // We dropped the old item, but were unable to create the + // new one. Try to restore the backup query. + $result = $this->dbi->tryQuery($create_item); + + $errors = $this->general->checkResult( + $result, + __( + 'Sorry, we failed to restore the dropped trigger.' + ), + $create_item, + $errors + ); + } else { + $message = Message::success( + __('Trigger %1$s has been modified.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $drop_item . $item_query; + } + } + } else { + // 'Add a new item' mode + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '<br><br>' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $message = Message::success( + __('Trigger %1$s has been created.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $item_query; + } + } + } + + if (count($errors)) { + $message = Message::error( + '<b>' + . __( + 'One or more errors have occurred while processing your request:' + ) + . '</b>' + ); + $message->addHtml('<ul>'); + foreach ($errors as $string) { + $message->addHtml('<li>' . $string . '</li>'); + } + $message->addHtml('</ul>'); + } + + $output = Util::getMessage($message, $sql_query); + $response = Response::getInstance(); + if ($response->isAjax()) { + if ($message->isSuccess()) { + $items = $this->dbi->getTriggers($db, $table, ''); + $trigger = false; + foreach ($items as $value) { + if ($value['name'] == $_POST['item_name']) { + $trigger = $value; + } + } + $insert = false; + if (empty($table) + || ($trigger !== false && $table == $trigger['table']) + ) { + $insert = true; + $response->addJSON('new_row', $this->rteList->getTriggerRow($trigger)); + $response->addJSON( + 'name', + htmlspecialchars( + mb_strtoupper( + $_POST['item_name'] + ) + ) + ); + } + $response->addJSON('insert', $insert); + $response->addJSON('message', $output); + } else { + $response->addJSON('message', $message); + $response->setRequestStatus(false); + } + exit; + } + } + + /** + * Display a form used to add/edit a trigger, if necessary + */ + if (count($errors) + || (empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + && (! empty($_REQUEST['add_item']) + || ! empty($_REQUEST['edit_item']))) // FIXME: this must be simpler than that + ) { + // Get the data for the form (if any) + if (! empty($_REQUEST['add_item'])) { + $title = $this->words->get('add'); + $item = $this->getDataFromRequest(); + $mode = 'add'; + } elseif (! empty($_REQUEST['edit_item'])) { + $title = __("Edit trigger"); + if (! empty($_REQUEST['item_name']) + && empty($_POST['editor_process_edit']) + ) { + $item = $this->getDataFromName($_REQUEST['item_name']); + if ($item !== false) { + $item['item_original_name'] = $item['item_name']; + } + } else { + $item = $this->getDataFromRequest(); + } + $mode = 'edit'; + } + $this->general->sendEditor('TRI', $mode, $item, $title, $db); + } + } + + /** + * This function will generate the values that are required to for the editor + * + * @return array Data necessary to create the editor. + */ + public function getDataFromRequest() + { + $retval = []; + $indices = [ + 'item_name', + 'item_table', + 'item_original_name', + 'item_action_timing', + 'item_event_manipulation', + 'item_definition', + 'item_definer', + ]; + foreach ($indices as $index) { + $retval[$index] = isset($_POST[$index]) ? $_POST[$index] : ''; + } + return $retval; + } + + /** + * This function will generate the values that are required to complete + * the "Edit trigger" form given the name of a trigger. + * + * @param string $name The name of the trigger. + * + * @return array|bool Data necessary to create the editor. + */ + public function getDataFromName($name) + { + global $db, $table; + + $temp = []; + $items = $this->dbi->getTriggers($db, $table, ''); + foreach ($items as $value) { + if ($value['name'] == $name) { + $temp = $value; + } + } + if (empty($temp)) { + return false; + } else { + $retval = []; + $retval['create'] = $temp['create']; + $retval['drop'] = $temp['drop']; + $retval['item_name'] = $temp['name']; + $retval['item_table'] = $temp['table']; + $retval['item_action_timing'] = $temp['action_timing']; + $retval['item_event_manipulation'] = $temp['event_manipulation']; + $retval['item_definition'] = $temp['definition']; + $retval['item_definer'] = $temp['definer']; + return $retval; + } + } + + /** + * Displays a form used to add/edit a trigger + * + * @param string $mode If the editor will be used to edit a trigger + * or add a new one: 'edit' or 'add'. + * @param array $item Data for the trigger returned by getDataFromRequest() + * or getDataFromName() + * + * @return string HTML code for the editor. + */ + public function getEditorForm($mode, array $item) + { + global $db, $table, $event_manipulations, $action_timings; + + $modeToUpper = mb_strtoupper($mode); + $response = Response::getInstance(); + + // Escape special characters + $need_escape = [ + 'item_original_name', + 'item_name', + 'item_definition', + 'item_definer', + ]; + foreach ($need_escape as $key => $index) { + $item[$index] = htmlentities($item[$index], ENT_QUOTES, 'UTF-8'); + } + $original_data = ''; + if ($mode == 'edit') { + $original_data = "<input name='item_original_name' " + . "type='hidden' value='{$item['item_original_name']}'>\n"; + } + $query = "SELECT `TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` "; + $query .= "WHERE `TABLE_SCHEMA`='" . $this->dbi->escapeString($db) . "' "; + $query .= "AND `TABLE_TYPE` IN ('BASE TABLE', 'SYSTEM VERSIONED')"; + $tables = $this->dbi->fetchResult($query); + + // Create the output + $retval = ""; + $retval .= "<!-- START " . $modeToUpper . " TRIGGER FORM -->\n\n"; + $retval .= "<form class='rte_form' action='db_triggers.php' method='post'>\n"; + $retval .= "<input name='{$mode}_item' type='hidden' value='1'>\n"; + $retval .= $original_data; + $retval .= Url::getHiddenInputs($db, $table) . "\n"; + $retval .= "<fieldset>\n"; + $retval .= "<legend>" . __('Details') . "</legend>\n"; + $retval .= "<table class='rte_table'>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Trigger name') . "</td>\n"; + $retval .= " <td><input type='text' name='item_name' maxlength='64'\n"; + $retval .= " value='{$item['item_name']}'></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Table') . "</td>\n"; + $retval .= " <td>\n"; + $retval .= " <select name='item_table'>\n"; + foreach ($tables as $key => $value) { + $selected = ""; + if ($mode == 'add' && $value == $table) { + $selected = " selected='selected'"; + } elseif ($mode == 'edit' && $value == $item['item_table']) { + $selected = " selected='selected'"; + } + $retval .= "<option$selected>"; + $retval .= htmlspecialchars($value); + $retval .= "</option>\n"; + } + $retval .= " </select>\n"; + $retval .= " </td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . _pgettext('Trigger action time', 'Time') . "</td>\n"; + $retval .= " <td><select name='item_timing'>\n"; + foreach ($action_timings as $key => $value) { + $selected = ""; + if (! empty($item['item_action_timing']) + && $item['item_action_timing'] == $value + ) { + $selected = " selected='selected'"; + } + $retval .= "<option$selected>$value</option>"; + } + $retval .= " </select></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Event') . "</td>\n"; + $retval .= " <td><select name='item_event'>\n"; + foreach ($event_manipulations as $key => $value) { + $selected = ""; + if (! empty($item['item_event_manipulation']) + && $item['item_event_manipulation'] == $value + ) { + $selected = " selected='selected'"; + } + $retval .= "<option$selected>$value</option>"; + } + $retval .= " </select></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Definition') . "</td>\n"; + $retval .= " <td><textarea name='item_definition' rows='15' cols='40'>"; + $retval .= $item['item_definition']; + $retval .= "</textarea></td>\n"; + $retval .= "</tr>\n"; + $retval .= "<tr>\n"; + $retval .= " <td>" . __('Definer') . "</td>\n"; + $retval .= " <td><input type='text' name='item_definer'\n"; + $retval .= " value='{$item['item_definer']}'></td>\n"; + $retval .= "</tr>\n"; + $retval .= "</table>\n"; + $retval .= "</fieldset>\n"; + if ($response->isAjax()) { + $retval .= "<input type='hidden' name='editor_process_{$mode}'\n"; + $retval .= " value='true'>\n"; + $retval .= "<input type='hidden' name='ajax_request' value='true'>\n"; + } else { + $retval .= "<fieldset class='tblFooters'>\n"; + $retval .= " <input type='submit' name='editor_process_{$mode}'\n"; + $retval .= " value='" . __('Go') . "'>\n"; + $retval .= "</fieldset>\n"; + } + $retval .= "</form>\n\n"; + $retval .= "<!-- END " . $modeToUpper . " TRIGGER FORM -->\n\n"; + + return $retval; + } + + /** + * Composes the query necessary to create a trigger from an HTTP request. + * + * @return string The CREATE TRIGGER query. + */ + public function getQueryFromRequest() + { + global $db, $errors, $action_timings, $event_manipulations; + + $query = 'CREATE '; + if (! empty($_POST['item_definer'])) { + if (mb_strpos($_POST['item_definer'], '@') !== false + ) { + $arr = explode('@', $_POST['item_definer']); + $query .= 'DEFINER=' . Util::backquote($arr[0]); + $query .= '@' . Util::backquote($arr[1]) . ' '; + } else { + $errors[] = __('The definer must be in the "username@hostname" format!'); + } + } + $query .= 'TRIGGER '; + if (! empty($_POST['item_name'])) { + $query .= Util::backquote($_POST['item_name']) . ' '; + } else { + $errors[] = __('You must provide a trigger name!'); + } + if (! empty($_POST['item_timing']) + && in_array($_POST['item_timing'], $action_timings) + ) { + $query .= $_POST['item_timing'] . ' '; + } else { + $errors[] = __('You must provide a valid timing for the trigger!'); + } + if (! empty($_POST['item_event']) + && in_array($_POST['item_event'], $event_manipulations) + ) { + $query .= $_POST['item_event'] . ' '; + } else { + $errors[] = __('You must provide a valid event for the trigger!'); + } + $query .= 'ON '; + if (! empty($_POST['item_table']) + && in_array($_POST['item_table'], $this->dbi->getTables($db)) + ) { + $query .= Util::backquote($_POST['item_table']); + } else { + $errors[] = __('You must provide a valid table name!'); + } + $query .= ' FOR EACH ROW '; + if (! empty($_POST['item_definition'])) { + $query .= $_POST['item_definition']; + } else { + $errors[] = __('You must provide a trigger definition.'); + } + + return $query; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Words.php b/srcs/phpmyadmin/libraries/classes/Rte/Words.php new file mode 100644 index 0000000..f308003 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Words.php @@ -0,0 +1,89 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Helper functions for RTE + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Rte; + +/** + * PhpMyAdmin\Rte\Words class + * + * @package PhpMyAdmin + */ +class Words +{ + /** + * This function is used to retrieve some language strings that are used + * in features that are common to routines, triggers and events. + * + * @param string $index The index of the string to get + * + * @return string The requested string or an empty string, if not available + */ + public function get($index) + { + global $_PMA_RTE; + + switch ($_PMA_RTE) { + case 'RTN': + $words = [ + 'add' => __('Add routine'), + 'docu' => 'STORED_ROUTINES', + 'export' => __('Export of routine %s'), + 'human' => __('routine'), + 'no_create' => __( + 'You do not have the necessary privileges to create a routine.' + ), + 'no_edit' => __( + 'No routine with name %1$s found in database %2$s. ' + . 'You might be lacking the necessary privileges to edit this routine.' + ), + 'no_view' => __( + 'No routine with name %1$s found in database %2$s. ' + . 'You might be lacking the necessary privileges to view/export this routine.' + ), + 'not_found' => __('No routine with name %1$s found in database %2$s.'), + 'nothing' => __('There are no routines to display.'), + 'title' => __('Routines'), + ]; + break; + case 'TRI': + $words = [ + 'add' => __('Add trigger'), + 'docu' => 'TRIGGERS', + 'export' => __('Export of trigger %s'), + 'human' => __('trigger'), + 'no_create' => __( + 'You do not have the necessary privileges to create a trigger.' + ), + 'not_found' => __('No trigger with name %1$s found in database %2$s.'), + 'nothing' => __('There are no triggers to display.'), + 'title' => __('Triggers'), + ]; + break; + case 'EVN': + $words = [ + 'add' => __('Add event'), + 'docu' => 'EVENTS', + 'export' => __('Export of event %s'), + 'human' => __('event'), + 'no_create' => __( + 'You do not have the necessary privileges to create an event.' + ), + 'not_found' => __('No event with name %1$s found in database %2$s.'), + 'nothing' => __('There are no events to display.'), + 'title' => __('Events'), + ]; + break; + default: + $words = []; + break; + } + + return isset($words[$index]) ? $words[$index] : ''; + } +} |
