From a7d0fc96289e6235e9d43802f3eb26da13b59f5f Mon Sep 17 00:00:00 2001
From: John Chilton
Date: Sat, 25 May 2019 20:48:08 -0400
Subject: [PATCH 01/19] clean-fix-page-api - remove unused pages code,
annotation param never set.
---
client/galaxy/scripts/galaxy.pages.js | 2 --
lib/galaxy/webapps/galaxy/controllers/page.py | 33 +------------------
2 files changed, 1 insertion(+), 34 deletions(-)
diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js
index d9a1cf470fd..ecca493eb26 100644
--- a/client/galaxy/scripts/galaxy.pages.js
+++ b/client/galaxy/scripts/galaxy.pages.js
@@ -557,8 +557,6 @@ export default function pagesEditorOnload(renderedConfigs = {}) {
data: {
id: configs.page_id,
content: editor.xhtml(),
- annotations: JSON.stringify({}),
- // annotations: JSON.stringify(annotations),
_: "true"
},
success: function() {
diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py
index 78b00076b42..669bdb24f9f 100644
--- a/lib/galaxy/webapps/galaxy/controllers/page.py
+++ b/lib/galaxy/webapps/galaxy/controllers/page.py
@@ -685,7 +685,7 @@ class PageController(BaseUIController, SharableMixin,
@web.expose
@web.require_login()
- def save(self, trans, id, content, annotations):
+ def save(self, trans, id, content):
id = self.decode_id(id)
page = trans.sa_session.query(model.Page).get(id)
assert page.user == trans.user
@@ -704,37 +704,6 @@ class PageController(BaseUIController, SharableMixin,
page.latest_revision = page_revision
page_revision.content = content
- # Save annotations.
- annotations = loads(annotations)
- for annotation_dict in annotations:
- item_id = self.decode_id(annotation_dict['item_id'])
- item_class = self.get_class(annotation_dict['item_class'])
- item = trans.sa_session.query(item_class).filter_by(id=item_id).first()
- if not item:
- raise RuntimeError("cannot find annotated item")
- text = sanitize_html(annotation_dict['text'])
-
- # Add/update annotation.
- if item_id and item_class and text:
- # Get annotation association.
- annotation_assoc_class = eval("model.%sAnnotationAssociation" % item_class.__name__)
- annotation_assoc = trans.sa_session.query(annotation_assoc_class).filter_by(user=trans.get_user())
- if item_class == model.History.__class__:
- annotation_assoc = annotation_assoc.filter_by(history=item)
- elif item_class == model.HistoryDatasetAssociation.__class__:
- annotation_assoc = annotation_assoc.filter_by(hda=item)
- elif item_class == model.StoredWorkflow.__class__:
- annotation_assoc = annotation_assoc.filter_by(stored_workflow=item)
- elif item_class == model.WorkflowStep.__class__:
- annotation_assoc = annotation_assoc.filter_by(workflow_step=item)
- annotation_assoc = annotation_assoc.first()
- if not annotation_assoc:
- # Create association.
- annotation_assoc = annotation_assoc_class()
- item.annotations.append(annotation_assoc)
- annotation_assoc.user = trans.get_user()
- # Set annotation user text.
- annotation_assoc.annotation = text
trans.sa_session.flush()
@web.expose
From 31aa773066464cdcfd69a85b043b4d3229c7e9ae Mon Sep 17 00:00:00 2001
From: John Chilton
Date: Sat, 25 May 2019 21:16:20 -0400
Subject: [PATCH 02/19] clean-fix-page-api - page_revisions reuse manager util
for checking security and decoding ids
---
.../webapps/galaxy/api/page_revisions.py | 26 ++++---------------
1 file changed, 5 insertions(+), 21 deletions(-)
diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py
index faf2be37035..9d2233f282b 100644
--- a/lib/galaxy/webapps/galaxy/api/page_revisions.py
+++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py
@@ -5,6 +5,7 @@ import logging
from galaxy import exceptions
from galaxy.model.item_attrs import UsesAnnotations
+from galaxy.managers.base import get_object
from galaxy.util.sanitize_html import sanitize_html
from galaxy.web import expose_api
from galaxy.web.base.controller import (
@@ -30,10 +31,8 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses
:rtype: list
:returns: dictionaries containing different revisions of the page
"""
- page = self._get_page(trans, page_id)
- self._verify_page_ownership(trans, page)
-
- r = trans.sa_session.query(trans.app.model.PageRevision).filter_by(page_id=trans.security.decode_id(page_id))
+ page = get_object(trans, page_id, 'Page', check_ownership=False, check_accessible=True)
+ r = trans.sa_session.query(trans.app.model.PageRevision).filter_by(page_id=page.id)
out = []
for page in r:
out.append(self.encode_all_ids(trans, page.to_dict(), True))
@@ -54,13 +53,12 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses
:rtype: dictionary
:returns: Dictionary with 'success' or 'error' element to indicate the result of the request
"""
+ page = get_object(trans, page_id, 'Page', check_ownership=True)
+
content = payload.get("content", None)
if not content:
raise exceptions.ObjectAttributeMissingException("content undefined or empty")
- page = self._get_page(trans, page_id)
- self._verify_page_ownership(trans, page)
-
if 'title' in payload:
title = payload['title']
else:
@@ -79,17 +77,3 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses
session.flush()
return page_revision.to_dict(view="element")
-
- def _get_page(self, trans, page_id):
- page = None
- try:
- page = trans.sa_session.query(trans.app.model.Page).get(trans.security.decode_id(page_id))
- except Exception:
- pass
- if not page:
- raise exceptions.ObjectNotFound()
- return page
-
- def _verify_page_ownership(self, trans, page):
- if not self.security_check(trans, page, True, True):
- raise exceptions.ItemOwnershipException()
From 86d21ab5695deb193a794bf1026bf9bfd40ea86c Mon Sep 17 00:00:00 2001
From: John Chilton
Date: Sat, 25 May 2019 22:25:24 -0400
Subject: [PATCH 03/19] clean-fix-page-api - pages API reuse manager util for
checking security and decoding ids
---
lib/galaxy/webapps/galaxy/api/pages.py | 20 +++-----------------
1 file changed, 3 insertions(+), 17 deletions(-)
diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py
index a5a8ac3d75e..de69766d5ef 100644
--- a/lib/galaxy/webapps/galaxy/api/pages.py
+++ b/lib/galaxy/webapps/galaxy/api/pages.py
@@ -4,6 +4,7 @@ API for updating Galaxy Pages
import logging
from galaxy import exceptions
+from galaxy.managers.base import get_object
from galaxy.managers.pages import (
PageManager,
PageSerializer
@@ -128,7 +129,7 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati
:rtype: dict
:returns: Dictionary with 'success' or 'error' element to indicate the result of the request
"""
- page = self._get_page(trans, id)
+ page = get_object(trans, id, 'Page', check_ownership=True)
# Mark a page as deleted
page.deleted = True
@@ -147,22 +148,7 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati
:rtype: dict
:returns: Dictionary return of the Page.to_dict call with the 'content' field populated by the most recent revision
"""
- page = self._get_page(trans, id)
- self.security_check(trans, page, check_ownership=False, check_accessible=True)
+ page = get_object(trans, id, 'Page', check_ownership=False, check_accessible=True)
rval = self.encode_all_ids(trans, page.to_dict(), True)
rval['content'] = page.latest_revision.content
return rval
-
- def _get_page(self, trans, id): # Fetches page object and verifies security.
- try:
- page = trans.sa_session.query(trans.app.model.Page).get(trans.security.decode_id(id))
- except Exception:
- page = None
-
- if not page:
- raise exceptions.ObjectNotFound()
-
- if page.user != trans.user and not trans.user_is_admin:
- raise exceptions.ItemOwnershipException()
-
- return page
From d324ad430e24bc5e1fdae946a91698abcff6b0e4 Mon Sep 17 00:00:00 2001
From: John Chilton
Date: Sat, 25 May 2019 17:39:49 -0400
Subject: [PATCH 04/19] clean-fix-page-api - refactor shared page creation code
into manager.
De-duplication between web and API controllers.
---
lib/galaxy/managers/base.py | 7 +++
lib/galaxy/managers/pages.py | 47 ++++++++++++++-
lib/galaxy/web/base/controller.py | 11 +---
.../webapps/galaxy/api/page_revisions.py | 5 ++
lib/galaxy/webapps/galaxy/api/pages.py | 37 +-----------
lib/galaxy/webapps/galaxy/controllers/page.py | 57 ++++++-------------
6 files changed, 79 insertions(+), 85 deletions(-)
diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py
index e06ca8de029..26b0fe2e01b 100644
--- a/lib/galaxy/managers/base.py
+++ b/lib/galaxy/managers/base.py
@@ -1128,3 +1128,10 @@ class ModelFilterParser(HasAModelManager):
def raise_filter_err(self, attr, op, val, msg):
raise exceptions.RequestParameterInvalidException(msg, column=attr, operation=op, val=val)
+
+
+def is_valid_slug(slug):
+ """Returns true iff slug is valid."""
+
+ VALID_SLUG_RE = re.compile(r"^[a-z0-9\-]+$")
+ return VALID_SLUG_RE.match(slug)
diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py
index 08f2c9f46fc..c8422cc638d 100644
--- a/lib/galaxy/managers/pages.py
+++ b/lib/galaxy/managers/pages.py
@@ -7,13 +7,16 @@ from within Galaxy.
"""
import logging
-from galaxy import model
+from galaxy import exceptions, model
from galaxy.managers import sharable
+from galaxy.managers.base import is_valid_slug
+from galaxy.model.item_attrs import UsesAnnotations
+from galaxy.util.sanitize_html import sanitize_html
log = logging.getLogger(__name__)
-class PageManager(sharable.SharableModelManager):
+class PageManager(sharable.SharableModelManager, UsesAnnotations):
"""
"""
@@ -35,6 +38,46 @@ class PageManager(sharable.SharableModelManager):
"""
pass
+ def create(self, trans, payload):
+ user = trans.get_user()
+
+ if not payload.get("title", None):
+ raise exceptions.ObjectAttributeMissingException("Page name is required")
+ elif not payload.get("slug", None):
+ raise exceptions.ObjectAttributeMissingException("Page id is required")
+ elif not is_valid_slug(payload["slug"]):
+ raise exceptions.ObjectAttributeInvalidException("Page identifier must consist of only lowercase letters, numbers, and the '-' character")
+ elif trans.sa_session.query(trans.app.model.Page).filter_by(user=user, slug=payload["slug"], deleted=False).first():
+ raise exceptions.DuplicatedSlugException("Page identifier must be unique")
+
+ content = payload.get("content", "")
+ content = sanitize_html(content)
+
+ # Create the new stored page
+ page = trans.app.model.Page()
+ page.title = payload['title']
+ page.slug = payload['slug']
+ page_annotation = payload.get("annotation", None)
+ if page_annotation is not None:
+ page_annotation = sanitize_html(page_annotation)
+ self.add_item_annotation(trans.sa_session, trans.get_user(), page, page_annotation)
+
+ page.user = user
+ # And the first (empty) page revision
+ page_revision = trans.app.model.PageRevision()
+ page_revision.title = payload['title']
+ page_revision.page = page
+ page.latest_revision = page_revision
+ page_revision.content = content
+ # Persist
+ session = trans.sa_session
+ session.add(page)
+ session.flush()
+ return page
+
+ def save_revision(self, trans, page_id, payload):
+ pass
+
class PageSerializer(sharable.SharableModelSerializer):
"""
diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py
index da39c15233e..98468a54e3e 100644
--- a/lib/galaxy/web/base/controller.py
+++ b/lib/galaxy/web/base/controller.py
@@ -54,13 +54,6 @@ log = logging.getLogger(__name__)
SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error"
-def _is_valid_slug(slug):
- """ Returns true if slug is valid. """
-
- VALID_SLUG_RE = re.compile(r"^[a-z0-9\-]+$")
- return VALID_SLUG_RE.match(slug)
-
-
class BaseController(object):
"""
Base class for Galaxy web application controllers.
@@ -1118,7 +1111,7 @@ class UsesVisualizationMixin(UsesLibraryMixinItems):
title_err = slug_err = ""
if not title:
title_err = "visualization name is required"
- elif slug and not _is_valid_slug(slug):
+ elif slug and not managers_base.is_valid_slug(slug):
slug_err = "visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
elif slug and trans.sa_session.query(trans.model.Visualization).filter_by(user=user, slug=slug, deleted=False).first():
slug_err = "visualization identifier must be unique"
@@ -1361,7 +1354,7 @@ class SharableMixin(object):
def _is_valid_slug(self, slug):
""" Returns true if slug is valid. """
- return _is_valid_slug(slug)
+ return managers_base.is_valid_slug(slug)
@web.expose
@web.require_login("modify Galaxy items")
diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py
index 9d2233f282b..c3877dea5cb 100644
--- a/lib/galaxy/webapps/galaxy/api/page_revisions.py
+++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py
@@ -6,6 +6,7 @@ import logging
from galaxy import exceptions
from galaxy.model.item_attrs import UsesAnnotations
from galaxy.managers.base import get_object
+from galaxy.managers.pages import PageManager
from galaxy.util.sanitize_html import sanitize_html
from galaxy.web import expose_api
from galaxy.web.base.controller import (
@@ -19,6 +20,10 @@ log = logging.getLogger(__name__)
class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotations, SharableMixin):
+ def __init__(self, app):
+ super(PageRevisionsController, self).__init__(app)
+ self.manager = PageManager(app)
+
@expose_api
def index(self, trans, page_id, **kwd):
"""
diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py
index de69766d5ef..29ddb522e96 100644
--- a/lib/galaxy/webapps/galaxy/api/pages.py
+++ b/lib/galaxy/webapps/galaxy/api/pages.py
@@ -10,7 +10,6 @@ from galaxy.managers.pages import (
PageSerializer
)
from galaxy.model.item_attrs import UsesAnnotations
-from galaxy.util.sanitize_html import sanitize_html
from galaxy.web import expose_api
from galaxy.web.base.controller import (
BaseAPIController,
@@ -82,40 +81,8 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati
:rtype: dict
:returns: Dictionary return of the Page.to_dict call
"""
- user = trans.get_user()
-
- if not payload.get("title", None):
- raise exceptions.ObjectAttributeMissingException("Page name is required")
- elif not payload.get("slug", None):
- raise exceptions.ObjectAttributeMissingException("Page id is required")
- elif not self._is_valid_slug(payload["slug"]):
- raise exceptions.ObjectAttributeInvalidException("Page identifier must consist of only lowercase letters, numbers, and the '-' character")
- elif trans.sa_session.query(trans.app.model.Page).filter_by(user=user, slug=payload["slug"], deleted=False).first():
- raise exceptions.DuplicatedSlugException("Page slug must be unique")
-
- content = payload.get("content", "")
- content = sanitize_html(content)
-
- # Create the new stored page
- page = trans.app.model.Page()
- page.title = payload['title']
- page.slug = payload['slug']
- page_annotation = sanitize_html(payload.get("annotation", ""))
- self.add_item_annotation(trans.sa_session, trans.get_user(), page, page_annotation)
- page.user = user
- # And the first (empty) page revision
- page_revision = trans.app.model.PageRevision()
- page_revision.title = payload['title']
- page_revision.page = page
- page.latest_revision = page_revision
- page_revision.content = content
- # Persist
- session = trans.sa_session
- session.add(page)
- session.flush()
-
- rval = self.encode_all_ids(trans, page.to_dict(), True)
- return rval
+ page = self.manager.create(trans, payload)
+ return self.encode_all_ids(trans, page.to_dict(), True)
@expose_api
def delete(self, trans, id, **kwd):
diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py
index 669bdb24f9f..92f9143e99c 100644
--- a/lib/galaxy/webapps/galaxy/controllers/page.py
+++ b/lib/galaxy/webapps/galaxy/controllers/page.py
@@ -16,11 +16,15 @@ from sqlalchemy.orm import (
)
from galaxy import (
- managers,
+ exceptions,
model,
util,
web
)
+from galaxy.managers import base
+from galaxy.managers.hdas import HDAManager
+from galaxy.managers.histories import HistoryManager, HistorySerializer
+from galaxy.managers.pages import PageManager
from galaxy.model.item_attrs import UsesItemRatings
from galaxy.util import unicodify
from galaxy.util.sanitize_html import sanitize_html
@@ -473,9 +477,10 @@ class PageController(BaseUIController, SharableMixin,
def __init__(self, app):
super(PageController, self).__init__(app)
- self.history_manager = managers.histories.HistoryManager(app)
- self.history_serializer = managers.histories.HistorySerializer(self.app)
- self.hda_manager = managers.hdas.HDAManager(app)
+ self.page_manager = PageManager(app)
+ self.history_manager = HistoryManager(app)
+ self.history_serializer = HistorySerializer(self.app)
+ self.hda_manager = HDAManager(app)
@web.expose
@web.json
@@ -541,37 +546,11 @@ class PageController(BaseUIController, SharableMixin,
}]
}
else:
- user = trans.get_user()
- p_title = payload.get('title')
- p_slug = payload.get('slug')
- p_annotation = payload.get('annotation')
- if not p_title:
- return self.message_exception(trans, 'Please provide a page name is required.')
- elif not p_slug:
- return self.message_exception(trans, 'Please provide a unique identifier.')
- elif not self._is_valid_slug(p_slug):
- return self.message_exception(trans, 'Page identifier can only contain lowercase letters, numbers, and dashes (-).')
- elif trans.sa_session.query(model.Page).filter_by(user=user, slug=p_slug, deleted=False).first():
- return self.message_exception(trans, 'Page id must be unique.')
- else:
- # Create the new stored page
- p = model.Page()
- p.title = p_title
- p.slug = p_slug
- p.user = user
- if p_annotation:
- p_annotation = sanitize_html(p_annotation)
- self.add_item_annotation(trans.sa_session, user, p, p_annotation)
- # And the first (empty) page revision
- p_revision = model.PageRevision()
- p_revision.title = p_title
- p_revision.page = p
- p.latest_revision = p_revision
- p_revision.content = ""
- # Persist
- trans.sa_session.add(p)
- trans.sa_session.flush()
- return {'message': 'Page \'%s\' successfully created.' % p.title, 'status': 'success'}
+ try:
+ page = self.page_manager.create(trans, payload)
+ except exceptions.MessageException as e:
+ return self.message_exception(trans, str(e))
+ return {'message': 'Page \'%s\' successfully created.' % page.title, 'status': 'success'}
@web.legacy_expose_api
@web.require_login("edit pages")
@@ -958,20 +937,20 @@ def _placeholderRenderForSave(trans, item_class, item_id, encode=False):
item_name = ''
if item_class == 'History':
history = trans.sa_session.query(trans.model.History).get(decoded_item_id)
- history = managers.base.security_check(trans, history, False, True)
+ history = base.security_check(trans, history, False, True)
item_name = history.name
elif item_class == 'HistoryDatasetAssociation':
hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id)
- hda_manager = managers.hdas.HDAManager(trans.app)
+ hda_manager = HDAManager(trans.app)
hda = hda_manager.get_accessible(decoded_item_id, trans.user)
item_name = hda.name
elif item_class == 'StoredWorkflow':
wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id)
- wf = managers.base.security_check(trans, wf, False, True)
+ wf = base.security_check(trans, wf, False, True)
item_name = wf.name
elif item_class == 'Visualization':
visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id)
- visualization = managers.base.security_check(trans, visualization, False, True)
+ visualization = base.security_check(trans, visualization, False, True)
item_name = visualization.title
class_shorthand = PAGE_CLASS_MAPPING[item_class]
if encode:
From ead233c0c1594d013f149eaf54773db7e29dcb8b Mon Sep 17 00:00:00 2001
From: John Chilton
Date: Sat, 25 May 2019 21:32:38 -0400
Subject: [PATCH 05/19] clean-fix-page-api - move page revision creation to
manager.
---
lib/galaxy/managers/pages.py | 298 +++++++++++++++++-
.../webapps/galaxy/api/page_revisions.py | 29 +-
lib/galaxy/webapps/galaxy/controllers/page.py | 293 +----------------
scripts/pages_identifier_conversion.py | 4 +-
4 files changed, 308 insertions(+), 316 deletions(-)
diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py
index c8422cc638d..b0b9c798f09 100644
--- a/lib/galaxy/managers/pages.py
+++ b/lib/galaxy/managers/pages.py
@@ -6,15 +6,51 @@ Pages are markup created and saved by users that can contain Galaxy objects
from within Galaxy.
"""
import logging
+import re
+
+from six.moves.html_entities import name2codepoint
+from six.moves.html_parser import HTMLParser
from galaxy import exceptions, model
-from galaxy.managers import sharable
-from galaxy.managers.base import is_valid_slug
+from galaxy.managers import base, sharable
+from galaxy.managers.hdas import HDAManager
from galaxy.model.item_attrs import UsesAnnotations
+from galaxy.util import unicodify
from galaxy.util.sanitize_html import sanitize_html
log = logging.getLogger(__name__)
+# Copied from https://github.com/kurtmckee/feedparser
+_cp1252 = {
+ 128: u'\u20ac', # euro sign
+ 130: u'\u201a', # single low-9 quotation mark
+ 131: u'\u0192', # latin small letter f with hook
+ 132: u'\u201e', # double low-9 quotation mark
+ 133: u'\u2026', # horizontal ellipsis
+ 134: u'\u2020', # dagger
+ 135: u'\u2021', # double dagger
+ 136: u'\u02c6', # modifier letter circumflex accent
+ 137: u'\u2030', # per mille sign
+ 138: u'\u0160', # latin capital letter s with caron
+ 139: u'\u2039', # single left-pointing angle quotation mark
+ 140: u'\u0152', # latin capital ligature oe
+ 142: u'\u017d', # latin capital letter z with caron
+ 145: u'\u2018', # left single quotation mark
+ 146: u'\u2019', # right single quotation mark
+ 147: u'\u201c', # left double quotation mark
+ 148: u'\u201d', # right double quotation mark
+ 149: u'\u2022', # bullet
+ 150: u'\u2013', # en dash
+ 151: u'\u2014', # em dash
+ 152: u'\u02dc', # small tilde
+ 153: u'\u2122', # trade mark sign
+ 154: u'\u0161', # latin small letter s with caron
+ 155: u'\u203a', # single right-pointing angle quotation mark
+ 156: u'\u0153', # latin small ligature oe
+ 158: u'\u017e', # latin small letter z with caron
+ 159: u'\u0178', # latin capital letter y with diaeresis
+}
+
class PageManager(sharable.SharableModelManager, UsesAnnotations):
"""
@@ -45,7 +81,7 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
raise exceptions.ObjectAttributeMissingException("Page name is required")
elif not payload.get("slug", None):
raise exceptions.ObjectAttributeMissingException("Page id is required")
- elif not is_valid_slug(payload["slug"]):
+ elif not base.is_valid_slug(payload["slug"]):
raise exceptions.ObjectAttributeInvalidException("Page identifier must consist of only lowercase letters, numbers, and the '-' character")
elif trans.sa_session.query(trans.app.model.Page).filter_by(user=user, slug=payload["slug"], deleted=False).first():
raise exceptions.DuplicatedSlugException("Page identifier must be unique")
@@ -75,8 +111,33 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
session.flush()
return page
- def save_revision(self, trans, page_id, payload):
- pass
+ def save_new_revision(self, trans, page, payload):
+ # Assumes security has already been checked by caller.
+ content = payload.get("content", None)
+ if not content:
+ raise exceptions.ObjectAttributeMissingException("content undefined or empty")
+
+ if 'title' in payload:
+ title = payload['title']
+ else:
+ title = page.title
+
+ content = sanitize_html(content)
+ processor = PageContentProcessor(trans, placeholderRenderForSave)
+ processor.feed(content)
+ # Output is string, so convert to unicode for saving.
+ content = unicodify(processor.output(), 'utf-8')
+
+ page_revision = trans.app.model.PageRevision()
+ page_revision.title = title
+ page_revision.page = page
+ page.latest_revision = page_revision
+ page_revision.content = content
+
+ # Persist
+ session = trans.sa_session
+ session.flush()
+ return page_revision
class PageSerializer(sharable.SharableModelSerializer):
@@ -116,3 +177,230 @@ class PageDeserializer(sharable.SharableModelDeserializer):
self.deserializers.update({
})
self.deserializable_keyset.update(self.deserializers.keys())
+
+
+class PageContentProcessor(HTMLParser, object):
+ """
+ Processes page content to produce HTML that is suitable for display.
+ For now, processor renders embedded objects.
+ """
+ bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)")
+ elements_no_end_tag = set([
+ 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame',
+ 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param',
+ 'source', 'track', 'wbr'
+ ])
+
+ def __init__(self, trans, render_embed_html_fn):
+ HTMLParser.__init__(self)
+ self.trans = trans
+ self.ignore_content = False
+ self.num_open_tags_for_ignore = 0
+ self.render_embed_html_fn = render_embed_html_fn
+
+ def reset(self):
+ self.pieces = []
+ HTMLParser.reset(self)
+
+ def _shorttag_replace(self, match):
+ tag = match.group(1)
+ if tag in self.elements_no_end_tag:
+ return '<' + tag + ' />'
+ else:
+ return '<' + tag + '>' + tag + '>'
+
+ def feed(self, data):
+ data = re.compile(r'\s]+?)\s*/>', self._shorttag_replace, data)
+ data = data.replace(''', "'")
+ data = data.replace('"', '"')
+ HTMLParser.feed(self, data)
+ HTMLParser.close(self)
+
+ def handle_starttag(self, tag, attrs):
+ """
+ Called for each start tag
+
+ attrs is a list of (attr, value) tuples, e.g. for
,
+ tag='pre', attrs=[('class', 'screen')]
+ """
+
+ # If ignoring content, just increment tag count and ignore.
+ if self.ignore_content:
+ self.num_open_tags_for_ignore += 1
+ return
+
+ # Not ignoring tag; look for embedded content.
+ embedded_item = False
+ for attribute in attrs:
+ if (attribute[0] == "class") and ("embedded-item" in attribute[1].split(" ")):
+ embedded_item = True
+ break
+ # For embedded content, set ignore flag to ignore current content and add new content for embedded item.
+ if embedded_item:
+ # Set processing attributes to ignore content.
+ self.ignore_content = True
+ self.num_open_tags_for_ignore = 1
+
+ # Insert content for embedded element.
+ for attribute in attrs:
+ name = attribute[0]
+ if name == "id":
+ # ID has form '-'
+ item_class, item_id = attribute[1].split("-")
+ embed_html = self.render_embed_html_fn(self.trans, item_class, item_id)
+ self.pieces.append(embed_html)
+ return
+
+ # Default behavior: not ignoring and no embedded content.
+ uattrs = []
+ strattrs = ''
+ if attrs:
+ for key, value in attrs:
+ value = value.replace('>', '>').replace('<', '<').replace('"', '"')
+ value = self.bare_ampersand.sub("&", value)
+ uattrs.append((key, value))
+ strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
+ if tag in self.elements_no_end_tag:
+ self.pieces.append('<%s%s />' % (tag, strattrs))
+ else:
+ self.pieces.append('<%s%s>' % (tag, strattrs))
+
+ def handle_endtag(self, tag):
+ """
+ Called for each end tag
+
+ E.g. for
[Do not edit this block; Galaxy will fill it in with the annotated {class_shorthand} when it is displayed]
'''
+
+# This is a mapping of the id portion of page contents to the cssclass/shortname.
+PAGE_CLASS_MAPPING = {
+ 'History': 'History',
+ 'HistoryDatasetAssociation': 'Dataset',
+ 'StoredWorkflow': 'Workflow',
+ 'Visualization': 'Visualization'
+}
+
+
+def placeholderRenderForEdit(trans, item_class, item_id):
+ return placeholderRenderForSave(trans, item_class, item_id, encode=True)
+
+
+def placeholderRenderForSave(trans, item_class, item_id, encode=False):
+ encoded_item_id, decoded_item_id = get_page_identifiers(item_id, trans.app)
+ item_name = ''
+ if item_class == 'History':
+ history = trans.sa_session.query(trans.model.History).get(decoded_item_id)
+ history = base.security_check(trans, history, False, True)
+ item_name = history.name
+ elif item_class == 'HistoryDatasetAssociation':
+ hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id)
+ hda_manager = HDAManager(trans.app)
+ hda = hda_manager.get_accessible(decoded_item_id, trans.user)
+ item_name = hda.name
+ elif item_class == 'StoredWorkflow':
+ wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id)
+ wf = base.security_check(trans, wf, False, True)
+ item_name = wf.name
+ elif item_class == 'Visualization':
+ visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id)
+ visualization = base.security_check(trans, visualization, False, True)
+ item_name = visualization.title
+ class_shorthand = PAGE_CLASS_MAPPING[item_class]
+ if encode:
+ item_id = encoded_item_id
+ else:
+ item_id = decoded_item_id
+ return PLACEHOLDER_TEMPLATE.format(
+ item_class=item_class,
+ class_shorthand=class_shorthand,
+ class_shorthand_lower=class_shorthand.lower(),
+ item_id=item_id,
+ item_name=item_name
+ )
diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py
index c3877dea5cb..5cee22441ca 100644
--- a/lib/galaxy/webapps/galaxy/api/page_revisions.py
+++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py
@@ -3,11 +3,9 @@ API for updating Galaxy Pages
"""
import logging
-from galaxy import exceptions
-from galaxy.model.item_attrs import UsesAnnotations
from galaxy.managers.base import get_object
from galaxy.managers.pages import PageManager
-from galaxy.util.sanitize_html import sanitize_html
+from galaxy.model.item_attrs import UsesAnnotations
from galaxy.web import expose_api
from galaxy.web.base.controller import (
BaseAPIController,
@@ -59,26 +57,5 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses
:returns: Dictionary with 'success' or 'error' element to indicate the result of the request
"""
page = get_object(trans, page_id, 'Page', check_ownership=True)
-
- content = payload.get("content", None)
- if not content:
- raise exceptions.ObjectAttributeMissingException("content undefined or empty")
-
- if 'title' in payload:
- title = payload['title']
- else:
- title = page.title
-
- content = sanitize_html(content)
-
- page_revision = trans.app.model.PageRevision()
- page_revision.title = title
- page_revision.page = page
- page.latest_revision = page_revision
- page_revision.content = content
-
- # Persist
- session = trans.sa_session
- session.flush()
-
- return page_revision.to_dict(view="element")
+ page_revision = self.manager.save_new_revision(trans, page, payload)
+ return self.encode_all_ids(trans, page_revision.to_dict(view="element"), True)
diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py
index 92f9143e99c..92de407430f 100644
--- a/lib/galaxy/webapps/galaxy/controllers/page.py
+++ b/lib/galaxy/webapps/galaxy/controllers/page.py
@@ -1,9 +1,5 @@
-import re
-from json import loads
from markupsafe import escape
-from six.moves.html_entities import name2codepoint
-from six.moves.html_parser import HTMLParser
from sqlalchemy import (
and_,
desc,
@@ -21,10 +17,14 @@ from galaxy import (
util,
web
)
-from galaxy.managers import base
from galaxy.managers.hdas import HDAManager
from galaxy.managers.histories import HistoryManager, HistorySerializer
-from galaxy.managers.pages import PageManager
+from galaxy.managers.pages import (
+ get_page_identifiers,
+ PageContentProcessor,
+ PageManager,
+ placeholderRenderForEdit,
+)
from galaxy.model.item_attrs import UsesItemRatings
from galaxy.util import unicodify
from galaxy.util.sanitize_html import sanitize_html
@@ -44,55 +44,6 @@ from galaxy.web.framework.helpers import (
)
-# Copied from https://github.com/kurtmckee/feedparser
-_cp1252 = {
- 128: u'\u20ac', # euro sign
- 130: u'\u201a', # single low-9 quotation mark
- 131: u'\u0192', # latin small letter f with hook
- 132: u'\u201e', # double low-9 quotation mark
- 133: u'\u2026', # horizontal ellipsis
- 134: u'\u2020', # dagger
- 135: u'\u2021', # double dagger
- 136: u'\u02c6', # modifier letter circumflex accent
- 137: u'\u2030', # per mille sign
- 138: u'\u0160', # latin capital letter s with caron
- 139: u'\u2039', # single left-pointing angle quotation mark
- 140: u'\u0152', # latin capital ligature oe
- 142: u'\u017d', # latin capital letter z with caron
- 145: u'\u2018', # left single quotation mark
- 146: u'\u2019', # right single quotation mark
- 147: u'\u201c', # left double quotation mark
- 148: u'\u201d', # right double quotation mark
- 149: u'\u2022', # bullet
- 150: u'\u2013', # en dash
- 151: u'\u2014', # em dash
- 152: u'\u02dc', # small tilde
- 153: u'\u2122', # trade mark sign
- 154: u'\u0161', # latin small letter s with caron
- 155: u'\u203a', # single right-pointing angle quotation mark
- 156: u'\u0153', # latin small ligature oe
- 158: u'\u017e', # latin small letter z with caron
- 159: u'\u0178', # latin capital letter y with diaeresis
-}
-
-
-PAGE_MAXRAW = 10**15
-
-
-def _get_page_identifiers(item_id, app):
- # Assume if item id is integer and less than 10**15, it's unencoded.
- try:
- decoded_id = int(item_id)
- if decoded_id >= PAGE_MAXRAW:
- raise ValueError("Identifier larger than maximum expected raw int, must be already encoded.")
- encoded_id = app.security.encode_id(item_id)
- except ValueError:
- # It's an encoded id.
- encoded_id = item_id
- decoded_id = app.security.decode_id(item_id)
- return (encoded_id, decoded_id)
-
-
def format_bool(b):
if b:
return "yes"
@@ -304,166 +255,6 @@ class VisualizationSelectionGrid(ItemSelectionGrid):
# Adapted from the _BaseHTMLProcessor class of https://github.com/kurtmckee/feedparser
-class _PageContentProcessor(HTMLParser, object):
- """
- Processes page content to produce HTML that is suitable for display.
- For now, processor renders embedded objects.
- """
- bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)")
- elements_no_end_tag = set([
- 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame',
- 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param',
- 'source', 'track', 'wbr'
- ])
-
- def __init__(self, trans, render_embed_html_fn):
- HTMLParser.__init__(self)
- self.trans = trans
- self.ignore_content = False
- self.num_open_tags_for_ignore = 0
- self.render_embed_html_fn = render_embed_html_fn
-
- def reset(self):
- self.pieces = []
- HTMLParser.reset(self)
-
- def _shorttag_replace(self, match):
- tag = match.group(1)
- if tag in self.elements_no_end_tag:
- return '<' + tag + ' />'
- else:
- return '<' + tag + '>' + tag + '>'
-
- def feed(self, data):
- data = re.compile(r'\s]+?)\s*/>', self._shorttag_replace, data)
- data = data.replace(''', "'")
- data = data.replace('"', '"')
- HTMLParser.feed(self, data)
- HTMLParser.close(self)
-
- def handle_starttag(self, tag, attrs):
- """
- Called for each start tag
-
- attrs is a list of (attr, value) tuples, e.g. for
,
- tag='pre', attrs=[('class', 'screen')]
- """
-
- # If ignoring content, just increment tag count and ignore.
- if self.ignore_content:
- self.num_open_tags_for_ignore += 1
- return
-
- # Not ignoring tag; look for embedded content.
- embedded_item = False
- for attribute in attrs:
- if (attribute[0] == "class") and ("embedded-item" in attribute[1].split(" ")):
- embedded_item = True
- break
- # For embedded content, set ignore flag to ignore current content and add new content for embedded item.
- if embedded_item:
- # Set processing attributes to ignore content.
- self.ignore_content = True
- self.num_open_tags_for_ignore = 1
-
- # Insert content for embedded element.
- for attribute in attrs:
- name = attribute[0]
- if name == "id":
- # ID has form '-'
- item_class, item_id = attribute[1].split("-")
- embed_html = self.render_embed_html_fn(self.trans, item_class, item_id)
- self.pieces.append(embed_html)
- return
-
- # Default behavior: not ignoring and no embedded content.
- uattrs = []
- strattrs = ''
- if attrs:
- for key, value in attrs:
- value = value.replace('>', '>').replace('<', '<').replace('"', '"')
- value = self.bare_ampersand.sub("&", value)
- uattrs.append((key, value))
- strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
- if tag in self.elements_no_end_tag:
- self.pieces.append('<%s%s />' % (tag, strattrs))
- else:
- self.pieces.append('<%s%s>' % (tag, strattrs))
-
- def handle_endtag(self, tag):
- """
- Called for each end tag
-
- E.g. for