1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Merge branch 'master' into Dev_PHP-Payments

Merged changes from master.
This commit is contained in:
blkmutt 2014-03-25 21:21:23 -04:00
commit 3ca03eb2b4
21 changed files with 605 additions and 95 deletions

View File

@ -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/)

View File

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

View File

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

View File

@ -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()

View File

@ -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',

View File

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

View File

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

View File

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

View File

@ -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',

View File

@ -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)
{

View File

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

View File

@ -15,18 +15,19 @@
<meta property="og:site_name" content="Invoice Ninja"></meta>
<meta property="og:url" content="https://www.invoiceninja.com"></meta>
<meta property="og:title" content="Invoice Ninja"></meta>
<meta property="og:image" content="https://www.invoiceninja.com/images/facebook.jpg"></meta>
<meta property="og:image" content="https://www.invoiceninja.com/images/social.jpg"></meta>
<meta property="og:description" content="Simple, Intuitive Invoicing."></meta>
<meta name="keywords" content="Invoice Ninja"></meta>
<meta name="keywords" content="Invoice Ninja"></meta>
<script src="{{ asset('vendor/jquery/jquery.min.js') }}" type="text/javascript"></script>
<script type="text/javascript">
window.onerror = function(e) {
try {
$.ajax({
type: 'GET',
url: '{{ URL::to('log_error') }}',
data: 'error='+e+'&url='+window.location
data: 'error='+encodeURIComponent(e)+'&url='+encodeURIComponent(window.location)
});
} catch(err) {}
return false;

View File

@ -74,7 +74,7 @@
<div class="col-md-3 center-block">
<a href="#">
<div class="cta">
<h2 onclick="getStarted()">Invoice Now <span>+</span></h2>
<h2 onclick="return getStarted()">Invoice Now <span>+</span></h2>
</div>
</a>
</div>

View File

@ -53,18 +53,6 @@ var contactForm = {
}
};
function isStorageSupported() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
}
function getStarted() {
$('#startForm').submit();
}
</script>
<section class="hero4" data-speed="2" data-type="background">
@ -139,7 +127,7 @@ function getStarted() {
<div class="col-md-3 center-block">
<a href="#">
<div class="cta">
<h2 onclick="getStarted()">Invoice Now <span>+</span></h2>
<h2 onclick="return getStarted()">Invoice Now <span>+</span></h2>
</div>
</a>
</div>

View File

@ -27,22 +27,31 @@
<script>
$(document).ready(function () {
$(document).ready(function () {
if (isStorageSupported()) {
$('[name="guest_key"]').val(localStorage.getItem('guest_key'));
}
});
function isStorageSupported() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
if ('localStorage' in window && window['localStorage'] !== null) {
var storage = window.localStorage;
} else {
return false;
}
var testKey = 'test';
try {
storage.setItem(testKey, '1');
storage.removeItem(testKey);
return true;
} catch (error) {
return false;
}
}
function getStarted() {
$('#startForm').submit();
return false;
}
</script>
@ -93,6 +102,13 @@
</script>
&nbsp;
<!--
<script src="//platform.linkedin.com/in.js" type="text/javascript">
lang: en_US
</script>
<script type="IN/Share" data-url="https://www.invoiceninja.com/"></script>
-->
<!--<iframe src="http://ghbtns.com/github-btn.html?user=hillelcoren&repo=invoice-ninja&type=watch" allowtransparency="true" frameborder="0" scrolling="0" width="62" height="20"></iframe>-->
<p>&nbsp;</p>

View File

@ -27,7 +27,7 @@
<div class="col-md-3 center-block">
<a href="#">
<div class="cta">
<h2 id="startButton" onclick="getStarted()">Invoice Now <span>+</span></h2>
<h2 id="startButton" onclick="return getStarted()">Invoice Now <span>+</span></h2>
</div>
</a>
</div>
@ -40,7 +40,7 @@
<div class="row">
<div class="col-md-3 one">
<div class="box">
<div class="icon"><img src="images/icon-free.png"></div>
<div class="icon"><img src="{{ asset('images/icon-free.png') }}"></div>
<h2>100% FREE, ALWAYS</h2>
<p>Invoicing with no monthly fee, because you have enough bills already! Free, now and forever! Quality invoicing to build your business and get paid.</p>
</div>
@ -48,8 +48,7 @@
<div class="col-md-3 two">
<div class="box">
<div class="icon"><img src=
"images/icon-opensource.png"></div>
<div class="icon"><img src="{{ asset('images/icon-opensource.png') }}"></div>
<h2>OPEN-SOURCE</h2>
<p>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?</p>
</div>
@ -57,7 +56,7 @@
<div class="col-md-3 three">
<div class="box">
<div class="icon"><img src="images/icon-pdf.png"></div>
<div class="icon"><img src="{{ asset('images/icon-pdf.png') }}"></div>
<h2>LIVE .PDF VIEW</h2>
<p>Create beautiful email-ready .PDF invoices created instantly as you type. Our Save & send feature saves you time and impresses clients.</p>
</div>
@ -65,8 +64,7 @@
<div class="col-md-3 four">
<div class="box">
<div class="icon"><img src=
"images/icon-payment.png"></div>
<div class="icon"><img src="{{ asset('images/icon-payment.png') }}"></div>
<h2>ONLINE PAYMENTS</h2>
<p>PayPal? Authorize.Net? Stripe? We support many payment technologies and if you need help or advice well lend a hand (were pretty friendly).</p>
</div>
@ -102,7 +100,7 @@
<div class="col-md-3 center-block">
<a href="#">
<div class="cta">
<h2 onclick="getStarted()">Invoice Now <span>+</span></h2>
<h2 onclick="return getStarted()">Invoice Now <span>+</span></h2>
</div>
</a>
</div>

BIN
public/apple-touch-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/images/social.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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 = {}
}

View File

@ -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<data.length; i++) {
doc.setFontType('normal');
if (invoice.invoice_design_id == 1 && 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 {

485
public/js/stacktrace.js Normal file
View File

@ -0,0 +1,485 @@
// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
// Luke Smith http://lucassmith.name/ (2008)
// Loic Dachary <loic@dachary.org> (2008)
// Johan Euphrosine <proppy@aminche.com> (2008)
// Oyvind Sean Kinsey http://kinsey.no/blog (2010)
// Victor Homyakov <victor-homyakov@users.sourceforge.net> (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<String> 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.<anonymous>\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<String> 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<String> 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<String> 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(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON);
result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, ''));
}
}
return result;
},
opera10b: function(e) {
// "<anonymous function: run>([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<String> 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 <Function> 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 <String> source url
* @return <Boolean> 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 <String> JS source URL
* @return <Array> 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;
}));