Passed
Push — develop ( c7a8b4...cee39f )
by Andrew
15:58
created

FileController   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 19
eloc 130
c 8
b 0
f 0
dl 0
loc 240
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A actionExportStatistics() 0 4 1
B actionImportCsv() 0 73 7
A exportCsvFile() 0 19 2
B actionImportCsvColumns() 0 58 8
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/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
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
19
use Craft;
20
use craft\db\Query;
21
use craft\helpers\ArrayHelper;
22
use craft\helpers\UrlHelper;
23
use craft\web\Controller;
24
25
use yii\base\InvalidConfigException;
26
use yii\web\Response;
27
use yii\web\UploadedFile;
28
29
use League\Csv\Reader;
30
use League\Csv\Writer;
31
32
/**
33
 * @author    nystudio107
34
 * @package   Retour
35
 * @since     3.0.0
36
 */
37
class FileController extends Controller
38
{
39
    // Constants
40
    // =========================================================================
41
42
    const DOCUMENTATION_URL = 'https://github.com/nystudio107/craft-retour/';
43
44
    const EXPORT_REDIRECTS_CSV_FIELDS = [
45
        'redirectSrcUrl' => 'Legacy URL Pattern',
46
        'redirectDestUrl' => 'Redirect To',
47
        'redirectMatchType' => 'Match Type',
48
        'redirectHttpCode' => 'HTTP Status',
49
        'siteId' => 'Site ID',
50
        'redirectSrcMatch' => 'Legacy URL Match Type',
51
        'hitCount' => 'Hits',
52
        'hitLastTime' => 'Last Hit',
53
    ];
54
55
    const EXPORT_STATISTICS_CSV_FIELDS = [
56
        'redirectSrcUrl' => '404 File Not Found URL',
57
        'referrerUrl' => 'Last Referrer URL',
58
        'remoteIp' => 'Remote IP',
59
        'hitCount' => 'Hits',
60
        'hitLastTime' => 'Last Hit',
61
        'handledByRetour' => 'Handled',
62
        'siteId' => 'Site ID',
63
    ];
64
65
    const IMPORT_REDIRECTS_CSV_FIELDS = [
66
        'redirectSrcUrl',
67
        'redirectDestUrl',
68
        'redirectMatchType',
69
        'redirectHttpCode',
70
        'siteId',
71
    ];
72
73
    // Protected Properties
74
    // =========================================================================
75
76
    protected $allowAnonymous = [];
77
78
    // Public Methods
79
    // =========================================================================
80
81
    /**
82
     * @throws \yii\web\BadRequestHttpException
83
     * @throws \yii\web\ForbiddenHttpException
84
     * @throws \craft\errors\MissingComponentException
85
     */
86
    public function actionImportCsvColumns()
87
    {
88
        PermissionHelper::controllerPermissionCheck('retour:redirects');
89
        // If your CSV document was created or is read on a Macintosh computer,
90
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
91
        if (!ini_get('auto_detect_line_endings')) {
92
            ini_set('auto_detect_line_endings', '1');
93
        }
94
        $this->requirePostRequest();
95
        $filename = Craft::$app->getRequest()->getRequiredBodyParam('filename');
96
        $columns = Craft::$app->getRequest()->getRequiredBodyParam('columns');
97
        $headers = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $headers is dead and can be removed.
Loading history...
98
        $csv = Reader::createFromPath($filename);
99
        try {
100
            $headers = array_flip($csv->fetchOne(0));
101
        } catch (\Exception $e) {
102
            // If this throws an exception, try to read the CSV file from the data cache
103
            // This can happen on load balancer setups where the Craft temp directory isn't shared
104
            $cache = Craft::$app->getCache();
105
            $cachedFile = $cache->get($filename);
106
            if ($cachedFile !== false) {
107
                $csv = Reader::createFromString($cachedFile);
108
                $headers = array_flip($csv->fetchOne(0));
109
                $cache->delete($filename);
110
            } else {
111
                Craft::error("Could not import ${$filename} from the file system, or the cache.", __METHOD__);
112
            }
113
        }
114
        // If we have headers, then we have a file, so parse it
115
        if ($headers !== null) {
116
            $csv->setOffset(1);
117
            $columns = ArrayHelper::filterEmptyStringsFromArray($columns);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type null and string; however, parameter $arr of craft\helpers\ArrayHelpe...EmptyStringsFromArray() 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

117
            $columns = ArrayHelper::filterEmptyStringsFromArray(/** @scrutinizer ignore-type */ $columns);
Loading history...
118
            $csv->each(function ($row) use ($headers, $columns) {
119
                $redirectConfig = [
120
                    'id' => 0,
121
                ];
122
                $index = 0;
123
                foreach (self::IMPORT_REDIRECTS_CSV_FIELDS as $importField) {
124
                    if (isset($columns[$index], $headers[$columns[$index]])) {
125
                        $redirectConfig[$importField] = empty($row[$headers[$columns[$index]]])
126
                            ? null
127
                            : $row[$headers[$columns[$index]]];
128
                    }
129
                    $index++;
130
                }
131
                Craft::debug('Importing row: '.print_r($redirectConfig, true), __METHOD__);
132
                Retour::$plugin->redirects->saveRedirect($redirectConfig);
133
134
                return true;
135
            });
136
            @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

136
            /** @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...
Bug introduced by
It seems like $filename can also be of type array and array; 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

136
            @unlink(/** @scrutinizer ignore-type */ $filename);
Loading history...
137
            Retour::$plugin->clearAllCaches();
138
            Craft::$app->getSession()->setNotice(Craft::t('retour', 'Redirects imported from CSV file.'));
139
        } else {
140
            Craft::$app->getSession()->setError(Craft::t('retour', 'Redirects could not be imported.'));
141
        }
142
143
        $this->redirect('retour/redirects');
144
    }
