Separated laravel app to its own folder (#540)

This commit is contained in:
Julien Nahum
2024-08-26 18:24:56 +02:00
committed by GitHub
parent 39b8df5eed
commit 5bd1dda504
546 changed files with 124 additions and 143 deletions

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Service;
use Illuminate\Support\Facades\App;
use Stripe\SubscriptionItem;
class BillingHelper
{
public static function getPricing($productName = 'default')
{
return App::environment() == 'production' ?
config('pricing.production.' . $productName . '.pricing') :
config('pricing.test.' . $productName . '.pricing');
}
public static function getProductId($productName = 'default')
{
return App::environment() == 'production' ?
config('pricing.production.' . $productName . '.product_id') :
config('pricing.test.' . $productName . '.product_id');
}
public static function getLineItemInterval(SubscriptionItem $item)
{
return $item->price->recurring->interval === 'year' ? 'yearly' : 'monthly';
;
}
}

View File

@@ -0,0 +1,289 @@
<?php
namespace App\Service\Forms;
use App\Http\Requests\UserFormRequest;
use App\Http\Resources\FormResource;
use App\Models\Forms\Form;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Stevebauman\Purify\Facades\Purify;
use function collect;
class FormCleaner
{
/**
* All the performed cleanings
*
* @var bool
*/
private array $cleanings = [];
private array $data;
// For remove keys those have empty value
private array $customKeys = ['seo_meta'];
private array $formDefaults = [
'no_branding' => false,
'database_fields_update' => null,
'editable_submissions' => false,
'custom_code' => null,
'seo_meta' => [],
'redirect_url' => null
];
private array $formNonTrialingDefaults = [
// Custom code protection disabled for now
// 'custom_code' => null,
];
private array $fieldDefaults = [
// 'name' => '' TODO: prevent name changing, use alias for column and keep original name as it is
'file_upload' => false,
];
private array $cleaningMessages = [
// For form
'no_branding' => 'OpenForm branding is not hidden.',
'database_fields_update' => 'Form submission will only create new records (no updates).',
'editable_submissions' => 'Users will not be able to edit their submissions.',
'custom_code' => 'Custom code was disabled',
'seo_meta' => 'Custom SEO was disabled',
'redirect_url' => 'Redirect Url was disabled',
// For fields
'file_upload' => 'Link field is not a file upload.',
'custom_block' => 'The custom block was removed.',
];
/**
* Returns form data after request ingestion
*/
public function getData(): array
{
return $this->data;
}
/**
* Returns true if at least one cleaning was done
*/
public function hasCleaned(): bool
{
return count($this->cleanings) > 0;
}
/**
* Returns the messages for each cleaning step performed
*/
public function getPerformedCleanings(): array
{
$cleaningMsgs = [];
foreach ($this->cleanings as $key => $val) {
$cleaningMsgs[$key] = collect($val)->map(function ($cleaning) {
return $this->cleaningMessages[$cleaning];
});
}
return $cleaningMsgs;
}
/**
* Removes form pro features from data if user isn't pro
*/
public function processRequest(UserFormRequest $request): FormCleaner
{
$data = $request->validated();
$this->data = $this->commonCleaning($data);
return $this;
}
/**
* Create form cleaner instance from existing form
*/
public function processForm(Request $request, Form $form): FormCleaner
{
$data = (new FormResource($form))->toArray($request);
$this->data = $this->commonCleaning($data);
return $this;
}
private function isPro(Workspace $workspace)
{
return $workspace->is_pro;
}
private function isTrialing(Workspace $workspace)
{
return $workspace->is_trialing;
}
/**
* Dry run celanings
*
* @param User|null $user
*/
public function simulateCleaning(Workspace $workspace): FormCleaner
{
if ($this->isTrialing($workspace)) {
$this->data = $this->removeNonTrialingFeatures($this->data, true);
}
if (!$this->isPro($workspace)) {
$this->data = $this->removeProFeatures($this->data, true);
}
return $this;
}
/**
* Perform Cleanigns
*
* @param User|null $user
* @return $this|array
*/
public function performCleaning(Workspace $workspace): FormCleaner
{
if ($this->isTrialing($workspace)) {
$this->data = $this->removeNonTrialingFeatures($this->data, true);
}
if (!$this->isPro($workspace)) {
$this->data = $this->removeProFeatures($this->data);
}
return $this;
}
/**
* Clean all forms:
* - Escape html of custom text block
*/
private function commonCleaning(array $data)
{
foreach ($data['properties'] as &$property) {
if ($property['type'] == 'nf-text' && isset($property['content'])) {
$property['content'] = Purify::clean($property['content']);
}
}
return $data;
}
private function removeNonTrialingFeatures(array $data, $simulation = false)
{
$this->clean($data, $this->formNonTrialingDefaults);
return $data;
}
private function removeProFeatures(array $data, $simulation = false)
{
$this->cleanForm($data, $simulation);
$this->cleanProperties($data, $simulation);
return $data;
}
private function cleanForm(array &$data, $simulation = false): void
{
$this->clean($data, $this->formDefaults, $simulation);
}
private function cleanProperties(array &$data, $simulation = false): void
{
foreach ($data['properties'] as $key => &$property) {
/*
// Remove pro custom blocks
if (\Str::of($property['type'])->startsWith('nf-')) {
$this->cleanings[$property['name']][] = 'custom_block';
if (!$simulation) {
unset($data['properties'][$key]);
}
continue;
}
// Remove logic
if (($property['logic']['conditions'] ?? null) != null || ($property['logic']['actions'] ?? []) != []) {
$this->cleanings[$property['name']][] = 'logic';
if (!$simulation) {
unset($data['properties'][$key]['logic']);
}
}
*/
// Clean pro field options
$this->cleanField($property, $this->fieldDefaults, $simulation);
}
}
private function clean(array &$data, array $defaults, $simulation = false): void
{
foreach ($defaults as $key => $value) {
// Get value from form
$formVal = Arr::get($data, $key);
// Transform customkeys values
$formVal = $this->cleanCustomKeys($key, $formVal);
// Transform boolean values
$formVal = (($formVal === 0 || $formVal === '0') ? false : $formVal);
$formVal = (($formVal === 1 || $formVal === '1') ? true : $formVal);
if (!is_null($formVal) && $formVal !== $value) {
if (!isset($this->cleanings['form'])) {
$this->cleanings['form'] = [];
}
$this->cleanings['form'][] = $key;
// If not a simulation, do the cleaning
if (!$simulation) {
Arr::set($data, $key, $value);
}
}
}
}
private function cleanField(array &$data, array $defaults, $simulation = false): void
{
foreach ($defaults as $key => $value) {
if (isset($data[$key]) && Arr::get($data, $key) !== $value) {
$this->cleanings[$data['name']][] = $key;
if (!$simulation) {
Arr::set($data, $key, $value);
}
}
}
// Remove pro types columns
/*foreach (['files'] as $proType) {
if ($data['type'] == $proType && (!isset($data['hidden']) || !$data['hidden'])) {
$this->cleanings[$data['name']][] = $proType;
if (!$simulation) {
$data['hidden'] = true;
}
}
}*/
}
// Remove keys those have empty value
private function cleanCustomKeys($key, $formVal)
{
if (in_array($key, $this->customKeys) && $formVal !== null) {
$newVal = [];
foreach ($formVal as $k => $val) {
if ($val) {
$newVal[$k] = $val;
}
}
return $newVal;
}
return $formVal;
}
}

View File

@@ -0,0 +1,453 @@
<?php
namespace App\Service\Forms;
class FormLogicConditionChecker
{
public function __construct(private ?array $conditions, private ?array $formData)
{
}
public static function conditionsMet(?array $conditions, array $formData): bool
{
return (new self($conditions, $formData))->conditionsAreMet($conditions, $formData);
}
private function conditionsAreMet(?array $conditions, array $formData): bool
{
if (!$conditions) {
return false;
}
// If it's not a group, just a single condition
if (!isset($conditions['operatorIdentifier'])) {
return $this->propertyConditionMet($conditions['value'], $formData[$conditions['value']['property_meta']['id']] ?? null);
}
if ($conditions['operatorIdentifier'] === 'and') {
$isvalid = true;
foreach ($conditions['children'] as $childrenCondition) {
if (!$this->conditionsMet($childrenCondition, $formData)) {
$isvalid = false;
break;
}
}
return $isvalid;
} elseif ($conditions['operatorIdentifier'] === 'or') {
$isvalid = false;
foreach ($conditions['children'] as $childrenCondition) {
if ($this->conditionsMet($childrenCondition, $formData)) {
$isvalid = true;
break;
}
}
return $isvalid;
}
throw new \Exception('Unexcepted operatorIdentifier:' . $conditions['operatorIdentifier']);
}
private function propertyConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['property_meta']['type']) {
case 'text':
case 'url':
case 'email':
case 'phone_number':
return $this->textConditionMet($propertyCondition, $value);
case 'number':
case 'rating':
case 'scale':
case 'slider':
return $this->numberConditionMet($propertyCondition, $value);
case 'checkbox':
return $this->checkboxConditionMet($propertyCondition, $value);
case 'select':
return $this->selectConditionMet($propertyCondition, $value);
case 'date':
return $this->dateConditionMet($propertyCondition, $value);
case 'multi_select':
return $this->multiSelectConditionMet($propertyCondition, $value);
case 'files':
return $this->filesConditionMet($propertyCondition, $value);
case 'matrix':
return $this->matrixConditionMet($propertyCondition, $value);
}
return false;
}
private function checkEquals($condition, $fieldValue): bool
{
return $condition['value'] === $fieldValue;
}
private function checkContains($condition, $fieldValue): bool
{
if (is_array($fieldValue)) {
return in_array($condition['value'], $fieldValue);
}
return \Str::contains($fieldValue, $condition['value']);
}
private function checkMatrixContains($condition, $fieldValue): bool
{
foreach($condition['value'] as $key => $value) {
if(!(array_key_exists($key, $condition['value']) && array_key_exists($key, $fieldValue))) {
return false;
}
if($condition['value'][$key] == $fieldValue[$key]) {
return true;
}
}
return false;
}
private function checkMatrixEquals($condition, $fieldValue): bool
{
foreach($condition['value'] as $key => $value) {
if($condition['value'][$key] !== $fieldValue[$key]) {
return false;
}
}
return true;
}
private function checkListContains($condition, $fieldValue): bool
{
if (is_null($fieldValue)) {
return false;
}
if (!is_array($fieldValue)) {
return $this->checkEquals($condition, $fieldValue);
}
if (is_array($condition['value'])) {
return count(array_intersect($condition['value'], $fieldValue)) === count($condition['value']);
} else {
return in_array($condition['value'], $fieldValue);
}
}
private function checkStartsWith($condition, $fieldValue): bool
{
return str_starts_with($fieldValue, $condition['value']);
}
private function checkEndsWith($condition, $fieldValue): bool
{
return str_ends_with($fieldValue, $condition['value']);
}
private function checkIsEmpty($condition, $fieldValue): bool
{
if (is_array($fieldValue)) {
return count($fieldValue) === 0;
}
return $fieldValue == '' || $fieldValue == null || !$fieldValue;
}
private function checkGreaterThan($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && (float) $fieldValue > (float) $condition['value'];
}
private function checkGreaterThanEqual($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && (float) $fieldValue >= (float) $condition['value'];
}
private function checkLessThan($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && (float) $fieldValue < (float) $condition['value'];
}
private function checkLessThanEqual($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && (float) $fieldValue <= (float) $condition['value'];
}
private function checkBefore($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && $fieldValue < $condition['value'];
}
private function checkAfter($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && $fieldValue > $condition['value'];
}
private function checkOnOrBefore($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && $fieldValue <= $condition['value'];
}
private function checkOnOrAfter($condition, $fieldValue): bool
{
return $condition['value'] && $fieldValue && $fieldValue >= $condition['value'];
}
private function checkPastWeek($condition, $fieldValue): bool
{
if (!$fieldValue) {
return false;
}
$fieldDate = date('Y-m-d', strtotime($fieldValue));
return $fieldDate <= now()->toDateString() && $fieldDate >= now()->subDays(7)->toDateString();
}
private function checkPastMonth($condition, $fieldValue): bool
{
if (!$fieldValue) {
return false;
}
$fieldDate = date('Y-m-d', strtotime($fieldValue));
return $fieldDate <= now()->toDateString() && $fieldDate >= now()->subMonths(1)->toDateString();
}
private function checkPastYear($condition, $fieldValue): bool
{
if (!$fieldValue) {
return false;
}
$fieldDate = date('Y-m-d', strtotime($fieldValue));
return $fieldDate <= now()->toDateString() && $fieldDate >= now()->subYears(1)->toDateString();
}
private function checkNextWeek($condition, $fieldValue): bool
{
if (!$fieldValue) {
return false;
}
$fieldDate = date('Y-m-d', strtotime($fieldValue));
return $fieldDate >= now()->toDateString() && $fieldDate <= now()->addDays(7)->toDateString();
}
private function checkNextMonth($condition, $fieldValue): bool
{
if (!$fieldValue) {
return false;
}
$fieldDate = date('Y-m-d', strtotime($fieldValue));
return $fieldDate >= now()->toDateString() && $fieldDate <= now()->addMonths(1)->toDateString();
}
private function checkNextYear($condition, $fieldValue): bool
{
if (!$fieldValue) {
return false;
}
$fieldDate = date('Y-m-d', strtotime($fieldValue));
return $fieldDate >= now()->toDateString() && $fieldDate <= now()->addYears(1)->toDateString();
}
private function checkLength($condition, $fieldValue, $operator = '==='): bool
{
if (!$fieldValue || strlen($fieldValue) === 0) {
return false;
}
switch ($operator) {
case '===':
return strlen($fieldValue) === (int) $condition['value'];
case '!==':
return strlen($fieldValue) !== (int) $condition['value'];
case '>':
return strlen($fieldValue) > (int) $condition['value'];
case '>=':
return strlen($fieldValue) >= (int) $condition['value'];
case '<':
return strlen($fieldValue) < (int) $condition['value'];
case '<=':
return strlen($fieldValue) <= (int) $condition['value'];
}
return false;
}
private function textConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'equals':
return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal':
return !$this->checkEquals($propertyCondition, $value);
case 'contains':
return $this->checkContains($propertyCondition, $value);
case 'does_not_contain':
return !$this->checkContains($propertyCondition, $value);
case 'starts_with':
return $this->checkStartsWith($propertyCondition, $value);
case 'ends_with':
return $this->checkEndsWith($propertyCondition, $value);
case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty':
return !$this->checkIsEmpty($propertyCondition, $value);
case 'content_length_equals':
return $this->checkLength($propertyCondition, $value, '===');
case 'content_length_does_not_equal':
return $this->checkLength($propertyCondition, $value, '!==');
case 'content_length_greater_than':
return $this->checkLength($propertyCondition, $value, '>');
case 'content_length_greater_than_or_equal_to':
return $this->checkLength($propertyCondition, $value, '>=');
case 'content_length_less_than':
return $this->checkLength($propertyCondition, $value, '<');
case 'content_length_less_than_or_equal_to':
return $this->checkLength($propertyCondition, $value, '<=');
}
return false;
}
private function numberConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'equals':
return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal':
return !$this->checkEquals($propertyCondition, $value);
case 'greater_than':
return $this->checkGreaterThan($propertyCondition, $value);
case 'less_than':
return $this->checkLessThan($propertyCondition, $value);
case 'greater_than_or_equal_to':
return $this->checkGreaterThanEqual($propertyCondition, $value);
case 'less_than_or_equal_to':
return $this->checkLessThanEqual($propertyCondition, $value);
case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty':
return !$this->checkIsEmpty($propertyCondition, $value);
case 'content_length_equals':
return $this->checkLength($propertyCondition, $value, '===');
case 'content_length_does_not_equal':
return $this->checkLength($propertyCondition, $value, '!==');
case 'content_length_greater_than':
return $this->checkLength($propertyCondition, $value, '>');
case 'content_length_greater_than_or_equal_to':
return $this->checkLength($propertyCondition, $value, '>=');
case 'content_length_less_than':
return $this->checkLength($propertyCondition, $value, '<');
case 'content_length_less_than_or_equal_to':
return $this->checkLength($propertyCondition, $value, '<=');
}
return false;
}
private function checkboxConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'equals':
return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal':
return !$this->checkEquals($propertyCondition, $value);
}
return false;
}
private function selectConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'equals':
return $this->checkEquals($propertyCondition, $value);
case 'does_not_equal':
return !$this->checkEquals($propertyCondition, $value);
case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty':
return !$this->checkIsEmpty($propertyCondition, $value);
}
return false;
}
private function dateConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'equals':
return $this->checkEquals($propertyCondition, $value);
case 'before':
return $this->checkBefore($propertyCondition, $value);
case 'after':
return $this->checkAfter($propertyCondition, $value);
case 'on_or_before':
return $this->checkOnOrBefore($propertyCondition, $value);
case 'on_or_after':
return $this->checkOnOrAfter($propertyCondition, $value);
case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value);
case 'past_week':
return $this->checkPastWeek($propertyCondition, $value);
case 'past_month':
return $this->checkPastMonth($propertyCondition, $value);
case 'past_year':
return $this->checkPastYear($propertyCondition, $value);
case 'next_week':
return $this->checkNextWeek($propertyCondition, $value);
case 'next_month':
return $this->checkNextMonth($propertyCondition, $value);
case 'next_year':
return $this->checkNextYear($propertyCondition, $value);
}
return false;
}
private function multiSelectConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'contains':
return $this->checkListContains($propertyCondition, $value);
case 'does_not_contain':
return !$this->checkListContains($propertyCondition, $value);
case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty':
return !$this->checkIsEmpty($propertyCondition, $value);
}
return false;
}
private function filesConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'is_empty':
return $this->checkIsEmpty($propertyCondition, $value);
case 'is_not_empty':
return !$this->checkIsEmpty($propertyCondition, $value);
}
return false;
}
private function matrixConditionMet(array $propertyCondition, $value): bool
{
switch ($propertyCondition['operator']) {
case 'equals':
return $this->checkMatrixEquals($propertyCondition, $value);
case 'does_not_equal':
return !$this->checkMatrixEquals($propertyCondition, $value);
case 'contains':
return $this->checkMatrixContains($propertyCondition, $value);
case 'does_not_contain':
return !$this->checkMatrixContains($propertyCondition, $value);
}
return false;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Service\Forms;
class FormLogicPropertyResolver
{
private $property = [];
private $formData = [];
private $logic = false;
public function __construct(private array $prop, private array $values)
{
$this->property = $prop;
$this->formData = $values;
$this->logic = isset($this->property['logic']) ? $this->property['logic'] : false;
}
public static function isRequired(array $property, array $values): bool
{
return (new self($property, $values))->shouldBeRequired();
}
public static function isHidden(array $property, array $values): bool
{
return (new self($property, $values))->shouldBeHidden();
}
public function shouldBeRequired(): bool
{
if (! isset($this->property['required'])) {
return false;
}
if (! $this->logic) {
return $this->property['required'];
}
$conditionsMet = FormLogicConditionChecker::conditionsMet($this->logic['conditions'], $this->formData);
if ($conditionsMet && $this->property['required'] && count($this->logic['actions']) > 0 && (in_array('make-it-optional', $this->logic['actions']) || in_array('hide-block', $this->logic['actions']))) {
return false;
} elseif ($conditionsMet && ! $this->property['required'] && count($this->logic['actions']) > 0 && in_array('require-answer', $this->logic['actions'])) {
return true;
} else {
return $this->property['required'];
}
}
public function shouldBeHidden(): bool
{
if (! isset($this->property['hidden'])) {
return false;
}
if (! $this->logic) {
return $this->property['hidden'];
}
$conditionsMet = FormLogicConditionChecker::conditionsMet($this->logic['conditions'], $this->formData);
if ($conditionsMet && $this->property['hidden'] && count($this->logic['actions']) > 0 && in_array('show-block', $this->logic['actions'])) {
return false;
} elseif ($conditionsMet && ! $this->property['hidden'] && count($this->logic['actions']) > 0 && in_array('hide-block', $this->logic['actions'])) {
return true;
} else {
return $this->property['hidden'];
}
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace App\Service\Forms;
use App\Models\Forms\Form;
use Carbon\Carbon;
class FormSubmissionFormatter
{
/**
* If true, creates html <a> links for emails and urls
*
* @var bool
*/
private $createLinks = false;
/**
* If true, serialize arrays
*
* @var bool
*/
private $outputStringsOnly = false;
private $showHiddenFields = false;
private $setEmptyForNoValue = false;
private $showRemovedFields = false;
private $useSignedUrlForFiles = false;
/**
* Logic resolver needs an array id => value, so we create it here
*/
private $idFormData = null;
public function __construct(private Form $form, private array $formData)
{
$this->initIdFormData();
}
public function createLinks()
{
$this->createLinks = true;
return $this;
}
public function showHiddenFields()
{
$this->showHiddenFields = true;
return $this;
}
public function outputStringsOnly()
{
$this->outputStringsOnly = true;
return $this;
}
public function setEmptyForNoValue()
{
$this->setEmptyForNoValue = true;
return $this;
}
public function showRemovedFields()
{
$this->showRemovedFields = true;
return $this;
}
public function useSignedUrlForFiles()
{
$this->useSignedUrlForFiles = true;
return $this;
}
public function getMatrixString(array $val): string
{
$parts = [];
foreach ($val as $key => $value) {
if ($key !== null && $value !== null) {
$parts[] = "$key: $value";
}
}
return implode(' | ', $parts);
}
/**
* Return a nice "FieldName": "Field Response" array
* - If createLink enabled, returns html link for emails and links
* Used for CSV exports
*/
public function getCleanKeyValue()
{
$data = $this->formData;
$fields = collect($this->form->properties);
$removeFields = collect($this->form->removed_properties)->map(function ($field) {
return [
...$field,
'removed' => true,
];
});
if ($this->showRemovedFields) {
$fields = $fields->merge($removeFields);
}
$fields = $fields->filter(function ($field) {
return !in_array($field['type'], ['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image']);
})->values();
$returnArray = [];
foreach ($fields as $field) {
if (in_array($field['id'], ['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image'])) {
continue;
}
if ($field['removed'] ?? false) {
$field['name'] = $field['name'] . ' (deleted)';
}
// Add ID to avoid name clashes
$field['name'] = $field['name'] . ' (' . \Str::of($field['id']) . ')';
// If not present skip
if (!isset($data[$field['id']])) {
if ($this->setEmptyForNoValue) {
$returnArray[$field['name']] = '';
}
continue;
}
// If hide hidden fields
if (!$this->showHiddenFields) {
if (FormLogicPropertyResolver::isHidden($field, $this->idFormData ?? [])) {
continue;
}
}
if ($this->createLinks && $field['type'] == 'url') {
$returnArray[$field['name']] = '<a href="' . $data[$field['id']] . '">' . $data[$field['id']] . '</a>';
} elseif ($this->createLinks && $field['type'] == 'email') {
$returnArray[$field['name']] = '<a href="mailto:' . $data[$field['id']] . '">' . $data[$field['id']] . '</a>';
} elseif ($field['type'] == 'multi_select') {
$val = $data[$field['id']];
if ($this->outputStringsOnly && is_array($val)) {
$returnArray[$field['name']] = implode(', ', $val);
} else {
$returnArray[$field['name']] = $val;
}
} elseif ($field['type'] == 'matrix' && is_array($data[$field['id']])) {
$returnArray[$field['name']] = $this->getMatrixString($data[$field['id']]);
} elseif ($field['type'] == 'files') {
if ($this->outputStringsOnly) {
$formId = $this->form->id;
$returnArray[$field['name']] = implode(
', ',
collect($data[$field['id']])->map(function ($file) use ($formId) {
return $this->getFileUrl($formId, $file);
})->toArray()
);
} else {
$formId = $this->form->id;
$returnArray[$field['name']] = collect($data[$field['id']])->map(function ($file) use ($formId) {
return [
'file_url' => $this->getFileUrl($formId, $file),
'file_name' => $file,
];
});
}
} else {
if (is_array($data[$field['id']]) && $this->outputStringsOnly) {
$data[$field['id']] = implode(', ', $data[$field['id']]);
}
$returnArray[$field['name']] = $data[$field['id']];
}
}
return $returnArray;
}
/**
* Return a list of fields, with a filled value attribute.
* Used for humans.
*/
public function getFieldsWithValue()
{
$data = $this->formData;
$fields = $this->form->properties;
$transformedFields = [];
foreach ($fields as $field) {
if (!isset($field['id']) || !isset($data[$field['id']])) {
continue;
}
// If hide hidden fields
if (!$this->showHiddenFields) {
if (FormLogicPropertyResolver::isHidden($field, $this->idFormData)) {
continue;
}
}
if ($this->createLinks && $field['type'] == 'url') {
$field['value'] = '<a href="' . $data[$field['id']] . '">' . $data[$field['id']] . '</a>';
} elseif ($this->createLinks && $field['type'] == 'email') {
$field['value'] = '<a href="mailto:' . $data[$field['id']] . '">' . $data[$field['id']] . '</a>';
} elseif ($field['type'] == 'checkbox') {
$field['value'] = $data[$field['id']] ? 'Yes' : 'No';
} elseif ($field['type'] == 'date') {
$dateFormat = ($field['date_format'] ?? 'dd/MM/yyyy') == 'dd/MM/yyyy' ? 'd/m/Y' : 'm/d/Y';
if (isset($field['with_time']) && $field['with_time']) {
$dateFormat .= (isset($field['time_format']) && $field['time_format'] == 24) ? ' H:i' : ' g:ia';
}
if (is_array($data[$field['id']])) {
$field['value'] = isset($data[$field['id']][1]) ? (new Carbon($data[$field['id']][0]))->format($dateFormat)
. ' - ' . (new Carbon($data[$field['id']][1]))->format($dateFormat) : (new Carbon($data[$field['id']][0]))->format($dateFormat);
} else {
$field['value'] = (new Carbon($data[$field['id']]))->format($dateFormat);
}
} elseif ($field['type'] == 'multi_select') {
$val = $data[$field['id']];
if ($this->outputStringsOnly) {
$field['value'] = implode(', ', $val);
} else {
$field['value'] = $val;
}
} elseif ($field['type'] == 'matrix') {
$field['value'] = str_replace(' | ', "\n", $this->getMatrixString($data[$field['id']]));
} elseif ($field['type'] == 'files') {
if ($this->outputStringsOnly) {
$formId = $this->form->id;
$field['value'] = implode(
', ',
collect($data[$field['id']])->map(function ($file) use ($formId) {
return $this->getFileUrl($formId, $file);
})->toArray()
);
$field['email_data'] = collect($data[$field['id']])->map(function ($file) use ($formId) {
$splitText = explode('.', $file);
return [
'unsigned_url' => route('open.forms.submissions.file', [$formId, $file]),
'signed_url' => $this->getFileUrl($formId, $file),
'label' => \Str::limit($file, 20, '[...].' . end($splitText)),
];
})->toArray();
} else {
$formId = $this->form->id;
$field['value'] = collect($data[$field['id']])->map(function ($file) use ($formId) {
return [
'file_url' => $this->getFileUrl($formId, $file),
'file_name' => $file,
];
});
}
} else {
if (is_array($data[$field['id']]) && $this->outputStringsOnly) {
$field['value'] = implode(', ', $data[$field['id']]);
} else {
$field['value'] = $data[$field['id']];
}
}
$transformedFields[] = $field;
}
return $transformedFields;
}
private function initIdFormData()
{
$formProperties = collect($this->form->properties);
foreach ($this->formData as $key => $value) {
$property = $formProperties->first(function ($item) use ($key) {
return $item['id'] == $key;
});
if ($property) {
$this->idFormData[$property['id']] = $value;
}
}
}
private function getFileUrl($formId, $file)
{
return $this->useSignedUrlForFiles ? \URL::signedRoute(
'open.forms.submissions.file',
[$formId, $file]
) : route('open.forms.submissions.file', [$formId, $file]);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Service\HtmlPurifier;
class HTMLPurifier_URIScheme_notion extends \HTMLPurifier_URIScheme
{
public $browsable = true;
public $may_omit_host = true;
public function doValidate(&$uri, $config, $context)
{
if ($uri->host == 'www.notion.so' || $uri->host == 'notion.so') {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,205 @@
<?php
namespace App\Service\OpenAi;
use App\Service\OpenAi\Utils\JsonFixer;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use OpenAI\Client;
use OpenAI\Exceptions\ErrorException;
/**
* Handles a GPT completion prompt with or without insert tag.
* Also parses output.
*/
class GptCompleter
{
public const AI_MODEL = 'gpt-4o';
protected Client $openAi;
protected mixed $result;
protected array $completionInput;
protected ?string $systemMessage;
protected bool $expectsJson = false;
protected int $tokenUsed = 0;
protected bool $useStreaming = false;
public function __construct(string $apiKey, protected int $retries = 2, protected string $model = self::AI_MODEL)
{
$this->openAi = \OpenAI::client($apiKey);
}
public function setAiModel(string $model)
{
$this->model = $model;
return $this;
}
public function setSystemMessage(string $systemMessage): self
{
$this->systemMessage = $systemMessage;
return $this;
}
public function useStreaming(): self
{
$this->useStreaming = true;
return $this;
}
public function expectsJson(): self
{
$this->expectsJson = true;
return $this;
}
public function doesNotExpectJson(): self
{
$this->expectsJson = false;
return $this;
}
public function completeChat(array $messages, int $maxTokens = 4096, float $temperature = 0.81, ?bool $exceptJson = null): self
{
if (! is_null($exceptJson)) {
$this->expectsJson = $exceptJson;
}
$this->computeChatCompletion($messages, $maxTokens, $temperature)
->queryCompletion();
return $this;
}
public function getBool(): bool
{
switch (strtolower($this->result)) {
case 'true':
return true;
case 'false':
return false;
default:
throw new \InvalidArgumentException("Expected a boolean value, got {$this->result}");
}
}
public function getArray(): array
{
for ($i = 0; $i < $this->retries; $i++) {
$payload = Str::of($this->result)->trim();
if ($payload->contains('```json')) {
$payload = $payload->after('```json')->before('```');
} elseif ($payload->contains('```')) {
$payload = $payload->after('```')->before('```');
}
$payload = $payload->toString();
$exception = null;
try {
$newPayload = (new JsonFixer())->fix($payload);
return json_decode($newPayload, true);
} catch (\Aws\Exception\InvalidJsonException $e) {
$exception = $e;
Log::warning('Invalid JSON, retrying:');
Log::warning($payload);
$this->queryCompletion();
}
}
throw $exception;
}
public function getHtml(): string
{
$payload = Str::of($this->result)->trim();
if ($payload->contains('```html')) {
$payload = $payload->after('```html')->before('```');
} elseif ($payload->contains('```')) {
$payload = $payload->after('```')->before('```');
}
return $payload->toString();
}
public function getString(): string
{
return trim($this->result);
}
public function getTokenUsed(): int
{
return $this->tokenUsed;
}
protected function computeChatCompletion(array $messages, int $maxTokens = 4096, float $temperature = 0.81): self
{
if (isset($this->systemMessage) && $messages[0]['role'] !== 'system') {
$messages = array_merge([[
'role' => 'system',
'content' => $this->systemMessage,
]], $messages);
}
$completionInput = [
'model' => $this->model,
'messages' => $messages,
'max_tokens' => $maxTokens,
'temperature' => $temperature,
];
if ($this->expectsJson) {
$completionInput['response_format'] = [
'type' => 'json_object',
];
}
$this->completionInput = $completionInput;
return $this;
}
protected function queryCompletion(): self
{
if ($this->useStreaming) {
return $this->queryStreamedCompletion();
}
try {
Log::debug('Open AI query: '.json_encode($this->completionInput));
$response = $this->openAi->chat()->create($this->completionInput);
} catch (ErrorException $errorException) {
// Retry once
Log::warning("Open AI error, retrying: {$errorException->getMessage()}");
$response = $this->openAi->chat()->create($this->completionInput);
}
$this->tokenUsed += $response->usage->totalTokens;
$this->result = $response->choices[0]->message->content;
return $this;
}
protected function queryStreamedCompletion(): self
{
Log::debug('Open AI query: '.json_encode($this->completionInput));
$this->result = '';
$response = $this->openAi->chat()->createStreamed($this->completionInput);
foreach ($response as $chunk) {
$choice = $chunk->choices[0];
if (is_null($choice->delta->role)) {
$this->result .= $choice->delta->content;
}
}
return $this;
}
}

View File

@@ -0,0 +1,274 @@
<?php
namespace App\Service\OpenAi\Utils;
/*
* This file is part of the PHP-JSON-FIXER package.
*
* (c) Jitendra Adhikari <jiten.adhikary@gmail.com>
* <https://github.com/adhocore>
*
* Licensed under MIT license.
*/
use Aws\Exception\InvalidJsonException;
/**
* Attempts to fix truncated JSON by padding contextual counterparts at the end.
*
* @author Jitendra Adhikari <jiten.adhikary@gmail.com>
* @license MIT
*
* @link https://github.com/adhocore/php-json-fixer
*/
class JsonFixer
{
use PadsJson;
/** @var array Current token stack indexed by position */
protected $stack = [];
/** @var bool If current char is within a string */
protected $inStr = false;
/** @var bool Whether to throw Exception on failure */
protected $silent = false;
/** @var array The complementary pairs */
protected $pairs = [
'{' => '}',
'[' => ']',
'"' => '"',
];
/** @var int The last seen object `{` type position */
protected $objectPos = -1;
/** @var int The last seen array `[` type position */
protected $arrayPos = -1;
/** @var string Missing value. (Options: true, false, null) */
protected $missingValue = 'null';
/**
* Set/unset silent mode.
*
* @param bool $silent
* @return $this
*/
public function silent($silent = true)
{
$this->silent = (bool) $silent;
return $this;
}
/**
* Set missing value.
*
* @param mixed $value
* @return $this
*/
public function missingValue($value)
{
if ($value === null) {
$value = 'null';
} elseif (\is_bool($value)) {
$value = $value ? 'true' : 'false';
}
$this->missingValue = $value;
return $this;
}
/**
* Fix the truncated JSON.
*
* @param string $json The JSON string to fix.
* @return string Fixed JSON. If failed with silent then original JSON.
*
* @throws InvalidJsonException When fixing fails.
*/
public function fix($json)
{
$json = preg_replace('/(?<!\\\\)(?:\\\\{2})*\p{C}+/u', '', $json);
[$head, $json, $tail] = $this->trim($json);
if (empty($json) || $this->isValid($json)) {
return $json;
}
if (null !== $tmpJson = $this->quickFix($json)) {
return $tmpJson;
}
$this->reset();
return $head.$this->doFix($json).$tail;
}
protected function trim($json)
{
\preg_match('/^(\s*)([^\s]+)(\s*)$/', $json, $match);
$match += ['', '', '', ''];
$match[2] = \trim($json);
\array_shift($match);
return $match;
}
protected function isValid($json)
{
\json_decode($json, true, 512, JSON_INVALID_UTF8_SUBSTITUTE);
return \json_last_error() === \JSON_ERROR_NONE;
}
protected function quickFix($json)
{
if (\strlen($json) === 1 && isset($this->pairs[$json])) {
return $json.$this->pairs[$json];
}
if ($json[0] !== '"') {
return $this->maybeLiteral($json);
}
return $this->padString($json);
}
protected function reset()
{
$this->stack = [];
$this->inStr = false;
$this->objectPos = -1;
$this->arrayPos = -1;
}
protected function maybeLiteral($json)
{
if (! \in_array($json[0], ['t', 'f', 'n'])) {
return null;
}
foreach (['true', 'false', 'null'] as $literal) {
if (\strpos($literal, $json) === 0) {
return $literal;
}
}
// @codeCoverageIgnoreStart
return null;
// @codeCoverageIgnoreEnd
}
protected function doFix($json)
{
[$index, $char] = [-1, ''];
while (isset($json[++$index])) {
[$prev, $char] = [$char, $json[$index]];
$next = isset($json[$index + 1]) ? $json[$index + 1] : '';
if (! \in_array($char, [' ', "\n", "\r"])) {
$this->stack($prev, $char, $index, $next);
}
}
return $this->fixOrFail($json);
}
protected function stack($prev, $char, $index, $next)
{
if ($this->maybeStr($prev, $char, $index)) {
return;
}
$last = $this->lastToken();
if (\in_array($last, [',', ':', '"']) && \preg_match('/\"|\d|\{|\[|t|f|n/', $char)) {
$this->popToken();
}
if (\in_array($char, [',', ':', '[', '{'])) {
$this->stack[$index] = $char;
}
$this->updatePos($char, $index);
}
protected function lastToken()
{
return \end($this->stack);
}
protected function popToken($token = null)
{
// Last one
if ($token === null) {
return \array_pop($this->stack);
}
$keys = \array_reverse(\array_keys($this->stack));
foreach ($keys as $key) {
if ($this->stack[$key] === $token) {
unset($this->stack[$key]);
break;
}
}
}
protected function maybeStr($prev, $char, $index)
{
if ($prev !== '\\' && $char === '"') {
$this->inStr = ! $this->inStr;
}
if ($this->inStr && $this->lastToken() !== '"') {
$this->stack[$index] = '"';
}
return $this->inStr;
}
protected function updatePos($char, $index)
{
if ($char === '{') {
$this->objectPos = $index;
} elseif ($char === '}') {
$this->popToken('{');
$this->objectPos = -1;
} elseif ($char === '[') {
$this->arrayPos = $index;
} elseif ($char === ']') {
$this->popToken('[');
$this->arrayPos = -1;
}
}
protected function fixOrFail($json)
{
$length = \strlen($json);
$tmpJson = $this->pad($json);
if ($this->isValid($tmpJson)) {
return $tmpJson;
}
if ($this->silent) {
return $json;
}
\Log::debug('Broken json received: ', [
'json' => $json,
]);
throw new InvalidJsonException(
\sprintf('Could not fix JSON (tried padding `%s`)', \substr($tmpJson, $length), $json)
);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Service\OpenAi\Utils;
/**
* Attempts to fix truncated JSON by padding contextual counterparts at the end.
*
* @author Jitendra Adhikari <jiten.adhikary@gmail.com>
* @license MIT
*
* @internal
*
* @link https://github.com/adhocore/php-json-fixer
*/
trait PadsJson
{
public function pad($tmpJson)
{
if (! $this->inStr) {
$tmpJson = \rtrim($tmpJson, ',');
while ($this->lastToken() === ',') {
$this->popToken();
}
}
$tmpJson = $this->padLiteral($tmpJson);
$tmpJson = $this->padObject($tmpJson);
return $this->padStack($tmpJson);
}
protected function padLiteral($tmpJson)
{
if ($this->inStr) {
return $tmpJson;
}
$match = \preg_match('/(tr?u?e?|fa?l?s?e?|nu?l?l?)$/', $tmpJson, $matches);
if (! $match || null === $literal = $this->maybeLiteral($matches[1])) {
return $tmpJson;
}
return \substr($tmpJson, 0, 0 - \strlen($matches[1])).$literal;
}
protected function padStack($tmpJson)
{
foreach (\array_reverse($this->stack, true) as $token) {
if (isset($this->pairs[$token])) {
$tmpJson .= $this->pairs[$token];
}
}
return $tmpJson;
}
protected function padObject($tmpJson)
{
if (! $this->objectNeedsPadding($tmpJson)) {
return $tmpJson;
}
$part = \substr($tmpJson, $this->objectPos + 1);
if (\preg_match('/(\s*\"[^"]+\"\s*:\s*[^,]+,?)+$/', $part, $matches)) {
return $tmpJson;
}
if ($this->inStr) {
$tmpJson .= '"';
}
$tmpJson = $this->padIf($tmpJson, ':');
$tmpJson = $tmpJson.$this->missingValue;
if ($this->lastToken() === '"') {
$this->popToken();
}
return $tmpJson;
}
protected function objectNeedsPadding($tmpJson)
{
$last = \substr($tmpJson, -1);
$empty = $last === '{' && ! $this->inStr;
return ! $empty && $this->arrayPos < $this->objectPos;
}
protected function padString($string)
{
$last = \substr($string, -1);
$last2 = \substr($string, -2);
if ($last2 === '\"' || $last !== '"') {
return $string.'"';
}
// @codeCoverageIgnoreStart
return null;
// @codeCoverageIgnoreEnd
}
protected function padIf($string, $substr)
{
if (\substr($string, 0 - \strlen($substr)) !== $substr) {
return $string.$substr;
}
return $string;
}
}

View File

@@ -0,0 +1,217 @@
<?php
namespace App\Service;
use App\Models\Forms\Form;
use App\Models\Template;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* Generates meta per-route matching. This is useful because Google, Twitter and Facebook struggle to load meta tags
* injected dynamically by JavaScript. This class allows us to inject meta tags in the HTML head tag.
*
* Here's how to use this class
* - Add a pattern to URL_PATTERNS
* - Then choose between a static meta or a dynamic meta:
* - If the content is dynamic (ex: needs to retrieve data from the database), then add a method to this class for
* the corresponding pattern. The method should be named "getMyPatternName" (where pattern name is
* my_pattern_name) and it should return an array of meta tags.
* - If the content is static, then add meta tags to the PATTERN_STATIC_META array.
*/
class SeoMetaResolver
{
private array $patternData = [];
public const URL_PATTERNS = [
'welcome' => '/',
'form_show' => '/forms/{slug}',
'login' => '/login',
'register' => '/register',
'reset_password' => '/password/reset',
'privacy_policy' => '/privacy-policy',
'terms_conditions' => '/terms-conditions',
'integrations' => '/integrations',
'templates' => '/form-templates',
'templates_show' => '/form-templates/{slug}',
'templates_types_show' => '/form-templates/types/{slug}',
'templates_industries_show' => '/form-templates/industries/{slug}',
];
/**
* Metas for simple route (without needing to access DB)
*/
public const PATTERN_STATIC_META = [
'login' => [
'title' => 'Login',
],
'register' => [
'title' => 'Create your account',
],
'reset_password' => [
'title' => 'Reset your password',
],
'privacy_policy' => [
'title' => 'Our Privacy Policy',
],
'terms_conditions' => [
'title' => 'Our Terms & Conditions',
],
'integrations' => [
'title' => 'Our Integrations',
],
'templates' => [
'title' => 'Templates',
'description' => 'Our collection of beautiful templates to create your own forms!',
],
];
public const META_CACHE_DURATION = 60 * 60 * 12; // 12 hours
public const META_CACHE_KEY_PREFIX = 'seo_meta_';
public function __construct(private Request $request)
{
}
/**
* Returns the right metas for a given route, caches meta for 1 hour.
*/
public function getMetas(): array
{
$cacheKey = self::META_CACHE_KEY_PREFIX.urlencode($this->request->path());
return Cache::remember($cacheKey, now()->addSeconds(self::META_CACHE_DURATION), function () {
$pattern = $this->resolvePattern();
if ($this->hasPatternMetaGetter($pattern)) {
// Custom function for pattern
try {
return array_merge($this->getDefaultMeta(), $this->{'get'.Str::studly($pattern).'Meta'}());
} catch (\Exception $e) {
return $this->getDefaultMeta();
}
} elseif (in_array($pattern, array_keys(self::PATTERN_STATIC_META))) {
// Simple meta for pattern
$meta = self::PATTERN_STATIC_META[$pattern];
if (isset($meta['title'])) {
$meta['title'] .= $this->titleSuffix();
}
if (isset($meta['image'])) {
$meta['image'] = asset($meta['image']);
}
return array_merge($this->getDefaultMeta(), $meta);
}
return $this->getDefaultMeta();
});
}
/**
* Simulates the Laravel router to match route with Metas
*
* @return string
*/
private function resolvePattern()
{
foreach (self::URL_PATTERNS as $patternName => $patternData) {
$path = rtrim($this->request->getPathInfo(), '/') ?: '/';
$route = (new Route('GET', $patternData, fn () => ''))->bind($this->request);
if (preg_match($route->getCompiled()->getRegex(), rawurldecode($path))) {
$this->patternData = $route->parameters();
return $patternName;
}
}
return 'default';
}
/**
* Determine if a get mutator exists for a pattern.
*
* @param string $key
* @return bool
*/
private function hasPatternMetaGetter($key)
{
return method_exists($this, 'get'.Str::studly($key).'Meta');
}
private function titleSuffix()
{
return ' · '.config('app.name');
}
private function getDefaultMeta(): array
{
return [
'title' => 'Create beautiful forms for free'.$this->titleSuffix(),
'description' => "Create beautiful forms for free. Unlimited fields, unlimited submissions. It's free and it takes less than 1 minute to create your first form.",
'image' => asset('/img/social-preview.jpg'),
];
}
private function getFormShowMeta(): array
{
$form = Form::whereSlug($this->patternData['slug'])->firstOrFail();
$meta = [];
if ($form->is_pro && $form->seo_meta->page_title) {
$meta['title'] = $form->seo_meta->page_title;
} else {
$meta['title'] = $form->title.$this->titleSuffix();
}
if ($form->is_pro && $form->seo_meta->page_description) {
$meta['description'] = $form->seo_meta->page_description;
} elseif ($form->description) {
$meta['description'] = Str::of($form->description)->limit(160);
}
if ($form->is_pro && $form->seo_meta->page_thumbnail) {
$meta['image'] = $form->seo_meta->page_thumbnail;
} elseif ($form->cover_picture) {
$meta['image'] = $form->cover_picture;
}
return $meta;
}
private function getTemplatesShowMeta(): array
{
$template = Template::whereSlug($this->patternData['slug'])->firstOrFail();
return [
'title' => $template->name.$this->titleSuffix(),
'description' => Str::of($template->short_description)->limit(140).' | Customize any template and create your own form in minutes.',
'image' => $template->image_url,
];
}
private function getTemplatesTypesShowMeta(): array
{
$types = json_decode(file_get_contents(resource_path('data/forms/templates/types.json')), true);
$type = $types[array_search($this->patternData['slug'], array_column($types, 'slug'))];
return [
'title' => $type['meta_title'],
'description' => Str::of($type['meta_description'])->limit(140),
];
}
private function getTemplatesIndustriesShowMeta(): array
{
$industries = json_decode(file_get_contents(resource_path('data/forms/templates/industries.json')), true);
$industry = $industries[array_search($this->patternData['slug'], array_column($industries, 'slug'))];
return [
'title' => $industry['meta_title'],
'description' => Str::of($industry['meta_description'])->limit(140),
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Service\Storage;
class S3KeyCleaner
{
public static function sanitize($objectKey, $separator = '-')
{
return (new self())->sanitizeS3Key($objectKey, $separator);
}
public function replaceLatinCharacters($value)
{
// Load the latin map
$latinMap = json_decode(\File::get(resource_path('data/latin-map.json')), true);
$result = '';
$length = mb_strlen($value);
for ($i = 0; $i < $length; $i++) {
$char = mb_substr($value, $i, 1);
$result .= array_key_exists($char, $latinMap) ? $latinMap[$char] : $char;
}
return $result;
}
private function removeIllegalCharacters($value)
{
$SAFE_CHARACTERS = '/[^0-9a-zA-Z! _\\.\\*\'\\(\\)\\-\\/]/';
return preg_replace($SAFE_CHARACTERS, '', $value);
}
private function isValidSeparator($separator)
{
$SAFE_CHARACTERS = '/[^0-9a-zA-Z! _\\.\\*\'\\(\\)\\-\\/]/';
return $separator && !preg_match($SAFE_CHARACTERS, $separator);
}
public function sanitizeS3Key($objectKey, $separator = '-')
{
if (!$this->isValidSeparator($separator)) {
throw new \Exception("${separator} is not a valid separator");
}
if (!$objectKey || (!is_string($objectKey) && !is_numeric($objectKey))) {
throw new \Exception("Expected non-empty string or number, got ${objectKey}");
}
if (is_numeric($objectKey)) {
return strval($objectKey);
}
return str_replace(' ', $separator, $this->removeIllegalCharacters($this->replaceLatinCharacters(trim($objectKey))));
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Service\Storage;
use Illuminate\Support\Str;
/**
* Used
* File can have 2 formats:
* - file_name-{uuid}.{ext}
* - {uuid}
*/
class StorageFileNameParser
{
public ?string $uuid = null;
public ?string $fileName = null;
public ?string $extension = null;
public function __construct(string $fileName)
{
$this->parseFileName($fileName);
}
/**
* If we have parsed a file name and an extension, we keep the same and append uuid to avoid collisions
* Otherwise we just return the uuid
*/
public function getMovedFileName(): ?string
{
if ($this->fileName && $this->extension) {
$fileName = substr($this->fileName, 0, 50) . '_' . $this->uuid . '.' . $this->extension;
$fileName = preg_replace('#\p{C}+#u', '', $fileName); // avoid CorruptedPathDetected exceptions
return S3KeyCleaner::sanitize($fileName);
}
return $this->uuid;
}
private function parseFileName(string $fileName)
{
if (Str::isUuid($fileName)) {
$this->uuid = $fileName;
return;
}
if (!str_contains($fileName, '_')) {
return;
}
$candidateString = substr($fileName, strrpos($fileName, '_') + 1);
if (
!str_contains($candidateString, '.')
|| !Str::isUuid(substr($candidateString, 0, strpos($candidateString, '.')))
) {
return;
}
try {
$this->uuid = substr($candidateString, 0, strpos($candidateString, '.'));
$this->fileName = substr($fileName, 0, strrpos($fileName, '_'));
// get everything after the last dot
$this->extension = substr($candidateString, strrpos($candidateString, '.') + 1);
} catch (\Exception $e) {
return;
}
}
public static function parse(string $fileName): self
{
return new self($fileName);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Service;
use App\Models\User;
class UserHelper
{
public function __construct(public User $user)
{
}
/**
* Function to get to total number of active members in each of this user's workspaces
*/
public function getActiveMembersCount(): ?int
{
$count = 1;
foreach ($this->user->workspaces as $workspace) {
$count += $workspace->users()->where('users.id', '!=', $this->user->id)->count();
}
return $count;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Service;
use App\Models\Workspace;
class WorkspaceHelper
{
public function __construct(public Workspace $workspace)
{
}
public function getAllUsers()
{
return $this->workspace->users()->withPivot('role')->get();
}
public function getAllInvites()
{
return $this->workspace->invites()->get();
}
}