4chan/team/developer/pma/libraries/plugins/import/ImportSql.class.php
2025-04-17 18:12:08 -05:00

620 lines
19 KiB
PHP

<?php
/* vim: set expandtab sw=4 ts=4 sts=4: */
/**
* SQL import plugin for phpMyAdmin
*
* @package PhpMyAdmin-Import
* @subpackage SQL
*/
if (! defined('PHPMYADMIN')) {
exit;
}
/* Get the import interface */
require_once 'libraries/plugins/ImportPlugin.class.php';
/**
* Handles the import for the SQL format
*
* @package PhpMyAdmin-Import
* @subpackage SQL
*/
class ImportSql extends ImportPlugin
{
const BIG_VALUE = 2147483647;
const READ_MB_FALSE = 0;
const READ_MB_TRUE = 1;
/**
* @var string SQL delimiter
*/
private $_delimiter;
/**
* @var int SQL delimiter length
*/
private $_delimiterLength;
/**
* @var bool|int SQL delimiter position or false if not found
*/
private $_delimiterPosition = false;
/**
* @var int Query start position
*/
private $_queryBeginPosition = 0;
/**
* @var int|false First special chars position or false if not found
*/
private $_firstSearchChar = null;
/**
* @var bool Current position is in string
*/
private $_isInString = false;
/**
* @var string Quote of current string or null if out of string
*/
private $_quote = null;
/**
* @var bool Current position is in comment
*/
private $_isInComment = false;
/**
* @var string Current comment opener
*/
private $_openingComment = null;
/**
* @var bool Current position is in delimiter definition
*/
private $_isInDelimiter = false;
/**
* @var string Delimiter keyword
*/
private $_delimiterKeyword = 'DELIMITER ';
/**
* @var int Import should be done using multibytes
*/
private $_readMb = self::READ_MB_FALSE;
/**
* @var string Data to parse
*/
private $_data = null;
/**
* @var int Length of data to parse
*/
private $_dataLength = 0;
/**
* @var array List of string functions
* @todo Move this part in string functions definition file.
*/
private $_stringFunctions = array(
self::READ_MB_FALSE => array(
'substr' => 'substr',
'strlen' => 'strlen',
'strpos' => 'strpos',
'strtoupper' => 'strtoupper',
),
self::READ_MB_TRUE => array(
'substr' => 'mb_substr',
'strlen' => 'mb_strlen',
'strpos' => 'mb_strpos',
'strtoupper' => 'mb_strtoupper',
),
);
/**
* @var bool|int List of string functions to use
*/
private $_stringFctToUse = false;
/**
* Constructor
*/
public function __construct()
{
$this->setProperties();
}
/**
* Sets the import plugin properties.
* Called in the constructor.
*
* @return void
*/
protected function setProperties()
{
$props = 'libraries/properties/';
include_once "$props/plugins/ImportPluginProperties.class.php";
include_once "$props/options/groups/OptionsPropertyRootGroup.class.php";
include_once "$props/options/groups/OptionsPropertyMainGroup.class.php";
include_once "$props/options/items/SelectPropertyItem.class.php";
include_once "$props/options/items/BoolPropertyItem.class.php";
$importPluginProperties = new ImportPluginProperties();
$importPluginProperties->setText('SQL');
$importPluginProperties->setExtension('sql');
$importPluginProperties->setOptionsText(__('Options'));
$compats = $GLOBALS['dbi']->getCompatibilities();
if (count($compats) > 0) {
$values = array();
foreach ($compats as $val) {
$values[$val] = $val;
}
// create the root group that will be the options field for
// $importPluginProperties
// this will be shown as "Format specific options"
$importSpecificOptions = new OptionsPropertyRootGroup();
$importSpecificOptions->setName("Format Specific Options");
// general options main group
$generalOptions = new OptionsPropertyMainGroup();
$generalOptions->setName("general_opts");
// create primary items and add them to the group
$leaf = new SelectPropertyItem();
$leaf->setName("compatibility");
$leaf->setText(__('SQL compatibility mode:'));
$leaf->setValues($values);
$leaf->setDoc(
array(
'manual_MySQL_Database_Administration',
'Server_SQL_mode',
)
);
$generalOptions->addProperty($leaf);
$leaf = new BoolPropertyItem();
$leaf->setName("no_auto_value_on_zero");
$leaf->setText(
__('Do not use <code>AUTO_INCREMENT</code> for zero values')
);
$leaf->setDoc(
array(
'manual_MySQL_Database_Administration',
'Server_SQL_mode',
'sqlmode_no_auto_value_on_zero'
)
);
$generalOptions->addProperty($leaf);
$leaf = new BoolPropertyItem();
$leaf->setName("read_as_multibytes");
$leaf->setText(
__('Read as multibytes')
);
$generalOptions->addProperty($leaf);
// add the main group to the root group
$importSpecificOptions->addProperty($generalOptions);
// set the options for the import plugin property item
$importPluginProperties->setOptions($importSpecificOptions);
}
$this->properties = $importPluginProperties;
}
/**
* Look for end of string
*
* @return bool End of string found
*/
private function _searchStringEnd()
{
//Search for closing quote
$posClosingString = $this->_stringFctToUse['strpos'](
$this->_data, $this->_quote, $this->_delimiterPosition
);
if (false === $posClosingString) {
return false;
}
//Quotes escaped by quote will be considered as 2 consecutive strings
//and won't pass in this loop.
$posEscape = $posClosingString-1;
while ($this->_stringFctToUse['substr']($this->_data, $posEscape, 1) == '\\'
) {
$posEscape--;
}
// Odd count means it was escaped
$quoteEscaped = (((($posClosingString - 1) - $posEscape) % 2) === 1);
//Move after the escaped quote.
$this->_delimiterPosition = $posClosingString + 1;
if ($quoteEscaped) {
return true;
}
$this->_isInString = false;
$this->_quote = null;
return true;
}
/**
* Return the position of first SQL delimiter or false if no SQL delimiter found.
*
* @return int|bool Delimiter position or false if no delimiter found
*/
private function _findDelimiterPosition()
{
$this->_firstSearchChar = null;
$firstSqlDelimiter = null;
$matches = null;
/* while not at end of line */
while ($this->_delimiterPosition < $this->_dataLength) {
if ($this->_isInString) {
if (false === $this->_searchStringEnd()) {
return false;
}
continue;
}
if ($this->_isInComment) {
if (in_array($this->_openingComment, array('#', '-- '))) {
$posClosingComment = $this->_stringFctToUse['strpos'](
$this->_data,
"\n",
$this->_delimiterPosition
);
if (false === $posClosingComment) {
return false;
}
//Move after the end of the line.
$this->_delimiterPosition = $posClosingComment + 1;
$this->_isInComment = false;
$this->_openingComment = null;
} elseif ('/*' === $this->_openingComment) {
//Search for closing comment
$posClosingComment = $this->_stringFctToUse['strpos'](
$this->_data,
'*/',
$this->_delimiterPosition
);
if (false === $posClosingComment) {
return false;
}
//Move after closing comment.
$this->_delimiterPosition = $posClosingComment + 2;
$this->_isInComment = false;
$this->_openingComment = null;
} else {
//We shouldn't be able to come here.
//throw new Exception('Unknown case.');
break;
}
continue;
}
if ($this->_isInDelimiter) {
//Search for new line.
if (!preg_match(
"/^(.*)\n/",
$this->_stringFctToUse['substr'](
$this->_data,
$this->_delimiterPosition
),
$matches,
PREG_OFFSET_CAPTURE
)) {
return false;
}
$this->_setDelimiter($matches[1][0]);
//Start after delimiter and new line.
$this->_queryBeginPosition = $this->_delimiterPosition
+ $matches[1][1] + $this->_delimiterLength + 1;
$this->_delimiterPosition = $this->_queryBeginPosition;
$this->_isInDelimiter = false;
$firstSqlDelimiter = null;
$this->_firstSearchChar = null;
continue;
}
$matches = $this->_searchSpecialChars($matches);
$firstSqlDelimiter = $this->_searchSqlDelimiter($firstSqlDelimiter);
if (false === $firstSqlDelimiter && false === $this->_firstSearchChar) {
return false;
}
//If first char is delimiter.
if (false === $this->_firstSearchChar
|| (false !== $firstSqlDelimiter
&& $firstSqlDelimiter < $this->_firstSearchChar)
) {
$this->_delimiterPosition = $firstSqlDelimiter;
return true;
}
//Else first char is result of preg_match.
$specialChars = $matches[1][0];
//If string is opened.
if (in_array($specialChars, array('\'', '"', '`'))) {
$this->_isInString = true;
$this->_quote = $specialChars;
//Move before quote.
$this->_delimiterPosition = $this->_firstSearchChar + 1;
continue;
}
//If comment is opened.
if (in_array($specialChars, array('#', '-- ', '/*'))) {
$this->_isInComment = true;
$this->_openingComment = $specialChars;
//Move before comment opening.
$this->_delimiterPosition = $this->_firstSearchChar
+ $this->_stringFctToUse['strlen']($specialChars);
continue;
}
//If DELIMITER is found.
$specialCharsUpper = $this->_stringFctToUse['strtoupper']($specialChars);
if ($specialCharsUpper === $this->_delimiterKeyword) {
$this->_isInDelimiter = true;
$this->_delimiterPosition = $this->_firstSearchChar
+ $this->_stringFctToUse['strlen']($specialChars);
continue;
}
}
return false;
}
/**
* Handles the whole import logic
*
* @param array &$sql_data 2-element array with sql data
*
* @return void
*/
public function doImport(&$sql_data = array())
{
global $error, $timeout_passed;
//Manage multibytes or not
if (isset($_REQUEST['sql_read_as_multibytes'])) {
$this->_readMb = self::READ_MB_TRUE;
}
$this->_stringFctToUse = $this->_stringFunctions[$this->_readMb];
if (isset($_POST['sql_delimiter'])) {
$this->_setDelimiter($_POST['sql_delimiter']);
} else {
$this->_setDelimiter(';');
}
// Handle compatibility options
$this->_setSQLMode($GLOBALS['dbi'], $_REQUEST);
//Initialise data.
$this->_setData(null);
/**
* will be set in PMA_importGetNextChunk()
*
* @global boolean $GLOBALS['finished']
*/
$GLOBALS['finished'] = false;
$delimiterFound = false;
while (!$error && !$timeout_passed) {
if (false === $delimiterFound) {
$newData = PMA_importGetNextChunk();
if ($newData === false) {
// subtract data we didn't handle yet and stop processing
$GLOBALS['offset'] -= $this->_dataLength;
break;
}
if ($newData === true) {
$GLOBALS['finished'] = true;
break;
}
//Convert CR (but not CRLF) to LF otherwise all queries
//may not get executed on some platforms
$this->_addData(preg_replace("/\r($|[^\n])/", "\n$1", $newData));
unset($newData);
}
//Find quotes, comments, delimiter definition or delimiter itself.
$delimiterFound = $this->_findDelimiterPosition();
//If no delimiter found, restart and get more data.
if (false === $delimiterFound) {
continue;
}
PMA_importRunQuery(
$this->_stringFctToUse['substr'](
$this->_data,
$this->_queryBeginPosition,
$this->_delimiterPosition - $this->_queryBeginPosition
), //Query to execute
$this->_stringFctToUse['substr'](
$this->_data,
0,
$this->_delimiterPosition + $this->_delimiterLength
), //Query to display
false,
$sql_data
);
$this->_setData(
$this->_stringFctToUse['substr'](
$this->_data,
$this->_delimiterPosition + $this->_delimiterLength
)
);
}
if (! $timeout_passed) {
//Commit any possible data in buffers
PMA_importRunQuery(
$this->_stringFctToUse['substr'](
$this->_data,
$this->_queryBeginPosition
), //Query to execute
$this->_data,
false,
$sql_data
);
}
PMA_importRunQuery('', '', false, $sql_data);
}
/**
* Handle compatibility options
*
* @param PMA_DatabaseInterface $dbi Database interface
* @param array $request Request array
*
* @return void
*/
private function _setSQLMode($dbi, $request)
{
$sql_modes = array();
if (isset($request['sql_compatibility'])
&& 'NONE' != $request['sql_compatibility']
) {
$sql_modes[] = $request['sql_compatibility'];
}
if (isset($request['sql_no_auto_value_on_zero'])) {
$sql_modes[] = 'NO_AUTO_VALUE_ON_ZERO';
}
if (count($sql_modes) > 0) {
$dbi->tryQuery(
'SET SQL_MODE="' . implode(',', $sql_modes) . '"'
);
}
}
/**
* Look for special chars: comment, string or DELIMITER
*
* @param array $matches Special chars found in data
*
* @return array matches
*/
private function _searchSpecialChars(
$matches
) {
//Don't look for a string/comment/"DELIMITER" if not found previously
//or if it's still after current position.
if (null === $this->_firstSearchChar
|| (false !== $this->_firstSearchChar
&& $this->_firstSearchChar < $this->_delimiterPosition)
) {
$bFind = preg_match(
'/(\'|"|#|-- |\/\*|`|(?i)(?<![A-Z0-9_])'
. $this->_delimiterKeyword . ')/',
$this->_stringFctToUse['substr'](
$this->_data,
$this->_delimiterPosition
),
$matches,
PREG_OFFSET_CAPTURE
);
if (1 === $bFind) {
$this->_firstSearchChar = $matches[1][1] + $this->_delimiterPosition;
} else {
$this->_firstSearchChar = false;
}
}
return $matches;
}
/**
* Look for SQL delimiter
*
* @param int $firstSqlDelimiter First found char position
*
* @return int
*/
private function _searchSqlDelimiter($firstSqlDelimiter)
{
//Don't look for the SQL delimiter if not found previously
//or if it's still after current position.
if (null === $firstSqlDelimiter
|| (false !== $firstSqlDelimiter
&& $firstSqlDelimiter < $this->_delimiterPosition)
) {
// the cost of doing this one with preg_match() would be too high
$firstSqlDelimiter = $this->_stringFctToUse['strpos'](
$this->_data,
$this->_delimiter,
$this->_delimiterPosition
);
}
return $firstSqlDelimiter;
}
/**
* Set new delimiter
*
* @param string $delimiter New delimiter
*
* @return int delimiter length
*/
private function _setDelimiter($delimiter)
{
$this->_delimiter = $delimiter;
$this->_delimiterLength = $this->_stringFctToUse['strlen']($delimiter);
return $this->_delimiterLength;
}
/**
* Set data to parse
*
* @param string $data Data to parse
*
* @return int Data length
*/
private function _setData($data)
{
$this->_data = ltrim($data);
$this->_dataLength = $this->_stringFctToUse['strlen']($this->_data);
$this->_queryBeginPosition = 0;
$this->_delimiterPosition = 0;
return $this->_dataLength;
}
/**
* Add data to parse
*
* @param string $data Data to add to data to parse
*
* @return int Data length
*/
private function _addData($data)
{
$this->_data .= $data;
$this->_dataLength += $this->_stringFctToUse['strlen']($data);
return $this->_dataLength;
}
}