You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2015/11/09 23:43:51 UTC
allura git commit: [#5694] limit_param_max config option;
enforce limit checks in missing places
Repository: allura
Updated Branches:
refs/heads/db/5694 [created] 7282b402a
[#5694] limit_param_max config option; enforce limit checks in missing places
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/7282b402
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/7282b402
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/7282b402
Branch: refs/heads/db/5694
Commit: 7282b402ae5b84c8f8cab1500e954211bd98cda0
Parents: 9fa2bc0
Author: Dave Brondsema <da...@brondsema.net>
Authored: Mon Nov 9 17:36:54 2015 -0500
Committer: Dave Brondsema <da...@brondsema.net>
Committed: Mon Nov 9 17:43:43 2015 -0500
----------------------------------------------------------------------
Allura/allura/controllers/discuss.py | 14 ++++++--------
Allura/allura/controllers/repository.py | 2 ++
Allura/allura/lib/app_globals.py | 2 ++
Allura/allura/lib/helpers.py | 7 ++++---
Allura/allura/model/artifact.py | 14 ++++++--------
Allura/allura/model/discuss.py | 8 +++++---
Allura/allura/model/project.py | 5 ++---
Allura/allura/tests/test_globals.py | 2 ++
Allura/allura/tests/test_helpers.py | 5 ++++-
Allura/development.ini | 3 +++
Allura/docs/api-rest/api.raml | 2 +-
Allura/docs/api-rest/traits.yaml | 2 +-
ForgeActivity/forgeactivity/main.py | 4 ++--
ForgeBlog/forgeblog/main.py | 4 ++--
ForgeBlog/forgeblog/model/blog.py | 4 ++--
ForgeDiscussion/forgediscussion/controllers/root.py | 7 +++----
ForgeSVN/forgesvn/svn_main.py | 6 +-----
ForgeTracker/forgetracker/model/ticket.py | 7 +++++--
.../forgetracker/tests/functional/test_rest.py | 2 +-
ForgeTracker/forgetracker/tracker_main.py | 2 +-
ForgeWiki/forgewiki/model/wiki.py | 4 ++--
ForgeWiki/forgewiki/wiki_main.py | 2 +-
22 files changed, 58 insertions(+), 50 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/controllers/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/discuss.py b/Allura/allura/controllers/discuss.py
index 459caf7..4c0c748 100644
--- a/Allura/allura/controllers/discuss.py
+++ b/Allura/allura/controllers/discuss.py
@@ -440,11 +440,8 @@ class ModerationController(BaseController):
query['flags'] = {'$gte': int(flag)}
q = self.PostModel.query.find(query)
count = q.count()
- if not page:
- page = 0
- page = int(page)
- limit = int(limit)
- q = q.skip(page)
+ limit, page, start = g.handle_paging(limit, page or 0, default=50)
+ q = q.skip(start)
q = q.limit(limit)
pgnum = (page // limit) + 1
pages = (count // limit) + 1
@@ -486,7 +483,7 @@ class PostRestController(PostController):
@expose('json:')
def index(self, **kw):
- return dict(post=self.post)
+ return dict(post=self.post.__json__())
@h.vardec
@expose()
@@ -503,8 +500,9 @@ class PostRestController(PostController):
class ThreadRestController(ThreadController):
@expose('json:')
- def index(self, **kw):
- return dict(thread=self.thread)
+ def index(self, limit=25, page=None, **kw):
+ limit, page = h.paging_sanitizer(limit, page)
+ return dict(thread=self.thread.__json__(limit=limit, page=page))
@h.vardec
@expose()
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/controllers/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index b51d3c7..85caa59 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -367,6 +367,7 @@ class MergeRequestController(object):
c.thread = self.thread_widget
c.log_widget = self.log_widget
c.mr_dispose_form = self.mr_dispose_form
+ limit, page = h.paging_sanitizer(limit, page)
with self.req.push_downstream_context():
downstream_app = c.app
result = dict(
@@ -613,6 +614,7 @@ class CommitBrowser(BaseController):
is_file = False
if path:
is_file = c.app.repo.is_file(path, self._commit._id)
+ limit, _ = h.paging_sanitizer(limit, 0)
commits = list(islice(c.app.repo.log(
revs=self._commit._id,
path=path,
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index c6b52d3..d63c93f 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -383,6 +383,8 @@ class Globals(object):
def handle_paging(self, limit, page, default=25):
limit = self.manage_paging_preference(limit, default)
+ limit = max(int(limit), 1)
+ limit = min(limit, asint(config.get('limit_param_max', 500)))
page = max(int(page), 0)
start = page * int(limit)
return (limit, page, start)
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/lib/helpers.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index bf5a580..72c64ad 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -53,7 +53,7 @@ from formencode.variabledecode import variable_decode
import formencode
from jinja2 import Markup
from jinja2.filters import contextfilter, escape
-from paste.deploy.converters import asbool, aslist
+from paste.deploy.converters import asbool, aslist, asint
from webhelpers import date, feedgenerator, html, number, misc, text
from webob.exc import HTTPUnauthorized
@@ -684,16 +684,17 @@ class log_action(object):
return result
-def paging_sanitizer(limit, page, total_count, zero_based_pages=True):
+def paging_sanitizer(limit, page, total_count=sys.maxint, zero_based_pages=True):
"""Return limit, page - both converted to int and constrained to
valid ranges based on total_count.
Useful for sanitizing limit and page query params.
"""
limit = max(int(limit), 1)
+ limit = min(limit, asint(tg.config.get('limit_param_max', 500)))
max_page = (total_count / limit) + (1 if total_count % limit else 0)
max_page = max(0, max_page - (1 if zero_based_pages else 0))
- page = min(max(int(page), (0 if zero_based_pages else 1)), max_page)
+ page = min(max(int(page or 0), (0 if zero_based_pages else 1)), max_page)
return limit, page
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 67cbf3b..4792f44 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -103,7 +103,7 @@ class Artifact(MappedClass, SearchIndexable):
import_id = FieldProperty(None, if_missing=None)
deleted = FieldProperty(bool, if_missing=False)
- def __json__(self):
+ def __json__(self, posts_limit=None):
"""Return a JSON-encodable :class:`dict` representation of this
Artifact.
@@ -113,7 +113,7 @@ class Artifact(MappedClass, SearchIndexable):
mod_date=self.mod_date,
labels=list(self.labels),
related_artifacts=[a.url() for a in self.related_artifacts()],
- discussion_thread=self.discussion_thread.__json__(),
+ discussion_thread=self.discussion_thread.__json__(limit=posts_limit),
discussion_thread_url=h.absurl('/rest%s' %
self.discussion_thread.url()),
)
@@ -867,7 +867,7 @@ class Feed(MappedClass):
@classmethod
def feed(cls, q, feed_type, title, link, description,
- since=None, until=None, offset=None, limit=None):
+ since=None, until=None, page=None, limit=None):
"Produces webhelper.feedgenerator Feed"
d = dict(title=title, link=h.absurl(link),
description=description, language=u'en',
@@ -876,6 +876,7 @@ class Feed(MappedClass):
feed = FG.Atom1Feed(**d)
elif feed_type == 'rss':
feed = RssFeed(**d)
+ limit, page = h.paging_sanitizer(limit or 10, page)
query = defaultdict(dict)
query.update(q)
if since is not None:
@@ -884,11 +885,8 @@ class Feed(MappedClass):
query['pubdate']['$lte'] = until
cur = cls.query.find(query)
cur = cur.sort('pubdate', pymongo.DESCENDING)
- if limit is None:
- limit = 10
- query = cur.limit(limit)
- if offset is not None:
- query = cur.offset(offset)
+ cur = cur.limit(limit)
+ cur = cur.skip(limit * page)
for r in cur:
feed.add_item(title=r.title,
link=h.absurl(r.link.encode('utf-8')),
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/model/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index a704da7..a7642a7 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -62,14 +62,14 @@ class Discussion(Artifact, ActivityObject):
threads = RelationProperty('Thread', via='discussion_id')
posts = RelationProperty('Post', via='discussion_id')
- def __json__(self):
+ def __json__(self, limit=None, posts_limit=None):
return dict(
_id=str(self._id),
shortname=self.shortname,
name=self.name,
description=self.description,
- threads=[t.__json__() for t in self.thread_class().query.find(
- dict(discussion_id=self._id))]
+ threads=[t.__json__(limit=posts_limit) for t
+ in self.thread_class().query.find(dict(discussion_id=self._id)).limit(limit or 0)]
)
@property
@@ -178,6 +178,8 @@ class Thread(Artifact, ActivityObject):
_id=self._id,
discussion_id=str(self.discussion_id),
subject=self.subject,
+ limit=limit,
+ page=page,
posts=[dict(slug=p.slug,
text=p.text,
subject=p.subject,
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/model/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py
index cef25bc..7d917f1 100644
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -1091,9 +1091,8 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject):
for u in self.users_with_role('Developer')],
tools=[self.app_instance(t) for t in self.app_configs if h.has_access(t, 'read')],
labels=list(self.labels),
- categories={
- n: [t.__json__(
- ) for t in ts] for n, ts in self.all_troves().items()},
+ categories={n: [t.__json__() for t in ts]
+ for n, ts in self.all_troves().items()},
icon_url=h.absurl(self.url() + 'icon') if self.icon else None,
screenshots=[
dict(
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/tests/test_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index c29cfa2..5d2148b 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -770,6 +770,8 @@ class TestHandlePaging(unittest.TestCase):
self.assertEqual(g.handle_paging(10, 2), (10, 2, 20))
# handle paging must not mess up user preferences
self.assertEqual(c.user.get_pref('results_per_page'), None)
+ # maximum enforced
+ self.assertEqual(g.handle_paging(99999999, 0), (500, 0, 0))
def test_without_limit(self):
# default limit = 25
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/allura/tests/test_helpers.py
----------------------------------------------------------------------
diff --git a/Allura/allura/tests/test_helpers.py b/Allura/allura/tests/test_helpers.py
index e9d13d2..8f2c01e 100644
--- a/Allura/allura/tests/test_helpers.py
+++ b/Allura/allura/tests/test_helpers.py
@@ -227,7 +227,10 @@ def test_paging_sanitizer():
(5, '-1', 25): (5, 0),
('5', -1, 25, False): (5, 1),
(5, '3', 25): (5, 3),
- ('5', 3, 25, False): (5, 3)
+ ('5', 3, 25, False): (5, 3),
+ (9999999, 0, 0): (500, 0),
+ (10, None, 0): (10, 0),
+ (10, 0): (10, 0),
}
for input, output in test_data.iteritems():
assert (h.paging_sanitizer(*input)) == output
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index 4c540d5..e524581 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -480,6 +480,9 @@ allow_project_undelete = true
lcd_thread_chunk_size = 10
lcd_timeout = 60
+; Many URLs support a param like limit=50 This setting controls the max value allowed for that parameter.
+; Allowing exceedingly high values may have a performance impact
+limit_param_max = 500
;
; Settings for the Blog tool
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/docs/api-rest/api.raml
----------------------------------------------------------------------
diff --git a/Allura/docs/api-rest/api.raml b/Allura/docs/api-rest/api.raml
index 244c5db..21d64ff 100755
--- a/Allura/docs/api-rest/api.raml
+++ b/Allura/docs/api-rest/api.raml
@@ -284,7 +284,7 @@ documentation:
get:
description: |
returns summary information about a thread, including the posts in a thread
- is: [ bearerAuth ]
+ is: [ bearerAuth, pageable ]
/{slug}:
uriParameters:
slug:
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/Allura/docs/api-rest/traits.yaml
----------------------------------------------------------------------
diff --git a/Allura/docs/api-rest/traits.yaml b/Allura/docs/api-rest/traits.yaml
index ab746f3..d3f17a4 100755
--- a/Allura/docs/api-rest/traits.yaml
+++ b/Allura/docs/api-rest/traits.yaml
@@ -47,7 +47,7 @@
type: integer
required: false
example: 10
- default: 10
+ default: 25
- permissionTestable:
description: |
**Endpoints**
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeActivity/forgeactivity/main.py
----------------------------------------------------------------------
diff --git a/ForgeActivity/forgeactivity/main.py b/ForgeActivity/forgeactivity/main.py
index 845eb9e..d51b0e2 100644
--- a/ForgeActivity/forgeactivity/main.py
+++ b/ForgeActivity/forgeactivity/main.py
@@ -110,8 +110,8 @@ class ForgeActivityController(BaseController):
actor_only = False
following = g.director.is_connected(c.user, followee)
- page = asint(kw.get('page', 0))
- limit = extra_limit = asint(kw.get('limit', 100))
+ limit, page = h.paging_sanitizer(kw.get('limit', 100), kw.get('page', 0))
+ extra_limit = limit
# get more in case perm check filters some out
if page == 0 and limit <= 10:
extra_limit = limit * 20
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeBlog/forgeblog/main.py
----------------------------------------------------------------------
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index a3483cf..622e036 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -536,7 +536,7 @@ class PostRestController(BaseController):
else:
if self.post.state == 'draft':
require_access(self.post, 'write')
- return self.post.__json__()
+ return self.post.__json__(posts_limit=10)
def _update_post(self, **post_data):
require_access(self.post, 'write')
@@ -552,4 +552,4 @@ class PostRestController(BaseController):
if 'labels' in post_data:
self.post.labels = post_data['labels'].split(',')
self.post.commit()
- return self.post.__json__()
+ return self.post.__json__(posts_limit=10)
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeBlog/forgeblog/model/blog.py
----------------------------------------------------------------------
diff --git a/ForgeBlog/forgeblog/model/blog.py b/ForgeBlog/forgeblog/model/blog.py
index 41b9f1b..818e207 100644
--- a/ForgeBlog/forgeblog/model/blog.py
+++ b/ForgeBlog/forgeblog/model/blog.py
@@ -288,8 +288,8 @@ class BlogPost(M.VersionedArtifact, ActivityObject):
subject='%s discussion' % post.title)
return post
- def __json__(self):
- return dict(super(BlogPost, self).__json__(),
+ def __json__(self, posts_limit=None):
+ return dict(super(BlogPost, self).__json__(posts_limit=posts_limit),
author=self.author().username,
title=self.title,
url=h.absurl('/rest' + self.url()),
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 8fe5d39..cb4b4f8 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -383,15 +383,14 @@ class ForumRestController(BaseController):
@expose('json:')
def index(self, limit=None, page=0, **kw):
limit, page, start = g.handle_paging(limit, int(page))
- topics = model.Forum.thread_class().query.find(
- dict(discussion_id=self.forum._id))
+ topics = model.Forum.thread_class().query.find(dict(discussion_id=self.forum._id))
topics = topics.sort([('flags', pymongo.DESCENDING),
('last_post_date', pymongo.DESCENDING)])
topics = topics.skip(start).limit(limit)
count = topics.count()
json = {}
- json['forum'] = self.forum.__json__()
- # it appears that topics replace threads here
+ json['forum'] = self.forum.__json__(limit=1) # small limit since we're going to "del" the threads anyway
+ # topics replace threads here
del json['forum']['threads']
json['forum']['topics'] = [dict(_id=t._id,
subject=t.subject,
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeSVN/forgesvn/svn_main.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/svn_main.py b/ForgeSVN/forgesvn/svn_main.py
index e87d106..72968b3 100644
--- a/ForgeSVN/forgesvn/svn_main.py
+++ b/ForgeSVN/forgesvn/svn_main.py
@@ -193,11 +193,7 @@ class SVNCommitBrowserController(BaseController):
'built_tree': {},
'next_commit': None,
}
- try:
- limit = asint(limit)
- except ValueError as e:
- pass
- limit = limit or 100
+ limit, _ = h.paging_sanitizer(limit or 100, 0, 0)
for i, commit in enumerate(c.app.repo.log(revs=start, id_only=False, page_size=limit+1)):
if i >= limit:
data['next_commit'] = str(commit['id'])
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeTracker/forgetracker/model/ticket.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index 20996ff..36fec0b 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -1106,11 +1106,14 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
ticket.discussion_thread.add_post(text=message, notify=notify)
return ticket
- def __json__(self):
+ def __json__(self, posts_limit=None):
parents_json = {}
for parent in reversed(type(self).mro()):
if parent != type(self) and hasattr(parent, '__json__'):
- parents_json.update(parent.__json__(self))
+ kwargs = {}
+ if parent == VersionedArtifact:
+ kwargs['posts_limit'] = posts_limit
+ parents_json.update(parent.__json__(self, **kwargs))
return dict(parents_json,
created_date=self.created_date,
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeTracker/forgetracker/tests/functional/test_rest.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tests/functional/test_rest.py b/ForgeTracker/forgetracker/tests/functional/test_rest.py
index a45477e..1b3ba80 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_rest.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_rest.py
@@ -110,7 +110,7 @@ class TestRestUpdateTicket(TestTrackerApiBase):
args = dict(self.ticket_args, summary='test update ticket', labels='',
assigned_to=self.ticket_args['assigned_to_id'] or '')
for bad_key in ('ticket_num', 'assigned_to_id', 'created_date',
- 'reported_by', 'reported_by_id', '_id', 'votes_up', 'votes_down'):
+ 'reported_by', 'reported_by_id', '_id', 'votes_up', 'votes_down', 'discussion_thread'):
del args[bad_key]
args['private'] = str(args['private'])
args['discussion_disabled'] = str(args['discussion_disabled'])
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeTracker/forgetracker/tracker_main.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index edd5555..4eec4d5 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -1865,7 +1865,7 @@ class TicketRestController(BaseController):
@expose('json:')
def index(self, **kw):
- return dict(ticket=self.ticket)
+ return dict(ticket=self.ticket.__json__(posts_limit=10))
@expose()
@h.vardec
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeWiki/forgewiki/model/wiki.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/model/wiki.py b/ForgeWiki/forgewiki/model/wiki.py
index 9ddc7f2..10af425 100644
--- a/ForgeWiki/forgewiki/model/wiki.py
+++ b/ForgeWiki/forgewiki/model/wiki.py
@@ -122,8 +122,8 @@ class Page(VersionedArtifact, ActivityObject):
d.update(summary=self.title)
return d
- def __json__(self):
- return dict(super(Page, self).__json__(),
+ def __json__(self, posts_limit=None):
+ return dict(super(Page, self).__json__(posts_limit=posts_limit),
title=self.title,
text=self.text,
labels=list(self.labels),
http://git-wip-us.apache.org/repos/asf/allura/blob/7282b402/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index cf37ac0..b08d03e 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -833,7 +833,7 @@ class PageRestController(BaseController):
return self._update_page(self.title, **kw)
if self.page is None:
raise exc.HTTPNotFound()
- return self.page.__json__()
+ return self.page.__json__(posts_limit=10)
def _update_page(self, title, **post_data):
with h.notifications_disabled(c.project):