##// END OF EJS Templates
Don't allow parsing a post when its thread is not parsed yet
neko259 -
r1838:c7821a56 default
parent child Browse files
Show More
@@ -1,378 +1,379 b''
1 1 import xml.etree.ElementTree as et
2 2 import logging
3 3 from xml.etree import ElementTree
4 4
5 5 from boards.abstracts.exceptions import SyncException
6 6 from boards.abstracts.sync_filters import ThreadFilter
7 7 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
8 8 from boards.models.attachment.downloaders import download
9 9 from boards.models.signature import TAG_REQUEST, ATTR_TYPE, TYPE_GET, \
10 10 ATTR_VERSION, TAG_MODEL, ATTR_NAME, TAG_ID, TYPE_LIST
11 11 from boards.utils import get_file_mimetype, get_file_hash
12 12 from django.db import transaction
13 13
14 14 EXCEPTION_NODE = 'Sync node returned an error: {}.'
15 15 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
16 16 EXCEPTION_HASH = 'File hash does not match attachment hash.'
17 17 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
18 18 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
19 EXCEPTION_THREAD = 'No thread exists for post {}'
19 20 ENCODING_UNICODE = 'unicode'
20 21
21 22 TAG_MODEL = 'model'
22 23 TAG_REQUEST = 'request'
23 24 TAG_RESPONSE = 'response'
24 25 TAG_ID = 'id'
25 26 TAG_STATUS = 'status'
26 27 TAG_MODELS = 'models'
27 28 TAG_TITLE = 'title'
28 29 TAG_TEXT = 'text'
29 30 TAG_THREAD = 'thread'
30 31 TAG_PUB_TIME = 'pub-time'
31 32 TAG_SIGNATURES = 'signatures'
32 33 TAG_SIGNATURE = 'signature'
33 34 TAG_CONTENT = 'content'
34 35 TAG_ATTACHMENTS = 'attachments'
35 36 TAG_ATTACHMENT = 'attachment'
36 37 TAG_TAGS = 'tags'
37 38 TAG_TAG = 'tag'
38 39 TAG_ATTACHMENT_REFS = 'attachment-refs'
39 40 TAG_ATTACHMENT_REF = 'attachment-ref'
40 41 TAG_TRIPCODE = 'tripcode'
41 42 TAG_VERSION = 'version'
42 43
43 44 TYPE_GET = 'get'
44 45
45 46 ATTR_VERSION = 'version'
46 47 ATTR_TYPE = 'type'
47 48 ATTR_NAME = 'name'
48 49 ATTR_VALUE = 'value'
49 50 ATTR_MIMETYPE = 'mimetype'
50 51 ATTR_KEY = 'key'
51 52 ATTR_REF = 'ref'
52 53 ATTR_URL = 'url'
53 54 ATTR_ID_TYPE = 'id-type'
54 55
55 56 ID_TYPE_MD5 = 'md5'
56 57 ID_TYPE_URL = 'url'
57 58
58 59 STATUS_SUCCESS = 'success'
59 60
60 61
61 62 logger = logging.getLogger('boards.sync')
62 63
63 64
64 65 class SyncManager:
65 66 @staticmethod
66 67 def generate_response_get(model_list: list):
67 68 response = et.Element(TAG_RESPONSE)
68 69
69 70 status = et.SubElement(response, TAG_STATUS)
70 71 status.text = STATUS_SUCCESS
71 72
72 73 models = et.SubElement(response, TAG_MODELS)
73 74
74 75 for post in model_list:
75 76 model = et.SubElement(models, TAG_MODEL)
76 77 model.set(ATTR_NAME, 'post')
77 78
78 79 global_id = post.global_id
79 80
80 81 attachments = post.attachments.all()
81 82 if global_id.content:
82 83 model.append(et.fromstring(global_id.content))
83 84 if len(attachments) > 0:
84 85 internal_attachments = False
85 86 for attachment in attachments:
86 87 if attachment.is_internal():
87 88 internal_attachments = True
88 89 break
89 90
90 91 if internal_attachments:
91 92 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
92 93 for file in attachments:
93 94 SyncManager._attachment_to_xml(
94 95 None, attachment_refs, file)
95 96 else:
96 97 content_tag = et.SubElement(model, TAG_CONTENT)
97 98
98 99 tag_id = et.SubElement(content_tag, TAG_ID)
99 100 global_id.to_xml_element(tag_id)
100 101
101 102 title = et.SubElement(content_tag, TAG_TITLE)
102 103 title.text = post.title
103 104
104 105 text = et.SubElement(content_tag, TAG_TEXT)
105 106 text.text = post.get_sync_text()
106 107
107 108 thread = post.get_thread()
108 109 if post.is_opening():
109 110 tag_tags = et.SubElement(content_tag, TAG_TAGS)
110 111 for tag in thread.get_tags():
111 112 tag_tag = et.SubElement(tag_tags, TAG_TAG)
112 113 tag_tag.text = tag.name
113 114 else:
114 115 tag_thread = et.SubElement(content_tag, TAG_THREAD)
115 116 thread_id = et.SubElement(tag_thread, TAG_ID)
116 117 thread.get_opening_post().global_id.to_xml_element(thread_id)
117 118
118 119 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
119 120 pub_time.text = str(post.get_pub_time_str())
120 121
121 122 if post.tripcode:
122 123 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
123 124 tripcode.text = post.tripcode
124 125
125 126 if len(attachments) > 0:
126 127 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
127 128
128 129 internal_attachments = False
129 130 for attachment in attachments:
130 131 if attachment.is_internal():
131 132 internal_attachments = True
132 133 break
133 134
134 135 if internal_attachments:
135 136 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
136 137 else:
137 138 attachment_refs = None
138 139
139 140 for file in attachments:
140 141 SyncManager._attachment_to_xml(
141 142 attachments_tag, attachment_refs, file)
142 143 version_tag = et.SubElement(content_tag, TAG_VERSION)
143 144 version_tag.text = str(post.version)
144 145
145 146 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
146 147 global_id.save()
147 148
148 149 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
149 150 post_signatures = global_id.signature_set.all()
150 151 if post_signatures:
151 152 signatures = post_signatures
152 153 else:
153 154 key = KeyPair.objects.get(public_key=global_id.key)
154 155 signature = Signature(
155 156 key_type=key.key_type,
156 157 key=key.public_key,
157 158 signature=key.sign(global_id.content),
158 159 global_id=global_id,
159 160 )
160 161 signature.save()
161 162 signatures = [signature]
162 163 for signature in signatures:
163 164 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
164 165 signature_tag.set(ATTR_TYPE, signature.key_type)
165 166 signature_tag.set(ATTR_VALUE, signature.signature)
166 167 signature_tag.set(ATTR_KEY, signature.key)
167 168
168 169 return et.tostring(response, ENCODING_UNICODE)
169 170
170 171 @staticmethod
171 172 def parse_response_get(response_xml, hostname):
172 173 tag_root = et.fromstring(response_xml)
173 174 tag_status = tag_root.find(TAG_STATUS)
174 175 if STATUS_SUCCESS == tag_status.text:
175 176 tag_models = tag_root.find(TAG_MODELS)
176 177 for tag_model in tag_models:
177 178 SyncManager.parse_post(tag_model, hostname)
178 179 else:
179 180 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
180 181
181 182 @staticmethod
182 183 @transaction.atomic
183 184 def parse_post(tag_model, hostname):
184 185 tag_content = tag_model.find(TAG_CONTENT)
185 186
186 187 content_str = et.tostring(tag_content, ENCODING_UNICODE)
187 188
188 189 tag_id = tag_content.find(TAG_ID)
189 190 global_id, exists = GlobalId.from_xml_element(tag_id)
190 191 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
191 192
192 193 version = int(tag_content.find(TAG_VERSION).text)
193 194 is_old = exists and global_id.post.version < version
194 195 if exists and not is_old:
195 196 print('Post with same ID exists and is up to date.')
196 197 else:
197 198 global_id.content = content_str
198 199 global_id.save()
199 200 for signature in signatures:
200 201 signature.global_id = global_id
201 202 signature.save()
202 203
203 204 title = tag_content.find(TAG_TITLE).text or ''
204 205 text = tag_content.find(TAG_TEXT).text or ''
205 206 pub_time = tag_content.find(TAG_PUB_TIME).text
206 207 tripcode_tag = tag_content.find(TAG_TRIPCODE)
207 208 if tripcode_tag is not None:
208 209 tripcode = tripcode_tag.text or ''
209 210 else:
210 211 tripcode = ''
211 212
212 213 thread = tag_content.find(TAG_THREAD)
213 214 tags = []
214 215 if thread:
215 216 thread_id = thread.find(TAG_ID)
216 217 op_global_id, exists = GlobalId.from_xml_element(thread_id)
217 218 if exists:
218 219 opening_post = Post.objects.get(global_id=op_global_id)
219 220 else:
220 logger.debug('No thread exists for post {}'.format(global_id))
221 raise Exception(EXCEPTION_THREAD.format(global_id))
221 222 else:
222 223 opening_post = None
223 224 tag_tags = tag_content.find(TAG_TAGS)
224 225 for tag_tag in tag_tags:
225 226 tag, created = Tag.objects.get_or_create(
226 227 name=tag_tag.text)
227 228 tags.append(tag)
228 229
229 230 # TODO Check that the replied posts are already present
230 231 # before adding new ones
231 232
232 233 files = []
233 234 urls = []
234 235 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
235 236 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
236 237 for attachment in tag_attachments:
237 238 if attachment.get(ATTR_ID_TYPE) == ID_TYPE_URL:
238 239 urls.append(attachment.text)
239 240 else:
240 241 tag_ref = tag_refs.find("{}[@ref='{}']".format(
241 242 TAG_ATTACHMENT_REF, attachment.text))
242 243 url = tag_ref.get(ATTR_URL)
243 244 attached_file = download(hostname + url, validate=False)
244 245 if attached_file is None:
245 246 raise SyncException(EXCEPTION_DOWNLOAD)
246 247
247 248 hash = get_file_hash(attached_file)
248 249 if hash != attachment.text:
249 250 raise SyncException(EXCEPTION_HASH)
250 251
251 252 files.append(attached_file)
252 253
253 254 if is_old:
254 255 post = global_id.post
255 256 Post.objects.update_post(
256 257 post, title=title, text=text, pub_time=pub_time,
257 258 tags=tags, files=files, file_urls=urls,
258 259 tripcode=tripcode, version=version)
259 260 logger.debug('Parsed updated post {}'.format(global_id))
260 261 else:
261 262 Post.objects.import_post(
262 263 title=title, text=text, pub_time=pub_time,
263 264 opening_post=opening_post, tags=tags,
264 265 global_id=global_id, files=files,
265 266 file_urls=urls, tripcode=tripcode,
266 267 version=version)
267 268 logger.debug('Parsed new post {}'.format(global_id))
268 269
269 270 @staticmethod
270 271 def generate_response_list(filters):
271 272 response = et.Element(TAG_RESPONSE)
272 273
273 274 status = et.SubElement(response, TAG_STATUS)
274 275 status.text = STATUS_SUCCESS
275 276
276 277 models = et.SubElement(response, TAG_MODELS)
277 278
278 279 posts = Post.objects.prefetch_related('global_id')
279 280 for post_filter in filters:
280 281 posts = post_filter.filter(posts)
281 282
282 283 for post in posts:
283 284 tag_model = et.SubElement(models, TAG_MODEL)
284 285 tag_id = et.SubElement(tag_model, TAG_ID)
285 286 post.global_id.to_xml_element(tag_id)
286 287 tag_version = et.SubElement(tag_model, TAG_VERSION)
287 288 tag_version.text = str(post.version)
288 289
289 290 return et.tostring(response, ENCODING_UNICODE)
290 291
291 292 @staticmethod
292 293 def _verify_model(global_id, content_str, tag_model):
293 294 """
294 295 Verifies all signatures for a single model.
295 296 """
296 297
297 298 signatures = []
298 299
299 300 tag_signatures = tag_model.find(TAG_SIGNATURES)
300 301 has_author_signature = False
301 302 for tag_signature in tag_signatures:
302 303 signature_type = tag_signature.get(ATTR_TYPE)
303 304 signature_value = tag_signature.get(ATTR_VALUE)
304 305 signature_key = tag_signature.get(ATTR_KEY)
305 306
306 307 if global_id.key_type == signature_type and\
307 308 global_id.key == signature_key:
308 309 has_author_signature = True
309 310
310 311 signature = Signature(key_type=signature_type,
311 312 key=signature_key,
312 313 signature=signature_value)
313 314
314 315 if not KeyPair.objects.verify(signature, content_str):
315 316 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
316 317
317 318 signatures.append(signature)
318 319 if not has_author_signature:
319 320 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
320 321
321 322 return signatures
322 323
323 324 @staticmethod
324 325 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
325 326 if tag_attachments is not None:
326 327 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
327 328 if attachment.is_internal():
328 329 mimetype = get_file_mimetype(attachment.file.file)
329 330 attachment_tag.set(ATTR_MIMETYPE, mimetype)
330 331 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
331 332 attachment_tag.text = attachment.hash
332 333 else:
333 334 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
334 335 attachment_tag.text = attachment.url
335 336
336 337 if tag_refs is not None:
337 338 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
338 339 attachment_ref.set(ATTR_REF, attachment.hash)
339 340 attachment_ref.set(ATTR_URL, attachment.file.url)
340 341
341 342 @staticmethod
342 343 def generate_request_get(global_id_list: list):
343 344 """
344 345 Form a get request from a list of ModelId objects.
345 346 """
346 347
347 348 request = et.Element(TAG_REQUEST)
348 349 request.set(ATTR_TYPE, TYPE_GET)
349 350 request.set(ATTR_VERSION, '1.0')
350 351
351 352 model = et.SubElement(request, TAG_MODEL)
352 353 model.set(ATTR_VERSION, '1.0')
353 354 model.set(ATTR_NAME, 'post')
354 355
355 356 for global_id in global_id_list:
356 357 tag_id = et.SubElement(model, TAG_ID)
357 358 global_id.to_xml_element(tag_id)
358 359
359 360 return et.tostring(request, 'unicode')
360 361
361 362 @staticmethod
362 363 def generate_request_list(opening_post=None):
363 364 """
364 365 Form a pull request from a list of ModelId objects.
365 366 """
366 367
367 368 request = et.Element(TAG_REQUEST)
368 369 request.set(ATTR_TYPE, TYPE_LIST)
369 370 request.set(ATTR_VERSION, '1.0')
370 371
371 372 model = et.SubElement(request, TAG_MODEL)
372 373 model.set(ATTR_VERSION, '1.0')
373 374 model.set(ATTR_NAME, 'post')
374 375
375 376 if opening_post:
376 377 ThreadFilter().add_filter(model, opening_post)
377 378
378 return et.tostring(request, 'unicode') No newline at end of file
379 return et.tostring(request, 'unicode')
General Comments 0
You need to be logged in to leave comments. Login now