diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 4d81705ea3f6..10036a77bda7 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -104,9 +104,9 @@ class ForgotPasswordController extends Controller */ public function sendResetLinkEmail(Request $request) { - //MultiDB::userFindAndSetDb($request->input('email')); + MultiDB::userFindAndSetDb($request->input('email')); - $user = MultiDB::hasUser(['email' => $request->input('email')]); + // $user = MultiDB::hasUser(['email' => $request->input('email')]); $this->validateEmail($request); diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php new file mode 100644 index 000000000000..e2d99efa8e93 --- /dev/null +++ b/app/Http/Controllers/ExportController.php @@ -0,0 +1,64 @@ +user()->getCompany(), auth()->user()); + + return response()->json(['message' => 'Processing'], 200); + + } +} diff --git a/app/Http/Requests/Export/StoreExportRequest.php b/app/Http/Requests/Export/StoreExportRequest.php new file mode 100644 index 000000000000..96d322a53df6 --- /dev/null +++ b/app/Http/Requests/Export/StoreExportRequest.php @@ -0,0 +1,39 @@ +user()->isAdmin(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return []; + } +} diff --git a/app/Jobs/Company/CompanyExport.php b/app/Jobs/Company/CompanyExport.php new file mode 100644 index 000000000000..f9206969fd8b --- /dev/null +++ b/app/Jobs/Company/CompanyExport.php @@ -0,0 +1,471 @@ +company = $company; + $this->user = $user; + $this->export_format = $export_format; + } + + /** + * Execute the job. + * + * @return CompanyToken|null + */ + public function handle() : void + { + + MultiDB::setDb($this->company->db); + + set_time_limit(0); + + $this->export_data['app_version'] = config('ninja.app_version'); + + $this->export_data['activities'] = $this->company->all_activities->map(function ($activity){ + + $activity = $this->transformArrayOfKeys($activity, [ + 'user_id', + 'company_id', + 'client_id', + 'client_contact_id', + 'account_id', + 'project_id', + 'vendor_id', + 'payment_id', + 'invoice_id', + 'credit_id', + 'invitation_id', + 'task_id', + 'expense_id', + 'token_id', + 'quote_id', + 'subscription_id', + 'recurring_invoice_id' + ]); + + return $activity; + + })->makeHidden(['id'])->toArray(); + + $this->export_data['backups'] = $this->company->all_activities()->with('backup')->cursor()->map(function ($activity){ + + $backup = $activity->backup; + + if(!$backup) + return; + + $backup->activity_id = $this->encodePrimaryKey($backup->activity_id); + + return $backup; + + })->toArray(); + + $this->export_data['client_contacts'] = $this->company->client_contacts->map(function ($client_contact){ + + $client_contact = $this->transformArrayOfKeys($client_contact, ['id', 'company_id', 'user_id',' client_id']); + + return $client_contact; + + })->toArray(); + + + $this->export_data['client_gateway_tokens'] = $this->company->client_gateway_tokens->map(function ($client_gateway_token){ + + $client_gateway_token = $this->transformArrayOfKeys($client_gateway_token, ['id', 'company_id', 'client_id']); + + return $client_gateway_token; + + })->toArray(); + + + $this->export_data['clients'] = $this->company->clients->map(function ($client){ + + $client = $this->transformArrayOfKeys($client, ['id', 'company_id', 'user_id',' assigned_user_id', 'group_settings_id']); + + return $client; + + })->toArray(); + + $temp_co = $this->company; + $temp_co->id = $this->encodePrimaryKey($temp_co->id); + $temp_co->account_id = $this->encodePrimaryKey($temp_co->account_id); + + $this->export_data['company'] = $temp_co->toArray(); + + $this->export_data['company_gateways'] = $this->company->company_gateways->map(function ($company_gateway){ + + $company_gateway = $this->transformArrayOfKeys($company_gateway, ['company_id', 'user_id']); + + return $company_gateway; + + })->toArray(); + + $this->export_data['company_tokens'] = $this->company->tokens->map(function ($token){ + + $token = $this->transformArrayOfKeys($token, ['company_id', 'account_id', 'user_id']); + + return $token; + + })->toArray(); + + $this->export_data['company_ledger'] = $this->company->ledger->map(function ($ledger){ + + $ledger = $this->transformArrayOfKeys($ledger, ['activity_id', 'client_id', 'company_id', 'account_id', 'user_id','company_ledgerable_id']); + + return $ledger; + + })->toArray(); + + $this->export_data['company_users'] = $this->company->company_users->map(function ($company_user){ + + $company_user = $this->transformArrayOfKeys($company_user, ['company_id', 'account_id', 'user_id']); + + return $company_user; + + })->toArray(); + + $this->export_data['credits'] = $this->company->credits->map(function ($credit){ + + $credit = $this->transformBasicEntities($credit); + $credit = $this->transformArrayOfKeys($credit, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','invoice_id']); + + return $credit; + + })->toArray(); + + + $this->export_data['credit_invitations'] = CreditInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($credit){ + + $credit = $this->transformArrayOfKeys($credit, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']); + + return $credit; + + })->toArray(); + + $this->export_data['designs'] = $this->company->user_designs->makeHidden(['id'])->toArray(); + + $this->export_data['documents'] = $this->company->documents->map(function ($document){ + + $document = $this->transformArrayOfKeys($document, ['user_id', 'assigned_user_id', 'company_id', 'project_id', 'vendor_id']); + + return $document; + + })->toArray(); + + $this->export_data['expense_categories'] = $this->company->expenses->map(function ($expense_category){ + + $expense_category = $this->transformArrayOfKeys($expense_category, ['user_id', 'company_id']); + + return $expense_category; + + })->toArray(); + + + $this->export_data['expenses'] = $this->company->expenses->map(function ($expense){ + + $expense = $this->transformBasicEntities($expense); + $expense = $this->transformArrayOfKeys($expense, ['vendor_id', 'invoice_id', 'client_id', 'category_id', 'recurring_expense_id','project_id']); + + return $expense; + + })->toArray(); + + $this->export_data['group_settings'] = $this->company->group_settings->map(function ($gs){ + + $gs = $this->transformArrayOfKeys($gs, ['user_id', 'company_id']); + + return $gs; + + })->toArray(); + + + $this->export_data['invoices'] = $this->company->invoices->map(function ($invoice){ + + $invoice = $this->transformBasicEntities($invoice); + $invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']); + + return $invoice; + + })->toArray(); + + + $this->export_data['invoice_invitations'] = InvoiceInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($invoice){ + + $invoice = $this->transformArrayOfKeys($invoice, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']); + + return $invoice; + + })->toArray(); + + $this->export_data['payment_terms'] = $this->company->user_payment_terms->map(function ($term){ + + $term = $this->transformArrayOfKeys($term, ['user_id', 'company_id']); + + return $term; + + })->makeHidden(['id'])->toArray(); + + $this->export_data['paymentables'] = $this->company->payments()->with('paymentables')->cursor()->map(function ($paymentable){ + + $paymentable = $this->transformArrayOfKeys($paymentable, ['payment_id','paymentable_id']); + + return $paymentable; + + })->toArray(); + + $this->export_data['payments'] = $this->company->payments->map(function ($payment){ + + $payment = $this->transformBasicEntities($payment); + $payment = $this->transformArrayOfKeys($payment, ['client_id','project_id', 'vendor_id', 'client_contact_id', 'invitation_id', 'company_gateway_id']); + + return $project; + + })->toArray(); + + + $this->export_data['projects'] = $this->company->projects->map(function ($project){ + + $project = $this->transformBasicEntities($project); + $project = $this->transformArrayOfKeys($project, ['client_id']); + + return $project; + + })->toArray(); + + $this->export_data['quotes'] = $this->company->quotes->map(function ($quote){ + + $quote = $this->transformBasicEntities($quote); + $quote = $this->transformArrayOfKeys($quote, ['invoice_id','recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']); + + return $quote; + + })->toArray(); + + + $this->export_data['quote_invitations'] = QuoteInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($quote){ + + $quote = $this->transformArrayOfKeys($quote, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']); + + return $quote; + + })->toArray(); + + + $this->export_data['recurring_invoices'] = $this->company->recurring_invoices->map(function ($ri){ + + $ri = $this->transformBasicEntities($ri); + $ri = $this->transformArrayOfKeys($ri, ['client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']); + return $ri; + + })->toArray(); + + + $this->export_data['recurring_invoice_invitations'] = RecurringInvoiceInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($ri){ + + $ri = $this->transformArrayOfKeys($ri, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']); + + return $ri; + + })->toArray(); + + $this->export_data['subscriptions'] = $this->company->subscriptions->map(function ($subscription){ + + $subscription = $this->transformBasicEntities($subscription); + $subscription->group_id = $this->encodePrimaryKey($subscription->group_id); + + return $subscription; + + })->toArray(); + + + $this->export_data['system_logs'] = $this->company->system_logs->map(function ($log){ + + $log->client_id = $this->encodePrimaryKey($log->client_id); + $log->company_id = $this->encodePrimaryKey($log->company_id); + + return $log; + + })->makeHidden(['id'])->toArray(); + + $this->export_data['tasks'] = $this->company->tasks->map(function ($task){ + + $task = $this->transformBasicEntities($task); + $task = $this->transformArrayOfKeys($task, ['client_id', 'invoice_id', 'project_id', 'status_id']); + + return $task; + + })->toArray(); + + $this->export_data['task_statuses'] = $this->company->task_statuses->map(function ($status){ + + $status->id = $this->encodePrimaryKey($status->id); + $status->user_id = $this->encodePrimaryKey($status->user_id); + $status->company_id = $this->encodePrimaryKey($status->company_id); + + return $status; + + })->toArray(); + + $this->export_data['tax_rates'] = $this->company->tax_rates->map(function ($rate){ + + $rate->company_id = $this->encodePrimaryKey($rate->company_id); + $rate->user_id = $this->encodePrimaryKey($rate->user_id); + + return $rate; + + })->makeHidden(['id'])->toArray(); + + $this->export_data['users'] = $this->company->users->map(function ($user){ + + $user->account_id = $this->encodePrimaryKey($user->account_id); + $user->id = $this->encodePrimaryKey($user->id); + + return $user; + + })->makeHidden(['ip'])->toArray(); + + $this->export_data['vendors'] = $this->company->vendors->map(function ($vendor){ + + return $this->transformBasicEntities($vendor); + + })->toArray(); + + + $this->export_data['vendor_contacts'] = VendorContact::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($vendor){ + + $vendor = $this->transformBasicEntities($vendor); + $vendor->vendor_id = $this->encodePrimaryKey($vendor->vendor_id); + + return $vendor; + + })->toArray(); + + $this->export_data['webhooks'] = $this->company->webhooks->map(function ($hook){ + + $hook->user_id = $this->encodePrimaryKey($hook->user_id); + $hook->company_id = $this->encodePrimaryKey($hook->company_id); + + return $hook; + + })->makeHidden(['id'])->toArray(); + + //write to tmp and email to owner(); + + $this->zipAndSend(); + } + + private function transformBasicEntities($model) + { + + return $this->transformArrayOfKeys($model, ['id', 'user_id', 'assigned_user_id', 'company_id']); + + } + + private function transformArrayOfKeys($model, $keys) + { + + foreach($keys as $key){ + $model->{$key} = $this->encodePrimaryKey($model->{$key}); + } + + return $model; + + } + + private function zipAndSend() + { + nlog("zipping"); + + $tempStream = fopen('php://memory', 'w+'); + + $options = new Archive(); + $options->setOutputStream($tempStream); + + $file_name = date('Y-m-d').'_'.str_replace(' ', '_', $this->company->present()->name() . '_' . $this->company->company_key .'.zip'); + + $zip = new ZipStream($file_name, $options); + + $fp = tmpfile(); + fwrite($fp, json_encode($this->export_data)); + rewind($fp); + $zip->addFileFromStream('backup.json', $fp); + + $zip->finish(); + + $path = 'backups/'; + + nlog($path.$file_name); + + Storage::disk(config('filesystems.default'))->put($path.$file_name, $tempStream); + // fclose($fp); + + nlog(Storage::disk(config('filesystems.default'))->url($path.$file_name)); + + fclose($tempStream); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new DownloadBackup(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company); + $nmo->to_user = $this->user; + $nmo->settings = $this->company->settings; + $nmo->company = $this->company; + + NinjaMailerJob::dispatch($nmo); + + UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1)); + } + +} diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 5796d038dce1..6e8777750635 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -54,7 +54,9 @@ class NinjaMailerJob implements ShouldQueue public $nmo; - public function __construct(NinjaMailerObject $nmo) + public $override; + + public function __construct(NinjaMailerObject $nmo, bool $override = false) { $this->nmo = $nmo; @@ -64,7 +66,7 @@ class NinjaMailerJob implements ShouldQueue public function handle() { /*If we are migrating data we don't want to fire any emails*/ - if ($this->nmo->company->is_disabled) + if ($this->nmo->company->is_disabled && !$this->override) return true; /*Set the correct database*/ @@ -83,6 +85,10 @@ class NinjaMailerJob implements ShouldQueue $this->nmo->mailable->replyTo($this->nmo->settings->reply_to_email, $reply_to_name); } + else { + $this->nmo->mailable->replyTo($this->nmo->company->owner()->email, $this->nmo->company->owner()->present()->name()); + } + if (strlen($this->nmo->settings->bcc_email) > 1) { nlog('bcc list available'); diff --git a/app/Jobs/User/UserEmailChanged.php b/app/Jobs/User/UserEmailChanged.php index de433a4c826f..a87a83b11f52 100644 --- a/app/Jobs/User/UserEmailChanged.php +++ b/app/Jobs/User/UserEmailChanged.php @@ -55,9 +55,6 @@ class UserEmailChanged implements ShouldQueue public function handle() { nlog("notifying user of email change"); - - if ($this->company->is_disabled) - return true; //Set DB MultiDB::setDb($this->company->db); @@ -78,7 +75,7 @@ class UserEmailChanged implements ShouldQueue $nmo->company = $this->company; $nmo->to_user = $this->old_user; - NinjaMailerJob::dispatch($nmo); + NinjaMailerJob::dispatch($nmo, true); // $nmo->to_user = $this->new_user; // NinjaMailerJob::dispatch($nmo); diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 7fc8ec6ea423..e3a2a267fcd3 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -209,6 +209,9 @@ class Import implements ShouldQueue $this->{$method}($data[$import]); } + if(Ninja::isHosted()) + $this->processNinjaTokens($data['ninja_tokens']); + $this->setInitialCompanyLedgerBalances(); // $this->fixClientBalances(); @@ -1636,6 +1639,10 @@ class Import implements ShouldQueue return $response->getBody(); } + private function processNinjaTokens(array $data) + { + + } /* In V4 we use negative invoices (credits) and add then into the client balance. In V5, these sit off ledger and are applied later. This next section will check for credit balances and reduce the client balance so that the V5 balances are correct diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 92132a118be2..27673891e09f 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -129,13 +129,12 @@ class MultiDB } foreach (self::$dbs as $db) { + self::setDB($db); - $user = User::where($data)->withTrashed()->first(); - - if ($user) { + if ($user = User::where($data)->withTrashed()->first()) return $user; - } + } self::setDefaultDatabase(); diff --git a/app/Mail/DownloadBackup.php b/app/Mail/DownloadBackup.php new file mode 100644 index 000000000000..5b354a5db265 --- /dev/null +++ b/app/Mail/DownloadBackup.php @@ -0,0 +1,41 @@ +file_path = $file_path; + + $this->company = $company; + } + + /** + * Build the message. + */ + public function build() + { + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject(ctrans('texts.download_backup_subject')) + ->markdown( + 'email.admin.download_files', + [ + 'url' => $this->file_path, + 'logo' => $this->company->present()->logo, + 'whitelabel' => $this->company->account->isPaid() ? true : false, + ] + ); + } +} diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index f9e3196c2fe7..1568c290e96a 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -107,7 +107,7 @@ class TemplateEmail extends Mailable }); //conditionally attach files - if ($settings->pdf_email_attachment !== false && ! empty($this->build_email->getAttachments())) { + // if ($settings->pdf_email_attachment !== false && ! empty($this->build_email->getAttachments())) { //hosted | plan check here foreach ($this->build_email->getAttachments() as $file) { @@ -118,7 +118,7 @@ class TemplateEmail extends Mailable $this->attach($file['path'], ['as' => $file['name'], 'mime' => $file['mime']]); } - } + // } return $this; } diff --git a/app/Models/Company.php b/app/Models/Company.php index ab808ac3510e..47ea068fea24 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -150,6 +150,11 @@ class Company extends BaseModel return $this->belongsTo(Account::class); } + public function client_contacts() + { + return $this->hasMany(ClientContact::class)->withTrashed(); + } + public function users() { return $this->hasManyThrough(User::class, CompanyUser::class, 'company_id', 'id', 'id', 'user_id'); @@ -203,6 +208,12 @@ class Company extends BaseModel return $this->hasMany(Vendor::class)->withTrashed(); } + public function all_activities() + { + return $this->hasMany(Activity::class); + } + + public function activities() { return $this->hasMany(Activity::class)->orderBy('id', 'DESC')->take(300); @@ -301,11 +312,21 @@ class Company extends BaseModel return $this->hasMany(Design::class)->whereCompanyId($this->id)->orWhere('company_id', null); } + public function user_designs() + { + return $this->hasMany(Design::class); + } + public function payment_terms() { return $this->hasMany(PaymentTerm::class)->whereCompanyId($this->id)->orWhere('company_id', null); } + public function user_payment_terms() + { + return $this->hasMany(PaymentTerm::class); + } + /** * @return BelongsTo */ diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index d3da0af8b89f..7acfb3258875 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -415,6 +415,8 @@ class Invoice extends BaseModel CreateEntityPdf::dispatchNow($invitation); } +nlog($storage_path); + return $storage_path; } diff --git a/app/PaymentDrivers/Stripe/SOFORT.php b/app/PaymentDrivers/Stripe/SOFORT.php index af96cdf1c693..77d6e63a57d8 100644 --- a/app/PaymentDrivers/Stripe/SOFORT.php +++ b/app/PaymentDrivers/Stripe/SOFORT.php @@ -102,8 +102,6 @@ class SOFORT { $server_response = $this->stripe->payment_hash->data; - PaymentFailureMailer::dispatch($this->stripe->client, $server_response->redirect_status, $this->stripe->client->company, $server_response->amount); - PaymentFailureMailer::dispatch( $this->stripe->client, $server_response, diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index 552d580df458..3d1d72317977 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -82,9 +82,20 @@ class SystemHealth 'mail_mailer' => (string)self::checkMailMailer(), 'flutter_renderer' => (string)config('ninja.flutter_canvas_kit'), 'jobs_pending' => (int) Queue::size(), + 'pdf_engine' => (string) self::getPdfEngine(), ]; } + public static function getPdfEngine() + { + if(config('ninja.invoiceninja_hosted_pdf_generation')) + return 'Invoice Ninja Hosted PDF Generator'; + elseif(config('ninja.phantomjs_pdf_generation')) + return 'Phantom JS Web Generator'; + else + return 'SnapPDF PDF Generator'; + } + public static function checkMailMailer() { return config('mail.default'); diff --git a/resources/lang/ca/texts.php b/resources/lang/ca/texts.php index 5302d2431d11..efa11815049c 100644 --- a/resources/lang/ca/texts.php +++ b/resources/lang/ca/texts.php @@ -2861,6 +2861,7 @@ $LANG = [ 'my_invoices' => 'My Invoices', 'mobile_refresh_warning' => 'If you\'re using the mobile app you may need to do a full refresh.', 'enable_proposals_for_background' => 'To upload a background image :link to enable the proposals module.', + ]; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 2a46a25013ba..7f1174d56bf1 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4248,6 +4248,7 @@ $LANG = array( 'activity_104' => ':user restored recurring invoice :recurring_invoice', 'new_login_detected' => 'New login detected for your account.', 'new_login_description' => 'You recently logged in to your Invoice Ninja account from a new location or device:

IP: :ip
Time: :time
Email: :email', + 'download_backup_subject' => 'Your company backup is ready for download', ); return $LANG; diff --git a/routes/api.php b/routes/api.php index ed9029c41ff2..b46bd12f0d2e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -75,6 +75,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::put('expenses/{expense}/upload', 'ExpenseController@upload'); Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); + Route::post('export', 'ExportController@index')->name('export.index'); + Route::resource('expense_categories', 'ExpenseCategoryController'); // name = (expense_categories. index / create / show / update / destroy / edit Route::post('expense_categories/bulk', 'ExpenseCategoryController@bulk')->name('expense_categories.bulk'); diff --git a/tests/Feature/Export/ExportCompanyTest.php b/tests/Feature/Export/ExportCompanyTest.php new file mode 100644 index 000000000000..bfdad43e3845 --- /dev/null +++ b/tests/Feature/Export/ExportCompanyTest.php @@ -0,0 +1,49 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + // $this->faker = \Faker\Factory::create(); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testCompanyExport() + { + CompanyExport::dispatchNow($this->company, $this->company->users->first()); + } +}