UNPKG

@pnp/cli-microsoft365

Version:

Manage Microsoft 365 and SharePoint Framework projects on any platform

1,011 lines • 87.8 kB
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, '&#xA;').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', '&#xA;'); 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