Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SFT-671] - Support ability to encrypt/decrypt submission data #882

Merged
merged 14 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,584 changes: 4,554 additions & 4,030 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { FormComponent } from '@components/form-controls';
import type { ControlType } from '@components/form-controls/types';
import { useAppDispatch } from '@editor/store';
import type { Field } from '@editor/store/slices/layout/fields';
import { fieldThunks } from '@editor/store/thunks/fields';
import {
useFetchFieldTypes,
useFieldTypeSearch,
} from '@ff-client/queries/field-types';
import type { FieldTypeProperty } from '@ff-client/types/properties';
import { PropertyType } from '@ff-client/types/properties';
import translate from '@ff-client/utils/translations';

const FieldType: React.FC<ControlType<FieldTypeProperty>> = ({
property,
context,
}) => {
const dispatch = useAppDispatch();
const searchFieldType = useFieldTypeSearch();
const { data: types } = useFetchFieldTypes();
const field = context as Field;

return (
<FormComponent
value={field.typeClass}
property={{
type: PropertyType.Select,
handle: 'typeClass',
label: translate(property.label),
instructions: translate(property.instructions),
options: types.map((type) => ({
label: type.name,
value: type.typeClass,
})),
}}
updateValue={(value) => {
if (
!confirm(
translate(
'Are you sure? You might potentially lose important data.'
)
)
) {
return;
}

dispatch(
fieldThunks.change.type(field, searchFieldType(value as string))
);
}}
/>
);
};

export default FieldType;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as colorPicker } from './color-picker/color-picker';
export { default as datePicker } from './date-picker/date-picker';
export { default as dynamicSelect } from './dynamic-select/dynamic-select';
export { default as field } from './field/field';
export { default as fieldType } from './field-type/field-type';
export { default as hidden } from './hidden/hidden';
export { default as int } from './int/int';
export { default as label } from './label/label';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import React from 'react';
import Skeleton from 'react-loading-skeleton';
import { useSelector } from 'react-redux';
import { FormComponent } from '@components/form-controls';
import { useAppDispatch } from '@editor/store';
import { contextActions } from '@editor/store/slices/context';
import { fieldSelectors } from '@editor/store/slices/layout/fields/fields.selectors';
import { fieldThunks } from '@editor/store/thunks/fields';
import CloseIcon from '@ff-client/assets/icons/circle-xmark-solid.svg';
import {
useFetchFieldPropertySections,
useFetchFieldTypes,
useFieldType,
useFieldTypeSearch,
} from '@ff-client/queries/field-types';
import { type Property, PropertyType } from '@ff-client/types/properties';
import translate from '@ff-client/utils/translations';
import { type Property } from '@ff-client/types/properties';

import { CloseLink, Icon, Title } from '../../property-editor.styles';
import { SectionBlock } from '../../section-block';
import { SectionWrapper } from '../../section-block.styles';

import { FavoriteButton } from './favorite/favorite.button';
import AdvancedIcon from './icons/advanced.svg';
import { FieldComponent } from './field-component';
import { FieldPropertiesWrapper } from './field-properties.styles';

Expand All @@ -32,13 +26,10 @@ export const FieldProperties: React.FC<{ uid: string }> = ({ uid }) => {
const dispatch = useAppDispatch();

const { data: sections, isFetching } = useFetchFieldPropertySections();
const { data: types } = useFetchFieldTypes();

const field = useSelector(fieldSelectors.one(uid));
const type = useFieldType(field?.typeClass);

const searchFieldType = useFieldTypeSearch();

if (!field || !type) {
return <FieldPropertiesWrapper />;
}
Expand Down Expand Up @@ -89,40 +80,7 @@ export const FieldProperties: React.FC<{ uid: string }> = ({ uid }) => {
<Icon dangerouslySetInnerHTML={{ __html: type.icon }} />
<span>{type.name}</span>
</Title>
<SectionWrapper>
{sectionBlocks}

<SectionBlock label={translate('Advanced')} icon={<AdvancedIcon />}>
<FormComponent
value={field.typeClass}
property={{
type: PropertyType.Select,
handle: 'typeClass',
label: translate('Field type'),
instructions: translate('Change the type of this field.'),
options: types.map((type) => ({
label: type.name,
value: type.typeClass,
})),
}}
updateValue={(value) => {
if (
!confirm(
translate(
'Are you sure? You might potentially lose important data.'
)
)
) {
return;
}

dispatch(
fieldThunks.change.type(field, searchFieldType(value as string))
);
}}
/>
</SectionBlock>
</SectionWrapper>
<SectionWrapper>{sectionBlocks}</SectionWrapper>
</FieldPropertiesWrapper>
);
};
3 changes: 3 additions & 0 deletions packages/client/src/types/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum PropertyType {
DateTime = 'dateTime',
Field = 'field',
FieldMapping = 'fieldMapping',
FieldType = 'fieldType',
Hidden = 'hidden',
Integer = 'int',
Label = 'label',
Expand Down Expand Up @@ -204,6 +205,7 @@ export type FieldMappingProperty = BaseProperty<
source?: string;
parameterFields?: string[];
};
export type FieldTypeProperty = BaseProperty<string, PropertyType.FieldType>;

