diff --git a/app/Console/Commands/Export/ExportData.php b/app/Console/Commands/Export/ExportData.php new file mode 100644 index 0000000000..6b59689527 --- /dev/null +++ b/app/Console/Commands/Export/ExportData.php @@ -0,0 +1,63 @@ +. + */ + +namespace FireflyIII\Console\Commands; + +use Illuminate\Console\Command; + +/** + * Class ExportData + */ +class ExportData extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'I export data.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:export-data'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + // + } +} diff --git a/app/Http/Controllers/Export/IndexController.php b/app/Http/Controllers/Export/IndexController.php new file mode 100644 index 0000000000..24cd52de0c --- /dev/null +++ b/app/Http/Controllers/Export/IndexController.php @@ -0,0 +1,109 @@ +. + */ + +namespace FireflyIII\Http\Controllers\Export; + + +use Carbon\Carbon; +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Export\ExportFileGenerator; +use Illuminate\Http\Response as LaravelResponse; + +/** + * Class IndexController + */ +class IndexController extends Controller +{ + + /** @var JournalRepositoryInterface */ + private $journalRepository; + + /** + * IndexController constructor. + * + * @codeCoverageIgnore + */ + public function __construct() + { + parent::__construct(); + + // translations: + $this->middleware( + function ($request, $next) { + app('view')->share('mainTitleIcon', 'fa-life-bouy'); + app('view')->share('title', (string)trans('firefly.export_data_title')); + $this->journalRepository = app(JournalRepositoryInterface::class); + + return $next($request); + } + ); + } + + /** + * + */ + public function export() + { + /** @var ExportFileGenerator $generator */ + $generator = app(ExportFileGenerator::class); + + // get first transaction in DB: + $firstDate = new Carbon; + $firstDate->subYear(); + $journal = $this->journalRepository->firstNull(); + if (null !== $journal) { + $firstDate = clone $journal->date; + } + $generator->setStart($firstDate); + $result = $generator->export(); + + $name = sprintf('%s_transaction_export.csv', date('Y_m_d')); + $quoted = sprintf('"%s"', addcslashes($name, '"\\')); + // headers for CSV file. + /** @var LaravelResponse $response */ + $response = response($result['transactions'], 200); + $response + ->header('Content-Description', 'File Transfer') + ->header('Content-Type', 'text/x-csv') + ->header('Content-Disposition', 'attachment; filename=' . $quoted) + //->header('Content-Transfer-Encoding', 'binary') + ->header('Connection', 'Keep-Alive') + ->header('Expires', '0') + ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->header('Pragma', 'public') + ->header('Content-Length', strlen($result['transactions'])); + + return $response; + + // return CSV file made from 'transactions' array. + return $result['transactions']; + } + + /** + * + */ + public function index() + { + return view('export.index'); + } + +} diff --git a/app/Support/Export/ExportFileGenerator.php b/app/Support/Export/ExportFileGenerator.php new file mode 100644 index 0000000000..1a947c1c38 --- /dev/null +++ b/app/Support/Export/ExportFileGenerator.php @@ -0,0 +1,166 @@ +. + */ + +namespace FireflyIII\Support\Export; + +use Carbon\Carbon; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use League\Csv\Writer; + +/** + * Class ExportFileGenerator + */ +class ExportFileGenerator +{ + /** @var Carbon */ + private $end; + /** @var bool */ + private $exportTransactions; + /** @var Carbon */ + private $start; + + public function __construct() + { + $this->start = new Carbon; + $this->start->subYear(); + $this->end = new Carbon; + $this->exportTransactions = true; + } + + /** + * @return array + */ + public function export(): array + { + $return = []; + if ($this->exportTransactions) { + $return['transactions'] = $this->exportTransactions(); + } + + return $return; + } + + /** + * @param Carbon $end + */ + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + /** + * @param bool $exportTransactions + */ + public function setExportTransactions(bool $exportTransactions): void + { + $this->exportTransactions = $exportTransactions; + } + + /** + * @param Carbon $start + */ + public function setStart(Carbon $start): void + { + $this->start = $start; + } + + /** + * @return string + */ + private function exportTransactions(): string + { + // TODO better place for keys? + $header = [ + 'user_id', + 'group_id', + 'journal_id', + 'created_at', + 'updated_at', + 'group_title', + 'type', + 'amount', + 'foreign_amount', + 'currency_code', + 'foreign_currency_code', + 'description', + 'date', + 'source_name', + 'source_iban', + 'source_type', + 'destination_name', + 'destination_iban', + 'destination_type', + 'reconciled', + 'category', + 'budget', + 'bill', + 'tags', + ]; + + $collector = app(GroupCollectorInterface::class); + $collector->setRange($this->start, $this->end)->withAccountInformation()->withCategoryInformation()->withBillInformation() + ->withBudgetInformation(); + $journals = $collector->getExtractedJournals(); + + $records = []; + /** @var array $journal */ + foreach ($journals as $journal) { + $records[] = [ + $journal['user_id'], + $journal['transaction_group_id'], + $journal['transaction_journal_id'], + $journal['created_at']->toAtomString(), + $journal['updated_at']->toAtomString(), + $journal['transaction_group_title'], + $journal['transaction_type_type'], + $journal['amount'], + $journal['foreign_amount'], + $journal['currency_code'], + $journal['foreign_currency_code'], + $journal['description'], + $journal['date']->toAtomString(), + $journal['source_account_name'], + $journal['source_account_iban'], + $journal['source_account_type'], + $journal['destination_account_name'], + $journal['destination_account_iban'], + $journal['destination_account_type'], + $journal['reconciled'], + $journal['category_name'], + $journal['budget_name'], + $journal['bill_name'], + implode(',', $journal['tags']), + ]; + } + + //load the CSV document from a string + $csv = Writer::createFromString(''); + + //insert the header + $csv->insertOne($header); + + //insert all the records + $csv->insertAll($records); + + return $csv->getContent(); //returns the CSV document as a string + } + +} diff --git a/config/firefly.php b/config/firefly.php index d85572f79c..296d0e56e4 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -132,29 +132,32 @@ return [ 'single_user_mode' => true, 'is_demo_site' => false, ], - 'encryption' => null === env('USE_ENCRYPTION') || env('USE_ENCRYPTION') === true, - 'version' => '5.0.0-alpha.1', - 'api_version' => '1.0.0', - 'db_version' => 11, - 'maxUploadSize' => 15242880, - 'send_error_message' => env('SEND_ERROR_MESSAGE', true), - 'site_owner' => env('SITE_OWNER', ''), - 'send_registration_mail' => env('SEND_REGISTRATION_MAIL', true), - 'demo_username' => env('DEMO_USERNAME', ''), - 'demo_password' => env('DEMO_PASSWORD', ''), - 'is_sandstorm' => env('IS_SANDSTORM', 'unknown'), - 'bunq_use_sandbox' => env('BUNQ_USE_SANDBOX', false), - 'fixer_api_key' => env('FIXER_API_KEY', ''), - 'mapbox_api_key' => env('MAPBOX_API_KEY', ''), - 'trusted_proxies' => env('TRUSTED_PROXIES', ''), - 'search_result_limit' => env('SEARCH_RESULT_LIMIT', 50), - 'send_report_journals' => envNonEmpty('SEND_REPORT_JOURNALS', true), - 'analytics_id' => env('ANALYTICS_ID', ''), - 'disable_frame_header' => env('DISABLE_FRAME_HEADER', false), - 'login_provider' => envNonEmpty('LOGIN_PROVIDER', 'eloquent'), - 'cer_provider' => envNonEmpty('CER_PROVIDER', 'fixer'), - 'update_endpoint' => 'https://version.firefly-iii.org/index.json', - 'allowedMimes' => [ + 'feature_flags' => [ + 'export' => false, + ], + 'encryption' => null === env('USE_ENCRYPTION') || env('USE_ENCRYPTION') === true, + 'version' => '5.0.0-alpha.1', + 'api_version' => '1.0.0', + 'db_version' => 11, + 'maxUploadSize' => 15242880, + 'send_error_message' => env('SEND_ERROR_MESSAGE', true), + 'site_owner' => env('SITE_OWNER', ''), + 'send_registration_mail' => env('SEND_REGISTRATION_MAIL', true), + 'demo_username' => env('DEMO_USERNAME', ''), + 'demo_password' => env('DEMO_PASSWORD', ''), + 'is_sandstorm' => env('IS_SANDSTORM', 'unknown'), + 'bunq_use_sandbox' => env('BUNQ_USE_SANDBOX', false), + 'fixer_api_key' => env('FIXER_API_KEY', ''), + 'mapbox_api_key' => env('MAPBOX_API_KEY', ''), + 'trusted_proxies' => env('TRUSTED_PROXIES', ''), + 'search_result_limit' => env('SEARCH_RESULT_LIMIT', 50), + 'send_report_journals' => envNonEmpty('SEND_REPORT_JOURNALS', true), + 'analytics_id' => env('ANALYTICS_ID', ''), + 'disable_frame_header' => env('DISABLE_FRAME_HEADER', false), + 'login_provider' => envNonEmpty('LOGIN_PROVIDER', 'eloquent'), + 'cer_provider' => envNonEmpty('CER_PROVIDER', 'fixer'), + 'update_endpoint' => 'https://version.firefly-iii.org/index.json', + 'allowedMimes' => [ /* plain files */ 'text/plain', diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 031ff04247..19bc826309 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -592,46 +592,56 @@ return [ 'password_changed' => 'Password changed!', 'should_change' => 'The idea is to change your password.', 'invalid_password' => 'Invalid password!', - 'what_is_pw_security' => 'What is "verify password security"?', - 'secure_pw_title' => 'How to choose a secure password', - 'secure_pw_history' => 'Not a week goes by that you read in the news about a site losing the passwords of its users. Hackers and thieves use these passwords to try to steal your private information. This information is valuable.', - 'secure_pw_ff' => 'Do you use the same password all over the internet? If one site loses your password, hackers have access to all your data. Firefly III relies on you to choose a strong and unique password to protect your financial records.', - 'secure_pw_check_box' => 'To help you do that Firefly III can check if the password you want to use has been stolen in the past. If this is the case, Firefly III advises you NOT to use that password.', - 'secure_pw_working_title' => 'How does it work?', - 'secure_pw_working' => 'By checking the box, Firefly III will send the first five characters of the SHA1 hash of your password to the website of Troy Hunt to see if it is on the list. This will stop you from using unsafe passwords as is recommended in the latest NIST Special Publication on this subject.', - 'secure_pw_should' => 'Should I check the box?', - 'secure_pw_long_password' => 'Yes. Always verify your password is safe.', - 'command_line_token' => 'Command line token', - 'explain_command_line_token' => 'You need this token to perform command line options, such as importing or exporting data. Without it, such sensitive commands will not work. Do not share your command line token. Nobody will ask you for this token, not even me. If you fear you lost this, or when you\'re paranoid, regenerate this token using the button.', - 'regenerate_command_line_token' => 'Regenerate command line token', - 'token_regenerated' => 'A new command line token was generated', - 'change_your_email' => 'Change your email address', - 'email_verification' => 'An email message will be sent to your old AND new email address. For security purposes, you will not be able to login until you verify your new email address. If you are unsure if your Firefly III installation is capable of sending email, please do not use this feature. If you are an administrator, you can test this in the Administration.', - 'email_changed_logout' => 'Until you verify your email address, you cannot login.', - 'login_with_new_email' => 'You can now login with your new email address.', - 'login_with_old_email' => 'You can now login with your old email address again.', - 'login_provider_local_only' => 'This action is not available when authenticating through ":login_provider".', - 'delete_local_info_only' => 'Because you authenticate through ":login_provider", this will only delete local Firefly III information.', + 'what_is_pw_security' => 'What is "verify password security"?', + 'secure_pw_title' => 'How to choose a secure password', + 'secure_pw_history' => 'Not a week goes by that you read in the news about a site losing the passwords of its users. Hackers and thieves use these passwords to try to steal your private information. This information is valuable.', + 'secure_pw_ff' => 'Do you use the same password all over the internet? If one site loses your password, hackers have access to all your data. Firefly III relies on you to choose a strong and unique password to protect your financial records.', + 'secure_pw_check_box' => 'To help you do that Firefly III can check if the password you want to use has been stolen in the past. If this is the case, Firefly III advises you NOT to use that password.', + 'secure_pw_working_title' => 'How does it work?', + 'secure_pw_working' => 'By checking the box, Firefly III will send the first five characters of the SHA1 hash of your password to the website of Troy Hunt to see if it is on the list. This will stop you from using unsafe passwords as is recommended in the latest NIST Special Publication on this subject.', + 'secure_pw_should' => 'Should I check the box?', + 'secure_pw_long_password' => 'Yes. Always verify your password is safe.', + 'command_line_token' => 'Command line token', + 'explain_command_line_token' => 'You need this token to perform command line options, such as importing or exporting data. Without it, such sensitive commands will not work. Do not share your command line token. Nobody will ask you for this token, not even me. If you fear you lost this, or when you\'re paranoid, regenerate this token using the button.', + 'regenerate_command_line_token' => 'Regenerate command line token', + 'token_regenerated' => 'A new command line token was generated', + 'change_your_email' => 'Change your email address', + 'email_verification' => 'An email message will be sent to your old AND new email address. For security purposes, you will not be able to login until you verify your new email address. If you are unsure if your Firefly III installation is capable of sending email, please do not use this feature. If you are an administrator, you can test this in the Administration.', + 'email_changed_logout' => 'Until you verify your email address, you cannot login.', + 'login_with_new_email' => 'You can now login with your new email address.', + 'login_with_old_email' => 'You can now login with your old email address again.', + 'login_provider_local_only' => 'This action is not available when authenticating through ":login_provider".', + 'delete_local_info_only' => 'Because you authenticate through ":login_provider", this will only delete local Firefly III information.', + + // export data: + 'import_and_export_menu' => 'Import and export', + 'export_data_title' => 'Export data from Firefly III', + 'export_data_menu' => 'Export data', + 'export_data_bc' => 'Export data from Firefly III', + 'export_data_main_title' => 'Export data from Firefly III', + 'export_data_expl' => 'This link allows you to export all transactions from Firefly III, with all the relevant meta-data. Please refer to the help (top right (?)-icon) for more information about the process.', + 'export_data_all_transactions' => 'Export all transactions', + 'export_data_advanced_expl' => 'If you want to have more advanced options, consult the command line options. More information is available in the help (top right (?)-icon) or consult the console command: php artisan help firefly-iii:export-data', // attachments - 'nr_of_attachments' => 'One attachment|:count attachments', - 'attachments' => 'Attachments', - 'edit_attachment' => 'Edit attachment ":name"', - 'update_attachment' => 'Update attachment', - 'delete_attachment' => 'Delete attachment ":name"', - 'attachment_deleted' => 'Deleted attachment ":name"', - 'liabilities_deleted' => 'Deleted liability ":name"', - 'attachment_updated' => 'Updated attachment ":name"', - 'upload_max_file_size' => 'Maximum file size: :size', - 'list_all_attachments' => 'List of all attachments', + 'nr_of_attachments' => 'One attachment|:count attachments', + 'attachments' => 'Attachments', + 'edit_attachment' => 'Edit attachment ":name"', + 'update_attachment' => 'Update attachment', + 'delete_attachment' => 'Delete attachment ":name"', + 'attachment_deleted' => 'Deleted attachment ":name"', + 'liabilities_deleted' => 'Deleted liability ":name"', + 'attachment_updated' => 'Updated attachment ":name"', + 'upload_max_file_size' => 'Maximum file size: :size', + 'list_all_attachments' => 'List of all attachments', // transaction index - 'title_expenses' => 'Expenses', - 'title_withdrawal' => 'Expenses', - 'title_revenue' => 'Revenue / income', - 'title_deposit' => 'Revenue / income', - 'title_transfer' => 'Transfers', - 'title_transfers' => 'Transfers', + 'title_expenses' => 'Expenses', + 'title_withdrawal' => 'Expenses', + 'title_revenue' => 'Revenue / income', + 'title_deposit' => 'Revenue / income', + 'title_transfer' => 'Transfers', + 'title_transfers' => 'Transfers', // convert stuff: 'convert_is_already_type_Withdrawal' => 'This transaction is already a withdrawal', diff --git a/resources/views/v1/export/index.twig b/resources/views/v1/export/index.twig new file mode 100644 index 0000000000..5f93768f95 --- /dev/null +++ b/resources/views/v1/export/index.twig @@ -0,0 +1,35 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.render(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} +
+
+
+
+

