Skip to content

Commit

Permalink
feat: add utils.cleanify function for HTML sanitization (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
r1cardohj committed Dec 12, 2023
1 parent 11ba463 commit ec4364c
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGES.rst
Expand Up @@ -6,6 +6,13 @@ Changelog

Release date: -

0.5.2
-----

Release date: N/A

- Add ``cleanify`` function to ``flask_ckeditor.utils`` for HTML sanitization.


0.5.1
-----
Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Expand Up @@ -30,3 +30,4 @@ Utils

.. autofunction:: get_url
.. autofunction:: random_filename
.. autofunction:: cleanify
29 changes: 27 additions & 2 deletions docs/basic.rst
Expand Up @@ -62,7 +62,7 @@ to True to use built-in resources. You can use ``custom_url`` to load your custo
CKEditor provides five types of preset (see `comparison table <https://ckeditor.com/cke4/presets-all>`_ for the differences):

- ``basic``
- ``standard`` default value
- ``standard`` (default value)
- ``full``
- ``standard-all`` (only available from CDN)
- ``full-all`` (only available from CDN)
Expand Down Expand Up @@ -100,7 +100,7 @@ It's quite simple, just call ``ckeditor.create()`` in the template:
<input type="submit">
</form>
You can use ``value`` parameter to pass preset value (i.e. ``ckeditor.create(value='blah...blah...')``.
You can use ``value`` parameter to pass preset value (i.e. ``ckeditor.create(value='blah...blah...')``).

Get the Data
------------
Expand All @@ -119,6 +119,31 @@ from ``request.form`` by passing ``ckeditor`` as key:
return render_template('index.html')
Clean the Data
--------------

It's recommended to sanitize the HTML input from user before saving it to the database.

The Flask-CKEditor provides a helper function `cleanify`. To use it, install the extra dependencies:

.. code-block:: bash
$ pip install flask-ckeditor[all]
Then call it for your form data (you could use ``allowed_tags`` to pass a list of custom allowed HTML tags):

.. code-block:: python
from flask import request, render_template
from flask_ckeditor.utils import cleanify
@app.route('/write')
def new_post():
if request.method == 'POST':
data = cleanify(request.form.get('ckeditor')) # <--
return render_template('index.html')
Working with Flask-WTF/WTForms
-------------------------------

Expand Down
20 changes: 19 additions & 1 deletion flask_ckeditor/utils.py
@@ -1,8 +1,13 @@
import os
import uuid

import warnings
from flask import url_for

try:
import bleach
except ImportError:
warnings.warn('The "bleach" library is not installed, `cleanify` function will not be available.')


def get_url(endpoint_or_url):
if endpoint_or_url.startswith(('https://', 'http://', '/')):
Expand All @@ -15,3 +20,16 @@ def random_filename(old_filename):
ext = os.path.splitext(old_filename)[1]
new_filename = uuid.uuid4().hex + ext
return new_filename


def cleanify(text, *, allow_tags=None):
"""Clean the input from client, this function rely on bleach.
:parm text: input str
:parm allow_tags: if you don't want to use default `allow_tags`,
you can provide a Iterable which include html tag string like ['a', 'li',...].
"""
default_allowed_tags = {'a', 'abbr', 'b', 'blockquote', 'code',
'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'}
return bleach.clean(text, tags=allow_tags or default_allowed_tags)
1 change: 1 addition & 0 deletions requirements/example.txt
Expand Up @@ -48,3 +48,4 @@ wtforms==3.1.1
# via
# flask-admin
# flask-wtf

1 change: 1 addition & 0 deletions requirements/tests.in
Expand Up @@ -5,3 +5,4 @@ flask-wtf
flask-admin
flask-sqlalchemy
tablib
bleach
18 changes: 15 additions & 3 deletions requirements/tests.txt
Expand Up @@ -6,14 +6,16 @@
#
--index-url https://pypi.tuna.tsinghua.edu.cn/simple

bleach==6.1.0
# via -r requirements/tests.in
blinker==1.7.0
# via flask
click==8.1.7
# via flask
coverage[toml]==7.3.2
# via
# coverage
# pytest-cov
# via pytest-cov
exceptiongroup==1.2.0
# via pytest
flask==3.0.0
# via
# -r requirements/tests.in
Expand All @@ -26,6 +28,8 @@ flask-sqlalchemy==3.1.1
# via -r requirements/tests.in
flask-wtf==1.2.1
# via -r requirements/tests.in
greenlet==3.0.2
# via sqlalchemy
iniconfig==2.0.0
# via pytest
itsdangerous==2.1.2
Expand All @@ -49,12 +53,20 @@ pytest==7.4.3
# pytest-cov
pytest-cov==4.1.0
# via -r requirements/tests.in
six==1.16.0
# via bleach
sqlalchemy==2.0.23
# via flask-sqlalchemy
tablib==3.5.0
# via -r requirements/tests.in
tomli==2.0.1
# via
# coverage
# pytest
typing-extensions==4.8.0
# via sqlalchemy
webencodings==0.5.1
# via bleach
werkzeug==3.0.1
# via flask
wtforms==3.1.1
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -33,6 +33,9 @@
install_requires=[
'Flask'
],
extras_require={
'all': ['flask-wtf', 'bleach']
},
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
Expand Down
63 changes: 63 additions & 0 deletions test_flask_ckeditor.py
Expand Up @@ -9,11 +9,14 @@
"""
import json
import unittest
import sys
import builtins

from flask import Flask, render_template_string, current_app
from flask_wtf import FlaskForm, CSRFProtect

from flask_ckeditor import CKEditorField, _CKEditor, CKEditor, upload_success, upload_fail
from flask_ckeditor.utils import cleanify


class CKEditorTestCase(unittest.TestCase):
Expand Down Expand Up @@ -287,6 +290,66 @@ def test_upload_fail(self):
{'uploaded': 0, 'error': {'message': 'new error message'}}
)

def test_cleanify_input_js(self):
input = 'an <script>evil()</script> example'
clean_ouput = cleanify(input)
self.assertEqual(clean_ouput,
u'an &lt;script&gt;evil()&lt;/script&gt; example')

def test_cleanify_by_allow_tags(self):
input = '<b> hello <a> this is a url </a> !</b> <h1> this is h1 </h1>'
clean_out = cleanify(input, allow_tags=['b'])
self.assertEqual(clean_out,
'<b> hello &lt;a&gt; this is a url &lt;/a&gt; !</b> &lt;h1&gt; this is h1 &lt;/h1&gt;')

def test_cleanify_by_default_allow_tags(self):
self.maxDiff = None
input = """<a>xxxxx</a>
<abbr>xxxxx</abbr>
<b>xxxxxxx</b>
<blockquote>xxxxxxx</blockquote>
<code>print(hello)</code>
<em>xxxxx</em>
<i>xxxxxx</i>
<li>xxxxxx</li>
<ol>xxxxxx</ol>
<pre>xxxxxx</pre>
<strong>xxxxxx</strong>
<ul>xxxxxx</ul>
<h1>xxxxxxx</h1>
<h2>xxxxxxx</h2>
<h3>xxxxxxx</h3>
<h4>xxxxxxx</h4>
<h5>xxxxxxx</h5>
<p>xxxxxxxx</p>
"""
clean_out = cleanify(input)
self.assertEqual(clean_out, input)

def test_import_cleanify_without_install_bleach(self):
origin_import = builtins.__import__
origin_modules = sys.modules.copy()

def import_hook(name, *args, **kwargs):
if name == 'bleach':
raise ImportError('test case module')
else:
return origin_import(name, *args, **kwargs)

if 'flask_ckeditor.utils' in sys.modules:
del sys.modules['flask_ckeditor.utils']
builtins.__import__ = import_hook

with self.assertWarns(UserWarning) as w:
from flask_ckeditor.utils import cleanify # noqa: F401

self.assertEqual(str(w.warning),
'The "bleach" library is not installed, `cleanify` function will not be available.')

# recover default
builtins.__import__ = origin_import
sys.modules = origin_modules


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions tox.ini
Expand Up @@ -10,6 +10,7 @@ deps =
pytest
coverage
flask_wtf
bleach

[testenv:coverage]
commands =
Expand Down

0 comments on commit ec4364c

Please sign in to comment.