PageRenderTime 44ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/BlogEngine/DotNetSlave.BusinessLogic/Web/HttpHandlers/CssHandler.cs

#
C# | 337 lines | 188 code | 54 blank | 95 comment | 17 complexity | f740ffafd434351722d92f0ee9478576 MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0, BSD-3-Clause
  1. namespace BlogEngine.Core.Web.HttpHandlers
  2. {
  3. using System;
  4. using System.IO;
  5. using System.Net;
  6. using System.Net.Sockets;
  7. using System.Security;
  8. using System.Text.RegularExpressions;
  9. using System.Web;
  10. using System.Web.Caching;
  11. using System.Linq;
  12. using BlogEngine.Core.Web.HttpModules;
  13. /// <summary>
  14. /// Removes whitespace in all stylesheets added to the
  15. /// header of the HTML document in site.master.
  16. /// </summary>
  17. public class CssHandler : IHttpHandler
  18. {
  19. #region Events
  20. /// <summary>
  21. /// Occurs when the requested file does not exist;
  22. /// </summary>
  23. public static event EventHandler<EventArgs> BadRequest;
  24. /// <summary>
  25. /// Occurs when a file is served;
  26. /// </summary>
  27. public static event EventHandler<EventArgs> Served;
  28. /// <summary>
  29. /// Occurs when the requested file does not exist;
  30. /// </summary>
  31. public static event EventHandler<EventArgs> Serving;
  32. #endregion
  33. #region Properties
  34. /// <summary>
  35. /// Gets a value indicating whether another request can use the <see cref = "T:System.Web.IHttpHandler"></see> instance.
  36. /// </summary>
  37. /// <value></value>
  38. /// <returns>true if the <see cref = "T:System.Web.IHttpHandler"></see> instance is reusable; otherwise, false.</returns>
  39. public bool IsReusable
  40. {
  41. get
  42. {
  43. return false;
  44. }
  45. }
  46. #endregion
  47. #region Implemented Interfaces
  48. #region IHttpHandler
  49. /// <summary>
  50. /// Enables processing of HTTP Web requests by a custom
  51. /// HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"></see> interface.
  52. /// </summary>
  53. /// <param name="context">
  54. /// An <see cref="T:System.Web.HttpContext"></see> object that provides
  55. /// references to the intrinsic server objects
  56. /// (for example, Request, Response, Session, and Server) used to service HTTP requests.
  57. /// </param>
  58. public void ProcessRequest(HttpContext context)
  59. {
  60. var request = context.Request;
  61. string fileName = (string)request.QueryString["name"];
  62. if (!string.IsNullOrEmpty(fileName))
  63. {
  64. fileName = fileName.Replace(BlogSettings.Instance.Version(), string.Empty);
  65. OnServing(fileName);
  66. if (StringComparer.InvariantCultureIgnoreCase.Compare(Path.GetExtension(fileName), ".css") != 0)
  67. {
  68. throw new SecurityException("Invalid CSS file extension");
  69. }
  70. string cacheKey = request.RawUrl.Trim();
  71. string css = (string)Blog.CurrentInstance.Cache[cacheKey];
  72. if (String.IsNullOrEmpty(css))
  73. {
  74. if (fileName.StartsWith("http", StringComparison.OrdinalIgnoreCase))
  75. {
  76. css = RetrieveRemoteCss(fileName, cacheKey);
  77. }
  78. else
  79. {
  80. css = RetrieveLocalCss(fileName, cacheKey);
  81. }
  82. }
  83. // Make sure css isn't empty
  84. if (!string.IsNullOrEmpty(css))
  85. {
  86. // Configure response headers
  87. SetHeaders(css.GetHashCode(), context);
  88. context.Response.Write(css);
  89. // Check if we should compress content
  90. if (BlogSettings.Instance.EnableHttpCompression)
  91. {
  92. CompressionModule.CompressResponse(context);
  93. }
  94. OnServed(fileName);
  95. }
  96. else
  97. {
  98. OnBadRequest(fileName);
  99. context.Response.Status = "404 Bad Request";
  100. }
  101. }
  102. }
  103. #endregion
  104. #endregion
  105. #region Methods
  106. /// <summary>
  107. /// Called when [bad request].
  108. /// </summary>
  109. /// <param name="file">The file name.</param>
  110. private static void OnBadRequest(string file)
  111. {
  112. if (BadRequest != null)
  113. {
  114. BadRequest(file, EventArgs.Empty);
  115. }
  116. }
  117. /// <summary>
  118. /// Called when [served].
  119. /// </summary>
  120. /// <param name="file">The file name.</param>
  121. private static void OnServed(string file)
  122. {
  123. if (Served != null)
  124. {
  125. Served(file, EventArgs.Empty);
  126. }
  127. }
  128. /// <summary>
  129. /// Called when [serving].
  130. /// </summary>
  131. /// <param name="file">The file name.</param>
  132. private static void OnServing(string file)
  133. {
  134. if (Serving != null)
  135. {
  136. Serving(file, EventArgs.Empty);
  137. }
  138. }
  139. /// <summary>
  140. /// Call this method to do any post-processing on the css before its returned in the context response.
  141. /// </summary>
  142. /// <param name="css"></param>
  143. /// <returns></returns>
  144. private static string ProcessCss(string css)
  145. {
  146. if (BlogSettings.Instance.RemoveWhitespaceInStyleSheets)
  147. {
  148. css = StripWhitespace(css);
  149. }
  150. return css;
  151. }
  152. /// <summary>
  153. /// Retrieves the local CSS from the disk
  154. /// </summary>
  155. /// <param name="file">
  156. /// The file name.
  157. /// </param>
  158. /// <param name="cacheKey">
  159. /// The key used to insert this script into the cache.
  160. /// </param>
  161. /// <returns>
  162. /// The retrieve local css.
  163. /// </returns>
  164. private static string RetrieveLocalCss(string file, string cacheKey)
  165. {
  166. var path = HttpContext.Current.Server.MapPath(file);
  167. try
  168. {
  169. string css;
  170. using (var reader = new StreamReader(path))
  171. {
  172. css = reader.ReadToEnd();
  173. }
  174. css = ProcessCss(css);
  175. Blog.CurrentInstance.Cache.Insert(cacheKey, css, new CacheDependency(path));
  176. return css;
  177. }
  178. catch
  179. {
  180. return string.Empty;
  181. }
  182. }
  183. /// <summary>
  184. /// Retrieves and caches the specified remote CSS.
  185. /// </summary>
  186. /// <param name="file">
  187. /// The remote URL
  188. /// </param>
  189. /// <param name="cacheKey">
  190. /// The key used to insert this script into the cache.
  191. /// </param>
  192. /// <returns>
  193. /// The retrieve remote css.
  194. /// </returns>
  195. private static string RetrieveRemoteCss(string file, string cacheKey)
  196. {
  197. Uri url;
  198. if (Uri.TryCreate(file, UriKind.Absolute, out url))
  199. {
  200. try
  201. {
  202. var remoteFile = new RemoteFile(url, false);
  203. string css = remoteFile.GetFileAsString();
  204. css = ProcessCss(css);
  205. // Insert into cache
  206. Blog.CurrentInstance.Cache.Insert(
  207. cacheKey,
  208. css,
  209. null,
  210. Cache.NoAbsoluteExpiration,
  211. new TimeSpan(3, 0, 0, 0));
  212. return css;
  213. }
  214. catch (SocketException)
  215. {
  216. // The remote site is currently down. Try again next time.
  217. }
  218. }
  219. return string.Empty;
  220. }
  221. /// <summary>
  222. /// This will make the browser and server keep the output
  223. /// in its cache and thereby improve performance.
  224. /// </summary>
  225. /// <param name="hash">
  226. /// The hash number.
  227. /// </param>
  228. /// <param name="context">
  229. /// The context.
  230. /// </param>
  231. private static void SetHeaders(int hash, HttpContext context)
  232. {
  233. var response = context.Response;
  234. response.ContentType = "text/css";
  235. var cache = response.Cache;
  236. cache.VaryByHeaders["Accept-Encoding"] = true;
  237. cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7));
  238. cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
  239. cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
  240. var etag = string.Format("\"{0}\"", hash);
  241. var incomingEtag = context.Request.Headers["If-None-Match"];
  242. cache.SetETag(etag);
  243. cache.SetCacheability(HttpCacheability.Public);
  244. if (String.Compare(incomingEtag, etag) != 0)
  245. {
  246. return;
  247. }
  248. response.Clear();
  249. response.StatusCode = (int)HttpStatusCode.NotModified;
  250. response.SuppressContent = true;
  251. }
  252. /// <summary>
  253. /// Strips the whitespace from any .css file.
  254. /// </summary>
  255. /// <param name="body">
  256. /// The body string.
  257. /// </param>
  258. /// <returns>
  259. /// The strip whitespace.
  260. /// </returns>
  261. private static string StripWhitespace(string body)
  262. {
  263. body = body.Replace(" ", " ");
  264. body = body.Replace(Environment.NewLine, String.Empty);
  265. body = body.Replace("\t", string.Empty);
  266. body = body.Replace(" {", "{");
  267. body = body.Replace(" :", ":");
  268. body = body.Replace(": ", ":");
  269. body = body.Replace(", ", ",");
  270. body = body.Replace("; ", ";");
  271. body = body.Replace(";}", "}");
  272. // sometimes found when retrieving CSS remotely
  273. body = body.Replace(@"?", string.Empty);
  274. // body = Regex.Replace(body, @"/\*[^\*]*\*+([^/\*]*\*+)*/", "$1");
  275. body = Regex.Replace(
  276. body, @"(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,}(?=&nbsp;)|(?<=&ndsp;)\s{2,}(?=[<])", String.Empty);
  277. // Remove comments from CSS
  278. body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty);
  279. return body;
  280. }
  281. #endregion
  282. }
  283. }