You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
11 KiB
295 lines
11 KiB
<?php
|
|
/*
|
|
* UploadController.php
|
|
* Copyright (c) 2021 james@firefly-iii.org
|
|
*
|
|
* This file is part of the Firefly III Data Importer
|
|
* (https://github.com/firefly-iii/data-importer).
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Import;
|
|
|
|
use App\Exceptions\ImporterErrorException;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Middleware\UploadControllerMiddleware;
|
|
use App\Services\CSV\Configuration\ConfigFileProcessor;
|
|
use App\Services\Session\Constants;
|
|
use App\Services\Shared\File\FileContentSherlock;
|
|
use App\Services\Storage\StorageService;
|
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
|
use Illuminate\Contracts\View\Factory;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Routing\Redirector;
|
|
use Illuminate\Support\MessageBag;
|
|
use Illuminate\View\View;
|
|
use League\Flysystem\FilesystemException;
|
|
|
|
/**
|
|
* Class UploadController
|
|
*/
|
|
class UploadController extends Controller
|
|
{
|
|
private string $contentType;
|
|
|
|
/**
|
|
* UploadController constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
app('view')->share('pageTitle', 'Upload files');
|
|
$this->middleware(UploadControllerMiddleware::class);
|
|
// This variable is used to make sure the configuration object also knows the file type.
|
|
$this->contentType = 'unknown';
|
|
}
|
|
|
|
/**
|
|
* @return Factory|View
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
app('log')->debug(sprintf('Now at %s', __METHOD__));
|
|
$mainTitle = 'Upload your file(s)';
|
|
$subTitle = 'Start page and instructions';
|
|
$flow = $request->cookie(Constants::FLOW_COOKIE);
|
|
|
|
// get existing configs.
|
|
$disk = \Storage::disk('configurations');
|
|
app('log')->debug(
|
|
sprintf(
|
|
'Going to check directory for config files: %s',
|
|
config('filesystems.disks.configurations.root'),
|
|
)
|
|
);
|
|
$all = $disk->files();
|
|
|
|
// remove files from list
|
|
$list = [];
|
|
$ignored = config('importer.ignored_files');
|
|
foreach ($all as $entry) {
|
|
if (!in_array($entry, $ignored, true)) {
|
|
$list[] = $entry;
|
|
}
|
|
}
|
|
|
|
app('log')->debug('List of files:', $list);
|
|
|
|
return view('import.003-upload.index', compact('mainTitle', 'subTitle', 'list', 'flow'));
|
|
}
|
|
|
|
/**
|
|
* @return Redirector|RedirectResponse
|
|
*
|
|
* @throws FileNotFoundException
|
|
* @throws FilesystemException
|
|
* @throws ImporterErrorException
|
|
*/
|
|
public function upload(Request $request)
|
|
{
|
|
app('log')->debug(sprintf('Now at %s', __METHOD__));
|
|
$importedFile = $request->file('importable_file');
|
|
$configFile = $request->file('config_file');
|
|
$flow = $request->cookie(Constants::FLOW_COOKIE);
|
|
$errors = new MessageBag();
|
|
|
|
// process uploaded file (if present)
|
|
$errors = $this->processUploadedFile($flow, $errors, $importedFile);
|
|
|
|
// process config file (if present)
|
|
if (0 === count($errors) && null !== $configFile) {
|
|
$errors = $this->processConfigFile($errors, $configFile);
|
|
}
|
|
|
|
// process pre-selected file (if present):
|
|
$errors = $this->processSelection($errors, (string)$request->get('existing_config'), $configFile);
|
|
|
|
if ($errors->count() > 0) {
|
|
return redirect(route('003-upload.index'))->withErrors($errors);
|
|
}
|
|
|
|
if ('nordigen' === $flow) {
|
|
// redirect to country + bank selector
|
|
session()->put(Constants::HAS_UPLOAD, true);
|
|
|
|
return redirect(route('009-selection.index'));
|
|
}
|
|
if ('spectre' === $flow) {
|
|
// redirect to spectre
|
|
session()->put(Constants::HAS_UPLOAD, true);
|
|
|
|
return redirect(route('011-connections.index'));
|
|
}
|
|
|
|
return redirect(route('004-configure.index'));
|
|
}
|
|
|
|
private function detectEOL(string $string): string
|
|
{
|
|
$eols = [
|
|
'\n\r' => "\n\r", // 0x0A - 0x0D - acorn BBC
|
|
'\r\n' => "\r\n", // 0x0D - 0x0A - Windows, DOS OS/2
|
|
'\n' => "\n", // 0x0A - - Unix, OSX
|
|
'\r' => "\r", // 0x0D - - Apple ][, TRS80
|
|
];
|
|
$curCount = 0;
|
|
$curEol = '';
|
|
foreach ($eols as $eolKey => $eol) {
|
|
$count = substr_count($string, $eol);
|
|
app('log')->debug(sprintf('Counted %dx "%s" EOL in upload.', $count, $eolKey));
|
|
if ($count > $curCount) {
|
|
$curCount = $count;
|
|
$curEol = $eol;
|
|
app('log')->debug(sprintf('Conclusion: "%s" is the EOL in this file.', $eolKey));
|
|
}
|
|
}
|
|
|
|
return $curEol;
|
|
}
|
|
|
|
private function getError(int $error): string
|
|
{
|
|
app('log')->debug(sprintf('Now at %s', __METHOD__));
|
|
$errors = [
|
|
UPLOAD_ERR_OK => 'There is no error, the file uploaded with success.',
|
|
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
|
|
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
|
|
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.',
|
|
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
|
|
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
|
|
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk. Introduced in PHP 5.1.0.',
|
|
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
|
|
];
|
|
|
|
return $errors[$error] ?? 'Unknown error';
|
|
}
|
|
|
|
/**
|
|
* @param null|UploadedFile $file
|
|
*
|
|
* @throws ImporterErrorException
|
|
*/
|
|
private function processConfigFile(MessageBag $errors, UploadedFile $file): MessageBag
|
|
{
|
|
app('log')->debug('Config file is present.');
|
|
$errorNumber = $file->getError();
|
|
if (0 !== $errorNumber) {
|
|
$errors->add('config_file', $errorNumber);
|
|
}
|
|
// upload the file to a temp directory and use it from there.
|
|
if (0 === $errorNumber) {
|
|
app('log')->debug('Config file uploaded.');
|
|
$configFileName = StorageService::storeContent(file_get_contents($file->getPathname()));
|
|
|
|
session()->put(Constants::UPLOAD_CONFIG_FILE, $configFileName);
|
|
|
|
// process the config file
|
|
$success = false;
|
|
$configuration = null;
|
|
|
|
try {
|
|
$configuration = ConfigFileProcessor::convertConfigFile($configFileName);
|
|
$configuration->setContentType($this->contentType);
|
|
session()->put(Constants::CONFIGURATION, $configuration->toSessionArray());
|
|
$success = true;
|
|
} catch (ImporterErrorException $e) {
|
|
$errors->add('config_file', $e->getMessage());
|
|
}
|
|
// if conversion of the config file was a success, store the new version again:
|
|
if (true === $success) {
|
|
$configuration->updateDateRange();
|
|
$configFileName = StorageService::storeContent(json_encode($configuration->toArray(), JSON_PRETTY_PRINT));
|
|
session()->put(Constants::UPLOAD_CONFIG_FILE, $configFileName);
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* @throws ImporterErrorException
|
|
*/
|
|
private function processSelection(MessageBag $errors, string $selection, null|UploadedFile $file): MessageBag
|
|
{
|
|
if (null === $file && '' !== $selection) {
|
|
app('log')->debug('User selected a config file from the store.');
|
|
$disk = \Storage::disk('configurations');
|
|
$configFileName = StorageService::storeContent($disk->get($selection));
|
|
|
|
session()->put(Constants::UPLOAD_CONFIG_FILE, $configFileName);
|
|
|
|
// process the config file
|
|
try {
|
|
$configuration = ConfigFileProcessor::convertConfigFile($configFileName);
|
|
session()->put(Constants::CONFIGURATION, $configuration->toSessionArray());
|
|
} catch (ImporterErrorException $e) {
|
|
$errors->add('config_file', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* @throws FilesystemException
|
|
* @throws ImporterErrorException
|
|
*/
|
|
private function processUploadedFile(string $flow, MessageBag $errors, null|UploadedFile $file): MessageBag
|
|
{
|
|
if (null === $file && 'file' === $flow) {
|
|
$errors->add('importable_file', 'No file was uploaded.');
|
|
|
|
return $errors;
|
|
}
|
|
if ('file' === $flow) {
|
|
$errorNumber = $file->getError();
|
|
if (0 !== $errorNumber) {
|
|
$errors->add('importable_file', $this->getError($errorNumber));
|
|
}
|
|
|
|
// upload the file to a temp directory and use it from there.
|
|
if (0 === $errorNumber) {
|
|
$detector = new FileContentSherlock();
|
|
$this->contentType = $detector->detectContentType($file->getPathname());
|
|
$content = '';
|
|
if ('csv' === $this->contentType) {
|
|
$content = file_get_contents($file->getPathname());
|
|
|
|
// https://stackoverflow.com/questions/11066857/detect-eol-type-using-php
|
|
// because apparently there are banks that use "\r" as newline. Looking at the morons of KBC Bank, Belgium.
|
|
// This one is for you: 🤦♀️
|
|
$eol = $this->detectEOL($content);
|
|
if ("\r" === $eol) {
|
|
app('log')->error('You bank is dumb. Tell them to fix their CSV files.');
|
|
$content = str_replace("\r", "\n", $content);
|
|
}
|
|
}
|
|
|
|
if ('camt' === $this->contentType) {
|
|
$content = file_get_contents($file->getPathname());
|
|
}
|
|
$fileName = StorageService::storeContent($content);
|
|
session()->put(Constants::UPLOAD_DATA_FILE, $fileName);
|
|
session()->put(Constants::HAS_UPLOAD, true);
|
|
}
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
}
|