Passed
Push — develop ( 23cbea...ae6640 )
by Andrew
04:12
created

FileController::actionImportCsvColumns()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 39
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 39
rs 9.1768
c 0
b 0
f 0
cc 5
nc 2
nop 0
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
        'siteId' => 'Site ID',
46
        'redirectSrcUrl' => 'Legacy URL Pattern',
47
        'redirectSrcMatch' => 'Legacy URL Match Type',
48
        'redirectDestUrl' => 'Redirect To',
49
        'redirectMatchType' => 'Match Type',
50
        'redirectHttpCode' => 'HTTP Status',
51
        'hitCount' => 'Hits',
52
        'hitLastTime' => 'Last Hit',
53
    ];
54
55
    const EXPORT_STATISTICS_CSV_FIELDS = [
56
        'siteId' => 'Site ID',
57
        'redirectSrcUrl' => '404 File Not Found URL',
58
        'referrerUrl' => 'Last Referrer URL',
59
        'remoteIp' => 'Remote IP',
60
        'hitCount' => 'Hits',
61
        'hitLastTime' => 'Last Hit',
62
        'handledByRetour' => 'Handled',
63
    ];
64
65
    const IMPORT_REDIRECTS_CSV_FIELDS = [
66
        'siteId',
67
        'redirectSrcUrl',
68
        'redirectDestUrl',
69
        'redirectMatchType',
70
        'redirectHttpCode',
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
        $csv = Reader::createFromPath($filename);
98
        $headers = array_flip($csv->fetchOne(0));
99
        $csv->setOffset(1);
100
        $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

100
        $columns = ArrayHelper::filterEmptyStringsFromArray(/** @scrutinizer ignore-type */ $columns);
Loading history...
101
        $csv->each(function ($row) use ($headers, $columns) {
102
            $redirectConfig = [
103
                'id' => 0,
104
            ];
105
            $index = 0;
106
            foreach (self::IMPORT_REDIRECTS_CSV_FIELDS as $importField) {
107
                if (isset($columns[$index], $headers[$columns[$index]])) {
108
                    $redirectConfig[$importField] = empty($row[$headers[$columns[$index]]])
109
                        ? null
110
                        : $row[$headers[$columns[$index]]];
111
                }
112
                $index++;
113
            }
114
            Craft::debug('Importing row: '.print_r($redirectConfig, true), __METHOD__);
115
            Retour::$plugin->redirects->saveRedirect($redirectConfig);
116
117
            return true;
118
        });
119
120
        @unlink($filename);
0 ignored issues
show
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

120
        @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

120
        /** @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...
121
        Retour::$plugin->clearAllCaches();
122
        Craft::$app->getSession()->setNotice(Craft::t('retour', 'Redirects imported from CSV file.'));
123
124
        $this->redirect('retour/redirects');
125
    }
126
127
    /**
128
     * @param string|null $siteHandle
129
     *
130
     * @return Response
131
     * @throws \yii\web\ForbiddenHttpException
132
     * @throws \yii\web\NotFoundHttpException
133
     */
134
    public function actionImportCsv(string $siteHandle = null): Response
135
    {
136
        $variables = [];
137
        PermissionHelper::controllerPermissionCheck('retour:redirects');
138
        // If your CSV document was created or is read on a Macintosh computer,
139
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
140
        if (!ini_get('auto_detect_line_endings')) {
141
            ini_set('auto_detect_line_endings', '1');
142
        }
143
        // Get the site to edit
144
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
145
        $pluginName = Retour::$settings->pluginName;
146
        $templateTitle = Craft::t('retour', 'Import CSV File');
147
        $view = Craft::$app->getView();
148
        // Asset bundle
149
        try {
150
            $view->registerAssetBundle(RetourImportAsset::class);
151
        } catch (InvalidConfigException $e) {
152
            Craft::error($e->getMessage(), __METHOD__);
153
        }
154
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
155
            '@nystudio107/retour/assetbundles/retour/dist',
156
            true
157
        );
158
        // Enabled sites
159
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
160
        $variables['controllerHandle'] = 'file';
161
162
        // Basic variables
163
        $variables['fullPageForm'] = true;
164
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
165
        $variables['pluginName'] = $pluginName;
166
        $variables['title'] = $templateTitle;
167
        $variables['crumbs'] = [
168
            [
169
                'label' => $pluginName,
170
                'url' => UrlHelper::cpUrl('retour'),
171
            ],
172
            [
173
                'label' => 'Redirects',
174
                'url' => UrlHelper::cpUrl('retour/redirects'),
175
            ],
176
        ];
177
        $variables['docTitle'] = "{$pluginName} - Redirects - {$templateTitle}";
178
        $variables['selectedSubnavItem'] = 'redirects';
179
180
        // The CSV file
181
        $file = UploadedFile::getInstanceByName('file');
182
        if ($file !== null) {
183
            $filename = Craft::$app->getPath()->getTempPath().DIRECTORY_SEPARATOR.uniqid($file->name, true);
184
            $file->saveAs($filename, false);
185
            $csv = Reader::createFromPath($file->tempName);
186
            $headers = $csv->fetchOne(0);
187
            Craft::info(print_r($headers, true), __METHOD__);
188
            $variables['headers'] = $headers;
189
            $variables['filename'] = $filename;
190
        }
191
192
        // Render the template
193
        return $this->renderTemplate('retour/import/index', $variables);
194
    }
195
196
    /**
197
     * Export the statistics table as a CSV file
198
     *
199
     * @throws \yii\web\ForbiddenHttpException
200
     */
201
    public function actionExportStatistics()
202
    {
203
        PermissionHelper::controllerPermissionCheck('retour:statistics');
204
        $this->exportCsvFile('retour-statistics', '{{%retour_stats}}', self::EXPORT_STATISTICS_CSV_FIELDS);
205
    }
206
207
    /**
208
     * Export the statistics table as a CSV file
209
     *
210
     * @throws \yii\web\ForbiddenHttpException
211
     */
212
    public function actionExportRedirects()
213
    {
214
        PermissionHelper::controllerPermissionCheck('retour:redirects');
215
        $this->exportCsvFile('retour-redirects', '{{%retour_static_redirects}}', self::EXPORT_REDIRECTS_CSV_FIELDS);
216
    }
217
218
    // Public Methods
219
    // =========================================================================
220
221
    /**
222
     * @param string $filename
223
     * @param string $table
224
     * @param array  $columns
225
     */
226
    protected function exportCsvFile(string $filename, string $table, array $columns)
227
    {
228
        // If your CSV document was created or is read on a Macintosh computer,
229
        // add the following lines before using the library to help PHP detect line ending in Mac OS X
230
        if (!ini_get('auto_detect_line_endings')) {
231
            ini_set('auto_detect_line_endings', '1');
232
        }
233
        // Query the db table
234
        $data = (new Query())
235
            ->from([$table])
236
            ->select(array_keys($columns))
237
            ->orderBy('hitCount DESC')
238
            ->all();
239
        // Create our CSV file writer
240
        $csv = Writer::createFromFileObject(new \SplTempFileObject());
241
        $csv->insertOne(array_values($columns));
242
        $csv->insertAll($data);
243
        $csv->output($filename.'.csv');
244
        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...
245
    }
246
}
247