File: D:/HostingSpaces/TDijk1/erp-apps.eu/wwwroot/CMSWebParts/Pux/HTML/ResourceBundler.ascx.cs
using CMS.Helpers;
using CMS.PortalControls;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Caching;
using System.Web.UI.HtmlControls;
public partial class CMSWebParts_Pux_HTML_ResourceBundler : CMSAbstractWebPart
{
#region "Properties"
/// <summary>
/// Input files to be bundled. Each file on a separate line.
/// </summary>
public string Files
{
get
{
return ValidationHelper.GetString(GetValue("Files"), string.Empty);
}
set
{
SetValue("Files", value);
}
}
/// <summary>
/// Name of file group (all files in the group will be merged into target file)
/// </summary>
public string Group
{
get
{
return DataHelper.GetNotEmpty(ValidationHelper.GetString(GetValue("Group"), string.Empty), WebPartID);
}
set
{
SetValue("Group", value);
}
}
/// <summary>
/// Target file which will be generated from bundled files.
/// </summary>
public string TargetFileNamePath
{
get
{
return ValidationHelper.GetString(GetValue("TargetFileNamePath"), string.Empty);
}
set
{
SetValue("TargetFileNamePath", value);
}
}
/// <summary>
/// Type of files -- javascript/css.
/// </summary>
public string FilesType
{
get
{
return ValidationHelper.GetString(GetValue("FilesType"), string.Empty);
}
set
{
SetValue("FilesType", value);
}
}
/// <summary>
/// TRUE to bundle files or FALSE to disable the merge and to render as individual elements
/// </summary>
public bool BundleFiles
{
get
{
return ValidationHelper.GetBoolean(GetValue("BundleFiles"), false);
}
set
{
SetValue("BundleFiles", value);
}
}
/// <summary>
/// How long store current bundled version in cache (in minutes)
/// </summary>
public int CacheTimeout
{
get
{
return ValidationHelper.GetInteger(GetValue("CacheTimeout"), 0);
}
set
{
SetValue("CacheTimeout", value);
}
}
/// <summary>
/// Comment to be included at the beginning of the bundle file
/// </summary>
public string BundleComment
{
get
{
return ValidationHelper.GetString(GetValue("BundleComment"), string.Empty);
}
set
{
SetValue("BundleComment", value);
}
}
/// <summary>
/// Whether the tags should be rendered in HEAD section or inline
/// </summary>
public bool IsInHeader
{
get
{
return ValidationHelper.GetBoolean(GetValue("IsInHeader"), false);
}
set
{
SetValue("IsInHeader", value);
}
}
/// <summary>
/// Use asynchronous loading
/// </summary>
public bool AsyncLoad
{
get
{
return ValidationHelper.GetBoolean(GetValue("AsyncLoad"), false);
}
set
{
SetValue("AsyncLoad", value);
}
}
#endregion
#region "Private properties"
/// <summary>
/// Files from multiple webparts will be group together to one budle based on type and final filename
/// </summary>
private List<string> GroupedFiles
{
get
{
return RequestStockHelper.GetItem("puxresourcebundler|" + FilesType + "|" + Group) as List<string>;
}
set
{
RequestStockHelper.Add("puxresourcebundler|" + FilesType + "|" + Group, value);
}
}
/// <summary>
/// Internal tag to determine if current bundle has already been processed (if there are multiple webparts belonging to the same bundle on one page)
/// </summary>
private bool HasBeenProcessed
{
get
{
var value = RequestStockHelper.GetItem("puxresourcebundlerprocessed|" + FilesType + "|" + Group) as bool?;
return value ?? false;
}
set
{
RequestStockHelper.Add("puxresourcebundlerprocessed|" + FilesType + "|" + Group, value);
}
}
/// <summary>
/// Target path of the bundle file.
/// </summary>
private string TargetPath
{
get
{
return string.Format("~/{0}/{1}", CurrentSiteName, TargetFileNamePath);
}
}
/// <summary>
/// Returns TRUE if current process is JavaScript merge
/// </summary>
private bool IsJavascript
{
get
{
return FilesType.Equals("javascript");
}
}
/// <summary>
/// Returns TRUE if current process is CSS merge
/// </summary>
private bool IsCss
{
get
{
return FilesType.Equals("css");
}
}
#endregion
#region "Webpart lifecycle"
public override void OnContentLoaded()
{
base.OnContentLoaded();
SetupControl();
}
protected void SetupControl()
{
if (this.StopProcessing || !Enabled)
{
// Do not process
Visible = false;
}
else
{
if (!string.IsNullOrEmpty(Files))
{
// update "grouped files" property to group files from multiple webparts to one bundle
var requestData = GroupedFiles;
if (requestData == null)
{
requestData = new List<string>();
}
requestData.AddRange(Files.Split("\n".ToCharArray()).Select(p => p.Trim()).Where(p => !string.IsNullOrEmpty(p)).ToList());
GroupedFiles = requestData;
}
}
}
protected void Page_Load(object sender, EventArgs e)
{
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (HasBeenProcessed || GroupedFiles == null)
{
// group has been already run
return;
}
var result = CacheHelper.Cache(cs => GetResourceLinks(), new CacheSettings(BundleFiles ? CacheTimeout : 0, "puxresourcebundler|" + FilesType + "|" + ClientID + "|" + GroupedFiles.Count));
var literal = new LiteralControl(result + Environment.NewLine);
if (IsInHeader)
{
Page.Header.Controls.Add(literal);
}
else
{
Controls.Add(literal);
}
HasBeenProcessed = true;
}
#endregion
#region "Minification methods"
private string GetResourceLinks()
{
List<string> result = null;
// do not merge, render as individual elements
if (!BundleFiles)
{
result = new List<string>();
// return all elements not modified
foreach (var file in GroupedFiles)
{
result.Add(GetResourceLink(file));
}
}
else
{
// bundle files into one
result = CreateBundle();
}
return result.Join(Environment.NewLine);
}
/// <summary>
/// Renders a SCRIPT or LINK element to the HEAD section of current page
/// </summary>
/// <param name="bundleUrl"></param>
private string GetResourceLink(string assetUrl)
{
if (IsJavascript)
{
var specialities = new List<string>();
if (assetUrl.EndsWith("[async]"))
{
assetUrl = assetUrl.Replace("[async]", "").Trim();
specialities.Add("async");
}
else if (AsyncLoad)
{
specialities.Add("async");
}
return string.Format("<script src=\"{0}\"{1}></script>", assetUrl, specialities.Count > 0 ? " " + specialities.Join(" ") : string.Empty);
}
else if (IsCss)
{
// TODO: async
return string.Format("<link href=\"{0}\" rel=\"stylesheet\" type=\"text/css\"/>", assetUrl);
}
return string.Empty;
}
/// <summary>
/// Creates bundle or returns existing if no change was made
/// </summary>
/// <param name="cs"></param>
/// <returns></returns>
private List<string> CreateBundle()
{
var kenticoHashCacheKey = "puxresourcebundlerchecksum|" + ClientID;
var validFiles = new List<string>();
var checksum = string.Empty;
var result = new List<string>();
var externalFiles = GroupedFiles.Where(p => p.StartsWith("http://") || p.StartsWith("https://") || p.StartsWith("//"));
var internalFiles = GroupedFiles.Except(externalFiles);
foreach (var externalFile in externalFiles)
{
// add link to external file
result.Add(GetResourceLink(externalFile));
}
foreach (var file in internalFiles)
{
var fileInputPath = file;
if (file.StartsWith("/"))
{
// add "~" at the beginning
fileInputPath = "~" + file;
}
// load file infos so we can compare current version of files with previously bundled version
if ((IsJavascript && file.EndsWith(".js")) || (IsCss && file.EndsWith(".css")))
{
var filePath = Server.MapPath(fileInputPath);
var fileInfo = new System.IO.FileInfo(filePath);
if (fileInfo == null || !fileInfo.Exists)
{
// file not exists
continue;
}
checksum += string.Format("{0}|{1}|{2}\n", fileInfo.Name, fileInfo.Length, fileInfo.LastWriteTime.TimeOfDay);
validFiles.Add(filePath);
}
}
// calculate hash from the checksum
var hash = CalculateMD5Hash(checksum);
// generate unique URL for this version
var bundleResult = URLHelper.AddParameterToUrl(TargetPath, "v", hash);
var resultPath = Server.MapPath(TargetPath);
result.Add(GetResourceLink(bundleResult));
// check if there are any new changes to be updated in the bundle file
if (!AreFilesModified(kenticoHashCacheKey, resultPath, hash))
{
// files has not been modified, return already existing bundle
return result;
}
// add new hash to cache
CacheHelper.Add(kenticoHashCacheKey, hash, null, DateTime.Now.AddYears(1), Cache.NoSlidingExpiration);
// join files to a bundle
var joinedFiles = JoinFiles(validFiles, hash);
// write file
// add hash on the first line
System.IO.File.WriteAllText(resultPath, joinedFiles.ToString());
return result;
}
/// <summary>
/// Checks if files has been modified since last run of bundle
/// </summary>
/// <param name="kenticoHashCacheKey"></param>
/// <param name="resultPath"></param>
/// <param name="hash"></param>
/// <returns></returns>
private bool AreFilesModified(string kenticoHashCacheKey, string resultPath, string hash)
{
if (!System.IO.File.Exists(resultPath))
{
return true;
}
// try to compare with hash in cache
var cacheHash = CacheHelper.GetItem(kenticoHashCacheKey) as string;
if (!string.IsNullOrEmpty(cacheHash))
{
if (cacheHash.Equals(hash))
{
// hashes are the same, no change
return false;
}
}
else
{
// compare with hash stored on the first line of the result file
using (StreamReader reader = new StreamReader(resultPath))
{
// load first line
string fileHash = (reader.ReadLine() ?? "").Replace("/*#", "").Replace("*/", "");
if (fileHash.Equals(hash))
{
// hashes are the same, no change
return false;
}
}
}
return true;
}
/// <summary>
/// Joins multiple files into one. Appends calculated hash to the first line of the bundle.
/// </summary>
/// <param name="validFiles"></param>
/// <param name="hash"></param>
/// <returns></returns>
private StringBuilder JoinFiles(List<string> validFiles, string hash)
{
var joinedFiles = new StringBuilder();
// add hash as a first line in the file
joinedFiles.AppendLine(string.Format("/*#{0}*/", hash));
if (!string.IsNullOrEmpty(BundleComment))
{
foreach (var commentLine in BundleComment.Split("\n".ToCharArray()))
{
joinedFiles.AppendLine("/* " + commentLine.Trim() + " */");
}
}
// minify files
IJavaScriptMinifier scriptMinifier = null;
ICssMinifier cssMinifier = null;
foreach (var filePath in validFiles)
{
var fileContent = System.IO.File.ReadAllText(filePath);
if (IsJavascript && ScriptHelper.ScriptMinificationEnabled)
{
if (scriptMinifier == null)
{
scriptMinifier = CMS.New.Instance<IJavaScriptMinifier>();
}
fileContent = scriptMinifier.Minify(fileContent);
if (!fileContent.EndsWith(";"))
{
// append semicolon at the end of JS files
fileContent += ";";
}
}
else if (IsCss && CSSHelper.StylesheetMinificationEnabled)
{
if (cssMinifier == null)
{
cssMinifier = CMS.New.Instance<ICssMinifier>();
}
fileContent = cssMinifier.Minify(fileContent);
}
joinedFiles.AppendLine(fileContent);
}
return joinedFiles;
}
#endregion
#region "Helper methods"
/// <summary>
/// Calculates MD5 hash from input text
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public string CalculateMD5Hash(string input)
{
// step 1, calculate MD5 hash from input
MD5 md5 = System.Security.Cryptography.MD5.Create();
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
byte[] hash = md5.ComputeHash(inputBytes);
// step 2, convert byte array to hex string
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
sb.Append(hash[i].ToString("X2"));
}
return sb.ToString();
}
#endregion
}