OSDN Git Service

メイン画面の言語プルダウンに存在しないコードを手入力するとエラーになる問題を修正。
[wptscs/wpts.git] / Wptscs / Websites / MediaWiki.cs
1 // ================================================================================================
2 // <summary>
3 //      MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>
4 //
5 // <copyright file="MediaWiki.cs" company="honeplusのメモ帳">
6 //      Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
7 // <author>
8 //      Honeplus</author>
9 // ================================================================================================
10
11 namespace Honememo.Wptscs.Websites
12 {
13     using System;
14     using System.Collections.Generic;
15     using System.IO;
16     using System.Xml;
17     using System.Xml.Serialization;
18     using Honememo.Utilities;
19     using Honememo.Wptscs.Models;
20     using Honememo.Wptscs.Properties;
21     using Honememo.Wptscs.Utilities;
22
23     /// <summary>
24     /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。
25     /// </summary>
26     public class MediaWiki : Website, IXmlSerializable
27     {
28         #region private変数
29         
30         /// <summary>
31         /// 名前空間情報取得用にアクセスするAPI。
32         /// </summary>
33         private string namespacePath;
34
35         /// <summary>
36         /// 記事のXMLデータが存在するパス。
37         /// </summary>
38         private string exportPath;
39
40         /// <summary>
41         /// リダイレクトの文字列。
42         /// </summary>
43         private string redirect;
44
45         /// <summary>
46         /// テンプレートの名前空間を示す番号。
47         /// </summary>
48         private int? templateNamespace;
49
50         /// <summary>
51         /// カテゴリの名前空間を示す番号。
52         /// </summary>
53         private int? categoryNamespace;
54
55         /// <summary>
56         /// 画像の名前空間を示す番号。
57         /// </summary>
58         private int? fileNamespace;
59
60         /// <summary>
61         /// Wikipedia書式のシステム定義変数。
62         /// </summary>
63         /// <remarks>初期値は http://www.mediawiki.org/wiki/Help:Magic_words を参照</remarks>
64         private IList<string> magicWords;
65
66         /// <summary>
67         /// MediaWikiの名前空間の情報。
68         /// </summary>
69         private IDictionary<int, IList<string>> namespaces = new Dictionary<int, IList<string>>();
70
71         #endregion
72
73         #region コンストラクタ
74
75         /// <summary>
76         /// コンストラクタ(MediaWiki全般)。
77         /// </summary>
78         /// <param name="language">ウェブサイトの言語。</param>
79         /// <param name="location">ウェブサイトの場所。</param>
80         public MediaWiki(Language language, string location) : this()
81         {
82             // メンバ変数の初期設定
83             this.Language = language;
84             this.Location = location;
85         }
86
87         /// <summary>
88         /// コンストラクタ(Wikipedia用)。
89         /// </summary>
90         /// <param name="language">ウェブサイトの言語。</param>
91         public MediaWiki(Language language) : this()
92         {
93             // メンバ変数の初期設定
94             // ※ オーバーロードメソッドを呼んでいないのは、languageがnullのときに先にエラーになるから
95             this.Language = language;
96             this.Location = String.Format(Settings.Default.WikipediaLocation, language.Code);
97         }
98
99         /// <summary>
100         /// コンストラクタ(シリアライズ or 拡張用)。
101         /// </summary>
102         protected MediaWiki()
103         {
104             this.WebProxy = new AppDefaultWebProxy();
105             this.DocumentationTemplates = new List<string>();
106         }
107
108         #endregion
109
110         #region 設定ファイルに初期値を持つプロパティ
111         
112         /// <summary>
113         /// MediaWiki名前空間情報取得用にアクセスするAPI。
114         /// </summary>
115         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
116         public string NamespacePath
117         {
118             get
119             {
120                 if (String.IsNullOrEmpty(this.namespacePath))
121                 {
122                     return Settings.Default.MediaWikiNamespacePath;
123                 }
124
125                 return this.namespacePath;
126             }
127
128             set
129             {
130                 this.namespacePath = value;
131             }
132         }
133
134         /// <summary>
135         /// 記事のXMLデータが存在するパス。
136         /// </summary>
137         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
138         public string ExportPath
139         {
140             get
141             {
142                 if (String.IsNullOrEmpty(this.exportPath))
143                 {
144                     return Settings.Default.MediaWikiExportPath;
145                 }
146
147                 return this.exportPath;
148             }
149
150             set
151             {
152                 this.exportPath = value;
153             }
154         }
155
156         /// <summary>
157         /// リダイレクトの文字列。
158         /// </summary>
159         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
160         public string Redirect
161         {
162             get
163             {
164                 if (String.IsNullOrEmpty(this.redirect))
165                 {
166                     return Settings.Default.MediaWikiRedirect;
167                 }
168
169                 return this.redirect;
170             }
171
172             set
173             {
174                 this.redirect = value;
175             }
176         }
177
178         /// <summary>
179         /// テンプレートの名前空間を示す番号。
180         /// </summary>
181         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
182         public int TemplateNamespace
183         {
184             get
185             {
186                 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;
187             }
188
189             set
190             {
191                 this.templateNamespace = value;
192             }
193         }
194
195         /// <summary>
196         /// カテゴリの名前空間を示す番号。
197         /// </summary>
198         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
199         public int CategoryNamespace
200         {
201             get
202             {
203                 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;
204             }
205
206             set
207             {
208                 this.categoryNamespace = value;
209             }
210         }
211
212         /// <summary>
213         /// 画像の名前空間を示す番号。
214         /// </summary>
215         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
216         public int FileNamespace
217         {
218             get
219             {
220                 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;
221             }
222
223             set
224             {
225                 this.fileNamespace = value;
226             }
227         }
228
229         /// <summary>
230         /// Wikipedia書式のシステム定義変数。
231         /// </summary>
232         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
233         public IList<string> MagicWords
234         {
235             get
236             {
237                 if (this.magicWords == null)
238                 {
239                     string[] w = new string[Settings.Default.MediaWikiMagicWords.Count];
240                     Settings.Default.MediaWikiMagicWords.CopyTo(w, 0);
241                     return w;
242                 }
243
244                 return this.magicWords;
245             }
246
247             set
248             {
249                 this.magicWords = value;
250             }
251         }
252
253         #endregion
254
255         #region それ以外のプロパティ
256
257         /// <summary>
258         /// MediaWikiの名前空間の情報。
259         /// </summary>
260         /// <remarks>値が指定されていない場合、サーバーから情報を取得。</remarks>
261         public IDictionary<int, IList<string>> Namespaces
262         {
263             get
264             {
265                 lock (this.namespaces)
266                 {
267                     // 値が設定されていない場合、サーバーから取得して初期化する
268                     // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
269                     // ※ MagicWordsがnullでこちらが空で若干条件が違うのは、あちらは設定ファイルに
270                     //    保存する設定だが、こちらは設定ファイルに保存しない基本的に読み込み用の設定だから。
271                     if (this.namespaces.Count > 0)
272                     {
273                         return this.namespaces;
274                     }
275
276                     // APIのXMLデータをMediaWikiサーバーから取得
277                     XmlDocument xml = new XmlDocument();
278                     using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.NamespacePath)))
279                     {
280                         xml.Load(reader);
281                     }
282
283                     // ルートエレメントまで取得し、フォーマットをチェック
284                     XmlElement rootElement = xml["api"];
285                     if (rootElement == null)
286                     {
287                         // XMLは取得できたが空 or フォーマットが想定外
288                         throw new InvalidDataException("parse failed");
289                     }
290
291                     // クエリーを取得
292                     XmlElement queryElement = rootElement["query"];
293                     if (queryElement == null)
294                     {
295                         // フォーマットが想定外
296                         throw new InvalidDataException("parse failed");
297                     }
298
299                     // ネームスペースブロックを取得、ネームスペースブロックまでは必須
300                     XmlElement namespacesElement = queryElement["namespaces"];
301                     if (namespacesElement == null)
302                     {
303                         // フォーマットが想定外
304                         throw new InvalidDataException("parse failed");
305                     }
306
307                     // ネームスペースを取得
308                     foreach (XmlNode node in namespacesElement.ChildNodes)
309                     {
310                         XmlElement namespaceElement = node as XmlElement;
311                         if (namespaceElement != null)
312                         {
313                             try
314                             {
315                                 int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));
316                                 IList<string> values = new List<string>();
317                                 values.Add(namespaceElement.InnerText);
318                                 this.namespaces[id] = values;
319
320                                 // あればシステム名?も設定
321                                 string canonical = namespaceElement.GetAttribute("canonical");
322                                 if (!String.IsNullOrEmpty(canonical))
323                                 {
324                                     values.Add(canonical);
325                                 }
326                             }
327                             catch (Exception e)
328                             {
329                                 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
330                                 System.Diagnostics.Debug.WriteLine("MediaWiki.Namespaces > 例外発生 : " + e);
331                             }
332                         }
333                     }
334
335                     // ネームスペースエイリアスブロックを取得、無い場合も想定
336                     XmlElement aliasesElement = queryElement["namespacealiases"];
337                     if (aliasesElement != null)
338                     {
339                         // ネームスペースエイリアスを取得
340                         foreach (XmlNode node in aliasesElement.ChildNodes)
341                         {
342                             XmlElement namespaceElement = node as XmlElement;
343                             if (namespaceElement != null)
344                             {
345                                 try
346                                 {
347                                     int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));
348                                     IList<string> values = new List<string>();
349                                     if (this.namespaces.ContainsKey(id))
350                                     {
351                                         values = this.namespaces[id];
352                                     }
353
354                                     values.Add(namespaceElement.InnerText);
355                                 }
356                                 catch (Exception e)
357                                 {
358                                     // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
359                                     System.Diagnostics.Debug.WriteLine("MediaWiki.Namespaces > 例外発生 : " + e);
360                                 }
361                             }
362                         }
363                     }
364                 }
365
366                 return this.namespaces;
367             }
368
369             set
370             {
371                 // ※必須な情報が設定されていない場合、ArgumentNullExceptionを返す
372                 if (value == null)
373                 {
374                     throw new ArgumentNullException("namespaces");
375                 }
376
377                 this.namespaces = value;
378             }
379         }
380
381         /// <summary>
382         /// Template:Documentation(言語間リンク等を別ページに記述するためのテンプレート)に相当するページ名。
383         /// </summary>
384         /// <remarks>空の場合、その言語版にはこれに相当する機能は無いものとして扱う。</remarks>
385         public IList<string> DocumentationTemplates
386         {
387             get;
388             protected set;
389         }
390
391         /// <summary>
392         /// Template:Documentationで指定が無い場合に参照するページ名。
393         /// </summary>
394         /// <remarks>
395         /// ほとんどの言語では[[/Doc]]の模様。
396         /// 空の場合、明示的な指定が無い場合は参照不能として扱う。
397         /// </remarks>
398         public string DocumentationTemplateDefaultPage
399         {
400             get;
401             set;
402         }
403
404         /// <summary>
405         /// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
406         /// </summary>
407         /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
408         public string LinkInterwikiFormat
409         {
410             get;
411             set;
412         }
413
414         /// <summary>
415         /// Template:Langで書式化するためのフォーマット。
416         /// </summary>
417         /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
418         public string LangFormat
419         {
420             get;
421             set;
422         }
423
424         /// <summary>
425         /// このクラスで使用するWebアクセス用Proxyインスタンス。
426         /// </summary>
427         /// <remarks>setterはユニットテスト用に公開。</remarks>
428         public IWebProxy WebProxy
429         {
430             protected get;
431             set;
432         }
433
434         #endregion
435
436         #region 公開メソッド
437
438         /// <summary>
439         /// ページを取得。
440         /// </summary>
441         /// <param name="title">ページタイトル。</param>
442         /// <returns>取得したページ。</returns>
443         /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
444         /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
445         public override Page GetPage(string title)
446         {
447             // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
448             // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
449             string escapeTitle = title;
450             if (new Uri(this.Location).Scheme == "file")
451             {
452                 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
453             }
454
455             // ページのXMLデータをMediaWikiサーバーから取得
456             XmlDocument xml = new XmlDocument();
457             try
458             {
459                 using (Stream reader = this.WebProxy.GetStream(
460                     new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ExportPath, escapeTitle))))
461                 {
462                     xml.Load(reader);
463                 }
464             }
465             catch (System.Net.WebException e)
466             {
467                 // 404エラーによるページ取得失敗は詰め替えて返す
468                 if (this.IsNotFound(e))
469                 {
470                     throw new FileNotFoundException("page not found", e);
471                 }
472
473                 throw e;
474             }
475
476             // ルートエレメントまで取得し、フォーマットをチェック
477             XmlElement rootElement = xml["mediawiki"];
478             if (rootElement == null)
479             {
480                 // XMLは取得できたが空 or フォーマットが想定外
481                 throw new InvalidDataException("parse failed");
482             }
483
484             // ページの解析
485             XmlElement pageElement = rootElement["page"];
486             if (pageElement == null)
487             {
488                 // ページ無し
489                 throw new FileNotFoundException("page not found");
490             }
491
492             // ページ名、ページ本文、最終更新日時
493             // ※ 一応、各項目が無くても動作するようにする
494             string pageTitle = XmlUtils.InnerText(pageElement["title"], title);
495             string text = null;
496             DateTime? time = null;
497             XmlElement revisionElement = pageElement["revision"];
498             if (revisionElement != null)
499             {
500                 text = XmlUtils.InnerText(revisionElement["text"], null);
501                 XmlElement timeElement = revisionElement["timestamp"];
502                 if (timeElement != null)
503                 {
504                     time = new DateTime?(DateTime.Parse(timeElement.InnerText));
505                 }
506             }
507
508             // ページ情報を作成して返す
509             return new MediaWikiPage(this, pageTitle, text, time);
510         }
511
512         /// <summary>
513         /// 指定された文字列がWikipediaのシステム変数に相当かを判定。
514         /// </summary>
515         /// <param name="text">チェックする文字列。</param>
516         /// <returns><c>true</c> システム変数に相当。</returns>
517         public bool IsMagicWord(string text)
518         {
519             string s = text != null ? text : String.Empty;
520
521             // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
522             foreach (string variable in this.MagicWords)
523             {
524                 if (s == variable || s.StartsWith(variable + ":"))
525                 {
526                     return true;
527                 }
528             }
529
530             return false;
531         }
532
533         /// <summary>
534         /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
535         /// </summary>
536         /// <param name="title">記事名。</param>
537         /// <param name="lang">言語。</param>
538         /// <param name="langTitle">他言語版記事名。</param>
539         /// <param name="label">表示名。</param>
540         /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
541         public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
542         {
543             if (String.IsNullOrEmpty(this.LinkInterwikiFormat))
544             {
545                 return null;
546             }
547
548             return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
549         }
550
551         /// <summary>
552         /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
553         /// </summary>
554         /// <param name="lang">言語。</param>
555         /// <param name="text">文字列。</param>
556         /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
557         /// <remarks>
558         /// この<para>lang</para>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
559         /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
560         /// </remarks>
561         public string FormatLang(string lang, string text)
562         {
563             if (String.IsNullOrEmpty(this.LangFormat))
564             {
565                 return null;
566             }
567
568             return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
569         }
570         
571         #endregion
572
573         #region XMLシリアライズ用メソッド
574
575         /// <summary>
576         /// シリアライズするXMLのスキーマ定義を返す。
577         /// </summary>
578         /// <returns>XML表現を記述するXmlSchema。</returns>
579         public System.Xml.Schema.XmlSchema GetSchema()
580         {
581             return null;
582         }
583
584         /// <summary>
585         /// XMLからオブジェクトをデシリアライズする。
586         /// </summary>
587         /// <param name="reader">デシリアライズ元のXmlReader</param>
588         public void ReadXml(XmlReader reader)
589         {
590             XmlDocument xml = new XmlDocument();
591             xml.Load(reader);
592
593             // Webサイト
594             // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
595             XmlElement siteElement = xml.DocumentElement;
596             this.Location = siteElement.SelectSingleNode("Location").InnerText;
597
598             using (XmlReader r = XmlReader.Create(
599                 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
600             {
601                 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
602             }
603
604             this.NamespacePath = XmlUtils.InnerText(siteElement.SelectSingleNode("NamespacePath"));
605             this.ExportPath = XmlUtils.InnerText(siteElement.SelectSingleNode("ExportPath"));
606             this.Redirect = XmlUtils.InnerText(siteElement.SelectSingleNode("Redirect"));
607
608             string text = XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace"));
609             if (!String.IsNullOrEmpty(text))
610             {
611                 this.TemplateNamespace = int.Parse(text);
612             }
613
614             text = XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace"));
615             if (!String.IsNullOrEmpty(text))
616             {
617                 this.CategoryNamespace = int.Parse(text);
618             }
619
620             text = XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace"));
621             if (!String.IsNullOrEmpty(text))
622             {
623                 this.FileNamespace = int.Parse(text);
624             }
625
626             // システム定義変数
627             IList<string> variables = new List<string>();
628             foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
629             {
630                 variables.Add(variableNode.InnerText);
631             }
632
633             if (variables.Count > 0)
634             {
635                 // 初期値の都合上、値がある場合のみ
636                 this.MagicWords = variables;
637             }
638
639             // Template:Documentationの設定
640             this.DocumentationTemplates = new List<string>();
641             foreach (XmlNode docNode in siteElement.SelectNodes("DocumentationTemplates/DocumentationTemplate"))
642             {
643                 this.DocumentationTemplates.Add(docNode.InnerText);
644                 XmlElement docElement = docNode as XmlElement;
645                 if (docElement != null)
646                 {
647                     // ※ XML上DefaultPageはテンプレートごとに異なる値を持てるが、
648                     //    そうした例を見かけたことがないため、代表で一つの値のみ使用
649                     //    (複数値が持てるのも、リダイレクトが存在するためその対策として)
650                     this.DocumentationTemplateDefaultPage = docElement.GetAttribute("DefaultPage");
651                 }
652             }
653
654             this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
655             this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
656         }
657
658         /// <summary>
659         /// オブジェクトをXMLにシリアライズする。
660         /// </summary>
661         /// <param name="writer">シリアライズ先のXmlWriter</param>
662         public void WriteXml(XmlWriter writer)
663         {
664             writer.WriteElementString("Location", this.Location);
665             new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
666
667             // MediaWiki固有の情報
668             // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
669             writer.WriteElementString("NamespacePath", this.namespacePath);
670             writer.WriteElementString("ExportPath", this.exportPath);
671             writer.WriteElementString("Redirect", this.redirect);
672             writer.WriteElementString(
673                 "TemplateNamespace",
674                 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : String.Empty);
675             writer.WriteElementString(
676                 "CategoryNamespace",
677                 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : String.Empty);
678             writer.WriteElementString(
679                 "FileNamespace",
680                 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : String.Empty);
681
682             // システム定義変数
683             writer.WriteStartElement("MagicWords");
684             if (this.magicWords != null)
685             {
686                 foreach (string variable in this.magicWords)
687                 {
688                     writer.WriteElementString("Variable", variable);
689                 }
690             }
691
692             // Template:Documentationの設定
693             writer.WriteEndElement();
694             writer.WriteStartElement("DocumentationTemplates");
695             foreach (string doc in this.DocumentationTemplates)
696             {
697                 writer.WriteStartElement("DocumentationTemplate");
698                 writer.WriteAttributeString("DefaultPage", this.DocumentationTemplateDefaultPage);
699                 writer.WriteValue(doc);
700                 writer.WriteEndElement();
701             }
702
703             writer.WriteEndElement();
704             writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
705             writer.WriteElementString("LangFormat", this.LangFormat);
706         }
707
708         #endregion
709     }
710 }