Initial commit
This commit is contained in:
116
app/Console/Commands/CleanDatabase.php
Normal file
116
app/Console/Commands/CleanDatabase.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Forms\FormStatistic;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Forms\FormView;
|
||||
use App\Models\Forms\FormSubmission;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CleanDatabase extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'forms:database-cleanup';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Database Cleanup';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->cleanFormStatistics();
|
||||
|
||||
$this->line('Database Cleanup Success.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage FormViews & FormSubmissions records
|
||||
*/
|
||||
private function cleanFormStatistics()
|
||||
{
|
||||
$this->line('Aggregating form views...');
|
||||
$now = now();
|
||||
$finalData = [];
|
||||
|
||||
// Form Views
|
||||
FormView::select('form_id', DB::raw('DATE(created_at) as date'), DB::raw('count(*) as views'))
|
||||
->where('created_at', '<', $now)
|
||||
->orderBy('date')
|
||||
->groupBy('form_id', 'date')
|
||||
->get()->each(function ($row) use (&$finalData) {
|
||||
$finalData[$row->form_id."-".$row->date] = [
|
||||
'form_id' => $row->form_id,
|
||||
'date' => $row->date,
|
||||
'data' => [
|
||||
'views' => $row->views,
|
||||
'submissions' => 0
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
// Form Submissions
|
||||
$this->line('Aggregating form submissions...');
|
||||
FormSubmission::select('form_id', DB::raw('DATE(created_at) as date'), DB::raw('count(*) as submissions'))
|
||||
->whereDate('created_at', '<=', now()->startOfDay())
|
||||
->orderBy('date')
|
||||
->groupBy('form_id', 'date')
|
||||
->get()->each(function ($row) use (&$finalData) {
|
||||
$key = $row->form_id."-".$row->date;
|
||||
if (isset($finalData[$key])) {
|
||||
$finalData[$key]['data']['submissions'] = $row->submissions;
|
||||
} else {
|
||||
$finalData[$key] = [
|
||||
'form_id' => $row->form_id,
|
||||
'date' => $row->date,
|
||||
'data' => [
|
||||
'views' => 0,
|
||||
'submissions' => $row->submissions
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if ($finalData) {
|
||||
$this->line('Storing aggregated data...');
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
// Insert into Form Statistic
|
||||
foreach ($finalData as $row) {
|
||||
$found = FormStatistic::where([['form_id', $row['form_id']], ['date', $row['date']]])->first();
|
||||
if ($found !== null) { // If found update
|
||||
$newData = $found->data;
|
||||
$newData['views'] = $newData['views'] + $row['data']['views'];
|
||||
$newData['submissions'] = $newData['submissions'] + $row['data']['submissions'];
|
||||
$found->update(['data' => $newData]);
|
||||
$updated++;
|
||||
} else { // Otherwise create new
|
||||
FormStatistic::create($row);
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->line($created.' form statistics records created.');
|
||||
$this->line($updated.' form statistics records updated.');
|
||||
|
||||
// Delete Form Views those are migrated
|
||||
$formViewRemovedCount = FormView::where('created_at', '<', $now)->delete();
|
||||
$this->line($formViewRemovedCount.' form views records deleted.');
|
||||
} else {
|
||||
$this->line('No aggregate to store.');
|
||||
}
|
||||
}
|
||||
}
|
||||
41
app/Console/Kernel.php
Normal file
41
app/Console/Kernel.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* The Artisan commands provided by your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('forms:database-cleanup')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
31
app/Events/Forms/FormSubmitted.php
Normal file
31
app/Events/Forms/FormSubmitted.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Forms;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FormSubmitted
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $form;
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Form $form, array $data)
|
||||
{
|
||||
$this->form = $form;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
25
app/Events/Models/FormCreated.php
Normal file
25
app/Events/Models/FormCreated.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Models;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FormCreated
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Form $form)
|
||||
{}
|
||||
}
|
||||
19
app/Exceptions/EmailTakenException.php
Normal file
19
app/Exceptions/EmailTakenException.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class EmailTakenException extends Exception
|
||||
{
|
||||
/**
|
||||
* Render the exception as an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request)
|
||||
{
|
||||
return response()->view('oauth.emailTaken', [], 400);
|
||||
}
|
||||
}
|
||||
89
app/Exceptions/Handler.php
Normal file
89
app/Exceptions/Handler.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported to Sentry.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $sentryDontReport = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert an authentication exception into a response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Auth\AuthenticationException $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
return $request->expectsJson()
|
||||
? response()->json(['message' => $exception->getMessage()], 401)
|
||||
: redirect()->guest(url('/login'));
|
||||
}
|
||||
|
||||
public function report(Throwable $exception)
|
||||
{
|
||||
if ($this->shouldReport($exception) ) {
|
||||
if (app()->bound('sentry') && $this->sentryShouldReport($exception)) {
|
||||
app('sentry')->captureException($exception);
|
||||
Log::debug('Un-handled Exception: '.$exception->getMessage(), [
|
||||
'exception' => $exception,
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTrace(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
parent::report($exception);
|
||||
}
|
||||
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
if ($this->shouldReport($e) && !in_array(\App::environment(),['testing'])) {
|
||||
Log::channel('slack')->error($e);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
private function sentryShouldReport(Throwable $e)
|
||||
{
|
||||
foreach ($this->sentryDontReport as $exceptionType) {
|
||||
if ($e instanceof $exceptionType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
22
app/Exceptions/VerifyEmailException.php
Normal file
22
app/Exceptions/VerifyEmailException.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VerifyEmailException extends ValidationException
|
||||
{
|
||||
/**
|
||||
* @param \App\User $user
|
||||
* @return static
|
||||
*/
|
||||
public static function forUser($user)
|
||||
{
|
||||
return static::withMessages([
|
||||
'email' => [__('You must :linkOpen verify :linkClose your email first.', [
|
||||
'linkOpen' => '<a href="/email/resend?email='.urlencode($user->email).'">',
|
||||
'linkClose' => '</a>',
|
||||
])],
|
||||
]);
|
||||
}
|
||||
}
|
||||
26
app/Exceptions/Workspaces/WorkspaceAlreadyExisting.php
Normal file
26
app/Exceptions/Workspaces/WorkspaceAlreadyExisting.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Workspaces;
|
||||
|
||||
use App\Models\Workspace;
|
||||
use Exception;
|
||||
|
||||
class WorkspaceAlreadyExisting extends Exception
|
||||
{
|
||||
public function __construct(public Workspace $workspace)
|
||||
{
|
||||
}
|
||||
|
||||
public function getErrorMessage()
|
||||
{
|
||||
$owner = $this->workspace->users()->first();
|
||||
if (!$owner) {
|
||||
return 'A user already connected that workspace to another NotionForms account. You or the current workspace
|
||||
owner must have a NotionForms Enterprise subscription for you to add this Notion workspace. Please upgrade
|
||||
with an Enterprise subscription, or contact us to get help.';
|
||||
}
|
||||
|
||||
return '"'.$owner->name.'" already connected that workspace to his NotionForms account. In order to collaborate,
|
||||
one of you must have a NotionForms Enterprise subscription. Please upgrade or contact us to get help.';
|
||||
}
|
||||
}
|
||||
10
app/Exceptions/Workspaces/WorkspaceLimit.php
Normal file
10
app/Exceptions/Workspaces/WorkspaceLimit.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Workspaces;
|
||||
|
||||
use Exception;
|
||||
|
||||
class WorkspaceLimit extends Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
34
app/Exports/FormSubmissionExport.php
Normal file
34
app/Exports/FormSubmissionExport.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromArray;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
|
||||
class FormSubmissionExport implements FromArray, WithHeadingRow
|
||||
{
|
||||
|
||||
protected array $submissionData;
|
||||
|
||||
public function __construct(array $submissionData)
|
||||
{
|
||||
$headingRow = [];
|
||||
$contentRow = [];
|
||||
foreach ($submissionData as $i => $row) {
|
||||
if($i==0){
|
||||
$headingRow[] = array_keys($row);
|
||||
}
|
||||
$contentRow[] = array_values($row);
|
||||
}
|
||||
|
||||
$this->submissionData = [
|
||||
$headingRow,
|
||||
$contentRow
|
||||
];
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
return $this->submissionData;
|
||||
}
|
||||
}
|
||||
42
app/Http/Controllers/Admin/ImpersonationController.php
Normal file
42
app/Http/Controllers/Admin/ImpersonationController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ImpersonationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('admin');
|
||||
}
|
||||
|
||||
public function impersonate($identifier) {
|
||||
$user = null;
|
||||
if (is_numeric($identifier)) {
|
||||
$user = User::find($identifier);
|
||||
} elseif (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
|
||||
$user = User::whereEmail($identifier)->first();
|
||||
} else {
|
||||
// Find by form slug
|
||||
$form = Form::whereSlug($identifier)->first();
|
||||
if ($form) {
|
||||
$user = $form->creator;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user) return $this->error([
|
||||
'message'=> 'User not found.'
|
||||
]);
|
||||
|
||||
// Be this user
|
||||
$token = auth()->login($user);
|
||||
return $this->success([
|
||||
'token' => $token
|
||||
]);
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
46
app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
use SendsPasswordResetEmails;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a successful password reset link.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function sendResetLinkResponse(Request $request, $response)
|
||||
{
|
||||
return ['status' => trans($response)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a failed password reset link.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function sendResetLinkFailedResponse(Request $request, $response)
|
||||
{
|
||||
return response()->json(['email' => trans($response)], 400);
|
||||
}
|
||||
}
|
||||
114
app/Http/Controllers/Auth/LoginController.php
Normal file
114
app/Http/Controllers/Auth/LoginController.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Exceptions\VerifyEmailException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to log the user into the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function attemptLogin(Request $request)
|
||||
{
|
||||
$token = $this->guard()->attempt($this->credentials($request));
|
||||
|
||||
if (! $token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->guard()->user();
|
||||
if ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->guard()->setToken($token);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the needed authorization credentials from the request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function credentials(Request $request)
|
||||
{
|
||||
return [
|
||||
$this->username() => strtolower($request->get($this->username())),
|
||||
'password' => $request->password,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the response after the user was authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function sendLoginResponse(Request $request)
|
||||
{
|
||||
$this->clearLoginAttempts($request);
|
||||
|
||||
$token = (string) $this->guard()->getToken();
|
||||
$expiration = $this->guard()->getPayload()->get('exp');
|
||||
|
||||
return response()->json([
|
||||
'token' => $token,
|
||||
'token_type' => 'bearer',
|
||||
'expires_in' => $expiration - time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the failed login response instance.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function sendFailedLoginResponse(Request $request)
|
||||
{
|
||||
$user = $this->guard()->user();
|
||||
if ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()) {
|
||||
throw VerifyEmailException::forUser($user);
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
$this->username() => [trans('auth.failed')],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the user out of the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$this->guard()->logout();
|
||||
}
|
||||
}
|
||||
112
app/Http/Controllers/Auth/OAuthController.php
Normal file
112
app/Http/Controllers/Auth/OAuthController.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Exceptions\EmailTakenException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\OAuthProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
config([
|
||||
'services.github.redirect' => route('oauth.callback', 'github'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the user to the provider authentication page.
|
||||
*
|
||||
* @param string $provider
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function redirect($provider)
|
||||
{
|
||||
return [
|
||||
'url' => Socialite::driver($provider)->stateless()->redirect()->getTargetUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the user information from the provider.
|
||||
*
|
||||
* @param string $driver
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function handleCallback($provider)
|
||||
{
|
||||
$user = Socialite::driver($provider)->stateless()->user();
|
||||
$user = $this->findOrCreateUser($provider, $user);
|
||||
|
||||
$this->guard()->setToken(
|
||||
$token = $this->guard()->login($user)
|
||||
);
|
||||
|
||||
return view('oauth/callback', [
|
||||
'token' => $token,
|
||||
'token_type' => 'bearer',
|
||||
'expires_in' => $this->guard()->getPayload()->get('exp') - time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $provider
|
||||
* @param \Laravel\Socialite\Contracts\User $sUser
|
||||
* @return \App\Models\User
|
||||
*/
|
||||
protected function findOrCreateUser($provider, $user)
|
||||
{
|
||||
$oauthProvider = OAuthProvider::where('provider', $provider)
|
||||
->where('provider_user_id', $user->getId())
|
||||
->first();
|
||||
|
||||
if ($oauthProvider) {
|
||||
$oauthProvider->update([
|
||||
'access_token' => $user->token,
|
||||
'refresh_token' => $user->refreshToken,
|
||||
]);
|
||||
|
||||
return $oauthProvider->user;
|
||||
}
|
||||
|
||||
if (User::where('email', $user->getEmail())->exists()) {
|
||||
throw new EmailTakenException;
|
||||
}
|
||||
|
||||
return $this->createUser($provider, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $provider
|
||||
* @param \Laravel\Socialite\Contracts\User $sUser
|
||||
* @return \App\Models\User
|
||||
*/
|
||||
protected function createUser($provider, $sUser)
|
||||
{
|
||||
$user = User::create([
|
||||
'name' => $sUser->getName(),
|
||||
'email' => $sUser->getEmail(),
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$user->oauthProviders()->create([
|
||||
'provider' => $provider,
|
||||
'provider_user_id' => $sUser->getId(),
|
||||
'access_token' => $sUser->token,
|
||||
'refresh_token' => $sUser->refreshToken,
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
88
app/Http/Controllers/Auth/RegisterController.php
Normal file
88
app/Http/Controllers/Auth/RegisterController.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
use RegistersUsers;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been registered.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \App\User $user
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function registered(Request $request, User $user)
|
||||
{
|
||||
if ($user instanceof MustVerifyEmail) {
|
||||
return response()->json(['status' => trans('verification.sent')]);
|
||||
}
|
||||
|
||||
return response()->json($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|email:filter|max:255|unique:users',
|
||||
'password' => 'required|min:6|confirmed',
|
||||
'hear_about_us' => 'required|string'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \App\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
{
|
||||
$workspace = Workspace::create([
|
||||
'name' => 'My Workspace',
|
||||
'icon' => '🧪',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => strtolower($data['email']),
|
||||
'password' => bcrypt($data['password']),
|
||||
'hear_about_us' => $data['hear_about_us']
|
||||
]);
|
||||
|
||||
// Add relation with user
|
||||
$user->workspaces()->sync([
|
||||
$workspace->id => [
|
||||
'role' => 'admin'
|
||||
]
|
||||
], false);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/Auth/ResetPasswordController.php
Normal file
46
app/Http/Controllers/Auth/ResetPasswordController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
use ResetsPasswords;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a successful password reset.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function sendResetResponse(Request $request, $response)
|
||||
{
|
||||
return ['status' => trans($response)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response for a failed password reset.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $response
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function sendResetFailedResponse(Request $request, $response)
|
||||
{
|
||||
return response()->json(['email' => trans($response)], 400);
|
||||
}
|
||||
}
|
||||
34
app/Http/Controllers/Auth/UserController.php
Normal file
34
app/Http/Controllers/Auth/UserController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get authenticated user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function current(Request $request)
|
||||
{
|
||||
return response()->json($request->user());
|
||||
}
|
||||
|
||||
public function deleteAccount() {
|
||||
$this->middleware('auth');
|
||||
if (Auth::user()->admin) {
|
||||
return $this->error([
|
||||
'message' => 'Cannot delete an admin. Stay with us 🙏'
|
||||
]);
|
||||
}
|
||||
Auth::user()->delete();
|
||||
return $this->success([
|
||||
'message' => 'User deleted.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
82
app/Http/Controllers/Auth/VerificationController.php
Normal file
82
app/Http/Controllers/Auth/VerificationController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VerificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('throttle:6,1')->only('verify', 'resend');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the user's email address as verified.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \App\User $user
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function verify(Request $request, User $user)
|
||||
{
|
||||
if (! URL::hasValidSignature($request)) {
|
||||
return response()->json([
|
||||
'status' => trans('verification.invalid'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
return response()->json([
|
||||
'status' => trans('verification.already_verified'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$user->markEmailAsVerified();
|
||||
|
||||
event(new Verified($user));
|
||||
|
||||
return response()->json([
|
||||
'status' => trans('verification.verified'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the email verification notification.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function resend(Request $request)
|
||||
{
|
||||
$this->validate($request, ['email' => 'required|email']);
|
||||
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (is_null($user)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans('verification.user')],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => [trans('verification.already_verified')],
|
||||
]);
|
||||
}
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
return response()->json(['status' => trans('verification.sent')]);
|
||||
}
|
||||
}
|
||||
22
app/Http/Controllers/Content/ChangelogController.php
Normal file
22
app/Http/Controllers/Content/ChangelogController.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Content;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChangelogController extends Controller
|
||||
{
|
||||
const CANNY_ENDPOINT = 'https://canny.io/api/v1/';
|
||||
|
||||
public function index()
|
||||
{
|
||||
return \Cache::remember('changelog_entries', now()->addHour(), function () {
|
||||
$response = \Http:: post(self::CANNY_ENDPOINT.'entries/list',[
|
||||
'apiKey' => config('services.canny.api_key'),
|
||||
'limit' => 3,
|
||||
]);
|
||||
return $response->json('entries');
|
||||
});
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/Content/SignedStorageUrlController.php
Normal file
46
app/Http/Controllers/Content/SignedStorageUrlController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Content;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Vapor\Http\Controllers\SignedStorageUrlController as Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SignedStorageUrlController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new signed URL.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->ensureEnvironmentVariablesAreAvailable($request);
|
||||
$bucket = $request->input('bucket') ?: $_ENV['AWS_BUCKET'];
|
||||
|
||||
$client = $this->storageClient();
|
||||
|
||||
$uuid = (string) Str::uuid();
|
||||
|
||||
$expiresAfter = config('vapor.signed_storage_url_expires_after', 5);
|
||||
|
||||
$signedRequest = $client->createPresignedRequest(
|
||||
$this->createCommand($request, $client, $bucket, $key = ('tmp/'.$uuid)),
|
||||
sprintf('+%s minutes', $expiresAfter)
|
||||
);
|
||||
|
||||
$uri = $signedRequest->getUri();
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $uuid,
|
||||
'bucket' => $bucket,
|
||||
'key' => $key,
|
||||
'url' => $uri->getScheme().'://'.$uri->getAuthority().$uri->getPath().'?'.$uri->getQuery(),
|
||||
'headers' => $this->headers($request, $signedRequest),
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Controller.php
Normal file
27
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
||||
|
||||
public function success($data = [])
|
||||
{
|
||||
return response()->json(array_merge([
|
||||
'type' => 'success'
|
||||
], $data));
|
||||
}
|
||||
|
||||
public function error($data = [], $statusCode = 400)
|
||||
{
|
||||
return response()->json(array_merge([
|
||||
'type' => 'error'
|
||||
], $data), $statusCode);
|
||||
}
|
||||
}
|
||||
189
app/Http/Controllers/Forms/FormController.php
Normal file
189
app/Http/Controllers/Forms/FormController.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Forms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreFormRequest;
|
||||
use App\Http\Requests\UpdateFormRequest;
|
||||
use App\Http\Requests\UploadAssetRequest;
|
||||
use App\Http\Resources\FormResource;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Workspace;
|
||||
use App\Service\Forms\FormCleaner;
|
||||
use App\Service\Storage\StorageFileNameParser;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class FormController extends Controller
|
||||
{
|
||||
const ASSETS_UPLOAD_PATH = 'assets/forms';
|
||||
|
||||
private FormCleaner $formCleaner;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->formCleaner = new FormCleaner();
|
||||
}
|
||||
|
||||
public function index($workspaceId)
|
||||
{
|
||||
$workspace = Workspace::findOrFail($workspaceId);
|
||||
$this->authorize('view', $workspace);
|
||||
$this->authorize('viewAny', Form::class);
|
||||
|
||||
return FormResource::collection($workspace->forms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all user forms, used for zapier
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function indexAll()
|
||||
{
|
||||
$forms = collect();
|
||||
foreach (Auth::user()->workspaces as $workspace) {
|
||||
$this->authorize('view', $workspace);
|
||||
$this->authorize('viewAny', Form::class);
|
||||
|
||||
$forms = $forms->merge($workspace->forms);
|
||||
}
|
||||
return FormResource::collection($forms);
|
||||
}
|
||||
|
||||
public function store(StoreFormRequest $request)
|
||||
{
|
||||
$this->authorize('create', Form::class);
|
||||
|
||||
$workspace = Workspace::findOrFail($request->get('workspace_id'));
|
||||
$this->authorize('view', $workspace);
|
||||
|
||||
$formData = $this->formCleaner
|
||||
->processRequest($request)
|
||||
->simulateCleaning($workspace)
|
||||
->getData();
|
||||
|
||||
$form = Form::create(array_merge($formData, [
|
||||
'creator_id' => $request->user()->id
|
||||
]));
|
||||
|
||||
return $this->success([
|
||||
'message' => $this->formCleaner->hasCleaned() ? 'Form successfully created, but the Pro features you used will be disabled when sharing your form:' : 'Form created.',
|
||||
'form_cleaning' => $this->formCleaner->getPerformedCleanings(),
|
||||
'form' => new FormResource($form),
|
||||
'users_first_form' => $request->user()->forms()->count() == 1
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateFormRequest $request, string $id)
|
||||
{
|
||||
$form = Form::findOrFail($id);
|
||||
$this->authorize('update', $form);
|
||||
|
||||
$formData = $this->formCleaner
|
||||
->processRequest($request)
|
||||
->simulateCleaning($form->workspace)
|
||||
->getData();
|
||||
|
||||
// Set Removed Properties
|
||||
$formData['removed_properties'] = array_merge($form->removed_properties, collect($form->properties)->filter(function ($field) use ($formData) {
|
||||
return (!Str::of($field['type'])->startsWith('nf-') && !in_array($field['id'], collect($formData['properties'])->pluck("id")->toArray()));
|
||||
})->toArray());
|
||||
|
||||
$form->update($formData);
|
||||
|
||||
return $this->success([
|
||||
'message' => $this->formCleaner->hasCleaned() ? 'Form successfully updated, but the Pro features you used will be disabled when sharing your form:' : 'Form updated.',
|
||||
'form_cleaning' => $this->formCleaner->getPerformedCleanings(),
|
||||
'form' => new FormResource($form)
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$form = Form::findOrFail($id);
|
||||
$this->authorize('delete', $form);
|
||||
|
||||
$form->delete();
|
||||
return $this->success([
|
||||
'message' => 'Form was deleted.'
|
||||
]);
|
||||
}
|
||||
|
||||
public function duplicate($id)
|
||||
{
|
||||
$form = Form::findOrFail($id);
|
||||
$this->authorize('update', $form);
|
||||
|
||||
// Create copy
|
||||
$formCopy = $form->replicate();
|
||||
$formCopy->title = 'Copy of '.$formCopy->title;
|
||||
$formCopy->save();
|
||||
|
||||
return $this->success([
|
||||
'message' => 'Form successfully duplicated.',
|
||||
'new_form' => new FormResource($formCopy)
|
||||
]);
|
||||
}
|
||||
|
||||
public function regenerateLink($id, $option)
|
||||
{
|
||||
$form = Form::findOrFail($id);
|
||||
$this->authorize('update', $form);
|
||||
|
||||
if ( $option == 'slug') {
|
||||
$form->generateSlug();
|
||||
} elseif ($option == 'uuid') {
|
||||
$form->slug = Str::uuid();
|
||||
}
|
||||
$form->save();
|
||||
|
||||
return $this->success([
|
||||
'message' => 'Form url successfully updated. Your new form url now is: '.$form->share_url.'.',
|
||||
'form' => new FormResource($form)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a form asset
|
||||
*/
|
||||
public function uploadAsset(UploadAssetRequest $request)
|
||||
{
|
||||
$this->authorize('viewAny', Form::class);
|
||||
|
||||
$fileNameParser = StorageFileNameParser::parse($request->url);
|
||||
|
||||
// Make sure we retrieve the file in tmp storage, move it to persistent
|
||||
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH.'/'.$fileNameParser->uuid;;
|
||||
if (!Storage::disk('s3')->exists($fileName)) {
|
||||
// File not found, we skip
|
||||
return null;
|
||||
}
|
||||
$newPath = self::ASSETS_UPLOAD_PATH.'/'.$fileNameParser->getMovedFileName();
|
||||
Storage::disk('s3')->move($fileName, $newPath);
|
||||
|
||||
return $this->success([
|
||||
'message' => 'File uploaded.',
|
||||
'url' => route("forms.assets.show", [$fileNameParser->getMovedFileName()])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* File uploads retrieval
|
||||
*/
|
||||
public function viewFile($id, $fileName)
|
||||
{
|
||||
$form = Form::findOrFail($id);
|
||||
$this->authorize('view', $form);
|
||||
|
||||
$path = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $form->id).'/'.$fileName;
|
||||
if (!Storage::disk('s3')->exists($path)) {
|
||||
return $this->error([
|
||||
'message' => 'File not found.'
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to(Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(5)));
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/Forms/FormStatsController.php
Normal file
39
app/Http/Controllers/Forms/FormStatsController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Forms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Forms\Form;
|
||||
use Carbon\CarbonPeriod;
|
||||
use App\Models\Forms\FormStatistic;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FormStatsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getFormStats(Request $request)
|
||||
{
|
||||
$form = $request->form; // Added by ProForm middleware
|
||||
$this->authorize('view', $form);
|
||||
|
||||
$formStats = $form->statistics()->where('date','>',now()->subDays(29)->startOfDay())->get();
|
||||
$periodStats = ["views" => [], "submissions" => []];
|
||||
foreach (CarbonPeriod::create(now()->subDays(29), now()) as $dateObj) {
|
||||
$date = $dateObj->format('d-m-Y');
|
||||
|
||||
$statisticData = $formStats->where('date', $dateObj->format('Y-m-d'))->first();
|
||||
$periodStats["views"][$date] = $statisticData->data["views"] ?? 0;
|
||||
$periodStats["submissions"][$date] = $statisticData->data["submissions"] ?? 0;
|
||||
|
||||
if($dateObj->toDateString() === now()->toDateString()){
|
||||
$periodStats["views"][$date] += $form->views()->count();
|
||||
$periodStats["submissions"][$date] += $form->submissions()->whereDate('created_at', '>=', now()->startOfDay())->count();
|
||||
}
|
||||
}
|
||||
return $periodStats;
|
||||
}
|
||||
}
|
||||
69
app/Http/Controllers/Forms/FormSubmissionController.php
Normal file
69
app/Http/Controllers/Forms/FormSubmissionController.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Forms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\FormSubmissionResource;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Exports\FormSubmissionExport;
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class FormSubmissionController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function submissions(string $id)
|
||||
{
|
||||
$form = Form::findOrFail((int) $id);
|
||||
$this->authorize('view', $form);
|
||||
|
||||
return FormSubmissionResource::collection($form->submissions()->paginate(100));
|
||||
}
|
||||
|
||||
public function export(string $id)
|
||||
{
|
||||
$form = Form::findOrFail((int) $id);
|
||||
$this->authorize('view', $form);
|
||||
|
||||
$allRows = [];
|
||||
foreach ($form->submissions->toArray() as $row) {
|
||||
$formatter = (new FormSubmissionFormatter($form, $row['data']))
|
||||
->outputStringsOnly()
|
||||
->setEmptyForNoValue();
|
||||
$tmp = $formatter->getCleanKeyValue();
|
||||
$tmp['Create Date'] = date("Y-m-d H:i", strtotime($row['created_at']));
|
||||
$allRows[] = $tmp;
|
||||
}
|
||||
$csvExport = (new FormSubmissionExport($allRows));
|
||||
return Excel::download(
|
||||
$csvExport,
|
||||
$form->slug.'-submission-data.csv',
|
||||
\Maatwebsite\Excel\Excel::CSV
|
||||
);
|
||||
}
|
||||
|
||||
public function submissionFile($id, $fileName)
|
||||
{
|
||||
$form = Form::findOrFail((int) $id);
|
||||
$this->authorize('view', $form);
|
||||
|
||||
$fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id).'/'
|
||||
.urldecode($fileName);
|
||||
|
||||
if (!Storage::disk('s3')->exists($fileName)) {
|
||||
return $this->error([
|
||||
'message' => 'File not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return redirect(
|
||||
Storage::disk('s3')->temporaryUrl($fileName, now()->addMinute())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Forms\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Integration\StoreFormZapierWebhookRequest;
|
||||
use App\Models\Integration\FormZapierWebhook;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\WebhookServer\WebhookCall;
|
||||
|
||||
class FormZapierWebhookController extends Controller
|
||||
{
|
||||
/**
|
||||
* Controller for Zappier webhook subscriptions.
|
||||
*/
|
||||
public function __construct() {
|
||||
// $this->middleware('subscribed');
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(StoreFormZapierWebhookRequest $request) {
|
||||
$hook = $request->instanciateHook();
|
||||
$this->authorize('store', $hook);
|
||||
|
||||
$hook->save();
|
||||
return $this->success([
|
||||
'message' => 'Webhook created.',
|
||||
'hook' => $hook
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$hook = FormZapierWebhook::findOrFail($id);
|
||||
$this->authorize('store', $hook);
|
||||
|
||||
$hook->delete();
|
||||
return $this->success([
|
||||
'message' => 'Webhook deleted.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
91
app/Http/Controllers/Forms/PublicFormController.php
Normal file
91
app/Http/Controllers/Forms/PublicFormController.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Forms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AnswerFormRequest;
|
||||
use App\Http\Resources\FormResource;
|
||||
use App\Jobs\Form\StoreFormSubmissionJob;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Service\Forms\FormCleaner;
|
||||
use App\Service\WorkspaceHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class PublicFormController extends Controller
|
||||
{
|
||||
|
||||
const FILE_UPLOAD_PATH = 'forms/?/submissions';
|
||||
const TMP_FILE_UPLOAD_PATH = 'tmp/';
|
||||
|
||||
public function show(Request $request, string $slug)
|
||||
{
|
||||
$form = Form::whereSlug($slug)->firstOrFail();
|
||||
if ($form->workspace == null) {
|
||||
// Workspace deleted
|
||||
return $this->error([
|
||||
'message' => 'Form not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$formCleaner = new FormCleaner();
|
||||
|
||||
// Disable pro features if needed
|
||||
$form->fill($formCleaner
|
||||
->processForm($request, $form)
|
||||
->performCleaning($form->workspace)
|
||||
->getData()
|
||||
);
|
||||
|
||||
// Increase form view counter if not login
|
||||
if(!Auth::check()){
|
||||
$form->views()->create();
|
||||
}
|
||||
|
||||
$formResource = new FormResource($form);
|
||||
$formResource->setCleanings($formCleaner->getPerformedCleanings());
|
||||
return $formResource;
|
||||
}
|
||||
|
||||
public function listUsers(Request $request)
|
||||
{
|
||||
// Check that form has user field
|
||||
$form = $request->form;
|
||||
if (!$form->has_user_field) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use serializer
|
||||
$workspace = $form->workspace;
|
||||
return (new WorkspaceHelper($workspace))->getAllUsers();
|
||||
}
|
||||
|
||||
public function showAsset($assetFileName)
|
||||
{
|
||||
$path = FormController::ASSETS_UPLOAD_PATH.'/'.$assetFileName;
|
||||
if (!Storage::disk('s3')->exists($path)) {
|
||||
return $this->error([
|
||||
'message' => 'File not found.',
|
||||
'file_name' => $assetFileName
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to(Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(5)));
|
||||
}
|
||||
|
||||
public function answer(AnswerFormRequest $request)
|
||||
{
|
||||
$form = $request->form;
|
||||
|
||||
StoreFormSubmissionJob::dispatch($form, $request->validated());
|
||||
return $this->success(array_merge([
|
||||
'message' => 'Form submission saved.',
|
||||
], $request->form->is_pro && $request->form->redirect_url ? [
|
||||
'redirect' => true,
|
||||
'redirect_url' => $request->form->redirect_url
|
||||
] : [
|
||||
'redirect' => false
|
||||
]));
|
||||
}
|
||||
}
|
||||
28
app/Http/Controllers/Settings/PasswordController.php
Normal file
28
app/Http/Controllers/Settings/PasswordController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'password' => 'required|confirmed|min:6',
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => bcrypt($request->password),
|
||||
]);
|
||||
|
||||
return response()->json(null, 204);
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Settings/ProfileController.php
Normal file
30
app/Http/Controllers/Settings/ProfileController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email,'.$user->id,
|
||||
]);
|
||||
|
||||
return tap($user)->update([
|
||||
'name' => $request->name,
|
||||
'email' => strtolower($request->email),
|
||||
]);
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/SitemapController.php
Normal file
39
app/Http/Controllers/SitemapController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Sitemap\Sitemap;
|
||||
use Spatie\Sitemap\Tags\Url;
|
||||
|
||||
class SitemapController extends Controller
|
||||
{
|
||||
/**
|
||||
* Contains route name and the associated priority
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $urls = [
|
||||
['/', 1],
|
||||
['/privacy-policy', 0.5],
|
||||
['/terms-conditions', 0.5],
|
||||
['/login', 0.4],
|
||||
['/register', 0.4],
|
||||
['/password/reset', 0.3],
|
||||
];
|
||||
|
||||
public function getSitemap(Request $request)
|
||||
{
|
||||
$sitemap = Sitemap::create();
|
||||
foreach ($this->urls as $url) {
|
||||
$sitemap->add($this->createUrl($url[0], $url[1]));
|
||||
}
|
||||
|
||||
return $sitemap->toResponse($request);
|
||||
}
|
||||
|
||||
private function createUrl($url, $priority, $frequency = 'daily')
|
||||
{
|
||||
return Url::create($url)->setPriority($priority)->setChangeFrequency($frequency);
|
||||
}
|
||||
}
|
||||
16
app/Http/Controllers/SpaController.php
Normal file
16
app/Http/Controllers/SpaController.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
class SpaController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get the SPA view.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
return view('spa');
|
||||
}
|
||||
}
|
||||
76
app/Http/Controllers/SubscriptionController.php
Normal file
76
app/Http/Controllers/SubscriptionController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Cashier\Subscription;
|
||||
|
||||
class SubscriptionController extends Controller
|
||||
{
|
||||
const SUBSCRIPTION_PLANS = ['monthly_2022', 'yearly_2022'];
|
||||
|
||||
const PRO_SUBSCRIPTION_NAME = 'default';
|
||||
const ENTERPRISE_SUBSCRIPTION_NAME = 'enterprise';
|
||||
|
||||
const SUBSCRIPTION_NAMES = [
|
||||
self::PRO_SUBSCRIPTION_NAME,
|
||||
self::ENTERPRISE_SUBSCRIPTION_NAME
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns stripe checkout URL
|
||||
*
|
||||
* $plan is constrained with regex in the api.php
|
||||
*/
|
||||
public function checkout($pricing, $plan, $trial = null)
|
||||
{
|
||||
$this->middleware('not-subscribed');
|
||||
|
||||
// Check User does not have a pending subscription
|
||||
$user = Auth::user();
|
||||
if ($user->subscriptions()->where('stripe_status', 'past_due')->first()) {
|
||||
return $this->error([
|
||||
'message' => 'You already have a past due subscription. Please verify your details in the billing page,
|
||||
and contact us if the issue persists.'
|
||||
]);
|
||||
}
|
||||
|
||||
$checkoutBuilder = $user
|
||||
->newSubscription($pricing, $this->getPricing($pricing)[$plan])
|
||||
->allowPromotionCodes();
|
||||
|
||||
if ($trial != null) {
|
||||
$checkoutBuilder->trialDays(3);
|
||||
}
|
||||
|
||||
$checkout = $checkoutBuilder
|
||||
->collectTaxIds()
|
||||
->checkout([
|
||||
'success_url' => url('/subscriptions/success'),
|
||||
'cancel_url' => url('/subscriptions/error'),
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'checkout_url' => $checkout->url
|
||||
]);
|
||||
}
|
||||
|
||||
public function billingPortal()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
if (!Auth::user()->has_customer_id) {
|
||||
return $this->error([
|
||||
"message" => "Please subscribe before accessing your billing portal."
|
||||
]);
|
||||
}
|
||||
return $this->success([
|
||||
'portal_url' => Auth::user()->billingPortalUrl(url('/home'))
|
||||
]);
|
||||
}
|
||||
|
||||
private function getPricing($product = 'pro')
|
||||
{
|
||||
return App::environment() == 'production' ? config('pricing.production.'.$product.'.pricing') : config('pricing.test.'.$product.'.pricing');
|
||||
}
|
||||
}
|
||||
131
app/Http/Controllers/Webhook/StripeController.php
Normal file
131
app/Http/Controllers/Webhook/StripeController.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Notifications\Subscription\FailedPaymentNotification;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Cashier\Http\Controllers\WebhookController;
|
||||
use Stripe\Subscription as StripeSubscription;
|
||||
|
||||
class StripeController extends WebhookController
|
||||
{
|
||||
|
||||
public function handleCustomerSubscriptionCreated(array $payload)
|
||||
{
|
||||
return parent::handleCustomerSubscriptionCreated($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to add a sleep, and to detect plan upgrades
|
||||
* @param array $payload
|
||||
* @return \Symfony\Component\HttpFoundation\Response|void
|
||||
*/
|
||||
protected function handleCustomerSubscriptionUpdated(array $payload)
|
||||
{
|
||||
sleep(1);
|
||||
|
||||
if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
|
||||
$data = $payload['data']['object'];
|
||||
|
||||
$subscription = $user->subscriptions()->firstOrNew(['stripe_id' => $data['id']]);
|
||||
|
||||
if (
|
||||
isset($data['status']) &&
|
||||
$data['status'] === StripeSubscription::STATUS_INCOMPLETE_EXPIRED
|
||||
) {
|
||||
$subscription->items()->delete();
|
||||
$subscription->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription->name = $subscription->name ?? $data['metadata']['name'] ?? $this->newSubscriptionName($payload);
|
||||
|
||||
$firstItem = $data['items']['data'][0];
|
||||
$isSinglePrice = count($data['items']['data']) === 1;
|
||||
|
||||
// Price...
|
||||
$subscription->stripe_price = $isSinglePrice ? $firstItem['price']['id'] : null;
|
||||
|
||||
// Name...
|
||||
$subscription->name = $this->getSubscriptionName($data['plan']['product']);
|
||||
|
||||
// Quantity...
|
||||
$subscription->quantity = $isSinglePrice && isset($firstItem['quantity']) ? $firstItem['quantity'] : null;
|
||||
|
||||
// Trial ending date...
|
||||
if (isset($data['trial_end'])) {
|
||||
$trialEnd = Carbon::createFromTimestamp($data['trial_end']);
|
||||
|
||||
if (!$subscription->trial_ends_at || $subscription->trial_ends_at->ne($trialEnd)) {
|
||||
$subscription->trial_ends_at = $trialEnd;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancellation date...
|
||||
if (isset($data['cancel_at_period_end'])) {
|
||||
if ($data['cancel_at_period_end']) {
|
||||
$subscription->ends_at = $subscription->onTrial()
|
||||
? $subscription->trial_ends_at
|
||||
: Carbon::createFromTimestamp($data['current_period_end']);
|
||||
} elseif (isset($data['cancel_at'])) {
|
||||
$subscription->ends_at = Carbon::createFromTimestamp($data['cancel_at']);
|
||||
} else {
|
||||
$subscription->ends_at = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Status...
|
||||
if (isset($data['status'])) {
|
||||
$subscription->stripe_status = $data['status'];
|
||||
}
|
||||
|
||||
$subscription->save();
|
||||
|
||||
// Update subscription items...
|
||||
if (isset($data['items'])) {
|
||||
$prices = [];
|
||||
|
||||
foreach ($data['items']['data'] as $item) {
|
||||
$prices[] = $item['price']['id'];
|
||||
|
||||
$subscription->items()->updateOrCreate([
|
||||
'stripe_id' => $item['id'],
|
||||
], [
|
||||
'stripe_product' => $item['price']['product'],
|
||||
'stripe_price' => $item['price']['id'],
|
||||
'quantity' => $item['quantity'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
// Delete items that aren't attached to the subscription anymore...
|
||||
$subscription->items()->whereNotIn('stripe_price', $prices)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->successMethod();
|
||||
}
|
||||
|
||||
protected function handleChargeFailed(array $payload)
|
||||
{
|
||||
if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
|
||||
$user->notify(new FailedPaymentNotification());
|
||||
}
|
||||
|
||||
return $this->successMethod();
|
||||
}
|
||||
|
||||
private function getSubscriptionName(string $stripeProductId)
|
||||
{
|
||||
$config = App::environment() == 'production' ? config('pricing.production') : config('pricing.test');
|
||||
foreach ($config as $plan => $data) {
|
||||
if ($stripeProductId == $config[$plan]['product_id']) {
|
||||
return $plan;
|
||||
}
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
}
|
||||
71
app/Http/Controllers/WorkspaceController.php
Normal file
71
app/Http/Controllers/WorkspaceController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Service\WorkspaceHelper;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkspaceController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('viewAny', Workspace::class);
|
||||
return Auth::user()->workspaces;
|
||||
}
|
||||
|
||||
public function listUsers(Request $request, $workspaceId)
|
||||
{
|
||||
$workspace = Workspace::findOrFail($workspaceId);
|
||||
$this->authorize('view', $workspace);
|
||||
|
||||
return (new WorkspaceHelper($workspace))->getAllUsers();
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$workspace = Workspace::findOrFail($id);
|
||||
$this->authorize('delete', $workspace);
|
||||
|
||||
$id = $workspace->id;
|
||||
$workspace->delete();
|
||||
return $this->success([
|
||||
'message' => 'Workspace deleted.',
|
||||
'workspace_id' => $id
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required'
|
||||
]);
|
||||
|
||||
// Create workspace
|
||||
$workspace = Workspace::create([
|
||||
'name' => $request->name,
|
||||
'icon' => ($request->emoji) ? $request->emoji : '',
|
||||
]);
|
||||
|
||||
// Add relation with user
|
||||
$user->workspaces()->sync([
|
||||
$workspace->id => [
|
||||
'role' => 'admin'
|
||||
]
|
||||
], false);
|
||||
|
||||
return $this->success([
|
||||
'message' => 'Workspace created.',
|
||||
'workspace_id' => $workspace->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
85
app/Http/Kernel.php
Normal file
85
app/Http/Kernel.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Http\Middleware\EmbeddableForms;
|
||||
use App\Http\Middleware\IsAdmin;
|
||||
use App\Http\Middleware\IsNotSubscribed;
|
||||
use App\Http\Middleware\IsSubscribed;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
\App\Http\Middleware\SetLocale::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
EmbeddableForms::class
|
||||
],
|
||||
|
||||
'spa' => [
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
EmbeddableForms::class
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'admin' => IsAdmin::class,
|
||||
'subscribed' => IsSubscribed::class,
|
||||
'not-subscribed' => IsNotSubscribed::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
|
||||
'pro-form' => \App\Http\Middleware\Form\ProForm::class,
|
||||
'password-protected-form' => \App\Http\Middleware\Form\PasswordProtectedForm::class,
|
||||
];
|
||||
}
|
||||
21
app/Http/Middleware/Authenticate.php
Normal file
21
app/Http/Middleware/Authenticate.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return string|null
|
||||
*/
|
||||
protected function redirectTo($request)
|
||||
{
|
||||
if (! $request->expectsJson()) {
|
||||
return redirect('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/Http/Middleware/EmbeddableForms.php
Normal file
36
app/Http/Middleware/EmbeddableForms.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class EmbeddableForms
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($request->expectsJson() || $request->wantsJson()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
if (!str_starts_with($request->url(), url('/forms/'))) {
|
||||
if ($response instanceof Response) {
|
||||
$response->header('X-Frame-Options', 'SAMEORIGIN');
|
||||
} elseif ($response instanceof \Symfony\Component\HttpFoundation\Response) {
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/EncryptCookies.php
Normal file
17
app/Http/Middleware/EncryptCookies.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
47
app/Http/Middleware/Form/PasswordProtectedForm.php
Normal file
47
app/Http/Middleware/Form/PasswordProtectedForm.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware\Form;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class PasswordProtectedForm
|
||||
{
|
||||
const PASSWORD_HEADER_NAME = 'form-password';
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
|
||||
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->route('slug')) {
|
||||
$form = Form::where('slug',$request->route('slug'))->firstOrFail();
|
||||
$request->merge([
|
||||
'form' => $form,
|
||||
]);
|
||||
$userIsFormOwner = Auth::check() && Auth::user()->workspaces()->find($form->workspace_id) !== null;
|
||||
if (!$userIsFormOwner && $form->is_pro && $form->has_password) {
|
||||
if($this->hasCorrectPassword($request, $form)){
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return response([
|
||||
'status' => 'Unauthorized',
|
||||
'message' => 'Form is password protected.',
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
public static function hasCorrectPassword(Request $request, Form $form)
|
||||
{
|
||||
return $request->headers->has(self::PASSWORD_HEADER_NAME) && $request->headers->get(self::PASSWORD_HEADER_NAME) == hash('sha256', $form->password);
|
||||
}
|
||||
}
|
||||
34
app/Http/Middleware/Form/ProForm.php
Normal file
34
app/Http/Middleware/Form/ProForm.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware\Form;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProForm
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
|
||||
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->route('formId') && $form = Form::findOrFail($request->route('formId'))) {
|
||||
if ($form->is_pro) {
|
||||
$request->merge([
|
||||
'form' => $form,
|
||||
]);
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
return response([
|
||||
'status' => 'Unauthorized',
|
||||
'message' => 'You need a subscription to access this content.',
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
32
app/Http/Middleware/IsAdmin.php
Normal file
32
app/Http/Middleware/IsAdmin.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class IsAdmin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->user() && !$request->user()->admin) {
|
||||
// This user is not a paying customer...
|
||||
if ($request->expectsJson()) {
|
||||
return response([
|
||||
'message' => 'You are not allowed.',
|
||||
'type' => 'error',
|
||||
], 403);
|
||||
}
|
||||
return redirect('home');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
32
app/Http/Middleware/IsNotSubscribed.php
Normal file
32
app/Http/Middleware/IsNotSubscribed.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class IsNotSubscribed
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->user() && $request->user()->subscribed()) {
|
||||
// This user is a paying customer...
|
||||
if ($request->expectsJson()) {
|
||||
return response([
|
||||
'message' => 'You are already subscribed to NotionForms Pro.',
|
||||
'type' => 'error',
|
||||
], 401);
|
||||
}
|
||||
return redirect('billing');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
32
app/Http/Middleware/IsSubscribed.php
Normal file
32
app/Http/Middleware/IsSubscribed.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class IsSubscribed
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($request->user() && !$request->user()->subscribed()) {
|
||||
// This user is not a paying customer...
|
||||
if ($request->expectsJson()) {
|
||||
return response([
|
||||
'message' => 'You are not subscribed to NotionForms Pro.',
|
||||
'type' => 'error',
|
||||
], 401);
|
||||
}
|
||||
return redirect('billing');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
||||
|
||||
class PreventRequestsDuringMaintenance extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be reachable while maintenance mode is enabled.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
36
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
36
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string|null ...$guards
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ...$guards)
|
||||
{
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['error' => 'Already authenticated.'], 400);
|
||||
} else {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
45
app/Http/Middleware/SetLocale.php
Normal file
45
app/Http/Middleware/SetLocale.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($locale = $this->parseLocale($request)) {
|
||||
app()->setLocale($locale);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return string|null
|
||||
*/
|
||||
protected function parseLocale($request)
|
||||
{
|
||||
$locales = config('app.locales');
|
||||
|
||||
$locale = $request->server('HTTP_ACCEPT_LANGUAGE');
|
||||
$locale = substr($locale, 0, strpos($locale, ',') ?: strlen($locale));
|
||||
|
||||
if (array_key_exists($locale, $locales)) {
|
||||
return $locale;
|
||||
}
|
||||
|
||||
$locale = substr($locale, 0, 2);
|
||||
if (array_key_exists($locale, $locales)) {
|
||||
return $locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
app/Http/Middleware/TrimStrings.php
Normal file
41
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* The route name where this shouldn't be applied
|
||||
* @var string[]
|
||||
*/
|
||||
protected $exceptUrls = [
|
||||
'/\/api\/forms\/(.*)\/answer/'
|
||||
];
|
||||
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
// Check if URL matches
|
||||
foreach ($this->exceptUrls as $urlRegex) {
|
||||
$matches = null;
|
||||
preg_match($urlRegex, $request->url(), $matches);
|
||||
|
||||
if (count($matches)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::handle($request, $next);
|
||||
}
|
||||
}
|
||||
20
app/Http/Middleware/TrustHosts.php
Normal file
20
app/Http/Middleware/TrustHosts.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function hosts()
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Middleware/TrustProxies.php
Normal file
28
app/Http/Middleware/TrustProxies.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array|string|null
|
||||
*/
|
||||
protected $proxies;
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
18
app/Http/Middleware/VerifyCsrfToken.php
Normal file
18
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'stripe/webhook',
|
||||
'vapor/signed-storage-url'
|
||||
];
|
||||
}
|
||||
203
app/Http/Requests/AnswerFormRequest.php
Normal file
203
app/Http/Requests/AnswerFormRequest.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
|
||||
use App\Rules\StorageFile;
|
||||
use App\Service\Forms\FormLogicPropertyResolver;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Rules\ValidHCaptcha;
|
||||
|
||||
class AnswerFormRequest extends FormRequest
|
||||
{
|
||||
const MAX_FILE_SIZE_PRO = 5000000;
|
||||
const MAX_FILE_SIZE_ENTERPRISE = 20000000;
|
||||
|
||||
public Form $form;
|
||||
|
||||
protected array $requestRules = [];
|
||||
protected int $maxFileSize;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->form = $request->form;
|
||||
|
||||
$this->maxFileSize = self::MAX_FILE_SIZE_PRO;
|
||||
$workspace = $this->form->workspace;
|
||||
if ($workspace && $workspace->is_enterprise) {
|
||||
$this->maxFileSize = self::MAX_FILE_SIZE_ENTERPRISE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form before use it
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return !$this->form->is_closed && !$this->form->max_number_of_submissions_reached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the form.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
foreach ($this->form->properties as $property) {
|
||||
$rules = [];
|
||||
|
||||
if (!$this->form->is_pro) { // If not pro then not check logic
|
||||
$property['logic'] = false;
|
||||
}
|
||||
|
||||
// For get values instead of Id for select/multi select options
|
||||
$data = $this->toArray();
|
||||
$selectionFields = collect($this->form->properties)->filter(function ($pro) {
|
||||
return in_array($pro['type'], ['select', 'multi_select']);
|
||||
});
|
||||
foreach ($selectionFields as $field){
|
||||
if(isset($data[$field['id']]) && is_array($data[$field['id']])){
|
||||
$data[$field['id']] = array_map(function ($val) use ($field) {
|
||||
$tmpop = collect($field[$field['type']]['options'])->first(function ($op) use ($val) {
|
||||
return ($op['id'] === $val);
|
||||
});
|
||||
return isset($tmpop['name']) ? $tmpop['name'] : "";
|
||||
}, $data[$field['id']]);
|
||||
}
|
||||
};
|
||||
if (FormLogicPropertyResolver::isRequired($property, $data)) {
|
||||
$rules[] = 'required';
|
||||
|
||||
// Required for checkboxes means true
|
||||
if ($property['type'] == 'checkbox') {
|
||||
$rules[] = 'accepted';
|
||||
}
|
||||
} else {
|
||||
$rules[] = 'nullable';
|
||||
}
|
||||
|
||||
// Clean id to escape "."
|
||||
$propertyId = $property['id'];
|
||||
if (in_array($property['type'], ['multi_select'])) {
|
||||
$rules[] = 'array';
|
||||
$this->requestRules[$propertyId.'.*'] = $this->getPropertyRules($property);
|
||||
} else {
|
||||
$rules = array_merge($rules, $this->getPropertyRules($property));
|
||||
}
|
||||
|
||||
$this->requestRules[$propertyId] = $rules;
|
||||
}
|
||||
|
||||
// Validate hCaptcha
|
||||
if ($this->form->is_pro && $this->form->use_captcha) {
|
||||
$this->requestRules['h-captcha-response'] = [new ValidHCaptcha()];
|
||||
}
|
||||
return $this->requestRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames validated fields (because field names are ids)
|
||||
* @return array
|
||||
*/
|
||||
public function attributes()
|
||||
{
|
||||
$fields = [];
|
||||
foreach ($this->form->properties as $property) {
|
||||
$fields[$property['id']] = $property['name'];
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return validation rules for a given form property
|
||||
* @param $property
|
||||
*/
|
||||
private function getPropertyRules($property): array
|
||||
{
|
||||
switch ($property['type']) {
|
||||
case 'text':
|
||||
case 'phone_number':
|
||||
return ['string'];
|
||||
case 'number':
|
||||
return ['numeric'];
|
||||
case 'select':
|
||||
case 'multi_select':
|
||||
if ($this->form->is_pro && ($property['allow_creation'] ?? false)) {
|
||||
return ['string'];
|
||||
}
|
||||
return [Rule::in($this->getSelectPropertyOptions($property))];
|
||||
case 'checkbox':
|
||||
return ['boolean'];
|
||||
case 'url':
|
||||
if (isset($property['file_upload']) && $property['file_upload']) {
|
||||
$this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize)];
|
||||
return ['array'];
|
||||
}
|
||||
return ['url'];
|
||||
case 'files':
|
||||
$allowedFileTypes = [];
|
||||
if($this->form->is_pro && !empty($property['allowed_file_types'])){
|
||||
$allowedFileTypes = explode(",", $property['allowed_file_types']);
|
||||
}
|
||||
$this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, $allowedFileTypes)];
|
||||
return ['array'];
|
||||
case 'email':
|
||||
return ['email:filter'];
|
||||
case 'date':
|
||||
if (isset($property['date_range']) && $property['date_range']) {
|
||||
$this->requestRules[$property['id'].'.*'] = ['date'];
|
||||
return ['array'];
|
||||
}
|
||||
return ['date'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function getSelectPropertyOptions($property): array
|
||||
{
|
||||
$type = $property['type'];
|
||||
if (!isset($property[$type])) {
|
||||
return [];
|
||||
}
|
||||
return array_column($property[$type]['options'], 'name');
|
||||
}
|
||||
|
||||
protected function prepareForValidation()
|
||||
{
|
||||
// Escape all '\' in select options
|
||||
$receivedData = $this->toArray();
|
||||
$mergeData = [];
|
||||
collect($this->form->properties)->filter(function ($property) {
|
||||
return in_array($property['type'], ['select', 'multi_select']);
|
||||
})->each(function ($property) use ($receivedData, &$mergeData) {
|
||||
$receivedValue = $receivedData[$property['id']] ?? null;
|
||||
if (!is_null($receivedValue)) {
|
||||
if (is_array($receivedValue)) {
|
||||
$mergeData[$property['id']] = collect($receivedValue)->map(function ($value) {
|
||||
$value = Str::of($value);
|
||||
return $value->replace(
|
||||
["\e", "\f", "\n", "\r", "\t", "\v", "\\"],
|
||||
["\\e", "\\f", "\\n", "\\r", "\\t", "\\v", "\\\\"]
|
||||
)->toString();
|
||||
})->toArray();
|
||||
} else {
|
||||
$receivedValue = Str::of($receivedValue);
|
||||
$mergeData[$property['id']] = $receivedValue->replace(
|
||||
["\e", "\f", "\n", "\r", "\t", "\v", "\\"],
|
||||
["\\e", "\\f", "\\n", "\\r", "\\t", "\\v", "\\\\"]
|
||||
)->toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->merge($mergeData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Integration;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Integration\FormZapierWebhook;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreFormZapierWebhookRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'form_slug' => 'required|exists:forms,slug',
|
||||
'hook_url' => 'required|string|url'
|
||||
];
|
||||
}
|
||||
|
||||
public function instanciateHook() {
|
||||
$form = Form::whereSlug($this->form_slug)->firstOrFail();
|
||||
return new FormZapierWebhook([
|
||||
'form_id' => $form->id,
|
||||
'hook_url' => $this->hook_url,
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
app/Http/Requests/StoreFormRequest.php
Normal file
21
app/Http/Requests/StoreFormRequest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreFormRequest extends UserFormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return array_merge(parent::rules(), [// Info about database
|
||||
'workspace_id' => 'required|exists:workspaces,id',
|
||||
]);
|
||||
}
|
||||
}
|
||||
8
app/Http/Requests/UpdateFormRequest.php
Normal file
8
app/Http/Requests/UpdateFormRequest.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class UpdateFormRequest extends UserFormRequest
|
||||
{
|
||||
|
||||
}
|
||||
29
app/Http/Requests/UploadAssetRequest.php
Normal file
29
app/Http/Requests/UploadAssetRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Rules\StorageFile;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UploadAssetRequest extends FormRequest
|
||||
{
|
||||
const FORM_ASSET_MAX_SIZE = 5000000;
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'url' => ['required',new StorageFile(self::FORM_ASSET_MAX_SIZE, [
|
||||
'png',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'bmp',
|
||||
'gif'
|
||||
])],
|
||||
];
|
||||
}
|
||||
}
|
||||
129
app/Http/Requests/UserFormRequest.php
Normal file
129
app/Http/Requests/UserFormRequest.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Rules\OneEmailPerLine;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Rules\FormPropertyLogicRule;
|
||||
|
||||
/**
|
||||
* Abstract class to validate create/update forms
|
||||
*
|
||||
* Class UserFormRequest
|
||||
* @package App\Http\Requests
|
||||
*/
|
||||
abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
// Form Info
|
||||
'title' => 'required|string|max:60',
|
||||
'description' => 'nullable|string|max:2000',
|
||||
'tags' => 'nullable|array',
|
||||
|
||||
// Notifications
|
||||
'notifies' => 'boolean',
|
||||
'notification_emails' => ['required_if:notifies,1', new OneEmailPerLine ],
|
||||
'send_submission_confirmation' => 'boolean',
|
||||
'notification_sender' => 'string|max:64',
|
||||
'notification_subject' => 'string|max:200',
|
||||
'notification_body' => 'string|nullable',
|
||||
'notifications_include_submission' => 'boolean',
|
||||
'webhook_url' => 'url|nullable',
|
||||
'use_captcha' => 'boolean',
|
||||
|
||||
// Customization
|
||||
'theme' => ['required',Rule::in(Form::THEMES)],
|
||||
'width' => ['required',Rule::in(Form::WIDTHS)],
|
||||
'cover_picture' => 'url|nullable',
|
||||
'logo_picture' => 'url|nullable',
|
||||
'dark_mode' => ['required',Rule::in(Form::DARK_MODE_VALUES)],
|
||||
'color' => 'required|string',
|
||||
'hide_title' => 'required|boolean',
|
||||
'uppercase_labels' => 'required|boolean',
|
||||
'no_branding' => 'required|boolean',
|
||||
'transparent_background' => 'required|boolean',
|
||||
'closes_at' => 'date|nullable',
|
||||
'closed_text' => 'string|nullable',
|
||||
|
||||
// Custom Code
|
||||
'custom_code' => 'string|nullable',
|
||||
|
||||
// Submission
|
||||
'submit_button_text' => 'string|min:1|max:50',
|
||||
're_fillable' => 'boolean',
|
||||
're_fill_button_text' => 'string|min:1|max:50',
|
||||
'submitted_text' => 'string|max:2000',
|
||||
'redirect_url' => 'nullable|active_url|max:255',
|
||||
'database_fields_update' => 'nullable|array',
|
||||
'max_submissions_count' => 'integer|nullable|min:1',
|
||||
'max_submissions_reached_text' => 'string|nullable',
|
||||
|
||||
// Properties
|
||||
'properties' => 'required|array',
|
||||
'properties.*.id' => 'required',
|
||||
'properties.*.name' => 'required',
|
||||
'properties.*.type' => 'required',
|
||||
'properties.*.placeholder' => 'sometimes|nullable',
|
||||
'properties.*.prefill' => 'sometimes|nullable',
|
||||
'properties.*.help' => 'sometimes|nullable',
|
||||
'properties.*.hidden' => 'boolean|nullable',
|
||||
'properties.*.required' => 'boolean|nullable',
|
||||
'properties.*.multiple' => 'boolean|nullable',
|
||||
'properties.*.timezone' => 'sometimes|nullable',
|
||||
'properties.*.width' => ['sometimes', Rule::in(['full','1/2','1/3','2/3','1/3','3/4','1/4'])],
|
||||
'properties.*.allowed_file_types' => 'sometimes|nullable',
|
||||
|
||||
// Logic
|
||||
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()],
|
||||
|
||||
// Form blocks
|
||||
'properties.*.content' => 'sometimes|nullable',
|
||||
|
||||
// Text field
|
||||
'properties.*.multi_lines' => 'boolean|nullable',
|
||||
'properties.*.max_char_limit' => 'integer|nullable|min:1|max:2000',
|
||||
'properties.*.show_char_limit ' => 'boolean|nullable',
|
||||
|
||||
// Date field
|
||||
'properties.*.with_time' => 'boolean|nullable',
|
||||
'properties.*.date_range' => 'boolean|nullable',
|
||||
|
||||
// Select / Multi Select field
|
||||
'properties.*.allow_creation' => 'boolean|nullable',
|
||||
'properties.*.without_dropdown' => 'boolean|nullable',
|
||||
|
||||
// Advanced Options
|
||||
'properties.*.generates_uuid' => 'boolean|nullable',
|
||||
'properties.*.generates_auto_increment_id' => 'boolean|nullable',
|
||||
|
||||
// Security & Privacy
|
||||
'can_be_indexed' => 'boolean',
|
||||
'password' => 'sometimes|nullable',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation messages that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'properties.*.name.required' => 'The form block number :position is missing a name.',
|
||||
'properties.*.type.required' => 'The form block number :position is missing a type.',
|
||||
'properties.*.max_char_limit.min' => 'The form block number :position max character limit must be at least 1 OR Empty',
|
||||
'properties.*.max_char_limit.max' => 'The form block number :position max character limit may not be greater than 2000.',
|
||||
];
|
||||
}
|
||||
}
|
||||
110
app/Http/Resources/FormResource.php
Normal file
110
app/Http/Resources/FormResource.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Http\Middleware\Form\PasswordProtectedForm;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FormResource extends JsonResource
|
||||
{
|
||||
private Array $cleanings = [];
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$userIsFormOwner = Auth::check() && Auth::user()->workspaces()->find($this->workspace_id) !== null;
|
||||
if(!$userIsFormOwner && $this->doesMissPassword($request)){
|
||||
return $this->getPasswordProtectedForm();
|
||||
}
|
||||
|
||||
$ownerData = $userIsFormOwner ? [
|
||||
'creator' => $this->creator,
|
||||
'views_count' => $this->when($this->workspace->is_pro, $this->views_count),
|
||||
'submissions_count' => $this->when($this->workspace->is_pro, $this->submissions_count),
|
||||
'notifies' => $this->notifies,
|
||||
'send_submission_confirmation' => $this->send_submission_confirmation,
|
||||
'webhook_url' => $this->webhook_url,
|
||||
'redirect_url' => $this->redirect_url,
|
||||
'database_fields_update' => $this->database_fields_update,
|
||||
'cleanings' => $this->cleanings,
|
||||
'notification_sender' => $this->notification_sender,
|
||||
'notification_subject' => $this->notification_subject,
|
||||
'notification_body' => $this->notification_body,
|
||||
'notifications_include_submission' => $this->notifications_include_submission,
|
||||
'can_be_indexed' => $this->can_be_indexed,
|
||||
'password' => $this->password,
|
||||
'tags' => $this->tags,
|
||||
'notification_emails' => $this->notification_emails,
|
||||
] : [];
|
||||
|
||||
$baseData = $this->getFilteredFormData(parent::toArray($request), $userIsFormOwner);
|
||||
|
||||
return array_merge($baseData, $ownerData, [
|
||||
'workspace_id' => $this->workspace_id,
|
||||
'workspace' => new WorkspaceResource($this->workspace),
|
||||
'is_closed' => $this->is_closed,
|
||||
'is_password_protected' => false,
|
||||
'has_password' => $this->has_password,
|
||||
'max_number_of_submissions_reached' => $this->max_number_of_submissions_reached
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter form data to hide properties from users.
|
||||
* - For relation fields, hides the relation information
|
||||
*/
|
||||
private function getFilteredFormData(array $data, bool $userIsFormOwner)
|
||||
{
|
||||
if ($userIsFormOwner) return $data;
|
||||
|
||||
$properties = collect($data['properties'])->map(function($property){
|
||||
// Remove database details from relation
|
||||
if ($property['type'] === 'relation') {
|
||||
if (isset($property['relation'])) {
|
||||
unset($property['relation']);
|
||||
}
|
||||
}
|
||||
return $property;
|
||||
});
|
||||
|
||||
$data['properties'] = $properties->toArray();
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function setCleanings(array $cleanings)
|
||||
{
|
||||
$this->cleanings = $cleanings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function doesMissPassword(Request $request)
|
||||
{
|
||||
if (!$this->is_pro || !$this->has_password) return false;
|
||||
|
||||
return !PasswordProtectedForm::hasCorrectPassword($request, $this->resource);
|
||||
}
|
||||
|
||||
private function getPasswordProtectedForm()
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'slug' => $this->slug,
|
||||
'custom_code' => $this->custom_code,
|
||||
'dark_mode' => $this->dark_mode,
|
||||
'transparent_background' => $this->transparent_background,
|
||||
'color' => $this->color,
|
||||
'is_password_protected' => true,
|
||||
'has_password' => $this->has_password,
|
||||
'width' => 'centered',
|
||||
'properties' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Http/Resources/FormSubmissionResource.php
Normal file
58
app/Http/Resources/FormSubmissionResource.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class FormSubmissionResource extends JsonResource
|
||||
{
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$this->generateFileLinks();
|
||||
$this->addTimestamp();
|
||||
|
||||
return [
|
||||
'data' => $this->data,
|
||||
'form_id' => $this->form_id,
|
||||
'id' => $this->id,
|
||||
];
|
||||
}
|
||||
|
||||
private function addTimestamp()
|
||||
{
|
||||
$this->data = array_merge($this->data, [
|
||||
"created_at" => $this->created_at->toDateTimeString()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to the file (generating signed s3 URL)
|
||||
* @return void
|
||||
*/
|
||||
private function generateFileLinks()
|
||||
{
|
||||
$data = $this->data;
|
||||
$formFields = collect($this->form->properties)->concat(collect($this->form->removed_properties));
|
||||
$fileFields = $formFields->filter(function ($field) {
|
||||
return $field['type'] == 'files';
|
||||
});
|
||||
foreach ($fileFields as $field) {
|
||||
if (isset($data[$field['id']]) && !empty($data[$field['id']])) {
|
||||
$data[$field['id']] = collect($data[$field['id']])->map(function ($file) {
|
||||
return [
|
||||
'file_url' => route('open.forms.submissions.file', [$this->form_id, $file]),
|
||||
'file_name' => $file,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
24
app/Http/Resources/WorkspaceResource.php
Normal file
24
app/Http/Resources/WorkspaceResource.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkspaceResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'is_enterprise' => $this->is_enterprise,
|
||||
'is_pro' => $this->is_pro,
|
||||
];
|
||||
}
|
||||
}
|
||||
155
app/Jobs/Form/StoreFormSubmissionJob.php
Normal file
155
app/Jobs/Form/StoreFormSubmissionJob.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Form;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Http\Controllers\Forms\PublicFormController;
|
||||
use App\Http\Requests\AnswerFormRequest;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Service\Storage\StorageFileNameParser;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoreFormSubmissionJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Form $form, public array $submissionData)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$formData = $this->getFormData();
|
||||
$this->addHiddenPrefills($formData);
|
||||
|
||||
$this->form->submissions()->create([
|
||||
'data' => $formData,
|
||||
]);
|
||||
|
||||
FormSubmitted::dispatch($this->form, $formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve data from request object, and pre-format it if needed.
|
||||
* - Replace notionforms id with notion field ids
|
||||
* - Clean \ in select id values
|
||||
* - Stores file and replace value with url
|
||||
* - Generate auto increment id & unique id features for rich text field
|
||||
*/
|
||||
private function getFormData()
|
||||
{
|
||||
$data = $this->submissionData;
|
||||
$finalData = [];
|
||||
|
||||
$properties = collect($this->form->properties);
|
||||
|
||||
// Do required transformation per type (e.g. file uploads)
|
||||
foreach ($data as $answerKey => $answerValue) {
|
||||
$field = $properties->where('id', $answerKey)->first();
|
||||
if (!$field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
($field['type'] == 'url' && isset($field['file_upload']) && $field['file_upload'])
|
||||
|| $field['type'] == 'files') {
|
||||
if (is_array($answerValue)) {
|
||||
$finalData[$field['id']] = [];
|
||||
foreach ($answerValue as $file) {
|
||||
$finalData[$field['id']][] = $this->storeFile($file);
|
||||
}
|
||||
} else {
|
||||
$finalData[$field['id']] = $this->storeFile($answerValue);
|
||||
}
|
||||
} else {
|
||||
if ($field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) {
|
||||
$finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid() : "Please upgrade your OpenForm subscription to use our ID generation features";
|
||||
} else {
|
||||
if ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
|
||||
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : "Please upgrade your OpenForm subscription to use our ID generation features";
|
||||
} else {
|
||||
$finalData[$field['id']] = $answerValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $finalData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Back-end Value formatting. Use case:
|
||||
* - File uploads (move file from tmp storage to persistent)
|
||||
*
|
||||
* File can have 2 formats:
|
||||
* - file_name-{uuid}.{ext}
|
||||
* - {uuid}
|
||||
*/
|
||||
private function storeFile(?string $value)
|
||||
{
|
||||
if ($value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fileNameParser = StorageFileNameParser::parse($value);
|
||||
|
||||
// Make sure we retrieve the file in tmp storage, move it to persistent
|
||||
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH.'/'.$fileNameParser->uuid;
|
||||
if (!Storage::disk('s3')->exists($fileName)) {
|
||||
// File not found, we skip
|
||||
return null;
|
||||
}
|
||||
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
$completeNewFilename = $newPath.'/'.$fileNameParser->getMovedFileName();
|
||||
|
||||
\Log::debug('Moving file to permanent storage.', [
|
||||
'uuid' => $fileNameParser->uuid,
|
||||
'destination' => $completeNewFilename,
|
||||
'form_id' => $this->form->id,
|
||||
'form_slug' => $this->form->slug,
|
||||
]);
|
||||
Storage::disk('s3')->move($fileName, $completeNewFilename);
|
||||
|
||||
return $fileNameParser->getMovedFileName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds prefill from hidden fields
|
||||
*
|
||||
* @param array $formData
|
||||
* @param AnswerFormRequest $request
|
||||
*/
|
||||
private function addHiddenPrefills(array &$formData): void
|
||||
{
|
||||
// Find hidden properties with prefill, set values
|
||||
collect($this->form->properties)->filter(function ($property) {
|
||||
return isset($property['hidden'])
|
||||
&& isset($property['prefill'])
|
||||
&& $property['hidden']
|
||||
&& !is_null($property['prefill']);
|
||||
})->each(function (array $property) use (&$formData) {
|
||||
if ($property['type'] === 'date' && isset($property['prefill_today']) && $property['prefill_today']) {
|
||||
$formData[$property['id']] = now();
|
||||
} else {
|
||||
$formData[$property['id']] = $property['prefill'];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
24
app/Listeners/Forms/FormCreationConfirmation.php
Normal file
24
app/Listeners/Forms/FormCreationConfirmation.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners\Forms;
|
||||
|
||||
use App\Events\Models\FormCreated;
|
||||
use App\Mail\Forms\FormCreationConfirmationMail;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class FormCreationConfirmation implements ShouldQueue
|
||||
{
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(FormCreated $event)
|
||||
{
|
||||
Mail::to($event->form->creator)->send(new FormCreationConfirmationMail($event->form));
|
||||
}
|
||||
}
|
||||
37
app/Listeners/Forms/NotifyFormSubmission.php
Normal file
37
app/Listeners/Forms/NotifyFormSubmission.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners\Forms;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Notifications\Forms\FormSubmissionNotification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
class NotifyFormSubmission implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
/**
|
||||
* Sends notification to pre-defined emails on form submissions
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(FormSubmitted $event)
|
||||
{
|
||||
if (!$event->form->notifies || !$event->form->is_pro) return;
|
||||
|
||||
$subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function($email) {
|
||||
return filter_var($email, FILTER_VALIDATE_EMAIL);
|
||||
});
|
||||
\Log::debug('Sending email notification',[
|
||||
'recipients' => $subscribers->toArray(),
|
||||
'form_id' => $event->form->id,
|
||||
'form_slug' => $event->form->slug,
|
||||
]);
|
||||
$subscribers->each(function ($subscriber) use ($event) {
|
||||
Notification::route('mail', $subscriber)->notify(new FormSubmissionNotification($event));
|
||||
});
|
||||
}
|
||||
}
|
||||
72
app/Listeners/Forms/PostFormDataToWebhook.php
Normal file
72
app/Listeners/Forms/PostFormDataToWebhook.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners\Forms;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Spatie\WebhookServer\WebhookCall;
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
|
||||
class PostFormDataToWebhook implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(FormSubmitted $event)
|
||||
{
|
||||
$form = $event->form;
|
||||
if (!$form->is_pro) return;
|
||||
$data = $this->getWebhookData($event);
|
||||
|
||||
$this->sendSimpleWebhook($form, $data);
|
||||
$this->sendZappierWebhooks($form, $data);
|
||||
}
|
||||
|
||||
private function sendSimpleWebhook(Form $form, array $data) {
|
||||
if ($form->webhook_url) {
|
||||
\Log::debug('Sending data to webhook URL',[
|
||||
'webhook_url' => $form->webhook_url,
|
||||
'form_id' => $form->id,
|
||||
'form_slug' => $form->slug,
|
||||
]);
|
||||
WebhookCall::create()
|
||||
->url($form->webhook_url)
|
||||
->doNotSign()
|
||||
->payload($data)
|
||||
->dispatch();
|
||||
}
|
||||
}
|
||||
|
||||
private function sendZappierWebhooks(Form $form, array $data) {
|
||||
foreach ($form->zappierHooks as $hook) {
|
||||
\Log::debug('Sending data to Zapier webhook',[
|
||||
'form_id' => $form->id,
|
||||
'form_slug' => $form->slug,
|
||||
]);
|
||||
$hook->triggerHook($data);
|
||||
}
|
||||
}
|
||||
|
||||
private function getWebhookData(FormSubmitted $event): array {
|
||||
$formatter = (new FormSubmissionFormatter($event->form, $event->data));
|
||||
|
||||
$formattedData = [];
|
||||
foreach ($formatter->getFieldsWithValue() as $field) {
|
||||
$formattedData[$field['name']] = $field['value'];
|
||||
}
|
||||
|
||||
return [
|
||||
'form_title' => $event->form->title,
|
||||
'form_slug' => $event->form->slug,
|
||||
'submission' => $formattedData
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
62
app/Listeners/Forms/SubmissionConfirmation.php
Normal file
62
app/Listeners/Forms/SubmissionConfirmation.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners\Forms;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Mail\Forms\SubmissionConfirmationMail;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
/**
|
||||
* Sends a confirmation to form respondant that form was submitted
|
||||
*
|
||||
* Class SubmissionConfirmation
|
||||
* @package App\Listeners\Forms
|
||||
*/
|
||||
class SubmissionConfirmation implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(FormSubmitted $event)
|
||||
{
|
||||
if (!$event->form->send_submission_confirmation) return;
|
||||
|
||||
$email = $this->getRespondentEmail($event);
|
||||
if (!$email) return;
|
||||
|
||||
\Log::info('Sending submission confirmation',[
|
||||
'recipient' => $email,
|
||||
'form_id' => $event->form->id,
|
||||
'form_slug' => $event->form->slug,
|
||||
]);
|
||||
Mail::to($email)->send(new SubmissionConfirmationMail($event));
|
||||
}
|
||||
|
||||
private function getRespondentEmail(FormSubmitted $event)
|
||||
{
|
||||
// Make sure we only have one email field in the form
|
||||
$emailFields = collect($event->form->properties)->filter(function($field) {
|
||||
$hidden = $field['hidden']?? false;
|
||||
return !$hidden && $field['type'] == 'email';
|
||||
});
|
||||
if ($emailFields->count() != 1) return null;
|
||||
|
||||
if (isset($event->data[$emailFields->first()['id']])) {
|
||||
$email = $event->data[$emailFields->first()['id']];
|
||||
if ($this->validateEmail($email)) return $email;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function validateEmail($email): bool {
|
||||
return (boolean) filter_var($email, FILTER_VALIDATE_EMAIL);
|
||||
}
|
||||
}
|
||||
38
app/Mail/Forms/FormCreationConfirmationMail.php
Normal file
38
app/Mail/Forms/FormCreationConfirmationMail.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail\Forms;
|
||||
|
||||
use App\Mail\OpenFormMail;
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FormCreationConfirmationMail extends OpenFormMail implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Form $form)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this
|
||||
->markdown('mail.form.created',[
|
||||
'form' => $this->form,
|
||||
])->subject('Your form was created!');
|
||||
}
|
||||
}
|
||||
53
app/Mail/Forms/SubmissionConfirmationMail.php
Normal file
53
app/Mail/Forms/SubmissionConfirmationMail.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail\Forms;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Mail\OpenFormMail;
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(private FormSubmitted $event)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$form = $this->event->form;
|
||||
|
||||
$formatter = (new FormSubmissionFormatter($form, $this->event->data))
|
||||
->createLinks()
|
||||
->outputStringsOnly();
|
||||
|
||||
return $this
|
||||
->replyTo($form->creator->email)
|
||||
->from($this->getFromEmail(), $form->notification_sender)
|
||||
->subject($form->notification_subject)
|
||||
->markdown('mail.form.confirmation-submission-notification',[
|
||||
'fields' => $formatter->getFieldsWithValue(),
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getFromEmail()
|
||||
{
|
||||
$originalFromAddress = Str::of(config('mail.from.address'))->explode('@');
|
||||
return $originalFromAddress->first(). '+' . time() . '@' . $originalFromAddress->last();
|
||||
}
|
||||
}
|
||||
13
app/Mail/OpenFormMail.php
Normal file
13
app/Mail/OpenFormMail.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
abstract class OpenFormMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
}
|
||||
226
app/Models/Forms/Form.php
Normal file
226
app/Models/Forms/Form.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Forms;
|
||||
|
||||
use App\Events\Models\FormCreated;
|
||||
use App\Models\Integration\FormZapierWebhook;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Database\Factories\FormFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Sluggable\HasSlug;
|
||||
use Spatie\Sluggable\SlugOptions;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Form extends Model
|
||||
{
|
||||
const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
|
||||
const THEMES = ['default', 'simple', 'notion'];
|
||||
const WIDTHS = ['centered', 'full'];
|
||||
|
||||
use HasFactory, HasSlug, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'workspace_id',
|
||||
'creator_id',
|
||||
'properties',
|
||||
'removed_properties',
|
||||
|
||||
// Notifications
|
||||
'notifies',
|
||||
'notification_emails',
|
||||
'send_submission_confirmation',
|
||||
'notification_sender',
|
||||
'notification_subject',
|
||||
'notification_body',
|
||||
'notifications_include_submission',
|
||||
|
||||
// integrations
|
||||
'webhook_url',
|
||||
|
||||
'title',
|
||||
'description',
|
||||
'tags',
|
||||
|
||||
// Customization
|
||||
'theme',
|
||||
'width',
|
||||
'cover_picture',
|
||||
'logo_picture',
|
||||
'dark_mode',
|
||||
'color',
|
||||
'uppercase_labels',
|
||||
'no_branding',
|
||||
'hide_title',
|
||||
'transparent_background',
|
||||
|
||||
// Custom Code
|
||||
'custom_code',
|
||||
|
||||
// Submission
|
||||
'submit_button_text',
|
||||
'database_fields_update',
|
||||
're_fillable',
|
||||
're_fill_button_text',
|
||||
'submitted_text',
|
||||
'redirect_url',
|
||||
'use_captcha',
|
||||
'closes_at',
|
||||
'closed_text',
|
||||
'max_submissions_count',
|
||||
'max_submissions_reached_text',
|
||||
|
||||
// Security & Privacy
|
||||
'can_be_indexed',
|
||||
'password'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'properties' => 'array',
|
||||
'database_fields_update' => 'array',
|
||||
'closes_at' => 'datetime',
|
||||
'tags' => 'array',
|
||||
'removed_properties' => 'array'
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'share_url',
|
||||
'is_pro'
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'workspace_id',
|
||||
'notifies',
|
||||
'webhook_url',
|
||||
'send_submission_confirmation',
|
||||
'redirect_url',
|
||||
'database_fields_update',
|
||||
'notification_sender',
|
||||
'notification_subject',
|
||||
'notification_body',
|
||||
'notifications_include_submission',
|
||||
'password',
|
||||
'tags',
|
||||
'notification_emails',
|
||||
'removed_properties'
|
||||
];
|
||||
|
||||
/**
|
||||
* The event map for the model.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dispatchesEvents = [
|
||||
'created' => FormCreated::class,
|
||||
];
|
||||
|
||||
public function getIsProAttribute()
|
||||
{
|
||||
return optional($this->workspace)->is_pro;
|
||||
}
|
||||
|
||||
public function getShareUrlAttribute()
|
||||
{
|
||||
return url('/forms/'.$this->slug);
|
||||
}
|
||||
|
||||
public function getSubmissionsCountAttribute()
|
||||
{
|
||||
return $this->submissions()->count();
|
||||
}
|
||||
|
||||
public function getViewsCountAttribute()
|
||||
{
|
||||
return $this->views()->count() +
|
||||
$this->statistics()->sum(DB::raw("cast(data->>'views' as integer)"));
|
||||
}
|
||||
|
||||
public function setDescriptionAttribute($value)
|
||||
{
|
||||
// Strip out unwanted html
|
||||
$this->attributes['description'] = Purify::clean($value);
|
||||
}
|
||||
|
||||
public function setSubmittedTextAttribute($value)
|
||||
{
|
||||
// Strip out unwanted html
|
||||
$this->attributes['submitted_text'] = Purify::clean($value);
|
||||
}
|
||||
|
||||
public function getIsClosedAttribute()
|
||||
{
|
||||
return ($this->closes_at && now()->gt($this->closes_at));
|
||||
}
|
||||
|
||||
public function getMaxNumberOfSubmissionsReachedAttribute()
|
||||
{
|
||||
return ($this->max_submissions_count && $this->max_submissions_count <= $this->submissions_count);
|
||||
}
|
||||
|
||||
public function setClosedTextAttribute($value)
|
||||
{
|
||||
$this->attributes['closed_text'] = Purify::clean($value);
|
||||
}
|
||||
|
||||
public function setMaxSubmissionsReachedTextAttribute($value)
|
||||
{
|
||||
$this->attributes['max_submissions_reached_text'] = Purify::clean($value);
|
||||
}
|
||||
|
||||
public function getHasPasswordAttribute()
|
||||
{
|
||||
return !empty($this->password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function workspace()
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function submissions()
|
||||
{
|
||||
return $this->hasMany(FormSubmission::class);
|
||||
}
|
||||
|
||||
public function views()
|
||||
{
|
||||
return $this->hasMany(FormView::class);
|
||||
}
|
||||
|
||||
public function statistics()
|
||||
{
|
||||
return $this->hasMany(FormStatistic::class);
|
||||
}
|
||||
|
||||
public function zappierHooks()
|
||||
{
|
||||
return $this->hasMany(FormZapierWebhook::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Config/options
|
||||
*/
|
||||
public function getSlugOptions(): SlugOptions
|
||||
{
|
||||
return SlugOptions::create()
|
||||
->doNotGenerateSlugsOnUpdate()
|
||||
->generateSlugsFrom('title')
|
||||
->saveSlugsTo('slug');
|
||||
}
|
||||
|
||||
public static function newFactory()
|
||||
{
|
||||
return FormFactory::new();
|
||||
}
|
||||
}
|
||||
38
app/Models/Forms/FormStatistic.php
Normal file
38
app/Models/Forms/FormStatistic.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Forms;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FormStatistic extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'form_id',
|
||||
'data',
|
||||
'date'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function form()
|
||||
{
|
||||
return $this->belongsTo(Form::class);
|
||||
}
|
||||
|
||||
}
|
||||
27
app/Models/Forms/FormSubmission.php
Normal file
27
app/Models/Forms/FormSubmission.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Forms;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FormSubmission extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'data'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array'
|
||||
];
|
||||
|
||||
/**
|
||||
* RelationShips
|
||||
*/
|
||||
public function form() {
|
||||
return $this->belongsTo(Form::class);
|
||||
}
|
||||
}
|
||||
19
app/Models/Forms/FormView.php
Normal file
19
app/Models/Forms/FormView.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Forms;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FormView extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* RelationShips
|
||||
*/
|
||||
public function form() {
|
||||
return $this->belongsTo(Form::class);
|
||||
}
|
||||
}
|
||||
37
app/Models/Integration/FormZapierWebhook.php
Normal file
37
app/Models/Integration/FormZapierWebhook.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Integration;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\WebhookServer\WebhookCall;
|
||||
|
||||
class FormZapierWebhook extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'form_zapier_webhooks';
|
||||
|
||||
protected $fillable = [
|
||||
'form_id',
|
||||
'hook_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function form()
|
||||
{
|
||||
return $this->belongsTo(Form::class);
|
||||
}
|
||||
|
||||
public function triggerHook(array $data) {
|
||||
WebhookCall::create()
|
||||
->url($this->hook_url)
|
||||
->doNotSign()
|
||||
->payload($data)
|
||||
->dispatch();
|
||||
}
|
||||
}
|
||||
39
app/Models/OAuthProvider.php
Normal file
39
app/Models/OAuthProvider.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OAuthProvider extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'oauth_providers';
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = ['id'];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'access_token', 'refresh_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
209
app/Models/User.php
Normal file
209
app/Models/User.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Http\Controllers\SubscriptionController;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Workspace;
|
||||
use App\Notifications\ResetPassword;
|
||||
use App\Notifications\VerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Cashier\Billable;
|
||||
use Rickycezar\Impersonate\Models\Impersonate;
|
||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||
|
||||
class User extends Authenticatable implements JWTSubject //, MustVerifyEmail
|
||||
{
|
||||
use Notifiable, HasFactory, Billable;
|
||||
|
||||
const ADMINS = ['julien@notionforms.io'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'hear_about_us'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'hear_about_us'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* The accessors to append to the model's array form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $appends = [
|
||||
'photo_url',
|
||||
'is_subscribed',
|
||||
'has_enterprise_subscription',
|
||||
'admin',
|
||||
'has_customer_id',
|
||||
'has_forms'
|
||||
];
|
||||
|
||||
protected $withCount = ['workspaces'];
|
||||
|
||||
/**
|
||||
* Get the profile photo URL attribute.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhotoUrlAttribute()
|
||||
{
|
||||
return vsprintf('https://www.gravatar.com/avatar/%s.jpg?s=200&d=%s', [
|
||||
md5(strtolower($this->email)),
|
||||
$this->name ? urlencode("https://ui-avatars.com/api/$this->name") : 'mp',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getHasFormsAttribute()
|
||||
{
|
||||
return $this->workspaces()->whereHas('forms')->exists();
|
||||
}
|
||||
|
||||
public function getIsSubscribedAttribute()
|
||||
{
|
||||
return $this->subscribed() || $this->subscribed(SubscriptionController::ENTERPRISE_SUBSCRIPTION_NAME);
|
||||
}
|
||||
|
||||
public function getHasEnterpriseSubscriptionAttribute()
|
||||
{
|
||||
return $this->subscribed(SubscriptionController::ENTERPRISE_SUBSCRIPTION_NAME);
|
||||
}
|
||||
|
||||
public function getHasCustomerIdAttribute()
|
||||
{
|
||||
return !is_null($this->stripe_id);
|
||||
}
|
||||
|
||||
public function getAdminAttribute()
|
||||
{
|
||||
return in_array($this->email, self::ADMINS);
|
||||
}
|
||||
|
||||
/**
|
||||
* =================================
|
||||
* Helper Related
|
||||
* =================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPassword($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email verification notification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
$this->notify(new VerifyEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
* =================================
|
||||
* Relationship
|
||||
* =================================
|
||||
*/
|
||||
|
||||
public function workspaces()
|
||||
{
|
||||
return $this->belongsToMany(Workspace::class);
|
||||
}
|
||||
|
||||
public function forms()
|
||||
{
|
||||
return $this->hasMany(Form::class,'creator_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* =================================
|
||||
* Oauth Related
|
||||
* =================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the oauth providers.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function oauthProviders()
|
||||
{
|
||||
return $this->hasMany(OAuthProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getJWTIdentifier()
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getJWTCustomClaims()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function boot ()
|
||||
{
|
||||
parent::boot();
|
||||
static::deleting(function(User $user) {
|
||||
// Remove user's workspace if he's the only one with this workspace
|
||||
foreach ($user->workspaces as $workspace) {
|
||||
if ($workspace->users()->count() == 1) {
|
||||
$workspace->delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeWithActiveSubscription($query)
|
||||
{
|
||||
return $query->whereHas('subscriptions', function($query) {
|
||||
$query->where(function($q){
|
||||
$q->where('stripe_status', 'trialing')
|
||||
->orWhere('stripe_status', 'active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
68
app/Models/Workspace.php
Normal file
68
app/Models/Workspace.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Workspace extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'icon',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
'is_pro',
|
||||
'is_enterprise'
|
||||
];
|
||||
|
||||
public function getIsProAttribute()
|
||||
{
|
||||
return true; // Temporary true for ALL
|
||||
|
||||
// Make sure at least one owner is pro
|
||||
foreach ($this->owners as $owner) {
|
||||
if ($owner->is_subscribed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getIsEnterpriseAttribute()
|
||||
{
|
||||
return true; // Temporary true for ALL
|
||||
|
||||
foreach ($this->owners as $owner) {
|
||||
if ($owner->has_enterprise_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
public function owners()
|
||||
{
|
||||
return $this->users()->wherePivot('role', 'admin');
|
||||
}
|
||||
|
||||
public function forms()
|
||||
{
|
||||
return $this->hasMany(Form::class);
|
||||
}
|
||||
}
|
||||
69
app/Notifications/Forms/FormSubmissionNotification.php
Normal file
69
app/Notifications/Forms/FormSubmissionNotification.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Forms;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class FormSubmissionNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public FormSubmitted $event;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(FormSubmitted $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$formatter = (new FormSubmissionFormatter($this->event->form, $this->event->data))
|
||||
->showHiddenFields()
|
||||
->createLinks()
|
||||
->outputStringsOnly();
|
||||
|
||||
return (new MailMessage)
|
||||
->replyTo($notifiable->routes['mail'])
|
||||
->from($this->getFromEmail(), config('app.name'))
|
||||
->subject('New form submission for "'.$this->event->form->title.'"')
|
||||
->markdown('mail.form.submission-notification', [
|
||||
'fields' => $formatter->getFieldsWithValue(),
|
||||
'form' => $this->event->form,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getFromEmail()
|
||||
{
|
||||
$originalFromAddress = Str::of(config('mail.from.address'))->explode('@');
|
||||
return $originalFromAddress->first(). '+' . time() . '@' . $originalFromAddress->last();
|
||||
}
|
||||
|
||||
}
|
||||
23
app/Notifications/ResetPassword.php
Normal file
23
app/Notifications/ResetPassword.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Auth\Notifications\ResetPassword as Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ResetPassword extends Notification
|
||||
{
|
||||
/**
|
||||
* Build the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage)
|
||||
->line('You are receiving this email because we received a password reset request for your account.')
|
||||
->action('Reset Password', url('password/reset/'.$this->token).'?email='.urlencode($notifiable->email))
|
||||
->line('If you did not request a password reset, no further action is required.');
|
||||
}
|
||||
}
|
||||
41
app/Notifications/Subscription/FailedPaymentNotification.php
Normal file
41
app/Notifications/Subscription/FailedPaymentNotification.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Subscription;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class FailedPaymentNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Your Payment Failed')
|
||||
->greeting(__('We tried to charge your card for your OpenForm subscription but the payment but did not work.'))
|
||||
->line(__('Please go to OpenForm, click on your name on the top right corner, and click on "Billing".
|
||||
You will then be able to update your card details. To avoid any service disruption, you can reply to this email whenever
|
||||
you updated your card details, and we\'ll manually attempt to charge your card.'))
|
||||
->action(__('Go to OpenForm'), url('/'));
|
||||
}
|
||||
}
|
||||
25
app/Notifications/VerifyEmail.php
Normal file
25
app/Notifications/VerifyEmail.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail as Notification;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class VerifyEmail extends Notification
|
||||
{
|
||||
/**
|
||||
* Get the verification URL for the given notifiable.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return string
|
||||
*/
|
||||
protected function verificationUrl($notifiable)
|
||||
{
|
||||
$url = URL::temporarySignedRoute(
|
||||
'verification.verify', Carbon::now()->addMinutes(60), ['user' => $notifiable->id]
|
||||
);
|
||||
|
||||
return str_replace('/api', '', $url);
|
||||
}
|
||||
}
|
||||
94
app/Policies/FormPolicy.php
Normal file
94
app/Policies/FormPolicy.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class FormPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewAny(User $user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Forms\Form $form
|
||||
* @return mixed
|
||||
*/
|
||||
public function view(User $user, Form $form)
|
||||
{
|
||||
return $user->workspaces()->find($form->workspace_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function create(User $user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Forms\Form $form
|
||||
* @return mixed
|
||||
*/
|
||||
public function update(User $user, Form $form)
|
||||
{
|
||||
return $user->workspaces()->find($form->workspace_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Forms\Form $form
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete(User $user, Form $form)
|
||||
{
|
||||
return $user->workspaces()->find($form->workspace_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Forms\Form $form
|
||||
* @return mixed
|
||||
*/
|
||||
public function restore(User $user, Form $form)
|
||||
{
|
||||
return $user->workspaces()->find($form->workspace_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Forms\Form $form
|
||||
* @return mixed
|
||||
*/
|
||||
public function forceDelete(User $user, Form $form)
|
||||
{
|
||||
return $user->workspaces()->find($form->workspace_id)->exists();
|
||||
}
|
||||
}
|
||||
30
app/Policies/Integration/FormZapierWebhookPolicy.php
Normal file
30
app/Policies/Integration/FormZapierWebhookPolicy.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies\Integration;
|
||||
|
||||
use App\Models\Integration\FormZapierWebhook;
|
||||
use App\Models\User;
|
||||
use App\Policies\FormPolicy;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class FormZapierWebhookPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
protected FormPolicy $formPolicy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->formPolicy = new FormPolicy();
|
||||
}
|
||||
|
||||
public function store(User $user, FormZapierWebhook $webhook)
|
||||
{
|
||||
return $this->formPolicy->update($user, $webhook->form); // && $user->is_subscribed;
|
||||
}
|
||||
|
||||
public function delete(User $user, FormZapierWebhook $webhook)
|
||||
{
|
||||
return $this->formPolicy->update($user, $webhook->form); // && $user->is_subscribed;
|
||||
}
|
||||
}
|
||||
94
app/Policies/WorkspacePolicy.php
Normal file
94
app/Policies/WorkspacePolicy.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Workspace;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class WorkspacePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewAny(User $user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Workspace $workspace
|
||||
* @return mixed
|
||||
*/
|
||||
public function view(User $user, Workspace $workspace)
|
||||
{
|
||||
return $user->workspaces()->find($workspace->id)!==null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function create(User $user)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Workspace $workspace
|
||||
* @return mixed
|
||||
*/
|
||||
public function update(User $user, Workspace $workspace)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Workspace $workspace
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete(User $user, Workspace $workspace)
|
||||
{
|
||||
return !$workspace->owners->where('id',$user->id)->isEmpty() && $user->workspaces()->count() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Workspace $workspace
|
||||
* @return mixed
|
||||
*/
|
||||
public function restore(User $user, Workspace $workspace)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Workspace $workspace
|
||||
* @return mixed
|
||||
*/
|
||||
public function forceDelete(User $user, Workspace $workspace)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
42
app/Providers/AppServiceProvider.php
Normal file
42
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Cashier\Cashier;
|
||||
use Laravel\Dusk\DuskServiceProvider;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
JsonResource::withoutWrapping();
|
||||
Cashier::calculateTaxes();
|
||||
|
||||
if ($this->app->runningUnitTests()) {
|
||||
Schema::defaultStringLength(191);
|
||||
}
|
||||
|
||||
Validator::includeUnvalidatedArrayKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
if ($this->app->environment('local', 'testing') && class_exists(DuskServiceProvider::class)) {
|
||||
$this->app->register(DuskServiceProvider::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
app/Providers/AuthServiceProvider.php
Normal file
41
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Integration\FormZapierWebhook;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\User;
|
||||
use App\Policies\FormPolicy;
|
||||
use App\Policies\Integration\FormZapierWebhookPolicy;
|
||||
use App\Policies\WorkspacePolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The policy mappings for the application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $policies = [
|
||||
Form::class => FormPolicy::class,
|
||||
Workspace::class => WorkspacePolicy::class,
|
||||
FormZapierWebhook::class => FormZapierWebhookPolicy::class
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
\Illuminate\Support\Facades\Gate::define('viewMailcoach', function ($user = null) {
|
||||
return optional($user)->admin;
|
||||
});
|
||||
}
|
||||
}
|
||||
21
app/Providers/BroadcastServiceProvider.php
Normal file
21
app/Providers/BroadcastServiceProvider.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
Broadcast::routes();
|
||||
|
||||
require base_path('routes/channels.php');
|
||||
}
|
||||
}
|
||||
48
app/Providers/EventServiceProvider.php
Normal file
48
app/Providers/EventServiceProvider.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Events\Models\FormCreated;
|
||||
use App\Listeners\Auth\RegisteredListener;
|
||||
use App\Listeners\Forms\FormCreationConfirmation;
|
||||
use App\Listeners\Forms\NotifyFormSubmission;
|
||||
use App\Listeners\Forms\PostFormDataToWebhook;
|
||||
use App\Listeners\Forms\SubmissionConfirmation;
|
||||
use App\Notifications\Forms\FormCreatedNotification;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event listener mappings for the application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
FormCreated::class => [
|
||||
FormCreationConfirmation::class
|
||||
],
|
||||
FormSubmitted::class => [
|
||||
NotifyFormSubmission::class,
|
||||
PostFormDataToWebhook::class,
|
||||
SubmissionConfirmation::class,
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
42
app/Providers/HorizonServiceProvider.php
Normal file
42
app/Providers/HorizonServiceProvider.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Horizon;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||
|
||||
// Horizon::night();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Horizon gate.
|
||||
*
|
||||
* This gate determines who can access Horizon in non-local environments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function gate()
|
||||
{
|
||||
Gate::define('viewHorizon', function ($user) {
|
||||
return in_array($user->email, [
|
||||
'julien@notionforms.io'
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
26
app/Providers/ModelStatsServiceProvider.php
Normal file
26
app/Providers/ModelStatsServiceProvider.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Jhumanj\LaravelModelStats\LaravelModelStats;
|
||||
use Jhumanj\LaravelModelStats\ModelStatsServiceProvider as Provider;
|
||||
|
||||
class ModelStatsServiceProvider extends Provider
|
||||
{
|
||||
/**
|
||||
* Register the LaravelModelStats gate.
|
||||
*
|
||||
* This gate determines who can access ModelStats in non-local environments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewModelStats', function ($user) {
|
||||
return in_array($user->email, [
|
||||
'julien@notionforms.io',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
19
app/Providers/PurifySetupProvider.php
Normal file
19
app/Providers/PurifySetupProvider.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Service\HtmlPurifier\HTMLPurifier_URIScheme_notion;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class PurifySetupProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
\HTMLPurifier_URISchemeRegistry::instance()->register('notion', new HTMLPurifier_URIScheme_notion());
|
||||
}
|
||||
}
|
||||
73
app/Providers/RouteServiceProvider.php
Normal file
73
app/Providers/RouteServiceProvider.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The path to the "home" route for your application.
|
||||
*
|
||||
* This is used by Laravel authentication to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/home';
|
||||
|
||||
/**
|
||||
* The controller namespace for the application.
|
||||
*
|
||||
* When present, controller route declarations will automatically be prefixed with this namespace.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
// protected $namespace = 'App\\Http\\Controllers';
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, etc.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->configureRateLimiting();
|
||||
$this->registerGlobalRouteParamConstraints();
|
||||
|
||||
$this->routes(function () {
|
||||
|
||||
Route::prefix('api')
|
||||
->middleware('api')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web.php'));
|
||||
|
||||
Route::middleware('spa')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/spa.php'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the rate limiters for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configureRateLimiting()
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerGlobalRouteParamConstraints() {
|
||||
Route::pattern('workspaceId', '[0-9]+');
|
||||
}
|
||||
}
|
||||
43
app/Providers/VaporUiServiceProvider.php
Normal file
43
app/Providers/VaporUiServiceProvider.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class VaporUiServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->gate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Vapor UI gate.
|
||||
*
|
||||
* This gate determines who can access Vapor UI in non-local environments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function gate()
|
||||
{
|
||||
Gate::define('viewVaporUI', function ($user) {
|
||||
return $user->admin;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
533
app/Rules/FormPropertyLogicRule.php
Normal file
533
app/Rules/FormPropertyLogicRule.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Contracts\Validation\DataAwareRule;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class FormPropertyLogicRule implements Rule, DataAwareRule {
|
||||
|
||||
const ACTIONS_VALUES = [
|
||||
'show-block',
|
||||
'hide-block',
|
||||
'make-it-optional',
|
||||
'require-answer'
|
||||
];
|
||||
|
||||
const CONDITION_MAPPING = [
|
||||
'text' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'contains' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_contain' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'starts_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'ends_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'url' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'contains' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_contain' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'starts_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'ends_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'email' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'contains' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_contain' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'starts_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'ends_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'phone_number' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'contains' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_contain' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'starts_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'ends_with' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'number' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'number',
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'number',
|
||||
],
|
||||
'greater_than' => [
|
||||
'expected_type' => 'number',
|
||||
],
|
||||
'less_than' => [
|
||||
'expected_type' => 'number',
|
||||
],
|
||||
'greater_than_or_equal_to' => [
|
||||
'expected_type' => 'number',
|
||||
],
|
||||
'less_than_or_equal_to' => [
|
||||
'expected_type' => 'number',
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'checkbox' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
|
||||
]
|
||||
],
|
||||
'select' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'does_not_equal' => [
|
||||
'expected_type' => 'string',
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'multi_select' => [
|
||||
'comparators' => [
|
||||
'contains' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'uuid',
|
||||
]
|
||||
],
|
||||
'does_not_contain' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'uuid',
|
||||
]
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'date' => [
|
||||
'comparators' => [
|
||||
'equals' => [
|
||||
'expected_type' => 'string',
|
||||
'format' => [
|
||||
'type' => 'date'
|
||||
]
|
||||
],
|
||||
'before' => [
|
||||
'expected_type' => 'string',
|
||||
'format' => [
|
||||
'type' => 'date'
|
||||
]
|
||||
],
|
||||
'after' => [
|
||||
'expected_type' => 'string',
|
||||
'format' => [
|
||||
'type' => 'date'
|
||||
]
|
||||
],
|
||||
'on_or_before' => [
|
||||
'expected_type' => 'string',
|
||||
'format' => [
|
||||
'type' => 'date'
|
||||
]
|
||||
],
|
||||
'on_or_after' => [
|
||||
'expected_type' => 'string',
|
||||
'format' => [
|
||||
'type' => 'date'
|
||||
]
|
||||
],
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'past_week' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'empty',
|
||||
'values' => '{}'
|
||||
]
|
||||
],
|
||||
'past_month' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'empty',
|
||||
'values' => '{}'
|
||||
]
|
||||
],
|
||||
'past_year' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'empty',
|
||||
'values' => '{}'
|
||||
]
|
||||
],
|
||||
'next_week' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'empty',
|
||||
'values' => '{}'
|
||||
]
|
||||
],
|
||||
'next_month' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'empty',
|
||||
'values' => '{}'
|
||||
]
|
||||
],
|
||||
'next_year' => [
|
||||
'expected_type' => 'object',
|
||||
'format' => [
|
||||
'type' => 'empty',
|
||||
'values' => '{}'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'files' => [
|
||||
'comparators' => [
|
||||
'is_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
],
|
||||
'is_not_empty' => [
|
||||
'expected_type' => 'boolean',
|
||||
'format' => [
|
||||
'type' => 'enum',
|
||||
'values' => [true]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
private $isConditionCorrect = true;
|
||||
private $isActionCorrect = true;
|
||||
private $field = [];
|
||||
private $data = [];
|
||||
|
||||
private function checkBaseCondition($condtion) {
|
||||
|
||||
if (!isset($condtion['value'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($condtion['value']['property_meta'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($condtion['value']['property_meta']['type'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($condtion['value']['operator'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($condtion['value']['value'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$typeField = $condtion['value']['property_meta']['type'];
|
||||
$operator = $condtion['value']['operator'];
|
||||
$value = $condtion['value']['value'];
|
||||
|
||||
if (!isset(self::CONDITION_MAPPING[$typeField])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset(self::CONDITION_MAPPING[$typeField]['comparators'][$operator])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$type = self::CONDITION_MAPPING[$typeField]['comparators'][$operator]['expected_type'];
|
||||
|
||||
// Type d'objet : string, boolean, number, object
|
||||
if (
|
||||
($type === 'string' && gettype($value) !== 'string') ||
|
||||
($type === 'boolean' && !is_bool($value)) ||
|
||||
($type === 'number' && !is_numeric($value)) ||
|
||||
($type === 'object' && !is_array($value))
|
||||
) {
|
||||
$this->isConditionCorrect = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkConditions($conditions) {
|
||||
if (isset($conditions['operatorIdentifier'])) {
|
||||
if (($conditions['operatorIdentifier'] !== 'and') && ($conditions['operatorIdentifier'] !== 'or')) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($conditions['operatorIdentifier']['children'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_array($conditions['children'])) {
|
||||
$this->isConditionCorrect = false;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($conditions['children'] as &$child) {
|
||||
$this->checkConditions($child);
|
||||
}
|
||||
} else if (isset($conditions['identifier'])) {
|
||||
$this->checkBaseCondition($conditions);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkActions($conditions) {
|
||||
if (is_array($conditions) && count($conditions) > 0) {
|
||||
foreach($conditions as $val){
|
||||
if (!in_array($val, static::ACTIONS_VALUES) ||
|
||||
(in_array($this->field["type"], ['nf-text', 'nf-page-break', 'nf-divider', 'nf-image']) && !in_array($val, ['hide-block'])) ||
|
||||
(isset($this->field["hidden"]) && $this->field["hidden"] && !in_array($val, ['show-block','require-answer'])) ||
|
||||
(isset($this->field["required"]) && $this->field["required"] && !in_array($val, ['make-it-optional','hide-block']))
|
||||
) {
|
||||
$this->isActionCorrect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value) {
|
||||
$this->setProperty($attribute);
|
||||
if(isset($value["conditions"])){
|
||||
$this->checkConditions($value["conditions"]);
|
||||
}
|
||||
if(isset($value["actions"])){
|
||||
$this->checkActions($value["actions"]);
|
||||
}
|
||||
return ($this->isConditionCorrect && $this->isActionCorrect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
*/
|
||||
public function message() {
|
||||
$errorList = [];
|
||||
if(!$this->isConditionCorrect){
|
||||
$errorList[] = "The logic conditions for ".$this->field['name']." are not complete.";
|
||||
}
|
||||
if(!$this->isActionCorrect){
|
||||
$errorList[] = "The logic actions for ".$this->field['name']." are not valid.";
|
||||
}
|
||||
return $errorList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data under validation.
|
||||
*
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setData($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function setProperty(string $attributeKey)
|
||||
{
|
||||
$attributeKey = Str::of($attributeKey)->replace('.logic','')->toString();
|
||||
$this->field = \Arr::get($this->data, $attributeKey);
|
||||
}
|
||||
}
|
||||
47
app/Rules/OneEmailPerLine.php
Normal file
47
app/Rules/OneEmailPerLine.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class OneEmailPerLine implements Rule
|
||||
{
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
if ($value === null || empty(trim($value))) return true;
|
||||
foreach (preg_split("/\r\n|\n|\r/", $value) as $email) {
|
||||
$email = trim($email);
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return 'You need one valid email per line.';
|
||||
}
|
||||
}
|
||||
68
app/Rules/StorageFile.php
Normal file
68
app/Rules/StorageFile.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use App\Http\Controllers\Forms\PublicFormController;
|
||||
use App\Service\Storage\StorageFileNameParser;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StorageFile implements Rule
|
||||
{
|
||||
public int $maxSize;
|
||||
|
||||
public string $error = 'Invalid file.';
|
||||
|
||||
/** @var string[] */
|
||||
public array $fileTypes;
|
||||
|
||||
/**
|
||||
* @param int $maxSize
|
||||
* @param string[] $fileTypes
|
||||
*/
|
||||
public function __construct(int $maxSize, array $fileTypes = [])
|
||||
{
|
||||
$this->maxSize = $maxSize;
|
||||
|
||||
$this->fileTypes = $fileTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* File can have 2 formats:
|
||||
* - file_name-{uuid}.{ext}
|
||||
* - {uuid}
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
$fileNameParser = StorageFileNameParser::parse($value);
|
||||
if (!$uuid = $fileNameParser->uuid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filePath = PublicFormController::TMP_FILE_UPLOAD_PATH.$uuid;
|
||||
if (!Storage::exists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Storage::size($filePath) > $this->maxSize) {
|
||||
$this->error = 'File is too large.';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($this->fileTypes) > 0) {
|
||||
$this->error = 'Incorrect file type. Allowed only: '.implode(",", $this->fileTypes);
|
||||
return in_array($fileNameParser->extension, $this->fileTypes);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
}
|
||||
43
app/Rules/ValidHCaptcha.php
Normal file
43
app/Rules/ValidHCaptcha.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\ImplicitRule;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class ValidHCaptcha implements ImplicitRule
|
||||
{
|
||||
const H_CAPTCHA_VERIFY_URL = "https://hcaptcha.com/siteverify";
|
||||
|
||||
private $error = 'Invalid CAPTCHA. Please prove you\'re not a bot.';
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
$this->error = "Please complete the captcha.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return Http::asForm()->post(self::H_CAPTCHA_VERIFY_URL, [
|
||||
'secret' => config('services.h_captcha.secret_key'),
|
||||
'response' => $value
|
||||
])->json('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
}
|
||||
262
app/Service/Forms/FormCleaner.php
Normal file
262
app/Service/Forms/FormCleaner.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Service\Forms;
|
||||
|
||||
use App\Http\Requests\UserFormRequest;
|
||||
use App\Http\Resources\FormResource;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
use function App\Service\str_starts_with;
|
||||
use function collect;
|
||||
|
||||
class FormCleaner
|
||||
{
|
||||
/**
|
||||
* All the performed cleanings
|
||||
* @var bool
|
||||
*/
|
||||
private array $cleanings = [];
|
||||
|
||||
private array $data;
|
||||
|
||||
private array $formDefaults = [
|
||||
'notifies' => false,
|
||||
'color' => '#3B82F6',
|
||||
'hide_title' => false,
|
||||
'no_branding' => false,
|
||||
'transparent_background' => false,
|
||||
'uppercase_labels' => true,
|
||||
'webhook_url' => null,
|
||||
'cover_picture' => null,
|
||||
'logo_picture' => null,
|
||||
'database_fields_update' => null,
|
||||
'theme' => 'default',
|
||||
'use_captcha' => false,
|
||||
'password' => null,
|
||||
];
|
||||
|
||||
private array $fieldDefaults = [
|
||||
// 'name' => '' TODO: prevent name changing, use alias for column and keep original name as it is
|
||||
'hide_field_name' => false,
|
||||
'prefill' => null,
|
||||
'placeholder' => null,
|
||||
'help' => null,
|
||||
'file_upload' => false,
|
||||
'with_time' => null,
|
||||
'width' => 'full',
|
||||
'generates_uuid' => false,
|
||||
'generates_auto_increment_id' => false,
|
||||
'logic' => null,
|
||||
'allow_creation' => false
|
||||
];
|
||||
|
||||
private array $cleaningMessages = [
|
||||
// For form
|
||||
'notifies' => "Email notification were disabled.",
|
||||
'color' => "Form color set to default blue.",
|
||||
'hide_title' => "Title is not hidden.",
|
||||
'no_branding' => "OpenForm branding is not hidden.",
|
||||
'transparent_background' => "Transparent background was disabled.",
|
||||
'uppercase_labels' => "Labels use uppercase letters",
|
||||
'webhook_url' => "Webhook disabled.",
|
||||
'cover_picture' => 'The cover picture was removed.',
|
||||
'logo_picture' => 'The logo was removed.',
|
||||
'database_fields_update' => 'Form submission will only create new records (no updates).',
|
||||
'theme' => 'Default theme was applied.',
|
||||
|
||||
// For fields
|
||||
'hide_field_name' => 'Hide field name removed.',
|
||||
'prefill' => "Field prefill removed.",
|
||||
'placeholder' => "Empty text (placeholder) removed",
|
||||
'help' => "Help text removed.",
|
||||
'file_upload' => "Link field is not a file upload.",
|
||||
'with_time' => "Time was removed from date input.",
|
||||
'custom_block' => 'The custom block was removed.',
|
||||
'files' => 'The file upload file was hidden.',
|
||||
'relation' => 'The relation file was hidden.',
|
||||
'width' => 'The field width was set to full width',
|
||||
'allow_creation' => 'Select option creation was disabled.',
|
||||
|
||||
// Advanced fields
|
||||
'generates_uuid' => 'ID generation disabled.',
|
||||
'generates_auto_increment_id' => 'ID generation disabled.',
|
||||
|
||||
'use_captcha' => 'Captcha form protection was disabled.',
|
||||
|
||||
// Security & Privacy
|
||||
'password' => 'Password protection was disabled',
|
||||
|
||||
'logic' => 'Logic disabled for this property'
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns form data after request ingestion
|
||||
* @return array
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at least one cleaning was done
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCleaned(): bool
|
||||
{
|
||||
return count($this->cleanings) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the messages for each cleaning step performed
|
||||
*/
|
||||
public function getPerformedCleanings(): array
|
||||
{
|
||||
$cleaningMsgs = [];
|
||||
foreach ($this->cleanings as $key => $val) {
|
||||
$cleaningMsgs[$key] = collect($val)->map(function ($cleaning) {
|
||||
return $this->cleaningMessages[$cleaning];
|
||||
});
|
||||
}
|
||||
return $cleaningMsgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes form pro features from data if user isn't pro
|
||||
*/
|
||||
public function processRequest(UserFormRequest $request): FormCleaner
|
||||
{
|
||||
$data = $request->validated();
|
||||
$this->data = $this->commonCleaning($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create form cleaner instance from existing form
|
||||
*/
|
||||
public function processForm(Request $request, Form $form) : FormCleaner {
|
||||
$data = (new FormResource($form))->toArray($request);
|
||||
$this->data = $this->commonCleaning($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function isPro(Workspace $workspace) {
|
||||
return $workspace->is_pro;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dry run celanings
|
||||
* @param User|null $user
|
||||
*/
|
||||
public function simulateCleaning(Workspace $workspace): FormCleaner {
|
||||
if($this->isPro($workspace)) return $this;
|
||||
|
||||
$this->data = $this->removeProFeatures($this->data, true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Cleanigns
|
||||
* @param User|null $user
|
||||
* @return $this|array
|
||||
*/
|
||||
public function performCleaning(Workspace $workspace): FormCleaner
|
||||
{
|
||||
if($this->isPro($workspace)) return $this;
|
||||
|
||||
$this->data = $this->removeProFeatures($this->data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean all forms:
|
||||
* - Escape html of custom text block
|
||||
*/
|
||||
private function commonCleaning(array $data)
|
||||
{
|
||||
foreach ($data['properties'] as &$property) {
|
||||
if ($property['type'] == 'nf-text' && isset($property['content'])) {
|
||||
$property['content'] = Purify::clean($property['content']);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function removeProFeatures(array $data, $simulation = false)
|
||||
{
|
||||
$this->cleanForm($data, $simulation);
|
||||
$this->cleanProperties($data, $simulation);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function cleanForm(array &$data, $simulation = false): void
|
||||
{
|
||||
$this->clean($data, $this->formDefaults, $simulation);
|
||||
}
|
||||
|
||||
private function cleanProperties(array &$data, $simulation = false): void
|
||||
{
|
||||
foreach ($data['properties'] as $key => &$property) {
|
||||
// Remove pro custom blocks
|
||||
if (\Str::of($property['type'])->startsWith('nf-')) {
|
||||
$this->cleanings[$property['name']][] = 'custom_block';
|
||||
if (!$simulation) {
|
||||
unset($data['properties'][$key]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clean pro field options
|
||||
$this->cleanField($property, $this->fieldDefaults, $simulation);
|
||||
}
|
||||
}
|
||||
|
||||
private function clean(array &$data, array $defaults, $simulation = false): void
|
||||
{
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (Arr::get($data, $key) !== $value) {
|
||||
if (!isset($this->cleanings['form'])) $this->cleanings['form'] = [];
|
||||
$this->cleanings['form'][] = $key;
|
||||
|
||||
// If not a simulation, do the cleaning
|
||||
if (!$simulation) {
|
||||
Arr::set($data, $key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanField(array &$data, array $defaults, $simulation = false): void
|
||||
{
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (isset($data[$key]) && Arr::get($data, $key) !== $value) {
|
||||
$this->cleanings[$data['name']][] = $key;
|
||||
if (!$simulation) {
|
||||
Arr::set($data, $key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove pro types columns
|
||||
foreach (['files'] as $proType) {
|
||||
if ($data['type'] == $proType && (!isset($data['hidden']) || !$data['hidden'])) {
|
||||
$this->cleanings[$data['name']][] = $proType;
|
||||
if (!$simulation) {
|
||||
$data['hidden'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
292
app/Service/Forms/FormLogicConditionChecker.php
Normal file
292
app/Service/Forms/FormLogicConditionChecker.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Forms;
|
||||
|
||||
use Mockery\Matcher\Any;
|
||||
|
||||
use function PHPUnit\Framework\isEmpty;
|
||||
|
||||
class FormLogicConditionChecker
|
||||
{
|
||||
public function __construct(private ?array $conditions, private ?array $formData)
|
||||
{
|
||||
}
|
||||
|
||||
public static function conditionsMet(?array $conditions, array $formData): bool {
|
||||
return (new self($conditions, $formData))->conditionsAreMet($conditions, $formData);
|
||||
}
|
||||
|
||||
private function conditionsAreMet(?array $conditions, array $formData): bool {
|
||||
if (!$conditions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's not a group, just a single condition
|
||||
if (!isset($conditions['operatorIdentifier'])) {
|
||||
return $this->propertyConditionMet($conditions['value'], $formData[$conditions['value']['property_meta']['id']]);
|
||||
}
|
||||
|
||||
if ($conditions['operatorIdentifier'] === 'and') {
|
||||
$isvalid = true;
|
||||
foreach($conditions['children'] as $childrenCondition){
|
||||
if (!$this->conditionsMet($childrenCondition, $formData)) {
|
||||
$isvalid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $isvalid;
|
||||
} else if ($conditions['operatorIdentifier'] === 'or') {
|
||||
$isvalid = false;
|
||||
foreach($conditions['children'] as $childrenCondition){
|
||||
if ($this->conditionsMet($childrenCondition, $formData)) {
|
||||
$isvalid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $isvalid;
|
||||
}
|
||||
|
||||
throw new \Exception('Unexcepted operatorIdentifier:'. $conditions['operatorIdentifier']);
|
||||
}
|
||||
|
||||
private function propertyConditionMet(array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['property_meta']['type']) {
|
||||
case 'text':
|
||||
case 'url':
|
||||
case 'email':
|
||||
case 'phone_number':
|
||||
return $this->textConditionMet($propertyCondition, $value);
|
||||
case 'number':
|
||||
return $this->numberConditionMet($propertyCondition, $value);
|
||||
case 'checkbox':
|
||||
return $this->checkboxConditionMet($propertyCondition, $value);
|
||||
case 'select':
|
||||
return $this->selectConditionMet($propertyCondition, $value);
|
||||
case 'date':
|
||||
return $this->dateConditionMet($propertyCondition, $value);
|
||||
case 'multi_select':
|
||||
return $this->multiSelectConditionMet($propertyCondition, $value);
|
||||
case 'files':
|
||||
return $this->filesConditionMet($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function checkEquals ($condition, $fieldValue): bool {
|
||||
return $condition['value'] === $fieldValue;
|
||||
}
|
||||
|
||||
private function checkContains ($condition, $fieldValue): bool {
|
||||
return ($fieldValue && is_array($fieldValue)) ? in_array($condition['value'], $fieldValue) : false;
|
||||
}
|
||||
|
||||
private function checkListContains ($condition, $fieldValue): bool {
|
||||
return ($fieldValue) ? (count(array_intersect($condition['value'], $fieldValue)) === count($condition['value'])) : false;
|
||||
}
|
||||
|
||||
private function checkStartsWith ($condition, $fieldValue): bool {
|
||||
return str_starts_with($fieldValue, $condition['value']);
|
||||
}
|
||||
|
||||
private function checkendsWith ($condition, $fieldValue): bool {
|
||||
return str_ends_with($fieldValue, $condition['value']);
|
||||
}
|
||||
|
||||
private function checkIsEmpty ($condition, $fieldValue): bool {
|
||||
if(is_array($fieldValue)){
|
||||
return count($fieldValue) === 0;
|
||||
}
|
||||
return (!$fieldValue || $fieldValue !== '' || $fieldValue !== null);
|
||||
}
|
||||
|
||||
private function checkGreaterThan ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && (float)$fieldValue > (float)$condition['value']);
|
||||
}
|
||||
|
||||
private function checkGreaterThanEqual ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && (float)$fieldValue >= (float)$condition['value']);
|
||||
}
|
||||
|
||||
private function checkLessThan ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && (float)$fieldValue < (float)$condition['value']);
|
||||
}
|
||||
|
||||
private function checkLessThanEqual ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && (float)$fieldValue <= (float)$condition['value']);
|
||||
}
|
||||
|
||||
private function checkBefore ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && $fieldValue > $condition['value']);
|
||||
}
|
||||
|
||||
private function checkAfter ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && $fieldValue < $condition['value']);
|
||||
}
|
||||
|
||||
private function checkOnOrBefore ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && $fieldValue >= $condition['value']);
|
||||
}
|
||||
|
||||
private function checkOnOrAfter ($condition, $fieldValue): bool {
|
||||
return ($condition['value'] && $fieldValue && $fieldValue <= $condition['value']);
|
||||
}
|
||||
|
||||
private function checkPastWeek ($condition, $fieldValue): bool {
|
||||
if(!$fieldValue) return false;
|
||||
$fieldDate = date('Y-m-d', strtotime($fieldValue));
|
||||
return ($fieldDate <= now()->toDateString() && $fieldDate >= now()->subDays(7)->toDateString());
|
||||
}
|
||||
|
||||
private function checkPastMonth ($condition, $fieldValue): bool {
|
||||
if(!$fieldValue) return false;
|
||||
$fieldDate = date('Y-m-d', strtotime($fieldValue));
|
||||
return ($fieldDate <= now()->toDateString() && $fieldDate >= now()->subMonths(1)->toDateString());
|
||||
}
|
||||
|
||||
private function checkPastYear ($condition, $fieldValue): bool {
|
||||
if(!$fieldValue) return false;
|
||||
$fieldDate = date('Y-m-d', strtotime($fieldValue));
|
||||
return ($fieldDate <= now()->toDateString() && $fieldDate >= now()->subYears(1)->toDateString());
|
||||
}
|
||||
|
||||
private function checkNextWeek ($condition, $fieldValue): bool {
|
||||
if(!$fieldValue) return false;
|
||||
$fieldDate = date('Y-m-d', strtotime($fieldValue));
|
||||
return ($fieldDate >= now()->toDateString() && $fieldDate <= now()->addDays(7)->toDateString());
|
||||
}
|
||||
|
||||
private function checkNextMonth ($condition, $fieldValue): bool {
|
||||
if(!$fieldValue) return false;
|
||||
$fieldDate = date('Y-m-d', strtotime($fieldValue));
|
||||
return ($fieldDate >= now()->toDateString() && $fieldDate <= now()->addMonths(1)->toDateString());
|
||||
}
|
||||
|
||||
private function checkNextYear ($condition, $fieldValue): bool {
|
||||
if(!$fieldValue) return false;
|
||||
$fieldDate = date('Y-m-d', strtotime($fieldValue));
|
||||
return ($fieldDate >= now()->toDateString() && $fieldDate <= now()->addYears(1)->toDateString());
|
||||
}
|
||||
|
||||
|
||||
private function textConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'equals':
|
||||
return $this->checkEquals($propertyCondition, $value);
|
||||
case 'does_not_equal':
|
||||
return !$this->checkEquals($propertyCondition, $value);
|
||||
case 'contains':
|
||||
return $this->checkContains($propertyCondition, $value);
|
||||
case 'does_not_contain':
|
||||
return !$this->checkContains($propertyCondition, $value);
|
||||
case 'starts_with':
|
||||
return $this->checkStartsWith($propertyCondition, $value);
|
||||
case 'ends_with':
|
||||
return $this->checkendsWith($propertyCondition, $value);
|
||||
case 'is_empty':
|
||||
return $this->checkIsEmpty($propertyCondition, $value);
|
||||
case 'is_not_empty':
|
||||
return !$this->checkIsEmpty($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function numberConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'equals':
|
||||
return $this->checkEquals($propertyCondition, $value);
|
||||
case 'does_not_equal':
|
||||
return !$this->checkEquals($propertyCondition, $value);
|
||||
case 'greater_than':
|
||||
return $this->checkGreaterThan($propertyCondition, $value);
|
||||
case 'less_than':
|
||||
return $this->checkLessThan($propertyCondition, $value);
|
||||
case 'greater_than_or_equal_to':
|
||||
return $this->checkGreaterThanEqual($propertyCondition, $value);
|
||||
case 'less_than_or_equal_to':
|
||||
return $this->checkLessThanEqual($propertyCondition, $value);
|
||||
case 'is_empty':
|
||||
return $this->checkIsEmpty($propertyCondition, $value);
|
||||
case 'is_not_empty':
|
||||
return !$this->checkIsEmpty($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function checkboxConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'equals':
|
||||
return $this->checkEquals($propertyCondition, $value);
|
||||
case 'does_not_equal':
|
||||
return !$this->checkEquals($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function selectConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'equals':
|
||||
return $this->checkEquals($propertyCondition, $value);
|
||||
case 'does_not_equal':
|
||||
return !$this->checkEquals($propertyCondition, $value);
|
||||
case 'is_empty':
|
||||
return $this->checkIsEmpty($propertyCondition, $value);
|
||||
case 'is_not_empty':
|
||||
return !$this->checkIsEmpty($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function dateConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'equals':
|
||||
return $this->checkEquals($propertyCondition, $value);
|
||||
case 'before':
|
||||
return $this->checkBefore($propertyCondition, $value);
|
||||
case 'after':
|
||||
return $this->checkAfter($propertyCondition, $value);
|
||||
case 'on_or_before':
|
||||
return $this->checkOnOrBefore($propertyCondition, $value);
|
||||
case 'on_or_after':
|
||||
return $this->checkOnOrAfter($propertyCondition, $value);
|
||||
case 'is_empty':
|
||||
return $this->checkIsEmpty($propertyCondition, $value);
|
||||
case 'past_week':
|
||||
return $this->checkPastWeek($propertyCondition, $value);
|
||||
case 'past_month':
|
||||
return $this->checkPastMonth($propertyCondition, $value);
|
||||
case 'past_year':
|
||||
return $this->checkPastYear($propertyCondition, $value);
|
||||
case 'next_week':
|
||||
return $this->checkNextWeek($propertyCondition, $value);
|
||||
case 'next_month':
|
||||
return $this->checkNextMonth($propertyCondition, $value);
|
||||
case 'next_year':
|
||||
return $this->checkNextYear($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function multiSelectConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'contains':
|
||||
return $this->checkListContains($propertyCondition, $value);
|
||||
case 'does_not_contain':
|
||||
return !$this->checkListContains($propertyCondition, $value);
|
||||
case 'is_empty':
|
||||
return $this->checkIsEmpty($propertyCondition, $value);
|
||||
case 'is_not_empty':
|
||||
return !$this->checkIsEmpty($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function filesConditionMet (array $propertyCondition, $value): bool {
|
||||
switch ($propertyCondition['operator']) {
|
||||
case 'is_empty':
|
||||
return $this->checkIsEmpty($propertyCondition, $value);
|
||||
case 'is_not_empty':
|
||||
return !$this->checkIsEmpty($propertyCondition, $value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
45
app/Service/Forms/FormLogicPropertyResolver.php
Normal file
45
app/Service/Forms/FormLogicPropertyResolver.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Forms;
|
||||
|
||||
use App\Service\Forms\FormLogicConditionChecker;
|
||||
|
||||
class FormLogicPropertyResolver
|
||||
{
|
||||
|
||||
private $property = [];
|
||||
private $formData = [];
|
||||
private $logic = false;
|
||||
|
||||
public function __construct(private array $prop, private array $values)
|
||||
{
|
||||
$this->property = $prop;
|
||||
$this->formData = $values;
|
||||
$this->logic = isset($this->property['logic']) ? $this->property['logic'] : false;
|
||||
}
|
||||
|
||||
public static function isRequired(array $property, array $values): bool
|
||||
{
|
||||
return (new self($property, $values))->shouldBeRequired();
|
||||
}
|
||||
|
||||
public function shouldBeRequired(): bool
|
||||
{
|
||||
if(!isset($this->property['required'])){
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->logic) {
|
||||
return $this->property['required'];
|
||||
}
|
||||
|
||||
$conditionsMet = FormLogicConditionChecker::conditionsMet($this->logic['conditions'], $this->formData);
|
||||
if ($conditionsMet && $this->property['required'] && count($this->logic['actions']) > 0 && in_array('make-it-optional', $this->logic['actions'])) {
|
||||
return false;
|
||||
} else if ($conditionsMet && !$this->property['required'] && count($this->logic['actions']) > 0 && in_array('require-answer', $this->logic['actions'])) {
|
||||
return true;
|
||||
} else {
|
||||
return $this->property['required'];
|
||||
}
|
||||
}
|
||||
}
|
||||
194
app/Service/Forms/FormSubmissionFormatter.php
Normal file
194
app/Service/Forms/FormSubmissionFormatter.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Service\Forms;
|
||||
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Service\WorkspaceHelper;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class FormSubmissionFormatter
|
||||
{
|
||||
|
||||
/**
|
||||
* If true, creates html <a> links for emails and urls
|
||||
* @var bool
|
||||
*/
|
||||
private $createLinks = false;
|
||||
|
||||
/**
|
||||
* If true, serialize arrays
|
||||
* @var bool
|
||||
*/
|
||||
private $outputStringsOnly = false;
|
||||
|
||||
private $showHiddenFields = false;
|
||||
|
||||
private $setEmptyForNoValue = false;
|
||||
|
||||
public function __construct(private Form $form, private array $formData)
|
||||
{
|
||||
}
|
||||
|
||||
public function createLinks()
|
||||
{
|
||||
$this->createLinks = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function showHiddenFields()
|
||||
{
|
||||
$this->showHiddenFields = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function outputStringsOnly()
|
||||
{
|
||||
$this->outputStringsOnly = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEmptyForNoValue()
|
||||
{
|
||||
$this->setEmptyForNoValue = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a nice "FieldName": "Field Response" array
|
||||
* - If createLink enabled, returns html link for emails and links
|
||||
* Used for CSV exports
|
||||
*/
|
||||
public function getCleanKeyValue()
|
||||
{
|
||||
$data = $this->formData;
|
||||
$fields = $this->form->properties;
|
||||
|
||||
$returnArray = [];
|
||||
foreach ($fields as &$field) {
|
||||
// If not present skip
|
||||
if (!isset($data[$field['id']])) {
|
||||
if ($this->setEmptyForNoValue) {
|
||||
$returnArray[$field['name']] = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If should hide hidden fields
|
||||
if (!$this->showHiddenFields) {
|
||||
if (isset($field['hidden']) && $field['hidden']) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->createLinks && $field['type'] == 'url') {
|
||||
$returnArray[$field['name']] = '<a href="'.$data[$field['id']].'">'.$data[$field['id']].'</a>';
|
||||
} elseif ($this->createLinks && $field['type'] == 'email') {
|
||||
$returnArray[$field['name']] = '<a href="mailto:'.$data[$field['id']].'">'.$data[$field['id']].'</a>';
|
||||
} elseif ($field['type'] == 'multi_select') {
|
||||
$val = $data[$field['id']];
|
||||
if ($this->outputStringsOnly) {
|
||||
$returnArray[$field['name']] = implode(', ', $val);
|
||||
} else {
|
||||
$returnArray[$field['name']] = $val;
|
||||
}
|
||||
} elseif ($field['type'] == 'files') {
|
||||
if ($this->outputStringsOnly) {
|
||||
$formId = $this->form->id;
|
||||
$returnArray[$field['name']] = implode(', ',
|
||||
collect($data[$field['id']])->map(function ($file) use ($formId) {
|
||||
return route('open.forms.submissions.file', [$formId, $file]);
|
||||
})->toArray()
|
||||
);
|
||||
} else {
|
||||
$formId = $this->form->id;
|
||||
$returnArray[$field['name']] = collect($data[$field['id']])->map(function ($file) use ($formId) {
|
||||
return [
|
||||
'file_url' => route('open.forms.submissions.file', [$formId, $file]),
|
||||
'file_name' => $file,
|
||||
];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (is_array($data[$field['id']])) {
|
||||
$data[$field['id']] = implode(', ', $data[$field['id']]);
|
||||
}
|
||||
$returnArray[$field['name']] = $data[$field['id']];
|
||||
}
|
||||
}
|
||||
return $returnArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of fields, with a filled value attribute.
|
||||
* Used for humans.
|
||||
*/
|
||||
public function getFieldsWithValue()
|
||||
{
|
||||
$data = $this->formData;
|
||||
$fields = $this->form->properties;
|
||||
$transformedFields = [];
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($field['id']) || !isset($data[$field['id']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If hide hidden fields
|
||||
if (!$this->showHiddenFields) {
|
||||
if (isset($field['hidden']) && $field['hidden']) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->createLinks && $field['type'] == 'url') {
|
||||
$field['value'] = '<a href="'.$data[$field['id']].'">'.$data[$field['id']].'</a>';
|
||||
} elseif ($this->createLinks && $field['type'] == 'email') {
|
||||
$field['value'] = '<a href="mailto:'.$data[$field['id']].'">'.$data[$field['id']].'</a>';
|
||||
} elseif ($field['type'] == 'checkbox') {
|
||||
$field['value'] = $data[$field['id']] ? 'Yes' : 'No';
|
||||
} elseif ($field['type'] == 'date') {
|
||||
if (is_array($data[$field['id']])) {
|
||||
$field['value'] = isset($data[$field['id']][1]) ? (new Carbon($data[$field['id']][0]))->format('d/m/Y')
|
||||
.' - '.(new Carbon($data[$field['id']][1]))->format('d/m/Y') : (new Carbon($data[$field['id']][0]))->format('d/m/Y');
|
||||
} else {
|
||||
$field['value'] = (new Carbon($data[$field['id']]))->format((isset($field['with_time']) && $field['with_time']) ? 'd/m/Y H:i' : 'd/m/Y');
|
||||
}
|
||||
} elseif ($field['type'] == 'multi_select') {
|
||||
$val = $data[$field['id']];
|
||||
if ($this->outputStringsOnly) {
|
||||
$field['value'] = implode(', ', $val);
|
||||
} else {
|
||||
$field['value'] = $val;
|
||||
}
|
||||
} elseif ($field['type'] == 'files') {
|
||||
if ($this->outputStringsOnly) {
|
||||
$formId = $this->form->id;
|
||||
$field['value'] = implode(', ',
|
||||
collect($data[$field['id']])->map(function ($file) use ($formId) {
|
||||
return route('open.forms.submissions.file', [$formId, $file]);
|
||||
})->toArray()
|
||||
);
|
||||
} else {
|
||||
$formId = $this->form->id;
|
||||
$field['value'] = collect($data[$field['id']])->map(function ($file) use ($formId) {
|
||||
return [
|
||||
'file_url' => route('open.forms.submissions.file', [$formId, $file]),
|
||||
'file_name' => $file,
|
||||
];
|
||||
});
|
||||
|
||||
}
|
||||
} else {
|
||||
if (is_array($data[$field['id']]) && $this->outputStringsOnly) {
|
||||
$field['value'] = implode(', ', $data[$field['id']]);
|
||||
} else {
|
||||
$field['value'] = $data[$field['id']];
|
||||
}
|
||||
}
|
||||
$transformedFields[] = $field;
|
||||
}
|
||||
return $transformedFields;
|
||||
}
|
||||
|
||||
}
|
||||
24
app/Service/HtmlPurifier/HTMLPurifier_URIScheme_notion.php
Normal file
24
app/Service/HtmlPurifier/HTMLPurifier_URIScheme_notion.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Service\HtmlPurifier;
|
||||
|
||||
|
||||
use HTMLPurifier_Config;
|
||||
use HTMLPurifier_Context;
|
||||
use HTMLPurifier_URI;
|
||||
|
||||
class HTMLPurifier_URIScheme_notion extends \HTMLPurifier_URIScheme
|
||||
{
|
||||
public $browsable = true;
|
||||
public $may_omit_host = true;
|
||||
|
||||
public function doValidate(&$uri, $config, $context)
|
||||
{
|
||||
if ($uri->host == 'www.notion.so' || $uri->host == 'notion.so') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
app/Service/Storage/StorageFileNameParser.php
Normal file
69
app/Service/Storage/StorageFileNameParser.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Storage;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Used
|
||||
* File can have 2 formats:
|
||||
* - file_name-{uuid}.{ext}
|
||||
* - {uuid}
|
||||
*/
|
||||
class StorageFileNameParser
|
||||
{
|
||||
public ?string $uuid = null;
|
||||
public ?string $fileName = null;
|
||||
public ?string $extension = null;
|
||||
|
||||
public function __construct(string $fileName)
|
||||
{
|
||||
$this->parseFileName($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have parsed a file name and an extension, we keep the same and append uuid to avoid collisions
|
||||
* Otherwise we just return the uuid
|
||||
* @return string
|
||||
*/
|
||||
public function getMovedFileName(): ?string
|
||||
{
|
||||
if ($this->fileName && $this->extension) {
|
||||
return substr($this->fileName,0,50).'_'.$this->uuid.'.'.$this->extension;
|
||||
}
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
private function parseFileName(string $fileName)
|
||||
{
|
||||
if (Str::isUuid($fileName)) {
|
||||
$this->uuid = $fileName;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_contains($fileName, '_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$candidateString = substr($fileName, strrpos($fileName, '_') + 1);
|
||||
if (!str_contains($candidateString, '.')
|
||||
|| !Str::isUuid(substr($candidateString, 0, strpos($candidateString, '.')))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->uuid = substr($candidateString, 0, strpos($candidateString, '.'));
|
||||
$this->fileName = substr($fileName, 0, strrpos($fileName, '_'));
|
||||
// get everything after the last dot
|
||||
$this->extension = substr($candidateString, strrpos($candidateString, '.') + 1);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static function parse(string $fileName): self
|
||||
{
|
||||
return new self($fileName);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user