OSDN Git Service

URLをWebブラウザで開く時にスキームが正しいか検証する
[opentween/open-tween.git] / OpenTween / MyCommon.cs
index 61ff71c..04b7d93 100644 (file)
@@ -7,56 +7,61 @@
 //           (c) 2011      Egtra (@egtra) <http://dev.activebasic.com/egtra/>
 //           (c) 2011      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
 // All rights reserved.
-// 
+//
 // This file is part of OpenTween.
-// 
+//
 // This program is free software; you can redistribute it and/or modify it
 // under the terms of the GNU General public License as published by the Free
 // Software Foundation; either version 3 of the License, or (at your option)
 // any later version.
-// 
+//
 // This program is distributed in the hope that it will be useful, but
 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
-// for more details. 
-// 
+// for more details.
+//
 // You should have received a copy of the GNU General public License along
 // with this program. if not, see <http://www.gnu.org/licenses/>, or write to
 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
 // Boston, MA 02110-1301, USA.
 
+#nullable enable
+
 using System;
+using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.IO;
-using System.Windows.Forms;
-using System.Web;
-using System.Globalization;
-using System.Security.Cryptography;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 using System.Drawing;
 using System.Drawing.Imaging;
-using System.Collections;
-using System.Security.Principal;
-using System.Runtime.Serialization.Json;
-using System.Reflection;
-using System.Diagnostics;
-using System.Text.RegularExpressions;
+using System.Globalization;
+using System.IO;
+using System.Linq;
 using System.Net;
 using System.Net.Http;
 using System.Net.NetworkInformation;
+using System.Reflection;
 using System.Runtime.InteropServices;
+using System.Runtime.Serialization.Json;
+using System.Security.Cryptography;
+using System.Security.Principal;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Web;
+using System.Windows.Forms;
 using OpenTween.Api;
 using OpenTween.Models;
+using OpenTween.Setting;
 
 namespace OpenTween
 {
     public static class MyCommon
     {
-        private static readonly object LockObj = new object();
-        public static bool _endingFlag;        //終了フラグ
-        public static string cultureStr = null;
-        public static string settingPath;
+        private static readonly object LockObj = new();
+
+        public static bool EndingFlag { get; set; } // 終了フラグ
 
         public enum IconSizes
         {
@@ -104,14 +109,28 @@ namespace OpenTween
         {
             TinyUrl,
             Isgd,
-            Twurl,
             Bitly,
             Jmp,
             Uxnu,
-            //特殊
+            // 特殊
             Nicoms,
-            //廃止
+            // 廃止
             Unu = -1,
+            Twurl = -1,
+        }
+
+        public enum ListItemDoubleClickActionType
+        {
+            // 設定ファイルの互換性を保つため新規の項目は途中に追加しないこと
+            Reply,
+            Favorite,
+            ShowProfile,
+            ShowTimeline,
+            ShowRelated,
+            OpenHomeInBrowser,
+            OpenStatusInBrowser,
+            None,
+            ReplyAll,
         }
 
         public enum HITRESULT
@@ -126,33 +145,32 @@ namespace OpenTween
         public enum HttpTimeOut
         {
             MinValue = 10,
-            MaxValue = 120,
+            MaxValue = 1000,
             DefaultValue = 20,
         }
 
-        //Backgroundworkerへ処理種別を通知するための引数用enum
+        // Backgroundworkerへ処理種別を通知するための引数用enum
         public enum WORKERTYPE
         {
-            Timeline,                //タイムライン取得
-            Reply,                   //返信取得
-            DirectMessegeRcv,        //受信DM取得
-            DirectMessegeSnt,        //送信DM取得
-            PostMessage,             //発言POST
-            FavAdd,                  //Fav追加
-            FavRemove,               //Fav削除
-            Follower,                //Followerリスト取得
-            Favorites,               //Fav取得
-            Retweet,                 //Retweetする
-            PublicSearch,            //公式検索
-            List,                    //Lists
-            Related,                 //関連発言
-            UserStream,              //UserStream
-            UserTimeline,            //UserTimeline
-            BlockIds,                //Blocking/ids
-            Configuration,           //Twitter Configuration読み込み
-            NoRetweetIds,            //RT非表示ユーザー取得
+            Timeline, // タイムライン取得
+            Reply, // 返信取得
+            DirectMessegeRcv, // 受信DM取得
+            DirectMessegeSnt, // 送信DM取得
+            PostMessage, // 発言POST
+            FavAdd, // Fav追加
+            FavRemove, // Fav削除
+            Follower, // Followerリスト取得
+            Favorites, // Fav取得
+            Retweet, // Retweetする
+            PublicSearch, // 公式検索
+            List, // Lists
+            Related, // 関連発言
+            UserTimeline, // UserTimeline
+            BlockIds, // Blocking/ids
+            Configuration, // Twitter Configuration読み込み
+            NoRetweetIds, // RT非表示ユーザー取得
             //////
-            ErrorState,              //エラー表示のみで後処理終了(認証エラー時など)
+            ErrorState, // エラー表示のみで後処理終了(認証エラー時など)
         }
 
         public static class DEFAULTTAB
@@ -162,20 +180,9 @@ namespace OpenTween
             public const string DM = "Direct";
             public const string FAV = "Favorites";
             public static readonly string MUTE = Properties.Resources.MuteTabName;
-
-            //private string dummy;
-
-            //private object ReferenceEquals()
-            //{
-            //    return new object();
-            //}
-            //private object Equals()
-            //{
-            //    return new object();
-            //}
         }
 
-        public static readonly object Block = null;
+        public static readonly object? Block = null;
         public static bool TraceFlag = false;
 
 #if DEBUG
@@ -197,37 +204,8 @@ namespace OpenTween
             BlinkIcon,
         }
 
-        [FlagsAttribute()]
-        public enum EVENTTYPE
-        {
-            None = 0,
-            Favorite = 1,
-            Unfavorite = 2,
-            Follow = 4,
-            ListMemberAdded = 8,
-            ListMemberRemoved = 16,
-            Block = 32,
-            Unblock = 64,
-            UserUpdate = 128,
-            Deleted = 256,
-            ListCreated = 512,
-            ListUpdated = 1024,
-            Unfollow = 2048,
-            ListUserSubscribed = 4096,
-            ListUserUnsubscribed = 8192,
-            ListDestroyed = 16384,
-            Mute = 32768,
-            Unmute = 65536,
-            QuotedTweet = 131072,
-            Retweet = 262144,
-
-            All = (None | Favorite | Unfavorite | Follow | ListMemberAdded | ListMemberRemoved |
-                   Block | Unblock | UserUpdate | Deleted | ListCreated | ListUpdated | Unfollow |
-                   ListUserSubscribed | ListUserUnsubscribed | ListDestroyed |
-                   Mute | Unmute | QuotedTweet | Retweet),
-        }
-
         public static _Assembly EntryAssembly { get; internal set; }
+
         public static string FileVersion { get; internal set; }
 
         static MyCommon()
@@ -235,15 +213,12 @@ namespace OpenTween
             var assembly = Assembly.GetExecutingAssembly();
             MyCommon.EntryAssembly = assembly;
 
-            var fileVersionAttribute = (AssemblyFileVersionAttribute)assembly
-                .GetCustomAttributes(typeof(AssemblyFileVersionAttribute)).First();
-            MyCommon.FileVersion = fileVersionAttribute.Version;
+            var assemblyVersion = assembly.GetName().Version;
+            MyCommon.FileVersion = assemblyVersion.ToString();
         }
 
         public static string GetErrorLogPath()