145
146
    /**
147
     * @param string|null $siteHandle
148
     *
149
     * @return Response
150
     * @throws \yii\web\ForbiddenHttpException
151
     * @throws \yii\web\NotFoundHttpException
152
     */
153
    public function actionImportCsv(string $siteHandle = null): Response
154
    {
155
        $variables = [];
156
        PermissionHelper::controllerPermissionCheck('retour:redirects');
157
        // If your CSV document was created or is read on a Macintosh computer,
158
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
159
        if (!ini_get('auto_detect_line_endings')) {
160
            ini_set('auto_detect_line_endings', '1');
161
        }
162
        // Get the site to edit
163
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
164
        $pluginName = Retour::$settings->pluginName;
165
        $templateTitle = Craft::t('retour', 'Import CSV File');
166
        $view = Craft::$app->getView();
167
        // Asset bundle
168
        try {
169
            $view->registerAssetBundle(RetourImportAsset::class);
170
        } catch (InvalidConfigException $e) {
171
            Craft::error($e->getMessage(), __METHOD__);
172
        }
173
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
174
            '@nystudio107/retour/assetbundles/retour/dist',
175
            true
176
        );
177
        // Enabled sites
178
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
179
        $variables['controllerHandle'] = 'file';
180
181
        // Basic variables
182
        $variables['fullPageForm'] = true;
183
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
184
        $variables['pluginName'] = $pluginName;
185
        $variables['title'] = $templateTitle;
186
        $siteHandleUri = Craft::$app->isMultiSite ? '/'.$siteHandle : '';
187
        $variables['crumbs'] = [
188
            [
189
                'label' => $pluginName,
190
                'url' => UrlHelper::cpUrl('retour'),
191
            ],
192
            [
193
                'label' => 'Redirects',
194
                'url' => UrlHelper::cpUrl('retour/redirects'.$siteHandleUri),
195
            ],
196
        ];
197
        $variables['docTitle'] = "{$pluginName} - Redirects - {$templateTitle}";
198
        $variables['selectedSubnavItem'] = 'redirects';
199
200
        // The CSV file
201
        $file = UploadedFile::getInstanceByName('file');
202
        if ($file !== null) {
203
            $filename = uniqid($file->name, true);
204
            $filePath = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.$filename;
205
            $file->saveAs($filePath, false);
206
            // Also save the file to the cache as a backup way to access it
207
            $cache = Craft::$app->getCache();
208
            $fileHandle = fopen($filePath, 'r');
209
            if ($fileHandle) {
0 ignored issues
show
introduced by
$fileHandle is of type false|resource, thus it always evaluated to false.
Loading history...
210
                $fileContents = fgets($fileHandle);
211
                if ($fileContents) {
212
                    $cache->set($filePath, $fileContents);
213
                }
214
                fclose($fileHandle);
215
            }
216
            // Read in the headers
217
            $csv = Reader::createFromPath($file->tempName);
218
            $headers = $csv->fetchOne(0);
219
            Craft::info(print_r($headers, true), __METHOD__);
220
            $variables['headers'] = $headers;
221
            $variables['filename'] = $filePath;
222
        }
223
224
        // Render the template
225
        return $this->renderTemplate('retour/import/index', $variables);
226
    }
227
228
    /**
229
     * Export the statistics table as a CSV file
230
     *
231
     * @throws \yii\web\ForbiddenHttpException
232
     */
233
    public function actionExportStatistics()
234
    {
235
        PermissionHelper::controllerPermissionCheck('retour:redirects');
236
        $this->exportCsvFile('retour-statistics', '{{%retour_stats}}', self::EXPORT_STATISTICS_CSV_FIELDS);
237
    }
238
239
    /**
240
     * Export the redirects table as a CSV file
241
     *
242
     * @throws \yii\web\ForbiddenHttpException
243
     */
244
    public function actionExportRedirects()
245
    {
246
        PermissionHelper::controllerPermissionCheck('retour:redirects');
247
        $this->exportCsvFile('retour-redirects', '{{%retour_static_redirects}}', self::EXPORT_REDIRECTS_CSV_FIELDS);
248
    }
249
250
    // Public Methods
251
    // =========================================================================
252
253
    /**
254
     * @param string $filename
255
     * @param string $table
256
     * @param array  $columns
257
     */
258
    protected function exportCsvFile(string $filename, string $table, array $columns)
259
    {
260
        // If your CSV document was created or is read on a Macintosh computer,
261
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
262
        if (!ini_get('auto_detect_line_endings')) {
263
            ini_set('auto_detect_line_endings', '1');
264
        }
265
        // Query the db table
266
        $data = (new Query())
267
            ->from([$table])
268
            ->select(array_keys($columns))
269
            ->orderBy('hitCount DESC')
270
            ->all();
271
        // Create our CSV file writer
272
        $csv = Writer::createFromFileObject(new \SplTempFileObject());
273
        $csv->insertOne(array_values($columns));
274
        $csv->insertAll($data);
275
        $csv->output($filename.'.csv');
276
        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...
277
    }
278
}
279