Lint PHP code psr-12, add GH action

This commit is contained in:
Julien Nahum 2024-02-23 11:54:12 +01:00
parent e85e4df7fe
commit 62971a2ef4
226 changed files with 2338 additions and 2144 deletions

View File

@ -6,6 +6,47 @@ on:
pull_request: pull_request:
jobs: jobs:
code_lint:
runs-on: ubuntu-latest
name: Run code linters
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Get Composer cache directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: dom, curl, mbstring, pdo, sqlite, pdo_sqlite
tools: composer:v2
coverage: none
- name: Prepare the environment
run: cp .env.example .env
- name: Install composer dependencies
run: composer install --prefer-dist --no-interaction --no-progress --ignore-platform-reqs --optimize-autoloader
- name: Directory permissions
run: chmod -R 777 storage bootstrap/cache
- name: Run PHP lint check
run: ./vendor/bin/pint --test
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -155,7 +196,7 @@ jobs:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
production-deploy: production-deploy:
needs: [ tests, build-nuxt-app ] needs: [ code_lint, tests, build-nuxt-app ]
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push' if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Triggers Deployment (Vapor & Amplify) name: Triggers Deployment (Vapor & Amplify)

View File

@ -3,9 +3,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Forms\FormStatistic; use App\Models\Forms\FormStatistic;
use Illuminate\Console\Command;
use App\Models\Forms\FormView;
use App\Models\Forms\FormSubmission; use App\Models\Forms\FormSubmission;
use App\Models\Forms\FormView;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class CleanDatabase extends Command class CleanDatabase extends Command
@ -51,13 +51,13 @@ class CleanDatabase extends Command
->orderBy('date') ->orderBy('date')
->groupBy('form_id', 'date') ->groupBy('form_id', 'date')
->get()->each(function ($row) use (&$finalData) { ->get()->each(function ($row) use (&$finalData) {
$finalData[$row->form_id."-".$row->date] = [ $finalData[$row->form_id.'-'.$row->date] = [
'form_id' => $row->form_id, 'form_id' => $row->form_id,
'date' => $row->date, 'date' => $row->date,
'data' => [ 'data' => [
'views' => $row->views, 'views' => $row->views,
'submissions' => 0 'submissions' => 0,
] ],
]; ];
}); });
@ -68,7 +68,7 @@ class CleanDatabase extends Command
->orderBy('date') ->orderBy('date')
->groupBy('form_id', 'date') ->groupBy('form_id', 'date')
->get()->each(function ($row) use (&$finalData) { ->get()->each(function ($row) use (&$finalData) {
$key = $row->form_id."-".$row->date; $key = $row->form_id.'-'.$row->date;
if (isset($finalData[$key])) { if (isset($finalData[$key])) {
$finalData[$key]['data']['submissions'] = $row->submissions; $finalData[$key]['data']['submissions'] = $row->submissions;
} else { } else {
@ -77,8 +77,8 @@ class CleanDatabase extends Command
'date' => $row->date, 'date' => $row->date,
'data' => [ 'data' => [
'views' => 0, 'views' => 0,
'submissions' => $row->submissions 'submissions' => $row->submissions,
] ],
]; ];
} }

View File

@ -24,9 +24,9 @@ class GenerateTemplate extends Command
*/ */
protected $description = 'Generates a new form template from a prompt'; protected $description = 'Generates a new form template from a prompt';
const MAX_RELATED_TEMPLATES = 8; public const MAX_RELATED_TEMPLATES = 8;
const FORM_STRUCTURE_PROMPT = <<<EOD public const FORM_STRUCTURE_PROMPT = <<<'EOD'
You are an AI assistant for OpnForm, a form builder and your job is to build a form for our user. You are an AI assistant for OpnForm, a form builder and your job is to build a form for our user.
Forms are represented as Json objects. Here's an example form: Forms are represented as Json objects. Here's an example form:
@ -157,7 +157,7 @@ class GenerateTemplate extends Command
Do not ask me for more information about required properties or types, only suggest me a form structure. Do not ask me for more information about required properties or types, only suggest me a form structure.
EOD; EOD;
const FORM_DESCRIPTION_PROMPT = <<<EOD public const FORM_DESCRIPTION_PROMPT = <<<'EOD'
You are an AI assistant for OpnForm, a form builder and your job is to help us build form templates for our users. You are an AI assistant for OpnForm, a form builder and your job is to help us build form templates for our users.
Give me some valid html code (using only h2, p, ul, li html tags) for the following form template page: "[REPLACE]". Give me some valid html code (using only h2, p, ul, li html tags) for the following form template page: "[REPLACE]".
@ -169,12 +169,12 @@ class GenerateTemplate extends Command
Each paragraph (except for the first one) MUST start with with a h2 tag containing a title for this paragraph. Each paragraph (except for the first one) MUST start with with a h2 tag containing a title for this paragraph.
EOD; EOD;
const FORM_SHORT_DESCRIPTION_PROMPT = <<<EOD public const FORM_SHORT_DESCRIPTION_PROMPT = <<<'EOD'
I own a form builder online named OpnForm. It's free to use. I own a form builder online named OpnForm. It's free to use.
Give me a 1 sentence description for the following form template page: "[REPLACE]". It should be short and concise, but still explain what the form is about. Give me a 1 sentence description for the following form template page: "[REPLACE]". It should be short and concise, but still explain what the form is about.
EOD; EOD;
const FORM_INDUSTRY_PROMPT = <<<EOD public const FORM_INDUSTRY_PROMPT = <<<'EOD'
You are an AI assistant for OpnForm, a form builder and your job is to help us build form templates for our users. You are an AI assistant for OpnForm, a form builder and your job is to help us build form templates for our users.
I am creating a form template: "[REPLACE]". You must assign the template to industries. Return a list of industries (minimum 1, maximum 3 but only if very relevant) and order them by relevance (most relevant first). I am creating a form template: "[REPLACE]". You must assign the template to industries. Return a list of industries (minimum 1, maximum 3 but only if very relevant) and order them by relevance (most relevant first).
@ -185,7 +185,7 @@ class GenerateTemplate extends Command
Ex: { "industries": ["banking_forms","customer_service_forms"]} Ex: { "industries": ["banking_forms","customer_service_forms"]}
EOD; EOD;
const FORM_TYPES_PROMPT = <<<EOD public const FORM_TYPES_PROMPT = <<<'EOD'
You are an AI assistant for OpnForm, a form builder and your job is to help us build form templates for our users. You are an AI assistant for OpnForm, a form builder and your job is to help us build form templates for our users.
I am creating a form template: "[REPLACE]". You must assign the template to one or more types. Return a list of types (minimum 1, maximum 3 but only if very accurate) and order them by relevance (most relevant first). I am creating a form template: "[REPLACE]". You must assign the template to one or more types. Return a list of types (minimum 1, maximum 3 but only if very accurate) and order them by relevance (most relevant first).
@ -196,17 +196,17 @@ class GenerateTemplate extends Command
Ex: { "types": ["consent_forms","award_forms"]} Ex: { "types": ["consent_forms","award_forms"]}
EOD; EOD;
const FORM_QAS_PROMPT = <<<EOD public const FORM_QAS_PROMPT = <<<'EOD'
Now give me 4 to 6 question and answers to put on the form template page. The questions should be about the reasons for this template (when to use, why, target audience, goal etc.). Now give me 4 to 6 question and answers to put on the form template page. The questions should be about the reasons for this template (when to use, why, target audience, goal etc.).
The questions should also explain why OpnForm is the best option to create this form (open-source, free to use, integrations etc). The questions should also explain why OpnForm is the best option to create this form (open-source, free to use, integrations etc).
Reply only with a valid JSON, being an array of object containing the keys "question" and "answer". Reply only with a valid JSON, being an array of object containing the keys "question" and "answer".
EOD; EOD;
const FORM_TITLE_PROMPT = <<<EOD public const FORM_TITLE_PROMPT = <<<'EOD'
Finally give me a title for the template. It must contain or end with "template". It should be short and to the point, without any quotes. Finally give me a title for the template. It must contain or end with "template". It should be short and to the point, without any quotes.
EOD; EOD;
const FORM_IMG_KEYWORDS_PROMPT = <<<EOD public const FORM_IMG_KEYWORDS_PROMPT = <<<'EOD'
I want to add an image to illustrate this form template page. Give me a relevant search query for unsplash. Reply only with a valid JSON like this: I want to add an image to illustrate this form template page. Give me a relevant search query for unsplash. Reply only with a valid JSON like this:
```json ```json
{ {
@ -227,14 +227,14 @@ class GenerateTemplate extends Command
->useStreaming() ->useStreaming()
->setSystemMessage('You are an assistant helping to generate forms.'); ->setSystemMessage('You are an assistant helping to generate forms.');
$completer->expectsJson()->completeChat([ $completer->expectsJson()->completeChat([
["role" => "user", "content" => Str::of(self::FORM_STRUCTURE_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()] ['role' => 'user', 'content' => Str::of(self::FORM_STRUCTURE_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()],
]); ]);
$formData = $completer->getArray(); $formData = $completer->getArray();
$completer->doesNotExpectJson(); $completer->doesNotExpectJson();
$formDescriptionPrompt = Str::of(self::FORM_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString(); $formDescriptionPrompt = Str::of(self::FORM_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString();
$formShortDescription = $completer->completeChat([ $formShortDescription = $completer->completeChat([
["role" => "user", "content" => Str::of(self::FORM_SHORT_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()] ['role' => 'user', 'content' => Str::of(self::FORM_SHORT_DESCRIPTION_PROMPT)->replace('[REPLACE]', $this->argument('prompt'))->toString()],
])->getString(); ])->getString();
// If description is between quotes, remove quotes // If description is between quotes, remove quotes
$formShortDescription = Str::of($formShortDescription)->replaceMatches('/^"(.*)"$/', '$1')->toString(); $formShortDescription = Str::of($formShortDescription)->replaceMatches('/^"(.*)"$/', '$1')->toString();
@ -250,30 +250,29 @@ class GenerateTemplate extends Command
// Now get description and QAs // Now get description and QAs
$completer->doesNotExpectJson(); $completer->doesNotExpectJson();
$formDescription = $completer->completeChat([ $formDescription = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt] ['role' => 'user', 'content' => $formDescriptionPrompt],
])->getHtml(); ])->getHtml();
$completer->expectsJson(); $completer->expectsJson();
$formCoverKeywords = $completer->completeChat([ $formCoverKeywords = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt], ['role' => 'user', 'content' => $formDescriptionPrompt],
["role" => "assistant", "content" => $formDescription], ['role' => 'assistant', 'content' => $formDescription],
["role" => "user", "content" => self::FORM_IMG_KEYWORDS_PROMPT] ['role' => 'user', 'content' => self::FORM_IMG_KEYWORDS_PROMPT],
])->getArray(); ])->getArray();
$imageUrl = $this->getImageCoverUrl($formCoverKeywords['search_query']); $imageUrl = $this->getImageCoverUrl($formCoverKeywords['search_query']);
$formQAs = $completer->completeChat([ $formQAs = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt], ['role' => 'user', 'content' => $formDescriptionPrompt],
["role" => "assistant", "content" => $formDescription], ['role' => 'assistant', 'content' => $formDescription],
["role" => "user", "content" => self::FORM_QAS_PROMPT] ['role' => 'user', 'content' => self::FORM_QAS_PROMPT],
])->getArray(); ])->getArray();
$completer->doesNotExpectJson(); $completer->doesNotExpectJson();
$formTitle = $completer->completeChat([ $formTitle = $completer->completeChat([
["role" => "user", "content" => $formDescriptionPrompt], ['role' => 'user', 'content' => $formDescriptionPrompt],
["role" => "assistant", "content" => $formDescription], ['role' => 'assistant', 'content' => $formDescription],
["role" => "user", "content" => self::FORM_TITLE_PROMPT] ['role' => 'user', 'content' => self::FORM_TITLE_PROMPT],
])->getString(); ])->getString();
$template = $this->createFormTemplate( $template = $this->createFormTemplate(
$formData, $formData,
$formTitle, $formTitle,
@ -304,28 +303,31 @@ class GenerateTemplate extends Command
if (isset($response['results'][$photoIndex]['urls']['regular'])) { if (isset($response['results'][$photoIndex]['urls']['regular'])) {
return Str::of($response['results'][$photoIndex]['urls']['regular'])->replace('w=1080', 'w=600')->toString(); return Str::of($response['results'][$photoIndex]['urls']['regular'])->replace('w=1080', 'w=600')->toString();
} }
return null; return null;
} }
private function getIndustries(GptCompleter $completer, string $formPrompt): array private function getIndustries(GptCompleter $completer, string $formPrompt): array
{ {
$industriesString = Template::getAllIndustries()->pluck('slug')->join(', '); $industriesString = Template::getAllIndustries()->pluck('slug')->join(', ');
return $completer->completeChat([ return $completer->completeChat([
["role" => "user", "content" => Str::of(self::FORM_INDUSTRY_PROMPT) ['role' => 'user', 'content' => Str::of(self::FORM_INDUSTRY_PROMPT)
->replace('[REPLACE]', $formPrompt) ->replace('[REPLACE]', $formPrompt)
->replace('[INDUSTRIES]', $industriesString) ->replace('[INDUSTRIES]', $industriesString)
->toString()] ->toString()],
])->getArray()['industries']; ])->getArray()['industries'];
} }
private function getTypes(GptCompleter $completer, string $formPrompt): array private function getTypes(GptCompleter $completer, string $formPrompt): array
{ {
$typesString = Template::getAllTypes()->pluck('slug')->join(', '); $typesString = Template::getAllTypes()->pluck('slug')->join(', ');
return $completer->completeChat([ return $completer->completeChat([
["role" => "user", "content" => Str::of(self::FORM_TYPES_PROMPT) ['role' => 'user', 'content' => Str::of(self::FORM_TYPES_PROMPT)
->replace('[REPLACE]', $formPrompt) ->replace('[REPLACE]', $formPrompt)
->replace('[TYPES]', $typesString) ->replace('[TYPES]', $typesString)
->toString()] ->toString()],
])->getArray()['types']; ])->getArray()['types'];
} }
@ -344,6 +346,7 @@ class GenerateTemplate extends Command
} }
}); });
arsort($templateScore); // Sort by Score arsort($templateScore); // Sort by Score
return array_slice(array_keys($templateScore), 0, self::MAX_RELATED_TEMPLATES); return array_slice(array_keys($templateScore), 0, self::MAX_RELATED_TEMPLATES);
} }
@ -357,8 +360,7 @@ class GenerateTemplate extends Command
array $industry, array $industry,
array $types, array $types,
array $relatedTemplates array $relatedTemplates
) ) {
{
// Add property uuids, improve form with options // Add property uuids, improve form with options
foreach ($formData['properties'] as &$property) { foreach ($formData['properties'] as &$property) {
$property['id'] = Str::uuid()->toString(); // Column ID $property['id'] = Str::uuid()->toString(); // Column ID
@ -387,13 +389,15 @@ class GenerateTemplate extends Command
'publicly_listed' => true, 'publicly_listed' => true,
'industries' => $industry, 'industries' => $industry,
'types' => $types, 'types' => $types,
'related_templates' => $relatedTemplates 'related_templates' => $relatedTemplates,
]); ]);
} }
private function setReverseRelatedTemplates(Template $newTemplate) private function setReverseRelatedTemplates(Template $newTemplate)
{ {
if (!$newTemplate || count($newTemplate->related_templates) === 0) return; if (! $newTemplate || count($newTemplate->related_templates) === 0) {
return;
}
$templates = Template::whereIn('slug', $newTemplate->related_templates)->get(); $templates = Template::whereIn('slug', $newTemplate->related_templates)->get();
foreach ($templates as $template) { foreach ($templates as $template) {

View File

@ -27,34 +27,34 @@ class GenerateTaxExport extends Command
*/ */
protected $description = 'Compute Stripe VAT per country'; protected $description = 'Compute Stripe VAT per country';
const EU_TAX_RATES = [ public const EU_TAX_RATES = [
"AT" => 20, 'AT' => 20,
"BE" => 21, 'BE' => 21,
"BG" => 20, 'BG' => 20,
"HR" => 25, 'HR' => 25,
"CY" => 19, 'CY' => 19,
"CZ" => 21, 'CZ' => 21,
"DK" => 25, 'DK' => 25,
"EE" => 20, 'EE' => 20,
"FI" => 24, 'FI' => 24,
"FR" => 20, 'FR' => 20,
"DE" => 19, 'DE' => 19,
"GR" => 24, 'GR' => 24,
"HU" => 27, 'HU' => 27,
"IE" => 23, 'IE' => 23,
"IT" => 22, 'IT' => 22,
"LV" => 21, 'LV' => 21,
"LT" => 21, 'LT' => 21,
"LU" => 17, 'LU' => 17,
"MT" => 18, 'MT' => 18,
"NL" => 21, 'NL' => 21,
"PL" => 23, 'PL' => 23,
"PT" => 23, 'PT' => 23,
"RO" => 19, 'RO' => 19,
"SK" => 20, 'SK' => 20,
"SI" => 22, 'SI' => 22,
"ES" => 21, 'ES' => 21,
"SE" => 25 'SE' => 25,
]; ];
/** /**
@ -71,11 +71,13 @@ class GenerateTaxExport extends Command
// Validate the date format // Validate the date format
if ($startDate && ! Carbon::createFromFormat('Y-m-d', $startDate)) { if ($startDate && ! Carbon::createFromFormat('Y-m-d', $startDate)) {
$this->error('Invalid start date format. Use YYYY-MM-DD.'); $this->error('Invalid start date format. Use YYYY-MM-DD.');
return Command::FAILURE; return Command::FAILURE;
} }
if ($endDate && ! Carbon::createFromFormat('Y-m-d', $endDate)) { if ($endDate && ! Carbon::createFromFormat('Y-m-d', $endDate)) {
$this->error('Invalid end date format. Use YYYY-MM-DD.'); $this->error('Invalid end date format. Use YYYY-MM-DD.');
return Command::FAILURE; return Command::FAILURE;
} elseif (! $endDate && $this->option('full-month')) { } elseif (! $endDate && $this->option('full-month')) {
$endDate = Carbon::parse($startDate)->endOfMonth()->endOfDay()->format('Y-m-d'); $endDate = Carbon::parse($startDate)->endOfMonth()->endOfDay()->format('Y-m-d');
@ -111,6 +113,7 @@ class GenerateTaxExport extends Command
// Ignore if payment was refunded // Ignore if payment was refunded
if (($invoice->payment_intent->status ?? null) !== 'succeeded') { if (($invoice->payment_intent->status ?? null) !== 'succeeded') {
$paymentNotSuccessfulCount++; $paymentNotSuccessfulCount++;
continue; continue;
} }
@ -163,7 +166,7 @@ class GenerateTaxExport extends Command
]; ];
$aggregatedReport[$country] = [ $aggregatedReport[$country] = [
'individual' => $defaultVal, 'individual' => $defaultVal,
'business' => $defaultVal 'business' => $defaultVal,
]; ];
} }
$aggregatedReport[$country][$customerType]['count']++; $aggregatedReport[$country][$customerType]['count']++;
@ -181,10 +184,11 @@ class GenerateTaxExport extends Command
$finalReport[] = [ $finalReport[] = [
'country' => $country, 'country' => $country,
'customer_type' => $customerType, 'customer_type' => $customerType,
...$aggData ...$aggData,
]; ];
} }
} }
return $finalReport; return $finalReport;
} }
@ -226,7 +230,9 @@ class GenerateTaxExport extends Command
if ($taxRate = (self::EU_TAX_RATES[$countryCode] ?? null)) { if ($taxRate = (self::EU_TAX_RATES[$countryCode] ?? null)) {
// If VAT ID is provided, then TAX is 0% // If VAT ID is provided, then TAX is 0%
if (!$vatId) return $taxRate; if (! $vatId) {
return $taxRate;
}
} }
return 0; return 0;
@ -241,12 +247,11 @@ class GenerateTaxExport extends Command
{ {
if (count($data) == 0) { if (count($data) == 0) {
$this->info('Empty data. No file generated.'); $this->info('Empty data. No file generated.');
return; return;
} }
(new ArrayExport($data))->store($filename, 'local', \Maatwebsite\Excel\Excel::XLSX); (new ArrayExport($data))->store($filename, 'local', \Maatwebsite\Excel\Excel::XLSX);
$this->line('File generated: '.storage_path('app/'.$filename)); $this->line('File generated: '.storage_path('app/'.$filename));
} }
} }

View File

@ -19,7 +19,6 @@ class Kernel extends ConsoleKernel
/** /**
* Define the application's command schedule. * Define the application's command schedule.
* *
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void * @return void
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)

View File

@ -3,19 +3,18 @@
namespace App\Events\Forms; namespace App\Events\Forms;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class FormSubmitted class FormSubmitted
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
public $form; public $form;
public $data; public $data;
/** /**

View File

@ -3,17 +3,15 @@
namespace App\Events\Models; namespace App\Events\Models;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class FormCreated class FormCreated
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** /**
* Create a new event instance. * Create a new event instance.
@ -21,5 +19,6 @@ class FormCreated
* @return void * @return void
*/ */
public function __construct(public Form $form) public function __construct(public Form $form)
{} {
}
} }

View File

@ -78,6 +78,7 @@ class Handler extends ExceptionHandler
return false; return false;
} }
} }
return true; return true;
} }
} }

