diff --git a/app/Http/Controllers/Migration/StepsController.php b/app/Http/Controllers/Migration/StepsController.php index 93bec24aeecd..6a1d08756e22 100644 --- a/app/Http/Controllers/Migration/StepsController.php +++ b/app/Http/Controllers/Migration/StepsController.php @@ -17,12 +17,37 @@ use App\Models\Product; use App\Models\TaxRate; use App\Models\User; use Illuminate\Support\Facades\Auth; + +use App\Http\Controllers\BaseController; +use App\Http\Requests\MigrationAuthRequest; +use App\Http\Requests\MigrationCompaniesRequest; +use App\Http\Requests\MigrationEndpointRequest; +use App\Http\Requests\MigrationTypeRequest; +use App\Models\Document; +use App\Services\Migration\AuthService; +use App\Services\Migration\CompanyService; +use App\Services\Migration\CompleteService; use Illuminate\Support\Facades\Crypt; class StepsController extends BaseController { private $account; + private $access = [ + 'auth' => [ + 'steps' => ['MIGRATION_TYPE'], + 'redirect' => '/migration/start', + ], + 'endpoint' => [ + 'steps' => ['MIGRATION_TYPE'], + 'redirect' => '/migration/start', + ], + 'companies' => [ + 'steps' => ['MIGRATION_TYPE', 'MIGRATION_ACCOUNT_TOKEN'], + 'redirect' => '/migration/auth', + ], + ]; + /** * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ @@ -44,12 +69,142 @@ class StepsController extends BaseController return view('migration.download'); } + public function handleType(MigrationTypeRequest $request) + { + session()->put('MIGRATION_TYPE', $request->option); + + if($request->option == 0) + return redirect('/migration/auth'); + + return redirect('/migration/endpoint'); + } + + public function endpoint() + { + if($this->shouldGoBack('endpoint')) + return redirect($this->access['endpoint']['redirect']); + + return view('migration.endpoint'); + } + + public function handleEndpoint(MigrationEndpointRequest $request) + { + if($this->shouldGoBack('endpoint')) + return redirect($this->access['endpoint']['redirect']); + + session()->put('MIGRATION_ENDPOINT', $request->endpoint); + + return redirect('/migration/auth'); + } + + public function auth() + { + if($this->shouldGoBack('auth')) + return redirect($this->access['auth']['redirect']); + + return view('migration.auth'); + } + + public function handleAuth(MigrationAuthRequest $request) + { + if($this->shouldGoBack('auth')) { + return redirect($this->access['auth']['redirect']); + } + + $authentication = (new AuthService($request->email, $request->password)) + ->endpoint(session('MIGRATION_ENDPOINT')) + ->start(); + + if($authentication->isSuccessful()) { + session()->put('MIGRATION_ACCOUNT_TOKEN', $authentication->getAccountToken()); + + return redirect('/migration/companies'); + } + + return back()->with('responseErrors', $authentication->getErrors()); + } + + public function companies() + { + if($this->shouldGoBack('companies')) + return redirect($this->access['companies']['redirect']); + + $companyService = (new CompanyService(session('MIGRATION_ACCOUNT_TOKEN'))) + ->endpoint(session('MIGRATION_ENDPOINT')) + ->start(); + + if($companyService->isSuccessful()) { + return view('migration.companies', ['companies' => $companyService->getCompanies()]); + } + + return response()->json([ + 'message' => 'Oops, looks like something failed. Please try again.' + ], 500); + } + + public function handleCompanies(MigrationCompaniesRequest $request) + { + if($this->shouldGoBack('companies')) + return redirect($this->access['companies']['redirect']); + + $successful = false; + + foreach ($request->companies as $company) { + $completeService = (new CompleteService(session('MIGRATION_ACCOUNT_TOKEN'))) + ->file($this->getMigrationFile()) + ->company($company) + ->endpoint(session('MIGRATION_ENDPOINT')) + ->start(); + + if($completeService->isSuccessful()) { + $successful = true; + } + + $successful = false; + } + + if($successful) { + return view('migration.completed'); + } + + return response([ + 'message' => 'Failed', + 'errors' => $completeService->getErrors(), + ]); + } + + public function completed() + { + return view('migration.completed'); + } + + /** + * ================================== + * Rest of functions that are used as 'actions', not controller methods. + * ================================== + */ + + public function shouldGoBack(string $step) + { + $redirect = true; + + foreach ($this->access[$step]['steps'] as $step) { + if(session()->has($step)) { + $redirect = false; + } else { + $redirect = true; + } + } + + return $redirect; + } + /** * Handle data downloading for the migration. * * @return \Illuminate\Http\JsonResponse */ - public function handleDownload() + public function getMigrationFile() { $this->account = Auth::user()->account; @@ -82,14 +237,11 @@ class StepsController extends BaseController $zip->addFromString('migration.json', json_encode($data, JSON_PRETTY_PRINT)); $zip->close(); - header('Content-Type: application/zip'); - header('Content-Length: ' . filesize($file)); - header("Content-Disposition: attachment; filename={$fileName}.zip"); + // header('Content-Type: application/zip'); + // header('Content-Length: ' . filesize($file)); + // header("Content-Disposition: attachment; filename={$fileName}.zip"); - readfile($file); - unlink($file); - - return response()->json($data); + return $file; } /** diff --git a/app/Http/Requests/MigrationAuthRequest.php b/app/Http/Requests/MigrationAuthRequest.php new file mode 100644 index 000000000000..a7469daf7dd1 --- /dev/null +++ b/app/Http/Requests/MigrationAuthRequest.php @@ -0,0 +1,31 @@ + 'required|email', + 'password' => 'required', + ]; + } +} diff --git a/app/Http/Requests/MigrationCompaniesRequest.php b/app/Http/Requests/MigrationCompaniesRequest.php new file mode 100644 index 000000000000..6819be32add6 --- /dev/null +++ b/app/Http/Requests/MigrationCompaniesRequest.php @@ -0,0 +1,30 @@ + 'required', + ]; + } +} diff --git a/app/Http/Requests/MigrationEndpointRequest.php b/app/Http/Requests/MigrationEndpointRequest.php new file mode 100644 index 000000000000..a7d6aef7151e --- /dev/null +++ b/app/Http/Requests/MigrationEndpointRequest.php @@ -0,0 +1,30 @@ + 'required|url', + ]; + } +} diff --git a/app/Http/Requests/MigrationTypeRequest.php b/app/Http/Requests/MigrationTypeRequest.php new file mode 100644 index 000000000000..54665aa553ed --- /dev/null +++ b/app/Http/Requests/MigrationTypeRequest.php @@ -0,0 +1,30 @@ + 'required|in:0,1', + ]; + } +} diff --git a/app/Services/Migration/AuthService.php b/app/Services/Migration/AuthService.php new file mode 100644 index 000000000000..e96341fab0a3 --- /dev/null +++ b/app/Services/Migration/AuthService.php @@ -0,0 +1,95 @@ +username = $username; + $this->password = $password; + } + + public function endpoint(string $endpoint) + { + $this->endpoint = $endpoint; + + return $this; + } + + public function start() + { + $data = [ + 'email' => $this->username, + 'password' => $this->password, + ]; + + $body = Body::json($data); + + $response = Request::post($this->getUrl(), $this->getHeaders(), $body); + + if ($response->code == 200) { + $this->isSuccessful = true; + $this->token = $response->body->data[0]->token->token; + } + + if (in_array($response->code, [401, 422, 500])) { + $this->isSuccessful = false; + $this->processErrors($response->body); + } + + return $this; + } + + public function isSuccessful() + { + return $this->isSuccessful; + } + + public function getAccountToken() + { + if ($this->isSuccessful) { + return $this->token; + } + + return null; + } + + + public function getErrors() + { + return $this->errors; + } + + private function getHeaders() + { + return [ + 'X-Requested-With' => 'XMLHttpRequest', + 'Content-Type' => 'application/json', + ]; + } + + private function getUrl() + { + return $this->endpoint . $this->uri; + } + + private function processErrors($errors) + { + $array = (array) $errors; + + $this->errors = $array; + } +} diff --git a/app/Services/Migration/CompanyService.php b/app/Services/Migration/CompanyService.php new file mode 100644 index 000000000000..ac655af9bfd5 --- /dev/null +++ b/app/Services/Migration/CompanyService.php @@ -0,0 +1,89 @@ +token = $token; + } + + public function endpoint(string $endpoint) + { + $this->endpoint = $endpoint; + + return $this; + } + + public function start() + { + $response = Request::get($this->getUrl(), $this->getHeaders()); + + if ($response->code == 200) { + $this->isSuccessful = true; + + foreach($response->body->data as $company) { + $this->companies[] = $company; + } + } + + if (in_array($response->code, [401, 422, 500])) { + $this->isSuccessful = false; + $this->processErrors($response->body); + } + + return $this; + } + + public function isSuccessful() + { + return $this->isSuccessful; + } + + public function getCompanies() + { + if ($this->isSuccessful) { + return $this->companies; + } + + return []; + } + + + public function getErrors() + { + return $this->errors; + } + + private function getHeaders() + { + return [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-Api-Token' => $this->token, + ]; + } + + private function getUrl() + { + return $this->endpoint . $this->uri; + } + + private function processErrors($errors) + { + $array = (array) $errors; + + $this->errors = $array; + } +} diff --git a/app/Services/Migration/CompleteService.php b/app/Services/Migration/CompleteService.php new file mode 100644 index 000000000000..1f07ce5b0f94 --- /dev/null +++ b/app/Services/Migration/CompleteService.php @@ -0,0 +1,98 @@ +token = $token; + } + + public function file($file) + { + $this->file = $file; + + return $this; + } + + public function company($company) + { + $this->company = $company; + + return $this; + } + + public function endpoint(string $endpoint) + { + $this->endpoint = $endpoint; + + return $this; + } + + public function start() + { + $body = [ + 'migration' => \Unirest\Request\Body::file($this->file, 'application/zip'), + ]; + + $response = Request::post($this->getUrl(), $this->getHeaders(), $body); + + if ($response->code == 200) { + $this->isSuccessful = true; + $this->deleteFile(); + } + + if (in_array($response->code, [401, 422, 500])) { + $this->isSuccessful = false; + $this->errors = [ + 'Oops, something went wrong. Migration can\'t be processed at the moment.', + ]; + } + + return $this; + } + + public function isSuccessful() + { + return $this->isSuccessful; + } + + + public function getErrors() + { + return $this->errors; + } + + private function getHeaders() + { + return [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-Api-Token' => $this->token, + 'Content-Type' => 'multipart/form-data', + ]; + } + + private function getUrl() + { + return $this->endpoint . $this->uri . $this->company; + } + + public function deleteFile() + { + Storage::delete($this->file); + } +} diff --git a/composer.json b/composer.json index 28a0f3238182..2fff201c2679 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,8 @@ "php": ">=7.0.0", "ext-gd": "*", "ext-gmp": "*", + "ext-json": "*", + "ext-zip": "*", "anahkiasen/former": "4.*", "asgrim/ofxparser": "^1.1", "bacon/bacon-qr-code": "^1.0", @@ -51,6 +53,7 @@ "league/flysystem-rackspace": "~1.0", "league/fractal": "0.13.*", "maatwebsite/excel": "~2.0", + "mashape/unirest-php": "^3.0", "mpdf/mpdf": "7.1.7", "nesbot/carbon": "^1.26", "nwidart/laravel-modules": "2.0.*", @@ -68,9 +71,7 @@ "webpatser/laravel-countries": "dev-master#75992ad", "websight/l5-google-cloud-storage": "dev-master", "wepay/php-sdk": "^0.2", - "wildbit/postmark-php": "^2.5", - "ext-json": "*", - "ext-zip": "*" + "wildbit/postmark-php": "^2.5" }, "require-dev": { "symfony/dom-crawler": "~3.1", diff --git a/composer.lock b/composer.lock index 5db1efb09e44..105fb01e2217 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9124fcb26c4f7a15410a4b6899151938", + "content-hash": "5ebbda0ec4f775dcd10261da641f4c27", "packages": [ { "name": "abdala/omnipay-pagseguro", @@ -561,12 +561,12 @@ "version": "v0.9.3", "source": { "type": "git", - "url": "https://github.com/barryvdh/laravel-cors.git", + "url": "https://github.com/fruitcake/laravel-cors.git", "reference": "2551489de60486471434b0c7050f7fc65f9c9119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-cors/zipball/2551489de60486471434b0c7050f7fc65f9c9119", + "url": "https://api.github.com/repos/fruitcake/laravel-cors/zipball/2551489de60486471434b0c7050f7fc65f9c9119", "reference": "2551489de60486471434b0c7050f7fc65f9c9119", "shasum": "" }, @@ -1274,6 +1274,7 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", + "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, { @@ -5124,6 +5125,52 @@ ], "time": "2018-03-09T13:14:19+00:00" }, + { + "name": "mashape/unirest-php", + "version": "v3.0.4", + "source": { + "type": "git", + "url": "https://github.com/Mashape/unirest-php.git", + "reference": "842c0f242dfaaf85f16b72e217bf7f7c19ab12cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Mashape/unirest-php/zipball/842c0f242dfaaf85f16b72e217bf7f7c19ab12cb", + "reference": "842c0f242dfaaf85f16b72e217bf7f7c19ab12cb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "codeclimate/php-test-reporter": "0.1.*", + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-json": "Allows using JSON Bodies for sending and parsing requests" + }, + "type": "library", + "autoload": { + "psr-0": { + "Unirest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Unirest PHP", + "homepage": "https://github.com/Mashape/unirest-php", + "keywords": [ + "client", + "curl", + "http", + "https", + "rest" + ], + "time": "2016-08-11T17:49:21+00:00" + }, { "name": "maximebf/debugbar", "version": "v1.14.1", @@ -5571,6 +5618,7 @@ "cron", "schedule" ], + "abandoned": "dragonmantank/cron-expression", "time": "2017-01-23T04:29:33+00:00" }, { @@ -11352,6 +11400,7 @@ "escaper", "zf2" ], + "abandoned": "laminas/laminas-escaper", "time": "2016-06-30T19:48:38+00:00" }, { @@ -11405,6 +11454,7 @@ "zend", "zf" ], + "abandoned": "laminas/laminas-http", "time": "2017-10-13T12:06:24+00:00" }, { @@ -11460,6 +11510,7 @@ "json", "zf2" ], + "abandoned": "laminas/laminas-json", "time": "2016-02-04T21:20:26+00:00" }, { @@ -11504,6 +11555,7 @@ "loader", "zf2" ], + "abandoned": "laminas/laminas-loader", "time": "2015-06-03T14:05:47+00:00" }, { @@ -11549,6 +11601,7 @@ "stdlib", "zf2" ], + "abandoned": "laminas/laminas-stdlib", "time": "2016-09-13T14:38:50+00:00" }, { @@ -11596,6 +11649,7 @@ "uri", "zf2" ], + "abandoned": "laminas/laminas-uri", "time": "2016-02-17T22:38:51+00:00" }, { @@ -11667,6 +11721,7 @@ "validator", "zf2" ], + "abandoned": "laminas/laminas-validator", "time": "2018-02-01T17:05:33+00:00" }, { @@ -11753,6 +11808,7 @@ "push", "zf2" ], + "abandoned": true, "time": "2017-01-17T13:57:50+00:00" }, { @@ -13626,7 +13682,9 @@ "platform": { "php": ">=7.0.0", "ext-gd": "*", - "ext-gmp": "*" + "ext-gmp": "*", + "ext-json": "*", + "ext-zip": "*" }, "platform-dev": [] } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 30c8a22d5383..8f7e11c7b1d2 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -3273,7 +3273,6 @@ $LANG = array( 'start_the_migration' => 'Start the migration', 'migration' => 'Migration', 'welcome_to_the_new_version' => 'Welcome to the new version of Invoice Ninja', - 'next_step_data_download' => 'At the next step, we\'ll let you download your data for the migration.', 'download_data' => 'Press button below to download the data.', 'migration_import' => 'Awesome! Now you are ready to import your migration. Go to your new installation to import your data', 'continue' => 'Continue', diff --git a/resources/views/migration/auth.blade.php b/resources/views/migration/auth.blade.php new file mode 100644 index 000000000000..c68281caad6f --- /dev/null +++ b/resources/views/migration/auth.blade.php @@ -0,0 +1,32 @@ +@extends('header') + +@section('content') + @parent + @include('accounts.nav', ['selected' => ACCOUNT_MANAGEMENT]) + + @include('migration.includes.errors') + +
+
+

{!! trans('texts.welcome_to_the_new_version') !!}

+
+
+

Let's continue with authentication.

+
+ {{ csrf_field() }} +
+ + +
+ +
+ + +
+
+
+ +
+@stop \ No newline at end of file diff --git a/resources/views/migration/companies.blade.php b/resources/views/migration/companies.blade.php new file mode 100644 index 000000000000..18cc11a82281 --- /dev/null +++ b/resources/views/migration/companies.blade.php @@ -0,0 +1,32 @@ +@extends('header') + +@section('content') + @parent + @include('accounts.nav', ['selected' => ACCOUNT_MANAGEMENT]) + + @include('migration.includes.errors') + +
+
+

{!! trans('texts.welcome_to_the_new_version') !!}

+
+
+

Awesome! Please select the company you would like to apply migration.

+
+ {{ csrf_field() }} + + @foreach($companies as $company) +
+ + +
+ @endforeach +
+
+ +
+@stop \ No newline at end of file diff --git a/resources/views/migration/completed.blade.php b/resources/views/migration/completed.blade.php new file mode 100644 index 000000000000..849525683a79 --- /dev/null +++ b/resources/views/migration/completed.blade.php @@ -0,0 +1,17 @@ +@extends('header') + +@section('content') + @parent + @include('accounts.nav', ['selected' => ACCOUNT_MANAGEMENT]) + +
+
+

{!! trans('texts.welcome_to_the_new_version') !!}

+
+
+ Completed, thanks! + +
+
+ +@stop \ No newline at end of file diff --git a/resources/views/migration/endpoint.blade.php b/resources/views/migration/endpoint.blade.php new file mode 100644 index 000000000000..bbf8da64485a --- /dev/null +++ b/resources/views/migration/endpoint.blade.php @@ -0,0 +1,28 @@ +@extends('header') + +@section('content') + @parent + @include('accounts.nav', ['selected' => ACCOUNT_MANAGEMENT]) + +
+
+

{!! trans('texts.welcome_to_the_new_version') !!}

+
+
+

We need to know the link of your application.

+
+ {{ csrf_field() }} +
+
+ + +
+
+
+
+ +
+ +@stop \ No newline at end of file diff --git a/resources/views/migration/includes/errors.blade.php b/resources/views/migration/includes/errors.blade.php new file mode 100644 index 000000000000..648ba05839a1 --- /dev/null +++ b/resources/views/migration/includes/errors.blade.php @@ -0,0 +1,9 @@ +@if(session('responseErrors')) +
+ +
+@endif \ No newline at end of file diff --git a/resources/views/migration/start.blade.php b/resources/views/migration/start.blade.php index 4fe0131d2891..0137061b7b12 100644 --- a/resources/views/migration/start.blade.php +++ b/resources/views/migration/start.blade.php @@ -9,10 +9,28 @@

{!! trans('texts.welcome_to_the_new_version') !!}

-

{!! trans('texts.next_step_data_download') !!}

+

In order to start the migration, we need to know where do you want to migrate.

+
+ {{ csrf_field() }} +
+ + +

If you chose 'hosted', we will migrate your data to official Invoice Ninja servers & take care of server handling.

+
+
+ + +

By choosing the 'self-hosted', you are the one in charge of servers.

+
+
+ diff --git a/routes/web.php b/routes/web.php index 3e25c95c4038..ab2062bfb435 100644 --- a/routes/web.php +++ b/routes/web.php @@ -149,9 +149,16 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () { Route::post('settings/enable_two_factor', 'TwoFactorController@enableTwoFactor'); Route::get('migration/start', 'Migration\StepsController@start'); - + Route::post('migration/type', 'Migration\StepsController@handleType'); Route::get('migration/download', 'Migration\StepsController@download'); Route::post('migration/download', 'Migration\StepsController@handleDownload'); + Route::get('migration/endpoint', 'Migration\StepsController@endpoint'); + Route::post('migration/endpoint', 'Migration\StepsController@handleEndpoint'); + Route::get('migration/auth', 'Migration\StepsController@auth'); + Route::post('migration/auth', 'Migration\StepsController@handleAuth'); + Route::get('migration/companies', 'Migration\StepsController@companies'); + Route::post('migration/companies', 'Migration\StepsController@handleCompanies'); + Route::get('migration/completed', 'Migration\StepsController@completed'); Route::get('migration/import', 'Migration\StepsController@import');