Passed
Push — v3 ( 2b006a...43f239 )
by Andrew
33:34 queued 16:32
created

src/controllers/FileController.php (63 issues)

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

138
            /** @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...
139
            Retour::$plugin->clearAllCaches();
140
            Craft::$app->getSession()->setNotice(Craft::t('retour', 'Redirects imported from CSV file.'));
141
        } else {
142
            Craft::$app->getSession()->setError(Craft::t('retour', 'Redirects could not be imported.'));
143
        }
144
145
        $this->redirect('retour/redirects');
146
    }
147
148
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
149
     * @param string|null $siteHandle
0 ignored issues
show
Missing parameter comment
Loading history...
150
     *
151
     * @return Response
152
     * @throws \yii\web\ForbiddenHttpException
153
     * @throws \yii\web\NotFoundHttpException
154
     */
155
    public function actionImportCsv(string $siteHandle = null): Response
156
    {
157
        $variables = [];
158
        PermissionHelper::controllerPermissionCheck('retour:redirects');
159
        // If your CSV document was created or is read on a Macintosh computer,
160
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
161
        if (!ini_get('auto_detect_line_endings')) {
162
            ini_set('auto_detect_line_endings', '1');
163
        }
164
        // Get the site to edit
165
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
166
        $pluginName = Retour::$settings->pluginName;
167
        $templateTitle = Craft::t('retour', 'Import CSV File');
168
        $view = Craft::$app->getView();
169
        // Asset bundle
170
        try {
171
            $view->registerAssetBundle(RetourImportAsset::class);
172
        } catch (InvalidConfigException $e) {
173
            Craft::error($e->getMessage(), __METHOD__);
174
        }
175
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
176
            '@nystudio107/retour/assetbundles/retour/dist',
177
            true
0 ignored issues
show
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

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

177
        /** @scrutinizer ignore-call */ 
178
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
178
        );
179
        // Enabled sites
180
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
181
        $variables['controllerHandle'] = 'file';
182
183
        // Basic variables
184
        $variables['fullPageForm'] = true;
185
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
186
        $variables['pluginName'] = $pluginName;
187
        $variables['title'] = $templateTitle;
188
        $siteHandleUri = Craft::$app->isMultiSite ? '/'.$siteHandle : '';
189
        $variables['crumbs'] = [
190
            [
191
                'label' => $pluginName,
192
                'url' => UrlHelper::cpUrl('retour'),
193
            ],
194
            [
195
                'label' => 'Redirects',
196
                'url' => UrlHelper::cpUrl('retour/redirects'.$siteHandleUri),
197
            ],
198
        ];
199
        $variables['docTitle'] = "{$pluginName} - Redirects - {$templateTitle}";
200
        $variables['selectedSubnavItem'] = 'redirects';
201
202
        // The CSV file
203
        $file = UploadedFile::getInstanceByName('file');
204
        if ($file !== null) {
205
            $filename = uniqid($file->name, true);
206
            $filePath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$filename;
207
            $file->saveAs($filePath, false);
208
            // Also save the file to the cache as a backup way to access it
209
            $cache = Craft::$app->getCache();
210
            $fileHandle = fopen($filePath, 'r');
211
            if ($fileHandle) {
0 ignored issues
show
$fileHandle is of type resource, thus it always evaluated to false.
Loading history...
212
                $fileContents = fgets($fileHandle);
213
                if ($fileContents) {
214
                    $cache->set($filePath, $fileContents);
215
                }
216
                fclose($fileHandle);
217
            }
218
            // Read in the headers
219
            $csv = Reader::createFromPath($file->tempName);
220
            $headers = $csv->fetchOne(0);
221
            Craft::info(print_r($headers, true), __METHOD__);
0 ignored issues
show
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

221
            Craft::info(/** @scrutinizer ignore-type */ print_r($headers, true), __METHOD__);
Loading history...
222
            $variables['headers'] = $headers;
223
            $variables['filename'] = $filePath;
224
        }
225
226
        // Render the template
227
        return $this->renderTemplate('retour/import/index', $variables);
228
    }
