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