2014-11-15 19:15:11 +01:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2015-03-18 02:08:48 +01:00
uBlock - a browser extension to block requests .
Copyright ( C ) 2015 The uBlock authors
2014-11-15 19:15:11 +01:00
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program . If not , see { http : //www.gnu.org/licenses/}.
2015-04-09 00:36:14 +02:00
Home : https : //github.com/chrisaljoudi/uBlock
2014-11-15 19:15:11 +01:00
* /
2015-01-21 14:59:23 +01:00
/* global self, safari, SafariBrowserTab, µBlock */
2014-12-17 21:33:53 +01:00
2014-11-15 19:15:11 +01:00
// For background page
/******************************************************************************/
2015-03-11 06:24:06 +01:00
2014-11-15 19:15:11 +01:00
( function ( ) {
2015-02-19 05:37:54 +01:00
"use strict" ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
var vAPI = self . vAPI = self . vAPI || { } ;
2015-03-12 00:54:32 +01:00
vAPI . isMainProcess = true ;
2015-01-30 05:20:28 +01:00
vAPI . safari = true ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . app = {
2015-03-10 05:00:33 +01:00
name : "uBlock" ,
2015-02-01 05:19:59 +01:00
version : safari . extension . displayVersion
2015-01-30 05:20:28 +01:00
} ;
2014-12-01 20:45:00 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-12-01 20:45:00 +01:00
2015-03-04 23:26:33 +01:00
if ( navigator . userAgent . indexOf ( "Safari/6" ) === - 1 ) { // If we're not on at least Safari 8
var _open = XMLHttpRequest . prototype . open ;
XMLHttpRequest . prototype . open = function ( m , u ) {
if ( u . lastIndexOf ( "safari-extension:" , 0 ) === 0 ) {
var i = u . length , seeDot = false ;
while ( i -- ) {
if ( u [ i ] === "." ) {
seeDot = true ;
}
else if ( u [ i ] === "/" ) {
break ;
}
}
if ( seeDot === false ) {
throw 'InvalidAccessError' ; // Avoid crash
return ;
}
}
_open . apply ( this , arguments ) ;
} ;
}
/******************************************************************************/
2015-03-11 23:29:08 +01:00
vAPI . app . restart = function ( ) {
µBlock . restart ( ) ;
} ;
2014-12-02 17:02:17 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-12-02 17:02:17 +01:00
2015-03-09 20:56:05 +01:00
safari . extension . addContentScriptFromURL ( vAPI . getURL ( "js/subscriber.js" ) , [
"https://*.adblockplus.org/*" ,
"https://*.adblockplus.me/*" ,
"https://www.fanboy.co.nz/*" ,
"http://*.adblockplus.org/*" ,
"http://*.adblockplus.me/*" ,
"http://www.fanboy.co.nz/*"
] , [ ] , true ) ;
/******************************************************************************/
2015-01-30 05:20:28 +01:00
safari . extension . settings . addEventListener ( 'change' , function ( e ) {
if ( e . key === 'open_prefs' ) {
vAPI . tabs . open ( {
url : 'dashboard.html' ,
active : true
} ) ;
}
} , false ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-03-11 06:24:06 +01:00
initStorageLib ( ) ; // Initialize storage library
/******************************************************************************/
var storageQuota = 104857600 ; // copied from Info.plist
localforage . config ( {
name : "ublock" ,
size : storageQuota ,
storeName : "keyvaluepairs"
} ) ;
2015-03-15 01:13:00 +01:00
var oldSettings = safari . extension . settings ; // To smoothly transition users
if ( oldSettings . hasOwnProperty ( "version" ) ) { // Old 'storage'!
for ( var key in oldSettings ) {
if ( ! oldSettings . hasOwnProperty ( key ) || key === "open_prefs" ) {
continue ;
}
localforage . setItem ( key , oldSettings [ key ] ) ;
}
oldSettings . clear ( ) ;
}
2015-01-30 05:20:28 +01:00
vAPI . storage = {
2015-03-11 06:24:06 +01:00
QUOTA _BYTES : storageQuota , // copied from Info.plist
2014-12-28 21:26:06 +01:00
2015-01-30 05:20:28 +01:00
get : function ( keys , callback ) {
2015-03-11 06:24:06 +01:00
if ( typeof callback !== "function" ) {
2015-01-30 05:20:28 +01:00
return ;
}
2015-03-11 06:24:06 +01:00
var result = { } ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
if ( keys === null ) {
2015-03-11 06:24:06 +01:00
localforage . iterate ( function ( value , key ) {
if ( typeof value === "string" ) {
result [ key ] = JSON . parse ( value ) ;
}
} , function ( ) {
callback ( result ) ;
} ) ;
}
else if ( typeof keys === "string" ) {
localforage . getItem ( keys , function ( err , value ) {
if ( typeof value === "string" ) {
result [ keys ] = JSON . parse ( value ) ;
2015-01-30 05:20:28 +01:00
}
2015-03-11 06:24:06 +01:00
callback ( result ) ;
} ) ;
}
else if ( Array . isArray ( keys ) ) {
var toSatisfy = keys . length , n = toSatisfy ;
if ( n === 0 ) {
callback ( result ) ;
return ;
2014-11-15 19:15:11 +01:00
}
2015-03-11 06:24:06 +01:00
for ( var i = 0 ; i < n ; i ++ ) {
var key = keys [ i ] ;
2015-03-11 07:03:54 +01:00
var func = function ( err , value ) {
2015-03-11 06:24:06 +01:00
toSatisfy -- ;
if ( typeof value === "string" ) {
2015-03-11 07:03:54 +01:00
result [ arguments . callee . myKey ] = JSON . parse ( value ) ;
2015-03-11 06:24:06 +01:00
}
if ( toSatisfy === 0 ) {
callback ( result ) ;
}
2015-03-11 07:03:54 +01:00
} ;
func . myKey = key ;
localforage . getItem ( key , func ) ;
2015-01-30 05:20:28 +01:00
}
2015-03-11 06:24:06 +01:00
}
else if ( typeof keys === "object" ) {
for ( var key in keys ) {
if ( ! keys . hasOwnProperty ( key ) ) {
continue ;
2015-01-30 05:20:28 +01:00
}
2015-03-11 07:03:54 +01:00
result [ key ] = keys [ key ] ;
2014-11-15 19:15:11 +01:00
}
2015-03-11 07:03:54 +01:00
localforage . iterate ( function ( value , key ) {
2015-03-11 22:53:23 +01:00
if ( ! keys . hasOwnProperty ( key ) ) return ;
2015-03-11 07:03:54 +01:00
if ( typeof value === "string" ) {
result [ key ] = JSON . parse ( value ) ;
2015-01-30 05:20:28 +01:00
}
2015-03-11 07:03:54 +01:00
} , function ( ) {
callback ( result ) ;
} ) ;
2014-11-15 19:15:11 +01:00
}
2015-01-30 05:20:28 +01:00
} ,
2014-12-28 21:26:06 +01:00
2015-01-30 05:20:28 +01:00
set : function ( details , callback ) {
2015-03-11 06:24:06 +01:00
var toSatisfy = 0 ;
2015-01-30 05:20:28 +01:00
for ( var key in details ) {
if ( ! details . hasOwnProperty ( key ) ) {
continue ;
}
2015-03-11 06:24:06 +01:00
toSatisfy ++ ;
2015-01-18 19:50:20 +01:00
}
2015-03-11 06:24:06 +01:00
for ( var key in details ) {
if ( ! details . hasOwnProperty ( key ) ) {
continue ;
}
localforage . setItem ( key , JSON . stringify ( details [ key ] ) , function ( ) {
if ( -- toSatisfy === 0 ) {
callback && callback ( ) ;
}
} ) ;
2015-01-30 05:20:28 +01:00
}
} ,
2014-12-28 21:26:06 +01:00
2015-01-30 05:20:28 +01:00
remove : function ( keys ) {
2015-03-11 06:24:06 +01:00
if ( typeof keys === "string" ) {
2015-01-30 05:20:28 +01:00
keys = [ keys ] ;
}
2014-11-15 19:15:11 +01:00
2015-03-11 06:24:06 +01:00
for ( var i = 0 , n = keys . length ; i < n ; i ++ ) {
localforage . removeItem ( keys [ i ] ) ;
2015-01-30 05:20:28 +01:00
}
} ,
2014-12-28 21:26:06 +01:00
2015-01-30 05:20:28 +01:00
clear : function ( callback ) {
2015-03-11 06:24:06 +01:00
localforage . clear ( function ( ) {
callback ( ) ;
} ) ;
2015-01-30 05:20:28 +01:00
} ,
2014-12-28 21:26:06 +01:00
2015-01-30 05:20:28 +01:00
getBytesInUse : function ( keys , callback ) {
2015-03-11 06:24:06 +01:00
if ( typeof callback !== "function" ) {
2015-01-30 05:20:28 +01:00
return ;
}
var size = 0 ;
2015-03-11 06:24:06 +01:00
localforage . iterate ( function ( value , key ) {
size += ( value || "" ) . length ;
} , function ( ) {
callback ( size ) ;
} ) ;
2014-11-15 19:15:11 +01:00
}
2015-01-30 05:20:28 +01:00
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . tabs = {
stack : { } ,
stackId : 1
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-04-14 02:58:50 +02:00
vAPI . isBehindTheSceneTabId = function ( tabId ) {
2015-04-01 01:06:12 +02:00
return tabId . toString ( ) === this . noTabId ;
2015-01-30 05:20:28 +01:00
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . noTabId = '-1' ;
2015-01-20 00:42:58 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2015-01-20 00:42:58 +01:00
2015-01-30 05:20:28 +01:00
vAPI . tabs . registerListeners = function ( ) {
2015-03-23 19:01:50 +01:00
safari . application . addEventListener ( "beforeNavigate" , function ( e ) {
if ( ! vAPI . tabs . popupCandidate || ! e . target || e . url === "about:blank" ) {
2015-01-30 05:20:28 +01:00
return ;
}
var url = e . url ,
tabId = vAPI . tabs . getTabId ( e . target ) ;
var details = {
2015-03-08 16:06:36 +01:00
targetURL : url ,
2015-04-14 02:58:50 +02:00
targetTabId : tabId . toString ( ) ,
2015-03-08 16:06:36 +01:00
openerTabId : vAPI . tabs . popupCandidate
2015-01-30 05:20:28 +01:00
} ;
2015-04-14 02:58:50 +02:00
vAPI . tabs . popupCandidate = false ;
2015-01-30 05:20:28 +01:00
if ( vAPI . tabs . onPopup ( details ) ) {
e . preventDefault ( ) ;
2015-03-08 16:06:36 +01:00
if ( vAPI . tabs . stack [ details . openerTabId ] ) {
vAPI . tabs . stack [ details . openerTabId ] . activate ( ) ;
2015-01-30 05:20:28 +01:00
}
}
} , true ) ;
// onClosed handled in the main tab-close event
// onUpdated handled via monitoring the history.pushState on web-pages
// onPopup is handled in window.open on web-pages
} ;
/******************************************************************************/
vAPI . tabs . getTabId = function ( tab ) {
2015-02-06 00:23:56 +01:00
if ( typeof tab . uBlockCachedID !== "undefined" ) {
return tab . uBlockCachedID ;
}
2015-01-30 05:20:28 +01:00
for ( var i in vAPI . tabs . stack ) {
if ( vAPI . tabs . stack [ i ] === tab ) {
2015-02-06 00:23:56 +01:00
return ( tab . uBlockCachedID = + i ) ;
2015-01-19 09:50:10 +01:00
}
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
return - 1 ;
} ;
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . tabs . get = function ( tabId , callback ) {
var tab ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
if ( tabId === null ) {
tab = safari . application . activeBrowserWindow . activeTab ;
tabId = this . getTabId ( tab ) ;
} else {
tab = this . stack [ tabId ] ;
2014-11-15 19:15:11 +01:00
}
2015-01-30 05:20:28 +01:00
if ( ! tab ) {
callback ( ) ;
return ;
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
callback ( {
id : tabId ,
index : tab . browserWindow . tabs . indexOf ( tab ) ,
windowId : safari . application . browserWindows . indexOf ( tab . browserWindow ) ,
active : tab === tab . browserWindow . activeTab ,
2015-02-10 03:31:21 +01:00
url : tab . url || "about:blank" ,
2015-01-30 05:20:28 +01:00
title : tab . title
} ) ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
// properties of the details object:
// url: 'URL', // the address that will be opened
// tabId: 1, // the tab is used if set, instead of creating a new one
// index: -1, // undefined: end of the list, -1: following tab, or after index
// active: false, // opens the tab in background - true and undefined: foreground
// select: true // if a tab is already opened with that url, then select it instead of opening a new one
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . tabs . open = function ( details ) {
if ( ! details . url ) {
return null ;
}
// extension pages
if ( /^[\w-]{2,}:/ . test ( details . url ) === false ) {
details . url = vAPI . getURL ( details . url ) ;
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
var curWin , tab ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
if ( details . select ) {
tab = safari . application . browserWindows . some ( function ( win ) {
var rgxHash = /#.*/ ;
// this is questionable
var url = details . url . replace ( rgxHash , '' ) ;
2014-12-01 16:23:19 +01:00
2015-01-30 05:20:28 +01:00
for ( var i = 0 ; i < win . tabs . length ; i ++ ) {
2015-02-10 03:31:21 +01:00
// Some tabs don't have a URL
if ( win . tabs [ i ] . url &&
win . tabs [ i ] . url . replace ( rgxHash , '' ) === url ) {
2015-01-30 05:20:28 +01:00
win . tabs [ i ] . activate ( ) ;
return true ;
}
2014-11-23 18:21:06 +01:00
}
2015-01-30 05:20:28 +01:00
} ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
if ( tab ) {
return ;
}
2014-11-23 18:21:06 +01:00
}
2015-01-30 05:20:28 +01:00
if ( details . active === undefined ) {
details . active = true ;
}
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
curWin = safari . application . activeBrowserWindow ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
// it must be calculated before opening a new tab,
// otherwise the new tab will be the active tab here
if ( details . index === - 1 ) {
details . index = curWin . tabs . indexOf ( curWin . activeTab ) + 1 ;
}
2014-11-23 18:21:06 +01:00
2015-02-10 06:24:50 +01:00
tab = ( details . tabId ? this . stack [ details . tabId ] : curWin . openTab ( details . active ? 'foreground' : 'background' ) ) ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
if ( details . index !== undefined ) {
curWin . insertTab ( tab , details . index ) ;
}
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
tab . url = details . url ;
} ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-23 18:21:06 +01:00
2015-03-30 23:42:12 +02:00
// Replace the URL of a tab. Noop if the tab does not exist.
vAPI . tabs . replace = function ( tabId , url ) {
var targetURL = url ;
// extension pages
if ( /^[\w-]{2,}:/ . test ( targetURL ) !== true ) {
targetURL = vAPI . getURL ( targetURL ) ;
}
var tab = this . stack [ tabId ] ;
if ( tab ) {
tab . url = targetURL ;
}
} ;
/******************************************************************************/
2015-01-30 05:20:28 +01:00
vAPI . tabs . remove = function ( tabIds ) {
if ( tabIds instanceof SafariBrowserTab ) {
tabIds = this . getTabId ( tabIds ) ;
}
2014-12-01 16:23:19 +01:00
2015-01-30 05:20:28 +01:00
if ( ! Array . isArray ( tabIds ) ) {
tabIds = [ tabIds ] ;
2014-12-01 16:23:19 +01:00
}
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
for ( var i = 0 ; i < tabIds . length ; i ++ ) {
if ( this . stack [ tabIds [ i ] ] ) {
this . stack [ tabIds [ i ] ] . close ( ) ;
}
}
} ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2015-01-08 21:11:43 +01:00
2015-01-30 05:20:28 +01:00
vAPI . tabs . reload = function ( tabId ) {
var tab = this . stack [ tabId ] ;
2015-01-08 21:11:43 +01:00
2015-01-30 05:20:28 +01:00
if ( tab ) {
tab . url = tab . url ;
}
} ;
2015-01-08 21:11:43 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
vAPI . tabs . injectScript = function ( tabId , details , callback ) {
var tab ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
if ( tabId ) {
tab = this . stack [ tabId ] ;
} else {
tab = safari . application . activeBrowserWindow . activeTab ;
}
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
if ( details . file ) {
var xhr = new XMLHttpRequest ( ) ;
2015-02-10 22:24:04 +01:00
xhr . open ( 'GET' , details . file , true ) ;
xhr . addEventListener ( "readystatechange" , function ( ) {
if ( this . readyState === 4 ) {
details . code = xhr . responseText ;
tab . page . dispatchMessage ( 'broadcast' , {
channelName : 'vAPI' ,
msg : {
cmd : 'injectScript' ,
details : details
}
} ) ;
if ( typeof callback === 'function' ) {
setTimeout ( callback , 13 ) ;
}
}
} ) ;
2015-01-30 05:20:28 +01:00
xhr . send ( ) ;
}
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-05-01 11:02:32 +02:00
// reload the popup when it's opened
safari . application . addEventListener ( "popover" , function ( event ) {
var w = event . target . contentWindow , body = w . document . body , child ;
while ( child = body . firstChild ) {
body . removeChild ( child ) ;
}
w . location . reload ( ) ;
} , true ) ;
/******************************************************************************/
var ICON _URLS = {
"on" : vAPI . getURL ( "img/browsericons/safari-icon16.png" ) ,
"off" : vAPI . getURL ( "img/browsericons/safari-icon16-off.png" )
} ;
var IconState = function ( badge , img , icon ) {
this . badge = badge ;
// ^ a number -- the badge 'value'
this . img = img ;
// ^ a string -- 'on' or 'off'
this . active = false ;
// ^ is this IconState active for rendering?
this . icon = typeof icon !== "undefined" ? icon : null ;
// ^ the corresponding browser toolbar-icon object
this . dirty = ( 1 << 1 ) | ( 1 << 0 ) ;
/ * ^ b i t m a s k A B : t w o b i t s , A a n d B
where A is whether img has changed and needs render
and B is whether badge has changed and needs render * /
} ;
var iconStateForTabId = { } ; // {tabId: IconState}
var getIconForWindow = function ( whichWindow ) {
// do we already have the right icon cached?
if ( typeof whichWindow . uBlockIcon !== "undefined" ) {
return whichWindow . uBlockIcon ;
}
// iterate through the icons to find the one which
// belongs to this window (whichWindow)
var items = safari . extension . toolbarItems ;
for ( var i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] . browserWindow === whichWindow ) {
return ( whichWindow . uBlockIcon = items [ i ] ) ;
}
}
} ;
safari . application . addEventListener ( "activate" , function ( event ) {
if ( ! ( event . target instanceof SafariBrowserTab ) ) {
return ;
}
// when a tab is activated...
var tab = event . target ;
if ( tab . browserWindow !== tab . oldBrowserWindow ) {
// looks like tab is now associated with a new window
tab . oldBrowserWindow = tab . browserWindow ;
// so, unvalidate icon
tab . uBlockKnowsIcon = false ;
}
var tabId = vAPI . tabs . getTabId ( tab ) ,
state = iconStateForTabId [ tabId ] ;
if ( typeof state === "undefined" ) {
state = iconStateForTabId [ tabId ] = new IconState ( 0 , "on" ) ;
// need to get the icon for this newly-encountered tab...
// uBlockKnowsIcon should be undefined here, so in theory
// we don't need this -- but to be sure,
// go ahead and explicitly unvalidate
tab . uBlockKnowsIcon = false ;
}
if ( ! tab . uBlockKnowsIcon ) {
// need to find the icon for this tab's window
state . icon = getIconForWindow ( tab . browserWindow ) ;
tab . uBlockKnowsIcon = true ;
}
state . active = true ;
// force re-render since we probably switched tabs
state . dirty = ( 1 << 1 ) | ( 1 << 0 ) ;
renderIcon ( state ) ;
} , true ) ;
safari . application . addEventListener ( "deactivate" , function ( event ) {
if ( ! ( event . target instanceof SafariBrowserTab ) ) {
return ;
}
// when a tab is deactivated...
var tabId = vAPI . tabs . getTabId ( event . target ) ,
state = iconStateForTabId [ tabId ] ;
if ( typeof state === "undefined" ) {
return ;
}
// mark its iconState as inactive so we don't visually
// render changes for now
state . active = false ;
} , true ) ;
var renderIcon = function ( iconState ) {
if ( iconState . dirty === 0 ) {
// quit if we don't need to touch the "DOM"
return ;
}
var icon = iconState . icon ;
icon . badge = iconState . badge ;
// only update the image if needed:
if ( iconState . dirty & 1 ) {
icon . image = ICON _URLS [ iconState . img ] ;
}
iconState . dirty = 0 ;
} ;
vAPI . setIcon = function ( tabId , iconStatus , badge ) {
badge = badge || 0 ;
var state = iconStateForTabId [ tabId ] ;
if ( typeof state === "undefined" ) {
state = iconStateForTabId [ tabId ] = new IconState ( badge , iconStatus ) ;
}
else {
state . dirty = ( ( state . badge !== badge ) << 1 ) | ( ( state . img !== iconStatus ) << 0 ) ;
state . badge = badge ;
state . img = iconStatus ;
}
if ( state . active === true ) {
renderIcon ( state ) ;
}
} ;
/******************************************************************************/
2015-01-30 05:20:28 +01:00
// bind tabs to unique IDs
( function ( ) {
var wins = safari . application . browserWindows ,
i = wins . length ,
2015-05-01 11:02:32 +02:00
j ,
curTab ,
curTabId ,
curWindow ;
while ( i -- ) {
curWindow = wins [ i ] ;
j = curWindow . tabs . length ;
2015-01-30 05:20:28 +01:00
while ( j -- ) {
2015-05-01 11:02:32 +02:00
curTab = wins [ i ] . tabs [ j ] , curTabId = vAPI . tabs . stackId ++ ;
iconStateForTabId [ curTabId ] = new IconState ( 0 , "on" , getIconForWindow ( curWindow ) ) ;
curTab . uBlockKnowsIcon = true ;
if ( curWindow . activeTab === curTab ) {
iconStateForTabId [ curTabId ] . active = true ;
}
vAPI . tabs . stack [ curTabId ] = curTab ;
2015-01-30 05:20:28 +01:00
}
2014-11-15 19:15:11 +01:00
}
2015-01-30 05:20:28 +01:00
} ) ( ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
safari . application . addEventListener ( 'open' , function ( e ) {
// ignore windows
if ( e . target instanceof SafariBrowserTab ) {
vAPI . tabs . stack [ vAPI . tabs . stackId ++ ] = e . target ;
}
} , true ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
safari . application . addEventListener ( 'close' , function ( e ) {
// ignore windows
if ( ! ( e . target instanceof SafariBrowserTab ) ) {
return ;
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
var tabId = vAPI . tabs . getTabId ( e . target ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
if ( tabId !== - 1 ) {
// to not add another listener, put this here
// instead of vAPI.tabs.registerListeners
if ( typeof vAPI . tabs . onClosed === 'function' ) {
vAPI . tabs . onClosed ( tabId ) ;
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
delete vAPI . tabs . stack [ tabId ] ;
2015-05-01 11:02:32 +02:00
delete iconStateForTabId [ tabId ] ;
2014-11-15 19:15:11 +01:00
}
2015-01-30 05:20:28 +01:00
} , true ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
vAPI . messaging = {
listeners : { } ,
defaultHandler : null ,
2015-02-28 20:18:58 +01:00
NOOPFUNC : function ( ) { } ,
2015-01-30 05:20:28 +01:00
UNHANDLED : 'vAPI.messaging.notHandled'
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . messaging . listen = function ( listenerName , callback ) {
this . listeners [ listenerName ] = callback ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2015-02-01 22:57:25 +01:00
var CallbackWrapper = function ( request , port ) {
// No need to bind every single time
this . callback = this . proxy . bind ( this ) ;
this . messaging = vAPI . messaging ;
this . init ( request , port ) ;
} ;
CallbackWrapper . junkyard = [ ] ;
CallbackWrapper . factory = function ( request , port ) {
var wrapper = CallbackWrapper . junkyard . pop ( ) ;
if ( wrapper ) {
wrapper . init ( request , port ) ;
return wrapper ;
}
return new CallbackWrapper ( request , port ) ;
} ;
CallbackWrapper . prototype . init = function ( request , port ) {
this . request = request ;
this . port = port ;
} ;
CallbackWrapper . prototype . proxy = function ( response ) {
this . port . dispatchMessage ( this . request . name , {
requestId : this . request . message . requestId ,
channelName : this . request . message . channelName ,
msg : response !== undefined ? response : null
} ) ;
this . port = this . request = null ;
CallbackWrapper . junkyard . push ( this ) ;
} ;
2015-01-30 05:20:28 +01:00
vAPI . messaging . onMessage = function ( request ) {
var callback = vAPI . messaging . NOOPFUNC ;
if ( request . message . requestId !== undefined ) {
2015-02-01 22:57:25 +01:00
callback = CallbackWrapper . factory ( request , request . target . page ) . callback ;
2015-01-30 05:20:28 +01:00
}
var sender = {
tab : {
id : vAPI . tabs . getTabId ( request . target )
}
2014-11-15 19:15:11 +01:00
} ;
2015-01-30 05:20:28 +01:00
// Specific handler
var r = vAPI . messaging . UNHANDLED ;
var listener = vAPI . messaging . listeners [ request . message . channelName ] ;
if ( typeof listener === 'function' ) {
r = listener ( request . message . msg , sender , callback ) ;
}
if ( r !== vAPI . messaging . UNHANDLED ) {
return ;
2014-11-15 19:15:11 +01:00
}
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
// Default handler
r = vAPI . messaging . defaultHandler ( request . message . msg , sender , callback ) ;
if ( r !== vAPI . messaging . UNHANDLED ) {
return ;
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
console . error ( 'µBlock> messaging > unknown request: %o' , request . message ) ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
// Unhandled:
// Need to callback anyways in case caller expected an answer, or
// else there is a memory leak on caller's side
callback ( ) ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . messaging . setup = function ( defaultHandler ) {
// Already setup?
if ( this . defaultHandler !== null ) {
return ;
}
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
if ( typeof defaultHandler !== 'function' ) {
defaultHandler = function ( ) {
return vAPI . messaging . UNHANDLED ;
} ;
}
this . defaultHandler = defaultHandler ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
// the third parameter must stay false (bubbling), so later
// onBeforeRequest will use true (capturing), where we can invoke
// stopPropagation() (this way this.onMessage won't be fired)
safari . application . addEventListener ( 'message' , this . onMessage , false ) ;
2014-11-23 18:21:06 +01:00
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . messaging . broadcast = function ( message ) {
message = {
broadcast : true ,
msg : message
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
for ( var tabId in vAPI . tabs . stack ) {
vAPI . tabs . stack [ tabId ] . page . dispatchMessage ( 'broadcast' , message ) ;
}
} ;
2014-12-07 20:51:49 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-12-01 20:45:00 +01:00
2015-01-30 05:20:28 +01:00
vAPI . net = { } ;
2015-01-11 18:41:52 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
// Fast `contains`
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
Array . prototype . contains = function ( a ) {
var b = this . length ;
while ( b -- ) {
if ( this [ b ] === a ) {
return true ;
}
}
return false ;
2014-12-28 21:26:06 +01:00
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . net . registerListeners = function ( ) {
var µb = µBlock ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
// Until Safari has more specific events, those are instead handled
// in the onBeforeRequestAdapter; clean them up so they're garbage-collected
vAPI . net . onBeforeSendHeaders = null ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
var onBeforeRequest = vAPI . net . onBeforeRequest ,
onBeforeRequestClient = onBeforeRequest . callback ,
2015-04-23 03:32:54 +02:00
onHeadersReceivedClient = vAPI . net . onHeadersReceived . callback ,
2015-01-30 05:20:28 +01:00
blockableTypes = onBeforeRequest . types ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
var onBeforeRequestAdapter = function ( e ) {
if ( e . name !== "canLoad" ) {
return ;
}
e . stopPropagation && e . stopPropagation ( ) ;
2015-02-25 19:37:33 +01:00
if ( e . message . type === "main_frame" ) {
vAPI . tabs . onNavigation ( {
url : e . message . url ,
frameId : 0 ,
tabId : vAPI . tabs . getTabId ( e . target )
} ) ;
2015-03-26 18:31:18 +01:00
e . message . hostname = µb . URI . hostnameFromURI ( e . message . url ) ;
e . message . tabId = vAPI . tabs . getTabId ( e . target ) ;
2015-04-23 03:32:54 +02:00
e . message . responseHeaders = [ ] ;
onBeforeRequestClient ( e . message ) ;
var blockVerdict = onHeadersReceivedClient ( e . message ) ;
if ( blockVerdict && blockVerdict . responseHeaders ) {
2015-03-26 18:31:18 +01:00
e . message = false ;
}
else {
e . message = true ;
}
return ;
2015-02-25 19:37:33 +01:00
}
2015-01-30 05:20:28 +01:00
switch ( e . message . type ) {
case "popup" :
2015-04-14 02:58:50 +02:00
var openerTabId = vAPI . tabs . getTabId ( e . target ) . toString ( ) ;
var shouldBlock = ! ! vAPI . tabs . onPopup ( {
targetURL : e . message . url ,
targetTabId : "preempt" ,
openerTabId : openerTabId
} ) ;
if ( shouldBlock ) {
2015-03-23 19:01:50 +01:00
e . message = false ;
2015-02-06 00:23:56 +01:00
}
else {
2015-04-14 02:58:50 +02:00
vAPI . tabs . popupCandidate = openerTabId ;
e . message = true ;
2015-01-30 05:20:28 +01:00
}
break ;
case "popstate" :
vAPI . tabs . onUpdated ( vAPI . tabs . getTabId ( e . target ) , {
url : e . message . url
} , {
url : e . message . url
} ) ;
break ;
default :
e . message . hostname = µb . URI . hostnameFromURI ( e . message . url ) ;
e . message . tabId = vAPI . tabs . getTabId ( e . target ) ;
var blockVerdict = onBeforeRequestClient ( e . message ) ;
if ( blockVerdict && blockVerdict . cancel ) {
e . message = false ;
2015-02-06 00:23:56 +01:00
return ;
2015-02-25 19:37:33 +01:00
}
else {
2015-01-30 05:20:28 +01:00
e . message = true ;
2015-02-06 00:23:56 +01:00
return ;
2015-01-30 05:20:28 +01:00
}
}
return ;
} ;
safari . application . addEventListener ( "message" , onBeforeRequestAdapter , true ) ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
vAPI . contextMenu = {
contextMap : {
frame : 'insideFrame' ,
link : 'linkHref' ,
image : 'srcUrl' ,
editable : 'editable'
2014-12-28 21:26:06 +01:00
}
2015-01-30 05:20:28 +01:00
} ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-12-19 21:24:30 +01:00
2015-01-30 05:20:28 +01:00
vAPI . contextMenu . create = function ( details , callback ) {
var contexts = details . contexts ;
var menuItemId = details . id ;
var menuTitle = details . title ;
2014-12-28 21:26:06 +01:00
2015-01-30 05:20:28 +01:00
if ( Array . isArray ( contexts ) && contexts . length ) {
contexts = contexts . indexOf ( 'all' ) === - 1 ? contexts : null ;
} else {
// default in Chrome
contexts = [ 'page' ] ;
}
this . onContextMenu = function ( e ) {
var uI = e . userInfo ;
if ( ! uI || /^https?:\/\//i . test ( uI . pageUrl ) === false ) {
return ;
}
if ( contexts ) {
var invalidContext = true ;
var ctxMap = vAPI . contextMenu . contextMap ;
for ( var i = 0 ; i < contexts . length ; i ++ ) {
var ctx = contexts [ i ] ;
if ( ctx === 'audio' || ctx === 'video' ) {
if ( uI [ ctxMap [ 'image' ] ] && uI . tagName === ctx ) {
invalidContext = false ;
break ;
}
} else if ( uI [ ctxMap [ ctx ] ] ) {
2014-12-28 21:26:06 +01:00
invalidContext = false ;
break ;
2015-01-30 05:20:28 +01:00
} else if ( ctx === 'page' ) {
if ( ! ( uI . insideFrame || uI . linkHref || uI . mediaType || uI . editable ) ) {
invalidContext = false ;
break ;
}
2014-11-15 19:15:11 +01:00
}
}
2015-01-30 05:20:28 +01:00
if ( invalidContext ) {
return ;
}
2014-12-28 21:26:06 +01:00
}
2015-01-30 05:20:28 +01:00
e . contextMenu . appendContextMenuItem ( menuItemId , menuTitle ) ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
this . onContextMenuCmd = function ( e ) {
if ( e . command === menuItemId ) {
var tab = e . currentTarget . activeBrowserWindow . activeTab ;
e . userInfo . menuItemId = menuItemId ;
callback ( e . userInfo , tab ? {
id : vAPI . tabs . getTabId ( tab ) ,
url : tab . url
} : undefined ) ;
}
} ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
safari . application . addEventListener ( 'contextmenu' , this . onContextMenu ) ;
safari . application . addEventListener ( 'command' , this . onContextMenuCmd ) ;
} ;
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-23 18:21:06 +01:00
2015-01-30 05:20:28 +01:00
vAPI . contextMenu . remove = function ( ) {
safari . application . removeEventListener ( 'contextmenu' , this . onContextMenu ) ;
safari . application . removeEventListener ( 'command' , this . onContextMenuCmd ) ;
this . onContextMenu = null ;
this . onContextMenuCmd = null ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
vAPI . lastError = function ( ) {
return null ;
} ;
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-11-15 19:15:11 +01:00
2015-01-30 05:20:28 +01:00
// This is called only once, when everything has been loaded in memory after
// the extension was launched. It can be used to inject content scripts
// in already opened web pages, to remove whatever nuisance could make it to
// the web pages before uBlock was ready.
2014-12-17 14:02:37 +01:00
2015-01-30 05:20:28 +01:00
vAPI . onLoadAllCompleted = function ( ) { } ;
2014-12-17 14:02:37 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2014-12-17 14:02:37 +01:00
2015-01-30 05:20:28 +01:00
vAPI . punycodeHostname = function ( hostname ) {
return hostname ;
} ;
2015-01-14 23:45:55 +01:00
2015-01-30 05:20:28 +01:00
vAPI . punycodeURL = function ( url ) {
return url ;
} ;
2015-01-14 23:45:55 +01:00
2015-01-30 05:20:28 +01:00
/******************************************************************************/
2015-03-11 06:24:06 +01:00
function initStorageLib ( ) {
/ * !
localForage -- Offline Storage , Improved
Version 1.2 . 2
https : //mozilla.github.io/localForage
( c ) 2013 - 2015 Mozilla , Apache License 2.0
* /
! function ( ) { var a , b , c , d ; ! function ( ) { var e = { } , f = { } ; a = function ( a , b , c ) { e [ a ] = { deps : b , callback : c } } , d = c = b = function ( a ) { function c ( b ) { if ( "." !== b . charAt ( 0 ) ) return b ; for ( var c = b . split ( "/" ) , d = a . split ( "/" ) . slice ( 0 , - 1 ) , e = 0 , f = c . length ; f > e ; e ++ ) { var g = c [ e ] ; if ( ".." === g ) d . pop ( ) ; else { if ( "." === g ) continue ; d . push ( g ) } } return d . join ( "/" ) } if ( d . _eak _seen = e , f [ a ] ) return f [ a ] ; if ( f [ a ] = { } , ! e [ a ] ) throw new Error ( "Could not find module " + a ) ; for ( var g , h = e [ a ] , i = h . deps , j = h . callback , k = [ ] , l = 0 , m = i . length ; m > l ; l ++ ) k . push ( "exports" === i [ l ] ? g = { } : b ( c ( i [ l ] ) ) ) ; var n = j . apply ( this , k ) ; return f [ a ] = g || n } } ( ) , a ( "promise/all" , [ "./utils" , "exports" ] , function ( a , b ) { "use strict" ; function c ( a ) { var b = this ; if ( ! d ( a ) ) throw new TypeError ( "You must pass an array to all." ) ; return new b ( function ( b , c ) { function d ( a ) { return function ( b ) { f ( a , b ) } } function f ( a , c ) { h [ a ] = c , 0 === -- i && b ( h ) } var g , h = [ ] , i = a . length ; 0 === i && b ( [ ] ) ; for ( var j = 0 ; j < a . length ; j ++ ) g = a [ j ] , g && e ( g . then ) ? g . then ( d ( j ) , c ) : f ( j , g ) } ) } var d = a . isArray , e = a . isFunction ; b . all = c } ) , a ( "promise/asap" , [ "exports" ] , function ( a ) { "use strict" ; function b ( ) { return function ( ) { process . nextTick ( e ) } } function c ( ) { var a = 0 , b = new i ( e ) , c = document . createTextNode ( "" ) ; return b . observe ( c , { characterData : ! 0 } ) , function ( ) { c . data = a = ++ a % 2 } } function d ( ) { return function ( ) { j . setTimeout ( e , 1 ) } } function e ( ) { for ( var a = 0 ; a < k . length ; a ++ ) { var b = k [ a ] , c = b [ 0 ] , d = b [ 1 ] ; c ( d ) } k = [ ] } function f ( a , b ) { var c = k . push ( [ a , b ] ) ; 1 === c && g ( ) } var g , h = "undefined" != typeof window ? window : { } , i = h . MutationObserver || h . WebKitMutationObserver , j = "undefined" != typeof global ? global : void 0 === this ? window : this , k = [ ] ; g = "undefined" != typeof process && "[object process]" === { } . toString . call ( process ) ? b ( ) : i ? c ( ) : d ( ) , a . asap = f } ) , a ( "promise/config" , [ "exports" ] , function ( a ) { "use strict" ; function b ( a , b ) { return 2 !== arguments . length ? c [ a ] : void ( c [ a ] = b ) } var c = { instrument : ! 1 } ; a . config = c , a . configure = b } ) , a ( "promise/polyfill" , [ "./promise" , "./utils" , "exports" ] , function ( a , b , c ) { "use strict" ; function d ( ) { var a ; a = "undefined" != typeof global ? global : "undefined" != typeof window && window . document ? window : self ; var b = "Promise" in a && "resolve" in a . Promise && "reject" in a . Promise && "all" in a . Promise && "race" in a . Promise && function ( ) { var b ; return new a . Promise ( function ( a ) { b = a } ) , f ( b ) } ( ) ; b || ( a . Promise = e ) } var e = a . Promise , f = b . isFunction ; c . polyfill = d } ) , a ( "promise/promise" , [ "./config" , "./utils" , "./all" , "./race" , "./resolve" , "./reject" , "./asap" , "exports" ] , function ( a , b , c , d , e , f , g , h ) { "use strict" ; function i ( a ) { if ( ! v ( a ) ) throw new TypeError ( "You must pass a resolver function as the first argument to the promise constructor" ) ; if ( ! ( this instanceof i ) ) throw new TypeError ( "Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function." ) ; this . _subscribers = [ ] , j ( a , this ) } function j ( a , b ) { function c ( a ) { o ( b , a ) } function d ( a ) { q ( b , a ) } try { a ( c , d ) } catch ( e ) { d ( e ) } } function k ( a , b , c , d ) { var e , f , g , h , i = v ( c ) ; if ( i ) try { e = c ( d ) , g = ! 0 } catch ( j ) { h = ! 0 , f = j } else e = d , g = ! 0 ; n ( b , e ) || ( i && g ? o ( b , e ) : h ? q ( b , f ) : a === D ? o ( b , e ) : a === E && q ( b , e ) ) } function l ( a , b , c , d ) { var e = a . _subscribers , f = e . length ; e [ f ] = b , e [ f + D ] = c , e [ f + E ] = d } function m ( a , b ) { for ( var c , d , e = a . _subscribers , f = a . _detail , g = 0 ; g < e . length ; g += 3 ) c = e [ g ] , d = e [ g + b ] , k ( b , c , d , f ) ; a . _subscribers = null } function n ( a , b ) { var c , d = null ; try { if ( a === b ) throw new TypeError ( "A promises callback cannot return that same promise." ) ; if ( u ( b ) && ( d = b . then , v ( d ) ) ) return d . call ( b , function ( d ) { return c ? ! 0 : ( c = ! 0 , void ( b !== d ? o ( a , d ) : p ( a , d ) ) ) } , function ( b ) { return c ? ! 0 : ( c = ! 0 , void q ( a , b ) ) } ) , ! 0 } catch ( e ) { return c ? ! 0 : ( q ( a , e ) , ! 0 ) } return ! 1 } function o ( a , b ) { a === b ? p ( a , b ) : n ( a , b ) || p ( a , b ) } function p ( a , b ) { a . _state === B && ( a . _state = C , a . _detail = b , t . async ( r , a ) ) } function q ( a , b ) { a . _state === B && ( a . _state = C , a . _detail = b , t . async ( s , a ) ) } function r ( a ) { m ( a , a . _state = D ) } function s ( a ) { m ( a , a . _state = E ) } var t = a . config , u = ( a . configure , b . objectOrFunction ) , v = b . isFunction , w = ( b . now , c . all ) , x = d . race , y = e . resolve , z = f . reject , A = g . asap ; t . async = A ; var B = void 0 , C = 0 , D = 1 , E = 2 ; i . prototype = { constructor : i , _state : void 0 , _detail : void 0 , _subscribers : void 0 , then : function ( a , b ) { var c = this , d = new this . constructor ( function ( ) { } ) ; if ( this . _state ) { var e = arguments ; t . async ( function ( ) { k ( c . _state , d , e [ c . _state - 1 ] , c . _detail ) } ) } else l ( this , d , a , b ) ; return
}
2015-01-14 23:45:55 +01:00
2014-11-15 19:15:11 +01:00
} ) ( ) ;