
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() {// 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*/var siteUrl = _spPageContextInfo.siteAbsoluteUrl;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, {method: 'GET',headers: {'Accept': 'application/json;odata=verbose'}}).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...");// Get the current site datafetchData(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));
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.