From c83d17e957ae1b725e9d2ea24e6eeba0e341409f Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Mon, 29 Oct 2012 00:35:19 -0300 Subject: reimplemented the link insertion rep barrier --- askbot/conf/minimum_reputation.py | 9 +++++++ askbot/doc/source/changelog.rst | 1 + askbot/models/__init__.py | 21 ++++++++++++++++ askbot/models/post.py | 2 +- askbot/tests/email_alert_tests.py | 6 ++++- askbot/tests/utils_tests.py | 52 +++++++++++++++++++++++++++++++++++++-- askbot/utils/html.py | 39 +++++++++++++++++++++++++++-- 7 files changed, 124 insertions(+), 6 deletions(-) diff --git a/askbot/conf/minimum_reputation.py b/askbot/conf/minimum_reputation.py index 152a2079..359855c9 100644 --- a/askbot/conf/minimum_reputation.py +++ b/askbot/conf/minimum_reputation.py @@ -104,6 +104,15 @@ settings.register( ) ) +settings.register( + livesettings.IntegerValue( + MIN_REP, + 'MIN_REP_TO_INSERT_LINK', + default=10, + description=_('Insert links') + ) +) + settings.register( livesettings.IntegerValue( MIN_REP, diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index a971d9d6..fcc85c21 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,7 @@ Changes in Askbot Development version ------------------- +* Added minimum reputation to insert links and hotlinked images (Evgeny) * Added support of Haystack for search (Adolfo) * Added minimum reputation setting to accept any answer as correct (Evgeny) * Added "VIP" option to groups - if checked, all posts belong to the group and users of that group in the future will be able to moderate those posts. Moderation features for VIP group are in progress (Evgeny) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index c2b6e5c1..ed687f86 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -57,6 +57,7 @@ from askbot.models.widgets import AskWidget, QuestionWidget from askbot import auth from askbot.utils.decorators import auto_now_timestamp from askbot.utils.slug import slugify +from askbot.utils.html import replace_links_with_text from askbot.utils.html import sanitize_html from askbot.utils.diff import textDiff as htmldiff from askbot.utils.url_utils import strip_path @@ -2478,6 +2479,25 @@ def _process_vote(user, post, timestamp=None, cancel=False, vote_type=None): ) return vote +def user_fix_html_links(self, text): + """depending on the user's privilege, allow links + and hotlinked images or replace them with plain text + url + """ + is_simple_user = not self.is_administrator_or_moderator() + has_low_rep = self.reputation < askbot_settings.MIN_REP_TO_INSERT_LINK + if is_simple_user and has_low_rep: + result = replace_links_with_text(text) + if result != text: + message = ungettext( + 'At least %d karma point is required to insert links', + 'At least %d karma points is required to insert links', + askbot_settings.MIN_REP_TO_INSERT_LINK + ) % askbot_settings.MIN_REP_TO_INSERT_LINK + self.message_set.create(message=message) + return result + return text + def user_unfollow_question(self, question = None): self.followed_threads.remove(question.thread) @@ -2790,6 +2810,7 @@ User.add_to_class('get_tag_filtered_questions', user_get_tag_filtered_questions) User.add_to_class('get_messages', get_messages) User.add_to_class('delete_messages', delete_messages) User.add_to_class('toggle_favorite_question', toggle_favorite_question) +User.add_to_class('fix_html_links', user_fix_html_links) User.add_to_class('follow_question', user_follow_question) User.add_to_class('unfollow_question', user_unfollow_question) User.add_to_class('is_following_question', user_is_following_question) diff --git a/askbot/models/post.py b/askbot/models/post.py index daf9c93f..4e841f13 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -503,8 +503,8 @@ class Post(models.Model): last_revision = self.html data = self.parse_post_text() + self.html = author.fix_html_links(data['html']) - self.html = data['html'] newly_mentioned_users = set(data['newly_mentioned_users']) - set([author]) removed_mentions = data['removed_mentions'] diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index f4ae052b..bdb361e5 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -1037,7 +1037,11 @@ class PostApprovalTests(utils.AskbotTestCase): class AbsolutizeUrlsInEmailsTests(utils.AskbotTestCase): - @with_settings(MIN_REP_TO_TRIGGER_EMAIL=1, APP_URL='http://example.com/') + @with_settings( + MIN_REP_TO_TRIGGER_EMAIL=1, + APP_URL='http://example.com/', + MIN_REP_TO_INSERT_LINK=1 + ) def test_urls_are_absolute(self): u1 = self.create_user('u1') max_email = models.EmailFeedSetting.MAX_EMAIL_SCHEDULE diff --git a/askbot/tests/utils_tests.py b/askbot/tests/utils_tests.py index c8526ad1..ed845f48 100644 --- a/askbot/tests/utils_tests.py +++ b/askbot/tests/utils_tests.py @@ -2,6 +2,7 @@ from django.test import TestCase from askbot.tests.utils import with_settings from askbot.utils.url_utils import urls_equal from askbot.utils.html import absolutize_urls +from askbot.utils.html import replace_links_with_text from askbot.conf import settings as askbot_settings class UrlUtilsTests(TestCase): @@ -17,11 +18,58 @@ class UrlUtilsTests(TestCase): self.assertTrue(e('http://cnn.com/path', 'http://cnn.com/path/', True)) self.assertFalse(e('http://cnn.com/path', 'http://cnn.com/path/')) - + + +class ReplaceLinksWithTextTests(TestCase): + """testing correctness of `askbot.utils.html.replace_links_with_text""" + + def test_local_link_not_replaced(self): + text = 'some link' + self.assertEqual(replace_links_with_text(text), text) + + def test_link_without_url_replaced(self): + text = 'some link' + self.assertEqual(replace_links_with_text(text), 'some link') + + def test_external_link_without_text_replaced(self): + text = '' + #in this case we delete the link + self.assertEqual(replace_links_with_text(text), '') + + def test_external_link_with_text_replaced(self): + text = 'some link' + self.assertEqual( + replace_links_with_text(text), + 'https://example.com/ (some link)' + ) + + def test_local_image_not_replaced(self): + text = '' + self.assertEqual(replace_links_with_text(text), text) + + def test_local_url_with_hotlinked_image_replaced(self): + text = 'picture some text' + self.assertEqual( + replace_links_with_text(text), + 'http://example.com/img.png (picture) some text' + ) + + def test_hotlinked_image_without_alt_replaced(self): + text = '' + self.assertEqual( + replace_links_with_text(text), + 'https://example.com/some-image.gif' + ) + + def test_hotlinked_image_with_alt_replaced(self): + text = 'picture' + self.assertEqual( + replace_links_with_text(text), + 'https://example.com/some-image.gif (picture)' + ) class HTMLUtilsTests(TestCase): """tests for :mod:`askbot.utils.html` module""" - @with_settings(APP_URL='http://example.com') def test_absolutize_urls(self): text = """ """ diff --git a/askbot/utils/html.py b/askbot/utils/html.py index 49eddee2..3e1bfba5 100644 --- a/askbot/utils/html.py +++ b/askbot/utils/html.py @@ -1,11 +1,11 @@ """Utilities for working with HTML.""" +from bs4 import BeautifulSoup import html5lib from html5lib import sanitizer, serializer, tokenizer, treebuilders, treewalkers import re import htmlentitydefs from urlparse import urlparse from django.core.urlresolvers import reverse -from django.utils.html import escape from askbot.conf import settings as askbot_settings class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin): @@ -55,12 +55,47 @@ def absolutize_urls(html): img_replacement = '\g"%s/\g" style="max-width:500px;"' % askbot_settings.APP_URL replacement = '\g"%s\g"' % askbot_settings.APP_URL html = url_re1.sub(img_replacement, html) - html= url_re2.sub(img_replacement, html) + html = url_re2.sub(img_replacement, html) html = url_re3.sub(replacement, html) #temporal fix for bad regex with wysiwyg editor return url_re4.sub(replacement, html).replace('%s//' % askbot_settings.APP_URL, '%s/' % askbot_settings.APP_URL) +def replace_links_with_text(html): + """any absolute links will be replaced with the + url in plain text, same with any img tags + """ + def format_url_replacement(url, text): + url = url.strip() + text = text.strip() + url_domain = urlparse(url).netloc + if url and text and url_domain != text and url != text: + return '%s (%s)' % (url, text) + return url or text or '' + + soup = BeautifulSoup(html) + abs_url_re = r'^http(s)?://' + + images = soup.find_all('img') + for image in images: + url = image.get('src', '') + text = image.get('alt', '') + if url == '' or re.match(abs_url_re, url): + image.replaceWith(format_url_replacement(url, text)) + + links = soup.find_all('a') + for link in links: + url = link.get('href', '') + text = ''.join(link.text) or '' + + if text == '':#this is due to an issue with url inlining in comments + link.replaceWith('') + elif url == '' or re.match(abs_url_re, url): + link.replaceWith(format_url_replacement(url, text)) + + return soup.find('body').renderContents() + + def sanitize_html(html): """Sanitizes an HTML fragment.""" p = html5lib.HTMLParser(tokenizer=HTMLSanitizer, -- cgit v1.2.3-1-g7c22