{{ 'export_data_main_title'|_ }}

+
+
+

+ {{ 'export_data_expl'|_ }} +

+ +

+ {{ 'export_data_advanced_expl'|_ }} +

+
+
+
+
+ +{% endblock %} + +{% block styles %} +{% endblock %} + +{% block scripts %} +{% endblock %} diff --git a/resources/views/v1/partials/menu-sidebar.twig b/resources/views/v1/partials/menu-sidebar.twig index 87467fbae8..847bdd96ce 100644 --- a/resources/views/v1/partials/menu-sidebar.twig +++ b/resources/views/v1/partials/menu-sidebar.twig @@ -58,12 +58,35 @@ {{ 'reports'|_ }} - -
  • - - - {{ 'import_transactions'|_ }} +
  • + + + {% if config('firefly.feature_flags.export') %} + {{ 'import_and_export_menu'|_ }} + {% else %} + {{ 'import_transactions'|_ }} + {% endif %} + + + + +
  • diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index fc08ecd78b..c9ecac8eb5 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -563,7 +563,7 @@ try { 'export.index', function (BreadcrumbsGenerator $breadcrumbs) { $breadcrumbs->parent('home'); - $breadcrumbs->push(trans('firefly.export_data'), route('export.index')); + $breadcrumbs->push(trans('firefly.export_data_bc'), route('export.index')); } ); diff --git a/routes/console.php b/routes/console.php index 6577bdbce0..d922fde31e 100644 --- a/routes/console.php +++ b/routes/console.php @@ -22,14 +22,3 @@ declare(strict_types=1); - -/* -|-------------------------------------------------------------------------- -| Console Routes -|-------------------------------------------------------------------------- -| -| This file is where you may define all of your Closure based console -| commands. Each Closure is bound to a command instance allowing a -| simple approach to interacting with each command's IO methods. -| -*/ diff --git a/routes/web.php b/routes/web.php index 031480ed93..eaf9e60f3d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -488,7 +488,17 @@ Route::group( } ); +/** + * Export controller + */ +Route::group( + ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'export', 'as' => 'export.'], static function () { + // index + Route::get('', ['uses' => 'Export\IndexController@index', 'as' => 'index']); + Route::get('export', ['uses' => 'Export\IndexController@export', 'as' => 'export']); + +}); /** * Import Controller */