diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee
index 02de5fa90..d2b8fba67 100644
--- a/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee
+++ b/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee
@@ -286,16 +286,24 @@ class App.ControllerTabs extends App.Controller
tabs: @tabs
)
+ # insert content
for tab in @tabs
- @el.find('.tab-content').append('
')
+ @el.find('.tab-content').append("")
if tab.controller
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 )
+ # 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')
- if @lastActiveTab && @el.find('.nav-tabs li a[href="' + @lastActiveTab + '"]')[0]
- @el.find('.nav-tabs li a[href="' + @lastActiveTab + '"]').tab('show')
+ if @lastActiveTab && @el.find(".nav-tabs li a[href=#{@lastActiveTab}]")[0]
+ @el.find(".nav-tabs li a[href=#{@lastActiveTab}]").tab('show')
else
@el.find('.nav-tabs li:first a').tab('show')
diff --git a/app/assets/javascripts/app/controllers/_settings/area.js.coffee b/app/assets/javascripts/app/controllers/_settings/area.js.coffee
index 4138c118b..3f2cbaa25 100644
--- a/app/assets/javascripts/app/controllers/_settings/area.js.coffee
+++ b/app/assets/javascripts/app/controllers/_settings/area.js.coffee
@@ -26,21 +26,30 @@ class App.SettingsArea extends App.Controller
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
settings = _.sortBy( settings, (setting) ->
return if !setting.preferences
setting.preferences.prio
)
- html = $('')
+ elements = []
for setting in settings
if setting.name is 'product_logo'
item = new App.SettingsAreaLogo( setting: setting )
else
item = new App.SettingsAreaItem( setting: setting )
- html.append( item.el )
+ elements.push item.el
- @html html
+ @html elements
class App.SettingsAreaItem extends App.Controller
events:
@@ -67,13 +76,13 @@ class App.SettingsAreaItem extends App.Controller
# item
@html App.view('settings/item')(
- setting: @setting,
+ setting: @setting
)
new App.ControllerForm(
el: @el.find('.form-item'),
- model: { configure_attributes: @configure_attributes, className: '' },
- autofocus: false,
+ model: { configure_attributes: @configure_attributes, className: '' }
+ autofocus: false
)
update: (e) =>
@@ -103,7 +112,6 @@ class App.SettingsAreaItem extends App.Controller
@setting.save(
done: =>
ui.formEnable(e)
-
App.Event.trigger 'notify', {
type: 'success'
msg: App.i18n.translateContent('Update successful!')
@@ -112,7 +120,6 @@ class App.SettingsAreaItem extends App.Controller
# rerender ui || get new collections and session data
if @setting.preferences
-
if @setting.preferences.render
ui.render()
App.Event.trigger( 'ui:rerender' )
@@ -121,6 +128,11 @@ class App.SettingsAreaItem extends App.Controller
App.Auth.loginCheck()
fail: =>
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
diff --git a/app/assets/javascripts/app/controllers/settings.js.coffee b/app/assets/javascripts/app/controllers/settings.js.coffee
index 6b632b657..19178754d 100644
--- a/app/assets/javascripts/app/controllers/settings.js.coffee
+++ b/app/assets/javascripts/app/controllers/settings.js.coffee
@@ -5,7 +5,7 @@ class Branding extends App.ControllerTabs
return if !@authenticate()
@title 'Branding', true
@tabs = [
- { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Branding' } },
+ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Branding' } }
]
@render()
@@ -15,12 +15,13 @@ class System extends App.ControllerTabs
super
return if !@authenticate()
@title 'System', true
- @tabs = [
- { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Base' } },
- { name: 'Storage', 'target': 'storage', controller: App.SettingsArea, params: { area: 'System::Storage' } },
- { name: 'Geo Services', 'target': 'geo', controller: App.SettingsArea, params: { area: 'System::Geo' } },
- { name: 'Frontend', 'target': 'ui', controller: App.SettingsArea, params: { area: 'System::UI' } },
- ]
+ @tabs = []
+ if !App.Config.get('system_online_service')
+ @tabs.push { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'System::Base' } }
+ @tabs.push { name: 'Services', 'target': 'services', controller: App.SettingsArea, params: { area: 'System::Services' } }
+ 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()
class Security extends App.ControllerTabs
@@ -30,11 +31,10 @@ class Security extends App.ControllerTabs
return if !@authenticate()
@title 'Security', true
@tabs = [
- { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Security::Base' } },
-# { name: 'Authentication', 'target': 'auth', controller: App.SettingsArea, params: { area: 'Security::Authentication' } },
- { 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: 'Session', 'target': 'session', controller: '' },
+ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Security::Base' } }
+# { name: 'Authentication', 'target': 'auth', controller: App.SettingsArea, params: { area: 'Security::Authentication' } }
+ { 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' } }
]
@render()
@@ -45,8 +45,8 @@ class Import extends App.ControllerTabs
return if !@authenticate()
@title 'Import', true
@tabs = [
- { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Import::Base' } },
- { name: 'OTRS', 'target': 'otrs', controller: App.SettingsArea, params: { area: 'Import::OTRS' } },
+ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Import::Base' } }
+ { name: 'OTRS', 'target': 'otrs', controller: App.SettingsArea, params: { area: 'Import::OTRS' } }
]
@render()
@@ -57,15 +57,14 @@ class Ticket extends App.ControllerTabs
return if !@authenticate()
@title 'Ticket', true
@tabs = [
- { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Ticket::Base' } },
- { name: 'Number', 'target': 'number', controller: App.SettingsArea, params: { area: 'Ticket::Number' } },
-# { name: 'Sender Format', 'target': 'sender-format', controller: App.SettingsArea, params: { area: 'Ticket::SenderFormat' } },
+ { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Ticket::Base' } }
+ { name: 'Number', 'target': 'number', controller: App.SettingsArea, params: { area: 'Ticket::Number' } }
]
@render()
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( 'SettingSecurity', { prio: 1500, 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( 'SettingImport', { prio: 1700, parent: '#settings', name: 'Import', target: '#settings/import', controller: Import, 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: 1600, parent: '#settings', name: 'Security', target: '#settings/security', controller: Security, 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: 1800, parent: '#settings', name: 'Import', target: '#settings/import', controller: Import, role: ['Admin'] }, 'NavBarAdmin' )
diff --git a/app/assets/javascripts/app/controllers/widget/ff_lt_35.js.coffee b/app/assets/javascripts/app/controllers/widget/ff_lt_35.js.coffee
index 31260a917..cffa499e6 100644
--- a/app/assets/javascripts/app/controllers/widget/ff_lt_35.js.coffee
+++ b/app/assets/javascripts/app/controllers/widget/ff_lt_35.js.coffee
@@ -1,7 +1,7 @@
class FFlt35
constructor: ->
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
# whole file can be removed after dropping firefox 34 and lower support
diff --git a/app/assets/javascripts/app/lib/app_init/log.js.coffee b/app/assets/javascripts/app/lib/app_init/log.js.coffee
index 013b9953d..8073e99db 100644
--- a/app/assets/javascripts/app/lib/app_init/log.js.coffee
+++ b/app/assets/javascripts/app/lib/app_init/log.js.coffee
@@ -45,16 +45,15 @@ class _Singleton
# detect color support
@colorSupport = false
data = App.Browser.detection()
- if data
- if data.browser is 'Chrome'
+ if data.browser
+ if data.browser.name is 'Chrome'
@colorSupport = true
- else if data.browser is 'Firefox'
- if data.version >= 31.0
+ else if data.browser.anem is 'Firefox'
+ if data.browser.major >= 31.0
@colorSupport = true
- else if data.browser is 'Safari'
+ else if data.browser.name is 'Safari'
@colorSupport = true
-
configReady: ->
for type, value of @currentConfig
if type is 'module' || type is 'content'
diff --git a/app/assets/javascripts/app/lib/app_post/browser.coffee b/app/assets/javascripts/app/lib/app_post/browser.coffee
index 880aa528f..d82f5a9a5 100644
--- a/app/assets/javascripts/app/lib/app_post/browser.coffee
+++ b/app/assets/javascripts/app/lib/app_post/browser.coffee
@@ -17,12 +17,14 @@ get used browser
}
###
+
class App.Browser
@detection: ->
+ parser = new UAParser()
data =
- browser: @searchString(@dataBrowser) or "An unknown browser"
- version: @searchVersion(navigator.userAgent) or @searchVersion(navigator.appVersion) or "an unknown version"
- os: @searchString(@dataOS) or "an unknown os"
+ browser: parser.getBrowser()
+ device: parser.getDevice()
+ os: parser.getOS()
@check: ->
data = @detection()
@@ -36,114 +38,21 @@ class App.Browser
Opera: 22
# disable id older
- if data.browser && data.version
- if map[data.browser] && data.version < map[data.browser]
- @message(data, data.browser, map[data.browser])
+ if data.browser
+ if map[data.browser.name] && data.browser.major < map[data.browser.name]
+ @message(data, map[data.browser.name])
console.log('Browser not supported')
return false
# allow browser
- return true
+ true
- @message: (data, browser, version) ->
+ @message: (data, version) ->
new App.ControllerModal(
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
backdrop: false
keyboard: false
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"
- ]
-
-
diff --git a/app/assets/javascripts/app/lib/base/ua-parser.js b/app/assets/javascripts/app/lib/base/ua-parser.js
new file mode 100644
index 000000000..b765142f5
--- /dev/null
+++ b/app/assets/javascripts/app/lib/base/ua-parser.js
@@ -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
+ * 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);
diff --git a/app/assets/javascripts/app/views/generic/tabs.jst.eco b/app/assets/javascripts/app/views/generic/tabs.jst.eco
index 721b36aec..da97ce037 100644
--- a/app/assets/javascripts/app/views/generic/tabs.jst.eco
+++ b/app/assets/javascripts/app/views/generic/tabs.jst.eco
@@ -3,7 +3,7 @@
<%- @T( @header ) %> <%- @T( @subHeader ) %>
-
+
<% for tab in @tabs: %>
- <%- @T( tab.name ) %>
<% end %>
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9ac15fd70..691b1197f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -86,7 +86,7 @@ class ApplicationController < ActionController::Base
# check if remote ip need to be updated
if !session[:remote_id] || 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
# fill user agent
diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
index 35df2d7ee..808453219 100644
--- a/app/controllers/settings_controller.rb
+++ b/app/controllers/settings_controller.rb
@@ -32,11 +32,13 @@ class SettingsController < ApplicationController
# PUT /settings/1
def update
return if deny_if_not_role(Z_ROLENAME_ADMIN)
+ return if !check_access
model_update_render(Setting, params)
end
# PUT /settings/image/:id
def update_image
+ return if deny_if_not_role(Z_ROLENAME_ADMIN)
if !params[:logo]
render json: {
@@ -91,4 +93,16 @@ class SettingsController < ApplicationController
return if deny_if_not_role(Z_ROLENAME_ADMIN)
model_destory_render(Setting, params)
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
diff --git a/app/controllers/translations_controller.rb b/app/controllers/translations_controller.rb
index 854e95aaa..5c98afbfc 100644
--- a/app/controllers/translations_controller.rb
+++ b/app/controllers/translations_controller.rb
@@ -22,6 +22,7 @@ class TranslationsController < ApplicationController
# POST /translations/sync
def sync
return if deny_if_not_role(Z_ROLENAME_ADMIN)
+ Locale.load
Translation.load
render json: { message: 'ok' }, status: :ok
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index aeaf6acb3..ef7139862 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -130,7 +130,7 @@ class UsersController < ApplicationController
# fetch org logo
if user.email
- Zammad::BigData::Organization.suggest_system_image(user.email)
+ Service::Image.organization_suggest(user.email)
end
end
diff --git a/app/models/avatar.rb b/app/models/avatar.rb
index 50c879c87..42630c8e0 100644
--- a/app/models/avatar.rb
+++ b/app/models/avatar.rb
@@ -146,7 +146,7 @@ add a avatar
end
# fetch image
- image = Zammad::BigData::User.image(data[:url])
+ image = Service::Image.user(data[:url])
return if !image
if !data[:resize]
data[:resize] = {}
diff --git a/app/models/channel/facebook.rb b/app/models/channel/facebook.rb
index 4cc289611..2afb09a9a 100644
--- a/app/models/channel/facebook.rb
+++ b/app/models/channel/facebook.rb
@@ -7,7 +7,7 @@ class Channel::Facebook
def fetch (channel)
@channel = channel
- @facebook = Facebook.new( @channel[:options][:auth] )
+ @facebook = Facebook.new( @channel[:options] )
@sync = @channel[:options][:sync]
Rails.logger.debug 'facebook fetch started'
@@ -22,7 +22,7 @@ class Channel::Facebook
def send(article, _notification = false)
@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)
disconnect
diff --git a/app/models/locale.rb b/app/models/locale.rb
index 5635f7ae6..6381e3b9f 100644
--- a/app/models/locale.rb
+++ b/app/models/locale.rb
@@ -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?
ActiveRecord::Base.transaction do
diff --git a/app/models/observer/user/geo.rb b/app/models/observer/user/geo.rb
index c666a41a7..92ffe2abb 100644
--- a/app/models/observer/user/geo.rb
+++ b/app/models/observer/user/geo.rb
@@ -54,7 +54,7 @@ class Observer::User::Geo < ActiveRecord::Observer
return if address == ''
# lookup
- latlng = GeoLocation.geocode( address )
+ latlng = Service::GeoLocation.geocode( address )
return if !latlng
# store data
diff --git a/app/models/ticket/number/increment.rb b/app/models/ticket/number/increment.rb
index 6827ade2f..9f1b97650 100644
--- a/app/models/ticket/number/increment.rb
+++ b/app/models/ticket/number/increment.rb
@@ -30,7 +30,7 @@ module Ticket::Number::Increment
if config[:checksum]
min_digs = min_digs.to_i - 1
end
- fillup = Setting.get('system_id') || '1'
+ fillup = Setting.get('system_id').to_s || '1'
( 1..100 ).each {
next if ( fillup.length.to_i + counter_increment.to_s.length.to_i ) >= min_digs.to_i
diff --git a/db/migrate/20150715000001_update_services.rb b/db/migrate/20150715000001_update_services.rb
new file mode 100644
index 000000000..23c2f6d44
--- /dev/null
+++ b/db/migrate/20150715000001_update_services.rb
@@ -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
diff --git a/db/migrate/20150716000001_update_settings.rb b/db/migrate/20150716000001_update_settings.rb
new file mode 100644
index 000000000..685412b9e
--- /dev/null
+++ b/db/migrate/20150716000001_update_settings.rb
@@ -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
diff --git a/db/migrate/20150716000003_add_setting_online_service.rb b/db/migrate/20150716000003_add_setting_online_service.rb
new file mode 100644
index 000000000..7c78ca3a5
--- /dev/null
+++ b/db/migrate/20150716000003_add_setting_online_service.rb
@@ -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
diff --git a/db/migrate/20150717000001_facebook_article_types.rb b/db/migrate/20150717000001_facebook_article_types.rb
new file mode 100644
index 000000000..b8fad083f
--- /dev/null
+++ b/db/migrate/20150717000001_facebook_article_types.rb
@@ -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
diff --git a/db/seeds.rb b/db/seeds.rb
index da79d6826..a28b1e794 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -13,6 +13,7 @@ Setting.create_if_not_exists(
description: 'Defines if application is in init mode.',
options: {},
state: false,
+ preferences: { online_service_disable: true },
frontend: true
)
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).',
options: {},
state: false,
+ preferences: { online_service_disable: true },
frontend: true
)
Setting.create_if_not_exists(
@@ -31,6 +33,7 @@ Setting.create_if_not_exists(
description: 'Defines if application is used as online service.',
options: {},
state: false,
+ preferences: { online_service_disable: true },
frontend: true
)
Setting.create_if_not_exists(
@@ -49,7 +52,7 @@ Setting.create_if_not_exists(
],
},
preferences: { render: true, session_check: true, prio: 1 },
- state: 'Zammad',
+ state: 'Zammad Helpdesk',
frontend: true
)
Setting.create_if_not_exists(
@@ -75,7 +78,7 @@ Setting.create_if_not_exists(
title: 'Organization',
name: 'organization',
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: {
form: [
{
@@ -113,6 +116,7 @@ Setting.create_if_not_exists(
],
},
state: '10',
+ preferences: { online_service_disable: true },
frontend: true
)
Setting.create_if_not_exists(
@@ -131,6 +135,7 @@ Setting.create_if_not_exists(
],
},
state: 'zammad.example.com',
+ preferences: { online_service_disable: true },
frontend: true
)
Setting.create_if_not_exists(
@@ -149,6 +154,7 @@ Setting.create_if_not_exists(
],
},
state: '6042',
+ preferences: { online_service_disable: true },
frontend: true
)
Setting.create_if_not_exists(
@@ -171,6 +177,7 @@ Setting.create_if_not_exists(
],
},
state: 'http',
+ preferences: { online_service_disable: true },
frontend: true
)
@@ -194,35 +201,39 @@ Setting.create_if_not_exists(
],
},
state: 'DB',
+ preferences: { online_service_disable: true },
frontend: false
)
+
Setting.create_if_not_exists(
- title: 'Geo Location Backend',
- name: 'geo_location_backend',
- area: 'System::Geo',
- description: 'Defines the backend for geo location lookups.',
+ 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: 'geo_location_backend',
+ name: 'image_backend',
tag: 'select',
options: {
'' => '-',
- 'GeoLocation::Gmaps' => 'Google Maps',
+ 'Service::Image::Zammad' => 'Zammad Image Service',
},
},
],
},
- state: 'GeoLocation::Gmaps',
+ state: 'Service::Image::Zammad',
+ preferences: { prio: 1 },
frontend: false
)
+
Setting.create_if_not_exists(
- title: 'Geo IP Backend',
+ title: 'Geo IP Service',
name: 'geo_ip_backend',
- area: 'System::Geo',
- description: 'Defines the backend for geo ip lookups.',
+ 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: [
{
@@ -232,12 +243,37 @@ Setting.create_if_not_exists(
tag: 'select',
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
)
@@ -245,7 +281,7 @@ Setting.create_if_not_exists(
title: 'Send client stats',
name: 'ui_send_client_stats',
area: 'System::UI',
- description: 'Send client stats to central server.',
+ description: 'Send client stats/error message to central server to improve the usability.',
options: {
form: [
{
@@ -261,6 +297,7 @@ Setting.create_if_not_exists(
],
},
state: true,
+ preferences: { prio: 1 },
frontend: true
)
Setting.create_if_not_exists(
@@ -283,6 +320,7 @@ Setting.create_if_not_exists(
],
},
state: false,
+ preferences: { prio: 2 },
frontend: true
)
@@ -1034,6 +1072,7 @@ Setting.create_if_not_exists(
],
},
state: 10,
+ preferences: { online_service_disable: true },
frontend: false
)
@@ -1078,6 +1117,7 @@ Setting.create_if_not_exists(
],
},
state: 'Notification Master ',
+ preferences: { online_service_disable: true },
frontend: false
)
@@ -1140,6 +1180,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch',
description: 'Define endpoint of Elastic Search.',
state: '',
+ preferences: { online_service_disable: true },
frontend: false
)
Setting.create_if_not_exists(
@@ -1148,6 +1189,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch',
description: 'Define http basic auth user of Elasticsearch.',
state: '',
+ preferences: { online_service_disable: true },
frontend: false
)
Setting.create_if_not_exists(
@@ -1156,6 +1198,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch',
description: 'Define http basic auth password of Elasticsearch.',
state: '',
+ preferences: { online_service_disable: true },
frontend: false
)
Setting.create_if_not_exists(
@@ -1164,6 +1207,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch',
description: 'Define Elasticsearch index name.',
state: 'zammad',
+ preferences: { online_service_disable: true },
frontend: false
)
Setting.create_if_not_exists(
@@ -1172,6 +1216,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch',
description: 'Define attachment extentions which are ignored for Elasticsearch.',
state: [ '.png', '.jpg', '.jpeg', '.mpeg', '.mpg', '.mov', '.bin', '.exe', '.box', '.mbox' ],
+ preferences: { online_service_disable: true },
frontend: false
)
Setting.create_if_not_exists(
@@ -1180,6 +1225,7 @@ Setting.create_if_not_exists(
area: 'SearchIndex::Elasticsearch',
description: 'Define max. attachment size for Elasticsearch.',
state: 50,
+ preferences: { online_service_disable: true },
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: 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: 8, name: 'facebook', communication: true )
-Ticket::Article::Type.create_if_not_exists( id: 9, name: 'note', communication: false )
-Ticket::Article::Type.create_if_not_exists( id: 10, name: 'web', 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: 'facebook feed comment', 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: 2, name: 'Customer' )
diff --git a/lib/auto_wizard.rb b/lib/auto_wizard.rb
index fac763fee..b1cfdb703 100644
--- a/lib/auto_wizard.rb
+++ b/lib/auto_wizard.rb
@@ -111,7 +111,7 @@ returns
# fetch org logo
if admin_user.email
- Zammad::BigData::Organization.suggest_system_image(admin_user.email)
+ Service::Image.organization_suggest(admin_user.email)
end
}
end
diff --git a/lib/facebook.rb b/lib/facebook.rb
index a2b8d880e..a07842d3d 100644
--- a/lib/facebook.rb
+++ b/lib/facebook.rb
@@ -130,37 +130,41 @@ class Facebook
ticket.save
end
- user = to_user(comment)
+ user = to_user(post)
return if !user
feed_post = {
- from: user.name,
+ from: "#{user.firstname} #{user.lastname}",
body: post['message'],
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.push( feed_post )
- post['comments']['data'].each { |comment|
+ if post['comments']
+ post['comments']['data'].each { |comment|
- user = to_user(comment)
+ user = to_user(comment)
- next if !user
+ next if !user
- post_comment = {
- from: user.name,
- body: comment['message'],
- message_id: comment['id'],
- type: Ticket::Article::Type.find_by( name: 'facebook feed comment' ),
+ post_comment = {
+ from: "#{user.firstname} #{user.lastname}",
+ body: comment['message'],
+ message_id: comment['id'],
+ type_id: Ticket::Article::Type.find_by( name: 'facebook feed comment' ).id,
+ }
+ articles.push( post_comment )
+
+ # TODO: sub-comments
+ # comment_data = @client.get_object( comment['id'] )
}
- articles.push( post_comment )
+ end
- # TODO: sub-comments
- # comment_data = @client.get_object( comment['id'] )
- }
+ inverted_articles = articles.reverse
- articles.invert.each { |article|
+ inverted_articles.each { |article|
break if Ticket::Article.find_by( message_id: article[:message_id] )
@@ -168,6 +172,9 @@ class Facebook
to: @account['name'],
ticket_id: ticket.id,
internal: false,
+ sender_id: Ticket::Article::Sender.lookup( name: 'Customer' ).id,
+ created_by_id: 1,
+ updated_by_id: 1,
}.merge( article )
Ticket::Article.create( article )
@@ -204,12 +211,11 @@ class Facebook
def from_article(article)
post = nil
- # TODO: article[:type] == 'facebook feed post'
if article[:type] == 'facebook feed comment'
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
fail "Can't handle unknown facebook article type '#{article[:type]}'."
end
diff --git a/lib/geo_location.rb b/lib/geo_location.rb
deleted file mode 100644
index ea3dd27cf..000000000
--- a/lib/geo_location.rb
+++ /dev/null
@@ -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
diff --git a/lib/geo_ip.rb b/lib/service/geo_ip.rb
similarity index 63%
rename from lib/geo_ip.rb
rename to lib/service/geo_ip.rb
index 9a111d5df..a2a6291c4 100644
--- a/lib/geo_ip.rb
+++ b/lib/service/geo_ip.rb
@@ -1,13 +1,14 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
-class GeoIp
- include ApplicationLib
+module Service
+ class GeoIp
+ include ApplicationLib
=begin
lookup location based on ip or hostname
- result = GeoIp.location( '172.0.0.1' )
+ result = Service::GeoIp.location( '172.0.0.1' )
returns
@@ -27,13 +28,14 @@ returns
=end
- def self.location(address)
+ def self.location(address)
- # load backend
- backend = load_adapter_by_setting( 'geo_ip_backend' )
- return if !backend
+ # load backend
+ backend = load_adapter_by_setting( 'geo_ip_backend' )
+ return if !backend
- # db lookup
- backend.location(address)
+ # db lookup
+ backend.location(address)
+ end
end
end
diff --git a/lib/geo_ip/zammad_geo_ip.rb b/lib/service/geo_ip/zammad.rb
similarity index 97%
rename from lib/geo_ip/zammad_geo_ip.rb
rename to lib/service/geo_ip/zammad.rb
index a49daa5e8..a69a7a1d8 100644
--- a/lib/geo_ip/zammad_geo_ip.rb
+++ b/lib/service/geo_ip/zammad.rb
@@ -2,7 +2,7 @@
require 'cache'
-class GeoIp::ZammadGeoIp
+class Service::GeoIp::Zammad
def self.location(address)
# check cache
diff --git a/lib/service/geo_location.rb b/lib/service/geo_location.rb
new file mode 100644
index 000000000..690e14830
--- /dev/null
+++ b/lib/service/geo_location.rb
@@ -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
diff --git a/lib/geo_location/gmaps.rb b/lib/service/geo_location/gmaps.rb
similarity index 96%
rename from lib/geo_location/gmaps.rb
rename to lib/service/geo_location/gmaps.rb
index 59a61ec1a..eb98313a6 100644
--- a/lib/geo_location/gmaps.rb
+++ b/lib/service/geo_location/gmaps.rb
@@ -1,6 +1,6 @@
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
-class GeoLocation::Gmaps
+class Service::GeoLocation::Gmaps
def self.geocode(address)
url = "http://maps.googleapis.com/maps/api/geocode/json?address=#{CGI.escape address}&sensor=true"
diff --git a/lib/service/image.rb b/lib/service/image.rb
new file mode 100644
index 000000000..0d3b87c7c
--- /dev/null
+++ b/lib/service/image.rb
@@ -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
diff --git a/lib/service/image/zammad.rb b/lib/service/image/zammad.rb
new file mode 100644
index 000000000..f1e23ec48
--- /dev/null
+++ b/lib/service/image/zammad.rb
@@ -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
diff --git a/lib/tweet.rb b/lib/tweet.rb
index 185c06b99..94c1fafbd 100644
--- a/lib/tweet.rb
+++ b/lib/tweet.rb
@@ -97,23 +97,32 @@ class Tweet
Rails.logger.debug group_id.inspect
if tweet.class.to_s == 'Twitter::DirectMessage'
- ticket = Ticket.find_by(
- customer_id: user.id,
- state: Ticket::State.where.not(
- state_type_id: Ticket::StateType.where(
- name: 'closed',
+
+ 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(
+ id: article.ticket_id,
+ customer_id: user.id,
+ state: Ticket::State.where.not(
+ state_type_id: Ticket::StateType.where(
+ name: 'closed',
+ )
)
)
- )
- return ticket if ticket
+ return ticket if ticket
+ end
end
Ticket.create(
customer_id: user.id,
title: "#{tweet.text[0, 37]}...",
group_id: group_id,
- state: Ticket::State.find_by( name: 'new' ),
- priority: Ticket::Priority.find_by( name: '2 normal' ),
+ state_id: Ticket::State.find_by( name: 'new' ).id,
+ priority_id: Ticket::Priority.find_by( name: '2 normal' ).id,
)
end
@@ -153,8 +162,8 @@ class Tweet
message_id: tweet.id,
ticket_id: ticket.id,
in_reply_to: in_reply_to,
- type: Ticket::Article::Type.find_by( name: article_type ),
- sender: Ticket::Article::Sender.find_by( name: 'Customer' ),
+ type_id: Ticket::Article::Type.find_by( name: article_type ).id,
+ sender_id: Ticket::Article::Sender.find_by( name: 'Customer' ).id,
internal: false,
)
end
diff --git a/lib/zammad/big_data/base.rb b/lib/zammad/big_data/base.rb
deleted file mode 100644
index 95f405f17..000000000
--- a/lib/zammad/big_data/base.rb
+++ /dev/null
@@ -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
diff --git a/lib/zammad/big_data/organization.rb b/lib/zammad/big_data/organization.rb
deleted file mode 100644
index f15f12056..000000000
--- a/lib/zammad/big_data/organization.rb
+++ /dev/null
@@ -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
diff --git a/lib/zammad/big_data/user.rb b/lib/zammad/big_data/user.rb
deleted file mode 100644
index b12d08aae..000000000
--- a/lib/zammad/big_data/user.rb
+++ /dev/null
@@ -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
diff --git a/script/local_browser_tests.sh b/script/local_browser_tests.sh
index 1591b48fa..5e00b6a98 100755
--- a/script/local_browser_tests.sh
+++ b/script/local_browser_tests.sh
@@ -50,6 +50,15 @@ sleep 15
#export REMOTE_URL='http://192.168.178.32: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 BROWSER=chrome"]
#rake test:browser["BROWSER_URL=http://192.168.178.28:4444"]
diff --git a/script/websocket-server.rb b/script/websocket-server.rb
index 578b4fdcf..21e75b78a 100755
--- a/script/websocket-server.rb
+++ b/script/websocket-server.rb
@@ -59,11 +59,10 @@ if ARGV[0] != 'start' && ARGV[0] != 'stop'
exit
end
-puts "Starting websocket server on #{@options[:b]}:#{@options[:p]} (secure:#{@options[:s]},pid:#{@options[:i]})"
-#puts options.inspect
-
if ARGV[0] == 'stop'
+ puts "Stopping websocket server (pid:#{@options[:i]})"
+
# read pid
pid = File.open( @options[:i].to_s ).read
pid.gsub!(/\r|\n/, '')
@@ -74,6 +73,8 @@ if ARGV[0] == 'stop'
end
if ARGV[0] == 'start' && @options[:d]
+ puts "Starting websocket server on #{@options[:b]}:#{@options[:p]} (secure:#{@options[:s]},pid:#{@options[:i]})"
+
Daemons.daemonize
# create pid file
diff --git a/test/browser/agent_ticket_actions_level3_test.rb b/test/browser/agent_ticket_actions_level3_test.rb
index 619ccd6fa..2e0ae75ad 100644
--- a/test/browser/agent_ticket_actions_level3_test.rb
+++ b/test/browser/agent_ticket_actions_level3_test.rb
@@ -206,7 +206,7 @@ class AgentTicketActionsLevel3Test < TestCase
css: '.content.active .js-reset',
browser: browser2,
)
- sleep 2
+ sleep 5
ticket_verify(
browser: browser2,
data: {
diff --git a/test/browser/agent_ticket_overview_level0_test.rb b/test/browser/agent_ticket_overview_level0_test.rb
index d2824bde8..4120eaeda 100644
--- a/test/browser/agent_ticket_overview_level0_test.rb
+++ b/test/browser/agent_ticket_overview_level0_test.rb
@@ -104,11 +104,11 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is shown
match(
- css: '.active table th:nth-child(3)',
+ css: '.active table th:nth-child(7)',
value: '#',
)
match(
- css: '.active table th:nth-child(8)',
+ css: '.active table th:nth-child(4)',
value: 'Article#',
)
@@ -118,11 +118,11 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is shown
match(
- css: '.active table th:nth-child(3)',
+ css: '.active table th:nth-child(7)',
value: '#',
)
match(
- css: '.active table th:nth-child(8)',
+ css: '.active table th:nth-child(4)',
value: 'Article#',
)
diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb
index 3defb2aa9..02bff8d11 100644
--- a/test/browser_test_helper.rb
+++ b/test/browser_test_helper.rb
@@ -9,6 +9,22 @@ class TestCase < Test::Unit::TestCase
ENV['BROWSER'] || 'firefox'
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
if browser =~ /(internet_explorer|ie)/i
return false
@@ -25,7 +41,7 @@ class TestCase < Test::Unit::TestCase
@browsers = {}
end
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)
@browsers[local_browser.hash] = local_browser
return local_browser
@@ -1226,7 +1242,7 @@ wait untill text in selector disabppears
found = nil
(1..10).each {
- next if found
+ break if found
begin
text = instance.find_elements( { css: '.content.active .js-reset' } )[0].text
@@ -1240,6 +1256,7 @@ wait untill text in selector disabppears
}
if !found
screenshot( browser: instance, comment: 'ticket_update_discard_message_failed' )
+
fail 'no discard message found'
end
end
diff --git a/test/integration/facebook_test.rb b/test/integration/facebook_test.rb
new file mode 100644
index 000000000..6e118ea02
--- /dev/null
+++ b/test/integration/facebook_test.rb
@@ -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
diff --git a/test/integration/geo_ip_test.rb b/test/integration/geo_ip_test.rb
index 07fdbbd00..477b77968 100644
--- a/test/integration/geo_ip_test.rb
+++ b/test/integration/geo_ip_test.rb
@@ -6,7 +6,7 @@ class GeoIpTest < ActiveSupport::TestCase
# check
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_equal(nil, result['country_name'])
assert_equal(nil, result['city_name'])
@@ -15,7 +15,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(nil, result['latitude'])
assert_equal(nil, result['longitude'])
- result = GeoIp.location( '195.65.29.254' )
+ result = Service::GeoIp.location( '195.65.29.254' )
assert(result)
assert_equal('Switzerland', result['country_name'])
assert_equal('Regensdorf', result['city_name'])
@@ -24,7 +24,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(47.4299, result['latitude'])
assert_equal(8.465100000000007, result['longitude'])
- result = GeoIp.location( '134.109.140.74' )
+ result = Service::GeoIp.location( '134.109.140.74' )
assert(result)
assert_equal('Germany', result['country_name'])
assert_equal('Chemnitz', result['city_name'])
@@ -33,7 +33,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(50.83330000000001, result['latitude'])
assert_equal(12.916699999999992, result['longitude'])
- result = GeoIp.location( '46.253.55.170' )
+ result = Service::GeoIp.location( '46.253.55.170' )
assert(result)
assert_equal('Germany', result['country_name'])
assert_equal('Halle', result['city_name'])
@@ -42,7 +42,7 @@ class GeoIpTest < ActiveSupport::TestCase
assert_equal(51.5, result['latitude'])
assert_equal(12.0, result['longitude'])
- result = GeoIp.location( '169.229.216.200' )
+ result = Service::GeoIp.location( '169.229.216.200' )
assert(result)
assert_equal('United States', result['country_name'])
assert_equal('Berkeley', result['city_name'])
diff --git a/test/integration/geo_location_test.rb b/test/integration/geo_location_test.rb
index da928f104..5a69ab11a 100644
--- a/test/integration/geo_location_test.rb
+++ b/test/integration/geo_location_test.rb
@@ -6,22 +6,22 @@ class GeoLocationTest < ActiveSupport::TestCase
# check
test 'check simple results' do
- result = GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' )
+ result = Service::GeoLocation.geocode( 'Marienstrasse 13, 10117 Berlin' )
assert(result)
assert_equal(52.52204, result[0])
assert_equal(13.38319, result[1])
- result = GeoLocation.geocode( 'Marienstrasse 13 10117 Berlin' )
+ result = Service::GeoLocation.geocode( 'Marienstrasse 13 10117 Berlin' )
assert(result)
assert_equal(52.52204, result[0])
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_equal(47.4366664, result[0])
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_equal(47.4366664, result[0])
assert_equal(9.409814899999999, result[1])
diff --git a/test/integration/twitter_test.rb b/test/integration/twitter_test.rb
index 0a2f4518d..b0c427945 100644
--- a/test/integration/twitter_test.rb
+++ b/test/integration/twitter_test.rb
@@ -147,8 +147,18 @@ class TwitterTest < ActiveSupport::TestCase
# fetch check system account
Channel.fetch
- # check if follow up article has been created
- article = Ticket::Article.find_by( message_id: tweet.id )
+ # 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 )
+
+ break if article
+
+ sleep 5
+ }
assert(article)
ticket = article.ticket
@@ -220,25 +230,86 @@ class TwitterTest < ActiveSupport::TestCase
sleep 5
}
- assert( article, 'inbound article created' )
+ assert( article, "inbound article '#{text}' created" )
ticket = article.ticket
assert( ticket, 'ticket of inbound article 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' )
# reply via ticket
outbound_article = Ticket::Article.create(
ticket_id: ticket.id,
to: 'me_bauer',
- body: text,
+ body: 'Will call you later!',
type: Ticket::Article::Type.find_by( name: 'twitter direct-message' ),
sender: Ticket::Article::Sender.find_by( name: 'Agent' ),
internal: false,
updated_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_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