summaryrefslogtreecommitdiffstats
path: root/bitbake/lib
diff options
context:
space:
mode:
authorAlexandru DAMIAN <alexandru.damian@intel.com>2014-11-11 17:01:09 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2014-11-12 17:04:50 +0000
commitc5d19aae55be158676eb0914bd5d0701f7d3fd3a (patch)
treeb549631196198eaa89a922c1088243b25c74ecd9 /bitbake/lib
parent326d5b1a284ca4d29f986d3d6a1cee838b841301 (diff)
downloadpoky-c5d19aae55be158676eb0914bd5d0701f7d3fd3a.tar.gz
bitbake: toastergui: fix XSS injection points in projects page
We close XSS injection points in Projects page. * modify the json filter to properly escape HTML tags in strings * enable $sanitize to automatically sanitize dangerous HTML in user-supplied input * clean dangerous characters in targets field, as that field contents will be directly passed to a shell command Based on the vulnerability discovered and the patch provided by Michael Wood. (Bitbake rev: 23c440db9c076ca37e651bdbbdbefee54998e1dc) Signed-off-by: Alexandru DAMIAN <alexandru.damian@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib')
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/projectapp.js7
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project.html16
-rw-r--r--bitbake/lib/toaster/toastergui/templatetags/projecttags.py6
-rwxr-xr-xbitbake/lib/toaster/toastergui/views.py48
4 files changed, 43 insertions, 34 deletions
diff --git a/bitbake/lib/toaster/toastergui/static/js/projectapp.js b/bitbake/lib/toaster/toastergui/static/js/projectapp.js
index f0569de04d..9f9a06476a 100644
--- a/bitbake/lib/toaster/toastergui/static/js/projectapp.js
+++ b/bitbake/lib/toaster/toastergui/static/js/projectapp.js
@@ -101,7 +101,7 @@ function _diffArrays(existingArray, newArray, compareElements, onAdded, onDelete
101} 101}
102 102
103 103
104var projectApp = angular.module('project', ['ngCookies', 'ngAnimate', 'ui.bootstrap' ], angular_formpost); 104var projectApp = angular.module('project', ['ngCookies', 'ngAnimate', 'ui.bootstrap', 'ngRoute', 'ngSanitize'], angular_formpost);
105 105
106// modify the template tag markers to prevent conflicts with Django 106// modify the template tag markers to prevent conflicts with Django
107projectApp.config(function($interpolateProvider) { 107projectApp.config(function($interpolateProvider) {
@@ -128,7 +128,7 @@ projectApp.filter('timediff', function() {
128 128
129 129
130// main controller for the project page 130// main controller for the project page
131projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $location, $cookies, $q, $sce, $anchorScroll, $animate) { 131projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $location, $cookies, $q, $sce, $anchorScroll, $animate, $sanitize) {
132 132
133 $scope.getSuggestions = function(type, currentValue) { 133 $scope.getSuggestions = function(type, currentValue) {
134 var deffered = $q.defer(); 134 var deffered = $q.defer();
@@ -475,6 +475,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
475 var alertText = undefined; 475 var alertText = undefined;
476 var alertZone = undefined; 476 var alertZone = undefined;
477 var oldLayers = []; 477 var oldLayers = [];
478
478 switch(elementid) { 479 switch(elementid) {
479 case '#select-machine': 480 case '#select-machine':
480 alertText = "You have changed the machine to: <strong>" + $scope.machineName + "</strong>"; 481 alertText = "You have changed the machine to: <strong>" + $scope.machineName + "</strong>";
@@ -594,7 +595,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc
594 var crtid = zone.maxid ++; 595 var crtid = zone.maxid ++;
595 angular.forEach(zone, function (o) { o.close() }); 596 angular.forEach(zone, function (o) { o.close() });
596 o = { 597 o = {
597 id: crtid, text: $sce.trustAsHtml(text), type: type, 598 id: crtid, text: text, type: type,
598 close: function() { 599 close: function() {
599 zone.splice((function(id){ for (var i = 0; i < zone.length; i++) if (id == zone[i].id) { return i}; return undefined;})(crtid), 1); 600 zone.splice((function(id){ for (var i = 0; i < zone.length; i++) if (id == zone[i].id) { return i}; return undefined;})(crtid), 1);
600 }, 601 },
diff --git a/bitbake/lib/toaster/toastergui/templates/project.html b/bitbake/lib/toaster/toastergui/templates/project.html
index 38863a3ac6..4e8a7e29aa 100644
--- a/bitbake/lib/toaster/toastergui/templates/project.html
+++ b/bitbake/lib/toaster/toastergui/templates/project.html
@@ -11,6 +11,8 @@ vim: expandtab tabstop=2
11<script src="{% static "js/angular.min.js" %}"></script> 11<script src="{% static "js/angular.min.js" %}"></script>
12<script src="{% static "js/angular-animate.min.js" %}"></script> 12<script src="{% static "js/angular-animate.min.js" %}"></script>
13<script src="{% static "js/angular-cookies.min.js" %}"></script> 13<script src="{% static "js/angular-cookies.min.js" %}"></script>
14<script src="{% static "js/angular-route.min.js" %}"></script>
15<script src="{% static "js/angular-sanitize.min.js" %}"></script>
14<script src="{% static "js/ui-bootstrap-tpls-0.11.0.js" %}"></script> 16<script src="{% static "js/ui-bootstrap-tpls-0.11.0.js" %}"></script>
15 17
16 18
@@ -365,13 +367,13 @@ angular.element(document).ready(function() {
365 scope.urls.layers = "{% url 'layers' %}"; 367 scope.urls.layers = "{% url 'layers' %}";
366 scope.urls.targets = "{% url 'targets' %}"; 368 scope.urls.targets = "{% url 'targets' %}";
367 scope.urls.importlayer = "{% url 'importlayer'%}" 369 scope.urls.importlayer = "{% url 'importlayer'%}"
368 scope.project = {{prj|safe}}; 370 scope.project = {{prj|json}};
369 scope.builds = {{builds|safe}}; 371 scope.builds = {{builds|json}};
370 scope.layers = {{layers|safe}}; 372 scope.layers = {{layers|json}};
371 scope.targets = {{targets|safe}}; 373 scope.targets = {{targets|json}};
372 scope.frequenttargets = {{freqtargets|safe}}; 374 scope.frequenttargets = {{freqtargets|json}};
373 scope.machine = {{machine|safe}}; 375 scope.machine = {{machine|json}};
374 scope.releases = {{releases|safe}}; 376 scope.releases = {{releases|json}};
375 377
376 var now = (new Date()).getTime(); 378 var now = (new Date()).getTime();
377 scope.todaydate = now - (now % 86400000); 379 scope.todaydate = now - (now % 86400000);
diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
index 4a97eb7ac4..99fd4cf287 100644
--- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
+++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
@@ -25,6 +25,7 @@ from django import template
25from django.utils import timezone 25from django.utils import timezone
26from django.template.defaultfilters import filesizeformat 26from django.template.defaultfilters import filesizeformat
27import json as JsonLib 27import json as JsonLib
28from django.utils.safestring import mark_safe
28 29
29register = template.Library() 30register = template.Library()
30 31
@@ -49,7 +50,10 @@ def mapselect(value, argument):
49 50
50@register.filter(name = "json") 51@register.filter(name = "json")
51def json(value): 52def json(value):
52 return JsonLib.dumps(value) 53 # JSON spec says that "\/" is functionally identical to "/" to allow for HTML-tag embedding in JSON strings
54 # unfortunately, I can't find any option in the json module to turn on forward-slash escaping, so we do
55 # it manually here
56 return mark_safe(JsonLib.dumps(value, ensure_ascii=False).replace('</', '<\\/'))
53 57
54@register.assignment_tag 58@register.assignment_tag
55def query(qs, **kwargs): 59def query(qs, **kwargs):
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 3b29dbcfb2..5e92c24a8d 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -36,6 +36,7 @@ from django.utils import timezone
36from django.utils.html import escape 36from django.utils.html import escape
37from datetime import timedelta 37from datetime import timedelta
38from django.utils import formats 38from django.utils import formats
39from toastergui.templatetags.projecttags import json as jsonfilter
39import json 40import json
40 41
41 42
@@ -871,7 +872,7 @@ def _get_dir_entries(build_id, target_id, start):
871 # sort by directories first, then by name 872 # sort by directories first, then by name
872 rsorted = sorted(response, key=lambda entry : entry['name']) 873 rsorted = sorted(response, key=lambda entry : entry['name'])
873 rsorted = sorted(rsorted, key=lambda entry : entry['isdir'], reverse=True) 874 rsorted = sorted(rsorted, key=lambda entry : entry['isdir'], reverse=True)
874 return json.dumps(rsorted, cls=LazyEncoder) 875 return json.dumps(rsorted, cls=LazyEncoder).replace('</', '<\\/')
875 876
876def dirinfo(request, build_id, target_id, file_path=None): 877def dirinfo(request, build_id, target_id, file_path=None):
877 template = "dirinfo.html" 878 template = "dirinfo.html"
@@ -1981,25 +1982,25 @@ if toastermain.settings.MANAGED:
1981 context = { 1982 context = {
1982 "project" : prj, 1983 "project" : prj,
1983 "completedbuilds": Build.objects.filter(project = prj).exclude(outcome = Build.IN_PROGRESS), 1984 "completedbuilds": Build.objects.filter(project = prj).exclude(outcome = Build.IN_PROGRESS),
1984 "prj" : json.dumps({"name": prj.name, "release": { "id": prj.release.pk, "name": prj.release.name, "desc": prj.release.description}}), 1985 "prj" : {"name": prj.name, "release": { "id": prj.release.pk, "name": prj.release.name, "desc": prj.release.description}},
1985 #"buildrequests" : prj.buildrequest_set.filter(state=BuildRequest.REQ_QUEUED), 1986 #"buildrequests" : prj.buildrequest_set.filter(state=BuildRequest.REQ_QUEUED),
1986 "builds" : json.dumps(_project_recent_build_list(prj)), 1987 "builds" : _project_recent_build_list(prj),
1987 "layers" : json.dumps(map(lambda x: { 1988 "layers" : map(lambda x: {
1988 "id": x.layercommit.pk, 1989 "id": x.layercommit.pk,
1989 "orderid": x.pk, 1990 "orderid": x.pk,
1990 "name" : x.layercommit.layer.name, 1991 "name" : x.layercommit.layer.name,
1991 "url": x.layercommit.layer.layer_index_url, 1992 "url": x.layercommit.layer.layer_index_url,
1992 "layerdetailurl": reverse("layerdetails", args=(x.layercommit.layer.pk,)), 1993 "layerdetailurl": reverse("layerdetails", args=(x.layercommit.layer.pk,)),
1993 "branch" : { "name" : x.layercommit.up_branch.name, "layersource" : x.layercommit.up_branch.layer_source.name}}, 1994 "branch" : { "name" : x.layercommit.up_branch.name, "layersource" : x.layercommit.up_branch.layer_source.name}},
1994 prj.projectlayer_set.all().order_by("id"))), 1995 prj.projectlayer_set.all().order_by("id")),
1995 "targets" : json.dumps(map(lambda x: {"target" : x.target, "task" : x.task, "pk": x.pk}, prj.projecttarget_set.all())), 1996 "targets" : map(lambda x: {"target" : x.target, "task" : x.task, "pk": x.pk}, prj.projecttarget_set.all()),
1996 "freqtargets": json.dumps(freqtargets), 1997 "freqtargets": freqtargets,
1997 "releases": json.dumps(map(lambda x: {"id": x.pk, "name": x.name}, Release.objects.all())), 1998 "releases": map(lambda x: {"id": x.pk, "name": x.name}, Release.objects.all()),
1998 } 1999 }
1999 try: 2000 try:
2000 context["machine"] = json.dumps({"name": prj.projectvariable_set.get(name="MACHINE").value}) 2001 context["machine"] = {"name": prj.projectvariable_set.get(name="MACHINE").value}
2001 except ProjectVariable.DoesNotExist: 2002 except ProjectVariable.DoesNotExist:
2002 context["machine"] = json.dumps(None) 2003 context["machine"] = None
2003 try: 2004 try:
2004 context["distro"] = prj.projectvariable_set.get(name="DISTRO").value 2005 context["distro"] = prj.projectvariable_set.get(name="DISTRO").value
2005 except ProjectVariable.DoesNotExist: 2006 except ProjectVariable.DoesNotExist:
@@ -2035,7 +2036,8 @@ if toastermain.settings.MANAGED:
2035 2036
2036 if 'targets' in request.POST: 2037 if 'targets' in request.POST:
2037 ProjectTarget.objects.filter(project = prj).delete() 2038 ProjectTarget.objects.filter(project = prj).delete()
2038 for t in request.POST['targets'].strip().split(" "): 2039 s = str(request.POST['targets'])
2040 for t in s.translate(None, ";%|\"").split(" "):
2039 if ":" in t: 2041 if ":" in t:
2040 target, task = t.split(":") 2042 target, task = t.split(":")
2041 else: 2043 else:
@@ -2045,11 +2047,11 @@ if toastermain.settings.MANAGED:
2045 2047
2046 br = prj.schedule_build() 2048 br = prj.schedule_build()
2047 2049
2048 return HttpResponse(json.dumps({"error":"ok", 2050 return HttpResponse(jsonfilter({"error":"ok",
2049 "builds" : _project_recent_build_list(prj), 2051 "builds" : _project_recent_build_list(prj),
2050 }), content_type = "application/json") 2052 }), content_type = "application/json")
2051 except Exception as e: 2053 except Exception as e:
2052 return HttpResponse(json.dumps({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json") 2054 return HttpResponse(jsonfilter({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json")
2053 2055
2054 def xhr_projectedit(request, pid): 2056 def xhr_projectedit(request, pid):
2055 try: 2057 try:
@@ -2088,7 +2090,7 @@ if toastermain.settings.MANAGED:
2088 machinevar.save() 2090 machinevar.save()
2089 2091
2090 # return all project settings 2092 # return all project settings
2091 return HttpResponse(json.dumps( { 2093 return HttpResponse(jsonfilter( {
2092 "error": "ok", 2094 "error": "ok",
2093 "layers" : map(lambda x: {"id": x.layercommit.pk, "orderid" : x.pk, "name" : x.layercommit.layer.name, "url": x.layercommit.layer.layer_index_url, "layerdetailurl": reverse("layerdetails", args=(x.layercommit.layer.pk,)), "branch" : { "name" : x.layercommit.up_branch.name, "layersource" : x.layercommit.up_branch.layer_source.name}}, prj.projectlayer_set.all().order_by("id")), 2095 "layers" : map(lambda x: {"id": x.layercommit.pk, "orderid" : x.pk, "name" : x.layercommit.layer.name, "url": x.layercommit.layer.layer_index_url, "layerdetailurl": reverse("layerdetails", args=(x.layercommit.layer.pk,)), "branch" : { "name" : x.layercommit.up_branch.name, "layersource" : x.layercommit.up_branch.layer_source.name}}, prj.projectlayer_set.all().order_by("id")),
2094 "builds" : _project_recent_build_list(prj), 2096 "builds" : _project_recent_build_list(prj),
@@ -2098,7 +2100,7 @@ if toastermain.settings.MANAGED:
2098 }), content_type = "application/json") 2100 }), content_type = "application/json")
2099 2101
2100 except Exception as e: 2102 except Exception as e:
2101 return HttpResponse(json.dumps({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json") 2103 return HttpResponse(jsonfilter({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json")
2102 2104
2103 2105
2104 from django.views.decorators.csrf import csrf_exempt 2106 from django.views.decorators.csrf import csrf_exempt
@@ -2112,7 +2114,7 @@ if toastermain.settings.MANAGED:
2112 prj = Project.objects.get(pk = request.session['project_id']) 2114 prj = Project.objects.get(pk = request.session['project_id'])
2113 queryset_all = queryset_all.filter(up_branch__release = prj.release).exclude(pk__in = map(lambda x: x.layercommit_id, prj.projectlayer_set.all())) 2115 queryset_all = queryset_all.filter(up_branch__release = prj.release).exclude(pk__in = map(lambda x: x.layercommit_id, prj.projectlayer_set.all()))
2114 queryset_all = queryset_all.filter(layer__name__icontains=request.GET.get('value','')) 2116 queryset_all = queryset_all.filter(layer__name__icontains=request.GET.get('value',''))
2115 return HttpResponse(json.dumps( { "error":"ok", 2117 return HttpResponse(jsonfilter( { "error":"ok",
2116 "list" : map( lambda x: {"id": x.pk, "name": x.layer.name, "detail": "(" + x.layer.layer_source.name + (")" if x.up_branch == None else " | "+x.up_branch.name+")")}, 2118 "list" : map( lambda x: {"id": x.pk, "name": x.layer.name, "detail": "(" + x.layer.layer_source.name + (")" if x.up_branch == None else " | "+x.up_branch.name+")")},
2117 queryset_all[:8]) 2119 queryset_all[:8])
2118 }), content_type = "application/json") 2120 }), content_type = "application/json")
@@ -2127,7 +2129,7 @@ if toastermain.settings.MANAGED:
2127 2129
2128 queryset_all.order_by("-up_id"); 2130 queryset_all.order_by("-up_id");
2129 2131
2130 return HttpResponse(json.dumps( { "error":"ok", 2132 return HttpResponse(jsonfilter( { "error":"ok",
2131 "list" : map( 2133 "list" : map(
2132 lambda x: {"id": x.pk, "name": x.layer.name, "detail": "(" + x.layer.layer_source.name + (")" if x.up_branch == None else " | "+x.up_branch.name+")"), 2134 lambda x: {"id": x.pk, "name": x.layer.name, "detail": "(" + x.layer.layer_source.name + (")" if x.up_branch == None else " | "+x.up_branch.name+")"),
2133 "layerdetailurl" : reverse('layerdetails', args=(x.pk,))}, 2135 "layerdetailurl" : reverse('layerdetails', args=(x.pk,))},
@@ -2146,7 +2148,7 @@ if toastermain.settings.MANAGED:
2146 if lv.count() != 1: # there is no layer_version with the new release id, and the same name 2148 if lv.count() != 1: # there is no layer_version with the new release id, and the same name
2147 retval.append(i) 2149 retval.append(i)
2148 2150
2149 return HttpResponse(json.dumps( {"error":"ok", 2151 return HttpResponse(jsonfilter( {"error":"ok",
2150 "list": map( 2152 "list": map(
2151 lambda x: {"id": x.layercommit.pk, "name": x.layercommit.layer.name, "detail": "(" + x.layercommit.layer.layer_source.name + (")" if x.layercommit.up_branch == None else " | "+x.layercommit.up_branch.name+")")}, 2153 lambda x: {"id": x.layercommit.pk, "name": x.layercommit.layer.name, "detail": "(" + x.layercommit.layer.layer_source.name + (")" if x.layercommit.up_branch == None else " | "+x.layercommit.up_branch.name+")")},
2152 retval) }), content_type = "application/json") 2154 retval) }), content_type = "application/json")
@@ -2156,7 +2158,7 @@ if toastermain.settings.MANAGED:
2156 queryset_all = Recipe.objects.all() 2158 queryset_all = Recipe.objects.all()
2157 if 'project_id' in request.session: 2159 if 'project_id' in request.session:
2158 queryset_all = queryset_all.filter(layer_version__layer__in = map(lambda x: x.layercommit.layer, ProjectLayer.objects.filter(project_id=request.session['project_id']))) 2160 queryset_all = queryset_all.filter(layer_version__layer__in = map(lambda x: x.layercommit.layer, ProjectLayer.objects.filter(project_id=request.session['project_id'])))
2159 return HttpResponse(json.dumps({ "error":"ok", 2161 return HttpResponse(jsonfilter({ "error":"ok",
2160 "list" : map ( lambda x: {"id": x.pk, "name": x.name, "detail":"[" + x.layer_version.layer.name+ (" | " + x.layer_version.up_branch.name + "]" if x.layer_version.up_branch is not None else "]")}, 2162 "list" : map ( lambda x: {"id": x.pk, "name": x.name, "detail":"[" + x.layer_version.layer.name+ (" | " + x.layer_version.up_branch.name + "]" if x.layer_version.up_branch is not None else "]")},
2161 queryset_all.filter(name__icontains=request.GET.get('value',''))[:8]), 2163 queryset_all.filter(name__icontains=request.GET.get('value',''))[:8]),
2162 2164
@@ -2166,7 +2168,7 @@ if toastermain.settings.MANAGED:
2166 queryset_all = Machine.objects.all() 2168 queryset_all = Machine.objects.all()
2167 if 'project_id' in request.session: 2169 if 'project_id' in request.session:
2168 queryset_all = queryset_all.filter(layer_version__layer__in = map(lambda x: x.layercommit.layer, ProjectLayer.objects.filter(project_id=request.session['project_id']))) 2170 queryset_all = queryset_all.filter(layer_version__layer__in = map(lambda x: x.layercommit.layer, ProjectLayer.objects.filter(project_id=request.session['project_id'])))
2169 return HttpResponse(json.dumps({ "error":"ok", 2171 return HttpResponse(jsonfilter({ "error":"ok",
2170 "list" : map ( lambda x: {"id": x.pk, "name": x.name, "detail":"[" + x.layer_version.layer.name+ (" | " + x.layer_version.up_branch.name + "]" if x.layer_version.up_branch is not None else "]")}, 2172 "list" : map ( lambda x: {"id": x.pk, "name": x.name, "detail":"[" + x.layer_version.layer.name+ (" | " + x.layer_version.up_branch.name + "]" if x.layer_version.up_branch is not None else "]")},
2171 queryset_all.filter(name__icontains=request.GET.get('value',''))[:8]), 2173 queryset_all.filter(name__icontains=request.GET.get('value',''))[:8]),
2172 2174
@@ -2174,7 +2176,7 @@ if toastermain.settings.MANAGED:
2174 2176
2175 raise Exception("Unknown request! " + request.GET.get('type', "No parameter supplied")) 2177 raise Exception("Unknown request! " + request.GET.get('type', "No parameter supplied"))
2176 except Exception as e: 2178 except Exception as e:
2177 return HttpResponse(json.dumps({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json") 2179 return HttpResponse(jsonfilter({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json")
2178 2180
2179 2181
2180 2182
@@ -2216,7 +2218,7 @@ if toastermain.settings.MANAGED:
2216 2218
2217 2219
2218 context = { 2220 context = {
2219 'projectlayerset' : json.dumps(map(lambda x: x.layercommit.id, prj.projectlayer_set.all())), 2221 'projectlayerset' : jsonfilter(map(lambda x: x.layercommit.id, prj.projectlayer_set.all())),
2220 'objects' : layer_info, 2222 'objects' : layer_info,
2221 'objectname' : "layers", 2223 'objectname' : "layers",
2222 'default_orderby' : 'layer__name:+', 2224 'default_orderby' : 'layer__name:+',
@@ -2309,7 +2311,7 @@ if toastermain.settings.MANAGED:
2309 2311
2310 2312
2311 context = { 2313 context = {
2312 'projectlayerset' : json.dumps(map(lambda x: x.layercommit.id, prj.projectlayer_set.all())), 2314 'projectlayerset' : jsonfilter(map(lambda x: x.layercommit.id, prj.projectlayer_set.all())),
2313 'objects' : target_info, 2315 'objects' : target_info,
2314 'objectname' : "targets", 2316 'objectname' : "targets",
2315 'default_orderby' : 'name:+', 2317 'default_orderby' : 'name:+',