OSDN Git Service

Exceptionクラスを直接スローしている箇所を派生型に置き換え (CA2201)
[opentween/open-tween.git] / OpenTween / MyCommon.cs
index c7e773b..426eedb 100644 (file)
@@ -42,12 +42,15 @@ using System.Runtime.Serialization.Json;
 using System.Reflection;
 using System.Diagnostics;
 using System.Text.RegularExpressions;
+using System.Net;
+using System.Net.Http;
 using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
+using OpenTween.Api;
 
 namespace OpenTween
 {
-    [Microsoft.VisualBasic.CompilerServices.StandardModule]
-    public sealed class MyCommon
+    public static class MyCommon
     {
         private static readonly object LockObj = new object();
         public static bool _endingFlag;        //終了フラグ
@@ -110,12 +113,6 @@ namespace OpenTween
             Unu = -1,
         }
 
-        public enum OutputzUrlmode
-        {
-            twittercom,
-            twittercomWithUsername,
-        }
-
         public enum HITRESULT
         {
             None,
@@ -143,7 +140,6 @@ namespace OpenTween
             FavAdd,                  //Fav追加
             FavRemove,               //Fav削除
             Follower,                //Followerリスト取得
-            OpenUri,                 //Uri開く
             Favorites,               //Fav取得
             Retweet,                 //Retweetする
             PublicSearch,            //公式検索
@@ -153,6 +149,7 @@ namespace OpenTween
             UserTimeline,            //UserTimeline
             BlockIds,                //Blocking/ids
             Configuration,           //Twitter Configuration読み込み
+            NoRetweetIds,            //RT非表示ユーザー取得
             //////
             ErrorState,              //エラー表示のみで後処理終了(認証エラー時など)
         }
@@ -213,9 +210,42 @@ namespace OpenTween
             Deleted = 256,
             ListCreated = 512,
             ListUpdated = 1024,
+            Unfollow = 2048,
+            ListUserSubscribed = 4096,
+            ListUserUnsubscribed = 8192,
+            ListDestroyed = 16384,
 
             All = (None | Favorite | Unfavorite | Follow | ListMemberAdded | ListMemberRemoved |
-                   Block | Unblock | UserUpdate | Deleted | ListCreated | ListUpdated),
+                   Block | Unblock | UserUpdate | Deleted | ListCreated | ListUpdated | Unfollow |
+                   ListUserSubscribed | ListUserUnsubscribed | ListDestroyed),
+        }
+
+        public static _Assembly EntryAssembly { get; internal set; }
+        public static string FileVersion { get; internal set; }
+
+        static MyCommon()
+        {
+            var assembly = Assembly.GetExecutingAssembly();
+            MyCommon.EntryAssembly = assembly;
+
+            var fileVersionAttribute = (AssemblyFileVersionAttribute)assembly
+                .GetCustomAttributes(typeof(AssemblyFileVersionAttribute)).First();
+            MyCommon.FileVersion = fileVersionAttribute.Version;
+        }
+
+        public static string GetErrorLogPath()
+        {
+            return Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
+        }
+
+        public static void TraceOut(WebApiException ex)
+        {
+            var message = ExceptionOutMessage(ex);
+
+            if (ex.ResponseText != null)
+                message += Environment.NewLine + "------- Response Data -------" + Environment.NewLine + ex.ResponseText;
+
+            TraceOut(TraceFlag, message);
         }
 
         public static void TraceOut(Exception ex, string Message)
