Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 264
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
VaultPress_Database
0.00% covered (danger)
0.00%
0 / 263
0.00% covered (danger)
0.00%
0 / 12
15500
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 attach
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_tables
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 show_create
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 explain
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 diff
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 count
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 wpdb
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
110
 get_cols
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
240
 convert_to_sql_string
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
 parse_create_table
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 1
2450
 restore
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
342
1<?php
2// don't call the file directly
3defined( 'ABSPATH' ) || die( 0 );
4
5class VaultPress_Database {
6
7    var $table = null;
8    var $pks = null;
9
10    /**
11     * Table structure.
12     *
13     * @var ?object
14     */
15    public $structure;
16
17    function __construct() {
18    }
19
20    function attach( $table, $parse_create_table = false ) {
21        $this->table=$table;
22        if ( $parse_create_table ) {
23            $this->structure = $this->parse_create_table( $this->show_create() );
24        }
25    }
26
27    function get_tables( $filter=null ) {
28        global $wpdb;
29        $rval = $wpdb->get_col( 'SHOW TABLES' );
30        if ( $filter )
31            $rval = preg_grep( $filter, $rval );
32        return $rval;
33    }
34
35    function show_create() {
36        global $wpdb;
37        if ( !$this->table )
38            return false;
39        $table = esc_sql( $this->table );
40        $results = $wpdb->get_row( "SHOW CREATE TABLE `$table`" );
41        $want = 'Create Table';
42        if ( $results ) {
43            if ( isset( $results->$want ) ) {
44                $results = $results->$want;
45            } else {
46                $results = false;
47            }
48        }
49        return $results;
50    }
51
52    function explain() {
53        global $wpdb;
54        if ( !$this->table )
55            return false;
56        $table = esc_sql( $this->table );
57        return $wpdb->get_results( "EXPLAIN `$table`" );
58    }
59
60    function diff( $signatures ) {
61        global $wpdb;
62        if ( !is_array( $signatures ) || !count( $signatures ) )
63            return false;
64        if ( !$this->table )
65            return false;
66        $table = esc_sql( $this->table );
67        $diff = array();
68        foreach ( $signatures as $where => $signature ) {
69            $pksig = md5( $where );
70            unset( $wpdb->queries );
71            $row = $wpdb->get_row( "SELECT * FROM `$table` WHERE $where" );
72            if ( !$row ) {
73                $diff[$pksig] = array ( 'change' => 'deleted', 'where' => $where );
74                continue;
75            }
76            $row = serialize( $row );
77            $hash = md5( $row );
78            if ( $hash != $signature )
79                $diff[$pksig] = array( 'change' => 'modified', 'where' => $where, 'signature' => $hash, 'row' => $row );
80        }
81        return $diff;
82    }
83
84    function count( $columns ) {
85        global $wpdb;
86        if ( !is_array( $columns ) || !count( $columns ) )
87            return false;
88        if ( !$this->table )
89            return false;
90        $table = esc_sql( $this->table );
91        $column = esc_sql( array_shift( $columns ) );
92        return $wpdb->get_var( "SELECT COUNT( $column ) FROM `$table`" );
93    }
94
95    function wpdb( $query, $function='get_results' ) {
96        global $wpdb;
97
98        if ( !is_callable( array( $wpdb, $function ) ) )
99            return false;
100
101        $res = $wpdb->$function( $query );
102        if ( !$res )
103            return $res;
104        switch ( $function ) {
105            case 'get_results':
106                foreach ( $res as $idx => $row ) {
107                    if ( isset( $row->option_name ) && $row->option_name == 'cron' )
108                        $res[$idx]->option_value = serialize( array() );
109                }
110                break;
111            case 'get_row':
112                if ( isset( $res->option_name ) && $res->option_name == 'cron' )
113                    $res->option_value = serialize( array() );
114                break;
115        }
116        return $res;
117    }
118
119    function get_cols( $columns, $limit=false, $offset=false, $where=false ) {
120        global $wpdb;
121        if ( !is_array( $columns ) || !count( $columns ) )
122            return false;
123        if ( !$this->table )
124            return false;
125        $table = esc_sql( $this->table );
126        $limitsql = '';
127        $offsetsql = '';
128        $wheresql = '';
129        if ( $limit )
130            $limitsql = ' LIMIT ' . intval( $limit );
131        if ( $offset )
132            $offsetsql = ' OFFSET ' . intval( $offset );
133        if ( $where )
134            $wheresql = ' WHERE ' . base64_decode($where);
135        $rval = array();
136        foreach ( $wpdb->get_results( "SELECT * FROM `$this->table` $wheresql $limitsql $offsetsql" ) as $row ) {
137            // We don't need to actually record a real cron option value, just an empty array
138            if ( isset( $row->option_name ) && $row->option_name == 'cron' )
139                $row->option_value = serialize( array() );
140            if ( !empty( $this->structure ) ) {
141                $hash = md5( $this->convert_to_sql_string( $row, $this->structure->columns ) );
142                foreach ( get_object_vars( $row ) as $i => $v ) {
143                    if ( !in_array( $i, $columns ) )
144                        unset( $row->$i );
145                }
146
147                $row->hash = $hash;
148            } else {
149                $keys = array();
150                $vals = array();
151                foreach ( get_object_vars( $row ) as $i => $v ) {
152                    $keys[] = sprintf( "`%s`", esc_sql( $i ) );
153                    $vals[] = sprintf( "'%s'", esc_sql( $v ) );
154                    if ( !in_array( $i, $columns ) )
155                        unset( $row->$i );
156                }
157                $row->hash = md5( sprintf( "(%s) VALUES(%s)", implode( ',',$keys ), implode( ',',$vals ) ) );
158            }
159            $rval[]=$row;
160        }
161        return $rval;
162    }
163
164    /**
165     * Convert a PHP object to a mysqldump compatible string, using the provided data type information.
166     **/
167    function convert_to_sql_string( $data, $datatypes ) {
168        global $wpdb;
169        if ( !is_object( $data ) || !is_object( $datatypes ) )
170            return false;
171
172        $keys = array();
173        $vals = array();
174
175        foreach ( array_keys( (array)$data ) as $key )
176            $keys[] = sprintf( "`%s`", esc_sql( $key ) );
177        foreach ( (array)$data as $key => $val ) {
178            if ( null === $val ) {
179                $vals[] = 'NULL';
180                continue;
181            }
182            $type = 'text';
183            if ( isset( $datatypes->$key->type ) )
184                $type= strtolower( $datatypes->$key->type );
185            if ( preg_match( '/int|double|float|decimal|bool/i', $type ) )
186                $type = 'number';
187
188            if ( 'number' === $type ) {
189                // do not add quotes to numeric types.
190                $vals[] = $val;
191            } else {
192                $val = esc_sql( $val );
193                // Escape characters that aren't escaped by esc_sql(): \n, \r, etc.
194                $val = str_replace( array( "\x0a", "\x0d", "\x1a" ), array( '\n', '\r', '\Z' ), $val );
195                $vals[] = sprintf( "'%s'", $val );
196            }
197        }
198        if ( !count($keys) )
199            return false;
200        // format the row as a mysqldump line: (`column1`, `column2`) VALUES (numeric_value1,'text value 2')
201        return sprintf( "(%s) VALUES (%s)", implode( ', ',$keys ), implode( ',',$vals ) );
202    }
203
204
205
206    function parse_create_table( $sql ) {
207        $table = new stdClass();
208
209        $table->raw = $sql;
210        $table->columns = new stdClass();
211        $table->primary = null;
212        $table->uniques = new stdClass();
213        $table->keys = new stdClass();
214        $sql = explode( "\n", trim( $sql ) );
215        $table->engine = preg_replace( '/^.+ ENGINE=(\S+) .+$/i', "$1", $sql[(count($sql)-1)] );
216        $table->charset = preg_replace( '/^.+ DEFAULT CHARSET=(\S+)( .+)?$/i', "$1", $sql[(count($sql)-1)] );
217        $table->single_int_paging_column = null;
218
219        foreach ( $sql as $idx => $val )
220            $sql[$idx] = trim($val);
221        $columns = preg_grep( '/^\s*`[^`]+`\s*\S*/', $sql );
222        if ( !$columns )
223            return false;
224
225        $table->name = preg_replace( '/(^[^`]+`|`[^`]+$)/', '', array_shift( preg_grep( '/^CREATE\s+TABLE\s+/', $sql ) ) );
226
227        foreach ( $columns as $line ) {
228            preg_match( '/^`([^`]+)`\s+([a-z]+)(\(\d+\))?\s*/', $line, $m );
229            $name = $m[1];
230            $table->columns->$name = new stdClass();
231            $table->columns->$name->null = (bool)stripos( $line, ' NOT NULL ' );
232            $table->columns->$name->type = $m[2];
233            if ( isset($m[3]) ) {
234                if ( substr( $m[3], 0, 1 ) == '(' )
235                    $table->columns->$name->length = substr( $m[3], 1, -1 );
236                else
237                    $table->columns->$name->length = $m[3];
238            } else {
239                $table->columns->$name->length = null;
240            }
241            if ( preg_match( '/ character set (\S+)/i', $line, $m ) ) {
242                $table->columns->$name->charset = $m[1];
243            } else {
244                $table->columns->$name->charset = '';
245            }
246            if ( preg_match( '/ collate (\S+)/i', $line, $m ) ) {
247                $table->columns->$name->collate = $m[1];
248            } else {
249                $table->columns->$name->collate = '';
250            }
251            if ( preg_match( '/ DEFAULT (.+),$/i', $line, $m ) ) {
252                if ( substr( $m[1], 0, 1 ) == "'" )
253                    $table->columns->$name->default = substr( $m[1], 1, -1 );
254                else
255                    $table->columns->$name->default = $m[1];
256            } else {
257                $table->columns->$name->default = null;
258            }
259            $table->columns->$name->line = $line;
260        }
261        $pk = preg_grep( '/^PRIMARY\s+KEY\s+/i', $sql );
262        if ( count( $pk ) ) {
263            $pk = array_pop( $pk );
264            $pk = preg_replace( '/(^[^\(]+\(`|`\),?$)/', '', $pk );
265            $pk = preg_replace( '/\([0-9]+\)/', '', $pk );
266            $pk = explode( '`,`', $pk );
267            $table->primary = $pk;
268        }
269        if ( is_array( $table->primary ) && count( $table->primary ) == 1 ) {
270            $pk_column_name = $table->primary[0];
271            switch( strtolower( $table->columns->$pk_column_name->type ) ) {
272                // Integers, exact value
273                case 'tinyint':
274                case 'smallint':
275                case 'int':
276                case 'integer':
277                case 'bigint':
278                    // Fixed point, exact value
279                case 'decimal':
280                case 'numeric':
281                    // Floating point, approximate value
282                case 'float':
283                case 'double':
284                case 'real':
285                    // Date and Time
286                case 'date':
287                case 'datetime':
288                case 'timestamp':
289                    $table->single_int_paging_column = $pk_column_name;
290                    break;
291            }
292        }
293        $keys = preg_grep( '/^((?:UNIQUE )?INDEX|(?:UNIQUE )?KEY)\s+/i', $sql );
294        if ( !count( $keys ) )
295            return $table;
296        foreach ( $keys as $idx => $key ) {
297            if ( 0 === strpos( $key, 'UNIQUE' ) )
298                $is_unique = false;
299            else
300                $is_unique = true;
301
302            // for KEY `refresh` (`ip`,`time_last`) USING BTREE,
303            $key = preg_replace( '/ USING \S+ ?(,?)$/', '$1', $key );
304
305            // for KEY `id` USING BTREE (`id`),
306            $key = preg_replace( '/` USING \S+ \(/i', '` (', $key );
307
308                    $key = preg_replace( '/^((?:UNIQUE )?INDEX|(?:UNIQUE )?KEY)\s+/i', '', $key );
309                    $key = preg_replace( '/\([0-9]+\)/', '', $key );
310                    preg_match( '/^`([^`]+)`\s+\(`(.+)`\),?$/', $key, $m );
311                    $key = $m[1]; //preg_replace( '/\([^)]+\)/', '', $m[1]);
312                    if ( !$key )
313                        continue;
314                    if ( $is_unique )
315                        $table->keys->$key = explode( '`,`', $m[2] );
316                    else
317                        $table->uniques->$key = explode( '`,`', $m[2] );
318        }
319
320        $uniques = get_object_vars( $table->uniques );
321        foreach( $uniques as $idx => $val ) {
322            if ( is_array( $val ) && count( $val ) == 1 ) {
323                $pk_column_name = $val[0];
324                switch( strtolower( $table->columns->$pk_column_name->type ) ) {
325                    // Integers, exact value
326                    case 'tinyint':
327                    case 'smallint':
328                    case 'int':
329                    case 'integer':
330                    case 'bigint':
331                        // Fixed point, exact value
332                    case 'decimal':
333                    case 'numeric':
334                        // Floating point, approximate value
335                    case 'float':
336                    case 'double':
337                    case 'real':
338                        // Date and Time
339                    case 'date':
340                    case 'datetime':
341                    case 'timestamp':
342                        $table->single_int_paging_column = $pk_column_name;
343                        break;
344                }
345            }
346        }
347
348        if ( empty( $table->primary ) ) {
349            if ( !empty( $uniques ) )
350                $table->primary = array_shift( $uniques );
351        }
352
353        return $table;
354    }
355
356    function restore( $data_file, $md5_sum, $delete = true ) {
357        global $wpdb;
358        if ( !file_exists( $data_file ) || !is_readable( $data_file ) || !filesize( $data_file ) )
359            return array( 'last_error' => 'File does not exist', 'data_file' => $data_file );
360        if ( $md5_sum && md5_file( $data_file ) !== $md5_sum )
361            return array( 'last_error' => 'Checksum mistmatch', 'data_file' => $data_file );
362        if ( function_exists( 'exec' ) && ( $mysql = exec( 'which mysql' ) ) ) {
363            $details = explode( ':', DB_HOST, 2 );
364            $params  = array( defined( 'DB_CHARSET' ) && DB_CHARSET ? DB_CHARSET : 'utf8', DB_USER, DB_PASSWORD, $details[0], $details[1] ?? 3306, DB_NAME, $data_file );
365            exec( sprintf( '%s %s', escapeshellcmd( $mysql ), vsprintf( '-A --default-character-set=%s -u%s -p%s -h%s -P%s %s < %s', array_map( 'escapeshellarg', $params ) ) ), $output, $r );
366            if ( 0 === $r ) {
367                if ( $delete )
368                    @unlink( $data_file );
369                return array( 'affected_rows' => 1, 'data_file' => $data_file, 'mysql_cli' => true );
370            }
371        }
372        $size = filesize( $data_file );
373        $fh = fopen( $data_file, 'r' );
374        $last_error = false;
375        $affected_rows = 0;
376        if ( $size == 0 || !is_resource( $fh ) ) {
377            if ( $delete )
378                @unlink( $data_file );
379            return array( 'last_error' => 'Empty file or not readable', 'data_file' => $data_file );
380        } else {
381            while( !feof( $fh ) ) {
382                $query = trim( stream_get_line( $fh, $size, ";\n" ) );
383                if ( !empty( $query ) ) {
384                    $affected_rows += $wpdb->query( $query );
385                    $last_error = $wpdb->last_error;
386                }
387            }
388            fclose( $fh );
389        }
390        if ( $delete )
391            @unlink( $data_file );
392        return array( 'affected_rows' => $affected_rows, 'last_error' => $last_error, 'data_file' => $data_file );
393    }
394}