-        {
-            return Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
-        }
+            => Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
 
         public static void TraceOut(WebApiException ex)
         {
@@ -255,44 +230,41 @@ namespace OpenTween
             TraceOut(TraceFlag, message);
         }
 
-        public static void TraceOut(Exception ex, string Message)
+        public static void TraceOut(Exception ex, string message)
         {
             var buf = ExceptionOutMessage(ex);
-            TraceOut(TraceFlag, Message + Environment.NewLine + buf);
+            TraceOut(TraceFlag, message + Environment.NewLine + buf);
         }
 
-        public static void TraceOut(string Message)
-        {
-            TraceOut(TraceFlag, Message);
-        }
+        public static void TraceOut(string message)
+            => TraceOut(TraceFlag, message);
 
-        public static void TraceOut(bool OutputFlag, string Message)
+        public static void TraceOut(bool outputFlag, string message)
         {
             lock (LockObj)
             {
-                if (!OutputFlag) return;
+                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);
+                var now = DateTimeUtc.Now;
+                var fileName = $"{ApplicationSettings.AssemblyName}Trace-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
                 fileName = Path.Combine(logPath, fileName);
 
-                using (var writer = new StreamWriter(fileName))
-                {
-                    writer.WriteLine("**** TraceOut: {0} ****", DateTime.Now.ToString());
-                    writer.WriteLine(Properties.Resources.TraceOutText1, ApplicationSettings.FeedbackEmailAddress);
-                    writer.WriteLine(Properties.Resources.TraceOutText2, ApplicationSettings.FeedbackTwitterName);
-                    writer.WriteLine();
-                    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, MyCommon.GetAssemblyName(), FileVersion);
-                    writer.WriteLine(Message);
-                    writer.WriteLine();
-                }
+                using var writer = new StreamWriter(fileName);
+
+                writer.WriteLine("**** TraceOut: {0} ****", now.ToLocalTimeString());
+                writer.WriteLine(Properties.Resources.TraceOutText1, ApplicationSettings.FeedbackEmailAddress);
+                writer.WriteLine(Properties.Resources.TraceOutText2, ApplicationSettings.FeedbackTwitterName);
+                writer.WriteLine();
+                writer.WriteLine(Properties.Resources.TraceOutText3);
+                writer.WriteLine(Properties.Resources.TraceOutText4, Environment.OSVersion.VersionString);
+                writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version);
+                writer.WriteLine(Properties.Resources.TraceOutText6, ApplicationSettings.AssemblyName, FileVersion);
+                writer.WriteLine(message);
+                writer.WriteLine();
             }
         }
 