View File

@ -7,7 +7,6 @@ use Maatwebsite\Excel\Concerns\WithHeadingRow;
class FormSubmissionExport implements FromArray, WithHeadingRow class FormSubmissionExport implements FromArray, WithHeadingRow
{ {
protected array $submissionData; protected array $submissionData;
public function __construct(array $submissionData) public function __construct(array $submissionData)
@ -23,7 +22,7 @@ class FormSubmissionExport implements FromArray, WithHeadingRow
$this->submissionData = [ $this->submissionData = [
$headingRow, $headingRow,
$contentRow $contentRow,
]; ];
} }

View File

@ -24,4 +24,3 @@ class ArrayExport implements FromArray, WithHeadings
return array_keys($this->data[0]); return array_keys($this->data[0]);
} }
} }

View File

@ -5,8 +5,6 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ImpersonationController extends Controller class ImpersonationController extends Controller
{ {
@ -15,7 +13,8 @@ class ImpersonationController extends Controller
$this->middleware('moderator'); $this->middleware('moderator');
} }
public function impersonate($identifier) { public function impersonate($identifier)
{
$user = null; $user = null;
if (is_numeric($identifier)) { if (is_numeric($identifier)) {
$user = User::find($identifier); $user = User::find($identifier);
@ -31,7 +30,7 @@ class ImpersonationController extends Controller
if (! $user) { if (! $user) {
return $this->error([ return $this->error([
'message'=> 'User not found.' 'message' => 'User not found.',
]); ]);
} elseif ($user->admin) { } elseif ($user->admin) {
return $this->error([ return $this->error([
@ -52,7 +51,7 @@ class ImpersonationController extends Controller
])->login($user); ])->login($user);
return $this->success([ return $this->success([
'token' => $token 'token' => $token,
]); ]);
} }
} }

View File

@ -24,7 +24,9 @@ class AppSumoAuthController extends Controller
$license = $this->fetchOrCreateLicense($accessToken); $license = $this->fetchOrCreateLicense($accessToken);
// If user connected, attach license // If user connected, attach license
if (Auth::check()) return $this->attachLicense($license); if (Auth::check()) {
return $this->attachLicense($license);
}
// otherwise start login flow by passing the encrypted license key id // otherwise start login flow by passing the encrypted license key id
if (is_null($license->user_id)) { if (is_null($license->user_id)) {
@ -37,7 +39,7 @@ class AppSumoAuthController extends Controller
private function retrieveAccessToken(string $requestCode): string private function retrieveAccessToken(string $requestCode): string
{ {
return Http::withHeaders([ return Http::withHeaders([
'Content-type' => 'application/json' 'Content-type' => 'application/json',
])->post('https://appsumo.com/openid/token/', [ ])->post('https://appsumo.com/openid/token/', [
'grant_type' => 'authorization_code', 'grant_type' => 'authorization_code',
'code' => $requestCode, 'code' => $requestCode,
@ -73,7 +75,8 @@ class AppSumoAuthController extends Controller
return $license; return $license;
} }
private function attachLicense(License $license) { private function attachLicense(License $license)
{
if (! Auth::check()) { if (! Auth::check()) {
throw new AuthenticationException('User not authenticated'); throw new AuthenticationException('User not authenticated');
} }
@ -82,6 +85,7 @@ class AppSumoAuthController extends Controller
if (is_null($license->user_id)) { if (is_null($license->user_id)) {
$license->user_id = Auth::id(); $license->user_id = Auth::id();
$license->save(); $license->save();
return redirect(front_url('/home?appsumo_connect=1')); return redirect(front_url('/home?appsumo_connect=1'));
} }
@ -90,8 +94,6 @@ class AppSumoAuthController extends Controller
} }
/** /**
* @param User $user
* @param string|null $licenseHash
* @return string|null * @return string|null
* *
* Returns null if no license found * Returns null if no license found
@ -109,6 +111,7 @@ class AppSumoAuthController extends Controller
if ($license && is_null($license->user_id)) { if ($license && is_null($license->user_id)) {
$license->user_id = $user->id; $license->user_id = $user->id;
$license->save(); $license->save();
return true; return true;
} }

View File

@ -23,7 +23,6 @@ class ForgotPasswordController extends Controller
/** /**
* Get the response for a successful password reset link. * Get the response for a successful password reset link.
* *
* @param \Illuminate\Http\Request $request
* @param string $response * @param string $response
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
@ -35,7 +34,6 @@ class ForgotPasswordController extends Controller
/** /**
* Get the response for a failed password reset link. * Get the response for a failed password reset link.
* *
* @param \Illuminate\Http\Request $request
* @param string $response * @param string $response
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */

View File

@ -26,7 +26,6 @@ class LoginController extends Controller
/** /**
* Attempt to log the user into the application. * Attempt to log the user into the application.
* *
* @param \Illuminate\Http\Request $request
* @return bool * @return bool
*/ */
protected function attemptLogin(Request $request) protected function attemptLogin(Request $request)
@ -50,7 +49,6 @@ class LoginController extends Controller
/** /**
* Get the needed authorization credentials from the request. * Get the needed authorization credentials from the request.
* *
* @param \Illuminate\Http\Request $request
* @return array * @return array
*/ */
protected function credentials(Request $request) protected function credentials(Request $request)
@ -64,7 +62,6 @@ class LoginController extends Controller
/** /**
* Send the response after the user was authenticated. * Send the response after the user was authenticated.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
protected function sendLoginResponse(Request $request) protected function sendLoginResponse(Request $request)
@ -84,7 +81,6 @@ class LoginController extends Controller
/** /**
* Get the failed login response instance. * Get the failed login response instance.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
@ -104,7 +100,6 @@ class LoginController extends Controller
/** /**
* Log the user out of the application. * Log the user out of the application.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function logout(Request $request) public function logout(Request $request)

View File

@ -81,7 +81,7 @@ class OAuthController extends Controller
} }
if (User::where('email', $user->getEmail())->exists()) { if (User::where('email', $user->getEmail())->exists()) {
throw new EmailTakenException; throw new EmailTakenException();
} }
return $this->createUser($provider, $user); return $this->createUser($provider, $user);

View File

@ -4,8 +4,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource; use App\Http\Resources\UserResource;
use App\Models\Workspace;
use App\Models\User; use App\Models\User;
use App\Models\Workspace;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -31,7 +31,6 @@ class RegisterController extends Controller
/** /**
* The user has been registered. * The user has been registered.
* *
* @param \Illuminate\Http\Request $request
* @param \App\User $user * @param \App\User $user
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
@ -45,13 +44,13 @@ class RegisterController extends Controller
(new UserResource($user))->toArray($request), (new UserResource($user))->toArray($request),
[ [
'appsumo_license' => $this->appsumoLicense, 'appsumo_license' => $this->appsumoLicense,
])); ]
));
} }
/** /**
* Get a validator for an incoming registration request. * Get a validator for an incoming registration request.
* *
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator * @return \Illuminate\Contracts\Validation\Validator
*/ */
protected function validator(array $data) protected function validator(array $data)
@ -64,14 +63,13 @@ class RegisterController extends Controller
'agree_terms' => ['required', Rule::in([true])], 'agree_terms' => ['required', Rule::in([true])],
'appsumo_license' => ['nullable'], 'appsumo_license' => ['nullable'],
], [ ], [
'agree_terms' => 'Please agree with the terms and conditions.' 'agree_terms' => 'Please agree with the terms and conditions.',
]); ]);
} }
/** /**
* Create a new user instance after a valid registration. * Create a new user instance after a valid registration.
* *
* @param array $data
* @return \App\User * @return \App\User
*/ */
protected function create(array $data) protected function create(array $data)
@ -85,14 +83,14 @@ class RegisterController extends Controller
'name' => $data['name'], 'name' => $data['name'],
'email' => strtolower($data['email']), 'email' => strtolower($data['email']),
'password' => bcrypt($data['password']), 'password' => bcrypt($data['password']),
'hear_about_us' => $data['hear_about_us'] 'hear_about_us' => $data['hear_about_us'],
]); ]);
// Add relation with user // Add relation with user
$user->workspaces()->sync([ $user->workspaces()->sync([
$workspace->id => [ $workspace->id => [
'role' => 'admin' 'role' => 'admin',
] ],
], false); ], false);
$this->appsumoLicense = AppSumoAuthController::registerWithLicense($user, $data['appsumo_license'] ?? null); $this->appsumoLicense = AppSumoAuthController::registerWithLicense($user, $data['appsumo_license'] ?? null);

View File

