1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Merge pull request #588 from joshuadwire/custom-css

Add custom CSS support
This commit is contained in:
Hillel Coren 2016-01-03 14:45:16 +02:00
commit 56330c1281
15 changed files with 271 additions and 5 deletions

View File

@ -152,6 +152,8 @@ class AccountController extends BaseController
return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); return View::make('accounts.import_export', ['title' => trans('texts.import_export')]);
} elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) {
return self::showInvoiceDesign($section); return self::showInvoiceDesign($section);
} elseif ($section == ACCOUNT_CLIENT_VIEW_STYLING) {
return self::showClientViewStyling();
} elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) {
return self::showTemplates(); return self::showTemplates();
} elseif ($section === ACCOUNT_PRODUCTS) { } elseif ($section === ACCOUNT_PRODUCTS) {
@ -367,6 +369,29 @@ class AccountController extends BaseController
return View::make("accounts.{$section}", $data); 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() private function showTemplates()
{ {
$account = Auth::user()->account->load('country'); $account = Auth::user()->account->load('country');
@ -408,6 +433,8 @@ class AccountController extends BaseController
return AccountController::saveInvoiceDesign(); return AccountController::saveInvoiceDesign();
} elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) { } elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) {
return AccountController::saveCustomizeDesign(); return AccountController::saveCustomizeDesign();
} elseif ($section === ACCOUNT_CLIENT_VIEW_STYLING) {
return AccountController::saveClientViewStyling();
} elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) {
return AccountController::saveEmailTemplates(); return AccountController::saveEmailTemplates();
} elseif ($section === ACCOUNT_PRODUCTS) { } elseif ($section === ACCOUNT_PRODUCTS) {
@ -430,6 +457,53 @@ class AccountController extends BaseController
return Redirect::to('settings/' . ACCOUNT_CUSTOMIZE_DESIGN); 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('/(?<![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 = $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() private function saveEmailTemplates()
{ {
if (Auth::user()->account->isPro()) { if (Auth::user()->account->isPro()) {

View File

@ -103,6 +103,7 @@ class InvoiceController extends BaseController
return response()->view('error', [ return response()->view('error', [
'error' => trans('texts.invoice_not_found'), 'error' => trans('texts.invoice_not_found'),
'hideHeader' => true, 'hideHeader' => true,
'clientViewCSS' => $account->clientViewCSS(),
]); ]);
} }
@ -154,6 +155,7 @@ class InvoiceController extends BaseController
'showBreadcrumbs' => false, 'showBreadcrumbs' => false,
'hideLogo' => $account->isWhiteLabel(), 'hideLogo' => $account->isWhiteLabel(),
'hideHeader' => $account->isNinjaAccount(), 'hideHeader' => $account->isNinjaAccount(),
'clientViewCSS' => $account->clientViewCSS(),
'invoice' => $invoice->hidePrivateFields(), 'invoice' => $invoice->hidePrivateFields(),
'invitation' => $invitation, 'invitation' => $invitation,
'invoiceLabels' => $account->getInvoiceLabels(), 'invoiceLabels' => $account->getInvoiceLabels(),

View File

@ -177,6 +177,7 @@ class PaymentController extends BaseController
'account' => $client->account, 'account' => $client->account,
'hideLogo' => $account->isWhiteLabel(), 'hideLogo' => $account->isWhiteLabel(),
'hideHeader' => $account->isNinjaAccount(), 'hideHeader' => $account->isNinjaAccount(),
'clientViewCSS' => $account->clientViewCSS(),
'showAddress' => $accountGateway->show_address, 'showAddress' => $accountGateway->show_address,
]; ];

View File

@ -37,6 +37,7 @@ class PublicClientController extends BaseController
'account' => $account, 'account' => $account,
'client' => $client, 'client' => $client,
'hideLogo' => $account->isWhiteLabel(), 'hideLogo' => $account->isWhiteLabel(),
'clientViewCSS' => $account->clientViewCSS(),
]; ];
return response()->view('invited.dashboard', $data); return response()->view('invited.dashboard', $data);
@ -81,6 +82,7 @@ class PublicClientController extends BaseController
$data = [ $data = [
'color' => $color, 'color' => $color,
'hideLogo' => $account->isWhiteLabel(), 'hideLogo' => $account->isWhiteLabel(),
'clientViewCSS' => $account->clientViewCSS(),
'title' => trans('texts.invoices'), 'title' => trans('texts.invoices'),
'entityType' => ENTITY_INVOICE, 'entityType' => ENTITY_INVOICE,
'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']), 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']),
@ -110,6 +112,7 @@ class PublicClientController extends BaseController
$data = [ $data = [
'color' => $color, 'color' => $color,
'hideLogo' => $account->isWhiteLabel(), 'hideLogo' => $account->isWhiteLabel(),
'clientViewCSS' => $account->clientViewCSS(),
'entityType' => ENTITY_PAYMENT, 'entityType' => ENTITY_PAYMENT,
'title' => trans('texts.payments'), 'title' => trans('texts.payments'),
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date'])
@ -145,6 +148,7 @@ class PublicClientController extends BaseController
$data = [ $data = [
'color' => $color, 'color' => $color,
'hideLogo' => $account->isWhiteLabel(), 'hideLogo' => $account->isWhiteLabel(),
'clientViewCSS' => $account->clientViewCSS(),
'title' => trans('texts.quotes'), 'title' => trans('texts.quotes'),
'entityType' => ENTITY_QUOTE, 'entityType' => ENTITY_QUOTE,
'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']), 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']),
@ -168,6 +172,7 @@ class PublicClientController extends BaseController
return response()->view('error', [ return response()->view('error', [
'error' => trans('texts.invoice_not_found'), 'error' => trans('texts.invoice_not_found'),
'hideHeader' => true, 'hideHeader' => true,
'clientViewCSS' => $account->clientViewCSS(),
]); ]);
} }

