fix(installer): use {autodesktop} for per-user desktop shortcut safety and fix IPersistFile 0x80070005 error
Some checks failed
release / test (push) Failing after 13m30s
release / macos (push) Has been cancelled
release / windows (push) Has been cancelled
release / android (push) Has been cancelled
release / linux (push) Has been cancelled

This commit is contained in:
Yun Chan 2026-09-19 08:25:34 +09:00
parent 86e59f1e94
commit 3ac77180b4
9 changed files with 153 additions and 14 deletions

2
.gitignore vendored
View file

@ -42,6 +42,8 @@ coverage*.json
*.log *.log
positions.json positions.json
library.json library.json
site/ad_*.html
# ── 다운로드 테스트 결과물 (엔진 검증 중 루트에 떨어질 수 있음) ───── # ── 다운로드 테스트 결과물 (엔진 검증 중 루트에 떨어질 수 있음) ─────
*.mp4 *.mp4

View file

@ -62,7 +62,8 @@ Source: "{#PUBLISH_DIR}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsub
[Icons] [Icons]
Name: "{group}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}" Name: "{group}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}"
Name: "{group}\{#APP_NAME} 제거"; Filename: "{uninstallexe}" 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] [Run]
Filename: "{app}\{#APP_EXE}"; Description: "{cm:LaunchProgram,{#APP_NAME}}"; Flags: nowait postinstall skipifsilent Filename: "{app}\{#APP_EXE}"; Description: "{cm:LaunchProgram,{#APP_NAME}}"; Flags: nowait postinstall skipifsilent

View file

@ -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.", "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/", "url": "https://paca.chanpaca.net/en/",
"downloadUrl": "https://paca.chanpaca.net/releases.html", "downloadUrl": "https://paca.chanpaca.net/releases.html",
"softwareVersion": "1.3.0", "softwareVersion": "1.3.1",
"datePublished": "2026-08-16", "datePublished": "2026-08-16",
"dateModified": "2026-09-19", "dateModified": "2026-09-19",
"inLanguage": "en", "inLanguage": "en",

View file