@@ -303,11 +275,11 @@ namespace OpenTween
 
         public static string ExceptionOutMessage(Exception ex)
         {
-            bool IsTerminatePermission = true;
-            return ExceptionOutMessage(ex, ref IsTerminatePermission);
+            var isTerminatePermission = true;
+            return ExceptionOutMessage(ex, ref isTerminatePermission);
         }
 
-        public static string ExceptionOutMessage(Exception ex, ref bool IsTerminatePermission)
+        public static string ExceptionOutMessage(Exception ex, ref bool isTerminatePermission)
         {
             if (ex == null) return "";
 
@@ -330,7 +302,7 @@ namespace OpenTween
                     buf.AppendLine();
                     if (dt.Key.Equals("IsTerminatePermission"))
                     {
-                        IsTerminatePermission = (bool)dt.Value;
+                        isTerminatePermission = (bool)dt.Value;
                     }
                 }
                 if (!needHeader)
@@ -341,20 +313,20 @@ namespace OpenTween
             buf.AppendLine(ex.StackTrace);
             buf.AppendLine();
 
-            //InnerExceptionが存在する場合書き出す
-            var _ex = ex.InnerException;
+            // InnerExceptionが存在する場合書き出す
+            var innerException = ex.InnerException;
             var nesting = 0;
-            while (_ex != null)
+            while (innerException != null)
             {
                 buf.AppendFormat("-----InnerException[{0}]-----\r\n", nesting);
                 buf.AppendLine();
-                buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, _ex.GetType().FullName, _ex.Message);
+                buf.AppendFormat(Properties.Resources.UnhandledExceptionText8, innerException.GetType().FullName, innerException.Message);
                 buf.AppendLine();
-                if (_ex.Data != null)
+                if (innerException.Data != null)
                 {
                     var needHeader = true;
 
-                    foreach (DictionaryEntry dt in _ex.Data)
+                    foreach (DictionaryEntry dt in innerException.Data)
                     {
                         if (needHeader)
                         {
@@ -365,7 +337,7 @@ namespace OpenTween
                         buf.AppendFormat("{0}  :  {1}", dt.Key, dt.Value);
                         if (dt.Key.Equals("IsTerminatePermission"))
                         {
-                            IsTerminatePermission = (bool)dt.Value;
+                            isTerminatePermission = (bool)dt.Value;
                         }
                     }
                     if (!needHeader)
@@ -373,10 +345,10 @@ namespace OpenTween
                         buf.AppendLine("-----End Extra Information-----");
                     }
                 }
-                buf.AppendLine(_ex.StackTrace);
+                buf.AppendLine(innerException.StackTrace);
                 buf.AppendLine();
                 nesting++;
-                _ex = _ex.InnerException;
+                innerException = innerException.InnerException;
             }
             return buf.ToString();
         }
@@ -385,39 +357,39 @@ namespace OpenTween
         {
             lock (LockObj)
             {
-                var IsTerminatePermission = true;
+                var isTerminatePermission = true;
 
                 var ident = WindowsIdentity.GetCurrent();
                 var princ = new WindowsPrincipal(ident);
+                var now = DateTimeUtc.Now;
 
                 var errorReport = string.Join(Environment.NewLine,
-                    string.Format(Properties.Resources.UnhandledExceptionText1, DateTime.Now.ToString()),
+                    string.Format(Properties.Resources.UnhandledExceptionText1, now.ToLocalTimeString()),
 
                     // 権限書き出し
-                    string.Format(Properties.Resources.UnhandledExceptionText11 + princ.IsInRole(WindowsBuiltInRole.Administrator).ToString()),
-                    string.Format(Properties.Resources.UnhandledExceptionText12 + princ.IsInRole(WindowsBuiltInRole.User).ToString()),
+                    string.Format(Properties.Resources.UnhandledExceptionText11 + princ.IsInRole(WindowsBuiltInRole.Administrator)),
+                    string.Format(Properties.Resources.UnhandledExceptionText12 + princ.IsInRole(WindowsBuiltInRole.User)),
                     "",
 
                     // OSVersion,AppVersion書き出し
                     string.Format(Properties.Resources.UnhandledExceptionText4),
                     string.Format(Properties.Resources.UnhandledExceptionText5, Environment.OSVersion.VersionString),
-                    string.Format(Properties.Resources.UnhandledExceptionText6, Environment.Version.ToString()),
-                    string.Format(Properties.Resources.UnhandledExceptionText7, MyCommon.GetAssemblyName(), FileVersion),
+                    string.Format(Properties.Resources.UnhandledExceptionText6, Environment.Version),
+                    string.Format(Properties.Resources.UnhandledExceptionText7, ApplicationSettings.AssemblyName, FileVersion),
 
-                    ExceptionOutMessage(ex, ref IsTerminatePermission));
+                    ExceptionOutMessage(ex, ref isTerminatePermission));
 
                 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);