229
230
    /**
231
     * Export the statistics table as a CSV file
232
     *
233
     * @throws \yii\web\ForbiddenHttpException
234
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
235
    public function actionExportStatistics()
236
    {
237
        PermissionHelper::controllerPermissionCheck('retour:redirects');
238
        $this->exportCsvFile('retour-statistics', '{{%retour_stats}}', self::EXPORT_STATISTICS_CSV_FIELDS);
239
    }
240
241
    /**
242
     * Export the redirects table as a CSV file
243
     *
244
     * @throws \yii\web\ForbiddenHttpException
245
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
246
    public function actionExportRedirects()
247
    {
248
        PermissionHelper::controllerPermissionCheck('retour:redirects');
249
        $this->exportCsvFile('retour-redirects', '{{%retour_static_redirects}}', self::EXPORT_REDIRECTS_CSV_FIELDS);
250
    }
251
252
    // Public Methods
253
    // =========================================================================
254
255
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
256
     * @param string $filename
0 ignored issues
show
Missing parameter comment
Loading history...
257
     * @param string $table
0 ignored issues
show
Missing parameter comment
Loading history...
258
     * @param array  $columns
0 ignored issues
show
Missing parameter comment
Loading history...
259
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
260
    protected function exportCsvFile(string $filename, string $table, array $columns)
261
    {
262
        // If your CSV document was created or is read on a Macintosh computer,
263
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
264
        if (!ini_get('auto_detect_line_endings')) {
265
            ini_set('auto_detect_line_endings', '1');
266
        }
267
        // Query the db table
268
        $data = (new Query())
269
            ->from([$table])
270
            ->select(array_keys($columns))
271
            ->orderBy('hitCount DESC')
272
            ->all();
273
        // Create our CSV file writer
274
        $csv = Writer::createFromFileObject(new \SplTempFileObject());
275
        $csv->insertOne(array_values($columns));
276
        $csv->insertAll($data);
277
        $csv->output($filename.'.csv');
278
        exit(0);
0 ignored issues
show
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...
279
    }
280
281
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
282
     * @param AbstractCsv $csv
0 ignored issues
show
Missing parameter comment
Loading history...
283
     * @param array $columns
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 7 spaces after parameter type; 1 found
Loading history...
284
     * @param array $headers
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 7 spaces after parameter type; 1 found
Loading history...
285
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
286
    protected function importCsvApi8(AbstractCsv $csv, array $columns, array $headers)
287
    {
288
        $csv->setOffset(1);
0 ignored issues
show
The method setOffset() does not exist on League\Csv\AbstractCsv. ( Ignorable by Annotation )

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

288
        $csv->/** @scrutinizer ignore-call */ 
289
              setOffset(1);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
289
        $columns = ArrayHelper::filterEmptyStringsFromArray($columns);
290
        $csv->each(function ($row) use ($headers, $columns) {
0 ignored issues
show
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
The method each() does not exist on League\Csv\AbstractCsv. ( Ignorable by Annotation )

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

290
        $csv->/** @scrutinizer ignore-call */ 
291
              each(function ($row) use ($headers, $columns) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
291
            $redirectConfig = [
292
                'id' => 0,
293
            ];
294
            $index = 0;
295
            foreach (self::IMPORT_REDIRECTS_CSV_FIELDS as $importField) {
296
                if (isset($columns[$index], $headers[$columns[$index]])) {
297
                    $redirectConfig[$importField] = empty($row[$headers[$columns[$index]]])
298
                        ? null
299
                        : $row[$headers[$columns[$index]]];
300
                }
301
                $index++;
302
            }
303
            Craft::debug('Importing row: ' . print_r($redirectConfig, true), __METHOD__);
0 ignored issues
show
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

303
            Craft::debug('Importing row: ' . /** @scrutinizer ignore-type */ print_r($redirectConfig, true), __METHOD__);
Loading history...
304
            Retour::$plugin->redirects->saveRedirect($redirectConfig);
305
306
            return true;
307
        });
0 ignored issues
show
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...
308
    }
309
310
    /**
0 ignored issues
show
Missing short description in doc comment
Loading history...
311
     * @param AbstractCsv $csv
0 ignored issues
show
Missing parameter comment
Loading history...
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
312
     * @param array $columns
0 ignored issues
show
Missing parameter comment
Loading history...
Expected 7 spaces after parameter type; 1 found
Loading history...
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
313
     * @param array $headers
0 ignored issues
show
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
Missing parameter comment
Loading history...
Expected 7 spaces after parameter type; 1 found
Loading history...
314
     * @throws \League\Csv\Exception
0 ignored issues
show
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
315
     */
0 ignored issues
show
Missing @return tag in function comment
Loading history...
316
    protected function importCsvApi9(AbstractCsv $csv, array $columns, array $headers)
317
    {
318
        $stmt = (new Statement())
319
            ->offset(1)
0 ignored issues
show
Space after closing parenthesis of function call prohibited
Loading history...
320
        ;
321
        $rows = $stmt->process($csv);
0 ignored issues
show
$csv of type League\Csv\AbstractCsv is incompatible with the type League\Csv\TabularDataReader expected by parameter $tabular_data of League\Csv\Statement::process(). ( Ignorable by Annotation )

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

321
        $rows = $stmt->process(/** @scrutinizer ignore-type */ $csv);
Loading history...
322
        $columns = ArrayHelper::filterEmptyStringsFromArray($columns);
323
        foreach ($rows as $row) {
324
            $redirectConfig = [
325
                'id' => 0,
326
            ];
327
            $index = 0;
328
            foreach (self::IMPORT_REDIRECTS_CSV_FIELDS as $importField) {
329
                if (isset($columns[$index], $headers[$columns[$index]])) {
330
                    $redirectConfig[$importField] = empty($row[$headers[$columns[$index]]])
331
                        ? null
332
                        : $row[$headers[$columns[$index]]];
333
                }
334
                $index++;
335
            }
336
            Craft::debug('Importing row: ' . print_r($redirectConfig, true), __METHOD__);
0 ignored issues
show
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

336
            Craft::debug('Importing row: ' . /** @scrutinizer ignore-type */ print_r($redirectConfig, true), __METHOD__);
Loading history...
337
            Retour::$plugin->redirects->saveRedirect($redirectConfig);
338
        }
339
    }
340
}
341