
Sometimes, using PnP PowerShell just isn’t an option. Whether it’s because of restrictions in your environment or you simply need a quick peek at your SharePoint data, you need another way. I often find myself needing a fast report on lists, fields, subsites, etc.
Enter JavaScript! I know, it might sound a bit unconventional, but running JavaScript right in the browser console has come to the rescue more than once. This method gives me a quick report on the fly. No extra tools required. It’s not the most elegant approach out there, but when you need to discover your SharePoint data fast, it really gets the job done.
Looking for Graph API identifiers instead — Drive IDs, DriveItem IDs and Site IDs for Power Automate flows? See the companion article: Vanilla JS and the SharePoint Graph API.
Below is a small collection of scripts that does the heavy lifting for you. It starts at your current SharePoint site, grabs all the subsites (webs) recursively, and then pulls in every list along with its content types. Once it’s done, it shows you a neat table right in your browser. You can even downloads a CSV report. Super handy, don’t you agree? 😃
(async function() {// Resolve the SharePoint context — works on classic and modern pagesasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });const c = (await r.json()).d.GetContextWebInformation;return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl, formDigest: c.FormDigestValue };}const ctx = await getSPContext();const siteUrl = ctx.webUrl;// Create UI elementsconst container = document.createElement('div');container.style.cssText = 'position:fixed;top:20px;right:20px;width:98%;max-width:1600px;background:white;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,0.1);user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;';const controls = document.createElement('div');controls.style.cssText = 'margin-bottom:20px;display:flex;align-items:center;';const downloadButton = document.createElement('button');downloadButton.textContent = 'Download CSV';downloadButton.style.cssText = 'padding:8px 16px;margin-right:10px;background:#107c10;color:white;border:none;cursor:pointer;display:none;';const infoText = document.createElement('span');infoText.style.cssText = 'margin-right:auto;font-weight:bold;color:#107c10;';const closeButton = document.createElement('button');closeButton.textContent = 'Close';closeButton.style.cssText = 'padding:8px 16px;background:#d83b01;color:white;border:none;cursor:pointer;margin-left:20px;';const tableContainer = document.createElement('div');controls.appendChild(downloadButton);controls.appendChild(infoText);controls.appendChild(closeButton);container.appendChild(controls);container.appendChild(tableContainer);document.body.appendChild(container);let allContentTypesData = [];let rootWebUrl = '';// Recursively fetch all subsites (webs)async function fetchWebAndSubsites(webUrl) {const allWebs = [];try {// Get current web infoconst webResponse = await fetch(`${webUrl}/_api/web?$select=Title,Url,ServerRelativeUrl`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (!webResponse.ok) {throw new Error('Failed to fetch web info for: ' + webUrl);}const webData = await webResponse.json();const currentWeb = webData.d;allWebs.push({Title: currentWeb.Title,Url: currentWeb.Url,ServerRelativeUrl: currentWeb.ServerRelativeUrl});// Get child websconst subWebsResponse = await fetch(`${webUrl}/_api/web/webs?$select=Title,Url,ServerRelativeUrl`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (subWebsResponse.ok) {const subWebsData = await subWebsResponse.json();const subWebs = subWebsData.d.results;// Recursively get all child websfor (const subWeb of subWebs) {const childWebs = await fetchWebAndSubsites(subWeb.Url);allWebs.push(...childWebs);}}} catch (error) {console.error(`Error fetching web ${webUrl}:`, error);}return allWebs;}// Fetch lists for a given webasync function fetchWebLists(webUrl) {try {const response = await fetch(`${webUrl}/_api/web/lists?$select=Title,Id,BaseTemplate,DefaultViewUrl&$filter=Hidden eq false`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (!response.ok) {throw new Error('Failed to fetch lists for: ' + webUrl);}const data = await response.json();return data.d.results;} catch (error) {console.error(`Error fetching lists for ${webUrl}:`, error);return [];}}// Load all dataconst loadData = async () => {try {tableContainer.innerHTML = '<div style="color:#0078d4;font-style:italic;">Loading all sites and their lists with content types...</div>';allContentTypesData = [];// Use the resolved web URL from getSPContext()rootWebUrl = siteUrl;// Get all webs recursivelyinfoText.textContent = 'Fetching all sites...';const allWebs = await fetchWebAndSubsites(rootWebUrl);// For each web, get its lists and content typeslet totalSites = allWebs.length;let processedSites = 0;for (const web of allWebs) {processedSites++;infoText.textContent = `Processing site ${processedSites} of ${totalSites}: ${web.Title}`;const lists = await fetchWebLists(web.Url);// For each list, get content typesfor (const list of lists) {try {const ctResponse = await fetch(`${web.Url}/_api/web/lists/getbytitle('${encodeURIComponent(list.Title)}')/contenttypes?$select=Name,Id,Description,Group,Hidden,ReadOnly`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (ctResponse.ok) {const ctData = await ctResponse.json();const contentTypes = ctData.d.results;// Create a row for each content typecontentTypes.forEach(ct => {allContentTypesData.push({SiteTitle: web.Title,SiteUrl: web.Url,SiteRelativeUrl: web.ServerRelativeUrl,ListTitle: list.Title,ListUrl: list.DefaultViewUrl,ContentTypeName: ct.Name,ContentTypeId: ct.Id.StringValue,Description: ct.Description || '',Group: ct.Group || '',Hidden: ct.Hidden ? 'Yes' : 'No',ReadOnly: ct.ReadOnly ? 'Yes' : 'No'});});}} catch (error) {console.error(`Error loading content types for ${web.Title}/${list.Title}:`, error);}}}// Display infoconst totalCTs = allContentTypesData.length;const uniqueLists = new Set(allContentTypesData.map(item => `${item.SiteUrl}|${item.ListTitle}`)).size;infoText.textContent = `Total sites: ${totalSites}, Total lists: ${uniqueLists}, Total content types: ${totalCTs}`;// Create tablelet tableHTML = `<table style="width:100%;border-collapse:collapse;margin-top:10px;user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;"><thead><tr style="background:#0078d4;color:white;"><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Site Title</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Site URL</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">List Title</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">List URL</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Content Type Name</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;width:180px;max-width:180px;user-select:text;">Content Type ID</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;width:150px;user-select:text;">Description</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Group</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Hidden</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Read Only</th></tr></thead><tbody>`;let currentSite = '';let currentList = '';allContentTypesData.forEach((row, index) => {const bgColor = index % 2 === 0 ? '#f2f2f2' : 'white';const siteChanged = row.SiteUrl !== currentSite;const listChanged = siteChanged || `${row.SiteUrl}|${row.ListTitle}` !== currentList;const siteStyle = siteChanged ? 'font-weight:bold;border-top:3px solid #0078d4;' : '';const listStyle = listChanged ? 'font-weight:bold;border-top:2px solid #0078d4;' : '';currentSite = row.SiteUrl;currentList = `${row.SiteUrl}|${row.ListTitle}`;// Create clickable URLsconst listUrlHtml = row.ListUrl ?`<a href="${row.SiteUrl}${row.ListUrl}" target="_blank" style="color:#0078d4;text-decoration:none;">Open</a>` :'';tableHTML += `<tr style="background:${bgColor};"><td style="border:1px solid #ddd;padding:8px;${siteStyle}user-select:text;cursor:text;">${row.SiteTitle}</td><td style="border:1px solid #ddd;padding:8px;${siteStyle}user-select:text;cursor:text;font-size:12px;">${row.SiteRelativeUrl}</td><td style="border:1px solid #ddd;padding:8px;${listStyle}user-select:text;cursor:text;">${row.ListTitle}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;user-select:text;">${listUrlHtml}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;">${row.ContentTypeName}</td><td style="border:1px solid #ddd;padding:8px;font-size:11px;word-break:break-all;width:180px;max-width:180px;user-select:text;cursor:text;">${row.ContentTypeId}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;font-size:12px;width:150px;overflow:hidden;text-overflow:ellipsis;" title="${row.Description.replace(/"/g, '"')}">${row.Description}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;">${row.Group}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;user-select:text;cursor:text;">${row.Hidden}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;user-select:text;cursor:text;">${row.ReadOnly}</td></tr>`;});tableHTML += '</tbody></table>';tableContainer.innerHTML = tableHTML;// Show download buttondownloadButton.style.display = 'inline-block';// Also log to consoleconsole.table(allContentTypesData);} catch (error) {console.error('Error loading data:', error);tableContainer.innerHTML = `<div style="color:red;">Error: ${error.message}</div>`;}};// Download CSVdownloadButton.onclick = () => {if (!allContentTypesData.length) return;// Create CSV contentlet csv = 'Site Title,Site URL,List Title,List URL,Content Type Name,Content Type ID,Description,Group,Hidden,Read Only\n';allContentTypesData.forEach(row => {const fullListUrl = row.ListUrl ? `${row.SiteUrl}${row.ListUrl}` : '';csv += `"${(row.SiteTitle || '').replace(/"/g, '""')}","${(row.SiteUrl || '').replace(/"/g, '""')}","${(row.ListTitle || '').replace(/"/g, '""')}","${fullListUrl.replace(/"/g, '""')}","${(row.ContentTypeName || '').replace(/"/g, '""')}","${(row.ContentTypeId || '').replace(/"/g, '""')}","${(row.Description || '').replace(/"/g, '""')}","${(row.Group || '').replace(/"/g, '""')}","${row.Hidden}","${row.ReadOnly}"\n`;});// Downloadconst blob = new Blob([csv], { type: 'text/csv' });const url = window.URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `all_sites_lists_content_types_${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(url);};// Close buttoncloseButton.onclick = () => {document.body.removeChild(container);};// Auto-load on startloadData();})();
Results:
(async function() {// Resolve the SharePoint context — works on classic and modern pagesasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });const c = (await r.json()).d.GetContextWebInformation;return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl, formDigest: c.FormDigestValue };}// Get current site URL - will be determined from contextlet siteUrl = '';// Create floating button that persistsconst floatingButton = document.createElement('button');floatingButton.innerHTML = '📋 Lists';floatingButton.style.cssText = 'position:fixed;bottom:20px;right:20px;padding:12px 20px;background:#0078d4;color:white;border:none;border-radius:25px;cursor:pointer;z-index:9999;box-shadow:0 4px 6px rgba(0,0,0,0.2);font-size:14px;font-weight:bold;display:none;';floatingButton.onmouseover = function() { this.style.background = '#106ebe'; };floatingButton.onmouseout = function() { this.style.background = '#0078d4'; };document.body.appendChild(floatingButton);// Create UI elementsconst container = document.createElement('div');container.style.cssText = 'position:fixed;top:20px;right:20px;width:95%;max-width:1500px;background:white;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,0.1);user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;';const controls = document.createElement('div');controls.style.cssText = 'margin-bottom:20px;display:flex;align-items:center;';// Filter inputconst filterInput = document.createElement('input');filterInput.type = 'text';filterInput.placeholder = 'Filter lists by title...';filterInput.style.cssText = 'padding:8px;margin-right:10px;border:1px solid #ccc;border-radius:4px;width:200px;';// Toggle hidden lists buttonconst toggleHiddenButton = document.createElement('button');toggleHiddenButton.textContent = '👁️ Show System Lists';toggleHiddenButton.style.cssText = 'padding:8px 16px;margin-right:10px;background:#6c757d;color:white;border:none;cursor:pointer;';const downloadButton = document.createElement('button');downloadButton.textContent = 'Download CSV';downloadButton.style.cssText = 'padding:8px 16px;margin-right:10px;background:#107c10;color:white;border:none;cursor:pointer;display:none;';const infoText = document.createElement('span');infoText.style.cssText = 'margin-right:auto;font-weight:bold;color:#107c10;';const closeButton = document.createElement('button');closeButton.textContent = 'Close';closeButton.style.cssText = 'padding:8px 16px;background:#d83b01;color:white;border:none;cursor:pointer;margin-left:20px;';const contentContainer = document.createElement('div');controls.appendChild(filterInput);controls.appendChild(toggleHiddenButton);controls.appendChild(downloadButton);controls.appendChild(infoText);controls.appendChild(closeButton);container.appendChild(controls);container.appendChild(contentContainer);document.body.appendChild(container);let allListsData = [];let selectedListId = null;let showHiddenLists = false;// Create modal for list detailsfunction createDetailsModal(list) {// Create overlayconst overlay = document.createElement('div');overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.3);z-index:10000;';const modal = document.createElement('div');modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:800px;background:white;border:2px solid #0078d4;z-index:10001;box-shadow:0 8px 16px rgba(0,0,0,0.2);max-height:80vh;overflow:hidden;';const modalContent = `<div style="background:#0078d4;padding:10px 20px;cursor:move;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;" id="modalHeader"><h2 style="margin:0;color:white;">${list.Title}</h2></div><div style="padding:20px;overflow-y:auto;max-height:calc(80vh - 60px);"><div style="display:grid;grid-template-columns:150px 1fr;gap:10px;color:#333;"><strong style="color:#333;">List GUID:</strong><span style="user-select:text;font-family:monospace;color:#333;">${list.Id}</span><strong style="color:#333;">Title:</strong><span style="color:#333;">${list.Title}</span><strong style="color:#333;">Description:</strong><span style="color:#333;">${list.Description || 'No description'}</span><strong style="color:#333;">Item Count:</strong><span style="color:#333;">${list.ItemCount.toLocaleString()}</span><strong style="color:#333;">List URL:</strong><span style="word-break:break-all;color:#333;"><a href="${list.ListUrl}" target="_blank" style="color:#0078d4;">${list.ListUrl}</a></span><strong style="color:#333;">Default View URL:</strong><span style="word-break:break-all;color:#333;"><a href="${list.DefaultViewUrl}" target="_blank" style="color:#0078d4;">${list.DefaultViewUrl}</a></span><strong style="color:#333;">Root Folder:</strong><span style="color:#333;">${list.RootFolder}</span><strong style="color:#333;">Entity Type:</strong><span style="color:#333;">${list.EntityTypeName}</span><strong style="color:#333;">Base Template:</strong><span style="color:#333;">${list.BaseTemplate} (${list.BaseType})</span><strong style="color:#333;">Created:</strong><span style="color:#333;">${list.Created}</span><strong style="color:#333;">Last Modified:</strong><span style="color:#333;">${list.LastItemModifiedDate}</span><strong style="color:#333;">Last Deleted:</strong><span style="color:#333;">${list.LastItemDeletedDate}</span><strong style="color:#333;">Major Version Limit:</strong><span style="color:#333;">${list.MajorVersionLimit}</span><strong style="color:#333;">Major with Minor Limit:</strong><span style="color:#333;">${list.MajorWithMinorVersionsLimit}</span><strong style="color:#333;">Draft Version Visibility:</strong><span style="color:#333;">${list.DraftVersionVisibility}</span><strong style="color:#333;">Content Types Enabled:</strong><span style="color:#333;">${list.ContentTypesEnabled}</span><strong style="color:#333;">Attachments Enabled:</strong><span style="color:#333;">${list.EnableAttachments}</span><strong style="color:#333;">Folder Creation:</strong><span style="color:#333;">${list.EnableFolderCreation}</span><strong style="color:#333;">Versioning Enabled:</strong><span style="color:#333;">${list.EnableVersioning}</span><strong style="color:#333;">Minor Versions Enabled:</strong><span style="color:#333;">${list.EnableMinorVersions}</span><strong style="color:#333;">Moderation Enabled:</strong><span style="color:#333;">${list.EnableModeration}</span><strong style="color:#333;">Force Checkout:</strong><span style="color:#333;">${list.ForceCheckout}</span><strong style="color:#333;">Hidden:</strong><span style="color:#333;">${list.Hidden}</span><strong style="color:#333;">IRM Enabled:</strong><span style="color:#333;">${list.IrmEnabled}</span><strong style="color:#333;">Is Catalog:</strong><span style="color:#333;">${list.IsCatalog}</span><strong style="color:#333;">Is Private List:</strong><span style="color:#333;">${list.IsPrivate}</span><strong style="color:#333;">Is System List:</strong><span style="color:#333;">${list.IsSystemList}</span><strong style="color:#333;">Allow Deletion:</strong><span style="color:#333;">${list.AllowDeletion}</span><strong style="color:#333;">Has Unique Permissions:</strong><span style="color:#333;">${list.HasUniqueRoleAssignments}</span><strong style="color:#333;">Web URL:</strong><span style="word-break:break-all;color:#333;">${list.WebUrl}</span><strong style="color:#333;">Web Title:</strong><span style="color:#333;">${list.WebTitle}</span></div><div style="margin-top:20px;text-align:right;"><button id="closeModal" style="padding:8px 16px;background:#0078d4;color:white;border:none;cursor:pointer;">Close</button><button id="openList" style="padding:8px 16px;background:#107c10;color:white;border:none;cursor:pointer;margin-left:10px;">Open List</button><button id="openSettings" style="padding:8px 16px;background:#8b4513;color:white;border:none;cursor:pointer;margin-left:10px;">List Settings</button></div></div>`;modal.innerHTML = modalContent;document.body.appendChild(overlay);document.body.appendChild(modal);// Make modal draggablelet isDragging = false;let currentX;let currentY;let initialX;let initialY;const dragHeader = document.getElementById('modalHeader');// Get initial positionconst rect = modal.getBoundingClientRect();currentX = rect.left + rect.width / 2;currentY = rect.top + rect.height / 2;function dragStart(e) {if (e.target === dragHeader || dragHeader.contains(e.target)) {isDragging = true;initialX = e.clientX - currentX;initialY = e.clientY - currentY;modal.style.transform = 'none';modal.style.left = (currentX - rect.width / 2) + 'px';modal.style.top = (currentY - rect.height / 2) + 'px';}}function dragEnd(e) {isDragging = false;}function drag(e) {if (isDragging) {e.preventDefault();currentX = e.clientX - initialX;currentY = e.clientY - initialY;modal.style.left = (currentX - rect.width / 2) + 'px';modal.style.top = (currentY - rect.height / 2) + 'px';}}dragHeader.addEventListener('mousedown', dragStart);document.addEventListener('mousemove', drag);document.addEventListener('mouseup', dragEnd);// Function to close modalconst closeModal = () => {document.removeEventListener('mousemove', drag);document.removeEventListener('mouseup', dragEnd);document.body.removeChild(modal);document.body.removeChild(overlay);document.removeEventListener('keydown', escHandler);};// Modal event handlersdocument.getElementById('closeModal').onclick = closeModal;document.getElementById('openList').onclick = () => {window.open(list.DefaultViewUrl, '_blank');};document.getElementById('openSettings').onclick = () => {window.open(list.ListSettingsUrl, '_blank');};// Close on overlay click (click away)overlay.onclick = closeModal;// Close on ESC keyconst escHandler = (e) => {if (e.key === 'Escape') {closeModal();}};document.addEventListener('keydown', escHandler);}// Fetch all lists from all webs in site collectionasync function fetchAllLists(webUrl, webTitle = '') {const lists = [];try {// Get lists from current web with comprehensive dataconst listsResponse = await fetch(`${webUrl}/_api/web/lists?$select=Id,Title,Description,ItemCount,RootFolder/ServerRelativeUrl,EntityTypeName,BaseTemplate,BaseType,Created,LastItemModifiedDate,LastItemDeletedDate,DefaultViewUrl,MajorVersionLimit,MajorWithMinorVersionsLimit,DraftVersionVisibility,ContentTypesEnabled,EnableAttachments,EnableFolderCreation,EnableVersioning,EnableMinorVersions,EnableModeration,ForceCheckout,Hidden,IrmEnabled,IsCatalog,IsPrivate,IsSystemList,AllowDeletion,HasUniqueRoleAssignments,ParentWebUrl&$expand=RootFolder`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (listsResponse.ok) {const listsData = await listsResponse.json();const webLists = listsData.d.results;for (const list of webLists) {const listInfo = {Id: list.Id,Title: list.Title,Description: list.Description || '',ItemCount: list.ItemCount,ListUrl: `${webUrl}/Lists/${encodeURIComponent(list.Title)}`,DefaultViewUrl: list.DefaultViewUrl,ListSettingsUrl: `${webUrl}/_layouts/15/listedit.aspx?List={${list.Id}}`,RootFolder: list.RootFolder.ServerRelativeUrl,EntityTypeName: list.EntityTypeName,BaseTemplate: list.BaseTemplate,BaseType: list.BaseType,Created: new Date(list.Created).toLocaleString(),LastItemModifiedDate: list.LastItemModifiedDate ? new Date(list.LastItemModifiedDate).toLocaleString() : 'Never',LastItemDeletedDate: list.LastItemDeletedDate ? new Date(list.LastItemDeletedDate).toLocaleString() : 'Never',MajorVersionLimit: list.MajorVersionLimit || 'N/A',MajorWithMinorVersionsLimit: list.MajorWithMinorVersionsLimit || 'N/A',DraftVersionVisibility: list.DraftVersionVisibility === 0 ? 'Reader' : list.DraftVersionVisibility === 1 ? 'Author' : 'Approver',ContentTypesEnabled: list.ContentTypesEnabled ? 'Yes' : 'No',EnableAttachments: list.EnableAttachments ? 'Yes' : 'No',EnableFolderCreation: list.EnableFolderCreation ? 'Yes' : 'No',EnableVersioning: list.EnableVersioning ? 'Yes' : 'No',EnableMinorVersions: list.EnableMinorVersions ? 'Yes' : 'No',EnableModeration: list.EnableModeration ? 'Yes' : 'No',ForceCheckout: list.ForceCheckout ? 'Yes' : 'No',Hidden: list.Hidden ? 'Yes' : 'No',IrmEnabled: list.IrmEnabled ? 'Yes' : 'No',IsCatalog: list.IsCatalog ? 'Yes' : 'No',IsPrivate: list.IsPrivate ? 'Yes' : 'No',IsSystemList: list.IsSystemList ? 'Yes' : 'No',AllowDeletion: list.AllowDeletion ? 'Yes' : 'No',HasUniqueRoleAssignments: list.HasUniqueRoleAssignments ? 'Yes' : 'No',WebUrl: webUrl,WebTitle: webTitle || webUrl};lists.push(listInfo);}}// Get subsites and their lists recursivelyconst subWebsResponse = await fetch(`${webUrl}/_api/web/webs?$select=Title,Url`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (subWebsResponse.ok) {const subWebsData = await subWebsResponse.json();const subWebs = subWebsData.d.results;for (const subWeb of subWebs) {const subWebLists = await fetchAllLists(subWeb.Url, subWeb.Title);lists.push(...subWebLists);}}} catch (error) {console.error(`Error fetching lists from ${webUrl}:`, error);}return lists;}// Render lists tablefunction renderLists(filterText = '') {let filteredLists = allListsData;// Filter by hidden statusif (!showHiddenLists) {filteredLists = filteredLists.filter(list => list.Hidden !== 'Yes');}// Filter by search textif (filterText) {filteredLists = filteredLists.filter(list => list.Title.toLowerCase().includes(filterText.toLowerCase()));}let tableHTML = `<table id="listsTable" style="width:100%;border-collapse:collapse;margin-top:10px;user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;"><thead><tr style="background:#0078d4;color:white;"><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Title</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">List ID (GUID)</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Web</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Item Count</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Base Template</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Created</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Last Modified</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Hidden</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Actions</th></tr></thead><tbody>`;filteredLists.forEach((list, index) => {const bgColor = index % 2 === 0 ? '#f2f2f2' : 'white';const rowId = `list_row_${list.Id.replace(/[^a-zA-Z0-9]/g, '_')}`;tableHTML += `<tr data-list-id="${list.Id}" data-bg-color="${bgColor}" style="background:${bgColor};cursor:pointer;"><td class="list-title-cell" style="border:1px solid #ddd;padding:8px;font-weight:bold;color:#0078d4;">${list.Title}</td><td style="border:1px solid #ddd;padding:8px;font-family:monospace;font-size:11px;color:#333;user-select:text;">${list.Id}</td><td style="border:1px solid #ddd;padding:8px;color:#333;">${list.WebTitle}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;color:#333;">${list.ItemCount.toLocaleString()}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;color:#333;">${list.BaseTemplate}</td><td style="border:1px solid #ddd;padding:8px;font-size:11px;color:#333;">${list.Created}</td><td style="border:1px solid #ddd;padding:8px;font-size:11px;color:#333;">${list.LastItemModifiedDate}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;color:#333;">${list.Hidden}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;"><button class="btn-open" data-url="${list.DefaultViewUrl}" style="padding:4px 8px;background:#107c10;color:white;border:none;cursor:pointer;margin-right:5px;font-size:11px;">Open</button><button class="btn-settings" data-url="${list.ListSettingsUrl}" style="padding:4px 8px;background:#8b4513;color:white;border:none;cursor:pointer;font-size:11px;">Settings</button></td></tr>`;});tableHTML += '</tbody></table>';contentContainer.innerHTML = tableHTML;// Add event listeners using delegationconst table = document.getElementById('listsTable');if (table) {// Handle row hovertable.addEventListener('mouseover', function(e) {const row = e.target.closest('tr');if (row && row.dataset.listId) {row.style.background = '#e3f2fd';}});table.addEventListener('mouseout', function(e) {const row = e.target.closest('tr');if (row && row.dataset.listId) {row.style.background = row.dataset.bgColor;}});// Handle clickstable.addEventListener('click', function(e) {// Handle title cell clickif (e.target.classList.contains('list-title-cell')) {const row = e.target.closest('tr');if (row && row.dataset.listId) {const list = allListsData.find(l => l.Id === row.dataset.listId);if (list) {createDetailsModal(list);}}}// Handle Open buttonif (e.target.classList.contains('btn-open')) {e.stopPropagation();const url = e.target.dataset.url;if (url) {window.open(url, '_blank');}}// Handle Settings buttonif (e.target.classList.contains('btn-settings')) {e.stopPropagation();const url = e.target.dataset.url;if (url) {window.open(url, '_blank');}}});}// Update info text with better detailsconst hiddenCount = allListsData.filter(list => list.Hidden === 'Yes').length;const visibleCount = allListsData.length - hiddenCount;if (!showHiddenLists) {infoText.textContent = `Showing ${filteredLists.length} of ${visibleCount} visible lists (${hiddenCount} hidden lists excluded)`;} else {infoText.textContent = `Showing ${filteredLists.length} of ${allListsData.length} lists (${visibleCount} visible, ${hiddenCount} hidden)`;}}// Filter input handlerfilterInput.oninput = (e) => {renderLists(e.target.value);};// Toggle hidden lists handlertoggleHiddenButton.onclick = () => {showHiddenLists = !showHiddenLists;toggleHiddenButton.textContent = showHiddenLists ? '🚫 Hide System Lists' : '👁️ Show System Lists';toggleHiddenButton.style.background = showHiddenLists ? '#8b4513' : '#6c757d';renderLists(filterInput.value);};// Load all dataconst loadData = async () => {try {contentContainer.innerHTML = '<div style="color:#0078d4;font-style:italic;">Detecting site collection context...</div>';allListsData = [];// Resolve context — works on classic and modern pagesconst ctx = await getSPContext();siteUrl = ctx.siteUrl;let rootWebUrl = ctx.siteUrl;let rootWebTitle = '';console.log('Resolved SharePoint context:', ctx);// Get the actual site collection rootconst siteResponse = await fetch(`${rootWebUrl}/_api/site/rootweb?$select=Url,Title`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (siteResponse.ok) {const siteData = await siteResponse.json();rootWebUrl = siteData.d.Url;rootWebTitle = siteData.d.Title;console.log('Using site collection root URL:', rootWebUrl);}// Get all lists recursivelyinfoText.textContent = 'Fetching all lists...';allListsData = await fetchAllLists(rootWebUrl, rootWebTitle);// Sort by web and then by titleallListsData.sort((a, b) => {if (a.WebTitle !== b.WebTitle) {return a.WebTitle.localeCompare(b.WebTitle);}return a.Title.localeCompare(b.Title);});// Display infoconst totalLists = allListsData.length;const totalItems = allListsData.reduce((sum, list) => sum + list.ItemCount, 0);const hiddenLists = allListsData.filter(list => list.Hidden === 'Yes').length;const visibleLists = totalLists - hiddenLists;infoText.textContent = `Total: ${totalLists} lists (${visibleLists} visible, ${hiddenLists} hidden), ${totalItems.toLocaleString()} items`;// Render tablerenderLists();// Show download buttondownloadButton.style.display = 'inline-block';// Also log summary to consoleconsole.log('All Lists Summary:', {totalLists,visibleLists,hiddenLists,totalItems,lists: allListsData});} catch (error) {console.error('Error loading data:', error);contentContainer.innerHTML = `<div style="color:red;">Error: ${error.message}</div>`;}};// Download CSVdownloadButton.onclick = () => {if (!allListsData.length) return;// Create CSV contentlet csv = 'List ID,Title,Description,Web Title,Web URL,Item Count,List URL,Default View URL,Root Folder,Entity Type,Base Template,Base Type,Created,Last Modified,Last Deleted,Hidden,System List,Private List,Content Types Enabled,Versioning Enabled,Minor Versions,Moderation,Attachments,Folder Creation,Force Checkout,IRM Enabled,Has Unique Permissions\n';allListsData.forEach(list => {// Helper function to safely convert to string and escape quotesconst escapeCSV = (value) => {const str = (value || '').toString();return str.replace(/"/g, '""');};csv += `"${escapeCSV(list.Id)}","${escapeCSV(list.Title)}","${escapeCSV(list.Description)}","${escapeCSV(list.WebTitle)}","${escapeCSV(list.WebUrl)}","${escapeCSV(list.ItemCount)}","${escapeCSV(list.ListUrl)}","${escapeCSV(list.DefaultViewUrl)}","${escapeCSV(list.RootFolder)}","${escapeCSV(list.EntityTypeName)}","${escapeCSV(list.BaseTemplate)}","${escapeCSV(list.BaseType)}","${escapeCSV(list.Created)}","${escapeCSV(list.LastItemModifiedDate)}","${escapeCSV(list.LastItemDeletedDate)}","${escapeCSV(list.Hidden)}","${escapeCSV(list.IsSystemList)}","${escapeCSV(list.IsPrivate)}","${escapeCSV(list.ContentTypesEnabled)}","${escapeCSV(list.EnableVersioning)}","${escapeCSV(list.EnableMinorVersions)}","${escapeCSV(list.EnableModeration)}","${escapeCSV(list.EnableAttachments)}","${escapeCSV(list.EnableFolderCreation)}","${escapeCSV(list.ForceCheckout)}","${escapeCSV(list.IrmEnabled)}","${escapeCSV(list.HasUniqueRoleAssignments)}"\n`;});// Add UTF-8 BOM to handle special characters correctlyconst BOM = '\uFEFF';const csvWithBOM = BOM + csv;// Download with UTF-8 encodingconst blob = new Blob([csvWithBOM], { type: 'text/csv;charset=utf-8' });const url = window.URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `all_lists_site_collection_${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(url);};// Close buttoncloseButton.onclick = () => {container.style.display = 'none';floatingButton.style.display = 'block';};// Floating button click - reopen main dialogfloatingButton.onclick = () => {container.style.display = 'block';floatingButton.style.display = 'none';};// Auto-load on startloadData();})();
Results:
(async function() {const excluded = ["FileLeafRef","ParentLeafName","ParentVersionString","_UIVersionString","Edit","AppEditor","AppAuthor","FolderChildCount","ItemChildCount","FileSizeDisplay","DocIcon","LinkFilename","LinkFilenameNoMenu","_CheckinComment","CheckoutUser","_CopySource","Editor","Modified","Author","Created","ContentType","ID","_ColorTag","_ComplianceFlags","_ComplianceTag","_ComplianceTagWrittenTime","_ComplianceTagUserId","_IsRecord","LinkTitleNoMenu","LinkTitle","ComplianceAssetId"];// Resolve the SharePoint context — works on classic and modern pagesasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });const c = (await r.json()).d.GetContextWebInformation;return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl, formDigest: c.FormDigestValue };}const container = document.createElement('div');container.innerHTML = `<div style="position:fixed;top:20px;right:20px;width:90%;max-width:1100px;background:#fff;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,.1);user-select:text"><div style="margin-bottom:20px;display:flex;flex-wrap:wrap;gap:8px;align-items:center"><select id="lstSel" style="padding:8px;min-width:200px"><option value="">Select a list...</option></select><button id="csvBtn" style="padding:8px 16px;background:#107c10;color:#fff;border:none;cursor:pointer;display:none">⬇ Schema CSV</button><button id="itemsCsvBtn" style="padding:8px 16px;background:#107c10;color:#fff;border:none;cursor:pointer;display:none">⬇ 10 Items CSV</button><button id="schemaJsonBtn" style="padding:8px 16px;background:#0078d4;color:#fff;border:none;cursor:pointer;display:none">⧉ Copy Schema JSON (for AI)</button><button id="itemsJsonBtn" style="padding:8px 16px;background:#0078d4;color:#fff;border:none;cursor:pointer;display:none">⧉ Copy 10 Items JSON (for AI)</button><button id="bothJsonBtn" style="padding:8px 16px;background:#005a9e;color:#fff;border:none;cursor:pointer;display:none">⧉ Copy Schema + Items JSON (for AI)</button><label><input type="checkbox" id="showHidden"> Show hidden group</label><span id="cnt" style="margin-right:auto;font-weight:bold;color:#107c10"></span><button id="closeBtn" style="padding:8px 16px;background:#d83b01;color:#fff;border:none;cursor:pointer">Close</button></div><div id="tbl"></div></div>`;document.body.appendChild(container);const $ = id => container.querySelector('#'+id);const H = { headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' };let webUrl = '', siteUrl = '', curList = '', allFields = [], listInfo = null, itemsCache = null;const getType = f => f.TypeAsString === 'Choice' ? 'Choice' : f.TypeAsString === 'MultiChoice' ? 'Multichoice' : f.TypeAsString === 'Note' ? `Multiple lines of text (${f.RichText ? 'Rich' : 'Plain'})` : f.TypeDisplayName || '';const getChoices = f => (f.TypeAsString === 'Choice' || f.TypeAsString === 'MultiChoice') && f.Choices?.results ? f.Choices.results.join('; ') : '';const getFormula = f => f.TypeAsString === 'Calculated' ? f.Formula || '' : '';const esc = s => `"${(s==null?'':String(s)).replace(/"/g,'""')}"`;const visible = () => allFields.filter(f => !f.Hidden && !excluded.includes(f.InternalName) && ($('showHidden').checked || f.Group !== '_Hidden'));const flash = m => { $('cnt').textContent = m; };const dl = (data, name, type) => { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([data],{type})); a.download = name; a.click(); };const copy = (obj, m) => navigator.clipboard.writeText(JSON.stringify(obj, null, 2)).then(() => flash(m));const fname = (kind, ext) => `${(webUrl.split('/sites/')[1]?.split('/')[0])||'site'}_${curList}_${kind}_${new Date().toISOString().slice(0,10)}.${ext}`;// strip OData noise: __metadata envelope + unexpanded __deferred navigation linksconst clean = o => Array.isArray(o) ? o.map(clean) : (o && typeof o === 'object' ? Object.fromEntries(Object.entries(o).filter(([k,v])=>k!=='__metadata' && !(v && v.__deferred)).map(([k,v])=>[k,clean(v)])) : o);// List schema as JSON — list/site URLs + key list metadata + the same column info the CSV hasconst buildSchema = () => {const origin = new URL(webUrl).origin;return {listTitle: listInfo.Title, listId: listInfo.Id,listUrl: origin + listInfo.RootFolder.ServerRelativeUrl,defaultViewUrl: origin + listInfo.DefaultViewUrl,siteUrl, webUrl,description: listInfo.Description || '', baseTemplate: listInfo.BaseTemplate,itemCount: listInfo.ItemCount, created: listInfo.Created,lastItemModifiedDate: listInfo.LastItemModifiedDate,entityTypeName: listInfo.EntityTypeName, contentTypesEnabled: listInfo.ContentTypesEnabled,fields: visible().map(f => ({ title:f.Title, internalName:f.InternalName, type:getType(f), required:!!f.Required, choices:getChoices(f)||undefined, formula:getFormula(f)||undefined, description:f.Description||'', group:f.Group||'' }))};};// Latest 10 items by ID, with every column — cached so the buttons share one fetchconst getItems = async () => {if (!itemsCache) {const d = await fetch(`${webUrl}/_api/web/lists/getbytitle('${encodeURIComponent(curList)}')/items?$orderby=Id desc&$top=10`, H).then(r=>r.json());itemsCache = clean(d.d.results);}return itemsCache;};const itemsCsv = items => {const keys = [...new Set(items.flatMap(Object.keys))];const cell = v => v && typeof v === 'object' ? JSON.stringify(v) : v;return keys.map(esc).join(',') + '\n' + items.map(it => keys.map(k => esc(cell(it[k]))).join(',')).join('\n');};const render = () => {const fields = visible();$('cnt').textContent = `Total fields: ${fields.length}`;$('tbl').innerHTML = fields.length ? `<table style="width:100%;border-collapse:collapse;margin-top:10px"><thead><tr style="background:#0078d4;color:#fff">${['Title','Internal Name','Type','Required','Description','Group'].map(h=>`<th style="border:1px solid #ddd;padding:8px">${h}</th>`).join('')}</tr></thead><tbody>${fields.map((f,i)=>`<tr style="background:${i%2?'#fff':'#f2f2f2'}"><td style="border:1px solid #ddd;padding:8px">${f.Title||''}</td><td style="border:1px solid #ddd;padding:8px">${f.InternalName||''}</td><td style="border:1px solid #ddd;padding:8px">${getType(f)}</td><td style="border:1px solid #ddd;padding:8px">${f.Required?'Yes':'No'}</td><td style="border:1px solid #ddd;padding:8px">${f.Description||''}</td><td style="border:1px solid #ddd;padding:8px">${f.Group||''}</td></tr>`).join('')}</tbody></table>` : '';['csvBtn','itemsCsvBtn','schemaJsonBtn','itemsJsonBtn','bothJsonBtn'].forEach(id => $(id).style.display = fields.length ? 'inline-block' : 'none');};const load = async list => {itemsCache = null; listInfo = null; allFields = []; curList = list;if (!list) { render(); return; }$('tbl').innerHTML = '<div style="color:#0078d4;font-style:italic">Loading...</div>';const [info, f] = await Promise.all([fetch(`${webUrl}/_api/web/lists/getbytitle('${encodeURIComponent(list)}')?$select=Title,Id,Description,ItemCount,BaseTemplate,Created,LastItemModifiedDate,EntityTypeName,ContentTypesEnabled,DefaultViewUrl,RootFolder/ServerRelativeUrl&$expand=RootFolder`, H).then(r=>r.json()),fetch(`${webUrl}/_api/web/lists/getbytitle('${encodeURIComponent(list)}')/fields`, H).then(r=>r.json())]);listInfo = info.d; allFields = f.d.results;render();};$('csvBtn').onclick = () => dl('Title,Internal Name,Type,Required,Choices,Formula,Description,Group\n' + visible().map(f => [f.Title,f.InternalName,getType(f),f.Required?'Yes':'No',getChoices(f),getFormula(f),f.Description,f.Group].map(esc).join(',')).join('\n'), fname('fields','csv'), 'text/csv');$('itemsCsvBtn').onclick = async () => dl(itemsCsv(await getItems()), fname('items','csv'), 'text/csv');$('schemaJsonBtn').onclick = () => copy(buildSchema(), 'Copied list schema JSON ✓');$('itemsJsonBtn').onclick = async () => copy(await getItems(), 'Copied 10 items JSON ✓');$('bothJsonBtn').onclick = async () => copy({ schema: buildSchema(), items: await getItems() }, 'Copied schema + items JSON ✓');const ctx = await getSPContext();webUrl = ctx.webUrl; siteUrl = ctx.siteUrl;const lists = await fetch(`${webUrl}/_api/web/lists?$select=Title&$filter=Hidden eq false`, H).then(r=>r.json());lists.d.results.forEach(l => { const o = document.createElement('option'); o.value = o.textContent = l.Title; $('lstSel').appendChild(o); });$('lstSel').addEventListener('change', () => load($('lstSel').value));$('showHidden').addEventListener('change', render);$('closeBtn').addEventListener('click', () => container.remove());})();
Results:
(async function() {// Resolve the SharePoint context — works on classic and modern pagesasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });const c = (await r.json()).d.GetContextWebInformation;return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl, formDigest: c.FormDigestValue };}const ctx = await getSPContext();// Use the site collection root so we can crawl the entire sitelet siteUrl = ctx.siteUrl;// Create UI elementsconst container = document.createElement('div');container.style.cssText = 'position:fixed;top:20px;right:20px;width:95%;max-width:1500px;background:white;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,0.1);user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;';const controls = document.createElement('div');controls.style.cssText = 'margin-bottom:20px;display:flex;align-items:center;';// Tab buttonsconst tableTabButton = document.createElement('button');tableTabButton.textContent = 'Table View';tableTabButton.style.cssText = 'padding:8px 16px;margin-right:5px;background:#0078d4;color:white;border:none;cursor:pointer;border-radius:4px 4px 0 0;';const treeTabButton = document.createElement('button');treeTabButton.textContent = 'Tree View';treeTabButton.style.cssText = 'padding:8px 16px;margin-right:15px;background:#6c757d;color:white;border:none;cursor:pointer;border-radius:4px 4px 0 0;';const downloadButton = document.createElement('button');downloadButton.textContent = 'Download CSV';downloadButton.style.cssText = 'padding:8px 16px;margin-right:10px;background:#107c10;color:white;border:none;cursor:pointer;display:none;';const infoText = document.createElement('span');infoText.style.cssText = 'margin-right:auto;font-weight:bold;color:#107c10;';const closeButton = document.createElement('button');closeButton.textContent = 'Close';closeButton.style.cssText = 'padding:8px 16px;background:#d83b01;color:white;border:none;cursor:pointer;margin-left:20px;';const contentContainer = document.createElement('div');controls.appendChild(tableTabButton);controls.appendChild(treeTabButton);controls.appendChild(downloadButton);controls.appendChild(infoText);controls.appendChild(closeButton);container.appendChild(controls);container.appendChild(contentContainer);document.body.appendChild(container);let allWebsData = [];let hierarchicalData = [];let rootWebUrl = '';let currentView = 'table';// Recursively fetch all subsites (webs) with comprehensive dataasync function fetchWebAndSubsites(webUrl, level = 0) {const allWebs = [];try {// Get current web info with all needed propertiesconst webResponse = await fetch(`${webUrl}/_api/web?$select=Id,Title,Url,ServerRelativeUrl,ParentWeb/ServerRelativeUrl,Created,WebTemplate,Language,HasUniqueRoleAssignments,Description,Configuration,LastItemModifiedDate,LastItemUserModifiedDate,MasterUrl,IsMultilingual,RequestAccessEmail&$expand=ParentWeb`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (!webResponse.ok) {throw new Error('Failed to fetch web info for: ' + webUrl);}const webData = await webResponse.json();const currentWeb = webData.d;// Get list count for this webconst listsResponse = await fetch(`${webUrl}/_api/web/lists?$select=Id,ItemCount&$filter=Hidden eq false`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});let totalLists = 0;let totalItems = 0;if (listsResponse.ok) {const listsData = await listsResponse.json();const lists = listsData.d.results;totalLists = lists.length;totalItems = lists.reduce((sum, list) => sum + (list.ItemCount || 0), 0);}const webInfo = {Id: currentWeb.Id,Title: currentWeb.Title,Url: currentWeb.Url,ServerRelativeUrl: currentWeb.ServerRelativeUrl,ParentURL: currentWeb.ParentWeb ? currentWeb.ParentWeb.ServerRelativeUrl : '',Created: new Date(currentWeb.Created).toLocaleDateString(),Template: currentWeb.WebTemplate,Language: currentWeb.Language,HasUniquePermissions: currentWeb.HasUniqueRoleAssignments ? 'Yes' : 'No',Description: currentWeb.Description || '',Configuration: currentWeb.Configuration || '',LastItemModifiedDate: currentWeb.LastItemModifiedDate ? new Date(currentWeb.LastItemModifiedDate).toLocaleDateString() : '',LastItemUserModifiedDate: currentWeb.LastItemUserModifiedDate ? new Date(currentWeb.LastItemUserModifiedDate).toLocaleDateString() : '',MasterUrl: currentWeb.MasterUrl || '',IsMultilingual: currentWeb.IsMultilingual ? 'Yes' : 'No',RequestAccessEmail: currentWeb.RequestAccessEmail || '',TotalLists: totalLists,TotalItems: totalItems,Level: level,Children: []};allWebs.push(webInfo);// Get child websconst subWebsResponse = await fetch(`${webUrl}/_api/web/webs?$select=Title,Url,ServerRelativeUrl`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (subWebsResponse.ok) {const subWebsData = await subWebsResponse.json();const subWebs = subWebsData.d.results;// Recursively get all child websfor (const subWeb of subWebs) {const childWebs = await fetchWebAndSubsites(subWeb.Url, level + 1);webInfo.Children.push(...childWebs);allWebs.push(...childWebs);}}} catch (error) {console.error(`Error fetching web ${webUrl}:`, error);}return allWebs;}// Build hierarchical structure for tree viewfunction buildHierarchy(webs) {const webMap = new Map();const rootWebs = [];// Create a map of all webswebs.forEach(web => {webMap.set(web.ServerRelativeUrl, {...web,children: [],expanded: true,directChildrenCount: 0,totalChildrenCount: 0});});// Build parent-child relationshipswebs.forEach(web => {if (web.ParentURL) {const parent = webMap.get(web.ParentURL);if (parent) {parent.children.push(webMap.get(web.ServerRelativeUrl));parent.directChildrenCount++;} else {rootWebs.push(webMap.get(web.ServerRelativeUrl));}} else {rootWebs.push(webMap.get(web.ServerRelativeUrl));}});// Calculate total children count recursivelyfunction calculateTotalChildren(node) {let total = node.children.length;node.children.forEach(child => {total += calculateTotalChildren(child);});node.totalChildrenCount = total;return total;}rootWebs.forEach(root => calculateTotalChildren(root));return rootWebs;}// Render tree viewfunction renderTreeView(webs, level = 0, isLast = [], parentPath = '') {let html = '';webs.forEach((web, index) => {const isLastChild = index === webs.length - 1;const currentIsLast = [...isLast, isLastChild];const nodeId = `node_${web.ServerRelativeUrl.replace(/[^a-zA-Z0-9]/g, '_')}`;// Build tree connector lineslet connector = '';for (let i = 0; i < level; i++) {if (i === level - 1) {connector += isLastChild ? '└─ ' : '├─ ';} else {connector += isLast[i] ? ' ' : '│ ';}}const hasChildren = web.children && web.children.length > 0;// Expand/collapse iconlet expandIconHtml = '';if (hasChildren) {const expandIcon = web.expanded ? '[-]' : '[+]';expandIconHtml = `<span class="expand-icon" data-node="${nodeId}" style="color:#0066cc;cursor:pointer;margin-right:4px;user-select:none;font-weight:bold;">${expandIcon}</span>`;}// Create clickable linksconst siteContentsUrl = `${web.Url}/_layouts/15/viewlsts.aspx`;const metricsUrl = `${web.Url}/_layouts/15/usage.aspx`;// Children count displaylet childrenInfo = '';if (hasChildren) {if (web.directChildrenCount === web.totalChildrenCount) {childrenInfo = ` [${web.directChildrenCount} child sites]`;} else {childrenInfo = ` [${web.directChildrenCount} direct, ${web.totalChildrenCount} total child sites]`;}}html += `<div style="font-family:'Courier New',monospace;font-size:12px;line-height:1.2;margin:0;padding:2px 0;white-space:nowrap;border-bottom:1px dotted #ddd;"><span style="color:#666;user-select:none;">${connector}</span>${expandIconHtml}<span style="color:#0078d4;font-weight:bold;margin-right:8px;">${web.Title}</span><span style="color:#222;margin-right:8px;font-size:11px;font-weight:bold;">${web.ServerRelativeUrl}</span><span style="color:#8b4513;margin-right:6px;font-size:10px;font-weight:bold;">[${web.Template}]</span><span style="color:#006400;margin-right:6px;font-size:10px;font-weight:bold;">${web.TotalLists} lists</span><span style="color:#8b0000;margin-right:8px;font-size:10px;font-weight:bold;">${web.TotalItems} items</span><span style="color:#000080;margin-right:6px;font-size:10px;font-weight:bold;">${childrenInfo}</span><a href="${siteContentsUrl}" target="_blank" style="color:#1565c0;text-decoration:none;margin-right:6px;font-size:10px;">Contents</a><a href="${metricsUrl}" target="_blank" style="color:#1565c0;text-decoration:none;font-size:10px;">Metrics</a></div>`;if (hasChildren && web.expanded) {html += `<div id="${nodeId}_children">${renderTreeView(web.children, level + 1, currentIsLast, nodeId)}</div>`;} else if (hasChildren) {html += `<div id="${nodeId}_children" style="display:none;">${renderTreeView(web.children, level + 1, currentIsLast, nodeId)}</div>`;}});return html;}// Show table viewfunction showTableView() {currentView = 'table';tableTabButton.style.background = '#0078d4';treeTabButton.style.background = '#6c757d';let tableHTML = `<table style="width:100%;border-collapse:collapse;margin-top:10px;user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;"><thead><tr style="background:#0078d4;color:white;"><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Title</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Relative URL</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Created</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Template</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Language</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Unique Permissions</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Total Lists</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">Total Items</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">View Site Contents</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;user-select:text;">View Metrics</th></tr></thead><tbody>`;allWebsData.forEach((web, index) => {const bgColor = index % 2 === 0 ? '#f2f2f2' : 'white';// Create clickable linksconst siteContentsUrl = `${web.Url}/_layouts/15/viewlsts.aspx`;const metricsUrl = `${web.Url}/_layouts/15/usage.aspx`;const siteContentsLink = `<a href="${siteContentsUrl}" target="_blank" style="color:#0078d4;text-decoration:none;">View Contents</a>`;const metricsLink = `<a href="${metricsUrl}" target="_blank" style="color:#0078d4;text-decoration:none;">View Metrics</a>`;tableHTML += `<tr style="background:${bgColor};"><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;">${web.Title}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;font-size:12px;">${web.ServerRelativeUrl}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;">${web.Created}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;">${web.Template}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;text-align:center;">${web.Language}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;text-align:center;">${web.HasUniquePermissions}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;text-align:center;">${web.TotalLists}</td><td style="border:1px solid #ddd;padding:8px;user-select:text;cursor:text;text-align:center;">${web.TotalItems}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;">${siteContentsLink}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;">${metricsLink}</td></tr>`;});tableHTML += '</tbody></table>';contentContainer.innerHTML = tableHTML;}// Toggle node expansionfunction toggleNode(nodeId) {const childrenDiv = document.getElementById(`${nodeId}_children`);const expandIcon = document.querySelector(`[data-node="${nodeId}"]`);if (childrenDiv && expandIcon) {const isVisible = childrenDiv.style.display !== 'none';childrenDiv.style.display = isVisible ? 'none' : 'block';expandIcon.textContent = isVisible ? '[+]' : '[-]';}}// Show tree viewfunction showTreeView() {currentView = 'tree';treeTabButton.style.background = '#0078d4';tableTabButton.style.background = '#6c757d';const treeHTML = `<div style="margin-top:10px;border:1px solid #ddd;background:#f8f8f8;padding:15px;max-height:60vh;overflow:auto;"><div style="margin-bottom:10px;color:#0078d4;font-weight:bold;font-size:14px;">Site Collection Hierarchy</div><div style="font-size:11px;color:#666;margin-bottom:10px;">Click [+]/[-] to expand/collapse branches</div><div style="background:white;border:1px inset #ccc;padding:8px;">${renderTreeView(hierarchicalData)}</div></div>`;contentContainer.innerHTML = treeHTML;// Add click handlers for expand/collapsedocument.querySelectorAll('.expand-icon').forEach(icon => {if (icon.style.cursor === 'pointer') {icon.addEventListener('click', function(e) {e.preventDefault();toggleNode(this.getAttribute('data-node'));});}});}// Load all dataconst loadData = async () => {try {contentContainer.innerHTML = '<div style="color:#0078d4;font-style:italic;">Loading all webs in site collection...</div>';allWebsData = [];// First get the web context to ensure we have the right URLconst contextResponse = await fetch(`${siteUrl}/_api/web`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});if (!contextResponse.ok) {throw new Error('Failed to get site context');}const contextData = await contextResponse.json();rootWebUrl = contextData.d.Url;// Get all webs recursivelyinfoText.textContent = 'Fetching all webs...';allWebsData = await fetchWebAndSubsites(rootWebUrl);// Build hierarchical structurehierarchicalData = buildHierarchy(allWebsData);// Display infoconst totalWebs = allWebsData.length;const totalLists = allWebsData.reduce((sum, web) => sum + web.TotalLists, 0);const totalItems = allWebsData.reduce((sum, web) => sum + web.TotalItems, 0);infoText.textContent = `Total webs: ${totalWebs}, Total lists: ${totalLists}, Total items: ${totalItems}`;// Show default view (table)showTableView();// Show download buttondownloadButton.style.display = 'inline-block';// Also log to consoleconsole.table(allWebsData);} catch (error) {console.error('Error loading data:', error);contentContainer.innerHTML = `<div style="color:red;">Error: ${error.message}</div>`;}};// Tab click handlerstableTabButton.onclick = showTableView;treeTabButton.onclick = showTreeView;// Download CSVdownloadButton.onclick = () => {if (!allWebsData.length) return;// Create CSV contentlet csv = 'Id,Title,Relative URL,Parent URL,Full URL,Created,Template,Language,Has Unique Permissions,Description,Configuration,Last Item Modified,Last Item User Modified,Master URL,Is Multilingual,Request Access Email,Total Lists,Total Items,Site Contents URL,Metrics URL\n';allWebsData.forEach(web => {const siteContentsUrl = `${web.Url}/_layouts/15/viewlsts.aspx`;const metricsUrl = `${web.Url}/_layouts/15/usage.aspx`;// Helper function to safely convert to string and escape quotesconst escapeCSV = (value) => {const str = (value || '').toString();return str.replace(/"/g, '""');};csv += `"${escapeCSV(web.Id)}","${escapeCSV(web.Title)}","${escapeCSV(web.ServerRelativeUrl)}","${escapeCSV(web.ParentURL)}","${escapeCSV(web.Url)}","${escapeCSV(web.Created)}","${escapeCSV(web.Template)}","${escapeCSV(web.Language)}","${escapeCSV(web.HasUniquePermissions)}","${escapeCSV(web.Description)}","${escapeCSV(web.Configuration)}","${escapeCSV(web.LastItemModifiedDate)}","${escapeCSV(web.LastItemUserModifiedDate)}","${escapeCSV(web.MasterUrl)}","${escapeCSV(web.IsMultilingual)}","${escapeCSV(web.RequestAccessEmail)}","${escapeCSV(web.TotalLists)}","${escapeCSV(web.TotalItems)}","${escapeCSV(siteContentsUrl)}","${escapeCSV(metricsUrl)}"\n`;});// Downloadconst blob = new Blob([csv], { type: 'text/csv' });const url = window.URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `all_webs_site_collection_hierarchy_${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(url);};// Close buttoncloseButton.onclick = () => {document.body.removeChild(container);};// Auto-load on startloadData();})();
Result:
/*** SharePoint Subsite Permissions Reporter* Run directly in browser console on a SharePoint site*/(async function() {// Resolve the SharePoint context — works on classic and modern pagesasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });const c = (await r.json()).d.GetContextWebInformation;return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl, formDigest: c.FormDigestValue };}const ctx = await getSPContext();const siteUrl = ctx.webUrl;console.log("Resolved SharePoint context:", ctx);var subsitesEndpoint = "/_api/web/webs?$select=Title,ServerRelativeUrl,HasUniqueRoleAssignments";var roleAssignmentsEndpoint = "/_api/web/roleassignments?$expand=Member";var roleDefinitionsEndpoint = "/_api/web/roleassignments(principalid={0})/roledefinitionbindings";// Global variable to store the permissions reportwindow.sharePointPermissionsReport = { sites: [], errors: [] };function getFullUrl(serverRelativeUrl) {const urlParts = siteUrl.match(/^(https?:\/\/[^\/]+)/);if (urlParts && urlParts[1]) {return urlParts[1] + serverRelativeUrl;}return (siteUrl + serverRelativeUrl).replace(/([^:]\/)\/+/g, "$1");}function fetchData(url) {console.log("Fetching:", url);return fetch(url, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'}).then(response => {if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return response.json();});}function getRoleAssignments(webUrl) {return fetchData(webUrl + roleAssignmentsEndpoint).then(data => {return data.d.results;}).catch(error => {console.error(`Error getting role assignments for ${webUrl}:`, error);window.sharePointPermissionsReport.errors.push({URL: webUrl,Error: `Error getting role assignments: ${error.message}`});return [];});}function getRoleDefinitions(webUrl, principalId) {const url = webUrl + roleDefinitionsEndpoint.replace("{0}", principalId);return fetchData(url).then(data => {return data.d.results;}).catch(error => {console.error(`Error getting role definitions for principal ${principalId} at ${webUrl}:`, error);window.sharePointPermissionsReport.errors.push({URL: webUrl,PrincipalId: principalId,Error: `Error getting role definitions: ${error.message}`});return [];});}function getPrincipalType(typeCode) {switch(typeCode) {case 0: return "None";case 1: return "User";case 2: return "Distribution List";case 4: return "Security Group";case 8: return "SharePoint Group";case 15: return "All";default: return "Unknown (" + typeCode + ")";}}function processSitePermissions(webUrl, hasUniquePermissions) {if (!hasUniquePermissions) {console.log(`${webUrl} inherits permissions from parent`);return Promise.resolve([]);}return getRoleAssignments(webUrl).then(assignments => {if (assignments.length === 0) {return [];}const permissionPromises = assignments.map(assignment => {const principal = assignment.Member;return getRoleDefinitions(webUrl, principal.Id).then(roles => {return roles.map(role => {return {SiteUrl: webUrl,PrincipalId: principal.Id,PrincipalTitle: principal.Title,PrincipalType: getPrincipalType(principal.PrincipalType),PrincipalLogin: principal.LoginName || 'N/A',RoleId: role.Id,RoleName: role.Name,RoleDescription: role.Description || 'N/A'};});});});return Promise.all(permissionPromises).then(results => {// Flatten the array of arraysreturn results.flat();});});}function processSite(web, parentUrl = '') {const webUrl = getFullUrl(web.ServerRelativeUrl);console.log("Processing site:", webUrl);return processSitePermissions(webUrl, web.HasUniqueRoleAssignments).then(permissions => {const siteInfo = {Title: web.Title,URL: webUrl,RelativeURL: web.ServerRelativeUrl,ParentURL: parentUrl,HasUniquePermissions: web.HasUniqueRoleAssignments ? 'Yes' : 'No (inherits)'};// Add site info to reportwindow.sharePointPermissionsReport.sites.push(siteInfo);// Add permissions to reportpermissions.forEach(perm => {window.sharePointPermissionsReport.sites.push({Title: web.Title + " > " + perm.PrincipalTitle,URL: webUrl,RelativeURL: web.ServerRelativeUrl,ParentURL: parentUrl,HasUniquePermissions: 'Yes',PrincipalId: perm.PrincipalId,PrincipalTitle: perm.PrincipalTitle,PrincipalType: perm.PrincipalType,PrincipalLogin: perm.PrincipalLogin,RoleId: perm.RoleId,RoleName: perm.RoleName,RoleDescription: perm.RoleDescription});});return fetchData(webUrl + subsitesEndpoint).then(subsitesData => {const subsites = subsitesData.d.results;console.log(`Found ${subsites.length} subsites for ${webUrl}`);const subsitePromises = subsites.map(subsite =>processSite(subsite, webUrl));return Promise.all(subsitePromises);}).catch(error => {console.error(`Error processing subsites for ${webUrl}:`, error);window.sharePointPermissionsReport.errors.push({URL: webUrl,Error: `Error processing subsites: ${error.message}`});});}).catch(error => {console.error(`Error processing site ${webUrl}:`, error);window.sharePointPermissionsReport.errors.push({URL: webUrl,Error: `Error processing site: ${error.message}`});});}// Function to convert the report data to CSVfunction convertToCSV(objArray) {// Get all headers from the dataconst headers = [];objArray.forEach(obj => {Object.keys(obj).forEach(key => {if (!headers.includes(key)) {headers.push(key);}});});// Create CSV header rowlet csvStr = headers.join(',') + '\r\n';// Add each data rowobjArray.forEach(obj => {const values = headers.map(header => {let value = obj[header] === undefined ? '' : obj[header];// Handle values with commas, quotes, or newlinesif (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) {value = '"' + value.replace(/"/g, '""') + '"';}return value;});csvStr += values.join(',') + '\r\n';});return csvStr;}// Function to download the CSV filefunction downloadCSV(csvContent, fileName) {const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });const link = document.createElement('a');// Create a URL for the blobconst url = URL.createObjectURL(blob);link.setAttribute('href', url);link.setAttribute('download', fileName);link.style.visibility = 'hidden';// Append the link to the DOM, click it, and then remove itdocument.body.appendChild(link);link.click();document.body.removeChild(link);}// Function to create and add a download button to the pagefunction addDownloadButton() {const button = document.createElement('button');button.innerText = 'Download SharePoint Permissions as CSV';button.style.padding = '10px 15px';button.style.margin = '20px 0';button.style.backgroundColor = '#0078d4';button.style.color = 'white';button.style.border = 'none';button.style.borderRadius = '4px';button.style.cursor = 'pointer';button.addEventListener('click', () => {const csvContent = convertToCSV(window.sharePointPermissionsReport.sites);const timestamp = new Date().toISOString().replace(/[:.]/g, '-');downloadCSV(csvContent, `SharePoint_Permissions_Report_${timestamp}.csv`);});// Add button to the pageconst container = document.createElement('div');container.style.padding = '20px';container.appendChild(button);// If there are errors, add an errors download button tooif (window.sharePointPermissionsReport.errors.length > 0) {const errorsButton = document.createElement('button');errorsButton.innerText = 'Download Errors as CSV';errorsButton.style.padding = '10px 15px';errorsButton.style.margin = '20px 0 20px 10px';errorsButton.style.backgroundColor = '#d83b01';errorsButton.style.color = 'white';errorsButton.style.border = 'none';errorsButton.style.borderRadius = '4px';errorsButton.style.cursor = 'pointer';errorsButton.addEventListener('click', () => {const csvContent = convertToCSV(window.sharePointPermissionsReport.errors);const timestamp = new Date().toISOString().replace(/[:.]/g, '-');downloadCSV(csvContent, `SharePoint_Permissions_Errors_${timestamp}.csv`);});container.appendChild(errorsButton);}document.body.insertBefore(container, document.body.firstChild);}// Start the root site processing — siteUrl is already authoritative from getSPContext()console.log("Starting SharePoint permissions report...");fetchData(siteUrl + "/_api/web?$select=Title,ServerRelativeUrl,HasUniqueRoleAssignments").then(rootWebData => {return processSite(rootWebData.d);}).then(() => {console.log('Permissions information collected. Outputting table...');console.table(window.sharePointPermissionsReport.sites);if (window.sharePointPermissionsReport.errors.length > 0) {console.log('Errors encountered:');console.table(window.sharePointPermissionsReport.errors);}// Add download button to the pageaddDownloadButton();console.log('Report is now available in the global variable "sharePointPermissionsReport"');console.log('To copy the report to clipboard, run: copy(JSON.stringify(sharePointPermissionsReport))');console.log('You can also use the download button at the top of the page to save the report as a CSV file');}).catch(error => console.error('Error in main process:', error));})();
Sometimes you need a quick inventory of every link in a site’s navigation — to audit broken URLs, copy structure to another site, or just see what you’ve got. This script grabs both the Quick Launch (left-hand nav) and the Top Navigation in one go, walks every level of nested children recursively, and shows them in a tabbed table. One click downloads everything (both navs combined) as a CSV.
(async function() {// Resolve the SharePoint context — works on classic and modern pagesasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}const r = await fetch(`${url}/_api/contextinfo`, { method:'POST', headers:{'Accept':'application/json;odata=verbose'}, credentials:'same-origin' });const c = (await r.json()).d.GetContextWebInformation;return { webUrl: c.WebFullUrl, siteUrl: c.SiteFullUrl, formDigest: c.FormDigestValue };}const ctx = await getSPContext();const webUrl = ctx.webUrl;// UI elementsconst container = document.createElement('div');container.style.cssText = 'position:fixed;top:20px;right:20px;width:95%;max-width:1200px;background:white;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,0.1);user-select:text;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;';const controls = document.createElement('div');controls.style.cssText = 'margin-bottom:20px;display:flex;align-items:center;flex-wrap:wrap;gap:5px;';const quickTab = document.createElement('button');quickTab.textContent = 'Quick Launch';quickTab.style.cssText = 'padding:8px 16px;background:#0078d4;color:white;border:none;cursor:pointer;border-radius:4px 4px 0 0;';const topTab = document.createElement('button');topTab.textContent = 'Top Navigation';topTab.style.cssText = 'padding:8px 16px;margin-right:15px;background:#6c757d;color:white;border:none;cursor:pointer;border-radius:4px 4px 0 0;';const downloadQuickButton = document.createElement('button');downloadQuickButton.textContent = 'Download Quick Launch';downloadQuickButton.style.cssText = 'padding:8px 16px;background:#107c10;color:white;border:none;cursor:pointer;display:none;';const downloadTopButton = document.createElement('button');downloadTopButton.textContent = 'Download Top Navigation';downloadTopButton.style.cssText = 'padding:8px 16px;background:#107c10;color:white;border:none;cursor:pointer;display:none;';const downloadBothButton = document.createElement('button');downloadBothButton.textContent = 'Download Both';downloadBothButton.style.cssText = 'padding:8px 16px;background:#005a9e;color:white;border:none;cursor:pointer;display:none;';const infoText = document.createElement('span');infoText.style.cssText = 'margin-right:auto;font-weight:bold;color:#107c10;margin-left:10px;';const closeButton = document.createElement('button');closeButton.textContent = 'Close';closeButton.style.cssText = 'padding:8px 16px;background:#d83b01;color:white;border:none;cursor:pointer;margin-left:20px;';const tableContainer = document.createElement('div');controls.appendChild(quickTab);controls.appendChild(topTab);controls.appendChild(downloadQuickButton);controls.appendChild(downloadTopButton);controls.appendChild(downloadBothButton);controls.appendChild(infoText);controls.appendChild(closeButton);container.appendChild(controls);container.appendChild(tableContainer);document.body.appendChild(container);let quickNodes = [];let topNodes = [];let activeView = 'quick';// Recursively fetch a node's descendants. SharePoint can nest arbitrarily deep.async function fetchChildren(nodeId) {try {const r = await fetch(`${webUrl}/_api/web/navigation/getnodebyid(${nodeId})/children`, {headers: { 'Accept': 'application/json;odata=verbose' },credentials: 'same-origin'});if (!r.ok) return [];const d = await r.json();return d.d.results || [];} catch (e) {console.error(`Error fetching children of node ${nodeId}:`, e);return [];}}async function flattenNode(node, level, parentTitle) {const flat = [{Id: node.Id,Title: node.Title || '',Url: node.Url || '',IsExternal: node.IsExternal ? 'Yes' : 'No',IsVisible: node.IsVisible ? 'Yes' : 'No',Level: level,Parent: parentTitle || ''}];const children = await fetchChildren(node.Id);for (const child of children) {const sub = await flattenNode(child, level + 1, node.Title);flat.push(...sub);}return flat;}async function loadNav(endpoint) {const r = await fetch(`${webUrl}/_api/web/navigation/${endpoint}`, {headers: { 'Accept': 'application/json;odata=verbose' },credentials: 'same-origin'});if (!r.ok) throw new Error(`Failed to fetch ${endpoint}: ${r.status}`);const d = await r.json();const nodes = d.d.results || [];const all = [];for (const n of nodes) {all.push(...await flattenNode(n, 0, ''));}return all;}function absoluteUrl(u) {if (!u) return '';if (/^https?:\/\//i.test(u)) return u;if (u.startsWith('/')) {const origin = webUrl.match(/^(https?:\/\/[^\/]+)/);return origin ? origin[1] + u : u;}return `${webUrl}/${u}`;}function render() {const data = activeView === 'quick' ? quickNodes : topNodes;const label = activeView === 'quick' ? 'Quick Launch' : 'Top Navigation';infoText.textContent = `${label}: ${data.length} node${data.length === 1 ? '' : 's'}`;if (!data.length) {tableContainer.innerHTML = `<div style="color:#666;font-style:italic;padding:20px;">No ${label.toLowerCase()} nodes found.</div>`;return;}let html = `<table style="width:100%;border-collapse:collapse;margin-top:10px;user-select:text;"><thead><tr style="background:#0078d4;color:white;"><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Title</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">URL</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;width:70px;">Level</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;">Parent</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;width:90px;">External</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;width:80px;">Visible</th><th style="border:1px solid #ddd;padding:8px;position:sticky;top:0;background:#0078d4;width:70px;">ID</th></tr></thead><tbody>`;data.forEach((row, i) => {const bgColor = i % 2 === 0 ? '#f2f2f2' : 'white';const indent = ' '.repeat(row.Level);const arrow = row.Level > 0 ? '↳ ' : '';const linkHtml = row.Url? `<a href="${absoluteUrl(row.Url).replace(/"/g,'"')}" target="_blank" style="color:#0078d4;text-decoration:none;word-break:break-all;">${row.Url}</a>`: '';html += `<tr style="background:${bgColor};"><td style="border:1px solid #ddd;padding:8px;cursor:text;">${indent}${arrow}${row.Title}</td><td style="border:1px solid #ddd;padding:8px;font-size:12px;cursor:text;">${linkHtml}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;">${row.Level}</td><td style="border:1px solid #ddd;padding:8px;cursor:text;">${row.Parent}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;">${row.IsExternal}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;">${row.IsVisible}</td><td style="border:1px solid #ddd;padding:8px;text-align:center;font-size:11px;">${row.Id}</td></tr>`;});html += '</tbody></table>';tableContainer.innerHTML = html;}function setActive(view) {activeView = view;quickTab.style.background = view === 'quick' ? '#0078d4' : '#6c757d';topTab.style.background = view === 'top' ? '#0078d4' : '#6c757d';render();}quickTab.onclick = () => setActive('quick');topTab.onclick = () => setActive('top');closeButton.onclick = () => document.body.removeChild(container);const esc = s => `"${(s == null ? '' : String(s)).replace(/"/g, '""')}"`;const siteName = () => webUrl.split('/sites/')[1]?.split('/')[0] || webUrl.split('//')[1]?.split('/')[0] || 'site';const dateStamp = () => new Date().toISOString().slice(0, 10);function buildCsv(rows, includeSource) {const header = (includeSource ? 'Source,' : '') + 'Title,URL,AbsoluteURL,Level,Parent,IsExternal,IsVisible,Id';return header + '\n' + rows.map(r => {const cols = [r.Title, r.Url, absoluteUrl(r.Url), r.Level, r.Parent, r.IsExternal, r.IsVisible, r.Id];if (includeSource) cols.unshift(r.Source);return cols.map(esc).join(',');}).join('\n');}function downloadCsv(csv, suffix) {const blob = new Blob([csv], { type: 'text/csv' });const a = document.createElement('a');a.href = URL.createObjectURL(blob);a.download = `${siteName()}_navigation_${suffix}_${dateStamp()}.csv`;a.click();URL.revokeObjectURL(a.href);}downloadQuickButton.onclick = () => {downloadCsv(buildCsv(quickNodes, false), 'quicklaunch');};downloadTopButton.onclick = () => {downloadCsv(buildCsv(topNodes, false), 'topnav');};downloadBothButton.onclick = () => {const combined = [...quickNodes.map(n => ({ Source: 'Quick Launch', ...n })),...topNodes.map(n => ({ Source: 'Top Navigation', ...n }))];downloadCsv(buildCsv(combined, true), 'both');};// Load both navigations in paralleltableContainer.innerHTML = '<div style="color:#0078d4;font-style:italic;padding:20px;">Loading navigation nodes (Quick Launch & Top Navigation)...</div>';try {[quickNodes, topNodes] = await Promise.all([loadNav('quicklaunch'),loadNav('topnavigationbar')]);console.log('Quick Launch:', quickNodes);console.log('Top Navigation:', topNodes);if (quickNodes.length) downloadQuickButton.style.display = 'inline-block';if (topNodes.length) downloadTopButton.style.display = 'inline-block';if (quickNodes.length || topNodes.length) downloadBothButton.style.display = 'inline-block';render();} catch (e) {console.error('Error loading navigation:', e);tableContainer.innerHTML = `<div style="color:red;padding:20px;">Error loading navigation: ${e.message}</div>`;}})();
The script returns these per-node fields: Title, URL (raw value as stored — may be relative), Level (0 = top-level), Parent, IsExternal, IsVisible, and the SharePoint node ID. Each CSV adds an AbsoluteURL column that resolves relative paths against the current web.
You get three download buttons:
When you work on Power Automate and SharePoint Online together, one question keeps coming up: which flows are actually wired to this list? The Power Automate portal groups flows by environment, not by the list they trigger on, so tracing them back by hand is slow. There is a much faster way from the browser:
(async function() {// 1. Context Resolutionasync function getSPContext() {const ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;let url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {for (const s of ['/_layouts/','/_api/','/SitePages/','/Lists/','/Forms/','/Pages/','/SiteAssets/','/_app/']) {const i = url.toLowerCase().indexOf(s.toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}url = url.replace(/\/[^\/]*\.aspx$/i, '');}return { webUrl: url };}const ctx = await getSPContext();const webUrl = ctx.webUrl;const H = { headers: {'Accept': 'application/json;odata=verbose'}, credentials: 'same-origin' };// 2. Build the UIconst container = document.createElement('div');container.innerHTML = `<div style="position:fixed;top:20px;right:20px;width:90%;max-width:1100px;background:#fff;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,.1);font-family:Segoe UI, sans-serif;user-select:text;"><div style="margin-bottom:20px;display:flex;flex-wrap:wrap;gap:15px;align-items:center"><h3 style="margin:0;color:#0078d4;font-size:18px;">Flow Explorer</h3><select id="lstSel" style="padding:8px;min-width:250px;border:1px solid #ccc;border-radius:4px;"><option value="">Select a list to load flows...</option></select><button id="scanBtn" style="padding:8px 16px;background:#107c10;color:#fff;border:none;cursor:pointer;border-radius:4px;">Scan All Lists</button><span id="cnt" style="margin-right:auto;font-weight:bold;color:#107c10"></span><button id="closeBtn" style="padding:8px 16px;background:#d83b01;color:#fff;border:none;cursor:pointer;border-radius:4px;">Close</button></div><div id="tbl"></div></div>`;document.body.appendChild(container);const $ = id => container.querySelector('#'+id);// 3. Populate List Dropdowntry {const listsRes = await fetch(`${webUrl}/_api/web/lists?$select=Title&$filter=Hidden eq false`, H).then(r => r.json());listsRes.d.results.forEach(l => {const o = document.createElement('option');o.value = o.textContent = l.Title;$('lstSel').appendChild(o);});} catch (e) {$('cnt').textContent = "Error loading lists.";console.error("List fetch error:", e);}// 4. Shared helpers (used by both the single-list view and the scan view)// The "Open Flow" link cell — kept on one line so the arrow never wrapsconst linkCell = f => f.url? `<a href="${f.url}" target="_blank" style="color:#0078d4;text-decoration:none;font-weight:bold;white-space:nowrap;">Open Flow ↗</a>`: '<span style="color:#999">N/A</span>';// SyncFlowInstances is a POST, so it needs a fresh form digestconst getDigest = async () => {const digestRes = await fetch(`${webUrl}/_api/contextinfo`, { method: 'POST', ...H });if (!digestRes.ok) throw new Error(`Digest fetch failed: ${digestRes.status}`);return (await digestRes.json()).d.GetContextWebInformation.FormDigestValue;};// Fetch and normalise the flows bound to one listconst getFlowsForList = async (listTitle, formDigest) => {const flowRes = await fetch(`${webUrl}/_api/web/lists/GetByTitle('${encodeURIComponent(listTitle)}')/SyncFlowInstances`, {method: "POST",headers: {"Accept": "application/json;odata=verbose","Content-Type": "application/json;odata=verbose","X-RequestDigest": formDigest},credentials: "same-origin"});if (!flowRes.ok) throw new Error(`Flow fetch failed: ${flowRes.status}`);const syncDataString = (await flowRes.json()).d.SynchronizationData;let flows = [];if (syncDataString) {const parsed = JSON.parse(syncDataString);flows = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.value) ? parsed.value : Object.keys(parsed).length ? [parsed] : []);}return flows.map(f => {const envId = f.properties?.environment?.name;const flowId = f.name;return {name: f.properties?.displayName || '-',state: f.properties?.state || '-',id: flowId || '-',env: envId || '-',url: (envId && flowId) ? `https://make.powerautomate.com/environments/${envId}/flows/${flowId}/details` : null};});};// 5. View A — flows for the single list picked in the dropdownconst loadFlows = async (listTitle) => {if (!listTitle) {$('tbl').innerHTML = '';$('cnt').textContent = '';return;}$('tbl').innerHTML = '<div style="color:#0078d4;font-style:italic">Fetching digest & flows...</div>';$('cnt').textContent = '';try {const formDigest = await getDigest();const processedFlows = await getFlowsForList(listTitle, formDigest);$('cnt').textContent = `Flows found: ${processedFlows.length}`;if (processedFlows.length > 0) {$('tbl').innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:14px;text-align:left;"><thead><tr style="background:#0078d4;color:#fff">${['Name','State','ID','Environment','Link'].map(h=>`<th style="padding:8px;border:1px solid #ccc">${h}</th>`).join('')}</tr></thead><tbody>${processedFlows.map((f, i)=>`<tr style="background:${i%2?'#fff':'#f9f9f9'};border-bottom:1px solid #eee;"><td style="padding:8px;border:1px solid #ccc"><b>${f.name}</b></td><td style="padding:8px;border:1px solid #ccc">${f.state}</td><td style="padding:8px;border:1px solid #ccc;font-family:monospace">${f.id}</td><td style="padding:8px;border:1px solid #ccc">${f.env}</td><td style="padding:8px;border:1px solid #ccc">${linkCell(f)}</td></tr>`).join('')}</tbody></table>`;} else {$('tbl').innerHTML = '<div style="color:#666;font-style:italic;padding:10px 0;">No active flows attached to this list.</div>';}} catch (err) {console.error("Execution error:", err);$('tbl').innerHTML = `<div style="color:#d83b01;font-weight:bold;">Error fetching flows: ${err.message}</div>`;$('cnt').textContent = "Error";}};// 6. View B — scan every list on the site, updating each row's status liveconst scanAll = async () => {$('lstSel').value = '';const lists = [...$('lstSel').options].map(o => o.value).filter(Boolean);if (!lists.length) { $('tbl').innerHTML = '<div style="color:#666;">No lists to scan.</div>'; return; }// Pre-render one row per list so the user sees the full work list up front$('tbl').innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:14px;text-align:left;"><thead><tr style="background:#0078d4;color:#fff">${['List','Status','Flows on this list'].map(h=>`<th style="padding:8px;border:1px solid #ccc">${h}</th>`).join('')}</tr></thead><tbody>${lists.map((l, i)=>`<tr id="scanRow_${i}" style="background:${i%2?'#fff':'#f9f9f9'};border-bottom:1px solid #eee;"><td style="padding:8px;border:1px solid #ccc;font-weight:bold;white-space:nowrap;">${l}</td><td class="st" style="padding:8px;border:1px solid #ccc;color:#999;white-space:nowrap;">⏳ Pending</td><td class="fl" style="padding:8px;border:1px solid #ccc"></td></tr>`).join('')}</tbody></table>`;const setStatus = (i, html, color) => { const c = $('scanRow_'+i).querySelector('.st'); c.innerHTML = html; c.style.color = color; };const setFlows = (i, html) => { $('scanRow_'+i).querySelector('.fl').innerHTML = html; };try {const formDigest = await getDigest();let withFlows = 0, totalFlows = 0;for (let i = 0; i < lists.length; i++) {setStatus(i, '🔄 Scanning…', '#0078d4');$('cnt').textContent = `Scanning ${i + 1} / ${lists.length}…`;try {const flows = await getFlowsForList(lists[i], formDigest);if (flows.length) {withFlows++; totalFlows += flows.length;setStatus(i, `✓ ${flows.length} flow${flows.length > 1 ? 's' : ''}`, '#107c10');setFlows(i, flows.map(f => `<div style="margin:2px 0">${f.state === 'Started' ? '🟢' : '⚪'} <b>${f.name}</b> ${linkCell(f)}</div>`).join(''));} else {setStatus(i, '— No flows', '#999');}} catch (e) {setStatus(i, '✗ Error', '#d83b01');setFlows(i, `<span style="color:#d83b01">${e.message}</span>`);console.warn(`Skipped '${lists[i]}':`, e.message);}}$('cnt').textContent = `Done. ${withFlows} of ${lists.length} list(s) have flows, ${totalFlows} flow(s) total.`;} catch (err) {console.error("Scan error:", err);$('cnt').textContent = "Error: " + err.message;}};// 7. Event Listeners$('lstSel').addEventListener('change', () => loadFlows($('lstSel').value));$('scanBtn').addEventListener('click', scanAll);$('closeBtn').addEventListener('click', () => container.remove());})();
Pick a list and the panel fills in. Here it found two flows on the My Documents library, each with its state, flow ID, environment ID, and an Open Flow link:
Click Scan All Lists and the panel switches to the sweep view. Every list shows up with a running status, and the ones with flows surface their flow names and links inline. Here the scan checked 30 lists and found that only Large Document Library has a flow attached:
(async function () {// 1. Context Resolutionasync function getSPContext() {var ctxAvailable = typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.webAbsoluteUrl;var url = ctxAvailable ? _spPageContextInfo.webAbsoluteUrl : window.location.href.split('?')[0].split('#')[0];if (!ctxAvailable) {var markers = ['/_layouts/', '/_api/', '/SitePages/', '/Lists/', '/Forms/', '/Pages/', '/SiteAssets/', '/_app/'];for (var m = 0; m < markers.length; m++) {var i = url.toLowerCase().indexOf(markers[m].toLowerCase());if (i > -1) { url = url.substring(0, i); break; }}if (url.toLowerCase().endsWith('.aspx')) {var q = url.lastIndexOf('/');if (q > -1) url = url.substring(0, q);}}return { webUrl: url };}var ctx = await getSPContext();var webUrl = ctx.webUrl;var H = { headers: { 'Accept': 'application/json;odata=verbose' }, credentials: 'same-origin' };// 2. Build the UI (string concatenation only - no backticks)var panel ='<div style="position:fixed;top:20px;right:20px;width:90%;max-width:1100px;background:#fff;border:2px solid #0078d4;padding:20px;z-index:10000;max-height:80vh;overflow-y:auto;box-shadow:0 4px 6px rgba(0,0,0,.1);font-family:Segoe UI, sans-serif;user-select:text;">' +'<div style="margin-bottom:20px;display:flex;flex-wrap:wrap;gap:15px;align-items:center">' +'<h3 style="margin:0;color:#0078d4;font-size:18px;">XSN Finder</h3>' +'<select id="lstSel" style="padding:8px;min-width:250px;border:1px solid #ccc;border-radius:4px;"><option value="">Select a list or library to scan...</option></select>' +'<button id="scanBtn" style="padding:8px 16px;background:#0078d4;color:#fff;border:none;cursor:pointer;border-radius:4px;">Scan All Lists & Libraries</button>' +'<button id="dlAllBtn" style="padding:8px 16px;background:#107c10;color:#fff;border:none;cursor:pointer;border-radius:4px;display:none;">Download All XSN</button>' +'<span id="cnt" style="margin-right:auto;font-weight:bold;color:#107c10"></span>' +'<button id="closeBtn" style="padding:8px 16px;background:#d83b01;color:#fff;border:none;cursor:pointer;border-radius:4px;">Close</button>' +'</div>' +'<div id="tbl"></div>' +'</div>';var container = document.createElement('div');container.innerHTML = panel;document.body.appendChild(container);var $ = function (id) { return container.querySelector('#' + id); };var allFound = [];// 3. Shared helpers// Follow OData paging so big folders return every fileasync function getAll(url) {var results = [];while (url) {var res = await fetch(url, H);var d = await res.json().catch(function () { return {}; });if (!res.ok) {// e.g. SPQueryThrottledException (list view threshold) - log and stop,// rather than masking it as "no files".var msg = (d && d.error && d.error.message && d.error.message.value) || ('HTTP ' + res.status);console.warn('Folder request skipped:', msg);break;}results = results.concat((d.d && d.d.results) || []);url = (d.d && d.d.__next) || null;}return results;}function fmtSize(n) {n = +n || 0;if (n < 1024) return n + ' B';if (n < 1048576) return (n / 1024).toFixed(1) + ' KB';return (n / 1048576).toFixed(1) + ' MB';}function attrEsc(s) { return String(s == null ? '' : s).split('"').join('"'); }function sqlEsc(s) { return String(s).split("'").join("''"); }function lower(s) { return String(s || '').toLowerCase(); }// Normalise a server-relative URL for comparison (lowercase + decode %20 etc.)function normUrl(s) { var v = lower(s); try { return decodeURIComponent(v); } catch (e) { return v; } }// A "Download" link - delegated click handler below does the actual fetchfunction dlLink(url, name) {return '<a href="#" class="dlBtn" data-url="' + attrEsc(url) + '" data-name="' + attrEsc(name) +'" style="color:#0078d4;text-decoration:none;font-weight:bold;white-space:nowrap;">⬇ Download</a>';}// Pull one file to disk through the authenticated sessionasync function downloadFile(serverRelativeUrl, name) {var api = webUrl + "/_api/web/GetFileByServerRelativeUrl('" + sqlEsc(serverRelativeUrl) + "')/$value";var res = await fetch(api, { credentials: 'same-origin' });if (!res.ok) throw new Error('Download failed: ' + res.status);var blob = await res.blob();var a = document.createElement('a');a.href = URL.createObjectURL(blob);a.download = name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(a.href);}// Walk a folder recursively, collecting every .xsn file (Forms folder included)async function scanFolder(folderUrl, out, libTitle) {var esc = sqlEsc(folderUrl);var base = webUrl + "/_api/web/GetFolderByServerRelativeUrl('" + esc + "')";var files = await getAll(base + '/Files?$select=Name,ServerRelativeUrl,Length,TimeLastModified');for (var i = 0; i < files.length; i++) {var f = files[i];if (lower(f.Name).endsWith('.xsn')) {out.push({ name: f.Name, url: f.ServerRelativeUrl, size: f.Length, modified: f.TimeLastModified, library: libTitle });}}var folders = await getAll(base + '/Folders?$select=ServerRelativeUrl,Name');for (var j = 0; j < folders.length; j++) {// Skip the list "Attachments" store: it holds one subfolder per item ID// (per-item attachments), never form templates, and crawling it fires a// request per item - huge and pointless on big lists.if (lower(folders[j].Name) === 'attachments') continue;await scanFolder(folders[j].ServerRelativeUrl, out, libTitle);}}// De-duplicate found files by their (normalised) URLfunction dedupeFiles(arr) {var seen = {}, out = [];for (var i = 0; i < arr.length; i++) {var k = normUrl(arr[i].url);if (!seen[k]) { seen[k] = 1; out.push(arr[i]); }}return out;}// Content-type names for a list (these are the resource-folder names where templates// live). The content types collection is metadata, so this is safe even on huge lists.async function getListContentTypeNames(title) {try {var url = webUrl + "/_api/web/lists/getbytitle('" + encodeURIComponent(sqlEsc(title)) + "')/contenttypes?$select=Name";var d = await fetch(url, H).then(function (r) { return r.json(); });var arr = (d.d && d.d.results) || [];var names = [];for (var i = 0; i < arr.length; i++) { if (arr[i].Name) names.push(arr[i].Name); }return names;} catch (e) { return []; }}// Enumerate ONLY the folders where form templates live (Forms + each content-type// folder, e.g. "Item"). These are small, folder-scoped queries, so they do NOT trip// the list view threshold the way crawling a 5000+ item list root does.async function getTemplateFolderFiles(title, listRoot) {var out = [];var root = listRoot;if (root.charAt(root.length - 1) === '/') root = root.substring(0, root.length - 1);var candidates = ['Forms'].concat(await getListContentTypeNames(title));for (var c = 0; c < candidates.length; c++) {var folderUrl = root + '/' + candidates[c];var files = await getAll(webUrl + "/_api/web/GetFolderByServerRelativeUrl('" + sqlEsc(folderUrl) + "')/Files?$select=Name,ServerRelativeUrl,Length,TimeLastModified");for (var i = 0; i < files.length; i++) {var f = files[i];if (lower(f.Name).endsWith('.xsn')) {out.push({ name: f.Name, url: f.ServerRelativeUrl, size: f.Length, modified: f.TimeLastModified, library: title });}}}return out;}// Find .xsn files for a list/library WITHOUT tripping the list view threshold.// Always probes the template folders directly; for document libraries it also// crawls for loose uploads (skipping the per-item Attachments store).async function findXsn(title, root, baseType) {var out = await getTemplateFolderFiles(title, root);if (String(baseType) === '1') {var crawled = [];try { await scanFolder(root, crawled, title); } catch (e) { console.warn('Crawl skipped for', title, e && e.message); }out = out.concat(crawled);}return dedupeFiles(out);}// Document templates declared by the list's content types (each may point to an .xsn)async function getContentTypeTemplates(title) {try {var url = webUrl + "/_api/web/lists/getbytitle('" + encodeURIComponent(sqlEsc(title)) + "')/contenttypes?$select=DocumentTemplateUrl";var d = await fetch(url, H).then(function (r) { return r.json(); });var arr = (d.d && d.d.results) || [];var out = [];for (var i = 0; i < arr.length; i++) { if (arr[i].DocumentTemplateUrl) out.push(normUrl(arr[i].DocumentTemplateUrl)); }return out;} catch (e) { return []; }}// Flag each found file as active (the one SharePoint actually opens), inactive, or// - when no authoritative pointer exists - a labeled best guess by path.async function markActive(files, title, listTplUrl) {if (!files.length) return;var active = [];if (listTplUrl) active.push(normUrl(listTplUrl));active = active.concat(await getContentTypeTemplates(title));var haveMeta = active.length > 0;for (var i = 0; i < files.length; i++) {var f = files[i];if (haveMeta) {f.active = active.indexOf(normUrl(f.url)) > -1; // confirmed from metadata} else {f.active = null; // metadata silentf.guess = normUrl(f.url).endsWith('/template.xsn') ? 'default' : 'secondary';}}}function activeBadge(f) {if (f.active === true) return '<span style="color:#107c10;font-weight:bold">● In use</span>';if (f.active === false) return '<span style="color:#b0b0b0">not active</span>';if (f.guess === 'default') return '<span style="color:#107c10">likely (default template)</span>';if (f.guess === 'secondary') return '<span style="color:#b0b0b0">likely secondary</span>';return '<span style="color:#b0b0b0">?</span>';}// 4. View A - scan one library picked from the dropdownfunction renderFiles(files, title) {var heading = '<div style="font-size:16px;font-weight:bold;color:#0078d4;margin:0 0 10px 0;">' + (title || '') + '</div>';if (!files.length) {$('tbl').innerHTML = heading + '<div style="color:#666;font-style:italic;padding:10px 0;">No .xsn files found in this list or library.</div>';return;}var headers = ['File Name', 'In Use', 'Location', 'Size', 'Modified', 'Download'];var thead = '';for (var h = 0; h < headers.length; h++) {thead += '<th style="padding:8px;border:1px solid #ccc">' + headers[h] + '</th>';}var rows = '';for (var i = 0; i < files.length; i++) {var f = files[i];var bg = (i % 2) ? '#fff' : '#f9f9f9';rows +='<tr style="background:' + bg + ';border-bottom:1px solid #eee;">' +'<td style="padding:8px;border:1px solid #ccc"><b>' + f.name + '</b></td>' +'<td style="padding:8px;border:1px solid #ccc;white-space:nowrap">' + activeBadge(f) + '</td>' +'<td style="padding:8px;border:1px solid #ccc;font-size:12px;word-break:break-all">' + f.url + '</td>' +'<td style="padding:8px;border:1px solid #ccc;white-space:nowrap">' + fmtSize(f.size) + '</td>' +'<td style="padding:8px;border:1px solid #ccc;white-space:nowrap">' + new Date(f.modified).toLocaleString() + '</td>' +'<td style="padding:8px;border:1px solid #ccc">' + dlLink(f.url, f.name) + '</td>' +'</tr>';}$('tbl').innerHTML = heading +'<table style="width:100%;border-collapse:collapse;font-size:14px;text-align:left;">' +'<thead><tr style="background:#0078d4;color:#fff">' + thead + '</tr></thead>' +'<tbody>' + rows + '</tbody>' +'</table>';}async function loadLibrary(title) {allFound = [];$('dlAllBtn').style.display = 'none';if (!title) { $('tbl').innerHTML = ''; $('cnt').textContent = ''; return; }var opts = Array.prototype.slice.call($('lstSel').options);var opt = null;for (var k = 0; k < opts.length; k++) { if (opts[k].value === title) { opt = opts[k]; break; } }var root = opt && opt.dataset.root;$('tbl').innerHTML = '<div style="color:#0078d4;font-style:italic">Scanning library for .xsn files...</div>';$('cnt').textContent = '';try {var out = await findXsn(title, root, opt && opt.dataset.base);await markActive(out, title, opt && opt.dataset.tpl);allFound = out;$('cnt').textContent = 'XSN files found: ' + out.length;renderFiles(out, title);$('dlAllBtn').style.display = out.length ? 'inline-block' : 'none';} catch (err) {console.error('Scan error:', err);$('tbl').innerHTML = '<div style="color:#d83b01;font-weight:bold;">Error: ' + err.message + '</div>';$('cnt').textContent = 'Error';}}// 5. View B - scan every library, updating each row's status liveasync function scanAll() {$('lstSel').value = '';allFound = [];$('dlAllBtn').style.display = 'none';var opts = Array.prototype.slice.call($('lstSel').options);var libs = [];for (var k = 0; k < opts.length; k++) { if (opts[k].value) libs.push(opts[k]); }if (!libs.length) { $('tbl').innerHTML = '<div style="color:#666;">No lists or libraries to scan.</div>'; return; }var headers = ['List / Library', 'Status', 'XSN files found'];var thead = '';for (var h = 0; h < headers.length; h++) {thead += '<th style="padding:8px;border:1px solid #ccc">' + headers[h] + '</th>';}var rows = '';for (var r = 0; r < libs.length; r++) {var bg = (r % 2) ? '#fff' : '#f9f9f9';rows +='<tr id="scanRow_' + r + '" style="background:' + bg + ';border-bottom:1px solid #eee;">' +'<td style="padding:8px;border:1px solid #ccc;font-weight:bold;white-space:nowrap;">' + libs[r].value + '</td>' +'<td class="st" style="padding:8px;border:1px solid #ccc;color:#999;white-space:nowrap;">Pending</td>' +'<td class="fl" style="padding:8px;border:1px solid #ccc"></td>' +'</tr>';}$('tbl').innerHTML ='<table style="width:100%;border-collapse:collapse;font-size:14px;text-align:left;">' +'<thead><tr style="background:#0078d4;color:#fff">' + thead + '</tr></thead>' +'<tbody>' + rows + '</tbody>' +'</table>';function setStatus(i, html, color) { var c = $('scanRow_' + i).querySelector('.st'); c.innerHTML = html; c.style.color = color; }function setFiles(i, html) { $('scanRow_' + i).querySelector('.fl').innerHTML = html; }var withXsn = 0, total = 0;for (var i = 0; i < libs.length; i++) {setStatus(i, 'Scanning...', '#0078d4');$('cnt').textContent = 'Scanning ' + (i + 1) + ' / ' + libs.length + '...';try {var out = await findXsn(libs[i].value, libs[i].dataset.root, libs[i].dataset.base);await markActive(out, libs[i].value, libs[i].dataset.tpl);if (out.length) {withXsn++; total += out.length;allFound = allFound.concat(out);setStatus(i, 'OK - ' + out.length + ' file' + (out.length > 1 ? 's' : ''), '#107c10');var inner = '';for (var x = 0; x < out.length; x++) {var f = out[x];inner += '<div style="margin:2px 0"><b>' + f.name + '</b> <span style="color:#666">(' + fmtSize(f.size) + ')</span> [' + activeBadge(f) + '] ' + dlLink(f.url, f.name) + '</div>';}setFiles(i, inner);} else {setStatus(i, 'None', '#999');}} catch (e) {setStatus(i, 'Error', '#d83b01');setFiles(i, '<span style="color:#d83b01">' + e.message + '</span>');console.warn("Skipped '" + libs[i].value + "':", e.message);}}$('cnt').textContent = 'Done. ' + withXsn + ' of ' + libs.length + ' list(s)/library(ies) have XSN files, ' + total + ' total.';$('dlAllBtn').style.display = allFound.length ? 'inline-block' : 'none';}// 6. Populate the dropdown with every non-hidden list AND librarytry {var listUrl = webUrl + '/_api/web/lists?$select=Title,BaseType,DocumentTemplateUrl,RootFolder/ServerRelativeUrl&$expand=RootFolder&$filter=Hidden eq false';var lists = await fetch(listUrl, H).then(function (r) { return r.json(); });var arr = (lists.d && lists.d.results) || [];for (var i = 0; i < arr.length; i++) {var l = arr[i];var o = document.createElement('option');o.value = o.textContent = l.Title;o.dataset.root = l.RootFolder.ServerRelativeUrl;o.dataset.tpl = l.DocumentTemplateUrl || '';o.dataset.base = l.BaseType;$('lstSel').appendChild(o);}} catch (e) {$('cnt').textContent = 'Error loading libraries.';console.error('Library fetch error:', e);}// 7. Event Listeners$('lstSel').addEventListener('change', function () { loadLibrary($('lstSel').value); });$('scanBtn').addEventListener('click', scanAll);$('dlAllBtn').addEventListener('click', async function () {if (!allFound.length) return;$('dlAllBtn').disabled = true;for (var i = 0; i < allFound.length; i++) {$('cnt').textContent = 'Downloading ' + (i + 1) + ' / ' + allFound.length + ': ' + allFound[i].name;try { await downloadFile(allFound[i].url, allFound[i].name); }catch (e) { console.warn('Download failed:', allFound[i].name, e); }}$('cnt').textContent = 'Downloaded ' + allFound.length + ' XSN file(s).';$('dlAllBtn').disabled = false;});// Delegated handler for the per-row Download linkscontainer.addEventListener('click', async function (e) {var btn = e.target.closest('.dlBtn');if (!btn) return;e.preventDefault();var original = btn.innerHTML;btn.innerHTML = 'Downloading...';try { await downloadFile(btn.dataset.url, btn.dataset.name); btn.innerHTML = 'Saved'; }catch (err) { btn.innerHTML = 'Failed'; console.error(err); }setTimeout(function () { btn.innerHTML = original; }, 2000);});$('closeBtn').addEventListener('click', function () { container.remove(); });})();
A few notes. The scan reads files through GetFolderByServerRelativeUrl(...)/Files and .../Folders and follows the OData __next link, so libraries with thousands of files still return everything rather than stopping at the first page. Downloads go through GetFileByServerRelativeUrl(...)/$value, which streams the real file bytes using your current login, so there is no separate auth step. The dropdown lists document libraries only (BaseType eq 1), since that is where .xsn files live. When you use Download All XSN or trigger several single downloads quickly, the browser may ask once to allow multiple file downloads from the site. That is expected, click allow and the rest save without prompting.
I hear you! Indeed, I could build a Chrome extension. However, here’s a kicker: on many SharePoint projects I work on, no Chrome extensions are allowed due to the client’s security configurations.
And that’s it! This approach is a quick and dirty solution that works. It might not be the most elegant or polished approach, but sometimes, you need something fast that gets the job done. With just a bit of vanilla JavaScript and the SharePoint REST API, you can pull all your subsites, lists, and content types, display them in a neat console table, and even download a CSV report—all right from your browser.