@ -23,7 +23,6 @@ class ResetPasswordController extends Controller
/** /**
* Get the response for a successful password reset. * Get the response for a successful password reset.
* *
* @param \Illuminate\Http\Request $request
* @param string $response * @param string $response
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
@ -35,7 +34,6 @@ class ResetPasswordController extends Controller
/** /**
* Get the response for a failed password reset. * Get the response for a failed password reset.
* *
* @param \Illuminate\Http\Request $request
* @param string $response * @param string $response
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */

View File

@ -11,23 +11,24 @@ class UserController extends Controller
{ {
/** /**
* Get authenticated user. * Get authenticated user.
*
*/ */
public function current(Request $request) public function current(Request $request)
{ {
return new UserResource($request->user()); return new UserResource($request->user());
} }
public function deleteAccount() { public function deleteAccount()
{
$this->middleware('auth'); $this->middleware('auth');
if (Auth::user()->admin) { if (Auth::user()->admin) {
return $this->error([ return $this->error([
'message' => 'Cannot delete an admin. Stay with us 🙏' 'message' => 'Cannot delete an admin. Stay with us 🙏',
]); ]);
} }
Auth::user()->delete(); Auth::user()->delete();
return $this->success([ return $this->success([
'message' => 'User deleted.' 'message' => 'User deleted.',
]); ]);
} }
} }

View File

@ -24,7 +24,6 @@ class VerificationController extends Controller
/** /**
* Mark the user's email address as verified. * Mark the user's email address as verified.
* *
* @param \Illuminate\Http\Request $request
* @param \App\User $user * @param \App\User $user
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
@ -54,7 +53,6 @@ class VerificationController extends Controller
/** /**
* Resend the email verification notification. * Resend the email verification notification.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function resend(Request $request) public function resend(Request $request)

View File

@ -31,6 +31,7 @@ class CaddyController extends Controller
'domain' => $domain, 'domain' => $domain,
'workspace' => $workspace->id, 'workspace' => $workspace->id,
]); ]);
return $this->success([ return $this->success([
'success' => true, 'success' => true,
'message' => 'OK', 'message' => 'OK',

View File

@ -3,11 +3,10 @@
namespace App\Http\Controllers\Content; namespace App\Http\Controllers\Content;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ChangelogController extends Controller class ChangelogController extends Controller
{ {
const CANNY_ENDPOINT = 'https://canny.io/api/v1/'; public const CANNY_ENDPOINT = 'https://canny.io/api/v1/';
public function index() public function index()
{ {
@ -16,6 +15,7 @@ class ChangelogController extends Controller
'apiKey' => config('services.canny.api_key'), 'apiKey' => config('services.canny.api_key'),
'limit' => 3, 'limit' => 3,
]); ]);
return $response->json('entries'); return $response->json('entries');
}); });
} }

View File

@ -12,7 +12,6 @@ class FileUploadController extends Controller
/** /**
* Upload file to local temp * Upload file to local temp
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function upload(Request $request) public function upload(Request $request)
@ -23,7 +22,7 @@ class FileUploadController extends Controller
return response()->json([ return response()->json([
'uuid' => $uuid, 'uuid' => $uuid,
'key' => $path 'key' => $path,
], 201); ], 201);
} }
} }

View File

@ -2,19 +2,15 @@
namespace App\Http\Controllers\Content; namespace App\Http\Controllers\Content;
use App\Models\User; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Vapor\Http\Controllers\SignedStorageUrlController as Controller; use Laravel\Vapor\Http\Controllers\SignedStorageUrlController as Controller;
use Illuminate\Http\Request;
class SignedStorageUrlController extends Controller class SignedStorageUrlController extends Controller
{ {
/** /**
* Create a new signed URL. * Create a new signed URL.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function store(Request $request) public function store(Request $request)

View File

@ -9,19 +9,21 @@ use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests, DispatchesJobs, ValidatesRequests; use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
public function success($data = []) public function success($data = [])
{ {
return response()->json(array_merge([ return response()->json(array_merge([
'type' => 'success' 'type' => 'success',
], $data)); ], $data));
} }
public function error($data = [], $statusCode = 400) public function error($data = [], $statusCode = 400)
{ {
return response()->json(array_merge([ return response()->json(array_merge([
'type' => 'error' 'type' => 'error',
], $data), $statusCode); ], $data), $statusCode);
} }
} }

View File

@ -5,8 +5,6 @@ namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AiGenerateFormRequest; use App\Http\Requests\AiGenerateFormRequest;
use App\Models\Forms\AI\AiFormCompletion; use App\Models\Forms\AI\AiFormCompletion;
use App\Service\OpenAi\GptCompleter;
use Illuminate\Support\Str;
class AiFormController extends Controller class AiFormController extends Controller
{ {
@ -18,8 +16,8 @@ class AiFormController extends Controller
'message' => 'We\'re working on your form, please wait ~1 min.', 'message' => 'We\'re working on your form, please wait ~1 min.',
'ai_form_completion_id' => AiFormCompletion::create([ 'ai_form_completion_id' => AiFormCompletion::create([
'form_prompt' => $request->input('form_prompt'), 'form_prompt' => $request->input('form_prompt'),
'ip' => $request->ip() 'ip' => $request->ip(),
])->id ])->id,
]); ]);
} }
@ -30,7 +28,7 @@ class AiFormController extends Controller
} }
return $this->success([ return $this->success([
'ai_form_completion' => $aiFormCompletion 'ai_form_completion' => $aiFormCompletion,
]); ]);
} }
} }

View File

@ -17,7 +17,7 @@ use Illuminate\Support\Str;
class FormController extends Controller class FormController extends Controller
{ {
const ASSETS_UPLOAD_PATH = 'assets/forms'; public const ASSETS_UPLOAD_PATH = 'assets/forms';
private FormCleaner $formCleaner; private FormCleaner $formCleaner;
@ -46,16 +46,18 @@ class FormController extends Controller
'cleanings' => $this->formCleaner 'cleanings' => $this->formCleaner
->processForm(request(), $form) ->processForm(request(), $form)
->simulateCleaning($workspace) ->simulateCleaning($workspace)
->getPerformedCleanings() ->getPerformedCleanings(),
]; ];
return $form; return $form;
}); });
return FormResource::collection($forms); return FormResource::collection($forms);
} }
/** /**
* Return all user forms, used for zapier * Return all user forms, used for zapier
*
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
*/ */
public function indexAll() public function indexAll()
@ -73,11 +75,13 @@ class FormController extends Controller
'workspaceIsPro' => $workspaceIsPro, 'workspaceIsPro' => $workspaceIsPro,
'userIsOwner' => true, 'userIsOwner' => true,
]; ];
return $form; return $form;
}); });
$forms = $forms->merge($newForms); $forms = $forms->merge($newForms);
} }
return FormResource::collection($forms); return FormResource::collection($forms);
} }
@ -94,13 +98,13 @@ class FormController extends Controller
->getData(); ->getData();
$form = Form::create(array_merge($formData, [ $form = Form::create(array_merge($formData, [
'creator_id' => $request->user()->id 'creator_id' => $request->user()->id,
])); ]));
return $this->success([ 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->visibility == 'draft' ? ' But other people won\'t be able to see the form since it\'s currently in draft mode' : ''), 'message' => $this->formCleaner->hasCleaned() ? 'Form successfully created, but the Pro features you used will be disabled when sharing your form:' : 'Form created.'.($form->visibility == 'draft' ? ' But other people won\'t be able to see the form since it\'s currently in draft mode' : ''),
'form' => (new FormResource($form))->setCleanings($this->formCleaner->getPerformedCleanings()), 'form' => (new FormResource($form))->setCleanings($this->formCleaner->getPerformedCleanings()),
'users_first_form' => $request->user()->forms()->count() == 1 'users_first_form' => $request->user()->forms()->count() == 1,
]); ]);
} }
@ -116,7 +120,7 @@ class FormController extends Controller
// Set Removed Properties // Set Removed Properties
$formData['removed_properties'] = array_merge($form->removed_properties, collect($form->properties)->filter(function ($field) use ($formData) { $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())); return ! Str::of($field['type'])->startsWith('nf-') && ! in_array($field['id'], collect($formData['properties'])->pluck('id')->toArray());
})->toArray()); })->toArray());
$form->update($formData); $form->update($formData);
@ -133,8 +137,9 @@ class FormController extends Controller
$this->authorize('delete', $form); $this->authorize('delete', $form);
$form->delete(); $form->delete();
return $this->success([ return $this->success([
'message' => 'Form was deleted.' 'message' => 'Form was deleted.',
]); ]);
} }
@ -150,7 +155,7 @@ class FormController extends Controller
return $this->success([ return $this->success([
'message' => 'Form successfully duplicated.', 'message' => 'Form successfully duplicated.',
'new_form' => new FormResource($formCopy) 'new_form' => new FormResource($formCopy),
]); ]);
} }
@ -168,7 +173,7 @@ class FormController extends Controller
return $this->success([ return $this->success([
'message' => 'Form url successfully updated. Your new form url now is: '.$form->share_url.'.', 'message' => 'Form url successfully updated. Your new form url now is: '.$form->share_url.'.',
'form' => new FormResource($form) 'form' => new FormResource($form),
]); ]);
} }
@ -182,7 +187,7 @@ class FormController extends Controller
$fileNameParser = StorageFileNameParser::parse($request->url); $fileNameParser = StorageFileNameParser::parse($request->url);
// Make sure we retrieve the file in tmp storage, move it to persistent // Make sure we retrieve the file in tmp storage, move it to persistent
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH.'/'.$fileNameParser->uuid;; $fileName = PublicFormController::TMP_FILE_UPLOAD_PATH.'/'.$fileNameParser->uuid;
if (! Storage::exists($fileName)) { if (! Storage::exists($fileName)) {
// File not found, we skip // File not found, we skip
return null; return null;
@ -192,7 +197,7 @@ class FormController extends Controller
return $this->success([ return $this->success([
'message' => 'File uploaded.', 'message' => 'File uploaded.',
'url' => route("forms.assets.show", [$fileNameParser->getMovedFileName()]) 'url' => route('forms.assets.show', [$fileNameParser->getMovedFileName()]),
]); ]);
} }
@ -207,7 +212,7 @@ class FormController extends Controller
$path = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $form->id).'/'.$fileName; $path = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $form->id).'/'.$fileName;
if (! Storage::exists($path)) { if (! Storage::exists($path)) {
return $this->error([ return $this->error([
'message' => 'File not found.' 'message' => 'File not found.',
]); ]);
} }

View File

@ -20,19 +20,20 @@ class FormStatsController extends Controller
$this->authorize('view', $form); $this->authorize('view', $form);
$formStats = $form->statistics()->where('date', '>', now()->subDays(29)->startOfDay())->get(); $formStats = $form->statistics()->where('date', '>', now()->subDays(29)->startOfDay())->get();
$periodStats = ["views" => [], "submissions" => []]; $periodStats = ['views' => [], 'submissions' => []];
foreach (CarbonPeriod::create(now()->subDays(29), now()) as $dateObj) { foreach (CarbonPeriod::create(now()->subDays(29), now()) as $dateObj) {
$date = $dateObj->format('d-m-Y'); $date = $dateObj->format('d-m-Y');
$statisticData = $formStats->where('date', $dateObj->format('Y-m-d'))->first(); $statisticData = $formStats->where('date', $dateObj->format('Y-m-d'))->first();
$periodStats["views"][$date] = $statisticData->data["views"] ?? 0; $periodStats['views'][$date] = $statisticData->data['views'] ?? 0;
$periodStats["submissions"][$date] = $statisticData->data["submissions"] ?? 0; $periodStats['submissions'][$date] = $statisticData->data['submissions'] ?? 0;
if ($dateObj->toDateString() === now()->toDateString()) { if ($dateObj->toDateString() === now()->toDateString()) {
$periodStats["views"][$date] += $form->views()->count(); $periodStats['views'][$date] += $form->views()->count();
$periodStats["submissions"][$date] += $form->submissions()->whereDate('created_at', '>=', now()->startOfDay())->count(); $periodStats['submissions'][$date] += $form->submissions()->whereDate('created_at', '>=', now()->startOfDay())->count();
} }
} }
return $periodStats; return $periodStats;
} }
} }

View File