+                var fileName = $"{ApplicationSettings.AssemblyName}-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
                 using (var writer = new StreamWriter(Path.Combine(logPath, fileName)))
                 {
                     writer.Write(errorReport);
                 }
 
-                var settings = SettingCommon.Instance;
+                var settings = SettingManager.Instance;
                 var mainForm = Application.OpenForms.OfType<TweenMain>().FirstOrDefault();
 
                 ErrorReport report;
@@ -426,22 +398,22 @@ namespace OpenTween
                 else
                     report = new ErrorReport(errorReport);
 
-                report.AnonymousReport = settings.ErrorReportAnonymous;
+                report.AnonymousReport = settings.Common.ErrorReportAnonymous;
 
                 OpenErrorReportDialog(mainForm, report);
 
                 // ダイアログ内で設定が変更されていれば保存する
-                if (settings.ErrorReportAnonymous != report.AnonymousReport)
+                if (settings.Common.ErrorReportAnonymous != report.AnonymousReport)
                 {
-                    settings.ErrorReportAnonymous = report.AnonymousReport;
-                    settings.Save();
+                    settings.Common.ErrorReportAnonymous = report.AnonymousReport;
+                    settings.SaveCommon();
                 }
 
                 return false;
             }
         }
 
-        private static void OpenErrorReportDialog(Form owner, ErrorReport report)
+        private static void OpenErrorReportDialog(Form? owner, ErrorReport report)
         {
             if (owner != null && owner.InvokeRequired)
             {
@@ -449,80 +421,9 @@ namespace OpenTween
                 return;
             }
 
-            using (var dialog = new SendErrorReportForm())
-            {
-                dialog.ErrorReport = report;
-                dialog.ShowDialog(owner);
-            }
-        }
-
-        /// <summary>
-        /// URLに含まれているマルチバイト文字列を%xx形式でエンコードします。
-        /// <newpara>
-        /// マルチバイト文字のコードはUTF-8またはUnicodeで自動的に判断します。
-        /// </newpara>
-        /// </summary>
-        /// <param name="_input">エンコード対象のURL</param>
-        /// <returns>マルチバイト文字の部分をUTF-8/%xx形式でエンコードした文字列を返します。</returns>
-
-        public static string urlEncodeMultibyteChar(string _input)
-        {
-            Uri uri = null;
-            var sb = new StringBuilder(256);
-            var result = "";
-            var c_ = 'd';
-            foreach (var c in _input)
-            {
-                c_ = c;
-                if (Convert.ToInt32(c) > 127 || c == '%') break;
-            }
-            if (Convert.ToInt32(c_) <= 127 && c_ != '%') return _input;
-
-            var input = Uri.UnescapeDataString(_input);
-        retry:
-            foreach (char c in input)
-            {
-                if (Convert.ToInt32(c) > 255)
-                {
-                    // Unicodeの場合(1charが複数のバイトで構成されている)
-                    // Uriクラスをnewして再構成し、入力をPathAndQueryのみとしてやり直す
-                    foreach (var b in Encoding.UTF8.GetBytes(c.ToString()))
-                    {
-                        sb.AppendFormat("%{0:X2}", b);
-                    }
-                }
-                else if (Convert.ToInt32(c) > 127 || c == '%')
-                {
-                    // UTF-8の場合
-                    // Uriクラスをnewして再構成し、入力をinputからAuthority部分を除去してやり直す
-                    if (uri == null)
-                    {
-                        uri = new Uri(input);
-                        input = input.Remove(0, uri.GetLeftPart(UriPartial.Authority).Length);
-                        sb.Length = 0;
-                        goto retry;
-                    }
-                    else
-                    {
-                        sb.Append("%" + Convert.ToInt16(c).ToString("X2").ToUpperInvariant());
-                    }
-                }
-                else
-                {
-                    sb.Append(c);
-                }
-            }
-
-            if (uri == null)
-            {
-                result = sb.ToString();
-            }
-            else
-            {
-                result = uri.GetLeftPart(UriPartial.Authority) + sb.ToString();
-            }
-
-            return result;
+            using var dialog = new SendErrorReportForm();
+            dialog.ErrorReport = report;
+            dialog.ShowDialog(owner);
         }
 
         /// <summary>
@@ -532,9 +433,9 @@ namespace OpenTween
         /// ドメインラベルの区切り文字はFULLSTOP(.、U002E)に置き換えられます。
         /// </para>
         /// </summary>
-        /// <param name="input">展開対象のURL</param>
+        /// <param name="inputUrl">展開対象のURL</param>
         /// <returns>IDNが含まれていた場合はPunycodeに展開したURLをを返します。Punycode展開時にエラーが発生した場合はnullを返します。</returns>
-        public static string IDNEncode(string inputUrl)
+        public static string? IDNEncode(string inputUrl)
         {
             try
             {
@@ -567,7 +468,7 @@ namespace OpenTween
             }
             catch (Exception)
             {
-                return null;
+                return inputUrl;
             }
         }
 
