HomeAbout

Vanilla JS and SharePoint REST API

By Denis Molodtsov
Published in SharePoint
January 30, 2025
1 min read
Vanilla JS and SharePoint REST API

Table Of Contents

01
The Problem
02
Solution: JavaScript in the Browser Console for a Quick Report
03
Get Columns for Lists and Libraries
04
Get All Webs in a Site Collection
05
Get All Web Permissions in Site Collection
06
Conclusion

The Problem

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.

Solution: JavaScript in the Browser Console for a Quick Report

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.

Get List of Lists and Content Types

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 lists
var 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 fetched
return Promise.all(allListPromises).then(function(websListData) {
// Flatten the array of arrays
var finalData = [].concat.apply([], websListData);
// Show table in console
console.table(finalData);
// Convert to CSV
var csv = convertToCSV(finalData);
// Automatically download CSV
downloadCSV(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 one
var allWebs = [];
return fetchWebInfo(webUrl)
.then(function(currentWeb) {
// Add the current web to the array
allWebs.push({ Title: currentWeb.Title, Url: currentWeb.Url });
// Now get its direct child webs
return fetchSubWebs(webUrl);
})
.then(function(subwebs) {
// For each child web, recursively call fetchWebAndSubsites
var childWebPromises = subwebs.map(function(sw) {
return fetchWebAndSubsites(sw.Url);
});
// Wait for all children to return their webs
return Promise.all(childWebPromises);
})
.then(function(childWebArrays) {
// childWebArrays is an array of arrays, flatten them
childWebArrays.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 item
var headers = Object.keys(rows[0]);
var csvLines = [];
// Add header line
csvLines.push(headers.join(","));
// Add each row
rows.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 quotes
if (/[",]/.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:

Get list of lists and content types
Get list of lists and content types

Get Columns for Lists and Libraries

(async function() {
// Excluded fields list
const 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 location
const currentUrl = window.location.href;
const siteUrl = currentUrl.split('/_layouts')[0].split('/Lists')[0].split('/Forms')[0].split('/SitePages')[0];
// Create UI elements
const 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 URL
try {
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 URL
const 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 dropdown
dropdown.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 click
viewButton.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 fields
currentFields = allFields.filter(field =>
!field.Hidden &&
!excludedInternalNames.includes(field.InternalName)
);
// Display count
fieldCount.textContent = `Total fields: ${currentFields.length}`;
// Create table
let 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 button
downloadButton.style.display = 'inline-block';
// Also log to console
console.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 CSV
downloadButton.onclick = () => {
if (!currentFields.length) return;
const selectedList = dropdown.value;
// Create CSV content
let 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`;
});
// Download
const 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 button
closeButton.onclick = () => {
document.body.removeChild(container);
};
})();

Results:

Get all fields for the selected iste
Get all fields for the selected iste

Get All Webs in a Site Collection

  • Important: Navigate to the Site Contents page
  • Open Browser Console
  • Paste the following script and run it:
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 report
window.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 data
const 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 data
const permissionsPromise = fetchData(webUrl + permissionsEndpoint)
.then(permData => {
return permData.d.HasUniqueRoleAssignments;
});
// Process both promises
return 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 CSV
function convertToCSV(objArray) {
// Get all headers from the data
const headers = [];
objArray.forEach(obj => {
Object.keys(obj).forEach(key => {
if (!headers.includes(key)) {
headers.push(key);
}
});
});
// Create CSV header row
let csvStr = headers.join(',') + '\r\n';
// Add each data row
objArray.forEach(obj => {
const values = headers.map(header => {
let value = obj[header] === undefined ? '' : obj[header];
// Handle values with commas, quotes, or newlines
if (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 file
function downloadCSV(csvContent, fileName) {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
// Create a URL for the blob
const 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 it
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Function to create and add a download button to the page
function 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 page
const container = document.createElement('div');
container.style.padding = '20px';
container.appendChild(button);
// If there are errors, add an errors download button too
if (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 page
addDownloadButton();
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));

Get All Web Permissions in Site Collection

  • Important: Navigate to the Site Contents page
  • Open Browser Console
  • Paste the following script and run it:
/**
* 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 report
window.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 arrays
return 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 report
window.sharePointPermissionsReport.sites.push(siteInfo);
// Add permissions to report
permissions.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 CSV
function convertToCSV(objArray) {
// Get all headers from the data
const headers = [];
objArray.forEach(obj => {
Object.keys(obj).forEach(key => {
if (!headers.includes(key)) {
headers.push(key);
}
});
});
// Create CSV header row
let csvStr = headers.join(',') + '\r\n';
// Add each data row
objArray.forEach(obj => {
const values = headers.map(header => {
let value = obj[header] === undefined ? '' : obj[header];
// Handle values with commas, quotes, or newlines
if (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 file
function downloadCSV(csvContent, fileName) {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
// Create a URL for the blob
const 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 it
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Function to create and add a download button to the page
function 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 page
const container = document.createElement('div');
container.style.padding = '20px';
container.appendChild(button);
// If there are errors, add an errors download button too
if (window.sharePointPermissionsReport.errors.length > 0) {
const errorsButton = document.createElement('button');
errorsButton.innerText = 'Download Errors as CSV';
errorsButton.style.padding = '10px 15px';
errorsButton.style.margin = '20px 0 20px 10px';
errorsButton.style.backgroundColor = '#d83b01';
errorsButton.style.color = 'white';
errorsButton.style.border = 'none';
errorsButton.style.borderRadius = '4px';
errorsButton.style.cursor = 'pointer';
errorsButton.addEventListener('click', () => {
const csvContent = convertToCSV(window.sharePointPermissionsReport.errors);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
downloadCSV(csvContent, `SharePoint_Permissions_Errors_${timestamp}.csv`);
});
container.appendChild(errorsButton);
}
document.body.insertBefore(container, document.body.firstChild);
}
// Start the root site processing
console.log("Starting SharePoint permissions report...");
// Get the current site data
fetchData(siteUrl + "/_api/web?$select=Title,ServerRelativeUrl,HasUniqueRoleAssignments")
.then(rootWebData => {
return processSite(rootWebData.d);
})
.then(() => {
console.log('Permissions information collected. Outputting table...');
console.table(window.sharePointPermissionsReport.sites);
if (window.sharePointPermissionsReport.errors.length > 0) {
console.log('Errors encountered:');
console.table(window.sharePointPermissionsReport.errors);
}
// Add download button to the page
addDownloadButton();
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));

Conclusion

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.


Tags

SharePointJavaScriptRest API

Share

Previous Article
Retain SharePoint Online Logs
Denis Molodtsov

Denis Molodtsov

Microsoft 365 Architect

Related Posts

Find all Deletion Events in SharePoint Online
Find all Deletion Events in SharePoint Online
May 02, 2025
1 min

Quick Links

About

Social Media