Refactor client portal and email settings requests

This commit is contained in:
Hillel Coren 2017-01-12 13:52:37 +02:00
parent 6ba3b22ec3
commit b6527cddc3
12 changed files with 264 additions and 216 deletions

View File

@ -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('/(?<![a-z0-9\-\_\#\.])body(?![a-z0-9\-\_])/i', '.body', $input_css);
//
// Inspired by http://stackoverflow.com/a/5209050/1721527, dleavitt <https://stackoverflow.com/users/362110/dleavitt>
//
// 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('<style>'.$input_css.'</style>');
// 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
*/

View File

@ -0,0 +1,58 @@
<?php namespace App\Http\Requests;
use Utils;
use HTMLUtils;
class SaveClientPortalSettings extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->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();
}
}

View File

@ -0,0 +1,27 @@
<?php namespace App\Http\Requests;
class SaveEmailSettings extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->is_admin && $this->user()->isPro();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'bcc_email' => 'email',
];
}
}

View File

@ -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');

View File

@ -0,0 +1,37 @@
<?php namespace App\Libraries;
use HTMLPurifier;
use HTMLPurifier_Config;
class HTMLUtils
{
public static function sanitize($css)
{
// Allow referencing the body element
$css = preg_replace('/(?<![a-z0-9\-\_\#\.])body(?![a-z0-9\-\_])/i', '.body', $css);
//
// Inspired by http://stackoverflow.com/a/5209050/1721527, dleavitt <https://stackoverflow.com/users/362110/dleavitt>
//
// 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('<style>'.$css.'</style>');
// The "style" blocks are stored seperately
$css = $purifier->context->get('StyleBlocks');
// Get the first style block
return count($css) ? $css[0] : '';
}
}

View File

