mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #2521 from luisramos0/cookies_in_engine
[OFN Domains] Breaking OFN into domains - POC cookies inside an engine
This commit is contained in:
@@ -12,6 +12,8 @@ AllCops:
|
||||
- 'node_modules/**/*'
|
||||
# The parser gem fails to parse this file with out current Ruby version.
|
||||
- 'spec/factories.rb'
|
||||
# Excluding: inadequate Naming/FileName rule rejects GemFile name with camelcase
|
||||
- 'engines/web/Gemfile'
|
||||
|
||||
# OFN SETTINGS
|
||||
# Cop settings that have been agreed upon by the OFN community
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -10,6 +10,8 @@ gem 'i18n-js', '~> 3.0.0'
|
||||
# Patched version. See http://rubysec.com/advisories/CVE-2015-5312/.
|
||||
gem 'nokogiri', '>= 1.6.7.1'
|
||||
|
||||
gem 'web', path: './engines/web'
|
||||
|
||||
gem 'pg'
|
||||
gem 'spree', github: 'openfoodfoundation/spree', branch: 'step-6a', ref: '69db1c090f3711088d84b524f1b94d25e6d21616'
|
||||
gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable'
|
||||
|
||||
@@ -133,6 +133,11 @@ GIT
|
||||
activemodel (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
|
||||
PATH
|
||||
remote: engines/web
|
||||
specs:
|
||||
web (0.0.1)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
@@ -764,6 +769,7 @@ DEPENDENCIES
|
||||
capybara (>= 2.15.4)
|
||||
coffee-rails (~> 3.2.1)
|
||||
compass-rails
|
||||
web!
|
||||
custom_error_message!
|
||||
daemons
|
||||
dalli
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import 'base/*';
|
||||
@import '*';
|
||||
@import 'pages/*';
|
||||
@import '../web/all';
|
||||
|
||||
ofn-modal {
|
||||
display: block;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
class AngularTemplatesController < ApplicationController
|
||||
def show
|
||||
render params[:id].to_s, layout: nil
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
module Api
|
||||
class CookiesConsentController < BaseController
|
||||
include ActionController::Cookies
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
render json: { cookies_consent: cookies_consent.exists? }
|
||||
end
|
||||
|
||||
def create
|
||||
cookies_consent.set
|
||||
show
|
||||
end
|
||||
|
||||
def destroy
|
||||
cookies_consent.destroy
|
||||
show
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cookies_consent
|
||||
@cookies_consent ||= CookiesConsent.new(cookies, request.host)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
module CookiesPolicyHelper
|
||||
def render_cookie_entry(cookie_name, cookie_desc, cookie_domain = nil)
|
||||
render partial: 'cookies_policy_entry',
|
||||
locals: { cookie_name: cookie_name,
|
||||
cookie_desc: cookie_desc,
|
||||
cookie_domain: cookie_domain }
|
||||
end
|
||||
|
||||
def matomo_iframe_src
|
||||
"#{Spree::Config.matomo_url}"\
|
||||
"/index.php?module=CoreAdminHome&action=optOut"\
|
||||
"&language=#{locale_language}"\
|
||||
"&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif"
|
||||
end
|
||||
|
||||
# removes country from locale if needed
|
||||
# for example, both locales en and en_GB return language en
|
||||
def locale_language
|
||||
I18n.locale[0..1]
|
||||
end
|
||||
end
|
||||
11
app/helpers/footer_links_helper.rb
Normal file
11
app/helpers/footer_links_helper.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
require 'web/cookies_consent'
|
||||
|
||||
module FooterLinksHelper
|
||||
def cookies_policy_link
|
||||
link_to( t( '.footer_data_cookies_policy' ), '', 'cookies-policy-modal' => true, 'cookies-banner' => !Web::CookiesConsent.new(cookies, request.host).exists? && Spree::Config.cookies_consent_banner_toggle)
|
||||
end
|
||||
|
||||
def privacy_policy_link
|
||||
link_to( t( '.footer_data_privacy_policy' ), Spree::Config.privacy_policy_url, target: '_blank' )
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
class CookiesConsent
|
||||
COOKIE_NAME = 'cookies_consent'.freeze
|
||||
|
||||
def initialize(cookies, domain)
|
||||
@cookies = cookies
|
||||
@domain = domain
|
||||
end
|
||||
|
||||
def exists?
|
||||
cookies.key?(COOKIE_NAME)
|
||||
end
|
||||
|
||||
def destroy
|
||||
cookies.delete(COOKIE_NAME, domain: domain)
|
||||
end
|
||||
|
||||
def set
|
||||
cookies[COOKIE_NAME] = {
|
||||
value: COOKIE_NAME,
|
||||
expires: 1.year.from_now,
|
||||
domain: domain,
|
||||
httponly: true
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :cookies, :domain
|
||||
end
|
||||
@@ -19,6 +19,7 @@
|
||||
%script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry#{ ENV['GOOGLE_MAPS_API_KEY'] ? '&key=' + ENV['GOOGLE_MAPS_API_KEY'] : ''} "}
|
||||
= stylesheet_link_tag "darkswarm/all"
|
||||
= javascript_include_tag "darkswarm/all"
|
||||
= javascript_include_tag "web/all"
|
||||
|
||||
= render "layouts/i18n_script"
|
||||
= render "layouts/bugherd_script"
|
||||
|
||||
@@ -140,8 +140,6 @@
|
||||
= t '.footer_legal_text_html', {content_license: link_to('CC BY-SA 3.0', 'https://creativecommons.org/licenses/by-sa/3.0/'), code_license: link_to('AGPL 3', 'https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)' )}
|
||||
%p.text-small
|
||||
%div
|
||||
- cookies_policy_link = link_to( t( '.footer_data_cookies_policy' ), '', 'cookies-policy-modal' => true, 'cookies-banner' => !CookiesConsent.new(cookies, request.host).exists? && Spree::Config.cookies_consent_banner_toggle)
|
||||
- privacy_policy_link = link_to( t( '.footer_data_privacy_policy' ), Spree::Config.privacy_policy_url, :target => '_blank' )
|
||||
- if Spree::Config.privacy_policy_url.present?
|
||||
= t '.footer_data_text_with_privacy_policy_html', {cookies_policy: cookies_policy_link.html_safe, privacy_policy: privacy_policy_link.html_safe }
|
||||
- else
|
||||
|
||||
@@ -143,6 +143,7 @@ module Openfoodnetwork
|
||||
config.assets.initialize_on_precompile = true
|
||||
config.assets.precompile += ['iehack.js']
|
||||
config.assets.precompile += ['admin/all.css', 'admin/*.js', 'admin/**/*.js']
|
||||
config.assets.precompile += ['web/all.css', 'web/all.js']
|
||||
config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all.js']
|
||||
config.assets.precompile += ['mail/all.css']
|
||||
config.assets.precompile += ['shared/*']
|
||||
|
||||
@@ -88,8 +88,6 @@ Openfoodnetwork::Application.routes.draw do
|
||||
get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop'
|
||||
get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL
|
||||
|
||||
get "/angular-templates/:id", to: "angular_templates#show", constraints: { name: %r{[\/\w\.]+} }
|
||||
|
||||
namespace :api do
|
||||
resources :enterprises do
|
||||
post :update_image, on: :member
|
||||
@@ -109,10 +107,6 @@ Openfoodnetwork::Application.routes.draw do
|
||||
get :job_queue
|
||||
end
|
||||
|
||||
scope '/cookies' do
|
||||
resource :consent, only: [:show, :create, :destroy], :controller => "cookies_consent"
|
||||
end
|
||||
|
||||
resources :customers, only: [:index, :update]
|
||||
|
||||
post '/product_images/:product_id', to: 'product_images#update_product_image'
|
||||
@@ -120,6 +114,9 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
|
||||
|
||||
# Mount Web engine routes
|
||||
mount Web::Engine, :at => '/'
|
||||
|
||||
# Mount Spree's routes
|
||||
mount Spree::Core::Engine, :at => '/'
|
||||
end
|
||||
|
||||
5
engines/web/README.md
Normal file
5
engines/web/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Web
|
||||
|
||||
This is the rails engine for the Web domain.
|
||||
|
||||
See our wiki for [more info about domains and engines in OFN](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Tech-Doc:-How-OFN-is-organized-in-Domains-using-Rails-Engines).
|
||||
13
engines/web/app/assets/javascripts/web/all.js
Normal file
13
engines/web/app/assets/javascripts/web/all.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
||||
// listed below.
|
||||
//
|
||||
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
||||
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
||||
//
|
||||
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
||||
// the compiled file.
|
||||
//
|
||||
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
||||
// GO AFTER THE REQUIRES BELOW.
|
||||
//
|
||||
//= require_tree .
|
||||
@@ -1,6 +1,7 @@
|
||||
Darkswarm.directive 'cookiesBanner', (CookiesBannerService) ->
|
||||
Darkswarm.directive 'cookiesBanner', (CookiesBannerService, CookiesPolicyModalService) ->
|
||||
restrict: 'A'
|
||||
link: (scope, elm, attr)->
|
||||
return if not attr.cookiesBanner? || attr.cookiesBanner == 'false'
|
||||
CookiesBannerService.enable()
|
||||
return if CookiesPolicyModalService.isEnabled()
|
||||
CookiesBannerService.open()
|
||||
@@ -4,7 +4,7 @@ Darkswarm.factory "CookiesBannerService", (Navigation, $modal, $location, Redire
|
||||
modalMessage: null
|
||||
isEnabled: false
|
||||
|
||||
open: (path, template = 'darkswarm/cookies_banner/cookies_banner.html') =>
|
||||
open: (path, template = 'angular-templates/cookies_banner.html') =>
|
||||
return unless @isEnabled
|
||||
@modalInstance = $modal.open
|
||||
templateUrl: template
|
||||
@@ -5,7 +5,7 @@ Darkswarm.factory "CookiesPolicyModalService", (Navigation, $modal, $location, C
|
||||
modalMessage: null
|
||||
|
||||
constructor: ->
|
||||
if $location.path() is @defaultPath || location.pathname is @defaultPath
|
||||
if @isEnabled()
|
||||
@open ''
|
||||
|
||||
open: (path = false, template = 'angular-templates/cookies_policy.html') =>
|
||||
@@ -13,18 +13,16 @@ Darkswarm.factory "CookiesPolicyModalService", (Navigation, $modal, $location, C
|
||||
templateUrl: template
|
||||
windowClass: "cookies-policy-modal medium"
|
||||
|
||||
@closeCookiesBanner()
|
||||
@onCloseReOpenCookiesBanner()
|
||||
CookiesBannerService.close()
|
||||
@onCloseOpenCookiesBanner()
|
||||
|
||||
selectedPath = path || @defaultPath
|
||||
Navigation.navigate selectedPath
|
||||
|
||||
closeCookiesBanner: =>
|
||||
setTimeout ->
|
||||
CookiesBannerService.close()
|
||||
, 200
|
||||
|
||||
onCloseReOpenCookiesBanner: =>
|
||||
onCloseOpenCookiesBanner: =>
|
||||
@modalInstance.result.then(
|
||||
-> CookiesBannerService.open(),
|
||||
-> CookiesBannerService.open() )
|
||||
|
||||
isEnabled: =>
|
||||
$location.path() is @defaultPath || location.pathname is @defaultPath
|
||||
2
engines/web/app/assets/javascripts/web/web.js
Normal file
2
engines/web/app/assets/javascripts/web/web.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// Place all the behaviors and hooks related to the matching controller here.
|
||||
// All this logic will automatically be available in application.js.
|
||||
2
engines/web/app/assets/stylesheets/web/all.css.scss
Normal file
2
engines/web/app/assets/stylesheets/web/all.css.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import 'web/pages/cookies_banner';
|
||||
@import 'web/pages/cookies_policy_modal';
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../branding';
|
||||
@import 'darkswarm/branding';
|
||||
|
||||
.cookies-banner {
|
||||
background: $dark-grey;
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../branding';
|
||||
@import 'darkswarm/branding';
|
||||
|
||||
.cookies-policy-modal {
|
||||
background: $disabled-light;
|
||||
@@ -0,0 +1,9 @@
|
||||
module Web
|
||||
class AngularTemplatesController < ApplicationController
|
||||
helper Web::Engine.helpers
|
||||
|
||||
def show
|
||||
render params[:id].to_s, layout: nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
require_dependency 'web/cookies_consent'
|
||||
|
||||
module Web
|
||||
module Api
|
||||
class CookiesConsentController < BaseController
|
||||
include ActionController::Cookies
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
render json: { cookies_consent: cookies_consent.exists? }
|
||||
end
|
||||
|
||||
def create
|
||||
cookies_consent.set
|
||||
show
|
||||
end
|
||||
|
||||
def destroy
|
||||
cookies_consent.destroy
|
||||
show
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cookies_consent
|
||||
@cookies_consent ||= Web::CookiesConsent.new(cookies, request.host)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module Web
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
end
|
||||
end
|
||||
23
engines/web/app/helpers/web/cookies_policy_helper.rb
Normal file
23
engines/web/app/helpers/web/cookies_policy_helper.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module Web
|
||||
module CookiesPolicyHelper
|
||||
def render_cookie_entry(cookie_name, cookie_desc, cookie_domain = nil)
|
||||
render partial: 'cookies_policy_entry',
|
||||
locals: { cookie_name: cookie_name,
|
||||
cookie_desc: cookie_desc,
|
||||
cookie_domain: cookie_domain }
|
||||
end
|
||||
|
||||
def matomo_iframe_src
|
||||
"#{Spree::Config.matomo_url}"\
|
||||
"/index.php?module=CoreAdminHome&action=optOut"\
|
||||
"&language=#{locale_language}"\
|
||||
"&backgroundColor=&fontColor=222222&fontSize=16px&fontFamily=%22Roboto%22%2C%20Arial%2C%20sans-serif"
|
||||
end
|
||||
|
||||
# removes country from locale if needed
|
||||
# for example, both locales en and en_GB return language en
|
||||
def locale_language
|
||||
I18n.locale[0..1]
|
||||
end
|
||||
end
|
||||
end
|
||||
9
engines/web/config/routes.rb
Normal file
9
engines/web/config/routes.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
Web::Engine.routes.draw do
|
||||
namespace :api do
|
||||
scope '/cookies' do
|
||||
resource :consent, only: [:show, :create, :destroy], controller: "cookies_consent"
|
||||
end
|
||||
end
|
||||
|
||||
get "/angular-templates/:id", to: "angular_templates#show", constraints: { name: %r{[\/\w\.]+} }
|
||||
end
|
||||
4
engines/web/lib/web.rb
Normal file
4
engines/web/lib/web.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
require "web/engine"
|
||||
|
||||
module Web
|
||||
end
|
||||
31
engines/web/lib/web/cookies_consent.rb
Normal file
31
engines/web/lib/web/cookies_consent.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Web
|
||||
class CookiesConsent
|
||||
COOKIE_NAME = 'cookies_consent'.freeze
|
||||
|
||||
def initialize(cookies, domain)
|
||||
@cookies = cookies
|
||||
@domain = domain
|
||||
end
|
||||
|
||||
def exists?
|
||||
cookies.key?(COOKIE_NAME)
|
||||
end
|
||||
|
||||
def destroy
|
||||
cookies.delete(COOKIE_NAME, domain: domain)
|
||||
end
|
||||
|
||||
def set
|
||||
cookies[COOKIE_NAME] = {
|
||||
value: COOKIE_NAME,
|
||||
expires: 1.year.from_now,
|
||||
domain: domain,
|
||||
httponly: true
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :cookies, :domain
|
||||
end
|
||||
end
|
||||
5
engines/web/lib/web/engine.rb
Normal file
5
engines/web/lib/web/engine.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module Web
|
||||
class Engine < ::Rails::Engine
|
||||
isolate_namespace Web
|
||||
end
|
||||
end
|
||||
3
engines/web/lib/web/version.rb
Normal file
3
engines/web/lib/web/version.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
module Web
|
||||
VERSION = "0.0.1".freeze
|
||||
end
|
||||
52
engines/web/spec/helpers/cookies_policy_helper_spec.rb
Normal file
52
engines/web/spec/helpers/cookies_policy_helper_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Web
|
||||
describe CookiesPolicyHelper, type: :helper do
|
||||
# keeps global state unchanged
|
||||
around do |example|
|
||||
original_locale = I18n.locale
|
||||
original_matomo_url = Spree::Config.matomo_url
|
||||
example.run
|
||||
Spree::Config.matomo_url = original_matomo_url
|
||||
I18n.locale = original_locale
|
||||
end
|
||||
|
||||
describe "matomo optout iframe src" do
|
||||
describe "when matomo url is set" do
|
||||
before do
|
||||
Spree::Config.matomo_url = "http://matomo.org/"
|
||||
end
|
||||
|
||||
scenario "includes the matomo URL" do
|
||||
expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url
|
||||
end
|
||||
|
||||
scenario "is not equal to the matomo URL" do
|
||||
expect(helper.matomo_iframe_src).to_not eq Spree::Config.matomo_url
|
||||
end
|
||||
end
|
||||
|
||||
scenario "is not nil, when matomo url is nil" do
|
||||
Spree::Config.matomo_url = nil
|
||||
expect(helper.matomo_iframe_src).to_not eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "language from locale" do
|
||||
scenario "when locale is the language" do
|
||||
I18n.locale = "en"
|
||||
expect(helper.locale_language).to eq "en"
|
||||
end
|
||||
|
||||
scenario "is empty when locale is empty" do
|
||||
I18n.locale = ""
|
||||
expect(helper.locale_language).to be_empty
|
||||
end
|
||||
|
||||
scenario "is only the language, when locale includes country" do
|
||||
I18n.locale = "en_GB"
|
||||
expect(helper.locale_language).to eq "en"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
8
engines/web/spec/spec_helper.rb
Normal file
8
engines/web/spec/spec_helper.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
ENV["RAILS_ENV"] = "test"
|
||||
|
||||
require File.expand_path("dummy/config/environment.rb", __dir__)
|
||||
require "rails/test_help"
|
||||
|
||||
Rails.backtrace_cleaner.remove_silencers!
|
||||
|
||||
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
||||
13
engines/web/web.gemspec
Normal file
13
engines/web/web.gemspec
Normal file
@@ -0,0 +1,13 @@
|
||||
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
||||
|
||||
require "web/version"
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "web"
|
||||
s.version = Web::VERSION
|
||||
s.authors = ["developers@ofn"]
|
||||
s.summary = "Web domain of the OFN solution."
|
||||
|
||||
s.files = Dir["{app,config,db,lib}/**/*"] + ["LICENSE.txt", "Rakefile", "README.rdoc"]
|
||||
s.test_files = Dir["test/**/*"]
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe CookiesPolicyHelper, type: :helper do
|
||||
|
||||
# keeps global state unchanged
|
||||
around do |example|
|
||||
original_locale = I18n.locale
|
||||
original_matomo_url = Spree::Config.matomo_url
|
||||
example.run
|
||||
Spree::Config.matomo_url = original_matomo_url
|
||||
I18n.locale = original_locale
|
||||
end
|
||||
|
||||
describe "matomo optout iframe src" do
|
||||
describe "when matomo url is set" do
|
||||
before do
|
||||
Spree::Config.matomo_url = "http://matomo.org/"
|
||||
end
|
||||
|
||||
scenario "includes the matomo URL" do
|
||||
expect(helper.matomo_iframe_src).to include Spree::Config.matomo_url
|
||||
end
|
||||
|
||||
scenario "is not equal to the matomo URL" do
|
||||
expect(helper.matomo_iframe_src).to_not eq Spree::Config.matomo_url
|
||||
end
|
||||
end
|
||||
|
||||
scenario "is not nil, when matomo url is nil" do
|
||||
Spree::Config.matomo_url = nil
|
||||
expect(helper.matomo_iframe_src).to_not eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "language from locale" do
|
||||
scenario "when locale is the language" do
|
||||
I18n.locale = "en"
|
||||
expect(helper.locale_language).to eq "en"
|
||||
end
|
||||
|
||||
scenario "is empty when locale is empty" do
|
||||
I18n.locale = ""
|
||||
expect(helper.locale_language).to be_empty
|
||||
end
|
||||
|
||||
scenario "is only the language, when locale includes country" do
|
||||
I18n.locale = "en_GB"
|
||||
expect(helper.locale_language).to eq "en"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user