diff --git a/src/Umbraco.Core/Models/Css/CssCompactor.cs b/src/Umbraco.Core/Models/Css/CssCompactor.cs new file mode 100644 index 0000000000..4f015fff69 --- /dev/null +++ b/src/Umbraco.Core/Models/Css/CssCompactor.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Css +{ + internal static class CssCompactor + { + #region CssCompactor.Options + + [Flags] + public enum Options + { + None = 0x00, + PrettyPrint = 0x01, + Overwrite = 0x02 + } + + #endregion CssCompactor.Options + + #region Public Methods + + internal static List Compact(string inputFile, string outputFile, string copyright, string timeStamp, CssCompactor.Options options) + { + if (!System.IO.File.Exists(inputFile)) + { + throw new FileNotFoundException(String.Format("File (\"{0}\") not found.", inputFile), inputFile); + } + + if ((options & CssCompactor.Options.Overwrite) == 0x0 && System.IO.File.Exists(outputFile)) + { + throw new AccessViolationException(String.Format("File (\"{0}\") already exists.", outputFile)); + } + + if (inputFile.Equals(outputFile, StringComparison.OrdinalIgnoreCase)) + { + throw new ApplicationException("Input and output file are set to the same path."); + } + + FileUtility.PrepSavePath(outputFile); + using (TextWriter output = System.IO.File.CreateText(outputFile)) + { + return CssCompactor.Compact(inputFile, null, output, copyright, timeStamp, options); + } + } + + internal static List Compact(string inputFile, string inputSource, TextWriter output, string copyright, string timeStamp, CssCompactor.Options options) + { + if (output == null) + { + throw new NullReferenceException("Output TextWriter was null."); + } + + // write out header with copyright and timestamp + CssCompactor.WriteHeader(output, copyright, timeStamp); + + // verify, compact and write out results + CssParser parser = new CssParser(inputFile, inputSource); + parser.Write(output, options); + + // return any errors + return parser.Errors; + } + + #endregion Public Methods + + #region Private Methods + + private static void WriteHeader(TextWriter writer, string copyright, string timeStamp) + { + if (!String.IsNullOrEmpty(copyright) || !String.IsNullOrEmpty(timeStamp)) + { + int width = 6; + if (!String.IsNullOrEmpty(copyright)) + { + copyright = copyright.Replace("*/", "");// make sure not to nest commments + width = Math.Max(copyright.Length + 6, width); + } + if (!String.IsNullOrEmpty(timeStamp)) + { + timeStamp = DateTime.Now.ToString(timeStamp).Replace("*/", "");// make sure not to nest commments + width = Math.Max(timeStamp.Length + 6, width); + } + + writer.WriteLine("/*".PadRight(width, '-') + "*\\"); + + if (!String.IsNullOrEmpty(copyright)) + { + writer.WriteLine("\t" + copyright); + } + + if (!String.IsNullOrEmpty(timeStamp)) + { + writer.WriteLine("\t" + timeStamp); + } + + writer.WriteLine("\\*".PadRight(width, '-') + "*/"); + } + } + + #endregion Private Methods + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/CssParser.cs b/src/Umbraco.Core/Models/Css/CssParser.cs new file mode 100644 index 0000000000..6879bbff9d --- /dev/null +++ b/src/Umbraco.Core/Models/Css/CssParser.cs @@ -0,0 +1,653 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models.Css +{ + internal class CssParser + { + #region Constants + + // this defines comments for CSS + private static readonly ReadFilter[] ReadFilters = new ReadFilter[] { new ReadFilter("/*", "*/") }; + private readonly object SyncLock = new object(); + + #endregion Constants + + #region Fields + + private readonly List errors = new List(); + private LineReader reader; + private CssStyleSheet styleSheet; + private string filePath; + private string source; + + #endregion Fields + + #region Init + + /// + /// Ctor. + /// + /// path to source + public CssParser(string filePath) + : this(filePath, null) + { + } + + /// + /// Ctor. + /// + /// path to source + /// actual source + public CssParser(string filePath, string source) + { + this.filePath = filePath; + this.source = source; + } + + #endregion Init + + #region Properties + + public List Errors + { + get { return this.errors; } + } + + public CssStyleSheet StyleSheet + { + get + { + if (this.styleSheet == null) + { + lock (this.SyncLock) + { + // check again in case race condition + // so we don't parse twice + if (this.styleSheet == null) + { + this.styleSheet = this.ParseStyleSheet(); + } + } + } + return this.styleSheet; + } + } + + private int Position + { + get { return this.reader.Position; } + } + + #endregion Properties + + #region Parse Methods + + #region StyleSheet + + /// + /// (BNF) stylesheet : [ CDO | CDC | S | statement ]*; + /// + /// CSS StyleSheet parse tree + private CssStyleSheet ParseStyleSheet() + { + CssStyleSheet styleSheet = new CssStyleSheet(); + using (this.reader = new LineReader(this.filePath, this.source, CssParser.ReadFilters)) + { + this.reader.NormalizeWhiteSpace = true; + +#if DEBUG + System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew(); +#endif + + char ch; + while (this.Read(out ch)) + { + switch (ch) + { + case '\uFEFF': // BOM (UTF byte order mark) + case '\t': //TAB + case '\n': //LF + case '\r': //CR + case ' ': //Space + { + // skip whitespace + continue; + } + case '<': + { + // CDO (Char Data Open?) + if (!this.Read(out ch) || ch != '-' || + !this.Read(out ch) || ch != '-') + { + throw new SyntaxError("Expected \"\"", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + continue; + } + default: + { + try + { + CssStatement statement = this.ParseStatement(); + styleSheet.Statements.Add(statement); + } + catch (ParseException ex) + { + this.errors.Add(ex); + + while (this.Read(out ch) && ch != '}') + { + // restabilize on next statement + } + } + continue; + } + } + } + +#if DEBUG + watch.Stop(); + Console.WriteLine("CSS parse duration: {0} ms for {1} chars", watch.ElapsedMilliseconds, this.reader.Length); +#endif + } + + this.reader = null; + this.source = null; + + return styleSheet; + } + + #endregion StyleSheet + + #region Statement + + /// + /// (BNF) statement : ruleset | at-rule; + /// + /// + private CssStatement ParseStatement() + { + if (this.reader.Current == '@') + { + return this.ParseAtRule(); + } + else + { + this.PutBack(); + return this.ParseRuleSet(); + } + } + + #endregion Statement + + #region At-Rule + + /// + /// (BNF) at-rule : ATKEYWORD S* any* [ block | ';' S* ]; + /// + /// + /// + /// NOTE: each at-rule might parse differently according to CSS3 + /// The @media block for example contains a block of statements + /// while other at-rules with a block contain a block of declarations + /// + private CssAtRule ParseAtRule() + { + CssAtRule atRule = new CssAtRule(); + int start = this.Position + 1;// start with first char of ident + + char ch; + while (this.Read(out ch) && !Char.IsWhiteSpace(ch)) + { + // continue consuming + } + + atRule.Ident = this.Copy(start); + + while (this.Read(out ch) && Char.IsWhiteSpace(ch)) + { + // consuming whitespace + } + + start = this.Position;// start with current char + do + { + switch (ch) + { + case '{': //Block Begin + { + atRule.Value = this.Copy(start); + + bool containsRuleSets = String.Equals(atRule.Ident, CssAtRule.MediaIdent, StringComparison.Ordinal); + while (true) + { + while (this.Read(out ch) && Char.IsWhiteSpace(ch)) + { + // consume whitespace + } + + if (ch == '}') + { + break; + } + + try + { + if (containsRuleSets) + { + // includes @media + CssStatement statement = this.ParseStatement(); + atRule.Block.Values.Add(statement); + } + else + { + // includes @font-face, @page + this.PutBack(); + CssDeclaration declaration = this.ParseDeclaration(); + atRule.Block.Values.Add(declaration); + } + } + catch (ParseException ex) + { + this.errors.Add(ex); + + while (this.Read(out ch) && ch != '}') + { + // restabilize on block end + } + break; + } + } + return atRule; + } + case ';': //At-Rule End + { + atRule.Value = this.Copy(start); + return atRule; + } + } + } while (this.Read(out ch)); + + throw new UnexpectedEndOfFile("Unclosed At-Rule", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + + #endregion At-Rule + + #region RuleSet + + /// + /// (BNF) ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; + /// + /// + private CssRuleSet ParseRuleSet() + { + char ch; + CssRuleSet ruleSet = new CssRuleSet(); + + ParseSelectors: + while (true) + { + try + { + CssSelector selector = this.ParseSelector(); + if (selector == null) + { + break; + } + ruleSet.Selectors.Add(selector); + } + catch (ParseException ex) + { + this.errors.Add(ex); + + while (this.Read(out ch)) + { + // restabalize on next rulset + switch (ch) + { + case ',': + { + // continue parsing rest of Selectors + goto ParseSelectors; + } + case '{': + { + goto ParseDeclarations; + } + //case ':':// keep going + case ';': + case '}': + { + throw new SyntaxError("Invalid selector list", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + } + } + } + } + + ParseDeclarations: + while (true) + { + try + { + CssDeclaration declaration = this.ParseDeclaration(); + if (declaration == null) + { + break; + } + ruleSet.Declarations.Add(declaration); + } + catch (ParseException ex) + { + this.errors.Add(ex); + + while (this.Read(out ch)) + { + // restabalize on next declaration + switch (ch) + { + case '{': + { + throw new SyntaxError("Invalid ruleset", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + //case ':':// keep going + case ';': + { + // continue parsing rest of delcarations + goto ParseDeclarations; + } + case '}': + { + // no more declarations + return ruleSet; + } + } + } + } + } + + return ruleSet; + } + + #endregion RuleSet + + #region Selector + + /// + /// (BNF) selector: any+; + /// + /// + private CssSelector ParseSelector() + { + CssSelector selector = new CssSelector(); + char ch; + + while (this.Read(out ch) && (Char.IsWhiteSpace(ch) || ch == ',')) + { + // skip whitespace, and empty selectors + } + + // consume property name + switch (ch) + { + case '{': + { + // no more declarations + return null; + } + //case ':':// pseudoclass + case ';': + case '}': + { + throw new SyntaxError("Invalid chars in selector", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + } + + int start = this.Position;// start with current char + + while (this.Read(out ch)) + { + // continue consuming selector + switch (ch) + { + case ',': + case '{': + { + selector.Value = this.Copy(start); + if (ch == '{') + { + this.PutBack(); + } + return selector; + } + //case ':':// pseudoclass + case ';': + case '}': + { + throw new SyntaxError("Invalid selector", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + } + } + throw new UnexpectedEndOfFile("Unclosed ruleset", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + + #endregion Selector + + #region Declaration + + /// + /// (BNF) declaration : property ':' S* value; + /// (BNF) property : IDENT S*; + /// + /// + private CssDeclaration ParseDeclaration() + { + CssDeclaration declaration = new CssDeclaration(); + char ch; + + while (this.Read(out ch) && (Char.IsWhiteSpace(ch) || ch == ';')) + { + // skip whitespace, and empty declarations + } + + // consume property name + switch (ch) + { + case '{': + case ':': + //case ';': + { + throw new SyntaxError("Declaration missing property name", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + case '}': + { + // no more declarations + return null; + } + } + + // read property, starting with current char + int start = this.Position; + while (this.Read(out ch) && !Char.IsWhiteSpace(ch) && ch != ':') + { + // consume property name + switch (ch) + { + case '{': + //case ':': + case ';': + { + throw new SyntaxError("Invalid CSS property name: " + this.Copy(start), this.reader.FilePath, this.reader.Line, this.reader.Column); + } + case '}': + { + this.PutBack(); + goto case ';'; + } + } + } + declaration.Property = this.Copy(start); + + if (Char.IsWhiteSpace(ch)) + { + while (this.Read(out ch) && (Char.IsWhiteSpace(ch))) + { + // skip whitespace + } + } + + if (ch != ':') + { + // missing the property delim and value + + if (ch == ';' || ch == '}') + { + // these are good chars for resyncing + // so put them back on the stream to + // not create subsequent errors + this.PutBack(); + } + throw new SyntaxError("Expected : ", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + + CssValueList value = this.ParseValue(); + declaration.Value = value; + + return declaration; + } + + #endregion Declaration + + #region Value + + /// + /// (BNF) value : [ any | block | ATKEYWORD S* ]+; + /// (BNF) any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING + /// | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES + /// | FUNCTION S* any* ')' | DASHMATCH | '(' S* any* ')' + /// | '[' S* any* ']' ] S*; + /// + /// + private CssValueList ParseValue() + { + CssValueList value = new CssValueList(); + char ch; + + while (this.Read(out ch) && Char.IsWhiteSpace(ch)) + { + // skip whitespace, and empty declarations + } + + switch (ch) + { + case '{': + case ':': + case ';': + case '}': + { + throw new SyntaxError("Invalid char in CSS property value: '" + ch + "'", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + } + + // read value, starting with current char + int start = this.Position; + while (this.Read(out ch)) + { + // consume declaration value + + switch (ch) + { + case '{': + //case ':':// leave in for "filter: progid:DXImageTransform.Microsoft..." + { + throw new SyntaxError("Invalid CSS property value: " + this.Copy(start), this.reader.FilePath, this.reader.Line, this.reader.Column); + } + case '}': + case ';': + { + //Should this parse the value further? + + CssString any = new CssString(); + any.Value = this.Copy(start); + value.Values.Add(any); + if (ch == '}') + { + this.PutBack(); + } + return value; + } + } + } + throw new UnexpectedEndOfFile("Unclosed declaration", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + + #endregion Value + + #endregion Parse Methods + + #region Methods + + public void Write(TextWriter writer, CssCompactor.Options options) + { + this.StyleSheet.Write(writer, options); + } + + #endregion Methods + + #region Reader Methods + + /// + /// + /// + /// + /// Success + private bool Read(out char ch) + { + if (this.reader.EndOfFile) + { + throw new UnexpectedEndOfFile("Unexpected end of file", this.reader.FilePath, this.reader.Line, this.reader.Column); + } + + int c = this.reader.Read(); + if (c < 0) + { + ch = '\0'; + return false; + } + ch = (char)c; + return true; + } + + /// + /// Copies chars from start until the position before the current position + /// + /// + private string Copy(int start) + { + // read block + return this.reader.Copy(start, this.reader.Position - 1); + } + + /// + /// Put one character back + /// + private void PutBack() + { + this.reader.PutBack(); + } + + #endregion Reader Methods + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/CssSyntax.cs b/src/Umbraco.Core/Models/Css/CssSyntax.cs new file mode 100644 index 0000000000..b91047a6d9 --- /dev/null +++ b/src/Umbraco.Core/Models/Css/CssSyntax.cs @@ -0,0 +1,429 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; + +namespace Umbraco.Core.Models.Css +{ + #region Base Types + + /// + /// CSS3 inconsistently specifies more than one grammar: + /// http://www.w3.org/TR/css3-syntax/#style + /// http://www.w3.org/TR/css3-syntax/#detailed-grammar + /// + internal abstract class CssSyntax + { + #region Methods + + public abstract void Write(TextWriter writer, CssCompactor.Options options); + + protected static bool IsPrettyPrint(CssCompactor.Options options) + { + return (options & CssCompactor.Options.PrettyPrint) > 0; + } + + #endregion Methods + + #region Object Overrides + + public override string ToString() + { + var writer = new StringWriter(); + + this.Write(writer, CssCompactor.Options.PrettyPrint); + + return writer.ToString(); + } + + #endregion Object Overrides + } + + internal interface ICssValue + { + #region Methods + + void Write(TextWriter writer, CssCompactor.Options options); + + #endregion Methods + } + + internal class CssString : CssSyntax + { + #region Fields + + private string value; + + #endregion Fields + + #region Properties + + public virtual string Value + { + get { return this.value; } + set { this.value = value; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + writer.Write(this.Value); + } + + #endregion Methods + } + + #endregion Base Types + + #region Grammar + + internal class CssStyleSheet : CssSyntax + { + #region Fields + + private readonly List statements = new List(); + + #endregion Fields + + #region Properties + + public List Statements + { + get { return this.statements; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + bool prettyPrint = IsPrettyPrint(options); + + foreach (CssStatement statement in this.statements) + { + statement.Write(writer, options); + if (prettyPrint) + { + writer.WriteLine(); + } + } + } + + #endregion Methods + } + + internal abstract class CssStatement : CssSyntax, ICssValue + { + } + + /// + /// + /// + /// + /// NOTE: each at-rule might parse differently according to CSS3 + /// The @media block for example contains a block of statements + /// while other at-rules with a block contain a block of declarations + /// + internal class CssAtRule : CssStatement + { + #region Constants + + internal const string MediaIdent = "media"; + + #endregion Constants + + #region Fields + + private string ident; + private string value; + + private CssBlock block; + + #endregion Fields + + #region Properties + + public string Ident + { + get { return this.ident; } + set { this.ident = value; } + } + + public string Value + { + get { return this.value; } + set { this.value = value; } + } + + public CssBlock Block + { + get + { + if (this.block == null) + { + this.block = new CssBlock(); + } + return this.block; + } + set { this.block = value; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + bool prettyPrint = IsPrettyPrint(options); + + writer.Write('@'); + writer.Write(this.ident); + + if (!String.IsNullOrEmpty(this.value)) + { + writer.Write(' '); + writer.Write(this.value); + } + + if (this.block != null) + { + if (prettyPrint) + { + writer.WriteLine(); + } + this.block.Write(writer, options); + } + else + { + writer.Write(';'); + } + + if (prettyPrint) + { + writer.WriteLine(); + } + } + + #endregion Methods + } + + internal class CssBlock : CssSyntax, ICssValue + { + #region Fields + + private readonly List values = new List(); + + #endregion Fields + + #region Properties + + public List Values + { + get { return this.values; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + bool prettyPrint = IsPrettyPrint(options); + + writer.Write('{'); + if (prettyPrint) + { + writer.WriteLine(); + } + + foreach (ICssValue value in this.Values) + { + value.Write(writer, options); + } + + if (prettyPrint) + { + writer.WriteLine(); + } + writer.Write('}'); + } + + #endregion Methods + } + + internal class CssRuleSet : CssStatement + { + #region Fields + + private readonly List selectors = new List(); + private readonly List declarations = new List(); + + #endregion Fields + + #region Properties + + public List Selectors + { + get { return this.selectors; } + } + + public List Declarations + { + get { return this.declarations; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + bool prettyPrint = IsPrettyPrint(options); + + bool comma = false; + + foreach (CssString selector in this.Selectors) + { + if (comma) + { + writer.Write(","); + if (prettyPrint) + { + writer.WriteLine(); + } + } + else + { + comma = true; + } + + selector.Write(writer, options); + } + + if (prettyPrint) + { + writer.WriteLine(); + } + writer.Write("{"); + if (prettyPrint) + { + writer.WriteLine(); + } + + foreach (CssDeclaration dec in this.Declarations) + { + dec.Write(writer, options); + } + + writer.Write("}"); + if (prettyPrint) + { + writer.WriteLine(); + } + } + + #endregion Methods + } + + internal class CssSelector : CssString + { + } + + internal class CssDeclaration : CssSyntax, ICssValue + { + #region Fields + + private string property; + private CssValueList value; + + #endregion Fields + + #region Properties + + public string Property + { + get { return this.property; } + set { this.property = value; } + } + + public CssValueList Value + { + get { return this.value; } + set { this.value = value; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + bool prettyPrint = IsPrettyPrint(options); + if (prettyPrint) + { + writer.Write('\t'); + } + writer.Write(this.Property); + writer.Write(':'); + if (prettyPrint) + { + writer.Write(" "); + } + this.Value.Write(writer, options); + writer.Write(";"); + if (prettyPrint) + { + writer.WriteLine(); + } + } + + #endregion Methods + } + + internal class CssValueList : CssSyntax + { + #region Fields + + private readonly List values = new List(); + + #endregion Fields + + #region Properties + + public List Values + { + get { return this.values; } + } + + #endregion Properties + + #region Methods + + public override void Write(TextWriter writer, CssCompactor.Options options) + { + bool space = false; + + foreach (CssString value in this.Values) + { + if (space) + { + writer.Write(" "); + } + else + { + space = true; + } + + value.Write(writer, options); + } + } + + #endregion Methods + } + + #endregion Grammar +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/FileUtility.cs b/src/Umbraco.Core/Models/Css/FileUtility.cs new file mode 100644 index 0000000000..010feaf751 --- /dev/null +++ b/src/Umbraco.Core/Models/Css/FileUtility.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Umbraco.Core.Models.Css +{ + internal class FileUtility + { + #region Constants + + private static readonly char[] IllegalChars; + + #endregion Constants + + #region Init + + static FileUtility() + { + var chars = new List(Path.GetInvalidPathChars()); + foreach (char ch in Path.GetInvalidFileNameChars()) + { + if (!chars.Contains(ch) && ch != Path.DirectorySeparatorChar) + { + chars.Add(ch); + } + } + IllegalChars = chars.ToArray(); + } + + #endregion Init + + #region Methods + + /// + /// Makes sure directory exists and if file exists is not readonly. + /// + /// + /// if valid path + internal static bool PrepSavePath(string filename) + { + if (System.IO.File.Exists(filename)) + { + // make sure not readonly + FileAttributes attributes = System.IO.File.GetAttributes(filename); + attributes &= ~FileAttributes.ReadOnly; + System.IO.File.SetAttributes(filename, attributes); + } + else + { + string dir = Path.GetDirectoryName(filename); + if (!String.IsNullOrEmpty(dir) && dir.IndexOfAny(IllegalChars) >= 0) + { + return false; + } + if (!String.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + // make sure path exists + Directory.CreateDirectory(dir); + } + string file = Path.GetFileName(filename); + if (!String.IsNullOrEmpty(file) && file.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + { + return false; + } + } + return true; + } + + #endregion Methods + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/FilterTrie.cs b/src/Umbraco.Core/Models/Css/FilterTrie.cs new file mode 100644 index 0000000000..4ad4d2a774 --- /dev/null +++ b/src/Umbraco.Core/Models/Css/FilterTrie.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Css +{ + /// + /// Defines a character sequence to filter out when reading. + /// + /// + /// If the sequence exists in the read source, it will be read out as if it was never there. + /// + internal struct ReadFilter + { + #region Fields + + public readonly string StartToken; + public readonly string EndToken; + + #endregion Fields + + #region Init + + public ReadFilter(string start, string end) + { + if (String.IsNullOrEmpty(start)) + { + throw new ArgumentNullException("start"); + } + if (String.IsNullOrEmpty(end)) + { + throw new ArgumentNullException("end"); + } + + this.StartToken = start; + this.EndToken = end; + } + + #endregion Init + } + + /// + /// Creates a Trie out of ReadFilters + /// + internal class FilterTrie : TrieNode + { + #region Constants + + private const int DefaultTrieWidth = 1; + + #endregion Constants + + #region Init + + internal FilterTrie(IEnumerable filters) + : base(DefaultTrieWidth) + { + // load trie + foreach (ReadFilter filter in filters) + { + TrieNode node = this; + + // build out the path for StartToken + foreach (char ch in filter.StartToken) + { + if (!node.Contains(ch)) + { + node[ch] = new TrieNode(DefaultTrieWidth); + } + + node = (TrieNode)node[ch]; + } + + // at the end of StartToken path is the EndToken + node.Value = filter.EndToken; + } + } + + #endregion Init + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/LineReader.cs b/src/Umbraco.Core/Models/Css/LineReader.cs new file mode 100644 index 0000000000..af2fa2dab0 --- /dev/null +++ b/src/Umbraco.Core/Models/Css/LineReader.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Umbraco.Core.Models.Css +{ + internal class LineReader : TextReader + { + #region Fields + + private int line = 1; + private int column = 0; + private int position = -1; + + private string filePath; + private string source; + + private readonly FilterTrie trie; + + private bool normalizeWhiteSpace = false; + + #endregion Fields + + #region Init + + /// + /// Ctor. + /// + /// + /// + /// + internal LineReader(string filePath, IEnumerable filters) : this(filePath, null, filters) { } + + /// + /// Ctor. + /// + /// + /// + /// + internal LineReader(string filePath, string source, IEnumerable filters) + { + this.trie = new FilterTrie(filters); + this.source = source; + this.filePath = filePath; + + if (this.source == null) + { + if (System.IO.File.Exists(filePath)) + { + this.source = System.IO.File.ReadAllText(filePath); + } + else + { + throw new FileError("File not found", filePath, 0, 0); + } + } + } + + /// + /// Ctor. + /// + /// + /// + internal LineReader(string filePath, string source) + : this(filePath, source, new ReadFilter[0]) + { + } + + #endregion Init + + #region Properties + + /// + /// Gets the path to the source file + /// + public string FilePath + { + get { return this.filePath; } + } + + /// + /// Gets the size of source file in chars + /// + public int Length + { + get { return this.source.Length; } + } + + /// + /// Gets the current line number + /// + public int Line + { + get { return this.line; } + } + + /// + /// Gets the current col number + /// + public int Column + { + get { return this.column; } + } + + /// + /// Gets the current char position + /// + public int Position + { + get { return this.position; } + } + + /// + /// Gets if at end the end of file + /// + public bool EndOfFile + { + get { return this.position >= this.source.Length; } + } + + /// + /// Gets and sets if whitespace is normalized while reading + /// + public bool NormalizeWhiteSpace + { + get { return this.normalizeWhiteSpace; } + set { this.normalizeWhiteSpace = value; } + } + + /// + /// Gets the current char + /// + public int Current + { + get + { + if (this.EndOfFile) + { + return -1; + } + return this.source[this.position]; + } + } + + #endregion Properties + + #region TextReader Members + + /// + /// Unfiltered look ahead + /// + /// + public override int Peek() + { + return this.Peek(1); + } + + /// + /// Filtered read of the next source char. Counters are incremented. + /// + /// + /// + /// NewLine sequences (CR/LF, LF, CR) are normalized to LF. + /// + public override int Read() + { + return this.Read(true); + } + + #endregion TextReader Members + + #region Utility Methods + + /// + /// Backs the current position up one. + /// + public void PutBack() + { + if (this.position < 0) + { + throw new InvalidOperationException("Already at start of source"); + } + switch (this.source[this.position]) + { + case '\r': //CR + { + // manipulate CR/LF as one char + if ((this.position + 1 < this.Length) && this.source[this.position + 1] == '\n') + { + this.position--; + } + break; + } + case '\n': //LF + case '\f': //FF + { + this.line--; + break; + } + } + this.column--; + this.position--; + } + + /// + /// Copies a range from the source + /// + /// starting position, inclusive + /// ending position, inclusive + /// + public string Copy(int start, int end) + { + if (start < 0) + { + throw new ArgumentOutOfRangeException("start"); + } + if (end < 0) + { + throw new ArgumentOutOfRangeException("end"); + } + if (end < 1) + { + return null; + } + + // set to just before read, next char is start + int copyPosition = start - 1; + + // allocate the full range but may not use due to filtering + char[] buffer = new char[end - start + 1]; + + int count = 0; + while (copyPosition < end) + { + int ch = this.CopyRead(ref copyPosition); + if (ch == -1) + { + throw new UnexpectedEndOfFile("Read past end of file", this.FilePath, this.Line, this.Column); + } + buffer[count] = (char)ch; + count++; + } + + if (count < 1) + { + return null; + } + return new String(buffer, 0, count).Trim(); + } + + #endregion Utility Methods + + #region Filter Methods + + /// + /// Peeks with n chars of lookahead. + /// + /// + /// unfiltered read + protected int Peek(int lookahead) + { + int pos = this.position + lookahead; + if (pos >= this.source.Length) + { + return -1; + } + return this.source[pos]; + } + + /// + /// Reads the next char + /// + /// if filtering + /// the next char, or -1 if at EOF + protected int Read(bool filter) + { + if (this.position + 1 >= this.source.Length) + { + this.position = this.source.Length; + return -1; + } + + // increment counters + this.position++; + this.column++; + char ch = this.source[this.position]; + + if (Char.IsWhiteSpace(ch)) + { + ch = this.NormalizeSpaces(ch, ref this.position, ref this.line, ref this.column); + } + + return filter ? this.Filter(ch) : ch; + } + + /// + /// Normalized CR/CRLF/LF/FF to LF, or all whitespace to SPACE if NormalizeWhiteSpace is true + /// + /// + /// + /// + /// + /// + private char NormalizeSpaces(char ch, ref int pos, ref int line, ref int col) + { + int length = this.source.Length; + if (this.normalizeWhiteSpace) + { + // normalize runs of WhiteSpace to ' ' + while ((pos + 1 < length) && Char.IsWhiteSpace(this.source, pos + 1)) + { + pos++; + col++; + + // increment line count + switch (this.source[pos]) + { + case '\r': //CR + { + // manipulate CR/LF as one char + if ((pos + 1 < length) && this.source[pos + 1] == '\n') + { + pos++; + } + goto case '\n'; + } + case '\n': //LF + case '\f': //FF + { + line++; + col = 0; + break; + } + } + } + ch = ' '; + } + else + { + // normalize NewLines to '\n', increment line count + switch (ch) + { + case '\r': //CR + { + // manipulate CR/LF as one char + if ((pos + 1 < length) && this.source[pos + 1] == '\n') + { + pos++; + } + goto case '\n'; + } + case '\n': //LF + case '\f': //FF + { + line++; + col = 0; + ch = '\n'; + break; + } + } + } + return ch; + } + + /// + /// Read for Copying (doesn't reset line.col counters) + /// + /// + /// + protected int CopyRead(ref int copyPosition) + { + if (copyPosition + 1 >= this.source.Length) + { + return -1; + } + + // increment counters + copyPosition++; + char ch = this.source[copyPosition]; + + if (Char.IsWhiteSpace(ch)) + { + int dummyLine = 0, dummyCol = 0; + ch = this.NormalizeSpaces(ch, ref copyPosition, ref dummyLine, ref dummyCol); + } + + return this.Filter(ch); + } + + /// + /// Filters based upon an internal Trie + /// + /// + /// + private int Filter(char ch) + { + int lookAhead = 0; + ITrieNode node = this.trie[ch]; + + while (node != null) + { + if (node.HasValue) + { + // found StartToken + string endToken = node.Value; + int length = endToken.Length; + + // move to end of StartToken + this.position += lookAhead; + + for (int i = 0; i < length; i++) + { + int ch2 = this.Read(false); + if (ch < 0) + { + throw new UnexpectedEndOfFile("Expected " + endToken, this.FilePath, this.Line, this.Column); + } + if (ch2 != endToken[i]) + { + // reset search + while (i > 0) + { + i--; + this.PutBack(); + } + i--; + } + } + return this.Read(true); + } + else + { + lookAhead++; + int pk = this.Peek(lookAhead); + if (pk < 0) + { + return ch; + } + node = node[(char)pk]; + } + } + + return ch; + } + + #endregion Filter Methods + + #region IDisposable Members + + /// + /// Free source resources. + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + this.source = null; + } + + #endregion IDisposable Members + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/ParseException.cs b/src/Umbraco.Core/Models/Css/ParseException.cs new file mode 100644 index 0000000000..be01f9f50c --- /dev/null +++ b/src/Umbraco.Core/Models/Css/ParseException.cs @@ -0,0 +1,224 @@ +using System; + +namespace Umbraco.Core.Models.Css +{ + internal enum ParseExceptionType + { + Warning, + Error + } + + [Serializable] + internal abstract class ParseException : ApplicationException + { + #region Constants + + // this cannot change every char is important or Visual Studio will not list as error/warning + // http://blogs.msdn.com/msbuild/archive/2006/11/03/msbuild-visual-studio-aware-error-messages-and-message-formats.aspx + private const string MSBuildErrorFormat = "{0}({1},{2}): {3} {4}: {5}"; + + #endregion Constants + + #region Fields + + private string file; + private int line; + private int column; + private int code = 0; + + #endregion Fields + + #region Init + + public ParseException(string message, string file, int line, int column) + : base(message) + { + this.file = file; + this.line = line; + this.column = column; + } + + public ParseException(string message, string file, int line, int column, Exception innerException) + : base(message, innerException) + { + this.file = file; + this.line = line; + this.column = column; + } + + #endregion Init + + #region Properties + + public abstract ParseExceptionType Type + { + get; + } + + public virtual int Code + { + get { return this.code; } + } + + public string ErrorCode + { + get + { + string ext = System.IO.Path.GetExtension(file); + if (ext == null || ext.Length < 2) + { + return null; + } + + return ext.Substring(1).ToUpperInvariant() + this.Code.ToString("####0000"); + } + } + + public string File + { + get { return this.file; } + } + + public int Line + { + get { return this.line; } + } + + public int Column + { + get { return this.column; } + } + + #endregion Properties + + #region Methods + + public virtual string GetCompilerMessage() + { + return this.GetCompilerMessage(this.Type == ParseExceptionType.Warning); + } + + public virtual string GetCompilerMessage(bool isWarning) + { + // format exception as a VS2005 error/warning + return String.Format( + ParseException.MSBuildErrorFormat, + this.File, + (this.Line > 0) ? this.Line : 1, + (this.Column > 0) ? this.Column : 1, + isWarning ? "warning" : "error", + this.ErrorCode, + this.Message); + } + + #endregion Methods + } + + [Serializable] + internal class ParseWarning : ParseException + { + #region Init + + public ParseWarning(string message, string file, int line, int column) + : base(message, file, line, column) + { + } + + public ParseWarning(string message, string file, int line, int column, Exception innerException) + : base(message, file, line, column, innerException) + { + } + + #endregion Init + + #region Properties + + public override ParseExceptionType Type + { + get { return ParseExceptionType.Warning; } + } + + #endregion Properties + } + + [Serializable] + internal class ParseError : ParseException + { + #region Init + + public ParseError(string message, string file, int line, int column) + : base(message, file, line, column) + { + } + + public ParseError(string message, string file, int line, int column, Exception innerException) + : base(message, file, line, column, innerException) + { + } + + #endregion Init + + #region Properties + + public override ParseExceptionType Type + { + get { return ParseExceptionType.Error; } + } + + #endregion Properties + } + + [Serializable] + internal class UnexpectedEndOfFile : ParseError + { + #region Init + + public UnexpectedEndOfFile(string message, string file, int line, int column) + : base(message, file, line, column) + { + } + + public UnexpectedEndOfFile(string message, string file, int line, int column, Exception innerException) + : base(message, file, line, column, innerException) + { + } + + #endregion Init + } + + [Serializable] + internal class FileError : ParseWarning + { + #region Init + + public FileError(string message, string file, int line, int column) + : base(message, file, line, column) + { + } + + public FileError(string message, string file, int line, int column, Exception innerException) + : base(message, file, line, column, innerException) + { + } + + #endregion Init + } + + [Serializable] + internal class SyntaxError : ParseError + { + #region Init + + public SyntaxError(string message, string file, int line, int column) + : base(message, file, line, column) + { + } + + public SyntaxError(string message, string file, int line, int column, Exception innerException) + : base(message, file, line, column, innerException) + { + } + + #endregion Init + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Css/TrieNode.cs b/src/Umbraco.Core/Models/Css/TrieNode.cs new file mode 100644 index 0000000000..ba58c3b95e --- /dev/null +++ b/src/Umbraco.Core/Models/Css/TrieNode.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Css +{ + /// + /// A generic node for building a Trie + /// + /// the Type used for the node path + /// the Type used for the node value + /// + /// http://en.wikipedia.org/wiki/Trie + /// + internal class TrieNode : ITrieNode + { + #region Fields + + private readonly IDictionary> Children; + private TValue value = default(TValue); + + #endregion Fields + + #region Init + + /// + /// Ctor + /// + public TrieNode() + : this(-1) + { + } + + /// + /// Ctor. + /// + /// + public TrieNode(int capacity) + { + if (capacity < 1) + { + this.Children = new Dictionary>(); + } + else + { + this.Children = new Dictionary>(capacity); + } + } + + #endregion Init + + #region Properties + + public ITrieNode this[TKey key] + { + get + { + if (!this.Children.ContainsKey(key)) + { + return null; + } + return this.Children[key]; + } + + // added "internal" to get around change in C# 3.0 modifiers + // this worked fine in C# 2.0 but they fixed that bug + // http://blogs.msdn.com/ericlippert/archive/2008/03/28/why-can-t-i-access-a-protected-member-from-a-derived-class-part-two-why-can-i.aspx + protected internal set { this.Children[key] = value; } + } + + public TValue Value + { + get { return this.value; } + + // added "internal" to get around change in C# 3.0 modifiers + // this worked fine in C# 2.0 but they fixed that bug + // http://blogs.msdn.com/ericlippert/archive/2008/03/28/why-can-t-i-access-a-protected-member-from-a-derived-class-part-two-why-can-i.aspx + protected internal set + { + if (!EqualityComparer.Default.Equals(this.value, default(TValue))) + { + throw new InvalidOperationException("Trie path collision: the value for TrieNode<" + value.GetType().Name + "> has already been assigned."); + } + this.value = value; + } + } + + public bool HasValue + { + get + { + return !EqualityComparer.Default.Equals(this.value, default(TValue)); + } + } + + #endregion Properties + + #region Methods + + /// + /// Determines if child exists + /// + /// + /// + public bool Contains(TKey key) + { + return this.Children.ContainsKey(key); + } + + #endregion Methods + } + + internal interface ITrieNode + { + #region Properties + + ITrieNode this[TKey key] + { + get; + } + + TValue Value + { + get; + } + + bool HasValue + { + get; + } + + #endregion Methods + + #region Methods + + bool Contains(TKey key); + + #endregion Methods + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 439e3de082..0ff3d0df35 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -16,6 +16,8 @@ namespace Umbraco.Core.Models private Guid _parentId; private string _itemKey; private IEnumerable _translations; + //NOTE Add this to LocalizationService or Repository + //private static Guid _topLevelParent = new Guid("41c7638d-f529-4bff-853e-59a0c2fb1bde"); public DictionaryItem(Guid parentId, string itemKey) { diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs new file mode 100644 index 0000000000..09bc15217c --- /dev/null +++ b/src/Umbraco.Core/Models/File.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; + +namespace Umbraco.Core.Models +{ + public abstract class File : IFile + { + private string _name; + private string _alias; + + protected File(string path) + { + Path = path; + } + + /// + /// Gets or sets the Name of the File including extension + /// + public virtual string Name + { + get + { + if (!string.IsNullOrEmpty(_name)) + return _name; + + _name = new FileInfo(Path).Name; + return _name; + } + set { _name = value; } + } + + /// + /// Gets or sets the Alias of the File, which is the name without the extension + /// + public virtual string Alias + { + get + { + if (!string.IsNullOrEmpty(_alias)) + return _alias; + + var fileInfo = new FileInfo(Path); + var name = fileInfo.Name; + int lastIndexOf = name.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase) + 1; + _alias = name.Substring(0, lastIndexOf); + return _alias; + } + set { _alias = value; } + } + + /// + /// Gets or sets the Path to the File from the root of the site + /// + public virtual string Path { get; set; } + + /// + /// Gets or sets the Content of a File + /// + public virtual string Content { get; set; } + + /// + /// Boolean indicating whether the file could be validated + /// + /// True if file is valid, otherwise false + public abstract bool IsValid(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IFile.cs b/src/Umbraco.Core/Models/IFile.cs new file mode 100644 index 0000000000..25a51069f1 --- /dev/null +++ b/src/Umbraco.Core/Models/IFile.cs @@ -0,0 +1,37 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Defines a File + /// + /// Used for Scripts, Stylesheets and Templates + public interface IFile : IValueObject + { + /// + /// Gets or sets the Name of the File including extension + /// + string Name { get; set; } + + /// + /// Gets or sets the Alias of the File, which is the name without the extension + /// + string Alias { get; set; } + + /// + /// Gets or sets the Path to the File from the root of the site + /// + string Path { get; set; } + + /// + /// Gets or sets the Content of a File + /// + string Content { get; set; } + + /// + /// Boolean indicating whether the file could be validated + /// + /// True if file is valid, otherwise false + bool IsValid(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Script.cs b/src/Umbraco.Core/Models/Script.cs new file mode 100644 index 0000000000..48aa53987a --- /dev/null +++ b/src/Umbraco.Core/Models/Script.cs @@ -0,0 +1,52 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a Script file + /// + public class Script : File + { + public Script(string path) : base(path) + { + base.Path = path; + } + + /// + /// Boolean indicating whether the file could be validated + /// + /// + /// The validation logic was previsouly placed in the codebehind of editScript.aspx, + /// but has been moved to the script file so the validation is central. + /// + /// True if file is valid, otherwise false + public override bool IsValid() + { + //NOTE Since a script file can be both JS, Razor Views, Razor Macros and Xslt + //it might be an idea to create validations for all 3 and divide the validation + //into 4 private methods. + //See codeEditorSave.asmx.cs for reference. + + var exts = UmbracoSettings.ScriptFileTypes.Split(',').ToList(); + if (UmbracoSettings.EnableMvcSupport) + { + exts.Add("cshtml"); + exts.Add("vbhtml"); + } + + var dirs = SystemDirectories.Scripts; + if (UmbracoSettings.EnableMvcSupport) + dirs += "," + SystemDirectories.MvcViews; + + //Validate file + var validFile = IOHelper.ValidateEditPath(Path, dirs.Split(',')); + + //Validate extension + var validExtension = IOHelper.ValidateFileExtension(Path, exts); + + return validFile && validExtension; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Stylesheet.cs b/src/Umbraco.Core/Models/Stylesheet.cs new file mode 100644 index 0000000000..e4a985073c --- /dev/null +++ b/src/Umbraco.Core/Models/Stylesheet.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.IO; +using Umbraco.Core.Models.Css; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a Stylesheet file + /// + public class Stylesheet : File + { + public Stylesheet(string path) : base(path) + { + base.Path = path; + } + + /// + /// Returns a list of + /// + public IEnumerable Properties + { + get + { + var properties = new List(); + var parser = new CssParser(Path);//TODO change CssParser so we can use Content instead of Path + + foreach (CssAtRule statement in parser.StyleSheet.Statements) + { + properties.Add(new StylesheetProperty(statement.Value, "")); + } + + foreach (CssRuleSet statement in parser.StyleSheet.Statements) + { + var selector = statement.Selectors.First(); + properties.Add(new StylesheetProperty(selector.Value, "")); + } + + return properties; + } + } + + /// + /// Boolean indicating whether the file could be validated + /// + /// True if file is valid, otherwise false + public override bool IsValid() + { + var dirs = SystemDirectories.Css; + + //Validate file + var validFile = IOHelper.ValidateEditPath(Path, dirs.Split(',')); + + //Validate extension + var validExtension = IOHelper.ValidateFileExtension(Path, new List {"css"}); + + return validFile && validExtension; + } + + /// + /// Boolean indicating whether the file is valid css using a css parser + /// + /// True if css is valid, otherwise false + public bool IsFileValidCss() + { + var parser = new CssParser(Path);//TODO change CssParser so we can use Content instead of Path + return parser.Errors.Any(); + } + } + + public class StylesheetProperty : IValueObject + { + public StylesheetProperty(string @alias, string value) + { + Alias = alias; + Value = value; + } + + public string Alias { get; set; } + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs new file mode 100644 index 0000000000..3e49137718 --- /dev/null +++ b/src/Umbraco.Core/Models/Template.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a Template file + /// + public class Template : File + { + public Template(string path) + : base(path) + { + base.Path = path; + } + + /// + /// Boolean indicating whether the file could be validated + /// + /// True if file is valid, otherwise false + public override bool IsValid() + { + var exts = new List(); + if (UmbracoSettings.EnableMvcSupport) + { + exts.Add("cshtml"); + exts.Add("vbhtml"); + } + else + { + exts.Add(UmbracoSettings.UseAspNetMasterPages ? "masterpage" : "aspx"); + } + + var dirs = SystemDirectories.Masterpages; + if (UmbracoSettings.EnableMvcSupport) + dirs += "," + SystemDirectories.MvcViews; + + //Validate file + var validFile = IOHelper.ValidateEditPath(Path, dirs.Split(',')); + + //Validate extension + var validExtension = IOHelper.ValidateFileExtension(Path, exts); + + return validFile && validExtension; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4aa5b4e2c9..ef1e103e79 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -62,11 +62,24 @@ + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/Routing/LookupByNiceUrlAndTemplate.cs b/src/Umbraco.Web/Routing/LookupByNiceUrlAndTemplate.cs index 57e28cbf4e..8f56576a59 100644 --- a/src/Umbraco.Web/Routing/LookupByNiceUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/LookupByNiceUrlAndTemplate.cs @@ -3,6 +3,7 @@ using System.Xml; using Umbraco.Core.Logging; using Umbraco.Core.Models; using umbraco.cms.businesslogic.template; +using Template = umbraco.cms.businesslogic.template.Template; namespace Umbraco.Web.Routing { diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 3d4918d396..6db8a065d6 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -16,6 +16,7 @@ using umbraco.cms.businesslogic.web; using umbraco.cms.businesslogic.template; using umbraco.cms.businesslogic.member; using umbraco.interfaces; +using Template = umbraco.cms.businesslogic.template.Template; namespace Umbraco.Web.Routing {