Files
Umbraco-CMS/umbraco/cms/businesslogic/template/Template.cs
T

844 lines
31 KiB
C#

using System;
using System.Data;
using System.Linq;
using System.Collections;
using System.Xml;
using umbraco.DataLayer;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Generic;
using umbraco.cms.businesslogic.cache;
using umbraco.BusinessLogic;
using umbraco.IO;
using System.Web;
using umbraco.cms.businesslogic.web;
namespace umbraco.cms.businesslogic.template
{
/// <summary>
/// Summary description for Template.
/// </summary>
public class Template : CMSNode
{
#region Private members
private string _OutputContentType;
private string _design;
private string _alias;
private string _oldAlias;
private int _mastertemplate;
private bool _hasChildrenInitialized = false;
private bool _hasChildren;
#endregion
#region Static members
public static readonly string UmbracoMasterTemplate = SystemDirectories.Umbraco + "/masterpages/default.master";
private static Hashtable _templateAliases = new Hashtable();
private static volatile bool _templateAliasesInitialized = false;
private static object templateCacheSyncLock = new object();
private static readonly string UmbracoTemplateCacheKey = "UmbracoTemplateCache";
private static object _templateLoaderLocker = new object();
private static Guid _objectType = new Guid("6fbde604-4178-42ce-a10b-8a2600a2f07d");
#endregion
public string MasterPageFile
{
get { return IOHelper.MapPath(SystemDirectories.Masterpages + "/" + Alias.Replace(" ", "") + ".master"); }
}
public static Hashtable TemplateAliases
{
get { return _templateAliases; }
set { _templateAliases = value; }
}
#region Constructors
public Template(int id) : base(id) { }
public Template(Guid id) : base(id) { }
#endregion
/// <summary>
/// Used to persist object changes to the database. In Version3.0 it's just a stub for future compatibility
/// </summary>
public override void Save()
{
SaveEventArgs e = new SaveEventArgs();
FireBeforeSave(e);
if (!e.Cancel)
{
FlushCache();
base.Save();
FireAfterSave(e);
}
}
public string GetRawText()
{
return base.Text;
}
public override string Text
{
get
{
string tempText = base.Text;
if (!tempText.StartsWith("#"))
return tempText;
else
{
language.Language lang = language.Language.GetByCultureCode(System.Threading.Thread.CurrentThread.CurrentCulture.Name);
if (lang != null)
{
if (Dictionary.DictionaryItem.hasKey(tempText.Substring(1, tempText.Length - 1)))
{
Dictionary.DictionaryItem di = new Dictionary.DictionaryItem(tempText.Substring(1, tempText.Length - 1));
if (di != null)
return di.Value(lang.id);
}
}
return "[" + tempText + "]";
}
}
set
{
FlushCache();
base.Text = value;
}
}
public string OutputContentType
{
get { return _OutputContentType; }
set { _OutputContentType = value; }
}
protected override void setupNode()
{
base.setupNode();
IRecordsReader dr = SqlHelper.ExecuteReader("Select alias,design,master from cmsTemplate where nodeId = " + this.Id);
if (dr.HasRecords)
{
dr.Read();
_alias = dr.GetString("alias");
_design = dr.GetString("design");
//set the master template to zero if it's null
_mastertemplate = dr.IsNull("master") ? 0 : dr.GetInt("master");
}
dr.Close();
// test for masterpages
if (UmbracoSettings.UseAspNetMasterPages)
{
_design = getMasterPageContent();
}
}
private bool isMasterPageSyntax(string code)
{
return code.Contains("<%@ Master") || code.Contains("<umbraco:Item") || code.Contains("<asp:") || code.Contains("<umbraco:Macro");
}
private string getMasterPageContent()
{
string masterpageContent = "";
if (!File.Exists(MasterPageFile))
SaveAsMasterPage();
System.IO.TextReader tr = new StreamReader(MasterPageFile);
masterpageContent = tr.ReadToEnd();
tr.Close();
return masterpageContent;
}
public string Alias
{
get { return _alias; }
set
{
FlushCache();
_oldAlias = _alias;
_alias = value;
SqlHelper.ExecuteNonQuery("Update cmsTemplate set alias = @alias where NodeId = " + this.Id, SqlHelper.CreateParameter("@alias", _alias));
_templateAliasesInitialized = false;
initTemplateAliases();
}
}
public bool HasMasterTemplate
{
get { return (_mastertemplate > 0); }
}
public override bool HasChildren
{
get
{
if (!_hasChildrenInitialized)
{
_hasChildren = SqlHelper.ExecuteScalar<int>("select count(NodeId) as tmp from cmsTemplate where master = " + Id) > 0;
}
return _hasChildren;
}
set
{
_hasChildrenInitialized = true;
_hasChildren = value;
}
}
public int MasterTemplate
{
get { return _mastertemplate; }
set
{
FlushCache();
_mastertemplate = value;
//set to null if it's zero
object masterVal = value;
if (value == 0) masterVal = DBNull.Value;
SqlHelper.ExecuteNonQuery("Update cmsTemplate set master = @master where NodeId = @nodeId",
SqlHelper.CreateParameter("@master", masterVal),
SqlHelper.CreateParameter("@nodeId", this.Id));
}
}
public string Design
{
get { return _design; }
set
{
FlushCache();
_design = value.Trim(Environment.NewLine.ToCharArray());
// NH: Removing an generating the directive can mess up code behind
// We don't store the masterpage directive in the design value
// if (_design.StartsWith("<%@"))
// _design = _design.Substring(_design.IndexOf("%>") + 3).Trim(Environment.NewLine.ToCharArray());
if (UmbracoSettings.UseAspNetMasterPages && isMasterPageSyntax(_design))
{
SaveMasterPageFile(_design);
SqlHelper.ExecuteNonQuery("Update cmsTemplate set design = @design where NodeId = @id",
SqlHelper.CreateParameter("@design", value),
SqlHelper.CreateParameter("@id", Id));
}
else
SqlHelper.ExecuteNonQuery("Update cmsTemplate set design = @design where NodeId = @id",
SqlHelper.CreateParameter("@design", value),
SqlHelper.CreateParameter("@id", Id));
}
}
public XmlNode ToXml(XmlDocument doc)
{
XmlNode template = doc.CreateElement("Template");
template.AppendChild(xmlHelper.addTextNode(doc, "Name", this.Text));
template.AppendChild(xmlHelper.addTextNode(doc, "Alias", this.Alias));
if (this.MasterTemplate != 0)
{
template.AppendChild(xmlHelper.addTextNode(doc, "Master", new Template(this.MasterTemplate).Alias));
}
template.AppendChild(xmlHelper.addCDataNode(doc, "Design", this.Design));
return template;
}
/// <summary>
/// Removes any references to this templates from child templates, documenttypes and documents
/// </summary>
public void RemoveAllReferences()
{
if (HasChildren)
{
foreach (Template t in Template.GetAllAsList().FindAll(delegate(Template t) { return t.MasterTemplate == this.Id; }))
{
t.MasterTemplate = 0;
}
}
RemoveFromDocumentTypes();
// remove from documents
Document.RemoveTemplateFromDocument(this.Id);
}
public void RemoveFromDocumentTypes()
{
foreach (DocumentType dt in DocumentType.GetAllAsList().Where(x => x.allowedTemplates.Select(t => t.Id).Contains(this.Id)))
{
dt.RemoveTemplate(this.Id);
}
}
public static Template MakeNew(string Name, BusinessLogic.User u, Template master)
{
Template t = MakeNew(Name, u);
t.MasterTemplate = master.Id;
t.Design = "";
if (UmbracoSettings.UseAspNetMasterPages)
{
string design = t.getMasterPageHeader() + "\n";
foreach (string cpId in master.contentPlaceholderIds())
{
design += "<asp:content ContentPlaceHolderId=\"" + cpId + "\" runat=\"server\">\n\t\n</asp:content>\n\n";
}
t.Design = design;
}
t.Save();
return t;
}
public static Template MakeNew(string Name, BusinessLogic.User u)
{
// CMSNode MakeNew(int parentId, Guid objectType, int userId, int level, string text, Guid uniqueID)
CMSNode n = CMSNode.MakeNew(-1, _objectType, u.Id, 1, Name, Guid.NewGuid());
Name = Name.Replace("/", ".").Replace("\\", "");
if (Name.Length > 100)
Name = Name.Substring(0, 95) + "...";
SqlHelper.ExecuteNonQuery("INSERT INTO cmsTemplate (NodeId, Alias, design, master) VALUES (@nodeId, @alias, @design, @master)",
SqlHelper.CreateParameter("@nodeId", n.Id),
SqlHelper.CreateParameter("@alias", Name),
SqlHelper.CreateParameter("@design", ' '),
SqlHelper.CreateParameter("@master", DBNull.Value));
Template t = new Template(n.Id);
NewEventArgs e = new NewEventArgs();
t.OnNew(e);
return t;
}
public static Template GetByAlias(string Alias)
{
try
{
return new Template(SqlHelper.ExecuteScalar<int>("select nodeId from cmsTemplate where alias = @alias", SqlHelper.CreateParameter("@alias", Alias)));
}
catch
{
return null;
}
}
[Obsolete("Obsolete, please use GetAllAsList() method instead", true)]
public static Template[] getAll()
{
return GetAllAsList().ToArray();
}
public static List<Template> GetAllAsList()
{
Guid[] ids = CMSNode.TopMostNodeIds(_objectType);
List<Template> retVal = new List<Template>();
foreach (Guid id in ids)
{
retVal.Add(new Template(id));
}
retVal.Sort(delegate(Template t1, Template t2) { return t1.Text.CompareTo(t2.Text); });
return retVal;
}
public static int GetTemplateIdFromAlias(string alias)
{
initTemplateAliases();
if (TemplateAliases.ContainsKey(alias))
return (int)TemplateAliases[alias];
else
return 0;
}
private static void initTemplateAliases()
{
if (!_templateAliasesInitialized)
{
lock (_templateLoaderLocker)
{
//double check
if (!_templateAliasesInitialized)
{
_templateAliases.Clear();
foreach (Template t in GetAllAsList())
TemplateAliases.Add(t.Alias.ToLower(), t.Id);
_templateAliasesInitialized = true;
}
}
}
}
public override void delete()
{
// don't allow template deletion if it has child templates
if (this.HasChildren)
{
Log.Add(LogTypes.Error, this.Id, "Can't delete a master template. Remove any bindings from child templates first.");
throw new InvalidOperationException("Can't delete a master template. Remove any bindings from child templates first.");
}
// NH: Changed this; if you delete a template we'll remove all references instead of
// throwing an exception
if (DocumentType.GetAllAsList().Where(x => x.allowedTemplates.Select(t => t.Id).Contains(this.Id)).Count() > 0)
{
// the uncommented code below have been refactored into removeAllReferences method that clears template
// from documenttypes, subtemplates and documents.
RemoveAllReferences();
/*
// Added to remove template doctype relationship
SqlHelper.ExecuteNonQuery("delete from cmsDocumentType where templateNodeId =" + this.Id);
// Need to update any other template that references this one as it's master to NULL
SqlHelper.ExecuteNonQuery("update cmsTemplate set [master] = NULL where [master] = " + this.Id);
*/
// don't allow template deletion if it is in use
// (get all doc types and filter based on any that have the template id of this one)
/*
Log.Add(LogTypes.Error, this.Id, "Can't delete a template that is assigned to existing content");
throw new InvalidOperationException("Can't delete a template that is assigned to existing content");
*/
}
DeleteEventArgs e = new DeleteEventArgs();
FireBeforeDelete(e);
if (!e.Cancel)
{
//re-set the template aliases
_templateAliasesInitialized = false;
initTemplateAliases();
//delete the template
SqlHelper.ExecuteNonQuery("delete from cmsTemplate where NodeId =" + this.Id);
base.delete();
// remove masterpages
if (System.IO.File.Exists(MasterPageFile))
System.IO.File.Delete(MasterPageFile);
FireAfterDelete(e);
}
}
public void SaveAsMasterPage()
{
SaveMasterPageFile(ConvertToMasterPageSyntax(Design));
}
public string GetMasterContentElement(int masterTemplateId)
{
if (masterTemplateId != 0)
{
string masterAlias = new Template(masterTemplateId).Alias.Replace(" ", "");
return
String.Format("<asp:Content ContentPlaceHolderID=\"{1}ContentPlaceHolder\" runat=\"server\">",
Alias.Replace(" ", ""), masterAlias);
}
else
return
String.Format("<asp:Content ContentPlaceHolderID=\"ContentPlaceHolderDefault\" runat=\"server\">",
Alias.Replace(" ", ""));
}
public List<string> contentPlaceholderIds()
{
List<string> retVal = new List<string>();
string masterPageFile = this.MasterPageFile;
string mp = System.IO.File.ReadAllText(masterPageFile);
string pat = "<asp:ContentPlaceHolder+(\\s+[a-zA-Z]+\\s*=\\s*(\"([^\"]*)\"|'([^']*)'))*\\s*/?>";
Regex r = new Regex(pat, RegexOptions.IgnoreCase);
Match m = r.Match(mp);
while (m.Success)
{
CaptureCollection cc = m.Groups[3].Captures;
foreach (Capture c in cc)
{
if (c.Value != "server")
retVal.Add(c.Value);
}
m = m.NextMatch();
}
return retVal;
}
public string ConvertToMasterPageSyntax(string templateDesign)
{
string masterPageContent = GetMasterContentElement(MasterTemplate) + "\n";
masterPageContent += templateDesign;
// Parse the design for getitems
masterPageContent = EnsureMasterPageSyntax(masterPageContent);
// append ending asp:content element
masterPageContent += "\n</asp:Content>" + Environment.NewLine;
return masterPageContent;
}
public string EnsureMasterPageSyntax(string masterPageContent)
{
replaceElement(ref masterPageContent, "?UMBRACO_GETITEM", "umbraco:Item", true);
replaceElement(ref masterPageContent, "?UMBRACO_GETITEM", "umbraco:Item", false);
// Parse the design for macros
replaceElement(ref masterPageContent, "?UMBRACO_MACRO", "umbraco:Macro", true);
replaceElement(ref masterPageContent, "?UMBRACO_MACRO", "umbraco:Macro", false);
// Parse the design for load childs
masterPageContent = masterPageContent.Replace("<?UMBRACO_TEMPLATE_LOAD_CHILD/>", getAspNetMasterPageContentContainer()).Replace("<?UMBRACO_TEMPLATE_LOAD_CHILD />", getAspNetMasterPageContentContainer());
// Parse the design for aspnet forms
getAspNetMasterPageForm(ref masterPageContent);
masterPageContent = masterPageContent.Replace("</?ASPNET_FORM>", "</form>");
// Parse the design for aspnet heads
masterPageContent = masterPageContent.Replace("</ASPNET_HEAD>", String.Format("<head id=\"{0}Head\" runat=\"server\">", Alias.Replace(" ", "")));
masterPageContent = masterPageContent.Replace("</?ASPNET_HEAD>", "</head>");
return masterPageContent;
}
public void ImportDesign(string design)
{
if (!isMasterPageSyntax(design))
{
Design = ConvertToMasterPageSyntax(design);
}
else
{
Design = design;
}
}
private string getMasterPageHeader()
{
return String.Format("<%@ Master Language=\"C#\" MasterPageFile=\"{0}\" AutoEventWireup=\"true\" %>",
currentMasterTemplateFileName()) + Environment.NewLine;
}
private string currentMasterTemplateFileName()
{
if (MasterTemplate != 0)
return SystemDirectories.Masterpages + "/" + new Template(MasterTemplate).Alias.Replace(" ", "") + ".master";
else
return UmbracoMasterTemplate;
}
public void SaveMasterPageFile(string masterPageContent)
{
// Add header to master page if it doesn't exist
if (!masterPageContent.StartsWith("<%@"))
{
masterPageContent = getMasterPageHeader() + "\n" + masterPageContent;
}
else
{
// verify that the masterpage attribute is the same as the masterpage
string masterHeader = masterPageContent.Substring(0, masterPageContent.IndexOf("%>") + 2).Trim(Environment.NewLine.ToCharArray());
// find the masterpagefile attribute
MatchCollection m = Regex.Matches(masterHeader, "(?<attributeName>\\S*)=\"(?<attributeValue>[^\"]*)\"",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
foreach (Match attributeSet in m)
{
if (attributeSet.Groups["attributeName"].Value.ToLower() == "masterpagefile")
{
// validate the masterpagefile
string currentMasterPageFile = attributeSet.Groups["attributeValue"].Value;
string currentMasterTemplateFile = currentMasterTemplateFileName();
if (currentMasterPageFile != currentMasterTemplateFile)
{
masterPageContent =
masterPageContent.Replace(
attributeSet.Groups["attributeName"].Value + "=\"" + currentMasterPageFile + "\"",
attributeSet.Groups["attributeName"].Value + "=\"" + currentMasterTemplateFile + "\"");
}
}
}
}
//we have a Old Alias if the alias and therefor the masterpage file name has changed...
//so before we save the new masterfile, we'll clear the old one, so we don't up with
//Unused masterpage files
if (!string.IsNullOrEmpty(_oldAlias) && _oldAlias != _alias)
{
//Ensure that child templates have the right master masterpage file name
if (HasChildren)
{
//store children array here because iterating over an Array property object is very inneficient.
var c = Children;
foreach (CMSNode cmn in c)
{
Template t = new Template(cmn.Id);
t.SaveAsMasterPage();
t.Save();
}
}
//then kill the old file..
string _oldFile = IOHelper.MapPath(SystemDirectories.Masterpages + "/" + _oldAlias.Replace(" ", "") + ".master");
if (System.IO.File.Exists(_oldFile))
System.IO.File.Delete(_oldFile);
}
// save the file in UTF-8
File.WriteAllText(MasterPageFile, masterPageContent, System.Text.Encoding.UTF8);
}
private void getAspNetMasterPageForm(ref string design)
{
Match formElement = Regex.Match(design, getElementRegExp("?ASPNET_FORM", false), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
if (formElement != null && formElement.Value != "")
{
string formReplace = String.Format("<form id=\"{0}Form\" runat=\"server\">", Alias.Replace(" ", ""));
if (formElement.Groups.Count == 0)
{
formReplace += "<asp:scriptmanager runat=\"server\"></asp:scriptmanager>";
}
design = design.Replace(formElement.Value, formReplace);
}
}
private string getAspNetMasterPageContentContainer()
{
return String.Format(
"<asp:ContentPlaceHolder ID=\"{0}ContentPlaceHolder\" runat=\"server\"></asp:ContentPlaceHolder>",
Alias.Replace(" ", ""));
}
private void replaceElement(ref string design, string elementName, string newElementName, bool checkForQuotes)
{
MatchCollection m =
Regex.Matches(design, getElementRegExp(elementName, checkForQuotes),
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
foreach (Match match in m)
{
GroupCollection groups = match.Groups;
// generate new element (compensate for a closing trail on single elements ("/"))
string elementAttributes = groups[1].Value;
// test for macro alias
if (elementName == "?UMBRACO_MACRO")
{
Hashtable tags = helpers.xhtml.ReturnAttributes(match.Value);
if (tags["macroAlias"] != null)
elementAttributes = String.Format(" Alias=\"{0}\"", tags["macroAlias"].ToString()) + elementAttributes;
else if (tags["macroalias"] != null)
elementAttributes = String.Format(" Alias=\"{0}\"", tags["macroalias"].ToString()) + elementAttributes;
}
string newElement = "<" + newElementName + " runat=\"server\" " + elementAttributes.Trim() + ">";
if (elementAttributes.EndsWith("/"))
{
elementAttributes = elementAttributes.Substring(0, elementAttributes.Length - 1);
}
else if (groups[0].Value.StartsWith("</"))
// It's a closing element, so generate that instead of a starting element
newElement = "</" + newElementName + ">";
if (checkForQuotes)
{
// if it's inside quotes, we'll change element attribute quotes to single quotes
newElement = newElement.Replace("\"", "'");
newElement = String.Format("\"{0}\"", newElement);
}
design = design.Replace(match.Value, newElement);
}
}
private string getElementRegExp(string elementName, bool checkForQuotes)
{
if (checkForQuotes)
return String.Format("\"<[^>\\s]*\\b{0}(\\b[^>]*)>\"", elementName);
else
return String.Format("<[^>\\s]*\\b{0}(\\b[^>]*)>", elementName);
}
protected virtual void FlushCache()
{
// clear local cache
cache.Cache.ClearCacheItem(GetCacheKey(Id));
}
public static Template GetTemplate(int id)
{
return Cache.GetCacheItem<Template>(GetCacheKey(id), templateCacheSyncLock,
TimeSpan.FromMinutes(30),
delegate
{
try
{
return new Template(id);
}
catch
{
return null;
}
});
}
private void InvalidateCache()
{
Cache.ClearCacheItem(GetCacheKey(this.Id));
}
private static string GetCacheKey(int id)
{
return UmbracoTemplateCacheKey + id;
}
public static Template Import(XmlNode n, User u)
{
string alias = xmlHelper.GetNodeValue(n.SelectSingleNode("Alias"));
Template t = Template.GetByAlias(alias);
if (t == null)
{
t = MakeNew(xmlHelper.GetNodeValue(n.SelectSingleNode("Name")), u);
}
t.Alias = alias;
t.ImportDesign(xmlHelper.GetNodeValue(n.SelectSingleNode("Design")));
return t;
}
#region Events
//EVENTS
/// <summary>
/// The save event handler
/// </summary>
public delegate void SaveEventHandler(Template sender, SaveEventArgs e);
/// <summary>
/// The new event handler
/// </summary>
public delegate void NewEventHandler(Template sender, NewEventArgs e);
/// <summary>
/// The delete event handler
/// </summary>
public delegate void DeleteEventHandler(Template sender, DeleteEventArgs e);
/// <summary>
/// Occurs when [before save].
/// </summary>
public static event SaveEventHandler BeforeSave;
/// <summary>
/// Raises the <see cref="E:BeforeSave"/> event.
/// </summary>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected virtual void FireBeforeSave(SaveEventArgs e)
{
if (BeforeSave != null)
BeforeSave(this, e);
}
/// <summary>
/// Occurs when [after save].
/// </summary>
public static event SaveEventHandler AfterSave;
/// <summary>
/// Raises the <see cref="E:AfterSave"/> event.
/// </summary>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected virtual void FireAfterSave(SaveEventArgs e)
{
if (AfterSave != null)
AfterSave(this, e);
}
/// <summary>
/// Occurs when [new].
/// </summary>
public static event NewEventHandler New;
/// <summary>
/// Raises the <see cref="E:New"/> event.
/// </summary>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected virtual void OnNew(NewEventArgs e)
{
if (New != null)
New(this, e);
}
/// <summary>
/// Occurs when [before delete].
/// </summary>
public static event DeleteEventHandler BeforeDelete;
/// <summary>
/// Raises the <see cref="E:BeforeDelete"/> event.
/// </summary>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected virtual void FireBeforeDelete(DeleteEventArgs e)
{
if (BeforeDelete != null)
BeforeDelete(this, e);
}
/// <summary>
/// Occurs when [after delete].
/// </summary>
public static event DeleteEventHandler AfterDelete;
/// <summary>
/// Raises the <see cref="E:AfterDelete"/> event.
/// </summary>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected virtual void FireAfterDelete(DeleteEventArgs e)
{
if (AfterDelete != null)
AfterDelete(this, e);
}
#endregion
}
}