@@ -582,8 +483,6 @@ namespace OpenTween
 
                 // Punycodeをデコードする
                 outputUrl = MyCommon.IDNDecode(outputUrl);
-                if (outputUrl == null)
-                    return inputUrl;
 
                 // URL内で特殊な意味を持つ記号は元の文字に変換されることを避けるために二重エスケープする
                 // 参考: Firefoxの losslessDecodeURI() 関数
@@ -608,13 +507,11 @@ namespace OpenTween
 
             if (idx_to < idx_fr)
             {
-                Array.Copy(values, idx_to, values,
-                    idx_to + 1, num_moved);
+                Array.Copy(values, idx_to, values, idx_to + 1, num_moved);
             }
             else
             {
-                Array.Copy(values, idx_fr + 1, values,
-                    idx_fr, num_moved);
+                Array.Copy(values, idx_fr + 1, values, idx_fr, num_moved);
             }
 
             values[idx_to] = moved_value;
@@ -622,110 +519,106 @@ namespace OpenTween
 
         public static string EncryptString(string str)
         {
-            if (string.IsNullOrEmpty(str)) return "";
+            if (MyCommon.IsNullOrEmpty(str)) return "";
 
-            //文字列をバイト型配列にする
+            // 文字列をバイト型配列にする
             var bytesIn = Encoding.UTF8.GetBytes(str);
 
-            //DESCryptoServiceProviderオブジェクトの作成
-            using (var des = new DESCryptoServiceProvider())
+            // DESCryptoServiceProviderオブジェクトの作成
+            using var des = new DESCryptoServiceProvider();
+
+            // 共有キーと初期化ベクタを決定
+            // パスワードをバイト配列にする
+            var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
+            // 共有キーと初期化ベクタを設定
+            des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
+            des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
+
+            MemoryStream? msOut = null;
+            ICryptoTransform? desdecrypt = null;
+
+            try
             {
-                //共有キーと初期化ベクタを決定
-                //パスワードをバイト配列にする
-                var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
-                //共有キーと初期化ベクタを設定
-                des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
-                des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
+                // 暗号化されたデータを書き出すためのMemoryStream
+                msOut = new MemoryStream();
 
-                MemoryStream msOut = null;
-                ICryptoTransform desdecrypt = null;
+                // DES暗号化オブジェクトの作成
+                desdecrypt = des.CreateEncryptor();
 
-                try
-                {
-                    //暗号化されたデータを書き出すためのMemoryStream
-                    msOut = new MemoryStream();
+                // 書き込むためのCryptoStreamの作成
+                using var cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write);
 
-                    //DES暗号化オブジェクトの作成
-                    desdecrypt = des.CreateEncryptor();
+                // Disposeが重複して呼ばれないようにする
+                var msTmp = msOut;
+                msOut = null;
+                desdecrypt = null;
 
-                    //書き込むためのCryptoStreamの作成
-                    using (CryptoStream cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write))
-                    {
-                        //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
-                {
-                    msOut?.Dispose();
-                    desdecrypt?.Dispose();
-                }
+                // 書き込む
+                cryptStream.Write(bytesIn, 0, bytesIn.Length);
+                cryptStream.FlushFinalBlock();
+                // 暗号化されたデータを取得
+                var bytesOut = msTmp.ToArray();
+
+                // Base64で文字列に変更して結果を返す
+                return Convert.ToBase64String(bytesOut);
+            }
+            finally
+            {
+                msOut?.Dispose();
+                desdecrypt?.Dispose();
             }
         }
 
         public static string DecryptString(string str)
         {
-            if (string.IsNullOrEmpty(str)) return "";
+            if (MyCommon.IsNullOrEmpty(str)) return "";
+
+            // DESCryptoServiceProviderオブジェクトの作成
+            using var des = new DESCryptoServiceProvider();
+
+            // 共有キーと初期化ベクタを決定
+            // パスワードをバイト配列にする
+            var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
+            // 共有キーと初期化ベクタを設定
+            des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
+            des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
+
+            // Base64で文字列をバイト配列に戻す
+            var bytesIn = Convert.FromBase64String(str);
+
+            MemoryStream? msIn = null;
+            ICryptoTransform? desdecrypt = null;
+            CryptoStream? cryptStreem = null;
 
-            //DESCryptoServiceProviderオブジェクトの作成
-            using (var des = new System.Security.Cryptography.DESCryptoServiceProvider())
+            try
             {
-                //共有キーと初期化ベクタを決定
-                //パスワードをバイト配列にする
-                var bytesKey = Encoding.UTF8.GetBytes("_tween_encrypt_key_");
-                //共有キーと初期化ベクタを設定
-                des.Key = ResizeBytesArray(bytesKey, des.Key.Length);
-                des.IV = ResizeBytesArray(bytesKey, des.IV.Length);
+                // 暗号化されたデータを読み込むためのMemoryStream
+                msIn = new MemoryStream(bytesIn);
+                // DES復号化オブジェクトの作成
+                desdecrypt = des.CreateDecryptor();
+                // 読み込むためのCryptoStreamの作成
+                cryptStreem = new CryptoStream(msIn, desdecrypt, CryptoStreamMode.Read);
 
-                //Base64で文字列をバイト配列に戻す
-                var bytesIn = Convert.FromBase64String(str);
+                // Disposeが重複して呼ばれないようにする
+                msIn = null;
+                desdecrypt = null;
 
-                MemoryStream msIn = null;
-                ICryptoTransform desdecrypt = null;
-                CryptoStream cryptStreem = null;
+                // 復号化されたデータを取得するためのStreamReader
+                using var srOut = new StreamReader(cryptStreem, Encoding.UTF8);
 
-                try
-                {
-                    //暗号化されたデータを読み込むためのMemoryStream
-                    msIn = new MemoryStream(bytesIn);
-                    //DES復号化オブジェクトの作成
-                    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))
-                    {
-                        //Disposeが重複して呼ばれないようにする
-                        cryptStreem = null;
+                // Disposeが重複して呼ばれないようにする
+                cryptStreem = null;
 
-                        //復号化されたデータを取得する
-                        var result = srOut.ReadToEnd();
+                // 復号化されたデータを取得する
+                var result = srOut.ReadToEnd();
 
-                        return result;
-                    }
-                }
-                finally
-                {
-                    msIn?.Dispose();
-                    desdecrypt?.Dispose();
-                    cryptStreem?.Dispose();
-                }
+                return result;
+            }
+            finally
+            {
+                msIn?.Dispose();
+                desdecrypt?.Dispose();
+                cryptStreem?.Dispose();
             }
         }
 
