Passed
Push — v3 ( 456560...320aa9 )
by Andrew
17:53 queued 11:25
created

FileController   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 292
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 27
eloc 158
c 8
b 0
f 0
dl 0
loc 292
ccs 0
cts 174
cp 0
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A importCsvApi8() 0 21 4
A importCsvApi9() 0 22 5
A actionExportStatistics() 0 4 1
B actionImportCsv() 0 73 7
A exportCsvFile() 0 19 2
B actionImportCsvColumns() 0 49 7
A actionExportRedirects() 0 4 1
1
<?php
2
/**
3
 * Retour plugin for Craft CMS 3.x
4
 *
5
 * Retour allows you to intelligently redirect legacy URLs, so that you don't
6
 * lose SEO value when rebuilding & restructuring a website
7
 *
8
 * @link      https://nystudio107.com/
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
9
 * @copyright Copyright (c) 2018 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
10
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
11
12
namespace nystudio107\retour\controllers;
13
14
use nystudio107\retour\Retour;
15
use nystudio107\retour\assetbundles\retour\RetourImportAsset;
16
use nystudio107\retour\helpers\MultiSite as MultiSiteHelper;
17
use nystudio107\retour\helpers\Permission as PermissionHelper;
18
use nystudio107\retour\helpers\Version as VersionHelper;
19
20
use Craft;
21
use craft\db\Query;
22
use craft\helpers\ArrayHelper;
23
use craft\helpers\UrlHelper;
24
use craft\web\Controller;
25
26
use yii\base\InvalidConfigException;
27
use yii\web\Response;
28
use yii\web\UploadedFile;
29
30
use League\Csv\AbstractCsv;
31
use League\Csv\Reader;
32
use League\Csv\Statement;
0 ignored issues
show
Bug introduced by
The type League\Csv\Statement was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use League\Csv\Writer;
34
35
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
36
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
37
 * @package   Retour
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
38
 * @since     3.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
39
 */
