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,
@ -285,7 +284,7 @@ class GenerateTemplate extends Command
$types, $types,
$relatedTemplates $relatedTemplates
); );
$this->info('/form-templates/' . $template->slug); $this->info('/form-templates/'.$template->slug);
// Set reverse related Templates // Set reverse related Templates
$this->setReverseRelatedTemplates($template); $this->setReverseRelatedTemplates($template);
@ -298,34 +297,37 @@ class GenerateTemplate extends Command
*/ */
private function getImageCoverUrl($searchQuery): ?string private function getImageCoverUrl($searchQuery): ?string
{ {
$url = 'https://api.unsplash.com/search/photos?query=' . urlencode($searchQuery) . '&client_id=' . config('services.unsplash.access_key'); $url = 'https://api.unsplash.com/search/photos?query='.urlencode($searchQuery).'&client_id='.config('services.unsplash.access_key');
$response = Http::get($url)->json(); $response = Http::get($url)->json();
$photoIndex = rand(0, max(count($response['results']) - 1, 10)); $photoIndex = rand(0, max(count($response['results']) - 1, 10));
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,
]; ];
/** /**
@ -69,20 +69,22 @@ class GenerateTaxExport extends Command
$endDate = $this->option('end-date'); $endDate = $this->option('end-date');
// 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;
} else if (!$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');
} }
$this->info('Start date: ' . $startDate); $this->info('Start date: '.$startDate);
$this->info('End date: ' . $endDate); $this->info('End date: '.$endDate);
$processedInvoices = []; $processedInvoices = [];
@ -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;
} }
@ -132,14 +135,14 @@ class GenerateTaxExport extends Command
$aggregatedReport = $this->aggregateReport($processedInvoices); $aggregatedReport = $this->aggregateReport($processedInvoices);
$filePath = 'opnform-tax-export-per-invoice_' . $startDate . '_' . $endDate . '.xlsx'; $filePath = 'opnform-tax-export-per-invoice_'.$startDate.'_'.$endDate.'.xlsx';
$this->exportAsXlsx($processedInvoices, $filePath); $this->exportAsXlsx($processedInvoices, $filePath);
$aggregatedReportFilePath = 'opnform-tax-export-aggregated_' . $startDate . '_' . $endDate . '.xlsx'; $aggregatedReportFilePath = 'opnform-tax-export-aggregated_'.$startDate.'_'.$endDate.'.xlsx';
$this->exportAsXlsx($aggregatedReport, $aggregatedReportFilePath); $this->exportAsXlsx($aggregatedReport, $aggregatedReportFilePath);
// Display the results // Display the results
$this->info('Total invoices: ' . $totalInvoice . ' (with ' . $paymentNotSuccessfulCount . ' payment not successful or trial free invoice)'); $this->info('Total invoices: '.$totalInvoice.' (with '.$paymentNotSuccessfulCount.' payment not successful or trial free invoice)');
return Command::SUCCESS; return Command::SUCCESS;
} }
@ -151,7 +154,7 @@ class GenerateTaxExport extends Command
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
$country = $invoice['cust_country']; $country = $invoice['cust_country'];
$customerType = is_null($invoice['cust_vat_id']) && $this->isEuropeanCountry($country) ? 'individual' : 'business'; $customerType = is_null($invoice['cust_vat_id']) && $this->isEuropeanCountry($country) ? 'individual' : 'business';
if (!isset($aggregatedReport[$country])) { if (! isset($aggregatedReport[$country])) {
$defaultVal = [ $defaultVal = [
'count' => 0, 'count' => 0,
'total_usd' => 0, 'total_usd' => 0,
@ -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

@ -47,7 +47,7 @@ class Handler extends ExceptionHandler
public function report(Throwable $exception) public function report(Throwable $exception)
{ {
if ($this->shouldReport($exception) ) { if ($this->shouldReport($exception)) {
if (app()->bound('sentry') && $this->sentryShouldReport($exception)) { if (app()->bound('sentry') && $this->sentryShouldReport($exception)) {
app('sentry')->captureException($exception); app('sentry')->captureException($exception);
Log::debug('Un-handled Exception: '.$exception->getMessage(), [ Log::debug('Un-handled Exception: '.$exception->getMessage(), [
@ -64,7 +64,7 @@ class Handler extends ExceptionHandler
public function render($request, Throwable $e) public function render($request, Throwable $e)
{ {
if ($this->shouldReport($e) && !in_array(\App::environment(),['testing']) && config('logging.channels.slack.enabled')) { if ($this->shouldReport($e) && ! in_array(\App::environment(), ['testing']) && config('logging.channels.slack.enabled')) {
Log::channel('slack')->error($e); Log::channel('slack')->error($e);
} }
@ -78,6 +78,7 @@ class Handler extends ExceptionHandler
return false; return false;
} }
} }
return true; return true;
} }
} }

View File

@ -14,7 +14,7 @@ class WorkspaceAlreadyExisting extends Exception
public function getErrorMessage() public function getErrorMessage()
{ {
$owner = $this->workspace->users()->first(); $owner = $this->workspace->users()->first();
if (!$owner) { if (! $owner) {
return 'A user already connected that workspace to another NotionForms account. You or the current workspace return 'A user already connected that workspace to another NotionForms account. You or the current workspace
owner must have a NotionForms Enterprise subscription for you to add this Notion workspace. Please upgrade owner must have a NotionForms Enterprise subscription for you to add this Notion workspace. Please upgrade
with an Enterprise subscription, or contact us to get help.'; with an Enterprise subscription, or contact us to get help.';

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)
@ -15,7 +14,7 @@ class FormSubmissionExport implements FromArray, WithHeadingRow
$headingRow = []; $headingRow = [];
$contentRow = []; $contentRow = [];
foreach ($submissionData as $i => $row) { foreach ($submissionData as $i => $row) {
if($i==0){ if ($i == 0) {
$headingRow[] = $this->cleanColumnNames(array_keys($row)); $headingRow[] = $this->cleanColumnNames(array_keys($row));
} }
$contentRow[] = array_values($row); $contentRow[] = array_values($row);
@ -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);
@ -29,17 +28,17 @@ class ImpersonationController extends Controller
} }
} }
if (!$user) { if (! $user) {
return $this->error([ return $this->error([
'message'=> 'User not found.' 'message' => 'User not found.',
]); ]);
} else if ($user->admin) { } elseif ($user->admin) {
return $this->error([ return $this->error([
'message' => 'You cannot impersonate an admin.', 'message' => 'You cannot impersonate an admin.',
]); ]);
} }
\Log::warning('Impersonation started',[ \Log::warning('Impersonation started', [
'from_id' => auth()->id(), 'from_id' => auth()->id(),
'from_email' => auth()->user()->email, 'from_email' => auth()->user()->email,
'target_id' => $user->id, 'target_id' => $user->id,
@ -52,7 +51,7 @@ class ImpersonationController extends Controller
])->login($user); ])->login($user);
return $this->success([ return $this->success([
'token' => $token 'token' => $token,
]); ]);
} }
} }

View File

@ -17,14 +17,16 @@ class AppSumoAuthController extends Controller
public function handleCallback(Request $request) public function handleCallback(Request $request)
{ {
if (!$code = $request->code) { if (! $code = $request->code) {
return response()->json(['message' => 'Healthy'], 200); return response()->json(['message' => 'Healthy'], 200);
} }
$accessToken = $this->retrieveAccessToken($code); $accessToken = $this->retrieveAccessToken($code);
$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,
@ -50,13 +52,13 @@ class AppSumoAuthController extends Controller
private function fetchOrCreateLicense(string $accessToken): License private function fetchOrCreateLicense(string $accessToken): License
{ {
// Fetch license from API // Fetch license from API
$licenseKey = Http::get('https://appsumo.com/openid/license_key/?access_token=' . $accessToken) $licenseKey = Http::get('https://appsumo.com/openid/license_key/?access_token='.$accessToken)
->throw() ->throw()
->json('license_key'); ->json('license_key');
// Fetch or create license model // Fetch or create license model
$license = License::where('license_provider','appsumo')->where('license_key',$licenseKey)->first(); $license = License::where('license_provider', 'appsumo')->where('license_key', $licenseKey)->first();
if (!$license) { if (! $license) {
$licenseData = Http::withHeaders([ $licenseData = Http::withHeaders([
'X-AppSumo-Licensing-Key' => config('services.appsumo.api_key'), 'X-AppSumo-Licensing-Key' => config('services.appsumo.api_key'),
])->get('https://api.licensing.appsumo.com/v2/licenses/'.$licenseKey)->json(); ])->get('https://api.licensing.appsumo.com/v2/licenses/'.$licenseKey)->json();
@ -73,8 +75,9 @@ 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
@ -100,7 +102,7 @@ class AppSumoAuthController extends Controller
*/ */
public static function registerWithLicense(User $user, ?string $licenseHash): ?bool public static function registerWithLicense(User $user, ?string $licenseHash): ?bool
{ {
if (!$licenseHash) { if (! $licenseHash) {
return null; return null;
} }
$licenseId = decrypt($licenseHash); $licenseId = decrypt($licenseHash);
@ -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

@ -15,29 +15,30 @@ class CaddyController extends Controller
]); ]);
// make sure domain is valid // make sure domain is valid
$domain = $request->input('domain'); $domain = $request->input('domain');
if (!preg_match(CustomDomainRequest::CUSTOM_DOMAINS_REGEX, $domain)) { if (! preg_match(CustomDomainRequest::CUSTOM_DOMAINS_REGEX, $domain)) {
return $this->error([ return $this->error([
'success' => false, 'success' => false,
'message' => 'Invalid domain', 'message' => 'Invalid domain',
]); ]);
} }
\Log::info('Caddy request received',[ \Log::info('Caddy request received', [
'domain' => $domain, 'domain' => $domain,
]); ]);
if ($workspace = Workspace::whereJsonContains('custom_domains',$domain)->first()) { if ($workspace = Workspace::whereJsonContains('custom_domains', $domain)->first()) {
\Log::info('Caddy request successful',[ \Log::info('Caddy request successful', [
'domain' => $domain, 'domain' => $domain,
'workspace' => $workspace->id, 'workspace' => $workspace->id,
]); ]);
return $this->success([ return $this->success([
'success' => true, 'success' => true,
'message' => 'OK', 'message' => 'OK',
]); ]);
} }
\Log::info('Caddy request failed',[ \Log::info('Caddy request failed', [
'domain' => $domain, 'domain' => $domain,
'workspace' => $workspace?->id, 'workspace' => $workspace?->id,
]); ]);

