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