0 ignored issues
show
Coding Style introduced by
Missing @link tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @category tag in class comment
Loading history...
40
class FileController extends Controller
41
{
42
    // Constants
43
    // =========================================================================
44
45
    const DOCUMENTATION_URL = 'https://github.com/nystudio107/craft-retour/';
46
47
    const EXPORT_REDIRECTS_CSV_FIELDS = [
48
        'redirectSrcUrl' => 'Legacy URL Pattern',
49
        'redirectDestUrl' => 'Redirect To',
50
        'redirectMatchType' => 'Match Type',
51
        'redirectHttpCode' => 'HTTP Status',
52
        'siteId' => 'Site ID',
53
        'redirectSrcMatch' => 'Legacy URL Match Type',
54
        'hitCount' => 'Hits',
55
        'hitLastTime' => 'Last Hit',
56
    ];
57
58
    const EXPORT_STATISTICS_CSV_FIELDS = [
59
        'redirectSrcUrl' => '404 File Not Found URL',
60
        'referrerUrl' => 'Last Referrer URL',
61
        'remoteIp' => 'Remote IP',
62
        'hitCount' => 'Hits',
63
        'hitLastTime' => 'Last Hit',
64
        'handledByRetour' => 'Handled',
65
        'siteId' => 'Site ID',
66
    ];
67
68
    const IMPORT_REDIRECTS_CSV_FIELDS = [
69
        'redirectSrcUrl',
70
        'redirectDestUrl',
71
        'redirectMatchType',
72
        'redirectHttpCode',
73
        'siteId',
74
        'redirectSrcMatch',
75
        'hitCount'
76
    ];
77
78
    // Protected Properties
79
    // =========================================================================
80
81
    protected $allowAnonymous = [];
82
83
    // Public Methods
84
    // =========================================================================
85
86
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
87
     * @throws \yii\web\BadRequestHttpException
88
     * @throws \yii\web\ForbiddenHttpException
89
     * @throws \craft\errors\MissingComponentException
90
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
91
    public function actionImportCsvColumns()
92
    {
93
        PermissionHelper::controllerPermissionCheck('retour:redirects');
94
        // If your CSV document was created or is read on a Macintosh computer,
95
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
96
        if (!ini_get('auto_detect_line_endings')) {
97
            ini_set('auto_detect_line_endings', '1');
98
        }
99
        $this->requirePostRequest();
100
        $filename = Craft::$app->getRequest()->getRequiredBodyParam('filename');
101
        $columns = Craft::$app->getRequest()->getRequiredBodyParam('columns');
102
        $headers = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $headers is dead and can be removed.
Loading history...
103
        $csv = Reader::createFromPath($filename);
104
        try {
105
            $headers = array_flip($csv->fetchOne(0));
106
        } catch (\Exception $e) {
107
            // If this throws an exception, try to read the CSV file from the data cache
108
            // This can happen on load balancer setups where the Craft temp directory isn't shared
109
            $cache = Craft::$app->getCache();
110
            $cachedFile = $cache->get($filename);
111
            if ($cachedFile !== false) {
112
                $csv = Reader::createFromString($cachedFile);
113
                $headers = array_flip($csv->fetchOne(0));
114
                $cache->delete($filename);
115
            } else {
116
                Craft::error("Could not import ${$filename} from the file system, or the cache.", __METHOD__);
117
            }
118
        }
119
        // If we have headers, then we have a file, so parse it
120
        if ($headers !== null) {
121
            switch (VersionHelper::getLeagueCsvVersion()) {
122
                case 8:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
123
                    $this->importCsvApi8($csv, $columns, $headers);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type null and string; however, parameter $columns of nystudio107\retour\contr...roller::importCsvApi8() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

123
                    $this->importCsvApi8($csv, /** @scrutinizer ignore-type */ $columns, $headers);
Loading history...
124
                    break;
125
                case 9:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
126
                    $this->importCsvApi9($csv, $columns, $headers);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type null and string; however, parameter $columns of nystudio107\retour\contr...roller::importCsvApi9() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

126
                    $this->importCsvApi9($csv, /** @scrutinizer ignore-type */ $columns, $headers);
Loading history...
127
                    break;
128
                default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
129
                    Craft::$app->getSession()->setNotice(Craft::t('retour', 'Unknown league/csv package API version'));
130
                    break;
131
            }
132
            @unlink($filename);
0 ignored issues
show
Bug introduced by
It seems like $filename can also be of type array and null; however, parameter $filename of unlink() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

132
            @unlink(/** @scrutinizer ignore-type */ $filename);
Loading history...
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

132
            /** @scrutinizer ignore-unhandled */ @unlink($filename);

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...
133
            Retour::$plugin->clearAllCaches();
134
            Craft::$app->getSession()->setNotice(Craft::t('retour', 'Redirects imported from CSV file.'));
135
        } else {
136
            Craft::$app->getSession()->setError(Craft::t('retour', 'Redirects could not be imported.'));
137
        }
138
139
        $this->redirect('retour/redirects');
140
    }
141
142
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
143
     * @param string|null $siteHandle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
144
     *
145
     * @return Response
146
     * @throws \yii\web\ForbiddenHttpException
147
     * @throws \yii\web\NotFoundHttpException
148
     */
149
    public function actionImportCsv(string $siteHandle = null): Response
150
    {
151
        $variables = [];
152
        PermissionHelper::controllerPermissionCheck('retour:redirects');
153
        // If your CSV document was created or is read on a Macintosh computer,
154
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
155
        if (!ini_get('auto_detect_line_endings')) {
156
            ini_set('auto_detect_line_endings', '1');
157
        }
158
        // Get the site to edit
159
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
160
        $pluginName = Retour::$settings->pluginName;
161
        $templateTitle = Craft::t('retour', 'Import CSV File');
162
        $view = Craft::$app->getView();
163
        // Asset bundle
164
        try {
165
            $view->registerAssetBundle(RetourImportAsset::class);
166
        } catch (InvalidConfigException $e) {
167
            Craft::error($e->getMessage(), __METHOD__);
168
        }
169
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
170
            '@nystudio107/retour/assetbundles/retour/dist',
171
            true
172
        );
173
        // Enabled sites
174
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
175
        $variables['controllerHandle'] = 'file';
176
177
        // Basic variables
178
        $variables['fullPageForm'] = true;
179
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
180
        $variables['pluginName'] = $pluginName;
181
        $variables['title'] = $templateTitle;
182
        $siteHandleUri = Craft::$app->isMultiSite ? '/'.$siteHandle : '';
183
        $variables['crumbs'] = [
184
            [
185
                'label' => $pluginName,
186
                'url' => UrlHelper::cpUrl('retour'),
187
            ],
188
            [
189
                'label' => 'Redirects',
190
                'url' => UrlHelper::cpUrl('retour/redirects'.$siteHandleUri),
191
            ],
192
        ];
193
        $variables['docTitle'] = "{$pluginName} - Redirects - {$templateTitle}";
194
        $variables['selectedSubnavItem'] = 'redirects';
195
196
        // The CSV file
197
        $file = UploadedFile::getInstanceByName('file');
198
        if ($file !== null) {
199
            $filename = uniqid($file->name, true);
200
            $filePath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$filename;
201
            $file->saveAs($filePath, false);
202
            // Also save the file to the cache as a backup way to access it
203
            $cache = Craft::$app->getCache();
204
            $fileHandle = fopen($filePath, 'r');
205
            if ($fileHandle) {
0 ignored issues
show
introduced by
$fileHandle is of type resource, thus it always evaluated to false.
Loading history...
206
                $fileContents = fgets($fileHandle);
207
                if ($fileContents) {
208
                    $cache->set($filePath, $fileContents);
209
                }
210
                fclose($fileHandle);
211
            }
212
            // Read in the headers
213
            $csv = Reader::createFromPath($file->tempName);
214
            $headers = $csv->fetchOne(0);
215
            Craft::info(print_r($headers, true), __METHOD__);
0 ignored issues
show
Bug introduced by
It seems like print_r($headers, true) can also be of type true; however, parameter $message of yii\BaseYii::info() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
            Craft::info(/** @scrutinizer ignore-type */ print_r($headers, true), __METHOD__);
Loading history...
216
            $variables['headers'] = $headers;
217
            $variables['filename'] = $filePath;
218
        }
219
220
        // Render the template
221
        return $this->renderTemplate('retour/import/index', $variables);
222
    }
