Initial commit

This commit is contained in:
Julien Nahum
2022-09-20 21:59:52 +02:00
commit f8e6cd4dd6
479 changed files with 77078 additions and 0 deletions

View 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
]);
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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.'
]);
}
}

View 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')]);
}
}

View 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');
});
}
}

View 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);
}
}

View 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);
}
}

View 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)));
}
}

View 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;
}
}

View 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())
);
}
}

View File

@@ -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.',
]);
}
}

View 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
]));
}
}

View 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);
}
}

View 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),
]);
}
}

View 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);
}
}

View 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');
}
}

View 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');
}
}

View 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';
}
}

View 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
View 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,
];
}

View 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');
}
}
}

View 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;
}
}

View 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 = [
//
];
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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 = [
//
];
}

View 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);
}
}

View 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;
}
}
}

View 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);
}
}

View 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(),
];
}
}

View 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;
}

View 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'
];
}

View 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);
}
}

View File

@@ -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,
]);
}
}

View 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',
]);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class UpdateFormRequest extends UserFormRequest
{
}

View 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'
])],
];
}
}

View 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.',
];
}
}

View 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' => []
];
}
}

View 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;
}
}

View 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,
];
}
}