New tests.
[mono.git] / mcs / class / System.Web / System.Web.Compilation / AspParser.cs
1 //
2 // System.Web.Compilation.AspParser
3 //
4 // Authors:
5 //      Gonzalo Paniagua Javier (gonzalo@ximian.com)
6 //      Marek Habersack <mhabersack@novell.com>
7 //
8 // (C) 2002,2003 Ximian, Inc (http://www.ximian.com)
9 // (C) 2004-2009 Novell, Inc (http://novell.com)
10 //
11
12 //
13 // Permission is hereby granted, free of charge, to any person obtaining
14 // a copy of this software and associated documentation files (the
15 // "Software"), to deal in the Software without restriction, including
16 // without limitation the rights to use, copy, modify, merge, publish,
17 // distribute, sublicense, and/or sell copies of the Software, and to
18 // permit persons to whom the Software is furnished to do so, subject to
19 // the following conditions:
20 // 
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
23 // 
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 //
32 using System;
33 using System.ComponentModel;
34 using System.Collections;
35 using System.Globalization;
36 using System.IO;
37 using System.Text;
38 using System.Web.Util;
39 using System.Security.Cryptography;
40
41 namespace System.Web.Compilation
42 {
43         delegate void ParseErrorHandler (ILocation location, string message);
44         delegate void TextParsedHandler (ILocation location, string text);
45         delegate void TagParsedHandler (ILocation location, TagType tagtype, string id, TagAttributes attributes);
46         delegate void ParsingCompleteHandler ();
47         
48         class AspParser : ILocation
49         {
50                 static readonly object errorEvent = new object ();
51                 static readonly object tagParsedEvent = new object ();
52                 static readonly object textParsedEvent = new object ();
53                 static readonly object parsingCompleteEvent = new object();
54
55                 MD5 checksum;
56                 AspTokenizer tokenizer;
57                 int beginLine, endLine;
58                 int beginColumn, endColumn;
59                 int beginPosition, endPosition;
60                 string filename;
61                 string verbatimID;
62                 string fileText;
63                 StringReader fileReader;
64                 bool _internal;
65                 int _internalLineOffset;
66                 int _internalPositionOffset;
67                 AspParser outer;
68                 
69                 EventHandlerList events = new EventHandlerList ();
70                 
71                 public event ParseErrorHandler Error {
72                         add { events.AddHandler (errorEvent, value); }
73                         remove { events.RemoveHandler (errorEvent, value); }
74                 }
75                 
76                 public event TagParsedHandler TagParsed {
77                         add { events.AddHandler (tagParsedEvent, value); }
78                         remove { events.RemoveHandler (tagParsedEvent, value); }
79                 }
80                 
81                 public event TextParsedHandler TextParsed {
82                         add { events.AddHandler (textParsedEvent, value); }
83                         remove { events.RemoveHandler (textParsedEvent, value); }
84                 }
85
86                 public event ParsingCompleteHandler ParsingComplete {
87                         add { events.AddHandler (parsingCompleteEvent, value); }
88                         remove { events.RemoveHandler (parsingCompleteEvent, value); }
89                 }
90                 
91                 public AspParser (string filename, TextReader input)
92                 {
93                         this.filename = filename;
94                         this.fileText = input.ReadToEnd ();
95                         this.fileReader = new StringReader (this.fileText);
96                         this._internalLineOffset = 0;
97                         tokenizer = new AspTokenizer (this.fileReader);
98                 }
99
100                 public AspParser (string filename, TextReader input, int startLineOffset, int positionOffset, AspParser outer)
101                         : this (filename, input)
102                 {
103                         this._internal = true;
104                         this._internalLineOffset = startLineOffset;
105                         this._internalPositionOffset = positionOffset;
106                         this.outer = outer;
107                 }
108                 
109                 public byte[] MD5Checksum {
110                         get {
111                                 if (checksum == null)
112                                         return new byte[0];
113                                 
114                                 return checksum.Hash;
115                         }
116                 }
117
118                 public int BeginPosition {
119                         get { return beginPosition; }
120                 }
121
122                 public int EndPosition {
123                         get { return endPosition; }
124                 }
125                 
126                 public int BeginLine {
127                         get {
128                                 if (_internal)
129                                         return beginLine + _internalLineOffset;
130
131                                 return beginLine;
132                         }
133                 }
134
135                 public int BeginColumn {
136                         get { return beginColumn; }
137                 }
138
139                 public int EndLine {
140                         get {
141                                 if (_internal)
142                                         return endLine + _internalLineOffset;
143                                 return endLine;
144                         }
145                 }
146
147                 public int EndColumn {
148                         get { return endColumn; }
149                 }
150
151                 public string FileText {
152                         get {
153                                 string ret = null;
154                                 
155                                 if (_internal && outer != null)
156                                         ret = outer.FileText;
157                                 
158                                 if (ret == null && fileText != null)
159                                         ret = fileText;
160                                 
161                                 return ret;
162                         }
163                 }
164                 
165                 public string PlainText {
166                         get {
167                                 if (beginPosition >= endPosition || fileText == null)
168                                         return null;
169
170                                 string text = FileText;
171                                 int start, len;
172                                 
173                                 if (_internal && outer != null) {
174                                         start = beginPosition + _internalPositionOffset;
175                                         len = (endPosition + _internalPositionOffset) - start;
176                                 } else {
177                                         start = beginPosition;
178                                         len = endPosition - beginPosition;
179                                 }
180                                 
181                                 if (text != null)
182                                         return text.Substring (start, len);
183
184                                 return null;
185                         }
186                 }
187
188                 public string Filename {
189                         get {
190                                 if (_internal && outer != null)
191                                         return outer.Filename;
192                                 
193                                 return filename;
194                         }
195                 }
196
197                 public string VerbatimID {
198                         set {
199                                 tokenizer.Verbatim = true;
200                                 verbatimID = value;
201                         }
202                 }
203
204                 bool Eat (int expected_token)
205                 {
206                         int token = tokenizer.get_token ();
207                         if (token != expected_token) {
208                                 tokenizer.put_back ();
209                                 return false;
210                         }
211
212                         endLine = tokenizer.EndLine;
213                         endColumn = tokenizer.EndColumn;
214                         return true;
215                 }
216
217                 void BeginElement ()
218                 {
219                         beginLine = tokenizer.BeginLine;
220                         beginColumn = tokenizer.BeginColumn;
221                         beginPosition = tokenizer.Position - 1;
222                 }
223
224                 void EndElement ()
225                 {
226                         endLine = tokenizer.EndLine;
227                         endColumn = tokenizer.EndColumn;
228                         endPosition = tokenizer.Position;
229                 }
230
231                 public void Parse ()
232                 {
233                         if (tokenizer == null) {
234                                 OnError ("AspParser not initialized properly.");
235                                 return;
236                         }
237                         
238                         int token;
239                         string id;
240                         TagAttributes attributes;
241                         TagType tagtype = TagType.Text;
242                         StringBuilder text =  new StringBuilder ();
243
244                         try {
245                                 while ((token = tokenizer.get_token ()) != Token.EOF) {
246                                         BeginElement ();
247
248                                         if (tokenizer.Verbatim){
249                                                 string end_verbatim = "</" + verbatimID + ">";
250                                                 string verbatim_text = GetVerbatim (token, end_verbatim);
251
252                                                 if (verbatim_text == null)
253                                                         OnError ("Unexpected EOF processing " + verbatimID);
254
255                                                 tokenizer.Verbatim = false;
256
257                                                 EndElement ();
258                                                 endPosition -= end_verbatim.Length;
259                                                 OnTextParsed (verbatim_text);
260                                                 beginPosition = endPosition;
261                                                 endPosition += end_verbatim.Length;
262                                                 OnTagParsed (TagType.Close, verbatimID, null);
263                                                 continue;
264                                         }
265                                 
266                                         if (token == '<') {
267                                                 GetTag (out tagtype, out id, out attributes);
268                                                 EndElement ();
269                                                 if (tagtype == TagType.ServerComment)
270                                                         continue;
271
272                                                 if (tagtype == TagType.Text)
273                                                         OnTextParsed (id);
274                                                 else
275                                                         OnTagParsed (tagtype, id, attributes);
276
277                                                 continue;
278                                         }
279
280                                         if (tokenizer.Value.Trim ().Length == 0 && tagtype == TagType.Directive) {
281                                                 continue;
282                                         }
283
284                                         text.Length = 0;
285                                         do {
286                                                 text.Append (tokenizer.Value);
287                                                 token = tokenizer.get_token ();
288                                         } while (token != '<' && token != Token.EOF);
289
290                                         tokenizer.put_back ();
291                                         EndElement ();
292                                         OnTextParsed (text.ToString ());
293                                 }
294                         } finally {
295                                 if (fileReader != null) {
296                                         fileReader.Close ();
297                                         fileReader = null;
298                                 }
299                                 checksum = tokenizer.Checksum;
300                                 tokenizer = null;
301                         }
302
303                         OnParsingComplete ();
304                 }
305
306                 bool GetInclude (string str, out string pathType, out string filename)
307                 {
308                         pathType = null;
309                         filename = null;
310                         str = str.Substring (2).Trim ();
311                         int len = str.Length;
312                         int lastQuote = str.LastIndexOf ('"');
313                         if (len < 10 || lastQuote != len - 1)
314                                 return false;
315
316                         if (!StrUtils.StartsWith (str, "#include ", true))
317                                 return false;
318
319                         str = str.Substring (9).Trim ();
320                         bool isfile = (StrUtils.StartsWith (str ,"file", true));
321                         if (!isfile && !StrUtils.StartsWith (str, "virtual", true))
322                                 return false;
323
324                         pathType = (isfile) ? "file" : "virtual";
325                         if (str.Length < pathType.Length + 3)
326                                 return false;
327
328                         str = str.Substring (pathType.Length).Trim ();
329                         if (str.Length < 3 || str [0] != '=')
330                                 return false;
331
332                         int index = 1;
333                         for (; index < str.Length; index++) {
334                                 if (Char.IsWhiteSpace (str [index]))
335                                         continue;
336                                 else if (str [index] == '"')
337                                         break;
338                         }
339
340                         if (index == str.Length || index == lastQuote)
341                                 return false;
342
343                         str = str.Substring (index);
344                         if (str.Length == 2) { // only quotes
345                                 OnError ("Empty file name.");
346                                 return false;
347                         }
348
349                         filename = str.Trim ().Substring (index, str.Length - 2);
350                         if (filename.LastIndexOf  ('"') != -1)
351                                 return false; // file=""" -> no error
352
353                         return true;
354                 }
355
356                 void GetTag (out TagType tagtype, out string id, out TagAttributes attributes)
357                 {
358                         int token = tokenizer.get_token ();
359
360                         tagtype = TagType.ServerComment;
361                         id = null;
362                         attributes = null;
363                         switch (token){
364                         case '%':
365                                 GetServerTag (out tagtype, out id, out attributes);
366                                 break;
367                         case '/':
368                                 if (!Eat (Token.IDENTIFIER))
369                                         OnError ("expecting TAGNAME");
370
371                                 id = tokenizer.Value;
372                                 if (!Eat ('>'))
373                                         OnError ("expecting '>'. Got '" + id + "'");
374
375                                 tagtype = TagType.Close;
376                                 break;
377                         case '!':
378                                 bool double_dash = Eat (Token.DOUBLEDASH);
379                                 if (double_dash)
380                                         tokenizer.put_back ();
381
382                                 tokenizer.Verbatim = true;
383                                 string end = double_dash ? "-->" : ">";
384                                 string comment = GetVerbatim (tokenizer.get_token (), end);
385                                 tokenizer.Verbatim = false;
386                                 if (comment == null)
387                                         OnError ("Unfinished HTML comment/DTD");
388
389                                 string pathType, filename;
390                                 if (double_dash && GetInclude (comment, out pathType, out filename)) {
391                                         tagtype = TagType.Include;
392                                         attributes = new TagAttributes ();
393                                         attributes.Add (pathType, filename);
394                                 } else {
395                                         tagtype = TagType.Text;
396                                         id = "<!" + comment + end;
397                                 }
398                                 break;
399                         case Token.IDENTIFIER:
400                                 if (this.filename == "@@inner_string@@") {
401                                         // Actually not tag but "xxx < yyy" stuff in inner_string!
402                                         tagtype = TagType.Text;
403                                         tokenizer.InTag = false;
404                                         id = "<" + tokenizer.Odds + tokenizer.Value;
405                                 } else {
406                                         id = tokenizer.Value;
407                                         try {
408                                                 attributes = GetAttributes ();
409                                         } catch (Exception e) {
410                                                 OnError (e.Message);
411                                                 break;
412                                         }
413                                         
414                                         tagtype = TagType.Tag;
415                                         if (Eat ('/') && Eat ('>')) {
416                                                 tagtype = TagType.SelfClosing;
417                                         } else if (!Eat ('>')) {
418                                                 if (attributes.IsRunAtServer ()) {
419                                                         OnError ("The server tag is not well formed.");
420                                                         break;
421                                                 }
422                                                 tokenizer.Verbatim = true;
423                                                 attributes.Add ("", GetVerbatim (tokenizer.get_token (), ">") + ">");
424                                                 tokenizer.Verbatim = false;
425                                         }
426                                 }
427
428                                 break;
429                         default:
430                                 string idvalue = null;
431                                 // This is to handle code like:
432                                 //
433                                 //  <asp:ListItem runat="server"> < </asp:ListItem>
434                                 //
435                                 if ((char)token == '<') {
436                                         string odds = tokenizer.Odds;
437                                         if (odds != null && odds.Length > 0 && Char.IsWhiteSpace (odds [0])) {
438                                                 tokenizer.put_back ();
439                                                 idvalue = odds;
440                                         } else
441                                                 idvalue = tokenizer.Value;
442                                 } else
443                                         idvalue = tokenizer.Value;
444                                 
445                                 tagtype = TagType.Text;
446                                 tokenizer.InTag = false;
447                                 id = "<" + idvalue;
448                                 break;
449                         }
450                 }
451
452                 TagAttributes GetAttributes ()
453                 {
454                         int token;
455                         TagAttributes attributes;
456                         string id;
457                         bool wellFormedForServer = true;
458
459                         attributes = new TagAttributes ();
460                         while ((token = tokenizer.get_token ()) != Token.EOF){
461                                 if (token == '<' && Eat ('%')) {
462                                         tokenizer.Verbatim = true;
463                                         attributes.Add ("", "<%" + 
464                                                         GetVerbatim (tokenizer.get_token (), "%>") + "%>");
465                                         tokenizer.Verbatim = false;
466                                         tokenizer.InTag = true;
467                                         continue;
468                                 }
469                                         
470                                 if (token != Token.IDENTIFIER)
471                                         break;
472
473                                 id = tokenizer.Value;
474                                 if (Eat ('=')){
475                                         if (Eat (Token.ATTVALUE)){
476                                                 attributes.Add (id, tokenizer.Value);
477                                                 wellFormedForServer &= tokenizer.AlternatingQuotes;
478                                         } else if (Eat ('<') && Eat ('%')) {
479                                                 tokenizer.Verbatim = true;
480                                                 attributes.Add (id, "<%" + 
481                                                                 GetVerbatim (tokenizer.get_token (), "%>") + "%>");
482                                                 tokenizer.Verbatim = false;
483                                                 tokenizer.InTag = true;
484                                         } else {
485                                                 OnError ("expected ATTVALUE");
486                                                 return null;
487                                         }
488                                 } else {
489                                         attributes.Add (id, null);
490                                 }
491                         }
492
493                         tokenizer.put_back ();
494
495                         if (attributes.IsRunAtServer () && !wellFormedForServer) {
496                                 OnError ("The server tag is not well formed.");
497                                 return null;
498                         }
499                         
500                         return attributes;
501                 }
502
503                 string GetVerbatim (int token, string end)
504                 {
505                         StringBuilder vb_text = new StringBuilder ();
506                         StringBuilder tmp = new StringBuilder ();
507                         int i = 0;
508
509                         if (tokenizer.Value.Length > 1){
510                                 // May be we have a put_back token that is not a single character
511                                 vb_text.Append (tokenizer.Value);
512                                 token = tokenizer.get_token ();
513                         }
514
515                         end = end.ToLower (Helpers.InvariantCulture);
516                         int repeated = 0;
517                         for (int k = 0; k < end.Length; k++)
518                                 if (end [0] == end [k])
519                                         repeated++;
520                         
521                         while (token != Token.EOF){
522                                 if (Char.ToLower ((char) token, Helpers.InvariantCulture) == end [i]){
523                                         if (++i >= end.Length)
524                                                 break;
525                                         tmp.Append ((char) token);
526                                         token = tokenizer.get_token ();
527                                         continue;
528                                 } else if (i > 0) {
529                                         if (repeated > 1 && i == repeated && (char) token == end [0]) {
530                                                 vb_text.Append ((char) token);
531                                                 token = tokenizer.get_token ();
532                                                 continue;
533                                         }
534                                         vb_text.Append (tmp.ToString ());
535                                         tmp.Remove (0, tmp.Length);
536                                         i = 0;
537                                 }
538
539                                 vb_text.Append ((char) token);
540                                 token = tokenizer.get_token ();
541                         } 
542
543                         if (token == Token.EOF)
544                                 OnError ("Expecting " + end + " and got EOF.");
545
546                         return RemoveComments (vb_text.ToString ());
547                 }
548
549                 string RemoveComments (string text)
550                 {
551                         int end;
552                         int start = text.IndexOf ("<%--");
553
554                         while (start != -1) {
555                                 end = text.IndexOf ("--%>");
556                                 if (end == -1 || end <= start + 1)
557                                         break;
558
559                                 text = text.Remove (start, end - start + 4);
560                                 start = text.IndexOf ("<%--");
561                         }
562
563                         return text;
564                 }
565
566                 void GetServerTag (out TagType tagtype, out string id, out TagAttributes attributes)
567                 {
568                         string inside_tags;
569                         bool old = tokenizer.ExpectAttrValue;
570
571                         tokenizer.ExpectAttrValue = false;
572                         if (Eat ('@')){
573                                 tokenizer.ExpectAttrValue = old;
574                                 tagtype = TagType.Directive;
575                                 id = "";
576                                 if (Eat (Token.DIRECTIVE))
577                                         id = tokenizer.Value;
578
579                                 attributes = GetAttributes ();
580                                 if (!Eat ('%') || !Eat ('>'))
581                                         OnError ("expecting '%>'");
582
583                                 return;
584                         }
585                         
586                         if (Eat (Token.DOUBLEDASH)) {
587                                 tokenizer.ExpectAttrValue = old;
588                                 tokenizer.Verbatim = true;
589                                 inside_tags = GetVerbatim (tokenizer.get_token (), "--%>");
590                                 tokenizer.Verbatim = false;
591                                 id = null;
592                                 attributes = null;
593                                 tagtype = TagType.ServerComment;
594                                 return;
595                         }
596
597                         tokenizer.ExpectAttrValue = old;
598                         bool varname;
599                         bool databinding;
600 #if NET_4_0
601                         bool codeRenderEncode;
602 #endif
603                         varname = Eat ('=');
604                         databinding = !varname && Eat ('#');
605 #if NET_4_0
606                         codeRenderEncode = !databinding && !varname && Eat (':');
607 #endif
608                         string odds = tokenizer.Odds;
609                         
610                         tokenizer.Verbatim = true;
611                         inside_tags = GetVerbatim (tokenizer.get_token (), "%>");
612                         if (databinding && odds != null && odds.Length > 0) {
613                                 databinding = false;
614
615                                 // We encountered <% #something here %>, this should be passed
616                                 // verbatim to the compiler
617                                 inside_tags = '#' + inside_tags;
618                         }                       
619
620                         tokenizer.Verbatim = false;
621                         id = inside_tags;
622                         attributes = null;
623                         if (databinding)
624                                 tagtype = TagType.DataBinding;
625                         else if (varname)
626                                 tagtype = TagType.CodeRenderExpression;
627 #if NET_4_0
628                         else if (codeRenderEncode)
629                                 tagtype = TagType.CodeRenderEncode;
630 #endif
631                         else
632                                 tagtype = TagType.CodeRender;
633                 }
634
635                 public override string ToString ()
636                 {
637                         StringBuilder sb = new StringBuilder ("AspParser {");
638                         if (filename != null && filename.Length > 0)
639                                 sb.AppendFormat ("{0}:{1}.{2}", filename, beginLine, beginColumn);
640                         sb.Append ('}');
641
642                         return sb.ToString ();
643                 }
644                 
645                 void OnError (string msg)
646                 {
647                         ParseErrorHandler eh = events [errorEvent] as ParseErrorHandler;
648                         if (eh != null)
649                                 eh (this, msg);
650                 }
651
652                 void OnTagParsed (TagType tagtype, string id, TagAttributes attributes)
653                 {
654                         TagParsedHandler eh = events [tagParsedEvent] as TagParsedHandler;
655                         if (eh != null)
656                                 eh (this, tagtype, id, attributes);
657                 }
658
659                 void OnTextParsed (string text)
660                 {
661                         TextParsedHandler eh = events [textParsedEvent] as TextParsedHandler;
662                         if (eh != null)
663                                 eh (this, text);
664                 }
665
666                 void OnParsingComplete ()
667                 {
668                         ParsingCompleteHandler eh = events [parsingCompleteEvent] as ParsingCompleteHandler;
669                         if (eh != null)
670                                 eh ();
671                 }
672         }
673 }
674