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