validator for ExpenseMailbox property

This commit is contained in:
paulwer 2023-12-15 18:15:55 +01:00
parent dd9727a701
commit 36279be694
10 changed files with 471 additions and 352 deletions

View File

@ -11,10 +11,17 @@
namespace App\Helpers\Mail\Webhook; namespace App\Helpers\Mail\Webhook;
use App\Models\Company;
interface BaseWebhookHandler interface BaseWebhookHandler
{ {
public function process() public function process()
{ {
} }
protected function matchCompany(string $email)
{
return Company::where("expense_mailbox", $email)->first();
}
} }

View File

@ -11,12 +11,21 @@
namespace App\Helpers\Mail\Webhook\Postmark; namespace App\Helpers\Mail\Webhook\Postmark;
use App\Factory\ExpenseFactory;
use App\Helpers\Mail\Webhook\BaseWebhookHandler; use App\Helpers\Mail\Webhook\BaseWebhookHandler;
interface PostmarkWebhookHandler extends BaseWebhookHandler interface PostmarkWebhookHandler extends BaseWebhookHandler
{ {
public function process() public function process($data)
{ {
$email = '';
$company = $this->matchCompany($email);
if (!$company)
return false;
$expense = ExpenseFactory::create($company->id, $company->owner()->id);
} }
} }

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\Company;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Company\ValidCompanyQuantity; use App\Http\ValidationRules\Company\ValidCompanyQuantity;
use App\Http\ValidationRules\Company\ValidExpenseMailbox;
use App\Http\ValidationRules\Company\ValidSubdomain; use App\Http\ValidationRules\Company\ValidSubdomain;
use App\Http\ValidationRules\ValidSettingsRule; use App\Http\ValidationRules\ValidSettingsRule;
use App\Models\Company; use App\Models\Company;
@ -28,7 +29,7 @@ class StoreCompanyRequest extends Request
* *
* @return bool * @return bool
*/ */
public function authorize() : bool public function authorize(): bool
{ {
/** @var \App\Models\User auth()->user */ /** @var \App\Models\User auth()->user */
$user = auth()->user(); $user = auth()->user();
@ -55,6 +56,8 @@ class StoreCompanyRequest extends Request
} }
} }
$rules['expense_mailbox'] = new ValidExpenseMailbox($this->company->key, $this->company->account->isPaid() && $this->company->account->plan == 'enterprise'); // @turbo124 check if this is right
return $rules; return $rules;
} }

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\Company;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Company\ValidExpenseMailbox;
use App\Http\ValidationRules\Company\ValidSubdomain; use App\Http\ValidationRules\Company\ValidSubdomain;
use App\Http\ValidationRules\ValidSettingsRule; use App\Http\ValidationRules\ValidSettingsRule;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -35,7 +36,7 @@ class UpdateCompanyRequest extends Request
* *
* @return bool * @return bool
*/ */
public function authorize() : bool public function authorize(): bool
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth()->user();
@ -67,6 +68,8 @@ class UpdateCompanyRequest extends Request
$rules['subdomain'] = ['nullable', 'regex:/^[a-zA-Z0-9.-]+[a-zA-Z0-9]$/', new ValidSubdomain()]; $rules['subdomain'] = ['nullable', 'regex:/^[a-zA-Z0-9.-]+[a-zA-Z0-9]$/', new ValidSubdomain()];
} }
$rules['expense_mailbox'] = new ValidExpenseMailbox($this->company->key, $this->company->account->isPaid() && $this->company->account->plan == 'enterprise'); // @turbo124 check if this is right
return $rules; return $rules;
} }
@ -80,14 +83,14 @@ class UpdateCompanyRequest extends Request
} }
if (array_key_exists('settings', $input)) { if (array_key_exists('settings', $input)) {
$input['settings'] = (array)$this->filterSaveableSettings($input['settings']); $input['settings'] = (array) $this->filterSaveableSettings($input['settings']);
} }
if(array_key_exists('subdomain', $input) && $this->company->subdomain == $input['subdomain']) { if (array_key_exists('subdomain', $input) && $this->company->subdomain == $input['subdomain']) {
unset($input['subdomain']); unset($input['subdomain']);
} }
if(array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) { if (array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) {
unset($input['e_invoice_certificate_passphrase']); unset($input['e_invoice_certificate_passphrase']);
} }
@ -115,17 +118,17 @@ class UpdateCompanyRequest extends Request
} }
if (isset($settings['email_style_custom'])) { if (isset($settings['email_style_custom'])) {
$settings['email_style_custom'] = str_replace(['{{','}}'], ['',''], $settings['email_style_custom']); $settings['email_style_custom'] = str_replace(['{{', '}}'], ['', ''], $settings['email_style_custom']);
} }
if (! $account->isFreeHostedClient()) { if (!$account->isFreeHostedClient()) {
return $settings; return $settings;
} }
$saveable_casts = CompanySettings::$free_plan_casts; $saveable_casts = CompanySettings::$free_plan_casts;
foreach ($settings as $key => $value) { foreach ($settings as $key => $value) {
if (! array_key_exists($key, $saveable_casts)) { if (!array_key_exists($key, $saveable_casts)) {
unset($settings->{$key}); unset($settings->{$key});
} }
} }
@ -137,7 +140,7 @@ class UpdateCompanyRequest extends Request
{ {
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$url = str_replace('http://', '', $url); $url = str_replace('http://', '', $url);
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme.$url : $url; $url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme . $url : $url;
} }
return rtrim($url, '/'); return rtrim($url, '/');