223
224
    /**
225
     * Export the statistics table as a CSV file
226
     *
227
     * @throws \yii\web\ForbiddenHttpException
228
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
229
    public function actionExportStatistics()
230
    {
231
        PermissionHelper::controllerPermissionCheck('retour:redirects');
232
        $this->exportCsvFile('retour-statistics', '{{%retour_stats}}', self::EXPORT_STATISTICS_CSV_FIELDS);
233
    }
234
235
    /**
236
     * Export the redirects table as a CSV file
237
     *
238
     * @throws \yii\web\ForbiddenHttpException
239
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
240
    public function actionExportRedirects()
241
    {
242
        PermissionHelper::controllerPermissionCheck('retour:redirects');
243
        $this->exportCsvFile('retour-redirects', '{{%retour_static_redirects}}', self::EXPORT_REDIRECTS_CSV_FIELDS);
244
    }
245
246
    // Public Methods
247
    // =========================================================================
248
249
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
250
     * @param string $filename
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
251
     * @param string $table
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
252
     * @param array  $columns
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
253
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
254
    protected function exportCsvFile(string $filename, string $table, array $columns)
255
    {
256
        // If your CSV document was created or is read on a Macintosh computer,
257
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
258
        if (!ini_get('auto_detect_line_endings')) {
259
            ini_set('auto_detect_line_endings', '1');
260
        }
261
        // Query the db table
262
        $data = (new Query())
263
            ->from([$table])
264
            ->select(array_keys($columns))
265
            ->orderBy('hitCount DESC')
266
            ->all();
267
        // Create our CSV file writer
268
        $csv = Writer::createFromFileObject(new \SplTempFileObject());
269
        $csv->insertOne(array_values($columns));
270
        $csv->insertAll($data);
271
        $csv->output($filename.'.csv');
272
        exit(0);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
273
    }
274
275
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
276
     * @param AbstractCsv $csv
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
277
     * @param array $columns
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
278
     * @param array $headers
0 ignored issues
show
Coding Style introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
279
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
280
    protected function importCsvApi8(AbstractCsv $csv, array $columns, array $headers)
281
    {
282
        $csv->setOffset(1);
283
        $columns = ArrayHelper::filterEmptyStringsFromArray($columns);
284
        $csv->each(function ($row) use ($headers, $columns) {
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
Bug introduced by
The method each() does not exist on League\Csv\AbstractCsv. It seems like you code against a sub-type of League\Csv\AbstractCsv such as League\Csv\Reader. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

284
        $csv->/** @scrutinizer ignore-call */ 
