
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.
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() {// Get current site URL from window locationconst currentUrl = window.location.href;const siteUrl = currentUrl.split('/_layouts')[0].split('/Lists')[0].split('/Forms')[0].split('/SitePages')[0];// 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 = [];// 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 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() {// 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 = [];// Use _spPageContextInfo if available (most reliable)let rootWebUrl = '';let rootWebTitle = '';if (typeof _spPageContextInfo !== 'undefined' && _spPageContextInfo.siteAbsoluteUrl) {rootWebUrl = _spPageContextInfo.siteAbsoluteUrl;console.log('Using _spPageContextInfo for site URL:', rootWebUrl);} else {// Fallback: get context from current webconst currentUrl = window.location.href.split('/_layouts')[0].split('?')[0];const contextResponse = await fetch(`${currentUrl}/_api/web?$select=Url,Title`, {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;rootWebTitle = contextData.d.Title;console.log('Using API context for site URL:', rootWebUrl);}// 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() {// Excluded fields listconst excludedInternalNames = ["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"];// Get current site URL from window locationconst currentUrl = window.location.href;const siteUrl = currentUrl.split('/_layouts')[0].split('/Lists')[0].split('/Forms')[0].split('/SitePages')[0];// Create UI elementsconst container = document.createElement('div');container.style.cssText = 'position:fixed;top:20px;right:20px;width:90%;max-width:1100px;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);';const controls = document.createElement('div');controls.style.cssText = 'margin-bottom:20px;';const dropdown = document.createElement('select');dropdown.style.cssText = 'padding:8px;margin-right:10px;min-width:200px;';const viewButton = document.createElement('button');viewButton.textContent = 'View Fields';viewButton.style.cssText = 'padding:8px 16px;margin-right:10px;background:#0078d4;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 closeButton = document.createElement('button');closeButton.textContent = 'Close';closeButton.style.cssText = 'padding:8px 16px;background:#d83b01;color:white;border:none;cursor:pointer;';const fieldCount = document.createElement('span');fieldCount.style.cssText = 'margin-left:10px;font-weight:bold;color:#107c10;';const tableContainer = document.createElement('div');controls.appendChild(dropdown);controls.appendChild(viewButton);controls.appendChild(downloadButton);controls.appendChild(fieldCount);controls.appendChild(closeButton);container.appendChild(controls);container.appendChild(tableContainer);document.body.appendChild(container);// First get the web context to ensure we have the right URLtry {const 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();const webUrl = contextData.d.Url;// Now load lists using the confirmed web URLconst listsResponse = await fetch(`${webUrl}/_api/web/lists?$select=Title,Id,BaseTemplate&$filter=Hidden eq false`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});const listsData = await listsResponse.json();const lists = listsData.d.results;// Populate dropdowndropdown.innerHTML = '<option value="">Select a list...</option>';lists.forEach(list => {const option = document.createElement('option');option.value = list.Title;option.textContent = list.Title;dropdown.appendChild(option);});let currentFields = [];// View fields button clickviewButton.onclick = async () => {const selectedList = dropdown.value;if (!selectedList) {alert('Please select a list');return;}try {tableContainer.innerHTML = '<div style="color:#0078d4;font-style:italic;">Loading fields...</div>';const fieldsResponse = await fetch(`${webUrl}/_api/web/lists/getbytitle('${encodeURIComponent(selectedList)}')/fields`, {headers: {'Accept': 'application/json;odata=verbose','Content-Type': 'application/json'},credentials: 'same-origin'});const fieldsData = await fieldsResponse.json();const allFields = fieldsData.d.results;// Filter fieldscurrentFields = allFields.filter(field =>!field.Hidden &&!excludedInternalNames.includes(field.InternalName));// Display countfieldCount.textContent = `Total fields: ${currentFields.length}`;// Create tablelet tableHTML = `<table style="width:100%;border-collapse:collapse;margin-top:10px;"><thead><tr style="background:#0078d4;color:white;"><th style="border:1px solid #ddd;padding:8px;">Title</th><th style="border:1px solid #ddd;padding:8px;">Internal Name</th><th style="border:1px solid #ddd;padding:8px;">Type</th><th style="border:1px solid #ddd;padding:8px;">Description</th><th style="border:1px solid #ddd;padding:8px;">Group</th></tr></thead><tbody>`;currentFields.forEach((field, index) => {const bgColor = index % 2 === 0 ? '#f2f2f2' : 'white';tableHTML += `<tr style="background:${bgColor};"><td style="border:1px solid #ddd;padding:8px;">${field.Title || ''}</td><td style="border:1px solid #ddd;padding:8px;">${field.InternalName || ''}</td><td style="border:1px solid #ddd;padding:8px;">${field.TypeDisplayName || ''}</td><td style="border:1px solid #ddd;padding:8px;">${field.Description || ''}</td><td style="border:1px solid #ddd;padding:8px;">${field.Group || ''}</td></tr>`;});tableHTML += '</tbody></table>';tableContainer.innerHTML = tableHTML;// Show download buttondownloadButton.style.display = 'inline-block';// Also log to consoleconsole.table(currentFields.map(f => ({Title: f.Title,InternalName: f.InternalName,Type: f.TypeDisplayName,Description: f.Description,Group: f.Group})));} catch (error) {console.error('Error loading fields:', error);tableContainer.innerHTML = `<div style="color:red;">Error: ${error.message}</div>`;}};// Download CSVdownloadButton.onclick = () => {if (!currentFields.length) return;const selectedList = dropdown.value;// Create CSV contentlet csv = 'Title,Internal Name,Type,Description,Group\n';currentFields.forEach(field => {csv += `"${(field.Title || '').replace(/"/g, '""')}","${(field.InternalName || '').replace(/"/g, '""')}","${(field.TypeDisplayName || '').replace(/"/g, '""')}","${(field.Description || '').replace(/"/g, '""')}","${(field.Group || '').replace(/"/g, '""')}"\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 = `${selectedList}_fields_${new Date().toISOString().split('T')[0]}.csv`;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(url);};} catch (error) {console.error('Error loading lists:', error);alert('Error loading lists: ' + error.message);}// Close buttoncloseButton.onclick = () => {document.body.removeChild(container);};})();
Results:
(async function() {// Get current site URL from window locationconst currentUrl = window.location.href;let siteUrl = currentUrl;// Remove common SharePoint paths to get the site URLconst pathsToRemove = ['/_layouts','/Lists/','/Forms/','/SitePages/','/SiteAssets/','/Shared%20Documents/','/Documents/','/_api/','/Pages/'];for (const path of pathsToRemove) {if (siteUrl.includes(path)) {siteUrl = siteUrl.split(path)[0];break;}}// Handle managed paths and site collections properlyif (siteUrl.includes('/')) {const parts = siteUrl.split('/');// For URLs like https://collaborate.abcp.ab.bluecross.ca/sites/sitename/...if (parts.length > 4 && parts[3] === 'sites') {// Keep protocol, domain, managed path, and site name: https://domain/sites/sitenamesiteUrl = parts.slice(0, 5).join('/');} else if (parts.length > 3) {// For root site collections: https://domainsiteUrl = parts.slice(0, 3).join('/');}}// 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*/// Get current site URL from window locationconst currentUrl = window.location.href;const siteUrl = currentUrl.split('/_layouts')[0].split('/Lists')[0].split('/Forms')[0].split('/SitePages')[0];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 processingconsole.log("Starting SharePoint permissions report...");// First get the web context to ensure we have the right URLfetchData(`${siteUrl}/_api/web`).then(contextData => {const webUrl = contextData.d.Url;console.log("Confirmed web URL:", webUrl);// Now get the current site data using the confirmed URLreturn fetchData(webUrl + "/_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));
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.