@pnp/cli-microsoft365
Version:
Manage Microsoft 365 and SharePoint Framework projects on any platform
1,011 lines • 87.8 kB
JavaScript
import os from 'os';
import { urlUtil } from "./urlUtil.js";
import { validation } from "./validation.js";
import auth from '../Auth.js';
import config from "../config.js";
import { BasePermissions } from '../m365/spo/base-permissions.js';
import request from "../request.js";
import { formatting } from './formatting.js';
import { odata } from './odata.js';
import { RoleType } from '../m365/spo/commands/roledefinition/RoleType.js';
import { entraGroup } from './entraGroup.js';
import { SharingCapabilities } from '../m365/spo/commands/site/SharingCapabilities.js';
import { timersUtil } from './timersUtil.js';
export var CreateFileCopyJobsNameConflictBehavior;
(function (CreateFileCopyJobsNameConflictBehavior) {
CreateFileCopyJobsNameConflictBehavior[CreateFileCopyJobsNameConflictBehavior["Fail"] = 0] = "Fail";
CreateFileCopyJobsNameConflictBehavior[CreateFileCopyJobsNameConflictBehavior["Replace"] = 1] = "Replace";
CreateFileCopyJobsNameConflictBehavior[CreateFileCopyJobsNameConflictBehavior["Rename"] = 2] = "Rename";
})(CreateFileCopyJobsNameConflictBehavior || (CreateFileCopyJobsNameConflictBehavior = {}));
export var CreateFolderCopyJobsNameConflictBehavior;
(function (CreateFolderCopyJobsNameConflictBehavior) {
CreateFolderCopyJobsNameConflictBehavior[CreateFolderCopyJobsNameConflictBehavior["Fail"] = 0] = "Fail";
CreateFolderCopyJobsNameConflictBehavior[CreateFolderCopyJobsNameConflictBehavior["Rename"] = 2] = "Rename";
})(CreateFolderCopyJobsNameConflictBehavior || (CreateFolderCopyJobsNameConflictBehavior = {}));
// Wrapping this into a settings object so we can alter the values in tests
const pollingInterval = 3000;
export const spo = {
async getRequestDigest(siteUrl) {
const requestOptions = {
url: `${siteUrl}/_api/contextinfo`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
return request.post(requestOptions);
},
async ensureFormDigest(siteUrl, logger, context, debug) {
if (validation.isValidFormDigest(context)) {
if (debug) {
await logger.logToStderr('Existing form digest still valid');
}
return context;
}
const res = await spo.getRequestDigest(siteUrl);
const now = new Date();
now.setSeconds(now.getSeconds() + res.FormDigestTimeoutSeconds - 5);
context = {
FormDigestValue: res.FormDigestValue,
FormDigestTimeoutSeconds: res.FormDigestTimeoutSeconds,
FormDigestExpiresAt: now,
WebFullUrl: res.WebFullUrl
};
return context;
},
async waitUntilFinished({ operationId, siteUrl, logger, currentContext, debug, verbose }) {
const resFormDigest = await spo.ensureFormDigest(siteUrl, logger, currentContext, debug);
currentContext = resFormDigest;
if (debug) {
await logger.logToStderr(`Checking if operation ${operationId} completed...`);
}
const requestOptions = {
url: `${siteUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': currentContext.FormDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><Query Id="188" ObjectPathId="184"><Query SelectAllProperties="false"><Properties><Property Name="IsComplete" ScalarProperty="true" /><Property Name="PollingInterval" ScalarProperty="true" /></Properties></Query></Query></Actions><ObjectPaths><Identity Id="184" Name="${operationId.replace(/\\n/g, '
').replace(/"/g, '')}" /></ObjectPaths></Request>`
};
const res = await request.post(requestOptions);
const json = JSON.parse(res);
const response = json[0];
if (response.ErrorInfo) {
throw new Error(response.ErrorInfo.ErrorMessage);
}
else {
const operation = json[json.length - 1];
const isComplete = operation.IsComplete;
if (isComplete) {
if (!debug && verbose) {
process.stdout.write('\n');
}
return;
}
await timersUtil.setTimeout(pollingInterval);
await spo.waitUntilFinished({
operationId: JSON.stringify(operation._ObjectIdentity_),
siteUrl,
logger,
currentContext,
debug,
verbose
});
}
},
async getSpoUrl(logger, debug) {
if (auth.connection.spoUrl) {
if (debug) {
await logger.logToStderr(`SPO URL previously retrieved ${auth.connection.spoUrl}. Returning...`);
}
return auth.connection.spoUrl;
}
if (debug) {
await logger.logToStderr(`No SPO URL available. Retrieving from MS Graph...`);
}
const requestOptions = {
url: `https://graph.microsoft.com/v1.0/sites/root?$select=webUrl`,
headers: {
'accept': 'application/json;odata.metadata=none'
},
responseType: 'json'
};
const res = await request.get(requestOptions);
auth.connection.spoUrl = res.webUrl;
try {
await auth.storeConnectionInfo();
}
catch (e) {
if (debug) {
await logger.logToStderr('Error while storing connection info');
}
}
return auth.connection.spoUrl;
},
async getSpoAdminUrl(logger, debug) {
const spoUrl = await spo.getSpoUrl(logger, debug);
return (spoUrl.replace(/(https:\/\/)([^\.]+)(.*)/, '$1$2-admin$3'));
},
async getTenantId(logger, debug) {
if (auth.connection.spoTenantId) {
if (debug) {
await logger.logToStderr(`SPO Tenant ID previously retrieved ${auth.connection.spoTenantId}. Returning...`);
}
return auth.connection.spoTenantId;
}
if (debug) {
await logger.logToStderr(`No SPO Tenant ID available. Retrieving...`);
}
const spoAdminUrl = await spo.getSpoAdminUrl(logger, debug);
const contextInfo = await spo.getRequestDigest(spoAdminUrl);
const tenantInfoRequestOptions = {
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': contextInfo.FormDigestValue,
accept: 'application/json;odata=nometadata'
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="4" ObjectPathId="3" /><Query Id="5" ObjectPathId="3"><Query SelectAllProperties="true"><Properties /></Query></Query></Actions><ObjectPaths><Constructor Id="3" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /></ObjectPaths></Request>`
};
const res = await request.post(tenantInfoRequestOptions);
const json = JSON.parse(res);
auth.connection.spoTenantId = json[json.length - 1]._ObjectIdentity_.replace('\n', '
');
try {
await auth.storeConnectionInfo();
}
catch (e) {
if (debug) {
await logger.logToStderr('Error while storing connection info');
}
}
return auth.connection.spoTenantId;
},
/**
* Returns the Graph id of a site
* @param webUrl web url e.g. https://contoso.sharepoint.com/sites/site1
*/
async getSpoGraphSiteId(webUrl) {
const url = new URL(webUrl);
const requestOptions = {
url: `https://graph.microsoft.com/v1.0/sites/${url.hostname}:${url.pathname}?$select=id`,
headers: {
'accept': 'application/json;odata.metadata=none'
},
responseType: 'json'
};
const result = await request.get(requestOptions);
return result.id;
},
/**
* Ensures the folder path exists
* @param webFullUrl web full url e.g. https://contoso.sharepoint.com/sites/site1
* @param folderToEnsure web relative or server relative folder path e.g. /Documents/MyFolder or /sites/site1/Documents/MyFolder
* @param siteAccessToken a valid access token for the site specified in the webFullUrl param
*/
async ensureFolder(webFullUrl, folderToEnsure, logger, debug) {
try {
new URL(webFullUrl);
}
catch {
throw new Error('webFullUrl is not a valid URL');
}
if (!folderToEnsure) {
throw new Error('folderToEnsure cannot be empty');
}
// remove last '/' of webFullUrl if exists
const webFullUrlLastCharPos = webFullUrl.length - 1;
if (webFullUrl.length > 1 &&
webFullUrl[webFullUrlLastCharPos] === '/') {
webFullUrl = webFullUrl.substring(0, webFullUrlLastCharPos);
}
folderToEnsure = urlUtil.getWebRelativePath(webFullUrl, folderToEnsure);
if (debug) {
await logger.log(`folderToEnsure`);
await logger.log(folderToEnsure);
await logger.log('');
}
let nextFolder = '';
let prevFolder = '';
let folderIndex = 0;
// build array of folders e.g. ["Shared%20Documents","22","54","55"]
const folders = folderToEnsure.substring(1).split('/');
if (debug) {
await logger.log('folders to process');
await logger.log(JSON.stringify(folders));
await logger.log('');
}
// recursive function
async function checkOrAddFolder() {
if (folderIndex === folders.length) {
if (debug) {
await logger.log(`All sub-folders exist`);
}
return;
}
// append the next sub-folder to the folder path and check if it exists
prevFolder = nextFolder;
nextFolder += `/${folders[folderIndex]}`;
const folderServerRelativeUrl = urlUtil.getServerRelativePath(webFullUrl, nextFolder);
const requestOptions = {
url: `${webFullUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(folderServerRelativeUrl)}')`,
headers: {
'accept': 'application/json;odata=nometadata'
}
};
try {
await request.get(requestOptions);
folderIndex++;
await checkOrAddFolder();
}
catch {
const prevFolderServerRelativeUrl = urlUtil.getServerRelativePath(webFullUrl, prevFolder);
const requestOptions = {
url: `${webFullUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl=@a1)/AddSubFolderUsingPath(DecodedUrl=@a2)?@a1=%27${formatting.encodeQueryParameter(prevFolderServerRelativeUrl)}%27&@a2=%27${formatting.encodeQueryParameter(folders[folderIndex])}%27`,
headers: {
'accept': 'application/json;odata=nometadata'
},
responseType: 'json'
};
try {
await request.post(requestOptions);
folderIndex++;
await checkOrAddFolder();
}
catch (err) {
if (debug) {
await logger.log(`Could not create sub-folder ${folderServerRelativeUrl}`);
}
throw err;
}
}
}
;
return checkOrAddFolder();
},
/**
* Requests web object identity for the current web.
* That request is something similar to _contextinfo in REST.
* The response data looks like:
* _ObjectIdentity_=<GUID>|<GUID>:site:<GUID>:web:<GUID>
* _ObjectType_=SP.Web
* ServerRelativeUrl=/sites/contoso
* @param webUrl web url
* @param formDigestValue formDigestValue
*/
async getCurrentWebIdentity(webUrl, formDigestValue) {
const requestOptions = {
url: `${webUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': formDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><Query Id="1" ObjectPathId="5"><Query SelectAllProperties="false"><Properties><Property Name="ServerRelativeUrl" ScalarProperty="true" /></Properties></Query></Query></Actions><ObjectPaths><Property Id="5" ParentId="3" Name="Web" /><StaticProperty Id="3" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" /></ObjectPaths></Request>`
};
const res = await request.post(requestOptions);
const json = JSON.parse(res);
const contents = json.find(x => { return x.ErrorInfo; });
if (contents && contents.ErrorInfo) {
throw contents.ErrorInfo.ErrorMessage || 'ClientSvc unknown error';
}
const identityObject = json.find(x => { return x._ObjectIdentity_; });
if (identityObject) {
return {
objectIdentity: identityObject._ObjectIdentity_,
serverRelativeUrl: identityObject.ServerRelativeUrl
};
}
throw 'Cannot proceed. _ObjectIdentity_ not found';
},
/**
* Gets EffectiveBasePermissions for web return type is "_ObjectType_\":\"SP.Web\".
* @param webObjectIdentity ObjectIdentity. Has format _ObjectIdentity_=<GUID>|<GUID>:site:<GUID>:web:<GUID>
* @param webUrl web url
* @param siteAccessToken site access token
* @param formDigestValue formDigestValue
*/
async getEffectiveBasePermissions(webObjectIdentity, webUrl, formDigestValue, logger, debug) {
const basePermissionsResult = new BasePermissions();
const requestOptions = {
url: `${webUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': formDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><Query Id="11" ObjectPathId="5"><Query SelectAllProperties="false"><Properties><Property Name="EffectiveBasePermissions" ScalarProperty="true" /></Properties></Query></Query></Actions><ObjectPaths><Identity Id="5" Name="${webObjectIdentity}" /></ObjectPaths></Request>`
};
const res = await request.post(requestOptions);
if (debug) {
await logger.log('Attempt to get the web EffectiveBasePermissions');
}
const json = JSON.parse(res);
const contents = json.find(x => { return x.ErrorInfo; });
if (contents && contents.ErrorInfo) {
throw contents.ErrorInfo.ErrorMessage || 'ClientSvc unknown error';
}
const permissionsObj = json.find(x => { return x.EffectiveBasePermissions; });
if (permissionsObj) {
basePermissionsResult.high = permissionsObj.EffectiveBasePermissions.High;
basePermissionsResult.low = permissionsObj.EffectiveBasePermissions.Low;
return basePermissionsResult;
}
throw ('Cannot proceed. EffectiveBasePermissions not found'); // this is not supposed to happen
},
/**
* Gets folder by server relative url (GetFolderByServerRelativeUrl in REST)
* The response data looks like:
* _ObjectIdentity_=<GUID>|<GUID>:site:<GUID>:web:<GUID>:folder:<GUID>
* _ObjectType_=SP.Folder
* @param webObjectIdentity ObjectIdentity. Has format _ObjectIdentity_=<GUID>|<GUID>:site:<GUID>:web:<GUID>
* @param webUrl web url
* @param siteRelativeUrl site relative url e.g. /Shared Documents/Folder1
* @param formDigestValue formDigestValue
*/
async getFolderIdentity(webObjectIdentity, webUrl, siteRelativeUrl, formDigestValue) {
const serverRelativePath = urlUtil.getServerRelativePath(webUrl, siteRelativeUrl);
const requestOptions = {
url: `${webUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': formDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="10" ObjectPathId="9" /><ObjectIdentityQuery Id="11" ObjectPathId="9" /><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties><Property Name="Properties" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><Method Id="9" ParentId="5" Name="GetFolderByServerRelativeUrl"><Parameters><Parameter Type="String">${serverRelativePath}</Parameter></Parameters></Method><Identity Id="5" Name="${webObjectIdentity}" /></ObjectPaths></Request>`
};
const res = await request.post(requestOptions);
const json = JSON.parse(res);
const contents = json.find(x => { return x.ErrorInfo; });
if (contents && contents.ErrorInfo) {
throw contents.ErrorInfo.ErrorMessage || 'ClientSvc unknown error';
}
const objectIdentity = json.find(x => { return x._ObjectIdentity_; });
if (objectIdentity) {
return {
objectIdentity: objectIdentity._ObjectIdentity_,
serverRelativeUrl: serverRelativePath
};
}
throw 'Cannot proceed. Folder _ObjectIdentity_ not found';
},
/**
* Retrieves the SiteId, VroomItemId and VroomDriveId from a specific file.
* @param webUrl Web url
* @param fileId GUID ID of the file
* @param fileUrl Decoded site-relative or server-relative URL of the file
*/
async getVroomFileDetails(webUrl, fileId, fileUrl) {
let requestUrl = `${webUrl}/_api/web/`;
if (fileUrl) {
const fileServerRelativeUrl = urlUtil.getServerRelativePath(webUrl, fileUrl);
requestUrl += `GetFileByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(fileServerRelativeUrl)}')`;
}
else {
requestUrl += `GetFileById('${fileId}')`;
}
requestUrl += '?$select=SiteId,VroomItemId,VroomDriveId';
const requestOptions = {
url: requestUrl,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
const res = await request.get(requestOptions);
return res;
},
/**
* Retrieves a list of Custom Actions from a SharePoint site.
* @param webUrl Web url
* @param scope The scope of custom actions to retrieve, allowed values "Site", "Web" or "All".
* @param filter An OData filter query to limit the results.
*/
async getCustomActions(webUrl, scope, filter) {
if (scope && scope !== "All" && scope !== "Site" && scope !== "Web") {
throw `Invalid scope '${scope}'. Allowed values are 'Site', 'Web' or 'All'.`;
}
const queryString = filter ? `?$filter=${filter}` : "";
if (scope && scope !== "All") {
return await odata.getAllItems(`${webUrl}/_api/${scope}/UserCustomActions${queryString}`);
}
const customActions = [
...await odata.getAllItems(`${webUrl}/_api/Site/UserCustomActions${queryString}`),
...await odata.getAllItems(`${webUrl}/_api/Web/UserCustomActions${queryString}`)
];
return customActions;
},
/**
* Retrieves a Custom Actions from a SharePoint site by Id.
* @param webUrl Web url
* @param id The Id of the Custom Action
* @param scope The scope of custom actions to retrieve, allowed values "Site", "Web" or "All".
*/
async getCustomActionById(webUrl, id, scope) {
if (scope && scope !== "All" && scope !== "Site" && scope !== "Web") {
throw `Invalid scope '${scope}'. Allowed values are 'Site', 'Web' or 'All'.`;
}
async function getById(webUrl, id, scope) {
const requestOptions = {
url: `${webUrl}/_api/${scope}/UserCustomActions(guid'${id}')`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
const result = await request.get(requestOptions);
if (result["odata.null"] === true) {
return undefined;
}
return result;
}
if (scope && scope !== "All") {
return await getById(webUrl, id, scope);
}
const customActionOnWeb = await getById(webUrl, id, "Web");
if (customActionOnWeb) {
return customActionOnWeb;
}
const customActionOnSite = await getById(webUrl, id, "Site");
return customActionOnSite;
},
async getTenantAppCatalogUrl(logger, debug) {
const spoUrl = await spo.getSpoUrl(logger, debug);
const requestOptions = {
url: `${spoUrl}/_api/SP_TenantSettings_Current`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
const result = await request.get(requestOptions);
return result.CorporateCatalogUrl;
},
/**
* Retrieves the Microsoft Entra ID from a SP user.
* @param webUrl Web url
* @param id The Id of the user
*/
async getUserAzureIdBySpoId(webUrl, id) {
const requestOptions = {
url: `${webUrl}/_api/web/siteusers/GetById('${formatting.encodeQueryParameter(id)}')?$select=AadObjectId`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
const res = await request.get(requestOptions);
return res.AadObjectId.NameId;
},
/**
* Ensure a user exists on a specific SharePoint site.
* @param webUrl URL of the SharePoint site.
* @param logonName Logon name of the user to ensure on the SharePoint site.
* @returns SharePoint user object.
*/
async ensureUser(webUrl, logonName) {
const requestOptions = {
url: `${webUrl}/_api/web/EnsureUser`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json',
data: {
logonName: logonName
}
};
return request.post(requestOptions);
},
/**
* Ensure a Microsoft Entra ID group exists on a specific SharePoint site.
* @param webUrl URL of the SharePoint site.
* @param group Microsoft Entra ID group.
* @returns SharePoint user object.
*/
async ensureEntraGroup(webUrl, group) {
if (!group.securityEnabled) {
throw new Error('Cannot ensure a Microsoft Entra ID group that is not security enabled.');
}
return this.ensureUser(webUrl, group.mailEnabled ? `c:0o.c|federateddirectoryclaimprovider|${group.id}` : `c:0t.c|tenant|${group.id}`);
},
/**
* Retrieves the spo user by email.
* Returns a user object
* @param webUrl Web url
* @param email The email of the user
* @param logger the Logger object
* @param verbose Set for verbose logging
*/
async getUserByEmail(webUrl, email, logger, verbose) {
if (verbose && logger) {
await logger.logToStderr(`Retrieving the spo user by email ${email}`);
}
const requestUrl = `${webUrl}/_api/web/siteusers/GetByEmail('${formatting.encodeQueryParameter(email)}')`;
const requestOptions = {
url: requestUrl,
headers: {
'accept': 'application/json;odata=nometadata'
},
responseType: 'json'
};
const userInstance = await request.get(requestOptions);
return userInstance;
},
/**
* Retrieves the menu state for the quick launch.
* @param webUrl Web url
*/
async getQuickLaunchMenuState(webUrl) {
return this.getMenuState(webUrl);
},
/**
* Retrieves the menu state for the top navigation.
* @param webUrl Web url
*/
async getTopNavigationMenuState(webUrl) {
return this.getMenuState(webUrl, '1002');
},
/**
* Retrieves the menu state.
* @param webUrl Web url
* @param menuNodeKey Menu node key
*/
async getMenuState(webUrl, menuNodeKey) {
const requestBody = {
customProperties: null,
depth: 10,
mapProviderName: null,
menuNodeKey: menuNodeKey || null
};
const requestOptions = {
url: `${webUrl}/_api/navigation/MenuState`,
headers: {
accept: 'application/json;odata=nometadata'
},
data: requestBody,
responseType: 'json'
};
return request.post(requestOptions);
},
/**
* Saves the menu state.
* @param webUrl Web url
* @param menuState Updated menu state
*/
async saveMenuState(webUrl, menuState) {
const requestOptions = {
url: `${webUrl}/_api/navigation/SaveMenuState`,
headers: {
accept: 'application/json;odata=nometadata'
},
data: { menuState: menuState },
responseType: 'json'
};
return request.post(requestOptions);
},
/**
* Retrieves the spo group by name.
* Returns a group object
* @param webUrl Web url
* @param name The name of the group
* @param logger the Logger object
* @param verbose Set for verbose logging
*/
async getGroupByName(webUrl, name, logger, verbose) {
if (verbose && logger) {
await logger.logToStderr(`Retrieving the group by name ${name}`);
}
const requestUrl = `${webUrl}/_api/web/sitegroups/GetByName('${formatting.encodeQueryParameter(name)}')`;
const requestOptions = {
url: requestUrl,
headers: {
'accept': 'application/json;odata=nometadata'
},
responseType: 'json'
};
const groupInstance = await request.get(requestOptions);
return groupInstance;
},
/**
* Retrieves the role definition by name.
* Returns a RoleDefinition object
* Returns a RoleDefinition object
* @param webUrl Web url
* @param name the name of the role definition
* @param logger the Logger object
* @param verbose Set for verbose logging
* @param verbose Set for verbose logging
*/
async getRoleDefinitionByName(webUrl, name, logger, verbose) {
if (verbose && logger) {
await logger.logToStderr(`Retrieving the role definitions for ${name}`);
}
const roledefinitions = await odata.getAllItems(`${webUrl}/_api/web/roledefinitions`);
const roledefinition = roledefinitions.find((role) => role.Name === name);
if (!roledefinition) {
throw `No roledefinition is found for ${name}`;
}
const permissions = new BasePermissions();
permissions.high = roledefinition.BasePermissions.High;
permissions.low = roledefinition.BasePermissions.Low;
roledefinition.BasePermissionsValue = permissions.parse();
roledefinition.RoleTypeKindValue = RoleType[roledefinition.RoleTypeKind];
return roledefinition;
},
/**
* Adds a SharePoint site.
* @param type Type of sites to add. Allowed values TeamSite, CommunicationSite, ClassicSite, default TeamSite
* @param title Site title
* @param alias Site alias, used in the URL and in the team site group e-mail (applies to type TeamSite)
* @param url Site URL (applies to type CommunicationSite, ClassicSite)
* @param timeZone Integer representing time zone to use for the site (applies to type ClassicSite)
* @param description Site description
* @param lcid Site language in the LCID format
* @param owners Comma-separated list of users to set as site owners (applies to type TeamSite, ClassicSite)
* @param isPublic Determines if the associated group is public or not (applies to type TeamSite)
* @param classification Site classification (applies to type TeamSite, CommunicationSite)
* @param siteDesignType of communication site to create. Allowed values Topic, Showcase, Blank, default Topic. When creating a communication site, specify either siteDesign or siteDesignId (applies to type CommunicationSite)
* @param siteDesignId Id of the custom site design to use to create the site. When creating a communication site, specify either siteDesign or siteDesignId (applies to type CommunicationSite)
* @param shareByEmailEnabled Determines whether it's allowed to share file with guests (applies to type CommunicationSite)
* @param webTemplate Template to use for creating the site. Default `STS#0` (applies to type ClassicSite)
* @param resourceQuota The quota for this site collection in Sandboxed Solutions units. Default 0 (applies to type ClassicSite)
* @param resourceQuotaWarningLevel The warning level for the resource quota. Default 0 (applies to type ClassicSite)
* @param storageQuota The storage quota for this site collection in megabytes. Default 100 (applies to type ClassicSite)
* @param storageQuotaWarningLevel The warning level for the storage quota in megabytes. Default 100 (applies to type ClassicSite)
* @param removeDeletedSite Set, to remove existing deleted site with the same URL from the Recycle Bin (applies to type ClassicSite)
* @param wait Wait for the site to be provisioned before completing the command (applies to type ClassicSite)
* @param logger the Logger object
* @param verbose set if verbose logging should be logged
*/
async addSite(title, logger, verbose, wait, type, alias, description, owners, shareByEmailEnabled, removeDeletedSite, classification, isPublic, lcid, url, siteDesign, siteDesignId, timeZone, webTemplate, resourceQuota, resourceQuotaWarningLevel, storageQuota, storageQuotaWarningLevel) {
if (type === 'ClassicSite') {
const spoAdminUrl = await spo.getSpoAdminUrl(logger, verbose);
let context = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose);
let exists;
if (removeDeletedSite) {
exists = await spo.siteExists(url, logger, verbose);
}
else {
// assume site doesn't exist
exists = false;
}
if (exists) {
if (verbose) {
await logger.logToStderr('Site exists in the recycle bin');
}
await spo.deleteSiteFromTheRecycleBin(url, logger, verbose, wait);
}
else {
if (verbose) {
await logger.logToStderr('Site not found');
}
}
context = await spo.ensureFormDigest(spoAdminUrl, logger, context, verbose);
if (verbose) {
await logger.logToStderr(`Creating site collection ${url}...`);
}
const lcidOption = typeof lcid === 'number' ? lcid : 1033;
const storageQuotaOption = typeof storageQuota === 'number' ? storageQuota : 100;
const storageQuotaWarningLevelOption = typeof storageQuotaWarningLevel === 'number' ? storageQuotaWarningLevel : 100;
const resourceQuotaOption = typeof resourceQuota === 'number' ? resourceQuota : 0;
const resourceQuotaWarningLevelOption = typeof resourceQuotaWarningLevel === 'number' ? resourceQuotaWarningLevel : 0;
const webTemplateOption = webTemplate || 'STS#0';
const requestOptions = {
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': context.FormDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="4" ObjectPathId="3" /><ObjectPath Id="6" ObjectPathId="5" /><Query Id="7" ObjectPathId="3"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="8" ObjectPathId="5"><Query SelectAllProperties="false"><Properties><Property Name="IsComplete" ScalarProperty="true" /><Property Name="PollingInterval" ScalarProperty="true" /></Properties></Query></Query></Actions><ObjectPaths><Constructor Id="3" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="5" ParentId="3" Name="CreateSite"><Parameters><Parameter TypeId="{11f84fff-b8cf-47b6-8b50-34e692656606}"><Property Name="CompatibilityLevel" Type="Int32">0</Property><Property Name="Lcid" Type="UInt32">${lcidOption}</Property><Property Name="Owner" Type="String">${formatting.escapeXml(owners)}</Property><Property Name="StorageMaximumLevel" Type="Int64">${storageQuotaOption}</Property><Property Name="StorageWarningLevel" Type="Int64">${storageQuotaWarningLevelOption}</Property><Property Name="Template" Type="String">${formatting.escapeXml(webTemplateOption)}</Property><Property Name="TimeZoneId" Type="Int32">${timeZone}</Property><Property Name="Title" Type="String">${formatting.escapeXml(title)}</Property><Property Name="Url" Type="String">${formatting.escapeXml(url)}</Property><Property Name="UserCodeMaximumLevel" Type="Double">${resourceQuotaOption}</Property><Property Name="UserCodeWarningLevel" Type="Double">${resourceQuotaWarningLevelOption}</Property></Parameter></Parameters></Method></ObjectPaths></Request>`
};
const res = await request.post(requestOptions);
const json = JSON.parse(res);
const response = json[0];
if (response.ErrorInfo) {
throw response.ErrorInfo.ErrorMessage;
}
else {
const operation = json[json.length - 1];
const isComplete = operation.IsComplete;
if (!wait || isComplete) {
return;
}
await timersUtil.setTimeout(pollingInterval);
await spo.waitUntilFinished({
operationId: JSON.stringify(operation._ObjectIdentity_),
siteUrl: spoAdminUrl,
logger,
currentContext: context,
verbose: verbose,
debug: verbose
});
}
}
else {
const isTeamSite = type !== 'CommunicationSite';
const spoUrl = await spo.getSpoUrl(logger, verbose);
if (verbose) {
await logger.logToStderr(`Creating new site...`);
}
let requestOptions = {};
if (isTeamSite) {
requestOptions = {
url: `${spoUrl}/_api/GroupSiteManager/CreateGroupEx`,
headers: {
'content-type': 'application/json; odata=verbose; charset=utf-8',
accept: 'application/json;odata=nometadata'
},
responseType: 'json',
data: {
displayName: title,
alias: alias,
isPublic: isPublic,
optionalParams: {
Description: description || '',
CreationOptions: {
results: [],
Classification: classification || ''
}
}
}
};
if (lcid) {
requestOptions.data.optionalParams.CreationOptions.results.push(`SPSiteLanguage:${lcid}`);
}
if (owners) {
requestOptions.data.optionalParams.Owners = {
results: owners.split(',').map(o => o.trim())
};
}
}
else {
if (siteDesignId) {
siteDesignId = siteDesignId;
}
else {
if (siteDesign) {
switch (siteDesign) {
case 'Topic':
siteDesignId = '00000000-0000-0000-0000-000000000000';
break;
case 'Showcase':
siteDesignId = '6142d2a0-63a5-4ba0-aede-d9fefca2c767';
break;
case 'Blank':
siteDesignId = 'f6cc5403-0d63-442e-96c0-285923709ffc';
break;
}
}
else {
siteDesignId = '00000000-0000-0000-0000-000000000000';
}
}
requestOptions = {
url: `${spoUrl}/_api/SPSiteManager/Create`,
headers: {
'content-type': 'application/json;odata=nometadata',
accept: 'application/json;odata=nometadata'
},
responseType: 'json',
data: {
request: {
Title: title,
Url: url,
ShareByEmailEnabled: shareByEmailEnabled,
Description: description || '',
Classification: classification || '',
WebTemplate: 'SITEPAGEPUBLISHING#0',
SiteDesignId: siteDesignId
}
}
};
if (lcid) {
requestOptions.data.request.Lcid = lcid;
}
if (owners) {
requestOptions.data.request.Owner = owners;
}
}
const res = await request.post(requestOptions);
if (isTeamSite) {
if (res.ErrorMessage !== null) {
throw res.ErrorMessage;
}
else {
return res.SiteUrl;
}
}
else {
if (res.SiteStatus === 2) {
return res.SiteUrl;
}
else {
throw 'An error has occurred while creating the site';
}
}
}
},
/**
* Checks if a site exists
* Returns a boolean
* @param url The url of the site
* @param logger the Logger object
* @param verbose set if verbose logging should be logged
*/
async siteExists(url, logger, verbose) {
const spoAdminUrl = await spo.getSpoAdminUrl(logger, verbose);
const context = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose);
if (verbose) {
await logger.logToStderr(`Checking if the site ${url} exists...`);
}
const requestOptions = {
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': context.FormDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="197" ObjectPathId="196" /><ObjectPath Id="199" ObjectPathId="198" /><Query Id="200" ObjectPathId="198"><Query SelectAllProperties="true"><Properties /></Query></Query></Actions><ObjectPaths><Constructor Id="196" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="198" ParentId="196" Name="GetSitePropertiesByUrl"><Parameters><Parameter Type="String">${formatting.escapeXml(url)}</Parameter><Parameter Type="Boolean">false</Parameter></Parameters></Method></ObjectPaths></Request>`
};
const res1 = await request.post(requestOptions);
const json = JSON.parse(res1);
const response = json[0];
if (response.ErrorInfo) {
if (response.ErrorInfo.ErrorTypeName === 'Microsoft.Online.SharePoint.Common.SpoNoSiteException') {
return await this.siteExistsInTheRecycleBin(url, logger, verbose);
}
else {
throw response.ErrorInfo.ErrorMessage;
}
}
else {
const site = json[json.length - 1];
return site.Status === 'Recycled';
}
},
/**
* Checks if a site exists in the recycle bin
* Returns a boolean
* @param url The url of the site
* @param logger the Logger object
* @param verbose set if verbose logging should be logged
*/
async siteExistsInTheRecycleBin(url, logger, verbose) {
if (verbose) {
await logger.logToStderr(`Site doesn't exist. Checking if the site ${url} exists in the recycle bin...`);
}
const spoAdminUrl = await spo.getSpoAdminUrl(logger, verbose);
const context = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose);
const requestOptions = {
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': context.FormDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="181" ObjectPathId="180" /><Query Id="182" ObjectPathId="180"><Query SelectAllProperties="true"><Properties /></Query></Query></Actions><ObjectPaths><Method Id="180" ParentId="175" Name="GetDeletedSitePropertiesByUrl"><Parameters><Parameter Type="String">${formatting.escapeXml(url)}</Parameter></Parameters></Method><Constructor Id="175" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /></ObjectPaths></Request>`
};
const res = await request.post(requestOptions);
const json = JSON.parse(res);
const response = json[0];
if (response.ErrorInfo) {
if (response.ErrorInfo.ErrorTypeName === 'Microsoft.SharePoint.Client.UnknownError') {
return false;
}
throw response.ErrorInfo.ErrorMessage;
}
const site = json[json.length - 1];
return site.Status === 'Recycled';
},
/**
* Deletes a site from the recycle bin
* @param url The url of the site
* @param logger the Logger object
* @param verbose set if verbose logging should be logged
* @param wait set to wait until finished
*/
async deleteSiteFromTheRecycleBin(url, logger, verbose, wait) {
const spoAdminUrl = await spo.getSpoAdminUrl(logger, verbose);
const context = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose);
if (verbose) {
await logger.logToStderr(`Deleting site ${url} from the recycle bin...`);
}
const requestOptions = {
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
'X-RequestDigest': context.FormDigestValue
},
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="185" ObjectPathId="184" /><Query Id="186" ObjectPathId="184"><Query SelectAllProperties="false"><Properties><Property Name="IsComplete" ScalarProperty="true" /><Property Name="PollingInterval" ScalarProperty="true" /></Properties></Query></Query></Actions><ObjectPaths><Method Id="184" ParentId="175" Name="RemoveDeletedSite"><Parameters><Parameter Type="String">${formatting.escapeXml(url)}</Parameter></Parameters></Method><Constructor Id="175" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /></ObjectPaths></Request>`
};
const response = await request.post(requestOptions);
const json = JSON.parse(response);
const responseContent = json[0];
if (responseContent.ErrorInfo) {
throw responseContent.ErrorInfo.ErrorMessage;
}
const operation = json[json.length - 1];
const isComplete = operation.IsComplete;
if (!wait || isComplete) {
return;
}
await timersUtil.setTimeout(pollingInterval);
await spo.waitUntilFinished({
operationId: JSON.stringify(operation._ObjectIdentity_),
siteUrl: spoAdminUrl,
logger,
currentContext: context,
verbose: verbose,
debug: verbose
});
},
/**
* Updates a site with the given properties
* @param url The url of the site
* @param logger The logger object
* @param verbose Set for verbose logging
* @param title The new title
* @param classification The classification to be updated
* @param disableFlows If flows should be disabled or not
* @param isPublic If site should be public or private
* @param owners The owners to be updated
* @param shareByEmailEnabled If share by e-mail should be enabled or not
* @param siteDesignId The site design to be updated
* @param sharingCapability The sharing capability to be updated
*/
async updateSite(url, logger, verbose, title, classification, disableFlows, isPublic, owners, shareByEmailEnabled, siteDesignId, sharingCapability) {
const tenantId = await spo.getTenantId(logger, verbose);
const spoAdminUrl = await spo.getSpoAdminUrl(logger, verbose);
let context = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose);
if (verbose) {
await logger.logToStderr('Loading site IDs...');
}
const requestOptions = {
url: `${url}/_api/site?$select=GroupId,Id`,
headers: {
accept: 'application/json;odata=nometadata'
},
responseType: 'json'
};
const siteInfo = await request.get(requestOptions);
const groupId = siteInfo.GroupId;
const siteId = siteInfo.Id;
const isGroupConnectedSite = groupId !== '00000000-0000-0000-0000-000000000000';
if (verbose) {
await logger.logToStderr(`Retrieved site IDs. siteId: ${siteId}, groupId: ${groupId}`);
}
if (isGroupConnectedSite) {
if (verbose) {
await logger.logToStderr(`Site attached to group ${groupId}`);
}
if (typeof title !== 'undefined' &&
typeof isPublic !== 'undefined' &&
typeof owners !== 'undefined') {
const promises = [];
if (typeof title !== 'undefined') {
const requestOptions = {
url: `${spoAdminUrl}/_api/SPOGroup/UpdateGroupPropertiesBySiteId`,
headers: {
accept: 'application/json;odata=nometadata',
'content-type': 'application/json;charset=utf-8',
'X-RequestDigest': context.FormDigestValue
},
data: {
groupId: groupId,
siteId: siteId,
displayName: title
},
responseType: 'json'
};
promises.push(request.post(requestOptions));
}
if (typeof isPublic !== 'undefined') {
promises.push(entraGroup.setGroup(groupId, (isPublic === false), undefined, undefined, logger, verbose));
}
if (typeof owners !== 'undefined') {
promises.push(spo.setGroupifiedSiteOwners(spoAdminUrl, groupId, owners, logger, verbose));
}
await Promise.all(promises);
}
}
else {
if (verbose) {
await logger.logToStderr('Site is not group connected');
}
if (typeof isPublic !== 'undefined') {
throw `The isPublic option can't be set on a site that is not groupified`;
}
if (owners) {
await Promise.all(owners.split(',').map(async (o) => {
await spo.setSiteAdmin(spoAdminUrl, context, url, o.trim());
}));
}
}
context = await spo.ensureFormDigest(spoAdminUrl, logger, context, verbose);
if (verbose) {
await logger.logToS