mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-10-26 00:42:53 -04:00 
			
		
		
		
	Fixes for CSRF issues with client portal downloads
This commit is contained in:
		
							parent
							
								
									81d9e7a6ec
								
							
						
					
					
						commit
						e245d07a75
					
				| @ -30,6 +30,7 @@ use Illuminate\View\View; | |||||||
| use Symfony\Component\HttpFoundation\BinaryFileResponse; | use Symfony\Component\HttpFoundation\BinaryFileResponse; | ||||||
| use ZipStream\Option\Archive; | use ZipStream\Option\Archive; | ||||||
| use ZipStream\ZipStream; | use ZipStream\ZipStream; | ||||||
|  | use Illuminate\Http\Request; | ||||||
| 
 | 
 | ||||||
| class QuoteController extends Controller | class QuoteController extends Controller | ||||||
| { | { | ||||||
| @ -58,7 +59,6 @@ class QuoteController extends Controller | |||||||
|             'quote' => $quote, |             '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) { | ||||||
| @ -82,7 +82,7 @@ class QuoteController extends Controller | |||||||
|         $transformed_ids = $this->transformKeys($request->quotes); |         $transformed_ids = $this->transformKeys($request->quotes); | ||||||
| 
 | 
 | ||||||
|         if ($request->action == 'download') { |         if ($request->action == 'download') { | ||||||
|             return $this->downloadQuotePdf((array) $transformed_ids); |             return $this->downloadQuotes((array) $transformed_ids); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if ($request->action = 'approve') { |         if ($request->action = 'approve') { | ||||||
| @ -92,10 +92,32 @@ class QuoteController extends Controller | |||||||
|         return back(); |         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) |     protected function downloadQuotePdf(array $ids) | ||||||
|     { |     { | ||||||
|         $quotes = Quote::whereIn('id', $ids) |         $quotes = Quote::whereIn('id', $ids) | ||||||
|             ->whereClientId(auth()->user()->client->id) |             ->whereClientId(auth()->user()->client->id) | ||||||
|  |             ->withTrashed() | ||||||
|             ->get(); |             ->get(); | ||||||
| 
 | 
 | ||||||
|         if (! $quotes || $quotes->count() == 0) { |         if (! $quotes || $quotes->count() == 0) { | ||||||
| @ -136,6 +158,7 @@ class QuoteController extends Controller | |||||||
|             ->where('client_id', auth('contact')->user()->client->id) |             ->where('client_id', auth('contact')->user()->client->id) | ||||||
|             ->where('company_id', auth('contact')->user()->client->company_id) |             ->where('company_id', auth('contact')->user()->client->company_id) | ||||||
|             ->where('status_id', Quote::STATUS_SENT) |             ->where('status_id', Quote::STATUS_SENT) | ||||||
|  |             ->withTrashed() | ||||||
|             ->get(); |             ->get(); | ||||||
| 
 | 
 | ||||||
|         if (!$quotes || $quotes->count() == 0) { |         if (!$quotes || $quotes->count() == 0) { | ||||||
|  | |||||||
							
								
								
									
										108
									
								
								phpunit.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								phpunit.yml
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
							
								
								
									
										47
									
								
								resources/views/portal/ninja2020/invoices/download.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								resources/views/portal/ninja2020/invoices/download.blade.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | @extends('portal.ninja2020.layout.app') | ||||||
|  | @section('meta_title', ctrans('texts.view_invoice')) | ||||||
|  | 
 | ||||||
|  | @push('head') | ||||||
|  | 
 | ||||||
|  | @endpush | ||||||
|  | 
 | ||||||
|  | @section('body') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     <div class="container mx-auto"> | ||||||
|  |         <div class="grid grid-cols-6 gap-4"> | ||||||
|  |             <div class="flex float-right"> | ||||||
|  |                 <form action="{{ route('client.invoices.download') }}" method="post" id="bulkActions"> | ||||||
|  |                     @foreach($invoices as $invoice) | ||||||
|  |                         <input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}"> | ||||||
|  |                     @endforeach | ||||||
|  |                     @csrf | ||||||
|  |                     <button type="submit" onclick="setTimeout(() => this.disabled = true, 0); setTimeout(() => this.disabled = true, 5000); return true;" class="button button-primary bg-primary" name="action" value="download">{{ ctrans('texts.download') }}</button> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         @foreach($invoices as $invoice) | ||||||
|  |         <div> | ||||||
|  |             <dl> | ||||||
|  |                 @if(!empty($invoice->number) && !is_null($invoice->number)) | ||||||
|  |                 <div class="px-4 py-5 bg-white sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> | ||||||
|  |                     <dt class="text-sm font-medium leading-5 text-gray-500"> | ||||||
|  |                         {{ ctrans('texts.invoice_number') }} | ||||||
|  |                     </dt> | ||||||
|  |                     <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> | ||||||
|  |                         {{ $invoice->number }} | ||||||
|  |                     </dd> | ||||||
|  |                 </div> | ||||||
|  |                 @endif | ||||||
|  |             </dl> | ||||||
|  |         </div> | ||||||
|  |      | ||||||
|  |     @endforeach | ||||||
|  | 
 | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  | @endsection | ||||||
|  | 
 | ||||||
|  | @section('footer') | ||||||
|  | @endsection | ||||||
							
								
								
									
										46
									
								
								resources/views/portal/ninja2020/quotes/download.blade.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								resources/views/portal/ninja2020/quotes/download.blade.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | @extends('portal.ninja2020.layout.app') | ||||||
|  | @section('meta_title', ctrans('texts.view_quote')) | ||||||
|  | 
 | ||||||
|  | @push('head') | ||||||
|  | 
 | ||||||
|  | @endpush | ||||||
|  | 
 | ||||||
|  | @section('body') | ||||||
|  | 
 | ||||||
|  |     <div class="container mx-auto"> | ||||||
|  |         <div class="grid grid-cols-6 gap-4"> | ||||||
|  |             <div class="flex float-right"> | ||||||
|  |                 <form action="{{ route('client.quotes.download') }}" method="post" id="bulkActions"> | ||||||
|  |                     @foreach($quotes as $quote) | ||||||
|  |                         <input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}"> | ||||||
|  |                     @endforeach | ||||||
|  |                     @csrf | ||||||
|  |                     <button type="submit" onclick="setTimeout(() => this.disabled = true, 0); setTimeout(() => this.disabled = true, 5000); return true;" class="button button-primary bg-primary" name="action" value="download">{{ ctrans('texts.download') }}</button> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         @foreach($quotes as $quote) | ||||||
|  |         <div> | ||||||
|  |             <dl> | ||||||
|  |                 @if(!empty($quote->number) && !is_null($quote->number)) | ||||||
|  |                 <div class="px-4 py-5 bg-white sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> | ||||||
|  |                     <dt class="text-sm font-medium leading-5 text-gray-500"> | ||||||
|  |                         {{ ctrans('texts.quote_number') }} | ||||||
|  |                     </dt> | ||||||
|  |                     <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> | ||||||
|  |                         {{ $quote->number }} | ||||||
|  |                     </dd> | ||||||
|  |                 </div> | ||||||
|  |                 @endif | ||||||
|  |             </dl> | ||||||
|  |         </div> | ||||||
|  |      | ||||||
|  |     @endforeach | ||||||
|  | 
 | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  | @endsection | ||||||
|  | 
 | ||||||
|  | @section('footer') | ||||||
|  | @endsection | ||||||
| @ -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', 'ClientPortal\QuoteController@index')->name('quotes.index')->middleware('portal_enabled'); | ||||||
|     Route::get('quotes/{quote}', 'ClientPortal\QuoteController@show')->name('quote.show'); |     Route::get('quotes/{quote}', 'ClientPortal\QuoteController@show')->name('quote.show'); | ||||||
|     Route::get('quotes/{quote_invitation}', 'ClientPortal\QuoteController@show')->name('quote.show_invitation'); |     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', 'ClientPortal\CreditController@index')->name('credits.index'); | ||||||
|     Route::get('credits/{credit}', 'ClientPortal\CreditController@show')->name('credit.show'); |     Route::get('credits/{credit}', 'ClientPortal\CreditController@show')->name('credit.show'); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user