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
Loading history...
|
|||||||
9 | * @copyright Copyright (c) 2018 nystudio107 |
||||||
0 ignored issues
–
show
|
|||||||
10 | */ |
||||||
0 ignored issues
–
show
|
|||||||
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
|
|||||||
37 | * @author nystudio107 |
||||||
0 ignored issues
–
show
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
|
|||||||
38 | * @package Retour |
||||||
0 ignored issues
–
show
|
|||||||
39 | * @since 3.0.0 |
||||||
0 ignored issues
–
show
|
|||||||
40 | */ |
||||||
0 ignored issues
–
show
|
|||||||
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
|
|||||||
88 | * @throws \yii\web\BadRequestHttpException |
||||||
89 | * @throws \yii\web\ForbiddenHttpException |
||||||
90 | * @throws \craft\errors\MissingComponentException |
||||||
91 | */ |
||||||
0 ignored issues
–
show
|
|||||||
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
|
|||||||
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
|
|||||||
129 | $this->importCsvApi8($csv, $columns, $headers); |
||||||
130 | break; |
||||||
131 | case 9: |
||||||
0 ignored issues
–
show
|
|||||||
132 | $this->importCsvApi9($csv, $columns, $headers); |
||||||
133 | break; |
||||||
134 | default: |
||||||
0 ignored issues
–
show
|
|||||||
135 | Craft::$app->getSession()->setNotice(Craft::t('retour', 'Unknown league/csv package API version')); |
||||||
136 | break; |
||||||
137 | } |
||||||
138 | @unlink($filename); |
||||||
0 ignored issues
–
show
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
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
|
|||||||
149 | * @param string|null $siteHandle |
||||||
0 ignored issues
–
show
|
|||||||
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
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
|
|||||||
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
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
|
|||||||
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
|
|||||||
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
|
|||||||
256 | * @param string $filename |
||||||
0 ignored issues
–
show
|
|||||||
257 | * @param string $table |
||||||
0 ignored issues
–
show
|
|||||||
258 | * @param array $columns |
||||||
0 ignored issues
–
show
|
|||||||
259 | */ |
||||||
0 ignored issues
–
show
|
|||||||
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
|
|||||||
279 | } |
||||||
280 | |||||||
281 | /** |
||||||
0 ignored issues
–
show
|
|||||||
282 | * @param AbstractCsv $csv |
||||||
0 ignored issues
–
show
|
|||||||
283 | * @param array $columns |
||||||
0 ignored issues
–
show
|
|||||||
284 | * @param array $headers |
||||||
0 ignored issues
–
show
|
|||||||
285 | */ |
||||||
0 ignored issues
–
show
|
|||||||
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
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 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
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
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
|
|||||||
311 | * @param AbstractCsv $csv |
||||||
0 ignored issues
–
show
|
|||||||
312 | * @param array $columns |
||||||
0 ignored issues
–
show
|
|||||||
313 | * @param array $headers |
||||||
0 ignored issues
–
show
|
|||||||
314 | * @throws \League\Csv\Exception |
||||||
0 ignored issues
–
show
|
|||||||
315 | */ |
||||||
0 ignored issues
–
show
|
|||||||
316 | protected function importCsvApi9(AbstractCsv $csv, array $columns, array $headers) |
||||||
317 | { |
||||||
318 | $stmt = (new Statement()) |
||||||
319 | ->offset(1) |
||||||
0 ignored issues
–
show
|
|||||||
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
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
Loading history...
|
|||||||
337 | Retour::$plugin->redirects->saveRedirect($redirectConfig); |
||||||
338 | } |
||||||
339 | } |
||||||
340 | } |
||||||
341 |