##// END OF EJS Templates
Try all chunks when detecting file mimetype
neko259 -
r1869:f74167a6 default
parent child Browse files
Show More
@@ -1,239 +1,240 b''
1 import re
1 import re
2
2
3 from PIL import Image
3 from PIL import Image
4
4
5 from django.contrib.staticfiles import finders
5 from django.contrib.staticfiles import finders
6 from django.contrib.staticfiles.templatetags.staticfiles import static
6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 from django.core.files.images import get_image_dimensions
7 from django.core.files.images import get_image_dimensions
8 from django.template.defaultfilters import filesizeformat
8 from django.template.defaultfilters import filesizeformat
9 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
11
12 from boards.utils import get_domain, cached_result
12 from boards.utils import get_domain, cached_result
13 from boards import settings
13 from boards import settings
14
14
15
15
16 FILE_STUB_IMAGE = 'images/file.png'
16 FILE_STUB_IMAGE = 'images/file.png'
17 FILE_STUB_URL = 'url'
17 FILE_STUB_URL = 'url'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19
19
20
20
21 FILE_TYPES_VIDEO = (
21 FILE_TYPES_VIDEO = (
22 'video/webm',
22 'video/webm',
23 'video/mp4',
23 'video/mp4',
24 'video/mpeg',
24 'video/mpeg',
25 'video/ogv',
25 'video/ogv',
26 )
26 )
27 FILE_TYPE_SVG = 'image/svg'
27 FILE_TYPE_SVG = 'image/svg'
28 FILE_TYPES_AUDIO = (
28 FILE_TYPES_AUDIO = (
29 'audio/ogg',
29 'audio/ogg',
30 'audio/mp3',
30 'audio/mp3',
31 'audio/opus',
31 'audio/opus',
32 'audio/x-flac',
32 'audio/x-flac',
33 'audio/mpeg',
33 )
34 )
34 FILE_TYPES_IMAGE = (
35 FILE_TYPES_IMAGE = (
35 'image/jpeg',
36 'image/jpeg',
36 'image/jpg',
37 'image/jpg',
37 'image/png',
38 'image/png',
38 'image/bmp',
39 'image/bmp',
39 'image/gif',
40 'image/gif',
40 )
41 )
41
42
42 PLAIN_FILE_FORMATS = {
43 PLAIN_FILE_FORMATS = {
43 'application/zip': 'archive',
44 'application/zip': 'archive',
44 'application/x-tar': 'archive',
45 'application/x-tar': 'archive',
45 'gz': 'archive',
46 'gz': 'archive',
46 'audio/midi' : 'midi',
47 'audio/midi' : 'midi',
47 }
48 }
48
49
49 URL_PROTOCOLS = {
50 URL_PROTOCOLS = {
50 'magnet': 'magnet',
51 'magnet': 'magnet',
51 }
52 }
52
53
53 CSS_CLASS_IMAGE = 'image'
54 CSS_CLASS_IMAGE = 'image'
54 CSS_CLASS_THUMB = 'thumb'
55 CSS_CLASS_THUMB = 'thumb'
55
56
56 ABSTRACT_VIEW = '<div class="image">'\
57 ABSTRACT_VIEW = '<div class="image">'\
57 '{}'\
58 '{}'\
58 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
59 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
59 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
60 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
60 '</div>'
61 '</div>'
61 URL_VIEW = '<div class="image">' \
62 URL_VIEW = '<div class="image">' \
62 '{}' \
63 '{}' \
63 '<div class="image-metadata">{}</div>' \
64 '<div class="image-metadata">{}</div>' \
64 '</div>'
65 '</div>'
65 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
66 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
66 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
67 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
67 '</a>'
68 '</a>'
68 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
69 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
69 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
70 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
70 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
71 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
71 '<img class="post-image-preview"' \
72 '<img class="post-image-preview"' \
72 ' src="{}"' \
73 ' src="{}"' \
73 ' alt="{}"' \
74 ' alt="{}"' \
74 ' width="{}"' \
75 ' width="{}"' \
75 ' height="{}"' \
76 ' height="{}"' \
76 ' data-width="{}"' \
77 ' data-width="{}"' \
77 ' data-height="{}" />' \
78 ' data-height="{}" />' \
78 '</a>'
79 '</a>'
79 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
80 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
80 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
81 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
81 '</a>'
82 '</a>'
82 URL_FORMAT_VIEW = '<a href="{}">' \
83 URL_FORMAT_VIEW = '<a href="{}">' \
83 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
84 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
84 '</a>'
85 '</a>'
85
86
86
87
87 def get_viewers():
88 def get_viewers():
88 return AbstractViewer.__subclasses__()
89 return AbstractViewer.__subclasses__()
89
90
90
91
91 def get_static_dimensions(filename):
92 def get_static_dimensions(filename):
92 file_path = finders.find(filename)
93 file_path = finders.find(filename)
93 return get_image_dimensions(file_path)
94 return get_image_dimensions(file_path)
94
95
95
96
96 # TODO Move this to utils
97 # TODO Move this to utils
97 def file_exists(filename):
98 def file_exists(filename):
98 return finders.find(filename) is not None
99 return finders.find(filename) is not None
99
100
100
101
101 class AbstractViewer:
102 class AbstractViewer:
102 def __init__(self, file, file_type, hash, url):
103 def __init__(self, file, file_type, hash, url):
103 self.file = file
104 self.file = file
104 self.file_type = file_type
105 self.file_type = file_type
105 self.hash = hash
106 self.hash = hash
106 self.url = url
107 self.url = url
107
108
108 @staticmethod
109 @staticmethod
109 def supports(file_type):
110 def supports(file_type):
110 return True
111 return True
111
112
112 def get_view(self):
113 def get_view(self):
113 search_host = settings.get('External', 'ImageSearchHost')
114 search_host = settings.get('External', 'ImageSearchHost')
114 if search_host:
115 if search_host:
115 if search_host.endswith('/'):
116 if search_host.endswith('/'):
116 search_host = search_host[:-1]
117 search_host = search_host[:-1]
117 search_url = search_host + self.file.url
118 search_url = search_host + self.file.url
118 else:
119 else:
119 search_url = ''
120 search_url = ''
120
121
121 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
122 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
122 self.file_type, filesizeformat(self.file.size),
123 self.file_type, filesizeformat(self.file.size),
123 self.file_type, search_url, self.file.name)
124 self.file_type, search_url, self.file.name)
124
125
125 def get_format_view(self):
126 def get_format_view(self):
126 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
127 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
127 file_name = FILE_FILEFORMAT.format(image_name)
128 file_name = FILE_FILEFORMAT.format(image_name)
128
129
129 if file_exists(file_name):
130 if file_exists(file_name):
130 image = file_name
131 image = file_name
131 else:
132 else:
132 image = FILE_STUB_IMAGE
133 image = FILE_STUB_IMAGE
133
134
134 w, h = get_static_dimensions(image)
135 w, h = get_static_dimensions(image)
135
136
136 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
137 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
137
138
138
139
139 class VideoViewer(AbstractViewer):
140 class VideoViewer(AbstractViewer):
140 @staticmethod
141 @staticmethod
141 def supports(file_type):
142 def supports(file_type):
142 return file_type in FILE_TYPES_VIDEO
143 return file_type in FILE_TYPES_VIDEO
143
144
144 def get_format_view(self):
145 def get_format_view(self):
145 return VIDEO_FORMAT_VIEW.format(self.file.url)
146 return VIDEO_FORMAT_VIEW.format(self.file.url)
146
147
147
148
148 class AudioViewer(AbstractViewer):
149 class AudioViewer(AbstractViewer):
149 @staticmethod
150 @staticmethod
150 def supports(file_type):
151 def supports(file_type):
151 return file_type in FILE_TYPES_AUDIO
152 return file_type in FILE_TYPES_AUDIO
152
153
153 def get_format_view(self):
154 def get_format_view(self):
154 return AUDIO_FORMAT_VIEW.format(self.file.url)
155 return AUDIO_FORMAT_VIEW.format(self.file.url)
155
156
156
157
157 class SvgViewer(AbstractViewer):
158 class SvgViewer(AbstractViewer):
158 @staticmethod
159 @staticmethod
159 def supports(file_type):
160 def supports(file_type):
160 return file_type == FILE_TYPE_SVG
161 return file_type == FILE_TYPE_SVG
161
162
162 def get_format_view(self):
163 def get_format_view(self):
163 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
164 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
164
165
165
166
166 class ImageViewer(AbstractViewer):
167 class ImageViewer(AbstractViewer):
167 @staticmethod
168 @staticmethod
168 def supports(file_type):
169 def supports(file_type):
169 return file_type in FILE_TYPES_IMAGE
170 return file_type in FILE_TYPES_IMAGE
170
171
171 def get_format_view(self):
172 def get_format_view(self):
172 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
173 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
173 filesizeformat(self.file.size))
174 filesizeformat(self.file.size))
174
175
175 try:
176 try:
176 width, height = get_image_dimensions(self.file.path)
177 width, height = get_image_dimensions(self.file.path)
177 except Exception:
178 except Exception:
178 # If the image is a decompression bomb, treat it as just a regular
179 # If the image is a decompression bomb, treat it as just a regular
179 # file
180 # file
180 return super().get_format_view()
181 return super().get_format_view()
181
182
182 preview_path = self.file.path.replace('.', '.200x150.')
183 preview_path = self.file.path.replace('.', '.200x150.')
183 pre_width, pre_height = get_image_dimensions(preview_path)
184 pre_width, pre_height = get_image_dimensions(preview_path)
184
185
185 split = self.file.url.rsplit('.', 1)
186 split = self.file.url.rsplit('.', 1)
186 w, h = 200, 150
187 w, h = 200, 150
187 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
188 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
188
189
189 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
190 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
190 thumb_url,
191 thumb_url,
191 self.hash,
192 self.hash,
192 str(pre_width),
193 str(pre_width),
193 str(pre_height), str(width), str(height),
194 str(pre_height), str(width), str(height),
194 full=self.file.url, image_meta=metadata)
195 full=self.file.url, image_meta=metadata)
195
196
196
197
197 class UrlViewer(AbstractViewer):
198 class UrlViewer(AbstractViewer):
198 @staticmethod
199 @staticmethod
199 def supports(file_type):
200 def supports(file_type):
200 return file_type is None
201 return file_type is None
201
202
202 def get_view(self):
203 def get_view(self):
203 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
204 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
204
205
205 def get_format_view(self):
206 def get_format_view(self):
206 protocol = self.url.split(':')[0]
207 protocol = self.url.split(':')[0]
207
208
208 domain = get_domain(self.url)
209 domain = get_domain(self.url)
209
210
210 if protocol in URL_PROTOCOLS:
211 if protocol in URL_PROTOCOLS:
211 url_image_name = URL_PROTOCOLS.get(protocol)
212 url_image_name = URL_PROTOCOLS.get(protocol)
212 elif domain:
213 elif domain:
213 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
214 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
214 else:
215 else:
215 url_image_name = FILE_STUB_URL
216 url_image_name = FILE_STUB_URL
216
217
217 image_path = 'images/{}.png'.format(url_image_name)
218 image_path = 'images/{}.png'.format(url_image_name)
218 image = static(image_path)
219 image = static(image_path)
219 w, h = get_static_dimensions(image_path)
220 w, h = get_static_dimensions(image_path)
220
221
221 return URL_FORMAT_VIEW.format(self.url, image, w, h)
222 return URL_FORMAT_VIEW.format(self.url, image, w, h)
222
223
223 @cached_result()
224 @cached_result()
224 def _find_image_for_domains(self, domain):
225 def _find_image_for_domains(self, domain):
225 """
226 """
226 Searches for the domain image for every domain level except top.
227 Searches for the domain image for every domain level except top.
227 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
228 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
228 example.co.uk, then co.uk
229 example.co.uk, then co.uk
229 """
230 """
230 levels = domain.split('.')
231 levels = domain.split('.')
231 while len(levels) > 1:
232 while len(levels) > 1:
232 domain = '.'.join(levels)
233 domain = '.'.join(levels)
233
234
234 filename = 'images/domains/{}.png'.format(domain)
235 filename = 'images/domains/{}.png'.format(domain)
235 if file_exists(filename):
236 if file_exists(filename):
236 return 'domains/' + domain
237 return 'domains/' + domain
237 else:
238 else:
238 del levels[0]
239 del levels[0]
239
240
@@ -1,157 +1,157 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 import uuid
5 import uuid
6
6
7 from boards.abstracts.constants import FILE_DIRECTORY
7 from boards.abstracts.constants import FILE_DIRECTORY
8 from random import random
8 from random import random
9 import time
9 import time
10 import hmac
10 import hmac
11
11
12 from django.core.cache import cache
12 from django.core.cache import cache
13 from django.db.models import Model
13 from django.db.models import Model
14 from django import forms
14 from django import forms
15 from django.template.defaultfilters import filesizeformat
15 from django.template.defaultfilters import filesizeformat
16 from django.utils import timezone
16 from django.utils import timezone
17 from django.utils.translation import ugettext_lazy as _
17 from django.utils.translation import ugettext_lazy as _
18 import magic
18 import magic
19 import os
19 import os
20
20
21 import boards
21 import boards
22 from boards.settings import get_bool
22 from boards.settings import get_bool
23 from neboard import settings
23 from neboard import settings
24
24
25 CACHE_KEY_DELIMITER = '_'
25 CACHE_KEY_DELIMITER = '_'
26
26
27 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
27 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
28 META_REMOTE_ADDR = 'REMOTE_ADDR'
28 META_REMOTE_ADDR = 'REMOTE_ADDR'
29
29
30 SETTING_MESSAGES = 'Messages'
30 SETTING_MESSAGES = 'Messages'
31 SETTING_ANON_MODE = 'AnonymousMode'
31 SETTING_ANON_MODE = 'AnonymousMode'
32
32
33 ANON_IP = '127.0.0.1'
33 ANON_IP = '127.0.0.1'
34
34
35 FILE_EXTENSION_DELIMITER = '.'
35 FILE_EXTENSION_DELIMITER = '.'
36
36
37
37
38 def is_anonymous_mode():
38 def is_anonymous_mode():
39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40
40
41
41
42 def get_client_ip(request):
42 def get_client_ip(request):
43 if is_anonymous_mode():
43 if is_anonymous_mode():
44 ip = ANON_IP
44 ip = ANON_IP
45 else:
45 else:
46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 if x_forwarded_for:
47 if x_forwarded_for:
48 ip = x_forwarded_for.split(',')[-1].strip()
48 ip = x_forwarded_for.split(',')[-1].strip()
49 else:
49 else:
50 ip = request.META.get(META_REMOTE_ADDR)
50 ip = request.META.get(META_REMOTE_ADDR)
51 return ip
51 return ip
52
52
53
53
54 # TODO The output format is not epoch because it includes microseconds
54 # TODO The output format is not epoch because it includes microseconds
55 def datetime_to_epoch(datetime):
55 def datetime_to_epoch(datetime):
56 return int(time.mktime(timezone.localtime(
56 return int(time.mktime(timezone.localtime(
57 datetime,timezone.get_current_timezone()).timetuple())
57 datetime,timezone.get_current_timezone()).timetuple())
58 * 1000000 + datetime.microsecond)
58 * 1000000 + datetime.microsecond)
59
59
60
60
61 def get_websocket_token(user_id='', timestamp=''):
61 def get_websocket_token(user_id='', timestamp=''):
62 """
62 """
63 Create token to validate information provided by new connection.
63 Create token to validate information provided by new connection.
64 """
64 """
65
65
66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 sign.update(user_id.encode())
68 sign.update(user_id.encode())
69 sign.update(timestamp.encode())
69 sign.update(timestamp.encode())
70 token = sign.hexdigest()
70 token = sign.hexdigest()
71
71
72 return token
72 return token
73
73
74
74
75 # TODO Test this carefully
75 # TODO Test this carefully
76 def cached_result(key_method=None):
76 def cached_result(key_method=None):
77 """
77 """
78 Caches method result in the Django's cache system, persisted by object name,
78 Caches method result in the Django's cache system, persisted by object name,
79 object name, model id if object is a Django model, args and kwargs if any.
79 object name, model id if object is a Django model, args and kwargs if any.
80 """
80 """
81 def _cached_result(function):
81 def _cached_result(function):
82 def inner_func(obj, *args, **kwargs):
82 def inner_func(obj, *args, **kwargs):
83 cache_key_params = [obj.__class__.__name__, function.__name__]
83 cache_key_params = [obj.__class__.__name__, function.__name__]
84
84
85 cache_key_params += args
85 cache_key_params += args
86 for key, value in kwargs:
86 for key, value in kwargs:
87 cache_key_params.append(key + ':' + value)
87 cache_key_params.append(key + ':' + value)
88
88
89 if isinstance(obj, Model):
89 if isinstance(obj, Model):
90 cache_key_params.append(str(obj.id))
90 cache_key_params.append(str(obj.id))
91
91
92 if key_method is not None:
92 if key_method is not None:
93 cache_key_params += [str(arg) for arg in key_method(obj)]
93 cache_key_params += [str(arg) for arg in key_method(obj)]
94
94
95 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
95 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
96
96
97 persisted_result = cache.get(cache_key)
97 persisted_result = cache.get(cache_key)
98 if persisted_result is not None:
98 if persisted_result is not None:
99 result = persisted_result
99 result = persisted_result
100 else:
100 else:
101 result = function(obj, *args, **kwargs)
101 result = function(obj, *args, **kwargs)
102 if result is not None:
102 if result is not None:
103 cache.set(cache_key, result)
103 cache.set(cache_key, result)
104
104
105 return result
105 return result
106
106
107 return inner_func
107 return inner_func
108 return _cached_result
108 return _cached_result
109
109
110
110
111 def get_file_hash(file) -> str:
111 def get_file_hash(file) -> str:
112 md5 = hashlib.md5()
112 md5 = hashlib.md5()
113 for chunk in file.chunks():
113 for chunk in file.chunks():
114 md5.update(chunk)
114 md5.update(chunk)
115 return md5.hexdigest()
115 return md5.hexdigest()
116
116
117
117
118 def validate_file_size(size: int):
118 def validate_file_size(size: int):
119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
120 if max_size > 0 and size > max_size:
120 if max_size > 0 and size > max_size:
121 raise forms.ValidationError(
121 raise forms.ValidationError(
122 _('File must be less than %s but is %s.')
122 _('File must be less than %s but is %s.')
123 % (filesizeformat(max_size), filesizeformat(size)))
123 % (filesizeformat(max_size), filesizeformat(size)))
124
124
125
125
126 def get_extension(filename):
126 def get_extension(filename):
127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
128
128
129
129
130 def get_upload_filename(model_instance, old_filename):
130 def get_upload_filename(model_instance, old_filename):
131 extension = get_extension(old_filename)
131 extension = get_extension(old_filename)
132 new_name = '{}.{}'.format(uuid.uuid4(), extension)
132 new_name = '{}.{}'.format(uuid.uuid4(), extension)
133
133
134 return os.path.join(FILE_DIRECTORY, new_name)
134 return os.path.join(FILE_DIRECTORY, new_name)
135
135
136
136
137 def get_file_mimetype(file) -> str:
137 def get_file_mimetype(file) -> str:
138 file_type = magic.from_buffer(file.chunks().__next__(), mime=True)
138 file_type = magic.from_file(settings.MEDIA_ROOT + file.name, mime=True)
139 if file_type is None:
139 if file_type is None:
140 file_type = 'application/octet-stream'
140 file_type = 'application/octet-stream'
141 elif type(file_type) == bytes:
141 elif type(file_type) == bytes:
142 file_type = file_type.decode()
142 file_type = file_type.decode()
143 return file_type
143 return file_type
144
144
145
145
146 def get_domain(url: str) -> str:
146 def get_domain(url: str) -> str:
147 """
147 """
148 Gets domain from an URL with random number of domain levels.
148 Gets domain from an URL with random number of domain levels.
149 """
149 """
150 domain_parts = url.split('/')
150 domain_parts = url.split('/')
151 if len(domain_parts) >= 2:
151 if len(domain_parts) >= 2:
152 full_domain = domain_parts[2]
152 full_domain = domain_parts[2]
153 else:
153 else:
154 full_domain = ''
154 full_domain = ''
155
155
156 return full_domain
156 return full_domain
157
157
General Comments 0
You need to be logged in to leave comments. Login now