1
2
3 """ S3 Synchronization: Peer Repository Adapter
4
5 @copyright: 2012-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28 """
29
30 import sys
31 import urllib, urllib2
32
33 from gluon import *
34 from gluon.storage import Storage
35
36 from ..s3sync import S3SyncBaseAdapter
37
38
40 """
41 CiviCRM Synchronization Adapter
42
43 @status: experimental
44 """
45
46
47 RESOURCE = {
48 "pr_person": {
49 "q": "civicrm/contact",
50 "contact_type": "Individual"
51 },
52 }
53
54
55
56
58 """
59 Register this site at the peer repository
60
61 @return: True to indicate success, otherwise False
62 """
63
64
65 return True
66
67
69 """
70 Login at the peer repository
71
72 @return: None if successful, otherwise the error
73 """
74
75 _debug = current.log.debug
76
77 _debug("S3SyncCiviCRM.login()")
78
79 repository = self.repository
80
81 request = {
82 "q": "civicrm/login",
83 "name": repository.username,
84 "pass": repository.password,
85 }
86 response, error = self._send_request(**request)
87
88 if error:
89 _debug("S3SyncCiviCRM.login FAILURE: %s" % error)
90 return error
91
92 api_key = response.findall("//api_key")
93 if len(api_key):
94 self.api_key = api_key[0].text
95 else:
96 error = "No API Key returned by CiviCRM"
97 _debug("S3SyncCiviCRM.login FAILURE: %s" % error)
98 return error
99 PHPSESSID = response.findall("//PHPSESSID")
100 if len(PHPSESSID):
101 self.PHPSESSID = PHPSESSID[0].text
102 else:
103 error = "No PHPSESSID returned by CiviCRM"
104 _debug("S3SyncCiviCRM.login FAILURE: %s" % error)
105 return error
106
107 _debug("S3SyncCiviCRM.login SUCCESS")
108 return None
109
110
111 - def pull(self, task, onconflict=None):
112 """
113 Fetch updates from the peer repository and import them
114 into the local database (active pull)
115
116 @param task: the synchronization task (sync_task Row)
117 @param onconflict: callback for automatic conflict resolution
118
119 @return: tuple (error, mtime), with error=None if successful,
120 else error=message, and mtime=modification timestamp
121 of the youngest record sent
122 """
123
124 xml = current.xml
125 _debug = current.log.debug
126 repository = self.repository
127 log = repository.log
128 resource_name = task.resource_name
129
130 _debug("S3SyncCiviCRM.pull(%s, %s)" % (repository.url, resource_name))
131
132 mtime = None
133 message = ""
134 remote = False
135
136
137 if resource_name not in self.RESOURCE:
138 result = log.FATAL
139 message = "Resource type %s currently not supported for CiviCRM synchronization" % \
140 resource_name
141 output = xml.json_message(False, 400, message)
142 else:
143 args = Storage(self.RESOURCE[resource_name])
144 args["q"] += "/get"
145
146 tree, error = self._send_request(method="GET", **args)
147 if error:
148
149 result = log.FATAL
150 remote = True
151 message = error
152 output = xml.json_message(False, 400, error)
153
154 elif len(tree.getroot()):
155
156 result = log.SUCCESS
157 remote = False
158
159
160 strategy = task.strategy
161 update_policy = task.update_policy
162 conflict_policy = task.conflict_policy
163
164
165 folder = current.request.folder
166 import os
167 stylesheet = os.path.join(folder,
168 "static",
169 "formats",
170 "ccrm",
171 "import.xsl")
172
173
174
175 import urlparse
176 hostname = urlparse.urlsplit(repository.url).hostname
177
178
179 resource = current.s3db.resource(resource_name)
180 if onconflict:
181 onconflict_callback = lambda item: onconflict(item,
182 repository,
183 resource)
184 else:
185 onconflict_callback = None
186 count = 0
187 success = True
188 try:
189 success = resource.import_xml(tree,
190 stylesheet=stylesheet,
191 ignore_errors=True,
192 strategy=strategy,
193 update_policy=update_policy,
194 conflict_policy=conflict_policy,
195 last_sync=task.last_pull,
196 onconflict=onconflict_callback,
197 site=hostname)
198 count = resource.import_count
199 except IOError, e:
200 result = log.FATAL
201 message = "%s" % e
202 output = xml.json_message(False, 400, message)
203 mtime = resource.mtime
204
205
206 if resource.error_tree is not None:
207 result = log.WARNING
208 message = "%s" % resource.error
209 for element in resource.error_tree.findall("resource"):
210 for field in element.findall("data[@error]"):
211 error_msg = field.get("error", None)
212 if error_msg:
213 msg = "(UID: %s) %s.%s=%s: %s" % \
214 (element.get("uuid", None),
215 element.get("name", None),
216 field.get("field", None),
217 field.get("value", field.text),
218 field.get("error", None))
219 message = "%s, %s" % (message, msg)
220
221
222 if not success:
223 result = log.FATAL
224 if not message:
225 message = "%s" % resource.error
226 output = xml.json_message(False, 400, message)
227 mtime = None
228
229
230 elif not message:
231 message = "Data imported successfully (%s records)" % count
232 output = None
233
234 else:
235
236 result = log.ERROR
237 remote = True
238 message = "No data received from peer"
239 output = None
240
241
242 log.write(repository_id=repository.id,
243 resource_name=resource_name,
244 transmission=log.OUT,
245 mode=log.PULL,
246 action=None,
247 remote=remote,
248 result=result,
249 message=message)
250
251 _debug("S3SyncCiviCRM.pull import %s: %s" % (result, message))
252 return (output, mtime)
253
254
255 - def push(self, task):
256 """
257 Extract new updates from the local database and send
258 them to the peer repository (active push)
259
260 @param task: the synchronization task (sync_task Row)
261
262 @return: tuple (error, mtime), with error=None if successful,
263 else error=message, and mtime=modification timestamp
264 of the youngest record sent
265 """
266
267 xml = current.xml
268 _debug = current.log.debug
269 repository = self.repository
270
271 log = repository.log
272 resource_name = task.resource_name
273
274 _debug("S3SyncCiviCRM.push(%s, %s)" % (repository.url, resource_name))
275
276 result = log.FATAL
277 remote = False
278 message = "Push to CiviCRM currently not supported"
279 output = xml.json_message(False, 400, message)
280
281
282 log.write(repository_id=repository.id,
283 resource_name=resource_name,
284 transmission=log.OUT,
285 mode=log.PUSH,
286 action=None,
287 remote=remote,
288 result=result,
289 message=message)
290
291 _debug("S3SyncCiviCRM.push export %s: %s" % (result, message))
292 return(output, None)
293
294
295
296
298
299 repository = self.repository
300 config = repository.config
301
302
303 args = Storage(args)
304 if hasattr(self, "PHPSESSID") and self.PHPSESSID:
305 args["PHPSESSID"] = self.PHPSESSID
306 if hasattr(self, "api_key") and self.api_key:
307 args["api_key"] = self.api_key
308 if repository.site_key:
309 args["key"] = repository.site_key
310
311
312 url = repository.url + "?" + urllib.urlencode(args)
313 req = urllib2.Request(url=url)
314 handlers = []
315
316
317 proxy = repository.proxy or config.proxy or None
318 if proxy:
319 current.log.debug("using proxy=%s", proxy)
320 proxy_handler = urllib2.ProxyHandler({protocol: proxy})
321 handlers.append(proxy_handler)
322
323
324 if handlers:
325 opener = urllib2.build_opener(*handlers)
326 urllib2.install_opener(opener)
327
328
329 response = None
330 message = None
331
332 try:
333 if method == "POST":
334 f = urllib2.urlopen(req, data="")
335 else:
336 f = urllib2.urlopen(req)
337 except urllib2.HTTPError, e:
338 message = "HTTP %s: %s" % (e.code, e.reason)
339 else:
340
341 tree = current.xml.parse(f)
342 root = tree.getroot()
343 is_error = root.xpath("//ResultSet[1]/Result[1]/is_error")
344 if len(is_error) and int(is_error[0].text):
345 error = root.xpath("//ResultSet[1]/Result[1]/error_message")
346 if len(error):
347 message = error[0].text
348 else:
349 message = "Unknown error"
350 else:
351 response = tree
352
353 return response, message
354
355
356