View File

@ -278,6 +278,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings');
define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings');
define('ACCOUNT_INVOICE_DESIGN', 'invoice_design'); define('ACCOUNT_INVOICE_DESIGN', 'invoice_design');
define('ACCOUNT_CLIENT_VIEW_STYLING', 'client_view_styling');
define('ACCOUNT_EMAIL_SETTINGS', 'email_settings'); define('ACCOUNT_EMAIL_SETTINGS', 'email_settings');
define('ACCOUNT_CHARTS_AND_REPORTS', 'charts_and_reports'); define('ACCOUNT_CHARTS_AND_REPORTS', 'charts_and_reports');
define('ACCOUNT_USER_MANAGEMENT', 'user_management'); define('ACCOUNT_USER_MANAGEMENT', 'user_management');

View File

@ -35,6 +35,7 @@ class Account extends Eloquent
public static $advancedSettings = [ public static $advancedSettings = [
ACCOUNT_INVOICE_SETTINGS, ACCOUNT_INVOICE_SETTINGS,
ACCOUNT_INVOICE_DESIGN, ACCOUNT_INVOICE_DESIGN,
ACCOUNT_CLIENT_VIEW_STYLING,
ACCOUNT_EMAIL_SETTINGS, ACCOUNT_EMAIL_SETTINGS,
ACCOUNT_TEMPLATES_AND_REMINDERS, ACCOUNT_TEMPLATES_AND_REMINDERS,
ACCOUNT_CHARTS_AND_REPORTS, ACCOUNT_CHARTS_AND_REPORTS,
@ -873,6 +874,14 @@ class Account extends Eloquent
{ {
return $this->isPro() && $this->pdf_email_attachment; 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) { Account::updated(function ($account) {

View File

@ -72,6 +72,7 @@ class AccountTransformer extends EntityTransformer
'invoice_taxes' => (bool) $account->invoice_taxes, 'invoice_taxes' => (bool) $account->invoice_taxes,
'invoice_item_taxes' => (bool) $account->invoice_item_taxes, 'invoice_item_taxes' => (bool) $account->invoice_item_taxes,
'invoice_design_id' => (int) $account->invoice_design_id, 'invoice_design_id' => (int) $account->invoice_design_id,
'client_view_css' => (string) $account->client_view_css,
'work_phone' => $account->work_phone, 'work_phone' => $account->work_phone,
'work_email' => $account->work_email, 'work_email' => $account->work_email,
'language_id' => (int) $account->language_id, 'language_id' => (int) $account->language_id,

View File

@ -63,7 +63,9 @@
"collizo4sky/omnipay-wepay": "~1.0", "collizo4sky/omnipay-wepay": "~1.0",
"laracasts/presenter": "dev-master", "laracasts/presenter": "dev-master",
"jlapp/swaggervel": "master-dev", "jlapp/swaggervel": "master-dev",
"maatwebsite/excel": "~2.0" "maatwebsite/excel": "~2.0",
"ezyang/htmlpurifier": "~v4.7",
"cerdic/css-tidy": "~v1.5"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "~4.0", "phpunit/phpunit": "~4.0",

81
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "26f955f9f0cfc85f6ec046f8600b04c7", "hash": "f3b07ee34ed5086cd684a7207fd3617e",
"content-hash": "8c51afe64fbaf07534ceaedaddf66206", "content-hash": "7305e2f5d6864894aeb23ac91f3e1fe7",
"packages": [ "packages": [
{ {
"name": "agmscode/omnipay-agms", "name": "agmscode/omnipay-agms",
@ -496,6 +496,39 @@
], ],
"time": "2015-06-05 14:50:44" "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", "name": "chumper/datatable",
"version": "dev-develop", "version": "dev-develop",
@ -1695,6 +1728,50 @@
], ],
"time": "2015-06-05 13:57:26" "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", "name": "fotografde/omnipay-checkoutcom",
"version": "2.0", "version": "2.0",

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddClientViewCss extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function ($table) {
$table->text('client_view_css')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('accounts', function ($table) {
$table->dropColumn('client_view_css');
});
}
}

View File

@ -60,6 +60,7 @@ There are two options:
* [Troels Liebe Bentsen](https://github.com/tlbdk) * [Troels Liebe Bentsen](https://github.com/tlbdk)
* [Jeramy Simpson](https://github.com/JeramyMywork) - [MyWork](https://www.mywork.com.au) * [Jeramy Simpson](https://github.com/JeramyMywork) - [MyWork](https://www.mywork.com.au)
* [Sigitas Limontas](https://lt.linkedin.com/in/sigitaslimontas) * [Sigitas Limontas](https://lt.linkedin.com/in/sigitaslimontas)
* [Joshua Dwire](https://github.com/joshuadwire) - [Some Techie](https://www.sometechie.com)
### Frameworks/Libraries ### Frameworks/Libraries
* [laravel/laravel](https://github.com/laravel/laravel) - A PHP Framework For Web Artisans * [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 * [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 * [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 * [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 * [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

View File

@ -203,7 +203,9 @@ return array(
'client_will_create' => 'client will be created', 'client_will_create' => 'client will be created',
'clients_will_create' => 'clients will be created', 'clients_will_create' => 'clients will be created',
'email_settings' => 'Email Settings', 'email_settings' => 'Email Settings',
'client_view_styling' => 'Client View Styling',
'pdf_email_attachment' => 'Attach PDFs', 'pdf_email_attachment' => 'Attach PDFs',
'custom_css' => 'Custom CSS',
//import CSV data pages //import CSV data pages
'import_clients' => 'Import Client Data', 'import_clients' => 'Import Client Data',
@ -474,6 +476,8 @@ return array(
'id_number' => 'ID Number', 'id_number' => 'ID Number',
'white_label_link' => 'White label', '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_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', 'white_label_header' => 'White Label',
'bought_white_label' => 'Successfully enabled white label license', 'bought_white_label' => 'Successfully enabled white label license',
'white_labeled' => 'White labeled', 'white_labeled' => 'White labeled',

View File

@ -0,0 +1,52 @@
@extends('header')
@section('head')
@parent
<link href='https://fonts.googleapis.com/css?family=Roboto+Mono' rel='stylesheet' type='text/css'>
@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())
<div class="alert alert-warning" style="font-size:larger;">
<center>
{!! trans('texts.white_label_custom_css', ['link'=>'<a href="#" onclick="$(\'#whiteLabelModal\').modal(\'show\');">'.trans('texts.white_label_purchase_link').'</a>']) !!}
</center>
</div>
@endif
@include('accounts.nav', ['selected' => ACCOUNT_CLIENT_VIEW_STYLING])
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.custom_css') !!}</h3>
</div>
<div class="panel-body">
{!! 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;'") !!}
</div>
</div>
</div>
</div>
<center>
{!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!}
</center>
{!! Former::close() !!}
@stop

View File

@ -111,7 +111,7 @@
</head> </head>
<body> <body class="body">
@if (isset($_ENV['TAG_MANAGER_KEY']) && $_ENV['TAG_MANAGER_KEY']) @if (isset($_ENV['TAG_MANAGER_KEY']) && $_ENV['TAG_MANAGER_KEY'])
<!-- Google Tag Manager --> <!-- Google Tag Manager -->

View File

@ -2,6 +2,9 @@
@section('head') @section('head')
<link href="{{ asset('css/built.public.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/> <link href="{{ asset('css/built.public.css') }}?no_cache={{ NINJA_VERSION }}" rel="stylesheet" type="text/css"/>
@if (!empty($clientViewCSS))
<style>{!! $clientViewCSS !!}</style>
@endif
@stop @stop
@section('body') @section('body')