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
But Denis, why not build a Chrome extension?
07
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 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.

This is me, working on a SharePoint project
This is me, working on a SharePoint project

Get List of Lists and Content Types

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 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: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 info
const 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 webs
const 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 webs
for (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 web
async 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 data
const 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 URL
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();
rootWebUrl = contextData.d.Url;
// Get all webs recursively
infoText.textContent = 'Fetching all sites...';
const allWebs = await fetchWebAndSubsites(rootWebUrl);
// For each web, get its lists and content types
let 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 types
for (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 type
contentTypes.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 info
const 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 table
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;">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 URLs
const 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, '&quot;')}">${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 button
downloadButton.style.display = 'inline-block';
// Also log to console
console.table(allContentTypesData);
} catch (error) {
console.error('Error loading data:', error);
tableContainer.innerHTML = `<div style="color:red;">Error: ${error.message}</div>`;
}
};
// Download CSV
downloadButton.onclick = () => {
if (!allContentTypesData.length) return;
// Create CSV content
let 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`;
});
// 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 = `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 button
closeButton.onclick = () => {
document.body.removeChild(container);
};
// Auto-load on start
loadData();
})();

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 list
Get all fields for the selected list

Get All Webs in a Site Collection

  • Open Browser Console
  • Paste the following script and run it:
(async function() {
// Get current site URL from window location
const currentUrl = window.location.href;
let siteUrl = currentUrl;
// Remove common SharePoint paths to get the site URL
const 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 properly
if (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/sitename
siteUrl = parts.slice(0, 5).join('/');
} else if (parts.length > 3) {
// For root site collections: https://domain
siteUrl = parts.slice(0, 3).join('/');
}
}
// Create UI elements
const 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 buttons
const 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 data
async function fetchWebAndSubsites(webUrl, level = 0) {
const allWebs = [];
try {
// Get current web info with all needed properties
const 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 web
const 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 webs
const 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 webs
for (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 view
function buildHierarchy(webs) {
const webMap = new Map();
const rootWebs = [];
// Create a map of all webs
webs.forEach(web => {
webMap.set(web.ServerRelativeUrl, {
...web,
children: [],
expanded: true,
directChildrenCount: 0,
totalChildrenCount: 0
});
});
// Build parent-child relationships
webs.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 recursively
function 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 view
function 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 lines
let connector = '';
for (let i = 0; i < level; i++) {
if (i === level - 1) {
connector += isLastChild ? '└─ ' : '├─ ';
} else {
connector += isLast[i] ? '&nbsp;&nbsp;&nbsp; ' : '│&nbsp;&nbsp; ';
}
}
const hasChildren = web.children && web.children.length > 0;
// Expand/collapse icon
let 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 links
const siteContentsUrl = `${web.Url}/_layouts/15/viewlsts.aspx`;
const metricsUrl = `${web.Url}/_layouts/15/usage.aspx`;
// Children count display
let 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 view
function 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 links
const 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 expansion
function 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 view
function 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/collapse
document.querySelectorAll('.expand-icon').forEach(icon => {
if (icon.style.cursor === 'pointer') {
icon.addEventListener('click', function(e) {
e.preventDefault();
toggleNode(this.getAttribute('data-node'));
});
}
});
}
// Load all data
const 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 URL
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();
rootWebUrl = contextData.d.Url;
// Get all webs recursively
infoText.textContent = 'Fetching all webs...';
allWebsData = await fetchWebAndSubsites(rootWebUrl);
// Build hierarchical structure
hierarchicalData = buildHierarchy(allWebsData);
// Display info
const 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 button
downloadButton.style.display = 'inline-block';
// Also log to console
console.table(allWebsData);
} catch (error) {
console.error('Error loading data:', error);
contentContainer.innerHTML = `<div style="color:red;">Error: ${error.message}</div>`;
}
};
// Tab click handlers
tableTabButton.onclick = showTableView;
treeTabButton.onclick = showTreeView;
// Download CSV
downloadButton.onclick = () => {
if (!allWebsData.length) return;
// Create CSV content
let 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 quotes
const 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`;
});
// 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 = `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 button
closeButton.onclick = () => {
document.body.removeChild(container);
};
// Auto-load on start
loadData();
})();

Result:

View Subsites
View Subsites

Get All Web Permissions in Site Collection

  • 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));

But Denis, why not build a Chrome extension?

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.

Why didn't you build a Chrome extension?!
Why didn't you build a Chrome extension?!

Conclusion

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.


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