BatchInsert::createTableFromCSVFile()   F
last analyzed

Complexity

Conditions 13
Paths 288

Size

Total Lines 86
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
cc 13
eloc 43
nc 288
nop 4
dl 0
loc 86
ccs 0
cts 55
cp 0
crap 182
rs 3.7737
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Piwik - free/libre analytics platform
4
 *
5
 * @link http://piwik.org
6
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
 *
8
 */
9
namespace Piwik\Db;
10
11
use Exception;
12
use Piwik\Common;
13
use Piwik\Config;
14
use Piwik\Container\StaticContainer;
15
use Piwik\Db;
16
use Piwik\Log;
17
use Piwik\SettingsServer;
18
19
class BatchInsert
20
{
21
    /**
22
     * Performs a batch insert into a specific table by iterating through the data
23
     *
24
     * NOTE: you should use tableInsertBatch() which will fallback to this function if LOAD DATA INFILE not available
25
     *
26
     * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
27
     * @param array $fields array of unquoted field names
28
     * @param array $values array of data to be inserted
29
     * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows
30
     */
31
    public static function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = true)
32
    {
33
        $fieldList = '(' . join(',', $fields) . ')';
34
        $ignore    = $ignoreWhenDuplicate ? 'IGNORE' : '';
35
36
        foreach ($values as $row) {
37
            $query = "INSERT $ignore INTO " . $tableName . "
38
					  $fieldList
39
					  VALUES (" . Common::getSqlStringFieldsArray($row) . ")";
40
            Db::query($query, $row);
41
        }
42
    }
43
44
    /**
45
     * Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs,
46
     * as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs.
47
     *
48
     * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
49
     * @param array $fields array of unquoted field names
50
     * @param array $values array of data to be inserted
51
     * @param bool $throwException Whether to throw an exception that was caught while trying
52
     *                                LOAD DATA INFILE, or not.
53
     * @param string $charset The charset to use, defaults to utf8
54
     * @throws Exception
55
     * @return bool  True if the bulk LOAD was used, false if we fallback to plain INSERTs
56
     */
57
    public static function tableInsertBatch($tableName, $fields, $values, $throwException = false, $charset = 'utf8')
58
    {
59
        $loadDataInfileEnabled = Config::getInstance()->General['enable_load_data_infile'];
0 ignored issues
show
Documentation introduced by
The property General does not exist on object<Piwik\Config>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
60
61
        if ($loadDataInfileEnabled
62
            && Db::get()->hasBulkLoader()) {
63
64
            $path = self::getBestPathForLoadData();
65
            $filePath = $path . $tableName . '-' . Common::generateUniqId() . '.csv';
66
67
            try {
68
                $fileSpec = array(
69
                    'delim'            => "\t",
70
                    'quote'            => '"', // chr(34)
71
                    'escape'           => '\\\\', // chr(92)
72
                    'escapespecial_cb' => function ($str) {
73
                            return str_replace(array(chr(92), chr(34)), array(chr(92) . chr(92), chr(92) . chr(34)), $str);
74
                        },
75
                    'eol'              => "\r\n",
76
                    'null'             => 'NULL',
77
                    'charset'          => $charset
78
                );
79
80
                self::createCSVFile($filePath, $fileSpec, $values);
81
82
                if (!is_readable($filePath)) {
83
                    throw new Exception("File $filePath could not be read.");
84
                }
85
86
                $rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec);
87
                if ($rc) {
88
                    unlink($filePath);
89
                    return true;
90
                }
91
            } catch (Exception $e) {
92
                if ($throwException) {
93
                    throw $e;
94
                }
95
            }
96
97
            // if all else fails, fallback to a series of INSERTs
98
            if (file_exists($filePath)) {
99
                @unlink($filePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
100
            }
101
        }
102
103
        self::tableInsertBatchIterate($tableName, $fields, $values);
104
105
        return false;
106
    }
107
108
    private static function getBestPathForLoadData()
109
    {
110
        try {
111
            $path = Db::fetchOne('SELECT @@secure_file_priv'); // was introduced in 5.0.38
112
        } catch (Exception $e) {
113
            // we do not rethrow exception as an error is expected if MySQL is < 5.0.38
114
            // in this case tableInsertBatch might still work
115
        }
116
117
        if (empty($path) || !@is_dir($path) || !@is_writable($path)) {
118
            $path = StaticContainer::get('path.tmp') . '/assets/';
119
        } elseif (!Common::stringEndsWith($path, '/')) {
120
            $path .= '/';
121
        }
122
123
        return $path;
124
    }
125
126
    /**
127
     * Batch insert into table from CSV (or other delimited) file.
128
     *
129
     * @param string $tableName Name of table
130
     * @param array $fields Field names
131
     * @param string $filePath Path name of a file.
132
     * @param array $fileSpec File specifications (delimiter, line terminator, etc)
133
     *
134
     * @throws Exception
135
     * @return bool  True if successful; false otherwise
136
     */
137
    public static function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec)
