diff --git a/import.php b/import.php index 8ed8755f05..c77059acaf 100644 --- a/import.php +++ b/import.php @@ -74,6 +74,11 @@ if (! empty($sql_query)) { $import_type = 'query'; $format = 'sql'; + // If there is a request to ROLLBACK when finished. + if (isset($_REQUEST['rollback_query'])) { + PMA_handleRollbackRequest($import_text); + } + // refresh navigation and main panels if (preg_match('/^(DROP)\s+(VIEW|TABLE|DATABASE|SCHEMA)\s+/i', $sql_query)) { $GLOBALS['reload'] = true; @@ -675,4 +680,9 @@ if ($go_sql) { $active_page = $goto; include '' . $goto; } + +// If there is request for ROLLBACK in the end. +if (isset($_REQUEST['rollback_query'])) { + $GLOBALS['dbi']->query('ROLLBACK'); +} ?> diff --git a/libraries/import.lib.php b/libraries/import.lib.php index 8472bca4f3..492f4d8d1e 100644 --- a/libraries/import.lib.php +++ b/libraries/import.lib.php @@ -262,6 +262,11 @@ function PMA_importRunQuery($sql = '', $full = '', $controluser = false, // Do we have something to push into buffer? $import_run_buffer = PMA_ImportRunQuery_post($import_run_buffer, $sql, $full); + + // In case of ROLLBACK, notify the user. + if (isset($_REQUEST['rollback_query'])) { + $msg .= __('[ROLLBACK occured.]'); + } } /** @@ -1463,7 +1468,7 @@ function PMA_getSimulatedUpdateQuery($analyzed_sql_results) foreach ($analyzed_sql_results['parsed_sql'] as $key => $term) { if (! isset($get_set_expr) && preg_match( - '/SET/i', + '/\bSET\b/i', isset($term['data']) ? $term['data'] : '' ) ) { @@ -1473,7 +1478,7 @@ function PMA_getSimulatedUpdateQuery($analyzed_sql_results) if (isset($get_set_expr)) { if (preg_match( - '/WHERE|ORDER BY|LIMIT/i', + '/\bWHERE\b|\bORDER BY\b|\bLIMIT\b/i', isset($term['data']) ? $term['data'] : '' ) ) { @@ -1558,7 +1563,9 @@ function PMA_getSimulatedDeleteQuery($analyzed_sql_results) } /** - * Finds table_references from a given UPDATE/DELETE query. + * Finds table_references from a given query. + * Queries Supported: INSERT, UPDATE, DELETE, REPLACE, ALTER, DROP, TRUNCATE + * and RENAME. * * @param array $analyzed_sql_results Analyzed SQL results from parser * @@ -1568,25 +1575,80 @@ function PMA_getTableReferences($analyzed_sql_results) { $table_references = ''; foreach ($analyzed_sql_results['parsed_sql'] as $key => $term) { + // Skip first KeyWord and other invalid keys. + if ($key == 0 || ! isset($term['data'])) { + continue; + } - if (preg_match( - '/WHERE|SET/i', - isset($term['data']) ? $term['data'] : '' - ) + // Get the query type. + $query_type = (isset($analyzed_sql_results['analyzed_sql'][0]['querytype'])) + ? $analyzed_sql_results['analyzed_sql'][0]['querytype'] + : ''; + + // Terms to 'ignore' from query for table_references. + $ignore_re = '/'; + // Terminating condition for table_references. + $terminate_re = '/'; + + // Create relevant Regular Expressions. + switch ($query_type) { + case 'REPLACE': + case 'INSERT': + $ignore_re .= '\bINSERT\b|\bREPLACE\b|\bLOW_PRIORITY\b|\bDELAYED\b' + . '|\bHIGH_PRIORITY\b|\bIGNORE\b|\bINTO\b'; + $terminate_re .= '\bPARTITION\b|\(|\bVALUE\b|\bVALUES\b|\bSELECT\b'; + break; + case 'UPDATE': + $ignore_re .= '\bUPDATE\b|\bLOW_PRIORITY\b|\bIGNORE\b'; + $terminate_re .= '\bSET\b|\bUSING\b'; + break; + case 'DELETE': + $ignore_re .= '\bDELETE\b|\bLOW_PRIORITY\b|\bQUICK\b|\bIGNORE\b' + . '|\bFROM\b'; + $terminate_re .= '\bPARTITION\b|\bWHERE\b|\bORDER\b|\bLIMIT\b|\bUSING\b'; + break; + case 'ALTER': + $ignore_re .= '\bALTER\b|\bONLINE\b|\bOFFLINE\b|\bIGNORE\b|\bTABLE\b'; + $terminate_re .= '\bADD\b|\bALTER\b|\bCHANGE\b|\bMODIFY\b|\bDROP\b' + . '|\bDISABLE\b|\bENABLE\b|\bRENAME\b|\bORDER\b|\bCONVERT\b' + . '|\bDEFAULT\b|\bDISCARD\b|\bIMPORT\b|\bCOALESCE\b|\bREORGANIZE\b' + . '|\bANALYZE\b|\bCHECK\b|\bOPTIMIZE\b|\bREBUILD\b|\bREPAIR\b' + . '|\bPARTITION\b|\bREMOVE\b|\bCHARACTER\b'; + break; + case 'DROP': + $ignore_re .= '\bDROP\b|\bTEMPORARY\b|\bTABLE\b|\bIF\b|\bEXISTS\b'; + $terminate_re .= '\bRESTRICT\b|\bCASCADE\b'; + break; + case 'TRUNCATE': + $ignore_re .= '\bTRUNCATE\b|\bTABLE\b'; + $terminate_re .= ''; + break; + case 'RENAME': + $ignore_re .= '\bRENAME\b|\bTABLE\b'; + $terminate_re .= '\bTO\b'; + break; + default: + return false; + } + + // Ignore 'case' in RegEx. + $ignore_re .= '/i'; + $terminate_re .= '/i'; + + if ($query_type != 'TRUNCATE' + && preg_match($terminate_re, $term['data']) ) { break; } - if (preg_match( - '/UPDATE|DELETE|LOW_PRIORITY|QUICK|IGNORE|FROM/i', - isset($term['data']) ? $term['data'] : '' - ) + if (preg_match($ignore_re, $term['data']) || ! is_numeric($key) + || $key == 0 ) { continue; } - $table_references .= ' ' . isset($term['data']) ? $term['data'] : ''; + $table_references .= ' ' . $term['data']; } return $table_references; @@ -1609,4 +1671,245 @@ function PMA_executeMatchedRowQuery($matched_row_query) return $result; } + +/** + * Extracts unique table names from table_references. + * + * @param string $table_references table_references + * + * @return array $table_names + */ +function PMA_getTableNamesFromTableReferences($table_references) +{ + $table_names = array(); + $parsed_data = PMA_SQP_parse($table_references); + + $prev_term = array( + 'data' => '', + 'type' => '' + ); + $on_encountered = false; + $qualifier_encountered = false; + $i = 0; + foreach ($parsed_data as $key => $term) { + // To skip first 'raw' key and other invalid keys. + if (! is_numeric($key) + || ! isset($term['data']) + || ! isset($term['type']) + ) { + continue; + } + + $add_to_table_names = true; + + // Un-quote the data, if any. + if ($term['type'] == 'quote_backtick') { + $term['data'] = PMA_Util::unQuote($term['data']); + $term['type'] = 'alpha_identifier'; + } + + // New table name expected after 'JOIN' keyword. + if (preg_match('/\bJOIN\b/i', $term['data'])) { + $on_encountered = false; + } + + // If term is a qualifier, set flag. + if ($term['type'] == 'punct_qualifier') { + $qualifier_encountered = true; + } + + // Skip the JOIN conditions after 'ON' keyword. + if (preg_match('/\bON\b/i', $term['data'])) { + $on_encountered = true; + } + + // If the word is not an 'identifier', skip it. + if ($term['type'] != 'alpha_identifier') { + $add_to_table_names = false; + } + + // Skip table 'alias'. + if (preg_match('/\bAS\b/i', $prev_term['data']) + || $prev_term['type'] == 'alpha_identifier' + ) { + $add_to_table_names = false; + } + + // Everything fine upto now, add name to list if 'unique'. + if ($add_to_table_names + && ! $on_encountered + && ! in_array($term['data'], $table_names) + ) { + if (! $qualifier_encountered) { + $table_names[] = PMA_Util::backquote($term['data']); + $i++; + } else { + // If qualifier encountered, concatenate DB name and table name. + $table_names[$i-1] = $table_names[$i-1] + . '.' + . PMA_Util::backquote($term['data']); + $qualifier_encountered = false; + } + } + + // Update previous term. + $prev_term = $term; + } + + return $table_names; +} + +/** + * Handles request for ROLLBACK. + * + * @param string $sql_query SQL query(s) + * + * @return void + */ +function PMA_handleRollbackRequest($sql_query) +{ + $sql_delimiter = $_REQUEST['sql_delimiter']; + $queries = explode($sql_delimiter, $sql_query); + $error = false; + $error_msg = __('Only INSERT, UPDATE, DELETE and REPLACE ' + . 'SQL queries containing transactional engine tables can be rolled back.' + ); + foreach ($queries as $sql_query) { + if (empty($sql_query)) { + continue; + } + + // Check each query for ROLLBACK support. + if (! PMA_checkIfRollbackPossible($sql_query)) { + $global_error = $GLOBALS['dbi']->getError(); + if ($global_error) { + $error = $global_error; + } else { + $error = $error_msg; + } + break; + } + } + + if ($error) { + unset($_REQUEST['rollback_query']); + $response = PMA_Response::getInstance(); + $message = PMA_Message::rawError($error); + $response->addJSON('message', $message); + exit; + } else { + // If everything fine, START a transaction. + $GLOBALS['dbi']->query('START TRANSACTION'); + } +} + +/** + * Checks if ROLLBACK is possible for a SQL query or not. + * + * @param string $sql_query SQL query + * + * @return bool + */ +function PMA_checkIfRollbackPossible($sql_query) +{ + // Suppoerted queries. + $supported_queries = array( + 'INSERT', + 'UPDATE', + 'DELETE', + 'REPLACE' + ); + + // Parse and Analyze the query. + $parsed_sql = PMA_SQP_parse($sql_query); + $analyzed_sql = PMA_SQP_analyze($parsed_sql); + $analyzed_sql_results = array( + 'parsed_sql' => $parsed_sql, + 'analyzed_sql' => $analyzed_sql + ); + + // Get the query type. + $query_type = (isset($analyzed_sql_results['analyzed_sql'][0]['querytype'])) + ? $analyzed_sql_results['analyzed_sql'][0]['querytype'] + : ''; + + // Check if query is supported. + if (! in_array($query_type, $supported_queries)) { + return false; + } + + // Get table_references from the query. + $table_references = PMA_getTableReferences($analyzed_sql_results); + + // Get table names from table_references. + $tables = PMA_getTableNamesFromTableReferences($table_references); + + // Check if each table is 'InnoDB'. + foreach ($tables as $table) { + if (! PMA_isTableTransactional($table)) { + return false; + } + } + + return true; +} + +/** + * Checks if a table is 'InnoDB' or not. + * + * @param string $table Table details + * + * @return bool + */ +function PMA_isTableTransactional($table) +{ + $db = ''; + $table = explode('.', $table); + if (count($table) == 2) { + $db = PMA_Util::unQuote($table[0]); + $table = PMA_Util::unQuote($table[1]); + } else { + $db = $GLOBALS['db']; + $table = PMA_Util::unQuote($table[0]); + } + + // Query to check if table exists. + $check_table_query = 'SELECT * FROM ' . PMA_Util::backquote($db) + . '.' . PMA_Util::backquote($table) . ' ' + . 'LIMIT 1'; + + $result = $GLOBALS['dbi']->tryQuery($check_table_query); + + if (! $result) { + return false; + } + + // List of Transactional Engines. + $transactional_engines = array( + 'INNODB', + 'FALCON', + 'NDB', + 'INFINIDB', + 'TOKUDB', + 'XTRADB', + 'SEQUENCE', + 'BDB' + ); + + // Query to check if table is 'Transactional'. + $check_query = 'SELECT `ENGINE` FROM `information_schema`.`tables` ' + . 'WHERE `table_name` = "' . $table . '" ' + . 'AND `table_schema` = "' . $db . '" ' + . 'AND UPPER(`engine`) IN ("' + . implode('", "', $transactional_engines) + . '")'; + + $result = $GLOBALS['dbi']->tryQuery($check_query); + + if ($GLOBALS['dbi']->numRows($result) == 1) { + return true; + } else { + return false; + } +} ?> diff --git a/libraries/sql.lib.php b/libraries/sql.lib.php index 3a157cbf23..6df2ded07f 100644 --- a/libraries/sql.lib.php +++ b/libraries/sql.lib.php @@ -1674,6 +1674,11 @@ function PMA_getMessageForNoRowsReturned($message_to_show, $analyzed_sql_results $message->addMessage($_querytime); } + // In case of ROLLBACK, notify the user. + if (isset($_REQUEST['rollback_query'])) { + $message->addMessage(__('[ROLLBACK occured.]')); + } + return $message; } diff --git a/libraries/sql_query_form.lib.php b/libraries/sql_query_form.lib.php index f0efcb1020..1a6a1a4985 100644 --- a/libraries/sql_query_form.lib.php +++ b/libraries/sql_query_form.lib.php @@ -372,6 +372,12 @@ function PMA_getHtmlForSqlQueryFormInsert( . ''; } + + $html .= '' + . ''; + $html .= '' . "\n"; $html .= '