Merge branch 'develop' of github.com:martini/zammad into develop

This commit is contained in:
Felix Niklas 2015-07-20 12:57:18 +02:00
commit db863f7b1b
44 changed files with 1992 additions and 407 deletions

View file

@ -286,16 +286,24 @@ class App.ControllerTabs extends App.Controller
tabs: @tabs tabs: @tabs
) )
# insert content
for tab in @tabs for tab in @tabs
@el.find('.tab-content').append('<div class="tab-pane" id="' + tab.target + '"></div>') @el.find('.tab-content').append("<div class=\"tab-pane\" id=\"#{tab.target}\"></div>")
if tab.controller if tab.controller
params = tab.params || {} params = tab.params || {}
params.el = @el.find( '#' + tab.target ) params.name = tab.name
params.target = tab.target
params.el = @el.find( "##{tab.target}" )
new tab.controller( params ) new tab.controller( params )
# check if tabs need to be hidden
if @tabs.length <= 1
@el.find('.nav-tabs').addClass('hide')
# set last or first tab to active
@lastActiveTab = @Config.get('lastTab') @lastActiveTab = @Config.get('lastTab')
if @lastActiveTab && @el.find('.nav-tabs li a[href="' + @lastActiveTab + '"]')[0] if @lastActiveTab && @el.find(".nav-tabs li a[href=#{@lastActiveTab}]")[0]
@el.find('.nav-tabs li a[href="' + @lastActiveTab + '"]').tab('show') @el.find(".nav-tabs li a[href=#{@lastActiveTab}]").tab('show')
else else
@el.find('.nav-tabs li:first a').tab('show') @el.find('.nav-tabs li:first a').tab('show')

View file