@@ -756,33 +649,30 @@ namespace OpenTween
             return newBytes;
         }
 
-        [FlagsAttribute()]
+        [Flags]
         public enum TabUsageType
         {
             Undefined = 0,
-            Home = 1,      //Unique
-            Mentions = 2,     //Unique
-            DirectMessage = 4,   //Unique
-            Favorites = 8,       //Unique
+            Home = 1, // Unique
+            Mentions = 2, // Unique
+            DirectMessage = 4, // Unique
+            Favorites = 8, // Unique
             UserDefined = 16,
-            LocalQuery = 32,      //Pin(no save/no save query/distribute/no update(normal update))
-            Profile = 64,         //Pin(save/no distribute/manual update)
-            PublicSearch = 128,    //Pin(save/no distribute/auto update)
+            LocalQuery = 32, // Pin(no save/no save query/distribute/no update(normal update))
+            Profile = 64, // Pin(save/no distribute/manual update)
+            PublicSearch = 128, // Pin(save/no distribute/auto update)
             Lists = 256,
             Related = 512,
             UserTimeline = 1024,
             Mute = 2048,
             SearchResults = 4096,
-            //RTMyTweet
-            //RTByOthers
-            //RTByMe
         }
 
-        public static TwitterApiStatus TwitterApiInfo = new TwitterApiStatus();
+        public static TwitterApiStatus TwitterApiInfo = new();
 
         public static bool IsAnimatedGif(string filename)
         {
-            Image img = null;
+            Image? img = null;
             try
             {
                 img = Image.FromFile(filename);
@@ -812,45 +702,31 @@ namespace OpenTween
             }
         }
 
-        public static DateTime DateTimeParse(string input)
+        public static DateTimeUtc DateTimeParse(string input)
         {
-            DateTime rslt;
-            string[] format = {
+            var formats = new[]
+            {
                 "ddd MMM dd HH:mm:ss zzzz yyyy",
                 "ddd, d MMM yyyy HH:mm:ss zzzz",
             };
-            foreach (var fmt in format)
-            {
-                if (DateTime.TryParseExact(input,
-                                          fmt,
-                                          DateTimeFormatInfo.InvariantInfo,
-                                          DateTimeStyles.None,
-                                          out rslt))
-                {
-                    return rslt;
-                }
-                else
-                {
-                    continue;
-                }
-            }
+
+            if (DateTimeUtc.TryParseExact(input, formats, DateTimeFormatInfo.InvariantInfo, out var result))
+                return result;
+
             TraceOut("Parse Error(DateTimeFormat) : " + input);
-            return new DateTime();
+
+            return DateTimeUtc.Now;
         }
 
         public static T CreateDataFromJson<T>(string content)
         {
-            T data;
             var buf = Encoding.Unicode.GetBytes(content);
-            using (var stream = new MemoryStream(buf))
+            using var stream = new MemoryStream(buf);
+            var settings = new DataContractJsonSerializerSettings
             {
-                var settings = new DataContractJsonSerializerSettings
-                {
-                    UseSimpleDictionaryFormat = true,
-                };
-                data = (T)((new DataContractJsonSerializer(typeof(T), settings)).ReadObject(stream));
-            }
-            return data;
+                UseSimpleDictionaryFormat = true,
+            };
+            return (T)new DataContractJsonSerializer(typeof(T), settings).ReadObject(stream);
         }
 
         public static bool IsNetworkAvailable()
