From 5fb6bfed7df8fe840dc97130f58eb5c46081d808 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 29 Jan 2018 00:52:02 +0100 Subject: [PATCH] Added feature to restrict chat by ip or/and country. --- .../_ui_element/column_select.coffee | 16 +- .../app/lib/app_post/column_select.coffee | 52 ++-- app/assets/javascripts/app/models/chat.coffee | 250 +++++++++++++++++- .../app/views/generic/column_select.jst.eco | 4 + app/models/chat.rb | 42 +++ db/migrate/20120101000010_create_ticket.rb | 2 + .../20180128000001_chat_add_ip_country.rb | 10 + lib/sessions/event/chat_status_customer.rb | 28 ++ test/unit/chat_test.rb | 36 ++- 9 files changed, 412 insertions(+), 28 deletions(-) create mode 100644 db/migrate/20180128000001_chat_add_ip_country.rb diff --git a/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee b/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee index e7ab5301e..6741b3946 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee @@ -6,24 +6,24 @@ class App.UiElement.column_select extends App.UiElement.ApplicationUiElement attribute.multiple = 'multiple' # build options list based on config - @getConfigOptionList( attribute, params ) + @getConfigOptionList(attribute, params) # build options list based on relation - @getRelationOptionList( attribute, params ) + @getRelationOptionList(attribute, params) # add null selection if needed - @addNullOption( attribute, params ) + @addNullOption(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # find selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) # disable item of list - @disabledOptions( attribute, params ) + @disabledOptions(attribute, params) # filter attributes - @filterOption( attribute, params ) + @filterOption(attribute, params) - new App.ColumnSelect( attribute: attribute ).element() + new App.ColumnSelect(attribute: attribute).element() diff --git a/app/assets/javascripts/app/lib/app_post/column_select.coffee b/app/assets/javascripts/app/lib/app_post/column_select.coffee index 728c2d499..a38cca006 100644 --- a/app/assets/javascripts/app/lib/app_post/column_select.coffee +++ b/app/assets/javascripts/app/lib/app_post/column_select.coffee @@ -39,14 +39,27 @@ class App.ColumnSelect extends Spine.Controller ) render: -> + if !_.isEmpty(@attribute.seperator) + values = [] + if @attribute.value + values = @attribute.value.split(';') + else if @attribute.default + values = @attribute.default.split(';') + + for value in values + for option in @options.attribute.options + if option.value is value + option.selected = true + @values = [] _.each @options.attribute.options, (option) => if option.selected @values.push option.value.toString() - @html App.view('generic/column_select') + @html App.view('generic/column_select')( attribute: @options.attribute values: @values + ) # keep inital height # disabled for now since controls in modals get rendered hidden @@ -60,13 +73,17 @@ class App.ColumnSelect extends Spine.Controller @throttledSelect() select: (value) -> - @selected.find("[data-value='#{value}']").removeClass 'is-hidden' - @pool.find("[data-value='#{value}']").addClass 'is-hidden' + @selected.find("[data-value='#{value}']").removeClass('is-hidden') + @pool.find("[data-value='#{value}']").addClass('is-hidden') @values.push(value) - @shadow.val(@values) - @shadow.trigger('change') - @placeholder.addClass 'is-hidden' + if !_.isEmpty(@attribute.seperator) + @shadow.val(@values.join(';')) + else + @shadow.val(@values) + @shadow.trigger('change') + + @placeholder.addClass('is-hidden') if @search.val() and @poolOptions.not('.is-filtered').not('.is-hidden').size() is 0 @clear() @@ -76,14 +93,17 @@ class App.ColumnSelect extends Spine.Controller @throttledRemove() remove: (value) -> - @pool.find("[data-value='#{value}']").removeClass 'is-hidden' - @selected.find("[data-value='#{value}']").addClass 'is-hidden' + @pool.find("[data-value='#{value}']").removeClass('is-hidden') + @selected.find("[data-value='#{value}']").addClass('is-hidden') @values.splice(@values.indexOf(value), 1) - @shadow.val(@values) - @shadow.trigger('change') + if !_.isEmpty(@attribute.seperator) + @shadow.val(@values.join(';')) + else + @shadow.val(@values) + @shadow.trigger('change') if !@values.length - @placeholder.removeClass 'is-hidden' + @placeholder.removeClass('is-hidden') filter: (event) -> filter = $(event.currentTarget).val() @@ -92,16 +112,16 @@ class App.ColumnSelect extends Spine.Controller return if $(el).hasClass('is-hidden') if $(el).text().toLowerCase().indexOf(filter.toLowerCase()) > -1 - $(el).removeClass 'is-filtered' + $(el).removeClass('is-filtered') else - $(el).addClass 'is-filtered' + $(el).addClass('is-filtered') @clearButton.toggleClass 'is-hidden', filter.length is 0 clear: -> @search.val('') - @poolOptions.removeClass 'is-filtered' - @clearButton.addClass 'is-hidden' + @poolOptions.removeClass('is-filtered') + @clearButton.addClass('is-hidden') onFilterKeydown: (event) -> return if event.keyCode != 13 @@ -111,4 +131,4 @@ class App.ColumnSelect extends Spine.Controller firstVisibleOption = @poolOptions.not('.is-filtered').not('.is-hidden').first() if firstVisibleOption - @select firstVisibleOption.attr('data-value') \ No newline at end of file + @select firstVisibleOption.attr('data-value') diff --git a/app/assets/javascripts/app/models/chat.coffee b/app/assets/javascripts/app/models/chat.coffee index 4a93284bb..0be9acfb4 100644 --- a/app/assets/javascripts/app/models/chat.coffee +++ b/app/assets/javascripts/app/models/chat.coffee @@ -1,16 +1,260 @@ class App.Chat extends App.Model - @configure 'Chat', 'name', 'active', 'public', 'max_queue', 'note' + @configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'block_country', 'note' @extend Spine.Model.Ajax @url: @apiPath + '/chats' + @countries: + AF: 'Afghanistan' + AL: 'Albania' + DZ: 'Algeria' + AS: 'American Samoa' + AD: 'Andorra' + AO: 'Angola' + AI: 'Anguilla' + AQ: 'Antarctica' + AG: 'Antigua And Barbuda' + AR: 'Argentina' + AM: 'Armenia' + AW: 'Aruba' + AU: 'Australia' + AT: 'Austria' + AZ: 'Azerbaijan' + BS: 'Bahamas' + BH: 'Bahrain' + BD: 'Bangladesh' + BB: 'Barbados' + BY: 'Belarus' + BE: 'Belgium' + BZ: 'Belize' + BJ: 'Benin' + BM: 'Bermuda' + BT: 'Bhutan' + BO: 'Bolivia' + BA: 'Bosnia And Herzegovina' + BW: 'Botswana' + BV: 'Bouvet Island' + BR: 'Brazil' + IO: 'British Indian Ocean Territory' + BN: 'Brunei Darussalam' + BG: 'Bulgaria' + BF: 'Burkina Faso' + BI: 'Burundi' + KH: 'Cambodia' + CM: 'Cameroon' + CA: 'Canada' + CV: 'Cape Verde' + KY: 'Cayman Islands' + CF: 'Central African Republic' + TD: 'Chad' + CL: 'Chile' + CN: 'China' + CX: 'Christmas Island' + CC: 'Cocos (keeling) Islands' + CO: 'Colombia' + KM: 'Comoros' + CG: 'Congo' + CD: 'Congo, The Democratic Republic Of The' + CK: 'Cook Islands' + CR: 'Costa Rica' + CI: 'Cote D\'ivoire' + HR: 'Croatia' + CU: 'Cuba' + CY: 'Cyprus' + CZ: 'Czech Republic' + DK: 'Denmark' + DJ: 'Djibouti' + DM: 'Dominica' + DO: 'Dominican Republic' + TP: 'East Timor' + EC: 'Ecuador' + EG: 'Egypt' + SV: 'El Salvador' + GQ: 'Equatorial Guinea' + ER: 'Eritrea' + EE: 'Estonia' + ET: 'Ethiopia' + FK: 'Falkland Islands (malvinas)' + FO: 'Faroe Islands' + FJ: 'Fiji' + FI: 'Finland' + FR: 'France' + GF: 'French Guiana' + PF: 'French Polynesia' + TF: 'French Southern Territories' + GA: 'Gabon' + GM: 'Gambia' + GE: 'Georgia' + DE: 'Germany' + GH: 'Ghana' + GI: 'Gibraltar' + GR: 'Greece' + GL: 'Greenland' + GD: 'Grenada' + GP: 'Guadeloupe' + GU: 'Guam' + GT: 'Guatemala' + GN: 'Guinea' + GW: 'Guinea-bissau' + GY: 'Guyana' + HT: 'Haiti' + HM: 'Heard Island And Mcdonald Islands' + VA: 'Holy See (vatican City State)' + HN: 'Honduras' + HK: 'Hong Kong' + HU: 'Hungary' + IS: 'Iceland' + IN: 'India' + ID: 'Indonesia' + IR: 'Iran, Islamic Republic Of' + IQ: 'Iraq' + IE: 'Ireland' + IL: 'Israel' + IT: 'Italy' + JM: 'Jamaica' + JP: 'Japan' + JO: 'Jordan' + KZ: 'Kazakstan' + KE: 'Kenya' + KI: 'Kiribati' + KP: 'Korea, Democratic People\'s Republic Of' + KR: 'Korea, Republic Of' + KV: 'Kosovo' + KW: 'Kuwait' + KG: 'Kyrgyzstan' + LA: 'Lao People\'s Democratic Republic' + LV: 'Latvia' + LB: 'Lebanon' + LS: 'Lesotho' + LR: 'Liberia' + LY: 'Libyan Arab Jamahiriya' + LI: 'Liechtenstein' + LT: 'Lithuania' + LU: 'Luxembourg' + MO: 'Macau' + MK: 'Macedonia, The Former Yugoslav Republic Of' + MG: 'Madagascar' + MW: 'Malawi' + MY: 'Malaysia' + MV: 'Maldives' + ML: 'Mali' + MT: 'Malta' + MH: 'Marshall Islands' + MQ: 'Martinique' + MR: 'Mauritania' + MU: 'Mauritius' + YT: 'Mayotte' + MX: 'Mexico' + FM: 'Micronesia, Federated States Of' + MD: 'Moldova, Republic Of' + MC: 'Monaco' + MN: 'Mongolia' + MS: 'Montserrat' + ME: 'Montenegro' + MA: 'Morocco' + MZ: 'Mozambique' + MM: 'Myanmar' + NA: 'Namibia' + NR: 'Nauru' + NP: 'Nepal' + NL: 'Netherlands' + AN: 'Netherlands Antilles' + NC: 'New Caledonia' + NZ: 'New Zealand' + NI: 'Nicaragua' + NE: 'Niger' + NG: 'Nigeria' + NU: 'Niue' + NF: 'Norfolk Island' + MP: 'Northern Mariana Islands' + NO: 'Norway' + OM: 'Oman' + PK: 'Pakistan' + PW: 'Palau' + PS: 'Palestinian Territory, Occupied' + PA: 'Panama' + PG: 'Papua New Guinea' + PY: 'Paraguay' + PE: 'Peru' + PH: 'Philippines' + PN: 'Pitcairn' + PL: 'Poland' + PT: 'Portugal' + PR: 'Puerto Rico' + QA: 'Qatar' + RE: 'Reunion' + RO: 'Romania' + RU: 'Russian Federation' + RW: 'Rwanda' + SH: 'Saint Helena' + KN: 'Saint Kitts And Nevis' + LC: 'Saint Lucia' + PM: 'Saint Pierre And Miquelon' + VC: 'Saint Vincent And The Grenadines' + WS: 'Samoa' + SM: 'San Marino' + ST: 'Sao Tome And Principe' + SA: 'Saudi Arabia' + SN: 'Senegal' + RS: 'Serbia' + SC: 'Seychelles' + SL: 'Sierra Leone' + SG: 'Singapore' + SK: 'Slovakia' + SI: 'Slovenia' + SB: 'Solomon Islands' + SO: 'Somalia' + ZA: 'South Africa' + GS: 'South Georgia And The South Sandwich Islands' + ES: 'Spain' + LK: 'Sri Lanka' + SD: 'Sudan' + SR: 'Suriname' + SJ: 'Svalbard And Jan Mayen' + SZ: 'Swaziland' + SE: 'Sweden' + CH: 'Switzerland' + SY: 'Syrian Arab Republic' + TW: 'Taiwan, Province Of China' + TJ: 'Tajikistan' + TZ: 'Tanzania, United Republic Of' + TH: 'Thailand' + TG: 'Togo' + TK: 'Tokelau' + TO: 'Tonga' + TT: 'Trinidad And Tobago' + TN: 'Tunisia' + TR: 'Turkey' + TM: 'Turkmenistan' + TC: 'Turks And Caicos Islands' + TV: 'Tuvalu' + UG: 'Uganda' + UA: 'Ukraine' + AE: 'United Arab Emirates' + GB: 'United Kingdom' + US: 'United States' + UM: 'United States Minor Outlying Islands' + UY: 'Uruguay' + UZ: 'Uzbekistan' + VU: 'Vanuatu' + VE: 'Venezuela' + VN: 'Viet Nam' + VG: 'Virgin Islands, British' + VI: 'Virgin Islands, U.s.' + WF: 'Wallis And Futuna' + EH: 'Western Sahara' + YE: 'Yemen' + ZM: 'Zambia' + ZW: 'Zimbabwe' @configure_attributes = [ { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, - #{ name: 'public', display: 'Public', tag: 'boolean', default: true }, - { name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 }, + { name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 }, + { name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true }, + { name: 'block_country', display: 'Blocked contries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' }, { name: 'active', display: 'Active', tag: 'active', default: true }, { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }, { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }, { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, ] + diff --git a/app/assets/javascripts/app/views/generic/column_select.jst.eco b/app/assets/javascripts/app/views/generic/column_select.jst.eco index 301dd0415..196bcb833 100644 --- a/app/assets/javascripts/app/views/generic/column_select.jst.eco +++ b/app/assets/javascripts/app/views/generic/column_select.jst.eco @@ -1,3 +1,6 @@ +<% if @attribute.seperator: %> + +<% else: %> +<% end %>
<%- @T('Nothing selected') %>
<% for option in @attribute.options: %> diff --git a/app/models/chat.rb b/app/models/chat.rb index 858cd76b4..7b92a0e57 100644 --- a/app/models/chat.rb +++ b/app/models/chat.rb @@ -264,4 +264,46 @@ optional you can put the max oldest chat sessions as argument true end +=begin + +check if ip address is blocked for chat + + chat = Chat.find(123) + chat.blocked_ip?(ip) + +=end + + def blocked_ip?(ip) + return false if ip.blank? + return false if block_ip.blank? + ips = block_ip.split(';') + ips.each do |local_ip| + return true if ip == local_ip.strip + return true if ip.match?(/#{local_ip.strip.gsub(/\*/, '.+?')}/) + end + false + end + +=begin + +check if country is blocked for chat + + chat = Chat.find(123) + chat.blocked_country?(ip) + +=end + + def blocked_country?(ip) + return false if ip.blank? + retunn false if block_country.blank? + geo_ip = Service::GeoIp.location(ip) + return false if geo_ip.blank? + return false if geo_ip['country_code'].blank? + countries = block_country.split(';') + countries.each do |local_country| + return true if geo_ip['country_code'] == local_country + end + false + end + end diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb index 726eede9f..3d4a0c420 100644 --- a/db/migrate/20120101000010_create_ticket.rb +++ b/db/migrate/20120101000010_create_ticket.rb @@ -467,6 +467,8 @@ class CreateTicket < ActiveRecord::Migration[4.2] t.string :note, limit: 250, null: true t.boolean :active, null: false, default: true t.boolean :public, null: false, default: false + t.string :block_ip, limit: 5000, null: true + t.string :block_country, limit: 5000, null: true t.string :preferences, limit: 5000, null: true t.integer :updated_by_id, null: false t.integer :created_by_id, null: false diff --git a/db/migrate/20180128000001_chat_add_ip_country.rb b/db/migrate/20180128000001_chat_add_ip_country.rb new file mode 100644 index 000000000..7b70d32f5 --- /dev/null +++ b/db/migrate/20180128000001_chat_add_ip_country.rb @@ -0,0 +1,10 @@ +class ChatAddIpCountry < ActiveRecord::Migration[5.1] + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + add_column :chats, :block_ip, :string, limit: 5000, null: true + add_column :chats, :block_country, :string, limit: 5000, null: true + end +end diff --git a/lib/sessions/event/chat_status_customer.rb b/lib/sessions/event/chat_status_customer.rb index 2a561e8eb..c7843cd54 100644 --- a/lib/sessions/event/chat_status_customer.rb +++ b/lib/sessions/event/chat_status_customer.rb @@ -3,6 +3,8 @@ class Sessions::Event::ChatStatusCustomer < Sessions::Event::ChatBase def run return super if super return if !check_chat_exists + return if !check_chat_block_by_ip + return if !check_chat_block_by_country # check if it's a chat sessin reconnect session_id = nil @@ -32,4 +34,30 @@ class Sessions::Event::ChatStatusCustomer < Sessions::Event::ChatBase } end + def check_chat_block_by_ip + chat = current_chat + return true if !chat.blocked_ip?(@remote_ip) + error = { + event: 'chat_error', + data: { + state: 'chat_unavailable', + }, + } + Sessions.send(@client_id, error) + false + end + + def check_chat_block_by_country + chat = current_chat + return true if !chat.blocked_country?(@remote_ip) + error = { + event: 'chat_error', + data: { + state: 'chat_unavailable', + }, + } + Sessions.send(@client_id, error) + false + end + end diff --git a/test/unit/chat_test.rb b/test/unit/chat_test.rb index 27b428d38..488c56211 100644 --- a/test/unit/chat_test.rb +++ b/test/unit/chat_test.rb @@ -5,7 +5,7 @@ class ChatTest < ActiveSupport::TestCase setup do groups = Group.all - roles = Role.where( name: %w[Agent] ) + roles = Role.where(name: %w[Agent]) @agent1 = User.create_or_update( login: 'ticket-chat-agent1@example.com', firstname: 'Notification', @@ -352,4 +352,38 @@ class ChatTest < ActiveSupport::TestCase travel_back end + test 'blocked ip test' do + chat = Chat.create!( + name: 'ip test', + max_queue: 5, + note: '', + block_ip: '127.0.0.1;127.0.0.2;127.1.0.*', + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + + assert_not(chat.blocked_ip?('128.0.0.1')) + assert_not(chat.blocked_ip?('127.0.0.30')) + assert(chat.blocked_ip?('127.0.0.1')) + assert(chat.blocked_ip?('127.0.0.2')) + assert(chat.blocked_ip?('127.1.0.1')) + assert(chat.blocked_ip?('127.1.0.100')) + end + + test 'blocked country test' do + chat = Chat.create!( + name: 'country test', + max_queue: 5, + note: '', + block_country: 'AU;CH', + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + + assert_not(chat.blocked_country?('127.0.0.1')) + assert(chat.blocked_country?('1.1.1.8')) + end + end