From e245d07a75a0f600334ca909e8eb5bcc3aa07ba8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 14 Dec 2021 15:38:32 +1100 Subject: [PATCH] Fixes for CSRF issues with client portal downloads --- .../ClientPortal/QuoteController.php | 41 +++++-- phpunit.yml | 108 ++++++++++++++++++ .../ninja2020/invoices/download.blade.php | 47 ++++++++ .../ninja2020/quotes/download.blade.php | 46 ++++++++ routes/client.php | 1 + 5 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 phpunit.yml create mode 100644 resources/views/portal/ninja2020/invoices/download.blade.php create mode 100644 resources/views/portal/ninja2020/quotes/download.blade.php diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php index 8b338eb49e..c646d41efe 100644 --- a/app/Http/Controllers/ClientPortal/QuoteController.php +++ b/app/Http/Controllers/ClientPortal/QuoteController.php @@ -30,6 +30,7 @@ use Illuminate\View\View; use Symfony\Component\HttpFoundation\BinaryFileResponse; use ZipStream\Option\Archive; use ZipStream\ZipStream; +use Illuminate\Http\Request; class QuoteController extends Controller { @@ -58,17 +59,16 @@ class QuoteController extends Controller 'quote' => $quote, ]; + $invitation = $quote->invitations()->where('client_contact_id', auth()->user()->id)->first(); - $invitation = $quote->invitations()->where('client_contact_id', auth()->user()->id)->first(); + if ($invitation && auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) { - if ($invitation && auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) { + $invitation->markViewed(); - $invitation->markViewed(); - - event(new InvitationWasViewed($quote, $invitation, $quote->company, Ninja::eventVars())); - event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars())); - - } + event(new InvitationWasViewed($quote, $invitation, $quote->company, Ninja::eventVars())); + event(new QuoteWasViewed($invitation, $invitation->company, Ninja::eventVars())); + + } if ($request->query('mode') === 'fullscreen') { return render('quotes.show-fullscreen', $data); @@ -82,7 +82,7 @@ class QuoteController extends Controller $transformed_ids = $this->transformKeys($request->quotes); if ($request->action == 'download') { - return $this->downloadQuotePdf((array) $transformed_ids); + return $this->downloadQuotes((array) $transformed_ids); } if ($request->action = 'approve') { @@ -92,10 +92,32 @@ class QuoteController extends Controller return back(); } + public function downloadQuotes($ids) + { + + $data['quotes'] = Quote::whereIn('id', $ids) + ->whereClientId(auth()->user()->client->id) + ->withTrashed() + ->get(); + + if(count($data['quotes']) == 0) + return back()->with(['message' => ctrans('texts.no_items_selected')]); + + return $this->render('quotes.download', $data); + } + + public function download(Request $request) + { + $transformed_ids = $this->transformKeys($request->quotes); + + return $this->downloadQuotePdf((array) $transformed_ids); + } + protected function downloadQuotePdf(array $ids) { $quotes = Quote::whereIn('id', $ids) ->whereClientId(auth()->user()->client->id) + ->withTrashed() ->get(); if (! $quotes || $quotes->count() == 0) { @@ -136,6 +158,7 @@ class QuoteController extends Controller ->where('client_id', auth('contact')->user()->client->id) ->where('company_id', auth('contact')->user()->client->company_id) ->where('status_id', Quote::STATUS_SENT) + ->withTrashed() ->get(); if (!$quotes || $quotes->count() == 0) { diff --git a/phpunit.yml b/phpunit.yml new file mode 100644 index 0000000000..d77a3ccffa --- /dev/null +++ b/phpunit.yml @@ -0,0 +1,108 @@ +on: + push: + branches: + - v5-develop + pull_request: + branches: + - v5-develop + +name: phpunit +jobs: + run: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: ['ubuntu-18.04', 'ubuntu-20.04'] + php-versions: ['7.3','7.4','8.0'] + phpunit-versions: ['latest'] + + env: + DB_DATABASE1: ninja + DB_USERNAME1: root + DB_PASSWORD1: ninja + DB_HOST1: '127.0.0.1' + DB_DATABASE: ninja + DB_USERNAME: root + DB_PASSWORD: ninja + DB_HOST: '127.0.0.1' + BROADCAST_DRIVER: log + CACHE_DRIVER: file + QUEUE_CONNECTION: sync + SESSION_DRIVER: file + NINJA_ENVIRONMENT: hosted + MULTI_DB_ENABLED: false + NINJA_LICENSE: 123456 + TRAVIS: true + MAIL_MAILER: log + + services: + mariadb: + image: mariadb:latest + ports: + - 32768:3306 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_USER: ninja + MYSQL_PASSWORD: ninja + MYSQL_DATABASE: ninja + MYSQL_ROOT_PASSWORD: ninja + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Start mysql service + run: | + sudo systemctl start mysql.service + - name: Verify MariaDB connection + env: + DB_PORT: ${{ job.services.mariadb.ports[3306] }} + DB_PORT1: ${{ job.services.mariadb.ports[3306] }} + + run: | + while ! mysqladmin ping -h"127.0.0.1" -P"$DB_PORT" --silent; do + sleep 1 + done + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: mysql, mysqlnd, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml + + - uses: actions/checkout@v1 + with: + ref: v5-develop + fetch-depth: 1 + + - name: Copy .env + run: | + cp .env.ci .env + - name: Install composer dependencies + run: | + composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + composer install + - name: Prepare Laravel Application + run: | + php artisan key:generate + php artisan optimize + php artisan cache:clear + php artisan config:cache + - name: Create DB and schemas + run: | + mkdir -p database + touch database/database.sqlite + - name: Migrate Database + run: | + php artisan migrate:fresh --seed --force && php artisan db:seed --force + - name: Prepare JS/CSS assets + run: | + npm i + npm run production + - name: Run Testsuite + run: | + cat .env + vendor/bin/phpunit --testdox + env: + DB_PORT: ${{ job.services.mysql.ports[3306] }} + + - name: Run php-cs-fixer + run: | + vendor/bin/php-cs-fixer fix diff --git a/resources/views/portal/ninja2020/invoices/download.blade.php b/resources/views/portal/ninja2020/invoices/download.blade.php new file mode 100644 index 0000000000..e50eb74bd9 --- /dev/null +++ b/resources/views/portal/ninja2020/invoices/download.blade.php @@ -0,0 +1,47 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.view_invoice')) + +@push('head') + +@endpush + +@section('body') + + +
+
+
+
+ @foreach($invoices as $invoice) + + @endforeach + @csrf + +
+
+
+ + @foreach($invoices as $invoice) +
+
+ @if(!empty($invoice->number) && !is_null($invoice->number)) +
+
+ {{ ctrans('texts.invoice_number') }} +
+
+ {{ $invoice->number }} +
+
+ @endif +
+
+ + @endforeach + +
+ +@endsection + +@section('footer') +@endsection diff --git a/resources/views/portal/ninja2020/quotes/download.blade.php b/resources/views/portal/ninja2020/quotes/download.blade.php new file mode 100644 index 0000000000..2dfbd90405 --- /dev/null +++ b/resources/views/portal/ninja2020/quotes/download.blade.php @@ -0,0 +1,46 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.view_quote')) + +@push('head') + +@endpush + +@section('body') + +
+
+
+
+ @foreach($quotes as $quote) + + @endforeach + @csrf + +
+
+
+ + @foreach($quotes as $quote) +
+
+ @if(!empty($quote->number) && !is_null($quote->number)) +
+
+ {{ ctrans('texts.quote_number') }} +
+
+ {{ $quote->number }} +
+
+ @endif +
+
+ + @endforeach + +
+ +@endsection + +@section('footer') +@endsection diff --git a/routes/client.php b/routes/client.php index b2b2c7f1af..2b3e30ee37 100644 --- a/routes/client.php +++ b/routes/client.php @@ -67,6 +67,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence Route::get('quotes', 'ClientPortal\QuoteController@index')->name('quotes.index')->middleware('portal_enabled'); Route::get('quotes/{quote}', 'ClientPortal\QuoteController@show')->name('quote.show'); Route::get('quotes/{quote_invitation}', 'ClientPortal\QuoteController@show')->name('quote.show_invitation'); + Route::post('quotes/download', 'ClientPortal\QuoteController@download')->name('quotes.download'); Route::get('credits', 'ClientPortal\CreditController@index')->name('credits.index'); Route::get('credits/{credit}', 'ClientPortal\CreditController@show')->name('credit.show');