@@ -859,7 +735,7 @@ namespace OpenTween
             {
                 return NetworkInterface.GetIsNetworkAvailable();
             }
-            catch(Exception)
+            catch (Exception)
             {
                 return false;
             }
@@ -867,10 +743,11 @@ namespace OpenTween
 
         public static bool IsValidEmail(string strIn)
         {
+            var pattern = @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" +
+                @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$";
+
             // Return true if strIn is in valid e-mail format.
-            return Regex.IsMatch(strIn,
-                   @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" +
-                   @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$");
+            return Regex.IsMatch(strIn, pattern);
         }
 
         /// <summary>
@@ -879,13 +756,11 @@ namespace OpenTween
         /// <param name="keys">状態を調べるキー</param>
         /// <returns><paramref name="keys"/> で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。</returns>
         public static bool IsKeyDown(params Keys[] keys)
-        {
-            return MyCommon._IsKeyDown(Control.ModifierKeys, keys);
-        }
+            => MyCommon.IsKeyDownInternal(Control.ModifierKeys, keys);
 
-        internal static bool _IsKeyDown(Keys modifierKeys, Keys[] targetKeys)
+        internal static bool IsKeyDownInternal(Keys modifierKeys, Keys[] targetKeys)
         {
-            foreach (Keys key in targetKeys)
+            foreach (var key in targetKeys)
             {
                 if ((modifierKeys & key) != key)
                 {
@@ -903,9 +778,7 @@ namespace OpenTween
         /// </remarks>
         /// <returns>アプリケーションのアセンブリ名</returns>
         public static string GetAssemblyName()
-        {
-            return MyCommon.EntryAssembly.GetName().Name;
-        }
+            => MyCommon.EntryAssembly.GetName().Name;
 
         /// <summary>
         /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
@@ -913,9 +786,7 @@ namespace OpenTween
         /// <param name="orig">対象となる文字列</param>
         /// <returns>置換後の文字列</returns>
         public static string ReplaceAppName(string orig)
-        {
-            return MyCommon.ReplaceAppName(orig, Application.ProductName);
-        }
+            => MyCommon.ReplaceAppName(orig, ApplicationSettings.ApplicationName);
 
         /// <summary>
         /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
@@ -924,9 +795,7 @@ namespace OpenTween
         /// <param name="appname">アプリケーション名</param>
         /// <returns>置換後の文字列</returns>
         public static string ReplaceAppName(string orig, string appname)
-        {
-            return orig.Replace("%AppName%", appname);
-        }
+            => orig.Replace("%AppName%", appname);
 
         /// <summary>
         /// 表示用のバージョン番号の文字列を生成する
@@ -937,7 +806,7 @@ namespace OpenTween
         /// <returns>
         /// 生成されたバージョン番号の文字列
         /// </returns>
-        public static string GetReadableVersion(string versionStr = null)
+        public static string GetReadableVersion(string? versionStr = null)
         {
             var version = Version.Parse(versionStr ?? MyCommon.FileVersion);
 
@@ -965,23 +834,10 @@ namespace OpenTween
             {
                 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;
-                    }
-                }
-
                 if (versionNum[3] == 1)
                     return string.Format("{0}.{1}.{2}-dev", versionNum[0], versionNum[1], versionNum[2]);
                 else
-                    return string.Format("{0}.{1}.{2}-dev (Build {3})", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
+                    return string.Format("{0}.{1}.{2}-dev+build.{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
             }
         }
 
@@ -996,9 +852,7 @@ namespace OpenTween
         }
 
         public static string GetStatusUrl(string screenName, long statusId)
-        {
-            return TwitterUrl + screenName + "/status/" + statusId.ToString();
-        }
+            => TwitterUrl + screenName + "/status/" + statusId;
 
         /// <summary>
         /// 指定された IDictionary を元にクエリ文字列を生成します
@@ -1018,7 +872,7 @@ namespace OpenTween
 
         // .NET 4.5+: Reserved characters のうち、Uriクラスによってエスケープ強制解除されてしまうものも最初から Unreserved として扱う
         private static readonly HashSet<char> UnreservedChars =
-            new HashSet<char>("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:");
+            new("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!'()*:");
 
         /// <summary>
         /// 2バイト文字も考慮したクエリ用エンコード
@@ -1072,6 +926,40 @@ namespace OpenTween
                 yield return i;
         }
 
+        public static IEnumerable<int> CircularCountUp(int length, int startIndex)
+        {
+            if (length < 1)
+                throw new ArgumentOutOfRangeException(nameof(length));
+            if (startIndex < 0 || startIndex >= length)
+                throw new ArgumentOutOfRangeException(nameof(startIndex));
+
+            // startindex ... 末尾
+            var indices = MyCommon.CountUp(startIndex, length - 1);
+
+            // 先頭 ... (startIndex - 1)
+            if (startIndex != 0)
+                indices = indices.Concat(MyCommon.CountUp(0, startIndex - 1));
+
+            return indices;
+        }
+
+        public static IEnumerable<int> CircularCountDown(int length, int startIndex)
+        {
+            if (length < 1)
+                throw new ArgumentOutOfRangeException(nameof(length));
+            if (startIndex < 0 || startIndex >= length)
+                throw new ArgumentOutOfRangeException(nameof(startIndex));
+
+            // startIndex ... 先頭
+            var indices = MyCommon.CountDown(startIndex, 0);
+
+            // 末尾 ... (startIndex + 1)
+            if (startIndex != length - 1)
+                indices = indices.Concat(MyCommon.CountDown(length - 1, startIndex + 1));
+
+            return indices;
+        }
+
         /// <summary>
         /// 2バイト文字も考慮したUrlエンコード
         /// </summary>
@@ -1080,10 +968,10 @@ namespace OpenTween
         public static string UrlEncode(string stringToEncode)
         {
             const string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
-            StringBuilder sb = new StringBuilder();
-            byte[] bytes = Encoding.UTF8.GetBytes(stringToEncode);
+            var sb = new StringBuilder();
+            var bytes = Encoding.UTF8.GetBytes(stringToEncode);
 
-            foreach (byte b in bytes)
+            foreach (var b in bytes)
             {
                 if (UnreservedChars.IndexOf((char)b) != -1)
                     sb.Append((char)b);
@@ -1092,5 +980,117 @@ namespace OpenTween
             }
             return sb.ToString();
         }
+
+        public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
+            => string.IsNullOrEmpty(value);
+
+        public static Task OpenInBrowserAsync(IWin32Window? owner, string urlStr)
+            => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, urlStr);
+
+        public static Task OpenInBrowserAsync(IWin32Window? owner, Uri uri)
+            => MyCommon.OpenInBrowserAsync(owner, SettingManager.Instance.Local.BrowserPath, uri);
+
+        public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, string urlStr)
+        {
+            if (!Uri.TryCreate(urlStr, UriKind.Absolute, out var uri))
+            {
+                var message = string.Format(Properties.Resources.CannotOpenUriText, urlStr);
+                MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+            }
+            await MyCommon.OpenInBrowserAsync(owner, browserPath, uri);
+        }
+
+        public static async Task OpenInBrowserAsync(IWin32Window? owner, string? browserPath, Uri uri)
+        {
+            if (uri.Scheme != "http" && uri.Scheme != "https")
+            {
+                var message = string.Format(Properties.Resources.CannotOpenUriText, uri.OriginalString);
+                MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+            }
+
+            try
+            {
+                await Task.Run(() =>
+                {
+                    var startInfo = MyCommon.CreateBrowserProcessStartInfo(browserPath, url);
+                    Process.Start(startInfo);
+                });
+            }
+            catch (Win32Exception ex)
+            {
+                var message = string.Format(Properties.Resources.BrowserStartFailed, ex.Message);
+                MessageBox.Show(owner, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+            }
+        }
+
+        public static ProcessStartInfo CreateBrowserProcessStartInfo(string? browserPathWithArgs, string url)
+        {
+            if (MyCommon.IsNullOrEmpty(browserPathWithArgs))
+            {
+                return new ProcessStartInfo
+                {
+                    FileName = url,
+                    UseShellExecute = true,
+                };
+            }
+
+            var quoteEnd = -1;
+            if (browserPathWithArgs.StartsWith("\"", StringComparison.Ordinal))
+                quoteEnd = browserPathWithArgs.IndexOf("\"", 1, StringComparison.Ordinal);
+
+            string browserPath, browserArgs;
+            var isQuoted = quoteEnd != -1;
+            if (isQuoted)
+            {
+                browserPath = browserPathWithArgs.Substring(1, quoteEnd - 1);
+                browserArgs = browserPathWithArgs.Substring(quoteEnd + 1).Trim();
+            }
+            else
+            {
+                browserPath = browserPathWithArgs;
+                browserArgs = "";
+            }
+
+            var quotedUrl = "\"" + url.Replace("\"", "\\\"") + "\"";
+            var args = MyCommon.IsNullOrEmpty(browserArgs) ? quotedUrl : browserArgs + " " + quotedUrl;
+
+            return new ProcessStartInfo
+            {
+                FileName = browserPath,
+                Arguments = args,
+                UseShellExecute = false,
+            };
+        }
+
+        public static IEnumerable<(int Start, int End)> ToRangeChunk(IEnumerable<int> values)
+        {
+            var start = -1;
+            var end = -1;
+
+            foreach (var value in values.OrderBy(x => x))
+            {
+                if (start == -1)
+                {
+                    start = value;
+                    end = value;
+                }
+                else
+                {
+                    if (value == end + 1)
+                    {
+                        end = value;
+                    }
+                    else
+                    {
+                        yield return (start, end);
+                        start = value;
+                        end = value;
+                    }
+                }
+            }
+
+            if (start != -1)
+                yield return (start, end);
+        }
     }
 }