@@ -234,8 +264,14 @@ namespace OpenTween
             lock (LockObj)
             {
                 if (!OutputFlag) return;
+
+                var logPath = MyCommon.GetErrorLogPath();
+                if (!Directory.Exists(logPath))
+                    Directory.CreateDirectory(logPath);
+
                 var now = DateTime.Now;
                 var fileName = string.Format("{0}Trace-{1:0000}{2:00}{3:00}-{4:00}{5:00}{6:00}.log", GetAssemblyName(), now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
+                fileName = Path.Combine(logPath, fileName);
 
                 using (var writer = new StreamWriter(fileName))
                 {
@@ -246,7 +282,7 @@ namespace OpenTween
                     writer.WriteLine(Properties.Resources.TraceOutText3);
                     writer.WriteLine(Properties.Resources.TraceOutText4, Environment.OSVersion.VersionString);
                     writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version.ToString());
-                    writer.WriteLine(Properties.Resources.TraceOutText6, fileVersion);
+                    writer.WriteLine(Properties.Resources.TraceOutText6, MyCommon.GetAssemblyName(), FileVersion);
                     writer.WriteLine(Message);
                     writer.WriteLine();
                 }
@@ -343,8 +379,14 @@ namespace OpenTween
             lock (LockObj)
             {
                 var IsTerminatePermission = true;
+
+                var logPath = MyCommon.GetErrorLogPath();
+                if (!Directory.Exists(logPath))
+                    Directory.CreateDirectory(logPath);
+
                 var now = DateTime.Now;
                 var fileName = string.Format("{0}-{1:0000}{2:00}{3:00}-{4:00}{5:00}{6:00}.log", GetAssemblyName(), now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
+                fileName = Path.Combine(logPath, fileName);
 
                 using (var writer = new StreamWriter(fileName))
                 {
@@ -362,13 +404,13 @@ namespace OpenTween
                     writer.WriteLine(Properties.Resources.UnhandledExceptionText4);
                     writer.WriteLine(Properties.Resources.UnhandledExceptionText5, Environment.OSVersion.VersionString);
                     writer.WriteLine(Properties.Resources.UnhandledExceptionText6, Environment.Version.ToString());
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText7, fileVersion);
+                    writer.WriteLine(Properties.Resources.UnhandledExceptionText7, MyCommon.GetAssemblyName(), FileVersion);
 
                     writer.Write(ExceptionOutMessage(ex, ref IsTerminatePermission));
                     writer.Flush();
                 }
 
-                switch (MessageBox.Show(string.Format(Properties.Resources.UnhandledExceptionText9, fileName, ApplicationSettings.FeedbackEmailAddress, ApplicationSettings.FeedbackTwitterName, Environment.NewLine),
+                switch (MessageBox.Show(MyCommon.ReplaceAppName(string.Format(Properties.Resources.UnhandledExceptionText9, fileName, ApplicationSettings.FeedbackEmailAddress, ApplicationSettings.FeedbackTwitterName, Environment.NewLine)),
                                    Properties.Resources.UnhandledExceptionText10, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Error))
                 {
                     case DialogResult.Yes:
@@ -377,9 +419,8 @@ namespace OpenTween
                     case DialogResult.No:
                         return false;
                     case DialogResult.Cancel:
-                        return IsTerminatePermission;
                     default:
-                        throw new Exception("");
+                        return IsTerminatePermission;
                 }
             }
         }
@@ -390,7 +431,7 @@ namespace OpenTween
         /// マルチバイト文字のコードはUTF-8またはUnicodeで自動的に判断します。
         /// </newpara>
         /// </summary>
-        /// <param name = input>エンコード対象のURL</param>
+        /// <param name="_input">エンコード対象のURL</param>
         /// <returns>マルチバイト文字の部分をUTF-8/%xx形式でエンコードした文字列を返します。</returns>
 
         public static string urlEncodeMultibyteChar(string _input)
@@ -402,11 +443,11 @@ namespace OpenTween
             foreach (var c in _input)
             {
                 c_ = c;
-                if (Convert.ToInt32(c) > 127) break;
+                if (Convert.ToInt32(c) > 127 || c == '%') break;
             }
-            if (Convert.ToInt32(c_) <= 127) return _input;
+            if (Convert.ToInt32(c_) <= 127 && c_ != '%') return _input;
 
-            var input = HttpUtility.UrlDecode(_input);
+            var input = Uri.UnescapeDataString(_input);
         retry:
             foreach (char c in input)
             {
@@ -453,38 +494,80 @@ namespace OpenTween
             return result;
         }
 
-        ////// <summary>
-        ////// URLのドメイン名をPunycode展開します。
-        ////// <para>
-        ////// ドメイン名がIDNでない場合はそのまま返します。
-        ////// ドメインラベルの区切り文字はFULLSTOP(.、U002E)に置き換えられます。
-        ////// </para>
-        ////// </summary>
-        ////// <param name="input">展開対象のURL</param>
-        ////// <returns>IDNが含まれていた場合はPunycodeに展開したURLをを返します。Punycode展開時にエラーが発生した場合はnullを返します。</returns>
-
-        public static string IDNDecode(string input)
+        /// <summary>
+        /// URLのドメイン名をPunycode展開します。
+        /// <para>
+        /// ドメイン名がIDNでない場合はそのまま返します。
+        /// ドメインラベルの区切り文字はFULLSTOP(.、U002E)に置き換えられます。
+        /// </para>
+        /// </summary>
+        /// <param name="input">展開対象のURL</param>
+        /// <returns>IDNが含まれていた場合はPunycodeに展開したURLをを返します。Punycode展開時にエラーが発生した場合はnullを返します。</returns>
+        public static string IDNEncode(string inputUrl)
         {
-            var result = "";
-            var IDNConverter = new IdnMapping();
+            try
+            {
+                var uriBuilder = new UriBuilder(inputUrl);
 
-            if (!input.Contains("://")) return null;
+                var idnConverter = new IdnMapping();
+                uriBuilder.Host = idnConverter.GetAscii(uriBuilder.Host);
 
-            // ドメイン名をPunycode展開
-            string Domain;
-            string AsciiDomain;
+                return uriBuilder.Uri.ToString();
+            }
+            catch (Exception)
+            {
+                return null;
+            }
+        }
 
+        public static string IDNDecode(string inputUrl)
+        {
             try
             {
-                Domain = input.Split('/')[2];
-                AsciiDomain = IDNConverter.GetAscii(Domain);
+                var uriBuilder = new UriBuilder(inputUrl);
+
+                if (uriBuilder.Host != null)
+                {
+                    var idnConverter = new IdnMapping();
+                    uriBuilder.Host = idnConverter.GetUnicode(uriBuilder.Host);
+                }
+
+                return uriBuilder.Uri.ToString();
             }
             catch (Exception)
             {
                 return null;
             }
+        }
+
+        /// <summary>
+        /// URL を画面上で人間に読みやすい文字列に変換する(エスケープ解除など)
+        /// </summary>
+        public static string ConvertToReadableUrl(string inputUrl)
+        {
+            try
+            {
+                var outputUrl = inputUrl;
+
+                // Punycodeをデコードする
+                outputUrl = MyCommon.IDNDecode(outputUrl);
+                if (outputUrl == null)
+                    return inputUrl;
+
+                // URL内で特殊な意味を持つ記号は元の文字に変換されることを避けるために二重エスケープする
+                // 参考: Firefoxの losslessDecodeURI() 関数
+                //   http://hg.mozilla.org/mozilla-central/annotate/FIREFOX_AURORA_27_BASE/browser/base/content/browser.js#l2128
+                outputUrl = Regex.Replace(outputUrl, @"%(2[3456BCF]|3[ABDF]|40)", @"%25$1", RegexOptions.IgnoreCase);
 
-            return input.Replace("://" + Domain, "://" + AsciiDomain);
+                // エスケープを解除する
+                outputUrl = Uri.UnescapeDataString(outputUrl);
+
+                return outputUrl;
+            }
+            catch (UriFormatException)
+            {
+                return inputUrl;
+            }
         }
 
         public static void MoveArrayItem(int[] values, int idx_fr, int idx_to)
@@ -523,32 +606,40 @@ namespace OpenTween
                 des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
                 des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
 
-                //暗号化されたデータを書き出すためのMemoryStream
-                using (var msOut = new MemoryStream())
+                MemoryStream msOut = null;
+                ICryptoTransform desdecrypt = null;
+
+                try
                 {
+                    //暗号化されたデータを書き出すためのMemoryStream
+                    msOut = new MemoryStream();
+
                     //DES暗号化オブジェクトの作成
-                    using (var desdecrypt = des.CreateEncryptor())
+                    desdecrypt = des.CreateEncryptor();
+
+                    //書き込むためのCryptoStreamの作成
+                    using (CryptoStream cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write))
                     {
-                        //書き込むためのCryptoStreamの作成
-                        using (var cryptStream = new System.Security.Cryptography.CryptoStream(
-                            msOut, desdecrypt,
-                            CryptoStreamMode.Write))
-                        {
-                            //書き込む
-                            cryptStream.Write(bytesIn, 0, bytesIn.Length);
-                            cryptStream.FlushFinalBlock();
-                            //暗号化されたデータを取得
-                            var bytesOut = msOut.ToArray();
-
-                            //閉じる
-                            cryptStream.Close();
-                            msOut.Close();
-
-                            //Base64で文字列に変更して結果を返す
-                            return Convert.ToBase64String(bytesOut);
-                        }
+                        //Disposeが重複して呼ばれないようにする
+                        MemoryStream msTmp = msOut;
+                        msOut = null;
+                        desdecrypt = null;
+
+                        //書き込む
+                        cryptStream.Write(bytesIn, 0, bytesIn.Length);
+                        cryptStream.FlushFinalBlock();
+                        //暗号化されたデータを取得
+                        var bytesOut = msTmp.ToArray();
+
+                        //Base64で文字列に変更して結果を返す
+                        return Convert.ToBase64String(bytesOut);
                     }
                 }
+                finally
+                {
+                    if (msOut != null) msOut.Dispose();
+                    if (desdecrypt != null) desdecrypt.Dispose();
+                }
             }
         }
 