138
    {
139
        // Chroot environment: prefix the path with the absolute chroot path
140
        $chrootPath = Config::getInstance()->General['absolute_chroot_path'];
0 ignored issues
show
Documentation introduced by
The property General does not exist on object<Piwik\Config>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
141
        if (!empty($chrootPath)) {
142
            $filePath = $chrootPath . $filePath;
143
        }
144
145
        // On Windows, MySQL expects forward slashes as directory separators
146
        if (SettingsServer::isWindows()) {
147
            $filePath = str_replace('\\', '/', $filePath);
148
        }
149
150
        $query = "
151
				'$filePath'
152
			REPLACE
153
			INTO TABLE
154
				`" . $tableName . "`";
155
156
        if (isset($fileSpec['charset'])) {
157
            $query .= ' CHARACTER SET ' . $fileSpec['charset'];
158
        }
159
160
        $fieldList = '(' . join(',', $fields) . ')';
161
162
        $query .= "
163
			FIELDS TERMINATED BY
164
				'" . $fileSpec['delim'] . "'
165
			ENCLOSED BY
166
				'" . $fileSpec['quote'] . "'
167
		";
168
        if (isset($fileSpec['escape'])) {
169
            $query .= " ESCAPED BY '" . $fileSpec['escape'] . "'";
170
        }
171
        $query .= "
172
			LINES TERMINATED BY
173
				'" . $fileSpec['eol'] . "'
174
			$fieldList
175
		";
176
177
        /*
178
         * First attempt: assume web server and MySQL server are on the same machine;
179
         * this requires that the db user have the FILE privilege; however, since this is
180
         * a global privilege, it may not be granted due to security concerns
181
         */
182
        $keywords = array('');
183
184
        /*
185
         * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server;
186
         * the LOCAL keyword may trigger a known PHP PDO\MYSQL bug when MySQL not built with --enable-local-infile
187
         * @see http://bugs.php.net/bug.php?id=54158
188
         */
189
        $openBaseDir = ini_get('open_basedir');
190
        $safeMode    = ini_get('safe_mode');
191
192
        if (empty($openBaseDir) && empty($safeMode)) {
193
            // php 5.x - LOAD DATA LOCAL INFILE is disabled if open_basedir restrictions or safe_mode enabled
194
            $keywords[] = 'LOCAL ';
195
        }
196
197
        $exceptions = array();
198
        foreach ($keywords as $keyword) {
199
            $queryStart = 'LOAD DATA ' . $keyword . 'INFILE ';
200
            $sql = $queryStart . $query;
201
            try {
202
                $result = @Db::exec($sql);
203
                if (empty($result) || $result < 0) {
204
                    continue;
205
                }
206
207
                return true;
208
            } catch (Exception $e) {
209
                $code = $e->getCode();
210
                $message = $e->getMessage() . ($code ? "[$code]" : '');
211
                $exceptions[] = "\n  Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message;
212
            }
213
        }
214
215
        if (count($exceptions)) {
216
            $message = "LOAD DATA INFILE failed... Error was: " . implode(",", $exceptions);
217
            Log::info($message);
0 ignored issues
show
Deprecated Code introduced by
The method Piwik\Log::info() has been deprecated with message: Inject and call Psr\Log\LoggerInterface::info() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
218
            throw new Exception($message);
219
        }
220
221
        return false;
222
    }
223
224
    /**
225
     * Create CSV (or other delimited) files
226
     *
227
     * @param string $filePath filename to create
228
     * @param array $fileSpec File specifications (delimiter, line terminator, etc)
229
     * @param array $rows Array of array corresponding to rows of values
230
     * @throws Exception  if unable to create or write to file
231
     */
232
    protected static function createCSVFile($filePath, $fileSpec, $rows)
233
    {
234
        // Set up CSV delimiters, quotes, etc
235
        $delim = $fileSpec['delim'];
236
        $quote = $fileSpec['quote'];
237
        $eol   = $fileSpec['eol'];
238
        $null  = $fileSpec['null'];
239
        $escapespecial_cb = $fileSpec['escapespecial_cb'];
240
241
        $fp = @fopen($filePath, 'wb');
242
        if (!$fp) {
243
            throw new Exception('Error creating the tmp file ' . $filePath . ', please check that the webserver has write permission to write this file.');
244
        }
245
246
        foreach ($rows as $row) {
247
            $output = '';
248
            foreach ($row as $value) {
249
                if (!isset($value) || is_null($value) || $value === false) {
250
                    $output .= $null . $delim;
251
                } else {
252
                    $output .= $quote . $escapespecial_cb($value) . $quote . $delim;
253
                }
254
            }
255
256
            // Replace delim with eol
257
            $output = substr_replace($output, $eol, -1);
258
259
            $ret = fwrite($fp, $output);
260
            if (!$ret) {
261
                fclose($fp);
262
                throw new Exception('Error writing to the tmp file ' . $filePath);
263
            }
264
        }
265
266
        fclose($fp);
267
268
        @chmod($filePath, 0777);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
269
    }
270
}
271