diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index fe0702d9588a..00f47d844266 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -39,6 +39,9 @@ use App\Services\AuthService; use App\Services\PaymentService; use App\Http\Requests\UpdateAccountRequest; +use App\Http\Requests\SaveClientPortalSettings; +use App\Http\Requests\SaveEmailSettings; + /** * Class AccountController */ @@ -714,14 +717,10 @@ class AccountController extends BaseController return AccountController::export(); } elseif ($section === ACCOUNT_INVOICE_SETTINGS) { return AccountController::saveInvoiceSettings(); - } elseif ($section === ACCOUNT_EMAIL_SETTINGS) { - return AccountController::saveEmailSettings(); } elseif ($section === ACCOUNT_INVOICE_DESIGN) { return AccountController::saveInvoiceDesign(); } elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) { return AccountController::saveCustomizeDesign(); - } elseif ($section === ACCOUNT_CLIENT_PORTAL) { - return AccountController::saveClientPortal(); } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { return AccountController::saveEmailTemplates(); } elseif ($section === ACCOUNT_PRODUCTS) { @@ -789,53 +788,27 @@ class AccountController extends BaseController /** * @return \Illuminate\Http\RedirectResponse */ - private function saveClientPortal() + public function saveClientPortalSettings(SaveClientPortalSettings $request) { - $account = Auth::user()->account; - $account->fill(Input::all()); - - // Only allowed for pro Invoice Ninja users or white labeled self-hosted users - if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { - $input_css = Input::get('client_view_css'); - if (Utils::isNinja()) { - // Allow referencing the body element - $input_css = preg_replace('/(? - // - - // Create a new configuration object - $config = \HTMLPurifier_Config::createDefault(); - $config->set('Filter.ExtractStyleBlocks', true); - $config->set('CSS.AllowImportant', true); - $config->set('CSS.AllowTricky', true); - $config->set('CSS.Trusted', true); - - // Create a new purifier instance - $purifier = new \HTMLPurifier($config); - - // Wrap our CSS in style tags and pass to purifier. - // we're not actually interested in the html response though - $html = $purifier->purify(''); - - // The "style" blocks are stored seperately - $output_css = $purifier->context->get('StyleBlocks'); - - // Get the first style block - $sanitized_css = count($output_css) ? $output_css[0] : ''; - } else { - $sanitized_css = $input_css; - } - - $account->client_view_css = $sanitized_css; - } - + $account = $request->user()->account; + $account->fill($request->all()); $account->save(); - Session::flash('message', trans('texts.updated_settings')); + return redirect('settings/' . ACCOUNT_CLIENT_PORTAL) + ->with('message', trans('texts.updated_settings')); + } - return Redirect::to('settings/'.ACCOUNT_CLIENT_PORTAL); + /** + * @return $this|\Illuminate\Http\RedirectResponse + */ + public function saveEmailSettings(SaveEmailSettings $request) + { + $account = $request->user()->account; + $account->fill($request->all()); + $account->save(); + + return redirect('settings/' . ACCOUNT_EMAIL_SETTINGS) + ->with('message', trans('texts.updated_settings')); } /** @@ -905,74 +878,6 @@ class AccountController extends BaseController return Redirect::to('settings/'.ACCOUNT_PRODUCTS); } - /** - * @return $this|\Illuminate\Http\RedirectResponse - */ - private function saveEmailSettings() - { - if (Auth::user()->account->hasFeature(FEATURE_CUSTOM_EMAILS)) { - $user = Auth::user(); - $subdomain = null; - $iframeURL = null; - $rules = []; - - if (Input::get('custom_link') == 'subdomain') { - $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); - if (Utils::isNinja()) { - $exclude = [ - 'www', - 'app', - 'mail', - 'admin', - 'blog', - 'user', - 'contact', - 'payment', - 'payments', - 'billing', - 'invoice', - 'business', - 'owner', - 'info', - 'ninja', - 'docs', - 'doc', - 'documents' - ]; - $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id|not_in:" . implode(',', $exclude); - } - } else { - $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); - $iframeURL = rtrim($iframeURL, '/'); - } - - $validator = Validator::make(Input::all(), $rules); - - if ($validator->fails()) { - return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS) - ->withErrors($validator) - ->withInput(); - } else { - $account = Auth::user()->account; - $account->subdomain = $subdomain; - $account->iframe_url = $iframeURL; - $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; - $account->document_email_attachment = Input::get('document_email_attachment') ? true : false; - $account->email_design_id = Input::get('email_design_id'); - $account->bcc_email = Input::get('bcc_email'); - - if (Utils::isNinja()) { - $account->enable_email_markup = Input::get('enable_email_markup') ? true : false; - } - - $account->save(); - Session::flash('message', trans('texts.updated_settings')); - } - } - - return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS); - } - /** * @return $this|\Illuminate\Http\RedirectResponse */ diff --git a/app/Http/Requests/SaveClientPortalSettings.php b/app/Http/Requests/SaveClientPortalSettings.php new file mode 100644 index 000000000000..3c2485e7d7ef --- /dev/null +++ b/app/Http/Requests/SaveClientPortalSettings.php @@ -0,0 +1,58 @@ +user()->is_admin && $this->user()->isPro(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = []; + + if ($this->custom_link == 'subdomain' && Utils::isNinja()) { + $rules['subdomain'] = "unique:accounts,subdomain,{$this->user()->account_id},id|valid_subdomain"; + } + + return $rules; + } + + public function sanitize() + { + $input = $this->all(); + + if ($this->client_view_css && Utils::isNinja()) { + $input['client_view_css'] = HTMLUtils::sanitize($this->client_view_css); + } + + if ($this->custom_link == 'subdomain') { + $subdomain = substr(strtolower($input['subdomain']), 0, MAX_SUBDOMAIN_LENGTH); + $input['subdomain'] = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $subdomain); + $input['iframe_url'] = null; + } else { + $iframeURL = substr(strtolower($input['iframe_url']), 0, MAX_IFRAME_URL_LENGTH); + $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', $iframeURL); + $input['iframe_url'] = rtrim($iframeURL, '/'); + $input['subdomain'] = null; + } + + $this->replace($input); + + return $this->all(); + } + +} diff --git a/app/Http/Requests/SaveEmailSettings.php b/app/Http/Requests/SaveEmailSettings.php new file mode 100644 index 000000000000..84f8c5baf8d0 --- /dev/null +++ b/app/Http/Requests/SaveEmailSettings.php @@ -0,0 +1,27 @@ +user()->is_admin && $this->user()->isPro(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'bcc_email' => 'email', + ]; + } + +} diff --git a/app/Http/routes.php b/app/Http/routes.php index ea11794dc264..ae0c55de3b97 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -244,6 +244,8 @@ Route::group([ Route::post('tax_rates/bulk', 'TaxRateController@bulk'); Route::get('settings/email_preview', 'AccountController@previewEmail'); + Route::post('settings/client_portal', 'AccountController@saveClientPortalSettings'); + Route::post('settings/email_settings', 'AccountController@saveEmailSettings'); Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy'); Route::get('settings/data_visualizations', 'ReportController@d3'); Route::get('settings/reports', 'ReportController@showReports'); diff --git a/app/Libraries/HTMLUtils.php b/app/Libraries/HTMLUtils.php new file mode 100644 index 000000000000..cb0ce92c02ec --- /dev/null +++ b/app/Libraries/HTMLUtils.php @@ -0,0 +1,37 @@ + + // + + // Create a new configuration object + $config = HTMLPurifier_Config::createDefault(); + $config->set('Filter.ExtractStyleBlocks', true); + $config->set('CSS.AllowImportant', true); + $config->set('CSS.AllowTricky', true); + $config->set('CSS.Trusted', true); + + // Create a new purifier instance + $purifier = new HTMLPurifier($config); + + // Wrap our CSS in style tags and pass to purifier. + // we're not actually interested in the html response though + $purifier->purify(''); + + // The "style" blocks are stored seperately + $css = $purifier->context->get('StyleBlocks'); + + // Get the first style block + return count($css) ? $css[0] : ''; + } +} diff --git a/app/Models/Account.php b/app/Models/Account.php index 790ede8c098c..56c1bd2013f5 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -82,6 +82,13 @@ class Account extends Eloquent 'show_accept_quote_terms', 'require_invoice_signature', 'require_quote_signature', + 'pdf_email_attachment', + 'document_email_attachment', + 'email_design_id', + 'bcc_email', + 'enable_email_markup', + 'subdomain', + 'iframe_url', ]; /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6a4514cb53b7..4efe0990f81a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -198,6 +198,9 @@ class AppServiceProvider extends ServiceProvider return $total <= MAX_INVOICE_AMOUNT; }); + Validator::extend('valid_subdomain', function($attribute, $value, $parameters) { + return ! in_array($value, ['www', 'app','mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner', 'info', 'ninja', 'docs', 'doc', 'documents']); + }); } /** diff --git a/config/app.php b/config/app.php index 84fabfb28d5d..9d9881dfda23 100644 --- a/config/app.php +++ b/config/app.php @@ -222,7 +222,6 @@ return [ 'View' => 'Illuminate\Support\Facades\View', // Added Class Aliases - 'Utils' => 'App\Libraries\Utils', 'Form' => 'Collective\Html\FormFacade', 'HTML' => 'Collective\Html\HtmlFacade', 'SSH' => 'Illuminate\Support\Facades\SSH', @@ -261,6 +260,9 @@ return [ 'Crawler' => 'Jaybizzle\LaravelCrawlerDetect\Facades\LaravelCrawlerDetect', 'Updater' => Codedge\Updater\UpdaterFacade::class, 'Module' => Nwidart\Modules\Facades\Module::class, + + 'Utils' => App\Libraries\Utils::class, + 'HTMLUtils' => App\Libraries\HTMLUtils::class, ], ]; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 57b176ae9872..541ca8fad158 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -2312,6 +2312,8 @@ $LANG = array( 'tax_invoice' => 'Tax Invoice', 'emailed_invoices' => 'Successfully emailed invoices', 'emailed_quotes' => 'Successfully emailed quotes', + 'website_url' => 'Website URL', + ); return $LANG; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index d17016b2b53f..b47a12ef4e4c 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -76,6 +76,7 @@ return array( "has_counter" => "The value must contain {\$counter}", "valid_contacts" => "The contact must have either an email or name", "valid_invoice_items" => "The invoice exceeds the maximum amount", + "valid_subdomain" => "The subdomain is restricted", /* |-------------------------------------------------------------------------- diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php index c859330a92cc..960aaa98c742 100644 --- a/resources/views/accounts/client_portal.blade.php +++ b/resources/views/accounts/client_portal.blade.php @@ -19,11 +19,14 @@ @parent {!! Former::open_for_files() -->addClass('warn-on-exit') !!} + ->rules([ + 'iframe_url' => 'url', + ]) + ->addClass('warn-on-exit') !!} +{!! Former::populate($account) !!} {!! Former::populateField('enable_client_portal', intval($account->enable_client_portal)) !!} {!! Former::populateField('enable_client_portal_dashboard', intval($account->enable_client_portal_dashboard)) !!} -{!! Former::populateField('client_view_css', $client_view_css) !!} {!! Former::populateField('enable_portal_password', intval($enable_portal_password)) !!} {!! Former::populateField('send_portal_password', intval($send_portal_password)) !!} {!! Former::populateField('enable_buy_now_buttons', intval($account->enable_buy_now_buttons)) !!} @@ -47,16 +50,41 @@