@ -26,21 +26,30 @@ class App.SettingsArea extends App.Controller
area: @area area: @area
) )
# filter online service settings
if App.Config.get('system_online_service')
settings = _.filter(settings, (setting) ->
return if setting.online_service
return if setting.preferences && setting.preferences.online_service_disable
setting
)
return if _.isEmpty(settings)
# sort by prio # sort by prio
settings = _.sortBy( settings, (setting) -> settings = _.sortBy( settings, (setting) ->
return if !setting.preferences return if !setting.preferences
setting.preferences.prio setting.preferences.prio
) )
html = $('<div></div>') elements = []
for setting in settings for setting in settings
if setting.name is 'product_logo' if setting.name is 'product_logo'
item = new App.SettingsAreaLogo( setting: setting ) item = new App.SettingsAreaLogo( setting: setting )
else else
item = new App.SettingsAreaItem( setting: setting ) item = new App.SettingsAreaItem( setting: setting )
html.append( item.el ) elements.push item.el
@html html @html elements
class App.SettingsAreaItem extends App.Controller class App.SettingsAreaItem extends App.Controller
events: events:
@ -67,13 +76,13 @@ class App.SettingsAreaItem extends App.Controller
# item # item
@html App.view('settings/item')( @html App.view('settings/item')(
setting: @setting, setting: @setting
) )
new App.ControllerForm( new App.ControllerForm(
el: @el.find('.form-item'), el: @el.find('.form-item'),
model: { configure_attributes: @configure_attributes, className: '' }, model: { configure_attributes: @configure_attributes, className: '' }
autofocus: false, autofocus: false
) )
update: (e) => update: (e) =>
@ -103,7 +112,6 @@ class App.SettingsAreaItem extends App.Controller
@setting.save( @setting.save(
done: => done: =>
ui.formEnable(e) ui.formEnable(e)
App.Event.trigger 'notify', { App.Event.trigger 'notify', {
type: 'success' type: 'success'
msg: App.i18n.translateContent('Update successful!') msg: App.i18n.translateContent('Update successful!')
@ -112,7 +120,6 @@ class App.SettingsAreaItem extends App.Controller
# rerender ui || get new collections and session data # rerender ui || get new collections and session data
if @setting.preferences if @setting.preferences
if @setting.preferences.render if @setting.preferences.render
ui.render() ui.render()
App.Event.trigger( 'ui:rerender' ) App.Event.trigger( 'ui:rerender' )
@ -121,6 +128,11 @@ class App.SettingsAreaItem extends App.Controller
App.Auth.loginCheck() App.Auth.loginCheck()
fail: => fail: =>
ui.formEnable(e) ui.formEnable(e)
App.Event.trigger 'notify', {
type: 'error'
msg: App.i18n.translateContent('Can\'t update item!')
timeout: 2000
}
) )
class App.SettingsAreaLogo extends App.Controller class App.SettingsAreaLogo extends App.Controller

View file

@ -5,7 +5,7 @@ class Branding extends App.ControllerTabs
return if !@authenticate() return if !@authenticate()
@title 'Branding', true @title 'Branding', true
@tabs = [ @tabs = [
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Branding' } }, { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Branding' } }
] ]
@render() @render()
@ -15,12 +15,13 @@ class System extends App.ControllerTabs
super super
return if !@authenticate() return if !@authenticate()
@title 'System', true @title 'System', true
@tabs = [ @tabs = []
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Base' } }, if !App.Config.get('system_online_service')
{ name: 'Storage', 'target': 'storage', controller: App.SettingsArea, params: { area: 'System::Storage' } }, @tabs.push { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Base' } }
{ name: 'Geo Services', 'target': 'geo', controller: App.SettingsArea, params: { area: 'System::Geo' } }, @tabs.push { name: 'Services', 'target': 'services', controller: App.SettingsArea, params: { area: 'System::Services' } }
{ name: 'Frontend', 'target': 'ui', controller: App.SettingsArea, params: { area: 'System::UI' } }, if !App.Config.get('system_online_service')
] @tabs.push { name: 'Storage', 'target': 'storage', controller: App.SettingsArea, params: { area: 'System::Storage' } }
@tabs.push { name: 'Frontend', 'target': 'ui', controller: App.SettingsArea, params: { area: 'System::UI' } }
@render() @render()
class Security extends App.ControllerTabs class Security extends App.ControllerTabs
@ -30,11 +31,10 @@ class Security extends App.ControllerTabs
return if !@authenticate() return if !@authenticate()
@title 'Security', true @title 'Security', true
@tabs = [ @tabs = [
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Security::Base' } }, { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Security::Base' } }
# { name: 'Authentication', 'target': 'auth', controller: App.SettingsArea, params: { area: 'Security::Authentication' } }, # { name: 'Authentication', 'target': 'auth', controller: App.SettingsArea, params: { area: 'Security::Authentication' } }
{ name: 'Password', 'target': 'password', controller: App.SettingsArea, params: { area: 'Security::Password' } }, { name: 'Password', 'target': 'password', controller: App.SettingsArea, params: { area: 'Security::Password' } }
{ name: 'Third-Party Applications', 'target': 'third_party_auth', controller: App.SettingsArea, params: { area: 'Security::ThirdPartyAuthentication' } }, { name: 'Third-Party Applications', 'target': 'third_party_auth', controller: App.SettingsArea, params: { area: 'Security::ThirdPartyAuthentication' } }
# { name: 'Session', 'target': 'session', controller: '' },
] ]
@render() @render()
@ -45,8 +45,8 @@ class Import extends App.ControllerTabs
return if !@authenticate() return if !@authenticate()
@title 'Import', true @title 'Import', true
@tabs = [ @tabs = [
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Import::Base' } }, { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Import::Base' } }
{ name: 'OTRS', 'target': 'otrs', controller: App.SettingsArea, params: { area: 'Import::OTRS' } }, { name: 'OTRS', 'target': 'otrs', controller: App.SettingsArea, params: { area: 'Import::OTRS' } }
] ]
@render() @render()
@ -57,15 +57,14 @@ class Ticket extends App.ControllerTabs
return if !@authenticate() return if !@authenticate()
@title 'Ticket', true @title 'Ticket', true
@tabs = [ @tabs = [
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Ticket::Base' } }, { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Ticket::Base' } }
{ name: 'Number', 'target': 'number', controller: App.SettingsArea, params: { area: 'Ticket::Number' } }, { name: 'Number', 'target': 'number', controller: App.SettingsArea, params: { area: 'Ticket::Number' } }
# { name: 'Sender Format', 'target': 'sender-format', controller: App.SettingsArea, params: { area: 'Ticket::SenderFormat' } },
] ]
@render() @render()
App.Config.set( 'SettingBranding', { prio: 1200, parent: '#settings', name: 'Branding', target: '#settings/branding', controller: Branding, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'SettingBranding', { prio: 1200, parent: '#settings', name: 'Branding', target: '#settings/branding', controller: Branding, role: ['Admin'] }, 'NavBarAdmin' )
App.Config.set( 'SettingSystem', { prio: 1400, parent: '#settings', name: 'System', target: '#settings/system', controller: System, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'SettingSystem', { prio: 1400, parent: '#settings', name: 'System', target: '#settings/system', controller: System, role: ['Admin'] }, 'NavBarAdmin' )
App.Config.set( 'SettingSecurity', { prio: 1500, parent: '#settings', name: 'Security', target: '#settings/security', controller: Security, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'SettingSecurity', { prio: 1600, parent: '#settings', name: 'Security', target: '#settings/security', controller: Security, role: ['Admin'] }, 'NavBarAdmin' )
App.Config.set( 'SettingTicket', { prio: 1600, parent: '#settings', name: 'Ticket', target: '#settings/ticket', controller: Ticket, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'SettingTicket', { prio: 1700, parent: '#settings', name: 'Ticket', target: '#settings/ticket', controller: Ticket, role: ['Admin'] }, 'NavBarAdmin' )
App.Config.set( 'SettingImport', { prio: 1700, parent: '#settings', name: 'Import', target: '#settings/import', controller: Import, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'SettingImport', { prio: 1800, parent: '#settings', name: 'Import', target: '#settings/import', controller: Import, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -1,7 +1,7 @@
class FFlt35 class FFlt35
constructor: -> constructor: ->
data = App.Browser.detection() data = App.Browser.detection()
if data.browser is 'Firefox' && data.version && data.version < 35 if data.browser.name is 'Firefox' && data.browser.major && data.browser.major < 35
# for firefox lower 35 we need to set a class to hide own dropdown images # for firefox lower 35 we need to set a class to hide own dropdown images
# whole file can be removed after dropping firefox 34 and lower support # whole file can be removed after dropping firefox 34 and lower support

View file

@ -45,16 +45,15 @@ class _Singleton
# detect color support # detect color support
@colorSupport = false @colorSupport = false
data = App.Browser.detection() data = App.Browser.detection()
if data if data.browser
if data.browser is 'Chrome' if data.browser.name is 'Chrome'
@colorSupport = true @colorSupport = true
else if data.browser is 'Firefox' else if data.browser.anem is 'Firefox'
if data.version >= 31.0 if data.browser.major >= 31.0
@colorSupport = true @colorSupport = true
else if data.browser is 'Safari' else if data.browser.name is 'Safari'
@colorSupport = true @colorSupport = true
configReady: -> configReady: ->
for type, value of @currentConfig for type, value of @currentConfig
if type is 'module' || type is 'content' if type is 'module' || type is 'content'

View file

@ -17,12 +17,14 @@ get used browser
} }
### ###
class App.Browser class App.Browser
@detection: -> @detection: ->
parser = new UAParser()
data = data =
browser: @searchString(@dataBrowser) or "An unknown browser" browser: parser.getBrowser()
version: @searchVersion(navigator.userAgent) or @searchVersion(navigator.appVersion) or "an unknown version" device: parser.getDevice()
os: @searchString(@dataOS) or "an unknown os" os: parser.getOS()
@check: -> @check: ->
data = @detection() data = @detection()
@ -36,114 +38,21 @@ class App.Browser
Opera: 22 Opera: 22
# disable id older # disable id older
if data.browser && data.version if data.browser
if map[data.browser] && data.version < map[data.browser] if map[data.browser.name] && data.browser.major < map[data.browser.name]
@message(data, data.browser, map[data.browser]) @message(data, map[data.browser.name])
console.log('Browser not supported') console.log('Browser not supported')
return false return false
# allow browser # allow browser
return true true
@message: (data, browser, version) -> @message: (data, version) ->
new App.ControllerModal( new App.ControllerModal(
head: 'Browser too old!' head: 'Browser too old!'
message: "Your Browser is not supported (#{data.browser} #{data.version} #{data.OS}). Please use a newer one (e. g. #{browser} #{version} or higher)." message: "Your Browser is not supported (#{data.browser.name} #{data.browser.major} on #{data.os.name}). Please use a newer one (e. g. #{data.browser.name} #{version} or higher)."
close: false close: false
backdrop: false backdrop: false
keyboard: false keyboard: false
shown: true shown: true
) )
@searchString: (data) ->
i = 0
while i < data.length
dataString = data[i].string
dataProp = data[i].prop
@versionSearchString = data[i].versionSearch or data[i].identity
if dataString
return data[i].identity unless dataString.indexOf(data[i].subString) is -1
else return data[i].identity if dataProp
i++
@searchVersion: (dataString) ->
index = dataString.indexOf(@versionSearchString)
return if index is -1
parseFloat dataString.substring(index + @versionSearchString.length + 1)
@dataBrowser: [
string: navigator.userAgent
subString: "Chrome"
identity: "Chrome"
,
string: navigator.userAgent
subString: "OmniWeb"
versionSearch: "OmniWeb/"
identity: "OmniWeb"
,
string: navigator.vendor
subString: "Apple"
identity: "Safari"
versionSearch: "Version"
,
prop: window.opera
identity: "Opera"
versionSearch: "Version"
,
string: navigator.vendor
subString: "iCab"
identity: "iCab"
,
string: navigator.vendor
subString: "KDE"
identity: "Konqueror"
,
string: navigator.userAgent
subString: "Firefox"
identity: "Firefox"
,
string: navigator.vendor
subString: "Camino"
identity: "Camino"
,
# for newer Netscapes (6+)
string: navigator.userAgent
subString: "Netscape"
identity: "Netscape"
,
string: navigator.userAgent
subString: "MSIE"
identity: "Explorer"
versionSearch: "MSIE"
,
string: navigator.userAgent
subString: "Gecko"
identity: "Mozilla"
versionSearch: "rv"
,
# for older Netscapes (4-)
string: navigator.userAgent
subString: "Mozilla"
identity: "Netscape"
versionSearch: "Mozilla"
]
@dataOS: [
string: navigator.platform
subString: "Win"
identity: "Windows"
,
string: navigator.platform
subString: "Mac"
identity: "Mac"
,
string: navigator.userAgent
subString: "iPhone"
identity: "iPhone/iPod"
,
string: navigator.platform
subString: "Linux"
identity: "Linux"
]

View file

@ -0,0 +1,868 @@
/**
* UAParser.js v0.7.8
* Lightweight JavaScript-based User-Agent string parser
* https://github.com/faisalman/ua-parser-js
*
* Copyright © 2012-2015 Faisal Salman <fyzlman@gmail.com>
* Dual licensed under GPLv2 & MIT
*/
(function (window, undefined) {
'use strict';
//////////////
// Constants
/////////////
var LIBVERSION = '0.7.8',
EMPTY = '',
UNKNOWN = '?',
FUNC_TYPE = 'function',
UNDEF_TYPE = 'undefined',
OBJ_TYPE = 'object',
STR_TYPE = 'string',
MAJOR = 'major', // deprecated
MODEL = 'model',
NAME = 'name',
TYPE = 'type',
VENDOR = 'vendor',
VERSION = 'version',
ARCHITECTURE= 'architecture',
CONSOLE = 'console',
MOBILE = 'mobile',
TABLET = 'tablet',
SMARTTV = 'smarttv',
WEARABLE = 'wearable',
EMBEDDED = 'embedded';
///////////
// Helper
//////////
var util = {
extend : function (regexes, extensions) {
for (var i in extensions) {
if ("browser cpu device engine os".indexOf(i) !== -1 && extensions[i].length % 2 === 0) {
regexes[i] = extensions[i].concat(regexes[i]);
}
}
return regexes;
},
has : function (str1, str2) {
if (typeof str1 === "string") {
return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1;
} else {
return false;
}
},
lowerize : function (str) {
return str.toLowerCase();
},
major : function (version) {
return typeof(version) === STR_TYPE ? version.split(".")[0] : undefined;
}
};
///////////////
// Map helper
//////////////
var mapper = {
rgx : function () {
var result, i = 0, j, k, p, q, matches, match, args = arguments;
// loop through all regexes maps
while (i < args.length && !matches) {
var regex = args[i], // even sequence (0,2,4,..)
props = args[i + 1]; // odd sequence (1,3,5,..)
// construct object barebones
if (typeof result === UNDEF_TYPE) {
result = {};
for (p in props) {
q = props[p];
if (typeof q === OBJ_TYPE) {
result[q[0]] = undefined;
} else {
result[q] = undefined;
}
}
}
// try matching uastring with regexes
j = k = 0;
while (j < regex.length && !matches) {
matches = regex[j++].exec(this.getUA());
if (!!matches) {
for (p = 0; p < props.length; p++) {
match = matches[++k];
q = props[p];
// check if given property is actually array
if (typeof q === OBJ_TYPE && q.length > 0) {
if (q.length == 2) {
if (typeof q[1] == FUNC_TYPE) {
// assign modified match
result[q[0]] = q[1].call(this, match);
} else {
// assign given value, ignore regex match
result[q[0]] = q[1];
}
} else if (q.length == 3) {
// check whether function or regex
if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
// call function (usually string mapper)
result[q[0]] = match ? q[1].call(this, match, q[2]) : undefined;
} else {
// sanitize match using given regex
result[q[0]] = match ? match.replace(q[1], q[2]) : undefined;
}
} else if (q.length == 4) {
result[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined;
}
} else {
result[q] = match ? match : undefined;
}
}
}
}
i += 2;
}
return result;
},
str : function (str, map) {
for (var i in map) {
// check if array
if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
for (var j = 0; j < map[i].length; j++) {
if (util.has(map[i][j], str)) {
return (i === UNKNOWN) ? undefined : i;
}
}
} else if (util.has(map[i], str)) {
return (i === UNKNOWN) ? undefined : i;
}
}
return str;
}
};
///////////////
// String map
//////////////
var maps = {
browser : {
oldsafari : {
version : {
'1.0' : '/8',
'1.2' : '/1',
'1.3' : '/3',
'2.0' : '/412',
'2.0.2' : '/416',
'2.0.3' : '/417',
'2.0.4' : '/419',
'?' : '/'
}
}
},
device : {
amazon : {
model : {
'Fire Phone' : ['SD', 'KF']
}
},
sprint : {
model : {
'Evo Shift 4G' : '7373KT'
},
vendor : {
'HTC' : 'APA',
'Sprint' : 'Sprint'
}
}
},
os : {
windows : {
version : {
'ME' : '4.90',
'NT 3.11' : 'NT3.51',
'NT 4.0' : 'NT4.0',
'2000' : 'NT 5.0',
'XP' : ['NT 5.1', 'NT 5.2'],
'Vista' : 'NT 6.0',
'7' : 'NT 6.1',
'8' : 'NT 6.2',
'8.1' : 'NT 6.3',
'10' : ['NT 6.4', 'NT 10.0'],
'RT' : 'ARM'
}
}
}
};
//////////////
// Regex map
/////////////
var regexes = {
browser : [[
// Presto based
/(opera\smini)\/([\w\.-]+)/i, // Opera Mini
/(opera\s[mobiletab]+).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet
/(opera).+version\/([\w\.]+)/i, // Opera > 9.80
/(opera)[\/\s]+([\w\.]+)/i // Opera < 9.80
], [NAME, VERSION], [
/\s(opr)\/([\w\.]+)/i // Opera Webkit
], [[NAME, 'Opera'], VERSION], [
// Mixed
/(kindle)\/([\w\.]+)/i, // Kindle
/(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i,
// Lunascape/Maxthon/Netfront/Jasmine/Blazer
// Trident based
/(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i,
// Avant/IEMobile/SlimBrowser/Baidu
/(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer
// Webkit/KHTML based
/(rekonq)\/([\w\.]+)*/i, // Rekonq
/(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi)\/([\w\.-]+)/i
// Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron
], [NAME, VERSION], [
/(trident).+rv[:\s]([\w\.]+).+like\sgecko/i // IE11
], [[NAME, 'IE'], VERSION], [
/(edge)\/((\d+)?[\w\.]+)/i // Microsoft Edge
], [NAME, VERSION], [
/(yabrowser)\/([\w\.]+)/i // Yandex
], [[NAME, 'Yandex'], VERSION], [
/(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon
], [[NAME, /_/g, ' '], VERSION], [
/(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i,
// Chrome/OmniWeb/Arora/Tizen/Nokia
/(uc\s?browser|qqbrowser)[\/\s]?([\w\.]+)/i
// UCBrowser/QQBrowser
], [NAME, VERSION], [
/(dolfin)\/([\w\.]+)/i // Dolphin
], [[NAME, 'Dolphin'], VERSION], [
/((?:android.+)crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS
], [[NAME, 'Chrome'], VERSION], [
/XiaoMi\/MiuiBrowser\/([\w\.]+)/i // MIUI Browser
], [VERSION, [NAME, 'MIUI Browser']], [
/android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)/i // Android Browser
], [VERSION, [NAME, 'Android Browser']], [
/FBAV\/([\w\.]+);/i // Facebook App for iOS
], [VERSION, [NAME, 'Facebook']], [
/version\/([\w\.]+).+?mobile\/\w+\s(safari)/i // Mobile Safari
], [VERSION, [NAME, 'Mobile Safari']], [
/version\/([\w\.]+).+?(mobile\s?safari|safari)/i // Safari & Safari Mobile
], [VERSION, NAME], [
/webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Safari < 3.0
], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [
/(konqueror)\/([\w\.]+)/i, // Konqueror
/(webkit|khtml)\/([\w\.]+)/i
], [NAME, VERSION], [
// Gecko based
/(navigator|netscape)\/([\w\.-]+)/i // Netscape
], [[NAME, 'Netscape'], VERSION], [
/(swiftfox)/i, // Swiftfox
/(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,
// IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror
/(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i,
// Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
/(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i, // Mozilla
// Other
/(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf)[\/\s]?([\w\.]+)/i,
// Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf
/(links)\s\(([\w\.]+)/i, // Links
/(gobrowser)\/?([\w\.]+)*/i, // GoBrowser
/(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser
/(mosaic)[\/\s]([\w\.]+)/i // Mosaic
], [NAME, VERSION]
/* /////////////////////
// Media players BEGIN
////////////////////////
, [
/(apple(?:coremedia|))\/((\d+)[\w\._]+)/i, // Generic Apple CoreMedia
/(coremedia) v((\d+)[\w\._]+)/i
], [NAME, VERSION], [
/(aqualung|lyssna|bsplayer)\/((\d+)?[\w\.-]+)/i // Aqualung/Lyssna/BSPlayer
], [NAME, VERSION], [
/(ares|ossproxy)\s((\d+)[\w\.-]+)/i // Ares/OSSProxy
], [NAME, VERSION], [
/(audacious|audimusicstream|amarok|bass|core|dalvik|gnomemplayer|music on console|nsplayer|psp-internetradioplayer|videos)\/((\d+)[\w\.-]+)/i,
// Audacious/AudiMusicStream/Amarok/BASS/OpenCORE/Dalvik/GnomeMplayer/MoC
// NSPlayer/PSP-InternetRadioPlayer/Videos
/(clementine|music player daemon)\s((\d+)[\w\.-]+)/i, // Clementine/MPD
/(lg player|nexplayer)\s((\d+)[\d\.]+)/i,
/player\/(nexplayer|lg player)\s((\d+)[\w\.-]+)/i // NexPlayer/LG Player
], [NAME, VERSION], [
/(nexplayer)\s((\d+)[\w\.-]+)/i // Nexplayer
], [NAME, VERSION], [
/(flrp)\/((\d+)[\w\.-]+)/i // Flip Player
], [[NAME, 'Flip Player'], VERSION], [
/(fstream|nativehost|queryseekspider|ia-archiver|facebookexternalhit)/i
// FStream/NativeHost/QuerySeekSpider/IA Archiver/facebookexternalhit
], [NAME], [
/(gstreamer) souphttpsrc (?:\([^\)]+\)){0,1} libsoup\/((\d+)[\w\.-]+)/i
// Gstreamer
], [NAME, VERSION], [
/(htc streaming player)\s[\w_]+\s\/\s((\d+)[\d\.]+)/i, // HTC Streaming Player
/(java|python-urllib|python-requests|wget|libcurl)\/((\d+)[\w\.-_]+)/i,
// Java/urllib/requests/wget/cURL
/(lavf)((\d+)[\d\.]+)/i // Lavf (FFMPEG)
], [NAME, VERSION], [
/(htc_one_s)\/((\d+)[\d\.]+)/i // HTC One S
], [[NAME, /_/g, ' '], VERSION], [
/(mplayer)(?:\s|\/)(?:(?:sherpya-){0,1}svn)(?:-|\s)(r\d+(?:-\d+[\w\.-]+){0,1})/i
// MPlayer SVN
], [NAME, VERSION], [
/(mplayer)(?:\s|\/|[unkow-]+)((\d+)[\w\.-]+)/i // MPlayer
], [NAME, VERSION], [
/(mplayer)/i, // MPlayer (no other info)
/(yourmuze)/i, // YourMuze
/(media player classic|nero showtime)/i // Media Player Classic/Nero ShowTime
], [NAME], [
/(nero (?:home|scout))\/((\d+)[\w\.-]+)/i // Nero Home/Nero Scout
], [NAME, VERSION], [
/(nokia\d+)\/((\d+)[\w\.-]+)/i // Nokia
], [NAME, VERSION], [
/\s(songbird)\/((\d+)[\w\.-]+)/i // Songbird/Philips-Songbird
], [NAME, VERSION], [
/(winamp)3 version ((\d+)[\w\.-]+)/i, // Winamp
/(winamp)\s((\d+)[\w\.-]+)/i,
/(winamp)mpeg\/((\d+)[\w\.-]+)/i
], [NAME, VERSION], [
/(ocms-bot|tapinradio|tunein radio|unknown|winamp|inlight radio)/i // OCMS-bot/tap in radio/tunein/unknown/winamp (no other info)
// inlight radio
], [NAME], [
/(quicktime|rma|radioapp|radioclientapplication|soundtap|totem|stagefright|streamium)\/((\d+)[\w\.-]+)/i
// QuickTime/RealMedia/RadioApp/RadioClientApplication/
// SoundTap/Totem/Stagefright/Streamium
], [NAME, VERSION], [
/(smp)((\d+)[\d\.]+)/i // SMP
], [NAME, VERSION], [
/(vlc) media player - version ((\d+)[\w\.]+)/i, // VLC Videolan
/(vlc)\/((\d+)[\w\.-]+)/i,
/(xbmc|gvfs|xine|xmms|irapp)\/((\d+)[\w\.-]+)/i, // XBMC/gvfs/Xine/XMMS/irapp
/(foobar2000)\/((\d+)[\d\.]+)/i, // Foobar2000
/(itunes)\/((\d+)[\d\.]+)/i // iTunes
], [NAME, VERSION], [
/(wmplayer)\/((\d+)[\w\.-]+)/i, // Windows Media Player
/(windows-media-player)\/((\d+)[\w\.-]+)/i
], [[NAME, /-/g, ' '], VERSION], [
/windows\/((\d+)[\w\.-]+) upnp\/[\d\.]+ dlnadoc\/[\d\.]+ (home media server)/i
// Windows Media Server
], [VERSION, [NAME, 'Windows']], [
/(com\.riseupradioalarm)\/((\d+)[\d\.]*)/i // RiseUP Radio Alarm
], [NAME, VERSION], [
/(rad.io)\s((\d+)[\d\.]+)/i, // Rad.io
/(radio.(?:de|at|fr))\s((\d+)[\d\.]+)/i
], [[NAME, 'rad.io'], VERSION]
//////////////////////
// Media players END
////////////////////*/
],
cpu : [[
/(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i // AMD64
], [[ARCHITECTURE, 'amd64']], [
/(ia32(?=;))/i // IA32 (quicktime)
], [[ARCHITECTURE, util.lowerize]], [
/((?:i[346]|x)86)[;\)]/i // IA32
], [[ARCHITECTURE, 'ia32']], [
// PocketPC mistakenly identified as PowerPC
/windows\s(ce|mobile);\sppc;/i
], [[ARCHITECTURE, 'arm']], [
/((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i // PowerPC
], [[ARCHITECTURE, /ower/, '', util.lowerize]], [
/(sun4\w)[;\)]/i // SPARC
], [[ARCHITECTURE, 'sparc']], [
/((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i
// IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC
], [[ARCHITECTURE, util.lowerize]]
],
device : [[
/\((ipad|playbook);[\w\s\);-]+(rim|apple)/i // iPad/PlayBook
], [MODEL, VENDOR, [TYPE, TABLET]], [
/applecoremedia\/[\w\.]+ \((ipad)/ // iPad
], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [
/(apple\s{0,1}tv)/i // Apple TV
], [[MODEL, 'Apple TV'], [VENDOR, 'Apple']], [
/(archos)\s(gamepad2?)/i, // Archos
/(hp).+(touchpad)/i, // HP TouchPad
/(kindle)\/([\w\.]+)/i, // Kindle
/\s(nook)[\w\s]+build\/(\w+)/i, // Nook
/(dell)\s(strea[kpr\s\d]*[\dko])/i // Dell Streak
], [VENDOR, MODEL, [TYPE, TABLET]], [
/(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i // Kindle Fire HD
], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
/(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i // Fire Phone
], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [
/\((ip[honed|\s\w*]+);.+(apple)/i // iPod/iPhone
], [MODEL, VENDOR, [TYPE, MOBILE]], [
/\((ip[honed|\s\w*]+);/i // iPod/iPhone
], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [
/(blackberry)[\s-]?(\w+)/i, // BlackBerry
/(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|huawei|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i,
// BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Huawei/Meizu/Motorola/Polytron
/(hp)\s([\w\s]+\w)/i, // HP iPAQ
/(asus)-?(\w+)/i // Asus
], [VENDOR, MODEL, [TYPE, MOBILE]], [
/\(bb10;\s(\w+)/i // BlackBerry 10
], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [
// Asus Tablets
/android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7)/i
], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [
/(sony)\s(tablet\s[ps])\sbuild\//i, // Sony
/(sony)?(?:sgp.+)\sbuild\//i
], [[VENDOR, 'Sony'], [MODEL, 'Xperia Tablet'], [TYPE, TABLET]], [
/(?:sony)?(?:(?:(?:c|d)\d{4})|(?:so[-l].+))\sbuild\//i
], [[VENDOR, 'Sony'], [MODEL, 'Xperia Phone'], [TYPE, MOBILE]], [
/\s(ouya)\s/i, // Ouya
/(nintendo)\s([wids3u]+)/i // Nintendo
], [VENDOR, MODEL, [TYPE, CONSOLE]], [
/android.+;\s(shield)\sbuild/i // Nvidia
], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [
/(playstation\s[3portablevi]+)/i // Playstation
], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [
/(sprint\s(\w+))/i // Sprint Phones
], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [
/(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i // Lenovo tablets
], [VENDOR, MODEL, [TYPE, TABLET]], [
/(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i, // HTC
/(zte)-(\w+)*/i, // ZTE
/(alcatel|geeksphone|huawei|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i
// Alcatel/GeeksPhone/Huawei/Lenovo/Nexian/Panasonic/Sony
], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [
/(nexus\s9)/i // HTC Nexus 9
], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [
/[\s\(;](xbox(?:\sone)?)[\s\);]/i // Microsoft Xbox
], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [
/(kin\.[onetw]{3})/i // Microsoft Kin
], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [
// Motorola
/\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i,
/mot[\s-]?(\w+)*/i,
/(XT\d{3,4}) build\//i
], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [
/android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i
], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [
/android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n8000|sgh-t8[56]9|nexus 10))/i,
/((SM-T\w+))/i
], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [ // Samsung
/((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-n900))/i,
/(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i,
/sec-((sgh\w+))/i
], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [
/(samsung);smarttv/i
], [VENDOR, MODEL, [TYPE, SMARTTV]], [
/\(dtv[\);].+(aquos)/i // Sharp
], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [
/sie-(\w+)*/i // Siemens
], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [
/(maemo|nokia).*(n900|lumia\s\d+)/i, // Nokia
/(nokia)[\s_-]?([\w-]+)*/i
], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [
/android\s3\.[\s\w;-]{10}(a\d{3})/i // Acer
], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [
/android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i // LG Tablet
], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [
/(lg) netcast\.tv/i // LG SmartTV
], [VENDOR, MODEL, [TYPE, SMARTTV]], [
/(nexus\s[45])/i, // LG
/lg[e;\s\/-]+(\w+)*/i
], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [
/android.+(ideatab[a-z0-9\-\s]+)/i // Lenovo
], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [
/linux;.+((jolla));/i // Jolla
], [VENDOR, MODEL, [TYPE, MOBILE]], [
/((pebble))app\/[\d\.]+\s/i // Pebble
], [VENDOR, MODEL, [TYPE, WEARABLE]], [
/android.+;\s(glass)\s\d/i // Google Glass
], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [
/android.+(\w+)\s+build\/hm\1/i, // Xiaomi Hongmi 'numeric' models
/android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i, // Xiaomi Hongmi
/android.+(mi[\s\-_]*(?:one|one[\s_]plus)?[\s_]*(?:\d\w)?)\s+build/i // Xiaomi Mi
], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [
/(mobile|tablet);.+rv\:.+gecko\//i // Unidentifiable
], [[TYPE, util.lowerize], VENDOR, MODEL]
/*//////////////////////////
// TODO: move to string map
////////////////////////////
/(C6603)/i // Sony Xperia Z C6603
], [[MODEL, 'Xperia Z C6603'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [
/(C6903)/i // Sony Xperia Z 1
], [[MODEL, 'Xperia Z 1'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [
/(SM-G900[F|H])/i // Samsung Galaxy S5
], [[MODEL, 'Galaxy S5'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
/(SM-G7102)/i // Samsung Galaxy Grand 2
], [[MODEL, 'Galaxy Grand 2'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
/(SM-G530H)/i // Samsung Galaxy Grand Prime
], [[MODEL, 'Galaxy Grand Prime'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
/(SM-G313HZ)/i // Samsung Galaxy V
], [[MODEL, 'Galaxy V'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
/(SM-T805)/i // Samsung Galaxy Tab S 10.5
], [[MODEL, 'Galaxy Tab S 10.5'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [
/(SM-G800F)/i // Samsung Galaxy S5 Mini
], [[MODEL, 'Galaxy S5 Mini'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
/(SM-T311)/i // Samsung Galaxy Tab 3 8.0
], [[MODEL, 'Galaxy Tab 3 8.0'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [
/(R1001)/i // Oppo R1001
], [MODEL, [VENDOR, 'OPPO'], [TYPE, MOBILE]], [
/(X9006)/i // Oppo Find 7a
], [[MODEL, 'Find 7a'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [
/(R2001)/i // Oppo YOYO R2001
], [[MODEL, 'Yoyo R2001'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [
/(R815)/i // Oppo Clover R815
], [[MODEL, 'Clover R815'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [
/(U707)/i // Oppo Find Way S
], [[MODEL, 'Find Way S'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [
/(T3C)/i // Advan Vandroid T3C
], [MODEL, [VENDOR, 'Advan'], [TYPE, TABLET]], [
/(ADVAN T1J\+)/i // Advan Vandroid T1J+
], [[MODEL, 'Vandroid T1J+'], [VENDOR, 'Advan'], [TYPE, TABLET]], [
/(ADVAN S4A)/i // Advan Vandroid S4A
], [[MODEL, 'Vandroid S4A'], [VENDOR, 'Advan'], [TYPE, MOBILE]], [
/(V972M)/i // ZTE V972M
], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [
/(i-mobile)\s(IQ\s[\d\.]+)/i // i-mobile IQ
], [VENDOR, MODEL, [TYPE, MOBILE]], [
/(IQ6.3)/i // i-mobile IQ IQ 6.3
], [[MODEL, 'IQ 6.3'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [
/(i-mobile)\s(i-style\s[\d\.]+)/i // i-mobile i-STYLE
], [VENDOR, MODEL, [TYPE, MOBILE]], [
/(i-STYLE2.1)/i // i-mobile i-STYLE 2.1
], [[MODEL, 'i-STYLE 2.1'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [
/(mobiistar touch LAI 512)/i // mobiistar touch LAI 512
], [[MODEL, 'Touch LAI 512'], [VENDOR, 'mobiistar'], [TYPE, MOBILE]], [
/////////////
// END TODO
///////////*/
],
engine : [[
/windows.+\sedge\/([\w\.]+)/i // EdgeHTML
], [VERSION, [NAME, 'EdgeHTML']], [
/(presto)\/([\w\.]+)/i, // Presto
/(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m
/(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links
/(icab)[\/\s]([23]\.[\d\.]+)/i // iCab
], [NAME, VERSION], [
/rv\:([\w\.]+).*(gecko)/i // Gecko
], [VERSION, NAME]
],
os : [[
// Windows based
/microsoft\s(windows)\s(vista|xp)/i // Windows (iTunes)
], [NAME, VERSION], [
/(windows)\snt\s6\.2;\s(arm)/i, // Windows RT
/(windows\sphone(?:\sos)*|windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i
], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [
/(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i
], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [
// Mobile/Embedded OS
/\((bb)(10);/i // BlackBerry 10
], [[NAME, 'BlackBerry'], VERSION], [
/(blackberry)\w*\/?([\w\.]+)*/i, // Blackberry
/(tizen)[\/\s]([\w\.]+)/i, // Tizen
/(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i,
// Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki
/linux;.+(sailfish);/i // Sailfish OS
], [NAME, VERSION], [
/(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i // Symbian
], [[NAME, 'Symbian'], VERSION], [
/\((series40);/i // Series 40
], [NAME], [
/mozilla.+\(mobile;.+gecko.+firefox/i // Firefox OS
], [[NAME, 'Firefox OS'], VERSION], [
// Console
/(nintendo|playstation)\s([wids3portablevu]+)/i, // Nintendo/Playstation
// GNU/Linux based
/(mint)[\/\s\(]?(\w+)*/i, // Mint
/(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux
/(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?([\w\.-]+)*/i,
// Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware
// Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus
/(hurd|linux)\s?([\w\.]+)*/i, // Hurd/Linux
/(gnu)\s?([\w\.]+)*/i // GNU
], [NAME, VERSION], [
/(cros)\s[\w]+\s([\w\.]+\w)/i // Chromium OS
], [[NAME, 'Chromium OS'], VERSION],[
// Solaris
/(sunos)\s?([\w\.]+\d)*/i // Solaris
], [[NAME, 'Solaris'], VERSION], [
// BSD based
/\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly
], [NAME, VERSION],[
/(ip[honead]+)(?:.*os\s*([\w]+)*\slike\smac|;\sopera)/i // iOS
], [[NAME, 'iOS'], [VERSION, /_/g, '.']], [
/(mac\sos\sx)\s?([\w\s\.]+\w)*/i,
/(macintosh|mac(?=_powerpc)\s)/i // Mac OS
], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [
// Other
/((?:open)?solaris)[\/\s-]?([\w\.]+)*/i, // Solaris
/(haiku)\s(\w+)/i, // Haiku
/(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i, // AIX
/(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i,
// Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS
/(unix)\s?([\w\.]+)*/i // UNIX
], [NAME, VERSION]
]
};
/////////////////
// Constructor
////////////////
var UAParser = function (uastring, extensions) {
if (!(this instanceof UAParser)) {
return new UAParser(uastring, extensions).getResult();
}
var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY);
var rgxmap = extensions ? util.extend(regexes, extensions) : regexes;
this.getBrowser = function () {
var browser = mapper.rgx.apply(this, rgxmap.browser);
browser.major = util.major(browser.version);
return browser;
};
this.getCPU = function () {
return mapper.rgx.apply(this, rgxmap.cpu);
};
this.getDevice = function () {
return mapper.rgx.apply(this, rgxmap.device);
};
this.getEngine = function () {
return mapper.rgx.apply(this, rgxmap.engine);
};
this.getOS = function () {
return mapper.rgx.apply(this, rgxmap.os);
};
this.getResult = function() {
return {
ua : this.getUA(),
browser : this.getBrowser(),
engine : this.getEngine(),
os : this.getOS(),
device : this.getDevice(),
cpu : this.getCPU()
};
};
this.getUA = function () {
return ua;
};
this.setUA = function (uastring) {
ua = uastring;
return this;
};
this.setUA(ua);
return this;
};
UAParser.VERSION = LIBVERSION;
UAParser.BROWSER = {
NAME : NAME,
MAJOR : MAJOR, // deprecated
VERSION : VERSION
};
UAParser.CPU = {
ARCHITECTURE : ARCHITECTURE
};
UAParser.DEVICE = {
MODEL : MODEL,
VENDOR : VENDOR,
TYPE : TYPE,
CONSOLE : CONSOLE,
MOBILE : MOBILE,
SMARTTV : SMARTTV,
TABLET : TABLET,
WEARABLE: WEARABLE,
EMBEDDED: EMBEDDED
};
UAParser.ENGINE = {
NAME : NAME,
VERSION : VERSION
};
UAParser.OS = {
NAME : NAME,
VERSION : VERSION
};
///////////
// Export
//////////
// check js environment
if (typeof(exports) !== UNDEF_TYPE) {
// nodejs env
if (typeof module !== UNDEF_TYPE && module.exports) {
exports = module.exports = UAParser;
}
exports.UAParser = UAParser;
} else {
// requirejs env (optional)
if (typeof(define) === FUNC_TYPE && define.amd) {
define(function () {
return UAParser;
});
} else {
// browser env
window.UAParser = UAParser;
}
}
// jQuery/Zepto specific (optional)
// Note:
// In AMD env the global scope should be kept clean, but jQuery is an exception.
// jQuery always exports to global scope, unless jQuery.noConflict(true) is used,
// and we should catch that.
var $ = window.jQuery || window.Zepto;
if (typeof $ !== UNDEF_TYPE) {
var parser = new UAParser();
$.ua = parser.getResult();
$.ua.get = function() {
return parser.getUA();
};
$.ua.set = function (uastring) {
parser.setUA(uastring);
var result = parser.getResult();
for (var prop in result) {
$.ua[prop] = result[prop];
}
};
}
})(typeof window === 'object' ? window : this);

View file

@ -3,7 +3,7 @@
<h1><%- @T( @header ) %> <small><%- @T( @subHeader ) %></small></h1> <h1><%- @T( @header ) %> <small><%- @T( @subHeader ) %></small></h1>
</div> </div>
</div> </div>
<ul class="nav nav-tabs <% if @tabs.length <= 1: %>hide<% end %>" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<% for tab in @tabs: %> <% for tab in @tabs: %>
<li><a href="#<%= tab.target %>" role="tab" data-toggle="tab"><%- @T( tab.name ) %></a></li> <li><a href="#<%= tab.target %>" role="tab" data-toggle="tab"><%- @T( tab.name ) %></a></li>
<% end %> <% end %>

View file

@ -86,7 +86,7 @@ class ApplicationController < ActionController::Base
# check if remote ip need to be updated # check if remote ip need to be updated
if !session[:remote_id] || session[:remote_id] != request.remote_ip if !session[:remote_id] || session[:remote_id] != request.remote_ip
session[:remote_id] = request.remote_ip session[:remote_id] = request.remote_ip
session[:geo] = GeoIp.location( request.remote_ip ) session[:geo] = Service::GeoIp.location( request.remote_ip )
end end
# fill user agent # fill user agent

View file

@ -32,11 +32,13 @@ class SettingsController < ApplicationController
# PUT /settings/1 # PUT /settings/1
def update def update
return if deny_if_not_role(Z_ROLENAME_ADMIN) return if deny_if_not_role(Z_ROLENAME_ADMIN)
return if !check_access
model_update_render(Setting, params) model_update_render(Setting, params)
end end
# PUT /settings/image/:id # PUT /settings/image/:id
def update_image def update_image
return if deny_if_not_role(Z_ROLENAME_ADMIN)
if !params[:logo] if !params[:logo]
render json: { render json: {
@ -91,4 +93,16 @@ class SettingsController < ApplicationController
return if deny_if_not_role(Z_ROLENAME_ADMIN) return if deny_if_not_role(Z_ROLENAME_ADMIN)
model_destory_render(Setting, params) model_destory_render(Setting, params)
end end
private
def check_access
return true if !Setting.get('system_online_service')
setting = Setting.find(params[:id])
return true if setting.preferences && !setting.preferences[:online_service_disable]
response_access_deny
false
end
end end

View file

@ -22,6 +22,7 @@ class TranslationsController < ApplicationController
# POST /translations/sync # POST /translations/sync
def sync def sync
return if deny_if_not_role(Z_ROLENAME_ADMIN) return if deny_if_not_role(Z_ROLENAME_ADMIN)
Locale.load
Translation.load Translation.load
render json: { message: 'ok' }, status: :ok render json: { message: 'ok' }, status: :ok
end end

View file

@ -130,7 +130,7 @@ class UsersController < ApplicationController
# fetch org logo # fetch org logo
if user.email if user.email
Zammad::BigData::Organization.suggest_system_image(user.email) Service::Image.organization_suggest(user.email)
end end
end end

View file

@ -146,7 +146,7 @@ add a avatar
end end
# fetch image # fetch image
image = Zammad::BigData::User.image(data[:url]) image = Service::Image.user(data[:url])
return if !image return if !image
if !data[:resize] if !data[:resize]
data[:resize] = {} data[:resize] = {}

View file

@ -7,7 +7,7 @@ class Channel::Facebook
def fetch (channel) def fetch (channel)
@channel = channel @channel = channel
@facebook = Facebook.new( @channel[:options][:auth] ) @facebook = Facebook.new( @channel[:options] )
@sync = @channel[:options][:sync] @sync = @channel[:options][:sync]
Rails.logger.debug 'facebook fetch started' Rails.logger.debug 'facebook fetch started'
@ -22,7 +22,7 @@ class Channel::Facebook
def send(article, _notification = false) def send(article, _notification = false)
@channel = Channel.find_by( area: 'Facebook::Inbound', active: true ) @channel = Channel.find_by( area: 'Facebook::Inbound', active: true )
@facebook = Facebook.new( @channel[:options][:auth] ) @facebook = Facebook.new( @channel[:options] )
tweet = @facebook.from_article(article) tweet = @facebook.from_article(article)
disconnect disconnect

View file

@ -13,6 +13,7 @@ class Locale < ApplicationModel
} }
) )
fail "Can't load locales from #{url}" if !result
fail "Can't load locales from #{url}: #{result.error}" if !result.success? fail "Can't load locales from #{url}: #{result.error}" if !result.success?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do

View file

@ -54,7 +54,7 @@ class Observer::User::Geo < ActiveRecord::Observer
return if address == '' return if address == ''
# lookup # lookup
latlng = GeoLocation.geocode( address ) latlng = Service::GeoLocation.geocode( address )
return if !latlng return if !latlng
# store data # store data

View file

@ -30,7 +30,7 @@ module Ticket::Number::Increment
if config[:checksum] if config[:checksum]
min_digs = min_digs.to_i - 1 min_digs = min_digs.to_i - 1
end end
fillup = Setting.get('system_id') || '1' fillup = Setting.get('system_id').to_s || '1'
( 1..100 ).each { ( 1..100 ).each {
next if ( fillup.length.to_i + counter_increment.to_s.length.to_i ) >= min_digs.to_i next if ( fillup.length.to_i + counter_increment.to_s.length.to_i ) >= min_digs.to_i

View file

@ -0,0 +1,76 @@
class UpdateServices < ActiveRecord::Migration
def up
Setting.create_or_update(
title: 'Image Service',
name: 'image_backend',
area: 'System::Services',
description: 'Defines the backend for user and organization image lookups.',
options: {
form: [
{
display: '',
null: true,
name: 'image_backend',
tag: 'select',
options: {
'' => '-',
'Service::Image::Zammad' => 'Zammad Image Service',
},
},
],
},
state: 'Service::Image::Zammad',
preferences: { prio: 1 },
frontend: false
)
Setting.create_or_update(
title: 'Geo IP Service',
name: 'geo_ip_backend',
area: 'System::Services',
description: 'Defines the backend for geo IP lookups. Show also location of an IP address if an IP address is shown.',
options: {
form: [
{
display: '',
null: true,
name: 'geo_ip_backend',
tag: 'select',
options: {
'' => '-',
'Service::GeoIp::Zammad' => 'Zammad GeoIP Service',
},
},
],
},
state: 'Service::GeoIp::Zammad',
preferences: { prio: 2 },
frontend: false
)
Setting.create_or_update(
title: 'Geo Location Service',
name: 'geo_location_backend',
area: 'System::Services',
description: 'Defines the backend for geo location lookups to store geo locations for addresses.',
options: {
form: [
{
display: '',
null: true,
name: 'geo_location_backend',
tag: 'select',
options: {
'' => '-',
'Service::GeoLocation::Gmaps' => 'Google Maps',
},
},
],
},
state: 'Service::GeoLocation::Gmaps',
preferences: { prio: 3 },
frontend: false
)
end
end

View file

@ -0,0 +1,69 @@
class UpdateSettings < ActiveRecord::Migration
def up
Setting.create_or_update(
title: 'Organization',
name: 'organization',
area: 'System::Branding',
description: 'Will be shown in the app and is included in email footers.',
options: {
form: [
{
display: '',
null: false,
name: 'organization',
tag: 'input',
},
],
},
state: '',
preferences: { prio: 2 },
frontend: true
)
Setting.create_or_update(
title: 'Send client stats',
name: 'ui_send_client_stats',
area: 'System::UI',
description: 'Send client stats/error message to central server to improve the usability.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_send_client_stats',
tag: 'boolean',
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: { prio: 1 },
frontend: true
)
Setting.create_or_update(
title: 'Client storage',
name: 'ui_client_storage',
area: 'System::UI',
description: 'Use client storage to cache data to perform speed of application.',
options: {
form: [
{
display: '',
null: true,
name: 'ui_client_storage',
tag: 'boolean',
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: { prio: 2 },
frontend: true
)
end
end

View file

@ -0,0 +1,265 @@
class AddSettingOnlineService < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
Setting.create_or_update(
title: 'System Init Done',
name: 'system_init_done',
area: 'Core',
description: 'Defines if application is in init mode.',
options: {},
state: Setting.get('system_init_done'),
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'Developer System',
name: 'developer_mode',
area: 'Core::Develop',
description: 'Defines if application is in developer mode (useful for developer, all users have the same password, password reset will work without email delivery).',
options: {},
state: Setting.get('developer_mode'),
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'Online Service',
name: 'system_online_service',
area: 'Core',
description: 'Defines if application is used as online service.',
options: {},
state: Setting.get('system_online_service'),
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'SystemID',
name: 'system_id',
area: 'System::Base',
description: 'Defines the system identifier. Every ticket number contains this ID. This ensures that only tickets which belong to your system will be processed as follow-ups (useful when communicating between two instances of Zammad).',
options: {
form: [
{
display: '',
null: true,
name: 'system_id',
tag: 'select',
options: {
'10' => '10',
'11' => '11',
'12' => '12',
'13' => '13',
},
},
],
},
state: '10',
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'Fully Qualified Domain Name',
name: 'fqdn',
area: 'System::Base',
description: 'Defines the fully qualified domain name of the system. This setting is used as a variable, #{setting.fqdn} which is found in all forms of messaging used by the application, to build links to the tickets within your system.',
options: {
form: [
{
display: '',
null: false,
name: 'fqdn',
tag: 'input',
},
],
},
state: Setting.get('fqdn'),
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'websocket port',
name: 'websocket_port',
area: 'System::WebSocket',
description: 'Defines the port of the websocket server.',
options: {
form: [
{
display: '',
null: false,
name: 'websocket_port',
tag: 'input',
},
],
},
state: Setting.get('websocket_port'),
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'http type',
name: 'http_type',
area: 'System::Base',
description: 'Defines the type of protocol, used by the web server, to serve the application. If https protocol will be used instead of plain http, it must be specified in here. Since this has no affect on the web server\'s settings or behavior, it will not change the method of access to the application and, if it is wrong, it will not prevent you from logging into the application. This setting is used as a variable, #{setting.http_type} which is found in all forms of messaging used by the application, to build links to the tickets within your system.',
options: {
form: [
{
display: '',
null: true,
name: 'http_type',
tag: 'select',
options: {
'https' => 'https',
'http' => 'http',
},
},
],
},
state: Setting.get('http_type'),
preferences: { online_service_disable: true },
frontend: true
)
Setting.create_or_update(
title: 'Storage Mechanism',
name: 'storage',
area: 'System::Storage',
description: '"Database" stores all attachments in the database (not recommended for storing large amounts of data). "Filesystem" stores the data on the filesystem. You can switch between the modules even on a system that is already in production without any loss of data.',
options: {
form: [
{
display: '',
null: true,
name: 'storage',
tag: 'select',
options: {
'DB' => 'Database',
'FS' => 'Filesystem',
},
},
],
},
state: Setting.get('storage'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Max. Email Size',
name: 'postmaster_max_size',
area: 'Email::Base',
description: 'Maximal size in MB of emails.',
options: {
form: [
{
display: '',
null: true,
name: 'postmaster_max_size',
tag: 'select',
options: {
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
9 => 9,
10 => 10,
11 => 11,
12 => 12,
13 => 13,
14 => 14,
15 => 15,
16 => 16,
17 => 17,
18 => 18,
19 => 19,
20 => 20,
},
},
],
},
state: Setting.get('postmaster_max_size'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Notification Sender',
name: 'notification_sender',
area: 'Email::Base',
description: 'Defines the sender of email notifications.',
options: {
form: [
{
display: '',
null: false,
name: 'notification_sender',
tag: 'input',
},
],
},
state: Setting.get('notification_sender'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Elasticsearch Endpoint URL',
name: 'es_url',
area: 'SearchIndex::Elasticsearch',
description: 'Define endpoint of Elastic Search.',
state: Setting.get('es_url'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Elasticsearch Endpoint User',
name: 'es_user',
area: 'SearchIndex::Elasticsearch',
description: 'Define http basic auth user of Elasticsearch.',
state: Setting.get('es_user'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Elastic Search Endpoint Password',
name: 'es_password',
area: 'SearchIndex::Elasticsearch',
description: 'Define http basic auth password of Elasticsearch.',
state: Setting.get('es_password'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Elastic Search Endpoint Index',
name: 'es_index',
area: 'SearchIndex::Elasticsearch',
description: 'Define Elasticsearch index name.',
state: Setting.get('es_index'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Elastic Search Attachment Extentions',
name: 'es_attachment_ignore',
area: 'SearchIndex::Elasticsearch',
description: 'Define attachment extentions which are ignored for Elasticsearch.',
state: Setting.get('es_attachment_ignore'),
preferences: { online_service_disable: true },
frontend: false
)
Setting.create_or_update(
title: 'Elastic Search Attachment Size',
name: 'es_attachment_max_size_in_mb',
area: 'SearchIndex::Elasticsearch',
description: 'Define max. attachment size for Elasticsearch.',
state: Setting.get('es_attachment_max_size_in_mb'),
preferences: { online_service_disable: true },
frontend: false
)
end
end

View file

@ -0,0 +1,17 @@
class FacebookArticleTypes < ActiveRecord::Migration
def up
facebook_at = Ticket::Article::Type.find_by( name: 'facebook' )
return if !facebook_at
facebook_at.name = 'facebook feed post'
facebook_at.save
Ticket::Article::Type.create(
name: 'facebook feed comment',
communication: true,
updated_by_id: 1,
created_by_id: 1
)
end
end

View file

@ -13,6 +13,7 @@ Setting.create_if_not_exists(
description: 'Defines if application is in init mode.', description: 'Defines if application is in init mode.',
options: {}, options: {},
state: false, state: false,
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -22,6 +23,7 @@ Setting.create_if_not_exists(
description: 'Defines if application is in developer mode (useful for developer, all users have the same password, password reset will work without email delivery).', description: 'Defines if application is in developer mode (useful for developer, all users have the same password, password reset will work without email delivery).',
options: {}, options: {},
state: false, state: false,
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -31,6 +33,7 @@ Setting.create_if_not_exists(
description: 'Defines if application is used as online service.', description: 'Defines if application is used as online service.',
options: {}, options: {},
state: false, state: false,
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -49,7 +52,7 @@ Setting.create_if_not_exists(
], ],
}, },
preferences: { render: true, session_check: true, prio: 1 }, preferences: { render: true, session_check: true, prio: 1 },
state: 'Zammad', state: 'Zammad Helpdesk',
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -75,7 +78,7 @@ Setting.create_if_not_exists(
title: 'Organization', title: 'Organization',
name: 'organization', name: 'organization',
area: 'System::Branding', area: 'System::Branding',
description: 'Will be shown in the app and is included in email headers.', description: 'Will be shown in the app and is included in email footers.',
options: { options: {
form: [ form: [
{ {
@ -113,6 +116,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: '10', state: '10',
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -131,6 +135,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: 'zammad.example.com', state: 'zammad.example.com',
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -149,6 +154,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: '6042', state: '6042',
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -171,6 +177,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: 'http', state: 'http',
preferences: { online_service_disable: true },
frontend: true frontend: true
) )
@ -194,35 +201,39 @@ Setting.create_if_not_exists(
], ],
}, },
state: 'DB', state: 'DB',
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Geo Location Backend', title: 'Image Service',
name: 'geo_location_backend', name: 'image_backend',
area: 'System::Geo', area: 'System::Services',
description: 'Defines the backend for geo location lookups.', description: 'Defines the backend for user and organization image lookups.',
options: { options: {
form: [ form: [
{ {
display: '', display: '',
null: true, null: true,
name: 'geo_location_backend', name: 'image_backend',
tag: 'select', tag: 'select',
options: { options: {
'' => '-', '' => '-',
'GeoLocation::Gmaps' => 'Google Maps', 'Service::Image::Zammad' => 'Zammad Image Service',
}, },
}, },
], ],
}, },
state: 'GeoLocation::Gmaps', state: 'Service::Image::Zammad',
preferences: { prio: 1 },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Geo IP Backend', title: 'Geo IP Service',
name: 'geo_ip_backend', name: 'geo_ip_backend',
area: 'System::Geo', area: 'System::Services',
description: 'Defines the backend for geo ip lookups.', description: 'Defines the backend for geo IP lookups. Show also location of an IP address if an IP address is shown.',
options: { options: {
form: [ form: [
{ {
@ -232,12 +243,37 @@ Setting.create_if_not_exists(
tag: 'select', tag: 'select',
options: { options: {
'' => '-', '' => '-',
'GeoIp::ZammadGeoIp' => 'Zammad GeoIP Service', 'Service::GeoIp::Zammad' => 'Zammad GeoIP Service',
}, },
}, },
], ],
}, },
state: 'GeoIp::ZammadGeoIp', state: 'Service::GeoIp::Zammad',
preferences: { prio: 2 },
frontend: false
)
Setting.create_if_not_exists(
title: 'Geo Location Service',
name: 'geo_location_backend',
area: 'System::Services',
description: 'Defines the backend for geo location lookups to store geo locations for addresses.',
options: {
form: [
{
display: '',
null: true,
name: 'geo_location_backend',
tag: 'select',
options: {
'' => '-',
'Service::GeoLocation::Gmaps' => 'Google Maps',
},
},
],
},
state: 'Service::GeoLocation::Gmaps',
preferences: { prio: 3 },
frontend: false frontend: false
) )
@ -245,7 +281,7 @@ Setting.create_if_not_exists(
title: 'Send client stats', title: 'Send client stats',
name: 'ui_send_client_stats', name: 'ui_send_client_stats',
area: 'System::UI', area: 'System::UI',
description: 'Send client stats to central server.', description: 'Send client stats/error message to central server to improve the usability.',
options: { options: {
form: [ form: [
{ {
@ -261,6 +297,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: true, state: true,
preferences: { prio: 1 },
frontend: true frontend: true
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -283,6 +320,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: false, state: false,
preferences: { prio: 2 },
frontend: true frontend: true
) )
@ -1034,6 +1072,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: 10, state: 10,
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
@ -1078,6 +1117,7 @@ Setting.create_if_not_exists(
], ],
}, },
state: 'Notification Master <noreply@#{config.fqdn}>', state: 'Notification Master <noreply@#{config.fqdn}>',
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
@ -1140,6 +1180,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch', area: 'SearchIndex::Elasticsearch',
description: 'Define endpoint of Elastic Search.', description: 'Define endpoint of Elastic Search.',
state: '', state: '',
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -1148,6 +1189,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch', area: 'SearchIndex::Elasticsearch',
description: 'Define http basic auth user of Elasticsearch.', description: 'Define http basic auth user of Elasticsearch.',
state: '', state: '',
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -1156,6 +1198,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch', area: 'SearchIndex::Elasticsearch',
description: 'Define http basic auth password of Elasticsearch.', description: 'Define http basic auth password of Elasticsearch.',
state: '', state: '',
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -1164,6 +1207,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch', area: 'SearchIndex::Elasticsearch',
description: 'Define Elasticsearch index name.', description: 'Define Elasticsearch index name.',
state: 'zammad', state: 'zammad',
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -1172,6 +1216,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch', area: 'SearchIndex::Elasticsearch',
description: 'Define attachment extentions which are ignored for Elasticsearch.', description: 'Define attachment extentions which are ignored for Elasticsearch.',
state: [ '.png', '.jpg', '.jpeg', '.mpeg', '.mpg', '.mov', '.bin', '.exe', '.box', '.mbox' ], state: [ '.png', '.jpg', '.jpeg', '.mpeg', '.mpg', '.mov', '.bin', '.exe', '.box', '.mbox' ],
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
Setting.create_if_not_exists( Setting.create_if_not_exists(
@ -1180,6 +1225,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch', area: 'SearchIndex::Elasticsearch',
description: 'Define max. attachment size for Elasticsearch.', description: 'Define max. attachment size for Elasticsearch.',
state: 50, state: 50,
preferences: { online_service_disable: true },
frontend: false frontend: false
) )
@ -1455,9 +1501,10 @@ Ticket::Article::Type.create_if_not_exists( id: 4, name: 'fax', communication: t
Ticket::Article::Type.create_if_not_exists( id: 5, name: 'phone', communication: true ) Ticket::Article::Type.create_if_not_exists( id: 5, name: 'phone', communication: true )
Ticket::Article::Type.create_if_not_exists( id: 6, name: 'twitter status', communication: true ) Ticket::Article::Type.create_if_not_exists( id: 6, name: 'twitter status', communication: true )
Ticket::Article::Type.create_if_not_exists( id: 7, name: 'twitter direct-message', communication: true ) Ticket::Article::Type.create_if_not_exists( id: 7, name: 'twitter direct-message', communication: true )
Ticket::Article::Type.create_if_not_exists( id: 8, name: 'facebook', communication: true ) Ticket::Article::Type.create_if_not_exists( id: 8, name: 'facebook feed post', communication: true )
Ticket::Article::Type.create_if_not_exists( id: 9, name: 'note', communication: false ) Ticket::Article::Type.create_if_not_exists( id: 9, name: 'facebook feed comment', communication: true )
Ticket::Article::Type.create_if_not_exists( id: 10, name: 'web', communication: true ) Ticket::Article::Type.create_if_not_exists( id: 10, name: 'note', communication: false )
Ticket::Article::Type.create_if_not_exists( id: 11, name: 'web', communication: true )
Ticket::Article::Sender.create_if_not_exists( id: 1, name: 'Agent' ) Ticket::Article::Sender.create_if_not_exists( id: 1, name: 'Agent' )
Ticket::Article::Sender.create_if_not_exists( id: 2, name: 'Customer' ) Ticket::Article::Sender.create_if_not_exists( id: 2, name: 'Customer' )

View file

@ -111,7 +111,7 @@ returns
# fetch org logo # fetch org logo
if admin_user.email if admin_user.email
Zammad::BigData::Organization.suggest_system_image(admin_user.email) Service::Image.organization_suggest(admin_user.email)
end end
} }
end end

View file

@ -130,18 +130,19 @@ class Facebook
ticket.save ticket.save
end end
user = to_user(comment) user = to_user(post)
return if !user return if !user
feed_post = { feed_post = {
from: user.name, from: "#{user.firstname} #{user.lastname}",
body: post['message'], body: post['message'],
message_id: post['id'], message_id: post['id'],
type: Ticket::Article::Type.find_by( name: 'facebook feed post' ), type_id: Ticket::Article::Type.find_by( name: 'facebook feed post' ).id,
} }
articles = [] articles = []
articles.push( feed_post ) articles.push( feed_post )
if post['comments']
post['comments']['data'].each { |comment| post['comments']['data'].each { |comment|
user = to_user(comment) user = to_user(comment)
@ -149,18 +150,21 @@ class Facebook
next if !user next if !user
post_comment = { post_comment = {
from: user.name, from: "#{user.firstname} #{user.lastname}",
body: comment['message'], body: comment['message'],
message_id: comment['id'], message_id: comment['id'],
type: Ticket::Article::Type.find_by( name: 'facebook feed comment' ), type_id: Ticket::Article::Type.find_by( name: 'facebook feed comment' ).id,
} }
articles.push( post_comment ) articles.push( post_comment )
# TODO: sub-comments # TODO: sub-comments
# comment_data = @client.get_object( comment['id'] ) # comment_data = @client.get_object( comment['id'] )
} }
end
articles.invert.each { |article| inverted_articles = articles.reverse
inverted_articles.each { |article|
break if Ticket::Article.find_by( message_id: article[:message_id] ) break if Ticket::Article.find_by( message_id: article[:message_id] )
@ -168,6 +172,9 @@ class Facebook
to: @account['name'], to: @account['name'],
ticket_id: ticket.id, ticket_id: ticket.id,
internal: false, internal: false,
sender_id: Ticket::Article::Sender.lookup( name: 'Customer' ).id,
created_by_id: 1,
updated_by_id: 1,
}.merge( article ) }.merge( article )
Ticket::Article.create( article ) Ticket::Article.create( article )
@ -204,12 +211,11 @@ class Facebook
def from_article(article) def from_article(article)
post = nil post = nil
# TODO: article[:type] == 'facebook feed post'
if article[:type] == 'facebook feed comment' if article[:type] == 'facebook feed comment'
Rails.logger.debug 'Create feed comment from article...' Rails.logger.debug 'Create feed comment from article...'
post = @client.put_wall_post(article[:body], {}, article[:in_reply_to]) post = @client.put_comment(article[:in_reply_to], article[:body])
else else
fail "Can't handle unknown facebook article type '#{article[:type]}'." fail "Can't handle unknown facebook article type '#{article[:type]}'."
end end

View file

@ -1,49 +0,0 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class GeoLocation
include ApplicationLib
=begin
lookup lat and lng for address
result = GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' )
returns
result = [ 4.21312, 1.3123 ]
=end
def self.geocode(address)
# load backend
backend = load_adapter_by_setting( 'geo_location_backend' )
return if !backend
# db lookup
backend.geocode(address)
end
=begin
lookup address for lat and lng
result = GeoLocation.reverse_geocode( 4.21312, 1.3123 )
returns
result = 'some address'
=end
def self.reverse_geocode(lat, lng)
# load backend
backend = load_adapter_by_setting( 'geo_location_backend' )
return if !backend
# db lookup
backend.reverse_geocode(lat, lng)
end
end

View file

@ -1,13 +1,14 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class GeoIp module Service
class GeoIp
include ApplicationLib include ApplicationLib
=begin =begin
lookup location based on ip or hostname lookup location based on ip or hostname
result = GeoIp.location( '172.0.0.1' ) result = Service::GeoIp.location( '172.0.0.1' )
returns returns
@ -36,4 +37,5 @@ returns
# db lookup # db lookup
backend.location(address) backend.location(address)
end end
end
end end

View file

@ -2,7 +2,7 @@
require 'cache' require 'cache'
class GeoIp::ZammadGeoIp class Service::GeoIp::Zammad
def self.location(address) def self.location(address)
# check cache # check cache

View file

@ -0,0 +1,51 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
module Service
class GeoLocation
include ApplicationLib
=begin
lookup lat and lng for address
result = Service::GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' )
returns
result = [ 4.21312, 1.3123 ]
=end
def self.geocode(address)
# load backend
backend = load_adapter_by_setting( 'geo_location_backend' )
return if !backend
# db lookup
backend.geocode(address)
end
=begin
lookup address for lat and lng
result = GeoLocation.reverse_geocode( 4.21312, 1.3123 )
returns
result = 'some address'
=end
def self.reverse_geocode(lat, lng)
# load backend
backend = load_adapter_by_setting( 'geo_location_backend' )
return if !backend
# db lookup
backend.reverse_geocode(lat, lng)
end
end
end

View file

@ -1,6 +1,6 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class GeoLocation::Gmaps class Service::GeoLocation::Gmaps
def self.geocode(address) def self.geocode(address)
url = "http://maps.googleapis.com/maps/api/geocode/json?address=#{CGI.escape address}&sensor=true" url = "http://maps.googleapis.com/maps/api/geocode/json?address=#{CGI.escape address}&sensor=true"

79
lib/service/image.rb Normal file
View file

@ -0,0 +1,79 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
module Service
class Image
include ApplicationLib
=begin
lookup user image based on email address
file = Service::Image.user( 'skywalker@zammad.org' )
returns
{
content: content,
mime_type: mime_type,
}
=end
def self.user(address)
# load backend
backend = load_adapter_by_setting( 'image_backend' )
return if !backend
backend.user(address)
end
=begin
lookup organization image based on domain
file = Service::Image.organization('edenhofer.de')
file = Service::Image.organization('user@edenhofer.de') # will just use domain
returns
{
content: content,
mime_type: mime_type,
}
=end
def self.organization(domain)
# load backend
backend = load_adapter_by_setting( 'image_backend' )
return if !backend
backend.organization(domain)
end
=begin
find organization image suggestion
result = Service::Image.organization_suggest('edenhofer.de')
returns
true # or false
=end
def self.organization_suggest(domain)
# load backend
backend = load_adapter_by_setting( 'image_backend' )
return if !backend
backend.organization_suggest(domain)
end
end
end

View file

@ -0,0 +1,74 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
class Service::Image::Zammad
# rubocop:disable Style/ClassVars
@@api_host = 'https://images.zammad.com'
@@open_timeout = 4
@@read_timeout = 6
def self.user(email)
# fetch image
response = UserAgent.post(
"#{@@api_host}/api/v1/person/image",
{
email: email,
},
{
open_timeout: @@open_timeout,
read_timeout: @@read_timeout,
},
)
if !response.success?
Rails.logger.info "Can't fetch image for '#{email}' (maybe no avatar available), http code: #{response.code}"
return
end
Rails.logger.info "Fetched image for '#{email}', http code: #{response.code}"
mime_type = 'image/jpeg'
{
content: response.body,
mime_type: mime_type,
}
end
def self.organization(domain)
# strip, just use domain name
domain = domain.sub(/^.+?@(.+?)$/, '\1')
# fetch org logo
response = UserAgent.post(
"#{@@api_host}/api/v1/organization/image",
{
domain: domain
},
{
open_timeout: @@open_timeout,
read_timeout: @@read_timeout,
},
)
if !response.success?
Rails.logger.info "Can't fetch image for '#{domain}' (maybe no avatar available), http code: #{response.code}"
return
end
Rails.logger.info "Fetched image for '#{domain}', http code: #{response.code}"
mime_type = 'image/png'
{
content: response.body,
mime_type: mime_type,
}
end
def self.organization_suggest(domain)
image = organization(domain)
return false if !image
# store image 1:1
product_logo = StaticAssets.store_raw( image[:content], image[:mime_type] )
Setting.set('product_logo', product_logo)
true
end
end

View file

@ -97,7 +97,15 @@ class Tweet
Rails.logger.debug group_id.inspect Rails.logger.debug group_id.inspect
if tweet.class.to_s == 'Twitter::DirectMessage' if tweet.class.to_s == 'Twitter::DirectMessage'
article = Ticket::Article.find_by(
from: 'me_bauer',
type_id: Ticket::Article::Type.find_by( name: 'twitter direct-message' ).id,
)
if article
ticket = Ticket.find_by( ticket = Ticket.find_by(
id: article.ticket_id,
customer_id: user.id, customer_id: user.id,
state: Ticket::State.where.not( state: Ticket::State.where.not(
state_type_id: Ticket::StateType.where( state_type_id: Ticket::StateType.where(
@ -107,13 +115,14 @@ class Tweet
) )
return ticket if ticket return ticket if ticket
end end
end
Ticket.create( Ticket.create(
customer_id: user.id, customer_id: user.id,
title: "#{tweet.text[0, 37]}...", title: "#{tweet.text[0, 37]}...",
group_id: group_id, group_id: group_id,
state: Ticket::State.find_by( name: 'new' ), state_id: Ticket::State.find_by( name: 'new' ).id,
priority: Ticket::Priority.find_by( name: '2 normal' ), priority_id: Ticket::Priority.find_by( name: '2 normal' ).id,
) )
end end
@ -153,8 +162,8 @@ class Tweet
message_id: tweet.id, message_id: tweet.id,
ticket_id: ticket.id, ticket_id: ticket.id,
in_reply_to: in_reply_to, in_reply_to: in_reply_to,
type: Ticket::Article::Type.find_by( name: article_type ), type_id: Ticket::Article::Type.find_by( name: article_type ).id,
sender: Ticket::Article::Sender.find_by( name: 'Customer' ), sender_id: Ticket::Article::Sender.find_by( name: 'Customer' ).id,
internal: false, internal: false,
) )
end end

View file

@ -1,10 +0,0 @@
module Zammad
module BigData
class Base
# rubocop:disable Style/ClassVars
@@api_host = 'https://bigdata.zammad.com'
@@open_timeout = 4
@@read_timeout = 6
end
end
end

View file

@ -1,69 +0,0 @@
module Zammad
module BigData
class Organization < Zammad::BigData::Base
=begin
file = Zammad::BigData::Organization.image('edenhofer.de')
file = Zammad::BigData::Organization.image('user@edenhofer.de') # will just use domain
returns
{
content: content,
mime_type: mime_type,
}
=end
def self.image(domain)
# strip, just use domain name
domain = domain.sub(/^.+?@(.+?)$/, '\1')
# fetch org logo
response = UserAgent.post(
"#{@@api_host}/api/v1/organization/image",
{
domain: domain
},
{
open_timeout: @@open_timeout,
read_timeout: @@read_timeout,
},
)
if !response.success?
Rails.logger.info "Can't fetch image for '#{domain}' (maybe no avatar available), http code: #{response.code}"
return
end
Rails.logger.info "Fetched image for '#{domain}', http code: #{response.code}"
mime_type = 'image/png'
{
content: response.body,
mime_type: mime_type,
}
end
=begin
result = Zammad::BigData::Organization.suggest_system_image('edenhofer.de')
returns
true # or false
=end
def self.suggest_system_image(domain)
image = self.image(domain)
return false if !image
# store image 1:1
product_logo = StaticAssets.store_raw( image[:content], image[:mime_type] )
Setting.set('product_logo', product_logo)
true
end
end
end
end

View file

@ -1,45 +0,0 @@
module Zammad
module BigData
class User < Zammad::BigData::Base
=begin
file = Zammad::BigData::User.image('client@edenhofer.de')
returns
{
content: content,
mime_type: mime_type,
}
=end
def self.image(email)
# fetch logo
response = UserAgent.post(
"#{@@api_host}/api/v1/person/image",
{
email: email,
},
{
open_timeout: @@open_timeout,
read_timeout: @@read_timeout,
},
)
if !response.success?
Rails.logger.info "Can't fetch image for '#{email}' (maybe no avatar available), http code: #{response.code}"
return
end
Rails.logger.info "Fetched image for '#{email}', http code: #{response.code}"
mime_type = 'image/jpeg'
{
content: response.body,
mime_type: mime_type,
}
end
end
end
end

View file

@ -50,6 +50,15 @@ sleep 15
#export REMOTE_URL='http://192.168.178.32:4444/wd/hub' #export REMOTE_URL='http://192.168.178.32:4444/wd/hub'
#export REMOTE_URL='http://192.168.178.45:4444/wd/hub' #export REMOTE_URL='http://192.168.178.45:4444/wd/hub'
export RAILS_ENV=test
echo "rake db:drop"
time rake db:drop
echo "rake db:create"
time rake db:create
echo "rake db:migrate"
time rake db:migrate
rake test:browser["BROWSER_URL=http://localhost:4444"] rake test:browser["BROWSER_URL=http://localhost:4444"]
#rake test:browser["BROWSER_URL=http://localhost:4444 BROWSER=chrome"] #rake test:browser["BROWSER_URL=http://localhost:4444 BROWSER=chrome"]
#rake test:browser["BROWSER_URL=http://192.168.178.28:4444"] #rake test:browser["BROWSER_URL=http://192.168.178.28:4444"]

View file

@ -59,11 +59,10 @@ if ARGV[0] != 'start' && ARGV[0] != 'stop'
exit exit
end end
puts "Starting websocket server on #{@options[:b]}:#{@options[:p]} (secure:#{@options[:s]},pid:#{@options[:i]})"
#puts options.inspect
if ARGV[0] == 'stop' if ARGV[0] == 'stop'
puts "Stopping websocket server (pid:#{@options[:i]})"
# read pid # read pid
pid = File.open( @options[:i].to_s ).read pid = File.open( @options[:i].to_s ).read
pid.gsub!(/\r|\n/, '') pid.gsub!(/\r|\n/, '')
@ -74,6 +73,8 @@ if ARGV[0] == 'stop'
end end
if ARGV[0] == 'start' && @options[:d] if ARGV[0] == 'start' && @options[:d]
puts "Starting websocket server on #{@options[:b]}:#{@options[:p]} (secure:#{@options[:s]},pid:#{@options[:i]})"
Daemons.daemonize Daemons.daemonize
# create pid file # create pid file

View file

@ -206,7 +206,7 @@ class AgentTicketActionsLevel3Test < TestCase
css: '.content.active .js-reset', css: '.content.active .js-reset',
browser: browser2, browser: browser2,
) )
sleep 2 sleep 5
ticket_verify( ticket_verify(
browser: browser2, browser: browser2,
data: { data: {

View file

@ -104,11 +104,11 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is shown # check if number and article count is shown
match( match(
css: '.active table th:nth-child(3)', css: '.active table th:nth-child(7)',
value: '#', value: '#',
) )
match( match(
css: '.active table th:nth-child(8)', css: '.active table th:nth-child(4)',
value: 'Article#', value: 'Article#',
) )
@ -118,11 +118,11 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is shown # check if number and article count is shown
match( match(
css: '.active table th:nth-child(3)', css: '.active table th:nth-child(7)',
value: '#', value: '#',
) )
match( match(
css: '.active table th:nth-child(8)', css: '.active table th:nth-child(4)',
value: 'Article#', value: 'Article#',
) )

View file

@ -9,6 +9,22 @@ class TestCase < Test::Unit::TestCase
ENV['BROWSER'] || 'firefox' ENV['BROWSER'] || 'firefox'
end end
def profile
browser_profile = nil
if browser == 'firefox'
browser_profile = Selenium::WebDriver::Firefox::Profile.new
browser_profile['intl.locale.matchOS'] = false
browser_profile['intl.accept_languages'] = 'en-US'
browser_profile['general.useragent.locale'] = 'en-US'
elsif browser == 'chrome'
browser_profile = Selenium::WebDriver::Chrome::Profile.new
browser_profile['intl.accept_languages'] = 'en'
end
browser_profile
end
def browser_support_cookies def browser_support_cookies
if browser =~ /(internet_explorer|ie)/i if browser =~ /(internet_explorer|ie)/i
return false return false
@ -25,7 +41,7 @@ class TestCase < Test::Unit::TestCase
@browsers = {} @browsers = {}
end end
if !ENV['REMOTE_URL'] || ENV['REMOTE_URL'].empty? if !ENV['REMOTE_URL'] || ENV['REMOTE_URL'].empty?
local_browser = Selenium::WebDriver.for( browser.to_sym ) local_browser = Selenium::WebDriver.for( browser.to_sym, profile: profile )
browser_instance_preferences(local_browser) browser_instance_preferences(local_browser)
@browsers[local_browser.hash] = local_browser @browsers[local_browser.hash] = local_browser
return local_browser return local_browser
@ -1226,7 +1242,7 @@ wait untill text in selector disabppears
found = nil found = nil
(1..10).each { (1..10).each {
next if found break if found
begin begin
text = instance.find_elements( { css: '.content.active .js-reset' } )[0].text text = instance.find_elements( { css: '.content.active .js-reset' } )[0].text
@ -1240,6 +1256,7 @@ wait untill text in selector disabppears
} }
if !found if !found
screenshot( browser: instance, comment: 'ticket_update_discard_message_failed' ) screenshot( browser: instance, comment: 'ticket_update_discard_message_failed' )
fail 'no discard message found' fail 'no discard message found'
end end
end end

View file

@ -0,0 +1,154 @@
# encoding: utf-8
require 'integration_test_helper'
class FacebookTest < ActiveSupport::TestCase
# set system mode to done / to activate
Setting.set('system_init_done', true)
# needed to check correct behavior
Group.create_if_not_exists(
id: 2,
name: 'Facebook',
note: 'All Facebook feed posts.',
updated_by_id: 1,
created_by_id: 1
)
provider_key = 'CAACEdEose0cBAC56WJvrGb5avKbTlH0c7P4xZCZBfT8zG4nkgEWeFKGnnpNZC8xeedXzmqZCxEUrAumX245T4MborvAmRW52PSpuDiXwXXSMjYaZCJOih5v6CsP3xrZAfGxhPWBbI8dSoquBv8eRbUAMSir9SDSoDeKJSdSfhuytqx5wfveE8YibzT2ZAwYz0d7d2QZAN4b10d9j9UpBhXCCCahj4hyk9JQZD'
consumer_key = 'CAACEdEose0cBAHZCXAQ68snZBf2C7jT6G7pVXaWajbZCZAZAFWRZAVUb9FAMXHZBQECZBX0iL5qOeTsZA0mnR0586XTq9vYiWP8Y3qCzftrd9hnsP7J9VB6APnR67NEdY8SozxIFtctQA9Xp4Lb8lbxBmig2v5oXRIH513kImPYXJoCFUlQs0aJeZBCtRG6BekfPs5GPZB8tieQE3yGgtZBTZA3HI2TtQLZBNXyLAZD'
provider_page_name = 'Hansi Merkurs Hutfabrik'
provider_options = {
auth: {
access_token: provider_key
},
sync: {
page: provider_page_name,
group_id: 2,
limit: 1,
}
}
# add channel
current = Channel.where( adapter: 'Facebook' )
current.each(&:destroy)
Channel.create(
adapter: 'Facebook',
area: 'Facebook::Inbound',
options: provider_options,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
test 'pages' do
provider_options_clone = provider_options
provider_options_clone[:sync].delete(:page)
facebook = Facebook.new( provider_options_clone )
pages = facebook.pages
page_found = false
pages.each { |page|
next if page[:name] != provider_page_name
page_found = true
}
assert( page_found, "Page lookup for '#{provider_page_name}'" )
end
test 'feed post to ticket' do
consumer_client = Koala::Facebook::API.new( consumer_key )
feed_post = "I've got an issue with my hat, serial number ##{rand(9999)}"
facebook = Facebook.new( provider_options )
post = consumer_client.put_wall_post(feed_post, {}, facebook.account['id'])
# fetch check system account
Channel.fetch
# check if first article has been created
article = Ticket::Article.find_by( message_id: post['id'] )
assert( article, "article post '#{post['id']}' imported" )
assert_equal( article.body, feed_post, 'ticket article inbound body' )
assert_equal( 1, article.ticket.articles.count, 'ticket article inbound count' )
assert_equal( feed_post, article.ticket.articles.last.body, 'ticket article inbound body' )
post_comment = "Any updates yet? It's urgent. I love my hat."
comment = consumer_client.put_comment(post['id'], post_comment)
# fetch check system account
Channel.fetch
# check if second article has been created
article = Ticket::Article.find_by( message_id: comment['id'] )
assert( article, "article comment '#{comment['id']}' imported" )
assert_equal( article.body, post_comment, 'ticket article inbound body' )
assert_equal( 2, article.ticket.articles.count, 'ticket article inbound count' )
assert_equal( post_comment, article.ticket.articles.last.body, 'ticket article inbound body' )
end
test 'feed post and comment reply' do
consumer_client = Koala::Facebook::API.new( consumer_key )
feed_post = "I've got an issue with my hat, serial number ##{rand(9999)}"
facebook = Facebook.new( provider_options )
post = consumer_client.put_wall_post(feed_post, {}, facebook.account['id'])
# fetch check system account
Channel.fetch
# check if first article has been created
article = Ticket::Article.find_by( message_id: post['id'] )
reply_text = "What's your issue Bernd?"
# reply via ticket
outbound_article = Ticket::Article.create(
ticket_id: article.ticket.id,
body: reply_text,
in_reply_to: post['id'],
type: Ticket::Article::Type.find_by( name: 'facebook feed comment' ),
sender: Ticket::Article::Sender.find_by( name: 'Agent' ),
internal: false,
updated_by_id: 1,
created_by_id: 1,
)
assert( outbound_article, 'outbound article created' )
assert_equal( outbound_article.ticket.articles.count, 2, 'ticket article outbound count' )
post_comment = 'The peacock feather is fallen off.'
comment = consumer_client.put_comment(post['id'], post_comment)
# fetch check system account
Channel.fetch
reply_text = "Please send it to our address and add the ticket number #{article.ticket.number}."
# reply via ticket
outbound_article = Ticket::Article.create(
ticket_id: article.ticket.id,
body: reply_text,
in_reply_to: comment['id'],
type: Ticket::Article::Type.find_by( name: 'facebook feed comment' ),
sender: Ticket::Article::Sender.find_by( name: 'Agent' ),
internal: false,
updated_by_id: 1,
created_by_id: 1,
)
assert( outbound_article, 'outbound article created' )
assert_equal( outbound_article.ticket.articles.count, 4, 'ticket article outbound count' )
end
end

View file

@ -6,7 +6,7 @@ class GeoIpTest < ActiveSupport::TestCase
# check # check
test 'check some results' do test 'check some results' do
result = GeoIp.location( '127.0.0.0.1' ) result = Service::GeoIp.location( '127.0.0.0.1' )
assert(result) assert(result)
assert_equal(nil, result['country_name']) assert_equal(nil, result['country_name'])
assert_equal(nil, result['city_name']) assert_equal(nil, result['city_name'])
@ -15,7 +15,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(nil, result['latitude']) assert_equal(nil, result['latitude'])
assert_equal(nil, result['longitude']) assert_equal(nil, result['longitude'])
result = GeoIp.location( '195.65.29.254' ) result = Service::GeoIp.location( '195.65.29.254' )
assert(result) assert(result)
assert_equal('Switzerland', result['country_name']) assert_equal('Switzerland', result['country_name'])
assert_equal('Regensdorf', result['city_name']) assert_equal('Regensdorf', result['city_name'])
@ -24,7 +24,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(47.4299, result['latitude']) assert_equal(47.4299, result['latitude'])
assert_equal(8.465100000000007, result['longitude']) assert_equal(8.465100000000007, result['longitude'])
result = GeoIp.location( '134.109.140.74' ) result = Service::GeoIp.location( '134.109.140.74' )
assert(result) assert(result)
assert_equal('Germany', result['country_name']) assert_equal('Germany', result['country_name'])
assert_equal('Chemnitz', result['city_name']) assert_equal('Chemnitz', result['city_name'])
@ -33,7 +33,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(50.83330000000001, result['latitude']) assert_equal(50.83330000000001, result['latitude'])
assert_equal(12.916699999999992, result['longitude']) assert_equal(12.916699999999992, result['longitude'])
result = GeoIp.location( '46.253.55.170' ) result = Service::GeoIp.location( '46.253.55.170' )
assert(result) assert(result)
assert_equal('Germany', result['country_name']) assert_equal('Germany', result['country_name'])
assert_equal('Halle', result['city_name']) assert_equal('Halle', result['city_name'])
@ -42,7 +42,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(51.5, result['latitude']) assert_equal(51.5, result['latitude'])
assert_equal(12.0, result['longitude']) assert_equal(12.0, result['longitude'])
result = GeoIp.location( '169.229.216.200' ) result = Service::GeoIp.location( '169.229.216.200' )
assert(result) assert(result)
assert_equal('United States', result['country_name']) assert_equal('United States', result['country_name'])
assert_equal('Berkeley', result['city_name']) assert_equal('Berkeley', result['city_name'])

View file

@ -6,22 +6,22 @@ class GeoLocationTest < ActiveSupport::TestCase
# check # check
test 'check simple results' do test 'check simple results' do
result = GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' ) result = Service::GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' )
assert(result) assert(result)
assert_equal(52.52204, result[0]) assert_equal(52.52204, result[0])
assert_equal(13.38319, result[1]) assert_equal(13.38319, result[1])
result = GeoLocation.geocode( 'Marienstrasse 13 10117 Berlin' ) result = Service::GeoLocation.geocode( 'Marienstrasse 13 10117 Berlin' )
assert(result) assert(result)
assert_equal(52.52204, result[0]) assert_equal(52.52204, result[0])
assert_equal(13.38319, result[1]) assert_equal(13.38319, result[1])
result = GeoLocation.geocode( 'Martinsbruggstrasse 35, 9016 St. Gallen' ) result = Service::GeoLocation.geocode( 'Martinsbruggstrasse 35, 9016 St. Gallen' )
assert(result) assert(result)
assert_equal(47.4366664, result[0]) assert_equal(47.4366664, result[0])
assert_equal(9.409814899999999, result[1]) assert_equal(9.409814899999999, result[1])
result = GeoLocation.geocode( 'Martinsbruggstrasse 35 9016 St. Gallen' ) result = Service::GeoLocation.geocode( 'Martinsbruggstrasse 35 9016 St. Gallen' )
assert(result) assert(result)
assert_equal(47.4366664, result[0]) assert_equal(47.4366664, result[0])
assert_equal(9.409814899999999, result[1]) assert_equal(9.409814899999999, result[1])

View file

@ -147,8 +147,18 @@ class TwitterTest < ActiveSupport::TestCase
# fetch check system account # fetch check system account
Channel.fetch Channel.fetch
# check if follow up article has been created # fetch check system account
article = nil
(1..4).each {
Channel.fetch
# check if ticket and article has been created
article = Ticket::Article.find_by( message_id: tweet.id ) article = Ticket::Article.find_by( message_id: tweet.id )
break if article
sleep 5
}
assert(article) assert(article)
ticket = article.ticket ticket = article.ticket
@ -220,25 +230,86 @@ class TwitterTest < ActiveSupport::TestCase
sleep 5 sleep 5
} }
assert( article, 'inbound article created' ) assert( article, "inbound article '#{text}' created" )
ticket = article.ticket ticket = article.ticket
assert( ticket, 'ticket of inbound article exists' ) assert( ticket, 'ticket of inbound article exists' )
assert( ticket.articles, 'ticket.articles exists' ) assert( ticket.articles, 'ticket.articles exists' )
assert_equal( ticket.articles.count, 1, 'ticket article inbound count' ) assert_equal( 1, ticket.articles.count, 'ticket article inbound count' )
assert_equal( ticket.state.name, 'new' ) assert_equal( ticket.state.name, 'new' )
# reply via ticket # reply via ticket
outbound_article = Ticket::Article.create( outbound_article = Ticket::Article.create(
ticket_id: ticket.id, ticket_id: ticket.id,
to: 'me_bauer', to: 'me_bauer',
body: text, body: 'Will call you later!',
type: Ticket::Article::Type.find_by( name: 'twitter direct-message' ), type: Ticket::Article::Type.find_by( name: 'twitter direct-message' ),
sender: Ticket::Article::Sender.find_by( name: 'Agent' ), sender: Ticket::Article::Sender.find_by( name: 'Agent' ),
internal: false, internal: false,
updated_by_id: 1, updated_by_id: 1,
created_by_id: 1, created_by_id: 1,
) )
ticket.state = Ticket::State.find_by( name: 'pending reminder' )
ticket.save
assert( outbound_article, 'outbound article created' ) assert( outbound_article, 'outbound article created' )
assert_equal( outbound_article.ticket.articles.count, 2, 'ticket article outbound count' ) assert_equal( 2, outbound_article.ticket.articles.count, 'ticket article outbound count' )
text = 'Ok. ' + hash
dm = client.create_direct_message(
'armin_theo',
text,
)
assert( dm, "second dm with ##{hash} created" )
# fetch check system account
article = nil
(1..4).each {
Channel.fetch
# check if ticket and article has been created
article = Ticket::Article.find_by( message_id: dm.id )
break if article
sleep 5
}
assert( article, "inbound article '#{text}' created" )
ticket = article.ticket
assert( ticket, 'ticket of inbound article exists' )
assert( ticket.articles, 'ticket.articles exists' )
assert_equal( 3, ticket.articles.count, 'ticket article inbound count' )
assert_equal( ticket.state.name, 'open' )
# close dm ticket, next dm should open a new
ticket.state = Ticket::State.find_by( name: 'closed' )
ticket.save
text = 'Thanks for your call . I just have one question. ' + hash
dm = client.create_direct_message(
'armin_theo',
text,
)
assert( dm, "third dm with ##{hash} created" )
# fetch check system account
article = nil
(1..4).each {
Channel.fetch
# check if ticket and article has been created
article = Ticket::Article.find_by( message_id: dm.id )
break if article
sleep 5
}
assert( article, "inbound article '#{text}' created" )
ticket = article.ticket
assert( ticket, 'ticket of inbound article exists' )
assert( ticket.articles, 'ticket.articles exists' )
assert_equal( 1, ticket.articles.count, 'ticket article inbound count' )
assert_equal( ticket.state.name, 'new' )
end end
end end