wovnrb 3.8.0 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,173 +1,183 @@
1
- require 'addressable'
2
- require 'digest'
3
- require 'json'
4
- require 'zlib'
5
-
6
- module Wovnrb
7
- class ApiTranslator
8
- def initialize(store, headers, uuid)
9
- @store = store
10
- @headers = headers
11
- @uuid = uuid
12
- end
13
-
14
- def translate(body)
15
- connection = prepare_connection
16
- request = prepare_request(body)
17
-
18
- begin
19
- response = connection.request(request)
20
- rescue => e
21
- WovnLogger.error("\"#{e.message}\" error occurred when contacting WOVNio translation API")
22
- return body
23
- end
24
-
25
- case response
26
- when Net::HTTPSuccess
27
- begin
28
- raw_response_body = @store.dev_mode? ? response.body : Zlib::GzipReader.new(StringIO.new(response.body)).read
29
- rescue Zlib::GzipFile::Error
30
- raw_response_body = response.body
31
- end
32
-
33
- begin
34
- JSON.parse(raw_response_body)['body'] || body
35
- rescue JSON::JSONError
36
- body
37
- end
38
- else
39
- WovnLogger.error("HTML-swapper call failed. Received \"#{response.message}\" from WOVNio translation API.")
40
- body
41
- end
42
- end
43
-
44
- private
45
-
46
- def prepare_connection
47
- connection = Net::HTTP.new(api_uri.host, api_uri.port)
48
-
49
- connection.open_timeout = api_timeout
50
- connection.read_timeout = api_timeout
51
-
52
- connection
53
- end
54
-
55
- def prepare_request(body)
56
- if @store.compress_api_requests?
57
- gzip_request(body)
58
- else
59
- json_request(body)
60
- end
61
- end
62
-
63
- def gzip_request(html_body)
64
- api_params = build_api_params(html_body)
65
- compressed_body = compress_request_data(api_params.to_json)
66
- request = Net::HTTP::Post.new(request_path(html_body), {
67
- 'Accept-Encoding' => 'gzip',
68
- 'Content-Type' => 'application/json',
69
- 'Content-Encoding' => 'gzip',
70
- 'Content-Length' => compressed_body.bytesize.to_s,
71
- 'X-Request-Id' => @uuid
72
- })
73
- request.body = compressed_body
74
-
75
- request
76
- end
77
-
78
- def json_request(html_body)
79
- api_params = build_api_params(html_body)
80
- request = Net::HTTP::Post.new(request_path(html_body), {
81
- 'Accept-Encoding' => 'gzip',
82
- 'Content-Type' => 'application/json',
83
- 'X-Request-Id' => @uuid
84
- })
85
- request.body = api_params.to_json
86
-
87
- request
88
- end
89
-
90
- def request_path(body)
91
- "#{api_uri.path}/translation?cache_key=#{cache_key(body)}"
92
- end
93
-
94
- def cache_key(body)
95
- cache_key_components = {
96
- 'token' => token,
97
- 'settings_hash' => settings_hash,
98
- 'body_hash' => Digest::MD5.hexdigest(body),
99
- 'path' => page_pathname,
100
- 'lang' => lang_code,
101
- 'version' => "wovnrb_#{VERSION}"
102
- }.map { |k, v| "#{k}=#{v}" }.join('&')
103
-
104
- CGI.escape("(#{cache_key_components})")
105
- end
106
-
107
- def build_api_params(body)
108
- result = {
109
- 'url' => page_url,
110
- 'token' => token,
111
- 'lang_code' => lang_code,
112
- 'url_pattern' => url_pattern,
113
- 'lang_param_name' => lang_param_name,
114
- 'translate_canonical_tag' => translate_canonical_tag,
115
- 'product' => 'WOVN.rb',
116
- 'version' => VERSION,
117
- 'body' => body
118
- }
119
-
120
- result['custom_lang_aliases'] = JSON.dump(custom_lang_aliases) unless custom_lang_aliases.empty?
121
-
122
- result
123
- end
124
-
125
- def compress_request_data(data_hash)
126
- ActiveSupport::Gzip.compress(data_hash)
127
- end
128
-
129
- def api_uri
130
- Addressable::URI.parse("#{@store.settings['api_url']}/v0")
131
- end
132
-
133
- def api_timeout
134
- @headers.search_engine_bot? ? @store.settings['api_timeout_search_engine_bots'] : @store.settings['api_timeout_seconds']
135
- end
136
-
137
- def settings_hash
138
- Digest::MD5.hexdigest(JSON.dump(@store.settings))
139
- end
140
-
141
- def token
142
- @store.settings['project_token']
143
- end
144
-
145
- def lang_code
146
- @headers.lang_code
147
- end
148
-
149
- def url_pattern
150
- @store.settings['url_pattern']
151
- end
152
-
153
- def lang_param_name
154
- @store.settings['lang_param_name']
155
- end
156
-
157
- def custom_lang_aliases
158
- @store.settings['custom_lang_aliases']
159
- end
160
-
161
- def translate_canonical_tag
162
- @store.settings['translate_canonical_tag']
163
- end
164
-
165
- def page_url
166
- "#{@headers.protocol}://#{@headers.url}"
167
- end
168
-
169
- def page_pathname
170
- @headers.pathname_with_trailing_slash_if_present
171
- end
172
- end
173
- end
1
+ require 'addressable'
2
+ require 'digest'
3
+ require 'json'
4
+ require 'zlib'
5
+
6
+ module Wovnrb
7
+ class ApiTranslator
8
+ def initialize(store, headers, uuid)
9
+ @store = store
10
+ @headers = headers
11
+ @uuid = uuid
12
+ end
13
+
14
+ def translate(body)
15
+ connection = prepare_connection
16
+ request = prepare_request(body)
17
+
18
+ begin
19
+ response = connection.request(request)
20
+ rescue => e
21
+ WovnLogger.error("\"#{e.message}\" error occurred when contacting WOVNio translation API")
22
+ return body
23
+ end
24
+
25
+ case response
26
+ when Net::HTTPSuccess
27
+ begin
28
+ raw_response_body = @store.dev_mode? ? response.body : Zlib::GzipReader.new(StringIO.new(response.body)).read
29
+ rescue Zlib::GzipFile::Error
30
+ raw_response_body = response.body
31
+ end
32
+
33
+ begin
34
+ JSON.parse(raw_response_body)['body'] || body
35
+ rescue JSON::JSONError
36
+ body
37
+ end
38
+ else
39
+ WovnLogger.error("HTML-swapper call failed. Received \"#{response.message}\" from WOVNio translation API.")
40
+ body
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def prepare_connection
47
+ connection = Net::HTTP.new(api_uri.host, api_uri.port)
48
+
49
+ connection.open_timeout = api_timeout
50
+ connection.read_timeout = api_timeout
51
+
52
+ connection
53
+ end
54
+
55
+ def prepare_request(body)
56
+ if @store.compress_api_requests?
57
+ gzip_request(body)
58
+ else
59
+ json_request(body)
60
+ end
61
+ end
62
+
63
+ def gzip_request(html_body)
64
+ api_params = build_api_params(html_body)
65
+ compressed_body = compress_request_data(api_params.to_json)
66
+ request = Net::HTTP::Post.new(request_path(html_body), {
67
+ 'Accept-Encoding' => 'gzip',
68
+ 'Content-Type' => 'application/json',
69
+ 'Content-Encoding' => 'gzip',
70
+ 'Content-Length' => compressed_body.bytesize.to_s,
71
+ 'X-Request-Id' => @uuid
72
+ })
73
+ request.body = compressed_body
74
+
75
+ request
76
+ end
77
+
78
+ def json_request(html_body)
79
+ api_params = build_api_params(html_body)
80
+ request = Net::HTTP::Post.new(request_path(html_body), {
81
+ 'Accept-Encoding' => 'gzip',
82
+ 'Content-Type' => 'application/json',
83
+ 'X-Request-Id' => @uuid
84
+ })
85
+ request.body = api_params.to_json
86
+
87
+ request
88
+ end
89
+
90
+ def request_path(body)
91
+ "#{api_uri.path}/translation?cache_key=#{cache_key(body)}"
92
+ end
93
+
94
+ def cache_key(body)
95
+ cache_key_components = {
96
+ 'token' => token,
97
+ 'settings_hash' => settings_hash,
98
+ 'body_hash' => Digest::MD5.hexdigest(body),
99
+ 'path' => page_pathname,
100
+ 'lang' => lang_code,
101
+ 'version' => "wovnrb_#{VERSION}"
102
+ }.map { |k, v| "#{k}=#{v}" }.join('&')
103
+
104
+ CGI.escape("(#{cache_key_components})")
105
+ end
106
+
107
+ def build_api_params(body)
108
+ result = {
109
+ 'url' => page_url,
110
+ 'token' => token,
111
+ 'lang_code' => lang_code,
112
+ 'url_pattern' => url_pattern,
113
+ 'lang_param_name' => lang_param_name,
114
+ 'translate_canonical_tag' => translate_canonical_tag,
115
+ 'insert_hreflangs' => insert_hreflangs,
116
+ 'product' => 'WOVN.rb',
117
+ 'version' => VERSION,
118
+ 'body' => body
119
+ }
120
+
121
+ result['custom_lang_aliases'] = JSON.dump(custom_lang_aliases) unless custom_lang_aliases.empty?
122
+ result['custom_domain_langs'] = JSON.dump(custom_domain_langs) unless custom_domain_langs.empty?
123
+
124
+ result
125
+ end
126
+
127
+ def compress_request_data(data_hash)
128
+ ActiveSupport::Gzip.compress(data_hash)
129
+ end
130
+
131
+ def api_uri
132
+ Addressable::URI.parse("#{@store.settings['api_url']}/v0")
133
+ end
134
+
135
+ def api_timeout
136
+ @headers.search_engine_bot? ? @store.settings['api_timeout_search_engine_bots'] : @store.settings['api_timeout_seconds']
137
+ end
138
+
139
+ def settings_hash
140
+ Digest::MD5.hexdigest(JSON.dump(@store.settings))
141
+ end
142
+
143
+ def token
144
+ @store.settings['project_token']
145
+ end
146
+
147
+ def lang_code
148
+ @headers.lang_code
149
+ end
150
+
151
+ def url_pattern
152
+ @store.settings['url_pattern']
153
+ end
154
+
155
+ def lang_param_name
156
+ @store.settings['lang_param_name']
157
+ end
158
+
159
+ def custom_lang_aliases
160
+ @store.settings['custom_lang_aliases']
161
+ end
162
+
163
+ def translate_canonical_tag
164
+ @store.settings['translate_canonical_tag']
165
+ end
166
+
167
+ def insert_hreflangs
168
+ @store.settings['insert_hreflangs']
169
+ end
170
+
171
+ def custom_domain_langs
172
+ @store.custom_domain_langs.to_html_swapper_hash
173
+ end
174
+
175
+ def page_url
176
+ "#{@headers.protocol}://#{@headers.url}"
177
+ end
178
+
179
+ def page_pathname
180
+ @headers.pathname_with_trailing_slash_if_present
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,31 @@
1
+ module Wovnrb
2
+ # Represents a custom domain for a given language
3
+ class CustomDomainLang
4
+ attr_accessor :host, :path, :lang
5
+
6
+ def initialize(host, path, lang)
7
+ @host = host
8
+ @path = path.end_with?('/') ? path : "#{path}/"
9
+ @lang = lang
10
+ end
11
+
12
+ # @param uri [Addressable::URI]
13
+ def match?(parsed_uri)
14
+ @host.casecmp?(parsed_uri.host) && path_is_equal_or_subset_of?(@path, parsed_uri.path)
15
+ end
16
+
17
+ def host_and_path_without_trailing_slash
18
+ host_and_path = @host + @path
19
+ host_and_path.end_with?('/') ? host_and_path.delete_suffix('/') : host_and_path
20
+ end
21
+
22
+ private
23
+
24
+ def path_is_equal_or_subset_of?(path1, path2)
25
+ path1_segments = path1.split('/').reject(&:empty?)
26
+ path2_segments = path2.split('/').reject(&:empty?)
27
+
28
+ path1_segments == path2_segments.slice(0, path1_segments.length)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ require 'wovnrb/custom_domain/custom_domain_lang'
2
+
3
+ module Wovnrb
4
+ # Helper class for transforming actual domains to user-defined custom domains
5
+ class CustomDomainLangUrlHandler
6
+ class << self
7
+ def add_custom_domain_lang_to_absolute_url(absolute_url, target_lang, custom_domain_langs)
8
+ current_custom_domain = custom_domain_langs.custom_domain_lang_by_url(absolute_url)
9
+ new_lang_custom_domain = custom_domain_langs.custom_domain_lang_by_lang(target_lang)
10
+ change_to_new_custom_domain_lang(absolute_url, current_custom_domain, new_lang_custom_domain)
11
+ end
12
+
13
+ def change_to_new_custom_domain_lang(absolute_url, current_custom_domain, new_lang_custom_domain)
14
+ return absolute_url unless current_custom_domain.present? && new_lang_custom_domain.present?
15
+
16
+ current_host_and_path = current_custom_domain.host_and_path_without_trailing_slash
17
+ new_host_and_path = new_lang_custom_domain.host_and_path_without_trailing_slash
18
+
19
+ # ^(.*://|//)? 1: schema, e.g. https://
20
+ # (#{current_host_and_path}) 2: host and path, e.g. wovn.io/foo
21
+ # ((?:/|\?|#).*)?$ 3: other / query params, e.g. ?hello=world
22
+ regex = %r{^(.*://|//)?(#{current_host_and_path})((?:/|\?|#).*)?$}
23
+ absolute_url.gsub(regex) { "#{Regexp.last_match(1)}#{new_host_and_path}#{Regexp.last_match(3)}" }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ require 'wovnrb/custom_domain/custom_domain_lang'
2
+
3
+ module Wovnrb
4
+ # Represents a list of custom domains with corresponding languages
5
+ class CustomDomainLangs
6
+ def initialize(setting)
7
+ @custom_domain_langs = setting.map do |lang_code, config|
8
+ parsed_uri = Addressable::URI.parse(add_protocol_if_needed(config['url']))
9
+ CustomDomainLang.new(parsed_uri.host, parsed_uri.path, lang_code)
10
+ end
11
+ end
12
+
13
+ def custom_domain_lang_by_lang(lang_code)
14
+ @custom_domain_langs.find { |c| c.lang == lang_code }
15
+ end
16
+
17
+ def custom_domain_lang_by_url(uri)
18
+ parsed_uri = Addressable::URI.parse(add_protocol_if_needed(uri))
19
+
20
+ # "/" path will naturally match every URL, so by comparing longest paths first we will get the best match
21
+ @custom_domain_langs
22
+ .sort_by { |c| -c.path.length }
23
+ .find { |c| c.match?(parsed_uri) }
24
+ end
25
+
26
+ def to_html_swapper_hash
27
+ result = {}
28
+ @custom_domain_langs.each do |custom_domain_lang|
29
+ result[custom_domain_lang.host_and_path_without_trailing_slash] = custom_domain_lang.lang
30
+ end
31
+ result
32
+ end
33
+
34
+ private
35
+
36
+ def add_protocol_if_needed(url)
37
+ url.match?(%r{https?://}) ? url : "http://#{url}"
38
+ end
39
+ end
40
+ end