diff --git a/app/Enums/PageCvsIndex.php b/app/Enums/PageCvsIndex.php index cef26b78e..1deb24656 100755 --- a/app/Enums/PageCvsIndex.php +++ b/app/Enums/PageCvsIndex.php @@ -21,7 +21,8 @@ final class PageCvsIndex extends EnumsBase const header_color = 3; const theme = 4; const layout = 5; - const base_display_flag = 6; + const layout_inherit_flag = 6; + const base_display_flag = 7; /** key/valueの連想配列 */ const enum = [ @@ -31,6 +32,7 @@ final class PageCvsIndex extends EnumsBase self::header_color => 'header_color', self::theme => 'theme', self::layout => 'layout', + self::layout_inherit_flag => 'layout_inherit_flag', self::base_display_flag => 'base_display_flag', ]; } diff --git a/app/Enums/PhotoalbumFrameConfig.php b/app/Enums/PhotoalbumFrameConfig.php index 023b25ec0..ec3362610 100644 --- a/app/Enums/PhotoalbumFrameConfig.php +++ b/app/Enums/PhotoalbumFrameConfig.php @@ -25,6 +25,8 @@ final class PhotoalbumFrameConfig extends EnumsBase const embed_code = 'embed_code'; const play_view = 'play_view'; const description_list_length = 'description_list_length'; + const load_more_use_flag = 'load_more_use_flag'; + const load_more_count = 'load_more_count'; // key/valueの連想配列 const enum = [ @@ -38,5 +40,7 @@ final class PhotoalbumFrameConfig extends EnumsBase self::embed_code => '動画埋め込みコード', self::play_view => '動画の再生形式', self::description_list_length => '一覧の説明表示文字数', + self::load_more_use_flag => 'もっと見る機能', + self::load_more_count => 'もっと見る表示件数', ]; } diff --git a/app/Enums/SpamBlockType.php b/app/Enums/SpamBlockType.php new file mode 100644 index 000000000..247c83326 --- /dev/null +++ b/app/Enums/SpamBlockType.php @@ -0,0 +1,28 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 + * @package Enum + */ +class SpamBlockType extends EnumsBase +{ + // 定数メンバ + const email = 'email'; + const domain = 'domain'; + const ip_address = 'ip_address'; + const honeypot = 'honeypot'; + + // key/valueの連想配列 + const enum = [ + self::email => 'メールアドレス', + self::domain => 'ドメイン', + self::ip_address => 'IPアドレス', + self::honeypot => 'ハニーポット', + ]; +} diff --git a/app/Http/Controllers/Core/DefaultController.php b/app/Http/Controllers/Core/DefaultController.php index 9ecfbd97d..adfd903bd 100644 --- a/app/Http/Controllers/Core/DefaultController.php +++ b/app/Http/Controllers/Core/DefaultController.php @@ -335,7 +335,7 @@ private function getTopPage($page, $languages = null) private function getLayout($page, $page_tree) { // レイアウトの初期値 - $layout_default = '1|1|0|1'; + $layout_default = config('connect.BASE_LAYOUT_DEFAULT'); // if (empty($this->page)) { if (empty($page)) { @@ -344,15 +344,24 @@ private function getLayout($page, $page_tree) // レイアウト $layout = null; - foreach ($page_tree as $tmp_page) { // レイアウト - if (empty($layout) && $tmp_page->layout) { - $layout = $tmp_page->layout; + if (empty($tmp_page->layout)) { + continue; + } + if ($tmp_page->id != $page->id && !is_null($tmp_page->layout_inherit_flag) && (int)$tmp_page->layout_inherit_flag === 0) { + // 祖先のレイアウトが「このページのみ」指定なら下層への継承対象から外す。 + continue; } + $layout = $tmp_page->layout; + break; + } + // 親も含めて空の場合は、基本レイアウトを使い、未設定なら初期値を返却 + if (empty($layout)) { + $layout = Configs::getSharedConfigsValue('base_layout', $layout_default); } - // 親も含めて空の場合は、初期値を返却 if (empty($layout)) { + // DBに不正な値が入るなどして予期せず空になった場合の安全弁 $layout = $layout_default; } return $layout; diff --git a/app/Http/Controllers/Core/FrameCore.php b/app/Http/Controllers/Core/FrameCore.php index 28fd15d23..86b53f855 100644 --- a/app/Http/Controllers/Core/FrameCore.php +++ b/app/Http/Controllers/Core/FrameCore.php @@ -72,7 +72,7 @@ public function addPlugin($request, $page_id = null, $frame_id = null) $frame->frame_design = "default"; $frame->plugin_name = $request->add_plugin; $frame->frame_col = 0; - $frame->template = "default"; + $frame->template = config('cc_plugin_defaults.default_template_map.' . $request->add_plugin, 'default'); $frame->bucket_id = null; $frame->display_sequence = 0; $frame->save(); diff --git a/app/Models/Common/Page.php b/app/Models/Common/Page.php index 437f93341..548f17567 100644 --- a/app/Models/Common/Page.php +++ b/app/Models/Common/Page.php @@ -34,6 +34,7 @@ class Page extends Model 'header_color', 'theme', 'layout', + 'layout_inherit_flag', 'base_display_flag', 'membership_flag', 'container_flag', diff --git a/app/Models/Common/SpamBlockHistory.php b/app/Models/Common/SpamBlockHistory.php new file mode 100644 index 000000000..1fadc5391 --- /dev/null +++ b/app/Models/Common/SpamBlockHistory.php @@ -0,0 +1,44 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 + * @package Model + */ +class SpamBlockHistory extends Model +{ + use HasFactory; + + /** + * updated_at は使用しない + */ + const UPDATED_AT = null; + + /** + * create()やupdate()で入力を受け付ける ホワイトリスト + */ + protected $fillable = [ + 'spam_list_id', + 'forms_id', + 'block_type', + 'block_value', + 'client_ip', + 'submitted_email', + ]; + + /** + * スパムリストとのリレーション + */ + public function spamList() + { + return $this->belongsTo(SpamList::class, 'spam_list_id'); + } +} diff --git a/app/Models/Common/SpamList.php b/app/Models/Common/SpamList.php new file mode 100644 index 000000000..18550f76c --- /dev/null +++ b/app/Models/Common/SpamList.php @@ -0,0 +1,101 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 + * @package Model + */ +class SpamList extends Model +{ + // 保存時のユーザー関連データの保持(履歴なしUserable) + use HasFactory; + use UserableNohistory; + use SoftDeletes; + + /** + * create()やupdate()で入力を受け付ける ホワイトリスト + */ + protected $fillable = [ + 'target_plugin_name', + 'target_id', + 'block_type', + 'block_value', + 'memo', + ]; + + /** + * フォームプラグイン用のスパムリストを取得 + * + * @param int|null $forms_id フォームID(nullの場合は全体のみ) + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getFormsSpamLists($forms_id = null) + { + $query = self::where('target_plugin_name', 'forms'); + + if ($forms_id) { + $query->where(function ($q) use ($forms_id) { + $q->where('target_id', $forms_id) + ->orWhereNull('target_id'); + }); + } else { + $query->whereNull('target_id'); + } + + return $query->orderBy('block_type') + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * 全体適用かどうか + * + * @return bool + */ + public function isGlobalScope() + { + return is_null($this->target_id); + } + + /** + * 重複チェック付きでスパムリストに追加 + * + * @param string $target_plugin_name 対象プラグイン名 + * @param int|null $target_id 対象ID(nullの場合は全体適用) + * @param string $block_type ブロック種別 + * @param string $block_value ブロック対象の値 + * @param string|null $memo メモ + * @return bool 追加成功時true、重複時false + */ + public static function addIfNotExists($target_plugin_name, $target_id, $block_type, $block_value, $memo = null) + { + $exists = self::where('target_plugin_name', $target_plugin_name) + ->where('target_id', $target_id) + ->where('block_type', $block_type) + ->where('block_value', $block_value) + ->exists(); + + if ($exists) { + return false; + } + + self::create([ + 'target_plugin_name' => $target_plugin_name, + 'target_id' => $target_id, + 'block_type' => $block_type, + 'block_value' => $block_value, + 'memo' => $memo, + ]); + return true; + } +} diff --git a/app/Models/User/Forms/Forms.php b/app/Models/User/Forms/Forms.php index 6735fa456..31293ec81 100644 --- a/app/Models/User/Forms/Forms.php +++ b/app/Models/User/Forms/Forms.php @@ -2,13 +2,14 @@ namespace App\Models\User\Forms; -use Illuminate\Database\Eloquent\Model; - use App\UserableNohistory; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class Forms extends Model { // 保存時のユーザー関連データの保持(履歴なしUserable) + use HasFactory; use UserableNohistory; /** @@ -45,7 +46,9 @@ class Forms extends Model 'data_save_flag', 'after_message', 'numbering_use_flag', - 'numbering_prefix' + 'numbering_prefix', + 'use_spam_filter_flag', + 'spam_filter_message', ]; /** diff --git a/app/Models/User/Forms/FormsColumns.php b/app/Models/User/Forms/FormsColumns.php index f72038b4f..7b27b806d 100644 --- a/app/Models/User/Forms/FormsColumns.php +++ b/app/Models/User/Forms/FormsColumns.php @@ -2,13 +2,14 @@ namespace App\Models\User\Forms; -use Illuminate\Database\Eloquent\Model; - use App\UserableNohistory; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class FormsColumns extends Model { // 保存時のユーザー関連データの保持(履歴なしUserable) + use HasFactory; use UserableNohistory; // 更新する項目の定義 diff --git a/app/Models/User/Forms/FormsInputCols.php b/app/Models/User/Forms/FormsInputCols.php index 9c75668b7..a784f5579 100644 --- a/app/Models/User/Forms/FormsInputCols.php +++ b/app/Models/User/Forms/FormsInputCols.php @@ -2,13 +2,14 @@ namespace App\Models\User\Forms; -use Illuminate\Database\Eloquent\Model; - use App\UserableNohistory; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class FormsInputCols extends Model { // 保存時のユーザー関連データの保持(履歴なしUserable) + use HasFactory; use UserableNohistory; // 更新する項目の定義 diff --git a/app/Models/User/Forms/FormsInputs.php b/app/Models/User/Forms/FormsInputs.php index bd32b962a..b5e09e239 100644 --- a/app/Models/User/Forms/FormsInputs.php +++ b/app/Models/User/Forms/FormsInputs.php @@ -2,13 +2,14 @@ namespace App\Models\User\Forms; -use Illuminate\Database\Eloquent\Model; - use App\UserableNohistory; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class FormsInputs extends Model { // 保存時のユーザー関連データの保持(履歴なしUserable) + use HasFactory; use UserableNohistory; // 更新する項目の定義 diff --git a/app/Plugins/Manage/PageManage/PageManage.php b/app/Plugins/Manage/PageManage/PageManage.php index 0aba57bad..3c85ffbd5 100644 --- a/app/Plugins/Manage/PageManage/PageManage.php +++ b/app/Plugins/Manage/PageManage/PageManage.php @@ -185,6 +185,7 @@ private function pageValidator($request, $page_id = null) 'ip_address' => ['nullable', new CustomValiTextMax()], 'othersite_url' => ['nullable', new CustomValiUrlMax()], 'class' => ['nullable', 'max:255'], + 'layout_inherit_flag' => ['nullable', Rule::in(['0', '1'])], 'meta_robots' => ['nullable', 'array', new CustomValiMetaRobots()], ]); $validator->setAttributeNames([ @@ -196,6 +197,7 @@ private function pageValidator($request, $page_id = null) 'ip_address' => 'IPアドレス制限', 'othersite_url' => '外部サイトURL', 'class' => 'メニュークラス名', + 'layout_inherit_flag' => 'レイアウトの適用範囲', 'meta_robots' => '検索避け設定', ]); return $validator; @@ -248,6 +250,7 @@ private function pageUploadValidatorRules() PageCvsIndex::header_color => ['required', 'max:255'], PageCvsIndex::theme => ['required'], PageCvsIndex::layout => ['required'], + PageCvsIndex::layout_inherit_flag => ['required', Rule::in(['0', '1'])], PageCvsIndex::base_display_flag => ['required', Rule::in(['0', '1'])], ]; @@ -285,6 +288,7 @@ public function store($request) $page->header_color = $request->header_color; $page->theme = $request->theme; $page->layout = $request->layout; + $page->layout_inherit_flag = $request->layout_inherit_flag ?? 1; $page->base_display_flag = (isset($request->base_display_flag) ? $request->base_display_flag : 0); $page->membership_flag = (isset($request->membership_flag) ? $request->membership_flag : 0); $page->container_flag = (isset($request->container_flag) ? $request->container_flag : 0); @@ -334,6 +338,7 @@ public function update($request, $page_id) 'header_color' => $request->header_color, 'theme' => $request->theme, 'layout' => $request->layout, + 'layout_inherit_flag' => $request->layout_inherit_flag ?? 1, 'base_display_flag' => (isset($request->base_display_flag) ? $request->base_display_flag : 0), 'membership_flag' => (isset($request->membership_flag) ? $request->membership_flag : 0), 'container_flag' => (isset($request->container_flag) ? $request->container_flag : 0), @@ -509,6 +514,7 @@ public function downloadCsvSample($request, $id = null) PageCvsIndex::header_color => 'NULL', PageCvsIndex::theme => 'NULL', PageCvsIndex::layout => 'NULL', + PageCvsIndex::layout_inherit_flag => '1', PageCvsIndex::base_display_flag => '1', ]; $csv_array[2] = [ @@ -518,6 +524,7 @@ public function downloadCsvSample($request, $id = null) PageCvsIndex::header_color => 'NULL', PageCvsIndex::theme => 'NULL', PageCvsIndex::layout => 'NULL', + PageCvsIndex::layout_inherit_flag => '1', PageCvsIndex::base_display_flag => '1', ]; @@ -543,16 +550,20 @@ private function checkHeader($header_columns) return array("CSVファイルが空です。"); } - // ヘッダーカラム - $header_column_format = $this->getCsvHeader(); + // 必須ヘッダーカラム + $required_header_columns = $this->getCsvRequiredHeader(); // 項目の不足チェック - $shortness = array_diff($header_column_format, $header_columns); + $shortness = array_diff($required_header_columns, $header_columns); if (!empty($shortness)) { return array(implode(",", $shortness) . " が不足しています。"); } + + // 許可されるヘッダーカラム(必須 + 任意) + $allowed_header_columns = $this->getCsvHeader(); + // 項目の不要チェック - $excess = array_diff($header_columns, $header_column_format); + $excess = array_diff($header_columns, $allowed_header_columns); if (!empty($excess)) { return array(implode(",", $excess) . " は不要です。"); } @@ -574,10 +585,61 @@ private function getCsvHeader(): array return $header_column_format; } + /** + * CSV必須ヘッダー取得 + */ + private function getCsvRequiredHeader(): array + { + $required_header = $this->getCsvHeader(); + + foreach (array_keys($this->getOptionalCsvHeaderDefaults()) as $optional_header) { + $optional_header_index = array_search($optional_header, $required_header, true); + if ($optional_header_index !== false) { + unset($required_header[$optional_header_index]); + } + } + + return $required_header; + } + + /** + * CSV任意ヘッダーの既定値 + */ + private function getOptionalCsvHeaderDefaults(): array + { + return [ + PageCvsIndex::getDescription(PageCvsIndex::layout_inherit_flag) => '1', + ]; + } + + /** + * CSV列をヘッダー名ベースで正規化(任意ヘッダー欠落時は既定値で補完) + */ + private function normalizeCsvColumns(array $header_columns, array $csv_columns): array + { + $header_name_values = []; + foreach ($header_columns as $column_index => $header_name) { + $header_name_values[$header_name] = $csv_columns[$column_index] ?? null; + } + + $normalized_columns = []; + $optional_defaults = $this->getOptionalCsvHeaderDefaults(); + foreach ($this->getCsvHeader() as $column_index => $header_name) { + if (array_key_exists($header_name, $header_name_values)) { + $normalized_columns[$column_index] = $header_name_values[$header_name]; + continue; + } + + $normalized_columns[$column_index] = $optional_defaults[$header_name] ?? null; + } + + return $normalized_columns; + } + /** * CSVデータ行チェック */ - private function checkPageline($fp) + private function checkPageline($fp, array $header_columns) { // CSVインポート時のエラーチェックルール $rules = $this->pageUploadValidatorRules(); @@ -587,6 +649,8 @@ private function checkPageline($fp) $permanent_links = []; while (($csv_columns = fgetcsv($fp, 0, ",")) !== false) { + $csv_columns = $this->normalizeCsvColumns($header_columns, $csv_columns); + // バリデーション $validator = Validator::make($csv_columns, $rules); @@ -695,7 +759,7 @@ public function upload($request, $page_id) } // データ項目のエラーチェック - $error_msgs = $this->checkPageline($fp); + $error_msgs = $this->checkPageline($fp, $header_columns); if (!empty($error_msgs)) { // 一時ファイルの削除 fclose($fp); @@ -714,6 +778,8 @@ public function upload($request, $page_id) try { // データ while (($csv_columns = fgetcsv($fp, 0, ",")) !== false) { + $csv_columns = $this->normalizeCsvColumns($header_columns, $csv_columns); + // --- 入力値変換 // 入力値をトリム(preg_replace(/u)で置換. /u = UTF-8 として処理) $csv_columns = StringUtils::trimInput($csv_columns); @@ -736,6 +802,7 @@ public function upload($request, $page_id) 'header_color' => $csv_columns[PageCvsIndex::header_color], 'theme' => $csv_columns[PageCvsIndex::theme], 'layout' => $csv_columns[PageCvsIndex::layout], + 'layout_inherit_flag' => $csv_columns[PageCvsIndex::layout_inherit_flag], 'base_display_flag' => $csv_columns[PageCvsIndex::base_display_flag] ]); diff --git a/app/Plugins/Manage/SiteManage/SiteManage.php b/app/Plugins/Manage/SiteManage/SiteManage.php index 22d7ef1a4..c9f1f9110 100644 --- a/app/Plugins/Manage/SiteManage/SiteManage.php +++ b/app/Plugins/Manage/SiteManage/SiteManage.php @@ -317,6 +317,13 @@ public function update($request, $page_id = null) 'value' => $request->base_theme] ); + // 基本レイアウト + $configs = Configs::updateOrCreate( + ['name' => 'base_layout'], + ['category' => 'general', + 'value' => $request->base_layout] + ); + // 追加テーマ $configs = Configs::updateOrCreate( ['name' => 'additional_theme'], diff --git a/app/Plugins/Manage/SpamManage/SpamManage.php b/app/Plugins/Manage/SpamManage/SpamManage.php new file mode 100644 index 000000000..1a50d42e7 --- /dev/null +++ b/app/Plugins/Manage/SpamManage/SpamManage.php @@ -0,0 +1,505 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 + * @package Controller + * @plugin_title スパム管理 + * @plugin_desc スパムリストに関する機能が集まった管理機能です。 + */ +class SpamManage extends ManagePluginBase +{ + /** + * 権限定義 + */ + public function declareRole() + { + // 権限チェックテーブル + $role_check_table = array(); + $role_check_table["index"] = array('admin_site'); + $role_check_table["store"] = array('admin_site'); + $role_check_table["edit"] = array('admin_site'); + $role_check_table["update"] = array('admin_site'); + $role_check_table["destroy"] = array('admin_site'); + $role_check_table["downloadCsv"] = array('admin_site'); + $role_check_table["blockHistory"] = array('admin_site'); + $role_check_table["downloadBlockHistoryCsv"] = array('admin_site'); + return $role_check_table; + } + + /** + * スパムリスト一覧表示 + * + * @return view + * @method_title スパムリスト一覧 + * @method_desc スパムリストを一覧で確認できます。 + * @method_detail メールアドレス、ドメイン、IPアドレスを登録してスパムをブロックできます。 + */ + public function index($request) + { + // ページネートの表示ページを取得 + $page = $this->getPaginatePageFromRequestOrSession($request, 'spam_list_page', 'page'); + + // 検索条件を取得 + $search_block_type = $request->input('search_block_type', ''); + $search_block_value = $request->input('search_block_value', ''); + $search_scope_type = $request->input('search_scope_type', ''); + $search_memo = $request->input('search_memo', ''); + + // スパムリストを取得(検索条件適用) + $query = SpamList::query(); + $query = $this->applySearchConditions($query, $request); + + $spam_lists = $query->orderBy('block_type') + ->orderBy('created_at', 'desc') + ->paginate(20, ['*'], 'page', $page) + ->appends($request->except('page')); + + // フォーム一覧を取得(ID連想配列) + $forms = Forms::orderBy('forms_name')->get()->keyBy('id'); + + // フォームが配置されているページのURLマップを作成 + $form_page_urls = $this->getFormPageUrls($forms); + + // 画面の呼び出し + return view('plugins.manage.spam.index', [ + "function" => __FUNCTION__, + "plugin_name" => "spam", + "spam_lists" => $spam_lists, + "forms" => $forms, + "form_page_urls" => $form_page_urls, + "search_block_type" => $search_block_type, + "search_block_value" => $search_block_value, + "search_scope_type" => $search_scope_type, + "search_memo" => $search_memo, + ]); + } + + /** + * スパムリスト追加処理 + */ + public function store($request) + { + // httpメソッド確認 + if (!$request->isMethod('post')) { + abort(403, '権限がありません。'); + } + + // ハニーポットの場合は値不要、それ以外は必須 + $block_value_rules = $request->block_type === SpamBlockType::honeypot + ? ['nullable', 'max:255'] + : ['required', 'max:255']; + + // 項目のエラーチェック + $validator = Validator::make($request->all(), [ + 'block_type' => ['required', 'in:' . implode(',', SpamBlockType::getMemberKeys())], + 'block_value' => $block_value_rules, + 'target_forms_id' => ['required_if:scope_type,form'], + ], [ + 'target_forms_id.required_if' => '適用範囲で特定フォームを選択した場合、フォームを選択してください。', + ]); + $validator->setAttributeNames([ + 'block_type' => '種別', + 'block_value' => '値', + 'target_forms_id' => 'フォーム', + ]); + + // エラーがあった場合は入力画面に戻る。 + if ($validator->fails()) { + return redirect('manage/spam') + ->withErrors($validator) + ->withInput(); + } + + // 適用範囲の処理 + $target_id = null; + if ($request->scope_type === 'form' && $request->filled('target_forms_id')) { + $target_id = $request->target_forms_id; + } + + // ハニーポットの場合は値をnullにする + $block_value = $request->block_type === SpamBlockType::honeypot + ? null + : $request->block_value; + + // スパムリストの追加 + SpamList::create([ + 'target_plugin_name' => 'forms', + 'target_id' => $target_id, + 'block_type' => $request->block_type, + 'block_value' => $block_value, + 'memo' => $request->memo, + ]); + + // スパムリスト一覧画面に戻る + return redirect("/manage/spam")->with('flash_message', 'スパムリストに追加しました。'); + } + + /** + * スパムリスト編集画面 + * + * @return view + * @method_title スパムリスト編集 + * @method_desc スパムリストを編集できます。 + * @method_detail + */ + public function edit($request, $id) + { + // スパムリストデータの呼び出し + $spam = SpamList::findOrFail($id); + + // フォーム一覧を取得(ID連想配列) + $forms = Forms::orderBy('forms_name')->get()->keyBy('id'); + + // 画面の呼び出し + return view('plugins.manage.spam.edit', [ + "function" => __FUNCTION__, + "plugin_name" => "spam", + "spam" => $spam, + "forms" => $forms, + ]); + } + + /** + * スパムリスト更新処理 + */ + public function update($request, $id) + { + // httpメソッド確認 + if (!$request->isMethod('post')) { + abort(403, '権限がありません。'); + } + + // スパムリストデータの呼び出し(バリデーション前に取得してblock_typeを参照) + $spam = SpamList::findOrFail($id); + + // ハニーポットの場合は値不要、それ以外は必須 + $block_value_rules = $spam->block_type === SpamBlockType::honeypot + ? ['nullable', 'max:255'] + : ['required', 'max:255']; + + // 項目のエラーチェック + $validator = Validator::make($request->all(), [ + 'block_value' => $block_value_rules, + 'target_forms_id' => ['required_if:scope_type,form'], + ], [ + 'target_forms_id.required_if' => '適用範囲で特定フォームを選択した場合、フォームを選択してください。', + ]); + $validator->setAttributeNames([ + 'block_value' => '値', + 'target_forms_id' => 'フォーム', + ]); + + // エラーがあった場合は入力画面に戻る。 + if ($validator->fails()) { + return redirect('manage/spam/edit/' . $id) + ->withErrors($validator) + ->withInput(); + } + + // 適用範囲の処理 + $target_id = null; + if ($request->scope_type === 'form' && $request->filled('target_forms_id')) { + $target_id = $request->target_forms_id; + } + + // ハニーポットの場合は値をnullにする + $block_value = $spam->block_type === SpamBlockType::honeypot + ? null + : $request->block_value; + + // 更新 + $spam->target_id = $target_id; + $spam->block_value = $block_value; + $spam->memo = $request->memo; + $spam->save(); + + // スパムリスト一覧画面に戻る + return redirect("/manage/spam")->with('flash_message', 'スパムリストを更新しました。'); + } + + /** + * スパムリスト削除処理 + */ + public function destroy($request, $id) + { + // httpメソッド確認 + if (!$request->isMethod('post')) { + abort(403, '権限がありません。'); + } + + // 削除 + SpamList::where('id', $id)->delete(); + + // スパムリスト一覧画面に戻る + return redirect("/manage/spam")->with('flash_message', 'スパムリストから削除しました。'); + } + + /** + * CSVダウンロード + */ + public function downloadCsv($request) + { + // スパムリストを取得(検索条件適用) + $query = SpamList::query(); + $query = $this->applySearchConditions($query, $request); + + $spam_lists = $query->orderBy('block_type') + ->orderBy('created_at', 'desc') + ->get(); + + // フォーム一覧を取得 + $forms = Forms::pluck('forms_name', 'id'); + + // CSVデータの作成 + $csv_data = ''; + + // ヘッダー行 + $csv_data .= '"種別","値","適用範囲","メモ","登録日時"' . "\n"; + + // データ行 + foreach ($spam_lists as $spam) { + $scope_name = is_null($spam->target_id) ? '全体' : ($forms[$spam->target_id] ?? '不明'); + $csv_data .= '"' . SpamBlockType::getDescription($spam->block_type) . '",'; + $csv_data .= '"' . str_replace('"', '""', $spam->block_value) . '",'; + $csv_data .= '"' . $scope_name . '",'; + $csv_data .= '"' . str_replace('"', '""', $spam->memo ?? '') . '",'; + $csv_data .= '"' . $spam->created_at . '"' . "\n"; + } + + // 文字コード変換(UTF-8 BOM付き) + $csv_data = "\xEF\xBB\xBF" . $csv_data; + + // ファイル名 + $filename = 'spam_list_' . date('Ymd_His') . '.csv'; + + // レスポンス + return response($csv_data) + ->header('Content-Type', 'text/csv') + ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } + + /** + * ブロック履歴一覧表示 + * + * @return view + * @method_title ブロック履歴 + * @method_desc スパムブロックの履歴を確認できます。 + * @method_detail スパムフィルタリングでブロックされた履歴を一覧で確認できます。 + */ + public function blockHistory($request) + { + // ページネートの表示ページを取得 + $page = $this->getPaginatePageFromRequestOrSession($request, 'spam_block_history_page', 'page'); + + // 検索条件を取得 + $search_block_type = $request->input('search_block_type', ''); + $search_block_value = $request->input('search_block_value', ''); + $search_client_ip = $request->input('search_client_ip', ''); + $search_forms_name = $request->input('search_forms_name', ''); + $search_date_from = $request->input('search_date_from', ''); + $search_date_to = $request->input('search_date_to', ''); + + // ブロック履歴を取得(検索条件適用) + $query = SpamBlockHistory::query(); + $query = $this->applyBlockHistorySearchConditions($query, $request); + + $block_histories = $query->orderBy('created_at', 'desc') + ->paginate(20, ['*'], 'page', $page) + ->appends($request->except('page')); + + // フォーム一覧を取得(ID連想配列) + $forms = Forms::orderBy('forms_name')->get()->keyBy('id'); + + // フォームが配置されているページのURLマップを作成 + $form_page_urls = $this->getFormPageUrls($forms); + + // 画面の呼び出し + return view('plugins.manage.spam.block_history', [ + "function" => __FUNCTION__, + "plugin_name" => "spam", + "block_histories" => $block_histories, + "forms" => $forms, + "form_page_urls" => $form_page_urls, + "search_block_type" => $search_block_type, + "search_block_value" => $search_block_value, + "search_client_ip" => $search_client_ip, + "search_forms_name" => $search_forms_name, + "search_date_from" => $search_date_from, + "search_date_to" => $search_date_to, + ]); + } + + /** + * ブロック履歴CSVダウンロード + */ + public function downloadBlockHistoryCsv($request) + { + // ブロック履歴を取得(検索条件適用) + $query = SpamBlockHistory::query(); + $query = $this->applyBlockHistorySearchConditions($query, $request); + + $block_histories = $query->orderBy('created_at', 'desc')->get(); + + // フォーム一覧を取得 + $forms = Forms::pluck('forms_name', 'id'); + + // CSVデータの作成 + $csv_data = ''; + + // ヘッダー行 + $csv_data .= '"ブロック日時","種別","マッチした値","フォーム名","IPアドレス","送信メールアドレス"' . "\n"; + + // データ行 + foreach ($block_histories as $history) { + $form_name = $history->forms_id ? ($forms[$history->forms_id] ?? '不明') : ''; + $csv_data .= '"' . $history->created_at . '",'; + $csv_data .= '"' . SpamBlockType::getDescription($history->block_type) . '",'; + $csv_data .= '"' . str_replace('"', '""', $history->block_value) . '",'; + $csv_data .= '"' . str_replace('"', '""', $form_name) . '",'; + $csv_data .= '"' . ($history->client_ip ?? '') . '",'; + $csv_data .= '"' . str_replace('"', '""', $history->submitted_email ?? '') . '"' . "\n"; + } + + // 文字コード変換(UTF-8 BOM付き) + $csv_data = "\xEF\xBB\xBF" . $csv_data; + + // ファイル名 + $filename = 'spam_block_history_' . date('Ymd_His') . '.csv'; + + // レスポンス + return response($csv_data) + ->header('Content-Type', 'text/csv') + ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } + + /** + * ブロック履歴の検索条件をクエリに適用 + * + * @param \Illuminate\Database\Eloquent\Builder $query クエリビルダー + * @param \Illuminate\Http\Request $request リクエスト + * @return \Illuminate\Database\Eloquent\Builder + */ + private function applyBlockHistorySearchConditions($query, $request) + { + // 種別 + $search_block_type = $request->input('search_block_type', ''); + if (!empty($search_block_type)) { + $query->where('block_type', $search_block_type); + } + + // 値(部分一致) + $search_block_value = $request->input('search_block_value', ''); + if (!empty($search_block_value)) { + $query->where('block_value', 'like', '%' . $search_block_value . '%'); + } + + // IPアドレス(部分一致) + $search_client_ip = $request->input('search_client_ip', ''); + if (!empty($search_client_ip)) { + $query->where('client_ip', 'like', '%' . $search_client_ip . '%'); + } + + // フォーム名(フォームIDで検索) + $search_forms_name = $request->input('search_forms_name', ''); + if (!empty($search_forms_name)) { + $form_ids = Forms::where('forms_name', 'like', '%' . $search_forms_name . '%')->pluck('id'); + $query->whereIn('forms_id', $form_ids); + } + + // 日付範囲(開始) + $search_date_from = $request->input('search_date_from', ''); + if (!empty($search_date_from)) { + $query->where('created_at', '>=', $search_date_from . ' 00:00:00'); + } + + // 日付範囲(終了) + $search_date_to = $request->input('search_date_to', ''); + if (!empty($search_date_to)) { + $query->where('created_at', '<=', $search_date_to . ' 23:59:59'); + } + + return $query; + } + + /** + * 検索条件をクエリに適用 + * + * @param \Illuminate\Database\Eloquent\Builder $query クエリビルダー + * @param \Illuminate\Http\Request $request リクエスト + * @return \Illuminate\Database\Eloquent\Builder + */ + private function applySearchConditions($query, $request) + { + // 種別 + $search_block_type = $request->input('search_block_type', ''); + if (!empty($search_block_type)) { + $query->where('block_type', $search_block_type); + } + + // 値(部分一致) + $search_block_value = $request->input('search_block_value', ''); + if (!empty($search_block_value)) { + $query->where('block_value', 'like', '%' . $search_block_value . '%'); + } + + // 適用範囲 + $search_scope_type = $request->input('search_scope_type', ''); + if ($search_scope_type === 'global') { + $query->whereNull('target_id'); + } elseif ($search_scope_type === 'form') { + $query->whereNotNull('target_id'); + } + + // メモ(部分一致) + $search_memo = $request->input('search_memo', ''); + if (!empty($search_memo)) { + $query->where('memo', 'like', '%' . $search_memo . '%'); + } + + return $query; + } + + /** + * フォームが配置されているページのURLマップを作成 + * + * @param \Illuminate\Support\Collection $forms フォーム一覧(ID連想配列) + * @return array forms_id => ページURL の連想配列 + */ + private function getFormPageUrls($forms) + { + $form_page_urls = []; + + // フォームのbucket_idからFrameを取得し、ページURLを特定 + $frames = Frame::whereIn('bucket_id', $forms->pluck('bucket_id')) + ->where('plugin_name', 'forms') + ->with('page') + ->get(); + + foreach ($frames as $frame) { + $form = $forms->firstWhere('bucket_id', $frame->bucket_id); + if ($form && $frame->page && !isset($form_page_urls[$form->id])) { + $form_page_urls[$form->id] = url($frame->page->permanent_link); + } + } + + return $form_page_urls; + } +} diff --git a/app/Plugins/Mypage/ProfileMypage/ProfileMypage.php b/app/Plugins/Mypage/ProfileMypage/ProfileMypage.php index 64a2db954..7c68dc328 100644 --- a/app/Plugins/Mypage/ProfileMypage/ProfileMypage.php +++ b/app/Plugins/Mypage/ProfileMypage/ProfileMypage.php @@ -104,7 +104,12 @@ public function update($request, $id) $validator_array['column']['userid'] = UsersTool::getDefaultColumnAdditionalRules($base_rules, $users_column); } elseif ($users_column->column_type == UserColumnType::user_email) { // $validator_array['column']['email'] = ['nullable', 'email', 'max:255', Rule::unique('users')->ignore($id)]; - $base_rules = ['nullable', 'email', 'max:255', new CustomValiUserEmailUnique($request->columns_set_id, $id)]; + $base_rules = ['email', 'max:255', new CustomValiUserEmailUnique($request->columns_set_id, $id)]; + if ($users_column->required) { + array_unshift($base_rules, 'required'); + } else { + array_unshift($base_rules, 'nullable'); + } $validator_array['column']['email'] = UsersTool::getDefaultColumnAdditionalRules($base_rules, $users_column); } elseif ($users_column->column_type == UserColumnType::user_password) { // 入力があったら、ここで現在のパスワードチェック diff --git a/app/Plugins/PluginBase.php b/app/Plugins/PluginBase.php index 82bd2c392..f80e4d33f 100644 --- a/app/Plugins/PluginBase.php +++ b/app/Plugins/PluginBase.php @@ -6,6 +6,7 @@ use Illuminate\View\FileViewFinder; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Log; use App\Models\Common\Numbers; use App\Traits\ConnectMailTrait; @@ -38,10 +39,66 @@ public function __construct() */ public function ccErrorHandler($errno, $errstr, $errfile, $errline) { + // deprecated は例外化せずログのみ出力する + if (in_array($errno, [E_DEPRECATED, E_USER_DEPRECATED], true)) { + $this->logDeprecated($errno, $errstr, $errfile, $errline); + return true; + } + // 例外を投げる。 throw new \ErrorException($errstr, $errno, 0, $errfile, $errline); } + /** + * deprecated のログ出力 + */ + private function logDeprecated($errno, $errstr, $errfile, $errline) + { + // Log 出力中の再帰的なエラーハンドラ呼び出しを避けるためのガード + // static なので同一リクエスト内では状態が保持され、2回目以降は即 return する + static $logging = false; + if ($logging) { + return; + } + + $logging = true; + try { + $context = [ + 'message' => $errstr, + 'file' => $errfile, + 'line' => $errline, + 'errno' => $errno, + 'environment' => app()->environment(), + 'php' => PHP_VERSION, + ]; + + if (app()->bound('request')) { + $request = app('request'); + $context += [ + 'method' => $request->method(), + 'path' => $request->path(), + 'route' => optional($request->route())->getName(), + ]; + } + + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 8); + $context['trace'] = array_map(function ($frame) { + return sprintf( + '%s:%s %s%s%s', + $frame['file'] ?? '[internal]', + $frame['line'] ?? '-', + $frame['class'] ?? '', + $frame['type'] ?? '', + $frame['function'] ?? '' + ); + }, array_slice($trace, 1)); + + Log::warning('PHP deprecated', $context); + } finally { + $logging = false; + } + } + // プラグインの対象者(User or Manager):view ファイルの呼び出しでディレクトリ判定に使用するため。 // var $plugin_target = null; diff --git a/app/Plugins/User/Forms/FormsPlugin.php b/app/Plugins/User/Forms/FormsPlugin.php index a007aa559..fb273ad39 100644 --- a/app/Plugins/User/Forms/FormsPlugin.php +++ b/app/Plugins/User/Forms/FormsPlugin.php @@ -20,6 +20,8 @@ use App\Models\User\Forms\FormsColumnsSelects; use App\Models\User\Forms\FormsInputs; use App\Models\User\Forms\FormsInputCols; +use App\Models\Common\SpamBlockHistory; +use App\Models\Common\SpamList; use App\Rules\CustomValiAlphaNumForMultiByte; use App\Rules\CustomValiCheckWidthForString; @@ -42,6 +44,7 @@ use App\Enums\FormsRegisterTargetPlugin; use App\Enums\FormStatusType; use App\Enums\PluginName; +use App\Enums\SpamBlockType; use App\Enums\Required; use App\Enums\StatusType; use App\Models\User\Bbses\Bbs; @@ -99,6 +102,7 @@ public function getPublicFunctions() 'listInputs', 'editInput', 'thanks', + 'editSpamFilter', ]; $functions['post'] = [ 'index', @@ -115,6 +119,10 @@ public function getPublicFunctions() 'registerOtherPlugins', 'updateSelectSequenceAll', 'updateColumnSequenceAll', + 'saveSpamFilter', + 'addSpamList', + 'deleteSpamList', + 'addToSpamListFromInput', ]; return $functions; } @@ -140,6 +148,11 @@ public function declareRole() $role_check_table["registerOtherPlugins"] = ['role_article']; $role_check_table['updateSelectSequenceAll'] = ['buckets.upColumnSequence', 'buckets.downColumnSequence']; $role_check_table['updateColumnSequenceAll'] = ['buckets.upColumnSequence', 'buckets.downColumnSequence']; + $role_check_table['editSpamFilter'] = ['frames.edit']; + $role_check_table['saveSpamFilter'] = ['frames.create']; + $role_check_table['addSpamList'] = ['frames.create']; + $role_check_table['deleteSpamList'] = ['frames.delete']; + $role_check_table['addToSpamListFromInput'] = ['frames.create']; return $role_check_table; } @@ -335,6 +348,15 @@ public function index($request, $page_id, $frame_id, $errors = null) return false; } + // スパムブロックエラーメッセージの確認 + $spam_blocked_error = session("spam_blocked_error_{$frame_id}"); + if ($spam_blocked_error) { + // エラー画面へ + return $this->commonView('error_messages', [ + 'error_messages' => [$spam_blocked_error], + ]); + } + // 登録期間外か if ($this->isOutOfTermRegist($form)) { // エラー画面へ @@ -436,6 +458,9 @@ public function index($request, $page_id, $frame_id, $errors = null) } } + // ハニーポット設定確認 + $has_honeypot = $this->hasHoneypot($form); + if ($form->form_mode == FormMode::form) { // フォーム return $this->view('forms', [ @@ -444,6 +469,7 @@ public function index($request, $page_id, $frame_id, $errors = null) 'forms_columns' => $forms_columns, 'forms_columns_id_select' => $forms_columns_id_select, 'errors' => $errors, + 'has_honeypot' => $has_honeypot, ]); } else { // アンケート @@ -453,6 +479,7 @@ public function index($request, $page_id, $frame_id, $errors = null) 'forms_columns' => $forms_columns, 'forms_columns_id_select' => $forms_columns_id_select, 'errors' => $errors, + 'has_honeypot' => $has_honeypot, ]); } } else { @@ -800,6 +827,27 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) ]); } + // ハニーポットチェック + $honeypot_check = $this->checkHoneypot($request, $form); + if ($honeypot_check['blocked']) { + $this->recordHoneypotBlock($honeypot_check, $form->id); + + return $this->commonView('error_messages', [ + 'error_messages' => [__('messages.honeypot_blocked')], + ]); + } + + // スパムフィルタリングチェック + $spam_check = $this->checkSpamFilter($request, $form); + if ($spam_check['blocked']) { + $this->recordSpamBlock($spam_check, $form->id); + + $spam_message = $form->spam_filter_message ?: '入力されたメールアドレス、または、IPアドレスからの送信は現在制限されています。'; + return $this->commonView('error_messages', [ + 'error_messages' => [$spam_message], + ]); + } + // フォームのカラムデータ $forms_columns = $this->getFormsColumns($form); @@ -916,6 +964,9 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) } } + // ハニーポット設定確認 + $has_honeypot = $this->hasHoneypot($form); + // 表示テンプレートを呼び出す if ($form->form_mode == FormMode::form) { // フォーム @@ -925,6 +976,7 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) 'form' => $form, 'forms_columns' => $forms_columns, 'uploads' => $uploads, + 'has_honeypot' => $has_honeypot, ]); } else { // アンケート @@ -934,6 +986,7 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) 'form' => $form, 'forms_columns' => $forms_columns, 'uploads' => $uploads, + 'has_honeypot' => $has_honeypot, ]); } } @@ -989,6 +1042,31 @@ public function publicStore($request, $page_id, $frame_id, $id = null) return collect(['redirect_path' => url($this->page->permanent_link)]); } + // ハニーポットチェック(二重防御) + $honeypot_check = $this->checkHoneypot($request, $form); + if ($honeypot_check['blocked']) { + $this->recordHoneypotBlock($honeypot_check, $form->id); + + // エラーメッセージをセッションに保存 + session()->flash("spam_blocked_error_{$frame_id}", __('messages.honeypot_blocked')); + + // 初期表示にリダイレクトして、初期表示処理にまかせる(エラー表示) + return collect(['redirect_path' => url($this->page->permanent_link)]); + } + + // スパムフィルタリングチェック(二重防御) + $spam_check = $this->checkSpamFilter($request, $form); + if ($spam_check['blocked']) { + $this->recordSpamBlock($spam_check, $form->id); + + // エラーメッセージをセッションに保存 + $spam_message = $form->spam_filter_message ?: '入力されたメールアドレス、または、IPアドレスからの送信は現在制限されています。'; + session()->flash("spam_blocked_error_{$frame_id}", $spam_message); + + // 初期表示にリダイレクトして、初期表示処理にまかせる(エラー表示) + return collect(['redirect_path' => url($this->page->permanent_link)]); + } + // forms_inputs 登録 $forms_inputs = new FormsInputs(); $forms_inputs->forms_id = $form->id; @@ -1015,6 +1093,11 @@ public function publicStore($request, $page_id, $frame_id, $id = null) } } + // IPアドレスを記録(スパムフィルタリングが有効な場合のみ) + if ($form->use_spam_filter_flag) { + $forms_inputs->ip_address = $request->ip(); + } + $forms_inputs->save(); // フォームのカラムデータ @@ -2741,6 +2824,21 @@ public function listInputs($request, $page_id, $frame_id, $forms_id = null) ->orderBy('forms_inputs_id', 'asc')->orderBy('forms_columns_id', 'asc') ->get(); + // メールアドレス型カラムのID取得 + $email_column_ids = $columns->where('column_type', FormColumnType::mail)->pluck('id')->toArray(); + + // 各投稿のメールアドレス有無マップを作成 + $has_email_map = []; + foreach ($inputs as $input) { + $has_email = $input_cols->where('forms_inputs_id', $input->id) + ->whereIn('forms_columns_id', $email_column_ids) + ->filter(function ($col) { + return !empty($col->value); + }) + ->isNotEmpty(); + $has_email_map[$input->id] = $has_email; + } + // bucktsで開いていたページの保持 // $frame_page = "frame_{$frame_id}_buckets_page"; @@ -2750,6 +2848,7 @@ public function listInputs($request, $page_id, $frame_id, $forms_id = null) 'columns' => $columns, 'inputs' => $inputs, 'input_cols' => $input_cols, + 'has_email_map' => $has_email_map, ]); } @@ -3111,4 +3210,493 @@ public static function canDownload($request, Uploads $upload): array return [false, '対象ファイルに対する権限なし']; } } + + /** + * スパムブロックのログ記録とDB履歴記録 + * + * @param array $spam_check checkSpamFilter() の戻り値 + * @param int $forms_id フォームID + * @return void + */ + private function recordSpamBlock(array $spam_check, int $forms_id): void + { + Log::info('Spam blocked', [ + 'form_id' => $forms_id, + 'ip' => $spam_check['client_ip'], + 'block_type' => $spam_check['matched_spam_list']->block_type ?? null, + 'matched_rule' => $spam_check['matched_spam_list']->id ?? null, + ]); + + // 履歴記録は補助的な機能のため、DB記録が失敗しても本来のスパムブロック処理(エラーメッセージ表示)を続行する + try { + SpamBlockHistory::create([ + 'spam_list_id' => $spam_check['matched_spam_list']->id, + 'forms_id' => $forms_id, + 'block_type' => $spam_check['matched_spam_list']->block_type, + 'block_value' => $spam_check['matched_spam_list']->block_value, + 'client_ip' => $spam_check['client_ip'], + 'submitted_email' => $spam_check['email'], + ]); + } catch (\Exception $e) { + Log::error('Failed to record spam block history', ['error' => $e->getMessage()]); + } + } + + /** + * ハニーポットチェック + * + * @param \Illuminate\Http\Request $request リクエスト + * @param Forms $form フォームデータ + * @return array ブロック情報の配列 + */ + private function checkHoneypot($request, $form): array + { + $client_ip = $request->ip(); + + // このフォーム用のハニーポット設定を取得 + $honeypot_spam_list = SpamList::where('target_plugin_name', 'forms') + ->where('block_type', SpamBlockType::honeypot) + ->where(function ($q) use ($form) { + $q->where('target_id', $form->id) + ->orWhereNull('target_id'); + }) + ->first(); + + // ハニーポットが設定されていなければ早期リターン + if (!$honeypot_spam_list) { + return [ + 'blocked' => false, + 'client_ip' => $client_ip, + 'matched_spam_list' => null, + ]; + } + + // ハニーポットフィールドに値があればボットと判定 + $honeypot_value = $request->input('website_url', ''); + if (!empty($honeypot_value)) { + return [ + 'blocked' => true, + 'client_ip' => $client_ip, + 'honeypot_value' => $honeypot_value, + 'matched_spam_list' => $honeypot_spam_list, + ]; + } + + return [ + 'blocked' => false, + 'client_ip' => $client_ip, + 'matched_spam_list' => $honeypot_spam_list, + ]; + } + + /** + * ハニーポットブロックのログ記録とDB履歴記録 + * + * @param array $honeypot_check checkHoneypot() の戻り値 + * @param int $forms_id フォームID + * @return void + */ + private function recordHoneypotBlock(array $honeypot_check, int $forms_id): void + { + Log::info('Honeypot blocked', [ + 'form_id' => $forms_id, + 'ip' => $honeypot_check['client_ip'], + 'honeypot_value' => $honeypot_check['honeypot_value'] ?? null, + 'spam_list_id' => $honeypot_check['matched_spam_list']->id ?? null, + ]); + + // 履歴記録は補助的な機能のため、DB記録が失敗しても本来のブロック処理(エラーメッセージ表示)を続行する + try { + SpamBlockHistory::create([ + 'spam_list_id' => $honeypot_check['matched_spam_list']->id ?? null, + 'forms_id' => $forms_id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => $honeypot_check['honeypot_value'] ?? null, + 'client_ip' => $honeypot_check['client_ip'], + 'submitted_email' => null, + ]); + } catch (\Exception $e) { + Log::error('Failed to record honeypot block history', ['error' => $e->getMessage()]); + } + } + + /** + * ハニーポットが設定されているか確認 + * + * @param Forms $form フォームデータ + * @return bool ハニーポットが設定されている場合true + */ + private function hasHoneypot($form): bool + { + return SpamList::where('target_plugin_name', 'forms') + ->where('block_type', SpamBlockType::honeypot) + ->where(function ($q) use ($form) { + $q->where('target_id', $form->id) + ->orWhereNull('target_id'); + }) + ->exists(); + } + + /** + * スパムフィルタリングチェック + * + * @param \Illuminate\Http\Request $request リクエスト + * @param Forms $form フォームデータ + * @return array ブロック情報の配列 + */ + private function checkSpamFilter($request, $form): array + { + $client_ip = $request->ip(); + + // スパムフィルタリングが無効なら早期リターン + if (!$form->use_spam_filter_flag) { + return [ + 'blocked' => false, + 'matched_spam_list' => null, + 'client_ip' => $client_ip, + 'email' => null, + ]; + } + + // 取得対象のスパムリスト(このフォーム用 + サイト全体用) + $spam_lists = SpamList::getFormsSpamLists($form->id); + + if ($spam_lists->isEmpty()) { + return [ + 'blocked' => false, + 'matched_spam_list' => null, + 'client_ip' => $client_ip, + 'email' => null, + ]; + } + + // IPアドレスチェック + $matched_spam_list = $spam_lists->where('block_type', SpamBlockType::ip_address) + ->where('block_value', $client_ip) + ->first(); + if ($matched_spam_list) { + return [ + 'blocked' => true, + 'matched_spam_list' => $matched_spam_list, + 'client_ip' => $client_ip, + 'email' => null, + ]; + } + + // メールアドレス・ドメインチェック(メール型カラムの値を取得) + $email_columns = FormsColumns::where('forms_id', $form->id) + ->where('column_type', FormColumnType::mail) + ->pluck('id'); + + foreach ($email_columns as $column_id) { + $email = $request->forms_columns_value[$column_id] ?? null; + if (empty($email)) { + continue; + } + + // メールアドレス完全一致チェック + $matched_spam_list = $spam_lists->where('block_type', SpamBlockType::email) + ->where('block_value', $email) + ->first(); + if ($matched_spam_list) { + return [ + 'blocked' => true, + 'matched_spam_list' => $matched_spam_list, + 'client_ip' => $client_ip, + 'email' => $email, + ]; + } + + // ドメインチェック + $domain = substr(strrchr($email, "@"), 1); + if ($domain) { + $matched_spam_list = $spam_lists->where('block_type', SpamBlockType::domain) + ->where('block_value', $domain) + ->first(); + if ($matched_spam_list) { + return [ + 'blocked' => true, + 'matched_spam_list' => $matched_spam_list, + 'client_ip' => $client_ip, + 'email' => $email, + ]; + } + } + } + + return [ + 'blocked' => false, + 'matched_spam_list' => null, + 'client_ip' => $client_ip, + 'email' => null, + ]; + } + + /** + * スパムフィルタリング設定画面 + * + * @method_title スパムフィルタリング + * @method_desc スパムフィルタリングの設定ができます。 + * @method_detail スパムリストの管理や、ブロック時のメッセージを設定できます。 + */ + public function editSpamFilter($request, $page_id, $frame_id) + { + // フォーム&フレームデータ + $form_frame = $this->getFormFrame($frame_id); + + // フォームデータ + $form = null; + if (!empty($form_frame->bucket_id)) { + $form = Forms::where('bucket_id', $form_frame->bucket_id)->first(); + } + + if (empty($form)) { + // ワーニング画面へ + return $this->view('forms_edit_warning_messages', [ + 'warning_messages' => ["フォーム選択から選択するか、フォーム作成で作成してください。"], + ]); + } + + // このフォームに適用されるスパムリスト(フォーム固有 + サイト全体) + $spam_lists = SpamList::getFormsSpamLists($form->id); + + // 表示テンプレートを呼び出す + return $this->view('forms_edit_spam_filter', [ + 'form' => $form, + 'spam_lists' => $spam_lists, + ]); + } + + /** + * スパムフィルタリング設定の保存 + */ + public function saveSpamFilter($request, $page_id, $frame_id, $forms_id) + { + // フォームデータ取得 + $form = Forms::find($forms_id); + if (empty($form)) { + abort(404, 'フォームが見つかりません。'); + } + + // 更新 + $form->use_spam_filter_flag = $request->input('use_spam_filter_flag', 0); + $form->spam_filter_message = $request->spam_filter_message; + $form->save(); + + // リダイレクト + return redirect("/plugin/forms/editSpamFilter/{$page_id}/{$frame_id}#frame-{$frame_id}") + ->with('flash_message', 'スパムフィルタリング設定を保存しました。'); + } + + /** + * スパムリストへの追加 + */ + public function addSpamList($request, $page_id, $frame_id, $forms_id) + { + // ハニーポットの場合は値不要、それ以外は必須 + $block_value_rules = $request->block_type === SpamBlockType::honeypot + ? ['nullable', 'max:255'] + : ['required', 'max:255']; + + // 項目のエラーチェック + $validator = Validator::make($request->all(), [ + 'block_type' => ['required', 'in:' . implode(',', SpamBlockType::getMemberKeys())], + 'block_value' => $block_value_rules, + ]); + $validator->setAttributeNames([ + 'block_type' => '種別', + 'block_value' => '値', + ]); + + // エラーがあった場合は入力画面に戻る + if ($validator->fails()) { + return redirect("/plugin/forms/editSpamFilter/{$page_id}/{$frame_id}#frame-{$frame_id}") + ->withErrors($validator) + ->withInput(); + } + + // ハニーポットの場合は値をnullにする + $block_value = $request->block_type === SpamBlockType::honeypot + ? null + : $request->block_value; + + // スパムリストの追加 + SpamList::create([ + 'target_plugin_name' => 'forms', + 'target_id' => $forms_id, + 'block_type' => $request->block_type, + 'block_value' => $block_value, + 'memo' => $request->memo, + ]); + + // リダイレクト + return redirect("/plugin/forms/editSpamFilter/{$page_id}/{$frame_id}#frame-{$frame_id}") + ->with('flash_message', 'スパムリストに追加しました。'); + } + + /** + * スパムリストからの削除 + */ + public function deleteSpamList($request, $page_id, $frame_id, $spam_id) + { + // スパムリストデータの取得 + $spam = SpamList::find($spam_id); + + // このフォーム専用のスパムリストのみ削除可能 + if ($spam && !$spam->isGlobalScope()) { + $spam->delete(); + } + + // リダイレクト + return redirect("/plugin/forms/editSpamFilter/{$page_id}/{$frame_id}#frame-{$frame_id}") + ->with('flash_message', 'スパムリストから削除しました。'); + } + + /** + * メールアドレス型カラムの入力値を取得 + * + * @param int $forms_id フォームID + * @param int $inputs_id 投稿ID + * @return \Illuminate\Database\Eloquent\Collection + */ + private function getEmailInputCols($forms_id, $inputs_id) + { + $email_columns = FormsColumns::where('forms_id', $forms_id) + ->where('column_type', FormColumnType::mail) + ->pluck('id'); + + return FormsInputCols::where('forms_inputs_id', $inputs_id) + ->whereIn('forms_columns_id', $email_columns) + ->get(); + } + + /** + * スパムリストへの追加(重複チェック付き) + * + * @param int|null $target_id 対象ID(nullの場合は全体適用) + * @param string $block_type ブロック種別 + * @param string $block_value ブロック対象の値 + * @param string|null $memo メモ + * @return string 'added'(追加成功)または'duplicate'(重複のためスキップ) + */ + private function addToSpamListWithDuplicateCheck($target_id, $block_type, $block_value, $memo) + { + $added = SpamList::addIfNotExists('forms', $target_id, $block_type, $block_value, $memo); + return $added ? 'added' : 'duplicate'; + } + + /** + * 投稿一覧からスパムリストへ追加 + */ + public function addToSpamListFromInput($request, $page_id, $frame_id, $inputs_id) + { + // 投稿データの取得 + $input = FormsInputs::find($inputs_id); + if (empty($input)) { + abort(404, '投稿データが見つかりません。'); + } + + $forms_id = $input->forms_id; + $added_count = 0; + $skipped_duplicate_count = 0; + $skipped_no_data_count = 0; + + // メモ + $memo = $request->input('memo'); + + // チェックボックスが1つも選択されていない場合 + $has_selection = $request->filled('add_ip_address') || $request->filled('add_email') || $request->filled('add_domain'); + if (!$has_selection) { + return redirect("/plugin/forms/listInputs/{$page_id}/{$frame_id}/{$forms_id}#frame-{$frame_id}") + ->with('flash_message', '追加する項目(IPアドレス、メールアドレス、ドメイン)を1つ以上選択してください。'); + } + + // 適用範囲の決定 + $scope_type = $request->input('scope_type', 'form'); + $target_id = ($scope_type === 'global') ? null : $forms_id; + + // IPアドレスをスパムリストに追加 + if ($request->filled('add_ip_address')) { + if (empty($input->ip_address)) { + $skipped_no_data_count++; + } else { + $result = $this->addToSpamListWithDuplicateCheck($target_id, SpamBlockType::ip_address, $input->ip_address, $memo); + if ($result === 'duplicate') { + $skipped_duplicate_count++; + } else { + $added_count++; + } + } + } + + // メールアドレスをスパムリストに追加 + if ($request->filled('add_email')) { + $input_cols = $this->getEmailInputCols($forms_id, $inputs_id); + + $has_email_value = false; + foreach ($input_cols as $col) { + if (empty($col->value)) { + continue; + } + $has_email_value = true; + + $result = $this->addToSpamListWithDuplicateCheck($target_id, SpamBlockType::email, $col->value, $memo); + if ($result === 'duplicate') { + $skipped_duplicate_count++; + } else { + $added_count++; + } + } + if (!$has_email_value) { + $skipped_no_data_count++; + } + } + + // ドメインをスパムリストに追加 + if ($request->filled('add_domain')) { + $input_cols = $this->getEmailInputCols($forms_id, $inputs_id); + + $has_domain_value = false; + foreach ($input_cols as $col) { + if (empty($col->value)) { + continue; + } + + $domain = substr(strrchr($col->value, "@"), 1); + if (empty($domain)) { + continue; + } + $has_domain_value = true; + + $result = $this->addToSpamListWithDuplicateCheck($target_id, SpamBlockType::domain, $domain, $memo); + if ($result === 'duplicate') { + $skipped_duplicate_count++; + } else { + $added_count++; + } + } + if (!$has_domain_value) { + $skipped_no_data_count++; + } + } + + // メッセージの組み立て + $messages = []; + if ($added_count > 0) { + $messages[] = "{$added_count}件をスパムリストに追加しました。"; + } + if ($skipped_duplicate_count > 0) { + $messages[] = "{$skipped_duplicate_count}件は既に登録済みのためスキップしました。"; + } + if ($skipped_no_data_count > 0) { + $messages[] = "{$skipped_no_data_count}件は該当データがないためスキップしました。"; + } + if (empty($messages)) { + $messages[] = 'スパムリストへの追加はありませんでした。'; + } + + // リダイレクト + return redirect("/plugin/forms/listInputs/{$page_id}/{$frame_id}/{$forms_id}#frame-{$frame_id}") + ->with('flash_message', implode(' ', $messages)); + } } diff --git a/app/Plugins/User/Photoalbums/PhotoalbumsPlugin.php b/app/Plugins/User/Photoalbums/PhotoalbumsPlugin.php index 174e0dc69..6bab9e2b4 100644 --- a/app/Plugins/User/Photoalbums/PhotoalbumsPlugin.php +++ b/app/Plugins/User/Photoalbums/PhotoalbumsPlugin.php @@ -2,6 +2,7 @@ namespace App\Plugins\User\Photoalbums; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; @@ -66,7 +67,7 @@ public function getPublicFunctions() { // 標準関数以外で画面などから呼ばれる関数の定義 $functions = array(); - $functions['get'] = ['index', 'download', 'changeDirectory', 'embed', 'detail']; + $functions['get'] = ['index', 'download', 'changeDirectory', 'embed', 'detail', 'moreContents']; $functions['post'] = ['makeFolder', 'editFolder', 'upload', 'uploadVideo', 'editContents', 'editVideo', 'deleteContents', 'updateViewSequence', 'updateHiddenFolders']; return $functions; } @@ -141,29 +142,312 @@ public function index($request, $page_id, $frame_id, $parent_id = null) return; } - // フォルダ、ファイルの比較条件の取得 - $sort_folder = FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::sort_folder); - $sort_file = FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::sort_file); - - $photoalbum_contents = $this->getSortedChildren($parent, $sort_folder, $sort_file); - if (!empty($hidden_folder_ids)) { - $photoalbum_contents = $photoalbum_contents->reject(function ($content) use ($hidden_folder_ids) { - return $content->is_folder == PhotoalbumContent::is_folder_on - && in_array($content->id, $hidden_folder_ids, true); - }); - } - - // カバー写真に指定されている写真 - $covers = PhotoalbumContent::whereIn('parent_id', $photoalbum_contents->where('is_folder', PhotoalbumContent::is_folder_on)->pluck('id'))->where('is_cover', PhotoalbumContent::is_cover_on)->get(); + $photoalbum_contents = $this->getSortedVisibleChildren($parent, $hidden_folder_ids); + $load_more_state = $this->getIndexLoadMoreState(); + $index_display_items = $this->buildIndexDisplayItems( + $photoalbum_contents, + $load_more_state['load_more_use'], + $load_more_state['load_more_count'] + ); + $covers = $this->fetchFolderCovers($index_display_items['photoalbum_folder_items'], $parent->photoalbum_id); // 表示テンプレートを呼び出す。 - return $this->view('index', [ + return $this->view('index', array_merge([ 'photoalbum' => $photoalbum, 'photoalbum_contents' => $photoalbum_contents, 'breadcrumbs' => $this->fetchBreadCrumbs($photoalbum->id, $parent->id), 'parent_id' => $parent->id, 'covers' => $covers, - ]); + ], $index_display_items)); + } + + /** + * 親配下の表示可能コンテンツを、表示設定の並び順で取得する。 + * + * @param \App\Models\User\Photoalbums\PhotoalbumContent $parent + * @param array $hidden_folder_ids + * @return \Illuminate\Support\Collection + */ + private function getSortedVisibleChildren(PhotoalbumContent $parent, array $hidden_folder_ids): Collection + { + $folder_items = $this->getSortedVisibleItemsByTarget($parent, $hidden_folder_ids, 'folder'); + $image_items = $this->getSortedVisibleItemsByTarget($parent, $hidden_folder_ids, 'image'); + + return $folder_items->concat($image_items)->values(); + } + + /** + * 並び順設定(フォルダ/画像)を取得する。 + * + * @return array + */ + private function getPhotoalbumSortConfig(): array + { + return [ + 'sort_folder' => FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::sort_folder), + 'sort_file' => FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::sort_file), + ]; + } + + /** + * 親配下の対象種別コンテンツを取得するクエリを返す。 + * + * @param \App\Models\User\Photoalbums\PhotoalbumContent $parent + * @param string $target 対象種別(folder|image) + * @param array $hidden_folder_ids + * @return \Illuminate\Database\Eloquent\Builder + */ + private function getVisibleChildrenQueryByTarget(PhotoalbumContent $parent, string $target, array $hidden_folder_ids): Builder + { + $is_folder = $target === 'folder' + ? PhotoalbumContent::is_folder_on + : PhotoalbumContent::is_folder_off; + + $query = PhotoalbumContent::query() + ->where('photoalbum_id', $parent->photoalbum_id) + ->where('parent_id', $parent->id) + ->where('is_folder', $is_folder); + + if ($is_folder == PhotoalbumContent::is_folder_on && !empty($hidden_folder_ids)) { + $query->whereNotIn('id', $hidden_folder_ids); + } + + return $this->appendTargetRelations($query, $target); + } + + /** + * フォトアルバム全体の対象種別コンテンツを取得するクエリを返す。 + */ + private function getPhotoalbumItemsQueryByTarget(int $photoalbum_id, string $target): Builder + { + $is_folder = $target === 'folder' + ? PhotoalbumContent::is_folder_on + : PhotoalbumContent::is_folder_off; + + $query = PhotoalbumContent::query() + ->where('photoalbum_id', $photoalbum_id) + ->where('is_folder', $is_folder); + + return $this->appendTargetRelations($query, $target); + } + + /** + * 対象種別で必要な関連をクエリへ付与する。 + */ + private function appendTargetRelations(Builder $query, string $target): Builder + { + if ($target === 'image') { + $query->with('upload'); + } + + return $query; + } + + /** + * ソート設定値を有効値へ正規化する。 + */ + private function normalizePhotoalbumSortKey(?string $sort_key): string + { + if (in_array($sort_key, [ + PhotoalbumSort::name_asc, + PhotoalbumSort::name_desc, + PhotoalbumSort::created_asc, + PhotoalbumSort::created_desc, + PhotoalbumSort::manual_order, + ], true)) { + return $sort_key; + } + + return PhotoalbumSort::name_asc; + } + + /** + * 対象種別のソートキーを取得する。 + */ + private function getTargetSortKey(string $target, ?string $sort_folder, ?string $sort_file): string + { + $target_sort = $target === 'folder' ? $sort_folder : $sort_file; + return $this->normalizePhotoalbumSortKey($target_sort); + } + + /** + * 対象種別のクエリにソート条件を適用する。 + */ + private function applySortToVisibleChildrenQuery(Builder $query, string $target, ?string $sort_folder, ?string $sort_file): Builder + { + $target_sort = $this->getTargetSortKey($target, $sort_folder, $sort_file); + $name_column = 'photoalbum_contents.name'; + + if ($target === 'image' && in_array($target_sort, [PhotoalbumSort::name_asc, PhotoalbumSort::name_desc], true)) { + $query->leftJoin('uploads as sort_uploads', 'sort_uploads.id', '=', 'photoalbum_contents.upload_id') + ->select('photoalbum_contents.*'); + $name_column = 'sort_uploads.client_original_name'; + } + + switch ($target_sort) { + case PhotoalbumSort::name_desc: + $query->orderBy($name_column, 'desc'); + break; + case PhotoalbumSort::created_asc: + $query->orderBy('photoalbum_contents.created_at', 'asc'); + break; + case PhotoalbumSort::created_desc: + $query->orderBy('photoalbum_contents.created_at', 'desc'); + break; + case PhotoalbumSort::manual_order: + $query->orderBy('photoalbum_contents.display_sequence', 'asc'); + break; + case PhotoalbumSort::name_asc: + default: + $query->orderBy($name_column, 'asc'); + break; + } + + return $query->orderBy('photoalbum_contents.id', 'asc'); + } + + /** + * 親配下の表示可能コンテンツ(対象種別)を並び順付きクエリで取得する。 + */ + private function getSortedVisibleItemsQueryByTarget( + PhotoalbumContent $parent, + array $hidden_folder_ids, + string $target, + ?string $sort_folder = null, + ?string $sort_file = null + ): Builder { + if (is_null($sort_folder) || is_null($sort_file)) { + $sort_config = $this->getPhotoalbumSortConfig(); + $sort_folder = $sort_config['sort_folder']; + $sort_file = $sort_config['sort_file']; + } + + $query = $this->getVisibleChildrenQueryByTarget($parent, $target, $hidden_folder_ids); + return $this->applySortToVisibleChildrenQuery($query, $target, $sort_folder, $sort_file); + } + + /** + * 親配下の表示可能コンテンツを、対象種別ごとに並び替えて取得する。 + * + * @param \App\Models\User\Photoalbums\PhotoalbumContent $parent + * @param array $hidden_folder_ids + * @param string $target 対象種別(folder|image) + * @param string|null $sort_folder + * @param string|null $sort_file + * @return \Illuminate\Support\Collection + */ + private function getSortedVisibleItemsByTarget( + PhotoalbumContent $parent, + array $hidden_folder_ids, + string $target, + ?string $sort_folder = null, + ?string $sort_file = null + ): Collection { + return $this->getSortedVisibleItemsQueryByTarget( + $parent, + $hidden_folder_ids, + $target, + $sort_folder, + $sort_file + )->get()->values(); + } + + /** + * フォトアルバム全体の対象種別コンテンツを並び順付きで取得する。 + */ + private function getSortedPhotoalbumItemsByTarget( + int $photoalbum_id, + string $target, + ?string $sort_folder = null, + ?string $sort_file = null + ): Collection { + if (is_null($sort_folder) || is_null($sort_file)) { + $sort_config = $this->getPhotoalbumSortConfig(); + $sort_folder = $sort_config['sort_folder']; + $sort_file = $sort_config['sort_file']; + } + + $query = $this->getPhotoalbumItemsQueryByTarget($photoalbum_id, $target); + return $this->applySortToVisibleChildrenQuery($query, $target, $sort_folder, $sort_file)->get()->values(); + } + + /** + * 一覧表示で利用する「もっと見る」設定値を取得する。 + * + * @return array + */ + private function getIndexLoadMoreState(): array + { + return [ + 'load_more_use' => FrameConfig::getConfigValueAndOld( + $this->frame_configs, + PhotoalbumFrameConfig::load_more_use_flag, + \App\Enums\UseType::not_use + ), + 'load_more_count' => FrameConfig::getConfigValueAndOld( + $this->frame_configs, + PhotoalbumFrameConfig::load_more_count, + (int) config('photoalbums.load_more_image_limit', 10) + ), + ]; + } + + /** + * 一覧画面で表示するフォルダ/画像の初期表示情報を構築する。 + * + * @param \Illuminate\Support\Collection $photoalbum_contents + * @param mixed $load_more_use + * @param mixed $load_more_count + * @return array + */ + private function buildIndexDisplayItems(Collection $photoalbum_contents, $load_more_use, $load_more_count): array + { + $folder_items_all = $photoalbum_contents->where('is_folder', PhotoalbumContent::is_folder_on)->values(); + $image_items_all = $photoalbum_contents->where('is_folder', PhotoalbumContent::is_folder_off)->values(); + + if ($load_more_use == \App\Enums\UseType::use) { + $folder_limit = $this->getLoadMoreLimit('folder', $load_more_count); + $image_limit = $this->getLoadMoreLimit('image', $load_more_count); + } else { + $folder_limit = $folder_items_all->count(); + $image_limit = $image_items_all->count(); + } + + $folder_items = $folder_items_all->slice(0, $folder_limit)->values(); + $image_items = $image_items_all->slice(0, $image_limit)->values(); + + return [ + 'photoalbum_folder_items' => $folder_items, + 'photoalbum_image_items' => $image_items, + 'photoalbum_folder_total' => $folder_items_all->count(), + 'photoalbum_image_total' => $image_items_all->count(), + 'photoalbum_folder_limit' => $folder_limit, + 'photoalbum_image_limit' => $image_limit, + 'photoalbum_folder_offset' => $folder_items->count(), + 'photoalbum_image_offset' => $image_items->count(), + 'photoalbum_load_more_use' => $load_more_use, + ]; + } + + /** + * 表示対象フォルダに設定されたカバー画像一覧を取得する。 + * + * @param \Illuminate\Support\Collection $folder_items + * @param int $photoalbum_id + * @return \Illuminate\Support\Collection + */ + private function fetchFolderCovers(Collection $folder_items, int $photoalbum_id): Collection + { + $folder_ids = $folder_items->pluck('id'); + if ($folder_ids->isEmpty()) { + return collect(); + } + + return PhotoalbumContent::where('photoalbum_id', $photoalbum_id) + ->whereIn('parent_id', $folder_ids) + ->where('is_cover', PhotoalbumContent::is_cover_on) + ->with(['upload', 'posterUpload']) + ->get(); } /** @@ -369,106 +653,33 @@ private function isHiddenPhotoalbumContent(PhotoalbumContent $content, array $hi return false; } - /** - * 指定した親の子要素をフレーム設定に合わせて並び替えて取得する - */ - private function getSortedChildren(PhotoalbumContent $parent, ?string $sort_folder, ?string $sort_file, ?Collection $preloaded_children = null) - { - $children = is_null($preloaded_children) - ? $parent->children()->get() - : $preloaded_children->get($parent->id, collect()); - - // 設定画面などで事前に読み込んだ子要素一覧を再利用し、追加クエリを避ける - if (!is_null($preloaded_children) && $children->isEmpty()) { - return collect(); - } - - return $children->sort(function ($first, $second) use ($sort_folder, $sort_file) { - return $this->comparePhotoalbumContents($first, $second, $sort_folder, $sort_file); - })->values(); - } - - /** - * 並び替え比較処理 - */ - private function comparePhotoalbumContents(PhotoalbumContent $first, PhotoalbumContent $second, ?string $sort_folder, ?string $sort_file) - { - if ($first->is_folder == $second->is_folder) { - $sort_key = $first->is_folder == PhotoalbumContent::is_folder_on ? $sort_folder : $sort_file; - - switch ($sort_key) { - case PhotoalbumSort::name_desc: - return strnatcasecmp($second->displayName, $first->displayName); - case PhotoalbumSort::created_asc: - return $this->compareDates($first->created_at, $second->created_at); - case PhotoalbumSort::created_desc: - return $this->compareDates($second->created_at, $first->created_at); - case PhotoalbumSort::manual_order: - $sequence = $first->display_sequence <=> $second->display_sequence; - return $sequence !== 0 ? $sequence : $first->id <=> $second->id; - default: - return strnatcasecmp($first->displayName, $second->displayName); - } - } - - return $second->is_folder <=> $first->is_folder; - } - - /** - * 日付比較 - */ - private function compareDates($first, $second) - { - $first_timestamp = $this->convertToTimestamp($first); - $second_timestamp = $this->convertToTimestamp($second); - - return $first_timestamp <=> $second_timestamp; - } - - /** - * 日付をタイムスタンプへ変換する - */ - private function convertToTimestamp($value) - { - if (empty($value)) { - return 0; - } - - if ($value instanceof \Carbon\Carbon) { - return $value->timestamp; - } - - return strtotime((string) $value) ?: 0; - } - /** * 並び替え済みの子要素マップを作成する */ - private function buildSortedChildrenMap(PhotoalbumContent $root, ?string $sort_folder, ?string $sort_file, ?Collection $preloaded_children = null) + private function buildSortedChildrenMap(PhotoalbumContent $root, Collection $folder_children_map, Collection $image_children_map) { $map = []; - $this->appendSortedChildrenToMap($root, $sort_folder, $sort_file, $map, $preloaded_children); + $this->appendSortedChildrenToMap($root, $folder_children_map, $image_children_map, $map); return $map; } /** - * 事前取得済みデータを元に、各親IDの並び済み子リストをマップへ格納する + * 各親IDの並び済み子リストをマップへ格納する */ - private function appendSortedChildrenToMap(PhotoalbumContent $node, ?string $sort_folder, ?string $sort_file, array &$map, ?Collection $preloaded_children = null) + private function appendSortedChildrenToMap(PhotoalbumContent $node, Collection $folder_children_map, Collection $image_children_map, array &$map) { if (isset($map[$node->id])) { return; } - $children = $this->getSortedChildren($node, $sort_folder, $sort_file, $preloaded_children); - $map[$node->id] = $children; + $folder_children = $folder_children_map->get($node->id, collect())->values(); + $image_children = $image_children_map->get($node->id, collect())->values(); + $children = $folder_children->concat($image_children)->values(); - foreach ($children as $child) { - if ($child->is_folder == PhotoalbumContent::is_folder_off) { - continue; - } + $map[$node->id] = $children; - $this->appendSortedChildrenToMap($child, $sort_folder, $sort_file, $map, $preloaded_children); + foreach ($folder_children as $child) { + $this->appendSortedChildrenToMap($child, $folder_children_map, $image_children_map, $map); } } @@ -1747,16 +1958,20 @@ public function editView($request, $page_id, $frame_id) $sorted_children_map = []; $focus_open_ids = []; if (!empty($photoalbum->id)) { - $all_contents = PhotoalbumContent::with(['upload', 'posterUpload']) - ->where('photoalbum_id', $photoalbum->id) - ->get(); + // 表示設定画面はフォルダ・画像をそれぞれ1回ずつ取得し、親IDごとに再利用してN+1を防ぐ + $folder_contents = $this->getSortedPhotoalbumItemsByTarget($photoalbum->id, 'folder', $sort_folder, $sort_file); + $image_contents = $this->getSortedPhotoalbumItemsByTarget($photoalbum->id, 'image', $sort_folder, $sort_file); + $all_contents = $folder_contents->concat($image_contents)->values(); - $preview_root = $all_contents->firstWhere('parent_id', null); + $preview_root = $folder_contents->firstWhere('parent_id', null); if (!empty($preview_root)) { // プレビュー/編集の双方で使えるよう、親IDごとの並び済みリストを構築 - $grouped_children = $all_contents->groupBy('parent_id'); - $sorted_children_map = $this->buildSortedChildrenMap($preview_root, $sort_folder, $sort_file, $grouped_children); + $sorted_children_map = $this->buildSortedChildrenMap( + $preview_root, + $folder_contents->groupBy('parent_id'), + $image_contents->groupBy('parent_id') + ); } $focus_open_ids = $this->buildFocusOpenIds($all_contents, session('photoalbum_sort_focus')); @@ -1806,11 +2021,20 @@ private function buildFocusOpenIds($contents, $focus_content_id) public function saveView($request, $page_id, $frame_id, $photoalbum_id) { // 項目のエラーチェック + $max_load_more_limit = (int) config('photoalbums.load_more_max_limit', 100); + if ($max_load_more_limit < 1) { + $max_load_more_limit = 100; + } + $validator = Validator::make($request->all(), [ 'description_list_length' => ['nullable', 'integer', 'min:1'], + 'load_more_use_flag' => ['required', Rule::in(\App\Enums\UseType::getMemberKeys())], + 'load_more_count' => ['nullable', 'integer', 'min:1', 'max:' . $max_load_more_limit], ]); $validator->setAttributeNames([ 'description_list_length' => PhotoalbumFrameConfig::enum['description_list_length'], + 'load_more_use_flag' => PhotoalbumFrameConfig::enum['load_more_use_flag'], + 'load_more_count' => PhotoalbumFrameConfig::enum['load_more_count'], ]); // エラーがあった場合は入力画面に戻る。 @@ -1930,16 +2154,10 @@ private function respondViewSequenceError($request, string $message, int $status */ private function respondViewSequenceJson(PhotoalbumContent $content) { - $sort_folder = FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::sort_folder); - $sort_file = FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::sort_file); - - $siblings = PhotoalbumContent::where('parent_id', $content->parent_id) - ->with('upload') - ->get(['id', 'is_folder', 'display_sequence', 'created_at', 'name', 'upload_id']) - ->sort(function ($first, $second) use ($sort_folder, $sort_file) { - return $this->comparePhotoalbumContents($first, $second, $sort_folder, $sort_file); - }) - ->values(); + $parent = PhotoalbumContent::find($content->parent_id); + $siblings = empty($parent) + ? collect() + : $this->getSortedVisibleChildren($parent, []); return response()->json([ 'message' => '並び順を更新しました。', @@ -2004,6 +2222,218 @@ public function updateHiddenFolders($request, $page_id, $frame_id) ]); } + /** + * フォトアルバムの「もっと見る」用データを返す(JSON) + */ + public function moreContents($request, $page_id, $frame_id, $parent_id = null) + { + $target = $request->get('target'); + if (!$this->isValidMoreContentsTarget($target)) { + return response()->json(['message' => 'invalid target'], 400); + } + + $hidden_folder_ids = $this->getHiddenFolderIds($this->frame_configs); + $parent = $this->resolveMoreContentsParent($parent_id, $hidden_folder_ids); + if (empty($parent)) { + return response()->json(['message' => 'フォトアルバムが見つかりません。'], 404); + } + + $offset = max(0, (int) $request->get('offset', 0)); + $limit = $this->resolveMoreContentsLimit($target, $request->get('limit')); + list($slice, $total) = $this->getMoreContentsSlice($parent, $target, $hidden_folder_ids, $offset, $limit); + + if ($offset >= $total) { + return $this->makeMoreContentsResponse('', $total, $total); + } + + $html = $this->renderMoreContentsHtml($target, $slice, $parent->photoalbum_id); + $next_offset = min($offset + $slice->count(), $total); + + return $this->makeMoreContentsResponse($html, $next_offset, $total); + } + + /** + * もっと見る機能の対象値が有効か判定する。 + * + * @param mixed $target + * @return bool + */ + private function isValidMoreContentsTarget($target): bool + { + return in_array($target, ['folder', 'image'], true); + } + + /** + * もっと見る対象の親コンテンツを取得する。 + * + * @param int|null $parent_id + * @param array $hidden_folder_ids + * @return \App\Models\User\Photoalbums\PhotoalbumContent|null + */ + private function resolveMoreContentsParent($parent_id, array $hidden_folder_ids): ?PhotoalbumContent + { + $photoalbum = $this->getPluginBucket($this->frame->bucket_id); + $parent = $this->fetchPhotoalbumContent($parent_id, $photoalbum->id); + if (empty($parent) || $parent->photoalbum_id != $photoalbum->id) { + return null; + } + if ($this->isHiddenPhotoalbumContent($parent, $hidden_folder_ids)) { + return null; + } + return $parent; + } + + /** + * もっと見る対象のアイテム一覧(対象種別ごとの1ページ分)を取得する。 + * + * @param \App\Models\User\Photoalbums\PhotoalbumContent $parent + * @param string $target 対象種別(folder|image) + * @param array $hidden_folder_ids + * @param int $offset + * @param int $limit + * @return array [\Illuminate\Support\Collection $slice, int $total] + */ + private function getMoreContentsSlice(PhotoalbumContent $parent, string $target, array $hidden_folder_ids, int $offset, int $limit): array + { + $sort_config = $this->getPhotoalbumSortConfig(); + $sort_folder = $sort_config['sort_folder']; + $sort_file = $sort_config['sort_file']; + + $query = $this->getSortedVisibleItemsQueryByTarget($parent, $hidden_folder_ids, $target, $sort_folder, $sort_file); + $total = (clone $query)->count(); + if ($total === 0 || $offset >= $total) { + return [collect(), $total]; + } + + $slice = $query + ->offset($offset) + ->limit($limit) + ->get() + ->values(); + + return [$slice, $total]; + } + + /** + * もっと見る機能で使用する取得件数を決定する。 + * + * @param string $target 対象種別(folder|image) + * @param mixed $request_limit リクエスト上書き件数 + * @return int + */ + private function resolveMoreContentsLimit(string $target, $request_limit): int + { + $load_more_use = FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::load_more_use_flag, \App\Enums\UseType::not_use); + if ($load_more_use != \App\Enums\UseType::use) { + return $this->getLoadMoreLimit($target, $request_limit); + } + + $load_more_count = FrameConfig::getConfigValue( + $this->frame_configs, + PhotoalbumFrameConfig::load_more_count, + (int) config('photoalbums.load_more_image_limit', 10) + ); + + return $this->getLoadMoreLimit($target, $load_more_count); + } + + /** + * もっと見る機能のHTML断片を描画する。 + * + * @param string $target 対象種別(folder|image) + * @param \Illuminate\Support\Collection $slice + * @param int $photoalbum_id + * @return string + */ + private function renderMoreContentsHtml(string $target, Collection $slice, int $photoalbum_id): string + { + $download_check = $this->getDownloadCheck(); + if ($target === 'folder') { + $folder_ids = $slice->pluck('id'); + $covers = $folder_ids->isEmpty() + ? collect() + : PhotoalbumContent::where('photoalbum_id', $photoalbum_id) + ->whereIn('parent_id', $folder_ids) + ->where('is_cover', PhotoalbumContent::is_cover_on) + ->with(['upload', 'posterUpload']) + ->get(); + + return $this->view('index_folder_items', [ + 'photoalbum_folder_items' => $slice, + 'covers' => $covers, + 'download_check' => $download_check, + 'page' => $this->page, + ])->render(); + } + + return $this->view('index_image_items', [ + 'photoalbum_image_items' => $slice, + 'download_check' => $download_check, + 'page' => $this->page, + ])->render(); + } + + /** + * もっと見る機能のJSONレスポンスを生成する。 + * + * @param string $html + * @param int $next_offset + * @param int $total + * @return \Illuminate\Http\JsonResponse + */ + private function makeMoreContentsResponse(string $html, int $next_offset, int $total) + { + return response()->json([ + 'html' => $html, + 'next_offset' => $next_offset, + 'total' => $total, + ]); + } + + /** + * もっと見る機能の取得件数を返す。 + * + * @param string $target 対象種別(folder|image) + * @param mixed $override 上書き件数 + * @return int + */ + private function getLoadMoreLimit(string $target, $override = null): int + { + $folder_default = (int) config('photoalbums.load_more_folder_limit', 10); + if ($folder_default < 1) { + $folder_default = 10; + } + $image_default = (int) config('photoalbums.load_more_image_limit', 10); + if ($image_default < 1) { + $image_default = 10; + } + $max_limit = (int) config('photoalbums.load_more_max_limit', 100); + if ($max_limit < 1) { + $max_limit = 100; + } + + $default = $target === 'folder' ? $folder_default : $image_default; + $limit = is_numeric($override) ? (int) $override : $default; + if ($limit < 1) { + $limit = $default; + } + return min($limit, $max_limit); + } + + /** + * ダウンロード操作の表示可否を返す。 + * + * @return bool + */ + private function getDownloadCheck(): bool + { + if (Auth::user() && Auth::user()->can('posts.create', [[null, $this->frame->plugin_name, $this->buckets]])) { + return true; + } + + return (bool) FrameConfig::getConfigValue($this->frame_configs, PhotoalbumFrameConfig::download); + } + /** * フレーム設定を保存する。 * diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fb9e32fcc..ba7c4b96b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -532,7 +532,19 @@ private function getPostBucketsRoles($buckets) return false; } - // return $buckets->getBucketsRoles(); + $request = app(Request::class); + $bucket_id = $buckets->id ?? null; + // GETは読み取り専用のため、同一リクエスト内キャッシュでSQLを減らしても整合性に影響しない。 + // POST系は同一リクエスト内で権限やバケツ設定が更新される可能性があるためキャッシュしない。 + if ($bucket_id && $request->isMethod('get')) { + static $cached_post_roles = []; + if (array_key_exists($bucket_id, $cached_post_roles)) { + return $cached_post_roles[$bucket_id]; + } + $cached_post_roles[$bucket_id] = $buckets->getPostArrayBucketsRoles(); + return $cached_post_roles[$bucket_id]; + } + return $buckets->getPostArrayBucketsRoles(); // // Buckets にrole がない場合などで、Buckets のrole を使用しない場合はfalse を返す。 diff --git a/config/app.php b/config/app.php index 6fb8c685b..78ec27853 100644 --- a/config/app.php +++ b/config/app.php @@ -295,6 +295,7 @@ 'MenuFrameConfig' => \App\Enums\MenuFrameConfig::class, 'MailAuthMethod' => \App\Enums\MailAuthMethod::class, 'PhotoalbumPlayviewType' => \App\Enums\PhotoalbumPlayviewType::class, + 'SpamBlockType' => \App\Enums\SpamBlockType::class, // utils 'DateUtils' => \App\Utilities\Date\DateUtils::class, diff --git a/config/cc_plugin_defaults.php b/config/cc_plugin_defaults.php new file mode 100644 index 000000000..08ae419ff --- /dev/null +++ b/config/cc_plugin_defaults.php @@ -0,0 +1,8 @@ + [ + 'whatsnews' => 'onerow', + ], +]; diff --git a/config/connect.php b/config/connect.php index 7e551c0a3..e7bf1a847 100644 --- a/config/connect.php +++ b/config/connect.php @@ -64,6 +64,9 @@ // 設定メニューの折り畳みcol 'CC_SETTING_EXPAND_COL' => 6, + // レイアウトの初期値 + 'BASE_LAYOUT_DEFAULT' => '1|1|0|1', + // Cache-Control 'CACHE_CONTROL' => env('CACHE_CONTROL', 'max-age=604800'), diff --git a/config/photoalbums.php b/config/photoalbums.php new file mode 100644 index 000000000..6bef94641 --- /dev/null +++ b/config/photoalbums.php @@ -0,0 +1,14 @@ + 10, + + // 「もっと見る」初期表示件数(画像) + 'load_more_image_limit' => 10, + + // 「もっと見る」表示件数の最大値 + 'load_more_max_limit' => 100, + +]; diff --git a/config/version.php b/config/version.php index aa722b667..331f4a57d 100644 --- a/config/version.php +++ b/config/version.php @@ -12,7 +12,7 @@ | */ - 'cc_version' => '2.40.0', + 'cc_version' => '2.41.0', 'show_cc_version' => true, ]; diff --git a/database/factories/Common/SpamBlockHistoryFactory.php b/database/factories/Common/SpamBlockHistoryFactory.php new file mode 100644 index 000000000..85fde4426 --- /dev/null +++ b/database/factories/Common/SpamBlockHistoryFactory.php @@ -0,0 +1,96 @@ + null, + 'forms_id' => null, + 'block_type' => SpamBlockType::email, + 'block_value' => $this->faker->email, + 'client_ip' => $this->faker->ipv4, + 'submitted_email' => $this->faker->email, + ]; + } + + /** + * メールアドレス型のブロック履歴 + */ + public function emailType() + { + return $this->state(function (array $attributes) { + return [ + 'block_type' => SpamBlockType::email, + 'block_value' => $this->faker->email, + ]; + }); + } + + /** + * ドメイン型のブロック履歴 + */ + public function domainType() + { + return $this->state(function (array $attributes) { + return [ + 'block_type' => SpamBlockType::domain, + 'block_value' => $this->faker->domainName, + ]; + }); + } + + /** + * IPアドレス型のブロック履歴 + */ + public function ipAddressType() + { + return $this->state(function (array $attributes) { + return [ + 'block_type' => SpamBlockType::ip_address, + 'block_value' => $this->faker->ipv4, + ]; + }); + } + + /** + * 特定フォームに紐付け + */ + public function forForm($forms_id) + { + return $this->state(function (array $attributes) use ($forms_id) { + return [ + 'forms_id' => $forms_id, + ]; + }); + } + + /** + * 特定のスパムルールに紐付け + */ + public function withSpamList($spam_list_id) + { + return $this->state(function (array $attributes) use ($spam_list_id) { + return [ + 'spam_list_id' => $spam_list_id, + ]; + }); + } +} diff --git a/database/factories/Common/SpamListFactory.php b/database/factories/Common/SpamListFactory.php new file mode 100644 index 000000000..b75511ff8 --- /dev/null +++ b/database/factories/Common/SpamListFactory.php @@ -0,0 +1,95 @@ + 'forms', + 'target_id' => null, + 'block_type' => SpamBlockType::email, + 'block_value' => $this->faker->email, + 'memo' => $this->faker->sentence, + ]; + } + + /** + * グローバルスコープ(全体適用)のスパムリスト + */ + public function global() + { + return $this->state(function (array $attributes) { + return [ + 'target_id' => null, + ]; + }); + } + + /** + * 特定フォーム用のスパムリスト + */ + public function forForm($forms_id) + { + return $this->state(function (array $attributes) use ($forms_id) { + return [ + 'target_id' => $forms_id, + ]; + }); + } + + /** + * メールアドレス型のスパムリスト + */ + public function emailType() + { + return $this->state(function (array $attributes) { + return [ + 'block_type' => SpamBlockType::email, + 'block_value' => $this->faker->email, + ]; + }); + } + + /** + * ドメイン型のスパムリスト + */ + public function domainType() + { + return $this->state(function (array $attributes) { + return [ + 'block_type' => SpamBlockType::domain, + 'block_value' => $this->faker->domainName, + ]; + }); + } + + /** + * IPアドレス型のスパムリスト + */ + public function ipAddressType() + { + return $this->state(function (array $attributes) { + return [ + 'block_type' => SpamBlockType::ip_address, + 'block_value' => $this->faker->ipv4, + ]; + }); + } +} diff --git a/database/factories/User/Forms/FormsColumnsFactory.php b/database/factories/User/Forms/FormsColumnsFactory.php new file mode 100644 index 000000000..525ad8abf --- /dev/null +++ b/database/factories/User/Forms/FormsColumnsFactory.php @@ -0,0 +1,71 @@ + null, + 'column_type' => FormColumnType::text, + 'column_name' => $this->faker->word, + 'required' => 0, + 'frame_col' => 12, + 'caption' => null, + 'caption_color' => 'text-dark', + 'place_holder' => null, + 'minutes_increments' => 10, + 'minutes_increments_from' => 10, + 'minutes_increments_to' => 10, + 'rule_allowed_numeric' => 0, + 'rule_allowed_alpha_numeric' => 0, + 'rule_digits_or_less' => null, + 'rule_max' => null, + 'rule_min' => null, + 'rule_word_count' => null, + 'rule_date_after_equal' => null, + 'display_sequence' => 0, + ]; + } + + /** + * メールアドレス型のカラム + */ + public function emailType() + { + return $this->state(function (array $attributes) { + return [ + 'column_type' => FormColumnType::mail, + 'column_name' => 'メールアドレス', + ]; + }); + } + + /** + * テキスト型のカラム + */ + public function textType() + { + return $this->state(function (array $attributes) { + return [ + 'column_type' => FormColumnType::text, + ]; + }); + } +} diff --git a/database/factories/User/Forms/FormsFactory.php b/database/factories/User/Forms/FormsFactory.php new file mode 100644 index 000000000..13b6ae5ec --- /dev/null +++ b/database/factories/User/Forms/FormsFactory.php @@ -0,0 +1,64 @@ + null, + 'forms_name' => $this->faker->sentence(3), + 'form_mode' => 'form', + 'access_limit_type' => 0, + 'form_password' => null, + 'entry_limit' => null, + 'entry_limit_over_message' => null, + 'display_control_flag' => 0, + 'display_from' => null, + 'display_to' => null, + 'regist_control_flag' => 0, + 'regist_from' => null, + 'regist_to' => null, + 'can_view_inputs_moderator' => 0, + 'mail_send_flag' => 0, + 'mail_send_address' => null, + 'user_mail_send_flag' => 0, + 'mail_subject' => null, + 'mail_format' => null, + 'data_save_flag' => 1, + 'after_message' => null, + 'numbering_use_flag' => 0, + 'numbering_prefix' => null, + 'use_spam_filter_flag' => 0, + 'spam_filter_message' => null, + ]; + } + + /** + * スパムフィルタリングを有効にする + */ + public function withSpamFilter() + { + return $this->state(function (array $attributes) { + return [ + 'use_spam_filter_flag' => 1, + 'spam_filter_message' => $this->faker->sentence, + ]; + }); + } +} diff --git a/database/factories/User/Forms/FormsInputColsFactory.php b/database/factories/User/Forms/FormsInputColsFactory.php new file mode 100644 index 000000000..a1217e97b --- /dev/null +++ b/database/factories/User/Forms/FormsInputColsFactory.php @@ -0,0 +1,29 @@ + null, + 'forms_columns_id' => null, + 'value' => $this->faker->word, + ]; + } +} diff --git a/database/factories/User/Forms/FormsInputsFactory.php b/database/factories/User/Forms/FormsInputsFactory.php new file mode 100644 index 000000000..b8bb6b0ff --- /dev/null +++ b/database/factories/User/Forms/FormsInputsFactory.php @@ -0,0 +1,41 @@ + null, + 'status' => 0, // 本登録 + 'ip_address' => null, + ]; + } + + /** + * IPアドレスを記録 + */ + public function withIpAddress($ip_address = null) + { + return $this->state(function (array $attributes) use ($ip_address) { + return [ + 'ip_address' => $ip_address ?? $this->faker->ipv4, + ]; + }); + } +} diff --git a/database/migrations/2026_01_13_170722_add_base_layout_to_configs.php b/database/migrations/2026_01_13_170722_add_base_layout_to_configs.php new file mode 100644 index 000000000..2df7cc192 --- /dev/null +++ b/database/migrations/2026_01_13_170722_add_base_layout_to_configs.php @@ -0,0 +1,44 @@ +where('name', 'base_layout')->exists()) { + return; + } + + $layout_default = config('connect.BASE_LAYOUT_DEFAULT'); + $top_page_layout = DB::table('pages')->orderBy('_lft', 'asc')->value('layout'); + $layout = $top_page_layout ?: $layout_default; + + DB::table('configs')->insert([ + 'name' => 'base_layout', + 'value' => $layout, + 'category' => 'general', + 'additional1' => null, + 'additional2' => null, + 'additional3' => null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::table('configs')->where('name', 'base_layout')->delete(); + } +} diff --git a/database/migrations/2026_01_13_173712_add_layout_inherit_flag_to_pages_table.php b/database/migrations/2026_01_13_173712_add_layout_inherit_flag_to_pages_table.php new file mode 100644 index 000000000..075daafcb --- /dev/null +++ b/database/migrations/2026_01_13_173712_add_layout_inherit_flag_to_pages_table.php @@ -0,0 +1,44 @@ +tinyInteger('layout_inherit_flag')->default(1)->after('layout'); + }); + + DB::table('pages') + ->whereNull('layout_inherit_flag') + ->update(['layout_inherit_flag' => 1]); + + $top_page_id = DB::table('pages')->orderBy('_lft', 'asc')->value('id'); + if ($top_page_id) { + DB::table('pages') + ->where('id', $top_page_id) + ->update(['layout_inherit_flag' => 0]); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('pages', function (Blueprint $table) { + $table->dropColumn('layout_inherit_flag'); + }); + } +} diff --git a/database/migrations/2026_01_18_143634_create_spam_lists_table.php b/database/migrations/2026_01_18_143634_create_spam_lists_table.php new file mode 100644 index 000000000..cd3929939 --- /dev/null +++ b/database/migrations/2026_01_18_143634_create_spam_lists_table.php @@ -0,0 +1,45 @@ +increments('id'); + $table->string('target_plugin_name', 255)->comment('対象プラグイン名(forms等)'); + $table->integer('target_id')->nullable()->comment('対象ID(フォーム毎の場合はforms_id、全体の場合はnull)'); + $table->string('block_type', 50)->comment('ブロック種別: email, domain, ip_address'); + $table->string('block_value', 255)->comment('ブロック対象の値'); + $table->text('memo')->nullable()->comment('メモ'); + $table->integer('created_id')->nullable(); + $table->string('created_name', 255)->nullable(); + $table->timestamp('created_at')->nullable(); + $table->integer('updated_id')->nullable(); + $table->string('updated_name', 255)->nullable(); + $table->timestamp('updated_at')->nullable(); + $table->softDeletes(); + + $table->index(['target_plugin_name', 'target_id', 'block_type'], 'spam_lists_target_index'); + $table->index(['block_type', 'block_value'], 'spam_lists_block_index'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('spam_lists'); + } +} diff --git a/database/migrations/2026_01_18_143635_add_spam_filter_to_forms.php b/database/migrations/2026_01_18_143635_add_spam_filter_to_forms.php new file mode 100644 index 000000000..ed9af4903 --- /dev/null +++ b/database/migrations/2026_01_18_143635_add_spam_filter_to_forms.php @@ -0,0 +1,34 @@ +integer('use_spam_filter_flag')->default(0)->comment('スパムフィルタリング使用フラグ'); + $table->text('spam_filter_message')->nullable()->comment('スパムブロック時のメッセージ'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('forms', function (Blueprint $table) { + $table->dropColumn('use_spam_filter_flag'); + $table->dropColumn('spam_filter_message'); + }); + } +} diff --git a/database/migrations/2026_01_18_143636_add_ip_address_to_forms_inputs.php b/database/migrations/2026_01_18_143636_add_ip_address_to_forms_inputs.php new file mode 100644 index 000000000..acb787e2b --- /dev/null +++ b/database/migrations/2026_01_18_143636_add_ip_address_to_forms_inputs.php @@ -0,0 +1,32 @@ +string('ip_address', 255)->nullable()->comment('投稿者のIPアドレス'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('forms_inputs', function (Blueprint $table) { + $table->dropColumn('ip_address'); + }); + } +} diff --git a/database/migrations/2026_01_28_000000_create_spam_block_histories_table.php b/database/migrations/2026_01_28_000000_create_spam_block_histories_table.php new file mode 100644 index 000000000..35eb2d586 --- /dev/null +++ b/database/migrations/2026_01_28_000000_create_spam_block_histories_table.php @@ -0,0 +1,47 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 + */ +class CreateSpamBlockHistoriesTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('spam_block_histories', function (Blueprint $table) { + $table->increments('id'); + $table->integer('spam_list_id')->nullable(); + $table->integer('forms_id')->nullable(); + $table->string('block_type', 50); + $table->string('block_value', 255); + $table->string('client_ip', 45)->nullable(); + $table->string('submitted_email', 255)->nullable(); + $table->timestamp('created_at')->nullable(); + + $table->index('forms_id'); + $table->index('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('spam_block_histories'); + } +} diff --git a/database/migrations/2026_01_29_100001_make_block_value_nullable_in_spam_lists.php b/database/migrations/2026_01_29_100001_make_block_value_nullable_in_spam_lists.php new file mode 100644 index 000000000..6907d033f --- /dev/null +++ b/database/migrations/2026_01_29_100001_make_block_value_nullable_in_spam_lists.php @@ -0,0 +1,34 @@ +string('block_value', 255)->nullable()->comment('ブロック対象の値(ハニーポットの場合はnull)')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('spam_lists', function (Blueprint $table) { + $table->string('block_value', 255)->nullable(false)->comment('ブロック対象の値')->change(); + }); + } +} diff --git a/database/migrations/2026_02_12_000000_add_index_to_photoalbum_contents_for_more_contents.php b/database/migrations/2026_02_12_000000_add_index_to_photoalbum_contents_for_more_contents.php new file mode 100644 index 000000000..9950741fd --- /dev/null +++ b/database/migrations/2026_02_12_000000_add_index_to_photoalbum_contents_for_more_contents.php @@ -0,0 +1,32 @@ +index( + ['photoalbum_id', 'parent_id', 'is_folder'], + 'photoalbum_contents_photoalbum_parent_folder_idx' + ); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('photoalbum_contents', function (Blueprint $table) { + $table->dropIndex('photoalbum_contents_photoalbum_parent_folder_idx'); + }); + } +}; + diff --git a/database/seeders/DefaultConfigsTableSeeder.php b/database/seeders/DefaultConfigsTableSeeder.php index 1403e944e..78d36829a 100644 --- a/database/seeders/DefaultConfigsTableSeeder.php +++ b/database/seeders/DefaultConfigsTableSeeder.php @@ -32,6 +32,7 @@ private function getDefaultConfigs() 'base_touch_callout' => ['value' => '0'], 'base_header_login_link' => ['value' => '1'], 'base_theme' => ['value' => null], + 'base_layout' => ['value' => config('connect.BASE_LAYOUT_DEFAULT')], 'smartphone_menu_template' => ['value' => SmartphoneMenuTemplateType::opencurrenttree], 'base_login_password_reset' => ['value' => 1], 'use_mypage' => ['value' => 1], diff --git a/database/seeders/DefaultPagesTableSeeder.php b/database/seeders/DefaultPagesTableSeeder.php index 9920c0770..dce15482f 100644 --- a/database/seeders/DefaultPagesTableSeeder.php +++ b/database/seeders/DefaultPagesTableSeeder.php @@ -22,6 +22,7 @@ public function run() 'page_name' => 'home', 'permanent_link' => '/', 'base_display_flag' => '1', + 'layout_inherit_flag' => 0, 'depth' => 0, // bugfix: tree構造で _lft, _rgtが両方0は間違った値で、上から2番目のページを上に移動するとNode must exists.エラーになるため修正 // '_lft'=>'0', diff --git a/database/seeders/ElementaryschoolSeeder.php b/database/seeders/ElementaryschoolSeeder.php index aca1ef6ee..3e7cf0daf 100644 --- a/database/seeders/ElementaryschoolSeeder.php +++ b/database/seeders/ElementaryschoolSeeder.php @@ -683,6 +683,7 @@ private function insertGetPageId($row) $header_color = (isset($row['header_color'])) ? $row['header_color'] : NULL; $theme = (isset($row['theme'])) ? $row['theme'] : NULL; $layout = (isset($row['layout'])) ? $row['layout'] : NULL; + $layout_inherit_flag = (isset($row['layout_inherit_flag'])) ? $row['layout_inherit_flag'] : NULL; $base_display_flag = (isset($row['base_display_flag'])) ? $row['base_display_flag'] : 1;// 1:表示する $membership_flag = (isset($row['membership_flag'])) ? $row['membership_flag'] : 0; $ip_address = (isset($row['ip_address'])) ? $row['ip_address'] : NULL; @@ -694,6 +695,9 @@ private function insertGetPageId($row) $_lft = (isset($row['_lft'])) ? $row['_lft'] : NULL; $_rgt = (isset($row['_rgt'])) ? $row['_rgt'] : NULL; $parent_id = (isset($row['parent_id'])) ? $row['parent_id'] : NULL; + if (is_null($layout_inherit_flag)) { + $layout_inherit_flag = ($permanent_link === '/') ? 0 : 1; + } return DB::table('pages')->insertGetId([ 'page_name' => $page_name, 'permanent_link' => $permanent_link, @@ -701,6 +705,7 @@ private function insertGetPageId($row) 'header_color' => $header_color, 'theme' => $theme, 'layout' => $layout, + 'layout_inherit_flag' => $layout_inherit_flag, 'base_display_flag' => $base_display_flag, 'membership_flag' => $membership_flag, 'ip_address' => $ip_address, diff --git a/database/seeders/PTASeeder.php b/database/seeders/PTASeeder.php index 02238615a..ea6a632bd 100644 --- a/database/seeders/PTASeeder.php +++ b/database/seeders/PTASeeder.php @@ -843,6 +843,7 @@ private function insertGetPageId($row) $header_color = (isset($row['header_color'])) ? $row['header_color'] : NULL; $theme = (isset($row['theme'])) ? $row['theme'] : NULL; $layout = (isset($row['layout'])) ? $row['layout'] : NULL; + $layout_inherit_flag = (isset($row['layout_inherit_flag'])) ? $row['layout_inherit_flag'] : NULL; $base_display_flag = (isset($row['base_display_flag'])) ? $row['base_display_flag'] : 1;// 1:表示する $membership_flag = (isset($row['membership_flag'])) ? $row['membership_flag'] : 0; $ip_address = (isset($row['ip_address'])) ? $row['ip_address'] : NULL; @@ -854,6 +855,9 @@ private function insertGetPageId($row) $_lft = (isset($row['_lft'])) ? $row['_lft'] : NULL; $_rgt = (isset($row['_rgt'])) ? $row['_rgt'] : NULL; $parent_id = (isset($row['parent_id'])) ? $row['parent_id'] : NULL; + if (is_null($layout_inherit_flag)) { + $layout_inherit_flag = ($permanent_link === '/') ? 0 : 1; + } return DB::table('pages')->insertGetId([ 'page_name' => $page_name, 'permanent_link' => $permanent_link, @@ -861,6 +865,7 @@ private function insertGetPageId($row) 'header_color' => $header_color, 'theme' => $theme, 'layout' => $layout, + 'layout_inherit_flag' => $layout_inherit_flag, 'base_display_flag' => $base_display_flag, 'membership_flag' => $membership_flag, 'ip_address' => $ip_address, diff --git a/package-lock.json b/package-lock.json index 2579a0a56..d6a1f5aab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "dayjs": "^1.11.13", "jquery": "^3.7.0", "laravel-mix": "^6.0.49", - "lodash": "^4.17.19", + "lodash": "^4.17.23", "popper.js": "^1.16.1", "resolve-url-loader": "^5.0.0", "sass": "^1.62.1", diff --git a/package.json b/package.json index 6f5bc94c3..b37dda843 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dayjs": "^1.11.13", "jquery": "^3.7.0", "laravel-mix": "^6.0.49", - "lodash": "^4.17.19", + "lodash": "^4.17.23", "popper.js": "^1.16.1", "resolve-url-loader": "^5.0.0", "sass": "^1.62.1", diff --git a/public/css/app.css b/public/css/app.css index 13987e8d7..0e9bdd5af 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -27,4 +27,4 @@ * Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) * Copyright 2024 Fonticons, Inc. - */:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-display:block;font-family:Font Awesome\ 6 Brands;font-style:normal;font-weight:400;src:url(../fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.woff2?c210719e60948b211a1260f79812efe5) format("woff2"),url(../fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.ttf?1815e00441357e01619e5793e1caa78a) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero{--fa:"\f3d0"}.fa-hooli{--fa:"\f427"}.fa-yelp{--fa:"\f1e9"}.fa-cc-visa{--fa:"\f1f0"}.fa-lastfm{--fa:"\f202"}.fa-shopware{--fa:"\f5b5"}.fa-creative-commons-nc{--fa:"\f4e8"}.fa-aws{--fa:"\f375"}.fa-redhat{--fa:"\f7bc"}.fa-yoast{--fa:"\f2b1"}.fa-cloudflare{--fa:"\e07d"}.fa-ups{--fa:"\f7e0"}.fa-pixiv{--fa:"\e640"}.fa-wpexplorer{--fa:"\f2de"}.fa-dyalog{--fa:"\f399"}.fa-bity{--fa:"\f37a"}.fa-stackpath{--fa:"\f842"}.fa-buysellads{--fa:"\f20d"}.fa-first-order{--fa:"\f2b0"}.fa-modx{--fa:"\f285"}.fa-guilded{--fa:"\e07e"}.fa-vnv{--fa:"\f40b"}.fa-js-square,.fa-square-js{--fa:"\f3b9"}.fa-microsoft{--fa:"\f3ca"}.fa-qq{--fa:"\f1d6"}.fa-orcid{--fa:"\f8d2"}.fa-java{--fa:"\f4e4"}.fa-invision{--fa:"\f7b0"}.fa-creative-commons-pd-alt{--fa:"\f4ed"}.fa-centercode{--fa:"\f380"}.fa-glide-g{--fa:"\f2a6"}.fa-drupal{--fa:"\f1a9"}.fa-jxl{--fa:"\e67b"}.fa-dart-lang{--fa:"\e693"}.fa-hire-a-helper{--fa:"\f3b0"}.fa-creative-commons-by{--fa:"\f4e7"}.fa-unity{--fa:"\e049"}.fa-whmcs{--fa:"\f40d"}.fa-rocketchat{--fa:"\f3e8"}.fa-vk{--fa:"\f189"}.fa-untappd{--fa:"\f405"}.fa-mailchimp{--fa:"\f59e"}.fa-css3-alt{--fa:"\f38b"}.fa-reddit-square,.fa-square-reddit{--fa:"\f1a2"}.fa-vimeo-v{--fa:"\f27d"}.fa-contao{--fa:"\f26d"}.fa-square-font-awesome{--fa:"\e5ad"}.fa-deskpro{--fa:"\f38f"}.fa-brave{--fa:"\e63c"}.fa-sistrix{--fa:"\f3ee"}.fa-instagram-square,.fa-square-instagram{--fa:"\e055"}.fa-battle-net{--fa:"\f835"}.fa-the-red-yeti{--fa:"\f69d"}.fa-hacker-news-square,.fa-square-hacker-news{--fa:"\f3af"}.fa-edge{--fa:"\f282"}.fa-threads{--fa:"\e618"}.fa-napster{--fa:"\f3d2"}.fa-snapchat-square,.fa-square-snapchat{--fa:"\f2ad"}.fa-google-plus-g{--fa:"\f0d5"}.fa-artstation{--fa:"\f77a"}.fa-markdown{--fa:"\f60f"}.fa-sourcetree{--fa:"\f7d3"}.fa-google-plus{--fa:"\f2b3"}.fa-diaspora{--fa:"\f791"}.fa-foursquare{--fa:"\f180"}.fa-stack-overflow{--fa:"\f16c"}.fa-github-alt{--fa:"\f113"}.fa-phoenix-squadron{--fa:"\f511"}.fa-pagelines{--fa:"\f18c"}.fa-algolia{--fa:"\f36c"}.fa-red-river{--fa:"\f3e3"}.fa-creative-commons-sa{--fa:"\f4ef"}.fa-safari{--fa:"\f267"}.fa-google{--fa:"\f1a0"}.fa-font-awesome-alt,.fa-square-font-awesome-stroke{--fa:"\f35c"}.fa-atlassian{--fa:"\f77b"}.fa-linkedin-in{--fa:"\f0e1"}.fa-digital-ocean{--fa:"\f391"}.fa-nimblr{--fa:"\f5a8"}.fa-chromecast{--fa:"\f838"}.fa-evernote{--fa:"\f839"}.fa-hacker-news{--fa:"\f1d4"}.fa-creative-commons-sampling{--fa:"\f4f0"}.fa-adversal{--fa:"\f36a"}.fa-creative-commons{--fa:"\f25e"}.fa-watchman-monitoring{--fa:"\e087"}.fa-fonticons{--fa:"\f280"}.fa-weixin{--fa:"\f1d7"}.fa-shirtsinbulk{--fa:"\f214"}.fa-codepen{--fa:"\f1cb"}.fa-git-alt{--fa:"\f841"}.fa-lyft{--fa:"\f3c3"}.fa-rev{--fa:"\f5b2"}.fa-windows{--fa:"\f17a"}.fa-wizards-of-the-coast{--fa:"\f730"}.fa-square-viadeo,.fa-viadeo-square{--fa:"\f2aa"}.fa-meetup{--fa:"\f2e0"}.fa-centos{--fa:"\f789"}.fa-adn{--fa:"\f170"}.fa-cloudsmith{--fa:"\f384"}.fa-opensuse{--fa:"\e62b"}.fa-pied-piper-alt{--fa:"\f1a8"}.fa-dribbble-square,.fa-square-dribbble{--fa:"\f397"}.fa-codiepie{--fa:"\f284"}.fa-node{--fa:"\f419"}.fa-mix{--fa:"\f3cb"}.fa-steam{--fa:"\f1b6"}.fa-cc-apple-pay{--fa:"\f416"}.fa-scribd{--fa:"\f28a"}.fa-debian{--fa:"\e60b"}.fa-openid{--fa:"\f19b"}.fa-instalod{--fa:"\e081"}.fa-files-pinwheel{--fa:"\e69f"}.fa-expeditedssl{--fa:"\f23e"}.fa-sellcast{--fa:"\f2da"}.fa-square-twitter,.fa-twitter-square{--fa:"\f081"}.fa-r-project{--fa:"\f4f7"}.fa-delicious{--fa:"\f1a5"}.fa-freebsd{--fa:"\f3a4"}.fa-vuejs{--fa:"\f41f"}.fa-accusoft{--fa:"\f369"}.fa-ioxhost{--fa:"\f208"}.fa-fonticons-fi{--fa:"\f3a2"}.fa-app-store{--fa:"\f36f"}.fa-cc-mastercard{--fa:"\f1f1"}.fa-itunes-note{--fa:"\f3b5"}.fa-golang{--fa:"\e40f"}.fa-kickstarter,.fa-square-kickstarter{--fa:"\f3bb"}.fa-grav{--fa:"\f2d6"}.fa-weibo{--fa:"\f18a"}.fa-uncharted{--fa:"\e084"}.fa-firstdraft{--fa:"\f3a1"}.fa-square-youtube,.fa-youtube-square{--fa:"\f431"}.fa-wikipedia-w{--fa:"\f266"}.fa-rendact,.fa-wpressr{--fa:"\f3e4"}.fa-angellist{--fa:"\f209"}.fa-galactic-republic{--fa:"\f50c"}.fa-nfc-directional{--fa:"\e530"}.fa-skype{--fa:"\f17e"}.fa-joget{--fa:"\f3b7"}.fa-fedora{--fa:"\f798"}.fa-stripe-s{--fa:"\f42a"}.fa-meta{--fa:"\e49b"}.fa-laravel{--fa:"\f3bd"}.fa-hotjar{--fa:"\f3b1"}.fa-bluetooth-b{--fa:"\f294"}.fa-square-letterboxd{--fa:"\e62e"}.fa-sticker-mule{--fa:"\f3f7"}.fa-creative-commons-zero{--fa:"\f4f3"}.fa-hips{--fa:"\f452"}.fa-css{--fa:"\e6a2"}.fa-behance{--fa:"\f1b4"}.fa-reddit{--fa:"\f1a1"}.fa-discord{--fa:"\f392"}.fa-chrome{--fa:"\f268"}.fa-app-store-ios{--fa:"\f370"}.fa-cc-discover{--fa:"\f1f2"}.fa-wpbeginner{--fa:"\f297"}.fa-confluence{--fa:"\f78d"}.fa-shoelace{--fa:"\e60c"}.fa-mdb{--fa:"\f8ca"}.fa-dochub{--fa:"\f394"}.fa-accessible-icon{--fa:"\f368"}.fa-ebay{--fa:"\f4f4"}.fa-amazon{--fa:"\f270"}.fa-unsplash{--fa:"\e07c"}.fa-yarn{--fa:"\f7e3"}.fa-square-steam,.fa-steam-square{--fa:"\f1b7"}.fa-500px{--fa:"\f26e"}.fa-square-vimeo,.fa-vimeo-square{--fa:"\f194"}.fa-asymmetrik{--fa:"\f372"}.fa-font-awesome,.fa-font-awesome-flag,.fa-font-awesome-logo-full{--fa:"\f2b4"}.fa-gratipay{--fa:"\f184"}.fa-apple{--fa:"\f179"}.fa-hive{--fa:"\e07f"}.fa-gitkraken{--fa:"\f3a6"}.fa-keybase{--fa:"\f4f5"}.fa-apple-pay{--fa:"\f415"}.fa-padlet{--fa:"\e4a0"}.fa-amazon-pay{--fa:"\f42c"}.fa-github-square,.fa-square-github{--fa:"\f092"}.fa-stumbleupon{--fa:"\f1a4"}.fa-fedex{--fa:"\f797"}.fa-phoenix-framework{--fa:"\f3dc"}.fa-shopify{--fa:"\e057"}.fa-neos{--fa:"\f612"}.fa-square-threads{--fa:"\e619"}.fa-hackerrank{--fa:"\f5f7"}.fa-researchgate{--fa:"\f4f8"}.fa-swift{--fa:"\f8e1"}.fa-angular{--fa:"\f420"}.fa-speakap{--fa:"\f3f3"}.fa-angrycreative{--fa:"\f36e"}.fa-y-combinator{--fa:"\f23b"}.fa-empire{--fa:"\f1d1"}.fa-envira{--fa:"\f299"}.fa-google-scholar{--fa:"\e63b"}.fa-gitlab-square,.fa-square-gitlab{--fa:"\e5ae"}.fa-studiovinari{--fa:"\f3f8"}.fa-pied-piper{--fa:"\f2ae"}.fa-wordpress{--fa:"\f19a"}.fa-product-hunt{--fa:"\f288"}.fa-firefox{--fa:"\f269"}.fa-linode{--fa:"\f2b8"}.fa-goodreads{--fa:"\f3a8"}.fa-odnoklassniki-square,.fa-square-odnoklassniki{--fa:"\f264"}.fa-jsfiddle{--fa:"\f1cc"}.fa-sith{--fa:"\f512"}.fa-themeisle{--fa:"\f2b2"}.fa-page4{--fa:"\f3d7"}.fa-hashnode{--fa:"\e499"}.fa-react{--fa:"\f41b"}.fa-cc-paypal{--fa:"\f1f4"}.fa-squarespace{--fa:"\f5be"}.fa-cc-stripe{--fa:"\f1f5"}.fa-creative-commons-share{--fa:"\f4f2"}.fa-bitcoin{--fa:"\f379"}.fa-keycdn{--fa:"\f3ba"}.fa-opera{--fa:"\f26a"}.fa-itch-io{--fa:"\f83a"}.fa-umbraco{--fa:"\f8e8"}.fa-galactic-senate{--fa:"\f50d"}.fa-ubuntu{--fa:"\f7df"}.fa-draft2digital{--fa:"\f396"}.fa-stripe{--fa:"\f429"}.fa-houzz{--fa:"\f27c"}.fa-gg{--fa:"\f260"}.fa-dhl{--fa:"\f790"}.fa-pinterest-square,.fa-square-pinterest{--fa:"\f0d3"}.fa-xing{--fa:"\f168"}.fa-blackberry{--fa:"\f37b"}.fa-creative-commons-pd{--fa:"\f4ec"}.fa-playstation{--fa:"\f3df"}.fa-quinscape{--fa:"\f459"}.fa-less{--fa:"\f41d"}.fa-blogger-b{--fa:"\f37d"}.fa-opencart{--fa:"\f23d"}.fa-vine{--fa:"\f1ca"}.fa-signal-messenger{--fa:"\e663"}.fa-paypal{--fa:"\f1ed"}.fa-gitlab{--fa:"\f296"}.fa-typo3{--fa:"\f42b"}.fa-reddit-alien{--fa:"\f281"}.fa-yahoo{--fa:"\f19e"}.fa-dailymotion{--fa:"\e052"}.fa-affiliatetheme{--fa:"\f36b"}.fa-pied-piper-pp{--fa:"\f1a7"}.fa-bootstrap{--fa:"\f836"}.fa-odnoklassniki{--fa:"\f263"}.fa-nfc-symbol{--fa:"\e531"}.fa-mintbit{--fa:"\e62f"}.fa-ethereum{--fa:"\f42e"}.fa-speaker-deck{--fa:"\f83c"}.fa-creative-commons-nc-eu{--fa:"\f4e9"}.fa-patreon{--fa:"\f3d9"}.fa-avianex{--fa:"\f374"}.fa-ello{--fa:"\f5f1"}.fa-gofore{--fa:"\f3a7"}.fa-bimobject{--fa:"\f378"}.fa-brave-reverse{--fa:"\e63d"}.fa-facebook-f{--fa:"\f39e"}.fa-google-plus-square,.fa-square-google-plus{--fa:"\f0d4"}.fa-web-awesome{--fa:"\e682"}.fa-mandalorian{--fa:"\f50f"}.fa-first-order-alt{--fa:"\f50a"}.fa-osi{--fa:"\f41a"}.fa-google-wallet{--fa:"\f1ee"}.fa-d-and-d-beyond{--fa:"\f6ca"}.fa-periscope{--fa:"\f3da"}.fa-fulcrum{--fa:"\f50b"}.fa-cloudscale{--fa:"\f383"}.fa-forumbee{--fa:"\f211"}.fa-mizuni{--fa:"\f3cc"}.fa-schlix{--fa:"\f3ea"}.fa-square-xing,.fa-xing-square{--fa:"\f169"}.fa-bandcamp{--fa:"\f2d5"}.fa-wpforms{--fa:"\f298"}.fa-cloudversify{--fa:"\f385"}.fa-usps{--fa:"\f7e1"}.fa-megaport{--fa:"\f5a3"}.fa-magento{--fa:"\f3c4"}.fa-spotify{--fa:"\f1bc"}.fa-optin-monster{--fa:"\f23c"}.fa-fly{--fa:"\f417"}.fa-square-bluesky{--fa:"\e6a3"}.fa-aviato{--fa:"\f421"}.fa-itunes{--fa:"\f3b4"}.fa-cuttlefish{--fa:"\f38c"}.fa-blogger{--fa:"\f37c"}.fa-flickr{--fa:"\f16e"}.fa-viber{--fa:"\f409"}.fa-soundcloud{--fa:"\f1be"}.fa-digg{--fa:"\f1a6"}.fa-tencent-weibo{--fa:"\f1d5"}.fa-letterboxd{--fa:"\e62d"}.fa-symfony{--fa:"\f83d"}.fa-maxcdn{--fa:"\f136"}.fa-etsy{--fa:"\f2d7"}.fa-facebook-messenger{--fa:"\f39f"}.fa-audible{--fa:"\f373"}.fa-think-peaks{--fa:"\f731"}.fa-bilibili{--fa:"\e3d9"}.fa-erlang{--fa:"\f39d"}.fa-x-twitter{--fa:"\e61b"}.fa-cotton-bureau{--fa:"\f89e"}.fa-dashcube{--fa:"\f210"}.fa-42-group,.fa-innosoft{--fa:"\e080"}.fa-stack-exchange{--fa:"\f18d"}.fa-elementor{--fa:"\f430"}.fa-pied-piper-square,.fa-square-pied-piper{--fa:"\e01e"}.fa-creative-commons-nd{--fa:"\f4eb"}.fa-palfed{--fa:"\f3d8"}.fa-superpowers{--fa:"\f2dd"}.fa-resolving{--fa:"\f3e7"}.fa-xbox{--fa:"\f412"}.fa-square-web-awesome-stroke{--fa:"\e684"}.fa-searchengin{--fa:"\f3eb"}.fa-tiktok{--fa:"\e07b"}.fa-facebook-square,.fa-square-facebook{--fa:"\f082"}.fa-renren{--fa:"\f18b"}.fa-linux{--fa:"\f17c"}.fa-glide{--fa:"\f2a5"}.fa-linkedin{--fa:"\f08c"}.fa-hubspot{--fa:"\f3b2"}.fa-deploydog{--fa:"\f38e"}.fa-twitch{--fa:"\f1e8"}.fa-flutter{--fa:"\e694"}.fa-ravelry{--fa:"\f2d9"}.fa-mixer{--fa:"\e056"}.fa-lastfm-square,.fa-square-lastfm{--fa:"\f203"}.fa-vimeo{--fa:"\f40a"}.fa-mendeley{--fa:"\f7b3"}.fa-uniregistry{--fa:"\f404"}.fa-figma{--fa:"\f799"}.fa-creative-commons-remix{--fa:"\f4ee"}.fa-cc-amazon-pay{--fa:"\f42d"}.fa-dropbox{--fa:"\f16b"}.fa-instagram{--fa:"\f16d"}.fa-cmplid{--fa:"\e360"}.fa-upwork{--fa:"\e641"}.fa-facebook{--fa:"\f09a"}.fa-gripfire{--fa:"\f3ac"}.fa-jedi-order{--fa:"\f50e"}.fa-uikit{--fa:"\f403"}.fa-fort-awesome-alt{--fa:"\f3a3"}.fa-phabricator{--fa:"\f3db"}.fa-ussunnah{--fa:"\f407"}.fa-earlybirds{--fa:"\f39a"}.fa-trade-federation{--fa:"\f513"}.fa-autoprefixer{--fa:"\f41c"}.fa-whatsapp{--fa:"\f232"}.fa-square-upwork{--fa:"\e67c"}.fa-slideshare{--fa:"\f1e7"}.fa-google-play{--fa:"\f3ab"}.fa-viadeo{--fa:"\f2a9"}.fa-line{--fa:"\f3c0"}.fa-google-drive{--fa:"\f3aa"}.fa-servicestack{--fa:"\f3ec"}.fa-simplybuilt{--fa:"\f215"}.fa-bitbucket{--fa:"\f171"}.fa-imdb{--fa:"\f2d8"}.fa-deezer{--fa:"\e077"}.fa-raspberry-pi{--fa:"\f7bb"}.fa-jira{--fa:"\f7b1"}.fa-docker{--fa:"\f395"}.fa-screenpal{--fa:"\e570"}.fa-bluetooth{--fa:"\f293"}.fa-gitter{--fa:"\f426"}.fa-d-and-d{--fa:"\f38d"}.fa-microblog{--fa:"\e01a"}.fa-cc-diners-club{--fa:"\f24c"}.fa-gg-circle{--fa:"\f261"}.fa-pied-piper-hat{--fa:"\f4e5"}.fa-kickstarter-k{--fa:"\f3bc"}.fa-yandex{--fa:"\f413"}.fa-readme{--fa:"\f4d5"}.fa-html5{--fa:"\f13b"}.fa-sellsy{--fa:"\f213"}.fa-square-web-awesome{--fa:"\e683"}.fa-sass{--fa:"\f41e"}.fa-wirsindhandwerk,.fa-wsh{--fa:"\e2d0"}.fa-buromobelexperte{--fa:"\f37f"}.fa-salesforce{--fa:"\f83b"}.fa-octopus-deploy{--fa:"\e082"}.fa-medapps{--fa:"\f3c6"}.fa-ns8{--fa:"\f3d5"}.fa-pinterest-p{--fa:"\f231"}.fa-apper{--fa:"\f371"}.fa-fort-awesome{--fa:"\f286"}.fa-waze{--fa:"\f83f"}.fa-bluesky{--fa:"\e671"}.fa-cc-jcb{--fa:"\f24b"}.fa-snapchat,.fa-snapchat-ghost{--fa:"\f2ab"}.fa-fantasy-flight-games{--fa:"\f6dc"}.fa-rust{--fa:"\e07a"}.fa-wix{--fa:"\f5cf"}.fa-behance-square,.fa-square-behance{--fa:"\f1b5"}.fa-supple{--fa:"\f3f9"}.fa-webflow{--fa:"\e65c"}.fa-rebel{--fa:"\f1d0"}.fa-css3{--fa:"\f13c"}.fa-staylinked{--fa:"\f3f5"}.fa-kaggle{--fa:"\f5fa"}.fa-space-awesome{--fa:"\e5ac"}.fa-deviantart{--fa:"\f1bd"}.fa-cpanel{--fa:"\f388"}.fa-goodreads-g{--fa:"\f3a9"}.fa-git-square,.fa-square-git{--fa:"\f1d2"}.fa-square-tumblr,.fa-tumblr-square{--fa:"\f174"}.fa-trello{--fa:"\f181"}.fa-creative-commons-nc-jp{--fa:"\f4ea"}.fa-get-pocket{--fa:"\f265"}.fa-perbyte{--fa:"\e083"}.fa-grunt{--fa:"\f3ad"}.fa-weebly{--fa:"\f5cc"}.fa-connectdevelop{--fa:"\f20e"}.fa-leanpub{--fa:"\f212"}.fa-black-tie{--fa:"\f27e"}.fa-themeco{--fa:"\f5c6"}.fa-python{--fa:"\f3e2"}.fa-android{--fa:"\f17b"}.fa-bots{--fa:"\e340"}.fa-free-code-camp{--fa:"\f2c5"}.fa-hornbill{--fa:"\f592"}.fa-js{--fa:"\f3b8"}.fa-ideal{--fa:"\e013"}.fa-git{--fa:"\f1d3"}.fa-dev{--fa:"\f6cc"}.fa-sketch{--fa:"\f7c6"}.fa-yandex-international{--fa:"\f414"}.fa-cc-amex{--fa:"\f1f3"}.fa-uber{--fa:"\f402"}.fa-github{--fa:"\f09b"}.fa-php{--fa:"\f457"}.fa-alipay{--fa:"\f642"}.fa-youtube{--fa:"\f167"}.fa-skyatlas{--fa:"\f216"}.fa-firefox-browser{--fa:"\e007"}.fa-replyd{--fa:"\f3e6"}.fa-suse{--fa:"\f7d6"}.fa-jenkins{--fa:"\f3b6"}.fa-twitter{--fa:"\f099"}.fa-rockrms{--fa:"\f3e9"}.fa-pinterest{--fa:"\f0d2"}.fa-buffer{--fa:"\f837"}.fa-npm{--fa:"\f3d4"}.fa-yammer{--fa:"\f840"}.fa-btc{--fa:"\f15a"}.fa-dribbble{--fa:"\f17d"}.fa-stumbleupon-circle{--fa:"\f1a3"}.fa-internet-explorer{--fa:"\f26b"}.fa-stubber{--fa:"\e5c7"}.fa-telegram,.fa-telegram-plane{--fa:"\f2c6"}.fa-old-republic{--fa:"\f510"}.fa-odysee{--fa:"\e5c6"}.fa-square-whatsapp,.fa-whatsapp-square{--fa:"\f40c"}.fa-node-js{--fa:"\f3d3"}.fa-edge-legacy{--fa:"\e078"}.fa-slack,.fa-slack-hash{--fa:"\f198"}.fa-medrt{--fa:"\f3c8"}.fa-usb{--fa:"\f287"}.fa-tumblr{--fa:"\f173"}.fa-vaadin{--fa:"\f408"}.fa-quora{--fa:"\f2c4"}.fa-square-x-twitter{--fa:"\e61a"}.fa-reacteurope{--fa:"\f75d"}.fa-medium,.fa-medium-m{--fa:"\f23a"}.fa-amilia{--fa:"\f36d"}.fa-mixcloud{--fa:"\f289"}.fa-flipboard{--fa:"\f44d"}.fa-viacoin{--fa:"\f237"}.fa-critical-role{--fa:"\f6c9"}.fa-sitrox{--fa:"\e44a"}.fa-discourse{--fa:"\f393"}.fa-joomla{--fa:"\f1aa"}.fa-mastodon{--fa:"\f4f6"}.fa-airbnb{--fa:"\f834"}.fa-wolf-pack-battalion{--fa:"\f514"}.fa-buy-n-large{--fa:"\f8a6"}.fa-gulp{--fa:"\f3ae"}.fa-creative-commons-sampling-plus{--fa:"\f4f1"}.fa-strava{--fa:"\f428"}.fa-ember{--fa:"\f423"}.fa-canadian-maple-leaf{--fa:"\f785"}.fa-teamspeak{--fa:"\f4f9"}.fa-pushed{--fa:"\f3e1"}.fa-wordpress-simple{--fa:"\f411"}.fa-nutritionix{--fa:"\f3d6"}.fa-wodu{--fa:"\e088"}.fa-google-pay{--fa:"\e079"}.fa-intercom{--fa:"\f7af"}.fa-zhihu{--fa:"\f63f"}.fa-korvue{--fa:"\f42f"}.fa-pix{--fa:"\e43a"}.fa-steam-symbol{--fa:"\f3f6"}:root{--td-light:#fff;--td-widget-background:#fff;--td-font-color:#000;--td-timepicker-font-size:1.2em;--td-active-bg:#0d6efd;--td-range-bg:#01419e;--td-active-color:#fff;--td-active-border-color:#fff;--td-border-radius:999px;--td-btn-hover-bg:#e9ecef;--td-disabled-color:#6c757d;--td-alternate-color:rgba(0,0,0,.38);--td-secondary-border-color:#ccc;--td-secondary-border-color-rgba:rgba(0,0,0,.2);--td-primary-border-color:#fff;--td-text-shadow:0 -1px 0 rgba(0,0,0,.25);--td-dow-color:rgba(0,0,0,.5);--td-dark:#1b1b1b;--td-dark-widget-background:#1b1b1b;--td-dark-font-color:#e3e3e3;--td-dark-active-bg:#4db2ff;--td-dark-range-bg:#0071c7;--td-dark-active-color:#fff;--td-dark-active-border-color:#1b1b1b;--td-dark-btn-hover-bg:#232627;--td-dark-disabled-color:#6c757d;--td-dark-alternate-color:hsla(36,10%,90%,.38);--td-dark-secondary-border-color:#ccc;--td-dark-secondary-border-color-rgba:hsla(36,10%,90%,.2);--td-dark-primary-border-color:#1b1b1b;--td-dark-text-shadow:0 -1px 0 hsla(36,10%,90%,.25);--td-dark-dow-color:hsla(36,10%,90%,.5);--td-widget-z-index:9999}.tempus-dominus-widget [data-action]:after,.visually-hidden{clip:rect(0,0,0,0)!important;border:0!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:1px!important}.tempus-dominus-widget{border-radius:4px;box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12);display:none;list-style:none;padding:4px;width:19rem;z-index:var(--td-widget-z-index)}.tempus-dominus-widget :focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.25);outline:0}.tempus-dominus-widget.calendarWeeks{width:21rem}.tempus-dominus-widget.calendarWeeks .date-container-days{grid-auto-columns:12.5%;grid-template-areas:"a a a a a a a a"}.tempus-dominus-widget [data-action]{cursor:pointer}.tempus-dominus-widget [data-action]:after{content:attr(title)}.tempus-dominus-widget [data-action].disabled,.tempus-dominus-widget [data-action].disabled:hover{background:none;cursor:not-allowed}.tempus-dominus-widget .arrow{display:none}.tempus-dominus-widget.show{display:block}.tempus-dominus-widget.show.date-container{min-height:315px}.tempus-dominus-widget.show.time-container{min-height:217px}.tempus-dominus-widget .td-collapse:not(.show){display:none}.tempus-dominus-widget .td-collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (min-width:576px){.tempus-dominus-widget.timepicker-sbs{width:38em}}@media (min-width:768px){.tempus-dominus-widget.timepicker-sbs{width:38em}}@media (min-width:992px){.tempus-dominus-widget.timepicker-sbs{width:38em}}.tempus-dominus-widget.timepicker-sbs .td-row{display:flex}.tempus-dominus-widget.timepicker-sbs .td-row .td-half{flex:0 0 auto;width:50%}.tempus-dominus-widget div[data-action]:active{box-shadow:none}.tempus-dominus-widget .timepicker-hour,.tempus-dominus-widget .timepicker-minute,.tempus-dominus-widget .timepicker-second{font-size:1.2em;font-weight:700;margin:0;width:54px}.tempus-dominus-widget button[data-action]{padding:6px}.tempus-dominus-widget .toggleMeridiem{height:38px;text-align:center}.tempus-dominus-widget .calendar-header{display:grid;font-weight:700;grid-template-areas:"a a a";margin-bottom:10px}.tempus-dominus-widget .calendar-header .next{padding-right:10px;text-align:right}.tempus-dominus-widget .calendar-header .previous{padding-left:10px;text-align:left}.tempus-dominus-widget .calendar-header .picker-switch{text-align:center}.tempus-dominus-widget .toolbar{display:grid;grid-auto-flow:column;grid-auto-rows:40px}.tempus-dominus-widget .toolbar div{align-items:center;border-radius:var(--td-border-radius);box-sizing:border-box;display:flex;justify-content:center}.tempus-dominus-widget .date-container-days{display:grid;grid-auto-columns:14.2857142857%;grid-auto-rows:40px;grid-template-areas:"a a a a a a a"}.tempus-dominus-widget .date-container-days .range-in{background-color:var(--td-range-bg)!important;border:none;border-radius:0!important;box-shadow:-5px 0 0 var(--td-range-bg),5px 0 0 var(--td-range-bg)}.tempus-dominus-widget .date-container-days .range-end{border-radius:0 50px 50px 0!important}.tempus-dominus-widget .date-container-days .range-start{border-radius:50px 0 0 50px!important}.tempus-dominus-widget .date-container-days .dow{align-items:center;justify-content:center;text-align:center}.tempus-dominus-widget .date-container-days .cw{align-items:center;cursor:default;display:flex;font-size:.8em;height:90%;justify-content:center;line-height:20px;width:90%}.tempus-dominus-widget .date-container-decades,.tempus-dominus-widget .date-container-months,.tempus-dominus-widget .date-container-years{display:grid;grid-auto-rows:calc(2.71429rem - 1.14286px);grid-template-areas:"a a a"}.tempus-dominus-widget .time-container-hour,.tempus-dominus-widget .time-container-minute,.tempus-dominus-widget .time-container-second{display:grid;grid-auto-rows:calc(2.71429rem - 1.14286px);grid-template-areas:"a a a a"}.tempus-dominus-widget .time-container-clock{display:grid;grid-auto-rows:calc(2.71429rem - 1.14286px)}.tempus-dominus-widget .time-container-clock .no-highlight{align-items:center;display:flex;height:90%;justify-content:center;width:90%}.tempus-dominus-widget .date-container-days div:not(.no-highlight),.tempus-dominus-widget .date-container-decades div:not(.no-highlight),.tempus-dominus-widget .date-container-months div:not(.no-highlight),.tempus-dominus-widget .date-container-years div:not(.no-highlight),.tempus-dominus-widget .time-container-clock div:not(.no-highlight),.tempus-dominus-widget .time-container-hour div:not(.no-highlight),.tempus-dominus-widget .time-container-minute div:not(.no-highlight),.tempus-dominus-widget .time-container-second div:not(.no-highlight){align-items:center;border-radius:var(--td-border-radius);box-sizing:border-box;display:flex;height:90%;justify-content:center;width:90%}.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled:hover{background:none;cursor:not-allowed}.tempus-dominus-widget .date-container-days div:not(.no-highlight).today,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today,.tempus-dominus-widget .date-container-months div:not(.no-highlight).today,.tempus-dominus-widget .date-container-years div:not(.no-highlight).today,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today,.tempus-dominus-widget .time-container-second div:not(.no-highlight).today{position:relative}.tempus-dominus-widget .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-second div:not(.no-highlight).today:before{border:solid transparent;border-width:0 0 7px 7px;bottom:6px;content:"";display:inline-block;position:absolute;right:6px}.tempus-dominus-widget .time-container{margin-bottom:.5rem}.tempus-dominus-widget button{border-radius:.25rem;cursor:pointer;display:inline-block;font-size:1rem;font-weight:400;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle}.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod],.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second{cursor:default;pointer-events:none}.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second:hover{background:none}.tempus-dominus-widget.light{background-color:var(--td-widget-background);color:var(--td-font-color)}.tempus-dominus-widget.light [data-action].disabled,.tempus-dominus-widget.light [data-action].disabled:hover{color:var(--td-disabled-color)}.tempus-dominus-widget.light .toolbar div:hover{background:var(--td-btn-hover-bg)}.tempus-dominus-widget.light .date-container-days .dow{color:var(--td-dow-color)}.tempus-dominus-widget.light .date-container-days .cw{color:var(--td-alternate-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight):hover{background:var(--td-btn-hover-bg)}.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight),.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight),.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight),.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active{background-color:var(--td-active-bg);color:var(--td-active-color);text-shadow:var(--td-text-shadow)}.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.old{color:var(--td-active-color)}.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.today:before{border-bottom-color:var(--td-active-border-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).old{color:var(--td-alternate-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled:hover{color:var(--td-disabled-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).today:before{border-bottom-color:var(--td-active-bg);border-top-color:var(--td-secondary-border-color-rgba)}.tempus-dominus-widget.light button{background-color:var(--td-active-bg);border-color:var(--td-active-bg);color:var(--td-active-color)}.tempus-dominus-widget.dark{background-color:var(--td-dark-widget-background);color:var(--td-dark-font-color)}.tempus-dominus-widget.dark [data-action].disabled,.tempus-dominus-widget.dark [data-action].disabled:hover{color:var(--td-dark-disabled-color)}.tempus-dominus-widget.dark .toolbar div:hover{background:var(--td-dark-btn-hover-bg)}.tempus-dominus-widget.dark .date-container-days .dow{color:var(--td-dark-dow-color)}.tempus-dominus-widget.dark .date-container-days .range-in{background-color:var(--td-dark-range-bg)!important;box-shadow:-5px 0 0 var(--td-dark-range-bg),5px 0 0 var(--td-dark-range-bg)}.tempus-dominus-widget.dark .date-container-days .cw{color:var(--td-dark-alternate-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight):hover{background:var(--td-dark-btn-hover-bg)}.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight),.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight),.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight),.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active{background-color:var(--td-dark-active-bg);color:var(--td-dark-active-color);text-shadow:var(--td-dark-text-shadow)}.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.old{color:var(--td-dark-active-color)}.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.today:before{border-bottom-color:var(--td-dark-active-border-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).old{color:var(--td-dark-alternate-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled:hover{color:var(--td-dark-disabled-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).today:before{border-bottom-color:var(--td-dark-active-bg);border-top-color:var(--td-dark-secondary-border-color-rgba)}.tempus-dominus-widget.dark button{background-color:var(--td-dark-active-bg);border-color:var(--td-dark-active-bg);color:var(--td-dark-active-color)}#ccHeaderArea.with-border{min-height:2.2em;position:relative}#ccHeaderArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccHeaderArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"ヘッダー";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccFooterArea.with-border{min-height:2.2em;position:relative}#ccFooterArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccFooterArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"フッター";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccMainArea.with-border{min-height:2.2em;position:relative}#ccMainArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccMainArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"メイン";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccLeftArea.with-border{min-height:2.2em;position:relative}#ccLeftArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccLeftArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"左";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccRightArea.with-border{min-height:2.2em;position:relative}#ccRightArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccRightArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"右";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#cc-tui-actual-editor .tui-image-editor-header-buttons,#cc-tui-actual-editor .tui-image-editor-header-logo{display:none!important}.mce-content-body{margin:revert}.mce-text{color:#000!important}.mce-active .mce-text,.mce-text{background-color:transparent!important}.mce-active{background-color:#ededee!important}.embed-responsive span.mce-preview-object{position:static}.cc-column{background-color:#f2f2f2;border-radius:7px;margin:15px 0;padding:25px}.cc-exclamation{padding-left:1.1rem;text-indent:-1.2rem}.cc-exclamation:before{background:url(../images/acc_exclamation.png?337f20c894645de99ffc0ea9f1cfea67) no-repeat;background-size:contain;content:"";display:inline-block;height:1rem;margin:0 5px 3px 0;vertical-align:middle;width:1rem}.cc-note{padding-left:.8rem;text-indent:-.8rem}.cc-note:before{background:url(../images/acc_note.png?991226f05685a2d9d7f75a5348359153) no-repeat;background-size:contain;content:"";display:inline-block;height:.8rem;margin:0 3px 2px 0;vertical-align:middle;width:.8rem}.cc-note+.cc-note{margin-top:-10px}a.cc-icon-pdf:after{background:url(../images/icon_pdf.png?7cfbb334a509ddf1dc22044fa7557f4d) no-repeat;background-size:contain}a.cc-icon-excel:after,a.cc-icon-pdf:after{content:"";display:inline-block;height:1.25rem;margin:0 0 3px 5px;vertical-align:middle;width:1.25rem}a.cc-icon-excel:after{background:url(../images/icon_excel.png?37ecf7f67956f42395945014cddae393) no-repeat;background-size:contain}a.cc-icon-word:after{background:url(../images/icon_word.png?623f8b7fc1b3e9c8ff69f33176f28133) no-repeat;background-size:contain;height:1.25rem;width:1.25rem}a.cc-icon-external:after,a.cc-icon-word:after{content:"";display:inline-block;margin:0 0 3px 5px;vertical-align:middle}a.cc-icon-external:after{background:url(../images/icon_external.png?a6c57082171ba4ca205358df7ddda13f) no-repeat;background-size:contain;height:1rem;width:1rem}.cc-menu-select-locked{opacity:.6}.cc-menu-select-locked #page-select-list{background-color:#f1f3f5;border:1px solid #e2e3e5;border-radius:.25rem;padding:.5rem .75rem}.cc-menu-select-locked .custom-control-input,.cc-menu-select-locked .custom-control-label{cursor:not-allowed}.cc-menu-page-conditions i{margin-right:.25rem}.cc-menu-page-conditions i:last-child{margin-right:0}.photoalbum-manual-sort__thumb{align-items:center;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;display:inline-flex;height:64px;justify-content:center;overflow:hidden;width:64px}.photoalbum-manual-sort__thumb-image{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.photoalbum-manual-sort__thumb--video{background-color:#f8f9fa;color:#6c757d}.photoalbum-manual-sort__thumb--video i{font-size:24px}.photoalbum-manual-sort__toggle{align-items:center;color:inherit;display:inline-flex;text-decoration:none}.photoalbum-manual-sort__toggle:focus,.photoalbum-manual-sort__toggle:hover{text-decoration:none}.photoalbum-manual-sort__toggle-icon{transition:transform .2s ease}.photoalbum-manual-sort__toggle[aria-expanded=true] .photoalbum-manual-sort__toggle-icon{transform:rotate(90deg)}.photoalbum-manual-sort__item{scroll-margin-top:120px}.photoalbum-manual-sort__item--hidden{background-color:#f8f9fa}.photoalbum-manual-sort__item--pending-hidden{background-color:#e9ecef;background-image:repeating-linear-gradient(135deg,#e9ecef,#e9ecef 6px,#dee2e6 0,#dee2e6 12px)}.photoalbum-manual-sort__item--pending-show{background-color:#ffe083;background-image:repeating-linear-gradient(135deg,#ffe083,#ffe083 6px,#ffe69c 0,#ffe69c 12px)}.photoalbum-manual-sort__item--active{animation:photoalbum-highlight 1.5s ease}@keyframes photoalbum-highlight{0%{background-color:#fff3cd}to{background-color:transparent}}.photoalbum-preview__card{border-color:#17a2b8}.photoalbum-preview__card .card-header{background-color:#e8f6f8;color:#128293}.photoalbum-manual-sort__badge{background-color:#17a2b8;border-radius:.25rem;color:#fff;font-size:.75rem;padding:.2rem .45rem}.photoalbum-visibility-toggle{align-items:center;display:inline-flex;position:relative}.photoalbum-visibility-toggle__input{height:1px;opacity:0;position:absolute;width:1px}.photoalbum-visibility-toggle__label{align-items:center;border:1px solid transparent;border-radius:.25rem;color:#6c757d;cursor:pointer;display:inline-flex;height:1.75rem;justify-content:center;margin-bottom:0;min-width:2rem;padding:.15rem .35rem;transition:background-color .15s ease,border-color .15s ease,box-shadow .15s ease,color .15s ease}.photoalbum-visibility-toggle__icon{font-size:.95rem;line-height:1}.photoalbum-visibility-toggle__icon--off{display:none}.photoalbum-visibility-toggle__label:hover{background-color:#f8f9fa;border-color:#dee2e6;box-shadow:0 1px 2px rgba(0,0,0,.08);color:#343a40}.photoalbum-visibility-toggle__input:focus-visible+.photoalbum-visibility-toggle__label{background-color:#f8f9fa;border-color:#ced4da;box-shadow:0 0 0 .15rem rgba(23,162,184,.25);color:#343a40}.photoalbum-visibility-toggle__input:disabled+.photoalbum-visibility-toggle__label{box-shadow:none;cursor:not-allowed;opacity:.6}.photoalbum-visibility-toggle__input:checked+.photoalbum-visibility-toggle__label{background-color:#e9ecef;border-color:#ced4da;color:#495057}.photoalbum-visibility-toggle__input:checked+.photoalbum-visibility-toggle__label .photoalbum-visibility-toggle__icon--on{display:none}.photoalbum-visibility-toggle__input:checked+.photoalbum-visibility-toggle__label .photoalbum-visibility-toggle__icon--off{display:inline-flex} + */:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-display:block;font-family:Font Awesome\ 6 Brands;font-style:normal;font-weight:400;src:url(../fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.woff2?c210719e60948b211a1260f79812efe5) format("woff2"),url(../fonts/vendor/@fortawesome/fontawesome-free/webfa-brands-400.ttf?1815e00441357e01619e5793e1caa78a) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero{--fa:"\f3d0"}.fa-hooli{--fa:"\f427"}.fa-yelp{--fa:"\f1e9"}.fa-cc-visa{--fa:"\f1f0"}.fa-lastfm{--fa:"\f202"}.fa-shopware{--fa:"\f5b5"}.fa-creative-commons-nc{--fa:"\f4e8"}.fa-aws{--fa:"\f375"}.fa-redhat{--fa:"\f7bc"}.fa-yoast{--fa:"\f2b1"}.fa-cloudflare{--fa:"\e07d"}.fa-ups{--fa:"\f7e0"}.fa-pixiv{--fa:"\e640"}.fa-wpexplorer{--fa:"\f2de"}.fa-dyalog{--fa:"\f399"}.fa-bity{--fa:"\f37a"}.fa-stackpath{--fa:"\f842"}.fa-buysellads{--fa:"\f20d"}.fa-first-order{--fa:"\f2b0"}.fa-modx{--fa:"\f285"}.fa-guilded{--fa:"\e07e"}.fa-vnv{--fa:"\f40b"}.fa-js-square,.fa-square-js{--fa:"\f3b9"}.fa-microsoft{--fa:"\f3ca"}.fa-qq{--fa:"\f1d6"}.fa-orcid{--fa:"\f8d2"}.fa-java{--fa:"\f4e4"}.fa-invision{--fa:"\f7b0"}.fa-creative-commons-pd-alt{--fa:"\f4ed"}.fa-centercode{--fa:"\f380"}.fa-glide-g{--fa:"\f2a6"}.fa-drupal{--fa:"\f1a9"}.fa-jxl{--fa:"\e67b"}.fa-dart-lang{--fa:"\e693"}.fa-hire-a-helper{--fa:"\f3b0"}.fa-creative-commons-by{--fa:"\f4e7"}.fa-unity{--fa:"\e049"}.fa-whmcs{--fa:"\f40d"}.fa-rocketchat{--fa:"\f3e8"}.fa-vk{--fa:"\f189"}.fa-untappd{--fa:"\f405"}.fa-mailchimp{--fa:"\f59e"}.fa-css3-alt{--fa:"\f38b"}.fa-reddit-square,.fa-square-reddit{--fa:"\f1a2"}.fa-vimeo-v{--fa:"\f27d"}.fa-contao{--fa:"\f26d"}.fa-square-font-awesome{--fa:"\e5ad"}.fa-deskpro{--fa:"\f38f"}.fa-brave{--fa:"\e63c"}.fa-sistrix{--fa:"\f3ee"}.fa-instagram-square,.fa-square-instagram{--fa:"\e055"}.fa-battle-net{--fa:"\f835"}.fa-the-red-yeti{--fa:"\f69d"}.fa-hacker-news-square,.fa-square-hacker-news{--fa:"\f3af"}.fa-edge{--fa:"\f282"}.fa-threads{--fa:"\e618"}.fa-napster{--fa:"\f3d2"}.fa-snapchat-square,.fa-square-snapchat{--fa:"\f2ad"}.fa-google-plus-g{--fa:"\f0d5"}.fa-artstation{--fa:"\f77a"}.fa-markdown{--fa:"\f60f"}.fa-sourcetree{--fa:"\f7d3"}.fa-google-plus{--fa:"\f2b3"}.fa-diaspora{--fa:"\f791"}.fa-foursquare{--fa:"\f180"}.fa-stack-overflow{--fa:"\f16c"}.fa-github-alt{--fa:"\f113"}.fa-phoenix-squadron{--fa:"\f511"}.fa-pagelines{--fa:"\f18c"}.fa-algolia{--fa:"\f36c"}.fa-red-river{--fa:"\f3e3"}.fa-creative-commons-sa{--fa:"\f4ef"}.fa-safari{--fa:"\f267"}.fa-google{--fa:"\f1a0"}.fa-font-awesome-alt,.fa-square-font-awesome-stroke{--fa:"\f35c"}.fa-atlassian{--fa:"\f77b"}.fa-linkedin-in{--fa:"\f0e1"}.fa-digital-ocean{--fa:"\f391"}.fa-nimblr{--fa:"\f5a8"}.fa-chromecast{--fa:"\f838"}.fa-evernote{--fa:"\f839"}.fa-hacker-news{--fa:"\f1d4"}.fa-creative-commons-sampling{--fa:"\f4f0"}.fa-adversal{--fa:"\f36a"}.fa-creative-commons{--fa:"\f25e"}.fa-watchman-monitoring{--fa:"\e087"}.fa-fonticons{--fa:"\f280"}.fa-weixin{--fa:"\f1d7"}.fa-shirtsinbulk{--fa:"\f214"}.fa-codepen{--fa:"\f1cb"}.fa-git-alt{--fa:"\f841"}.fa-lyft{--fa:"\f3c3"}.fa-rev{--fa:"\f5b2"}.fa-windows{--fa:"\f17a"}.fa-wizards-of-the-coast{--fa:"\f730"}.fa-square-viadeo,.fa-viadeo-square{--fa:"\f2aa"}.fa-meetup{--fa:"\f2e0"}.fa-centos{--fa:"\f789"}.fa-adn{--fa:"\f170"}.fa-cloudsmith{--fa:"\f384"}.fa-opensuse{--fa:"\e62b"}.fa-pied-piper-alt{--fa:"\f1a8"}.fa-dribbble-square,.fa-square-dribbble{--fa:"\f397"}.fa-codiepie{--fa:"\f284"}.fa-node{--fa:"\f419"}.fa-mix{--fa:"\f3cb"}.fa-steam{--fa:"\f1b6"}.fa-cc-apple-pay{--fa:"\f416"}.fa-scribd{--fa:"\f28a"}.fa-debian{--fa:"\e60b"}.fa-openid{--fa:"\f19b"}.fa-instalod{--fa:"\e081"}.fa-files-pinwheel{--fa:"\e69f"}.fa-expeditedssl{--fa:"\f23e"}.fa-sellcast{--fa:"\f2da"}.fa-square-twitter,.fa-twitter-square{--fa:"\f081"}.fa-r-project{--fa:"\f4f7"}.fa-delicious{--fa:"\f1a5"}.fa-freebsd{--fa:"\f3a4"}.fa-vuejs{--fa:"\f41f"}.fa-accusoft{--fa:"\f369"}.fa-ioxhost{--fa:"\f208"}.fa-fonticons-fi{--fa:"\f3a2"}.fa-app-store{--fa:"\f36f"}.fa-cc-mastercard{--fa:"\f1f1"}.fa-itunes-note{--fa:"\f3b5"}.fa-golang{--fa:"\e40f"}.fa-kickstarter,.fa-square-kickstarter{--fa:"\f3bb"}.fa-grav{--fa:"\f2d6"}.fa-weibo{--fa:"\f18a"}.fa-uncharted{--fa:"\e084"}.fa-firstdraft{--fa:"\f3a1"}.fa-square-youtube,.fa-youtube-square{--fa:"\f431"}.fa-wikipedia-w{--fa:"\f266"}.fa-rendact,.fa-wpressr{--fa:"\f3e4"}.fa-angellist{--fa:"\f209"}.fa-galactic-republic{--fa:"\f50c"}.fa-nfc-directional{--fa:"\e530"}.fa-skype{--fa:"\f17e"}.fa-joget{--fa:"\f3b7"}.fa-fedora{--fa:"\f798"}.fa-stripe-s{--fa:"\f42a"}.fa-meta{--fa:"\e49b"}.fa-laravel{--fa:"\f3bd"}.fa-hotjar{--fa:"\f3b1"}.fa-bluetooth-b{--fa:"\f294"}.fa-square-letterboxd{--fa:"\e62e"}.fa-sticker-mule{--fa:"\f3f7"}.fa-creative-commons-zero{--fa:"\f4f3"}.fa-hips{--fa:"\f452"}.fa-css{--fa:"\e6a2"}.fa-behance{--fa:"\f1b4"}.fa-reddit{--fa:"\f1a1"}.fa-discord{--fa:"\f392"}.fa-chrome{--fa:"\f268"}.fa-app-store-ios{--fa:"\f370"}.fa-cc-discover{--fa:"\f1f2"}.fa-wpbeginner{--fa:"\f297"}.fa-confluence{--fa:"\f78d"}.fa-shoelace{--fa:"\e60c"}.fa-mdb{--fa:"\f8ca"}.fa-dochub{--fa:"\f394"}.fa-accessible-icon{--fa:"\f368"}.fa-ebay{--fa:"\f4f4"}.fa-amazon{--fa:"\f270"}.fa-unsplash{--fa:"\e07c"}.fa-yarn{--fa:"\f7e3"}.fa-square-steam,.fa-steam-square{--fa:"\f1b7"}.fa-500px{--fa:"\f26e"}.fa-square-vimeo,.fa-vimeo-square{--fa:"\f194"}.fa-asymmetrik{--fa:"\f372"}.fa-font-awesome,.fa-font-awesome-flag,.fa-font-awesome-logo-full{--fa:"\f2b4"}.fa-gratipay{--fa:"\f184"}.fa-apple{--fa:"\f179"}.fa-hive{--fa:"\e07f"}.fa-gitkraken{--fa:"\f3a6"}.fa-keybase{--fa:"\f4f5"}.fa-apple-pay{--fa:"\f415"}.fa-padlet{--fa:"\e4a0"}.fa-amazon-pay{--fa:"\f42c"}.fa-github-square,.fa-square-github{--fa:"\f092"}.fa-stumbleupon{--fa:"\f1a4"}.fa-fedex{--fa:"\f797"}.fa-phoenix-framework{--fa:"\f3dc"}.fa-shopify{--fa:"\e057"}.fa-neos{--fa:"\f612"}.fa-square-threads{--fa:"\e619"}.fa-hackerrank{--fa:"\f5f7"}.fa-researchgate{--fa:"\f4f8"}.fa-swift{--fa:"\f8e1"}.fa-angular{--fa:"\f420"}.fa-speakap{--fa:"\f3f3"}.fa-angrycreative{--fa:"\f36e"}.fa-y-combinator{--fa:"\f23b"}.fa-empire{--fa:"\f1d1"}.fa-envira{--fa:"\f299"}.fa-google-scholar{--fa:"\e63b"}.fa-gitlab-square,.fa-square-gitlab{--fa:"\e5ae"}.fa-studiovinari{--fa:"\f3f8"}.fa-pied-piper{--fa:"\f2ae"}.fa-wordpress{--fa:"\f19a"}.fa-product-hunt{--fa:"\f288"}.fa-firefox{--fa:"\f269"}.fa-linode{--fa:"\f2b8"}.fa-goodreads{--fa:"\f3a8"}.fa-odnoklassniki-square,.fa-square-odnoklassniki{--fa:"\f264"}.fa-jsfiddle{--fa:"\f1cc"}.fa-sith{--fa:"\f512"}.fa-themeisle{--fa:"\f2b2"}.fa-page4{--fa:"\f3d7"}.fa-hashnode{--fa:"\e499"}.fa-react{--fa:"\f41b"}.fa-cc-paypal{--fa:"\f1f4"}.fa-squarespace{--fa:"\f5be"}.fa-cc-stripe{--fa:"\f1f5"}.fa-creative-commons-share{--fa:"\f4f2"}.fa-bitcoin{--fa:"\f379"}.fa-keycdn{--fa:"\f3ba"}.fa-opera{--fa:"\f26a"}.fa-itch-io{--fa:"\f83a"}.fa-umbraco{--fa:"\f8e8"}.fa-galactic-senate{--fa:"\f50d"}.fa-ubuntu{--fa:"\f7df"}.fa-draft2digital{--fa:"\f396"}.fa-stripe{--fa:"\f429"}.fa-houzz{--fa:"\f27c"}.fa-gg{--fa:"\f260"}.fa-dhl{--fa:"\f790"}.fa-pinterest-square,.fa-square-pinterest{--fa:"\f0d3"}.fa-xing{--fa:"\f168"}.fa-blackberry{--fa:"\f37b"}.fa-creative-commons-pd{--fa:"\f4ec"}.fa-playstation{--fa:"\f3df"}.fa-quinscape{--fa:"\f459"}.fa-less{--fa:"\f41d"}.fa-blogger-b{--fa:"\f37d"}.fa-opencart{--fa:"\f23d"}.fa-vine{--fa:"\f1ca"}.fa-signal-messenger{--fa:"\e663"}.fa-paypal{--fa:"\f1ed"}.fa-gitlab{--fa:"\f296"}.fa-typo3{--fa:"\f42b"}.fa-reddit-alien{--fa:"\f281"}.fa-yahoo{--fa:"\f19e"}.fa-dailymotion{--fa:"\e052"}.fa-affiliatetheme{--fa:"\f36b"}.fa-pied-piper-pp{--fa:"\f1a7"}.fa-bootstrap{--fa:"\f836"}.fa-odnoklassniki{--fa:"\f263"}.fa-nfc-symbol{--fa:"\e531"}.fa-mintbit{--fa:"\e62f"}.fa-ethereum{--fa:"\f42e"}.fa-speaker-deck{--fa:"\f83c"}.fa-creative-commons-nc-eu{--fa:"\f4e9"}.fa-patreon{--fa:"\f3d9"}.fa-avianex{--fa:"\f374"}.fa-ello{--fa:"\f5f1"}.fa-gofore{--fa:"\f3a7"}.fa-bimobject{--fa:"\f378"}.fa-brave-reverse{--fa:"\e63d"}.fa-facebook-f{--fa:"\f39e"}.fa-google-plus-square,.fa-square-google-plus{--fa:"\f0d4"}.fa-web-awesome{--fa:"\e682"}.fa-mandalorian{--fa:"\f50f"}.fa-first-order-alt{--fa:"\f50a"}.fa-osi{--fa:"\f41a"}.fa-google-wallet{--fa:"\f1ee"}.fa-d-and-d-beyond{--fa:"\f6ca"}.fa-periscope{--fa:"\f3da"}.fa-fulcrum{--fa:"\f50b"}.fa-cloudscale{--fa:"\f383"}.fa-forumbee{--fa:"\f211"}.fa-mizuni{--fa:"\f3cc"}.fa-schlix{--fa:"\f3ea"}.fa-square-xing,.fa-xing-square{--fa:"\f169"}.fa-bandcamp{--fa:"\f2d5"}.fa-wpforms{--fa:"\f298"}.fa-cloudversify{--fa:"\f385"}.fa-usps{--fa:"\f7e1"}.fa-megaport{--fa:"\f5a3"}.fa-magento{--fa:"\f3c4"}.fa-spotify{--fa:"\f1bc"}.fa-optin-monster{--fa:"\f23c"}.fa-fly{--fa:"\f417"}.fa-square-bluesky{--fa:"\e6a3"}.fa-aviato{--fa:"\f421"}.fa-itunes{--fa:"\f3b4"}.fa-cuttlefish{--fa:"\f38c"}.fa-blogger{--fa:"\f37c"}.fa-flickr{--fa:"\f16e"}.fa-viber{--fa:"\f409"}.fa-soundcloud{--fa:"\f1be"}.fa-digg{--fa:"\f1a6"}.fa-tencent-weibo{--fa:"\f1d5"}.fa-letterboxd{--fa:"\e62d"}.fa-symfony{--fa:"\f83d"}.fa-maxcdn{--fa:"\f136"}.fa-etsy{--fa:"\f2d7"}.fa-facebook-messenger{--fa:"\f39f"}.fa-audible{--fa:"\f373"}.fa-think-peaks{--fa:"\f731"}.fa-bilibili{--fa:"\e3d9"}.fa-erlang{--fa:"\f39d"}.fa-x-twitter{--fa:"\e61b"}.fa-cotton-bureau{--fa:"\f89e"}.fa-dashcube{--fa:"\f210"}.fa-42-group,.fa-innosoft{--fa:"\e080"}.fa-stack-exchange{--fa:"\f18d"}.fa-elementor{--fa:"\f430"}.fa-pied-piper-square,.fa-square-pied-piper{--fa:"\e01e"}.fa-creative-commons-nd{--fa:"\f4eb"}.fa-palfed{--fa:"\f3d8"}.fa-superpowers{--fa:"\f2dd"}.fa-resolving{--fa:"\f3e7"}.fa-xbox{--fa:"\f412"}.fa-square-web-awesome-stroke{--fa:"\e684"}.fa-searchengin{--fa:"\f3eb"}.fa-tiktok{--fa:"\e07b"}.fa-facebook-square,.fa-square-facebook{--fa:"\f082"}.fa-renren{--fa:"\f18b"}.fa-linux{--fa:"\f17c"}.fa-glide{--fa:"\f2a5"}.fa-linkedin{--fa:"\f08c"}.fa-hubspot{--fa:"\f3b2"}.fa-deploydog{--fa:"\f38e"}.fa-twitch{--fa:"\f1e8"}.fa-flutter{--fa:"\e694"}.fa-ravelry{--fa:"\f2d9"}.fa-mixer{--fa:"\e056"}.fa-lastfm-square,.fa-square-lastfm{--fa:"\f203"}.fa-vimeo{--fa:"\f40a"}.fa-mendeley{--fa:"\f7b3"}.fa-uniregistry{--fa:"\f404"}.fa-figma{--fa:"\f799"}.fa-creative-commons-remix{--fa:"\f4ee"}.fa-cc-amazon-pay{--fa:"\f42d"}.fa-dropbox{--fa:"\f16b"}.fa-instagram{--fa:"\f16d"}.fa-cmplid{--fa:"\e360"}.fa-upwork{--fa:"\e641"}.fa-facebook{--fa:"\f09a"}.fa-gripfire{--fa:"\f3ac"}.fa-jedi-order{--fa:"\f50e"}.fa-uikit{--fa:"\f403"}.fa-fort-awesome-alt{--fa:"\f3a3"}.fa-phabricator{--fa:"\f3db"}.fa-ussunnah{--fa:"\f407"}.fa-earlybirds{--fa:"\f39a"}.fa-trade-federation{--fa:"\f513"}.fa-autoprefixer{--fa:"\f41c"}.fa-whatsapp{--fa:"\f232"}.fa-square-upwork{--fa:"\e67c"}.fa-slideshare{--fa:"\f1e7"}.fa-google-play{--fa:"\f3ab"}.fa-viadeo{--fa:"\f2a9"}.fa-line{--fa:"\f3c0"}.fa-google-drive{--fa:"\f3aa"}.fa-servicestack{--fa:"\f3ec"}.fa-simplybuilt{--fa:"\f215"}.fa-bitbucket{--fa:"\f171"}.fa-imdb{--fa:"\f2d8"}.fa-deezer{--fa:"\e077"}.fa-raspberry-pi{--fa:"\f7bb"}.fa-jira{--fa:"\f7b1"}.fa-docker{--fa:"\f395"}.fa-screenpal{--fa:"\e570"}.fa-bluetooth{--fa:"\f293"}.fa-gitter{--fa:"\f426"}.fa-d-and-d{--fa:"\f38d"}.fa-microblog{--fa:"\e01a"}.fa-cc-diners-club{--fa:"\f24c"}.fa-gg-circle{--fa:"\f261"}.fa-pied-piper-hat{--fa:"\f4e5"}.fa-kickstarter-k{--fa:"\f3bc"}.fa-yandex{--fa:"\f413"}.fa-readme{--fa:"\f4d5"}.fa-html5{--fa:"\f13b"}.fa-sellsy{--fa:"\f213"}.fa-square-web-awesome{--fa:"\e683"}.fa-sass{--fa:"\f41e"}.fa-wirsindhandwerk,.fa-wsh{--fa:"\e2d0"}.fa-buromobelexperte{--fa:"\f37f"}.fa-salesforce{--fa:"\f83b"}.fa-octopus-deploy{--fa:"\e082"}.fa-medapps{--fa:"\f3c6"}.fa-ns8{--fa:"\f3d5"}.fa-pinterest-p{--fa:"\f231"}.fa-apper{--fa:"\f371"}.fa-fort-awesome{--fa:"\f286"}.fa-waze{--fa:"\f83f"}.fa-bluesky{--fa:"\e671"}.fa-cc-jcb{--fa:"\f24b"}.fa-snapchat,.fa-snapchat-ghost{--fa:"\f2ab"}.fa-fantasy-flight-games{--fa:"\f6dc"}.fa-rust{--fa:"\e07a"}.fa-wix{--fa:"\f5cf"}.fa-behance-square,.fa-square-behance{--fa:"\f1b5"}.fa-supple{--fa:"\f3f9"}.fa-webflow{--fa:"\e65c"}.fa-rebel{--fa:"\f1d0"}.fa-css3{--fa:"\f13c"}.fa-staylinked{--fa:"\f3f5"}.fa-kaggle{--fa:"\f5fa"}.fa-space-awesome{--fa:"\e5ac"}.fa-deviantart{--fa:"\f1bd"}.fa-cpanel{--fa:"\f388"}.fa-goodreads-g{--fa:"\f3a9"}.fa-git-square,.fa-square-git{--fa:"\f1d2"}.fa-square-tumblr,.fa-tumblr-square{--fa:"\f174"}.fa-trello{--fa:"\f181"}.fa-creative-commons-nc-jp{--fa:"\f4ea"}.fa-get-pocket{--fa:"\f265"}.fa-perbyte{--fa:"\e083"}.fa-grunt{--fa:"\f3ad"}.fa-weebly{--fa:"\f5cc"}.fa-connectdevelop{--fa:"\f20e"}.fa-leanpub{--fa:"\f212"}.fa-black-tie{--fa:"\f27e"}.fa-themeco{--fa:"\f5c6"}.fa-python{--fa:"\f3e2"}.fa-android{--fa:"\f17b"}.fa-bots{--fa:"\e340"}.fa-free-code-camp{--fa:"\f2c5"}.fa-hornbill{--fa:"\f592"}.fa-js{--fa:"\f3b8"}.fa-ideal{--fa:"\e013"}.fa-git{--fa:"\f1d3"}.fa-dev{--fa:"\f6cc"}.fa-sketch{--fa:"\f7c6"}.fa-yandex-international{--fa:"\f414"}.fa-cc-amex{--fa:"\f1f3"}.fa-uber{--fa:"\f402"}.fa-github{--fa:"\f09b"}.fa-php{--fa:"\f457"}.fa-alipay{--fa:"\f642"}.fa-youtube{--fa:"\f167"}.fa-skyatlas{--fa:"\f216"}.fa-firefox-browser{--fa:"\e007"}.fa-replyd{--fa:"\f3e6"}.fa-suse{--fa:"\f7d6"}.fa-jenkins{--fa:"\f3b6"}.fa-twitter{--fa:"\f099"}.fa-rockrms{--fa:"\f3e9"}.fa-pinterest{--fa:"\f0d2"}.fa-buffer{--fa:"\f837"}.fa-npm{--fa:"\f3d4"}.fa-yammer{--fa:"\f840"}.fa-btc{--fa:"\f15a"}.fa-dribbble{--fa:"\f17d"}.fa-stumbleupon-circle{--fa:"\f1a3"}.fa-internet-explorer{--fa:"\f26b"}.fa-stubber{--fa:"\e5c7"}.fa-telegram,.fa-telegram-plane{--fa:"\f2c6"}.fa-old-republic{--fa:"\f510"}.fa-odysee{--fa:"\e5c6"}.fa-square-whatsapp,.fa-whatsapp-square{--fa:"\f40c"}.fa-node-js{--fa:"\f3d3"}.fa-edge-legacy{--fa:"\e078"}.fa-slack,.fa-slack-hash{--fa:"\f198"}.fa-medrt{--fa:"\f3c8"}.fa-usb{--fa:"\f287"}.fa-tumblr{--fa:"\f173"}.fa-vaadin{--fa:"\f408"}.fa-quora{--fa:"\f2c4"}.fa-square-x-twitter{--fa:"\e61a"}.fa-reacteurope{--fa:"\f75d"}.fa-medium,.fa-medium-m{--fa:"\f23a"}.fa-amilia{--fa:"\f36d"}.fa-mixcloud{--fa:"\f289"}.fa-flipboard{--fa:"\f44d"}.fa-viacoin{--fa:"\f237"}.fa-critical-role{--fa:"\f6c9"}.fa-sitrox{--fa:"\e44a"}.fa-discourse{--fa:"\f393"}.fa-joomla{--fa:"\f1aa"}.fa-mastodon{--fa:"\f4f6"}.fa-airbnb{--fa:"\f834"}.fa-wolf-pack-battalion{--fa:"\f514"}.fa-buy-n-large{--fa:"\f8a6"}.fa-gulp{--fa:"\f3ae"}.fa-creative-commons-sampling-plus{--fa:"\f4f1"}.fa-strava{--fa:"\f428"}.fa-ember{--fa:"\f423"}.fa-canadian-maple-leaf{--fa:"\f785"}.fa-teamspeak{--fa:"\f4f9"}.fa-pushed{--fa:"\f3e1"}.fa-wordpress-simple{--fa:"\f411"}.fa-nutritionix{--fa:"\f3d6"}.fa-wodu{--fa:"\e088"}.fa-google-pay{--fa:"\e079"}.fa-intercom{--fa:"\f7af"}.fa-zhihu{--fa:"\f63f"}.fa-korvue{--fa:"\f42f"}.fa-pix{--fa:"\e43a"}.fa-steam-symbol{--fa:"\f3f6"}:root{--td-light:#fff;--td-widget-background:#fff;--td-font-color:#000;--td-timepicker-font-size:1.2em;--td-active-bg:#0d6efd;--td-range-bg:#01419e;--td-active-color:#fff;--td-active-border-color:#fff;--td-border-radius:999px;--td-btn-hover-bg:#e9ecef;--td-disabled-color:#6c757d;--td-alternate-color:rgba(0,0,0,.38);--td-secondary-border-color:#ccc;--td-secondary-border-color-rgba:rgba(0,0,0,.2);--td-primary-border-color:#fff;--td-text-shadow:0 -1px 0 rgba(0,0,0,.25);--td-dow-color:rgba(0,0,0,.5);--td-dark:#1b1b1b;--td-dark-widget-background:#1b1b1b;--td-dark-font-color:#e3e3e3;--td-dark-active-bg:#4db2ff;--td-dark-range-bg:#0071c7;--td-dark-active-color:#fff;--td-dark-active-border-color:#1b1b1b;--td-dark-btn-hover-bg:#232627;--td-dark-disabled-color:#6c757d;--td-dark-alternate-color:hsla(36,10%,90%,.38);--td-dark-secondary-border-color:#ccc;--td-dark-secondary-border-color-rgba:hsla(36,10%,90%,.2);--td-dark-primary-border-color:#1b1b1b;--td-dark-text-shadow:0 -1px 0 hsla(36,10%,90%,.25);--td-dark-dow-color:hsla(36,10%,90%,.5);--td-widget-z-index:9999}.tempus-dominus-widget [data-action]:after,.visually-hidden{clip:rect(0,0,0,0)!important;border:0!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:1px!important}.tempus-dominus-widget{border-radius:4px;box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12);display:none;list-style:none;padding:4px;width:19rem;z-index:var(--td-widget-z-index)}.tempus-dominus-widget :focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.25);outline:0}.tempus-dominus-widget.calendarWeeks{width:21rem}.tempus-dominus-widget.calendarWeeks .date-container-days{grid-auto-columns:12.5%;grid-template-areas:"a a a a a a a a"}.tempus-dominus-widget [data-action]{cursor:pointer}.tempus-dominus-widget [data-action]:after{content:attr(title)}.tempus-dominus-widget [data-action].disabled,.tempus-dominus-widget [data-action].disabled:hover{background:none;cursor:not-allowed}.tempus-dominus-widget .arrow{display:none}.tempus-dominus-widget.show{display:block}.tempus-dominus-widget.show.date-container{min-height:315px}.tempus-dominus-widget.show.time-container{min-height:217px}.tempus-dominus-widget .td-collapse:not(.show){display:none}.tempus-dominus-widget .td-collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (min-width:576px){.tempus-dominus-widget.timepicker-sbs{width:38em}}@media (min-width:768px){.tempus-dominus-widget.timepicker-sbs{width:38em}}@media (min-width:992px){.tempus-dominus-widget.timepicker-sbs{width:38em}}.tempus-dominus-widget.timepicker-sbs .td-row{display:flex}.tempus-dominus-widget.timepicker-sbs .td-row .td-half{flex:0 0 auto;width:50%}.tempus-dominus-widget div[data-action]:active{box-shadow:none}.tempus-dominus-widget .timepicker-hour,.tempus-dominus-widget .timepicker-minute,.tempus-dominus-widget .timepicker-second{font-size:1.2em;font-weight:700;margin:0;width:54px}.tempus-dominus-widget button[data-action]{padding:6px}.tempus-dominus-widget .toggleMeridiem{height:38px;text-align:center}.tempus-dominus-widget .calendar-header{display:grid;font-weight:700;grid-template-areas:"a a a";margin-bottom:10px}.tempus-dominus-widget .calendar-header .next{padding-right:10px;text-align:right}.tempus-dominus-widget .calendar-header .previous{padding-left:10px;text-align:left}.tempus-dominus-widget .calendar-header .picker-switch{text-align:center}.tempus-dominus-widget .toolbar{display:grid;grid-auto-flow:column;grid-auto-rows:40px}.tempus-dominus-widget .toolbar div{align-items:center;border-radius:var(--td-border-radius);box-sizing:border-box;display:flex;justify-content:center}.tempus-dominus-widget .date-container-days{display:grid;grid-auto-columns:14.2857142857%;grid-auto-rows:40px;grid-template-areas:"a a a a a a a"}.tempus-dominus-widget .date-container-days .range-in{background-color:var(--td-range-bg)!important;border:none;border-radius:0!important;box-shadow:-5px 0 0 var(--td-range-bg),5px 0 0 var(--td-range-bg)}.tempus-dominus-widget .date-container-days .range-end{border-radius:0 50px 50px 0!important}.tempus-dominus-widget .date-container-days .range-start{border-radius:50px 0 0 50px!important}.tempus-dominus-widget .date-container-days .dow{align-items:center;justify-content:center;text-align:center}.tempus-dominus-widget .date-container-days .cw{align-items:center;cursor:default;display:flex;font-size:.8em;height:90%;justify-content:center;line-height:20px;width:90%}.tempus-dominus-widget .date-container-decades,.tempus-dominus-widget .date-container-months,.tempus-dominus-widget .date-container-years{display:grid;grid-auto-rows:calc(2.71429rem - 1.14286px);grid-template-areas:"a a a"}.tempus-dominus-widget .time-container-hour,.tempus-dominus-widget .time-container-minute,.tempus-dominus-widget .time-container-second{display:grid;grid-auto-rows:calc(2.71429rem - 1.14286px);grid-template-areas:"a a a a"}.tempus-dominus-widget .time-container-clock{display:grid;grid-auto-rows:calc(2.71429rem - 1.14286px)}.tempus-dominus-widget .time-container-clock .no-highlight{align-items:center;display:flex;height:90%;justify-content:center;width:90%}.tempus-dominus-widget .date-container-days div:not(.no-highlight),.tempus-dominus-widget .date-container-decades div:not(.no-highlight),.tempus-dominus-widget .date-container-months div:not(.no-highlight),.tempus-dominus-widget .date-container-years div:not(.no-highlight),.tempus-dominus-widget .time-container-clock div:not(.no-highlight),.tempus-dominus-widget .time-container-hour div:not(.no-highlight),.tempus-dominus-widget .time-container-minute div:not(.no-highlight),.tempus-dominus-widget .time-container-second div:not(.no-highlight){align-items:center;border-radius:var(--td-border-radius);box-sizing:border-box;display:flex;height:90%;justify-content:center;width:90%}.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled:hover{background:none;cursor:not-allowed}.tempus-dominus-widget .date-container-days div:not(.no-highlight).today,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today,.tempus-dominus-widget .date-container-months div:not(.no-highlight).today,.tempus-dominus-widget .date-container-years div:not(.no-highlight).today,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today,.tempus-dominus-widget .time-container-second div:not(.no-highlight).today{position:relative}.tempus-dominus-widget .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget .time-container-second div:not(.no-highlight).today:before{border:solid transparent;border-width:0 0 7px 7px;bottom:6px;content:"";display:inline-block;position:absolute;right:6px}.tempus-dominus-widget .time-container{margin-bottom:.5rem}.tempus-dominus-widget button{border-radius:.25rem;cursor:pointer;display:inline-block;font-size:1rem;font-weight:400;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle}.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds],.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod],.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second{cursor:default;pointer-events:none}.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod]:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute:hover,.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second:hover{background:none}.tempus-dominus-widget.light{background-color:var(--td-widget-background);color:var(--td-font-color)}.tempus-dominus-widget.light [data-action].disabled,.tempus-dominus-widget.light [data-action].disabled:hover{color:var(--td-disabled-color)}.tempus-dominus-widget.light .toolbar div:hover{background:var(--td-btn-hover-bg)}.tempus-dominus-widget.light .date-container-days .dow{color:var(--td-dow-color)}.tempus-dominus-widget.light .date-container-days .cw{color:var(--td-alternate-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight):hover,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight):hover,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight):hover{background:var(--td-btn-hover-bg)}.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight),.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight),.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight),.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active{background-color:var(--td-active-bg);color:var(--td-active-color);text-shadow:var(--td-text-shadow)}.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.old,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.new,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.old,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.new,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.old{color:var(--td-active-color)}.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.today:before,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.today:before{border-bottom-color:var(--td-active-border-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).old,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).new,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).old,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).new,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).old{color:var(--td-alternate-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled:hover{color:var(--td-disabled-color)}.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).today:before{border-bottom-color:var(--td-active-bg);border-top-color:var(--td-secondary-border-color-rgba)}.tempus-dominus-widget.light button{background-color:var(--td-active-bg);border-color:var(--td-active-bg);color:var(--td-active-color)}.tempus-dominus-widget.dark{background-color:var(--td-dark-widget-background);color:var(--td-dark-font-color)}.tempus-dominus-widget.dark [data-action].disabled,.tempus-dominus-widget.dark [data-action].disabled:hover{color:var(--td-dark-disabled-color)}.tempus-dominus-widget.dark .toolbar div:hover{background:var(--td-dark-btn-hover-bg)}.tempus-dominus-widget.dark .date-container-days .dow{color:var(--td-dark-dow-color)}.tempus-dominus-widget.dark .date-container-days .range-in{background-color:var(--td-dark-range-bg)!important;box-shadow:-5px 0 0 var(--td-dark-range-bg),5px 0 0 var(--td-dark-range-bg)}.tempus-dominus-widget.dark .date-container-days .cw{color:var(--td-dark-alternate-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight):hover,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight):hover,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight):hover{background:var(--td-dark-btn-hover-bg)}.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight),.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight),.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight),.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active{background-color:var(--td-dark-active-bg);color:var(--td-dark-active-color);text-shadow:var(--td-dark-text-shadow)}.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.old,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.new,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.old{color:var(--td-dark-active-color)}.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.today:before,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.today:before{border-bottom-color:var(--td-dark-active-border-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).old,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).new,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).old,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).new,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).old{color:var(--td-dark-alternate-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled:hover,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled:hover{color:var(--td-dark-disabled-color)}.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).today:before,.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).today:before{border-bottom-color:var(--td-dark-active-bg);border-top-color:var(--td-dark-secondary-border-color-rgba)}.tempus-dominus-widget.dark button{background-color:var(--td-dark-active-bg);border-color:var(--td-dark-active-bg);color:var(--td-dark-active-color)}#ccHeaderArea.with-border{min-height:2.2em;position:relative}#ccHeaderArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccHeaderArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"ヘッダー";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccFooterArea.with-border{min-height:2.2em;position:relative}#ccFooterArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccFooterArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"フッター";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccMainArea.with-border{min-height:2.2em;position:relative}#ccMainArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccMainArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"メイン";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccLeftArea.with-border{min-height:2.2em;position:relative}#ccLeftArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccLeftArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"左";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}#ccRightArea.with-border{min-height:2.2em;position:relative}#ccRightArea.with-border:after{border:1px dashed #17a2b8;bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:2}#ccRightArea.with-border:before{background-color:hsla(0,0%,100%,.9);border-radius:.2em;color:#17a2b8;content:"右";left:.5em;padding:0 .4em;pointer-events:none;position:absolute;top:.5em;z-index:3}.cc-table-scroll{--cc-table-sticky-offset:0px;--cc-table-header-height:0px;--cc-table-sticky-viewport-offset:0px}.cc-table-scroll__sticky{background-color:#fff;position:sticky;top:var(--cc-table-sticky-viewport-offset,0);z-index:5}.cc-table-scroll__top{display:none;height:16px;margin-bottom:.25rem;overflow-x:auto;overflow-y:hidden}.cc-table-scroll.is-cc-scrollable .cc-table-scroll__top{display:block}.cc-table-scroll__top-inner{height:1px}.cc-table-scroll__header,.cc-table-scroll__header-inner{overflow:hidden}.cc-table-scroll__header-table{margin:0}.cc-table-scroll__header-table thead th{position:static;z-index:auto}.cc-table-scroll.is-cc-header-cloned .cc-table-scroll__body thead{opacity:0;pointer-events:none}.cc-table-scroll.is-cc-header-cloned .cc-table-scroll__body table{margin-top:calc(var(--cc-table-header-height, 0px)*-1)}.cc-table-scroll__body{overflow-x:auto;overflow-y:visible}.cc-table-sticky-header thead th{background-color:#fff;position:sticky;top:0;z-index:3}.cc-table-sticky-header thead.thead-light th{background-color:#f8f9fa}.cc-table-sticky-header-2 thead tr:first-child th{background-color:#fff;position:sticky;top:0;z-index:3}.cc-table-sticky-header-2 thead tr:nth-child(2) th{background-color:#fff;position:sticky;top:var(--cc-table-sticky-offset,0);z-index:3}.cc-table-sticky-first-col tbody tr{--cc-table-sticky-bg:#fff}.cc-table-sticky-first-col.table-striped tbody tr:nth-of-type(odd){--cc-table-sticky-bg:#f2f2f2}.cc-table-sticky-first-col .cc-sticky-col{background-clip:padding-box;background-color:var(--cc-table-sticky-bg);box-shadow:2px 0 0 #dee2e6;left:0;position:sticky;z-index:2}.cc-table-sticky-first-col thead th{position:relative;z-index:1}.cc-table-sticky-first-col thead .cc-sticky-col{background-clip:padding-box;background-color:#fff;box-shadow:2px 0 0 #dee2e6;z-index:10}.cc-table-sticky-first-col thead.thead-light .cc-sticky-col{background-color:#f8f9fa}.cc-table-scroll__header-table .cc-sticky-col{left:0;position:sticky!important;z-index:20!important}#cc-tui-actual-editor .tui-image-editor-header-buttons,#cc-tui-actual-editor .tui-image-editor-header-logo{display:none!important}.mce-content-body{margin:revert}.mce-text{color:#000!important}.mce-active .mce-text,.mce-text{background-color:transparent!important}.mce-active{background-color:#ededee!important}.embed-responsive span.mce-preview-object{position:static}.cc-column{background-color:#f2f2f2;border-radius:7px;margin:15px 0;padding:25px}.cc-exclamation{padding-left:1.1rem;text-indent:-1.2rem}.cc-exclamation:before{background:url(../images/acc_exclamation.png?337f20c894645de99ffc0ea9f1cfea67) no-repeat;background-size:contain;content:"";display:inline-block;height:1rem;margin:0 5px 3px 0;vertical-align:middle;width:1rem}.cc-note{padding-left:.8rem;text-indent:-.8rem}.cc-note:before{background:url(../images/acc_note.png?991226f05685a2d9d7f75a5348359153) no-repeat;background-size:contain;content:"";display:inline-block;height:.8rem;margin:0 3px 2px 0;vertical-align:middle;width:.8rem}.cc-note+.cc-note{margin-top:-10px}a.cc-icon-pdf:after{background:url(../images/icon_pdf.png?7cfbb334a509ddf1dc22044fa7557f4d) no-repeat;background-size:contain}a.cc-icon-excel:after,a.cc-icon-pdf:after{content:"";display:inline-block;height:1.25rem;margin:0 0 3px 5px;vertical-align:middle;width:1.25rem}a.cc-icon-excel:after{background:url(../images/icon_excel.png?37ecf7f67956f42395945014cddae393) no-repeat;background-size:contain}a.cc-icon-word:after{background:url(../images/icon_word.png?623f8b7fc1b3e9c8ff69f33176f28133) no-repeat;background-size:contain;height:1.25rem;width:1.25rem}a.cc-icon-external:after,a.cc-icon-word:after{content:"";display:inline-block;margin:0 0 3px 5px;vertical-align:middle}a.cc-icon-external:after{background:url(../images/icon_external.png?a6c57082171ba4ca205358df7ddda13f) no-repeat;background-size:contain;height:1rem;width:1rem}.cc-menu-select-locked{opacity:.6}.cc-menu-select-locked #page-select-list{background-color:#f1f3f5;border:1px solid #e2e3e5;border-radius:.25rem;padding:.5rem .75rem}.cc-menu-select-locked .custom-control-input,.cc-menu-select-locked .custom-control-label{cursor:not-allowed}.cc-menu-page-conditions i{margin-right:.25rem}.cc-menu-page-conditions i:last-child{margin-right:0}.photoalbum-manual-sort__thumb{align-items:center;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;display:inline-flex;height:64px;justify-content:center;overflow:hidden;width:64px}.photoalbum-manual-sort__thumb-image{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.photoalbum-manual-sort__thumb--video{background-color:#f8f9fa;color:#6c757d}.photoalbum-manual-sort__thumb--video i{font-size:24px}.photoalbum-manual-sort__toggle{align-items:center;color:inherit;display:inline-flex;text-decoration:none}.photoalbum-manual-sort__toggle:focus,.photoalbum-manual-sort__toggle:hover{text-decoration:none}.photoalbum-manual-sort__toggle-icon{transition:transform .2s ease}.photoalbum-manual-sort__toggle[aria-expanded=true] .photoalbum-manual-sort__toggle-icon{transform:rotate(90deg)}.photoalbum-manual-sort__item{scroll-margin-top:120px}.photoalbum-manual-sort__item--hidden{background-color:#f8f9fa}.photoalbum-manual-sort__item--pending-hidden{background-color:#e9ecef;background-image:repeating-linear-gradient(135deg,#e9ecef,#e9ecef 6px,#dee2e6 0,#dee2e6 12px)}.photoalbum-manual-sort__item--pending-show{background-color:#ffe083;background-image:repeating-linear-gradient(135deg,#ffe083,#ffe083 6px,#ffe69c 0,#ffe69c 12px)}.photoalbum-manual-sort__item--active{animation:photoalbum-highlight 1.5s ease}@keyframes photoalbum-highlight{0%{background-color:#fff3cd}to{background-color:transparent}}.photoalbum-preview__card{border-color:#17a2b8}.photoalbum-preview__card .card-header{background-color:#e8f6f8;color:#128293}.photoalbum-manual-sort__badge{background-color:#17a2b8;border-radius:.25rem;color:#fff;font-size:.75rem;padding:.2rem .45rem}.photoalbum-visibility-toggle{align-items:center;display:inline-flex;position:relative}.photoalbum-visibility-toggle__input{height:1px;opacity:0;position:absolute;width:1px}.photoalbum-visibility-toggle__label{align-items:center;border:1px solid transparent;border-radius:.25rem;color:#6c757d;cursor:pointer;display:inline-flex;height:1.75rem;justify-content:center;margin-bottom:0;min-width:2rem;padding:.15rem .35rem;transition:background-color .15s ease,border-color .15s ease,box-shadow .15s ease,color .15s ease}.photoalbum-visibility-toggle__icon{font-size:.95rem;line-height:1}.photoalbum-visibility-toggle__icon--off{display:none}.photoalbum-visibility-toggle__label:hover{background-color:#f8f9fa;border-color:#dee2e6;box-shadow:0 1px 2px rgba(0,0,0,.08);color:#343a40}.photoalbum-visibility-toggle__input:focus-visible+.photoalbum-visibility-toggle__label{background-color:#f8f9fa;border-color:#ced4da;box-shadow:0 0 0 .15rem rgba(23,162,184,.25);color:#343a40}.photoalbum-visibility-toggle__input:disabled+.photoalbum-visibility-toggle__label{box-shadow:none;cursor:not-allowed;opacity:.6}.photoalbum-visibility-toggle__input:checked+.photoalbum-visibility-toggle__label{background-color:#e9ecef;border-color:#ced4da;color:#495057}.photoalbum-visibility-toggle__input:checked+.photoalbum-visibility-toggle__label .photoalbum-visibility-toggle__icon--on{display:none}.photoalbum-visibility-toggle__input:checked+.photoalbum-visibility-toggle__label .photoalbum-visibility-toggle__icon--off{display:inline-flex} diff --git a/public/css/connect.css b/public/css/connect.css index 820aaf269..150ebede3 100644 --- a/public/css/connect.css +++ b/public/css/connect.css @@ -569,3 +569,15 @@ a.cc-cursor-text:hover { 0% {opacity: 0;} 100% {opacity: 1;} } + +/* ハニーポット(スパムボット対策用の隠しフィールド) +------------------------------------- */ +.connect-hp-field { + opacity: 0 !important; + position: absolute !important; + top: 0; + left: 0; + height: 0; + width: 0; + z-index: -1; +} diff --git a/public/js/app.js b/public/js/app.js index 173f61347..bc0025555 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,2 +1,2 @@ /*! For license information please see app.js.LICENSE.txt */ -(()=>{var e,t,n={95:()=>{!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=i=e,o=(r=String).prototype,o.isPrototypeOf(n)||(null===(s=i.constructor)||void 0===s?void 0:s.name)===r.name)?"string":t;var n,o;var i,r,s})(t)===e,n=e=>t=>typeof t===e,o=t("string"),i=t("object"),r=t("array"),s=(a=null,e=>a===e);var a;const l=n("boolean"),c=e=>!(e=>null==e)(e),u=n("function"),d=(e,t)=>{if(r(e)){for(let n=0,o=e.length;n{},f=(e,t)=>e===t;class p{constructor(e,t){this.tag=e,this.value=t}static some(e){return new p(!0,e)}static none(){return p.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?p.some(e(this.value)):p.none()}bind(e){return this.tag?e(this.value):p.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:p.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return c(e)?p.some(e):p.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}p.singletonNone=new p(!1);const m=Array.prototype.indexOf,g=Array.prototype.push,O=(e,t)=>((e,t)=>m.call(e,t))(e,t)>-1,v=e=>{const t=[];for(let n=0,o=e.length;nv(((e,t)=>{const n=e.length,o=new Array(n);for(let i=0;i{for(let n=0;ne.exists((e=>n(e,t))),w=e=>{const t=[],n=e=>{t.push(e)};for(let t=0;te?p.some(t):p.none(),_=e=>t=>t.options.get(e),k=_("link_assume_external_targets"),C=_("link_context_toolbar"),T=_("link_list"),$=_("link_default_target"),P=_("link_default_protocol"),A=_("link_target_list"),Q=_("link_rel_list"),E=_("link_class_list"),M=_("link_title"),D=_("allow_unsafe_link_target"),I=_("link_quicklink"),R=_("link_attributes_postprocess"),z=Object.keys,N=Object.hasOwnProperty,L=(e,t,n,o)=>{((e,t)=>{const n=z(e);for(let o=0,i=n.length;o{(t(e,i)?n:o)(e,i)}))},j=(e,t)=>N.call(e,t);var V=tinymce.util.Tools.resolve("tinymce.util.URI"),B=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),Z=tinymce.util.Tools.resolve("tinymce.util.Tools");const X=e=>c(e)&&"a"===e.nodeName.toLowerCase(),W=e=>X(e)&&!!F(e),U=(e,t)=>{if(e.collapsed)return[];{const n=e.cloneContents(),o=n.firstChild,i=new B(o,n),r=[];let s=o;do{t(s)&&r.push(s)}while(s=i.next());return r}},Y=e=>/^\w+:/i.test(e),F=e=>{var t,n;return null!==(n=null!==(t=e.getAttribute("data-mce-href"))&&void 0!==t?t:e.getAttribute("href"))&&void 0!==n?n:""},q=(e,t)=>{const n=["noopener"],o=e?e.split(/\s+/):[],i=e=>e.filter((e=>-1===Z.inArray(n,e))),r=t?(e=>(e=i(e)).length>0?e.concat(n):n)(o):i(o);return r.length>0?(e=>Z.trim(e.sort().join(" ")))(r):""},H=(e,t)=>(t=t||J(e.selection.getRng())[0]||e.selection.getNode(),oe(t)?p.from(e.dom.select("a[href]",t)[0]):p.from(e.dom.getParent(t,"a[href]"))),G=(e,t)=>H(e,t).isSome(),K=(e,t)=>(e=>e.replace(/\uFEFF/g,""))(t.fold((()=>e.getContent({format:"text"})),(e=>e.innerText||e.textContent||""))),J=e=>U(e,W),ee=e=>Z.grep(e,W),te=e=>ee(e).length>0,ne=e=>{const t=e.schema.getTextInlineElements(),n=e=>1===e.nodeType&&!X(e)&&!j(t,e.nodeName.toLowerCase());if(H(e).exists((e=>e.hasAttribute("data-mce-block"))))return!1;const o=e.selection.getRng();if(o.collapsed)return!0;return 0===U(o,n).length},oe=e=>c(e)&&"FIGURE"===e.nodeName&&/\bimage\b/i.test(e.className),ie=(e,t)=>{const n={...t};if(0===Q(e).length&&!D(e)){const e=q(n.rel,"_blank"===n.target);n.rel=e||null}return p.from(n.target).isNone()&&!1===A(e)&&(n.target=$(e)),n.href=((e,t)=>"http"!==t&&"https"!==t||Y(e)?e:t+"://"+e)(n.href,k(e)),n},re=(e,t,n)=>{const o=e.selection.getNode(),i=H(e,o),r=ie(e,(e=>{return t=["title","rel","class","target"],n=(t,n)=>(e[n].each((e=>{t[n]=e.length>0?e:null})),t),o={href:e.href},((e,t)=>{for(let n=0,o=e.length;n{o=n(o,e,t)})),o;var t,n,o})(n)),s=R(e);c(s)&&s(r),e.undoManager.transact((()=>{n.href===t.href&&t.attach(),i.fold((()=>{((e,t,n,o)=>{const i=e.dom;oe(t)?ue(i,t,o):n.fold((()=>{e.execCommand("mceInsertLink",!1,o);const t=e.selection.getEnd(),n=i.createRng();n.setStartAfter(t),n.setEndAfter(t),e.selection.setRng(n)}),(t=>{e.insertContent(i.createHTML("a",o,i.encode(t)))}))})(e,o,n.text,r)}),(t=>{e.focus(),((e,t,n,o)=>{n.each((e=>{j(t,"innerText")?t.innerText=e:t.textContent=e})),e.dom.setAttribs(t,o);const i=e.dom.createRng();i.setStartAfter(t),i.setEndAfter(t),e.selection.setRng(i)})(e,t,n.text,r)}))}))},se=e=>{const{class:t,href:n,rel:o,target:i,text:r,title:a}=e;return((e,t)=>{const n={};var o;return L(e,t,(o=n,(e,t)=>{o[t]=e}),h),n})({class:t.getOrNull(),href:n,rel:o.getOrNull(),target:i.getOrNull(),text:r.getOrNull(),title:a.getOrNull()},((e,t)=>!1===s(e)))},ae=(e,t,n)=>{const o=((e,t)=>{const n=e.options.get,o={allow_html_data_urls:n("allow_html_data_urls"),allow_script_urls:n("allow_script_urls"),allow_svg_data_urls:n("allow_svg_data_urls")},i=t.href;return{...t,href:V.isDomSafe(i,"a",o)?i:""}})(e,n);e.hasPlugin("rtc",!0)?e.execCommand("createlink",!1,se(o)):re(e,t,o)},le=e=>{e.hasPlugin("rtc",!0)?e.execCommand("unlink"):(e=>{e.undoManager.transact((()=>{const t=e.selection.getNode();oe(t)?ce(e,t):(e=>{const t=e.dom,n=e.selection,o=n.getBookmark(),i=n.getRng().cloneRange(),r=t.getParent(i.startContainer,"a[href]",e.getBody()),s=t.getParent(i.endContainer,"a[href]",e.getBody());r&&i.setStartBefore(r),s&&i.setEndAfter(s),n.setRng(i),e.execCommand("unlink"),n.moveToBookmark(o)})(e),e.focus()}))})(e)},ce=(e,t)=>{var n;const o=e.dom.select("img",t)[0];if(o){const i=e.dom.getParents(o,"a[href]",t)[0];i&&(null===(n=i.parentNode)||void 0===n||n.insertBefore(o,i),e.dom.remove(i))}},ue=(e,t,n)=>{var o;const i=e.select("img",t)[0];if(i){const t=e.create("a",n);null===(o=i.parentNode)||void 0===o||o.insertBefore(t,i),t.appendChild(i)}},de=e=>o(e.value)?e.value:"",he=(e,t)=>{const n=[];return Z.each(e,(e=>{const i=(e=>o(e.text)?e.text:o(e.title)?e.title:"")(e);if(void 0!==e.menu){const o=he(e.menu,t);n.push({text:i,items:o})}else{const o=t(e);n.push({text:i,value:o})}})),n},fe=(e=de)=>t=>p.from(t).map((t=>he(t,e))),pe={sanitize:e=>fe(de)(e),sanitizeWith:fe,createUi:(e,t)=>n=>({name:e,type:"listbox",label:t,items:n}),getValue:de},me=e=>{return j(t=e,n="items")&&void 0!==t[n]&&null!==t[n];var t,n},ge=(e,t)=>y(t,(t=>me(t)?ge(e,t.items):S(t.value===e,t))),Oe=(e,t,n,o)=>{const i=o[t],r=e.length>0;return void 0!==i?ge(i,n).map((t=>({url:{value:t.value,meta:{text:r?e:t.text,attach:h}},text:r?e:t.text}))):p.none()},ve=(e,t)=>{const n={text:e.text,title:e.title},o=e=>{const t=(o=e.url,S(n.text.length<=0,p.from(null===(i=o.meta)||void 0===i?void 0:i.text).getOr(o.value)));var o,i;const r=(e=>{var t;return S(n.title.length<=0,p.from(null===(t=e.meta)||void 0===t?void 0:t.title).getOr(""))})(e.url);return t.isSome()||r.isSome()?p.some({...t.map((e=>({text:e}))).getOr({}),...r.map((e=>({title:e}))).getOr({})}):p.none()},i=(e,o)=>{const i=(r=t,s=o,"link"===s?r.link:"anchor"===s?r.anchor:p.none()).getOr([]);var r,s;return Oe(n.text,o,i,e)};return{onChange:(e,t)=>{const r=t.name;return"url"===r?o(e()):O(["anchor","link"],r)?i(e(),r):"text"===r||"title"===r?(n[r]=e()[r],p.none()):p.none()}}};var be=tinymce.util.Tools.resolve("tinymce.util.Delay");const ye=e=>{const t=e.href;return t.indexOf("@")>0&&-1===t.indexOf("/")&&-1===t.indexOf("mailto:")?p.some({message:"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?",preprocess:e=>({...e,href:"mailto:"+t})}):p.none()},xe=(e,t)=>n=>{const o=n.href;return 1===e&&!Y(o)||0===e&&/^\s*www(\.|\d\.)/i.test(o)?p.some({message:`The URL you entered seems to be an external link. Do you want to add the required ${t}:// prefix?`,preprocess:e=>({...e,href:t+"://"+o})}):p.none()},we=(e,t)=>y([ye,xe(k(e),P(e))],(e=>e(t))).fold((()=>Promise.resolve(t)),(n=>new Promise((o=>{((e,t,n)=>{const o=e.selection.getRng();be.setEditorTimeout(e,(()=>{e.windowManager.confirm(t,(t=>{e.selection.setRng(o),n(t)}))}))})(e,n.message,(e=>{o(e?n.preprocess(t):t)}))})))),Se=e=>{const t=e.dom.select("a:not([href])"),n=b(t,(e=>{const t=e.name||e.id;return t?[{text:t,value:"#"+t}]:[]}));return n.length>0?p.some([{text:"None",value:""}].concat(n)):p.none()},_e=e=>{const t=E(e);return t.length>0?pe.sanitize(t):p.none()},ke=e=>{try{return p.some(JSON.parse(e))}catch(e){return p.none()}},Ce=e=>{const t=t=>e.convertURL(t.value||t.url||"","href"),n=T(e);return new Promise((e=>{o(n)?fetch(n).then((e=>e.ok?e.text().then(ke):Promise.reject())).then(e,(()=>e(p.none()))):u(n)?n((t=>e(p.some(t)))):e(p.from(n))})).then((e=>e.bind(pe.sanitizeWith(t)).map((e=>{if(e.length>0){return[{text:"None",value:""}].concat(e)}return e}))))},Te=(e,t)=>{const n=Q(e);if(n.length>0){const o=x(t,"_blank"),i=e=>q(pe.getValue(e),o);return(!1===D(e)?pe.sanitizeWith(i):pe.sanitize)(n)}return p.none()},$e=[{text:"Current window",value:""},{text:"New window",value:"_blank"}],Pe=e=>{const t=A(e);return r(t)?pe.sanitize(t).orThunk((()=>p.some($e))):!1===t?p.none():p.some($e)},Ae=(e,t,n)=>{const o=e.getAttrib(t,n);return null!==o&&o.length>0?p.some(o):p.none()},Qe=(e,t)=>Ce(e).then((n=>{const o=((e,t)=>{const n=e.dom,o=ne(e)?p.some(K(e.selection,t)):p.none(),i=t.bind((e=>p.from(n.getAttrib(e,"href")))),r=t.bind((e=>p.from(n.getAttrib(e,"target")))),s=t.bind((e=>Ae(n,e,"rel"))),a=t.bind((e=>Ae(n,e,"class")));return{url:i,text:o,title:t.bind((e=>Ae(n,e,"title"))),target:r,rel:s,linkClass:a}})(e,t);return{anchor:o,catalogs:{targets:Pe(e),rels:Te(e,o.target),classes:_e(e),anchor:Se(e),link:n},optNode:t,flags:{titleEnabled:M(e)}}})),Ee=e=>{const t=(e=>{const t=H(e);return Qe(e,t)})(e);t.then((t=>{const n=((e,t)=>n=>{const o=n.getData();if(!o.url.value)return le(e),void n.close();const i=e=>p.from(o[e]).filter((n=>!x(t.anchor[e],n))),r={href:o.url.value,text:i("text"),target:i("target"),rel:i("rel"),class:i("linkClass"),title:i("title")},s={href:o.url.value,attach:void 0!==o.url.meta&&o.url.meta.attach?o.url.meta.attach:h};we(e,r).then((t=>{ae(e,s,t)})),n.close()})(e,t);return((e,t,n)=>{const o=e.anchor.text.map((()=>({name:"text",type:"input",label:"Text to display"}))).toArray(),i=e.flags.titleEnabled?[{name:"title",type:"input",label:"Title"}]:[],r=((e,t)=>{const n=e.anchor,o=n.url.getOr("");return{url:{value:o,meta:{original:{value:o}}},text:n.text.getOr(""),title:n.title.getOr(""),anchor:o,link:o,rel:n.rel.getOr(""),target:n.target.or(t).getOr(""),linkClass:n.linkClass.getOr("")}})(e,p.from($(n))),s=e.catalogs,a=ve(r,s);return{title:"Insert/Edit Link",size:"normal",body:{type:"panel",items:v([[{name:"url",type:"urlinput",filetype:"file",label:"URL",picker_text:"Browse links"}],o,i,w([s.anchor.map(pe.createUi("anchor","Anchors")),s.rels.map(pe.createUi("rel","Rel")),s.targets.map(pe.createUi("target","Open link in...")),s.link.map(pe.createUi("link","Link list")),s.classes.map(pe.createUi("linkClass","Class"))])])},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:r,onChange:(e,{name:t})=>{a.onChange(e.getData,{name:t}).each((t=>{e.setData(t)}))},onSubmit:t}})(t,n,e)})).then((t=>{e.windowManager.open(t)}))},Me=()=>{const e=(e=>{const t=(e=>{let t=e;return{get:()=>t,set:e=>{t=e}}})(p.none()),n=()=>t.get().each(e);return{clear:()=>{n(),t.set(p.none())},isSet:()=>t.get().isSome(),get:()=>t.get(),set:e=>{n(),t.set(p.some(e))}}})(h);return{...e,on:t=>e.get().each(t)}},De=(e,t)=>((e,t,n)=>""===t||e.length>=t.length&&e.substr(n,n+t.length)===t)(e,t,0);var Ie=tinymce.util.Tools.resolve("tinymce.util.VK");const Re=e=>{const t=document.createElement("a");t.target="_blank",t.href=e,t.rel="noreferrer noopener";const n=new MouseEvent("click",{bubbles:!0,cancelable:!0,view:window});document.dispatchEvent(n),((e,t)=>{document.body.appendChild(e),e.dispatchEvent(t),document.body.removeChild(e)})(t,n)},ze=(e,t)=>{if(t){const i=F(t);if(/^#/.test(i)){const t=e.dom.select(`${i},[name="${n=i,o="#",De(n,o)?((e,t)=>e.substring(t))(n,o.length):n}"]`);t.length&&e.selection.scrollIntoView(t[0],!0)}else Re(t.href)}var n,o},Ne=(e,t)=>{const n=ee(e.dom.getParents(t));return S(1===n.length,n[0])},Le=e=>e.selection.isCollapsed()||(e=>{const t=e.selection.getRng(),n=t.startContainer;return W(n)&&t.startContainer===t.endContainer&&1===e.dom.select("img",n).length})(e)?Ne(e,e.selection.getStart()):(e=>{const t=J(e.selection.getRng());return S(t.length>0,t[0]).or(Ne(e,e.selection.getNode()))})(e),je=e=>()=>{e.execCommand("mceLink",!1,{dialog:!0})},Ve=(e,t)=>(e.on("NodeChange",t),()=>e.off("NodeChange",t)),Be=e=>t=>{const n=()=>{t.setActive(!e.mode.isReadOnly()&&G(e,e.selection.getNode())),t.setEnabled(e.selection.isEditable())};return n(),Ve(e,n)},Ze=e=>t=>{const n=()=>{t.setEnabled(e.selection.isEditable())};return n(),Ve(e,n)},Xe=e=>t=>{const n=t=>{return te(t)||(n=e.selection.getRng(),J(n).length>0);var n},o=e.dom.getParents(e.selection.getStart()),i=o=>{t.setEnabled(n(o)&&e.selection.isEditable())};return i(o),Ve(e,(e=>i(e.parents)))},We=(e,t)=>{const n=t=>{const n=e.selection.getNode();return t.setEnabled(G(e,n)&&e.selection.isEditable()),h};e.ui.registry.addContextForm("quicklink",{launch:{type:"contextformtogglebutton",icon:"link",tooltip:"Link",onSetup:Be(e)},label:"Link",predicate:t=>C(e)&&G(e,t),initValue:()=>H(e).fold((e=>()=>e)(""),F),commands:[{type:"contextformtogglebutton",icon:"link",tooltip:"Link",primary:!0,onSetup:t=>{const n=e.selection.getNode();return t.setActive(G(e,n)),Be(e)(t)},onAction:t=>{const n=t.getValue(),o=(t=>{const n=H(e),o=ne(e);if(n.isNone()&&o){const o=K(e.selection,n);return S(0===o.length,t)}return p.none()})(n);ae(e,{href:n,attach:h},{href:n,text:o,title:p.none(),rel:p.none(),target:p.from($(e)),class:p.none()}),(e=>{e.selection.collapse(!1)})(e),t.hide()}},{type:"contextformbutton",icon:"unlink",tooltip:"Remove link",onSetup:n,onAction:t=>{le(e),t.hide()}},{type:"contextformbutton",icon:"new-tab",tooltip:"Open link",onSetup:n,onAction:e=>{t.gotoSelectedLink(),e.hide()}}]})},Ue=e=>{const t=(e=>{const t=Me(),n=()=>t.get().or(Le(e));return e.on("contextmenu",(n=>{Ne(e,n.target).each(t.set)})),e.on("SelectionChange",(()=>{t.isSet()||Le(e).each(t.set)})),e.on("click",(n=>{t.clear();const o=ee(e.dom.getParents(n.target));1===o.length&&Ie.metaKeyPressed(n)&&(n.preventDefault(),ze(e,o[0]))})),e.on("keydown",(o=>{t.clear(),!o.isDefaultPrevented()&&13===o.keyCode&&(e=>!0===e.altKey&&!1===e.shiftKey&&!1===e.ctrlKey&&!1===e.metaKey)(o)&&n().each((t=>{o.preventDefault(),ze(e,t)}))})),{gotoSelectedLink:()=>n().each((t=>ze(e,t)))}})(e);((e,t)=>{e.ui.registry.addToggleButton("link",{icon:"link",tooltip:"Insert/edit link",shortcut:"Meta+K",onAction:je(e),onSetup:Be(e)}),e.ui.registry.addButton("openlink",{icon:"new-tab",tooltip:"Open link",onAction:t.gotoSelectedLink,onSetup:Xe(e)}),e.ui.registry.addButton("unlink",{icon:"unlink",tooltip:"Remove link",onAction:()=>le(e),onSetup:Xe(e)})})(e,t),((e,t)=>{e.ui.registry.addMenuItem("openlink",{text:"Open link",icon:"new-tab",onAction:t.gotoSelectedLink,onSetup:Xe(e)}),e.ui.registry.addMenuItem("link",{icon:"link",text:"Link...",shortcut:"Meta+K",onAction:je(e),onSetup:Ze(e)}),e.ui.registry.addMenuItem("unlink",{icon:"unlink",text:"Remove link",onAction:()=>le(e),onSetup:Xe(e)})})(e,t),(e=>{e.ui.registry.addContextMenu("link",{update:t=>e.dom.isEditable(t)?te(e.dom.getParents(t,"a"))?"link unlink openlink":"link":""})})(e),We(e,t)};e.add("link",(e=>{(e=>{const t=e.options.register;t("link_assume_external_targets",{processor:e=>{const t=o(e)||l(e);return t?!0===e?{value:1,valid:t}:"http"===e||"https"===e?{value:e,valid:t}:{value:0,valid:t}:{valid:!1,message:"Must be a string or a boolean."}},default:!1}),t("link_context_toolbar",{processor:"boolean",default:!1}),t("link_list",{processor:e=>o(e)||u(e)||d(e,i)}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"}),t("link_target_list",{processor:e=>l(e)||d(e,i),default:!0}),t("link_rel_list",{processor:"object[]",default:[]}),t("link_class_list",{processor:"object[]",default:[]}),t("link_title",{processor:"boolean",default:!0}),t("allow_unsafe_link_target",{processor:"boolean",default:!1}),t("link_quicklink",{processor:"boolean",default:!1}),t("link_attributes_postprocess",{processor:"function"})})(e),(e=>{e.addCommand("mceLink",((t,n)=>{!0!==(null==n?void 0:n.dialog)&&I(e)?e.dispatch("contexttoolbar-show",{toolbarKey:"quicklink"}):Ee(e)}))})(e),Ue(e),(e=>{e.addShortcut("Meta+K","",(()=>{e.execCommand("mceLink")}))})(e)}))}()},128:()=>{tinymce.PluginManager.add("face",(function(e,t){function n(t){tinymce.activeEditor.windowManager.open({title:"AI顔認識",body:{type:"panel",items:[{type:"alertbanner",level:"info",text:"写真の顔をAIで判断して、モザイク処理を施します。",icon:"info"},{type:"urlinput",name:"photo",filetype:"file",label:"jpg, png 形式の画像ファイル"},{type:"collection",name:"upload_max_filesize_caption",label:e.options.get("cc_config").upload_max_filesize_caption},{type:"input",name:"alt",inputMode:"text",label:"代替テキスト",placeholder:"",disabled:!1,maximized:!1},{type:"listbox",name:"image_size",label:"画像サイズ(最大でこの大きさに縮小されます)",disabled:!1,items:e.options.get("cc_config").face_image_sizes},{type:"listbox",name:"mosaic_fineness",label:"モザイクの粗さ",disabled:!1,items:e.options.get("cc_config").finenesses}]},initialData:{image_size:e.options.get("cc_config").face_image_initial,mosaic_fineness:e.options.get("cc_config").fineness_initial},buttons:[{type:"cancel",text:"Close"},{type:"submit",text:"Save",primary:!0}],onSubmit:function(t){xhr=new XMLHttpRequest,xhr.withCredentials=!1,xhr.open("POST",tinymce.activeEditor.getParam("document_base_url")+"/upload/face"),xhr.onload=function(){var n;xhr.status<200||xhr.status>=300?console.error("HTTP Error: "+xhr.status):(void 0!==(n=JSON.parse(xhr.responseText)).link_text&&e.insertContent(n.link_text),document.getElementById("cc-face-upload-"+r).value="",t.close())};var n=document.getElementsByName("csrf-token"),o=document.getElementsByName("_page_id"),i=t.getData(),r=e.options.get("cc_config").frame_id;formData=new FormData,formData.append("_token",n[0].content),formData.append("page_id",o[0].content),formData.append("plugin_name",e.options.get("cc_config").plugin_name),formData.append("photo",document.getElementById("cc-face-upload-"+r).files[0]),formData.append("alt",i.alt),formData.append("image_size",i.image_size),formData.append("mosaic_fineness",i.mosaic_fineness),xhr.send(formData)}})}e.ui.registry.addIcon("face",'\x3c!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--\x3e'),e.ui.registry.addButton("face",{icon:"face",tooltip:"AI顔認識",onAction:n,onPostRender:function(){var t=this;e.on("NodeChange",(function(n){var o=jQuery(e.selection.getNode()).hasClass("plugin");t.active(o)})),e.on("DblClick",(function(e){"plugin"==e.target.className&&n(e.toElement.innerText)}))}})}))},246:(e,t,n)=>{"use strict";function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function i(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}n.d(t,{Ay:()=>at});function c(e){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(e)}var u=c(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),d=c(/Edge/i),h=c(/firefox/i),f=c(/safari/i)&&!c(/chrome/i)&&!c(/android/i),p=c(/iP(ad|od|hone)/i),m=c(/chrome/i)&&c(/android/i),g={capture:!1,passive:!1};function O(e,t,n){e.addEventListener(t,n,!u&&g)}function v(e,t,n){e.removeEventListener(t,n,!u&&g)}function b(e,t){if(t){if(">"===t[0]&&(t=t.substring(1)),e)try{if(e.matches)return e.matches(t);if(e.msMatchesSelector)return e.msMatchesSelector(t);if(e.webkitMatchesSelector)return e.webkitMatchesSelector(t)}catch(e){return!1}return!1}}function y(e){return e.host&&e!==document&&e.host.nodeType?e.host:e.parentNode}function x(e,t,n,o){if(e){n=n||document;do{if(null!=t&&(">"===t[0]?e.parentNode===n&&b(e,t):b(e,t))||o&&e===n)return e;if(e===n)break}while(e=y(e))}return null}var w,S=/\s+/g;function _(e,t,n){if(e&&t)if(e.classList)e.classList[n?"add":"remove"](t);else{var o=(" "+e.className+" ").replace(S," ").replace(" "+t+" "," ");e.className=(o+(n?" "+t:"")).replace(S," ")}}function k(e,t,n){var o=e&&e.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(e,""):e.currentStyle&&(n=e.currentStyle),void 0===t?n:n[t];t in o||-1!==t.indexOf("webkit")||(t="-webkit-"+t),o[t]=n+("string"==typeof n?"":"px")}}function C(e,t){var n="";if("string"==typeof e)n=e;else do{var o=k(e,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!t&&(e=e.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function T(e,t,n){if(e){var o=e.getElementsByTagName(t),i=0,r=o.length;if(n)for(;i=r:i<=r))return o;if(o===$())break;o=I(o,!1)}return!1}function Q(e,t,n,o){for(var i=0,r=0,s=e.children;r2&&void 0!==arguments[2]?arguments[2]:{},o=n.evt,r=l(n,Y);W.pluginEvent.bind(je)(e,t,i({dragEl:H,parentEl:G,ghostEl:K,rootEl:J,nextEl:ee,lastDownEl:te,cloneEl:ne,cloneHidden:oe,dragStarted:ge,putSortable:ce,activeSortable:je.active,originalEvent:o,oldIndex:ie,oldDraggableIndex:se,newIndex:re,newDraggableIndex:ae,hideGhostForTarget:Re,unhideGhostForTarget:ze,cloneNowHidden:function(){oe=!0},cloneNowShown:function(){oe=!1},dispatchSortableEvent:function(e){q({sortable:t,name:e,originalEvent:o})}},r))};function q(e){U(i({putSortable:ce,cloneEl:ne,targetEl:H,rootEl:J,oldIndex:ie,oldDraggableIndex:se,newIndex:re,newDraggableIndex:ae},e))}var H,G,K,J,ee,te,ne,oe,ie,re,se,ae,le,ce,ue,de,he,fe,pe,me,ge,Oe,ve,be,ye,xe=!1,we=!1,Se=[],_e=!1,ke=!1,Ce=[],Te=!1,$e=[],Pe="undefined"!=typeof document,Ae=p,Qe=d||u?"cssFloat":"float",Ee=Pe&&!m&&!p&&"draggable"in document.createElement("div"),Me=function(){if(Pe){if(u)return!1;var e=document.createElement("x");return e.style.cssText="pointer-events:auto","auto"===e.style.pointerEvents}}(),De=function(e,t){var n=k(e),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),i=Q(e,0,t),r=Q(e,1,t),s=i&&k(i),a=r&&k(r),l=s&&parseInt(s.marginLeft)+parseInt(s.marginRight)+P(i).width,c=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+P(r).width;if("flex"===n.display)return"column"===n.flexDirection||"column-reverse"===n.flexDirection?"vertical":"horizontal";if("grid"===n.display)return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(i&&s.float&&"none"!==s.float){var u="left"===s.float?"left":"right";return!r||"both"!==a.clear&&a.clear!==u?"horizontal":"vertical"}return i&&("block"===s.display||"flex"===s.display||"table"===s.display||"grid"===s.display||l>=o&&"none"===n[Qe]||r&&"none"===n[Qe]&&l+c>o)?"vertical":"horizontal"},Ie=function(e){function t(e,n){return function(o,i,r,s){var a=o.options.group.name&&i.options.group.name&&o.options.group.name===i.options.group.name;if(null==e&&(n||a))return!0;if(null==e||!1===e)return!1;if(n&&"clone"===e)return e;if("function"==typeof e)return t(e(o,i,r,s),n)(o,i,r,s);var l=(n?o:i).options.group.name;return!0===e||"string"==typeof e&&e===l||e.join&&e.indexOf(l)>-1}}var n={},o=e.group;o&&"object"==r(o)||(o={name:o}),n.name=o.name,n.checkPull=t(o.pull,!0),n.checkPut=t(o.put),n.revertClone=o.revertClone,e.group=n},Re=function(){!Me&&K&&k(K,"display","none")},ze=function(){!Me&&K&&k(K,"display","")};Pe&&!m&&document.addEventListener("click",(function(e){if(we)return e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.stopImmediatePropagation&&e.stopImmediatePropagation(),we=!1,!1}),!0);var Ne=function(e){if(H){e=e.touches?e.touches[0]:e;var t=(i=e.clientX,r=e.clientY,Se.some((function(e){var t=e[V].options.emptyInsertThreshold;if(t&&!E(e)){var n=P(e),o=i>=n.left-t&&i<=n.right+t,a=r>=n.top-t&&r<=n.bottom+t;return o&&a?s=e:void 0}})),s);if(t){var n={};for(var o in e)e.hasOwnProperty(o)&&(n[o]=e[o]);n.target=n.rootEl=t,n.preventDefault=void 0,n.stopPropagation=void 0,t[V]._onDragOver(n)}}var i,r,s},Le=function(e){H&&H.parentNode[V]._isOutsideThisEl(e.target)};function je(e,t){if(!e||!e.nodeType||1!==e.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(e));this.el=e,this.options=t=a({},t),e[V]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(e.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return De(e,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(e,t){e.setData("Text",t.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==je.supportPointer&&"PointerEvent"in window&&(!f||p),emptyInsertThreshold:5};for(var o in W.initializePlugins(this,e,n),n)!(o in t)&&(t[o]=n[o]);for(var i in Ie(t),this)"_"===i.charAt(0)&&"function"==typeof this[i]&&(this[i]=this[i].bind(this));this.nativeDraggable=!t.forceFallback&&Ee,this.nativeDraggable&&(this.options.touchStartThreshold=1),t.supportPointer?O(e,"pointerdown",this._onTapStart):(O(e,"mousedown",this._onTapStart),O(e,"touchstart",this._onTapStart)),this.nativeDraggable&&(O(e,"dragover",this),O(e,"dragenter",this)),Se.push(this.el),t.store&&t.store.get&&this.sort(t.store.get(this)||[]),a(this,B())}function Ve(e,t,n,o,i,r,s,a){var l,c,h=e[V],f=h.options.onMove;return!window.CustomEvent||u||d?(l=document.createEvent("Event")).initEvent("move",!0,!0):l=new CustomEvent("move",{bubbles:!0,cancelable:!0}),l.to=t,l.from=e,l.dragged=n,l.draggedRect=o,l.related=i||t,l.relatedRect=r||P(t),l.willInsertAfter=a,l.originalEvent=s,e.dispatchEvent(l),f&&(c=f.call(h,l,s)),c}function Be(e){e.draggable=!1}function Ze(){Te=!1}function Xe(e){for(var t=e.tagName+e.className+e.src+e.href+e.textContent,n=t.length,o=0;n--;)o+=t.charCodeAt(n);return o.toString(36)}function We(e){return setTimeout(e,0)}function Ue(e){return clearTimeout(e)}je.prototype={constructor:je,_isOutsideThisEl:function(e){this.el.contains(e)||e===this.el||(Oe=null)},_getDirection:function(e,t){return"function"==typeof this.options.direction?this.options.direction.call(this,e,t,H):this.options.direction},_onTapStart:function(e){if(e.cancelable){var t=this,n=this.el,o=this.options,i=o.preventOnFilter,r=e.type,s=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,a=(s||e).target,l=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||a,c=o.filter;if(function(e){$e.length=0;var t=e.getElementsByTagName("input"),n=t.length;for(;n--;){var o=t[n];o.checked&&$e.push(o)}}(n),!H&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||o.disabled)&&!l.isContentEditable&&(this.nativeDraggable||!f||!a||"SELECT"!==a.tagName.toUpperCase())&&!((a=x(a,o.draggable,n,!1))&&a.animated||te===a)){if(ie=M(a),se=M(a,o.draggable),"function"==typeof c){if(c.call(this,e,a,this))return q({sortable:t,rootEl:l,name:"filter",targetEl:a,toEl:n,fromEl:n}),F("filter",t,{evt:e}),void(i&&e.preventDefault())}else if(c&&(c=c.split(",").some((function(o){if(o=x(l,o.trim(),n,!1))return q({sortable:t,rootEl:o,name:"filter",targetEl:a,fromEl:n,toEl:n}),F("filter",t,{evt:e}),!0}))))return void(i&&e.preventDefault());o.handle&&!x(l,o.handle,n,!1)||this._prepareDragStart(e,s,a)}}},_prepareDragStart:function(e,t,n){var o,i=this,r=i.el,s=i.options,a=r.ownerDocument;if(n&&!H&&n.parentNode===r){var l=P(n);if(J=r,G=(H=n).parentNode,ee=H.nextSibling,te=n,le=s.group,je.dragged=H,ue={target:H,clientX:(t||e).clientX,clientY:(t||e).clientY},pe=ue.clientX-l.left,me=ue.clientY-l.top,this._lastX=(t||e).clientX,this._lastY=(t||e).clientY,H.style["will-change"]="all",o=function(){F("delayEnded",i,{evt:e}),je.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!h&&i.nativeDraggable&&(H.draggable=!0),i._triggerDragStart(e,t),q({sortable:i,name:"choose",originalEvent:e}),_(H,s.chosenClass,!0))},s.ignore.split(",").forEach((function(e){T(H,e.trim(),Be)})),O(a,"dragover",Ne),O(a,"mousemove",Ne),O(a,"touchmove",Ne),s.supportPointer?(O(a,"pointerup",i._onDrop),!this.nativeDraggable&&O(a,"pointercancel",i._onDrop)):(O(a,"mouseup",i._onDrop),O(a,"touchend",i._onDrop),O(a,"touchcancel",i._onDrop)),h&&this.nativeDraggable&&(this.options.touchStartThreshold=4,H.draggable=!0),F("delayStart",this,{evt:e}),!s.delay||s.delayOnTouchOnly&&!t||this.nativeDraggable&&(d||u))o();else{if(je.eventCanceled)return void this._onDrop();s.supportPointer?(O(a,"pointerup",i._disableDelayedDrag),O(a,"pointercancel",i._disableDelayedDrag)):(O(a,"mouseup",i._disableDelayedDrag),O(a,"touchend",i._disableDelayedDrag),O(a,"touchcancel",i._disableDelayedDrag)),O(a,"mousemove",i._delayedDragTouchMoveHandler),O(a,"touchmove",i._delayedDragTouchMoveHandler),s.supportPointer&&O(a,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,s.delay)}}},_delayedDragTouchMoveHandler:function(e){var t=e.touches?e.touches[0]:e;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){H&&Be(H),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var e=this.el.ownerDocument;v(e,"mouseup",this._disableDelayedDrag),v(e,"touchend",this._disableDelayedDrag),v(e,"touchcancel",this._disableDelayedDrag),v(e,"pointerup",this._disableDelayedDrag),v(e,"pointercancel",this._disableDelayedDrag),v(e,"mousemove",this._delayedDragTouchMoveHandler),v(e,"touchmove",this._delayedDragTouchMoveHandler),v(e,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(e,t){t=t||"touch"==e.pointerType&&e,!this.nativeDraggable||t?this.options.supportPointer?O(document,"pointermove",this._onTouchMove):O(document,t?"touchmove":"mousemove",this._onTouchMove):(O(H,"dragend",this),O(J,"dragstart",this._onDragStart));try{document.selection?We((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(e){}},_dragStarted:function(e,t){if(xe=!1,J&&H){F("dragStarted",this,{evt:t}),this.nativeDraggable&&O(document,"dragover",Le);var n=this.options;!e&&_(H,n.dragClass,!1),_(H,n.ghostClass,!0),je.active=this,e&&this._appendGhost(),q({sortable:this,name:"start",originalEvent:t})}else this._nulling()},_emulateDragOver:function(){if(de){this._lastX=de.clientX,this._lastY=de.clientY,Re();for(var e=document.elementFromPoint(de.clientX,de.clientY),t=e;e&&e.shadowRoot&&(e=e.shadowRoot.elementFromPoint(de.clientX,de.clientY))!==t;)t=e;if(H.parentNode[V]._isOutsideThisEl(e),t)do{if(t[V]){if(t[V]._onDragOver({clientX:de.clientX,clientY:de.clientY,target:e,rootEl:t})&&!this.options.dragoverBubble)break}e=t}while(t=y(t));ze()}},_onTouchMove:function(e){if(ue){var t=this.options,n=t.fallbackTolerance,o=t.fallbackOffset,i=e.touches?e.touches[0]:e,r=K&&C(K,!0),s=K&&r&&r.a,a=K&&r&&r.d,l=Ae&&ye&&D(ye),c=(i.clientX-ue.clientX+o.x)/(s||1)+(l?l[0]-Ce[0]:0)/(s||1),u=(i.clientY-ue.clientY+o.y)/(a||1)+(l?l[1]-Ce[1]:0)/(a||1);if(!je.active&&!xe){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))i.right+r||e.clientY>o.bottom&&e.clientX>o.left:e.clientY>i.bottom+r||e.clientX>o.right&&e.clientY>o.top}(e,r,this)&&!g.animated){if(g===H)return Z(!1);if(g&&s===e.target&&(a=g),a&&(n=P(a)),!1!==Ve(J,s,H,t,a,n,e,!!a))return B(),g&&g.nextSibling?s.insertBefore(H,g.nextSibling):s.appendChild(H),G=s,X(),Z(!0)}else if(g&&function(e,t,n){var o=P(Q(n.el,0,n.options,!0)),i=j(n.el,n.options,K),r=10;return t?e.clientXu+c*r/2:ld-be)return-ve}else if(l>u+c*(1-i)/2&&ld-c*r/2))return l>u+c/2?1:-1;return 0}(e,a,n,r,S?1:l.swapThreshold,null==l.invertedSwapThreshold?l.swapThreshold:l.invertedSwapThreshold,ke,Oe===a),0!==v){var D=M(H);do{D-=v,y=G.children[D]}while(y&&("none"===k(y,"display")||y===K))}if(0===v||y===a)return Z(!1);Oe=a,ve=v;var I=a.nextElementSibling,R=!1,z=Ve(J,s,H,t,a,n,e,R=1===v);if(!1!==z)return 1!==z&&-1!==z||(R=1===z),Te=!0,setTimeout(Ze,30),B(),R&&!I?s.appendChild(H):a.parentNode.insertBefore(H,R?I:a),T&&N(T,0,$-T.scrollTop),G=H.parentNode,void 0===b||ke||(be=Math.abs(b-P(a)[C])),X(),Z(!0)}if(s.contains(H))return Z(!1)}return!1}function L(l,c){F(l,p,i({evt:e,isOwner:d,axis:r?"vertical":"horizontal",revert:o,dragRect:t,targetRect:n,canSort:h,fromSortable:f,target:a,completed:Z,onMove:function(n,o){return Ve(J,s,H,t,n,P(n),e,o)},changed:X},c))}function B(){L("dragOverAnimationCapture"),p.captureAnimationState(),p!==f&&f.captureAnimationState()}function Z(t){return L("dragOverCompleted",{insertion:t}),t&&(d?u._hideClone():u._showClone(p),p!==f&&(_(H,ce?ce.options.ghostClass:u.options.ghostClass,!1),_(H,l.ghostClass,!0)),ce!==p&&p!==je.active?ce=p:p===je.active&&ce&&(ce=null),f===p&&(p._ignoreWhileAnimating=a),p.animateAll((function(){L("dragOverAnimationComplete"),p._ignoreWhileAnimating=null})),p!==f&&(f.animateAll(),f._ignoreWhileAnimating=null)),(a===H&&!H.animated||a===s&&!a.animated)&&(Oe=null),l.dragoverBubble||e.rootEl||a===document||(H.parentNode[V]._isOutsideThisEl(e.target),!t&&Ne(e)),!l.dragoverBubble&&e.stopPropagation&&e.stopPropagation(),m=!0}function X(){re=M(H),ae=M(H,l.draggable),q({sortable:p,name:"change",toEl:s,newIndex:re,newDraggableIndex:ae,originalEvent:e})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){v(document,"mousemove",this._onTouchMove),v(document,"touchmove",this._onTouchMove),v(document,"pointermove",this._onTouchMove),v(document,"dragover",Ne),v(document,"mousemove",Ne),v(document,"touchmove",Ne)},_offUpEvents:function(){var e=this.el.ownerDocument;v(e,"mouseup",this._onDrop),v(e,"touchend",this._onDrop),v(e,"pointerup",this._onDrop),v(e,"pointercancel",this._onDrop),v(e,"touchcancel",this._onDrop),v(document,"selectstart",this)},_onDrop:function(e){var t=this.el,n=this.options;re=M(H),ae=M(H,n.draggable),F("drop",this,{evt:e}),G=H&&H.parentNode,re=M(H),ae=M(H,n.draggable),je.eventCanceled||(xe=!1,ke=!1,_e=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),Ue(this.cloneId),Ue(this._dragStartId),this.nativeDraggable&&(v(document,"drop",this),v(t,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),f&&k(document.body,"user-select",""),k(H,"transform",""),e&&(ge&&(e.cancelable&&e.preventDefault(),!n.dropBubble&&e.stopPropagation()),K&&K.parentNode&&K.parentNode.removeChild(K),(J===G||ce&&"clone"!==ce.lastPutMode)&&ne&&ne.parentNode&&ne.parentNode.removeChild(ne),H&&(this.nativeDraggable&&v(H,"dragend",this),Be(H),H.style["will-change"]="",ge&&!xe&&_(H,ce?ce.options.ghostClass:this.options.ghostClass,!1),_(H,this.options.chosenClass,!1),q({sortable:this,name:"unchoose",toEl:G,newIndex:null,newDraggableIndex:null,originalEvent:e}),J!==G?(re>=0&&(q({rootEl:G,name:"add",toEl:G,fromEl:J,originalEvent:e}),q({sortable:this,name:"remove",toEl:G,originalEvent:e}),q({rootEl:G,name:"sort",toEl:G,fromEl:J,originalEvent:e}),q({sortable:this,name:"sort",toEl:G,originalEvent:e})),ce&&ce.save()):re!==ie&&re>=0&&(q({sortable:this,name:"update",toEl:G,originalEvent:e}),q({sortable:this,name:"sort",toEl:G,originalEvent:e})),je.active&&(null!=re&&-1!==re||(re=ie,ae=se),q({sortable:this,name:"end",toEl:G,originalEvent:e}),this.save())))),this._nulling()},_nulling:function(){F("nulling",this),J=H=G=K=ee=ne=te=oe=ue=de=ge=re=ae=ie=se=Oe=ve=ce=le=je.dragged=je.ghost=je.clone=je.active=null,$e.forEach((function(e){e.checked=!0})),$e.length=he=fe=0},handleEvent:function(e){switch(e.type){case"drop":case"dragend":this._onDrop(e);break;case"dragenter":case"dragover":H&&(this._onDragOver(e),function(e){e.dataTransfer&&(e.dataTransfer.dropEffect="move");e.cancelable&&e.preventDefault()}(e));break;case"selectstart":e.preventDefault()}},toArray:function(){for(var e,t=[],n=this.el.children,o=0,i=n.length,r=this.options;o{t.read=function(e,t,n,o,i){var r,s,a=8*i-o-1,l=(1<>1,u=-7,d=n?i-1:0,h=n?-1:1,f=e[t+d];for(d+=h,r=f&(1<<-u)-1,f>>=-u,u+=a;u>0;r=256*r+e[t+d],d+=h,u-=8);for(s=r&(1<<-u)-1,r>>=-u,u+=o;u>0;s=256*s+e[t+d],d+=h,u-=8);if(0===r)r=1-c;else{if(r===l)return s?NaN:1/0*(f?-1:1);s+=Math.pow(2,o),r-=c}return(f?-1:1)*s*Math.pow(2,r-o)},t.write=function(e,t,n,o,i,r){var s,a,l,c=8*r-i-1,u=(1<>1,h=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,f=o?0:r-1,p=o?1:-1,m=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=u):(s=Math.floor(Math.log(t)/Math.LN2),t*(l=Math.pow(2,-s))<1&&(s--,l*=2),(t+=s+d>=1?h/l:h*Math.pow(2,1-d))*l>=2&&(s++,l/=2),s+d>=u?(a=0,s=u):s+d>=1?(a=(t*l-1)*Math.pow(2,i),s+=d):(a=t*Math.pow(2,d-1)*Math.pow(2,i),s=0));i>=8;e[n+f]=255&a,f+=p,a/=256,i-=8);for(s=s<0;e[n+f]=255&s,f+=p,s/=256,c-=8);e[n+f-p]|=128*m}},307:(e,t,n)=>{n(6272),window.Vue=n(9726).default},378:(e,t,n)=>{n(95)},1093:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});var o=n(6314),i=n.n(o)()((function(e){return e[1]}));i.push([e.id,'/*!\n * TOAST UI ImageEditor\n * @version 3.15.3\n * @license MIT\n */body>textarea{position:fixed!important}.tui-image-editor-container{background-color:#282828;-webkit-box-sizing:border-box;box-sizing:border-box;height:100%;letter-spacing:.3px;margin:0;min-height:300px;overflow:hidden;padding:0;position:relative}.tui-image-editor-container div,.tui-image-editor-container input,.tui-image-editor-container label,.tui-image-editor-container li,.tui-image-editor-container ul{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;-ms-user-select:none;-moz-user-select:-moz-none;-webkit-user-select:none;user-select:none}.tui-image-editor-container .tui-image-editor-header{background-color:#151515;min-width:533px;position:absolute;top:0;width:100%}.tui-image-editor-container .tui-image-editor-controls-buttons,.tui-image-editor-container .tui-image-editor-header-buttons{float:right;margin:8px}.tui-image-editor-container .tui-image-editor-controls-logo,.tui-image-editor-container .tui-image-editor-header-logo{float:left;padding:17px;width:30%}.tui-image-editor-container .tui-image-editor-controls-buttons,.tui-image-editor-container .tui-image-editor-controls-logo{display:none;height:100%;width:270px}.tui-image-editor-container .tui-image-editor-controls-buttons button,.tui-image-editor-container .tui-image-editor-controls-buttons div,.tui-image-editor-container .tui-image-editor-header-buttons button,.tui-image-editor-container .tui-image-editor-header-buttons div{border:1px solid #ddd;border-radius:20px;cursor:pointer;display:inline-block;font-family:Noto Sans,sans-serif;font-size:12px;font-weight:700;height:40px;letter-spacing:.3px;line-height:40px;outline:none;padding:0;position:relative;text-align:center;vertical-align:middle;width:120px}.tui-image-editor-container .tui-image-editor-download-btn{background-color:#fdba3b;border-color:#fdba3b;color:#fff}.tui-image-editor-container .tui-image-editor-load-btn{bottom:0;cursor:pointer;display:inline-block;left:0;opacity:0;position:absolute;right:0;top:0;width:100%}.tui-image-editor-container .tui-image-editor-main-container{bottom:64px;position:absolute;top:0;width:100%}.tui-image-editor-container .tui-image-editor-main{bottom:0;left:0;position:absolute;right:0;text-align:center;top:64px}.tui-image-editor-container .tui-image-editor-wrap{bottom:0;overflow:auto;width:100%}.tui-image-editor-container .tui-image-editor-wrap .tui-image-editor-size-wrap{display:table;height:100%;width:100%}.tui-image-editor-container .tui-image-editor-wrap .tui-image-editor-size-wrap .tui-image-editor-align-wrap{display:table-cell;vertical-align:middle}.tui-image-editor-container .tui-image-editor{display:inline-block;position:relative}.tui-image-editor-container .tui-image-editor-help-menu,.tui-image-editor-container .tui-image-editor-menu{display:table-cell;list-style:none;margin:0 auto;padding:0;text-align:center;vertical-align:middle;white-space:nowrap;width:auto}.tui-image-editor-container .tui-image-editor-help-menu>.tui-image-editor-item,.tui-image-editor-container .tui-image-editor-menu>.tui-image-editor-item{border-radius:2px;cursor:pointer;display:inline-block;margin:0 4px;padding:7px 8px 3px;position:relative}.tui-image-editor-container .tui-image-editor-help-menu>.tui-image-editor-item[tooltip-content]:hover:before,.tui-image-editor-container .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:hover:before{border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #2f2f2f;content:"";display:inline-block;height:0;left:13px;margin:0 auto;position:absolute;top:-2px;width:0}.tui-image-editor-container .tui-image-editor-help-menu>.tui-image-editor-item[tooltip-content]:hover:after,.tui-image-editor-container .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:hover:after{background-color:#2f2f2f;border-radius:3px;color:#fff;content:attr(tooltip-content);display:inline-block;font-size:11px;font-weight:lighter;left:0;max-height:23px;min-width:24px;padding:5px 8px;position:absolute;top:-25px}.tui-image-editor-container .tui-image-editor-help-menu>.tui-image-editor-item.active,.tui-image-editor-container .tui-image-editor-menu>.tui-image-editor-item.active{background-color:#fff;-webkit-transition:all .3s ease;transition:all .3s ease}.tui-image-editor-container .tui-image-editor-wrap{position:absolute}.tui-image-editor-container .tui-image-editor-grid-visual{border:1px solid hsla(0,0%,100%,.7);display:none;height:100%;position:absolute;width:100%}.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-flip .tui-image-editor,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-rotate .tui-image-editor{-webkit-transition:none;transition:none}.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-flip .tui-image-editor-grid-visual,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-resize .tui-image-editor-grid-visual,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-rotate .tui-image-editor-grid-visual{display:block}.tui-image-editor-container .tui-image-editor-grid-visual table{border-collapse:collapse;height:100%;width:100%}.tui-image-editor-container .tui-image-editor-grid-visual table td{border:1px solid hsla(0,0%,100%,.3)}.tui-image-editor-container .tui-image-editor-grid-visual table td.dot:before{background-color:#fff;border:0;border-radius:100%;-webkit-box-shadow:0 0 1px 0 rgba(0,0,0,.3);box-shadow:0 0 1px 0 rgba(0,0,0,.3);-webkit-box-sizing:border-box;box-sizing:border-box;content:"";height:10px;position:absolute;width:10px}.tui-image-editor-container .tui-image-editor-grid-visual table td.dot.left-top:before{left:-5px;top:-5px}.tui-image-editor-container .tui-image-editor-grid-visual table td.dot.right-top:before{right:-5px;top:-5px}.tui-image-editor-container .tui-image-editor-grid-visual table td.dot.left-bottom:before{bottom:-5px;left:-5px}.tui-image-editor-container .tui-image-editor-grid-visual table td.dot.right-bottom:before{bottom:-5px;right:-5px}.tui-image-editor-container .tui-image-editor-submenu{bottom:0;display:none;height:150px;position:absolute;white-space:nowrap;width:100%;z-index:2}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-button:hover svg>use.active{display:block}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item li{display:inline-block;vertical-align:top}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-image-editor-newline{display:block;margin-top:0}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-image-editor-button{cursor:pointer;display:inline-block;font-size:11px;font-weight:400;margin:0 9px;position:relative}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-image-editor-button.preset{margin:0 9px 20px 5px}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item label>span{cursor:pointer;display:inline-block;font-family:Noto Sans,sans-serif;font-size:11px;padding-top:5px}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-image-editor-button.apply label,.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-image-editor-button.cancel label{vertical-align:7px}.tui-image-editor-container .tui-image-editor-submenu>div{display:none;vertical-align:bottom}.tui-image-editor-container .tui-image-editor-submenu .tui-image-editor-submenu-style{bottom:0;display:block;left:0;opacity:.95;position:absolute;right:0;top:0;z-index:-1}.tui-image-editor-container .tui-image-editor-partition>div{border-left:1px solid #3c3c3c;height:52px;margin:0 8px;width:1px}.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-filter .tui-image-editor-partition>div{height:108px;margin:0 29px 0 0}.tui-image-editor-container .tui-image-editor-submenu-align{margin-right:30px;text-align:left}.tui-image-editor-container .tui-image-editor-submenu-align label>span{white-space:nowrap;width:55px}.tui-image-editor-container .tui-image-editor-submenu-align:first-child{margin-right:0}.tui-image-editor-container .tui-image-editor-submenu-align:first-child label>span{width:70px}.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-crop .tui-image-editor-submenu>div.tui-image-editor-menu-crop,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-draw .tui-image-editor-submenu>div.tui-image-editor-menu-draw,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-filter .tui-image-editor-submenu>div.tui-image-editor-menu-filter,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-flip .tui-image-editor-submenu>div.tui-image-editor-menu-flip,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-icon .tui-image-editor-submenu>div.tui-image-editor-menu-icon,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-mask .tui-image-editor-submenu>div.tui-image-editor-menu-mask,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-resize .tui-image-editor-submenu>div.tui-image-editor-menu-resize,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-rotate .tui-image-editor-submenu>div.tui-image-editor-menu-rotate,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-shape .tui-image-editor-submenu>div.tui-image-editor-menu-shape,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-text .tui-image-editor-submenu>div.tui-image-editor-menu-text,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-zoom .tui-image-editor-submenu>div.tui-image-editor-menu-zoom{display:table-cell}.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-crop .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-draw .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-filter .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-flip .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-icon .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-mask .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-resize .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-rotate .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-shape .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-text .tui-image-editor-submenu,.tui-image-editor-container .tui-image-editor-main.tui-image-editor-menu-zoom .tui-image-editor-submenu{display:table}.tui-image-editor-container .tui-image-editor-help-menu{background-color:hsla(0,0%,100%,.06);border-radius:20px;list-style:none;margin:0 auto;padding:0;position:absolute;text-align:center;vertical-align:middle;z-index:2}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history{background-color:#fff;-webkit-box-shadow:0 2px 6px 0 rgba(0,0,0,.15);box-shadow:0 2px 6px 0 rgba(0,0,0,.15);color:#444;cursor:auto;display:none;height:276px;padding:4px 2px;position:absolute;-webkit-transform:translateX(calc(-50% + 12px));transform:translateX(calc(-50% + 12px));width:196px}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list{height:268px;list-style:none;overflow:hidden scroll;padding:0}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item{font-size:11px;height:24px;line-height:24px}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item .tui-image-editor-history-item{cursor:pointer;height:24px;position:relative}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item .tui-image-editor-history-item svg{height:24px;width:24px}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item .tui-image-editor-history-item span{display:inline-block;height:24px;text-align:left;width:128px}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item .tui-image-editor-history-item .history-item-icon{display:inline-block;height:24px;left:6px;position:absolute;top:6px;width:24px}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item .tui-image-editor-history-item .history-item-checkbox{display:none;height:24px;position:absolute;right:-6px;top:5px;width:24px}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item.selected-item{background-color:hsla(0,0%,47%,.12)}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item.selected-item .history-item-checkbox{display:inline-block}.tui-image-editor-container .tui-image-editor-help-menu .tie-panel-history .history-list .history-item.disabled-item{color:#333;opacity:.3}.tui-image-editor-container .tui-image-editor-help-menu .opened .tie-panel-history{display:block}.tui-image-editor-container .tui-image-editor-help-menu .opened .tie-panel-history:before{content:"";display:inline-block;height:0;margin:0 auto;position:absolute;width:0}.tui-image-editor-container .filter-color-item{display:inline-block}.tui-image-editor-container .filter-color-item .tui-image-editor-checkbox{display:block}.tui-image-editor-container .tui-image-editor-checkbox-wrap{display:inline-block!important;text-align:left}.tui-image-editor-container .tui-image-editor-checkbox-wrap.fixed-width{white-space:normal;width:187px}.tui-image-editor-container .tui-image-editor-checkbox{display:inline-block;margin:1px 0}.tui-image-editor-container .tui-image-editor-checkbox input{height:14px;opacity:0;width:14px}.tui-image-editor-container .tui-image-editor-checkbox>label>span{color:#fff;height:14px;position:relative}.tui-image-editor-container .tui-image-editor-checkbox input+label:before,.tui-image-editor-container .tui-image-editor-checkbox>label>span:before{background-color:#fff;border:0;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;content:"";display:inline-block;font-size:11px;height:14px;left:-19px;margin:0;padding-top:1px;position:absolute;text-align:center;top:6px;width:14px}.tui-image-editor-container .tui-image-editor-checkbox input[type=checkbox]:checked+span:before{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAMBJREFUKBWVkjEOwjAMRe2WgZW7IIHEDdhghhuwcQ42rlJugAQS54Cxa5cq1QM5TUpByZfS2j9+dlJVt/tX5ZxbS4ZU9VLkQvSHKTIGRaVJYFmKrBbTCJxE2UgCdDzMZDkHrOV6b95V0US6UmgKodujEZbJg0B0ZgEModO5lrY1TMQf1TpyJGBEjD+E2NPN7ukIUDiF/BfEXgRiGEw8NgkffYGYwCi808fpn/6OvfUfsDr/Vc1IfRf8sKnFVqeiVQfDu0tf/nWH9gAAAABJRU5ErkJggg==);background-size:cover}.tui-image-editor-container .tui-image-editor-selectlist-wrap{position:relative}.tui-image-editor-container .tui-image-editor-selectlist-wrap select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1px solid #cbdbdb;border-radius:0;height:28px;margin-top:4px;outline:0;padding:0 7px 0 10px;width:100%}.tui-image-editor-container .tui-image-editor-selectlist-wrap .tui-image-editor-selectlist{background-color:#fff;border:1px solid #ccc;border-top:0;display:none;padding:4px 0;position:relative;top:-1px}.tui-image-editor-container .tui-image-editor-selectlist-wrap .tui-image-editor-selectlist li{display:block;font-family:Noto Sans,sans-serif;padding:7px 10px;text-align:left}.tui-image-editor-container .tui-image-editor-selectlist-wrap .tui-image-editor-selectlist li:hover{background-color:rgba(81,92,230,.05)}.tui-image-editor-container .tui-image-editor-selectlist-wrap:before{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAHlJREFUKBVjYBgFOEOAEVkmPDxc89+/f6eAYjzI4kD2FyYmJrOVK1deh4kzwRggGiQBVJCELAZig8SQNYHEmEEEMrh69eo1HR0dfqCYJUickZGxf9WqVf3IakBsFBthklpaWmVA9mEQhrJhUoTp0NBQCRAmrHL4qgAAuu4cWZOZIGsAAAAASUVORK5CYII=);background-size:cover;content:"";display:inline-block;height:14px;position:absolute;right:5px;top:10px;width:14px}.tui-image-editor-container .tui-image-editor-selectlist-wrap select::-ms-expand{display:none}.tui-image-editor-container .tui-image-editor-virtual-range-bar .tui-image-editor-disabled,.tui-image-editor-container .tui-image-editor-virtual-range-pointer .tui-image-editor-disabled,.tui-image-editor-container .tui-image-editor-virtual-range-subbar .tui-image-editor-disabled{backbround-color:red}.tui-image-editor-container .tui-image-editor-range{display:inline-block;height:17px;position:relative;top:5px;width:166px}.tui-image-editor-container .tui-image-editor-virtual-range-bar{background-color:#666;height:2px;position:absolute;top:7px;width:100%}.tui-image-editor-container .tui-image-editor-virtual-range-subbar{background-color:#d1d1d1;height:100%;left:0;position:absolute;right:0}.tui-image-editor-container .tui-image-editor-virtual-range-pointer{background-color:#fff;border-radius:100%;cursor:pointer;height:12px;left:0;position:absolute;top:-5px;width:12px}.tui-image-editor-container .tui-image-editor-range-wrap{display:inline-block;margin-left:4px}.tui-image-editor-container .tui-image-editor-range-wrap.short .tui-image-editor-range{width:100px}.tui-image-editor-container .color-picker-control .tui-image-editor-range{margin-left:10px;width:108px}.tui-image-editor-container .color-picker-control .tui-image-editor-virtual-range-pointer{background-color:#333}.tui-image-editor-container .color-picker-control .tui-image-editor-virtual-range-bar{background-color:#ccc}.tui-image-editor-container .color-picker-control .tui-image-editor-virtual-range-subbar{background-color:#606060}.tui-image-editor-container .tui-image-editor-range-wrap.tui-image-editor-newline.short{margin-left:19px;margin-top:-2px}.tui-image-editor-container .tui-image-editor-range-wrap.tui-image-editor-newline.short label{color:#8e8e8e;font-weight:400}.tui-image-editor-container .tui-image-editor-range-wrap label{color:#fff;font-size:11px;margin-right:7px;vertical-align:baseline}.tui-image-editor-container .tui-image-editor-range-value{background-color:#1c1c1c;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:none;box-shadow:none;color:#fff;cursor:default;font-family:Noto Sans,sans-serif;font-weight:lighter;height:24px;margin-left:4px;margin-top:15px;outline:none;text-align:center;vertical-align:baseline;width:40px}.tui-image-editor-container .tui-image-editor-controls{background-color:#151515;bottom:0;display:table;height:64px;position:absolute;width:100%;z-index:2}.tui-image-editor-container .tui-image-editor-icpartition{background-color:#444;display:inline-block;height:24px;width:1px}.tui-image-editor-container.left .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:before{border-bottom:7px solid transparent;border-right:7px solid #2f2f2f;border-top:7px solid transparent;left:28px;top:11px}.tui-image-editor-container.left .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:after{left:42px;top:7px;white-space:nowrap}.tui-image-editor-container.left .tui-image-editor-submenu{height:100%;left:0;width:248px}.tui-image-editor-container.left .tui-image-editor-main-container{height:100%;left:64px;width:calc(100% - 64px)}.tui-image-editor-container.left .tui-image-editor-controls{display:table;height:100%;width:64px}.tui-image-editor-container.left .tui-image-editor-menu,.tui-image-editor-container.right .tui-image-editor-menu{white-space:inherit}.tui-image-editor-container.left .tui-image-editor-submenu,.tui-image-editor-container.right .tui-image-editor-submenu{white-space:normal}.tui-image-editor-container.left .tui-image-editor-submenu>div,.tui-image-editor-container.right .tui-image-editor-submenu>div{vertical-align:middle}.tui-image-editor-container.left .tui-image-editor-controls li,.tui-image-editor-container.right .tui-image-editor-controls li{display:inline-block;margin:4px auto}.tui-image-editor-container.left .tui-image-editor-icpartition,.tui-image-editor-container.right .tui-image-editor-icpartition{height:1px;position:relative;top:-7px;width:24px}.tui-image-editor-container.left .tui-image-editor-submenu .tui-image-editor-partition,.tui-image-editor-container.right .tui-image-editor-submenu .tui-image-editor-partition{display:block;margin:auto;width:75%}.tui-image-editor-container.left .tui-image-editor-submenu .tui-image-editor-partition>div,.tui-image-editor-container.right .tui-image-editor-submenu .tui-image-editor-partition>div{border-bottom:1px solid #3c3c3c;border-left:0;height:10px;margin:0;width:100%}.tui-image-editor-container.left .tui-image-editor-submenu .tui-image-editor-submenu-align,.tui-image-editor-container.right .tui-image-editor-submenu .tui-image-editor-submenu-align{margin-right:0}.tui-image-editor-container.left .tui-image-editor-submenu .tui-image-editor-submenu-item li,.tui-image-editor-container.right .tui-image-editor-submenu .tui-image-editor-submenu-item li{margin-top:15px}.tui-image-editor-container.left .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-colorpicker-clearfix li,.tui-image-editor-container.right .tui-image-editor-submenu .tui-image-editor-submenu-item .tui-colorpicker-clearfix li{margin-top:0}.tui-image-editor-container.left .tui-image-editor-checkbox-wrap.fixed-width,.tui-image-editor-container.right .tui-image-editor-checkbox-wrap.fixed-width{white-space:normal;width:182px}.tui-image-editor-container.left .tui-image-editor-range-wrap.tui-image-editor-newline label.range,.tui-image-editor-container.right .tui-image-editor-range-wrap.tui-image-editor-newline label.range{display:block;margin:auto;text-align:left;width:75%}.tui-image-editor-container.left .tui-image-editor-range,.tui-image-editor-container.right .tui-image-editor-range{width:136px}.tui-image-editor-container.right .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:before{border-bottom:7px solid transparent;border-left:7px solid #2f2f2f;border-top:7px solid transparent;left:-3px;top:11px}.tui-image-editor-container.right .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:after{left:unset;right:43px;top:7px;white-space:nowrap}.tui-image-editor-container.right .tui-image-editor-submenu{height:100%;right:0;width:248px}.tui-image-editor-container.right .tui-image-editor-main-container{height:100%;right:64px;width:calc(100% - 64px)}.tui-image-editor-container.right .tui-image-editor-controls{display:table;height:100%;right:0;width:64px}.tui-image-editor-container.bottom .tui-image-editor-submenu .tui-image-editor-partition.only-left-right,.tui-image-editor-container.top .tui-image-editor-submenu .tui-image-editor-partition.only-left-right{display:none}.tui-image-editor-container.bottom .tui-image-editor-submenu>div{padding-bottom:24px}.tui-image-editor-container.top .color-picker-control .triangle{border-bottom:8px solid #fff;border-left:7px solid transparent;border-right:7px solid transparent;border-top:0;top:-8px}.tui-image-editor-container.top .tui-image-editor-size-wrap{height:100%}.tui-image-editor-container.top .tui-image-editor-main-container{bottom:0}.tui-image-editor-container.top .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:before{border-bottom:7px solid #2f2f2f;border-top:0;left:13px;top:33px}.tui-image-editor-container.top .tui-image-editor-menu>.tui-image-editor-item[tooltip-content]:after{top:38px}.tui-image-editor-container.top .tui-image-editor-submenu{bottom:auto;top:0}.tui-image-editor-container.top .tui-image-editor-submenu>div{padding-top:24px;vertical-align:top}.tui-image-editor-container.top .tui-image-editor-controls-buttons,.tui-image-editor-container.top .tui-image-editor-controls-logo{display:table-cell}.tui-image-editor-container.top .tui-image-editor-main{height:calc(100% - 64px);top:64px}.tui-image-editor-container.top .tui-image-editor-controls{bottom:inherit;top:0}.tui-image-editor-container .tui-image-editor-help-menu.top{height:40px;left:50%;top:8px;-webkit-transform:translateX(-50%);transform:translateX(-50%);white-space:nowrap;width:506px}.tui-image-editor-container .tui-image-editor-help-menu.top .tie-panel-history{top:45px}.tui-image-editor-container .tui-image-editor-help-menu.top .opened .tie-panel-history:before{border-bottom:8px solid #fff;border-left:8px solid transparent;border-right:8px solid transparent;left:90px;top:-8px}.tui-image-editor-container .tui-image-editor-help-menu.top>.tui-image-editor-item[tooltip-content]:before{border:7px solid transparent;border-bottom:7px solid #2f2f2f;border-top:none;left:13px;top:35px}.tui-image-editor-container .tui-image-editor-help-menu.top>.tui-image-editor-item[tooltip-content]:after{left:-4px;top:41px;white-space:nowrap}.tui-image-editor-container .tui-image-editor-help-menu.top>.tui-image-editor-item[tooltip-content].opened:after,.tui-image-editor-container .tui-image-editor-help-menu.top>.tui-image-editor-item[tooltip-content].opened:before{content:none}.tui-image-editor-container .tui-image-editor-help-menu.bottom{bottom:8px;height:40px;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);white-space:nowrap;width:506px}.tui-image-editor-container .tui-image-editor-help-menu.bottom .tie-panel-history{bottom:45px}.tui-image-editor-container .tui-image-editor-help-menu.bottom .opened .tie-panel-history:before{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #fff;bottom:-8px;left:90px}.tui-image-editor-container .tui-image-editor-help-menu.bottom>.tui-image-editor-item[tooltip-content]:before{border:7px solid transparent;border-bottom:none;border-top-color:#2f2f2f;bottom:36px;left:13px;top:auto}.tui-image-editor-container .tui-image-editor-help-menu.bottom>.tui-image-editor-item[tooltip-content]:after{bottom:41px;left:-4px;top:auto;white-space:nowrap}.tui-image-editor-container .tui-image-editor-help-menu.bottom>.tui-image-editor-item[tooltip-content].opened:after,.tui-image-editor-container .tui-image-editor-help-menu.bottom>.tui-image-editor-item[tooltip-content].opened:before{content:none}.tui-image-editor-container .tui-image-editor-help-menu.left{height:506px;left:8px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);white-space:inherit;width:40px}.tui-image-editor-container .tui-image-editor-help-menu.left .tie-panel-history{left:140px;top:-4px}.tui-image-editor-container .tui-image-editor-help-menu.left .opened .tie-panel-history:before{border-bottom:8px solid transparent;border-right:8px solid #fff;border-top:8px solid transparent;left:-8px;top:14px}.tui-image-editor-container .tui-image-editor-help-menu.left .tui-image-editor-item{margin:4px auto;padding:6px 8px}.tui-image-editor-container .tui-image-editor-help-menu.left>.tui-image-editor-item[tooltip-content]:before{border:7px solid transparent;border-left:none;border-right-color:#2f2f2f;left:27px;top:11px}.tui-image-editor-container .tui-image-editor-help-menu.left>.tui-image-editor-item[tooltip-content]:after{left:40px;top:7px;white-space:nowrap}.tui-image-editor-container .tui-image-editor-help-menu.left>.tui-image-editor-item[tooltip-content].opened:after,.tui-image-editor-container .tui-image-editor-help-menu.left>.tui-image-editor-item[tooltip-content].opened:before{content:none}.tui-image-editor-container .tui-image-editor-help-menu.right{height:506px;right:8px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);white-space:inherit;width:40px}.tui-image-editor-container .tui-image-editor-help-menu.right .tie-panel-history{right:-30px;top:-4px}.tui-image-editor-container .tui-image-editor-help-menu.right .opened .tie-panel-history:before{border-bottom:8px solid transparent;border-left:8px solid #fff;border-top:8px solid transparent;right:-8px;top:14px}.tui-image-editor-container .tui-image-editor-help-menu.right .tui-image-editor-item{margin:4px auto;padding:6px 8px}.tui-image-editor-container .tui-image-editor-help-menu.right>.tui-image-editor-item[tooltip-content]:before{border:7px solid transparent;border-left:7px solid #2f2f2f;border-right:none;left:-6px;top:11px}.tui-image-editor-container .tui-image-editor-help-menu.right>.tui-image-editor-item[tooltip-content]:after{left:auto;right:39px;top:7px;white-space:nowrap}.tui-image-editor-container .tui-image-editor-help-menu.right>.tui-image-editor-item[tooltip-content].opened:after,.tui-image-editor-container .tui-image-editor-help-menu.right>.tui-image-editor-item[tooltip-content].opened:before{content:none}.tui-image-editor-container .tie-icon-add-button .tui-image-editor-button{min-width:42px}.tui-image-editor-container .svg_ic-helpmenu,.tui-image-editor-container .svg_ic-menu{height:24px;width:24px}.tui-image-editor-container .svg_ic-submenu{height:32px;width:32px}.tui-image-editor-container .svg_img-bi{height:26px;width:257px}.tui-image-editor-container .tui-image-editor-controls svg>use,.tui-image-editor-container .tui-image-editor-help-menu svg>use{display:none}.tui-image-editor-container .tui-image-editor-controls .enabled svg:hover>use.hover,.tui-image-editor-container .tui-image-editor-controls .normal svg:hover>use.hover,.tui-image-editor-container .tui-image-editor-help-menu .enabled svg:hover>use.hover,.tui-image-editor-container .tui-image-editor-help-menu .normal svg:hover>use.hover{display:block}.tui-image-editor-container .tui-image-editor-controls .active svg:hover>use.hover,.tui-image-editor-container .tui-image-editor-help-menu .active svg:hover>use.hover{display:none}.tui-image-editor-container .tui-image-editor-controls .active svg>use.active,.tui-image-editor-container .tui-image-editor-controls .enabled svg>use.enabled,.tui-image-editor-container .tui-image-editor-controls .on svg>use.hover,.tui-image-editor-container .tui-image-editor-controls .opened svg>use.hover,.tui-image-editor-container .tui-image-editor-controls svg>use.normal,.tui-image-editor-container .tui-image-editor-help-menu .active svg>use.active,.tui-image-editor-container .tui-image-editor-help-menu .enabled svg>use.enabled,.tui-image-editor-container .tui-image-editor-help-menu .on svg>use.hover,.tui-image-editor-container .tui-image-editor-help-menu .opened svg>use.hover,.tui-image-editor-container .tui-image-editor-help-menu svg>use.normal{display:block}.tui-image-editor-container .tui-image-editor-controls .active svg>use.normal,.tui-image-editor-container .tui-image-editor-controls .enabled svg>use.normal,.tui-image-editor-container .tui-image-editor-help-menu .active svg>use.normal,.tui-image-editor-container .tui-image-editor-help-menu .enabled svg>use.normal{display:none}.tui-image-editor-container .tui-image-editor-controls .help svg>use.disabled,.tui-image-editor-container .tui-image-editor-controls .help.enabled svg>use.normal,.tui-image-editor-container .tui-image-editor-help-menu .help svg>use.disabled,.tui-image-editor-container .tui-image-editor-help-menu .help.enabled svg>use.normal{display:block}.tui-image-editor-container .tui-image-editor-controls .help.enabled svg>use.disabled,.tui-image-editor-container .tui-image-editor-help-menu .help.enabled svg>use.disabled{display:none}.tui-image-editor-container .tui-image-editor-controls:hover{z-index:3}.tui-image-editor-container div.tui-colorpicker-clearfix{background-color:#f5f5f5;border:1px solid #d5d5d5;border-radius:2px;height:28px;margin-top:6px;padding:4px 7px;width:159px}.tui-image-editor-container .tui-colorpicker-palette-hex{background-color:#f5f5f5;border:0;font-family:Noto Sans,sans-serif;font-size:11px;margin-top:2px;width:114px}.tui-image-editor-container .tui-colorpicker-palette-hex[value=""]+.tui-colorpicker-palette-preview,.tui-image-editor-container .tui-colorpicker-palette-hex[value="#ffffff"]+.tui-colorpicker-palette-preview{border:1px solid #ccc}.tui-image-editor-container .tui-colorpicker-palette-hex[value=""]+.tui-colorpicker-palette-preview{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAdBJREFUWAnFl0FuwjAQRZ0ukiugHqFSOQNdseuKW3ALzkA4BateICvUGyCxrtRFd4WuunH/TzykaYJrnLEYaTJJsP2+x8GZZCbQrLU5mj7Bn+EP8HvnCObd+R7xBV5lWfaNON4AnsA38E94qLEt+0yiFaBzAV/Bv+Cxxr4co7hKCDpw1q9wLeNYYdlAwyn8TYt8Hme3+8D5ozcTaMCZ68PXa2tnM2sbEcOZAJhrrpl2DAcTOGNjZPSfCdzkw6JrfbiMv+osBe4y9WOedhm4jZfhbENWuxS44H9Wz/xw4WzqLOAqh1+zycgAwzEMzr5k5gaHOa9ULBwuuDkFlHI1Kl4PJ66kgIpnoywOTmRFAYcbwYk9UMApWkD8zAV5ihcwHk4Rx7gl0IFTQL0EFc+CTQ9OZHWH3YhlVJiVpTHbrTGLhTHLZVgff6s9lyBsI9KduSS83oj+34rTwJutmBmCnMsvozRwZqB5GTkBw6/jdPDu69iJ6BYk6eCcfbcgcQIK/MByaaiMqm8rHcjol2TnpWDhyAKSGdA3FrxtJUToX0ODqatetfGE+8tyEUOV8GY5dGRwLP/MBS4RHQr4bT7NRAQjlcOTfZxmv2G+c4hI8nn+Ax5PG/zhI393AAAAAElFTkSuQmCC);background-size:cover}.tui-image-editor-container .tui-colorpicker-palette-preview{border:0;border-radius:100%;float:left;height:17px;width:17px}.tui-image-editor-container .color-picker-control{background-color:#fff;border-radius:2px;-webkit-box-shadow:0 3px 22px 6px rgba(0,0,0,.15);box-shadow:0 3px 22px 6px rgba(0,0,0,.15);display:none;padding:16px;position:absolute;width:192px;z-index:99}.tui-image-editor-container .color-picker-control .tui-colorpicker-palette-toggle-slider{display:none}.tui-image-editor-container .color-picker-control .tui-colorpicker-palette-button{background-size:cover;border:0;border-radius:100%;font-size:1px;margin:2px}.tui-image-editor-container .color-picker-control .tui-colorpicker-palette-button[title=""],.tui-image-editor-container .color-picker-control .tui-colorpicker-palette-button[title="#ffffff"]{border:1px solid #ccc}.tui-image-editor-container .color-picker-control .triangle{border-left:7px solid transparent;border-right:7px solid transparent;border-top:8px solid #fff;bottom:-8px;height:0;left:84px;position:absolute;width:0}.tui-image-editor-container .color-picker-control .tui-colorpicker-container,.tui-image-editor-container .color-picker-control .tui-colorpicker-palette-container,.tui-image-editor-container .color-picker-control .tui-colorpicker-palette-container ul{height:auto;width:100%}.tui-image-editor-container .filter-color-item .color-picker-control label{font-color:#333;font-weight:400;margin-right:7pxleft}.tui-image-editor-container .filter-color-item .tui-image-editor-checkbox{margin-top:0}.tui-image-editor-container .filter-color-item .tui-image-editor-checkbox input+label:before,.tui-image-editor-container .filter-color-item .tui-image-editor-checkbox>label:before{left:-16px}.tui-image-editor-container .color-picker{height:auto;width:100%}.tui-image-editor-container .color-picker-value{border:0;border-radius:100%;height:32px;margin:auto auto 1px;width:32px}.tui-image-editor-container .color-picker-value.transparent{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAdBJREFUWAnFl0FuwjAQRZ0ukiugHqFSOQNdseuKW3ALzkA4BateICvUGyCxrtRFd4WuunH/TzykaYJrnLEYaTJJsP2+x8GZZCbQrLU5mj7Bn+EP8HvnCObd+R7xBV5lWfaNON4AnsA38E94qLEt+0yiFaBzAV/Bv+Cxxr4co7hKCDpw1q9wLeNYYdlAwyn8TYt8Hme3+8D5ozcTaMCZ68PXa2tnM2sbEcOZAJhrrpl2DAcTOGNjZPSfCdzkw6JrfbiMv+osBe4y9WOedhm4jZfhbENWuxS44H9Wz/xw4WzqLOAqh1+zycgAwzEMzr5k5gaHOa9ULBwuuDkFlHI1Kl4PJ66kgIpnoywOTmRFAYcbwYk9UMApWkD8zAV5ihcwHk4Rx7gl0IFTQL0EFc+CTQ9OZHWH3YhlVJiVpTHbrTGLhTHLZVgff6s9lyBsI9KduSS83oj+34rTwJutmBmCnMsvozRwZqB5GTkBw6/jdPDu69iJ6BYk6eCcfbcgcQIK/MByaaiMqm8rHcjol2TnpWDhyAKSGdA3FrxtJUToX0ODqatetfGE+8tyEUOV8GY5dGRwLP/MBS4RHQr4bT7NRAQjlcOTfZxmv2G+c4hI8nn+Ax5PG/zhI393AAAAAElFTkSuQmCC);background-size:cover;border:1px solid #cbcbcb}.tui-image-editor-container .color-picker-value+label{color:#fff}.tui-image-editor-container .tui-image-editor-submenu svg>use{display:none}.tie-icon-add-button.icon-arrow .tui-image-editor-button[data-icontype=icon-arrow] svg>use.active,.tie-icon-add-button.icon-arrow-2 .tui-image-editor-button[data-icontype=icon-arrow-2] svg>use.active,.tie-icon-add-button.icon-arrow-3 .tui-image-editor-button[data-icontype=icon-arrow-3] svg>use.active,.tie-icon-add-button.icon-bubble .tui-image-editor-button[data-icontype=icon-bubble] svg>use.active,.tie-icon-add-button.icon-heart .tui-image-editor-button[data-icontype=icon-heart] svg>use.active,.tie-icon-add-button.icon-location .tui-image-editor-button[data-icontype=icon-location] svg>use.active,.tie-icon-add-button.icon-polygon .tui-image-editor-button[data-icontype=icon-polygon] svg>use.active,.tie-icon-add-button.icon-star .tui-image-editor-button[data-icontype=icon-star] svg>use.active,.tie-icon-add-button.icon-star-2 .tui-image-editor-button[data-icontype=icon-star-2] svg>use.active,.tui-image-editor-container .tui-image-editor-submenu svg>use.normal{display:block}.tie-draw-line-select-button.free .tui-image-editor-button.free svg>use.normal,.tie-draw-line-select-button.line .tui-image-editor-button.line svg>use.normal{display:none}.tie-draw-line-select-button.free .tui-image-editor-button.free svg>use.active,.tie-draw-line-select-button.line .tui-image-editor-button.line svg>use.active{display:block}.tie-flip-button.flipX .tui-image-editor-button.flipX svg>use.normal,.tie-flip-button.flipY .tui-image-editor-button.flipY svg>use.normal,.tie-flip-button.resetFlip .tui-image-editor-button.resetFlip svg>use.normal{display:none}.tie-flip-button.flipX .tui-image-editor-button.flipX svg>use.active,.tie-flip-button.flipY .tui-image-editor-button.flipY svg>use.active,.tie-flip-button.resetFlip .tui-image-editor-button.resetFlip svg>use.active{display:block}.tie-mask-apply.apply.active .tui-image-editor-button.apply label{color:#fff}.tie-mask-apply.apply.active .tui-image-editor-button.apply svg>use.active{display:block}.tie-crop-button .tui-image-editor-button.apply,.tie-crop-preset-button .tui-image-editor-button.apply{margin-right:24px}.tie-crop-button .tui-image-editor-button.apply.active svg>use.active,.tie-crop-button .tui-image-editor-button.preset.active svg>use.active,.tie-crop-preset-button .tui-image-editor-button.apply.active svg>use.active,.tie-crop-preset-button .tui-image-editor-button.preset.active svg>use.active{display:block}.tie-resize-button .tui-image-editor-button.apply,.tie-resize-preset-button .tui-image-editor-button.apply{margin-right:24px}.tie-resize-button .tui-image-editor-button.apply.active svg>use.active,.tie-resize-button .tui-image-editor-button.preset.active svg>use.active,.tie-resize-preset-button .tui-image-editor-button.apply.active svg>use.active,.tie-resize-preset-button .tui-image-editor-button.preset.active svg>use.active{display:block}.tie-shape-button.circle .tui-image-editor-button.circle svg>use.normal,.tie-shape-button.rect .tui-image-editor-button.rect svg>use.normal,.tie-shape-button.triangle .tui-image-editor-button.triangle svg>use.normal{display:none}.tie-shape-button.circle .tui-image-editor-button.circle svg>use.active,.tie-shape-button.rect .tui-image-editor-button.rect svg>use.active,.tie-shape-button.triangle .tui-image-editor-button.triangle svg>use.active,.tie-text-align-button.tie-text-align-center .tui-image-editor-button.center svg>use.active,.tie-text-align-button.tie-text-align-left .tui-image-editor-button.left svg>use.active,.tie-text-align-button.tie-text-align-right .tui-image-editor-button.right svg>use.active,.tie-text-effect-button .tui-image-editor-button.active svg>use.active{display:block}.tie-icon-image-file,.tie-mask-image-file{border:1px solid green;cursor:inherit;height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.tie-zoom-button.flipX .tui-image-editor-button.flipX svg>use.normal,.tie-zoom-button.flipY .tui-image-editor-button.flipY svg>use.normal,.tie-zoom-button.resetFlip .tui-image-editor-button.resetFlip svg>use.normal{display:none}.tie-zoom-button.flipX .tui-image-editor-button.flipX svg>use.active,.tie-zoom-button.flipY .tui-image-editor-button.flipY svg>use.active,.tie-zoom-button.resetFlip .tui-image-editor-button.resetFlip svg>use.active{display:block}.tui-image-editor-container.top.tui-image-editor-top-optimization .tui-image-editor-controls ul{text-align:right}.tui-image-editor-container.top.tui-image-editor-top-optimization .tui-image-editor-controls-logo{display:none}',""]);const r=i},1145:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});var o=n(6314),i=n.n(o)()((function(e){return e[1]}));i.push([e.id,'/*!\n * TOAST UI Color Picker\n * @version 2.2.8\n * @author NHN Cloud FE Development Team \n * @license MIT\n */.tui-colorpicker-clearfix{zoom:1}.tui-colorpicker-clearfix:after{clear:both;content:"";display:block}.tui-colorpicker-vml{behavior:url(#default#VML);display:block}.tui-colorpicker-container,.tui-colorpicker-palette-container{width:152px}.tui-colorpicker-palette-container ul{margin:0;padding:0;width:152px}.tui-colorpicker-palette-container li{float:left;list-style:none;margin:0;padding:0 3px 3px 0}.tui-colorpicker-palette-button{border:1px solid #ccc;cursor:pointer;display:block;height:16px;margin:0;outline:none;overflow:hidden;padding:0;width:16px}.tui-colorpicker-palette-button.tui-colorpicker-selected{border:2px solid #000}.tui-colorpicker-palette-button.tui-colorpicker-color-transparent{barckground-repeat:repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAOCAYAAAD0f5bSAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4BZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPDRcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWmZ5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBLmsbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGbaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjEzPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE0PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CghrN1AAAABzSURBVCgVldKxEYAgDAXQD5VOpLuwgi4jlrTMqF00oOd5Aia/CcV/F4oYOgNlrLjvVyCEVJchBjEC25538PeaWTzRMBLxvIL7UZwFwL06qoA6aoAy+gFfJABvJAQPUoCMlICRRd8BzgHzJL4ok9aJ67l4AK9AxVKhHryUAAAAAElFTkSuQmCC");background-repeat:no-repeat}.tui-colorpicker-palette-hex{font-family:monospace;width:60px}.tui-colorpicker-palette-hex,.tui-colorpicker-palette-preview{zoom:1;display:inline-block;*display:inline;vertical-align:middle}.tui-colorpicker-palette-preview{border:1px solid #ccc;height:12px;overflow:hidden;width:12px}.tui-colorpicker-palette-toggle-slider{zoom:1;display:inline-block;*display:inline;float:right;vertical-align:middle}.tui-colorpicker-slider-container{zoom:1;height:122px;margin:5px 0 0}.tui-colorpicker-slider-container:after{clear:both;content:"";display:block}.tui-colorpicker-slider-left{float:left;height:120px;width:120px}.tui-colorpicker-slider-right{float:right;height:120px;width:32px}.tui-colorpicker-svg{display:block}.tui-colorpicker-slider-handle{height:1px;left:0;opacity:.9;overflow:visible;position:absolute;top:0;width:1px;z-index:2}.tui-colorpicker-svg-slider,.tui-colorpicker-vml-slider{border:1px solid #ccc;height:120px;overflow:hidden;width:120px}.tui-colorpicker-vml-slider{position:relative}.tui-colorpicker-vml-slider-bg{height:122px;left:0;margin:-1px 0 0 -1px;position:absolute;top:0;width:122px}.tui-colorpicker-svg-huebar{border:1px solid #ccc;float:right;height:120px;overflow:visible;width:18px}.tui-colorpicker-vml-huebar{position:relative;width:32px}.tui-colorpicker-vml-huebar-bg{height:121px;position:absolute;right:0;top:0;width:18px}',""]);const r=i},1148:()=>{!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");e.add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const t=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:t},onSubmit:t=>{((e,t)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(t)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,t.getData().code),t.close()}})})(e)}))})(e),(e=>{const t=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:t}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:t})})(e),{})))}()},1627:(e,t,n)=>{n(9504)},1688:()=>{},1694:(e,t,n)=>{n(7235)},2171:()=>{!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=(n=void 0,e=>n===e);var n;class o{constructor(e,t){this.tag=e,this.value=t}static some(e){return new o(!0,e)}static none(){return o.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?o.some(e(this.value)):o.none()}bind(e){return this.tag?e(this.value):o.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:o.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(null!=e?e:"Called getOrDie on None")}static from(e){return(e=>null==e)(e)?o.none():o.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}o.singletonNone=new o(!1);const i=e=>()=>e,r=i(!1),s=(e,t)=>((e,t,n)=>{for(let i=0,r=e.length;il(0,0),l=(e,t)=>({major:e,minor:t}),c={nu:l,detect:(e,t)=>{const n=String(t).toLowerCase();return 0===e.length?a():((e,t)=>{const n=((e,t)=>{for(let n=0;nNumber(t.replace(n,"$"+e));return l(o(1),o(2))})(e,n)},unknown:a},u=(e,t)=>((e,t)=>{for(let n=0;n{const n=t.brand.toLowerCase();return s(e,(e=>{var t;return n===(null===(t=e.brand)||void 0===t?void 0:t.toLowerCase())})).map((e=>({current:e.name,version:c.nu(parseInt(t.version,10),0)})))})),d=(e,t)=>{const n=String(t).toLowerCase();return s(e,(e=>e.search(n)))},h=(e,n,o=0,i)=>{const r=e.indexOf(n,o);return-1!==r&&(!!t(i)||r+n.length<=i)},f=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,p=e=>t=>h(t,e),m=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:e=>h(e,"edge/")&&h(e,"chrome")&&h(e,"safari")&&h(e,"applewebkit")},{name:"Chromium",brand:"Chromium",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,f],search:e=>h(e,"chrome")&&!h(e,"chromeframe")},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:e=>h(e,"msie")||h(e,"trident")},{name:"Opera",versionRegexes:[f,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:p("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:p("firefox")},{name:"Safari",versionRegexes:[f,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:e=>(h(e,"safari")||h(e,"mobile/"))&&h(e,"applewebkit")}],g=[{name:"Windows",search:p("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:e=>h(e,"iphone")||h(e,"ipad"),versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:p("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"macOS",search:p("mac os x"),versionRegexes:[/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:p("linux"),versionRegexes:[]},{name:"Solaris",search:p("sunos"),versionRegexes:[]},{name:"FreeBSD",search:p("freebsd"),versionRegexes:[]},{name:"ChromeOS",search:p("cros"),versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/]}],O={browsers:i(m),oses:i(g)},v="Edge",b="Chromium",y="Opera",x="Firefox",w="Safari",S=e=>{const t=e.current,n=e.version,o=e=>()=>t===e;return{current:t,version:n,isEdge:o(v),isChromium:o(b),isIE:o("IE"),isOpera:o(y),isFirefox:o(x),isSafari:o(w)}},_={unknown:()=>S({current:void 0,version:c.unknown()}),nu:S,edge:i(v),chromium:i(b),ie:i("IE"),opera:i(y),firefox:i(x),safari:i(w)},k="Windows",C="Android",T="Linux",$="macOS",P="Solaris",A="FreeBSD",Q="ChromeOS",E=e=>{const t=e.current,n=e.version,o=e=>()=>t===e;return{current:t,version:n,isWindows:o(k),isiOS:o("iOS"),isAndroid:o(C),isMacOS:o($),isLinux:o(T),isSolaris:o(P),isFreeBSD:o(A),isChromeOS:o(Q)}},M={unknown:()=>E({current:void 0,version:c.unknown()}),nu:E,windows:i(k),ios:i("iOS"),android:i(C),linux:i(T),macos:i($),solaris:i(P),freebsd:i(A),chromeos:i(Q)},D=(e,t,n)=>{const o=O.browsers(),r=O.oses(),s=t.bind((e=>u(o,e))).orThunk((()=>((e,t)=>d(e,t).map((e=>{const n=c.detect(e.versionRegexes,t);return{current:e.name,version:n}})))(o,e))).fold(_.unknown,_.nu),a=((e,t)=>d(e,t).map((e=>{const n=c.detect(e.versionRegexes,t);return{current:e.name,version:n}})))(r,e).fold(M.unknown,M.nu),l=((e,t,n,o)=>{const r=e.isiOS()&&!0===/ipad/i.test(n),s=e.isiOS()&&!r,a=e.isiOS()||e.isAndroid(),l=a||o("(pointer:coarse)"),c=r||!s&&a&&o("(min-device-width:768px)"),u=s||a&&!c,d=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(n),h=!u&&!c&&!d;return{isiPad:i(r),isiPhone:i(s),isTablet:i(c),isPhone:i(u),isTouch:i(l),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:i(d),isDesktop:i(h)}})(a,s,e,n);return{browser:s,os:a,deviceType:l}},I=e=>window.matchMedia(e).matches;let R=(e=>{let t,n=!1;return(...o)=>(n||(n=!0,t=e.apply(null,o)),t)})((()=>D(window.navigator.userAgent,o.from(window.navigator.userAgentData),I)));const z=()=>R(),N=()=>{const e=z().os.isMacOS()||z().os.isiOS();return` + +@endsection diff --git a/resources/views/plugins/manage/spam/index.blade.php b/resources/views/plugins/manage/spam/index.blade.php new file mode 100644 index 000000000..e9b50bb46 --- /dev/null +++ b/resources/views/plugins/manage/spam/index.blade.php @@ -0,0 +1,239 @@ +{{-- + * スパム管理のメインテンプレート + * + * @author 井上 雅人 + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 +--}} +@php +use App\Enums\SpamBlockType; +@endphp +{{-- 管理画面ベース画面 --}} +@extends('plugins.manage.manage') + +{{-- 管理画面メイン部分のコンテンツ section:manage_content で作ること --}} +@section('manage_content') + +
+
+ {{-- 機能選択タブ --}} + @include('plugins.manage.spam.spam_tab') +
+ +
+ + {{-- 登録後メッセージ表示 --}} + @include('plugins.common.flash_message') + + @include('plugins.common.errors_form_line') + +
+ サイト全体で適用されるブロックリストを管理します。 +
+ + {{-- 検索フォーム --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + クリア +
+
+
+ + {{-- ブロックリスト一覧 --}} +
+ ブロックリスト一覧 +
+ {{ csrf_field() }} + + + + + +
+
+ +
+ + + + + + + + + + + + + @forelse($spam_lists as $spam) + + + + + + + + + @empty + + + + @endforelse + +
種別適用範囲メモ登録日時操作
+ @include('plugins.common.spam_block_type_badge', ['block_type' => $spam->block_type]) + {{ $spam->block_value }} + @if (is_null($spam->target_id)) + 全体 + @else + @php + $form_name = $forms[$spam->target_id]->forms_name ?? '不明'; + @endphp + @if (isset($form_page_urls[$spam->target_id])) + {{ $form_name }} + @else + {{ $form_name }} + @endif + @endif + {{ Str::limit($spam->memo, 30) }}{{ $spam->created_at->format('Y/m/d H:i') }} + + 編集 + +
+ {{ csrf_field() }} + +
+
ブロックリストは登録されていません。
+
+ + {{-- ページング処理 --}} + {{ $spam_lists->links() }} + +
+ + {{-- ブロックリスト追加フォーム --}} +
ブロックリストへ追加
+ +
+ {{ csrf_field() }} + +
+ +
+ @foreach (SpamBlockType::getMembers() as $key => $value) +
+ + +
+ @endforeach + @include('plugins.common.errors_inline', ['name' => 'block_type']) + + ※ メールアドレス:完全一致でブロックします。
+ ※ ドメイン:メールアドレスの@以降と一致する場合にブロックします。
+ ※ メールアドレス・ドメインはフォームに「メールアドレス」型項目がある場合に有効です。
+ ※ IPアドレス:送信元IPアドレスと一致する場合にブロックします。
+ ※ ハニーポット:ボット対策用の隠しフィールドを設置します。値の入力は不要です。 +
+
+
+ +
+ +
+ + @include('plugins.common.errors_inline', ['name' => 'block_value']) +
+
+ +
+ +
+
+ + +
+
+ + +
+ + @include('plugins.common.errors_inline', ['name' => 'target_forms_id']) + ※「全体」を選択すると、スパムフィルタリングを有効にしているすべてのフォームに適用されます。 +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + + +@endsection diff --git a/resources/views/plugins/manage/spam/spam_tab.blade.php b/resources/views/plugins/manage/spam/spam_tab.blade.php new file mode 100644 index 000000000..419c96800 --- /dev/null +++ b/resources/views/plugins/manage/spam/spam_tab.blade.php @@ -0,0 +1,38 @@ +{{-- + * スパム管理のタブ + * + * @author 井上 雅人 + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 +--}} +
+ +
diff --git a/resources/views/plugins/manage/theme/theme.blade.php b/resources/views/plugins/manage/theme/theme.blade.php index 9c27a7771..6fa54d317 100644 --- a/resources/views/plugins/manage/theme/theme.blade.php +++ b/resources/views/plugins/manage/theme/theme.blade.php @@ -100,7 +100,7 @@ function view_list_images(dir_name) {{-- ディレクトリ名 --}}
- +
@if ($errors) @@ -108,11 +108,15 @@ function view_list_images(dir_name) @endif @if ($errors && $errors->has('dir_name'))
{{$errors->first('dir_name')}}
@endif + +
※ テーマファイルを保存するフォルダ名です。
+
※ 英数字と記号の「-」「_」のみ使用できます。
+
{{-- テーマ名 --}}
- +
@if ($errors) @@ -121,6 +125,7 @@ function view_list_images(dir_name) @endif @if ($errors && $errors->has('theme_name'))
{{$errors->first('theme_name')}}
@endif +
※ サイト管理で表示・選択されるテーマ名です。
※ 独自テーマを作成できます。独自テーマではCSS、javascript、画像を独自に定義することができます。
※ 作成した独自テーマはサイト管理から設定することができます。
※ テーマファイルはサーバ上の [ドキュメントルート]/public/themes/Users/[ディレクトリ名] に作成されます。
diff --git a/resources/views/plugins/manage/theme/theme_generate.blade.php b/resources/views/plugins/manage/theme/theme_generate.blade.php index 2aa2a1e36..2540b7ba0 100644 --- a/resources/views/plugins/manage/theme/theme_generate.blade.php +++ b/resources/views/plugins/manage/theme/theme_generate.blade.php @@ -76,7 +76,7 @@ {{-- ディレクトリ名 --}}
- +
@if ($errors || $theme_css) @@ -84,11 +84,15 @@ @endif @if ($errors && $errors->has('dir_name'))
{{$errors->first('dir_name')}}
@endif + +
※ テーマファイルを保存するフォルダ名です。
+
※ 英数字と記号の「-」「_」のみ使用できます。
+
{{-- テーマ名 --}}
- +
@if ($errors || $theme_css) @@ -96,6 +100,9 @@ @endif @if ($errors && $errors->has('theme_name'))
{{$errors->first('theme_name')}}
@endif + +
※ サイト管理で表示・選択されるテーマ名です。
+
{{-- テーマセット --}} diff --git a/resources/views/plugins/manage/user/list.blade.php b/resources/views/plugins/manage/user/list.blade.php index 7c33191d5..a64c9fb92 100644 --- a/resources/views/plugins/manage/user/list.blade.php +++ b/resources/views/plugins/manage/user/list.blade.php @@ -347,8 +347,15 @@ function submit_download_utf_8() { @endif
-
- +
+
+ + +
+
+
@@ -449,8 +456,9 @@ function submit_download_utf_8() { @endforeach -
{{ UsersColumns::getLabelLoginId($users_columns) }}
- ※ 表示内容が多い場合、横スクロールできます。 + + ※ 表示内容が多い場合、横スクロールできます。 +
{{-- ページング処理 --}} diff --git a/resources/views/plugins/mypage/profile/edit_form.blade.php b/resources/views/plugins/mypage/profile/edit_form.blade.php index b6b82c9f9..a84b9be26 100644 --- a/resources/views/plugins/mypage/profile/edit_form.blade.php +++ b/resources/views/plugins/mypage/profile/edit_form.blade.php @@ -45,10 +45,12 @@ @elseif ($column->column_type == UserColumnType::user_email) {{-- メールアドレス --}}
- +
- + required) required @endif> @include('plugins.common.errors_inline', ['name' => 'email'])
{!! nl2br((string)$column->caption) !!}
@@ -117,4 +119,3 @@
- diff --git a/resources/views/plugins/user/databases/card_02/card_common.blade.php b/resources/views/plugins/user/databases/card_02/card_common.blade.php index f6d611a36..dea721d22 100644 --- a/resources/views/plugins/user/databases/card_02/card_common.blade.php +++ b/resources/views/plugins/user/databases/card_02/card_common.blade.php @@ -13,6 +13,11 @@ @if ($default_hide_list) @else + {{-- データベースの表示件数変更セレクトボックス --}} + @include('plugins.user.databases.default.databases_include_view_count') + {{-- 現在表示している件数テキスト --}} + @include('plugins.user.databases.default.databases_include_page_total_views') +
@php diff --git a/resources/views/plugins/user/databases/table/databases.blade.php b/resources/views/plugins/user/databases/table/databases.blade.php index f43802aea..e32e7790d 100644 --- a/resources/views/plugins/user/databases/table/databases.blade.php +++ b/resources/views/plugins/user/databases/table/databases.blade.php @@ -17,6 +17,10 @@ @if ($default_hide_list) @else + {{-- データベースの表示件数変更セレクトボックス --}} + @include('plugins.user.databases.default.databases_include_view_count') + {{-- 現在表示している件数テキスト --}} + @include('plugins.user.databases.default.databases_include_page_total_views') @php $database_show_like_list = FrameConfig::getConfigValueAndOld($frame_configs, DatabaseFrameConfig::database_show_like_list, ShowType::show); $like_button_caption = $database_frame->like_button_name ?: Like::like_button_default; diff --git a/resources/views/plugins/user/forms/default/forms.blade.php b/resources/views/plugins/user/forms/default/forms.blade.php index 73ad1fb02..12e844a6c 100644 --- a/resources/views/plugins/user/forms/default/forms.blade.php +++ b/resources/views/plugins/user/forms/default/forms.blade.php @@ -149,6 +149,9 @@
@endforeach + {{-- ハニーポットフィールド --}} + @include('plugins.user.forms.default.include_honeypot_field') + {{-- Captcha フィールド --}} @include('plugins.user.forms.default.forms_captcha_field') diff --git a/resources/views/plugins/user/forms/default/forms_confirm.blade.php b/resources/views/plugins/user/forms/default/forms_confirm.blade.php index e0adced1e..184fae4cf 100644 --- a/resources/views/plugins/user/forms/default/forms_confirm.blade.php +++ b/resources/views/plugins/user/forms/default/forms_confirm.blade.php @@ -36,6 +36,10 @@ function submit_forms_cancel() {
{{ csrf_field() }} + {{-- ハニーポット値引き継ぎ(二重防御用) --}} + @if ($has_honeypot) + + @endif @foreach($forms_columns as $form_column)
diff --git a/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php b/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php index d6d663f6c..bdc55fcce 100644 --- a/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php +++ b/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php @@ -35,6 +35,10 @@ function submit_forms_cancel() { {{ csrf_field() }} + {{-- ハニーポット値引き継ぎ(二重防御用) --}} + @if ($has_honeypot) + + @endif @php $no = 1; @endphp @foreach($forms_columns as $form_column) diff --git a/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php b/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php new file mode 100644 index 000000000..b182c260e --- /dev/null +++ b/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php @@ -0,0 +1,201 @@ +{{-- + * スパムフィルタリング設定画面テンプレート + * + * @author 井上 雅人 + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category フォームプラグイン +--}} +@php +use App\Enums\SpamBlockType; +@endphp +@extends('core.cms_frame_base_setting') + +@section("core.cms_frame_edit_tab_$frame->id") + {{-- プラグイン側のフレームメニュー --}} + @include('plugins.user.forms.forms_frame_edit_tab') +@endsection + +@section("plugin_setting_$frame->id") + +{{-- 登録後メッセージ表示 --}} +@include('plugins.common.flash_message') + +@include('plugins.common.errors_form_line') + +
+ スパムフィルタリングの設定を行います。 +
+ +{{-- スパムフィルタリング設定フォーム --}} + + {{ csrf_field() }} + + +
+ +
+
+ + use_spam_filter_flag)) checked @endif> + +
+ + 本機能を有効にすると、スパムフィルタリングのために送信元のIPアドレスをフォーム投稿時に収集します。サイトのプライバシーポリシーにその旨を記載されることを推奨いたします。 + +
+
+ +
use_spam_filter_flag)) style="display: none;" @endif> +
+ +
+ + ※ 未入力の場合、デフォルトメッセージ「入力されたメールアドレス、または、IPアドレスからの送信は現在制限されています。」が表示されます。 +
+
+
+ + {{-- Submitボタン --}} +
+ +
+ + +
use_spam_filter_flag)) style="display: none;" @endif> +
+ +{{-- 適用されるブロックリスト --}} +
適用されるブロックリスト
+ +
+ + + + + + + + + + + + @forelse($spam_lists as $spam) + + + + + + + + @empty + + + + @endforelse + +
種別適用範囲メモ操作
+ @include('plugins.common.spam_block_type_badge', ['block_type' => $spam->block_type]) + {{ $spam->block_value }} + @if ($spam->isGlobalScope()) + 全体 + @else + このフォーム + @endif + {{ $spam->memo }} + @if ($spam->isGlobalScope()) + + @else +
+ {{ csrf_field() }} + + +
+ @endif +
ブロックリストは登録されていません。
+
+ +※ 適用範囲が「全体」のブロックリストはスパム管理から編集できます。 + +
+ +{{-- ブロックリスト追加フォーム --}} +
ブロックリストへ追加(このフォーム用)
+ +
+ {{ csrf_field() }} + + +
+ +
+ @foreach (SpamBlockType::getMembers() as $key => $value) +
+ + +
+ @endforeach + @include('plugins.common.errors_inline', ['name' => 'block_type']) + + ※ メールアドレス:完全一致でブロックします。
+ ※ ドメイン:メールアドレスの@以降と一致する場合にブロックします。
+ ※ メールアドレス・ドメインはフォームに「メールアドレス」型項目がある場合に有効です。
+ ※ IPアドレス:送信元IPアドレスと一致する場合にブロックします。
+ ※ ハニーポット:ボット対策用の隠しフィールドを設置します。値の入力は不要です。 +
+
+
+ +
+ +
+ + @include('plugins.common.errors_inline', ['name' => 'block_value']) +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + +@endsection diff --git a/resources/views/plugins/user/forms/default/forms_list_inputs.blade.php b/resources/views/plugins/user/forms/default/forms_list_inputs.blade.php index c9f4749f8..215fd736a 100644 --- a/resources/views/plugins/user/forms/default/forms_list_inputs.blade.php +++ b/resources/views/plugins/user/forms/default/forms_list_inputs.blade.php @@ -83,8 +83,16 @@ function submit_register_other_plugins(id) {
{{-- データのループ --}} - - +
+
+ + +
+
+
+ @if ($form->other_plugins_register_use_flag) @@ -100,6 +108,8 @@ function submit_register_other_plugins(id) { @endif + + @@ -151,10 +161,64 @@ function submit_register_other_plugins(id) { {{$input->created_at}} + + + + @endforeach - -
連携登録ユーザ 登録日時IPアドレス操作
+ {{$input->ip_address}} + +
+ + +
+
+ + + + @@ -180,4 +244,18 @@ function submit_register_other_plugins(id) { + + + @endsection diff --git a/resources/views/plugins/user/forms/default/include_honeypot_field.blade.php b/resources/views/plugins/user/forms/default/include_honeypot_field.blade.php new file mode 100644 index 000000000..14a5de009 --- /dev/null +++ b/resources/views/plugins/user/forms/default/include_honeypot_field.blade.php @@ -0,0 +1,14 @@ +{{-- + * ハニーポットフィールド(スパムボット対策用の隠しフィールド) + * + * @author 井上 雅人 + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category フォームプラグイン + * @note CSSは public/css/connect.css の .connect-hp-field を参照 +--}} +@if ($has_honeypot) + +@endif diff --git a/resources/views/plugins/user/forms/default/index_tandem.blade.php b/resources/views/plugins/user/forms/default/index_tandem.blade.php index 4a8adce86..7ca165e68 100644 --- a/resources/views/plugins/user/forms/default/index_tandem.blade.php +++ b/resources/views/plugins/user/forms/default/index_tandem.blade.php @@ -119,6 +119,9 @@ @endforeach + {{-- ハニーポットフィールド --}} + @include('plugins.user.forms.default.include_honeypot_field') + {{-- Captcha フィールド --}} @php $is_tandem_template = true; @endphp @include('plugins.user.forms.default.forms_captcha_field') diff --git a/resources/views/plugins/user/forms/forms_frame_edit_tab.blade.php b/resources/views/plugins/user/forms/forms_frame_edit_tab.blade.php index 3e7b62461..230ad4738 100644 --- a/resources/views/plugins/user/forms/forms_frame_edit_tab.blade.php +++ b/resources/views/plugins/user/forms/forms_frame_edit_tab.blade.php @@ -46,6 +46,15 @@ 登録一覧 @endif +@if ($action == 'editSpamFilter') + +@else + +@endif @if ($action == 'listBuckets') {{-- 表示順 --}} - {{-- 表示フラグ--}} -
+ +
表示順
{{-- 上移動 --}}
+ +
表示/非表示
seed(); + } + + /** + * CSV文字列を生成する。 + * + * @param array $headers + * @param array> $rows + */ + private function createCsvContent(array $headers, array $rows): string + { + $output = fopen('php://temp', 'r+'); + fputcsv($output, $headers); + foreach ($rows as $row) { + fputcsv($output, $row); + } + rewind($output); + $content = stream_get_contents($output); + fclose($output); + + return $content; + } + + /** + * admin_page権限を持つユーザーを作成する。 + */ + private function createPageAdminUser(): User + { + $user = User::factory()->create(); + UsersRoles::factory()->create([ + 'users_id' => $user->id, + 'target' => 'manage', + 'role_name' => 'admin_page', + 'role_value' => 1, + ]); + + return $user; + } + + /** + * 旧形式ヘッダー(layout_inherit_flag なし)でもインポートできること。 + * + * @test + */ + public function legacyCsvWithoutLayoutInheritFlagCanBeImported(): void + { + $admin = $this->createPageAdminUser(); + + $csv_content = $this->createCsvContent( + ['page_name', 'permanent_link', 'background_color', 'header_color', 'theme', 'layout', 'base_display_flag'], + [['legacy', '/legacy-import-test', 'NULL', 'NULL', 'NULL', 'NULL', '0']] + ); + $csv_file = UploadedFile::fake()->createWithContent('legacy_page.csv', $csv_content); + + $response = $this->actingAs($admin)->post('/manage/page/upload', [ + 'page_csv' => $csv_file, + ]); + + $response->assertStatus(302); + $response->assertRedirect('/manage/page/import'); + $response->assertSessionHas('flash_message', 'インポートしました。'); + $this->assertDatabaseHas('pages', [ + 'page_name' => 'legacy', + 'permanent_link' => '/legacy-import-test', + 'layout_inherit_flag' => 1, + 'base_display_flag' => 0, + ]); + } + + /** + * 新形式ヘッダーでは layout_inherit_flag の値をそのまま使うこと。 + * + * @test + */ + public function currentCsvLayoutInheritFlagValueIsAppliedAsIs(): void + { + $admin = $this->createPageAdminUser(); + + $csv_content = $this->createCsvContent( + ['page_name', 'permanent_link', 'background_color', 'header_color', 'theme', 'layout', 'layout_inherit_flag', 'base_display_flag'], + [['current', '/current-import-test', 'NULL', 'NULL', 'NULL', 'NULL', '0', '1']] + ); + $csv_file = UploadedFile::fake()->createWithContent('current_page.csv', $csv_content); + + $response = $this->actingAs($admin)->post('/manage/page/upload', [ + 'page_csv' => $csv_file, + ]); + + $response->assertStatus(302); + $response->assertRedirect('/manage/page/import'); + $response->assertSessionHas('flash_message', 'インポートしました。'); + $this->assertDatabaseHas('pages', [ + 'page_name' => 'current', + 'permanent_link' => '/current-import-test', + 'layout_inherit_flag' => 0, + 'base_display_flag' => 1, + ]); + } +} diff --git a/tests/Feature/Plugins/User/Forms/FormsHoneypotTest.php b/tests/Feature/Plugins/User/Forms/FormsHoneypotTest.php new file mode 100644 index 000000000..dfe9c3c70 --- /dev/null +++ b/tests/Feature/Plugins/User/Forms/FormsHoneypotTest.php @@ -0,0 +1,330 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category フォームプラグイン + */ +class FormsHoneypotTest extends TestCase +{ + use RefreshDatabase; + + protected function setUp(): void + { + parent::setUp(); + $this->seed(); + } + + /** + * テスト用のページ、フレーム、バケツ、フォームを作成 + */ + private function createFormSetup() + { + $page = Page::factory()->create(); + $bucket = Buckets::factory()->create(['plugin_name' => 'forms']); + $frame = Frame::factory()->create([ + 'page_id' => $page->id, + 'plugin_name' => 'forms', + 'bucket_id' => $bucket->id, + ]); + + $form = Forms::factory()->create(['bucket_id' => $bucket->id]); + + return [$page, $frame, $bucket, $form]; + } + + /** + * ハニーポットをスパムリストに追加 + */ + private function addHoneypotToSpamList($form, $is_global = false): SpamList + { + return SpamList::factory()->create([ + 'target_plugin_name' => 'forms', + 'target_id' => $is_global ? null : $form->id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => null, + ]); + } + + /** + * コンテンツ管理者ユーザーを作成 + */ + private function createAdminUser(): User + { + $user = User::factory()->create(); + UsersRoles::factory()->create([ + 'users_id' => $user->id, + 'target' => 'base', + 'role_name' => 'role_article_admin', + ]); + return $user; + } + + /** + * addSpamList(): ハニーポットをスパムリストに追加できる + */ + public function testAddSpamListCanAddHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addSpamList/{$page->id}/{$frame->id}/{$form->id}", + [ + 'block_type' => SpamBlockType::honeypot, + // ハニーポットは値不要 + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $response->assertSessionHas('flash_message'); + + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => 'forms', + 'target_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => null, + ]); + } + + /** + * deleteSpamList(): ハニーポットをスパムリストから削除できる + */ + public function testDeleteSpamListCanRemoveHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + $honeypot = $this->addHoneypotToSpamList($form); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/deleteSpamList/{$page->id}/{$frame->id}/{$honeypot->id}", + [ + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + // 論理削除されていることを確認 + $this->assertSoftDeleted('spam_lists', [ + 'id' => $honeypot->id, + ]); + } + + /** + * publicConfirm(): ハニーポットが空なら確認画面に進める(履歴は記録されない) + */ + public function testPublicConfirmAllowsEmptyHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + $history_count_before = SpamBlockHistory::count(); + + // Act: /plugin/ を使用して直接メソッドを呼び出す + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => '', // ハニーポットは空 + ] + ); + + // Assert: ハニーポットブロック履歴が作成されていない + $this->assertEquals($history_count_before, SpamBlockHistory::where('block_type', SpamBlockType::honeypot)->count()); + } + + /** + * publicConfirm(): ハニーポットに値があるとブロックされ、履歴が記録される + */ + public function testPublicConfirmBlocksFilledHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act: /plugin/ を使用して直接メソッドを呼び出す + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', // ハニーポットに値がある + ] + ); + + // Assert: ハニーポットブロック履歴が作成されている + $this->assertDatabaseHas('spam_block_histories', [ + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => 'http://spam-site.com', + 'spam_list_id' => $honeypot->id, + ]); + + // レスポンスにエラーメッセージが含まれている + $response->assertSee(__('messages.honeypot_blocked')); + } + + /** + * publicConfirm(): ハニーポット無効時は値があってもブロックされない + */ + public function testPublicConfirmDoesNotBlockWhenHoneypotDisabled(): void + { + // Arrange: ハニーポット無効(スパムリストに登録なし) + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + $history_count_before = SpamBlockHistory::count(); + + // Act + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', // ハニーポットに値がある + ] + ); + + // Assert: ハニーポットブロック履歴が作成されていない + $this->assertEquals($history_count_before, SpamBlockHistory::where('block_type', SpamBlockType::honeypot)->count()); + } + + /** + * publicConfirm(): グローバルハニーポットでもブロックされる + */ + public function testPublicConfirmBlocksWithGlobalHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form, true); // グローバル設定 + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', + ] + ); + + // Assert: グローバルハニーポットでもブロックされる + $this->assertDatabaseHas('spam_block_histories', [ + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'spam_list_id' => $honeypot->id, + ]); + } + + /** + * publicStore(): ハニーポットに値があるとブロックされる(二重防御) + */ + public function testPublicStoreBlocksFilledHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act: 直接登録にPOST (リダイレクト経由) + $response = $this->post( + "/redirect/plugin/forms/publicStore/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', + 'redirect_path' => $page->permanent_link, + ] + ); + + // Assert: リダイレクトされる + $response->assertRedirect(); + + // spam_block_histories にレコードが作成されている + $this->assertDatabaseHas('spam_block_histories', [ + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'spam_list_id' => $honeypot->id, + ]); + } + + /** + * 履歴に記録されるデータが正しい + */ + public function testHoneypotBlockHistoryRecordsCorrectData(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'bot-filled-value', + ], + ['REMOTE_ADDR' => '10.0.0.1'] + ); + + // Assert: 記録されたデータの各フィールドが正しい + $history = SpamBlockHistory::latest('id')->first(); + $this->assertNotNull($history); + $this->assertEquals($honeypot->id, $history->spam_list_id); + $this->assertEquals($form->id, $history->forms_id); + $this->assertEquals(SpamBlockType::honeypot, $history->block_type); + $this->assertEquals('bot-filled-value', $history->block_value); + $this->assertNull($history->submitted_email); + $this->assertNotNull($history->client_ip); + $this->assertNotNull($history->created_at); + } +} diff --git a/tests/Feature/Plugins/User/Forms/FormsSpamFilterTest.php b/tests/Feature/Plugins/User/Forms/FormsSpamFilterTest.php new file mode 100644 index 000000000..2b2667cf7 --- /dev/null +++ b/tests/Feature/Plugins/User/Forms/FormsSpamFilterTest.php @@ -0,0 +1,487 @@ +seed(); + } + + /** + * テスト用のページ、フレーム、バケツ、フォームを作成 + */ + private function createFormSetup($with_spam_filter = false) + { + $page = Page::factory()->create(); + $bucket = Buckets::factory()->create(['plugin_name' => 'forms']); + $frame = Frame::factory()->create([ + 'page_id' => $page->id, + 'plugin_name' => 'forms', + 'bucket_id' => $bucket->id, + ]); + + $form_data = ['bucket_id' => $bucket->id]; + if ($with_spam_filter) { + $form_data['use_spam_filter_flag'] = 1; + $form_data['spam_filter_message'] = 'スパムとして検出されました。'; + } + + $form = Forms::factory()->create($form_data); + + return [$page, $frame, $bucket, $form]; + } + + /** + * コンテンツ管理者ユーザーを作成 + */ + private function createAdminUser(): User + { + $user = User::factory()->create(); + UsersRoles::factory()->create([ + 'users_id' => $user->id, + 'target' => 'base', + 'role_name' => 'role_article_admin', + ]); + return $user; + } + + /** + * saveSpamFilter(): スパムフィルタリングを有効にできる + */ + public function testSaveSpamFilterCanEnableFiltering(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/saveSpamFilter/{$page->id}/{$frame->id}/{$form->id}", + [ + 'use_spam_filter_flag' => 1, + 'spam_filter_message' => 'カスタムメッセージ', + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $response->assertSessionHas('flash_message'); + + $this->assertDatabaseHas('forms', [ + 'id' => $form->id, + 'use_spam_filter_flag' => 1, + 'spam_filter_message' => 'カスタムメッセージ', + ]); + } + + /** + * saveSpamFilter(): スパムフィルタリングを無効にできる + */ + public function testSaveSpamFilterCanDisableFiltering(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(true); + $admin = $this->createAdminUser(); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/saveSpamFilter/{$page->id}/{$frame->id}/{$form->id}", + [ + 'use_spam_filter_flag' => 0, + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + + $this->assertDatabaseHas('forms', [ + 'id' => $form->id, + 'use_spam_filter_flag' => 0, + ]); + } + + /** + * addSpamList(): フォーム個別のスパムリストを追加できる + */ + public function testAddSpamListCanAddFormSpecificSpamList(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addSpamList/{$page->id}/{$frame->id}/{$form->id}", + [ + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + 'memo' => 'テストメモ', + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $response->assertSessionHas('flash_message'); + + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => 'forms', + 'target_id' => $form->id, + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + 'memo' => 'テストメモ', + ]); + } + + /** + * addSpamList(): block_typeが必須 + */ + public function testAddSpamListValidatesBlockTypeRequired(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addSpamList/{$page->id}/{$frame->id}/{$form->id}", + [ + 'block_value' => 'test@example.com', + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $response->assertSessionHasErrors('block_type'); + } + + /** + * addToSpamListFromInput(): IPアドレスをスパムリストに追加できる + */ + public function testAddToSpamListFromInputCanAddIpAddress(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + $input = FormsInputs::factory()->withIpAddress('192.168.1.100')->create([ + 'forms_id' => $form->id, + ]); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addToSpamListFromInput/{$page->id}/{$frame->id}/{$input->id}", + [ + 'add_ip_address' => 1, + 'scope_type' => 'form', + 'memo' => 'スパム投稿', + 'redirect_path' => "/plugin/forms/listInputs/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $response->assertSessionHas('flash_message'); + + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => 'forms', + 'target_id' => $form->id, + 'block_type' => SpamBlockType::ip_address, + 'block_value' => '192.168.1.100', + 'memo' => 'スパム投稿', + ]); + } + + /** + * addToSpamListFromInput(): メールアドレスをスパムリストに追加できる + */ + public function testAddToSpamListFromInputCanAddEmail(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + $input = FormsInputs::factory()->create(['forms_id' => $form->id]); + $email_column = FormsColumns::factory()->emailType()->create(['forms_id' => $form->id]); + \App\Models\User\Forms\FormsInputCols::factory()->create([ + 'forms_inputs_id' => $input->id, + 'forms_columns_id' => $email_column->id, + 'value' => 'spam@example.com', + ]); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addToSpamListFromInput/{$page->id}/{$frame->id}/{$input->id}", + [ + 'add_email' => 1, + 'scope_type' => 'global', + 'redirect_path' => "/plugin/forms/listInputs/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => 'forms', + 'target_id' => null, // global + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + ]); + } + + /** + * addToSpamListFromInput(): ドメインをスパムリストに追加できる + */ + public function testAddToSpamListFromInputCanAddDomain(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + $input = FormsInputs::factory()->create(['forms_id' => $form->id]); + $email_column = FormsColumns::factory()->emailType()->create(['forms_id' => $form->id]); + \App\Models\User\Forms\FormsInputCols::factory()->create([ + 'forms_inputs_id' => $input->id, + 'forms_columns_id' => $email_column->id, + 'value' => 'user@spam-domain.com', + ]); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addToSpamListFromInput/{$page->id}/{$frame->id}/{$input->id}", + [ + 'add_domain' => 1, + 'scope_type' => 'global', + 'redirect_path' => "/plugin/forms/listInputs/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => 'forms', + 'target_id' => null, + 'block_type' => SpamBlockType::domain, + 'block_value' => 'spam-domain.com', + ]); + } + + /** + * addToSpamListFromInput(): 複数項目を同時に追加できる + */ + public function testAddToSpamListFromInputCanAddMultipleItems(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + $input = FormsInputs::factory()->withIpAddress('192.168.1.100')->create([ + 'forms_id' => $form->id, + ]); + $email_column = FormsColumns::factory()->emailType()->create(['forms_id' => $form->id]); + \App\Models\User\Forms\FormsInputCols::factory()->create([ + 'forms_inputs_id' => $input->id, + 'forms_columns_id' => $email_column->id, + 'value' => 'user@spam-domain.com', + ]); + + // Act: IPアドレス、メール、ドメインを同時に追加 + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addToSpamListFromInput/{$page->id}/{$frame->id}/{$input->id}", + [ + 'add_ip_address' => 1, + 'add_email' => 1, + 'add_domain' => 1, + 'scope_type' => 'global', + 'redirect_path' => "/plugin/forms/listInputs/{$page->id}/{$frame->id}", + ] + ); + + // Assert: 3件追加される + $response->assertStatus(302); + $this->assertEquals(3, SpamList::count()); + } + + /** + * addToSpamListFromInput(): 重複データは追加されない + */ + public function testAddToSpamListFromInputDoesNotAddDuplicates(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + $input = FormsInputs::factory()->withIpAddress('192.168.1.100')->create([ + 'forms_id' => $form->id, + ]); + + // 事前にスパムリストに追加 + SpamList::factory()->forForm($form->id)->create([ + 'target_plugin_name' => 'forms', + 'block_type' => SpamBlockType::ip_address, + 'block_value' => '192.168.1.100', + ]); + + $count_before = SpamList::count(); + + // Act: 同じデータで追加試行 + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addToSpamListFromInput/{$page->id}/{$frame->id}/{$input->id}", + [ + 'add_ip_address' => 1, + 'scope_type' => 'form', + 'redirect_path' => "/plugin/forms/listInputs/{$page->id}/{$frame->id}", + ] + ); + + // Assert: 件数が増えていない + $response->assertStatus(302); + $this->assertEquals($count_before, SpamList::count()); + + // フラッシュメッセージに登録済みの旨が含まれる + $flash_message = session('flash_message'); + $this->assertStringContainsString('登録済み', $flash_message); + } + + /** + * 確認画面でスパムブロック時に spam_block_histories にレコードが作成される + */ + public function testSpamBlockCreatesHistoryRecordOnConfirm(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(true); + + // メールカラムを作成 + $email_column = FormsColumns::factory()->emailType()->create(['forms_id' => $form->id]); + + // IPアドレスでブロックするスパムリスト + $spam_list = SpamList::factory()->forForm($form->id)->create([ + 'target_plugin_name' => 'forms', + 'block_type' => SpamBlockType::ip_address, + 'block_value' => '127.0.0.1', + ]); + + // Act: 確認画面にPOST + $response = $this->post( + "/redirect/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $email_column->id => 'test@example.com', + ], + 'redirect_path' => $page->permanent_link, + ], + ['REMOTE_ADDR' => '127.0.0.1'] + ); + + // Assert: spam_block_histories にレコードが作成されている + $this->assertDatabaseHas('spam_block_histories', [ + 'spam_list_id' => $spam_list->id, + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::ip_address, + 'block_value' => '127.0.0.1', + ]); + } + + /** + * publicStore(直接登録)でスパムブロック時にレコードが作成される + */ + public function testSpamBlockCreatesHistoryRecordOnPublicStore(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(true); + + // メールカラムを作成 + $email_column = FormsColumns::factory()->emailType()->create(['forms_id' => $form->id]); + + // メールアドレスでブロックするスパムリスト + $spam_list = SpamList::factory()->global()->create([ + 'target_plugin_name' => 'forms', + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + ]); + + // Act: 直接登録にPOST + $response = $this->post( + "/redirect/plugin/forms/publicStore/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $email_column->id => 'spam@example.com', + ], + 'redirect_path' => $page->permanent_link, + ] + ); + + // Assert: spam_block_histories にレコードが作成されている + $this->assertDatabaseHas('spam_block_histories', [ + 'spam_list_id' => $spam_list->id, + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + 'submitted_email' => 'spam@example.com', + ]); + } + + /** + * 記録されるデータが正しい + */ + public function testSpamBlockHistoryRecordsCorrectData(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(true); + + // メールカラムを作成 + $email_column = FormsColumns::factory()->emailType()->create(['forms_id' => $form->id]); + + // ドメインでブロックするスパムリスト + $spam_list = SpamList::factory()->forForm($form->id)->create([ + 'target_plugin_name' => 'forms', + 'block_type' => SpamBlockType::domain, + 'block_value' => 'spam-domain.com', + ]); + + // Act: 確認画面にPOST(ドメインがマッチするメールアドレスを送信) + $response = $this->post( + "/redirect/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $email_column->id => 'user@spam-domain.com', + ], + 'redirect_path' => $page->permanent_link, + ], + ['REMOTE_ADDR' => '10.0.0.1'] + ); + + // Assert: 記録されたデータの各フィールドが正しい + $history = SpamBlockHistory::latest('id')->first(); + $this->assertNotNull($history); + $this->assertEquals($spam_list->id, $history->spam_list_id); + $this->assertEquals($form->id, $history->forms_id); + $this->assertEquals(SpamBlockType::domain, $history->block_type); + $this->assertEquals('spam-domain.com', $history->block_value); + $this->assertEquals('user@spam-domain.com', $history->submitted_email); + $this->assertNotNull($history->client_ip); + $this->assertNotNull($history->created_at); + } +} diff --git a/tests/Feature/Plugins/User/Photoalbums/PhotoalbumsMoreContentsFeatureTest.php b/tests/Feature/Plugins/User/Photoalbums/PhotoalbumsMoreContentsFeatureTest.php new file mode 100644 index 000000000..6488ad3ae --- /dev/null +++ b/tests/Feature/Plugins/User/Photoalbums/PhotoalbumsMoreContentsFeatureTest.php @@ -0,0 +1,212 @@ +seed(); + } + + /** + * target が不正な場合は 400 を返すこと。 + */ + public function testMoreContentsReturns400WhenTargetIsInvalid(): void + { + [$page, $frame, $root] = $this->makePhotoalbumFrame(); + + $response = $this->getJson("/json/photoalbums/moreContents/{$page->id}/{$frame->id}/{$root->id}?target=invalid"); + + $response->assertStatus(400) + ->assertJson([ + 'message' => 'invalid target', + ]); + } + + /** + * offset / limit の境界値を安全に扱うこと。 + */ + public function testMoreContentsOffsetAndLimitBoundary(): void + { + [$page, $frame, $root, $photoalbum] = $this->makePhotoalbumFrame([ + PhotoalbumFrameConfig::sort_file => PhotoalbumSort::manual_order, + PhotoalbumFrameConfig::load_more_use_flag => UseType::not_use, + ]); + + $first = $this->createImageContent($root, $photoalbum->id, 1, 'first.jpg'); + $second = $this->createImageContent($root, $photoalbum->id, 2, 'second.jpg'); + $this->createImageContent($root, $photoalbum->id, 3, 'third.jpg'); + $this->createImageContent($root, $photoalbum->id, 4, 'fourth.jpg'); + + Config::set('photoalbums.load_more_max_limit', 2); + + $response = $this->getJson("/json/photoalbums/moreContents/{$page->id}/{$frame->id}/{$root->id}?target=image&offset=-5&limit=999"); + $response->assertStatus(200) + ->assertJson([ + 'next_offset' => 2, + 'total' => 4, + ]); + + $ids = $this->extractImageIds($response->json('html'), $frame->id); + $this->assertSame([$first->id, $second->id], $ids); + + $last_response = $this->getJson("/json/photoalbums/moreContents/{$page->id}/{$frame->id}/{$root->id}?target=image&offset=4&limit=1"); + $last_response->assertStatus(200) + ->assertJson([ + 'html' => '', + 'next_offset' => 4, + 'total' => 4, + ]); + } + + /** + * index 初回表示と moreContents の並び順が一致すること(名前順・降順)。 + */ + public function testIndexAndMoreContentsKeepSameOrderForNameSort(): void + { + [$page, $frame, $root, $photoalbum] = $this->makePhotoalbumFrame([ + PhotoalbumFrameConfig::sort_file => PhotoalbumSort::name_desc, + PhotoalbumFrameConfig::load_more_use_flag => UseType::use, + PhotoalbumFrameConfig::load_more_count => 2, + ]); + + $gamma = $this->createImageContent($root, $photoalbum->id, 1, 'gamma.jpg'); + $alpha = $this->createImageContent($root, $photoalbum->id, 2, 'alpha.jpg'); + $beta = $this->createImageContent($root, $photoalbum->id, 3, 'beta.jpg'); + + $expected_order = [$gamma->id, $beta->id, $alpha->id]; + + $index_response = $this->get("/plugin/photoalbums/changeDirectory/{$page->id}/{$frame->id}/{$root->id}"); + $index_response->assertStatus(200); + $index_ids = $this->extractImageIds($index_response->getContent(), $frame->id); + $this->assertSame(array_slice($expected_order, 0, 2), $index_ids); + + $first_more_response = $this->getJson("/json/photoalbums/moreContents/{$page->id}/{$frame->id}/{$root->id}?target=image&offset=0&limit=99"); + $first_more_response->assertStatus(200) + ->assertJson([ + 'next_offset' => 2, + 'total' => 3, + ]); + $first_more_ids = $this->extractImageIds($first_more_response->json('html'), $frame->id); + $this->assertSame($index_ids, $first_more_ids); + + $second_more_response = $this->getJson("/json/photoalbums/moreContents/{$page->id}/{$frame->id}/{$root->id}?target=image&offset=2&limit=99"); + $second_more_response->assertStatus(200) + ->assertJson([ + 'next_offset' => 3, + 'total' => 3, + ]); + $second_more_ids = $this->extractImageIds($second_more_response->json('html'), $frame->id); + + $this->assertSame($expected_order, array_merge($first_more_ids, $second_more_ids)); + } + + /** + * フォトアルバム用のページ・フレーム・バケツ・ルートを作る。 + * + * @param array $frame_config_values + * @return array + */ + private function makePhotoalbumFrame(array $frame_config_values = []): array + { + $page = Page::where('permanent_link', '/')->first() ?? Page::factory()->create([ + 'permanent_link' => '/', + 'page_name' => 'home', + ]); + + $bucket = Buckets::factory()->create([ + 'bucket_name' => 'テストフォトアルバム', + 'plugin_name' => 'photoalbums', + ]); + + $frame = Frame::factory()->create([ + 'page_id' => $page->id, + 'area_id' => 2, + 'plugin_name' => 'photoalbums', + 'bucket_id' => $bucket->id, + 'template' => 'default', + 'display_sequence' => 1, + ]); + + $photoalbum = Photoalbum::create([ + 'bucket_id' => $bucket->id, + 'name' => 'テストフォトアルバム', + 'image_upload_max_size' => UploadMaxSize::two_mega_byte, + 'image_upload_max_px' => ResizedImageSize::big, + 'video_upload_max_size' => UploadMaxSize::ten_mega_byte, + ]); + + $root = PhotoalbumContent::create([ + 'photoalbum_id' => $photoalbum->id, + 'upload_id' => null, + 'name' => $photoalbum->name, + 'is_folder' => PhotoalbumContent::is_folder_on, + 'is_cover' => PhotoalbumContent::is_cover_off, + 'display_sequence' => 1, + 'parent_id' => null, + ]); + + foreach ($frame_config_values as $name => $value) { + FrameConfig::updateOrCreate( + ['frame_id' => $frame->id, 'name' => $name], + ['value' => (string) $value] + ); + } + + return [$page, $frame, $root, $photoalbum]; + } + + /** + * 画像コンテンツを作成する。 + */ + private function createImageContent(PhotoalbumContent $parent, int $photoalbum_id, int $display_sequence, string $original_name): PhotoalbumContent + { + $upload = Uploads::factory()->jpg()->create([ + 'client_original_name' => $original_name, + 'plugin_name' => 'photoalbums', + ]); + + return $parent->children()->create([ + 'photoalbum_id' => $photoalbum_id, + 'upload_id' => $upload->id, + 'name' => pathinfo($original_name, PATHINFO_FILENAME), + 'is_folder' => PhotoalbumContent::is_folder_off, + 'is_cover' => PhotoalbumContent::is_cover_off, + 'display_sequence' => $display_sequence, + 'mimetype' => $upload->mimetype, + ]); + } + + /** + * レンダリングされた HTML から画像IDを順番に抽出する。 + * + * @param string $html + * @param int $frame_id + * @return array + */ + private function extractImageIds(string $html, int $frame_id): array + { + preg_match_all('/id="photo_' . $frame_id . '_(\d+)"/', $html, $matches); + return array_map('intval', $matches[1] ?? []); + } +} diff --git a/tests/Unit/Controllers/Core/DefaultControllerLayoutTest.php b/tests/Unit/Controllers/Core/DefaultControllerLayoutTest.php new file mode 100644 index 000000000..4ce610bdc --- /dev/null +++ b/tests/Unit/Controllers/Core/DefaultControllerLayoutTest.php @@ -0,0 +1,112 @@ +attributes->set('configs', collect($configs)); + } + + protected function newConfig(string $name, ?string $value): Configs + { + $config = new Configs(); + $config->name = $name; + $config->value = $value; + return $config; + } + + protected function callGetLayout(Page $page, $page_tree): string + { + $controller = new DefaultController(); + $method = new \ReflectionMethod(DefaultController::class, 'getLayout'); + $method->setAccessible(true); + + return $method->invoke($controller, $page, $page_tree); + } + + protected function tearDown(): void + { + $this->setSharedConfigs([]); + parent::tearDown(); + } + + /** + * 祖先の layout_inherit_flag=0 を継承対象から除外すること。 + */ + public function testGetLayoutSkipsAncestorsWithLayoutInheritFlagOff(): void + { + $current = new Page(); + $current->id = 3; + $current->layout = null; + $current->layout_inherit_flag = 1; + + $parent = new Page(); + $parent->id = 2; + $parent->layout = '1|1|1|1'; + $parent->layout_inherit_flag = 0; + + $grandparent = new Page(); + $grandparent->id = 1; + $grandparent->layout = '0|1|0|1'; + $grandparent->layout_inherit_flag = 1; + + $this->setSharedConfigs([ + $this->newConfig('base_layout', '1|0|0|1'), + ]); + + $layout = $this->callGetLayout($current, collect([$current, $parent, $grandparent])); + + $this->assertSame('0|1|0|1', $layout); + } + + /** + * ツリー内にレイアウトが無い場合は基本レイアウトを採用すること。 + */ + public function testGetLayoutFallsBackToBaseLayoutWhenNoLayoutInTree(): void + { + $current = new Page(); + $current->id = 1; + $current->layout = null; + + $parent = new Page(); + $parent->id = 2; + $parent->layout = null; + + $this->setSharedConfigs([ + $this->newConfig('base_layout', '1|0|0|1'), + ]); + + $layout = $this->callGetLayout($current, collect([$current, $parent])); + + $this->assertSame('1|0|0|1', $layout); + } + + /** + * 基本レイアウトが空なら初期値にフォールバックすること。 + */ + public function testGetLayoutFallsBackToDefaultWhenBaseLayoutEmpty(): void + { + $current = new Page(); + $current->id = 1; + $current->layout = null; + + $this->setSharedConfigs([ + $this->newConfig('base_layout', ''), + ]); + + $layout = $this->callGetLayout($current, collect([$current])); + + $this->assertSame(config('connect.BASE_LAYOUT_DEFAULT'), $layout); + } +} diff --git a/tests/Unit/Models/Common/SpamBlockHistoryTest.php b/tests/Unit/Models/Common/SpamBlockHistoryTest.php new file mode 100644 index 000000000..efcc35bed --- /dev/null +++ b/tests/Unit/Models/Common/SpamBlockHistoryTest.php @@ -0,0 +1,156 @@ +seed(); + } + + /** + * fillable属性でレコードを作成できる + */ + public function testCreateBlockHistory(): void + { + // Arrange + $spam_list = SpamList::factory()->global()->create(); + + // Act + $history = SpamBlockHistory::create([ + 'spam_list_id' => $spam_list->id, + 'forms_id' => 1, + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + 'client_ip' => '192.168.1.1', + 'submitted_email' => 'user@example.com', + ]); + + // Assert + $this->assertDatabaseHas('spam_block_histories', [ + 'id' => $history->id, + 'spam_list_id' => $spam_list->id, + 'forms_id' => 1, + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + 'client_ip' => '192.168.1.1', + 'submitted_email' => 'user@example.com', + ]); + } + + /** + * updated_at が使用されない(UPDATED_AT = null) + */ + public function testUpdatedAtIsNull(): void + { + // Act + $history = SpamBlockHistory::factory()->create(); + + // Assert: updated_at カラムは存在しないため、属性に含まれない + $this->assertNull(SpamBlockHistory::UPDATED_AT); + $this->assertNotNull($history->created_at); + } + + /** + * spamList() リレーションが正しく動作する + */ + public function testSpamListRelation(): void + { + // Arrange + $spam_list = SpamList::factory()->global()->create([ + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + ]); + + $history = SpamBlockHistory::factory()->withSpamList($spam_list->id)->create([ + 'block_type' => SpamBlockType::email, + 'block_value' => 'spam@example.com', + ]); + + // Act + $related = $history->spamList; + + // Assert + $this->assertNotNull($related); + $this->assertEquals($spam_list->id, $related->id); + $this->assertInstanceOf(SpamList::class, $related); + } + + /** + * spam_list_id に対応するレコードが存在しない場合 null を返す + */ + public function testSpamListRelationReturnsNullWhenDeleted(): void + { + // Arrange: 存在しないIDを指定 + $history = SpamBlockHistory::factory()->create([ + 'spam_list_id' => 99999, + ]); + + // Act + $related = $history->spamList; + + // Assert + $this->assertNull($related); + } + + /** + * forms_id が null でも作成できる + */ + public function testFormsIdIsNullable(): void + { + // Act + $history = SpamBlockHistory::factory()->create([ + 'forms_id' => null, + ]); + + // Assert + $this->assertDatabaseHas('spam_block_histories', [ + 'id' => $history->id, + 'forms_id' => null, + ]); + } + + /** + * client_ip が null でも作成できる + */ + public function testClientIpIsNullable(): void + { + // Act + $history = SpamBlockHistory::factory()->create([ + 'client_ip' => null, + ]); + + // Assert + $this->assertDatabaseHas('spam_block_histories', [ + 'id' => $history->id, + 'client_ip' => null, + ]); + } + + /** + * submitted_email が null でも作成できる + */ + public function testSubmittedEmailIsNullable(): void + { + // Act + $history = SpamBlockHistory::factory()->create([ + 'submitted_email' => null, + ]); + + // Assert + $this->assertDatabaseHas('spam_block_histories', [ + 'id' => $history->id, + 'submitted_email' => null, + ]); + } +} diff --git a/tests/Unit/Models/Common/SpamListTest.php b/tests/Unit/Models/Common/SpamListTest.php new file mode 100644 index 000000000..00943d527 --- /dev/null +++ b/tests/Unit/Models/Common/SpamListTest.php @@ -0,0 +1,295 @@ +seed(); + } + + /** + * getFormsSpamLists(): forms_idを指定した場合、該当フォームと全体のスパムリストを取得 + */ + public function testGetFormsSpamListsWithFormsId(): void + { + // Arrange: テストデータ作成 + $target_forms_id = 1; + $other_forms_id = 2; + + // 対象フォーム用スパムリスト + $spam_for_target = SpamList::factory()->forForm($target_forms_id)->create(); + // 全体スパムリスト + $spam_global = SpamList::factory()->global()->create(); + // 他のフォーム用スパムリスト(取得されないはず) + $spam_for_other = SpamList::factory()->forForm($other_forms_id)->create(); + + // Act: メソッド実行 + $result = SpamList::getFormsSpamLists($target_forms_id); + + // Assert: 対象フォームと全体のスパムリストのみ取得される + $this->assertCount(2, $result); + $this->assertTrue($result->contains('id', $spam_for_target->id)); + $this->assertTrue($result->contains('id', $spam_global->id)); + $this->assertFalse($result->contains('id', $spam_for_other->id)); + } + + /** + * getFormsSpamLists(): forms_idがnullの場合、全体のみのスパムリストを取得 + */ + public function testGetFormsSpamListsWithNullFormsId(): void + { + // Arrange + $forms_id = 1; + + // 全体スパムリスト + $spam_global = SpamList::factory()->global()->create(); + // フォーム個別スパムリスト(取得されないはず) + $spam_for_form = SpamList::factory()->forForm($forms_id)->create(); + + // Act + $result = SpamList::getFormsSpamLists(null); + + // Assert: 全体のみ取得される + $this->assertCount(1, $result); + $this->assertTrue($result->contains('id', $spam_global->id)); + $this->assertFalse($result->contains('id', $spam_for_form->id)); + } + + /** + * getFormsSpamLists(): block_typeとcreated_atで正しくソート + */ + public function testGetFormsSpamListsOrderedByBlockTypeAndCreatedAt(): void + { + // Arrange: 異なるblock_typeとcreated_atでデータ作成 + $spam1 = SpamList::factory()->global()->create([ + 'block_type' => SpamBlockType::email, + 'created_at' => now()->subDays(2), + ]); + $spam2 = SpamList::factory()->global()->create([ + 'block_type' => SpamBlockType::email, + 'created_at' => now()->subDays(1), + ]); + $spam3 = SpamList::factory()->global()->create([ + 'block_type' => SpamBlockType::domain, + 'created_at' => now()->subDays(3), + ]); + $spam4 = SpamList::factory()->global()->create([ + 'block_type' => SpamBlockType::ip_address, + 'created_at' => now(), + ]); + + // Act + $result = SpamList::getFormsSpamLists(null); + + // Assert: block_type順、同じblock_typeならcreated_at降順 + $this->assertCount(4, $result); + + // block_typeでグループ化してチェック + $email_spams = $result->filter(fn($s) => $s->block_type == SpamBlockType::email)->values(); + $this->assertEquals($spam2->id, $email_spams[0]->id); + $this->assertEquals($spam1->id, $email_spams[1]->id); + + $domain_spams = $result->filter(fn($s) => $s->block_type == SpamBlockType::domain)->values(); + $this->assertEquals($spam3->id, $domain_spams[0]->id); + + $ip_spams = $result->filter(fn($s) => $s->block_type == SpamBlockType::ip_address)->values(); + $this->assertEquals($spam4->id, $ip_spams[0]->id); + } + + /** + * getFormsSpamLists(): 境界値 - スパムリストが0件の場合 + */ + public function testGetFormsSpamListsWithNoData(): void + { + // Act + $result = SpamList::getFormsSpamLists(1); + + // Assert: 空のCollectionが返る + $this->assertCount(0, $result); + } + + /** + * getFormsSpamLists(): 異なるプラグインのスパムリストは含まれない + */ + public function testGetFormsSpamListsExcludesOtherPlugins(): void + { + // Arrange + $forms_id = 1; + + // forms用スパムリスト + $spam_forms = SpamList::factory()->forForm($forms_id)->create([ + 'target_plugin_name' => 'forms', + ]); + + // 他のプラグイン用スパムリスト(取得されないはず) + $spam_other_plugin = SpamList::factory()->forForm($forms_id)->create([ + 'target_plugin_name' => 'bbs', + ]); + + // Act + $result = SpamList::getFormsSpamLists($forms_id); + + // Assert: forms用のみ取得される + $this->assertCount(1, $result); + $this->assertTrue($result->contains('id', $spam_forms->id)); + $this->assertFalse($result->contains('id', $spam_other_plugin->id)); + } + + /** + * addIfNotExists(): 新規データを正常に追加できる + */ + public function testAddIfNotExistsAddsNewRecord(): void + { + // Arrange + $target_plugin_name = 'forms'; + $target_id = 1; + $block_type = SpamBlockType::email; + $block_value = 'spam@example.com'; + $memo = 'テストメモ'; + + // 現在のユーザーを設定(UserableNohistoryのため) + $user = User::factory()->create(); + $this->actingAs($user); + + // Act + $result = SpamList::addIfNotExists($target_plugin_name, $target_id, $block_type, $block_value, $memo); + + // Assert: 追加成功(戻り値true) + $this->assertTrue($result); + + // データが存在する + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => $target_plugin_name, + 'target_id' => $target_id, + 'block_type' => $block_type, + 'block_value' => $block_value, + 'memo' => $memo, + 'created_id' => $user->id, + ]); + } + + /** + * addIfNotExists(): 重複データは追加しない + */ + public function testAddIfNotExistsDoesNotAddDuplicate(): void + { + // Arrange: 既存データ作成 + $target_plugin_name = 'forms'; + $target_id = 1; + $block_type = SpamBlockType::email; + $block_value = 'spam@example.com'; + + $existing_spam = SpamList::factory()->create([ + 'target_plugin_name' => $target_plugin_name, + 'target_id' => $target_id, + 'block_type' => $block_type, + 'block_value' => $block_value, + 'memo' => '既存メモ', + ]); + + $count_before = SpamList::count(); + + // Act: 同じデータで追加試行 + $result = SpamList::addIfNotExists($target_plugin_name, $target_id, $block_type, $block_value, '新しいメモ'); + + // Assert: 追加失敗(戻り値false) + $this->assertFalse($result); + + // レコード数が増えていない + $this->assertEquals($count_before, SpamList::count()); + } + + /** + * addIfNotExists(): memoは重複判定に影響しない + */ + public function testAddIfNotExistsMemoDoesNotAffectDuplication(): void + { + // Arrange: 既存データ作成 + $target_plugin_name = 'forms'; + $target_id = 1; + $block_type = SpamBlockType::email; + $block_value = 'spam@example.com'; + + SpamList::factory()->create([ + 'target_plugin_name' => $target_plugin_name, + 'target_id' => $target_id, + 'block_type' => $block_type, + 'block_value' => $block_value, + 'memo' => 'メモ1', + ]); + + // Act: memoだけ違うデータで追加試行 + $result = SpamList::addIfNotExists($target_plugin_name, $target_id, $block_type, $block_value, 'メモ2'); + + // Assert: 重複と判定される + $this->assertFalse($result); + } + + /** + * addIfNotExists(): target_idがnullでも正しく動作 + */ + public function testAddIfNotExistsWorksWithNullTargetId(): void + { + // Arrange + $target_plugin_name = 'forms'; + $target_id = null; + $block_type = SpamBlockType::domain; + $block_value = 'spam-domain.com'; + + $user = User::factory()->create(); + $this->actingAs($user); + + // Act + $result = SpamList::addIfNotExists($target_plugin_name, $target_id, $block_type, $block_value); + + // Assert: 追加成功 + $this->assertTrue($result); + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => $target_plugin_name, + 'target_id' => null, + 'block_type' => $block_type, + 'block_value' => $block_value, + ]); + + // 再度追加試行 + $result2 = SpamList::addIfNotExists($target_plugin_name, $target_id, $block_type, $block_value); + + // Assert: 重複で追加失敗 + $this->assertFalse($result2); + } + + /** + * isGlobalScope(): target_idがnullの場合、trueを返す + */ + public function testIsGlobalScopeReturnsTrueWhenTargetIdIsNull(): void + { + // Arrange + $spam = SpamList::factory()->global()->create(); + + // Act & Assert + $this->assertTrue($spam->isGlobalScope()); + } + + /** + * isGlobalScope(): target_idが設定されている場合、falseを返す + */ + public function testIsGlobalScopeReturnsFalseWhenTargetIdIsSet(): void + { + // Arrange + $spam = SpamList::factory()->forForm(1)->create(); + + // Act & Assert + $this->assertFalse($spam->isGlobalScope()); + } +}