@ -2,15 +2,14 @@
namespace App\Http\Controllers\Forms; 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\Exports\FormSubmissionExport;
use App\Http\Controllers\Controller;
use App\Http\Requests\AnswerFormRequest; use App\Http\Requests\AnswerFormRequest;
use App\Http\Resources\FormSubmissionResource;
use App\Jobs\Form\StoreFormSubmissionJob; use App\Jobs\Form\StoreFormSubmissionJob;
use App\Models\Forms\Form;
use App\Models\Forms\FormSubmission; use App\Models\Forms\FormSubmission;
use App\Service\Forms\FormSubmissionFormatter; use App\Service\Forms\FormSubmissionFormatter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
@ -40,9 +39,10 @@ class FormSubmissionController extends Controller
$job->setSubmissionId($submissionId)->handle(); $job->setSubmissionId($submissionId)->handle();
$data = new FormSubmissionResource(FormSubmission::findOrFail($submissionId)); $data = new FormSubmissionResource(FormSubmission::findOrFail($submissionId));
return $this->success([ return $this->success([
'message' => 'Record successfully updated.', 'message' => 'Record successfully updated.',
'data' => $data 'data' => $data,
]); ]);
} }
@ -61,11 +61,12 @@ class FormSubmissionController extends Controller
->useSignedUrlForFiles(); ->useSignedUrlForFiles();
$allRows[] = [ $allRows[] = [
'id' => Hashids::encode($row['id']), 'id' => Hashids::encode($row['id']),
'created_at' => date("Y-m-d H:i", strtotime($row['created_at'])), 'created_at' => date('Y-m-d H:i', strtotime($row['created_at'])),
...$formatter->getCleanKeyValue() ...$formatter->getCleanKeyValue(),
]; ];
} }
$csvExport = (new FormSubmissionExport($allRows)); $csvExport = (new FormSubmissionExport($allRows));
return Excel::download( return Excel::download(
$csvExport, $csvExport,
$form->slug.'-submission-data.csv', $form->slug.'-submission-data.csv',

View File

@ -5,35 +5,38 @@ namespace App\Http\Controllers\Forms\Integration;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Integration\StoreFormZapierWebhookRequest; use App\Http\Requests\Integration\StoreFormZapierWebhookRequest;
use App\Models\Integration\FormZapierWebhook; use App\Models\Integration\FormZapierWebhook;
use Illuminate\Http\Request;
use Spatie\WebhookServer\WebhookCall;
class FormZapierWebhookController extends Controller class FormZapierWebhookController extends Controller
{ {
/** /**
* Controller for Zappier webhook subscriptions. * Controller for Zappier webhook subscriptions.
*/ */
public function __construct() { public function __construct()
{
// $this->middleware('subscribed'); // $this->middleware('subscribed');
$this->middleware('auth'); $this->middleware('auth');
} }
public function store(StoreFormZapierWebhookRequest $request) { public function store(StoreFormZapierWebhookRequest $request)
{
$hook = $request->instanciateHook(); $hook = $request->instanciateHook();
$this->authorize('store', $hook); $this->authorize('store', $hook);
$hook->save(); $hook->save();
return $this->success([ return $this->success([
'message' => 'Webhook created.', 'message' => 'Webhook created.',
'hook' => $hook 'hook' => $hook,
]); ]);
} }
public function delete($id) { public function delete($id)
{
$hook = FormZapierWebhook::findOrFail($id); $hook = FormZapierWebhook::findOrFail($id);
$this->authorize('store', $hook); $this->authorize('store', $hook);
$hook->delete(); $hook->delete();
return $this->success([ return $this->success([
'message' => 'Webhook deleted.', 'message' => 'Webhook deleted.',
]); ]);

View File

@ -11,15 +11,15 @@ use App\Models\Forms\FormSubmission;
use App\Service\Forms\FormCleaner; use App\Service\Forms\FormCleaner;
use App\Service\WorkspaceHelper; use App\Service\WorkspaceHelper;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Vinkla\Hashids\Facades\Hashids; use Vinkla\Hashids\Facades\Hashids;
class PublicFormController extends Controller class PublicFormController extends Controller
{ {
public const FILE_UPLOAD_PATH = 'forms/?/submissions';
const FILE_UPLOAD_PATH = 'forms/?/submissions'; public const TMP_FILE_UPLOAD_PATH = 'tmp/';
const TMP_FILE_UPLOAD_PATH = 'tmp/';
public function show(Request $request, string $slug) public function show(Request $request, string $slug)
{ {
@ -27,14 +27,15 @@ class PublicFormController extends Controller
if ($form->workspace == null) { if ($form->workspace == null) {
// Workspace deleted // Workspace deleted
return $this->error([ return $this->error([
'message' => 'Form not found.' 'message' => 'Form not found.',
], 404); ], 404);
} }
$formCleaner = new FormCleaner(); $formCleaner = new FormCleaner();
// Disable pro features if needed // Disable pro features if needed
$form->fill($formCleaner $form->fill(
$formCleaner
->processForm($request, $form) ->processForm($request, $form)
->performCleaning($form->workspace) ->performCleaning($form->workspace)
->getData() ->getData()
@ -59,6 +60,7 @@ class PublicFormController extends Controller
// Use serializer // Use serializer
$workspace = $form->workspace; $workspace = $form->workspace;
return (new WorkspaceHelper($workspace))->getAllUsers(); return (new WorkspaceHelper($workspace))->getAllUsers();
} }
@ -68,7 +70,7 @@ class PublicFormController extends Controller
if (! Storage::exists($path)) { if (! Storage::exists($path)) {
return $this->error([ return $this->error([
'message' => 'File not found.', 'message' => 'File not found.',
'file_name' => $assetFileName 'file_name' => $assetFileName,
]); ]);
} }
@ -90,12 +92,12 @@ class PublicFormController extends Controller
return $this->success(array_merge([ return $this->success(array_merge([
'message' => 'Form submission saved.', 'message' => 'Form submission saved.',
'submission_id' => $submissionId 'submission_id' => $submissionId,
], $request->form->is_pro && $request->form->redirect_url ? [ ], $request->form->is_pro && $request->form->redirect_url ? [
'redirect' => true, 'redirect' => true,
'redirect_url' => $request->form->redirect_url 'redirect_url' => $request->form->redirect_url,
] : [ ] : [
'redirect' => false 'redirect' => false,
])); ]));
} }
@ -120,5 +122,4 @@ class PublicFormController extends Controller
return $this->success(['data' => ($submission) ? $submission->data : []]); return $this->success(['data' => ($submission) ? $submission->data : []]);
} }
} }

View File

@ -17,8 +17,7 @@ class RecordController extends Controller
$record->delete(); $record->delete();
return $this->success([ return $this->success([
'message' => 'Record successfully removed.' 'message' => 'Record successfully removed.',
]); ]);
} }
} }

View File

@ -10,7 +10,6 @@ class PasswordController extends Controller
/** /**
* Update the user's password. * Update the user's password.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(Request $request) public function update(Request $request)

View File

@ -10,7 +10,6 @@ class ProfileController extends Controller
/** /**
* Update the user's profile information. * Update the user's profile information.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(Request $request) public function update(Request $request)

View File

@ -2,18 +2,15 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;
use App\Models\Template; use App\Models\Template;
use Illuminate\Http\Request;
class SitemapController extends Controller class SitemapController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
return [ return [
...$this->getTemplatesUrls() ...$this->getTemplatesUrls(),
]; ];
} }
@ -23,10 +20,11 @@ class SitemapController extends Controller
Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) { Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) {
foreach ($templates as $template) { foreach ($templates as $template) {
$urls[] = [ $urls[] = [
'loc' => '/templates/' . $template->slug 'loc' => '/templates/'.$template->slug,
]; ];
} }
}); });
return $urls; return $urls;
} }
} }

View File

@ -9,10 +9,11 @@ use Laravel\Cashier\Subscription;
class SubscriptionController extends Controller class SubscriptionController extends Controller
{ {
const SUBSCRIPTION_PLANS = ['monthly', 'yearly']; public const SUBSCRIPTION_PLANS = ['monthly', 'yearly'];
const PRO_SUBSCRIPTION_NAME = 'default'; public const PRO_SUBSCRIPTION_NAME = 'default';
const SUBSCRIPTION_NAMES = [
public const SUBSCRIPTION_NAMES = [
self::PRO_SUBSCRIPTION_NAME, self::PRO_SUBSCRIPTION_NAME,
]; ];
@ -30,7 +31,7 @@ class SubscriptionController extends Controller
if ($user->subscriptions()->where('stripe_status', 'past_due')->first()) { if ($user->subscriptions()->where('stripe_status', 'past_due')->first()) {
return $this->error([ return $this->error([
'message' => 'You already have a past due subscription. Please verify your details in the billing page, 'message' => 'You already have a past due subscription. Please verify your details in the billing page,
and contact us if the issue persists.' and contact us if the issue persists.',
]); ]);
} }
@ -51,11 +52,11 @@ class SubscriptionController extends Controller
'customer_update' => [ 'customer_update' => [
'address' => 'auto', 'address' => 'auto',
'name' => 'never', 'name' => 'never',
] ],
]); ]);
return $this->success([ return $this->success([
'checkout_url' => $checkout->url 'checkout_url' => $checkout->url,
]); ]);
} }
@ -80,11 +81,12 @@ class SubscriptionController extends Controller
$this->middleware('auth'); $this->middleware('auth');
if (! Auth::user()->has_customer_id) { if (! Auth::user()->has_customer_id) {
return $this->error([ return $this->error([
"message" => "Please subscribe before accessing your billing portal." 'message' => 'Please subscribe before accessing your billing portal.',
]); ]);
} }
return $this->success([ return $this->success([
'portal_url' => Auth::user()->billingPortalUrl(front_url('/home')) 'portal_url' => Auth::user()->billingPortalUrl(front_url('/home')),
]); ]);
} }

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\Templates\FormTemplateRequest; use App\Http\Requests\Templates\FormTemplateRequest;
use App\Http\Resources\FormTemplateResource; use App\Http\Resources\FormTemplateResource;
use App\Models\Template; use App\Models\Template;
@ -49,7 +48,7 @@ class TemplateController extends Controller
return $this->success([ return $this->success([
'message' => 'Template was created.', 'message' => 'Template was created.',
'template_id' => $template->id, 'template_id' => $template->id,
'data' => new FormTemplateResource($template) 'data' => new FormTemplateResource($template),
]); ]);
} }
@ -63,7 +62,7 @@ class TemplateController extends Controller
return $this->success([ return $this->success([
'message' => 'Template was updated.', 'message' => 'Template was updated.',
'template_id' => $template->id, 'template_id' => $template->id,
'data' => new FormTemplateResource($template) 'data' => new FormTemplateResource($template),
]); ]);
} }

View File

@ -4,8 +4,8 @@ namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\License; use App\Models\License;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\UnauthorizedException; use Illuminate\Validation\UnauthorizedException;
class AppSumoController extends Controller class AppSumoController extends Controller
@ -16,6 +16,7 @@ class AppSumoController extends Controller
if ($request->test) { if ($request->test) {
Log::info('[APPSUMO] test request received', $request->toArray()); Log::info('[APPSUMO] test request received', $request->toArray());
return $this->success([ return $this->success([
'message' => 'Webhook received.', 'message' => 'Webhook received.',
'event' => $request->event, 'event' => $request->event,
@ -69,11 +70,13 @@ class AppSumoController extends Controller
$license->user_id = $licenseData['user_id'] ?? null; $license->user_id = $licenseData['user_id'] ?? null;
$license->save(); $license->save();
Log::info('[APPSUMO] creating new license', Log::info(
'[APPSUMO] creating new license',
[ [
'license_key' => $license->license_key, 'license_key' => $license->license_key,
'license_id' => $license->id, 'license_id' => $license->id,
]); ]
);
return $license; return $license;
} }

View File

@ -5,13 +5,11 @@ namespace App\Http\Controllers\Webhook;
use App\Notifications\Subscription\FailedPaymentNotification; use App\Notifications\Subscription\FailedPaymentNotification;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Http\Controllers\WebhookController; use Laravel\Cashier\Http\Controllers\WebhookController;
use Stripe\Subscription as StripeSubscription; use Stripe\Subscription as StripeSubscription;
class StripeController extends WebhookController class StripeController extends WebhookController
{ {
public function handleCustomerSubscriptionCreated(array $payload) public function handleCustomerSubscriptionCreated(array $payload)
{ {
return parent::handleCustomerSubscriptionCreated($payload); return parent::handleCustomerSubscriptionCreated($payload);
@ -19,7 +17,7 @@ class StripeController extends WebhookController
/** /**
* Override to add a sleep, and to detect plan upgrades * Override to add a sleep, and to detect plan upgrades
* @param array $payload *
* @return \Symfony\Component\HttpFoundation\Response|void * @return \Symfony\Component\HttpFoundation\Response|void
*/ */
protected function handleCustomerSubscriptionUpdated(array $payload) protected function handleCustomerSubscriptionUpdated(array $payload)
@ -125,7 +123,7 @@ class StripeController extends WebhookController
return $plan; return $plan;
} }
} }
return 'default'; return 'default';
} }
} }

View File

@ -5,8 +5,8 @@ namespace App\Http\Controllers;
use App\Http\Requests\Workspace\CustomDomainRequest; use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Http\Resources\WorkspaceResource; use App\Http\Resources\WorkspaceResource;
use App\Models\Workspace; use App\Models\Workspace;
use Illuminate\Http\Request;
use App\Service\WorkspaceHelper; use App\Service\WorkspaceHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class WorkspaceController extends Controller class WorkspaceController extends Controller
@ -19,6 +19,7 @@ class WorkspaceController extends Controller
public function index() public function index()
{ {
$this->authorize('viewAny', Workspace::class); $this->authorize('viewAny', Workspace::class);
return WorkspaceResource::collection(Auth::user()->workspaces); return WorkspaceResource::collection(Auth::user()->workspaces);
} }
@ -34,6 +35,7 @@ class WorkspaceController extends Controller
{ {
$request->workspace->custom_domains = $request->customDomains; $request->workspace->custom_domains = $request->customDomains;
$request->workspace->save(); $request->workspace->save();
return new WorkspaceResource($request->workspace); return new WorkspaceResource($request->workspace);
} }
@ -44,9 +46,10 @@ class WorkspaceController extends Controller
$id = $workspace->id; $id = $workspace->id;
$workspace->delete(); $workspace->delete();
return $this->success([ return $this->success([
'message' => 'Workspace deleted.', 'message' => 'Workspace deleted.',
'workspace_id' => $id 'workspace_id' => $id,
]); ]);
} }
@ -55,7 +58,7 @@ class WorkspaceController extends Controller
$user = $request->user(); $user = $request->user();
$this->validate($request, [ $this->validate($request, [
'name' => 'required' 'name' => 'required',
]); ]);
// Create workspace // Create workspace
@ -67,14 +70,14 @@ class WorkspaceController extends Controller
// Add relation with user // Add relation with user
$user->workspaces()->sync([ $user->workspaces()->sync([
$workspace->id => [ $workspace->id => [
'role' => 'admin' 'role' => 'admin',
] ],
], false); ], false);
return $this->success([ return $this->success([
'message' => 'Workspace created.', 'message' => 'Workspace created.',
'workspace_id' => $workspace->id, 'workspace_id' => $workspace->id,
'workspace' => new WorkspaceResource($workspace) 'workspace' => new WorkspaceResource($workspace),
]); ]);
} }
} }

View File

@ -32,7 +32,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\SetLocale::class, \App\Http\Middleware\SetLocale::class,
AuthenticateJWT::class, AuthenticateJWT::class,
CustomDomainRestriction::class, CustomDomainRestriction::class,
AcceptsJsonMiddleware::class AcceptsJsonMiddleware::class,
]; ];
/** /**

View File

@ -10,7 +10,6 @@ class AcceptsJsonMiddleware
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */

View File

@ -8,7 +8,7 @@ use Tymon\JWTAuth\Exceptions\JWTException;
class AuthenticateJWT class AuthenticateJWT
{ {
const API_SERVER_SECRET_HEADER_NAME = 'x-api-secret'; public const API_SERVER_SECRET_HEADER_NAME = 'x-api-secret';
/** /**
* Verifies the JWT token and validates the IP and User Agent * Verifies the JWT token and validates the IP and User Agent
@ -43,8 +43,9 @@ class AuthenticateJWT
if ($error) { if ($error) {
auth()->invalidate(); auth()->invalidate();
return response()->json([ return response()->json([
'message' => $error 'message' => $error,
], 403); ], 403);
} }
} }

View File

@ -6,12 +6,12 @@ use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Models\Workspace; use App\Models\Workspace;
use Closure; use Closure;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class CustomDomainRestriction class CustomDomainRestriction
{ {
const CUSTOM_DOMAIN_HEADER = "x-custom-domain"; public const CUSTOM_DOMAIN_HEADER = 'x-custom-domain';
/** /**
* Handle an incoming request. * Handle an incoming request.

View File

@ -11,7 +11,6 @@ class ProForm
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
@ -22,6 +21,7 @@ class ProForm
$request->merge([ $request->merge([
'form' => $form, 'form' => $form,
]); ]);
return $next($request); return $next($request);
} }
} }

View File

@ -9,12 +9,11 @@ use Illuminate\Support\Facades\Auth;
class ProtectedForm class ProtectedForm
{ {
const PASSWORD_HEADER_NAME = 'form-password'; public const PASSWORD_HEADER_NAME = 'form-password';
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */

View File

@ -11,16 +11,16 @@ class ResolveFormMiddleware
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
public function handle(Request $request, Closure $next, string $routeParamName = "id") public function handle(Request $request, Closure $next, string $routeParamName = 'id')
{ {
$form = Form::where($routeParamName, $request->route($routeParamName))->firstOrFail(); $form = Form::where($routeParamName, $request->route($routeParamName))->firstOrFail();
$request->merge([ $request->merge([
'form' => $form, 'form' => $form,
]); ]);
return $next($request); return $next($request);
} }
} }