@@ -568,34 +659,42 @@ namespace OpenTween
 
                 //Base64で文字列をバイト配列に戻す
                 var bytesIn = Convert.FromBase64String(str);
-                //暗号化されたデータを読み込むためのMemoryStream
-                using (var msIn = new MemoryStream(bytesIn))
+
+                MemoryStream msIn = null;
+                ICryptoTransform desdecrypt = null;
+                CryptoStream cryptStreem = null;
+
+                try
                 {
+                    //暗号化されたデータを読み込むためのMemoryStream
+                    msIn = new MemoryStream(bytesIn);
                     //DES復号化オブジェクトの作成
-                    using (var desdecrypt = des.CreateDecryptor())
+                    desdecrypt = des.CreateDecryptor();
+                    //読み込むためのCryptoStreamの作成
+                    cryptStreem = new CryptoStream(msIn, desdecrypt, CryptoStreamMode.Read);
+
+                    //Disposeが重複して呼ばれないようにする
+                    msIn = null;
+                    desdecrypt = null;
+
+                    //復号化されたデータを取得するためのStreamReader
+                    using (StreamReader srOut = new StreamReader(cryptStreem, Encoding.UTF8))
                     {
-                        //読み込むためのCryptoStreamの作成
-                        using (var cryptStreem = new CryptoStream(
-                            msIn, desdecrypt,
-                            CryptoStreamMode.Read))
-                        {
-                            //復号化されたデータを取得するためのStreamReader
-                            using (var srOut = new StreamReader(
-                                cryptStreem, Encoding.UTF8))
-                            {
-                                //復号化されたデータを取得する
-                                var result = srOut.ReadToEnd();
-
-                                //閉じる
-                                srOut.Close();
-                                cryptStreem.Close();
-                                msIn.Close();
-
-                                return result;
-                            }
-                        }
+                        //Disposeが重複して呼ばれないようにする
+                        cryptStreem = null;
+
+                        //復号化されたデータを取得する
+                        var result = srOut.ReadToEnd();
+
+                        return result;
                     }
                 }
