bgneal@859: """ bgneal@859: ssl_images is a custom manage.py command to convert forum post and comment bgneal@859: images to https. It does this by rewriting the markup: bgneal@859: - Images with src = http://surfguitar101.com/something are rewritten to be bgneal@859: /something. bgneal@859: - Non SG101 images that use http: are downloaded, resized, and uploaded to bgneal@859: an S3 bucket. The src attribute is replaced with the new S3 URL. bgneal@859: """ bgneal@859: import logging bgneal@859: from optparse import make_option bgneal@859: import os.path bgneal@863: import re bgneal@863: import signal bgneal@868: import urlparse bgneal@859: bgneal@859: from django.core.management.base import NoArgsCommand, CommandError bgneal@859: from django.conf import settings bgneal@863: import markdown.inlinepatterns bgneal@859: bgneal@860: from comments.models import Comment bgneal@860: from forums.models import Post bgneal@860: bgneal@860: bgneal@859: LOGFILE = os.path.join(settings.PROJECT_PATH, 'logs', 'ssl_images.log') bgneal@859: logger = logging.getLogger(__name__) bgneal@859: bgneal@863: IMAGE_LINK_RE = re.compile(markdown.inlinepatterns.IMAGE_LINK_RE) bgneal@863: IMAGE_REF_RE = re.compile(markdown.inlinepatterns.IMAGE_REFERENCE_RE) bgneal@863: bgneal@868: SG101_HOSTS = set(['www.surfguitar101.com', 'surfguitar101.com']) bgneal@866: MODEL_CHOICES = ['comments', 'posts'] bgneal@866: bgneal@863: quit_flag = False bgneal@863: bgneal@863: bgneal@863: def signal_handler(signum, frame): bgneal@863: """SIGINT signal handler""" bgneal@863: global quit_flag bgneal@863: quit_flag = True bgneal@863: bgneal@859: bgneal@859: def _setup_logging(): bgneal@859: logger.setLevel(logging.DEBUG) bgneal@859: logger.propagate = False bgneal@859: handler = logging.FileHandler(filename=LOGFILE, encoding='utf-8') bgneal@859: formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') bgneal@859: handler.setFormatter(formatter) bgneal@859: logger.addHandler(handler) bgneal@859: bgneal@859: bgneal@868: def save_image_to_cloud(src): bgneal@868: # TODO bgneal@868: return src bgneal@868: bgneal@868: bgneal@866: def replace_image_markup(match): bgneal@868: src_parts = match.group(9).split() bgneal@868: if src_parts: bgneal@868: src = src_parts[0] bgneal@868: if src[0] == "<" and src[-1] == ">": bgneal@868: src = src[1:-1] bgneal@868: else: bgneal@868: src = '' bgneal@868: bgneal@868: title = '' bgneal@868: if len(src_parts) > 1: bgneal@868: title = " ".join(src_parts[1:]) bgneal@868: alt = match.group(2) bgneal@868: bgneal@868: new_src = '' bgneal@868: if src: bgneal@868: r = urlparse.urlparse(src) bgneal@868: if r.scheme == 'http': bgneal@868: if r.hostname in SG101_HOSTS: bgneal@868: new_src = r.path # convert to relative path bgneal@868: else: bgneal@868: new_src = save_image_to_cloud(src) bgneal@868: elif r.scheme == 'https': bgneal@868: new_src = src # already https, accept it as-is bgneal@868: bgneal@868: if new_src: bgneal@868: if title: bgneal@868: s = u'![{alt}]({src} "{title}")'.format(alt=alt, src=new_src, title=title) bgneal@868: else: bgneal@868: s = u'![{alt}]({src})'.format(alt=alt, src=new_src) bgneal@868: else: bgneal@868: # something's messed up, convert to a link using original src bgneal@868: s = u'[{alt}]({src})'.format(alt=alt, src=src) bgneal@868: bgneal@868: return s bgneal@860: bgneal@860: bgneal@866: def process_post(text): bgneal@863: """Process the post object: bgneal@863: bgneal@863: A regex substitution is run on the post's text field. This fixes up image bgneal@863: links, getting rid of plain old http sources; either converting to https bgneal@863: or relative style links (if the link is to SG101). bgneal@863: bgneal@863: We also do a search for Markdown image reference markup. We aren't expecting bgneal@863: these, but we will log something if we see any. bgneal@863: bgneal@863: """ bgneal@866: return IMAGE_LINK_RE.sub(replace_image_markup, text) bgneal@863: bgneal@863: bgneal@859: class Command(NoArgsCommand): bgneal@859: help = "Rewrite forum posts and comments to not use http for images" bgneal@859: option_list = NoArgsCommand.option_list + ( bgneal@866: make_option('-m', '--model', bgneal@866: choices=MODEL_CHOICES, bgneal@866: help="which model to update; must be one of {{{}}}".format( bgneal@866: ', '.join(MODEL_CHOICES))), bgneal@860: make_option('-i', '--i', bgneal@859: type='int', bgneal@863: help="optional first slice index; the i in [i:j]"), bgneal@860: make_option('-j', '--j', bgneal@859: type='int', bgneal@863: help="optional second slice index; the j in [i:j]"), bgneal@859: ) bgneal@859: bgneal@859: def handle_noargs(self, **options): bgneal@859: _setup_logging() bgneal@860: logger.info("Starting; arguments received: %s", options) bgneal@859: bgneal@866: if options['model'] not in MODEL_CHOICES: bgneal@866: raise CommandError('Please choose a --model option') bgneal@859: bgneal@866: if options['model'] == 'comments': bgneal@860: qs = Comment.objects.all() bgneal@866: text_attr = 'comment' bgneal@860: else: bgneal@860: qs = Post.objects.all() bgneal@866: text_attr = 'body' bgneal@860: bgneal@860: i, j = options['i'], options['j'] bgneal@860: bgneal@860: if i is not None and i < 0: bgneal@860: raise CommandError("-i must be >= 0") bgneal@860: if j is not None and j < 0: bgneal@860: raise CommandError("-j must be >= 0") bgneal@860: if j is not None and i is not None and j <= i: bgneal@860: raise CommandError("-j must be > -i") bgneal@860: bgneal@860: if i is not None and j is not None: bgneal@860: qs = qs[i:j] bgneal@860: elif i is not None and j is None: bgneal@860: qs = qs[i:] bgneal@860: elif i is None and j is not None: bgneal@860: qs = qs[:j] bgneal@860: bgneal@863: # Install signal handler for ctrl-c bgneal@863: signal.signal(signal.SIGINT, signal_handler) bgneal@863: bgneal@860: s = [] bgneal@860: for model in qs.iterator(): bgneal@863: if quit_flag: bgneal@863: logger.warning("SIGINT received, exiting") bgneal@866: txt = getattr(model, text_attr) bgneal@866: new_txt = process_post(txt) bgneal@866: s.append(new_txt) bgneal@860: bgneal@860: import pprint bgneal@860: pprint.pprint(s)