Skip to content

Commit 7fed364

Browse files
authored
Make sure cross origin fallback detection is in place (#315)
1 parent 2002ef7 commit 7fed364

File tree

2 files changed

+187
-39
lines changed

2 files changed

+187
-39
lines changed

_layouts/default.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
<title>{{ page.title }}</title>
99
<script>
10+
// Expose Jekyll config to JavaScript for parent origin detection
11+
window.JEKYLL_CONFIG = {
12+
url: "{{ site.url }}",
13+
baseurl: "{{ site.baseurl }}"
14+
};
15+
1016
// Only set base target="_parent" if NOT in an iframe
1117
// This allows iframed pages to navigate within the iframe
1218
if (window.self === window.top) {

assets/js/nav.js

Lines changed: 181 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,182 @@
4848
// Ensure search results stay on developers.procore.com under /documentation
4949
var basePath = "/documentation";
5050

51+
// Helper function to check if a hostname matches a domain (handles subdomains correctly)
52+
function isDomainMatch(hostname, domain) {
53+
if (!hostname || !domain) return false;
54+
// Exact match
55+
if (hostname === domain) return true;
56+
// Subdomain match: hostname ends with '.' + domain
57+
// This prevents matching malicious domains like 'malicious-developers.procore.com'
58+
return hostname.endsWith('.' + domain);
59+
}
60+
61+
// Helper function to normalize origin (remove trailing slash, ensure proper format)
62+
function normalizeOrigin(origin) {
63+
if (!origin) return null;
64+
// Remove trailing slash if present
65+
origin = origin.replace(/\/+$/, '');
66+
// Ensure it's a valid origin format (protocol://host)
67+
try {
68+
var url = new URL(origin);
69+
var hostname = url.hostname;
70+
71+
// Check hostname against known domains (parse URL first, then check hostname)
72+
if (isDomainMatch(hostname, 'developers.procore.com')) {
73+
return 'https://developers.procore.com';
74+
}
75+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('127.0.0.1') || hostname.startsWith('localhost')) {
76+
return 'http://' + hostname;
77+
}
78+
79+
return url.origin;
80+
} catch (e) {
81+
// If it's not a full URL, try to construct it
82+
if (origin.startsWith('http://') || origin.startsWith('https://')) {
83+
// Try parsing again with the protocol
84+
try {
85+
var url = new URL(origin);
86+
var hostname = url.hostname;
87+
88+
if (isDomainMatch(hostname, 'developers.procore.com')) {
89+
return 'https://developers.procore.com';
90+
}
91+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
92+
return 'http://' + hostname;
93+
}
94+
95+
return url.origin;
96+
} catch (e2) {
97+
return origin;
98+
}
99+
}
100+
// If it's just a hostname string, check it directly
101+
if (isDomainMatch(origin, 'developers.procore.com')) {
102+
return 'https://developers.procore.com';
103+
}
104+
if (origin === 'localhost' || origin === '127.0.0.1' || origin.startsWith('localhost') || origin.startsWith('127.0.0.1')) {
105+
return 'http://' + origin.replace(/^https?:\/\//, '');
106+
}
107+
return origin;
108+
}
109+
}
110+
111+
// Function to get parent origin (works for both same-origin and cross-origin)
112+
// Note: Production is cross-origin (procore.github.io -> developers.procore.com)
113+
function getParentOrigin() {
114+
if (window.self === window.top) {
115+
return null; // Not in an iframe
116+
}
117+
118+
var parentOrigin = null;
119+
120+
try {
121+
// For same-origin iframes, we can access window.top.location.origin
122+
// This will fail in production (cross-origin) and fall through to catch block
123+
if (window.top.location.origin) {
124+
parentOrigin = window.top.location.origin;
125+
console.log("getParentOrigin: Using window.top.location.origin:", parentOrigin);
126+
return parentOrigin;
127+
}
128+
} catch (e) {
129+
// Cross-origin: can't access window.top.location (production scenario)
130+
// Use other methods to determine parent origin
131+
console.log("getParentOrigin: Cross-origin detected, trying alternative methods");
132+
133+
// Try ancestorOrigins API (modern browsers)
134+
if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) {
135+
parentOrigin = normalizeOrigin(window.location.ancestorOrigins[0]);
136+
console.log("getParentOrigin: Using ancestorOrigins[0] (normalized):", parentOrigin);
137+
if (parentOrigin) return parentOrigin;
138+
}
139+
140+
// Try document.referrer
141+
if (document.referrer) {
142+
try {
143+
parentOrigin = normalizeOrigin(new URL(document.referrer).origin);
144+
console.log("getParentOrigin: Using document.referrer origin (normalized):", parentOrigin);
145+
if (parentOrigin) return parentOrigin;
146+
} catch (err) {
147+
console.error("getParentOrigin: Error parsing referrer origin:", err, "referrer:", document.referrer);
148+
}
149+
}
150+
151+
// Fallback: Use Jekyll config value (from _config.yml, exposed via window.JEKYLL_CONFIG)
152+
// This is the production parent origin: developers.procore.com
153+
if (typeof window.JEKYLL_CONFIG !== 'undefined' && window.JEKYLL_CONFIG.url) {
154+
try {
155+
parentOrigin = normalizeOrigin(new URL(window.JEKYLL_CONFIG.url).origin);
156+
console.log("getParentOrigin: Using JEKYLL_CONFIG.url as fallback (normalized):", parentOrigin);
157+
if (parentOrigin) return parentOrigin;
158+
} catch (err) {
159+
console.error("getParentOrigin: Error parsing JEKYLL_CONFIG.url:", err);
160+
}
161+
}
162+
163+
// Hardcoded fallback for production
164+
// If we're on procore.github.io, the parent is likely developers.procore.com
165+
if (window.location.hostname === 'procore.github.io' ||
166+
window.location.hostname.endsWith('.github.io')) {
167+
parentOrigin = normalizeOrigin('https://developers.procore.com');
168+
console.log("getParentOrigin: Using hardcoded production fallback (normalized):", parentOrigin);
169+
if (parentOrigin) return parentOrigin;
170+
}
171+
172+
console.warn("getParentOrigin: Could not determine parent origin. Available info:", {
173+
ancestorOrigins: window.location.ancestorOrigins,
174+
referrer: document.referrer,
175+
hostname: window.location.hostname,
176+
jekyllConfig: typeof window.JEKYLL_CONFIG !== 'undefined' ? window.JEKYLL_CONFIG : 'undefined'
177+
});
178+
}
179+
180+
return parentOrigin;
181+
}
182+
183+
// Function to update parent window URL (works for both same-origin and cross-origin)
184+
// Note: Production is cross-origin, so postMessage is required
185+
function updateParentUrl(path, parentOrigin) {
186+
if (!parentOrigin) {
187+
console.warn("Cannot update parent URL: parentOrigin is null");
188+
return;
189+
}
190+
191+
var fullUrl = parentOrigin + path;
192+
console.log("Updating parent URL:", { path: path, parentOrigin: parentOrigin, fullUrl: fullUrl });
193+
194+
// Try direct access first (same-origin scenarios only, e.g., reverse proxy testing)
195+
// This will fail in production (cross-origin: procore.github.io -> developers.procore.com)
196+
try {
197+
if (window.top.location.origin === parentOrigin) {
198+
// Same-origin: can directly update parent URL (testing scenarios only)
199+
console.log("Using direct access (same-origin) to update parent URL");
200+
window.top.history.pushState({}, '', fullUrl);
201+
return;
202+
}
203+
} catch (e) {
204+
// Cross-origin: can't access window.top.location (production scenario)
205+
// Must use postMessage for cross-origin communication
206+
console.log("Cannot use direct access (cross-origin), using postMessage");
207+
}
208+
209+
// Fallback: use postMessage (required for production cross-origin scenario)
210+
try {
211+
console.log("Sending postMessage to parent:", { type: 'documentationNavigation', path: path, fullUrl: fullUrl });
212+
window.parent.postMessage({
213+
type: 'documentationNavigation',
214+
path: path,
215+
fullUrl: fullUrl
216+
}, parentOrigin);
217+
} catch (err) {
218+
console.error("Could not send navigation message to parent:", err);
219+
}
220+
}
221+
51222
// Function to rewrite search result links for iframe display
52223
function rewriteSearchResultLinks() {
53224
if (window.self !== window.top) {
54225
try {
55-
var parentOrigin = window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0
56-
? window.location.ancestorOrigins[0]
57-
: document.referrer ? new URL(document.referrer).origin : null;
226+
var parentOrigin = getParentOrigin();
58227

59228
if (parentOrigin) {
60229
$("#results-container a").each(function() {
@@ -137,25 +306,15 @@
137306
// If in iframe and link points to parent origin, navigate within iframe
138307
if (window.self !== window.top) {
139308
try {
140-
var parentOrigin = window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0
141-
? window.location.ancestorOrigins[0]
142-
: document.referrer ? new URL(document.referrer).origin : null;
309+
var parentOrigin = getParentOrigin();
143310

144311
if (parentOrigin && currentHref.startsWith(parentOrigin)) {
145312
e.preventDefault();
146313
var path = new URL(currentHref).pathname + new URL(currentHref).search + new URL(currentHref).hash;
147314
// Update iframe location
148315
window.location.href = path;
149-
// Update parent window URL via postMessage (cross-origin safe)
150-
try {
151-
window.parent.postMessage({
152-
type: 'documentationNavigation',
153-
path: path,
154-
fullUrl: parentOrigin + path
155-
}, parentOrigin);
156-
} catch (e) {
157-
console.warn("Could not send navigation message to parent:", e);
158-
}
316+
// Update parent window URL
317+
updateParentUrl(path, parentOrigin);
159318
return false;
160319
}
161320
} catch (err) {
@@ -186,9 +345,7 @@
186345
// If in an iframe, rewrite link hrefs for hover display but intercept clicks to navigate within iframe
187346
if (window.self !== window.top) {
188347
try {
189-
var parentOrigin = window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0
190-
? window.location.ancestorOrigins[0]
191-
: document.referrer ? new URL(document.referrer).origin : null;
348+
var parentOrigin = getParentOrigin();
192349

193350
if (parentOrigin) {
194351
// Function to get the actual path from a href
@@ -257,16 +414,8 @@
257414
var path = new URL(currentHref).pathname + new URL(currentHref).search + new URL(currentHref).hash;
258415
// Update iframe location
259416
window.location.href = path;
260-
// Update parent window URL via postMessage (cross-origin safe)
261-
try {
262-
window.parent.postMessage({
263-
type: 'documentationNavigation',
264-
path: path,
265-
fullUrl: parentOrigin + path
266-
}, parentOrigin);
267-
} catch (e) {
268-
console.warn("Could not send navigation message to parent:", e);
269-
}
417+
// Update parent window URL
418+
updateParentUrl(path, parentOrigin);
270419
return false;
271420
} else if (hrefToUse.match(/^https?:\/\//) && !hrefToUse.startsWith(window.location.origin) && !hrefToUse.startsWith(parentOrigin)) {
272421
// External link (not parent origin, not current origin) - let it open normally
@@ -294,16 +443,8 @@
294443
var path = hrefToUse.startsWith("/") ? hrefToUse : "/" + hrefToUse;
295444
// Update iframe location
296445
window.location.href = path;
297-
// Update parent window URL via postMessage (cross-origin safe)
298-
try {
299-
window.parent.postMessage({
300-
type: 'documentationNavigation',
301-
path: path,
302-
fullUrl: parentOrigin + path
303-
}, parentOrigin);
304-
} catch (e) {
305-
console.warn("Could not send navigation message to parent:", e);
306-
}
446+
// Update parent window URL
447+
updateParentUrl(path, parentOrigin);
307448
return false;
308449
}
309450
// For other cases, let default behavior work
@@ -314,3 +455,4 @@
314455
}
315456
}
316457
})();
458+

0 commit comments

Comments
 (0)