@ -34,7 +34,7 @@
"description": "재생하는 순간 스트림(HLS·DASH·MP4)을 감지해 로그인 세션 그대로 저장하고, 휴대폰으로 이어보는 무료 미디어 브라우저.", "description": "재생하는 순간 스트림(HLS·DASH·MP4)을 감지해 로그인 세션 그대로 저장하고, 휴대폰으로 이어보는 무료 미디어 브라우저.",
"url": "https://paca.chanpaca.net/", "url": "https://paca.chanpaca.net/",
"downloadUrl": "https://paca.chanpaca.net/releases.html", "downloadUrl": "https://paca.chanpaca.net/releases.html",
"softwareVersion": "1.3.0", "softwareVersion": "1.3.1",
"offers": { "offers": {
"@type": "Offer", "@type": "Offer",
"price": "0", "price": "0",

View file

@ -52,6 +52,14 @@ public partial class App : Application
ApplyFontSettings(); ApplyFontSettings();
try { File.AppendAllText(startupLog, "[Startup 6] ApplyReduceMotion\n"); } catch { } try { File.AppendAllText(startupLog, "[Startup 6] ApplyReduceMotion\n"); } catch { }
ApplyReduceMotion(config.ReduceMotion); 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 { } try { File.AppendAllText(startupLog, "[Startup 7] new MainWindow()\n"); } catch { }
var mw = new MainWindow(); var mw = new MainWindow();
try { File.AppendAllText(startupLog, "[Startup 8] mw.Show()\n"); } catch { } try { File.AppendAllText(startupLog, "[Startup 8] mw.Show()\n"); } catch { }

View file

@ -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.NavigationCompleted += OnNavigationCompleted; _webView.NavigationCompleted += OnNavigationCompleted;
_webView.WebMessageReceived -= OnWebMessageReceived; _webView.WebMessageReceived -= OnWebMessageReceived;
@ -236,7 +244,7 @@ public partial class DesktopAdBannerControl : UserControl
var isBottom = _placement == DesktopAdPlacement.BottomAppBanner; var isBottom = _placement == DesktopAdPlacement.BottomAppBanner;
var adWidth = isBottom ? 728 : 300; var adWidth = isBottom ? 728 : 300;
var adHeight = isBottom ? 90 : 250; 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) if (_webView != null)
{ {
@ -246,12 +254,15 @@ public partial class DesktopAdBannerControl : UserControl
_webView.VerticalAlignment = VerticalAlignment.Center; _webView.VerticalAlignment = VerticalAlignment.Center;
} }
var isProd = !DesktopAdManager.Instance.Config.ForceTestAds;
var slotId = !string.IsNullOrWhiteSpace(_adData.AdUnitId) var slotId = !string.IsNullOrWhiteSpace(_adData.AdUnitId)
? _adData.AdUnitId ? _adData.AdUnitId
: (isBottom ? DesktopAdManager.Instance.Config.EffectiveNtpBannerUnitId : DesktopAdManager.Instance.Config.EffectiveDownloadBannerUnitId); : (isBottom ? DesktopAdManager.Instance.Config.EffectiveNtpBannerUnitId : DesktopAdManager.Instance.Config.EffectiveDownloadBannerUnitId);
var snippet = _adData.HtmlEmbedSnippet ?? 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); var pageHtml = GenerateRealAdHtml(_adData, _placement, snippet, adWidth, adHeight);
@ -276,12 +287,18 @@ public partial class DesktopAdBannerControl : UserControl
PART_NativeCard.Visibility = Visibility.Collapsed; PART_NativeCard.Visibility = Visibility.Collapsed;
var cacheBuster = DateTime.UtcNow.Ticks; 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}"; var fileUrl = new Uri(tempAdHtmlPath).AbsoluteUri + $"?v={cacheBuster}";
string targetUrl = virtualUrl; string targetUrl = virtualUrl;
try try
{ {
// 실제 배포 도메인(ads.txt 검증 호스트) 및 레거시 ads.paca.cloud 가상 호스트 동시 매핑
_webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping(
"mediabrowser.chanpaca.net",
adDir,
Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
_webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping( _webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping(
"ads.paca.cloud", "ads.paca.cloud",
adDir, adDir,
@ -332,7 +349,8 @@ public partial class DesktopAdBannerControl : UserControl
<meta name=""viewport"" content=""width={width}, initial-scale=1.0, maximum-scale=1.0, user-scalable=no""> <meta name=""viewport"" content=""width={width}, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"">
<meta name=""referrer"" content=""always""> <meta name=""referrer"" content=""always"">
<meta name=""google-adsense-account"" content=""ca-pub-1039714767792854""> <meta name=""google-adsense-account"" content=""ca-pub-1039714767792854"">
<link rel=""canonical"" href=""https://ads.paca.cloud/ad_{placement}.html""> <link rel=""canonical"" href=""https://mediabrowser.chanpaca.net/ad_{placement}.html"">
<!-- Legacy virtual host compatibility alias: https://ads.paca.cloud/ad_{placement}.html -->
<title>PACA Google Ad</title> <title>PACA Google Ad</title>
<script> <script>
(function() {{ (function() {{

View file

@ -116,6 +116,47 @@ public record DesktopAdConfig(
try {{ try {{
(adsbygoogle = window.adsbygoogle || []).push({{}}); (adsbygoogle = window.adsbygoogle || []).push({{}});
}} catch(e) {{}} }} catch(e) {{}}
</script>";
}
/// <summary>
/// Google 공식 실시간 프로덕션 광고 스니펫 생성 (Google AdSense ca-pub-1039714767792854 및 슬롯 바인딩 + 실시간 트래킹)
/// </summary>
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 $@"<script async src=""https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client={cleanPub}"" crossorigin=""anonymous""></script>
<ins class=""adsbygoogle""
id=""{divId}""
style=""display:inline-block;width:{width}px;height:{height}px;margin:0 auto;text-align:center;""
data-ad-client=""{cleanPub}""
data-ad-slot=""{cleanSlot}""
data-ad-format=""auto""
data-full-width-responsive=""false""></ins>
<script>
try {{
(adsbygoogle = window.adsbygoogle || []).push({{}});
}} catch(e) {{}}
(function() {{
var ins = document.getElementById('{divId}');
if (ins && window.chrome && window.chrome.webview) {{
var reportStatus = function() {{
var status = ins.getAttribute('data-ad-status');
var hasIframe = !!ins.querySelector('iframe');
if (status === 'filled' || hasIframe) {{
window.chrome.webview.postMessage(JSON.stringify({{ type: 'AD_RENDERED', slotId: '{cleanSlot}', status: 'filled' }}));
}} else if (status === 'unfilled') {{
window.chrome.webview.postMessage(JSON.stringify({{ type: 'AD_EMPTY', slotId: '{cleanSlot}', status: 'unfilled' }}));
}}
}};
var observer = new MutationObserver(function() {{ reportStatus(); }});
observer.observe(ins, {{ attributes: true, childList: true, subtree: true }});
setTimeout(reportStatus, 2500);
}}
}})();
</script>"; </script>";
} }
} }
@ -156,15 +197,24 @@ public sealed class DesktopAdManager
public event Action<string>? AdDismissed; public event Action<string>? AdDismissed;
public DesktopAdManager() public DesktopAdManager() : this(DesktopAdConfig.PacaProduction)
{ {
}
public DesktopAdManager(DesktopAdConfig config)
{
Config = config;
InitializeDefaultAds(); InitializeDefaultAds();
} }
private void InitializeDefaultAds() private void InitializeDefaultAds()
{ {
bool isProd = !Config.ForceTestAds;
// 1. 새 탭 페이지(NTP) Google 실시간 스폰서 슬롯 (728x90) // 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 var ntpAd = new DesktopAdData
{ {
Id = "paca-desktop-ntp-sponsor", Id = "paca-desktop-ntp-sponsor",
@ -173,7 +223,7 @@ public sealed class DesktopAdManager
Badge = "AD", Badge = "AD",
Title = "Google Sponsor Network", Title = "Google Sponsor Network",
Description = "Google 공식 인증 파트너 실시간 디스플레이 광고", Description = "Google 공식 인증 파트너 실시간 디스플레이 광고",
Advertiser = "Google Ad Manager", Advertiser = isProd ? "Google AdSense" : "Google Ad Manager",
CtaText = "자세히 보기", CtaText = "자세히 보기",
TargetUrl = "https://www.google.com/ads", TargetUrl = "https://www.google.com/ads",
IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", 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) // 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 var dlAd = new DesktopAdData
{ {
Id = "paca-desktop-dl-sponsor", Id = "paca-desktop-dl-sponsor",
@ -192,7 +244,7 @@ public sealed class DesktopAdManager
Badge = "AD", Badge = "AD",
Title = "Google Display Network", Title = "Google Display Network",
Description = "Google 공식 실시간 타깃 디스플레이 광고 슬롯", Description = "Google 공식 실시간 타깃 디스플레이 광고 슬롯",
Advertiser = "Google Ad Manager", Advertiser = isProd ? "Google AdSense" : "Google Ad Manager",
CtaText = "자세히 보기", CtaText = "자세히 보기",
TargetUrl = "https://www.google.com/ads", TargetUrl = "https://www.google.com/ads",
IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png",
@ -217,7 +269,9 @@ public sealed class DesktopAdManager
}; };
// 4. 앱 메인 하단 고정 배너 (728x90 Leaderboard) // 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 var bottomAd = new DesktopAdData
{ {
Id = "paca-desktop-bottom-sponsor", Id = "paca-desktop-bottom-sponsor",
@ -226,7 +280,7 @@ public sealed class DesktopAdManager
Badge = "AD", Badge = "AD",
Title = "Google Sponsor Network", Title = "Google Sponsor Network",
Description = "Google 공식 실시간 인증 디스플레이 광고 배너", Description = "Google 공식 실시간 인증 디스플레이 광고 배너",
Advertiser = "Google Ad Manager", Advertiser = isProd ? "Google AdSense" : "Google Ad Manager",
CtaText = "자세히 보기", CtaText = "자세히 보기",
TargetUrl = "https://www.google.com/ads", TargetUrl = "https://www.google.com/ads",
IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png",

View file

@ -62,4 +62,21 @@ public class DesktopGatewayDaemonCommandLineGateTests
Assert.Equal(52101, config.BeaconUdpPort); Assert.Equal(52101, config.BeaconUdpPort);
Assert.True(config.AutoStartUdpBeacon); 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);
} }
}

View file

@ -425,6 +425,45 @@ public class DesktopAdSlotAndNtpSponsoredGateTests
Assert.Contains("data-ad-client=\"ca-pub-1039714767792854\"", adsenseSnippet); Assert.Contains("data-ad-client=\"ca-pub-1039714767792854\"", adsenseSnippet);
Assert.Contains("data-ad-slot=\"4387520844\"", 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);
}
} }