export type WYSIWYGProperty = BaseProperty<string, PropertyType.WYSIWYG>;
export type CodeEditorProperty = BaseProperty<
Expand All @@ -222,6 +224,7 @@ export type Property =
| DateTimeProperty
| DynamicSelectProperty
| FieldMappingProperty
| FieldTypeProperty
| FieldProperty
| HiddenProperty
| IntegerProperty
Expand Down
14 changes: 14 additions & 0 deletions packages/plugin/src/Attributes/Property/Input/FieldType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Solspace\Freeform\Attributes\Property\Input;

use Solspace\Freeform\Attributes\Property\Property;

/**
* @extends Property<string>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class FieldType extends Property
{
public ?string $type = 'fieldType';
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
use craft\web\Application;
use Solspace\Freeform\Freeform;
use Solspace\Freeform\Library\Bundles\FeatureBundle;
use Solspace\Freeform\Library\Exceptions\FreeformException;
use Solspace\Freeform\Library\Helpers\EncryptionHelper;
use Solspace\Freeform\Records\NotificationLogRecord;
use Solspace\Freeform\Records\NotificationTemplateRecord;
use Solspace\Freeform\Records\Pro\ExportNotificationRecord;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use yii\base\Event;
use yii\base\Exception;
use yii\base\InvalidConfigException;

class ExportNotifications extends FeatureBundle
{
Expand All @@ -24,6 +30,13 @@ public function __construct()
Event::on(Application::class, Application::EVENT_AFTER_REQUEST, [$this, 'handleNotifications']);
}

/**
* @throws InvalidConfigException
* @throws FreeformException
* @throws LoaderError
* @throws SyntaxError
* @throws Exception
*/
public function handleNotifications(): void
{
if (Freeform::isLocked(self::CACHE_KEY, self::CACHE_TTL)) {
Expand Down Expand Up @@ -70,6 +83,9 @@ public function handleNotifications(): void

$data = $profile->getSubmissionData();

$key = EncryptionHelper::getKey($form->getUid());
$data = EncryptionHelper::decryptExportData($key, $data);

$exporter = $exportService->createExporter($notification->fileType, $form, $data);

$fileName = $mailer->renderString(
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/src/Bundles/Routing/routes/cp/export.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
return [
// quick export
'freeform/export/export-dialogue' => 'freeform/export/quick-export/export-dialogue',
'freeform/export' => 'freeform/export/quick-export/index',
'freeform/export/quick-export' => 'freeform/export/quick-export/index',

// Export Profiles
'freeform/export/profiles' => 'freeform/export/profiles/index',
Expand Down
59 changes: 59 additions & 0 deletions packages/plugin/src/Bundles/Submissions/EncryptionBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Solspace\Freeform\Bundles\Submissions;

use craft\events\PopulateElementEvent;
use Solspace\Freeform\Elements\Db\SubmissionQuery;
use Solspace\Freeform\Elements\Submission;
use Solspace\Freeform\Events\Submissions\ProcessFieldValueEvent;
use Solspace\Freeform\Fields\Interfaces\EncryptionInterface;
use Solspace\Freeform\Freeform;
use Solspace\Freeform\Library\Bundles\FeatureBundle;
use Solspace\Freeform\Library\Helpers\EncryptionHelper;
use yii\base\Event;

class EncryptionBundle extends FeatureBundle
{
public function __construct()
{
Event::on(
Submission::class,
Submission::EVENT_PROCESS_FIELD_VALUE,
[$this, 'encrypt']
);

Event::on(
SubmissionQuery::class,
SubmissionQuery::EVENT_BEFORE_POPULATE_ELEMENT,
[$this, 'decrypt']
);
}

public function encrypt(ProcessFieldValueEvent $event): void
{
$field = $event->getField();

$value = $event->getValue();

if ($this->plugin()->edition()->isBelow(Freeform::EDITION_PRO) || !$field instanceof EncryptionInterface || !$field->isEncrypted() || !$value) {
return;
}

$key = EncryptionHelper::getKey($field->getForm()->getUid());

$value = EncryptionHelper::encrypt($key, $value);

$event->setValue($value);
}

public function decrypt(PopulateElementEvent $event): void
{
$form = Freeform::getInstance()->forms->getFormById($event->row['formId']);

$key = EncryptionHelper::getKey($form->getUid());

foreach ($event->row as $field => $value) {
$event->row[$field] = EncryptionHelper::decrypt($key, $value);
}
}
}
25 changes: 15 additions & 10 deletions packages/plugin/src/Elements/Submission.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Solspace\Freeform\Elements\Actions\SendNotificationAction;
use Solspace\Freeform\Elements\Actions\SetSubmissionStatusAction;
use Solspace\Freeform\Elements\Db\SubmissionQuery;
use Solspace\Freeform\Events\Submissions\ProcessFieldValueEvent;
use Solspace\Freeform\Fields\AbstractField;
use Solspace\Freeform\Fields\FieldInterface;
use Solspace\Freeform\Fields\Implementations\CheckboxField;
Expand Down Expand Up @@ -51,6 +52,7 @@ class Submission extends Element
public const FIELD_COLUMN_PREFIX = 'field_';

public const EVENT_PROCESS_SUBMISSION = 'process-submission';
public const EVENT_PROCESS_FIELD_VALUE = 'process-field-value';

public const OPT_IN_DATA_TOKEN_LENGTH = 100;

Expand Down Expand Up @@ -417,7 +419,10 @@ public function afterSave(bool $isNew): void
$value = LitEmoji::unicodeToShortcode($value);
}

$contentData[self::getFieldColumnName($field)] = $value;
$event = new ProcessFieldValueEvent($field, $value);
Event::trigger(self::class, self::EVENT_PROCESS_FIELD_VALUE, $event);

$contentData[self::getFieldColumnName($field)] = $event->getValue();
}

$contentTable = self::getContentTableName($this->getForm());
Expand Down Expand Up @@ -521,6 +526,15 @@ public static function actions(string $source): array
return $event->actions;
}

public function getFieldCollection(): FieldCollection
{
if (null === $this->fieldCollection && $this->getForm()) {
$this->fieldCollection = Freeform::getInstance()->fields->getFieldCollection($this->getForm());
}

return $this->fieldCollection;
}

protected static function defineSources(string $context = null): array
{
static $sources;
Expand Down Expand Up @@ -771,15 +785,6 @@ private function generateToken(): void
$this->token = CryptoHelper::getUniqueToken(self::OPT_IN_DATA_TOKEN_LENGTH);
}

private function getFieldCollection(): FieldCollection
{
if (null === $this->fieldCollection && $this->getForm()) {
$this->fieldCollection = Freeform::getInstance()->fields->getFieldCollection($this->getForm());
}

return $this->fieldCollection;
}

private function getNewIncrementalId(): int
{
$maxIncrementalId = (int) (new Query())
Expand Down
29 changes: 29 additions & 0 deletions packages/plugin/src/Events/Submissions/ProcessFieldValueEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Solspace\Freeform\Events\Submissions;

use craft\events\CancelableEvent;
use Solspace\Freeform\Fields\FieldInterface;

class ProcessFieldValueEvent extends CancelableEvent
{
public function __construct(private FieldInterface $field, private mixed $value)
{
parent::__construct();
}

public function getField(): FieldInterface
{
return $this->field;
}

public function getValue(): mixed
{
return $this->value;
}

public function setValue(mixed $value): void
{
$this->value = $value;
}
}
Loading