View File

@ -0,0 +1,64 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\Company;
use App\Libraries\MultiDB;
use App\Utils\Ninja;
use Illuminate\Contracts\Validation\Rule;
/**
* Class ValidCompanyQuantity.
*/
class ValidExpenseMailbox implements Rule
{
private $validated_schema = false;
private $company_key = false;
private $isEnterprise = false;
public function __construct(string $company_key, bool $isEnterprise = false)
{
$this->company_key = $company_key;
$this->isEnterprise = $isEnterprise;
}
public function passes($attribute, $value)
{
if (empty($value)) {
return true;
}
// early return, if we dont have any additional validation
if (!config('ninja.inbound_expense.webhook.mailbox_schema') && !(Ninja::isHosted() && config('ninja.inbound_expense.webhook.mailbox_schema_enterprise'))) {
$this->validated_schema = true;
return MultiDB::checkExpenseMailboxAvailable($value);
}
// Validate Schema
$validated = !config('ninja.inbound_expense.webhook.mailbox_schema') || (preg_match(config('ninja.inbound_expense.webhook.mailbox_schema'), $value) && (!config('ninja.inbound_expense.webhook.mailbox_schema_hascompanykey') || str_contains($value, $this->company_key))) ? true : false;
$validated_enterprise = !config('ninja.inbound_expense.webhook.mailbox_schema_enterprise') || (Ninja::isHosted() && $this->isEnterprise && preg_match(config('ninja.inbound_expense.webhook.mailbox_schema_enterprise'), $value));
if (!$validated && !$validated_enterprise)
return false;
$this->validated_schema = true;
return MultiDB::checkExpenseMailboxAvailable($value);
}
/**
* @return string
*/
public function message()
{
return $this->validated_schema ? ctrans('texts.expense_mailbox_taken') : ctrans('texts.expense_mailbox_invalid');
}
}

View File

