diff --git a/cds/modules/deposit/static/json/cds_deposit/forms/project.json b/cds/modules/deposit/static/json/cds_deposit/forms/project.json index 9a78674e0..6f8c4d49c 100644 --- a/cds/modules/deposit/static/json/cds_deposit/forms/project.json +++ b/cds/modules/deposit/static/json/cds_deposit/forms/project.json @@ -19,6 +19,11 @@ "enterMode": 2, "entities": false, "height": 200, + "allowedContent": true, + "forcePasteAsPlainText": false, + "extraAllowedContent": "*{color}; *[style]", + "disableAutoInline": true, + "extraPlugins": "colorbutton", "toolbar": [ [ "PasteText", @@ -32,6 +37,9 @@ "Subscript", "Superscript" ], + [ + "TextColor" + ], [ "NumberedList", "BulletedList", diff --git a/cds/modules/deposit/static/json/cds_deposit/forms/video.json b/cds/modules/deposit/static/json/cds_deposit/forms/video.json index 274cda3f8..9ec11f092 100644 --- a/cds/modules/deposit/static/json/cds_deposit/forms/video.json +++ b/cds/modules/deposit/static/json/cds_deposit/forms/video.json @@ -18,6 +18,11 @@ "enterMode": 2, "entities": false, "height": 200, + "allowedContent": true, + "forcePasteAsPlainText": false, + "extraAllowedContent": "*{color}; *[style]", + "disableAutoInline": true, + "extraPlugins": "colorbutton", "toolbar": [ [ "PasteText", @@ -31,6 +36,9 @@ "Subscript", "Superscript" ], + [ + "TextColor" + ], [ "NumberedList", "BulletedList", diff --git a/cds/modules/records/serializers/json.py b/cds/modules/records/serializers/json.py index 2a6e06b01..ef5a103c7 100644 --- a/cds/modules/records/serializers/json.py +++ b/cds/modules/records/serializers/json.py @@ -32,7 +32,15 @@ has_read_record_permission, ) from ..utils import HTMLTagRemover, parse_video_chapters, remove_html_tags -from marshmallow_utils.html import sanitize_html +from marshmallow_utils.html import sanitize_html, ALLOWED_HTML_ATTRS, ALLOWED_CSS_STYLES + +CUSTOM_ALLOWED_ATTRS = { + **ALLOWED_HTML_ATTRS, + "span": ALLOWED_HTML_ATTRS.get("span", []) + ["style"], + "p": ALLOWED_HTML_ATTRS.get("p", []) + ["style"], +} + +CUSTOM_ALLOWED_CSS = ALLOWED_CSS_STYLES + ["color"] class CDSJSONSerializer(JSONSerializer): @@ -60,7 +68,11 @@ def _sanitize_metadata(self, metadata): if "description" in metadata: description = metadata["description"] description = self.html_tag_remover.unescape(description) - metadata["description"] = sanitize_html(description) + metadata["description"] = sanitize_html( + description, + attrs=CUSTOM_ALLOWED_ATTRS, + css_styles=CUSTOM_ALLOWED_CSS, + ) if "translations" in metadata: for t in metadata["translations"]: @@ -108,12 +120,12 @@ def preprocess_record(self, pid, record, links_factory=None): except KeyError: # ignore error if keys are missing in the metadata pass - - description = metadata.get('description', '') + + description = metadata.get("description", "") if description: - metadata['chapters'] = parse_video_chapters(description) + metadata["chapters"] = parse_video_chapters(description) else: - metadata['chapters'] = [] + metadata["chapters"] = [] return result diff --git a/cds/modules/records/serializers/schemas/common.py b/cds/modules/records/serializers/schemas/common.py index 9c95bb781..fb8c54ae5 100644 --- a/cds/modules/records/serializers/schemas/common.py +++ b/cds/modules/records/serializers/schemas/common.py @@ -22,6 +22,7 @@ from marshmallow import RAISE, Schema, ValidationError, fields, validates_schema from marshmallow.validate import Length from marshmallow_utils.fields import SanitizedHTML +from marshmallow_utils.html import sanitize_html from ...api import Keyword from ...resolver import keyword_resolver @@ -219,4 +220,42 @@ class RelatedIdentifiersSchema(Schema): identifier = fields.Str(required=True) scheme = fields.Str(required=True) relation_type = fields.Str(required=True) - resource_type = fields.Str() \ No newline at end of file + resource_type = fields.Str() + + +class SanitizedHTMLWithCSS(fields.String): + """Enhanced SanitizedHTML supporting inline CSS sanitization. + + Fully compatible with marshmallow_utils.fields.SanitizedHTML, + but adds CSS. + """ + + def __init__( + self, + tags=None, + attrs=None, + css_styles=None, + *args, + **kwargs, + ): + """ + :param tags: Allowed HTML tags. + :param attrs: Allowed HTML attributes per tag. + :param css_styles: List of allowed CSS properties (e.g., ["color"]). + """ + super().__init__(*args, **kwargs) + + self.tags = tags + self.attrs = attrs + self.css_styles = css_styles + + def _deserialize(self, value, attr, data, **kwargs): + """Run bleach sanitize with CSS support.""" + value = super()._deserialize(value, attr, data, **kwargs) + + return sanitize_html( + value, + tags=self.tags, + attrs=self.attrs, + css_styles=self.css_styles, + ) diff --git a/cds/modules/records/serializers/schemas/project.py b/cds/modules/records/serializers/schemas/project.py index 5ddd620e9..5b46ef555 100644 --- a/cds/modules/records/serializers/schemas/project.py +++ b/cds/modules/records/serializers/schemas/project.py @@ -18,9 +18,12 @@ # 59 Temple Place, Suite 330, Boston, MA 02D111-1307, USA. """Project JSON schema.""" +from cds.modules.records.serializers.json import ( + CUSTOM_ALLOWED_ATTRS, + CUSTOM_ALLOWED_CSS, +) from invenio_jsonschemas import current_jsonschemas from marshmallow import Schema, fields, pre_load, post_load -from marshmallow_utils.fields import SanitizedHTML from ....deposit.api import Project, deposit_video_resolver from .common import ( @@ -32,6 +35,7 @@ KeywordsSchema, LicenseSchema, OaiSchema, + SanitizedHTMLWithCSS, StrictKeysSchema, TitleSchema, TranslationsSchema, @@ -77,7 +81,10 @@ class ProjectSchema(StrictKeysSchema): _deposit = fields.Nested(ProjectDepositSchema, required=True) _cds = fields.Nested(_CDSSSchema, required=True) title = fields.Nested(TitleSchema, required=True) - description = SanitizedHTML() + description = SanitizedHTMLWithCSS( + attrs=CUSTOM_ALLOWED_ATTRS, + css_styles=CUSTOM_ALLOWED_CSS, + ) category = fields.Str(required=True) type = fields.Str(required=True) note = fields.Str() diff --git a/cds/modules/records/serializers/schemas/video.py b/cds/modules/records/serializers/schemas/video.py index e04ce3f0b..315c5530e 100644 --- a/cds/modules/records/serializers/schemas/video.py +++ b/cds/modules/records/serializers/schemas/video.py @@ -18,9 +18,12 @@ # 59 Temple Place, Suite 330, Boston, MA 02D111-1307, USA. """Video JSON schema.""" +from cds.modules.records.serializers.json import ( + CUSTOM_ALLOWED_ATTRS, + CUSTOM_ALLOWED_CSS, +) from invenio_jsonschemas import current_jsonschemas from marshmallow import Schema, fields, pre_load, post_load -from marshmallow_utils.fields import SanitizedHTML from ....deposit.api import Video from ..fields.datetime import DateString from .common import ( @@ -38,6 +41,7 @@ OaiSchema, RelatedIdentifiersSchema, RelatedLinksSchema, + SanitizedHTMLWithCSS, StrictKeysSchema, TitleSchema, TranslationsSchema, @@ -131,7 +135,9 @@ class VideoSchema(StrictKeysSchema): contributors = fields.Nested(ContributorSchema, many=True, required=True) copyright = fields.Nested(CopyrightSchema) date = DateString(required=True) - description = SanitizedHTML(required=True) + description = SanitizedHTMLWithCSS( + attrs=CUSTOM_ALLOWED_ATTRS, css_styles=CUSTOM_ALLOWED_CSS, required=True + ) doi = DOI() duration = fields.Str() external_system_identifiers = fields.Nested( @@ -147,7 +153,7 @@ class VideoSchema(StrictKeysSchema): note = fields.Str() publication_date = fields.Str() recid = fields.Number() - legacy_recid =fields.Number() + legacy_recid = fields.Number() related_links = fields.Nested(RelatedLinksSchema, many=True) report_number = fields.List(fields.Str, many=True) schema = fields.Str(attribute="$schema", data_key="$schema") @@ -158,15 +164,11 @@ class VideoSchema(StrictKeysSchema): _curation = fields.Nested(CurationSchema) additional_titles = fields.List(fields.Nested(AdditionalTitlesSchema)) additional_descriptions = fields.List(fields.Nested(AdditionalDescriptionsSchema)) - alternate_identifiers = fields.Nested( - AlternateIdentifiersSchema, many=True - ) - related_identifiers = fields.Nested( - RelatedIdentifiersSchema, many=True - ) + alternate_identifiers = fields.Nested(AlternateIdentifiersSchema, many=True) + related_identifiers = fields.Nested(RelatedIdentifiersSchema, many=True) collections = fields.List(fields.Str, many=True) additional_languages = fields.List(fields.Str, many=True) - + # Preservation fields location = fields.Str() original_source = fields.Str()