View File

@ -9,14 +9,15 @@ use Tymon\JWTAuth\Exceptions\JWTException;
class ImpersonationMiddleware class ImpersonationMiddleware
{ {
public const ADMIN_LOG_PREFIX = '[admin_action] '; public const ADMIN_LOG_PREFIX = '[admin_action] ';
const LOG_ROUTES = [
public const LOG_ROUTES = [
'open.forms.store', 'open.forms.store',
'open.forms.update', 'open.forms.update',
'open.forms.duplicate', 'open.forms.duplicate',
'open.forms.regenerate-link', 'open.forms.regenerate-link',
]; ];
const ALLOWED_ROUTES = [ public const ALLOWED_ROUTES = [
'logout', 'logout',
// Forms // Forms
@ -59,7 +60,6 @@ class ImpersonationMiddleware
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ */
@ -82,7 +82,7 @@ class ImpersonationMiddleware
'impersonator' => auth()->payload()->get('impersonator_id'), 'impersonator' => auth()->payload()->get('impersonator_id'),
'impersonated_account' => auth()->id(), 'impersonated_account' => auth()->id(),
'url' => $request->fullUrl(), 'url' => $request->fullUrl(),
'payload' => $request->all() 'payload' => $request->all(),
], 403); ], 403);
} elseif (in_array($routeName, self::LOG_ROUTES)) { } elseif (in_array($routeName, self::LOG_ROUTES)) {
\Log::warning(self::ADMIN_LOG_PREFIX.'Impersonator action', [ \Log::warning(self::ADMIN_LOG_PREFIX.'Impersonator action', [
@ -90,7 +90,7 @@ class ImpersonationMiddleware
'url' => $request->fullUrl(), 'url' => $request->fullUrl(),
'impersonated_account' => auth()->id(), 'impersonated_account' => auth()->id(),
'impersonator' => auth()->payload()->get('impersonator_id'), 'impersonator' => auth()->payload()->get('impersonator_id'),
'payload' => $request->all() 'payload' => $request->all(),
]); ]);
} }

View File

@ -10,8 +10,6 @@ class IsAdmin
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
@ -24,6 +22,7 @@ class IsAdmin
'type' => 'error', 'type' => 'error',
], 403); ], 403);
} }
return redirect('home'); return redirect('home');
} }

View File

@ -10,8 +10,6 @@ class IsModerator
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
@ -24,6 +22,7 @@ class IsModerator
'type' => 'error', 'type' => 'error',
], 403); ], 403);
} }
return redirect('home'); return redirect('home');
} }

View File

@ -10,8 +10,6 @@ class IsNotSubscribed
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
@ -24,6 +22,7 @@ class IsNotSubscribed
'type' => 'error', 'type' => 'error',
], 401); ], 401);
} }
return redirect('billing'); return redirect('billing');
} }

View File

@ -10,8 +10,6 @@ class IsSubscribed
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
@ -24,6 +22,7 @@ class IsSubscribed
'type' => 'error', 'type' => 'error',
], 401); ], 401);
} }
return redirect('billing'); return redirect('billing');
} }

View File

@ -12,8 +12,6 @@ class RedirectIfAuthenticated
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards * @param string|null ...$guards
* @return mixed * @return mixed
*/ */

View File

@ -10,7 +10,6 @@ class SetLocale
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)

View File

@ -18,10 +18,11 @@ class TrimStrings extends Middleware
/** /**
* The route name where this shouldn't be applied * The route name where this shouldn't be applied
*
* @var string[] * @var string[]
*/ */
protected $exceptUrls = [ protected $exceptUrls = [
'/\/api\/forms\/(.*)\/answer/' '/\/api\/forms\/(.*)\/answer/',
]; ];
public function handle($request, \Closure $next) public function handle($request, \Closure $next)

View File

@ -14,6 +14,6 @@ class VerifyCsrfToken extends Middleware
protected $except = [ protected $except = [
'stripe/webhook', 'stripe/webhook',
'vapor/signed-storage-url', 'vapor/signed-storage-url',
'upload-file' 'upload-file',
]; ];
} }

View File

@ -14,7 +14,7 @@ class AiGenerateFormRequest extends FormRequest
public function rules() public function rules()
{ {
return [ return [
'form_prompt' => 'required|string|max:1000' 'form_prompt' => 'required|string|max:1000',
]; ];
} }
} }

View File

@ -3,22 +3,22 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Rules\StorageFile; 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; use App\Rules\ValidHCaptcha;
use App\Rules\ValidPhoneInputRule; use App\Rules\ValidPhoneInputRule;
use App\Rules\ValidUrl; use App\Rules\ValidUrl;
use App\Service\Forms\FormLogicPropertyResolver;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class AnswerFormRequest extends FormRequest class AnswerFormRequest extends FormRequest
{ {
public Form $form; public Form $form;
protected array $requestRules = []; protected array $requestRules = [];
protected int $maxFileSize; protected int $maxFileSize;
public function __construct(Request $request) public function __construct(Request $request)
@ -60,12 +60,13 @@ class AnswerFormRequest extends FormRequest
if (isset($data[$field['id']]) && is_array($data[$field['id']])) { if (isset($data[$field['id']]) && is_array($data[$field['id']])) {
$data[$field['id']] = array_map(function ($val) use ($field) { $data[$field['id']] = array_map(function ($val) use ($field) {
$tmpop = collect($field[$field['type']]['options'])->first(function ($op) use ($val) { $tmpop = collect($field[$field['type']]['options'])->first(function ($op) use ($val) {
return ($op['id'] ?? $op['value'] === $val); return $op['id'] ?? $op['value'] === $val;
}); });
return isset($tmpop['name']) ? $tmpop['name'] : "";
return isset($tmpop['name']) ? $tmpop['name'] : '';
}, $data[$field['id']]); }, $data[$field['id']]);
} }
}; }
if (FormLogicPropertyResolver::isRequired($property, $data)) { if (FormLogicPropertyResolver::isRequired($property, $data)) {
$rules[] = 'required'; $rules[] = 'required';
@ -107,6 +108,7 @@ class AnswerFormRequest extends FormRequest
/** /**
* Renames validated fields (because field names are ids) * Renames validated fields (because field names are ids)
*
* @return array * @return array
*/ */
public function attributes() public function attributes()
@ -115,6 +117,7 @@ class AnswerFormRequest extends FormRequest
foreach ($this->form->properties as $property) { foreach ($this->form->properties as $property) {
$fields[$property['id']] = $property['name']; $fields[$property['id']] = $property['name'];
} }
return $fields; return $fields;
} }
@ -128,20 +131,20 @@ class AnswerFormRequest extends FormRequest
$messages = []; $messages = [];
foreach ($this->form->properties as $property) { foreach ($this->form->properties as $property) {
if ($property['type'] == 'date' && isset($property['date_range']) && $property['date_range']) { if ($property['type'] == 'date' && isset($property['date_range']) && $property['date_range']) {
$messages[$property['id'].'.0.required_with'] = "From date is required"; $messages[$property['id'].'.0.required_with'] = 'From date is required';
$messages[$property['id'].'.1.required_with'] = "To date is required"; $messages[$property['id'].'.1.required_with'] = 'To date is required';
$messages[$property['id'].'.0.before_or_equal'] = "From date must be before or equal To date"; $messages[$property['id'].'.0.before_or_equal'] = 'From date must be before or equal To date';
} }
if ($property['type'] == 'number' && isset($property['is_rating']) && $property['is_rating']) { if ($property['type'] == 'number' && isset($property['is_rating']) && $property['is_rating']) {
$messages[$property['id'] . '.min'] = "A rating must be selected"; $messages[$property['id'].'.min'] = 'A rating must be selected';
} }
} }
return $messages; return $messages;
} }
/** /**
* Return validation rules for a given form property * Return validation rules for a given form property
* @param $property
*/ */
private function getPropertyRules($property): array private function getPropertyRules($property): array
{ {
@ -153,27 +156,32 @@ class AnswerFormRequest extends FormRequest
if ($property['is_rating'] ?? false) { if ($property['is_rating'] ?? false) {
return ['numeric']; return ['numeric'];
} }
return ['numeric']; return ['numeric'];
case 'select': case 'select':
case 'multi_select': case 'multi_select':
if (($property['allow_creation'] ?? false)) { if (($property['allow_creation'] ?? false)) {
return ['string']; return ['string'];
} }
return [Rule::in($this->getSelectPropertyOptions($property))]; return [Rule::in($this->getSelectPropertyOptions($property))];
case 'checkbox': case 'checkbox':
return ['boolean']; return ['boolean'];
case 'url': case 'url':
if (isset($property['file_upload']) && $property['file_upload']) { if (isset($property['file_upload']) && $property['file_upload']) {
$this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, [], $this->form)]; $this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, [], $this->form)];
return ['array']; return ['array'];
} }
return [new ValidUrl];
return [new ValidUrl()];
case 'files': case 'files':
$allowedFileTypes = []; $allowedFileTypes = [];
if (! empty($property['allowed_file_types'])) { if (! empty($property['allowed_file_types'])) {
$allowedFileTypes = explode(",", $property['allowed_file_types']); $allowedFileTypes = explode(',', $property['allowed_file_types']);
} }
$this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, $allowedFileTypes, $this->form)]; $this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, $allowedFileTypes, $this->form)];
return ['array']; return ['array'];
case 'email': case 'email':
return ['email:filter']; return ['email:filter'];
@ -182,14 +190,17 @@ class AnswerFormRequest extends FormRequest
$this->requestRules[$property['id'].'.*'] = $this->getRulesForDate($property); $this->requestRules[$property['id'].'.*'] = $this->getRulesForDate($property);
$this->requestRules[$property['id'].'.0'] = ['required_with:'.$property['id'].'.1', 'before_or_equal:'.$property['id'].'.1']; $this->requestRules[$property['id'].'.0'] = ['required_with:'.$property['id'].'.1', 'before_or_equal:'.$property['id'].'.1'];
$this->requestRules[$property['id'].'.1'] = ['required_with:'.$property['id'].'.0']; $this->requestRules[$property['id'].'.1'] = ['required_with:'.$property['id'].'.0'];
return ['array', 'min:2']; return ['array', 'min:2'];
} }
return $this->getRulesForDate($property); return $this->getRulesForDate($property);
case 'phone_number': case 'phone_number':
if (isset($property['use_simple_text_input']) && $property['use_simple_text_input']) { if (isset($property['use_simple_text_input']) && $property['use_simple_text_input']) {
return ['string']; return ['string'];
} }
return ['string', 'min:6', new ValidPhoneInputRule];
return ['string', 'min:6', new ValidPhoneInputRule()];
default: default:
return []; return [];
} }
@ -202,6 +213,7 @@ class AnswerFormRequest extends FormRequest
} elseif (isset($property['disable_future_dates']) && $property['disable_future_dates']) { } elseif (isset($property['disable_future_dates']) && $property['disable_future_dates']) {
return ['date', 'before_or_equal:today']; return ['date', 'before_or_equal:today'];
} }
return ['date']; return ['date'];
} }
@ -211,6 +223,7 @@ class AnswerFormRequest extends FormRequest
if (! isset($property[$type])) { if (! isset($property[$type])) {
return []; return [];
} }
return array_column($property[$type]['options'], 'name'); return array_column($property[$type]['options'], 'name');
} }
@ -227,16 +240,17 @@ class AnswerFormRequest extends FormRequest
if (is_array($receivedValue)) { if (is_array($receivedValue)) {
$mergeData[$property['id']] = collect($receivedValue)->map(function ($value) { $mergeData[$property['id']] = collect($receivedValue)->map(function ($value) {
$value = Str::of($value); $value = Str::of($value);
return $value->replace( return $value->replace(
["\e", "\f", "\n", "\r", "\t", "\v", "\\"], ["\e", "\f", "\n", "\r", "\t", "\v", '\\'],
["\\e", "\\f", "\\n", "\\r", "\\t", "\\v", "\\\\"] ['\\e', '\\f', '\\n', '\\r', '\\t', '\\v', '\\\\']
)->toString(); )->toString();
})->toArray(); })->toArray();
} else { } else {
$receivedValue = Str::of($receivedValue); $receivedValue = Str::of($receivedValue);
$mergeData[$property['id']] = $receivedValue->replace( $mergeData[$property['id']] = $receivedValue->replace(
["\e", "\f", "\n", "\r", "\t", "\v", "\\"], ["\e", "\f", "\n", "\r", "\t", "\v", '\\'],
["\\e", "\\f", "\\n", "\\r", "\\t", "\\v", "\\\\"] ['\\e', '\\f', '\\n', '\\r', '\\t', '\\v', '\\\\']
)->toString(); )->toString();
} }
} }

View File