@ -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',
];
/**

View File

@ -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']);
});
}
/**

View File

@ -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,
],
];

View File

@ -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;

View File

@ -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",
/*
|--------------------------------------------------------------------------

View File

@ -19,11 +19,14 @@
@parent
{!! Former::open_for_files()
->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 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.navigation') !!}</h3>
<h3 class="panel-title">{!! trans('texts.settings') !!}</h3>
</div>
<div class="panel-body">
<div class="col-md-10 col-md-offset-1">
{!! Former::inline_radios('custom_invoice_link')
->onchange('onCustomLinkChange()')
->label(trans('texts.website_url'))
->radios([
trans('texts.subdomain') => ['value' => 'subdomain', 'name' => 'custom_link'],
trans('texts.website') => ['value' => 'website', 'name' => 'custom_link'],
])->check($account->iframe_url ? 'website' : 'subdomain') !!}
{{ Former::setOption('capitalize_translations', false) }}
{!! Former::text('subdomain')
->placeholder(trans('texts.www'))
->onchange('onSubdomainChange()')
->addGroupClass('subdomain')
->label(' ')
->help(trans('texts.subdomain_help')) !!}
{!! Former::text('iframe_url')
->placeholder('https://www.example.com/invoice')
->appendIcon('question-sign')
->addGroupClass('iframe_url')
->label(' ')
->help(trans('texts.subdomain_help')) !!}
{!! Former::checkbox('enable_client_portal')
->text(trans('texts.enable'))
->help(trans('texts.enable_client_portal_help'))
->value(1) !!}
</div>
<div class="col-md-10 col-md-offset-1">
{!! Former::checkbox('enable_client_portal_dashboard')
->text(trans('texts.enable'))
->help(trans('texts.enable_client_portal_dashboard_help'))
@ -251,6 +279,38 @@
{!! Former::close() !!}
<div class="modal fade" id="iframeHelpModal" tabindex="-1" role="dialog" aria-labelledby="iframeHelpModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="iframeHelpModalLabel">{{ trans('texts.iframe_url') }}</h4>
</div>
<div class="modal-body">
<p>{{ trans('texts.iframe_url_help1') }}</p>
<pre>&lt;center&gt;
&lt;iframe id="invoiceIFrame" width="100%" height="1200" style="max-width:1000px"&gt;&lt;/iframe&gt;
&lt;center&gt;
&lt;script language="javascript"&gt;
var iframe = document.getElementById('invoiceIFrame');
iframe.src = '{{ rtrim(SITE_URL ,'/') }}/view/'
+ window.location.search.substring(1);
&lt;/script&gt;</pre>
<p>{{ trans('texts.iframe_url_help2') }}</p>
<p><b>{{ trans('texts.iframe_url_help3') }}</b></p>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div>
</div>
</div>
</div>
<script>
var products = {!! $products !!};
@ -320,6 +380,44 @@
}
function onSubdomainChange() {
var input = $('#subdomain');
var val = input.val();
if (!val) return;
val = val.replace(/[^a-zA-Z0-9_\-]/g, '').toLowerCase().substring(0, {{ MAX_SUBDOMAIN_LENGTH }});
input.val(val);
}
function onCustomLinkChange() {
var val = $('input[name=custom_link]:checked').val()
if (val == 'subdomain') {
$('.subdomain').show();
$('.iframe_url').hide();
} else {
$('.subdomain').hide();
$('.iframe_url').show();
}
}
$('.iframe_url .input-group-addon').click(function() {
$('#iframeHelpModal').modal('show');
});
$('.email_design_id .input-group-addon').click(function() {
$('#designHelpModal').modal('show');
});
$(function() {
onCustomLinkChange();
$('#subdomain').change(function() {
$('#iframe_url').val('');
});
$('#iframe_url').change(function() {
$('#subdomain').val('');
});
});
</script>

View File

@ -15,9 +15,9 @@
@include('accounts.nav', ['selected' => ACCOUNT_EMAIL_SETTINGS, 'advanced' => true])
{!! Former::open()->rules([
'iframe_url' => 'url',
'bcc_email' => 'email',
])->addClass('warn-on-exit') !!}
{{ Former::populate($account) }}
{{ Former::populateField('pdf_email_attachment', intval($account->pdf_email_attachment)) }}
{{ Former::populateField('document_email_attachment', intval($account->document_email_attachment)) }}
@ -49,29 +49,6 @@
{{-- Former::select('recurring_hour')->options($recurringHours) --}}
{!! Former::inline_radios('custom_invoice_link')
->onchange('onCustomLinkChange()')
->label(trans('texts.invoice_link'))
->radios([
trans('texts.subdomain') => ['value' => 'subdomain', 'name' => 'custom_link'],
trans('texts.website') => ['value' => 'website', 'name' => 'custom_link'],
])->check($account->iframe_url ? 'website' : 'subdomain') !!}
{{ Former::setOption('capitalize_translations', false) }}
{!! Former::text('subdomain')
->placeholder(trans('texts.www'))
->onchange('onSubdomainChange()')
->addGroupClass('subdomain')
->label(' ')
->help(trans('texts.subdomain_help')) !!}
{!! Former::text('iframe_url')
->placeholder('https://www.example.com/invoice')
->appendIcon('question-sign')
->addGroupClass('iframe_url')
->label(' ')
->help(trans('texts.subdomain_help')) !!}
</div>
</div>
@ -107,36 +84,6 @@
</center>
@endif
<div class="modal fade" id="iframeHelpModal" tabindex="-1" role="dialog" aria-labelledby="iframeHelpModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="iframeHelpModalLabel">{{ trans('texts.iframe_url') }}</h4>
</div>
<div class="modal-body">
<p>{{ trans('texts.iframe_url_help1') }}</p>
<pre>&lt;center&gt;
&lt;iframe id="invoiceIFrame" width="100%" height="1200" style="max-width:1000px"&gt;&lt;/iframe&gt;
&lt;center&gt;
&lt;script language="javascript"&gt;
var iframe = document.getElementById('invoiceIFrame');
iframe.src = '{{ rtrim(SITE_URL ,'/') }}/view/'
+ window.location.search.substring(1);
&lt;/script&gt;</pre>
<p>{{ trans('texts.iframe_url_help2') }}</p>
<p><b>{{ trans('texts.iframe_url_help3') }}</b></p>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="designHelpModal" tabindex="-1" role="dialog" aria-labelledby="designHelpModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
@ -172,45 +119,4 @@
{!! Former::close() !!}
<script type="text/javascript">
function onSubdomainChange() {
var input = $('#subdomain');
var val = input.val();
if (!val) return;
val = val.replace(/[^a-zA-Z0-9_\-]/g, '').toLowerCase().substring(0, {{ MAX_SUBDOMAIN_LENGTH }});
input.val(val);
}
function onCustomLinkChange() {
var val = $('input[name=custom_link]:checked').val()
if (val == 'subdomain') {
$('.subdomain').show();
$('.iframe_url').hide();
} else {
$('.subdomain').hide();
$('.iframe_url').show();
}
}
$('.iframe_url .input-group-addon').click(function() {
$('#iframeHelpModal').modal('show');
});
$('.email_design_id .input-group-addon').click(function() {
$('#designHelpModal').modal('show');
});
$(function() {
onCustomLinkChange();
$('#subdomain').change(function() {
$('#iframe_url').val('');
});
$('#iframe_url').change(function() {
$('#subdomain').val('');
});
});
</script>
@stop