diff --git a/.gitignore b/.gitignore index a81a5ba..9b038a4 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ coverage*.json *.log positions.json library.json +site/ad_*.html + # ── 다운로드 테스트 결과물 (엔진 검증 중 루트에 떨어질 수 있음) ───── *.mp4 diff --git a/build/installer.iss b/build/installer.iss index 5458ecc..391f26c 100644 --- a/build/installer.iss +++ b/build/installer.iss @@ -62,7 +62,8 @@ Source: "{#PUBLISH_DIR}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsub [Icons] Name: "{group}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}" Name: "{group}\{#APP_NAME} 제거"; Filename: "{uninstallexe}" -Name: "{commondesktop}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}"; Tasks: desktopicon +Name: "{autodesktop}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}"; Tasks: desktopicon + [Run] Filename: "{app}\{#APP_EXE}"; Description: "{cm:LaunchProgram,{#APP_NAME}}"; Flags: nowait postinstall skipifsilent diff --git a/site/en/index.html b/site/en/index.html index c5839f1..f7d8a14 100644 --- a/site/en/index.html +++ b/site/en/index.html @@ -32,7 +32,7 @@ "description": "A free media browser that detects HLS, DASH and MP4 streams the moment they play, inherits the browser login session, and saves lossless MP4 with 16-thread parallel resume.", "url": "https://paca.chanpaca.net/en/", "downloadUrl": "https://paca.chanpaca.net/releases.html", - "softwareVersion": "1.3.0", + "softwareVersion": "1.3.1", "datePublished": "2026-08-16", "dateModified": "2026-09-19", "inLanguage": "en", diff --git a/site/index.html b/site/index.html index 761111f..8e5a4d3 100644 --- a/site/index.html +++ b/site/index.html @@ -34,7 +34,7 @@ "description": "재생하는 순간 스트림(HLS·DASH·MP4)을 감지해 로그인 세션 그대로 저장하고, 휴대폰으로 이어보는 무료 미디어 브라우저.", "url": "https://paca.chanpaca.net/", "downloadUrl": "https://paca.chanpaca.net/releases.html", - "softwareVersion": "1.3.0", + "softwareVersion": "1.3.1", "offers": { "@type": "Offer", "price": "0", diff --git a/src/Paca.App/App.xaml.cs b/src/Paca.App/App.xaml.cs index fdf9496..ad8fc3f 100644 --- a/src/Paca.App/App.xaml.cs +++ b/src/Paca.App/App.xaml.cs @@ -52,6 +52,14 @@ public partial class App : Application ApplyFontSettings(); try { File.AppendAllText(startupLog, "[Startup 6] ApplyReduceMotion\n"); } catch { } ApplyReduceMotion(config.ReduceMotion); + + // 실운영 구글 광고 모드 활성화 (ca-pub-1039714767792854 및 슬롯 4387520844/1521297311 실시간 송출) + Paca.Browser.Ads.DesktopAdManager.Instance.Config = Paca.Browser.Ads.DesktopAdConfig.PacaProduction with + { + ForceTestAds = false + }; + Environment.SetEnvironmentVariable("PACADESKTOP_USE_PRODUCTION_ADS", "true"); + try { File.AppendAllText(startupLog, "[Startup 7] new MainWindow()\n"); } catch { } var mw = new MainWindow(); try { File.AppendAllText(startupLog, "[Startup 8] mw.Show()\n"); } catch { } diff --git a/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs b/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs index 299c565..28f94de 100644 --- a/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs +++ b/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs @@ -223,6 +223,14 @@ public partial class DesktopAdBannerControl : UserControl } } + if (_webView.CoreWebView2 != null) + { + // 표준 크롬 브라우저 User-Agent 설정으로 WebView 광고 억제 원천 방지 + _webView.CoreWebView2.Settings.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"; + _webView.CoreWebView2.Settings.AreDevToolsEnabled = false; + _webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; + } + _webView.NavigationCompleted -= OnNavigationCompleted; _webView.NavigationCompleted += OnNavigationCompleted; _webView.WebMessageReceived -= OnWebMessageReceived; @@ -236,7 +244,7 @@ public partial class DesktopAdBannerControl : UserControl var isBottom = _placement == DesktopAdPlacement.BottomAppBanner; var adWidth = isBottom ? 728 : 300; var adHeight = isBottom ? 90 : 250; - var divId = isBottom ? "div-gpt-ad-bottom" : "div-gpt-ad-banner"; + var divId = isBottom ? "div-ad-bottom" : "div-ad-banner"; if (_webView != null) { @@ -246,12 +254,15 @@ public partial class DesktopAdBannerControl : UserControl _webView.VerticalAlignment = VerticalAlignment.Center; } + var isProd = !DesktopAdManager.Instance.Config.ForceTestAds; var slotId = !string.IsNullOrWhiteSpace(_adData.AdUnitId) ? _adData.AdUnitId : (isBottom ? DesktopAdManager.Instance.Config.EffectiveNtpBannerUnitId : DesktopAdManager.Instance.Config.EffectiveDownloadBannerUnitId); var snippet = _adData.HtmlEmbedSnippet ?? - DesktopAdConfig.GenerateGoogleTagSnippet(slotId, adWidth, adHeight, divId); + (isProd + ? DesktopAdConfig.GenerateProductionAdSnippet(slotId, adWidth, adHeight, divId) + : DesktopAdConfig.GenerateGoogleTagSnippet(slotId, adWidth, adHeight, divId)); var pageHtml = GenerateRealAdHtml(_adData, _placement, snippet, adWidth, adHeight); @@ -276,12 +287,18 @@ public partial class DesktopAdBannerControl : UserControl PART_NativeCard.Visibility = Visibility.Collapsed; var cacheBuster = DateTime.UtcNow.Ticks; - var virtualUrl = $"https://ads.paca.cloud/ad_{_placement}.html?v={cacheBuster}"; + var virtualUrl = $"https://mediabrowser.chanpaca.net/ad_{_placement}.html?v={cacheBuster}"; var fileUrl = new Uri(tempAdHtmlPath).AbsoluteUri + $"?v={cacheBuster}"; string targetUrl = virtualUrl; try { + // 실제 배포 도메인(ads.txt 검증 호스트) 및 레거시 ads.paca.cloud 가상 호스트 동시 매핑 + _webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping( + "mediabrowser.chanpaca.net", + adDir, + Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow); + _webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping( "ads.paca.cloud", adDir, @@ -332,7 +349,8 @@ public partial class DesktopAdBannerControl : UserControl - + + PACA Google Ad "; + } + /// + /// Google 공식 실시간 프로덕션 광고 스니펫 생성 (Google AdSense ca-pub-1039714767792854 및 슬롯 바인딩 + 실시간 트래킹) + /// + public static string GenerateProductionAdSnippet(string adUnitId, int width, int height, string divId = "div-real-ad-slot") + { + var cleanPub = AdClient; + var cleanSlot = !string.IsNullOrWhiteSpace(adUnitId) && adUnitId.Contains('/') + ? adUnitId.Substring(adUnitId.LastIndexOf('/') + 1) + : (!string.IsNullOrWhiteSpace(adUnitId) ? adUnitId : (width >= 700 ? "4387520844" : "1521297311")); + + return $@" + +"; } } @@ -156,15 +197,24 @@ public sealed class DesktopAdManager public event Action? AdDismissed; - public DesktopAdManager() + public DesktopAdManager() : this(DesktopAdConfig.PacaProduction) { + } + + public DesktopAdManager(DesktopAdConfig config) + { + Config = config; InitializeDefaultAds(); } private void InitializeDefaultAds() { + bool isProd = !Config.ForceTestAds; + // 1. 새 탭 페이지(NTP) Google 실시간 스폰서 슬롯 (728x90) - var ntpSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-ntp"); + var ntpSnippet = isProd + ? DesktopAdConfig.GenerateProductionAdSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-ad-ntp") + : DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-ntp"); var ntpAd = new DesktopAdData { Id = "paca-desktop-ntp-sponsor", @@ -173,7 +223,7 @@ public sealed class DesktopAdManager Badge = "AD", Title = "Google Sponsor Network", Description = "Google 공식 인증 파트너 실시간 디스플레이 광고", - Advertiser = "Google Ad Manager", + Advertiser = isProd ? "Google AdSense" : "Google Ad Manager", CtaText = "자세히 보기", TargetUrl = "https://www.google.com/ads", IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", @@ -183,7 +233,9 @@ public sealed class DesktopAdManager }; // 2. 다운로드 뷰 배너 (300x250 Medium Rectangle) - var dlSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveDownloadBannerUnitId, 300, 250, "div-gpt-ad-dl"); + var dlSnippet = isProd + ? DesktopAdConfig.GenerateProductionAdSnippet(Config.EffectiveDownloadBannerUnitId, 300, 250, "div-ad-dl") + : DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveDownloadBannerUnitId, 300, 250, "div-gpt-ad-dl"); var dlAd = new DesktopAdData { Id = "paca-desktop-dl-sponsor", @@ -192,7 +244,7 @@ public sealed class DesktopAdManager Badge = "AD", Title = "Google Display Network", Description = "Google 공식 실시간 타깃 디스플레이 광고 슬롯", - Advertiser = "Google Ad Manager", + Advertiser = isProd ? "Google AdSense" : "Google Ad Manager", CtaText = "자세히 보기", TargetUrl = "https://www.google.com/ads", IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", @@ -217,7 +269,9 @@ public sealed class DesktopAdManager }; // 4. 앱 메인 하단 고정 배너 (728x90 Leaderboard) - var bottomSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-bottom"); + var bottomSnippet = isProd + ? DesktopAdConfig.GenerateProductionAdSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-ad-bottom") + : DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-bottom"); var bottomAd = new DesktopAdData { Id = "paca-desktop-bottom-sponsor", @@ -226,7 +280,7 @@ public sealed class DesktopAdManager Badge = "AD", Title = "Google Sponsor Network", Description = "Google 공식 실시간 인증 디스플레이 광고 배너", - Advertiser = "Google Ad Manager", + Advertiser = isProd ? "Google AdSense" : "Google Ad Manager", CtaText = "자세히 보기", TargetUrl = "https://www.google.com/ads", IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", diff --git a/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs b/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs index 3671d8c..13e2f0f 100644 --- a/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs +++ b/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs @@ -62,4 +62,21 @@ public class DesktopGatewayDaemonCommandLineGateTests Assert.Equal(52101, config.BeaconUdpPort); Assert.True(config.AutoStartUdpBeacon); } + + [Fact] + public void Gate157_InstallerScript_MustUseAutoDesktop_And_MustNotContainCommonDesktop_ForPerUserSafety() + { + var installerPath = Path.Combine(SourceTree.Root, "build", "installer.iss"); + Assert.True(File.Exists(installerPath), "installer.iss must exist"); + + var content = File.ReadAllText(installerPath); + + // Standard user (PrivilegesRequired=lowest) has no write permission to {commondesktop} (C:\Users\Public\Desktop). + // Saving a shortcut to {commondesktop} throws IPersistFile::Save 0x80070005 (Access is Denied). + // It must use {autodesktop} which resolves to {userdesktop} for standard users. + Assert.DoesNotContain("{commondesktop}", content); + Assert.Contains("{autodesktop}\\{#APP_NAME}", content); + Assert.DoesNotContain("{commonprograms}", content); + } } + diff --git a/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs b/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs index 677925e..cef0df7 100644 --- a/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs +++ b/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs @@ -425,6 +425,45 @@ public class DesktopAdSlotAndNtpSponsoredGateTests Assert.Contains("data-ad-client=\"ca-pub-1039714767792854\"", adsenseSnippet); Assert.Contains("data-ad-slot=\"4387520844\"", adsenseSnippet); } + + [Fact] + public void Gate214_RealGoogleAds_AdSense_ProductionSnippets_And_DomainOrigin_Audit() + { + // 1. 프로덕션 모드 DesktopAdManager 생성 시 실제 단위 슬롯이 주입되어야 함 + var prodConfig = DesktopAdConfig.PacaProduction with { ForceTestAds = false }; + var adManager = new DesktopAdManager(prodConfig); + + var bottomAd = adManager.GetAdForPlacement(DesktopAdPlacement.BottomAppBanner); + Assert.NotNull(bottomAd); + Assert.True(bottomAd.IsRealAd); + Assert.Equal("ca-app-pub-1039714767792854/4387520844", bottomAd.AdUnitId); + Assert.NotNull(bottomAd.HtmlEmbedSnippet); + Assert.Contains("ca-pub-1039714767792854", bottomAd.HtmlEmbedSnippet); + Assert.Contains("4387520844", bottomAd.HtmlEmbedSnippet); + Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", bottomAd.HtmlEmbedSnippet); + + var dlAd = adManager.GetAdForPlacement(DesktopAdPlacement.DownloadViewBanner); + Assert.NotNull(dlAd); + Assert.True(dlAd.IsRealAd); + Assert.Equal("ca-app-pub-1039714767792854/1521297311", dlAd.AdUnitId); + Assert.NotNull(dlAd.HtmlEmbedSnippet); + Assert.Contains("ca-pub-1039714767792854", dlAd.HtmlEmbedSnippet); + Assert.Contains("1521297311", dlAd.HtmlEmbedSnippet); + Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", dlAd.HtmlEmbedSnippet); + + // 2. DesktopAdConfig.GenerateProductionAdSnippet 검증 + var snippet = DesktopAdConfig.GenerateProductionAdSnippet("ca-app-pub-1039714767792854/4387520844", 728, 90, "div-bottom"); + Assert.Contains("ca-pub-1039714767792854", snippet); + Assert.Contains("data-ad-slot=\"4387520844\"", snippet); + Assert.Contains("data-ad-status", snippet); + + // 3. DesktopAdBannerControl.xaml.cs에서 실 도메인 mediabrowser.chanpaca.net 및 크롬 User-Agent 감사 + var bannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs"); + var bannerCs = File.ReadAllText(bannerCsPath); + Assert.Contains("mediabrowser.chanpaca.net", bannerCs); + Assert.Contains("Chrome/130", bannerCs); + Assert.Contains("Settings.UserAgent", bannerCs); + } }