@ -17,12 +17,14 @@ class StoreFormZapierWebhookRequest extends FormRequest
{ {
return [ return [
'form_slug' => 'required|exists:forms,slug', 'form_slug' => 'required|exists:forms,slug',
'hook_url' => 'required|string|url' 'hook_url' => 'required|string|url',
]; ];
} }
public function instanciateHook() { public function instanciateHook()
{
$form = Form::whereSlug($this->form_slug)->firstOrFail(); $form = Form::whereSlug($this->form_slug)->firstOrFail();
return new FormZapierWebhook([ return new FormZapierWebhook([
'form_id' => $form->id, 'form_id' => $form->id,
'hook_url' => $this->hook_url, 'hook_url' => $this->hook_url,

View File

@ -2,9 +2,6 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Models\Forms\Form;
use Illuminate\Validation\Rule;
class StoreFormRequest extends UserFormRequest class StoreFormRequest extends UserFormRequest
{ {
/** /**

View File

@ -7,7 +7,7 @@ use Illuminate\Foundation\Http\FormRequest;
class FormTemplateRequest extends FormRequest class FormTemplateRequest extends FormRequest
{ {
const IGNORED_KEYS = [ public const IGNORED_KEYS = [
'id', 'id',
'creator', 'creator',
'cleanings', 'cleanings',
@ -52,6 +52,7 @@ class FormTemplateRequest extends FormRequest
if ($this->id) { if ($this->id) {
$slugRule = ','.$this->id; $slugRule = ','.$this->id;
} }
return [ return [
'form' => 'required|array', 'form' => 'required|array',
'publicly_listed' => 'boolean', 'publicly_listed' => 'boolean',
@ -88,7 +89,7 @@ class FormTemplateRequest extends FormRequest
'types' => $this->types ?? [], 'types' => $this->types ?? [],
'industries' => $this->industries ?? [], 'industries' => $this->industries ?? [],
'related_templates' => $this->related_templates ?? [], 'related_templates' => $this->related_templates ?? [],
'questions' => $this->questions ?? [] 'questions' => $this->questions ?? [],
]); ]);
} }
} }

View File

@ -4,5 +4,4 @@ namespace App\Http\Requests;
class UpdateFormRequest extends UserFormRequest class UpdateFormRequest extends UserFormRequest
{ {
} }

View File

@ -7,7 +7,7 @@ use Illuminate\Foundation\Http\FormRequest;
class UploadAssetRequest extends FormRequest class UploadAssetRequest extends FormRequest
{ {
const FORM_ASSET_MAX_SIZE = 5000000; public const FORM_ASSET_MAX_SIZE = 5000000;
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
@ -22,7 +22,7 @@ class UploadAssetRequest extends FormRequest
'jpg', 'jpg',
'bmp', 'bmp',
'gif', 'gif',
'svg' 'svg',
]; ];
if ($this->offsetExists('type') && $this->get('type') === 'files') { if ($this->offsetExists('type') && $this->get('type') === 'files') {
$fileTypes = []; $fileTypes = [];

View File

@ -1,20 +1,17 @@
<?php <?php
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Http\Requests\Workspace\CustomDomainRequest; use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Rules\FormPropertyLogicRule;
use App\Rules\OneEmailPerLine; use App\Rules\OneEmailPerLine;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use App\Rules\FormPropertyLogicRule;
/** /**
* Abstract class to validate create/update forms * Abstract class to validate create/update forms
* *
* Class UserFormRequest * Class UserFormRequest
* @package App\Http\Requests
*/ */
abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
{ {
@ -34,7 +31,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
// Notifications // Notifications
'notifies' => 'boolean', 'notifies' => 'boolean',
'notification_emails' => ['required_if:notifies,1', new OneEmailPerLine ], 'notification_emails' => ['required_if:notifies,1', new OneEmailPerLine()],
'send_submission_confirmation' => 'boolean', 'send_submission_confirmation' => 'boolean',
'notification_sender' => 'string|max:64', 'notification_sender' => 'string|max:64',
'notification_subject' => 'string|max:200', 'notification_subject' => 'string|max:200',

View File

@ -8,10 +8,12 @@ use Illuminate\Http\Request;
class CustomDomainRequest extends FormRequest class CustomDomainRequest extends FormRequest
{ {
const CUSTOM_DOMAINS_REGEX = '/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,20}$/'; public const CUSTOM_DOMAINS_REGEX = '/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,20}$/';
public Workspace $workspace; public Workspace $workspace;
public array $customDomains = []; public array $customDomains = [];
public function __construct(Request $request, Workspace $workspace) public function __construct(Request $request, Workspace $workspace)
{ {
$this->workspace = Workspace::findOrFail($request->workspaceId); $this->workspace = Workspace::findOrFail($request->workspaceId);
@ -48,12 +50,13 @@ class CustomDomainRequest extends FormRequest
} }
$this->customDomains = $domains->toArray(); $this->customDomains = $domains->toArray();
} },
] ],
]; ];
} }
protected function passedValidation(){ protected function passedValidation()
{
$this->replace(['custom_domains' => $this->customDomains]); $this->replace(['custom_domains' => $this->customDomains]);
} }
} }

View File

@ -3,10 +3,8 @@
namespace App\Http\Resources; namespace App\Http\Resources;
use App\Http\Middleware\Form\ProtectedForm; use App\Http\Middleware\Form\ProtectedForm;
use App\Http\Resources\UserResource;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
class FormResource extends JsonResource class FormResource extends JsonResource
{ {
@ -69,6 +67,7 @@ class FormResource extends JsonResource
public function setCleanings(array $cleanings) public function setCleanings(array $cleanings)
{ {
$this->cleanings = $cleanings; $this->cleanings = $cleanings;
return $this; return $this;
} }
@ -96,15 +95,18 @@ class FormResource extends JsonResource
]; ];
} }
private function getWorkspace() { private function getWorkspace()
{
return $this->extra?->loadedWorkspace ?? $this->workspace; return $this->extra?->loadedWorkspace ?? $this->workspace;
} }
private function workspaceIsPro() { private function workspaceIsPro()
{
return $this->extra?->workspaceIsPro ?? $this->getWorkspace()->is_pro ?? $this->is_pro; return $this->extra?->workspaceIsPro ?? $this->getWorkspace()->is_pro ?? $this->is_pro;
} }
private function userIsFormOwner() { private function userIsFormOwner()
{
return $this->extra?->userIsOwner ?? return $this->extra?->userIsOwner ??
( (
Auth::check() && Auth::user()->ownsForm($this->resource) Auth::check() && Auth::user()->ownsForm($this->resource)

View File

@ -6,7 +6,6 @@ use Illuminate\Http\Resources\Json\JsonResource;
class FormSubmissionResource extends JsonResource class FormSubmissionResource extends JsonResource
{ {
/** /**
* Transform the resource into an array. * Transform the resource into an array.
* *
@ -28,13 +27,14 @@ class FormSubmissionResource extends JsonResource
private function addExtraData() private function addExtraData()
{ {
$this->data = array_merge($this->data, [ $this->data = array_merge($this->data, [
"created_at" => $this->created_at->toDateTimeString(), 'created_at' => $this->created_at->toDateTimeString(),
'id' => $this->id 'id' => $this->id,
]); ]);
} }
/** /**
* Link to the file (generating signed s3 URL) * Link to the file (generating signed s3 URL)
*
* @return void * @return void
*/ */
private function generateFileLinks() private function generateFileLinks()

View File

@ -15,7 +15,7 @@ class FormTemplateResource extends JsonResource
public function toArray($request) public function toArray($request)
{ {
return array_merge(parent::toArray($request), [ return array_merge(parent::toArray($request), [
'is_new' => $this->created_at->isAfter(now()->subDays(7)) 'is_new' => $this->created_at->isAfter(now()->subDays(7)),
]); ]);
} }
} }

View File

@ -3,7 +3,6 @@
namespace App\Http\Resources; namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
class WorkspaceResource extends JsonResource class WorkspaceResource extends JsonResource
{ {

View File

@ -14,7 +14,10 @@ use Illuminate\Support\Str;
class GenerateAiForm implements ShouldQueue class GenerateAiForm implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/** /**
* Create a new job instance. * Create a new job instance.
@ -34,7 +37,7 @@ class GenerateAiForm implements ShouldQueue
public function handle() public function handle()
{ {
$this->completion->update([ $this->completion->update([
'status' => AiFormCompletion::STATUS_PROCESSING 'status' => AiFormCompletion::STATUS_PROCESSING,
]); ]);
$completer = (new GptCompleter(config('services.openai.api_key'))) $completer = (new GptCompleter(config('services.openai.api_key')))
@ -44,13 +47,13 @@ class GenerateAiForm implements ShouldQueue
try { try {
$completer->completeChat([ $completer->completeChat([
["role" => "user", "content" => Str::of(GenerateTemplate::FORM_STRUCTURE_PROMPT) ['role' => 'user', 'content' => Str::of(GenerateTemplate::FORM_STRUCTURE_PROMPT)
->replace('[REPLACE]', $this->completion->form_prompt)->toString()] ->replace('[REPLACE]', $this->completion->form_prompt)->toString()],
], 3000); ], 3000);
$this->completion->update([ $this->completion->update([
'status' => AiFormCompletion::STATUS_COMPLETED, 'status' => AiFormCompletion::STATUS_COMPLETED,
'result' => $this->cleanOutput($completer->getArray()) 'result' => $this->cleanOutput($completer->getArray()),
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->onError($e); $this->onError($e);
@ -76,10 +79,11 @@ class GenerateAiForm implements ShouldQueue
$this->onError($exception); $this->onError($exception);
} }
private function onError(\Throwable $e) { private function onError(\Throwable $e)
{
$this->completion->update([ $this->completion->update([
'status' => AiFormCompletion::STATUS_FAILED, 'status' => AiFormCompletion::STATUS_FAILED,
'result' => ['error' => $e->getMessage()] 'result' => ['error' => $e->getMessage()],
]); ]);
} }
} }

View File

@ -3,15 +3,14 @@
namespace App\Jobs\Form; namespace App\Jobs\Form;
use App\Events\Forms\FormSubmitted; use App\Events\Forms\FormSubmitted;
use App\Http\Controllers\Forms\PublicFormController;
use App\Http\Controllers\Forms\FormController; use App\Http\Controllers\Forms\FormController;
use App\Http\Controllers\Forms\PublicFormController;
use App\Http\Requests\AnswerFormRequest; use App\Http\Requests\AnswerFormRequest;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Models\Forms\FormSubmission; use App\Models\Forms\FormSubmission;
use App\Service\Forms\FormLogicPropertyResolver; use App\Service\Forms\FormLogicPropertyResolver;
use App\Service\Storage\StorageFileNameParser; use App\Service\Storage\StorageFileNameParser;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@ -22,7 +21,10 @@ use Vinkla\Hashids\Facades\Hashids;
class StoreFormSubmissionJob implements ShouldQueue class StoreFormSubmissionJob implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public ?string $submissionId = null; public ?string $submissionId = null;
@ -47,7 +49,7 @@ class StoreFormSubmissionJob implements ShouldQueue
$this->storeSubmission($formData); $this->storeSubmission($formData);
$formData["submission_id"] = $this->submissionId; $formData['submission_id'] = $this->submissionId;
FormSubmitted::dispatch($this->form, $formData); FormSubmitted::dispatch($this->form, $formData);
} }
@ -59,6 +61,7 @@ class StoreFormSubmissionJob implements ShouldQueue
public function setSubmissionId(int $id) public function setSubmissionId(int $id)
{ {
$this->submissionId = $id; $this->submissionId = $id;
return $this; return $this;
} }
@ -88,6 +91,7 @@ class StoreFormSubmissionJob implements ShouldQueue
if ($this->form->editable_submissions && isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) { if ($this->form->editable_submissions && isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) {
$submissionId = $this->submissionData['submission_id'] ? Hashids::decode($this->submissionData['submission_id']) : false; $submissionId = $this->submissionData['submission_id'] ? Hashids::decode($this->submissionData['submission_id']) : false;
$submissionId = $submissionId[0] ?? null; $submissionId = $submissionId[0] ?? null;
return $this->form->submissions()->findOrFail($submissionId); return $this->form->submissions()->findOrFail($submissionId);
} }
@ -128,10 +132,10 @@ class StoreFormSubmissionJob implements ShouldQueue
} }
} else { } else {
if ($field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) { 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"; $finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid() : 'Please upgrade your OpenForm subscription to use our ID generation features';
} else { } else {
if ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) { 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"; $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 { } else {
$finalData[$field['id']] = $answerValue; $finalData[$field['id']] = $answerValue;
} }
@ -156,6 +160,7 @@ class StoreFormSubmissionJob implements ShouldQueue
private function isSkipForUpload($value) private function isSkipForUpload($value)
{ {
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id); $newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
return Storage::exists($newPath.'/'.$value); return Storage::exists($newPath.'/'.$value);
} }
@ -178,6 +183,7 @@ class StoreFormSubmissionJob implements ShouldQueue
$path = FormController::ASSETS_UPLOAD_PATH.'/'.$fileName; $path = FormController::ASSETS_UPLOAD_PATH.'/'.$fileName;
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id); $newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
Storage::move($path, $newPath.'/'.$fileName); Storage::move($path, $newPath.'/'.$fileName);
return $fileName; return $fileName;
} }
@ -225,7 +231,6 @@ class StoreFormSubmissionJob implements ShouldQueue
/** /**
* Adds prefill from hidden fields * Adds prefill from hidden fields
* *
* @param array $formData
* @param AnswerFormRequest $request * @param AnswerFormRequest $request
*/ */
private function addHiddenPrefills(array &$formData): void private function addHiddenPrefills(array &$formData): void

View File

@ -3,8 +3,6 @@
namespace App\Listeners; namespace App\Listeners;
use App\Notifications\Forms\FailedWebhookNotification; use App\Notifications\Forms\FailedWebhookNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Spatie\WebhookServer\Events\WebhookCallFailedEvent; use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
class FailedWebhookListener class FailedWebhookListener
@ -24,15 +22,16 @@ class FailedWebhookListener
'webhook_url' => $event->webhookUrl, 'webhook_url' => $event->webhookUrl,
'exception' => $event->errorType, 'exception' => $event->errorType,
'message' => $event->errorMessage, 'message' => $event->errorMessage,
'form_id' => $event->meta['form']->id 'form_id' => $event->meta['form']->id,
]); ]);
return; return;
} }
\Log::error('Failed webhook', [ \Log::error('Failed webhook', [
'webhook_url' => $event->webhookUrl, 'webhook_url' => $event->webhookUrl,
'exception' => $event->errorType, 'exception' => $event->errorType,
'message' => $event->errorMessage 'message' => $event->errorMessage,
]); ]);
} }
} }

View File

