Browse Source

Added feature to restrict chat by ip or/and country.

Martin Edenhofer 7 years ago
parent
commit
5fb6bfed7d

+ 8 - 8
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()

+ 36 - 16
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')
+      @select firstVisibleOption.attr('data-value')

+ 247 - 3
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 },
   ]
+

+ 4 - 0
app/assets/javascripts/app/views/generic/column_select.jst.eco

@@ -1,3 +1,6 @@
+<% if @attribute.seperator: %>
+<input class="js-shadow hide" id="<%= @attribute.id %>" name="<%= @attribute.name %>" value="<%= @attribute.value %>">
+<% else: %>
 <select
   class="columnSelect-shadow js-shadow"
   id="<%= @attribute.id %>"
@@ -11,6 +14,7 @@
   <option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option>
   <% end %>
 </select>
+<% end %>
 <div class="columnSelect-column columnSelect-column--selected js-selected" data-name="<%= @attribute.name %>">
   <div class="u-placeholder u-unselectable js-placeholder<%= ' is-hidden' if @values.length %>"><%- @T('Nothing selected') %></div>
   <% for option in @attribute.options: %>

+ 42 - 0
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

+ 2 - 0
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

+ 10 - 0
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

+ 28 - 0
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

+ 35 - 1
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