+                finally
+                {
+                    if (msIn != null) msIn.Dispose();
+                    if (desdecrypt != null) desdecrypt.Dispose();
+                    if (cryptStreem != null) cryptStreem.Dispose();
+                }
             }
         }
 
@@ -652,18 +751,7 @@ namespace OpenTween
             //RTByMe
         }
 
-        public static string fileVersion = "";
-
-        public static string GetUserAgentString()
-        {
-            if (string.IsNullOrEmpty(fileVersion))
-            {
-                throw new Exception("fileversion is not Initialized.");
-            }
-            return GetAssemblyName() + "/" + fileVersion;
-        }
-
-        public static ApiInformation TwitterApiInfo = new ApiInformation();
+        public static TwitterApiStatus TwitterApiInfo = new TwitterApiStatus();
 
         public static bool IsAnimatedGif(string filename)
         {
@@ -702,6 +790,7 @@ namespace OpenTween
             DateTime rslt;
             string[] format = {
                 "ddd MMM dd HH:mm:ss zzzz yyyy",
+                "ddd, d MMM yyyy HH:mm:ss zzzz",
             };
             foreach (var fmt in format)
             {
@@ -725,11 +814,9 @@ namespace OpenTween
         public static T CreateDataFromJson<T>(string content)
         {
             T data;
-            using (var stream = new MemoryStream())
+            var buf = Encoding.Unicode.GetBytes(content);
+            using (var stream = new MemoryStream(buf))
             {
-                var buf = Encoding.Unicode.GetBytes(content);
-                stream.Write(Encoding.Unicode.GetBytes(content), offset: 0, count: buf.Length);
-                stream.Seek(offset: 0, loc: SeekOrigin.Begin);
                 data = (T)((new DataContractJsonSerializer(typeof(T))).ReadObject(stream));
             }
             return data;
@@ -747,7 +834,7 @@ namespace OpenTween
             }
         }
 
-        static bool IsValidEmail(string strIn)
+        public static bool IsValidEmail(string strIn)
         {
             // Return true if strIn is in valid e-mail format.
             return Regex.IsMatch(strIn,
@@ -762,9 +849,14 @@ namespace OpenTween
         /// <returns><paramref name="keys"/> で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。</returns>
         public static bool IsKeyDown(params Keys[] keys)
         {
-            foreach (Keys key in keys)
+            return MyCommon._IsKeyDown(Control.ModifierKeys, keys);
+        }
+
+        internal static bool _IsKeyDown(Keys modifierKeys, Keys[] targetKeys)
+        {
+            foreach (Keys key in targetKeys)
             {
-                if ((Control.ModifierKeys & key) != key)
+                if ((modifierKeys & key) != key)
                 {
                     return false;
                 }
@@ -781,7 +873,137 @@ namespace OpenTween
         /// <returns>アプリケーションのアセンブリ名</returns>
         public static string GetAssemblyName()
         {
-            return Assembly.GetEntryAssembly().GetName().Name;
+            return MyCommon.EntryAssembly.GetName().Name;
+        }
+
+        /// <summary>
+        /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
+        /// </summary>
+        /// <param name="orig">対象となる文字列</param>
+        /// <returns>置換後の文字列</returns>
+        public static string ReplaceAppName(string orig)
+        {
+            return MyCommon.ReplaceAppName(orig, Application.ProductName);
+        }
+
+        /// <summary>
+        /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
+        /// </summary>
+        /// <param name="orig">対象となる文字列</param>
+        /// <param name="appname">アプリケーション名</param>
+        /// <returns>置換後の文字列</returns>
+        public static string ReplaceAppName(string orig, string appname)
+        {
+            return orig.Replace("%AppName%", appname);
+        }
+
+        /// <summary>
+        /// 表示用のバージョン番号の文字列を生成する
+        /// </summary>
+        /// <remarks>
+        /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-beta1」が出力される
+        /// </remarks>
+        /// <returns>
+        /// 生成されたバージョン番号の文字列
+        /// </returns>
+        public static string GetReadableVersion(string versionStr = null)
+        {
+            var version = Version.Parse(versionStr ?? MyCommon.FileVersion);
+
+            return GetReadableVersion(version);
+        }
+
+        /// <summary>
+        /// 表示用のバージョン番号の文字列を生成する
+        /// </summary>
+        /// <remarks>
+        /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-beta1」が出力される
+        /// </remarks>
+        /// <returns>
+        /// 生成されたバージョン番号の文字列
+        /// </returns>
+        public static string GetReadableVersion(Version version)
+        {
+            var versionNum = new[] { version.Major, version.Minor, version.Build, version.Revision };
+
+            if (versionNum[3] == 0)
+            {
+                return string.Format("{0}.{1}.{2}", versionNum[0], versionNum[1], versionNum[2]);
+            }
+            else
+            {
+                versionNum[2] = versionNum[2] + 1;
+
+                // 10を越えたら桁上げ
+                if (versionNum[2] >= 10)
+                {
+                    versionNum[1] += versionNum[2] / 10;
+                    versionNum[2] %= 10;
+
+                    if (versionNum[1] >= 10)
+                    {
+                        versionNum[0] += versionNum[1] / 10;
+                        versionNum[1] %= 10;
+                    }
+                }
+
+                return string.Format("{0}.{1}.{2}-beta{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
+            }
+        }
+
+        public const string TwitterUrl = "https://twitter.com/";
+
+        public static string GetStatusUrl(PostClass post)
+        {
+            if (post.RetweetedId == null)
+                return GetStatusUrl(post.ScreenName, post.StatusId);
+            else
+                return GetStatusUrl(post.ScreenName, post.RetweetedId.Value);
+        }
+
+        public static string GetStatusUrl(string screenName, long statusId)
+        {
+            return TwitterUrl + screenName + "/status/" + statusId.ToString();
+        }
+
+        /// <summary>
+        /// 指定された IDictionary を元にクエリ文字列を生成します
+        /// </summary>
+        /// <param name="param">生成するクエリの key-value コレクション</param>
+        public static string BuildQueryString(IDictionary<string, string> param)
+        {
+            if (param == null || param.Count == 0)
+                return string.Empty;
+
+            var query = param
+                .Where(x => x.Value != null)
+                .Select(x => EscapeQueryString(x.Key) + '=' + EscapeQueryString(x.Value));
+
+            return string.Join("&", query);
+        }
+
+        // .NET 4.5+: Reserved characters のうち、Uriクラスによってエスケープ強制解除されてしまうものも最初から Unreserved として扱う
+        private static readonly HashSet<char> UnreservedChars =
+            new HashSet<char>("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:");
+
+        /// <summary>
+        /// 2バイト文字も考慮したクエリ用エンコード
+        /// </summary>
+        /// <param name="stringToEncode">エンコードする文字列</param>
+        /// <returns>エンコード結果文字列</returns>
+        public static string EscapeQueryString(string stringToEncode)
+        {
+            var sb = new StringBuilder(stringToEncode.Length * 2);
+
+            foreach (var b in Encoding.UTF8.GetBytes(stringToEncode))
+            {
+                if (UnreservedChars.Contains((char)b))
+                    sb.Append((char)b);
+                else
+                    sb.AppendFormat("%{0:X2}", b);
+            }
+
+            return sb.ToString();
         }
     }
 }