1
2
3 """ S3 RESTful API
4
5 @copyright: 2009-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 __all__ = ("S3Request",
31 "S3Method",
32 "s3_request",
33 )
34
35 import json
36 import os
37 import re
38 import sys
39 import types
40 try:
41 from cStringIO import StringIO
42 except ImportError:
43 from StringIO import StringIO
44
45 from gluon import current, redirect, HTTP, URL
46 from gluon.storage import Storage
47
48 from s3datetime import s3_parse_datetime
49 from s3resource import S3Resource
50 from s3utils import s3_get_extension, s3_remove_last_record_id, s3_store_last_record_id
51
52 REGEX_FILTER = re.compile(r".+\..+|.*\(.+\).*")
53 HTTP_METHODS = ("GET", "PUT", "POST", "DELETE")
57 """
58 Class to handle RESTful requests
59 """
60
61 INTERACTIVE_FORMATS = ("html", "iframe", "popup", "dl")
62 DEFAULT_REPRESENTATION = "html"
63
64
65 - def __init__(self,
66 prefix=None,
67 name=None,
68 r=None,
69 c=None,
70 f=None,
71 args=None,
72 vars=None,
73 extension=None,
74 get_vars=None,
75 post_vars=None,
76 http=None):
77 """
78 Constructor
79
80 @param prefix: the table name prefix
81 @param name: the table name
82 @param c: the controller prefix
83 @param f: the controller function
84 @param args: list of request arguments
85 @param vars: dict of request variables
86 @param extension: the format extension (representation)
87 @param get_vars: the URL query variables (overrides vars)
88 @param post_vars: the POST variables (overrides vars)
89 @param http: the HTTP method (GET, PUT, POST, or DELETE)
90
91 @note: all parameters fall back to the attributes of the
92 current web2py request object
93 """
94
95 auth = current.auth
96
97
98
99
100 self.XSLT_PATH = "static/formats"
101 self.XSLT_EXTENSION = "xsl"
102
103
104 self.files = Storage()
105
106
107 self.controller = c or self.controller
108 self.function = f or self.function
109 if "." in self.function:
110 self.function, ext = self.function.split(".", 1)
111 if extension is None:
112 extension = ext
113 if c or f:
114 if not auth.permission.has_permission("read",
115 c=self.controller,
116 f=self.function):
117 auth.permission.fail()
118
119
120 if args is not None:
121 if isinstance(args, (list, tuple)):
122 self.args = args
123 else:
124 self.args = [args]
125 if get_vars is not None:
126 self.get_vars = get_vars
127 self.vars = get_vars.copy()
128 if post_vars is not None:
129 self.vars.update(post_vars)
130 else:
131 self.vars.update(self.post_vars)
132 if post_vars is not None:
133 self.post_vars = post_vars
134 if get_vars is None:
135 self.vars = post_vars.copy()
136 self.vars.update(self.get_vars)
137 if get_vars is None and post_vars is None and vars is not None:
138 self.vars = vars
139 self.get_vars = vars
140 self.post_vars = Storage()
141
142 self.extension = extension or current.request.extension
143 self.http = http or current.request.env.request_method
144
145
146 if r is not None:
147 if not prefix:
148 prefix = r.prefix
149 if not name:
150 name = r.name
151 self.prefix = prefix or self.controller
152 self.name = name or self.function
153
154
155 self.__parse()
156 self.custom_action = None
157 get_vars = Storage(self.get_vars)
158
159
160 self.interactive = self.representation in self.INTERACTIVE_FORMATS
161
162
163 include_deleted = False
164 if self.representation == "xml" and "include_deleted" in get_vars:
165 include_deleted = True
166 if "components" in get_vars:
167 cnames = get_vars["components"]
168 if isinstance(cnames, list):
169 cnames = ",".join(cnames)
170 cnames = cnames.split(",")
171 if len(cnames) == 1 and cnames[0].lower() == "none":
172 cnames = []
173 else:
174 cnames = None
175
176
177 component_name = self.component_name
178 component_id = self.component_id
179 if component_name and component_id:
180 varname = "%s.id" % component_name
181 if varname in get_vars:
182 var = get_vars[varname]
183 if not isinstance(var, (list, tuple)):
184 var = [var]
185 var.append(component_id)
186 get_vars[varname] = var
187 else:
188 get_vars[varname] = component_id
189
190
191 _filter = current.response.s3.filter
192 components = component_name
193 if components is None:
194 components = cnames
195
196 tablename = "%s_%s" % (self.prefix, self.name)
197
198 if not current.deployment_settings.get_auth_record_approval():
199
200 approved, unapproved = True, False
201 elif self.method == "review":
202 approved, unapproved = False, True
203 elif auth.s3_has_permission("review", tablename, self.id):
204
205
206
207 approved, unapproved = True, True
208 else:
209 approved, unapproved = True, False
210
211 self.resource = S3Resource(tablename,
212 id=self.id,
213 filter=_filter,
214 vars=get_vars,
215 components=components,
216 approved=approved,
217 unapproved=unapproved,
218 include_deleted=include_deleted,
219 context=True,
220 filter_component=component_name,
221 )
222
223 self.tablename = self.resource.tablename
224 table = self.table = self.resource.table
225
226
227 self.record = None
228 uid = self.vars.get("%s.uid" % self.name)
229 if self.id or uid and not isinstance(uid, (list, tuple)):
230
231 self.resource.load()
232 if len(self.resource) == 1:
233 self.record = self.resource.records().first()
234 _id = table._id.name
235 self.id = self.record[_id]
236 s3_store_last_record_id(self.tablename, self.id)
237 else:
238 raise KeyError(current.ERROR.BAD_RECORD)
239
240
241 self.component = None
242 if self.component_name:
243 c = self.resource.components.get(self.component_name)
244 if c:
245 self.component = c
246 else:
247 error = "%s not a component of %s" % (self.component_name,
248 self.resource.tablename)
249 raise AttributeError(error)
250
251
252 self.link = None
253 self.link_id = None
254
255 if self.component is not None:
256 self.link = self.component.link
257 if self.link and self.id and self.component_id:
258 self.link_id = self.link.link_id(self.id, self.component_id)
259 if self.link_id is None:
260 raise KeyError(current.ERROR.BAD_RECORD)
261
262
263 self._handlers = {}
264 set_handler = self.set_handler
265
266 set_handler("export_tree", self.get_tree,
267 http=("GET",), transform=True)
268 set_handler("import_tree", self.put_tree,
269 http=("GET", "PUT", "POST"), transform=True)
270 set_handler("fields", self.get_fields,
271 http=("GET",), transform=True)
272 set_handler("options", self.get_options,
273 http=("GET",),
274 representation = ("__transform__", "json"),
275 )
276
277 sync = current.sync
278 set_handler("sync", sync,
279 http=("GET", "PUT", "POST",), transform=True)
280 set_handler("sync_log", sync.log,
281 http=("GET",), transform=True)
282 set_handler("sync_log", sync.log,
283 http=("GET",), transform=False)
284
285
286 self.resource.crud(self, method="_init")
287 if self.component is not None:
288 self.component.crud(self, method="_init")
289
290
291
292
293 - def set_handler(self, method, handler,
294 http=None,
295 representation=None,
296 transform=False):
297 """
298 Set a method handler for this request
299
300 @param method: the method name
301 @param handler: the handler function
302 @type handler: handler(S3Request, **attr)
303 @param http: restrict to these HTTP methods, list|tuple
304 @param representation: register handler for non-transformable data
305 formats
306 @param transform: register handler for transformable data formats
307 (overrides representation)
308 """
309
310 if http is None:
311 http = HTTP_METHODS
312 else:
313 if not isinstance(http, (tuple, list)):
314 http = (http,)
315
316 if transform:
317 representation = ("__transform__",)
318 elif not representation:
319 representation = (self.DEFAULT_REPRESENTATION,)
320 else:
321 if not isinstance(representation, (tuple, list)):
322 representation = (representation,)
323
324 if not isinstance(method, (tuple, list)):
325 method = (method,)
326
327 handlers = self._handlers
328 for h in http:
329 if h not in HTTP_METHODS:
330 continue
331 format_hooks = handlers.get(h)
332 if format_hooks is None:
333 format_hooks = handlers[h] = {}
334 for r in representation:
335 method_hooks = format_hooks.get(r)
336 if method_hooks is None:
337 method_hooks = format_hooks[r] = {}
338 for m in method:
339 method_hooks[m] = handler
340
341
343 """
344 Get a method handler for this request
345
346 @param method: the method name
347 @param transform: get handler for transformable data format
348
349 @return: the method handler
350 """
351
352 handlers = self._handlers
353
354 http_hooks = handlers.get(self.http)
355 if not http_hooks:
356 return None
357
358 DEFAULT_REPRESENTATION = self.DEFAULT_REPRESENTATION
359 hooks = http_hooks.get(DEFAULT_REPRESENTATION)
360 if hooks:
361 method_hooks = dict(hooks)
362 else:
363 method_hooks = {}
364
365 representation = "__transform__" if transform else self.representation
366 if representation and representation != DEFAULT_REPRESENTATION:
367 hooks = http_hooks.get(representation)
368 if hooks:
369 method_hooks.update(hooks)
370
371 if not method:
372 methods = (None,)
373 else:
374 methods = (method, None)
375 for m in methods:
376 handler = method_hooks.get(m)
377 if handler is not None:
378 break
379
380 if isinstance(handler, (type, types.ClassType)):
381 return handler()
382 else:
383 return handler
384
385
445
446
447
448
450 """ Parses the web2py request object """
451
452 self.id = None
453 self.component_name = None
454 self.component_id = None
455 self.method = None
456
457
458 tablename = "%s_%s" % (self.prefix, self.name)
459
460
461 f = []
462 append = f.append
463 args = self.args
464 if len(args) > 4:
465 args = args[:4]
466 method = self.name
467 for arg in args:
468 if "." in arg:
469 arg, representation = arg.rsplit(".", 1)
470 if method is None:
471 method = arg
472 elif arg.isdigit():
473 append((method, arg))
474 method = None
475 else:
476 append((method, None))
477 method = arg
478 if method:
479 append((method, None))
480
481 self.id = f[0][1]
482
483
484 l = len(f)
485 if l > 1:
486 m = f[1][0].lower()
487 i = f[1][1]
488 components = current.s3db.get_components(tablename, names=[m])
489 if components and m in components:
490 self.component_name = m
491 self.component_id = i
492 else:
493 self.method = m
494 if not self.id:
495 self.id = i
496 if self.component_name and l > 2:
497 self.method = f[2][0].lower()
498 if not self.component_id:
499 self.component_id = f[2][1]
500
501 representation = s3_get_extension(self)
502 if representation:
503 self.representation = representation
504 else:
505 self.representation = self.DEFAULT_REPRESENTATION
506
507
508
509 if self.http == "POST" and "$search" in self.get_vars:
510 self.__search()
511
512
514 """
515 Process filters in POST, interprets URL filter expressions
516 in POST vars (if multipart), or from JSON request body (if
517 not multipart or $search=ajax).
518
519 NB: overrides S3Request method as GET (r.http) to trigger
520 the correct method handlers, but will not change
521 current.request.env.request_method
522 """
523
524 get_vars = self.get_vars
525 content_type = self.env.get("content_type") or ""
526
527 mode = get_vars.get("$search")
528
529
530 if mode:
531 self.http = "GET"
532
533
534 if content_type == "application/x-www-form-urlencoded":
535
536 filters = self.post_vars
537 decode = None
538 elif mode == "ajax" or content_type[:10] != "multipart/":
539
540 s = self.body
541 s.seek(0)
542 try:
543 filters = json.load(s)
544 except ValueError:
545 filters = {}
546 if not isinstance(filters, dict):
547 filters = {}
548 decode = None
549 else:
550
551 filters = self.post_vars
552 decode = json.loads
553
554
555 get_vars = Storage(get_vars)
556 post_vars = Storage(self.post_vars)
557
558 del get_vars["$search"]
559 for k, v in filters.items():
560 k0 = k[0]
561 if k == "$filter" or \
562 k0 != "_" and ("." in k or k0 == "(" and ")" in k):
563 try:
564 value = decode(v) if decode else v
565 except ValueError:
566 continue
567
568 if type(value) is list:
569 value = [str(item)
570 if not isinstance(item, basestring) else item
571 for item in value
572 ]
573 elif not isinstance(value, basestring):
574 value = str(value)
575 get_vars[k] = value
576
577 if k in post_vars:
578 del post_vars[k]
579
580
581 self.get_vars = get_vars
582 self.post_vars = post_vars
583
584
585 self.vars = get_vars.copy()
586 self.vars.update(self.post_vars)
587
588
589
590
592 """
593 Execute this request
594
595 @param attr: Parameters for the method handler
596 """
597
598 response = current.response
599 s3 = response.s3
600 self.next = None
601
602 bypass = False
603 output = None
604 preprocess = None
605 postprocess = None
606
607 representation = self.representation
608
609
610 if not self.id and representation == "html":
611 if self.component or self.method in ("read", "profile", "update"):
612 count = self.resource.count()
613 if self.vars is not None and count == 1:
614 self.resource.load()
615 self.record = self.resource._rows[0]
616 self.id = self.record.id
617 else:
618
619 redirect(URL(r=self, c=self.prefix, f=self.name))
620
621
622 if s3 is not None:
623 preprocess = s3.get("prep")
624 if preprocess:
625 pre = preprocess(self)
626
627 representation = self.representation
628 if pre and isinstance(pre, dict):
629 bypass = pre.get("bypass", False) is True
630 output = pre.get("output")
631 if not bypass:
632 success = pre.get("success", True)
633 if not success:
634 if representation == "html" and output:
635 if isinstance(output, dict):
636 output["r"] = self
637 return output
638 else:
639 status = pre.get("status", 400)
640 message = pre.get("message",
641 current.ERROR.BAD_REQUEST)
642 self.error(status, message)
643 elif not pre:
644 self.error(400, current.ERROR.BAD_REQUEST)
645
646
647 if representation not in ("html", "popup"):
648 response.view = "xml.html"
649
650
651 response.headers["Content-Type"] = s3.content_type.get(representation,
652 "text/html")
653
654
655 if not self.custom_action:
656 action = current.s3db.get_method(self.prefix,
657 self.name,
658 component_name=self.component_name,
659 method=self.method)
660 if isinstance(action, (type, types.ClassType)):
661 self.custom_action = action()
662 else:
663 self.custom_action = action
664
665
666 http = self.http
667 handler = None
668 if not bypass:
669
670 if self.method and self.custom_action:
671 handler = self.custom_action
672 elif http == "GET":
673 handler = self.__GET()
674 elif http == "PUT":
675 handler = self.__PUT()
676 elif http == "POST":
677 handler = self.__POST()
678 elif http == "DELETE":
679 handler = self.__DELETE()
680 else:
681 self.error(405, current.ERROR.BAD_METHOD)
682
683 if handler is not None:
684 output = handler(self, **attr)
685 else:
686
687 output = self.resource.crud(self, **attr)
688
689
690 if s3 is not None:
691 postprocess = s3.get("postp")
692 if postprocess is not None:
693 output = postprocess(self, output)
694 if output is not None and isinstance(output, dict):
695
696
697 output["r"] = self
698
699
700
701
702 if self.next is not None and \
703 (self.http != "GET" or self.method == "clear"):
704 if isinstance(output, dict):
705 form = output.get("form")
706 if form:
707 if not hasattr(form, "errors"):
708
709 form = form.elements('form', first_only=True)
710 form = form[0] if form else None
711 if form and form.errors:
712 return output
713
714 session = current.session
715 session.flash = response.flash
716 session.confirmation = response.confirmation
717 session.error = response.error
718 session.warning = response.warning
719 redirect(self.next)
720
721 return output
722
723
724 - def __GET(self, resource=None):
725 """
726 Get the GET method handler
727 """
728
729 method = self.method
730 transform = False
731 if method is None or method in ("read", "display", "update"):
732 if self.transformable():
733 method = "export_tree"
734 transform = True
735 elif self.component:
736 resource = self.resource
737 if self.interactive and resource.count() == 1:
738
739 if not resource._rows:
740 resource.load(start=0, limit=1)
741 if resource._rows:
742 self.record = resource._rows[0]
743 self.id = resource.get_id()
744 self.uid = resource.get_uid()
745 if self.component.multiple and not self.component_id:
746 method = "list"
747 else:
748 method = "read"
749 elif self.id or method in ("read", "display", "update"):
750
751 resource = self.resource
752 if not resource._rows:
753 resource.load(start=0, limit=1)
754 if resource._rows:
755 self.record = resource._rows[0]
756 self.id = resource.get_id()
757 self.uid = resource.get_uid()
758 else:
759 self.error(404, current.ERROR.BAD_RECORD)
760 method = "read"
761 else:
762 method = "list"
763
764 elif method in ("create", "update"):
765 if self.transformable(method="import"):
766 method = "import_tree"
767 transform = True
768
769 elif method == "delete":
770 return self.__DELETE()
771
772 elif method == "clear" and not self.component:
773 s3_remove_last_record_id(self.tablename)
774 self.next = URL(r=self, f=self.name)
775 return lambda r, **attr: None
776
777 elif self.transformable():
778 transform = True
779
780 return self.get_handler(method, transform=transform)
781
782
795
796
798 """
799 Get the POST method handler
800 """
801
802 if self.method == "delete":
803 return self.__DELETE()
804 else:
805 if self.transformable(method="import"):
806 return self.__PUT()
807 else:
808 post_vars = self.post_vars
809 table = self.target()[2]
810 if "deleted" in table and "id" not in post_vars:
811 original = S3Resource.original(table, post_vars)
812 if original and original.deleted:
813 self.post_vars["id"] = original.id
814 self.vars["id"] = original.id
815 return self.__GET()
816
817
819 """
820 Get the DELETE method handler
821 """
822
823 if self.method:
824 return self.get_handler(self.method)
825 else:
826 return self.get_handler("delete")
827
828
829
830
831 @staticmethod
833 """
834 XML Element tree export method
835
836 @param r: the S3Request instance
837 @param attr: controller attributes
838 """
839
840 get_vars = r.get_vars
841 args = Storage()
842
843
844 start = get_vars.get("start")
845 if start is not None:
846 try:
847 start = int(start)
848 except ValueError:
849 start = None
850 limit = get_vars.get("limit")
851 if limit is not None:
852 try:
853 limit = int(limit)
854 except ValueError:
855 limit = None
856
857
858 msince = get_vars.get("msince")
859 if msince is not None:
860 msince = s3_parse_datetime(msince)
861
862
863 if "show_ids" in get_vars:
864 if get_vars["show_ids"].lower() == "true":
865 current.xml.show_ids = True
866
867
868 if "show_urls" in get_vars:
869 if get_vars["show_urls"].lower() == "false":
870 current.xml.show_urls = False
871
872
873 mdata = get_vars.get("mdata") == "1"
874
875
876 maxbounds = False
877 if "maxbounds" in get_vars:
878 if get_vars["maxbounds"].lower() == "true":
879 maxbounds = True
880 if r.representation in ("gpx", "osm"):
881 maxbounds = True
882
883
884 if "mcomponents" in get_vars:
885 mcomponents = get_vars["mcomponents"]
886 if str(mcomponents).lower() == "none":
887 mcomponents = None
888 elif not isinstance(mcomponents, list):
889 mcomponents = mcomponents.split(",")
890 else:
891 mcomponents = []
892
893
894 if "rcomponents" in get_vars:
895 rcomponents = get_vars["rcomponents"]
896 if str(rcomponents).lower() == "none":
897 rcomponents = None
898 elif not isinstance(rcomponents, list):
899 rcomponents = rcomponents.split(",")
900 else:
901 rcomponents = None
902
903
904 if "maxdepth" in get_vars:
905 try:
906 args["maxdepth"] = int(get_vars["maxdepth"])
907 except ValueError:
908 pass
909
910
911 if "references" in get_vars:
912 references = get_vars["references"]
913 if str(references).lower() == "none":
914 references = []
915 elif not isinstance(references, list):
916 references = references.split(",")
917 else:
918 references = None
919
920
921 if "fields" in get_vars:
922 fields = get_vars["fields"]
923 if str(fields).lower() == "none":
924 fields = []
925 elif not isinstance(fields, list):
926 fields = fields.split(",")
927 else:
928 fields = None
929
930
931 stylesheet = r.stylesheet()
932
933
934 if stylesheet is not None:
935 if r.component:
936 args["id"] = r.id
937 args["component"] = r.component.tablename
938 if r.component.alias:
939 args["alias"] = r.component.alias
940 mode = get_vars.get("xsltmode")
941 if mode is not None:
942 args["mode"] = mode
943
944
945 response = current.response
946 s3 = response.s3
947 headers = response.headers
948 representation = r.representation
949 if representation in s3.json_formats:
950 as_json = True
951 default = "application/json"
952 else:
953 as_json = False
954 default = "text/xml"
955 headers["Content-Type"] = s3.content_type.get(representation,
956 default)
957
958
959 resource = r.resource
960 target = r.target()[3]
961 if target == resource.tablename:
962
963 target = None
964 output = resource.export_xml(start = start,
965 limit = limit,
966 msince = msince,
967 fields = fields,
968 dereference = True,
969
970 references = references,
971 mdata = mdata,
972 mcomponents = mcomponents,
973 rcomponents = rcomponents,
974 stylesheet = stylesheet,
975 as_json = as_json,
976 maxbounds = maxbounds,
977 target = target,
978 **args)
979
980 if not output:
981 r.error(400, "XSLT Transformation Error: %s " % current.xml.error)
982
983 return output
984
985
986 @staticmethod
988 """
989 XML Element tree import method
990
991 @param r: the S3Request method
992 @param attr: controller attributes
993 """
994
995 get_vars = r.get_vars
996
997
998 if "ignore_errors" in get_vars:
999 ignore_errors = True
1000 else:
1001 ignore_errors = False
1002
1003
1004 def findnames(get_vars, name):
1005 nlist = []
1006 if name in get_vars:
1007 names = get_vars[name]
1008 if isinstance(names, (list, tuple)):
1009 names = ",".join(names)
1010 names = names.split(",")
1011 for n in names:
1012 if n[0] == "(" and ")" in n[1:]:
1013 nlist.append(n[1:].split(")", 1))
1014 else:
1015 nlist.append([None, n])
1016 return nlist
1017 filenames = findnames(get_vars, "filename")
1018 fetchurls = findnames(get_vars, "fetchurl")
1019 source_url = None
1020
1021
1022 s3 = current.response.s3
1023 json_formats = s3.json_formats
1024 csv_formats = s3.csv_formats
1025 source = []
1026 representation = r.representation
1027 if representation in json_formats or representation in csv_formats:
1028 if filenames:
1029 try:
1030 for f in filenames:
1031 source.append((f[0], open(f[1], "rb")))
1032 except:
1033 source = []
1034 elif fetchurls:
1035 import urllib
1036 try:
1037 for u in fetchurls:
1038 source.append((u[0], urllib.urlopen(u[1])))
1039 except:
1040 source = []
1041 elif r.http != "GET":
1042 source = r.read_body()
1043 else:
1044 if filenames:
1045 source = filenames
1046 elif fetchurls:
1047 source = fetchurls
1048
1049 source_url = fetchurls[0][1]
1050 elif r.http != "GET":
1051 source = r.read_body()
1052 if not source:
1053 if filenames or fetchurls:
1054
1055 r.error(400, "Invalid source")
1056 else:
1057
1058 return r.get_struct(r, **attr)
1059
1060
1061 stylesheet = r.stylesheet(method="import")
1062
1063 if r.method == "create":
1064 _id = None
1065 else:
1066 _id = r.id
1067
1068
1069 if "xsltmode" in get_vars:
1070 args = dict(xsltmode=get_vars["xsltmode"])
1071 else:
1072 args = dict()
1073
1074
1075
1076 if source_url:
1077 args["source_url"] = source_url
1078
1079 if "data_field" in get_vars:
1080 args["data_field"] = get_vars["data_field"]
1081
1082 if "image_field" in get_vars:
1083 args["image_field"] = get_vars["image_field"]
1084
1085
1086 if representation in json_formats:
1087 representation = "json"
1088 elif representation in csv_formats:
1089 representation = "csv"
1090 else:
1091 representation = "xml"
1092
1093 try:
1094 output = r.resource.import_xml(source,
1095 id=_id,
1096 format=representation,
1097 files=r.files,
1098 stylesheet=stylesheet,
1099 ignore_errors=ignore_errors,
1100 **args)
1101 except IOError:
1102 current.auth.permission.fail()
1103 except SyntaxError:
1104 e = sys.exc_info()[1]
1105 if hasattr(e, "message"):
1106 e = e.message
1107 r.error(400, e)
1108
1109 return output
1110
1111
1112 @staticmethod
1114 """
1115 Resource structure introspection method
1116
1117 @param r: the S3Request instance
1118 @param attr: controller attributes
1119 """
1120
1121 response = current.response
1122 json_formats = response.s3.json_formats
1123 if r.representation in json_formats:
1124 as_json = True
1125 content_type = "application/json"
1126 else:
1127 as_json = False
1128 content_type = "text/xml"
1129 get_vars = r.get_vars
1130 meta = str(get_vars.get("meta", False)).lower() == "true"
1131 opts = str(get_vars.get("options", False)).lower() == "true"
1132 refs = str(get_vars.get("references", False)).lower() == "true"
1133 stylesheet = r.stylesheet()
1134 output = r.resource.export_struct(meta=meta,
1135 options=opts,
1136 references=refs,
1137 stylesheet=stylesheet,
1138 as_json=as_json)
1139 if output is None:
1140
1141 r.error(400, current.xml.error)
1142 response.headers["Content-Type"] = content_type
1143 return output
1144
1145
1146 @staticmethod
1148 """
1149 Resource structure introspection method (single table)
1150
1151 @param r: the S3Request instance
1152 @param attr: controller attributes
1153 """
1154
1155 representation = r.representation
1156 if representation == "xml":
1157 output = r.resource.export_fields(component=r.component_name)
1158 content_type = "text/xml"
1159 elif representation == "s3json":
1160 output = r.resource.export_fields(component=r.component_name,
1161 as_json=True)
1162 content_type = "application/json"
1163 else:
1164 r.error(415, current.ERROR.BAD_FORMAT)
1165 response = current.response
1166 response.headers["Content-Type"] = content_type
1167 return output
1168
1169
1170 @staticmethod
1172 """
1173 Field options introspection method (single table)
1174
1175 @param r: the S3Request instance
1176 @param attr: controller attributes
1177 """
1178
1179 get_vars = r.get_vars
1180
1181 items = get_vars.get("field")
1182 if items:
1183 if not isinstance(items, (list, tuple)):
1184 items = [items]
1185 fields = []
1186 add_fields = fields.extend
1187 for item in items:
1188 f = item.split(",")
1189 if f:
1190 add_fields(f)
1191 else:
1192 fields = None
1193
1194 if "hierarchy" in get_vars:
1195 hierarchy = get_vars["hierarchy"].lower() not in ("false", "0")
1196 else:
1197 hierarchy = False
1198
1199 if "only_last" in get_vars:
1200 only_last = get_vars["only_last"].lower() not in ("false", "0")
1201 else:
1202 only_last = False
1203
1204 if "show_uids" in get_vars:
1205 show_uids = get_vars["show_uids"].lower() not in ("false", "0")
1206 else:
1207 show_uids = False
1208
1209 representation = r.representation
1210 flat = False
1211 if representation == "xml":
1212 only_last = False
1213 as_json = False
1214 content_type = "text/xml"
1215 elif representation == "s3json":
1216 show_uids = False
1217 as_json = True
1218 content_type = "application/json"
1219 elif representation == "json" and fields and len(fields) == 1:
1220
1221
1222 flat = True
1223 show_uids = False
1224 as_json = True
1225 content_type = "application/json"
1226 else:
1227 r.error(415, current.ERROR.BAD_FORMAT)
1228
1229 component = r.component_name
1230 output = r.resource.export_options(component=component,
1231 fields=fields,
1232 show_uids=show_uids,
1233 only_last=only_last,
1234 hierarchy=hierarchy,
1235 as_json=as_json,
1236 )
1237
1238 if flat:
1239 s3json = json.loads(output)
1240 output = {}
1241 options = s3json.get("option")
1242 if options:
1243 for item in options:
1244 output[item.get("@value")] = item.get("$", "")
1245 output = json.dumps(output)
1246
1247 current.response.headers["Content-Type"] = content_type
1248 return output
1249
1250
1251
1252
1254 """
1255 Generate a new request for the same resource
1256
1257 @param args: arguments for request constructor
1258 """
1259
1260 return s3_request(r=self, **args)
1261
1262
1264 """
1265 Called upon S3Request.<key> - looks up the value for the <key>
1266 attribute. Falls back to current.request if the attribute is
1267 not defined in this S3Request.
1268
1269 @param key: the key to lookup
1270 """
1271
1272 if key in self.__dict__:
1273 return self.__dict__[key]
1274
1275 sentinel = object()
1276 value = getattr(current.request, key, sentinel)
1277 if value is sentinel:
1278 raise AttributeError
1279 return value
1280
1281
1298
1299
1301 """
1302 Determine whether to actuate a link or not
1303
1304 @param component_id: the component_id (if not self.component_id)
1305 """
1306
1307 if not component_id:
1308 component_id = self.component_id
1309 if self.component:
1310 single = component_id != None
1311 component = self.component
1312 if component.link:
1313 actuate = self.component.actuate
1314 if "linked" in self.get_vars:
1315 linked = self.get_vars.get("linked", False)
1316 linked = linked in ("true", "True")
1317 if linked:
1318 actuate = "replace"
1319 else:
1320 actuate = "hide"
1321 if actuate == "link":
1322 if self.method != "delete" and self.http != "DELETE":
1323 return single
1324 else:
1325 return not single
1326 elif actuate == "replace":
1327 return True
1328
1329
1330 else:
1331 return False
1332 else:
1333 return True
1334 else:
1335 return False
1336
1337
1338 @staticmethod
1340 """
1341 Action upon unauthorised request
1342 """
1343
1344 current.auth.permission.fail()
1345
1346
1347 - def error(self, status, message, tree=None, next=None):
1348 """
1349 Action upon error
1350
1351 @param status: HTTP status code
1352 @param message: the error message
1353 @param tree: the tree causing the error
1354 """
1355
1356 if self.representation == "html":
1357 current.session.error = message
1358 if next is not None:
1359 redirect(next)
1360 else:
1361 redirect(URL(r=self, f="index"))
1362 else:
1363 headers = {"Content-Type":"application/json"}
1364 current.log.error(message)
1365 raise HTTP(status,
1366 body=current.xml.json_message(success=False,
1367 statuscode=status,
1368 message=message,
1369 tree=tree),
1370 web2py_error=message,
1371 **headers)
1372
1373
1374 - def url(self,
1375 id=None,
1376 component=None,
1377 component_id=None,
1378 target=None,
1379 method=None,
1380 representation=None,
1381 vars=None,
1382 host=None):
1383 """
1384 Returns the URL of this request, use parameters to override
1385 current requests attributes:
1386
1387 - None to keep current attribute (default)
1388 - 0 or "" to set attribute to NONE
1389 - value to use explicit value
1390
1391 @param id: the master record ID
1392 @param component: the component name
1393 @param component_id: the component ID
1394 @param target: the target record ID (choose automatically)
1395 @param method: the URL method
1396 @param representation: the representation for the URL
1397 @param vars: the URL query variables
1398 @param host: string to force absolute URL with host (True means http_host)
1399
1400 Particular behavior:
1401 - changing the master record ID resets the component ID
1402 - removing the target record ID sets the method to None
1403 - removing the method sets the target record ID to None
1404 - [] as id will be replaced by the "[id]" wildcard
1405 """
1406
1407 if vars is None:
1408 vars = self.get_vars
1409 elif vars and isinstance(vars, str):
1410
1411
1412 vars = json.loads(vars.replace("'", "\""))
1413
1414 if "format" in vars:
1415 del vars["format"]
1416
1417 args = []
1418
1419 cname = self.component_name
1420
1421
1422 if target is not None:
1423 if cname and (component is None or component == cname):
1424 component_id = target
1425 else:
1426 id = target
1427
1428
1429 default_method = False
1430 if method is None:
1431 default_method = True
1432 method = self.method
1433 elif method == "":
1434
1435 if component_id is None:
1436 if self.component_id is not None:
1437 component_id = 0
1438 elif not self.component:
1439 if id is None:
1440 if self.id is not None:
1441 id = 0
1442 method = None
1443
1444
1445 if id is None:
1446 id = self.id
1447 elif id in (0, ""):
1448 id = None
1449 elif id in ([], "[id]", "*"):
1450 id = "[id]"
1451 component_id = 0
1452 elif str(id) != str(self.id):
1453 component_id = 0
1454
1455
1456 if component is None:
1457 component = cname
1458 elif component == "":
1459 component = None
1460 if cname and cname != component or not component:
1461 component_id = 0
1462
1463
1464 if component_id is None:
1465 component_id = self.component_id
1466 elif component_id == 0:
1467 component_id = None
1468 if self.component_id and default_method:
1469 method = None
1470
1471 if id is None and self.id and \
1472 (not component or not component_id) and default_method:
1473 method = None
1474
1475 if id:
1476 args.append(id)
1477 if component:
1478 args.append(component)
1479 if component_id:
1480 args.append(component_id)
1481 if method:
1482 args.append(method)
1483
1484
1485 if representation is None:
1486 representation = self.representation
1487 elif representation == "":
1488 representation = self.DEFAULT_REPRESENTATION
1489 f = self.function
1490 if not representation == self.DEFAULT_REPRESENTATION:
1491 if len(args) > 0:
1492 args[-1] = "%s.%s" % (args[-1], representation)
1493 else:
1494 f = "%s.%s" % (f, representation)
1495
1496 return URL(r=self,
1497 c=self.controller,
1498 f=f,
1499 args=args,
1500 vars=vars,
1501 host=host)
1502
1503
1505 """
1506 Get the target table of the current request
1507
1508 @return: a tuple of (prefix, name, table, tablename) of the target
1509 resource of this request
1510
1511 @todo: update for link table support
1512 """
1513
1514 component = self.component
1515 if component is not None:
1516 link = self.component.link
1517 if link and not self.actuate_link():
1518 return(link.prefix,
1519 link.name,
1520 link.table,
1521 link.tablename)
1522 return (component.prefix,
1523 component.name,
1524 component.table,
1525 component.tablename)
1526 else:
1527 return (self.prefix,
1528 self.name,
1529 self.table,
1530 self.tablename)
1531
1532
1533 - def stylesheet(self, method=None, skip_error=False):
1534 """
1535 Find the XSLT stylesheet for this request
1536
1537 @param method: "import" for data imports, else None
1538 @param skip_error: do not raise an HTTP error status
1539 if the stylesheet cannot be found
1540 """
1541
1542 stylesheet = None
1543 representation = self.representation
1544 if self.component:
1545 resourcename = self.component.name
1546 else:
1547 resourcename = self.name
1548
1549
1550 if representation == "xml":
1551 return stylesheet
1552
1553
1554 if "transform" in self.vars:
1555 return self.vars["transform"]
1556
1557
1558 extension = self.XSLT_EXTENSION
1559 filename = "%s.%s" % (resourcename, extension)
1560 if filename in self.post_vars:
1561 p = self.post_vars[filename]
1562 import cgi
1563 if isinstance(p, cgi.FieldStorage) and p.filename:
1564 stylesheet = p.file
1565 return stylesheet
1566
1567
1568 folder = self.folder
1569 path = self.XSLT_PATH
1570 if method != "import":
1571 method = "export"
1572 filename = "%s.%s" % (method, extension)
1573 stylesheet = os.path.join(folder, path, representation, filename)
1574 if not os.path.exists(stylesheet):
1575 if not skip_error:
1576 self.error(501, "%s: %s" % (current.ERROR.BAD_TEMPLATE,
1577 stylesheet))
1578 else:
1579 stylesheet = None
1580
1581 return stylesheet
1582
1583
1584 - def read_body(self):
1585 """
1586 Read data from request body
1587 """
1588
1589 self.files = Storage()
1590 content_type = self.env.get("content_type")
1591
1592 source = []
1593 if content_type and content_type.startswith("multipart/"):
1594 import cgi
1595 ext = ".%s" % self.representation
1596 post_vars = self.post_vars
1597 for v in post_vars:
1598 p = post_vars[v]
1599 if isinstance(p, cgi.FieldStorage) and p.filename:
1600 self.files[p.filename] = p.file
1601 if p.filename.endswith(ext):
1602 source.append((v, p.file))
1603 elif v.endswith(ext):
1604 if isinstance(p, cgi.FieldStorage):
1605 source.append((v, p.value))
1606 elif isinstance(p, basestring):
1607 source.append((v, StringIO(p)))
1608 else:
1609 s = self.body
1610 s.seek(0)
1611 source.append(s)
1612
1613 return source
1614
1615
1617 """
1618 Invoke the customization callback for a resource.
1619
1620 @param tablename: the tablename of the resource; if called
1621 without tablename it will invoke the callbacks
1622 for the target resources of this request:
1623 - master
1624 - active component
1625 - active link table
1626 (in this order)
1627
1628 Resource customization functions can be defined like:
1629
1630 def customise_resource_my_table(r, tablename):
1631
1632 current.s3db.configure(tablename,
1633 my_custom_setting = "example")
1634 return
1635
1636 settings.customise_resource_my_table = \
1637 customise_resource_my_table
1638
1639 @note: the hook itself can call r.customise_resource in order
1640 to cascade customizations as necessary
1641 @note: if a table is customised that is not currently loaded,
1642 then it will be loaded for this process
1643 """
1644
1645 if tablename is None:
1646 customise = self.customise_resource
1647
1648 customise(self.resource.tablename)
1649 component = self.component
1650 if component:
1651 customise(component.tablename)
1652 link = self.link
1653 if link:
1654 customise(link.tablename)
1655 else:
1656
1657
1658 db = current.db
1659 if tablename not in db:
1660 current.s3db.table(tablename)
1661 customise = current.deployment_settings.customise_resource(tablename)
1662 if customise:
1663 customise(self, tablename)
1664
1667 """
1668 REST Method Handler Base Class
1669
1670 Method handler classes should inherit from this class and
1671 implement the apply_method() method.
1672
1673 @note: instances of subclasses don't have any of the instance
1674 attributes available until they actually get invoked
1675 from a request - i.e. apply_method() should never be
1676 called directly.
1677 """
1678
1679
1680 - def __call__(self, r, method=None, widget_id=None, **attr):
1681 """
1682 Entry point for the REST interface
1683
1684 @param r: the S3Request
1685 @param method: the method established by the REST interface
1686 @param widget_id: widget ID
1687 @param attr: dict of parameters for the method handler
1688
1689 @return: output object to send to the view
1690 """
1691
1692
1693 self.request = r
1694
1695
1696 response = current.response
1697 self.download_url = response.s3.download_url
1698
1699
1700 self.next = None
1701
1702
1703 if method is not None:
1704 self.method = method
1705 else:
1706 self.method = r.method
1707
1708
1709 if r.component:
1710 component = r.component
1711 resource = component
1712 self.record_id = self._record_id(r)
1713 if not self.method:
1714 if component.multiple and not r.component_id:
1715 self.method = "list"
1716 else:
1717 self.method = "read"
1718 if component.link:
1719 actuate_link = r.actuate_link()
1720 if not actuate_link:
1721 resource = component.link
1722 else:
1723 self.record_id = r.id
1724 resource = r.resource
1725 if not self.method:
1726 if r.id or r.method in ("read", "display"):
1727 self.method = "read"
1728 else:
1729 self.method = "list"
1730
1731 self.prefix = resource.prefix
1732 self.name = resource.name
1733 self.tablename = resource.tablename
1734 self.table = resource.table
1735 self.resource = resource
1736
1737 if self.method == "_init":
1738 return None
1739
1740 if r.interactive:
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754 hide_filter = attr.get("hide_filter")
1755 if isinstance(hide_filter, dict):
1756 component_name = r.component_name
1757 if component_name in hide_filter:
1758 hide_filter = hide_filter[component_name]
1759 elif "_default" in hide_filter:
1760 hide_filter = hide_filter["_default"]
1761 else:
1762 hide_filter = None
1763 if hide_filter is None:
1764 hide_filter = r.component is not None
1765 self.hide_filter = hide_filter
1766 else:
1767 self.hide_filter = True
1768
1769
1770 if widget_id and hasattr(self, "widget"):
1771 output = self.widget(r,
1772 method=self.method,
1773 widget_id=widget_id,
1774 **attr)
1775 else:
1776 output = self.apply_method(r, **attr)
1777
1778
1779 if self.next and resource.lastid:
1780 self.next = str(self.next)
1781 placeholder = "%5Bid%5D"
1782 self.next = self.next.replace(placeholder, resource.lastid)
1783 placeholder = "[id]"
1784 self.next = self.next.replace(placeholder, resource.lastid)
1785 if not response.error:
1786 r.next = self.next
1787
1788
1789 self._extend_view(output, r, **attr)
1790
1791 return output
1792
1793
1795 """
1796 Stub, to be implemented in subclass. This method is used
1797 to get the results as a standalone page.
1798
1799 @param r: the S3Request
1800 @param attr: dictionary of parameters for the method handler
1801
1802 @return: output object to send to the view
1803 """
1804
1805 output = dict()
1806 return output
1807
1808
1852
1853
1854
1855
1857 """
1858 Check permission for the requested resource
1859
1860 @param method: method to check, defaults to the actually
1861 requested method
1862 """
1863
1864 auth = current.auth
1865 has_permission = auth.s3_has_permission
1866
1867 r = self.request
1868
1869 if not method:
1870 method = self.method
1871 if method in ("list", "datatable", "datalist"):
1872
1873 method = "read"
1874
1875 if r.component is None:
1876 table = r.table
1877 record_id = r.id
1878 else:
1879 table = r.component.table
1880 record_id = r.component_id
1881
1882 if method == "create":
1883
1884
1885 master_access = has_permission("update",
1886 r.table,
1887 record_id=r.id)
1888
1889 if not master_access:
1890 return False
1891
1892 return has_permission(method, table, record_id=record_id)
1893
1894
1895 @staticmethod
1931
1932
1933 - def _config(self, key, default=None):
1934 """
1935 Get a configuration setting of the current table
1936
1937 @param key: the setting key
1938 @param default: the default value
1939 """
1940
1941 return current.s3db.get_config(self.tablename, key, default)
1942
1943
1944 @staticmethod
1946 """
1947 Get the path to the view template
1948
1949 @param r: the S3Request
1950 @param default: name of the default view template
1951 """
1952
1953 folder = r.folder
1954 prefix = r.controller
1955
1956 exists = os.path.exists
1957 join = os.path.join
1958
1959 settings = current.deployment_settings
1960 theme = settings.get_theme()
1961 theme_layouts = settings.get_theme_layouts()
1962
1963 if theme != "default":
1964
1965 view = join(folder, "modules", "templates", theme_layouts, "views",
1966 "%s_%s_%s" % (prefix, r.name, default))
1967 if exists(view):
1968
1969
1970
1971 return open(view, "rb")
1972 else:
1973 if "/" in default:
1974 subfolder, default_ = default.split("/", 1)
1975 else:
1976 subfolder = ""
1977 default_ = default
1978 if exists(join(folder, "modules", "templates", theme_layouts, "views",
1979 subfolder, "_%s" % default_)):
1980
1981
1982 if subfolder:
1983 subfolder = "%s/" % subfolder
1984
1985 current.response.s3.views[default] = \
1986 "../modules/templates/%s/views/%s_%s" % (theme_layouts,
1987 subfolder,
1988 default_,
1989 )
1990
1991 if r.component:
1992 view = "%s_%s_%s" % (r.name, r.component_name, default)
1993 path = join(folder, "views", prefix, view)
1994 if exists(path):
1995 return "%s/%s" % (prefix, view)
1996 else:
1997 view = "%s_%s" % (r.name, default)
1998 path = join(folder, "views", prefix, view)
1999 else:
2000 view = "%s_%s" % (r.name, default)
2001 path = join(folder, "views", prefix, view)
2002
2003 if exists(path):
2004 return "%s/%s" % (prefix, view)
2005 else:
2006 return default
2007
2008
2009 @staticmethod
2011 """
2012 Add additional view variables (invokes all callables)
2013
2014 @param output: the output dict
2015 @param r: the S3Request
2016 @param attr: the view variables (e.g. 'rheader')
2017
2018 @note: overload this method in subclasses if you don't want
2019 additional view variables to be added automatically
2020 """
2021
2022 if r.interactive and isinstance(output, dict):
2023 for key in attr:
2024 handler = attr[key]
2025 if callable(handler):
2026 resolve = True
2027 try:
2028 display = handler(r)
2029 except TypeError:
2030
2031
2032 display = handler
2033 continue
2034 except:
2035
2036 raise
2037 else:
2038 resolve = False
2039 display = handler
2040 if isinstance(display, dict) and resolve:
2041 output.update(**display)
2042 elif display is not None:
2043 output[key] = display
2044 elif key in output and callable(handler):
2045 del output[key]
2046
2047
2048 @staticmethod
2050 """
2051 Remove all filters from URL vars
2052
2053 @param get_vars: the URL vars as dict
2054 """
2055
2056 return Storage((k, v) for k, v in get_vars.iteritems()
2057 if not REGEX_FILTER.match(k))
2058
2059
2060 @staticmethod
2062 """
2063 Get a CRUD info string for interactive pages
2064
2065 @param tablename: the table name
2066 @param name: the name of the CRUD string
2067 """
2068
2069 crud_strings = current.response.s3.crud_strings
2070
2071 _crud_strings = crud_strings.get(tablename, crud_strings)
2072 return _crud_strings.get(name,
2073
2074 crud_strings.get(name))
2075
2080 """
2081 Helper function to generate S3Request instances
2082
2083 @param args: arguments for the S3Request
2084 @param kwargs: keyword arguments for the S3Request
2085
2086 @keyword catch_errors: if set to False, errors will be raised
2087 instead of returned to the client, useful
2088 for optional sub-requests, or if the caller
2089 implements fallbacks
2090 """
2091
2092 error = None
2093 try:
2094 r = S3Request(*args, **kwargs)
2095 except (AttributeError, SyntaxError):
2096 if kwargs.get("catch_errors") is False:
2097 raise
2098 error = 400
2099 except KeyError:
2100 if kwargs.get("catch_errors") is False:
2101 raise
2102 error = 404
2103 if error:
2104 message = sys.exc_info()[1]
2105 if hasattr(message, "message"):
2106 message = message.message
2107 if current.auth.permission.format == "html":
2108 current.session.error = message
2109 redirect(URL(f="index"))
2110 else:
2111 headers = {"Content-Type":"application/json"}
2112 current.log.error(message)
2113 raise HTTP(error,
2114 body=current.xml.json_message(success=False,
2115 statuscode=error,
2116 message=message,
2117 ),
2118 web2py_error=message,
2119 **headers)
2120 return r
2121
2122
2123