@ -5,12 +5,10 @@ namespace App\Listeners\Forms;
use App\Events\Models\FormCreated; use App\Events\Models\FormCreated;
use App\Mail\Forms\FormCreationConfirmationMail; use App\Mail\Forms\FormCreationConfirmationMail;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
class FormCreationConfirmation implements ShouldQueue class FormCreationConfirmation implements ShouldQueue
{ {
/** /**
* Handle the event. * Handle the event.
* *

View File

@ -2,16 +2,13 @@
namespace App\Listeners\Forms; namespace App\Listeners\Forms;
use App\Models\Forms\Form;
use App\Events\Forms\FormSubmitted; use App\Events\Forms\FormSubmitted;
use Illuminate\Support\Facades\Http; use App\Models\Forms\Form;
use Spatie\WebhookServer\WebhookCall;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
use App\Service\Forms\FormSubmissionFormatter;
use App\Service\Forms\Webhooks\WebhookHandlerProvider;
use App\Notifications\Forms\FormSubmissionNotification; use App\Notifications\Forms\FormSubmissionNotification;
use App\Service\Forms\Webhooks\WebhookHandlerProvider;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Notification;
class NotifyFormSubmission implements ShouldQueue class NotifyFormSubmission implements ShouldQueue
{ {
@ -46,12 +43,14 @@ class NotifyFormSubmission implements ShouldQueue
/** /**
* Sends an email to each email address in the form's notification_emails field * Sends an email to each email address in the form's notification_emails field
* @param FormSubmitted $event *
* @return void * @return void
*/ */
private function sendEmailNotifications(FormSubmitted $event) private function sendEmailNotifications(FormSubmitted $event)
{ {
if (!$event->form->is_pro || !$event->form->notifies) return; if (! $event->form->is_pro || ! $event->form->notifies) {
return;
}
$subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function ( $subscribers = collect(preg_split("/\r\n|\n|\r/", $event->form->notification_emails))->filter(function (
$email $email

View File

@ -12,13 +12,12 @@ use Illuminate\Support\Facades\Mail;
* Sends a confirmation to form respondant that form was submitted * Sends a confirmation to form respondant that form was submitted
* *
* Class SubmissionConfirmation * Class SubmissionConfirmation
* @package App\Listeners\Forms
*/ */
class SubmissionConfirmation implements ShouldQueue class SubmissionConfirmation implements ShouldQueue
{ {
use InteractsWithQueue; use InteractsWithQueue;
const RISKY_USERS_LIMIT = 120; public const RISKY_USERS_LIMIT = 120;
/** /**
* Handle the event. * Handle the event.
@ -37,7 +36,9 @@ class SubmissionConfirmation implements ShouldQueue
} }
$email = $this->getRespondentEmail($event); $email = $this->getRespondentEmail($event);
if (!$email) return; if (! $email) {
return;
}
\Log::info('Sending submission confirmation', [ \Log::info('Sending submission confirmation', [
'recipient' => $email, 'recipient' => $email,
@ -52,13 +53,18 @@ class SubmissionConfirmation implements ShouldQueue
// Make sure we only have one email field in the form // Make sure we only have one email field in the form
$emailFields = collect($event->form->properties)->filter(function ($field) { $emailFields = collect($event->form->properties)->filter(function ($field) {
$hidden = $field['hidden'] ?? false; $hidden = $field['hidden'] ?? false;
return ! $hidden && $field['type'] == 'email'; return ! $hidden && $field['type'] == 'email';
}); });
if ($emailFields->count() != 1) return null; if ($emailFields->count() != 1) {
return null;
}
if (isset($event->data[$emailFields->first()['id']])) { if (isset($event->data[$emailFields->first()['id']])) {
$email = $event->data[$emailFields->first()['id']]; $email = $event->data[$emailFields->first()['id']];
if ($this->validateEmail($email)) return $email; if ($this->validateEmail($email)) {
return $email;
}
} }
return null; return null;
@ -73,13 +79,16 @@ class SubmissionConfirmation implements ShouldQueue
'form_id' => $event->form->id, 'form_id' => $event->form->id,
'workspace_id' => $event->form->workspace->id, 'workspace_id' => $event->form->workspace->id,
]); ]);
return true; return true;
} }
} }
return false; return false;
} }
public static function validateEmail($email): bool { public static function validateEmail($email): bool
return (boolean) filter_var($email, FILTER_VALIDATE_EMAIL); {
return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
} }
} }

View File

@ -6,12 +6,12 @@ use App\Mail\OpenFormMail;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class FormCreationConfirmationMail extends OpenFormMail implements ShouldQueue class FormCreationConfirmationMail extends OpenFormMail implements ShouldQueue
{ {
use Queueable, SerializesModels; use Queueable;
use SerializesModels;
/** /**
* Create a new message instance. * Create a new message instance.

View File

@ -8,13 +8,14 @@ use App\Service\Forms\FormSubmissionFormatter;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Vinkla\Hashids\Facades\Hashids; use Vinkla\Hashids\Facades\Hashids;
class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
{ {
use Queueable, SerializesModels; use Queueable;
use SerializesModels;
/** /**
* Create a new message instance. * Create a new message instance.
@ -22,7 +23,8 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
* @return void * @return void
*/ */
public function __construct(private FormSubmitted $event) public function __construct(private FormSubmitted $event)
{} {
}
/** /**
* Build the message. * Build the message.
@ -46,19 +48,21 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
'fields' => $formatter->getFieldsWithValue(), 'fields' => $formatter->getFieldsWithValue(),
'form' => $form, 'form' => $form,
'noBranding' => $form->no_branding, 'noBranding' => $form->no_branding,
'submission_id' => (isset($this->event->data['submission_id']) && $this->event->data['submission_id']) ? Hashids::encode($this->event->data['submission_id']) : null 'submission_id' => (isset($this->event->data['submission_id']) && $this->event->data['submission_id']) ? Hashids::encode($this->event->data['submission_id']) : null,
]); ]);
} }
private function getFromEmail() private function getFromEmail()
{ {
$originalFromAddress = Str::of(config('mail.from.address'))->explode('@'); $originalFromAddress = Str::of(config('mail.from.address'))->explode('@');
return $originalFromAddress->first().'+'.time().'@'.$originalFromAddress->last(); return $originalFromAddress->first().'+'.time().'@'.$originalFromAddress->last();
} }
private function getReplyToEmail($default) private function getReplyToEmail($default)
{ {
$replyTo = Arr::get((array) $this->event->form->notification_settings, 'confirmation_reply_to', null); $replyTo = Arr::get((array) $this->event->form->notification_settings, 'confirmation_reply_to', null);
return $replyTo ?? $default; return $replyTo ?? $default;
} }
} }

View File

@ -3,11 +3,11 @@
namespace App\Mail; namespace App\Mail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
abstract class OpenFormMail extends Mailable abstract class OpenFormMail extends Mailable
{ {
use Queueable, SerializesModels; use Queueable;
use SerializesModels;
} }

View File

@ -10,10 +10,13 @@ class AiFormCompletion extends Model
{ {
use HasFactory; use HasFactory;
const STATUS_PENDING = 'pending'; public const STATUS_PENDING = 'pending';
const STATUS_PROCESSING = 'processing';
const STATUS_COMPLETED = 'completed'; public const STATUS_PROCESSING = 'processing';
const STATUS_FAILED = 'failed';
public const STATUS_COMPLETED = 'completed';
public const STATUS_FAILED = 'failed';
protected $table = 'ai_form_completions'; protected $table = 'ai_form_completions';
@ -21,11 +24,11 @@ class AiFormCompletion extends Model
'form_prompt', 'form_prompt',
'status', 'status',
'result', 'result',
'ip' 'ip',
]; ];
protected $attributes = [ protected $attributes = [
'status' => self::STATUS_PENDING 'status' => self::STATUS_PENDING,
]; ];
protected static function booted() protected static function booted()

View File

@ -9,26 +9,31 @@ use App\Models\Traits\CachesAttributes;
use App\Models\User; use App\Models\User;
use App\Models\Workspace; use App\Models\Workspace;
use Database\Factories\FormFactory; use Database\Factories\FormFactory;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Sluggable\HasSlug; use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions; use Spatie\Sluggable\SlugOptions;
use Stevebauman\Purify\Facades\Purify; use Stevebauman\Purify\Facades\Purify;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Form extends Model implements CachableAttributes class Form extends Model implements CachableAttributes
{ {
use CachesAttributes; use CachesAttributes;
const DARK_MODE_VALUES = ['auto', 'light', 'dark']; use HasFactory;
const THEMES = ['default', 'simple', 'notion']; use HasSlug;
const WIDTHS = ['centered', 'full']; use SoftDeletes;
const VISIBILITY = ['public', 'draft', 'closed'];
use HasFactory, HasSlug, SoftDeletes; public const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
public const THEMES = ['default', 'simple', 'notion'];
public const WIDTHS = ['centered', 'full'];
public const VISIBILITY = ['public', 'draft', 'closed'];
protected $fillable = [ protected $fillable = [
'workspace_id', 'workspace_id',
@ -94,7 +99,7 @@ class Form extends Model implements CachableAttributes
'password', 'password',
// Custom SEO // Custom SEO
'seo_meta' 'seo_meta',
]; ];
protected $casts = [ protected $casts = [
@ -104,7 +109,7 @@ class Form extends Model implements CachableAttributes
'tags' => 'array', 'tags' => 'array',
'removed_properties' => 'array', 'removed_properties' => 'array',
'seo_meta' => 'object', 'seo_meta' => 'object',
'notification_settings' => 'object' 'notification_settings' => 'object',
]; ];
protected $appends = [ protected $appends = [
@ -127,13 +132,13 @@ class Form extends Model implements CachableAttributes
'password', 'password',
'tags', 'tags',
'notification_emails', 'notification_emails',
'removed_properties' 'removed_properties',
]; ];
protected $cachableAttributes = [ protected $cachableAttributes = [
'is_pro', 'is_pro',
'views_count', 'views_count',
'max_file_size' 'max_file_size',
]; ];
/** /**
@ -157,6 +162,7 @@ class Form extends Model implements CachableAttributes
if ($this->custom_domain) { if ($this->custom_domain) {
return 'https://'.$this->custom_domain.'/forms/'.$this->slug; return 'https://'.$this->custom_domain.'/forms/'.$this->slug;
} }
return front_url('/forms/'.$this->slug); return front_url('/forms/'.$this->slug);
} }
@ -177,6 +183,7 @@ class Form extends Model implements CachableAttributes
return (int) ($this->views()->count() + return (int) ($this->views()->count() +
$this->statistics()->sum(DB::raw("json_extract(data, '$.views')"))); $this->statistics()->sum(DB::raw("json_extract(data, '$.views')")));
} }
return $this->views()->count() + return $this->views()->count() +
$this->statistics()->sum(DB::raw("cast(data->>'views' as integer)")); $this->statistics()->sum(DB::raw("cast(data->>'views' as integer)"));
}); });
@ -204,20 +211,21 @@ class Form extends Model implements CachableAttributes
public function getIsClosedAttribute() public function getIsClosedAttribute()
{ {
return ($this->closes_at && now()->gt($this->closes_at)); return $this->closes_at && now()->gt($this->closes_at);
} }
public function getFormPendingSubmissionKeyAttribute() public function getFormPendingSubmissionKeyAttribute()
{ {
if ($this->updated_at?->timestamp) { if ($this->updated_at?->timestamp) {
return "openform-" . $this->id . "-pending-submission-" . substr($this->updated_at?->timestamp, -6); return 'openform-'.$this->id.'-pending-submission-'.substr($this->updated_at?->timestamp, -6);
} }
return null; return null;
} }
public function getMaxNumberOfSubmissionsReachedAttribute() public function getMaxNumberOfSubmissionsReachedAttribute()
{ {
return ($this->max_submissions_count && $this->max_submissions_count <= $this->submissions_count); return $this->max_submissions_count && $this->max_submissions_count <= $this->submissions_count;
} }
public function setClosedTextAttribute($value) public function setClosedTextAttribute($value)
@ -302,7 +310,6 @@ class Form extends Model implements CachableAttributes
return FormFactory::new(); return FormFactory::new();
} }
public function getNotifiesWebhookAttribute() public function getNotifiesWebhookAttribute()
{ {
return ! empty($this->webhook_url); return ! empty($this->webhook_url);

View File

@ -2,7 +2,6 @@
namespace App\Models\Forms; namespace App\Models\Forms;
use App\Models\Forms\Form;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -15,7 +14,7 @@ class FormStatistic extends Model
protected $fillable = [ protected $fillable = [
'form_id', 'form_id',
'data', 'data',
'date' 'date',
]; ];
/** /**
@ -34,5 +33,4 @@ class FormStatistic extends Model
{ {
return $this->belongsTo(Form::class); return $this->belongsTo(Form::class);
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Models\Forms; namespace App\Models\Forms;
use App\Models\Forms\Form;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -11,17 +10,18 @@ class FormSubmission extends Model
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'data' 'data',
]; ];
protected $casts = [ protected $casts = [
'data' => 'array' 'data' => 'array',
]; ];
/** /**
* RelationShips * RelationShips
*/ */
public function form() { public function form()
{
return $this->belongsTo(Form::class); return $this->belongsTo(Form::class);
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Models\Forms; namespace App\Models\Forms;
use App\Models\Forms\Form;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -13,7 +12,8 @@ class FormView extends Model
/** /**
* RelationShips * RelationShips
*/ */
public function form() { public function form()
{
return $this->belongsTo(Form::class); return $this->belongsTo(Form::class);
} }
} }

View File

@ -7,11 +7,11 @@ use App\Service\Forms\Webhooks\WebhookHandlerProvider;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\WebhookServer\WebhookCall;
class FormZapierWebhook extends Model class FormZapierWebhook extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory;
use SoftDeletes;
protected $table = 'form_zapier_webhooks'; protected $table = 'form_zapier_webhooks';

View File

@ -7,17 +7,17 @@ use Illuminate\Database\Eloquent\Model;
class License extends Model class License extends Model
{ {
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
use HasFactory; use HasFactory;
public const STATUS_ACTIVE = 'active';
public const STATUS_INACTIVE = 'inactive';
protected $fillable = [ protected $fillable = [
'license_key', 'license_key',
'user_id', 'user_id',
'license_provider', 'license_provider',
'status', 'status',
'meta' 'meta',
]; ];
protected $casts = [ protected $casts = [

View File

@ -11,7 +11,8 @@ use Stevebauman\Purify\Facades\Purify;
class Template extends Model class Template extends Model
{ {
use HasFactory, HasSlug; use HasFactory;
use HasSlug;
protected $fillable = [ protected $fillable = [
'creator_id', 'creator_id',
@ -25,7 +26,7 @@ class Template extends Model
'publicly_listed', 'publicly_listed',
'industries', 'industries',
'types', 'types',
'related_templates' 'related_templates',
]; ];
protected $casts = [ protected $casts = [
@ -93,7 +94,8 @@ class Template extends Model
array_values( array_values(
json_decode( json_decode(
file_get_contents(resource_path('data/forms/templates/types.json')), file_get_contents(resource_path('data/forms/templates/types.json')),
true) true
)
) )
)->values(); )->values();
} }
@ -104,7 +106,8 @@ class Template extends Model
array_values( array_values(
json_decode( json_decode(
file_get_contents(resource_path('data/forms/templates/industries.json')), file_get_contents(resource_path('data/forms/templates/industries.json')),
true) true
)
) )
)->values(); )->values();
} }

View File

@ -9,9 +9,6 @@ interface CachableAttributes
/** /**
* Get an item from the cache, or execute the given Closure and store the result. * Get an item from the cache, or execute the given Closure and store the result.
* *
* @param string $key
* @param int|null $ttl
* @param Closure $callback
* *
* @return mixed * @return mixed
*/ */
@ -20,8 +17,6 @@ interface CachableAttributes
/** /**
* Get an item from the cache, or execute the given Closure and store the result forever. * Get an item from the cache, or execute the given Closure and store the result forever.
* *
* @param string $key
* @param \Closure $callback
* *
* @return mixed * @return mixed
*/ */
@ -29,17 +24,11 @@ interface CachableAttributes
/** /**
* Remove an item from the cache. * Remove an item from the cache.
*
* @param string $key
*
* @return bool
*/ */
public function forget(string $key): bool; public function forget(string $key): bool;
/** /**
* Remove all items from the cache. * Remove all items from the cache.
*
* @return bool
*/ */
public function flush(): bool; public function flush(): bool;
} }

View File

@ -84,7 +84,7 @@ trait CachesAttributes
$this->getTable(), $this->getTable(),
$this->getKey(), $this->getKey(),
$attribute, $attribute,
$this->updated_at?->timestamp ?? '0' $this->updated_at?->timestamp ?? '0',
]); ]);
} }

View File

@ -7,14 +7,15 @@ use App\Notifications\ResetPassword;
use App\Notifications\VerifyEmail; use App\Notifications\VerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable; use Laravel\Cashier\Billable;
use Tymon\JWTAuth\Contracts\JWTSubject; use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject class User extends Authenticatable implements JWTSubject
{ {
use Notifiable, HasFactory, Billable; use Billable;
use HasFactory;
use Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -25,7 +26,7 @@ class User extends Authenticatable implements JWTSubject
'name', 'name',
'email', 'email',
'password', 'password',
'hear_about_us' 'hear_about_us',
]; ];
/** /**
@ -36,7 +37,7 @@ class User extends Authenticatable implements JWTSubject
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
'hear_about_us' 'hear_about_us',
]; ];
/** /**
@ -136,7 +137,7 @@ class User extends Authenticatable implements JWTSubject
*/ */
public function sendEmailVerificationNotification() public function sendEmailVerificationNotification()
{ {
$this->notify(new VerifyEmail); $this->notify(new VerifyEmail());
} }
/** /**
@ -144,7 +145,6 @@ class User extends Authenticatable implements JWTSubject
* Relationship * Relationship
* ================================= * =================================
*/ */
public function workspaces() public function workspaces()
{ {
return $this->belongsToMany(Workspace::class); return $this->belongsToMany(Workspace::class);
@ -246,5 +246,4 @@ class User extends Authenticatable implements JWTSubject
}); });
}); });
} }
} }