285
              each(function ($row) use ($headers, $columns) {
Loading history...
285
            $redirectConfig = [
286
                'id' => 0,
287
            ];
288
            $index = 0;
289
            foreach (self::IMPORT_REDIRECTS_CSV_FIELDS as $importField) {
290
                if (isset($columns[$index], $headers[$columns[$index]])) {
291
                    $redirectConfig[$importField] = empty($row[$headers[$columns[$index]]])
292
                        ? null
293
                        : $row[$headers[$columns[$index]]];
294
                }
295
                $index++;
296
            }
297
            Craft::debug('Importing row: ' . print_r($redirectConfig, true), __METHOD__);
0 ignored issues
show
Bug introduced by
Are you sure print_r($redirectConfig, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

297
            Craft::debug('Importing row: ' . /** @scrutinizer ignore-type */ print_r($redirectConfig, true), __METHOD__);
Loading history...
298
            Retour::$plugin->redirects->saveRedirect($redirectConfig);
299
300
            return true;
301
        });
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
302
    }
303
304
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
305
     * @param AbstractCsv $csv
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
306
     * @param array $columns
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
Coding Style introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
307
     * @param array $headers
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
Coding Style introduced by
Expected 7 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
308
     * @throws \League\Csv\Exception
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
309
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
310
    protected function importCsvApi9(AbstractCsv $csv, array $columns, array $headers)
311
    {
312
        $stmt = (new Statement())
313
            ->offset(1)
0 ignored issues
show
Coding Style introduced by
Space after closing parenthesis of function call prohibited
Loading history...
314
        ;
315
        $rows = $stmt->process($csv);
316
        $columns = ArrayHelper::filterEmptyStringsFromArray($columns);
317
        foreach ($rows as $row) {
318
            $redirectConfig = [
319
                'id' => 0,
320
            ];
321
            $index = 0;
322
            foreach (self::IMPORT_REDIRECTS_CSV_FIELDS as $importField) {
323
                if (isset($columns[$index], $headers[$columns[$index]])) {
324
                    $redirectConfig[$importField] = empty($row[$headers[$columns[$index]]])
325
                        ? null
326
                        : $row[$headers[$columns[$index]]];
327
                }
328
                $index++;
329
            }
330
            Craft::debug('Importing row: ' . print_r($redirectConfig, true), __METHOD__);
0 ignored issues
show
Bug introduced by
Are you sure print_r($redirectConfig, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

330
            Craft::debug('Importing row: ' . /** @scrutinizer ignore-type */ print_r($redirectConfig, true), __METHOD__);
Loading history...
331
            Retour::$plugin->redirects->saveRedirect($redirectConfig);
332
        }
333
    }
334
}
335