Эх сурвалжийг харах

[core] Add HTML validation in debug mode

When the env var DESKTOP_DEBUG is present, we add an HtmlValidationMiddleware,
which runs the response content through libtidy:
- The original response content is always returned to the browser
- The tidy results are logged
- The original HTML and the tidy HTML are saved to a tmp directory
bc Wong 13 жил өмнө
parent
commit
e797285943

+ 110 - 2
desktop/core/src/desktop/middleware.py

@@ -16,10 +16,13 @@
 # limitations under the License.
 
 import logging
+import os.path
+import re
+import tempfile
 
 from django.conf import settings
 from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.core import exceptions
+from django.core import exceptions, urlresolvers
 import django.db
 from django.http import HttpResponseRedirect, HttpResponse
 from django.shortcuts import render_to_response
@@ -30,7 +33,7 @@ import django.views.generic.simple
 import django.contrib.auth.views
 
 import desktop.conf
-from desktop.lib import apputil
+from desktop.lib import apputil, i18n
 from desktop.lib.django_util import render_json, is_jframe_request, PopupException
 from desktop.log.access import access_log, log_page_hit
 from desktop import appmanager
@@ -391,3 +394,108 @@ class DatabaseLoggingMiddleware(object):
           for query in getattr(django.db.connection, "queries", []):
             self.DATABASE_LOG.info("(%s) %s" % (query["time"], query["sql"]))
     return response
+
+
+try:
+  import tidylib
+  _has_tidylib = True
+except Exception, ex:
+  # The exception type is not ImportError. It's actually an OSError.
+  logging.exception("Failed to import tidylib. Is libtidy installed?")
+  _has_tidylib = False
+
+
+class HtmlValidationMiddleware(object):
+  """
+  If configured, validate output html for every response.
+  """
+  def __init__(self):
+    self._logger = logging.getLogger('HtmlValidationMiddleware')
+
+    if not _has_tidylib:
+      logging.error("HtmlValidationMiddleware not activatived: "
+                    "Failed to import tidylib.")
+      return
+
+    # Things that we don't care about
+    self._to_ignore = (
+      re.compile('- Warning: <.*> proprietary attribute "data-'),
+      re.compile('- Warning: trimming empty'),
+      re.compile('- Info:'),
+    )
+
+    # Find the directory to write tidy html output
+    try:
+      self._outdir = os.path.join(tempfile.gettempdir(), 'hue_html_validation')
+      if not os.path.isdir(self._outdir):
+        os.mkdir(self._outdir, 0755)
+    except Exception, ex:
+      self._logger.exception('Failed to get temp directory: %s', (ex,))
+      self._outdir = tempfile.mkdtemp(prefix='hue_html_validation-')
+
+    # Options to pass to libtidy. See
+    # http://tidy.sourceforge.net/docs/quickref.html
+    self._options = {
+      'show-warnings': 1,
+      'output-html': 0,
+      'output-xhtml': 1,
+      'char-encoding': 'utf8',
+      'output-encoding': 'utf8',
+      'indent': 1,
+      'wrap': 0,
+    }
+
+
+  def process_response(self, request, response):
+    if not _has_tidylib or not self._is_html(request, response):
+      return response
+
+    html, errors = tidylib.tidy_document(response.content,
+                                         self._options,
+                                         keep_doc=True)
+    if not errors:
+      return response
+
+    # Filter out what we care about
+    err_list = errors.rstrip().split('\n')
+    err_list = self._filter_warnings(err_list)
+    if not err_list:
+      return response
+
+    try:
+      fn = urlresolvers.resolve(request.path)[0]
+      fn_name = '%s.%s' % (fn.__module__, fn.__name__)
+    except:
+      fn_name = '<unresolved_url>'
+
+    # Write the two versions of html out for offline debugging
+    filename = os.path.join(self._outdir, fn_name)
+    
+    result = "HTML tidy result: %s [%s]:" \
+             "\n\t%s" \
+             "\nPlease see %s.orig %s.tidy\n-------" % \
+             (request.path, fn_name, '\n\t'.join(err_list), filename, filename)
+
+    file(filename + '.orig', 'w').write(i18n.smart_str(response.content))
+    file(filename + '.tidy', 'w').write(i18n.smart_str(html))
+    file(filename + '.info', 'w').write(i18n.smart_str(result))
+
+    self._logger.error(result)
+    return response
+
+  def _filter_warnings(self, err_list):
+    """A hacky way to filter out things that we don't care about."""
+    res = [ ]
+    for err in err_list:
+      for ignore in self._to_ignore:
+        if ignore.search(err):
+          break
+      else:
+        res.append(err)
+    return res
+
+  def _is_html(self, request, response):
+    return not request.is_ajax() and \
+        'html' in response['Content-Type'] and \
+        200 <= response.status_code < 300
+

+ 5 - 0
desktop/core/src/desktop/settings.py

@@ -32,6 +32,7 @@ HUE_DESKTOP_VERSION = pkg_resources.get_distribution("desktop").version or "Unkn
 NICE_NAME = "Hue"
 
 ENV_HUE_PROCESS_NAME = "HUE_PROCESS_NAME"
+ENV_DESKTOP_DEBUG = "DESKTOP_DEBUG"
 
 ############################################################
 # Part 1: Logging and imports.
@@ -118,6 +119,10 @@ MIDDLEWARE_CLASSES = [
     # 'debug_toolbar.middleware.DebugToolbarMiddleware'
 ]
 
+if os.environ.get(ENV_DESKTOP_DEBUG):
+  MIDDLEWARE_CLASSES.append('desktop.middleware.HtmlValidationMiddleware')
+  logging.debug("Will try to validate generated HTML.")
+
 ROOT_URLCONF = 'desktop.urls'
 
 TEMPLATE_DIRS = (