HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
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
}