Miguel Nogueira
5f1f92a9ce
This commit fixes some superficial instances of Broken Access Control (https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A5-Broken_Access_Control). There may be some more instances of this, as authorization was only done after most of the controllers were done (big mistake). Some refactoring was also performed, where Route Model Binding with DI (dependency injection) was used whenever possible, to increase testability of the codebase. Some reused code was also moved to Helper classes as to enforce DRY; There may be some lines of code that are still copy-pasted from other parts of the codebase for reuse. Non-breaking refactoring changes were made, but the app as a whole still needs full manual testing, and customised responses to HTTP 500 responses. Some errors are also not handled gracefully and this wasn't checked in this commit.
272 lines
8.3 KiB
PHP
272 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Application;
|
|
|
|
use App\Response;
|
|
use App\Vacancy;
|
|
use App\User;
|
|
|
|
use App\Events\ApplicationDeniedEvent;
|
|
use App\Notifications\NewApplicant;
|
|
use App\Notifications\ApplicationMoved;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
use ContextAwareValidator;
|
|
|
|
class ApplicationController extends Controller
|
|
{
|
|
|
|
private function canVote($votes)
|
|
{
|
|
$allvotes = collect([]);
|
|
|
|
foreach ($votes as $vote)
|
|
{
|
|
if ($vote->userID == Auth::user()->id)
|
|
{
|
|
$allvotes->push($vote);
|
|
}
|
|
}
|
|
|
|
return ($allvotes->count() == 1) ? false : true;
|
|
}
|
|
|
|
|
|
|
|
public function showUserApps()
|
|
{
|
|
|
|
return view('dashboard.user.applications')
|
|
->with('applications', Auth::user()->applications);
|
|
}
|
|
|
|
|
|
public function showUserApp(Request $request, Application $application)
|
|
{
|
|
$this->authorize('view', $application);
|
|
|
|
if (!is_null($application))
|
|
{
|
|
return view('dashboard.user.viewapp')
|
|
->with(
|
|
[
|
|
'application' => $application,
|
|
'comments' => $application->comments,
|
|
'structuredResponses' => json_decode($application->response->responseData, true),
|
|
'formStructure' => $application->response->form,
|
|
'vacancy' => $application->response->vacancy,
|
|
'canVote' => $this->canVote($application->votes)
|
|
]
|
|
);
|
|
}
|
|
else
|
|
{
|
|
$request->session()->flash('error', 'The application you requested could not be found.');
|
|
}
|
|
|
|
return redirect()->back();
|
|
}
|
|
|
|
|
|
|
|
public function showAllApps()
|
|
{
|
|
$this->authorize('viewAny', Application::class);
|
|
|
|
return view('dashboard.appmanagement.all')
|
|
->with('applications', Application::paginate(6));
|
|
}
|
|
|
|
|
|
public function showAllPendingApps()
|
|
{
|
|
$this->authorize('viewAny', Application::class);
|
|
|
|
return view('dashboard.appmanagement.outstandingapps')
|
|
->with('applications', Application::where('applicationStatus', 'STAGE_SUBMITTED')->get());
|
|
}
|
|
|
|
|
|
public function showPendingInterview()
|
|
{
|
|
$this->authorize('viewAny', Application::class);
|
|
$applications = Application::with('appointment', 'user')->get();
|
|
$count = 0;
|
|
|
|
$pendingInterviews = collect([]);
|
|
$upcomingInterviews = collect([]);
|
|
|
|
|
|
foreach ($applications as $application)
|
|
{
|
|
if (!is_null($application->appointment) && $application->appointment->appointmentStatus == 'CONCLUDED')
|
|
{
|
|
$count =+ 1;
|
|
}
|
|
|
|
switch ($application->applicationStatus)
|
|
{
|
|
case 'STAGE_INTERVIEW':
|
|
$upcomingInterviews->push($application);
|
|
|
|
break;
|
|
|
|
case 'STAGE_INTERVIEW_SCHEDULED':
|
|
$pendingInterviews->push($application);
|
|
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
return view('dashboard.appmanagement.interview')
|
|
->with([
|
|
'finishedCount' => $count,
|
|
'applications' => $pendingInterviews,
|
|
'upcomingApplications' => $upcomingInterviews
|
|
]);
|
|
}
|
|
|
|
|
|
|
|
public function showPeerReview()
|
|
{
|
|
$this->authorize('viewAny', Application::class);
|
|
return view('dashboard.appmanagement.peerreview')
|
|
->with('applications', Application::where('applicationStatus', 'STAGE_PEERAPPROVAL')->get());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function renderApplicationForm(Request $request, $vacancySlug)
|
|
{
|
|
// FIXME: Get rid of references to first(), this is a wonky query
|
|
$vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get();
|
|
|
|
$firstVacancy = $vacancyWithForm->first();
|
|
|
|
if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN')
|
|
{
|
|
|
|
return view('dashboard.application-rendering.apply')
|
|
->with([
|
|
|
|
'vacancy' => $vacancyWithForm->first(),
|
|
'preprocessedForm' => json_decode($vacancyWithForm->first()->forms->formStructure, true)
|
|
|
|
]);
|
|
}
|
|
else
|
|
{
|
|
abort(404, 'The application you\'re looking for could not be found or it is currently unavailable.');
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function saveApplicationAnswers(Request $request, $vacancySlug)
|
|
{
|
|
$vacancy = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get();
|
|
|
|
if ($vacancy->first()->vacancyCount == 0 || $vacancy->first()->vacancyStatus !== 'OPEN')
|
|
{
|
|
|
|
$request->session()->flash('error', 'This application is unavailable.');
|
|
return redirect()->back();
|
|
|
|
}
|
|
|
|
Log::info('Processing new application!');
|
|
|
|
$formStructure = json_decode($vacancy->first()->forms->formStructure, true);
|
|
$responseValidation = ContextAwareValidator::getResponseValidator($request->all(), $formStructure);
|
|
|
|
Log::info('Built response & validator structure!');
|
|
|
|
if (!$responseValidation->get('validator')->fails())
|
|
{
|
|
$response = Response::create([
|
|
'responseFormID' => $vacancy->first()->forms->id,
|
|
'associatedVacancyID' => $vacancy->first()->id, // Since a form can be used by multiple vacancies, we can only know which specific vacancy this response ties to by using a vacancy ID
|
|
'responseData' => $responseValidation->get('responseStructure')
|
|
]);
|
|
|
|
Log::info('Registered form response for user ' . Auth::user()->name . ' for vacancy ' . $vacancy->first()->vacancyName);
|
|
|
|
$application = Application::create([
|
|
'applicantUserID' => Auth::user()->id,
|
|
'applicantFormResponseID' => $response->id,
|
|
'applicationStatus' => 'STAGE_SUBMITTED',
|
|
]);
|
|
|
|
Log::info('Submitted application for user ' . Auth::user()->name . ' with response ID' . $response->id);
|
|
|
|
foreach(User::all() as $user)
|
|
{
|
|
if ($user->hasRole('admin'))
|
|
{
|
|
$user->notify((new NewApplicant($application, $vacancy->first()))->delay(now()->addSeconds(10)));
|
|
}
|
|
}
|
|
|
|
$request->session()->flash('success', 'Thank you for your application! It will be reviewed as soon as possible.');
|
|
return redirect()->to(route('showUserApps'));
|
|
}
|
|
else
|
|
{
|
|
Log::warning('Application form for ' . Auth::user()->name . ' contained errors, resetting!');
|
|
$request->session()->flash('error', 'There are one or more errors in your application. Please make sure none of your fields are empty, since they are all required.');
|
|
|
|
}
|
|
|
|
return redirect()->back();
|
|
}
|
|
|
|
public function updateApplicationStatus(Request $request, $application, $newStatus)
|
|
{
|
|
$this->authorize('update', Application::class);
|
|
|
|
switch ($newStatus)
|
|
{
|
|
case 'deny':
|
|
|
|
event(new ApplicationDeniedEvent($application));
|
|
break;
|
|
|
|
case 'interview':
|
|
Log::info('User ' . Auth::user()->name . ' has moved application ID ' . $application->id . 'to interview stage');
|
|
$request->session()->flash('success', 'Application moved to interview stage! (:');
|
|
$application->setStatus('STAGE_INTERVIEW');
|
|
|
|
$application->user->notify(new ApplicationMoved());
|
|
break;
|
|
|
|
default:
|
|
$request->session()->flash('error', 'There are no suitable statuses to update to. Do not mess with the URL.');
|
|
}
|
|
|
|
return redirect()->back();
|
|
}
|
|
|
|
public function delete(Request $request, Application $application)
|
|
{
|
|
|
|
$this->authorize('delete', $application);
|
|
$application->delete(); // observers will run, cleaning it up
|
|
|
|
$request->session()->flash('success', 'Application deleted. Comments, appointments and responses have also been deleted.');
|
|
return redirect()->back();
|
|
|
|
}
|
|
|
|
}
|