View File

@ -10,11 +10,15 @@ use Illuminate\Database\Eloquent\Model;
class Workspace extends Model implements CachableAttributes class Workspace extends Model implements CachableAttributes
{ {
use HasFactory, CachesAttributes; use CachesAttributes;
use HasFactory;
public const MAX_FILE_SIZE_FREE = 5000000; // 5 MB
public const MAX_FILE_SIZE_PRO = 50000000; // 50 MB
public const MAX_DOMAIN_PRO = 1;
const MAX_FILE_SIZE_FREE = 5000000; // 5 MB
const MAX_FILE_SIZE_PRO = 50000000; // 50 MB
const MAX_DOMAIN_PRO = 1;
protected $fillable = [ protected $fillable = [
'name', 'name',
'icon', 'icon',
@ -24,7 +28,7 @@ class Workspace extends Model implements CachableAttributes
protected $appends = [ protected $appends = [
'is_pro', 'is_pro',
'is_enterprise' 'is_enterprise',
]; ];
protected $casts = [ protected $casts = [
@ -37,7 +41,7 @@ class Workspace extends Model implements CachableAttributes
'is_risky', 'is_risky',
'submissions_count', 'submissions_count',
'max_file_size', 'max_file_size',
'custom_domain_count' 'custom_domain_count',
]; ];
public function getMaxFileSizeAttribute() public function getMaxFileSizeAttribute()
@ -55,6 +59,7 @@ class Workspace extends Model implements CachableAttributes
return $license->max_file_size; return $license->max_file_size;
} }
} }
return self::MAX_FILE_SIZE_PRO; return self::MAX_FILE_SIZE_PRO;
} }
@ -75,6 +80,7 @@ class Workspace extends Model implements CachableAttributes
// In case of special License // In case of special License
return $license->custom_domain_limit_count; return $license->custom_domain_limit_count;
} }
return self::MAX_DOMAIN_PRO; return self::MAX_DOMAIN_PRO;
} }
} }
@ -96,6 +102,7 @@ class Workspace extends Model implements CachableAttributes
return true; return true;
} }
} }
return false; return false;
}); });
} }
@ -113,6 +120,7 @@ class Workspace extends Model implements CachableAttributes
return true; return true;
} }
} }
return false; return false;
}); });
} }
@ -146,7 +154,6 @@ class Workspace extends Model implements CachableAttributes
/** /**
* Relationships * Relationships
*/ */
public function users() public function users()
{ {
return $this->belongsToMany(User::class); return $this->belongsToMany(User::class);

View File

@ -6,7 +6,6 @@ use App\Models\Forms\Form;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
use Spatie\WebhookServer\Events\WebhookCallFailedEvent; use Spatie\WebhookServer\Events\WebhookCallFailedEvent;
class FailedWebhookNotification extends Notification class FailedWebhookNotification extends Notification
@ -14,6 +13,7 @@ class FailedWebhookNotification extends Notification
use Queueable; use Queueable;
public Form $form; public Form $form;
public string $provider; public string $provider;
/** /**
@ -46,7 +46,7 @@ class FailedWebhookNotification extends Notification
*/ */
public function toMail($notifiable) public function toMail($notifiable)
{ {
return (new MailMessage) return (new MailMessage())
->subject("Notification issue with your NotionForm: '".$this->form->title."'") ->subject("Notification issue with your NotionForm: '".$this->form->title."'")
->markdown('mail.form.webhook-error', [ ->markdown('mail.form.webhook-error', [
'provider' => $this->provider, 'provider' => $this->provider,

View File

@ -8,8 +8,8 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class FormSubmissionNotification extends Notification implements ShouldQueue class FormSubmissionNotification extends Notification implements ShouldQueue
{ {
@ -52,7 +52,7 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
->outputStringsOnly() ->outputStringsOnly()
->useSignedUrlForFiles(); ->useSignedUrlForFiles();
return (new MailMessage) return (new MailMessage())
->replyTo($this->getReplyToEmail($notifiable->routes['mail'])) ->replyTo($this->getReplyToEmail($notifiable->routes['mail']))
->from($this->getFromEmail(), config('app.name')) ->from($this->getFromEmail(), config('app.name'))
->subject('New form submission for "'.$this->event->form->title.'"') ->subject('New form submission for "'.$this->event->form->title.'"')
@ -65,6 +65,7 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
private function getFromEmail() private function getFromEmail()
{ {
$originalFromAddress = Str::of(config('mail.from.address'))->explode('@'); $originalFromAddress = Str::of(config('mail.from.address'))->explode('@');
return $originalFromAddress->first().'+'.time().'@'.$originalFromAddress->last(); return $originalFromAddress->first().'+'.time().'@'.$originalFromAddress->last();
} }
@ -74,6 +75,7 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
if ($replyTo && $this->validateEmail($replyTo)) { if ($replyTo && $this->validateEmail($replyTo)) {
return $replyTo; return $replyTo;
} }
return $this->getRespondentEmail() ?? $default; return $this->getRespondentEmail() ?? $default;
} }
@ -103,5 +105,4 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
{ {
return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
} }
} }

View File

@ -15,7 +15,7 @@ class ResetPassword extends Notification
*/ */
public function toMail($notifiable) public function toMail($notifiable)
{ {
return (new MailMessage) return (new MailMessage())
->line('You are receiving this email because we received a password reset request for your account.') ->line('You are receiving this email because we received a password reset request for your account.')
->action('Reset Password', front_url('password/reset/'.$this->token).'?email='.urlencode($notifiable->email)) ->action('Reset Password', front_url('password/reset/'.$this->token).'?email='.urlencode($notifiable->email))
->line('If you did not request a password reset, no further action is required.'); ->line('If you did not request a password reset, no further action is required.');

View File

@ -30,7 +30,7 @@ class FailedPaymentNotification extends Notification implements ShouldQueue
*/ */
public function toMail($notifiable) public function toMail($notifiable)
{ {
return (new MailMessage) return (new MailMessage())
->subject('Your Payment Failed') ->subject('Your Payment Failed')
->greeting(__('We tried to charge your card for your OpenForm subscription but the payment but did not work.')) ->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". ->line(__('Please go to OpenForm, click on your name on the top right corner, and click on "Billing".

View File

@ -17,7 +17,9 @@ class VerifyEmail extends Notification
protected function verificationUrl($notifiable) protected function verificationUrl($notifiable)
{ {
$url = URL::temporarySignedRoute( $url = URL::temporarySignedRoute(
'verification.verify', Carbon::now()->addMinutes(60), ['user' => $notifiable->id] 'verification.verify',
Carbon::now()->addMinutes(60),
['user' => $notifiable->id]
); );
return str_replace('/api', '', $url); return str_replace('/api', '', $url);

View File

@ -13,7 +13,6 @@ class FormPolicy
/** /**
* Determine whether the user can view any models. * Determine whether the user can view any models.
* *
* @param \App\Models\User $user
* @return mixed * @return mixed
*/ */
public function viewAny(User $user) public function viewAny(User $user)
@ -24,8 +23,6 @@ class FormPolicy
/** /**
* Determine whether the user can view the model. * Determine whether the user can view the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Forms\Form $form
* @return mixed * @return mixed
*/ */
public function view(User $user, Form $form) public function view(User $user, Form $form)
@ -36,7 +33,6 @@ class FormPolicy
/** /**
* Determine whether the user can create models. * Determine whether the user can create models.
* *
* @param \App\Models\User $user
* @return mixed * @return mixed
*/ */
public function create(User $user) public function create(User $user)
@ -47,8 +43,6 @@ class FormPolicy
/** /**
* Determine whether the user can update the model. * Determine whether the user can update the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Forms\Form $form
* @return mixed * @return mixed
*/ */
public function update(User $user, Form $form) public function update(User $user, Form $form)
@ -59,8 +53,6 @@ class FormPolicy
/** /**
* Determine whether the user can delete the model. * Determine whether the user can delete the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Forms\Form $form
* @return mixed * @return mixed
*/ */
public function delete(User $user, Form $form) public function delete(User $user, Form $form)
@ -71,8 +63,6 @@ class FormPolicy
/** /**
* Determine whether the user can restore the model. * Determine whether the user can restore the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Forms\Form $form
* @return mixed * @return mixed
*/ */
public function restore(User $user, Form $form) public function restore(User $user, Form $form)
@ -83,8 +73,6 @@ class FormPolicy
/** /**
* Determine whether the user can permanently delete the model. * Determine whether the user can permanently delete the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Forms\Form $form
* @return mixed * @return mixed
*/ */
public function forceDelete(User $user, Form $form) public function forceDelete(User $user, Form $form)

View File

@ -13,7 +13,6 @@ class TemplatePolicy
/** /**
* Determine whether the user can create models. * Determine whether the user can create models.
* *
* @param \App\Models\User $user
* @return \Illuminate\Auth\Access\Response|bool * @return \Illuminate\Auth\Access\Response|bool
*/ */
public function create(User $user) public function create(User $user)
@ -24,8 +23,6 @@ class TemplatePolicy
/** /**
* Determine whether the user can update the model. * Determine whether the user can update the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Template $template
* @return mixed * @return mixed
*/ */
public function update(User $user, Template $template) public function update(User $user, Template $template)
@ -36,8 +33,6 @@ class TemplatePolicy
/** /**
* Determine whether the user can delete the model. * Determine whether the user can delete the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Template $template
* @return mixed * @return mixed
*/ */
public function delete(User $user, Template $template) public function delete(User $user, Template $template)

View File

@ -2,8 +2,8 @@
namespace App\Policies; namespace App\Policies;
use App\Models\Workspace;
use App\Models\User; use App\Models\User;
use App\Models\Workspace;
use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Auth\Access\HandlesAuthorization;
class WorkspacePolicy class WorkspacePolicy
@ -13,7 +13,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can view any models. * Determine whether the user can view any models.
* *
* @param \App\Models\User $user
* @return mixed * @return mixed
*/ */
public function viewAny(User $user) public function viewAny(User $user)
@ -24,8 +23,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can view the model. * Determine whether the user can view the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Workspace $workspace
* @return mixed * @return mixed
*/ */
public function view(User $user, Workspace $workspace) public function view(User $user, Workspace $workspace)
@ -36,7 +33,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can create models. * Determine whether the user can create models.
* *
* @param \App\Models\User $user
* @return mixed * @return mixed
*/ */
public function create(User $user) public function create(User $user)
@ -47,8 +43,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can update the model. * Determine whether the user can update the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Workspace $workspace
* @return mixed * @return mixed
*/ */
public function update(User $user, Workspace $workspace) public function update(User $user, Workspace $workspace)
@ -59,8 +53,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can delete the model. * Determine whether the user can delete the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Workspace $workspace
* @return mixed * @return mixed
*/ */
public function delete(User $user, Workspace $workspace) public function delete(User $user, Workspace $workspace)
@ -71,8 +63,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can restore the model. * Determine whether the user can restore the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Workspace $workspace
* @return mixed * @return mixed
*/ */
public function restore(User $user, Workspace $workspace) public function restore(User $user, Workspace $workspace)
@ -83,8 +73,6 @@ class WorkspacePolicy
/** /**
* Determine whether the user can permanently delete the model. * Determine whether the user can permanently delete the model.
* *
* @param \App\Models\User $user
* @param \App\Models\Workspace $workspace
* @return mixed * @return mixed
*/ */
public function forceDelete(User $user, Workspace $workspace) public function forceDelete(User $user, Workspace $workspace)

View File

@ -5,12 +5,13 @@ namespace App\Providers;
use App\Models\Billing\Subscription; use App\Models\Billing\Subscription;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Cashier\Cashier; use Laravel\Cashier\Cashier;
use Laravel\Dusk\DuskServiceProvider; use Laravel\Dusk\DuskServiceProvider;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/** /**

View File

@ -6,12 +6,10 @@ use App\Models\Forms\Form;
use App\Models\Integration\FormZapierWebhook; use App\Models\Integration\FormZapierWebhook;
use App\Models\Template; use App\Models\Template;
use App\Models\Workspace; use App\Models\Workspace;
use App\Models\User;
use App\Policies\FormPolicy; use App\Policies\FormPolicy;
use App\Policies\Integration\FormZapierWebhookPolicy; use App\Policies\Integration\FormZapierWebhookPolicy;
use App\Policies\TemplatePolicy; use App\Policies\TemplatePolicy;
use App\Policies\WorkspacePolicy; use App\Policies\WorkspacePolicy;
use App\Policies\UserPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider

View File

@ -5,11 +5,9 @@ namespace App\Providers;
use App\Events\Forms\FormSubmitted; use App\Events\Forms\FormSubmitted;
use App\Events\Models\FormCreated; use App\Events\Models\FormCreated;
use App\Listeners\FailedWebhookListener; use App\Listeners\FailedWebhookListener;
use App\Listeners\Auth\RegisteredListener;
use App\Listeners\Forms\FormCreationConfirmation; use App\Listeners\Forms\FormCreationConfirmation;
use App\Listeners\Forms\NotifyFormSubmission; use App\Listeners\Forms\NotifyFormSubmission;
use App\Listeners\Forms\SubmissionConfirmation; use App\Listeners\Forms\SubmissionConfirmation;
use App\Notifications\Forms\FormCreatedNotification;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -28,15 +26,15 @@ class EventServiceProvider extends ServiceProvider
SendEmailVerificationNotification::class, SendEmailVerificationNotification::class,
], ],
FormCreated::class => [ FormCreated::class => [
FormCreationConfirmation::class FormCreationConfirmation::class,
], ],
FormSubmitted::class => [ FormSubmitted::class => [
NotifyFormSubmission::class, NotifyFormSubmission::class,
SubmissionConfirmation::class, SubmissionConfirmation::class,
], ],
WebhookCallFailedEvent::class => [ WebhookCallFailedEvent::class => [
FailedWebhookListener::class FailedWebhookListener::class,
] ],
]; ];
/** /**

Some files were not shown because too many files have changed in this diff Show More