View File

@ -3,19 +3,19 @@
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()
{ {
return \Cache::remember('changelog_entries', now()->addHour(), function () { return \Cache::remember('changelog_entries', now()->addHour(), function () {
$response = \Http:: post(self::CANNY_ENDPOINT.'entries/list',[ $response = \Http::post(self::CANNY_ENDPOINT.'entries/list', [
'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;
@ -36,7 +36,7 @@ class FormController extends Controller
$workspaceIsPro = $workspace->is_pro; $workspaceIsPro = $workspace->is_pro;
$forms = $workspace->forms() $forms = $workspace->forms()
->orderByDesc('updated_at') ->orderByDesc('updated_at')
->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){ ->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro) {
// Add attributes for faster loading // Add attributes for faster loading
$form->extra = (object) [ $form->extra = (object) [
@ -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()
@ -66,18 +68,20 @@ class FormController extends Controller
$this->authorize('viewAny', Form::class); $this->authorize('viewAny', Form::class);
$workspaceIsPro = $workspace->is_pro; $workspaceIsPro = $workspace->is_pro;
$newForms = $workspace->forms()->get()->map(function (Form $form) use ($workspace, $workspaceIsPro){ $newForms = $workspace->forms()->get()->map(function (Form $form) use ($workspace, $workspaceIsPro) {
// Add attributes for faster loading // Add attributes for faster loading
$form->extra = (object) [ $form->extra = (object) [
'loadedWorkspace' => $workspace, 'loadedWorkspace' => $workspace,
'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,13 +120,13 @@ 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);
return $this->success([ return $this->success([
'message' => $this->formCleaner->hasCleaned() ? 'Form successfully updated, but the Pro features you used will be disabled when sharing your form:' : 'Form updated.' . ($form->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 updated, but the Pro features you used will be disabled when sharing your form:' : 'Form updated.'.($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()),
]); ]);
} }
@ -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),
]); ]);
} }
@ -159,7 +164,7 @@ class FormController extends Controller
$form = Form::findOrFail($id); $form = Form::findOrFail($id);
$this->authorize('update', $form); $this->authorize('update', $form);
if ( $option == 'slug') { if ($option == 'slug') {
$form->generateSlug(); $form->generateSlug();
} elseif ($option == 'uuid') { } elseif ($option == 'uuid') {
$form->slug = Str::uuid(); $form->slug = Str::uuid();
@ -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,8 +187,8 @@ 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()]),
]); ]);
} }
@ -205,9 +210,9 @@ class FormController extends Controller
$this->authorize('view', $form); $this->authorize('view', $form);
$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

@ -19,20 +19,21 @@ 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;
@ -26,7 +25,7 @@ class FormSubmissionController extends Controller
public function submissions(string $id) public function submissions(string $id)
{ {
$form = Form::findOrFail((int)$id); $form = Form::findOrFail((int) $id);
$this->authorize('view', $form); $this->authorize('view', $form);
return FormSubmissionResource::collection($form->submissions()->paginate(100)); return FormSubmissionResource::collection($form->submissions()->paginate(100));
@ -40,15 +39,16 @@ 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,
]); ]);
} }
public function export(string $id) public function export(string $id)
{ {
$form = Form::findOrFail((int)$id); $form = Form::findOrFail((int) $id);
$this->authorize('view', $form); $this->authorize('view', $form);
$allRows = []; $allRows = [];
@ -61,24 +61,25 @@ 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',
\Maatwebsite\Excel\Excel::CSV \Maatwebsite\Excel\Excel::CSV
); );
} }
public function submissionFile($id, $fileName) public function submissionFile($id, $fileName)
{ {
$fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id) . '/' $fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id).'/'
. urldecode($fileName); .urldecode($fileName);
if (!Storage::exists($fileName)) { if (! Storage::exists($fileName)) {
return $this->error([ return $this->error([
'message' => 'File not found.', 'message' => 'File not found.',
], 404); ], 404);

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,21 +27,22 @@ 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()
); );
// Increase form view counter if not login // Increase form view counter if not login
if(!Auth::check()){ if (! Auth::check()) {
$form->views()->create(); $form->views()->create();
} }
@ -53,22 +54,23 @@ class PublicFormController extends Controller
{ {
// Check that form has user field // Check that form has user field
$form = $request->form; $form = $request->form;
if (!$form->has_user_field) { if (! $form->has_user_field) {
return []; return [];
} }
// Use serializer // Use serializer
$workspace = $form->workspace; $workspace = $form->workspace;
return (new WorkspaceHelper($workspace))->getAllUsers(); return (new WorkspaceHelper($workspace))->getAllUsers();
} }
public function showAsset($assetFileName) public function showAsset($assetFileName)
{ {
$path = FormController::ASSETS_UPLOAD_PATH.'/'.$assetFileName; $path = FormController::ASSETS_UPLOAD_PATH.'/'.$assetFileName;
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,
]); ]);
} }
@ -84,18 +86,18 @@ class PublicFormController extends Controller
$job = new StoreFormSubmissionJob($form, $request->validated()); $job = new StoreFormSubmissionJob($form, $request->validated());
$job->handle(); $job->handle();
$submissionId = Hashids::encode($job->getSubmissionId()); $submissionId = Hashids::encode($job->getSubmissionId());
}else{ } else {
StoreFormSubmissionJob::dispatch($form, $request->validated()); StoreFormSubmissionJob::dispatch($form, $request->validated());
} }
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,
])); ]));
} }
@ -104,7 +106,7 @@ class PublicFormController extends Controller
$submissionId = ($submissionId) ? Hashids::decode($submissionId) : false; $submissionId = ($submissionId) ? Hashids::decode($submissionId) : false;
$submissionId = isset($submissionId[0]) ? $submissionId[0] : false; $submissionId = isset($submissionId[0]) ? $submissionId[0] : false;
$form = Form::whereSlug($slug)->whereVisibility('public')->firstOrFail(); $form = Form::whereSlug($slug)->whereVisibility('public')->firstOrFail();
if ($form->workspace == null || !$form->editable_submissions || !$submissionId) { if ($form->workspace == null || ! $form->editable_submissions || ! $submissionId) {
return $this->error([ return $this->error([
'message' => 'Not allowed.', 'message' => 'Not allowed.',
]); ]);
@ -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,18 +52,18 @@ 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,
]); ]);
} }
public function updateStripeDetails(UpdateStripeDetailsRequest $request) public function updateStripeDetails(UpdateStripeDetailsRequest $request)
{ {
$user = Auth::user(); $user = Auth::user();
if (!$user->hasStripeId()) { if (! $user->hasStripeId()) {
$user->createAsStripeCustomer(); $user->createAsStripeCustomer();
} }
$user->updateStripeCustomer([ $user->updateStripeCustomer([
@ -78,13 +79,14 @@ class SubscriptionController extends Controller
public function billingPortal() public function billingPortal()
{ {
$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;
@ -13,8 +12,8 @@ class TemplateController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$limit = (int)$request->get('limit', 0); $limit = (int) $request->get('limit', 0);
$onlyMy = (bool)$request->get('onlymy', false); $onlyMy = (bool) $request->get('onlymy', false);
$templates = Template::when(Auth::check(), function ($query) use ($onlyMy) { $templates = Template::when(Auth::check(), function ($query) use ($onlyMy) {
if ($onlyMy) { if ($onlyMy) {
@ -26,7 +25,7 @@ class TemplateController extends Controller
}); });
} }
}) })
->when(!Auth::check(), function ($query) { ->when(! Auth::check(), function ($query) {
$query->where('publicly_listed', true); $query->where('publicly_listed', true);
}) })
->when($limit > 0, function ($query) use ($limit) { ->when($limit > 0, function ($query) use ($limit) {
@ -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)
@ -59,7 +57,7 @@ class StripeController extends WebhookController
if (isset($data['trial_end'])) { if (isset($data['trial_end'])) {
$trialEnd = Carbon::createFromTimestamp($data['trial_end']); $trialEnd = Carbon::createFromTimestamp($data['trial_end']);
if (!$subscription->trial_ends_at || $subscription->trial_ends_at->ne($trialEnd)) { if (! $subscription->trial_ends_at || $subscription->trial_ends_at->ne($trialEnd)) {
$subscription->trial_ends_at = $trialEnd; $subscription->trial_ends_at = $trialEnd;
} }
} }
@ -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

@ -22,7 +22,7 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $middleware = [ protected $middleware = [
// \App\Http\Middleware\TrustHosts::class, // \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class, \App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class, \Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
@ -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
@ -33,18 +33,19 @@ class AuthenticateJWT
} }
$error = null; $error = null;
if (!\Hash::check($request->ip(), $payload->get('ip'))) { if (! \Hash::check($request->ip(), $payload->get('ip'))) {
$error = 'Origin IP is invalid'; $error = 'Origin IP is invalid';
} }
if (!\Hash::check($request->userAgent(), $payload->get('ua'))) { if (! \Hash::check($request->userAgent(), $payload->get('ua'))) {
$error = 'Origin User Agent is invalid'; $error = 'Origin User Agent is invalid';
} }
if ($error) { if ($error) {
auth()->invalidate(); auth()->invalidate();
return response()->json([ return response()->json([
'message' => $error 'message' => $error,
], 403); ], 403);
} }
} }

View File

@ -12,14 +12,14 @@ class CaddyRequestMiddleware
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (!config('custom-domains.enabled')) { if (! config('custom-domains.enabled')) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Custom domains not enabled', 'message' => 'Custom domains not enabled',
], 401); ], 401);
} }
if (config('custom-domains.enabled') && !in_array($request->ip(), config('custom-domains.authorized_ips'))) { if (config('custom-domains.enabled') && ! in_array($request->ip(), config('custom-domains.authorized_ips'))) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Unauthorized IP', 'message' => 'Unauthorized IP',
@ -27,7 +27,7 @@ class CaddyRequestMiddleware
} }
$secret = $request->route('secret'); $secret = $request->route('secret');
if (config('custom-domains.caddy_secret') && (!$secret || $secret !== config('custom-domains.caddy_secret'))) { if (config('custom-domains.caddy_secret') && (! $secret || $secret !== config('custom-domains.caddy_secret'))) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Unauthorized', 'message' => 'Unauthorized',

View File

@ -6,24 +6,24 @@ 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.
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (!$request->hasHeader(self::CUSTOM_DOMAIN_HEADER) || !config('custom-domains.enabled')) { if (! $request->hasHeader(self::CUSTOM_DOMAIN_HEADER) || ! config('custom-domains.enabled')) {
return $next($request); return $next($request);
} }
$customDomain = $request->header(self::CUSTOM_DOMAIN_HEADER); $customDomain = $request->header(self::CUSTOM_DOMAIN_HEADER);
if (!preg_match(CustomDomainRequest::CUSTOM_DOMAINS_REGEX, $customDomain)) { if (! preg_match(CustomDomainRequest::CUSTOM_DOMAINS_REGEX, $customDomain)) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Invalid domain', 'message' => 'Invalid domain',
@ -38,7 +38,7 @@ class CustomDomainRestriction
} }
// Check if domain is known // Check if domain is known
if (!$workspaces = Workspace::whereJsonContains('custom_domains',$customDomain)->get()) { if (! $workspaces = Workspace::whereJsonContains('custom_domains', $customDomain)->get()) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Unknown domain', 'message' => 'Unknown domain',

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,27 +9,26 @@ 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
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (!$request->route('slug')) { if (! $request->route('slug')) {
return $next($request); return $next($request);
} }
$form = Form::where('slug',$request->route('slug'))->firstOrFail(); $form = Form::where('slug', $request->route('slug'))->firstOrFail();
$request->merge([ $request->merge([
'form' => $form, 'form' => $form,
]); ]);
$userIsFormOwner = Auth::check() && Auth::user()->ownsForm($form); $userIsFormOwner = Auth::check() && Auth::user()->ownsForm($form);
if (!$userIsFormOwner && $this->isProtected($request, $form)) { if (! $userIsFormOwner && $this->isProtected($request, $form)) {
return response([ return response([
'status' => 'Unauthorized', 'status' => 'Unauthorized',
'message' => 'Form is protected.', 'message' => 'Form is protected.',
@ -41,11 +40,11 @@ class ProtectedForm
public static function isProtected(Request $request, Form $form) public static function isProtected(Request $request, Form $form)
{ {
if (!$form->has_password) { if (! $form->has_password) {
return false; return false;
} }
return !self::hasCorrectPassword($request, $form); return ! self::hasCorrectPassword($request, $form);
} }
public static function hasCorrectPassword(Request $request, Form $form) public static function hasCorrectPassword(Request $request, Form $form)

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,14 +60,13 @@ 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
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
try { try {
if (!auth()->check() || !auth()->payload()->get('impersonating')) { if (! auth()->check() || ! auth()->payload()->get('impersonating')) {
return $next($request); return $next($request);
} }
} catch (JWTException $e) { } catch (JWTException $e) {
@ -75,22 +75,22 @@ class ImpersonationMiddleware
// Check that route is allowed // Check that route is allowed
$routeName = $request->route()->getName(); $routeName = $request->route()->getName();
if (!in_array($routeName, self::ALLOWED_ROUTES)) { if (! in_array($routeName, self::ALLOWED_ROUTES)) {
return response([ return response([
'message' => 'Unauthorized when impersonating', 'message' => 'Unauthorized when impersonating',
'route' => $routeName, 'route' => $routeName,
'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);
} else if (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', [
'route' => $routeName, 'route' => $routeName,
'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,13 +10,11 @@ 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)
{ {
if ($request->user() && !$request->user()->admin) { if ($request->user() && ! $request->user()->admin) {
// This user is not a paying customer... // This user is not a paying customer...
if ($request->expectsJson()) { if ($request->expectsJson()) {
return response([ return response([
@ -24,6 +22,7 @@ class IsAdmin
'type' => 'error', 'type' => 'error',
], 403); ], 403);
} }
return redirect('home'); return redirect('home');
} }

View File

@ -10,13 +10,11 @@ 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)
{ {
if ($request->user() && !$request->user()->moderator) { if ($request->user() && ! $request->user()->moderator) {
// This user is not a paying customer... // This user is not a paying customer...
if ($request->expectsJson()) { if ($request->expectsJson()) {
return response([ return response([
@ -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,13 +10,11 @@ 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)
{ {
if ($request->user() && !$request->user()->subscribed()) { if ($request->user() && ! $request->user()->subscribed()) {
// This user is not a paying customer... // This user is not a paying customer...
if ($request->expectsJson()) { if ($request->expectsJson()) {
return response([ return response([
@ -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)
@ -34,7 +34,7 @@ class AnswerFormRequest extends FormRequest
*/ */
public function authorize() public function authorize()
{ {
return !$this->form->is_closed && !$this->form->max_number_of_submissions_reached && $this->form->visibility === 'public'; return ! $this->form->is_closed && ! $this->form->max_number_of_submissions_reached && $this->form->visibility === 'public';
} }
/** /**
@ -56,23 +56,24 @@ class AnswerFormRequest extends FormRequest
$selectionFields = collect($this->form->properties)->filter(function ($pro) { $selectionFields = collect($this->form->properties)->filter(function ($pro) {
return in_array($pro['type'], ['select', 'multi_select']); return in_array($pro['type'], ['select', 'multi_select']);
}); });
foreach ($selectionFields as $field){ foreach ($selectionFields as $field) {
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';
if ($property['type'] == 'checkbox') { if ($property['type'] == 'checkbox') {
// Required for checkboxes means true // Required for checkboxes means true
$rules[] = 'accepted'; $rules[] = 'accepted';
} else if ($property['type'] == 'number' && isset($property['is_rating']) && $property['is_rating']) { } elseif ($property['type'] == 'number' && isset($property['is_rating']) && $property['is_rating']) {
// For star rating, needs a minimum of 1 star // For star rating, needs a minimum of 1 star
$rules[] = 'min:1'; $rules[] = 'min:1';
} }
@ -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;
} }
@ -127,21 +130,21 @@ 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 [];
} }
@ -199,18 +210,20 @@ class AnswerFormRequest extends FormRequest
{ {
if (isset($property['disable_past_dates']) && $property['disable_past_dates']) { if (isset($property['disable_past_dates']) && $property['disable_past_dates']) {
return ['date', 'after_or_equal:today']; return ['date', 'after_or_equal:today'];
}else if (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'];
} }
private function getSelectPropertyOptions($property): array private function getSelectPropertyOptions($property): array
{ {
$type = $property['type']; $type = $property['type'];
if (!isset($property[$type])) { if (! isset($property[$type])) {
return []; return [];
} }
return array_column($property[$type]['options'], 'name'); return array_column($property[$type]['options'], 'name');
} }
@ -223,25 +236,26 @@ class AnswerFormRequest extends FormRequest
$receivedValue = $receivedData[$property['id']] ?? null; $receivedValue = $receivedData[$property['id']] ?? null;
// Escape all '\' in select options // Escape all '\' in select options
if(in_array($property['type'], ['select', 'multi_select']) && !is_null($receivedValue)){ if (in_array($property['type'], ['select', 'multi_select']) && ! is_null($receivedValue)) {
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();
} }
} }
if($property['type'] === 'phone_number' && (!isset($property['use_simple_text_input']) || !$property['use_simple_text_input']) && $receivedValue && in_array($receivedValue, $countryCodeMapper)){ if ($property['type'] === 'phone_number' && (! isset($property['use_simple_text_input']) || ! $property['use_simple_text_input']) && $receivedValue && in_array($receivedValue, $countryCodeMapper)) {
$mergeData[$property['id']] = null; $mergeData[$property['id']] = null;
} }
}); });

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',
@ -49,9 +49,10 @@ class FormTemplateRequest extends FormRequest
public function rules() public function rules()
{ {
$slugRule = ''; $slugRule = '';
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
{ {
@ -30,11 +27,11 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
'title' => 'required|string|max:60', 'title' => 'required|string|max:60',
'description' => 'nullable|string|max:2000', 'description' => 'nullable|string|max:2000',
'tags' => 'nullable|array', 'tags' => 'nullable|array',
'visibility' => ['required',Rule::in(Form::VISIBILITY)], 'visibility' => ['required', Rule::in(Form::VISIBILITY)],
// 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',
@ -47,11 +44,11 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
'notification_settings' => 'nullable', 'notification_settings' => 'nullable',
// Customization // Customization
'theme' => ['required',Rule::in(Form::THEMES)], 'theme' => ['required', Rule::in(Form::THEMES)],
'width' => ['required',Rule::in(Form::WIDTHS)], 'width' => ['required', Rule::in(Form::WIDTHS)],
'cover_picture' => 'url|nullable', 'cover_picture' => 'url|nullable',
'logo_picture' => 'url|nullable', 'logo_picture' => 'url|nullable',
'dark_mode' => ['required',Rule::in(Form::DARK_MODE_VALUES)], 'dark_mode' => ['required', Rule::in(Form::DARK_MODE_VALUES)],
'color' => 'required|string', 'color' => 'required|string',
'hide_title' => 'required|boolean', 'hide_title' => 'required|boolean',
'uppercase_labels' => 'required|boolean', 'uppercase_labels' => 'required|boolean',
@ -75,7 +72,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
'editable_submissions' => 'boolean|nullable', 'editable_submissions' => 'boolean|nullable',
'editable_submissions_button_text' => 'string|min:1|max:50', 'editable_submissions_button_text' => 'string|min:1|max:50',
'confetti_on_submission' => 'boolean', 'confetti_on_submission' => 'boolean',
'auto_save'=> 'boolean', 'auto_save' => 'boolean',
// Properties // Properties
'properties' => 'required|array', 'properties' => 'required|array',
@ -90,7 +87,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
'properties.*.required' => 'boolean|nullable', 'properties.*.required' => 'boolean|nullable',
'properties.*.multiple' => 'boolean|nullable', 'properties.*.multiple' => 'boolean|nullable',
'properties.*.timezone' => 'sometimes|nullable', 'properties.*.timezone' => 'sometimes|nullable',
'properties.*.width' => ['sometimes', Rule::in(['full','1/2','1/3','2/3','1/3','3/4','1/4'])], 'properties.*.width' => ['sometimes', Rule::in(['full', '1/2', '1/3', '2/3', '1/3', '3/4', '1/4'])],
'properties.*.align' => ['sometimes', Rule::in(['left', 'center', 'right', 'justify'])], 'properties.*.align' => ['sometimes', Rule::in(['left', 'center', 'right', 'justify'])],
'properties.*.allowed_file_types' => 'sometimes|nullable', 'properties.*.allowed_file_types' => 'sometimes|nullable',
'properties.*.use_toggle_switch' => 'boolean|nullable', 'properties.*.use_toggle_switch' => 'boolean|nullable',
@ -127,7 +124,7 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
// Custom SEO // Custom SEO
'seo_meta' => 'nullable|array', 'seo_meta' => 'nullable|array',
'custom_domain' => 'sometimes|nullable|regex:'. CustomDomainRequest::CUSTOM_DOMAINS_REGEX, 'custom_domain' => 'sometimes|nullable|regex:'.CustomDomainRequest::CUSTOM_DOMAINS_REGEX,
]; ];
} }

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);
@ -28,13 +30,13 @@ class CustomDomainRequest extends FormRequest
'custom_domains' => [ 'custom_domains' => [
'present', 'present',
'array', 'array',
function($attribute, $value, $fail) { function ($attribute, $value, $fail) {
$errors = []; $errors = [];
$domains = collect($value)->filter(function ($domain) { $domains = collect($value)->filter(function ($domain) {
return !empty( trim($domain) ); return ! empty(trim($domain));
})->each(function($domain) use (&$errors) { })->each(function ($domain) use (&$errors) {
if (!preg_match(self::CUSTOM_DOMAINS_REGEX, $domain)) { if (! preg_match(self::CUSTOM_DOMAINS_REGEX, $domain)) {
$errors[] = 'Invalid domain: ' . $domain; $errors[] = 'Invalid domain: '.$domain;
} }
}); });
@ -44,16 +46,17 @@ class CustomDomainRequest extends FormRequest
$limit = $this->workspace->custom_domain_count_limit; $limit = $this->workspace->custom_domain_count_limit;
if ($limit && $domains->count() > $limit) { if ($limit && $domains->count() > $limit) {
$fail('You can only add ' . $limit . ' domain(s).'); $fail('You can only add '.$limit.' domain(s).');
} }
$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
{ {
@ -20,7 +18,7 @@ class FormResource extends JsonResource
*/ */
public function toArray($request) public function toArray($request)
{ {
if(!$this->userIsFormOwner() && ProtectedForm::isProtected($request, $this->resource)){ if (! $this->userIsFormOwner() && ProtectedForm::isProtected($request, $this->resource)) {
return $this->getProtectedForm(); return $this->getProtectedForm();
} }
@ -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()
@ -45,7 +45,7 @@ class FormSubmissionResource extends JsonResource
return in_array($field['type'], ['files', 'signature']); return in_array($field['type'], ['files', 'signature']);
}); });
foreach ($fileFields as $field) { foreach ($fileFields as $field) {
if (isset($data[$field['id']]) && !empty($data[$field['id']])) { if (isset($data[$field['id']]) && ! empty($data[$field['id']])) {
$data[$field['id']] = collect($data[$field['id']])->filter(function ($file) { $data[$field['id']] = collect($data[$field['id']])->filter(function ($file) {
return $file !== null && $file; return $file !== null && $file;
})->map(function ($file) { })->map(function ($file) {

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;
} }
@ -82,12 +85,13 @@ class StoreFormSubmissionJob implements ShouldQueue
*/ */
private function submissionToUpdate(): ?FormSubmission private function submissionToUpdate(): ?FormSubmission
{ {
if($this->submissionId){ if ($this->submissionId) {
return $this->form->submissions()->findOrFail($this->submissionId); return $this->form->submissions()->findOrFail($this->submissionId);
} }
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);
} }
@ -111,7 +115,7 @@ class StoreFormSubmissionJob implements ShouldQueue
// Do required transformation per type (e.g. file uploads) // Do required transformation per type (e.g. file uploads)
foreach ($data as $answerKey => $answerValue) { foreach ($data as $answerKey => $answerValue) {
$field = $properties->where('id', $answerKey)->first(); $field = $properties->where('id', $answerKey)->first();
if (!$field) { if (! $field) {
continue; continue;
} }
@ -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;
} }
@ -139,12 +143,12 @@ class StoreFormSubmissionJob implements ShouldQueue
} }
// For Singrature // For Singrature
if($this->form->is_pro && $field['type'] == 'signature') { if ($this->form->is_pro && $field['type'] == 'signature') {
$finalData[$field['id']] = $this->storeSignature($answerValue); $finalData[$field['id']] = $this->storeSignature($answerValue);
} }
// For Phone // For Phone
if($field['type'] == 'phone_number' && $answerValue && ctype_alpha(substr($answerValue, 0, 2)) && (!isset($field['use_simple_text_input']) || !$field['use_simple_text_input'])) { if ($field['type'] == 'phone_number' && $answerValue && ctype_alpha(substr($answerValue, 0, 2)) && (! isset($field['use_simple_text_input']) || ! $field['use_simple_text_input'])) {
$finalData[$field['id']] = substr($answerValue, 2); $finalData[$field['id']] = substr($answerValue, 2);
} }
} }
@ -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);
} }
@ -173,15 +178,16 @@ class StoreFormSubmissionJob implements ShouldQueue
return null; return null;
} }
if(filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3 if (filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3
$fileName = basename($value); $fileName = basename($value);
$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;
} }
if($this->isSkipForUpload($value)) { if ($this->isSkipForUpload($value)) {
return $value; return $value;
} }
@ -189,7 +195,7 @@ class StoreFormSubmissionJob implements ShouldQueue
// 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;
} }
@ -209,7 +215,7 @@ class StoreFormSubmissionJob implements ShouldQueue
private function storeSignature(?string $value) private function storeSignature(?string $value)
{ {
if ($value == null || !isset(explode(',', $value)[1])) { if ($value == null || ! isset(explode(',', $value)[1])) {
return null; return null;
} }
@ -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
@ -235,9 +240,9 @@ class StoreFormSubmissionJob implements ShouldQueue
return isset($property['hidden']) return isset($property['hidden'])
&& isset($property['prefill']) && isset($property['prefill'])
&& FormLogicPropertyResolver::isHidden($property, $this->submissionData) && FormLogicPropertyResolver::isHidden($property, $this->submissionData)
&& !is_null($property['prefill']) && ! is_null($property['prefill'])
&& !in_array($property['type'], ['files']) && ! in_array($property['type'], ['files'])
&& !($property['type'] == 'url' && isset($property['file_upload']) && $property['file_upload']); && ! ($property['type'] == 'url' && isset($property['file_upload']) && $property['file_upload']);
})->each(function (array $property) use (&$formData) { })->each(function (array $property) use (&$formData) {
if ($property['type'] === 'date' && isset($property['prefill_today']) && $property['prefill_today']) { if ($property['type'] === 'date' && isset($property['prefill_today']) && $property['prefill_today']) {
$formData[$property['id']] = now()->format((isset($property['with_time']) && $property['with_time']) ? 'Y-m-d H:i' : 'Y-m-d'); $formData[$property['id']] = now()->format((isset($property['with_time']) && $property['with_time']) ? 'Y-m-d H:i' : 'Y-m-d');

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.
@ -29,17 +28,19 @@ class SubmissionConfirmation implements ShouldQueue
public function handle(FormSubmitted $event) public function handle(FormSubmitted $event)
{ {
if ( if (
!$event->form->is_pro || ! $event->form->is_pro ||
!$event->form->send_submission_confirmation || ! $event->form->send_submission_confirmation ||
$this->riskLimitReached($event) // To avoid phishing abuse we limit this feature for risky users $this->riskLimitReached($event) // To avoid phishing abuse we limit this feature for risky users
) { ) {
return; return;
} }
$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,
'form_id' => $event->form->id, 'form_id' => $event->form->id,
'form_slug' => $event->form->slug, 'form_slug' => $event->form->slug,
@ -50,15 +51,20 @@ class SubmissionConfirmation implements ShouldQueue
private function getRespondentEmail(FormSubmitted $event) private function getRespondentEmail(FormSubmitted $event)
{ {
// 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.
@ -31,7 +31,7 @@ class FormCreationConfirmationMail extends OpenFormMail implements ShouldQueue
public function build() public function build()
{ {
return $this return $this
->markdown('mail.form.created',[ ->markdown('mail.form.created', [
'form' => $this->form, 'form' => $this->form,
])->subject('Your form was created!'); ])->subject('Your form was created!');
} }

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.
@ -42,23 +44,25 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue
->replyTo($this->getReplyToEmail($form->creator->email)) ->replyTo($this->getReplyToEmail($form->creator->email))
->from($this->getFromEmail(), $form->notification_sender) ->from($this->getFromEmail(), $form->notification_sender)
->subject($form->notification_subject) ->subject($form->notification_subject)
->markdown('mail.form.confirmation-submission-notification',[ ->markdown('mail.form.confirmation-submission-notification', [
'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',
]; ];
/** /**
@ -155,14 +160,15 @@ class Form extends Model implements CachableAttributes
public function getShareUrlAttribute() public function getShareUrlAttribute()
{ {
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);
} }
public function getEditUrlAttribute() public function getEditUrlAttribute()
{ {
return front_url('/forms/' . $this->slug . '/show'); return front_url('/forms/'.$this->slug.'/show');
} }
public function getSubmissionsCountAttribute() public function getSubmissionsCountAttribute()
@ -174,9 +180,10 @@ class Form extends Model implements CachableAttributes
{ {
return $this->remember('views_count', 15 * 60, function (): int { return $this->remember('views_count', 15 * 60, function (): int {
if (env('DB_CONNECTION') == 'mysql') { if (env('DB_CONNECTION') == 'mysql') {
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)
@ -232,12 +240,12 @@ class Form extends Model implements CachableAttributes
public function getHasPasswordAttribute() public function getHasPasswordAttribute()
{ {
return !empty($this->password); return ! empty($this->password);
} }
public function getMaxFileSizeAttribute() public function getMaxFileSizeAttribute()
{ {
return $this->remember('max_file_size', 15 * 60, function(): int { return $this->remember('max_file_size', 15 * 60, function (): int {
return $this->workspace->max_file_size; return $this->workspace->max_file_size;
}); });
} }
@ -292,7 +300,7 @@ class Form extends Model implements CachableAttributes
return SlugOptions::create() return SlugOptions::create()
->doNotGenerateSlugsOnUpdate() ->doNotGenerateSlugsOnUpdate()
->generateSlugsFrom(function (Form $form) { ->generateSlugsFrom(function (Form $form) {
return $form->title . ' ' . Str::random(6); return $form->title.' '.Str::random(6);
}) })
->saveSlugsTo('slug'); ->saveSlugsTo('slug');
} }
@ -302,19 +310,18 @@ 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);
} }
public function getNotifiesDiscordAttribute() public function getNotifiesDiscordAttribute()
{ {
return !empty($this->discord_webhook_url); return ! empty($this->discord_webhook_url);
} }
public function getNotifiesSlackAttribute() public function getNotifiesSlackAttribute()
{ {
return !empty($this->slack_webhook_url); return ! empty($this->slack_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',
]; ];
/** /**
@ -59,12 +60,12 @@ class User extends Authenticatable implements JWTSubject
public function ownsForm(Form $form) public function ownsForm(Form $form)
{ {
return $this->workspaces()->where('workspaces.id',$form->workspace_id)->exists(); return $this->workspaces()->where('workspaces.id', $form->workspace_id)->exists();
} }
public function ownsWorkspace(Workspace $workspace) public function ownsWorkspace(Workspace $workspace)
{ {
return $this->workspaces()->where('workspaces.id',$workspace->id)->exists(); return $this->workspaces()->where('workspaces.id', $workspace->id)->exists();
} }
/** /**
@ -89,12 +90,12 @@ class User extends Authenticatable implements JWTSubject
{ {
return $this->subscribed() return $this->subscribed()
|| in_array($this->email, config('opnform.extra_pro_users_emails')) || in_array($this->email, config('opnform.extra_pro_users_emails'))
|| !is_null($this->activeLicense()); || ! is_null($this->activeLicense());
} }
public function getHasCustomerIdAttribute() public function getHasCustomerIdAttribute()
{ {
return !is_null($this->stripe_id); return ! is_null($this->stripe_id);
} }
public function getAdminAttribute() public function getAdminAttribute()
@ -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,16 +41,16 @@ 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()
{ {
if(is_null(config('cashier.key'))){ if (is_null(config('cashier.key'))) {
return self::MAX_FILE_SIZE_PRO; return self::MAX_FILE_SIZE_PRO;
} }
return $this->remember('max_file_size', 15 * 60, function(): int { return $this->remember('max_file_size', 15 * 60, function (): int {
// Return max file size depending on subscription // Return max file size depending on subscription
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if ($owner->is_subscribed) { if ($owner->is_subscribed) {
@ -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;
} }
@ -64,17 +69,18 @@ class Workspace extends Model implements CachableAttributes
public function getCustomDomainCountLimitAttribute() public function getCustomDomainCountLimitAttribute()
{ {
if(is_null(config('cashier.key'))){ if (is_null(config('cashier.key'))) {
return null; return null;
} }
return $this->remember('custom_domain_count', 15 * 60, function(): ?int { return $this->remember('custom_domain_count', 15 * 60, function (): ?int {
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if ($owner->is_subscribed) { if ($owner->is_subscribed) {
if ($license = $owner->activeLicense()) { if ($license = $owner->activeLicense()) {
// 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;
} }
} }
@ -85,44 +91,46 @@ class Workspace extends Model implements CachableAttributes
public function getIsProAttribute() public function getIsProAttribute()
{ {
if(is_null(config('cashier.key'))){ if (is_null(config('cashier.key'))) {
return true; // If no paid plan so TRUE for ALL return true; // If no paid plan so TRUE for ALL
} }
return $this->remember('is_pro', 15 * 60, function(): bool { return $this->remember('is_pro', 15 * 60, function (): bool {
// Make sure at least one owner is pro // Make sure at least one owner is pro
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if ($owner->is_subscribed) { if ($owner->is_subscribed) {
return true; return true;
} }
} }
return false; return false;
}); });
} }
public function getIsEnterpriseAttribute() public function getIsEnterpriseAttribute()
{ {
if(is_null(config('cashier.key'))){ if (is_null(config('cashier.key'))) {
return true; // If no paid plan so TRUE for ALL return true; // If no paid plan so TRUE for ALL
} }
return $this->remember('is_enterprise', 15 * 60, function(): bool { return $this->remember('is_enterprise', 15 * 60, function (): bool {
// Make sure at least one owner is pro // Make sure at least one owner is pro
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if ($owner->has_enterprise_subscription) { if ($owner->has_enterprise_subscription) {
return true; return true;
} }
} }
return false; return false;
}); });
} }
public function getIsRiskyAttribute() public function getIsRiskyAttribute()
{ {
return $this->remember('is_risky', 15 * 60, function(): bool { return $this->remember('is_risky', 15 * 60, function (): bool {
// A workspace is risky if all of his users are risky // A workspace is risky if all of his users are risky
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if (!$owner->is_risky) { if (! $owner->is_risky) {
return false; return false;
} }
} }
@ -133,7 +141,7 @@ class Workspace extends Model implements CachableAttributes
public function getSubmissionsCountAttribute() public function getSubmissionsCountAttribute()
{ {
return $this->remember('submissions_count', 15 * 60, function(): int { return $this->remember('submissions_count', 15 * 60, function (): int {
$total = 0; $total = 0;
foreach ($this->forms as $form) { foreach ($this->forms as $form) {
$total += $form->submissions_count; $total += $form->submissions_count;
@ -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,8 +46,8 @@ 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,
'error' => $this->event->errorMessage, 'error' => $this->event->errorMessage,

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,15 +65,17 @@ 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();
} }
private function getReplyToEmail($default) private function getReplyToEmail($default)
{ {
$replyTo = Arr::get((array)$this->event->form->notification_settings, 'notification_reply_to', null); $replyTo = Arr::get((array) $this->event->form->notification_settings, 'notification_reply_to', null);
if ($replyTo && $this->validateEmail($replyTo)) { if ($replyTo && $this->validateEmail($replyTo)) {
return $replyTo; return $replyTo;
} }
return $this->getRespondentEmail() ?? $default; return $this->getRespondentEmail() ?? $default;
} }
@ -83,7 +85,7 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
$emailFields = collect($this->event->form->properties)->filter(function ($field) { $emailFields = collect($this->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) { if ($emailFields->count() != 1) {
return null; return null;
@ -101,7 +103,6 @@ class FormSubmissionNotification extends Notification implements ShouldQueue
public static function validateEmail($email): bool public static function validateEmail($email): bool
{ {
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,20 +53,16 @@ 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)
{ {
return !$workspace->owners->where('id',$user->id)->isEmpty() && $user->workspaces()->count() > 1; return ! $workspace->owners->where('id', $user->id)->isEmpty() && $user->workspaces()->count() > 1;
} }
/** /**
* 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
{ {
/** /**
@ -20,7 +21,7 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
if(config('filesystems.default') === 'local'){ if (config('filesystems.default') === 'local') {
Storage::disk('local')->buildTemporaryUrlsUsing(function ($path, $expiration, $options) { Storage::disk('local')->buildTemporaryUrlsUsing(function ($path, $expiration, $options) {
return URL::temporarySignedRoute( return URL::temporarySignedRoute(
'local.temp', 'local.temp',

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