|
|
@@ -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
|
|
|
+
|