2009-06-19 07:39:16 +00:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Web;
|
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.IO.Compression;
|
2009-07-13 14:51:26 +00:00
|
|
|
|
using System.Net;
|
2009-07-15 14:29:11 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using umbraco.presentation.ClientDependency.Config;
|
2009-06-19 07:39:16 +00:00
|
|
|
|
|
|
|
|
|
|
namespace umbraco.presentation.ClientDependency
|
|
|
|
|
|
{
|
|
|
|
|
|
public class CompositeDependencyHandler : IHttpHandler
|
|
|
|
|
|
{
|
|
|
|
|
|
static CompositeDependencyHandler()
|
|
|
|
|
|
{
|
|
|
|
|
|
HandlerFileName = DefaultHandlerFileName;
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//MaxHandlerUrlLength = DefaultMaxHandlerUrlLength;
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly string _versionNo = string.Empty;
|
|
|
|
|
|
private const string DefaultHandlerFileName = "DependencyHandler.axd";
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//private const int DefaultMaxHandlerUrlLength = 2000;
|
|
|
|
|
|
|
|
|
|
|
|
private object m_Lock = new object();
|
2009-06-19 07:39:16 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The handler file name, by default this is DependencyHandler.axd.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// If this handler path needs to change, it can be change by setting it in the global.asax on application start
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public static string HandlerFileName { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// When building composite includes, it creates a Base64 encoded string of all of the combined dependency file paths
|
|
|
|
|
|
/// for a given composite group. If this group contains too many files, then the file path with the query string will be very long.
|
|
|
|
|
|
/// This is the maximum allowed number of characters that there is allowed, otherwise an exception is thrown.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// If this handler path needs to change, it can be change by setting it in the global.asax on application start
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public static int MaxHandlerUrlLength { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
bool IHttpHandler.IsReusable
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void IHttpHandler.ProcessRequest(HttpContext context)
|
|
|
|
|
|
{
|
|
|
|
|
|
HttpResponse response = context.Response;
|
|
|
|
|
|
string fileset = context.Server.UrlDecode(context.Request["s"]);
|
|
|
|
|
|
ClientDependencyType type;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
type = (ClientDependencyType)Enum.Parse(typeof(ClientDependencyType), context.Request["t"], true);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException("Could not parse the type set in the request");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(fileset))
|
|
|
|
|
|
throw new ArgumentException("Must specify a fileset in the request");
|
|
|
|
|
|
|
2009-07-16 14:24:37 +00:00
|
|
|
|
string compositeFileName = "";
|
|
|
|
|
|
byte[] outputBytes = null;
|
2009-07-15 14:29:11 +00:00
|
|
|
|
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//get the map to the composite file for this file set, if it exists.
|
|
|
|
|
|
CompositeFileMap map = CompositeFileXmlMapper.Instance.GetCompositeFile(fileset);
|
|
|
|
|
|
|
|
|
|
|
|
if (map != null && map.HasFileBytes)
|
|
|
|
|
|
{
|
|
|
|
|
|
ProcessFromFile(context, map, out compositeFileName, out outputBytes);
|
2009-07-15 14:29:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2009-07-16 14:24:37 +00:00
|
|
|
|
bool fromFile = false;
|
2009-07-15 14:29:11 +00:00
|
|
|
|
|
2009-07-16 14:24:37 +00:00
|
|
|
|
lock (m_Lock)
|
2009-06-19 07:39:16 +00:00
|
|
|
|
{
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//check again...
|
|
|
|
|
|
if (map == null || !map.HasFileBytes)
|
2009-07-13 14:51:26 +00:00
|
|
|
|
{
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//need to do the combining, etc... and save the file map
|
|
|
|
|
|
|
|
|
|
|
|
//get the file list
|
|
|
|
|
|
string[] strFiles = DecodeFrom64(fileset).Split(';');
|
|
|
|
|
|
//combine files
|
|
|
|
|
|
byte[] fileBytes = CompositeFileProcessor.CombineFiles(strFiles, context, type);
|
|
|
|
|
|
//compress data
|
|
|
|
|
|
CompressionType cType = GetCompression(context);
|
|
|
|
|
|
outputBytes = CompositeFileProcessor.CompressBytes(cType, fileBytes);
|
|
|
|
|
|
SetContentEncodingHeaders(context, cType);
|
|
|
|
|
|
//save combined file
|
|
|
|
|
|
compositeFileName = CompositeFileProcessor.SaveCompositeFile(outputBytes, type);
|
|
|
|
|
|
|
|
|
|
|
|
//Update the XML file map
|
|
|
|
|
|
CompositeFileXmlMapper.Instance.CreateMap(fileset, cType.ToString(),
|
|
|
|
|
|
strFiles.Select(x => new FileInfo(context.Server.MapPath(x))).ToList(),
|
|
|
|
|
|
compositeFileName);
|
2009-07-13 14:51:26 +00:00
|
|
|
|
}
|
2009-07-16 14:24:37 +00:00
|
|
|
|
else
|
2009-07-13 14:51:26 +00:00
|
|
|
|
{
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//files are there now, process from file.
|
|
|
|
|
|
fromFile = true;
|
2009-07-13 14:51:26 +00:00
|
|
|
|
}
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
2009-07-13 14:51:26 +00:00
|
|
|
|
|
2009-07-16 14:24:37 +00:00
|
|
|
|
if (fromFile)
|
2009-07-13 14:51:26 +00:00
|
|
|
|
{
|
2009-07-16 14:24:37 +00:00
|
|
|
|
ProcessFromFile(context, map, out compositeFileName, out outputBytes);
|
2009-07-13 14:51:26 +00:00
|
|
|
|
}
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2009-07-16 14:24:37 +00:00
|
|
|
|
SetCaching(context, compositeFileName);
|
|
|
|
|
|
|
|
|
|
|
|
context.Response.ContentType = type == ClientDependencyType.Javascript ? "text/javascript" : "text/css";
|
|
|
|
|
|
context.Response.OutputStream.Write(outputBytes, 0, outputBytes.Length);
|
2009-07-13 14:51:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2009-07-16 14:24:37 +00:00
|
|
|
|
private void ProcessFromFile(HttpContext context, CompositeFileMap map, out string compositeFileName, out byte[] outputBytes)
|
2009-07-13 14:51:26 +00:00
|
|
|
|
{
|
2009-07-16 14:24:37 +00:00
|
|
|
|
//the saved file's bytes are already compressed.
|
|
|
|
|
|
outputBytes = map.GetCompositeFileBytes();
|
|
|
|
|
|
compositeFileName = map.CompositeFileName;
|
|
|
|
|
|
CompressionType cType = (CompressionType)Enum.Parse(typeof(CompressionType), map.CompressionType);
|
|
|
|
|
|
SetContentEncodingHeaders(context, cType);
|
2009-07-13 14:51:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2009-06-19 07:39:16 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sets the output cache parameters and also the client side caching parameters
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="context"></param>
|
2009-07-15 14:29:11 +00:00
|
|
|
|
private void SetCaching(HttpContext context, string fileName)
|
2009-06-19 07:39:16 +00:00
|
|
|
|
{
|
|
|
|
|
|
//This ensures OutputCaching is set for this handler and also controls
|
|
|
|
|
|
//client side caching on the browser side. Default is 10 days.
|
|
|
|
|
|
TimeSpan duration = TimeSpan.FromDays(10);
|
|
|
|
|
|
HttpCachePolicy cache = context.Response.Cache;
|
|
|
|
|
|
cache.SetCacheability(HttpCacheability.Public);
|
2009-07-13 14:51:26 +00:00
|
|
|
|
cache.SetExpires(DateTime.Now.Add(duration));
|
2009-06-19 07:39:16 +00:00
|
|
|
|
cache.SetMaxAge(duration);
|
|
|
|
|
|
cache.SetValidUntilExpires(true);
|
|
|
|
|
|
cache.SetLastModified(DateTime.Now);
|
|
|
|
|
|
cache.SetETag(Guid.NewGuid().ToString());
|
|
|
|
|
|
//set server OutputCache to vary by our params
|
|
|
|
|
|
cache.VaryByParams["t"] = true;
|
|
|
|
|
|
cache.VaryByParams["s"] = true;
|
|
|
|
|
|
//don't allow varying by wildcard
|
|
|
|
|
|
cache.SetOmitVaryStar(true);
|
|
|
|
|
|
//ensure client browser maintains strict caching rules
|
|
|
|
|
|
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
|
|
|
|
|
|
//This is the only way to set the max-age cachability header in ASP.Net!
|
|
|
|
|
|
FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
|
|
maxAgeField.SetValue(cache, duration);
|
2009-07-15 14:29:11 +00:00
|
|
|
|
|
|
|
|
|
|
//make this output cache dependent on the file.
|
|
|
|
|
|
context.Response.AddFileDependency(fileName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sets the content encoding headers based on compressions
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="context"></param>
|
|
|
|
|
|
/// <param name="type"></param>
|
|
|
|
|
|
private void SetContentEncodingHeaders(HttpContext context, CompressionType type)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (type == CompressionType.deflate)
|
|
|
|
|
|
{
|
|
|
|
|
|
context.Response.AddHeader("Content-encoding", "deflate");
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (type == CompressionType.gzip)
|
|
|
|
|
|
{
|
|
|
|
|
|
context.Response.AddHeader("Content-encoding", "gzip");
|
|
|
|
|
|
}
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2009-07-16 14:24:37 +00:00
|
|
|
|
/// Check what kind of compression to use
|
2009-06-19 07:39:16 +00:00
|
|
|
|
/// </summary>
|
2009-07-16 14:24:37 +00:00
|
|
|
|
private CompressionType GetCompression(HttpContext context)
|
2009-07-13 14:51:26 +00:00
|
|
|
|
{
|
2009-07-15 14:29:11 +00:00
|
|
|
|
CompressionType type = CompressionType.none;
|
2009-06-19 07:39:16 +00:00
|
|
|
|
string acceptEncoding = context.Request.Headers["Accept-Encoding"];
|
2009-07-15 14:29:11 +00:00
|
|
|
|
|
2009-06-19 07:39:16 +00:00
|
|
|
|
if (!string.IsNullOrEmpty(acceptEncoding))
|
2009-07-16 14:24:37 +00:00
|
|
|
|
{
|
2009-07-13 14:51:26 +00:00
|
|
|
|
//deflate is faster in .Net according to Mads Kristensen (blogengine.net)
|
|
|
|
|
|
if (acceptEncoding.Contains("deflate"))
|
2009-07-16 14:24:37 +00:00
|
|
|
|
{
|
2009-07-15 14:29:11 +00:00
|
|
|
|
type = CompressionType.deflate;
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
2009-07-13 14:51:26 +00:00
|
|
|
|
else if (acceptEncoding.Contains("gzip"))
|
2009-06-19 07:39:16 +00:00
|
|
|
|
{
|
2009-07-15 14:29:11 +00:00
|
|
|
|
type = CompressionType.gzip;
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2009-07-15 14:29:11 +00:00
|
|
|
|
|
|
|
|
|
|
return type;
|
2009-06-19 07:39:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string DecodeFrom64(string toDecode)
|
|
|
|
|
|
{
|
|
|
|
|
|
byte[] toDecodeAsBytes = System.Convert.FromBase64String(toDecode);
|
|
|
|
|
|
return System.Text.ASCIIEncoding.ASCII.GetString(toDecodeAsBytes);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|