@ -71,18 +71,20 @@ class MultiDB
'socket', 'socket',
]; ];
private static $protected_expense_mailboxes = [];
/** /**
* @return array * @return array
*/ */
public static function getDbs() : array public static function getDbs(): array
{ {
return self::$dbs; return self::$dbs;
} }
public static function checkDomainAvailable($subdomain) : bool public static function checkDomainAvailable($subdomain): bool
{ {
if (! config('ninja.db.multi_db_enabled')) { if (!config('ninja.db.multi_db_enabled')) {
return Company::whereSubdomain($subdomain)->count() == 0; return Company::whereSubdomain($subdomain)->count() == 0;
} }
@ -105,9 +107,35 @@ class MultiDB
return true; return true;
} }
public static function checkUserEmailExists($email) : bool public static function checkExpenseMailboxAvailable($expense_mailbox): bool
{ {
if (! config('ninja.db.multi_db_enabled')) {
if (!config('ninja.db.multi_db_enabled')) {
return Company::where("expense_mailbox", $expense_mailbox)->withTrashed()->exists();
}
if (in_array($expense_mailbox, self::$protected_expense_mailboxes)) {
return false;
}
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if (Company::on($db)->where("expense_mailbox", $expense_mailbox)->withTrashed()->exists()) {
self::setDb($current_db);
return false;
}
}
self::setDb($current_db);
return true;
}
public static function checkUserEmailExists($email): bool
{
if (!config('ninja.db.multi_db_enabled')) {
return User::where(['email' => $email])->withTrashed()->exists(); return User::where(['email' => $email])->withTrashed()->exists();
} // true >= 1 emails found / false -> == emails found } // true >= 1 emails found / false -> == emails found
@ -139,7 +167,7 @@ class MultiDB
* @param string $company_key The company key * @param string $company_key The company key
* @return bool True|False * @return bool True|False
*/ */
public static function checkUserAndCompanyCoExist($email, $company_key) :bool public static function checkUserAndCompanyCoExist($email, $company_key): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -166,9 +194,9 @@ class MultiDB
* @param array $data * @param array $data
* @return User|null * @return User|null
*/ */
public static function hasUser(array $data) : ?User public static function hasUser(array $data): ?User
{ {
if (! config('ninja.db.multi_db_enabled')) { if (!config('ninja.db.multi_db_enabled')) {
return User::where($data)->withTrashed()->first(); return User::where($data)->withTrashed()->first();
} }
@ -190,9 +218,9 @@ class MultiDB
* @param string $email * @param string $email
* @return ClientContact|null * @return ClientContact|null
*/ */
public static function hasContact(string $email) : ?ClientContact public static function hasContact(string $email): ?ClientContact
{ {
if (! config('ninja.db.multi_db_enabled')) { if (!config('ninja.db.multi_db_enabled')) {
return ClientContact::where('email', $email)->withTrashed()->first(); return ClientContact::where('email', $email)->withTrashed()->first();
} }
@ -217,9 +245,9 @@ class MultiDB
* @param array $search * @param array $search
* @return ClientContact|null * @return ClientContact|null
*/ */
public static function findContact(array $search) : ?ClientContact public static function findContact(array $search): ?ClientContact
{ {
if (! config('ninja.db.multi_db_enabled')) { if (!config('ninja.db.multi_db_enabled')) {
return ClientContact::where($search)->first(); return ClientContact::where($search)->first();
} }
@ -240,7 +268,7 @@ class MultiDB
return null; return null;
} }
public static function contactFindAndSetDb($token) :bool public static function contactFindAndSetDb($token): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -257,7 +285,7 @@ class MultiDB
return false; return false;
} }
public static function userFindAndSetDb($email) : bool public static function userFindAndSetDb($email): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -275,7 +303,7 @@ class MultiDB
return false; return false;
} }
public static function documentFindAndSetDb($hash) : bool public static function documentFindAndSetDb($hash): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -293,7 +321,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDb($token) :bool public static function findAndSetDb($token): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -310,7 +338,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDbByCompanyKey($company_key) :bool public static function findAndSetDbByCompanyKey($company_key): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -327,7 +355,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDbByCompanyId($company_id) :?Company public static function findAndSetDbByCompanyId($company_id): ?Company
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -344,7 +372,7 @@ class MultiDB
return null; return null;
} }
public static function findAndSetDbByShopifyName($shopify_name) :?Company public static function findAndSetDbByShopifyName($shopify_name): ?Company
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -361,7 +389,7 @@ class MultiDB
return null; return null;
} }
public static function findAndSetDbByAccountKey($account_key) :bool public static function findAndSetDbByAccountKey($account_key): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -378,7 +406,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDbByInappTransactionId($transaction_id) :bool public static function findAndSetDbByInappTransactionId($transaction_id): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -396,7 +424,7 @@ class MultiDB
} }
public static function findAndSetDbByContactKey($contact_key) :bool public static function findAndSetDbByContactKey($contact_key): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -413,7 +441,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDbByVendorContactKey($contact_key) :bool public static function findAndSetDbByVendorContactKey($contact_key): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -430,7 +458,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDbByClientHash($client_hash) :bool public static function findAndSetDbByClientHash($client_hash): bool
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -447,7 +475,7 @@ class MultiDB
return false; return false;
} }
public static function findAndSetDbByClientId($client_id) :?Client public static function findAndSetDbByClientId($client_id): ?Client
{ {
$current_db = config('database.default'); $current_db = config('database.default');
@ -466,7 +494,7 @@ class MultiDB
public static function findAndSetDbByDomain($query_array) public static function findAndSetDbByDomain($query_array)
{ {
if (! config('ninja.db.multi_db_enabled')) { if (!config('ninja.db.multi_db_enabled')) {
return Company::where($query_array)->first(); return Company::where($query_array)->first();
} }
@ -487,7 +515,7 @@ class MultiDB
public static function findAndSetDbByInvitation($entity, $invitation_key) public static function findAndSetDbByInvitation($entity, $invitation_key)
{ {
$class = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; $class = 'App\Models\\' . ucfirst(Str::camel($entity)) . 'Invitation';
$current_db = config('database.default'); $current_db = config('database.default');
foreach (self::$dbs as $db) { foreach (self::$dbs as $db) {
@ -507,9 +535,9 @@ class MultiDB
* @param string $phone * @param string $phone
* @return bool * @return bool
*/ */
public static function hasPhoneNumber(string $phone) : bool public static function hasPhoneNumber(string $phone): bool
{ {
if (! config('ninja.db.multi_db_enabled')) { if (!config('ninja.db.multi_db_enabled')) {
return Account::where('account_sms_verification_number', $phone)->where('account_sms_verified', true)->exists(); return Account::where('account_sms_verification_number', $phone)->where('account_sms_verified', true)->exists();
} }
@ -548,7 +576,7 @@ class MultiDB
$string .= $consonants[rand(0, 19)]; $string .= $consonants[rand(0, 19)];
$string .= $vowels[rand(0, 4)]; $string .= $vowels[rand(0, 4)];
} }
} while (! self::checkDomainAvailable($string)); } while (!self::checkDomainAvailable($string));
self::setDb($current_db); self::setDb($current_db);
@ -559,7 +587,7 @@ class MultiDB
* @param $database * @param $database
* @return void * @return void
*/ */
public static function setDB(string $database) : void public static function setDB(string $database): void
{ {
/* This will set the database connection for the request */ /* This will set the database connection for the request */
config(['database.default' => $database]); config(['database.default' => $database]);

View File

@ -90,7 +90,7 @@ class AccountTransformer extends EntityTransformer
'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap, 'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap,
'trial_days_left' => Ninja::isHosted() ? (int) $account->getTrialDays() : 0, 'trial_days_left' => Ninja::isHosted() ? (int) $account->getTrialDays() : 0,
'account_sms_verified' => (bool) $account->account_sms_verified, 'account_sms_verified' => (bool) $account->account_sms_verified,
'has_iap_plan' => (bool)$account->inapp_transaction_id, 'has_iap_plan' => (bool) $account->inapp_transaction_id,
'tax_api_enabled' => (bool) config('services.tax.zip_tax.key') ? true : false 'tax_api_enabled' => (bool) config('services.tax.zip_tax.key') ? true : false
]; ];

View File

@ -146,7 +146,7 @@ class CompanyTransformer extends EntityTransformer
'enabled_modules' => (int) $company->enabled_modules, 'enabled_modules' => (int) $company->enabled_modules,
'updated_at' => (int) $company->updated_at, 'updated_at' => (int) $company->updated_at,
'archived_at' => (int) $company->deleted_at, 'archived_at' => (int) $company->deleted_at,
'created_at' =>(int) $company->created_at, 'created_at' => (int) $company->created_at,
'slack_webhook_url' => (string) $company->slack_webhook_url, 'slack_webhook_url' => (string) $company->slack_webhook_url,
'google_analytics_url' => (string) $company->google_analytics_key, //@deprecate 1-2-2021 'google_analytics_url' => (string) $company->google_analytics_key, //@deprecate 1-2-2021
'google_analytics_key' => (string) $company->google_analytics_key, 'google_analytics_key' => (string) $company->google_analytics_key,
@ -158,7 +158,7 @@ class CompanyTransformer extends EntityTransformer
'is_large' => (bool) $this->isLarge($company), 'is_large' => (bool) $this->isLarge($company),
'is_disabled' => (bool) $company->is_disabled, 'is_disabled' => (bool) $company->is_disabled,
'enable_shop_api' => (bool) $company->enable_shop_api, 'enable_shop_api' => (bool) $company->enable_shop_api,
'mark_expenses_invoiceable'=> (bool) $company->mark_expenses_invoiceable, 'mark_expenses_invoiceable' => (bool) $company->mark_expenses_invoiceable,
'mark_expenses_paid' => (bool) $company->mark_expenses_paid, 'mark_expenses_paid' => (bool) $company->mark_expenses_paid,
'invoice_expense_documents' => (bool) $company->invoice_expense_documents, 'invoice_expense_documents' => (bool) $company->invoice_expense_documents,
'invoice_task_timelog' => (bool) $company->invoice_task_timelog, 'invoice_task_timelog' => (bool) $company->invoice_task_timelog,
@ -168,10 +168,10 @@ class CompanyTransformer extends EntityTransformer
'use_credits_payment' => 'always', // @deprecate 1-2-2021 'use_credits_payment' => 'always', // @deprecate 1-2-2021
'default_task_is_date_based' => (bool) $company->default_task_is_date_based, 'default_task_is_date_based' => (bool) $company->default_task_is_date_based,
'enable_product_discount' => (bool) $company->enable_product_discount, 'enable_product_discount' => (bool) $company->enable_product_discount,
'calculate_expense_tax_by_amount' =>(bool) $company->calculate_expense_tax_by_amount, 'calculate_expense_tax_by_amount' => (bool) $company->calculate_expense_tax_by_amount,
'hide_empty_columns_on_pdf' => false, // @deprecate 1-2-2021 'hide_empty_columns_on_pdf' => false, // @deprecate 1-2-2021
'expense_inclusive_taxes' => (bool) $company->expense_inclusive_taxes, 'expense_inclusive_taxes' => (bool) $company->expense_inclusive_taxes,
'expense_amount_is_pretax' =>(bool) true, //@deprecate 1-2-2021 'expense_amount_is_pretax' => (bool) true, //@deprecate 1-2-2021
'oauth_password_required' => (bool) $company->oauth_password_required, 'oauth_password_required' => (bool) $company->oauth_password_required,
'session_timeout' => (int) $company->session_timeout, 'session_timeout' => (int) $company->session_timeout,
'default_password_timeout' => (int) $company->default_password_timeout, 'default_password_timeout' => (int) $company->default_password_timeout,
@ -204,6 +204,7 @@ class CompanyTransformer extends EntityTransformer
'invoice_task_project_header' => (bool) $company->invoice_task_project_header, 'invoice_task_project_header' => (bool) $company->invoice_task_project_header,
'invoice_task_item_description' => (bool) $company->invoice_task_item_description, 'invoice_task_item_description' => (bool) $company->invoice_task_item_description,
'origin_tax_data' => $company->origin_tax_data ?: new \stdClass, 'origin_tax_data' => $company->origin_tax_data ?: new \stdClass,
'expense_mailbox' => $company->expense_mailbox,
]; ];
} }

View File

@ -238,7 +238,9 @@ return [
], ],
'webhook' => [ 'webhook' => [
'mailbox_template' => env('INBOUND_EXPENSE_WEBHOOK_MAILBOXTEMPLATE', null), 'mailbox_template' => env('INBOUND_EXPENSE_WEBHOOK_MAILBOXTEMPLATE', null),
'mailbox_template_enterprise' => env('INBOUND_EXPENSE_WEBHOOK_MAILBOXTEMPLATE_ENTERPRISE', '{{input}}@expense.invoicing.co'), 'mailbox_schema' => env('INBOUND_EXPENSE_WEBHOOK_MAILBOX_SCHEMA', null),
'mailbox_schema_hascompanykey' => env('INBOUND_EXPENSE_WEBHOOK_MAILBOX_SCHEMA_HASCOMPANYKEY', false),
'mailbox_schema_enterprise' => env('INBOUND_EXPENSE_WEBHOOK_MAILBOX_SCHEMA_ENTERPRISE', '.*@expense\.invoicing\.co$'),
], ],
], ],
]; ];

View File

@ -2534,6 +2534,8 @@ $lang = array(
'local_storage_required' => 'Error: local storage is not available.', 'local_storage_required' => 'Error: local storage is not available.',
'your_password_reset_link' => 'Your Password Reset Link', 'your_password_reset_link' => 'Your Password Reset Link',
'subdomain_taken' => 'The subdomain is already in use', 'subdomain_taken' => 'The subdomain is already in use',
'expense_mailbox_taken' => 'The mailbox is already in use',
'expense_mailbox_invalid' => 'The mailbox does not match the required schema',
'client_login' => 'Client Login', 'client_login' => 'Client Login',
'converted_amount' => 'Converted Amount', 'converted_amount' => 'Converted Amount',
'default' => 'Default', 'default' => 'Default',