Project

General

Profile

1
<?php
2

    
3
/**
4
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
 */
19

    
20
/**
21
 * SqlImport.php
22
 *
23
 * @category     Core
24
 * @package      Core_Database
25
 * @copyright    Manuela v.d.Decken <manuela@isteam.de>
26
 * @author       Manuela v.d.Decken <manuela@isteam.de>
27
 * @license      http://www.gnu.org/licenses/gpl.html   GPL License
28
 * @version      0.0.2
29
 * @revision     $Revision: 2070 $
30
 * @lastmodified $Date: 2014-01-03 02:21:42 +0100 (Fr, 03 Jan 2014) $
31
 * @since        File available since 30.11.2014
32
 * @description  can handle different actions with SQL-Import files
33
 */
34

    
35
class SqlImport
36
{
37

    
38
/** WbDatabase object */
39
    protected $oDb          = null;
40
/** valide structure file to use for import */
41
    protected $sStructFile  = '';
42
/** valide TablePrefix to use for import */
43
    protected $sTablePrefix = '';
44
/** default Collation to use for import of chars/text */
45
    protected $sCollation   = 'utf8_unicode_ci';
46
/** default engine for tables */
47
    protected $sEngine      = 'MyISAM';
48
/** collected error messages */
49
    protected $aErrMsg      = array();
50
/** possible actions for doImport() */
51
    protected $aActions     = array(
52
        'uninstall',
53
        'install',
54
        'upgrade'
55
    );
56
/** collected additional replacements pairs */
57
    protected $aAdditionalReplacements = array('key'=>array(), 'value'=>array());
58
/** possible engines to use */
59
    protected $aEngineTypesAvail = array(
60
        'myisam'     => 'MyISAM',
61
        'innodb'     => 'InnoDB'
62
    );
63
/** SQL objects witch can be handled */
64
    protected $aAvailSqlObjects = array(
65
        'TABLE',
66
        'VIEW',
67
        'INDEX',
68
        'PROCEDURE',
69
        'FUNCTION',
70
        'TRIGGER',
71
        'EVENT'
72
    );
73
/** prepared batch lists for all SQL statements */
74
    protected $aBatchLists = array();
75
/* ****************************************************************** */
76
/**
77
 * Constructor
78
 * @param WbDatabase $oDb
79
 * @param string $sStructFile
80
 */
81
    public function __construct(WbDatabase $oDb, $sStructFile)
82
    {
83
        $this->oDb = $oDb;
84
        $this->sTablePrefix = $oDb->TablePrefix;
85
        $sStructFile = str_replace('\\', '/', $sStructFile);
86
        if (!file_exists($sStructFile)) {
87
            throw new SqlImportException('no existing import file defined');
88
        }
89
        $this->sStructFile = $sStructFile;
90
    }
91
// ---------------------------------------------------------------------
92
/**
93
 * Start and execute the import
94
 * @param string $sAction
95
 * @return boolean
96
 */
97
    public function doImport($sAction)
98
    {
99
        try{
100
            $sAction           = $this->getSanitizeAction($sAction);
101
            $aRawCommandSource = $this->getSorceFile($this->sStructFile);
102
            $aSqlCommandList   = $this->doNormalizeSource($aRawCommandSource);
103
            $aSqlCommandList   = $this->replacePlaceholders($aSqlCommandList);
104
            $this->splitStatementsListIntoBatchLists($aSqlCommandList, $sAction);
105
            unset($aRawCommandSource, $aSqlCommandList); // free never needed memory
106
            $this->executeBatchStack();
107
        } catch(SqlImportException $e) {
108
            $this->aErrMsg[] = (string)$e;
109
            return false;
110
        }
111
        return true;
112
    }
113
// ---------------------------------------------------------------------
114
/**
115
 * Sanitize Action and get a valid value
116
 * @param string $sAction simple command or filename/filepath
117
 * @return string valid command
118
 */
119
    protected function getSanitizeAction($sAction)
120
    {
121
        $sAction = strtolower( // extract command from given argument
122
            preg_replace(
123
                '/^.*?('.implode('|', $this->aActions).')(\.php)?$/i',
124
                '$1',
125
                $sAction
126
            )
127
        );
128
        // return 'install' if no command found
129
        return ($sAction == '' ? 'install' : $sAction);
130
    }
131
// ---------------------------------------------------------------------
132
/**
133
 * Get content of structure file
134
 * @param string $sStructFile filepath of the structure file
135
 * @return array raw list of commands (including comments)
136
 */
137
    protected function getSorceFile($sStructFile)
138
    {
139
        if (!$sStructFile) {
140
        // no file found so ignore import method
141
            throw new SqlImportException('no import file defined');
142
        }
143
        if (!is_readable($sStructFile)) {
144
        // file fond but it's not readable
145
            throw new SqlImportException('not readable file ['.$sStructFile.']');
146
        }
147
        if (!($aCommandSource = file($sStructFile, FILE_SKIP_EMPTY_LINES|FILE_IGNORE_NEW_LINES))) {
148
        // file fond but it's not readable
149
            throw new SqlImportException('unable to import file ['.$sStructFile.']');
150
        }
151
        return $aCommandSource;
152
    }
153
// ---------------------------------------------------------------------
154
/**
155
 * Normalize SQL statements from source
156
 * @param array $aSqlCommandSource array of all lines from source file
157
 * @return array list of complete SQL statements with placeholders
158
 * @description concate each Statement into one line; remove all comments; remove possible BOM
159
 */
160
    protected function doNormalizeSource(array $aRawCommandSource)
161
    {
162
        $aSqlCommandList = array();
163
        // remove possible ByteOrderMark
164
        $aRawCommandSource[0] = preg_replace('/^[\xAA-\xFF]{3}/', '', $aRawCommandSource[0]);
165
        // init empty statement line
166
        $sStatementLine = '';
167
        // iterate through all lines of the source
168
        foreach ($aRawCommandSource as $iKey => $sRawLine) {
169
            // remove trailing and leading whitespaces
170
            $sRawLine = trim($sRawLine);
171
            // skip line if it's a comment
172
            if (preg_match('/^--/', $sRawLine)) { continue; }
173
            // attach line to buffer
174
            $sStatementLine .= ' '.$sRawLine;
175
            // detect end of statement
176
            if ((substr($sStatementLine,-1,1) == ';')) {
177
                $aSqlCommandList[] = trim($sStatementLine);
178
                $sStatementLine = '';
179
            }
180
        }
181
        return $aSqlCommandList;
182
    }
183
// ---------------------------------------------------------------------
184
/**
185
 * Replace all Placeholders in all SQL statements
186
 * @param array $aSqlCommandList  list of SQL statements with placeholders
187
 * @return array finished list of SQL statements
188
 */
189
    protected function replacePlaceholders(array $aSqlCommandList)
190
    {
191
        // sanitize arguments and extract Charset from Collation
192
        $aTmp = preg_split('/_/', $this->sCollation, null, PREG_SPLIT_NO_EMPTY);
193
        $sCharset = $aTmp[0];
194
        // get from addReplacements
195
        $aSearch  = $this->aAdditionalReplacements['key'];
196
        /* ***  ATTENTION:: Do Not Change The Order Of Search-Replace Statements !! *** */
197
        // define basic array of searches
198
        $aSearch[] = '/\{TABLE_PREFIX\}/';                                        /* step 0 */
199
        $aSearch[] = '/\{FIELD_CHARSET\}/';                                       /* step 1 */
200
        $aSearch[] = '/\{FIELD_COLLATION\}/';                                     /* step 2 */
201
        $aSearch[] = '/\{TABLE_ENGINE\}/';                                        /* step 3 */
202
        $aSearch[] = '/\{TABLE_ENGINE=([a-zA-Z_0-9]*)\}/';                        /* step 4 */
203
        $aSearch[] = '/\{CHARSET\}/';                                             /* step 5 */
204
        $aSearch[] = '/\{COLLATION\}/';                                           /* step 6 */
205
        // get from addReplacements
206
        $aReplace = $this->aAdditionalReplacements['value'];
207
        // define basic array of replacements
208
        $aReplace[] = $this->sTablePrefix;                                        /* step 0 */
209
        $aReplace[] = ' CHARACTER SET {CHARSET}';                                 /* step 1 */
210
        $aReplace[] = ' COLLATE {COLLATION}';                                     /* step 2 */
211
        $aReplace[] = ' {TABLE_ENGINE='.$this->sEngine.'}';                       /* step 3 */
212
        $aReplace[] = ' ENGINE=$1 DEFAULT CHARSET={CHARSET} COLLATE={COLLATION}'; /* step 4 */
213
        $aReplace[] = $sCharset;                                                  /* step 5 */
214
        $aReplace[] = $this->sCollation;                                          /* step 6 */
215
        // replace placeholders in statement
216
        $aSqlCommandList = preg_replace($aSearch, $aReplace, $aSqlCommandList);
217
        return $aSqlCommandList;
218
    }
219
// ---------------------------------------------------------------------
220
/**
221
 * Split list of commands into a batch list structure
222
 * @param array $aSqlCommandList normalized list of SQL statements
223
 * @param string $sAction a valid name of an action
224
 *
225
 */
226
    protected function splitStatementsListIntoBatchLists(array $aSqlCommandList, $sAction)
227
    {
228
        // initialize lists
229
        $this->aBatchLists = array(
230
            'Set'    => array(
231
                            'Header' => array(), // all SET before DROP/CREATE/ALTER
232
                            'Footer' => array()  // all SET after DROP/CREATE/ALTER
233
                        ),
234
            'Drop'   => array(),
235
            'Create' => array(),
236
            'Alter'  => array()
237
        );
238
        $sSetType = 'Header'; // start with 'before'
239
        foreach ($aSqlCommandList as $sSqlCommand) {
240
            if (preg_match('/^\s*DROP\s*/si', $sSqlCommand)) {
241
                switch ($sAction) {
242
                    case 'uninstall':
243
                    case 'install';
244
                        $this->aBatchLists['Drop'][] = $sSqlCommand;
245
                        break;
246
                    default: // skip DROP TABLE
247
                        break;
248
                }
249
                $sSetType = 'Footer'; // switch to 'after'
250
            } elseif (preg_match('/^\s*ALTER\s*/si', $sSqlCommand)) {
251
                switch ($sAction) {
252
                    case 'upgrade':
253
                    case 'install';
254
                        $this->aBatchLists['Alter'][] = $sSqlCommand;
255
                        break;
256
                    default: // skip ALTER TABLE
257
                        break;
258
                }
259
                $sSetType = 'Footer'; // switch to 'after'
260
            } elseif (preg_match('/^\s*\/\*\![^\s]+\s+SET\s|^\s*SET\s*/si', $sSqlCommand)) {
261
                $this->aBatchLists['Set'][$sSetType][] = $sSqlCommand;
262
            } else {
263
                switch ($sAction) {
264
                    case 'upgrade':
265
                    case 'install';
266
                        $this->aBatchLists['Create'][] = $sSqlCommand;
267
                        break;
268
                    default: // skip CREATE TABLE
269
                        break;
270
                }
271
                $sSetType = 'Footer'; // switch to 'after'
272
            }
273
        }
274
    }
275
// ---------------------------------------------------------------------
276
/**
277
 * Scan database for all matching tables
278
 * @return array List of matchng tables
279
 */
280
    protected function getAllExistingTablenames()
281
    {
282
        $aRetval = array();
283
        $sql = 'SHOW TABLES LIKE \''.addcslashes($this->sTablePrefix, '_%').'%\'';
284
        if ((($oRecordSet = $this->oDb->doQuery($sql)))) {
285
            while (($aRecord = $oRecordSet->fetchArray(MYSQLI_NUM))) {
286
                $aRetval[] = $aRecord[0];
287
            }
288
        }
289
        return $aRetval;
290
    }
291
// ---------------------------------------------------------------------
292
/**
293
 * Execute a stack of batch lists with SQL statements
294
 * @param array $aAllowed  list of tables which are allowed to be altered
295
 * @return boolean
296
 */
297
    protected function executeBatchStack()
298
    {
299
        if ($this->aBatchLists['Set']['Header']) {
300
            $this->executeBatchList($this->aBatchLists['Set']['Header']);
301
        }
302
        if ($this->aBatchLists['Drop']) {
303
            $this->executeBatchList($this->aBatchLists['Drop']);
304
        }
305
        if ($this->aBatchLists['Create']) {
306
            $aTablesListBeforeCreate = $this->getAllExistingTablenames();
307
            $this->executeBatchList($this->aBatchLists['Create']);
308
            $aTablesListAfterCreate = $this->getAllExistingTablenames();
309
        }
310
        if ($this->aBatchLists['Alter']) {
311
            $this->executeBatchList(
312
                $this->aBatchLists['Alter'],
313
                array_diff($aTablesListAfterCreate, $aTablesListBeforeCreate)
314
            );
315
        }
316
        if ($this->aBatchLists['Set']['Footer']) {
317
            $this->executeBatchList($this->aBatchLists['Set']['Footer']);
318
        }
319
    }
320
// ---------------------------------------------------------------------
321
/**
322
 * Execute a list of SQL statements
323
 * @param array $aList the list to execute
324
 * @param array $aAllowedTables list with allowed tables
325
 * @description Execute a list of SQL statements. The list can be restricted
326
 *              using $aAllowedTables
327
 */
328
    protected function executeBatchList(array $aList, array $aAllowedTables = null)
329
    {
330
        foreach ($aList as $sSql) {
331
            if ($aAllowedTables) {
332
                // extract name of table
333
                $sTableName = preg_replace('/^[^\`]*?\`([^\`]*?)\`.*$/si', '$1', $sSql);
334
                if (!in_array($sTableName, $aAllowedTables)) {
335
                // if table is not in allowed list then use next statement
336
                    continue;
337
                }
338
            }
339
            // execute statement
340
            if (!$this->oDb->doQuery(trim($sSql))) {
341
                throw new SqlImportException($this->oDb->getError());
342
                break;
343
            }
344
        }
345
    }
346
// ---------------------------------------------------------------------
347
/**
348
 * Add Key-Value pairs for additional placeholders
349
 * @param string $sKey
350
 * @param mixed $sValue
351
 */
352
    public function addReplacement($sKey, $sValue = '')
353
    {
354
        $sKey = strtoupper(preg_replace('/([a-z0-9])([A-Z])/', '\1_\2', ltrim($sKey, 'a..z')));
355
        $this->aAdditionalReplacements['key'][]  = '/\{'.$sKey.'\}/';
356
        $this->aAdditionalReplacements['value'][] = $sValue;
357
    }
358
// ---------------------------------------------------------------------
359
/**
360
 * define another collation then default utf8_unicode_ci
361
 * @param string $sCollation
362
 * @return boolean
363
 */
364
    public function setDefaultCollation($sCollation = 'utf8_unicode_ci')
365
    {
366
        $bRetval = false;
367
        // test if selected collation is available on current server.
368
        $sql = 'SHOW COLLATION LIKE \''.$this->oDb->escapeString(addcslashes($sCollation, '_%')).'\'';
369
        if (($oAvailCollations = $this->oDb->doQuery($sql))) {
370
            if (($oAvailCollations->numRows())) {
371
                //use new collation
372
                $this->sCollation = $sCollation;
373
                $bRetval = true;
374
            }
375
        }
376
        return $bRetval;
377
    }
378
// ---------------------------------------------------------------------
379
/**
380
 * define another default engine then 'MyISAM'
381
 * @param string $sEngine
382
 * @return boolean
383
 */
384
    public function setDefaultEngine($sEngine ='MyISAM')
385
    {
386
        $bRetval = false;
387
        if (isset($this->aEngineTypesAvail[strtolower($sEngine)])) {
388
            // use new engine
389
            $this->sEngine = $this->aEngineTypesAvail[strtolower($sEngine)];
390
            $bRetval = true;
391
        }
392
        return $bRetval;
393
    }
394
// ---------------------------------------------------------------------
395
/**
396
 * define another prefix then default in Wbdatabase
397
 * @param string $sTablePrefix
398
 * @return boolean
399
 */
400
    public function setTablePrefix($sTablePrefix = '')
401
    {
402
        $bRetval = false;
403
        if (
404
        // Prefix must be empty or matching allowed table names
405
            $sTablePrefix == '' ||
406
            preg_match('/^[a-z][a-z0-9_]*$/i', $sTablePrefix)
407
        ) {
408
            // use new TablePrefix
409
            $this->sTablePrefix = $sTablePrefix;
410
            $bRetval = true;
411
        }
412
        return $bRetval;
413
    }
414
// ---------------------------------------------------------------------
415
/**
416
 * return errorstrings, concatet by LF
417
 * @return string
418
 */
419
    public function getError()
420
    {
421
        return implode("\n", $this->aErrMsg);
422
    }
423

    
424
} // end of class SqlImport
425
// ---------------------------------------------------------------------
426
// ---------------------------------------------------------------------
427

    
428
class SqlImportException extends AppException {}
429

    
(10-10/38)