using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Net; using System.Text; using System.Web; using CMS.CMSHelper; using CMS.EventLog; using CMS.GlobalHelper; using CMS.IO; using CMS.IO.Compression; using CMS.OutputFilter; using CMS.PortalEngine; using CMS.SettingsProvider; using CMS.SiteProvider; using CMS.UIControls; using Microsoft.Ajax.Utilities; using ThoughtWorks.QRCode.Codec; /// /// Handler that serves minified and compressed resources. /// public class ResourceHandler : IHttpHandler { #region "Constants" /// /// List of the allowed general file extensions /// private static List mAllowedFileExtensions = new List() { ".woff", ".eot", ".svg", ".ttf", ".txt" }; /// /// Supported querystring argument used to identify javascript files. /// private const string JS_FILE_ARGUMENT = "scriptfile"; /// /// Supported querystring argument used to identify QR code /// private const string QR_CODE_ARGUMENT = "qrcode"; /// /// Supported querystring argument used to identify image files. /// private const string IMAGE_FILE_ARGUMENT = "image"; /// /// Supported querystring argument used to identify general files /// private const string FILE_ARGUMENT = "file"; /// /// Supported querystring argument used to identify newsletter stylesheets stored in a database. /// private const string NEWSLETTERCSS_DATABASE_ARGUMENT = "newslettertemplatename"; /// /// Extension of a CSS file. /// private const string CSS_FILE_EXTENSION = ".css"; /// /// Extension of JS file. /// private const string JS_FILE_EXTENSION = ".js"; /// /// Extension of image file. /// private const string IMAGE_FILE_EXTENSION = "##IMAGE##"; /// /// Extension of general file. /// private const string FILE_EXTENSION = "##FILE##"; /// /// Argument delimiter used for separating multiple values in a parameter. /// private static readonly char[] ARGUMENT_DELIMITER = { ';' }; #endregion #region "Variables" /// /// List of QR code versions that are supported by the system /// private static List mQRVersionsSupported = new List(); #endregion #region "Properties" /// /// Gets a value indicating whether another request can use the IHttpHandler instance. /// public bool IsReusable { get { return true; } } /// /// Site name. /// private static string SiteName { get { return CMSAppBase.ApplicationInitialized ? CMSContext.CurrentSiteName : string.Empty; } } /// /// Gets if page belongs to the live site, otherwise it's a CMS or preview mode page. /// private static bool IsLiveSite { get { return (CMSContext.ViewMode == ViewModeEnum.LiveSite); } } /// /// Gets the number of minutes the resource will be cached on the server. /// private static int CacheMinutes { get { if (CMSAppBase.ApplicationInitialized) { return CacheHelper.CacheImageMinutes(SiteName); } else { return 0; } } } /// /// Gets the number of minutes the resource will be cached on the client. /// private static int ClientCacheMinutes { get { if (CMSAppBase.ApplicationInitialized) { return CacheHelper.ClientCacheMinutes(SiteName); } else { return 0; } } } /// /// Gets if the client caching is enabled. /// private static bool ClientCacheEnabled { get { return CacheHelper.ClientCacheRequested; } } /// /// Gets if the client cache should be revalidated to ensure all data is up-to-date. /// private static bool RevalidateClientCache { get { return CMSAppBase.ApplicationInitialized ? CacheHelper.RevalidateClientCache(SiteName) : true; } } /// /// Gets the number of minutes large files (those over maximum allowed size) will be cached on the server. /// private static int PhysicalFilesCacheMinutes { get { return CacheHelper.PhysicalFilesCacheMinutes; } } /// /// Gets if Not Found response should be used when the resouce cannot be located or if the reuqest should terminate normally. /// private static bool Throw404WhenNotFound { get { return true; } } #endregion #region "Methods" /// /// Processes the incoming HTTP request that and returns the specified stylesheets. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests public void ProcessRequest(HttpContext context) { // Disable debugging if (!DebugHelper.DebugResources) { // Disable the debugging RequestSettings requestSettings = RequestSettings.Current; requestSettings.DebugFiles = false; requestSettings.DebugSecurity = false; requestSettings.DebugCache = false; requestSettings.DebugOutput = false; requestSettings.DebugRequest = false; requestSettings.DebugSQLQueries = false; OutputHelper.LogCurrentOutputToFile = false; } // When no parameters are specified, simply end the response if (!context.Request.QueryString.HasKeys()) { SendNoContent(context); } // Process JS file request if (QueryHelper.Contains(JS_FILE_ARGUMENT)) { ProcessJSFileRequest(context); return; } // Process QR code request if (QueryHelper.Contains(QR_CODE_ARGUMENT)) { ProcessQRCodeRequest(context); return; } // Process image file request if (QueryHelper.Contains(IMAGE_FILE_ARGUMENT)) { ProcessImageFileRequest(context); return; } // Process JS file request if (QueryHelper.Contains(FILE_ARGUMENT)) { ProcessFileRequest(context); return; } // Transfer to newsletter CSS string newsletterTemplateName = QueryHelper.GetString(NEWSLETTERCSS_DATABASE_ARGUMENT, ""); if (!String.IsNullOrEmpty(newsletterTemplateName)) { context.Server.Transfer("~/CMSModules/Newsletters/CMSPages/GetCSS.aspx?newslettertemplatename=" + newsletterTemplateName); return; } // Load the settings CMSCssSettings settings = new CMSCssSettings(); settings.LoadFromQueryString(); // Process the request ProcessRequest(context, settings); } /// /// Processes the given request. /// /// Http context /// CSS Settings private static void ProcessRequest(HttpContext context, CMSCssSettings settings) { CMSOutputResource resource = null; // Get cache setting for physical files int cacheMinutes = PhysicalFilesCacheMinutes; int clientCacheMinutes = cacheMinutes; bool hasVirtualContent = settings.HasVirtualContent(); if (hasVirtualContent) { // Use specific cache settings if DB resources are requested cacheMinutes = CacheMinutes; clientCacheMinutes = ClientCacheMinutes; } // Try to get data from cache (or store them if not found) using (CachedSection cs = new CachedSection(ref resource, cacheMinutes, true, null, "getresource", CMSContext.CurrentSiteName, URLHelper.GetCurrentDomain(), context.Request.QueryString, URLHelper.IsSSL)) { // Not found in cache; load the data if (cs.LoadData) { // Load the data resource = GetResource(settings, URLHelper.Url.ToString(), cs.Cached); // Cache the file if ((resource != null) && cs.Cached) { cs.CacheDependency = resource.CacheDependency; } cs.Data = resource; } } // Send response if there's something to send if (resource != null) { bool allowCache = (!hasVirtualContent || ClientCacheEnabled) && CacheHelper.AlwaysCacheResources; SendResponse(context, resource, allowCache, CSSHelper.StylesheetMinificationEnabled, clientCacheMinutes, false); } else { SendNotFoundResponse(context); } } #endregion #region "General methods" /// /// Combines the given list of resources into a single resource /// /// Resources to combine private static CMSOutputResource CombineResources(IEnumerable resources) { StringBuilder data = new StringBuilder(); StringBuilder etag = new StringBuilder(); DateTime lastModified = DateTimeHelper.ZERO_TIME; List files = new List(); int count = 0; bool atLeastOne = false; string fileName = null; // Build single resource foreach (CMSOutputResource resource in resources) { if (resource != null) { atLeastOne = true; string newData = resource.Data; // Join the data into a single string if ((data.Length > 0) && !String.IsNullOrEmpty(newData)) { // Trim the charset newData = CSSHelper.TrimCharset(newData); if (String.IsNullOrEmpty(newData)) { continue; } data.AppendLine(); data.AppendLine(); } data.Append(newData); // Join e-tags if (etag.Length > 0) { etag.Append('|'); } etag.Append(resource.Etag); // Remember the largest last modified if (resource.LastModified > lastModified) { lastModified = resource.LastModified; } files.AddRange(resource.ComponentFiles); fileName = resource.Name + ".css"; if (!string.IsNullOrEmpty(resource.FileName)) { fileName = resource.FileName; } } count++; } if (!atLeastOne) { return null; } // Build the result CMSOutputResource result = new CMSOutputResource() { Data = data.ToString(), Etag = etag.ToString(), LastModified = lastModified, ComponentFiles = files, Extension = ".css" }; // Set the file name if (count == 1) { result.FileName = ValidationHelper.GetSafeFileName(fileName); } else { result.FileName = "components.css"; } return result; } /// /// Sends a response containing the requested data. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Container with the data to serve /// True, if client caching is enabled, otherwise false /// True, if the data can be served minified, otherwise false /// Number of minutes after which the content in the client cache expires /// If true, the content is static private static void SendResponse(HttpContext context, CMSOutputResource resource, bool allowCache, bool minificationEnabled, int clientCacheMinutes, bool staticContent) { // Set client cache revalidation SetRevalidation(context, staticContent); // Let client use data cached in browser if versions match and there was no change in data if (allowCache && IsResourceUnchanged(context, resource)) { context.Response.ContentType = resource.ContentType; SendNotModifiedResponse(context, resource.LastModified, resource.Etag, clientCacheMinutes, true); return; } else { byte[] content = null; if (resource.Data == null) { // Binary content content = resource.BinaryData; } else { // Text content string contentCoding = null; content = GetOutputData(context, resource, minificationEnabled, out contentCoding); if (contentCoding != ContentCodingEnum.IDENTITY) { context.Response.AppendHeader("Content-Encoding", contentCoding); context.Response.AppendHeader("Vary", "Content-Encoding"); } } if (content == null) { // 404 if no content found RequestHelper.Respond404(); } // Set client caching if (allowCache) { SetClientCaching(context, allowCache, resource.LastModified, resource.Etag, clientCacheMinutes); } context.Response.ContentType = resource.ContentType; // Set the file name and extension if defined if (!String.IsNullOrEmpty(resource.FileName) && !String.IsNullOrEmpty(resource.Extension)) { HTTPHelper.SetFileDisposition(resource.FileName, resource.Extension); } // Do not send output if there's none if ((content != null) && (content.Length > 0)) { context.Response.OutputStream.Write(content, 0, content.Length); } } } /// /// Send a Not Found response when the requested data was not located successfully. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests private static void SendNotFoundResponse(HttpContext context) { if (Throw404WhenNotFound) { RequestHelper.LogRequestOperation("404NotFound", URLHelper.Url.ToString(), 1); RequestHelper.Respond404(); } else { RequestHelper.EndResponse(); } } /// /// Sends the Not Modified response when the data on the client matches those on the server. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Timestamp for the last modification of the data /// Etag used to identify the resources /// Number of minutes after which the content in the client cache expires /// True, if the data can be cached by cache servers on the way, false if only by requesting client private static void SendNotModifiedResponse(HttpContext context, DateTime lastModified, string etag, int clientCacheMinutes, bool publicCache) { // Set the status to Not modified context.Response.StatusCode = (int)HttpStatusCode.NotModified; context.Response.Cache.SetETag(etag); if (publicCache) { context.Response.Cache.SetCacheability(HttpCacheability.Public); } DateTime expires = DateTime.Now.AddMinutes(clientCacheMinutes); // No not allow time in future if (lastModified >= DateTime.Now) { lastModified = DateTime.Now.AddSeconds(-1); } context.Response.Cache.SetLastModified(lastModified); context.Response.Cache.SetExpires(expires); RequestHelper.LogRequestOperation("304NotModified", etag, 1); RequestHelper.EndResponse(); } /// /// Sends the No Content response when there was no data specified in request. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests private static void SendNoContent(HttpContext context) { context.Response.StatusCode = (int)HttpStatusCode.NoContent; RequestHelper.LogRequestOperation("204NoContent", string.Empty, 1); RequestHelper.EndResponse(); } /// /// Sets the client cache revalidation. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// If true, the content is static private static void SetRevalidation(HttpContext context, bool staticContent) { if (!staticContent && RevalidateClientCache) { context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); } else { context.Response.Cache.SetRevalidation(HttpCacheRevalidation.None); } } /// /// Sets the client caching. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Timestamp for the last modification of the data /// Etag used to identify the resources /// Number of minutes after which the content in the client cache expires private static void SetClientCaching(HttpContext context, bool allowCache, DateTime lastModified, string etag, int clientCacheMinutes) { DateTime expires = allowCache ? DateTime.Now.AddMinutes(clientCacheMinutes) : DateTime.Now; // No not allow time in future if (lastModified >= DateTime.Now) { lastModified = DateTime.Now.AddSeconds(-1); } context.Response.Cache.SetLastModified(lastModified); context.Response.Cache.SetExpires(expires); context.Response.Cache.SetETag(etag); context.Response.Cache.SetCacheability(HttpCacheability.Public); } /// /// Checks if resource in the client cache matches the server version. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Resource to check /// true, if resource is unchanged, otherwise false private static bool IsResourceUnchanged(HttpContext context, CMSOutputResource resource) { // Determine the last modified date and etag sent from the browser string currentETag = RequestHelper.GetHeader("If-None-Match", string.Empty); string ifModified = RequestHelper.GetHeader("If-Modified-Since", string.Empty); // If resources match, compare last modification timestamps if ((ifModified != string.Empty) && (currentETag == resource.Etag)) { // Get first part of header (colons can delimit additional data) DateTime modifiedStamp; if (DateTime.TryParse(ifModified.Split(";".ToCharArray())[0], out modifiedStamp)) { return (resource.LastModified.ToUniversalTime() <= modifiedStamp.ToUniversalTime().AddSeconds(1)); } } return false; } /// /// Checks if this is a revalidation request for a physical file that did not change since the last time. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Full physical path to the file private static void CheckRevalidation(HttpContext context, string path) { var lastModified = FileInfo.New(path).LastWriteTime; // Virtual resource, used only to check if revalidation can be short-circuited CMSOutputResource fileResource = new CMSOutputResource() { Etag = lastModified.ToString(), LastModified = lastModified }; if (IsResourceUnchanged(context, fileResource)) { SendNotModifiedResponse(context, fileResource.LastModified, fileResource.Etag, PhysicalFilesCacheMinutes, true); } } /// /// Reads a file in the given path. /// /// Path to the file /// File extension to check against /// Content of the file private static byte[] ReadBinaryFile(string path, string fileExtension) { // Return empty string if file doesn't exist or is not supported if (!File.Exists(path) || (Path.GetExtension(path) != fileExtension)) { return null; } // Try to read the contents of the file try { return File.ReadAllBytes(path); } catch { return null; } } /// /// Reads a file in the given path. /// /// Path to the file /// File extension to check against /// Content of the file private static string ReadFile(string path, string fileExtension) { // Return empty string if file doesn't exist or is not supported if (!File.Exists(path) || (Path.GetExtension(path) != fileExtension)) { return null; } // Try to read the contents of the file try { return File.ReadAllText(path); } catch { return string.Empty; } } /// /// Compresses a given text. /// /// Text to compress /// Compressed text private static byte[] Compress(string resource) { byte[] compressedBuffer = null; // Uses in-memory deflate stream to compress the resource using (MemoryStream memory = MemoryStream.New()) { using (DeflateStream compressor = DeflateStream.New(memory, CompressionMode.Compress)) { using (StreamWriter writer = StreamWriter.New(compressor)) { writer.Write(resource); } } compressedBuffer = memory.ToArray(); } return compressedBuffer; } /// /// Decompresses a given text. /// /// Text to decompress /// Decompressed text private static string Decompress(byte[] resource) { // Uses in-memory deflate stream to decompress the resource using (MemoryStream memory = MemoryStream.New(resource)) { using (DeflateStream decompressor = DeflateStream.New(memory, CompressionMode.Decompress)) { using (StreamReader reader = StreamReader.New(decompressor)) { return reader.ReadToEnd(); } } } } /// /// Minify supplied source according to settings. /// /// Resource to minifz /// Minifier to use when creating minified version of the data /// True, if the data should be minified, otherwise false /// True, if data should be compressed, otherwise false private static void MinifyResource(CMSOutputResource resource, IResourceMinifier minifier, bool minificationEnabled, bool compressionEnabled) { if (resource == null) { return; } // Set up the settings if (minificationEnabled && (minifier != null)) { resource.MinifiedData = minifier.Minify(resource.Data); } // Check whether the compression is enabled if (compressionEnabled && CMSAppBase.ConnectionAvailable) { compressionEnabled &= RequestHelper.AllowResourceCompression; } // Compress if (compressionEnabled) { resource.CompressedData = Compress(resource.Data); } // Compress and minify if (minificationEnabled && compressionEnabled) { resource.MinifiedCompressedData = Compress(resource.MinifiedData); } } /// /// Returns the data which will be served to client depending on minification and compression settings. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Data container with the data to serve /// True, if the data should be minified, otherwise false /// The content coding to use when sending a response /// Data to serve in a form of a byte block private static byte[] GetOutputData(HttpContext context, CMSOutputResource resource, bool minificationEnabled, out string contentCoding) { // minification must be allowed by the server and minified data must be available bool minified = minificationEnabled && resource.ContainsMinifiedData; // compression must be allowed by server, supported by client and compressed data must be available bool allowCompression = false; if (CMSAppBase.ConnectionAvailable) { allowCompression = RequestHelper.AllowResourceCompression; } bool compressed = allowCompression && RequestHelper.IsGZipSupported() && resource.ContainsCompressedData; // Set default content encoding contentCoding = ContentCodingEnum.IDENTITY; // Get the proper version of resource to serve based on the settings if (!minified && !compressed) { return Encoding.UTF8.GetBytes(resource.Data); } else if (minificationEnabled && !compressed) { return Encoding.UTF8.GetBytes(resource.MinifiedData); } else if (!minified && compressed) { contentCoding = ContentCodingEnum.DEFLATE; return resource.CompressedData; } else if (minified && compressed) { contentCoding = ContentCodingEnum.DEFLATE; return resource.MinifiedCompressedData; } else { return new byte[0]; } } #endregion #region "Specialized methods" /// /// Processes a request for a QR code /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests private static void ProcessQRCodeRequest(HttpContext context) { string code = QueryHelper.GetString(QR_CODE_ARGUMENT, string.Empty); string correction = QueryHelper.GetString("ec", "M"); string encoding = QueryHelper.GetString("e", "B"); int size = QueryHelper.GetInteger("s", 4); int version = QueryHelper.GetInteger("v", 4); int maxSideSize = QueryHelper.GetInteger("maxsidesize", 0); Color fgColor = QueryHelper.GetColor("fc", Color.Black); Color bgColor = QueryHelper.GetColor("bc", Color.White); CMSOutputResource resource = null; // Try to get data from cache (or store them if not found) using (CachedSection cs = new CachedSection(ref resource, PhysicalFilesCacheMinutes, true, null, "getresource|qrcode|", code, encoding, size, version, correction, maxSideSize, fgColor, bgColor, URLHelper.IsSSL)) { // Not found in cache; load the data if (cs.LoadData) { // Validate the request hash if (QueryHelper.ValidateHash("hash", null, false)) { // Prepare the QR code response resource = GetQRCode(code, encoding, size, version, correction, maxSideSize, fgColor, bgColor); } cs.Data = resource; } } // Send response if there's something to send if (resource != null) { bool allowCache = CacheHelper.AlwaysCacheResources; SendResponse(context, resource, allowCache, false, PhysicalFilesCacheMinutes, true); } else { SendNotFoundResponse(context); } } /// /// Processes a request for a general file identified by its URL. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests private static void ProcessFileRequest(HttpContext context) { // Process physical file ProcessFileRequest(context, FILE_ARGUMENT, FILE_EXTENSION, false); } /// /// Processes a request for a image file identified by its URL. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests private static void ProcessImageFileRequest(HttpContext context) { // Get URL to the resource file, resolve it in case it's virtual and map to physical path string url = QueryHelper.GetString(IMAGE_FILE_ARGUMENT, string.Empty); // Trim any potential query from path int queryIndex = url.IndexOf('?'); if (queryIndex >= 0) { url = url.Substring(0, queryIndex); } if (!url.StartsWith("/") && !url.StartsWith("~/")) { // Map path to the default folder and a real file url = StorageHelper.DEFAULT_IMAGES_PATH + url; // If file not found in physical folder, try to map the file if (!File.ExistsRelative(url)) { Path.GetMappedPath(ref url); } } bool useCache = (QueryHelper.GetString("chset", null) == null); // Process physical file ProcessPhysicalFileRequest(context, url, IMAGE_FILE_EXTENSION, false, useCache); } /// /// Processes a request for a JavaScript file identified by its URL. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests private static void ProcessJSFileRequest(HttpContext context) { // Check whether to use minification bool useMinification = false; if (CMSAppBase.ConnectionAvailable) { useMinification = ScriptHelper.ScriptMinificationEnabled; } ProcessFileRequest(context, JS_FILE_ARGUMENT, JS_FILE_EXTENSION, useMinification); } /// /// Processes a request for a file. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// Name of the argument whose value specifies the location of the data /// File extension to check against (to prevent serving unauthorized content) /// True, if the data should be minified, otherwise false private static void ProcessFileRequest(HttpContext context, string queryArgument, string fileExtension, bool minificationEnabled) { // Get URL to the resource file, resolve it in case it's virtual and map to physical path string url = QueryHelper.GetString(queryArgument, string.Empty); // Process physical file ProcessPhysicalFileRequest(context, url, fileExtension, minificationEnabled, true); } /// /// Processes a request for a file. /// /// An HTTPContext object that provides references to the intrinsic server objects used to service HTTP requests /// File URL /// File extension to check against (to prevent serving unauthorized content) /// True, if the data should be minified, otherwise false /// If true, cache is allowed to be used private static void ProcessPhysicalFileRequest(HttpContext context, string url, string fileExtension, bool minificationEnabled, bool useCache) { // Get physical path string path = URLHelper.GetPhysicalPath(URLHelper.GetVirtualPath(url)); // If this is revalidation request, try quick revalidation check before reading the file CheckRevalidation(context, path); CMSOutputResource resource = null; // Try to get data from cache (or store them if not found) using (CachedSection cs = new CachedSection(ref resource, PhysicalFilesCacheMinutes, useCache, null, "getresource", path, URLHelper.IsSSL)) { // Not found in cache; load the data if (cs.LoadData) { // Check the image extension switch (fileExtension) { case IMAGE_FILE_EXTENSION: case FILE_EXTENSION: { // Extension sets string fileExt = Path.GetExtension(path); if ((fileExtension == IMAGE_FILE_EXTENSION) && ImageHelper.IsImage(fileExt)) { // Image file fileExtension = fileExt; } else if ((fileExtension == FILE_EXTENSION) && mAllowedFileExtensions.Contains(fileExt.ToLowerCSafe())) { // General file fileExtension = fileExt; } resource = GetFile(new CMSItem(url), fileExtension, false, true); } break; default: { // Retrieve the file resource, rebase client URLs and wrap it in output container resource = GetFile(new CMSItem(url), fileExtension, false, false); MinifyResource(resource, new JavaScriptMinifier(), minificationEnabled, true); } break; } // Cache the file if ((resource != null) && cs.Cached) { path = StorageHelper.GetRealFilePath(path); if (File.Exists(path)) { cs.CacheDependency = new CMSCacheDependency(path); } } cs.Data = resource; } } // Send response if there's something to send if (resource != null) { bool allowCache = CacheHelper.AlwaysCacheResources; SendResponse(context, resource, allowCache, minificationEnabled, PhysicalFilesCacheMinutes, true); } else { SendNotFoundResponse(context); } } /// /// Retrieves the specified resources and wraps them in an data container. /// /// CSS settings /// Resource name /// If true, the result will be cached /// The data container with the resulting stylesheet data private static CMSOutputResource GetResource(CMSCssSettings settings, string name, bool cached) { List resources = new List(); // Add files if (settings.Files != null) { foreach (CMSItem item in settings.Files) { // Get the resource CMSOutputResource resource = GetFile(item, CSS_FILE_EXTENSION, true, false); resources.Add(resource); } } // Add stylesheets if (settings.Stylesheets != null) { foreach (CMSItem item in settings.Stylesheets) { // Get the resource CMSOutputResource resource = GetStylesheet(item); resources.Add(resource); } } // Add web part containers if (settings.Containers != null) { foreach (CMSItem item in settings.Containers) { // Get the resource CMSOutputResource resource = GetContainer(item); resources.Add(resource); } } // Add web parts if (settings.WebParts != null) { foreach (CMSItem item in settings.WebParts) { // Get the resource CMSOutputResource resource = GetWebPart(item); resources.Add(resource); } } // Add templates if (settings.Templates != null) { foreach (CMSItem item in settings.Templates) { // Get the resource CMSOutputResource resource = GetTemplate(item); resources.Add(resource); } } // Add layouts if (settings.Layouts != null) { foreach (CMSItem item in settings.Layouts) { // Get the resource CMSOutputResource resource = GetLayout(item); resources.Add(resource); } } // Add device layouts if (settings.DeviceLayouts != null) { foreach (CMSItem item in settings.DeviceLayouts) { // Get the resource CMSOutputResource resource = GetDeviceLayout(item); resources.Add(resource); } } // Add transformation containers if (settings.Transformations != null) { foreach (CMSItem item in settings.Transformations) { // Get the resource CMSOutputResource resource = GetTransformation(item); resources.Add(resource); } } // Add web part layouts if (settings.WebPartLayouts != null) { foreach (CMSItem item in settings.WebPartLayouts) { // Get the resource CMSOutputResource resource = GetWebPartLayout(item); resources.Add(resource); } } // Combine to a single output CMSOutputResource result = CombineResources(resources); if (result == null) { return null; } settings.ComponentFiles = result.ComponentFiles; result.ContentType = MimeTypeHelper.GetMimetype(CSS_FILE_EXTENSION, "text/css"); // Resolve the macros if (CSSHelper.ResolveMacrosInCSS) { var context = new MacroContext() { TrackCacheDependencies = cached }; if (cached) { // Add the default dependencies context.AddCacheDependencies(settings.GetCacheDependencies()); context.AddFileCacheDependencies(settings.GetFileCacheDependencies()); } result.Data = CMSContext.ResolveMacros(result.Data, context); if (cached) { // Add cache dependency result.CacheDependency = CacheHelper.GetCacheDependency(context.FileCacheDependencies, context.CacheDependencies); } } else if (cached) { // Only the cache dependency from settings result.CacheDependency = settings.GetCacheDependency(); } result.Data = HTMLHelper.ResolveCSSClientUrls(result.Data, URLHelper.GetAbsoluteUrl("~/CMSPages/GetResource.ashx")); // Minify MinifyResource(result, new CssMinifier(), CSSHelper.StylesheetMinificationEnabled && settings.EnableMinification, settings.EnableCompression); return result; } /// /// Retrieves the stylesheet from file /// /// File item /// File extension /// If true, the CSS URLs are resolved in the output /// If true, the file is a binary file /// The stylesheet data (plain version only) private static CMSOutputResource GetFile(CMSItem item, string extension, bool resolveCSSUrls, bool binary) { string url = item.Name; string relativePath = URLHelper.GetVirtualPath(url); string path = URLHelper.GetPhysicalPath(relativePath); // Get the file content string fileContent = null; byte[] binaryData = null; if (binary) { // Binary file binaryData = ReadBinaryFile(path, extension); } else { // Text file fileContent = ReadFile(path, extension); if (resolveCSSUrls) { fileContent = HTMLHelper.ResolveCSSClientUrls(fileContent, URLHelper.GetAbsoluteUrl(url)); } } var lastModified = FileInfo.New(path).LastWriteTime; // Build the result var resource = new CMSOutputResource() { Data = fileContent, BinaryData = binaryData, Name = URLHelper.ResolveUrl(relativePath), Etag = "file|" + lastModified.ToString(), LastModified = lastModified, FileName = Path.GetFileName(path), Extension = extension }; switch (extension.ToLowerCSafe()) { case CSS_FILE_EXTENSION: resource.ContentType = "text/css"; break; case JS_FILE_EXTENSION: resource.ContentType = "application/x-javascript"; break; default: resource.ContentType = MimeTypeHelper.GetMimetype(extension); break; } return resource; } /// /// Returns true if the given QR code version if supported /// /// Version to check private static bool QRVersionSupported(int version) { // Ensure the number of items in the list while (mQRVersionsSupported.Count < version) { mQRVersionsSupported.Add(null); } bool? supported = mQRVersionsSupported[version - 1]; if (supported == null) { // Validate by the file existence if the version is supported string path = "~/App_Data/CMSModules/QRCode/[Resources.zip]/qrvfr" + version + ".dat"; path = StorageHelper.GetFullFilePhysicalPath(path, null); supported = File.Exists(path); mQRVersionsSupported[version - 1] = supported; } return supported.Value; } /// /// Generates the QR code /// /// Code to generate by the QR code /// Image size, image is rendered with recommended resolution for the QR code /// Encoding, possible options (B - Byte, AN - Alphanumeric, N - Numeric) /// QR code version (by default supported 1 to 10), additional data templates may be put into ~/App_Data/CMS_Modules/QRCode/Resources.zip /// Correction type, possible options (L, M, Q, H) /// Maximum size of the code in pixels, the code will be resized if larger than this size /// Foreground color /// Background color private static CMSOutputResource GetQRCode(string code, string encoding, int size, int version, string correction, int maxSideSize, Color fgColor, Color bgColor) { QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(); try { // Set encoding switch (encoding.ToLowerCSafe()) { case "an": qrCodeEncoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.ALPHA_NUMERIC; break; case "n": qrCodeEncoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.NUMERIC; break; default: qrCodeEncoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE; break; } // Set scale try { qrCodeEncoder.QRCodeScale = size; } catch { qrCodeEncoder.QRCodeScale = 4; } // Set error correction switch (correction.ToLowerCSafe()) { case "l": qrCodeEncoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.L; break; case "q": qrCodeEncoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.Q; break; case "h": qrCodeEncoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.H; break; default: qrCodeEncoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.M; break; } // Set colors qrCodeEncoder.QRCodeForegroundColor = fgColor; qrCodeEncoder.QRCodeBackgroundColor = bgColor; Image image = null; // Attempt to process all versions while (version <= 40) { if (!QRVersionSupported(version)) { // Move to higher version version++; if (version > 40) { throw new Exception("Version higher than 40 is not supported."); } continue; } try { // Try to get requested version qrCodeEncoder.QRCodeVersion = version; image = qrCodeEncoder.Encode(code); break; } catch (IndexOutOfRangeException ex) { // Try next version to fit the data version++; if (version > 40) { throw ex; } } } byte[] bytes = ImageHelper.GetBytes(image, ImageFormat.Png); // Resize to a proper size if (maxSideSize > 0) { // Resize the image by image helper to a proper size ImageHelper ih = new ImageHelper(bytes); image = ih.GetResizedImage(maxSideSize); bytes = ImageHelper.GetBytes(image, ImageFormat.Png); } // Build the result var resource = new CMSOutputResource() { BinaryData = bytes, Name = code, Etag = "QRCode", LastModified = new DateTime(2012, 1, 1), ContentType = MimeTypeHelper.GetMimetype(".png"), FileName = "QRCode.png", Extension = ".png" }; return resource; } catch (Exception ex) { // Report too long text string message = "[GetResource.GetQRCode]: Failed to generate QR code with text '" + code + "', this may be caused by the text being too long. Original message: " + ex.Message; var newEx = new Exception(message, ex); EventLogProvider.LogException("GetResource", "QRCODE", newEx); throw newEx; } } /// /// Retrieve the stylesheet either from the database or file if checked out. /// /// Stylesheet item /// The stylesheet data (plain version only) private static CMSOutputResource GetStylesheet(CMSItem item) { // Get the stylesheet CssStylesheetInfo stylesheetInfo = null; if (item.Name != null) { stylesheetInfo = CssStylesheetInfoProvider.GetCssStylesheetInfo(item.Name); } else { stylesheetInfo = CssStylesheetInfoProvider.GetCssStylesheetInfo(item.ID); } if (stylesheetInfo == null) { return null; } // Get last modified date with dependency on external storage DateTime lastModified = stylesheetInfo.StylesheetLastModified; FileInfo fi = stylesheetInfo.GetFileInfo(CssStylesheetInfo.EXTERNAL_COLUMN_CSS); if (fi != null) { lastModified = fi.LastWriteTime.ToUniversalTime(); } string stylesheetName = stylesheetInfo.StylesheetName; // Build the output var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(stylesheetInfo.StylesheetText, URLHelper.ApplicationPath), Name = stylesheetName, LastModified = lastModified, Etag = "cssstylesheet|" + stylesheetInfo.StylesheetVersionGUID.ToString(), FileName = stylesheetName + ".css", Extension = ".css" }; // Add file dependency on component css file if (fi != null) { resource.ComponentFiles.Add(stylesheetInfo.GetVirtualFileRelativePath(CssStylesheetInfo.EXTERNAL_COLUMN_CSS)); } return resource; } /// /// Retrieves the stylesheets of the web part container from the database. /// /// Container item /// The stylesheet data (plain version only) private static CMSOutputResource GetContainer(CMSItem item) { WebPartContainerInfo containerInfo = null; if (item.Name != null) { containerInfo = WebPartContainerInfoProvider.GetWebPartContainerInfo(item.Name); } else { containerInfo = WebPartContainerInfoProvider.GetWebPartContainerInfo(item.ID); } if (containerInfo == null) { return null; } DateTime lastModified = containerInfo.ContainerLastModified; // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(containerInfo.ContainerCSS, URLHelper.ApplicationPath), LastModified = lastModified, Etag = "webpartcontainer|" + lastModified.ToString(), FileName = ValidationHelper.GetSafeFileName(containerInfo.ContainerName), Extension = ".css" }; return resource; } /// /// Retrieves the stylesheets of the web part layout from the database. /// /// Layout item /// The stylesheet data (plain version only) private static CMSOutputResource GetWebPartLayout(CMSItem item) { WebPartLayoutInfo layoutInfo = null; if (item.Name != null) { layoutInfo = WebPartLayoutInfoProvider.GetWebPartLayoutInfo(item.Name); } else { layoutInfo = WebPartLayoutInfoProvider.GetWebPartLayoutInfo(item.ID); } if (layoutInfo == null) { return null; } // Get last modified date with dependency on external storage DateTime lastModified = layoutInfo.WebPartLayoutLastModified; FileInfo fi = layoutInfo.GetFileInfo(WebPartLayoutInfo.EXTERNAL_COLUMN_CSS); if (fi != null) { lastModified = fi.LastWriteTime.ToUniversalTime(); } // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(layoutInfo.WebPartLayoutCSS, URLHelper.ApplicationPath), LastModified = lastModified, Etag = "webpartlayout|" + layoutInfo.WebPartLayoutVersionGUID, FileName = ValidationHelper.GetSafeFileName(layoutInfo.WebPartLayoutFullName) + ".css", Extension = ".css" }; if (fi != null) { resource.ComponentFiles.Add(layoutInfo.GetVirtualFileRelativePath(WebPartLayoutInfo.EXTERNAL_COLUMN_CSS)); } return resource; } /// /// Retrieves the stylesheets of the page template from the database. /// /// Template item /// The stylesheet data (plain version only) private static CMSOutputResource GetTemplate(CMSItem item) { // Try to get global one PageTemplateInfo templateInfo = null; if (item.Name != null) { string templateName = item.Name; templateInfo = PageTemplateInfoProvider.GetPageTemplateInfo(templateName); // Try to get site one (ad-hoc) if not found if (templateInfo == null) { templateInfo = PageTemplateInfoProvider.GetPageTemplateInfo(templateName, CMSContext.CurrentSiteID); } } else { templateInfo = PageTemplateInfoProvider.GetPageTemplateInfo(item.ID); } if (templateInfo == null) { return null; } // Get last modified date with dependency on external storage DateTime lastModified = templateInfo.PageTemplateLastModified; FileInfo fi = templateInfo.GetFileInfo(PageTemplateInfo.EXTERNAL_COLUMN_CSS); if (fi != null) { lastModified = fi.LastWriteTime.ToUniversalTime(); } // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(templateInfo.PageTemplateCSS, URLHelper.ApplicationPath), LastModified = lastModified, Etag = "template|" + templateInfo.PageTemplateVersionGUID, FileName = ValidationHelper.GetSafeFileName(templateInfo.CodeName), Extension = ".css" }; // Add file dependency on component css file if (fi != null) { resource.ComponentFiles.Add(templateInfo.GetVirtualFileRelativePath(PageTemplateInfo.EXTERNAL_COLUMN_CSS)); } return resource; } /// /// Retrieves the stylesheets of the layout from the database. /// /// Layout item /// The stylesheet data (plain version only) private static CMSOutputResource GetLayout(CMSItem item) { LayoutInfo layoutInfo = null; if (item.Name != null) { layoutInfo = LayoutInfoProvider.GetLayoutInfo(item.Name); } else { layoutInfo = LayoutInfoProvider.GetLayoutInfo(item.ID); } if (layoutInfo == null) { return null; } // Get last modified date with dependency on external storage DateTime lastModified = layoutInfo.LayoutLastModified; FileInfo fi = layoutInfo.GetFileInfo(LayoutInfo.EXTERNAL_COLUMN_CSS); if (fi != null) { lastModified = fi.LastWriteTime.ToUniversalTime(); } // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(layoutInfo.LayoutCSS, URLHelper.ApplicationPath), LastModified = lastModified, Etag = "layout|" + layoutInfo.LayoutVersionGUID, FileName = ValidationHelper.GetSafeFileName(layoutInfo.LayoutCodeName), Extension = ".css" }; // Add file dependency on component css file if (fi != null) { resource.ComponentFiles.Add(layoutInfo.GetVirtualFileRelativePath(LayoutInfo.EXTERNAL_COLUMN_CSS)); } return resource; } /// /// Retrieves the stylesheets of the device layout from the database. /// /// Device layout item /// The stylesheet data (plain version only) private static CMSOutputResource GetDeviceLayout(CMSItem item) { PageTemplateDeviceLayoutInfo layoutInfo = null; if (item.Name != null) { // Not supported, device layout doesn't contain name } else { layoutInfo = PageTemplateDeviceLayoutInfoProvider.GetTemplateDeviceLayoutInfo(item.ID); } if (layoutInfo == null) { return null; } // Get last modified date with dependency on external storage DateTime lastModified = layoutInfo.LayoutLastModified; FileInfo fi = layoutInfo.GetFileInfo(PageTemplateDeviceLayoutInfo.EXTERNAL_COLUMN_CSS); if (fi != null) { lastModified = fi.LastWriteTime.ToUniversalTime(); } // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(layoutInfo.LayoutCSS, URLHelper.ApplicationPath), LastModified = lastModified, Etag = "layout|" + layoutInfo.LayoutVersionGUID, FileName = Convert.ToString(layoutInfo.LayoutID), Extension = ".css" }; // Add file dependency on component css file if (fi != null) { resource.ComponentFiles.Add(layoutInfo.GetVirtualFileRelativePath(PageTemplateDeviceLayoutInfo.EXTERNAL_COLUMN_CSS)); } return resource; } /// /// Retrieves the stylesheet of the web part from the database. /// /// Web part item /// The stylesheet data (plain version only) private static CMSOutputResource GetWebPart(CMSItem item) { WebPartInfo webPartInfo = null; if (item.Name != null) { webPartInfo = WebPartInfoProvider.GetWebPartInfo(item.Name); } else { webPartInfo = WebPartInfoProvider.GetWebPartInfo(item.ID); } if (webPartInfo == null) { return null; } // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(webPartInfo.WebPartCSS, URLHelper.ApplicationPath), LastModified = webPartInfo.WebPartLastModified, Etag = "webpart|" + webPartInfo.WebPartName, FileName = ValidationHelper.GetSafeFileName(webPartInfo.WebPartName) + ".css", Extension = ".css" }; return resource; } /// /// Retrieves the stylesheets of the web part layout from the database. /// /// Layout item /// The stylesheet data (plain version only) private static CMSOutputResource GetTransformation(CMSItem item) { TransformationInfo transformationInfo = null; if (item.Name != null) { transformationInfo = TransformationInfoProvider.GetTransformation(item.Name); } else { transformationInfo = TransformationInfoProvider.GetTransformation(item.ID); } if (transformationInfo == null) { return null; } // Get last modified date with dependency on external storage DateTime lastModified = transformationInfo.TransformationLastModified; FileInfo fi = transformationInfo.GetFileInfo(TransformationInfo.EXTERNAL_COLUMN_CSS); if (fi != null) { lastModified = fi.LastWriteTime.ToUniversalTime(); } // Build the result var resource = new CMSOutputResource() { Data = HTMLHelper.ResolveCSSUrls(transformationInfo.TransformationCSS, URLHelper.ApplicationPath), LastModified = lastModified, Etag = "transformation|" + transformationInfo.TransformationVersionGUID, FileName = ValidationHelper.GetSafeFileName(transformationInfo.TransformationFullName) + ".css", Extension = ".css" }; // Add file dependency on component css file if (fi != null) { resource.ComponentFiles.Add(transformationInfo.GetVirtualFileRelativePath(TransformationInfo.EXTERNAL_COLUMN_CSS)); } return resource; } #endregion } #region "Minifiers" /// /// Defines an interface a compliant minifier must implement to be usable in resource handler. /// public interface IResourceMinifier { /// /// Returns a minified version of a given resource. /// /// Text to be minified /// Minified resource string Minify(string resource); } /// /// Minifier for Cascading StyleSheets. /// public class CssMinifier : IResourceMinifier { /// /// Indicates if parsing failed for specific reason /// private CssException minificationError = null; private bool? mLogMinifierParseError = null; // Indicates if errors from minification should be logged private bool LogMinifierParseError { get { if (mLogMinifierParseError == null) { mLogMinifierParseError = ValidationHelper.GetBoolean(SettingsHelper.AppSettings["CMSLogCSSMinifierParseError"], false); } return (bool)mLogMinifierParseError; } } /// /// Returns a minified version of a given CSS. /// /// CSS to be minified /// Minified CSS public string Minify(string resource) { if (String.IsNullOrEmpty(resource)) { return resource; } // Reset error minificationError = null; try { CssParser parser = new CssParser(); // Ignore error: 'An underscore is not a valid CSS1 or CSS2 identifier character' parser.Settings.IgnoreErrorList = "CSS1008"; parser.Settings.AllowEmbeddedAspNetBlocks = true; parser.CssError += parser_CssError; // Parse the resource string parsed = parser.Parse(resource); if (!String.IsNullOrEmpty(parsed) && (minificationError == null)) { resource = parsed; } } catch (CssException ex) { minificationError = ex; } if (minificationError != null) { if (LogMinifierParseError) { // Log exception to event log if allowed EventLogProvider.LogException("CSS Compression", "MINIFYCSS", minificationError); } // Add the error info to the end of non-minified resource resource += string.Format("\r\n\r\n/* Minification failed (line {0}, error number {1}): {2} */", minificationError.Line, minificationError.Error, minificationError.Message); } return resource; } /// /// Parsing error occured - event handler. /// private void parser_CssError(object sender, CssErrorEventArgs e) { // Do not minify in the case of parse exception and log the error minificationError = e.Exception; } } /// /// Minifier for JavaScript. /// public class JavaScriptMinifier : IResourceMinifier { /// /// Indicates if parsing failed for specific reason /// private JScriptException minificationError = null; /// /// Indicates if the parser can recover when an error occurs during minification /// private bool canRecover = true; private bool? mLogMinifierParseError = null; // Indicates if errors from minification should be logged private bool LogMinifierParseError { get { if (mLogMinifierParseError == null) { mLogMinifierParseError = ValidationHelper.GetBoolean(SettingsHelper.AppSettings["CMSLogJSMinifierParseError"], false); } return (bool)mLogMinifierParseError; } } /// /// Returns a minified version of a given JavaScript. /// /// JavaScript to be minified /// Minified JavaScript public string Minify(string resource) { if (String.IsNullOrEmpty(resource)) { return resource; } // Reset error minificationError = null; canRecover = true; try { JSParser parser = new JSParser(resource); parser.CompilerError += parser_CompilerError; // Parse the resource Block scriptBlock = parser.Parse(null); // Get minified code if no error occurs or parser was able to recover if ((scriptBlock != null) && ((minificationError == null) || ((minificationError != null) && canRecover))) { resource = scriptBlock.ToCode(); } } catch (JScriptException ex) { minificationError = ex; canRecover = false; } if (minificationError != null) { if (LogMinifierParseError) { // Log exception to event log if allowed EventLogProvider.LogException("JS Compression", "MINIFYJS", minificationError); } if (!canRecover) { // Add error info in front of non-minified resource resource += "\r\n\r\n// Minification failed (line " + minificationError.Line.ToString() + "): " + minificationError.Message; } } return resource; } private void parser_CompilerError(object sender, JScriptExceptionEventArgs e) { // Store exception and info if the compiler can recover minificationError = e.Exception; canRecover = e.Exception.CanRecover; } } #endregion