diff --git a/README.md b/README.md index 98dfbef05972..1be56f78a9f5 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Most online invoicing sites are expensive. They shouldn't be. The aim of this project is to provide a free, open-source alternative. Additionally, the hope is the codebase will serve as a sample site for Laravel as well as other JavaScript technologies. -For discussion of the code please use the [Google Group](https://groups.google.com/d/forum/invoiceninja). +The high level instructions for setting up the site are below but there's also a [setup guide](http://hillelcoren.com/invoice-ninja/laravel-ubuntu-virtualbox/). For discussion of the code please use the [Google Group](https://groups.google.com/d/forum/invoiceninja). -For updates follow [@invoiceninja](https://twitter.com/invoiceninja) or join the [Facebook Group](https://www.facebook.com/invoiceninja) +For updates follow [@invoiceninja](https://twitter.com/invoiceninja) or join the [Facebook Group](https://www.facebook.com/invoiceninja). Site design by [kantorp-wegl.in](http://kantorp-wegl.in/) diff --git a/app/controllers/AccountController.php b/app/controllers/AccountController.php index 30ec23aecab2..7cfef28609b9 100755 --- a/app/controllers/AccountController.php +++ b/app/controllers/AccountController.php @@ -532,7 +532,7 @@ class AccountController extends \BaseController { } else { - $account = Account::findOrFail(Auth::user()->account_id); + $account = Auth::user()->account; $account->name = trim(Input::get('name')); $account->work_email = trim(Input::get('work_email')); $account->work_phone = trim(Input::get('work_phone')); diff --git a/app/controllers/InvoiceController.php b/app/controllers/InvoiceController.php index 6cd3d2295f2b..f805745c152c 100755 --- a/app/controllers/InvoiceController.php +++ b/app/controllers/InvoiceController.php @@ -89,7 +89,7 @@ class InvoiceController extends \BaseController { $table->addColumn('frequency', function($model) { return link_to('invoices/' . $model->public_id, $model->frequency); }); if (!$clientPublicId) { - $table->addColumn('client', function($model) { return link_to('clients/' . $model->client_public_id, Utils::getClientDisplayName($model)); }); + $table->addColumn('client_name', function($model) { return link_to('clients/' . $model->client_public_id, Utils::getClientDisplayName($model)); }); } return $table->addColumn('start_date', function($model) { return Utils::fromSqlDate($model->start_date); }) diff --git a/app/handlers/UserEventHandler.php b/app/handlers/UserEventHandler.php index 31753f6a0157..5e6a24955287 100755 --- a/app/handlers/UserEventHandler.php +++ b/app/handlers/UserEventHandler.php @@ -17,11 +17,11 @@ class UserEventHandler public function onLogin() { - $account = Auth::user()->account; - $account->last_login = Carbon::now()->toDateTimeString(); - $account->save(); + $account = Auth::user()->account; + $account->last_login = Carbon::now()->toDateTimeString(); + $account->save(); - Event::fire('user.refresh'); + Event::fire('user.refresh'); } public function onRefresh() diff --git a/app/lang/en/fields.php b/app/lang/en/fields.php index 10f5d738c39a..6a0bcaa7b21b 100644 --- a/app/lang/en/fields.php +++ b/app/lang/en/fields.php @@ -36,7 +36,7 @@ return array( 'po_number' => 'PO Number', 'po_number_short' => 'PO #', 'frequency_id' => 'How often', - 'dicount' => 'Discount', + 'discount' => 'Discount', 'taxes' => 'Taxes', 'tax' => 'Tax', 'item' => 'Item', diff --git a/app/lang/fr/fields.php b/app/lang/fr/fields.php index 10f5d738c39a..37dc8e1df81d 100644 --- a/app/lang/fr/fields.php +++ b/app/lang/fr/fields.php @@ -3,52 +3,52 @@ return array( // client - 'organization' => 'Organization', - 'name' => 'Name', - 'website' => 'Website', - 'work_phone' => 'Phone', - 'address' => 'Address', - 'address1' => 'Street', - 'address2' => 'Apt/Suite', - 'city' => 'City', - 'state' => 'State/Province', - 'postal_code' => 'Postal Code', - 'country_id' => 'Country', - 'contacts' => 'Contacts', - 'first_name' => 'First Name', - 'last_name' => 'Last Name', - 'phone' => 'Phone', + 'organization' => 'Entreprise', + 'name' => 'Nom', + 'website' => 'Site web', + 'work_phone' => 'Téléphone', + 'address' => 'Adresse', + 'address1' => 'Rue', + 'address2' => 'Appt/Batîment', + 'city' => 'Ville', + 'state' => 'Région/Département', + 'postal_code' => 'Code Postal', + 'country_id' => 'Pays', + 'contacts' => 'Informations de contact', //if you speak about contact details + 'first_name' => 'Prénom', + 'last_name' => 'Nom', + 'phone' => 'Téléphone', 'email' => 'Email', - 'additional_info' => 'Additional Info', - 'payment_terms' => 'Payment Terms', - 'currency_id' => 'Currency', - 'size_id' => 'Size', - 'industry_id' => 'Industry', - 'private_notes' => 'Private Notes', + 'additional_info' => 'Informations complémentaires', + 'payment_terms' => 'Conditions de paiement', + 'currency_id' => 'Devise', + 'size_id' => 'Taille', + 'industry_id' => 'Secteur', // literal translation : Industrie + 'private_notes' => 'Note personnelle', // invoice - 'invoice' => 'Invoice', + 'invoice' => 'Facture', 'client' => 'Client', - 'invoice_date' => 'Invoice Date', - 'due_date' => 'Due Date', - 'invoice_number' => 'Invoice Number', - 'invoice_number_short' => 'Invoice #', - 'po_number' => 'PO Number', - 'po_number_short' => 'PO #', - 'frequency_id' => 'How often', - 'dicount' => 'Discount', + 'invoice_date' => 'Date de la facture', + 'due_date' => 'Date d\'échéance', + 'invoice_number' => 'Numéro de facture', + 'invoice_number_short' => 'Facture #', + 'po_number' => 'Numéro du bon de commande', + 'po_number_short' => 'Bon de commande #', + 'frequency_id' => 'Fréquence', //litteral translation : Combien de fois + 'discount' => 'Remise', //can be "rabais" or "réduction" 'taxes' => 'Taxes', - 'tax' => 'Tax', - 'item' => 'Item', + 'tax' => 'Taxe', + 'item' => 'Ligne', //I'm not sure, I need the context : screenshot ? 'description' => 'Description', - 'unit_cost' => 'Unit Cost', - 'quantity' => 'Quantity', - 'line_total' => 'Line Total', - 'subtotal' => 'Subtotal', - 'paid_to_date' => 'Paid to Date', - 'balance_due' => 'Balance Due', - 'invoice_design_id' => 'Design', - 'terms' => 'Terms', - 'your_invoice' => 'Your Invoice', + 'unit_cost' => 'Coût à l\'unité', + 'quantity' => 'Quantité', + 'line_total' => 'Total', + 'subtotal' => 'Total', + 'paid_to_date' => 'Versé à ce jour',//this one is not very used in France + 'balance_due' => 'Montant total',//can be "Montant à verser" or "Somme totale" + 'invoice_design_id' => 'Design', //if you speak about invoice's design -> "Modèle" + 'terms' => 'Conditions', + 'your_invoice' => 'Votre Facture', -); \ No newline at end of file +); diff --git a/app/lang/pt_BR/fields.php b/app/lang/pt_BR/fields.php index 523f0a85b598..a405467cfeb4 100644 --- a/app/lang/pt_BR/fields.php +++ b/app/lang/pt_BR/fields.php @@ -25,4 +25,29 @@ return array( 'industry_id' => 'Empresa', 'private_notes' => 'Notas Privadas', + // invoice + 'invoice' => 'Invoice', + 'client' => 'Client', + 'invoice_date' => 'Invoice Date', + 'due_date' => 'Due Date', + 'invoice_number' => 'Invoice Number', + 'invoice_number_short' => 'Invoice #', + 'po_number' => 'PO Number', + 'po_number_short' => 'PO #', + 'frequency_id' => 'How often', + 'discount' => 'Discount', + 'taxes' => 'Taxes', + 'tax' => 'Tax', + 'item' => 'Item', + 'description' => 'Description', + 'unit_cost' => 'Unit Cost', + 'quantity' => 'Quantity', + 'line_total' => 'Line Total', + 'subtotal' => 'Subtotal', + 'paid_to_date' => 'Paid to Date', + 'balance_due' => 'Balance Due', + 'invoice_design_id' => 'Design', + 'terms' => 'Terms', + 'your_invoice' => 'Your Invoice', + ); diff --git a/app/libraries/utils.php b/app/libraries/utils.php index f4fb3bc68348..086762c31a95 100755 --- a/app/libraries/utils.php +++ b/app/libraries/utils.php @@ -37,11 +37,10 @@ class Utils 'url' => Input::get('url', Request::url()), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'ip' => Request::getClientIp(), - 'count' => Session::get('error_count', 0), - 'input' => Input::all() + 'count' => Session::get('error_count', 0) ]; - Log::error('\n'.$error, $data); + Log::error($error."\n", $data); /* Mail::queue('emails.error', ['message'=>$error.' '.json_encode($data)], function($message) diff --git a/app/models/Account.php b/app/models/Account.php index bc74165573bc..f821fb009e86 100755 --- a/app/models/Account.php +++ b/app/models/Account.php @@ -166,7 +166,7 @@ class Account extends Eloquent Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT); Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT); Session::put(SESSION_DATETIME_FORMAT, $this->datetime_format ? $this->datetime_format->format : DEFAULT_DATETIME_FORMAT); - Session::put(SESSION_CURRENCY, $this->currency_id ? $this->currency_id : DEFAULT_CURRENCY); + Session::put(SESSION_CURRENCY, $this->currency_id ? $this->currency_id : DEFAULT_CURRENCY); } public function getInvoiceLabels() @@ -178,7 +178,7 @@ class Account extends Eloquent 'due_date', 'invoice_number', 'po_number', - 'dicount', + 'discount', 'taxes', 'tax', 'item', diff --git a/app/ninja/repositories/InvoiceRepository.php b/app/ninja/repositories/InvoiceRepository.php index c8327d0311a8..5f31ab7cc2c0 100755 --- a/app/ninja/repositories/InvoiceRepository.php +++ b/app/ninja/repositories/InvoiceRepository.php @@ -50,13 +50,13 @@ class InvoiceRepository { $query = \DB::table('invoices') ->join('clients', 'clients.id', '=','invoices.client_id') - ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') - ->join('contacts', 'contacts.client_id', '=', 'clients.id') - ->where('invoices.account_id', '=', $accountId) + ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') + ->join('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('invoices.account_id', '=', $accountId) ->where('clients.deleted_at', '=', null) ->where('invoices.is_recurring', '=', true) ->where('contacts.is_primary', '=', true) - ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'frequencies.name as frequency', 'start_date', 'end_date', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email'); + ->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'frequencies.name as frequency', 'start_date', 'end_date', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email'); if ($clientPublicId) { diff --git a/app/views/header.blade.php b/app/views/header.blade.php index 0303b6ad202f..4813ee4d6538 100755 --- a/app/views/header.blade.php +++ b/app/views/header.blade.php @@ -456,7 +456,7 @@ if (isStorageSupported()) { @if (Auth::check() && !Auth::user()->registered) localStorage.setItem('guest_key', '{{ Auth::user()->password }}'); - @endif + @endif } @if (!Auth::check() || !Auth::user()->registered) diff --git a/app/views/master.blade.php b/app/views/master.blade.php index 0007b13cd929..9dc7676cf63a 100755 --- a/app/views/master.blade.php +++ b/app/views/master.blade.php @@ -15,18 +15,19 @@ - + - + +
@@ -139,7 +127,7 @@ function getStarted() {
-

Invoice Now +

+

Invoice Now +

diff --git a/app/views/public/header.blade.php b/app/views/public/header.blade.php index 49f74f4b40f9..c7fc4859ea33 100644 --- a/app/views/public/header.blade.php +++ b/app/views/public/header.blade.php @@ -27,22 +27,31 @@ @@ -93,6 +102,13 @@   + +

 

diff --git a/app/views/public/splash.blade.php b/app/views/public/splash.blade.php index 2a520e2e525c..ec56f0dd9bcf 100755 --- a/app/views/public/splash.blade.php +++ b/app/views/public/splash.blade.php @@ -27,7 +27,7 @@
-

Invoice Now +

+

Invoice Now +

@@ -40,7 +40,7 @@
-
+

100% FREE, ALWAYS

Invoicing with no monthly fee, because you have enough bills already! Free, now and forever! Quality invoicing to build your business and get paid.

@@ -48,8 +48,7 @@
-
+

OPEN-SOURCE

Cloud-based, super secure, and user-developed. Open source platforms are a better way to do business (and save the world). Need we say more?

@@ -57,7 +56,7 @@
-
+

LIVE .PDF VIEW

Create beautiful email-ready .PDF invoices created instantly as you type. Our ‘Save & send’ feature saves you time and impresses clients.

@@ -65,8 +64,7 @@
-
+

ONLINE PAYMENTS

PayPal? Authorize.Net? Stripe? We support many payment technologies and if you need help or advice we’ll lend a hand (we’re pretty friendly).

@@ -102,7 +100,7 @@ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100755 index 000000000000..2a5a5d1f069d Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/images/social.jpg b/public/images/social.jpg new file mode 100644 index 000000000000..9770a82ccf58 Binary files /dev/null and b/public/images/social.jpg differ diff --git a/public/js/jspdf.plugin.split_text_to_size.js b/public/js/jspdf.plugin.split_text_to_size.js index 93b9a48f4a7b..9d74d442afea 100755 --- a/public/js/jspdf.plugin.split_text_to_size.js +++ b/public/js/jspdf.plugin.split_text_to_size.js @@ -39,7 +39,6 @@ cell ocupied by the width of the char in that position. @returns {Array} */ var getCharWidthsArray = API.getCharWidthsArray = function(text, options){ - if (!options) { options = {} } diff --git a/public/js/script.js b/public/js/script.js index dbbaac137766..f0e8dc402792 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -793,8 +793,6 @@ function GetReportTemplate1(doc, invoice, layout, checkMath) - - function GetReportTemplate2(doc, invoice, layout, checkMath) { var GlobalY=0;//Y position of line at current page @@ -1357,9 +1355,10 @@ function concatStrings() { function displayGrid(doc, invoice, data, x, y, layout, hasheader, rightAlignX, rightAlignTitleX) { var numLines = 0; var origY = y; + for (var i=0; i 0 && origY === layout.accountTop) { SetPdfColor('GrayText',doc); } @@ -1400,8 +1399,8 @@ function displayGrid(doc, invoice, data, x, y, layout, hasheader, rightAlignX, r */ doc.setFontType('normal'); - key = invoiceLabels[key]; + if (rightAlignTitleX) { marginLeft = rightAlignTitleX - (doc.getStringUnitWidth(key) * doc.internal.getFontSize()); } else { diff --git a/public/js/stacktrace.js b/public/js/stacktrace.js new file mode 100644 index 000000000000..92169fd825fc --- /dev/null +++ b/public/js/stacktrace.js @@ -0,0 +1,485 @@ +// Domain Public by Eric Wendelin http://eriwen.com/ (2008) +// Luke Smith http://lucassmith.name/ (2008) +// Loic Dachary (2008) +// Johan Euphrosine (2008) +// Oyvind Sean Kinsey http://kinsey.no/blog (2010) +// Victor Homyakov (2010) +/*global module, exports, define, ActiveXObject*/ +(function(global, factory) { + if (typeof exports === 'object') { + // Node + module.exports = factory(); + } else if (typeof define === 'function' && define.amd) { + // AMD + define(factory); + } else { + // Browser globals + global.printStackTrace = factory(); + } +}(this, function() { + /** + * Main function giving a function stack trace with a forced or passed in Error + * + * @cfg {Error} e The error to create a stacktrace from (optional) + * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions + * @return {Array} of Strings with functions, lines, files, and arguments where possible + */ + function printStackTrace(options) { + options = options || {guess: true}; + var ex = options.e || null, guess = !!options.guess; + var p = new printStackTrace.implementation(), result = p.run(ex); + return (guess) ? p.guessAnonymousFunctions(result) : result; + } + + printStackTrace.implementation = function() { + }; + + printStackTrace.implementation.prototype = { + /** + * @param {Error} [ex] The error to create a stacktrace from (optional) + * @param {String} [mode] Forced mode (optional, mostly for unit tests) + */ + run: function(ex, mode) { + ex = ex || this.createException(); + mode = mode || this.mode(ex); + if (mode === 'other') { + return this.other(arguments.callee); + } else { + return this[mode](ex); + } + }, + + createException: function() { + try { + this.undef(); + } catch (e) { + return e; + } + }, + + /** + * Mode could differ for different exception, e.g. + * exceptions in Chrome may or may not have arguments or stack. + * + * @return {String} mode of operation for the exception + */ + mode: function(e) { + if (e['arguments'] && e.stack) { + return 'chrome'; + } + + if (e.stack && e.sourceURL) { + return 'safari'; + } + + if (e.stack && e.number) { + return 'ie'; + } + + if (e.stack && e.fileName) { + return 'firefox'; + } + + if (e.message && e['opera#sourceloc']) { + // e.message.indexOf("Backtrace:") > -1 -> opera9 + // 'opera#sourceloc' in e -> opera9, opera10a + // !e.stacktrace -> opera9 + if (!e.stacktrace) { + return 'opera9'; // use e.message + } + if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) { + // e.message may have more stack entries than e.stacktrace + return 'opera9'; // use e.message + } + return 'opera10a'; // use e.stacktrace + } + + if (e.message && e.stack && e.stacktrace) { + // e.stacktrace && e.stack -> opera10b + if (e.stacktrace.indexOf("called from line") < 0) { + return 'opera10b'; // use e.stacktrace, format differs from 'opera10a' + } + // e.stacktrace && e.stack -> opera11 + return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b' + } + + if (e.stack && !e.fileName) { + // Chrome 27 does not have e.arguments as earlier versions, + // but still does not have e.fileName as Firefox + return 'chrome'; + } + + return 'other'; + }, + + /** + * Given a context, function name, and callback function, overwrite it so that it calls + * printStackTrace() first with a callback and then runs the rest of the body. + * + * @param {Object} context of execution (e.g. window) + * @param {String} functionName to instrument + * @param {Function} callback function to call with a stack trace on invocation + */ + instrumentFunction: function(context, functionName, callback) { + context = context || window; + var original = context[functionName]; + context[functionName] = function instrumented() { + callback.call(this, printStackTrace().slice(4)); + return context[functionName]._instrumented.apply(this, arguments); + }; + context[functionName]._instrumented = original; + }, + + /** + * Given a context and function name of a function that has been + * instrumented, revert the function to it's original (non-instrumented) + * state. + * + * @param {Object} context of execution (e.g. window) + * @param {String} functionName to de-instrument + */ + deinstrumentFunction: function(context, functionName) { + if (context[functionName].constructor === Function && + context[functionName]._instrumented && + context[functionName]._instrumented.constructor === Function) { + context[functionName] = context[functionName]._instrumented; + } + }, + + /** + * Given an Error object, return a formatted Array based on Chrome's stack string. + * + * @param e - Error object to inspect + * @return Array of function calls, files and line numbers + */ + chrome: function(e) { + return (e.stack + '\n') + .replace(/^[\s\S]+?\s+at\s+/, ' at ') // remove message + .replace(/^\s+(at eval )?at\s+/gm, '') // remove 'at' and indentation + .replace(/^([^\(]+?)([\n$])/gm, '{anonymous}() ($1)$2') + .replace(/^Object.\s*\(([^\)]+)\)/gm, '{anonymous}() ($1)') + .replace(/^(.+) \((.+)\)$/gm, '$1@$2') + .split('\n') + .slice(0, -1); + }, + + /** + * Given an Error object, return a formatted Array based on Safari's stack string. + * + * @param e - Error object to inspect + * @return Array of function calls, files and line numbers + */ + safari: function(e) { + return e.stack.replace(/\[native code\]\n/m, '') + .replace(/^(?=\w+Error\:).*$\n/m, '') + .replace(/^@/gm, '{anonymous}()@') + .split('\n'); + }, + + /** + * Given an Error object, return a formatted Array based on IE's stack string. + * + * @param e - Error object to inspect + * @return Array of function calls, files and line numbers + */ + ie: function(e) { + return e.stack + .replace(/^\s*at\s+(.*)$/gm, '$1') + .replace(/^Anonymous function\s+/gm, '{anonymous}() ') + .replace(/^(.+)\s+\((.+)\)$/gm, '$1@$2') + .split('\n') + .slice(1); + }, + + /** + * Given an Error object, return a formatted Array based on Firefox's stack string. + * + * @param e - Error object to inspect + * @return Array of function calls, files and line numbers + */ + firefox: function(e) { + return e.stack.replace(/(?:\n@:0)?\s+$/m, '') + .replace(/^(?:\((\S*)\))?@/gm, '{anonymous}($1)@') + .split('\n'); + }, + + opera11: function(e) { + var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/; + var lines = e.stacktrace.split('\n'), result = []; + + for (var i = 0, len = lines.length; i < len; i += 2) { + var match = lineRE.exec(lines[i]); + if (match) { + var location = match[4] + ':' + match[1] + ':' + match[2]; + var fnName = match[3] || "global code"; + fnName = fnName.replace(//, "$1").replace(//, ANON); + result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, '')); + } + } + + return result; + }, + + opera10b: function(e) { + // "([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" + + // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" + + // "@file://localhost/G:/js/test/functional/testcase1.html:15" + var lineRE = /^(.*)@(.+):(\d+)$/; + var lines = e.stacktrace.split('\n'), result = []; + + for (var i = 0, len = lines.length; i < len; i++) { + var match = lineRE.exec(lines[i]); + if (match) { + var fnName = match[1] ? (match[1] + '()') : "global code"; + result.push(fnName + '@' + match[2] + ':' + match[3]); + } + } + + return result; + }, + + /** + * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. + * + * @param e - Error object to inspect + * @return Array of function calls, files and line numbers + */ + opera10a: function(e) { + // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n" + // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n" + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; + var lines = e.stacktrace.split('\n'), result = []; + + for (var i = 0, len = lines.length; i < len; i += 2) { + var match = lineRE.exec(lines[i]); + if (match) { + var fnName = match[3] || ANON; + result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); + } + } + + return result; + }, + + // Opera 7.x-9.2x only! + opera9: function(e) { + // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n" + // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n" + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i; + var lines = e.message.split('\n'), result = []; + + for (var i = 2, len = lines.length; i < len; i += 2) { + var match = lineRE.exec(lines[i]); + if (match) { + result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); + } + } + + return result; + }, + + // Safari 5-, IE 9-, and others + other: function(curr) { + var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10; + var slice = Array.prototype.slice; + while (curr && curr['arguments'] && stack.length < maxStackSize) { + fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; + args = slice.call(curr['arguments'] || []); + stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; + try { + curr = curr.caller; + } catch (e) { + stack[stack.length] = '' + e; + break; + } + } + return stack; + }, + + /** + * Given arguments array as a String, substituting type names for non-string types. + * + * @param {Arguments,Array} args + * @return {String} stringified arguments + */ + stringifyArguments: function(args) { + var result = []; + var slice = Array.prototype.slice; + for (var i = 0; i < args.length; ++i) { + var arg = args[i]; + if (arg === undefined) { + result[i] = 'undefined'; + } else if (arg === null) { + result[i] = 'null'; + } else if (arg.constructor) { + // TODO constructor comparison does not work for iframes + if (arg.constructor === Array) { + if (arg.length < 3) { + result[i] = '[' + this.stringifyArguments(arg) + ']'; + } else { + result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']'; + } + } else if (arg.constructor === Object) { + result[i] = '#object'; + } else if (arg.constructor === Function) { + result[i] = '#function'; + } else if (arg.constructor === String) { + result[i] = '"' + arg + '"'; + } else if (arg.constructor === Number) { + result[i] = arg; + } else { + result[i] = '?'; + } + } + } + return result.join(','); + }, + + sourceCache: {}, + + /** + * @return the text from a given URL + */ + ajax: function(url) { + var req = this.createXMLHTTPObject(); + if (req) { + try { + req.open('GET', url, false); + //req.overrideMimeType('text/plain'); + //req.overrideMimeType('text/javascript'); + req.send(null); + //return req.status == 200 ? req.responseText : ''; + return req.responseText; + } catch (e) { + } + } + return ''; + }, + + /** + * Try XHR methods in order and store XHR factory. + * + * @return XHR function or equivalent + */ + createXMLHTTPObject: function() { + var xmlhttp, XMLHttpFactories = [ + function() { + return new XMLHttpRequest(); + }, function() { + return new ActiveXObject('Msxml2.XMLHTTP'); + }, function() { + return new ActiveXObject('Msxml3.XMLHTTP'); + }, function() { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + ]; + for (var i = 0; i < XMLHttpFactories.length; i++) { + try { + xmlhttp = XMLHttpFactories[i](); + // Use memoization to cache the factory + this.createXMLHTTPObject = XMLHttpFactories[i]; + return xmlhttp; + } catch (e) { + } + } + }, + + /** + * Given a URL, check if it is in the same domain (so we can get the source + * via Ajax). + * + * @param url source url + * @return False if we need a cross-domain request + */ + isSameDomain: function(url) { + return typeof location !== "undefined" && url.indexOf(location.hostname) !== -1; // location may not be defined, e.g. when running from nodejs. + }, + + /** + * Get source code from given URL if in the same domain. + * + * @param url JS source URL + * @return Array of source code lines + */ + getSource: function(url) { + // TODO reuse source from script tags? + if (!(url in this.sourceCache)) { + this.sourceCache[url] = this.ajax(url).split('\n'); + } + return this.sourceCache[url]; + }, + + guessAnonymousFunctions: function(stack) { + for (var i = 0; i < stack.length; ++i) { + var reStack = /\{anonymous\}\(.*\)@(.*)/, + reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/, + frame = stack[i], ref = reStack.exec(frame); + + if (ref) { + var m = reRef.exec(ref[1]); + if (m) { // If falsey, we did not get any file/line information + var file = m[1], lineno = m[2], charno = m[3] || 0; + if (file && this.isSameDomain(file) && lineno) { + var functionName = this.guessAnonymousFunction(file, lineno, charno); + stack[i] = frame.replace('{anonymous}', functionName); + } + } + } + } + return stack; + }, + + guessAnonymousFunction: function(url, lineNo, charNo) { + var ret; + try { + ret = this.findFunctionName(this.getSource(url), lineNo); + } catch (e) { + ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString(); + } + return ret; + }, + + findFunctionName: function(source, lineNo) { + // FIXME findFunctionName fails for compressed source + // (more than one function on the same line) + // function {name}({args}) m[1]=name m[2]=args + var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; + // {name} = function ({args}) TODO args capture + // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/ + var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/; + // {name} = eval() + var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/; + // Walk backwards in the source lines until we find + // the line which matches one of the patterns above + var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos; + for (var i = 0; i < maxLines; ++i) { + // lineNo is 1-based, source[] is 0-based + line = source[lineNo - i - 1]; + commentPos = line.indexOf('//'); + if (commentPos >= 0) { + line = line.substr(0, commentPos); + } + // TODO check other types of comments? Commented code may lead to false positive + if (line) { + code = line + code; + m = reFunctionExpression.exec(code); + if (m && m[1]) { + return m[1]; + } + m = reFunctionDeclaration.exec(code); + if (m && m[1]) { + //return m[1] + "(" + (m[2] || "") + ")"; + return m[1]; + } + m = reFunctionEvaluation.exec(code); + if (m && m[1]) { + return m[1]; + } + } + } + return '(?)'; + } + }; + + return printStackTrace; +}));