
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 and dirty report on the fly—no extra tools required. It’s not the most polished approach out there, but when you need to discover your SharePoint data fast, it really gets the job done.
Below is a fun little script 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’s console and even downloads a CSV file for you. Super handy when you need a quick report!
/************************************************************** 1) MAIN ENTRY POINT:* Start from the current site, get all webs (subsites),* then get lists from each web, build final data, and export.*************************************************************/fetchWebAndSubsites(_spPageContextInfo.webAbsoluteUrl).then(function(allWebs) {// For each web, retrieve its listsvar allListPromises = allWebs.map(function(web) {return fetchWebLists(web.Url).then(function(lists) {return lists.map(function(list) {var cts = list.ContentTypes && list.ContentTypes.results? list.ContentTypes.results.map(function(ct) { return ct.Name; }).join(", "): "";return {"Site Title": web.Title,"Site URL": web.Url,"List Title": list.Title,"List URL": list.DefaultViewUrl,"Content Types": cts};});});});// Wait until all list data is fetchedreturn Promise.all(allListPromises).then(function(websListData) {// Flatten the array of arraysvar finalData = [].concat.apply([], websListData);// Show table in consoleconsole.table(finalData);// Convert to CSVvar csv = convertToCSV(finalData);// Automatically download CSVdownloadCSV(csv, "AllSubsitesLists.csv");});}).catch(function(error) {console.error("Error retrieving webs/lists:", error);});/************************************************************** 2) RECURSIVELY FETCH ALL SUBSITES (webs)* Starting from a given web URL, gather that web’s info,* then recursively iterate its child webs.*************************************************************/function fetchWebAndSubsites(webUrl) {// We'll collect all webs in an array, including the current onevar allWebs = [];return fetchWebInfo(webUrl).then(function(currentWeb) {// Add the current web to the arrayallWebs.push({ Title: currentWeb.Title, Url: currentWeb.Url });// Now get its direct child websreturn fetchSubWebs(webUrl);}).then(function(subwebs) {// For each child web, recursively call fetchWebAndSubsitesvar childWebPromises = subwebs.map(function(sw) {return fetchWebAndSubsites(sw.Url);});// Wait for all children to return their websreturn Promise.all(childWebPromises);}).then(function(childWebArrays) {// childWebArrays is an array of arrays, flatten themchildWebArrays.forEach(function(arrayOfWebs) {allWebs = allWebs.concat(arrayOfWebs);});return allWebs;});}/*** Fetch the current web’s Title and Url*/function fetchWebInfo(webUrl) {var url = webUrl + "/_api/web?$select=Title,Url";return fetch(url, {method: "GET",headers: { "Accept": "application/json;odata=verbose" }}).then(function(response) {if (!response.ok) {throw new Error("Failed to fetch web info for: " + webUrl);}return response.json();}).then(function(data) {return data.d; // { Title, Url }});}/*** Fetch direct child webs (subsites) of a given web*/function fetchSubWebs(webUrl) {var url = webUrl + "/_api/web/webs?$select=Title,Url";return fetch(url, {method: "GET",headers: { "Accept": "application/json;odata=verbose" }}).then(function(response) {if (!response.ok) {throw new Error("Failed to fetch sub webs for: " + webUrl);}return response.json();}).then(function(data) {return data.d.results; // an array of { Title, Url }});}/************************************************************** 3) FETCH LISTS FOR A GIVEN WEB* We return each list’s Title, DefaultViewUrl, and ContentTypes.*************************************************************/function fetchWebLists(webUrl) {var url = webUrl+ "/_api/web/lists?$select=Title,DefaultViewUrl,ContentTypes/Name"+ "&$expand=ContentTypes";return fetch(url, {method: "GET",headers: { "Accept": "application/json;odata=verbose" }}).then(function(response) {if (!response.ok) {throw new Error("Failed to fetch lists for: " + webUrl);}return response.json();}).then(function(data) {return data.d.results; // array of list objects});}/************************************************************** 4) CSV CONVERSION / DOWNLOAD*************************************************************//*** Converts an array of objects into CSV format.* Automatically wraps fields containing commas or quotes* in quotes and escapes internal quotes.*/function convertToCSV(rows) {if (!rows || !rows.length) {return "";}// Extract headers (keys) from the first itemvar headers = Object.keys(rows[0]);var csvLines = [];// Add header linecsvLines.push(headers.join(","));// Add each rowrows.forEach(function(row) {var values = headers.map(function(header) {var val = row[header] ? row[header].toString() : "";// If the value contains a comma or quote, wrap in quotes & escape quotesif (/[",]/.test(val)) {val = '"' + val.replace(/"/g, '""') + '"';}return val;});csvLines.push(values.join(","));});return csvLines.join("\n");}/*** Initiates a download of the CSV file in the browser.*/function downloadCSV(csvContent, filename) {var blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });var link = document.createElement("a");var url = URL.createObjectURL(blob);link.setAttribute("href", url);link.setAttribute("download", filename);link.style.visibility = "hidden";document.body.appendChild(link);link.click();document.body.removeChild(link);URL.revokeObjectURL(url);}
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:
var siteUrl = _spPageContextInfo.siteAbsoluteUrl;var rootWebEndpoint = siteUrl + "/_api/web?$select=Title,ServerRelativeUrl,Description,Created,LastItemModifiedDate,WebTemplate,AllowRssFeeds,Language";var listsEndpoint = "/_api/web/lists?$select=Title,ItemCount";var subsitesEndpoint = "/_api/web/webs?$select=Title,ServerRelativeUrl,Description,Created,LastItemModifiedDate,WebTemplate,AllowRssFeeds,Language";var permissionsEndpoint = "/_api/web/HasUniqueRoleAssignments";// Global variable to store the reportwindow.sharePointWebReport = { webs: [], 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) {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 getSubsitePath(fullUrl) {const rootUrl = _spPageContextInfo.siteAbsoluteUrl;return fullUrl.replace(rootUrl, '').replace(/^\//, '');}function processWeb(web, parentUrl = '') {const webUrl = getFullUrl(web.ServerRelativeUrl);console.log("Processing web:", webUrl);// Fetch lists dataconst listsPromise = fetchData(webUrl + listsEndpoint).then(listsData => {const lists = listsData.d.results;const totalLists = lists.length;const totalItems = lists.reduce((sum, list) => sum + list.ItemCount, 0);return { totalLists, totalItems };});// Fetch permissions dataconst permissionsPromise = fetchData(webUrl + permissionsEndpoint).then(permData => {return permData.d.HasUniqueRoleAssignments;});// Process both promisesreturn Promise.all([listsPromise, permissionsPromise]).then(([listsData, hasUniquePermissions]) => {const siteContentsUrl = webUrl + '/_layouts/15/viewlsts.aspx';const subsitePath = getSubsitePath(webUrl);const viewMetricsUrl = `${siteUrl}/_layouts/15/storman.aspx?root=${subsitePath}`;return {Title: web.Title,URL: webUrl,RelativeURL: web.ServerRelativeUrl,ParentURL: parentUrl,Description: web.Description || 'N/A',Created: new Date(web.Created).toLocaleString(),LastModified: new Date(web.LastItemModifiedDate).toLocaleString(),Template: web.WebTemplate,Language: web.Language,HasUniquePermissions: hasUniquePermissions ? 'Yes' : 'No',TotalLists: listsData.totalLists,TotalItems: listsData.totalItems,SiteContentsPage: siteContentsUrl,ViewMetrics: viewMetricsUrl};}).catch(error => {console.error(`Error processing web: ${webUrl}`, error);window.sharePointWebReport.errors.push({ URL: webUrl, Error: error.message });return null;});}function processWebAndSubsites(web, parentUrl = '') {return processWeb(web, parentUrl).then(webData => {if (webData === null) return [];window.sharePointWebReport.webs.push(webData);const webUrl = getFullUrl(web.ServerRelativeUrl);return fetchData(webUrl + subsitesEndpoint).then(subsitesData => {const subsitePromises = subsitesData.d.results.map(subsite =>processWebAndSubsites(subsite, webUrl));return Promise.all(subsitePromises);}).then(() => [webData]).catch(error => {console.error(`Error fetching subsites for web: ${webUrl}`, error);window.sharePointWebReport.errors.push({ URL: webUrl, Error: `Error fetching subsites: ${error.message}` });return [webData];});});}// 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 Report 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.sharePointWebReport.webs);const timestamp = new Date().toISOString().replace(/[:.]/g, '-');downloadCSV(csvContent, `SharePoint_Site_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.sharePointWebReport.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.sharePointWebReport.errors);const timestamp = new Date().toISOString().replace(/[:.]/g, '-');downloadCSV(csvContent, `SharePoint_Site_Report_Errors_${timestamp}.csv`);});container.appendChild(errorsButton);}document.body.insertBefore(container, document.body.firstChild);}fetchData(rootWebEndpoint).then(rootWebData => processWebAndSubsites(rootWebData.d)).then(() => {console.log('Web Information collected. Outputting table...');console.table(window.sharePointWebReport.webs);if (window.sharePointWebReport.errors.length > 0) {console.log('Errors encountered:');console.table(window.sharePointWebReport.errors);}// Add download button to the pageaddDownloadButton();console.log('Report is now available in the global variable "sharePointWebReport"');console.log('To copy the report to clipboard, run: copy(JSON.stringify(sharePointWebReport))');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:', error));
/*** 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));
And that’s it! This approach is a quick and dirty solution that just works. It might not be the most elegant or polished approach out there, 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.