diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index bb2c82d8ff..5c0daa715c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -152,6 +152,8 @@ class AccountController extends BaseController return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { return self::showInvoiceDesign($section); + } elseif ($section == ACCOUNT_CLIENT_VIEW_STYLING) { + return self::showClientViewStyling(); } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { return self::showTemplates(); } elseif ($section === ACCOUNT_PRODUCTS) { @@ -367,6 +369,29 @@ class AccountController extends BaseController return View::make("accounts.{$section}", $data); } + private function showClientViewStyling() + { + $account = Auth::user()->account->load('country'); + $css = $account->client_view_css ? $account->client_view_css : ''; + + if(Utils::isNinja() && $css){ + // Unescape the CSS for display purposes + $css = str_replace( + array('\3C ', '\3E ', '\26 '), + array('<', '>', '&'), + $css + ); + } + + $data = [ + 'client_view_css' => $css, + 'title' => trans("texts.client_view_styling"), + 'section' => ACCOUNT_CLIENT_VIEW_STYLING + ]; + + return View::make("accounts.client_view_styling", $data); + } + private function showTemplates() { $account = Auth::user()->account->load('country'); @@ -408,6 +433,8 @@ class AccountController extends BaseController return AccountController::saveInvoiceDesign(); } elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) { return AccountController::saveCustomizeDesign(); + } elseif ($section === ACCOUNT_CLIENT_VIEW_STYLING) { + return AccountController::saveClientViewStyling(); } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { return AccountController::saveEmailTemplates(); } elseif ($section === ACCOUNT_PRODUCTS) { @@ -430,6 +457,53 @@ class AccountController extends BaseController return Redirect::to('settings/' . ACCOUNT_CUSTOMIZE_DESIGN); } + private function saveClientViewStyling() { + // Only allowed for pro Invoice Ninja users or white labeled self-hosted users + if ((Utils::isNinja() && Auth::user()->account->isPro()) || Auth::user()->account->isWhiteLabel()) { + + $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 = $output_css[0]; + } + else{ + $sanitized_css = $input_css; + } + + $account = Auth::user()->account; + $account->client_view_css = $sanitized_css; + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + } + + return Redirect::to('settings/' . ACCOUNT_CLIENT_VIEW_STYLING); + } + private function saveEmailTemplates() { if (Auth::user()->account->isPro()) { diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index bb65176232..63719526bd 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -103,6 +103,7 @@ class InvoiceController extends BaseController return response()->view('error', [ 'error' => trans('texts.invoice_not_found'), 'hideHeader' => true, + 'clientViewCSS' => $account->clientViewCSS(), ]); } @@ -154,6 +155,7 @@ class InvoiceController extends BaseController 'showBreadcrumbs' => false, 'hideLogo' => $account->isWhiteLabel(), 'hideHeader' => $account->isNinjaAccount(), + 'clientViewCSS' => $account->clientViewCSS(), 'invoice' => $invoice->hidePrivateFields(), 'invitation' => $invitation, 'invoiceLabels' => $account->getInvoiceLabels(), diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 3cd7d76d35..5cf6823520 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -177,6 +177,7 @@ class PaymentController extends BaseController 'account' => $client->account, 'hideLogo' => $account->isWhiteLabel(), 'hideHeader' => $account->isNinjaAccount(), + 'clientViewCSS' => $account->clientViewCSS(), 'showAddress' => $accountGateway->show_address, ]; diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index e56d5190ac..679e01a299 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -37,6 +37,7 @@ class PublicClientController extends BaseController 'account' => $account, 'client' => $client, 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), ]; return response()->view('invited.dashboard', $data); @@ -81,6 +82,7 @@ class PublicClientController extends BaseController $data = [ 'color' => $color, 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), 'title' => trans('texts.invoices'), 'entityType' => ENTITY_INVOICE, 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']), @@ -110,6 +112,7 @@ class PublicClientController extends BaseController $data = [ 'color' => $color, 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) @@ -145,6 +148,7 @@ class PublicClientController extends BaseController $data = [ 'color' => $color, 'hideLogo' => $account->isWhiteLabel(), + 'clientViewCSS' => $account->clientViewCSS(), 'title' => trans('texts.quotes'), 'entityType' => ENTITY_QUOTE, 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), @@ -168,6 +172,7 @@ class PublicClientController extends BaseController return response()->view('error', [ 'error' => trans('texts.invoice_not_found'), 'hideHeader' => true, + 'clientViewCSS' => $account->clientViewCSS(), ]); } diff --git a/app/Http/routes.php b/app/Http/routes.php index bfae03344f..a310c1135c 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -278,6 +278,7 @@ if (!defined('CONTACT_EMAIL')) { define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); define('ACCOUNT_INVOICE_DESIGN', 'invoice_design'); + define('ACCOUNT_CLIENT_VIEW_STYLING', 'client_view_styling'); define('ACCOUNT_EMAIL_SETTINGS', 'email_settings'); define('ACCOUNT_CHARTS_AND_REPORTS', 'charts_and_reports'); define('ACCOUNT_USER_MANAGEMENT', 'user_management'); diff --git a/app/Models/Account.php b/app/Models/Account.php index 9c35cdea00..e71762687b 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -35,6 +35,7 @@ class Account extends Eloquent public static $advancedSettings = [ ACCOUNT_INVOICE_SETTINGS, ACCOUNT_INVOICE_DESIGN, + ACCOUNT_CLIENT_VIEW_STYLING, ACCOUNT_EMAIL_SETTINGS, ACCOUNT_TEMPLATES_AND_REMINDERS, ACCOUNT_CHARTS_AND_REPORTS, @@ -873,6 +874,14 @@ class Account extends Eloquent { return $this->isPro() && $this->pdf_email_attachment; } + + public function clientViewCSS(){ + if (($this->isNinjaAccount() && $this->isPro()) || $this->isWhiteLabel()) { + return $this->client_view_css; + } + + return null; + } } Account::updated(function ($account) { diff --git a/app/Ninja/Transformers/AccountTransformer.php b/app/Ninja/Transformers/AccountTransformer.php index 3a1d14d76b..eed4743465 100644 --- a/app/Ninja/Transformers/AccountTransformer.php +++ b/app/Ninja/Transformers/AccountTransformer.php @@ -72,6 +72,7 @@ class AccountTransformer extends EntityTransformer 'invoice_taxes' => (bool) $account->invoice_taxes, 'invoice_item_taxes' => (bool) $account->invoice_item_taxes, 'invoice_design_id' => (int) $account->invoice_design_id, + 'client_view_css' => (string) $account->client_view_css, 'work_phone' => $account->work_phone, 'work_email' => $account->work_email, 'language_id' => (int) $account->language_id, diff --git a/composer.json b/composer.json index 2d20588eb4..a7417f9dea 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,9 @@ "collizo4sky/omnipay-wepay": "~1.0", "laracasts/presenter": "dev-master", "jlapp/swaggervel": "master-dev", - "maatwebsite/excel": "~2.0" + "maatwebsite/excel": "~2.0", + "ezyang/htmlpurifier": "~v4.7", + "cerdic/css-tidy": "~v1.5" }, "require-dev": { "phpunit/phpunit": "~4.0", diff --git a/composer.lock b/composer.lock index a9525a03fb..4e4a12f12e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "26f955f9f0cfc85f6ec046f8600b04c7", - "content-hash": "8c51afe64fbaf07534ceaedaddf66206", + "hash": "f3b07ee34ed5086cd684a7207fd3617e", + "content-hash": "7305e2f5d6864894aeb23ac91f3e1fe7", "packages": [ { "name": "agmscode/omnipay-agms", @@ -496,6 +496,39 @@ ], "time": "2015-06-05 14:50:44" }, + { + "name": "cerdic/css-tidy", + "version": "v1.5.5", + "source": { + "type": "git", + "url": "https://github.com/Cerdic/CSSTidy.git", + "reference": "b0c1bda3be5a35da44ba1ac28cc61c67d2ada465" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Cerdic/CSSTidy/zipball/b0c1bda3be5a35da44ba1ac28cc61c67d2ada465", + "reference": "b0c1bda3be5a35da44ba1ac28cc61c67d2ada465", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "." + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "Cédric MORIN", + "email": "changeme@mailinator.com" + } + ], + "description": "CSSTidy is a CSS minifier", + "time": "2015-11-28 21:47:43" + }, { "name": "chumper/datatable", "version": "dev-develop", @@ -1695,6 +1728,50 @@ ], "time": "2015-06-05 13:57:26" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.7.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "ae1828d955112356f7677c465f94f7deb7d27a40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/ae1828d955112356f7677c465f94f7deb7d27a40", + "reference": "ae1828d955112356f7677c465f94f7deb7d27a40", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2015-08-05 01:03:42" + }, { "name": "fotografde/omnipay-checkoutcom", "version": "2.0", diff --git a/database/migrations/2015_12_30_042035_add_client_view_css.php b/database/migrations/2015_12_30_042035_add_client_view_css.php new file mode 100644 index 0000000000..8b654161d6 --- /dev/null +++ b/database/migrations/2015_12_30_042035_add_client_view_css.php @@ -0,0 +1,32 @@ +text('client_view_css')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function ($table) { + $table->dropColumn('client_view_css'); + }); + } + +} diff --git a/readme.md b/readme.md index 4ec5442797..d710247c59 100644 --- a/readme.md +++ b/readme.md @@ -60,6 +60,7 @@ There are two options: * [Troels Liebe Bentsen](https://github.com/tlbdk) * [Jeramy Simpson](https://github.com/JeramyMywork) - [MyWork](https://www.mywork.com.au) * [Sigitas Limontas](https://lt.linkedin.com/in/sigitaslimontas) +* [Joshua Dwire](https://github.com/joshuadwire) - [Some Techie](https://www.sometechie.com) ### Frameworks/Libraries * [laravel/laravel](https://github.com/laravel/laravel) - A PHP Framework For Web Artisans @@ -93,4 +94,6 @@ There are two options: * [simshaun/recurr](https://github.com/simshaun/recurr) - PHP library for working with recurrence rules * [quilljs/quill](https://github.com/quilljs/quill/) - A cross browser rich text editor with an API * [Maatwebsite/Laravel-Excel](https://github.com/Maatwebsite/Laravel-Excel) - An eloquent way of importing and exporting Excel and CSV files for Laravel -* [thephpleague/fractal](https://github.com/thephpleague/fractal) - Output complex, flexible, AJAX/RESTful data structures \ No newline at end of file +* [thephpleague/fractal](https://github.com/thephpleague/fractal) - Output complex, flexible, AJAX/RESTful data structures +* [ezyang/htmlpurifier](https://github.com/ezyang/htmlpurifier) - Standards compliant HTML filter written in PHP +* [cerdic/css-tidy](https://github.com/Cerdic/CSSTidy) - CSSTidy is a CSS minifier diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 6820588eb4..b3814b673d 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -203,7 +203,9 @@ return array( 'client_will_create' => 'client will be created', 'clients_will_create' => 'clients will be created', 'email_settings' => 'Email Settings', + 'client_view_styling' => 'Client View Styling', 'pdf_email_attachment' => 'Attach PDFs', + 'custom_css' => 'Custom CSS', //import CSV data pages 'import_clients' => 'Import Client Data', @@ -474,6 +476,8 @@ return array( 'id_number' => 'ID Number', 'white_label_link' => 'White label', 'white_label_text' => 'Purchase a white label license for $'.WHITE_LABEL_PRICE.' to remove the Invoice Ninja branding from the client portal and help support our project.', + 'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.', + 'white_label_purchase_link' => 'Purchase a white label license', 'white_label_header' => 'White Label', 'bought_white_label' => 'Successfully enabled white label license', 'white_labeled' => 'White labeled', diff --git a/resources/views/accounts/client_view_styling.blade.php b/resources/views/accounts/client_view_styling.blade.php new file mode 100644 index 0000000000..f4c3150a17 --- /dev/null +++ b/resources/views/accounts/client_view_styling.blade.php @@ -0,0 +1,52 @@ +@extends('header') + +@section('head') + @parent + + +@stop + +@section('content') + @parent + + {!! Former::open_for_files() + ->addClass('warn-on-exit') !!} + + {!! Former::populateField('client_view_css', $client_view_css) !!} + + @if (!Utils::isNinja() && !Auth::user()->account->isWhiteLabel()) +
+
+ {!! trans('texts.white_label_custom_css', ['link'=>''.trans('texts.white_label_purchase_link').'']) !!} +
+
+ @endif + + @include('accounts.nav', ['selected' => ACCOUNT_CLIENT_VIEW_STYLING]) + +
+
+ +
+
+

{!! trans('texts.custom_css') !!}

+
+
+ {!! Former::textarea('client_view_css') + ->label(trans('texts.custom_css')) + ->rows(15) + ->autofocus() + ->maxlength(60000) + ->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!} +
+
+
+
+ +
+ {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
+ + {!! Former::close() !!} + +@stop \ No newline at end of file diff --git a/resources/views/master.blade.php b/resources/views/master.blade.php index 6d434ef08c..5ac9e80c71 100644 --- a/resources/views/master.blade.php +++ b/resources/views/master.blade.php @@ -111,7 +111,7 @@ - + @if (isset($_ENV['TAG_MANAGER_KEY']) && $_ENV['TAG_MANAGER_KEY']) diff --git a/resources/views/public/header.blade.php b/resources/views/public/header.blade.php index 61ec01a548..97d22cec85 100644 --- a/resources/views/public/header.blade.php +++ b/resources/views/public/header.blade.php @@ -2,6 +2,9 @@ @section('head') + @if (!empty($clientViewCSS)) + + @endif @stop @section('body')