undefined.
+ */
+ async applyMiddleware(app) {
+ await this.addStandardMiddleware();
+ await this.addCustomMiddleware();
+
+ return this.middlewareExecutionOrder.map((name) => {
+ const m = this.middleware[name];
+ app.use(m.mountPath, m.middleware);
+ });
+ }
+
+ /**
+ * Adds the given middleware configuration
+ *
+ * @private
+ * @param {string} middlewareName The name of the middleware
+ * @param {object} [options] The Options of the middleware
+ * @param {object} [options.customMiddleware] The custom middleware
+ * @param {Function} [options.wrapperCallback] Callback called when middleware is called
+ * @param {string} [options.mountPath="/"] The path hosting the middleware
+ * @param {string} [options.beforeMiddleware] The name of the middleware called before the added middleware
+ * @param {string} [options.afterMiddleware] The name of the middleware called after the added middleware
+ */
+ async addMiddleware(middlewareName, {
+ customMiddleware, wrapperCallback, mountPath = "/",
+ beforeMiddleware, afterMiddleware
+ } = {}) {
+ if (this.middleware[middlewareName]) {
+ throw new Error(`A middleware with the name ${middlewareName} has already been added`);
+ }
+
+ let middlewareCallback;
+ if (customMiddleware) {
+ middlewareCallback = customMiddleware;
+ } else {
+ const middlewareInfo = await middlewareRepository.getMiddleware(middlewareName);
+ if (wrapperCallback) {
+ middlewareCallback = wrapperCallback(middlewareInfo);
+ } else {
+ middlewareCallback = middlewareInfo.middleware;
+ }
+ }
+
+ if (this.middlewareExecutionOrder.includes(middlewareName)) {
+ throw new Error(`Middleware ${middlewareName} already added to execution order. This should not happen.`);
+ }
+
+ if (beforeMiddleware || afterMiddleware) {
+ const refMiddlewareName = beforeMiddleware || afterMiddleware;
+ let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName);
+
+ if (refMiddlewareName === "connectUi5Proxy") {
+ throw new Error(
+ `Standard middleware "connectUi5Proxy", referenced by middleware "${middlewareName}" ` +
+ `in project ${this.middlewareUtil.getProject()}, ` +
+ `has been removed in this version of UI5 CLI and can't be referenced anymore. ` +
+ `Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`);
+ }
+ if (refMiddlewareIdx === -1) {
+ throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` +
+ `middleware ${middlewareName}`);
+ }
+ if (afterMiddleware) {
+ // Insert after index of referenced middleware
+ refMiddlewareIdx++;
+ }
+ this.middlewareExecutionOrder.splice(refMiddlewareIdx, 0, middlewareName);
+ } else {
+ this.middlewareExecutionOrder.push(middlewareName);
+ }
+
+ this.middleware[middlewareName] = {
+ middleware: await Promise.resolve(middlewareCallback({
+ resources: this.resources,
+ middlewareUtil: this.middlewareUtil
+ })),
+ mountPath
+ };
+ }
+
+ /**
+ * Adds all registered standard middlewares
+ *
+ * @private
+ * @returns {Promise} Resolving to undefined once all standard middlewares are added
+ */
+ async addStandardMiddleware() {
+ await this.addMiddleware("csp", {
+ wrapperCallback: ({middleware: cspModule}) => {
+ const oCspConfig = {
+ allowDynamicPolicySelection: true,
+ allowDynamicPolicyDefinition: true,
+ definedPolicies: {
+ "sap-target-level-1":
+ "default-src 'self'; " +
+ "script-src 'self' 'unsafe-eval'; " +
+ "style-src 'self' 'unsafe-inline'; " +
+ "font-src 'self' data:; " +
+ "img-src 'self' https: http: data: blob:; " +
+ "media-src 'self' https: http: data: blob:; " +
+ "object-src blob:; " +
+ "frame-src 'self' https: gap: data: blob: mailto: tel:; " +
+ "worker-src 'self' blob:; " +
+ "child-src 'self' blob:; " +
+ "connect-src 'self' https: wss:; " +
+ "base-uri 'self';",
+ "sap-target-level-2":
+ "default-src 'self'; " +
+ "script-src 'self'; " +
+ "style-src 'self' 'unsafe-inline'; " +
+ "font-src 'self' data:; " +
+ "img-src 'self' https: http: data: blob:; " +
+ "media-src 'self' https: http: data: blob:; " +
+ "object-src blob:; " +
+ "frame-src 'self' https: gap: data: blob: mailto: tel:; " +
+ "worker-src 'self' blob:; " +
+ "child-src 'self' blob:; " +
+ "connect-src 'self' https: wss:; " +
+ "base-uri 'self';",
+ "sap-target-level-3":
+ "default-src 'self'; " +
+ "script-src 'self'; " +
+ "style-src 'self'; " +
+ "font-src 'self'; " +
+ "img-src 'self' https:; " +
+ "media-src 'self' https:; " +
+ "object-src 'self'; " +
+ "frame-src 'self' https: gap: mailto: tel:; " +
+ "worker-src 'self'; " +
+ "child-src 'self'; " +
+ "connect-src 'self' https: wss:; " +
+ "base-uri 'self';"
+ }
+ };
+ if (this.options.sendSAPTargetCSP) {
+ const defaultSAPTargetConfig = {
+ defaultPolicy: "sap-target-level-1",
+ defaultPolicyIsReportOnly: true,
+ defaultPolicy2: "sap-target-level-3",
+ defaultPolicy2IsReportOnly: true,
+ ignorePaths: ["test-resources/sap/ui/qunit/testrunner.html"]
+ };
+ Object.assign(oCspConfig, defaultSAPTargetConfig);
+
+ if (typeof this.options.sendSAPTargetCSP === "object") {
+ for (const [name, value] of Object.entries(this.options.sendSAPTargetCSP)) {
+ if (!hasOwn(defaultSAPTargetConfig, name)) {
+ throw new TypeError(
+ `Unknown SAP Target CSP configuration option '${name}'. Allowed options are ` +
+ `${Object.keys(defaultSAPTargetConfig)}`);
+ }
+ oCspConfig[name] = value;
+ }
+ }
+ }
+ if (this.options.serveCSPReports) {
+ Object.assign(oCspConfig, {
+ serveCSPReports: true,
+ });
+ }
+ return () => {
+ return cspModule("sap-ui-xx-csp-policy", oCspConfig);
+ };
+ }
+ });
+ await this.addMiddleware("compression");
+ await this.addMiddleware("cors");
+ await this.addMiddleware("discovery", {
+ mountPath: "/discovery"
+ });
+ await this.addMiddleware("serveResources");
+ await this.addMiddleware("testRunner");
+ await this.addMiddleware("serveThemes");
+ await this.addMiddleware("versionInfo", {
+ mountPath: "/resources/sap-ui-version.json"
+ });
+ // Handle anything but read operations *before* the serveIndex middleware
+ // as it will reject them with a 405 (Method not allowed) instead of 404 like our old tooling
+ await this.addMiddleware("nonReadRequests");
+ await this.addMiddleware("serveIndex", {
+ wrapperCallback: ({middleware: middleware}) => {
+ return ({resources, middlewareUtil}) => middleware({
+ resources,
+ middlewareUtil,
+ simpleIndex: this.options.simpleIndex
+ });
+ }
+ });
+ }
+
+ /**
+ * Adds all registered custom middlewares
+ *
+ * @private
+ * @returns {Promise} Resolving to undefined once all custom middlewares are added
+ */
+ async addCustomMiddleware() {
+ const project = this.graph.getRoot();
+ const projectCustomMiddleware = project.getCustomMiddleware();
+ if (!projectCustomMiddleware.length === 0) {
+ return; // No custom middleware defined
+ }
+
+ for (let i = 0; i < projectCustomMiddleware.length; i++) {
+ const middlewareDef = projectCustomMiddleware[i];
+ if (!middlewareDef.name) {
+ throw new Error(`Missing name for custom middleware definition of project ${project.getName()} ` +
+ `at index ${i}`);
+ }
+ if (middlewareDef.beforeMiddleware && middlewareDef.afterMiddleware) {
+ throw new Error(
+ `Custom middleware definition ${middlewareDef.name} of project ${project.getName()} ` +
+ `defines both "beforeMiddleware" and "afterMiddleware" parameters. Only one must be defined.`);
+ }
+ if (!middlewareDef.beforeMiddleware && !middlewareDef.afterMiddleware) {
+ throw new Error(
+ `Custom middleware definition ${middlewareDef.name} of project ${project.getName()} ` +
+ `defines neither a "beforeMiddleware" nor an "afterMiddleware" parameter. One must be defined.`);
+ }
+ const customMiddleware = this.graph.getExtension(middlewareDef.name);
+ if (!customMiddleware) {
+ throw new Error(
+ `Could not find custom middleware ${middlewareDef.name}, ` +
+ `referenced by project ${project.getName()}`);
+ }
+
+ let middlewareName = middlewareDef.name;
+ if (this.middleware[middlewareName]) {
+ // Middleware is already known
+ // => add a suffix to allow for multiple configurations of the same middleware
+ let suffixCounter = 0;
+ while (this.middleware[middlewareName]) {
+ suffixCounter++; // Start at 1
+ middlewareName = `${middlewareDef.name}--${suffixCounter}`;
+ }
+ }
+
+ await this.addMiddleware(middlewareName, {
+ customMiddleware: async ({resources, middlewareUtil}) => {
+ const params = {
+ resources,
+ options: {
+ configuration: middlewareDef.configuration
+ }
+ };
+
+ const specVersion = customMiddleware.getSpecVersion();
+ if (specVersion.gte("3.0")) {
+ params.options.middlewareName = middlewareName;
+ params.log = getLogger(`server:custom-middleware:${middlewareDef.name}`);
+ }
+ const middlewareUtilInterface = middlewareUtil.getInterface(specVersion);
+ if (middlewareUtilInterface) {
+ params.middlewareUtil = middlewareUtilInterface;
+ }
+ return (await customMiddleware.getMiddleware())(params);
+ },
+ mountPath: middlewareDef.mountPath,
+ beforeMiddleware: middlewareDef.beforeMiddleware,
+ afterMiddleware: middlewareDef.afterMiddleware
+ });
+ }
+ }
+}
+
+export default MiddlewareManager;
diff --git a/packages/server/lib/middleware/MiddlewareUtil.js b/packages/server/lib/middleware/MiddlewareUtil.js
new file mode 100644
index 00000000000..8b977c6a520
--- /dev/null
+++ b/packages/server/lib/middleware/MiddlewareUtil.js
@@ -0,0 +1,273 @@
+import parseurl from "parseurl";
+import mime from "mime-types";
+import {
+ createReaderCollection,
+ createReaderCollectionPrioritized,
+ createResource,
+ createFilterReader,
+ createLinkReader,
+ createFlatReader
+} from "@ui5/fs/resourceFactory";
+
+/**
+ * Convenience functions for UI5 Server middleware.
+ * An instance of this class is passed to every standard UI5 Server middleware.
+ * Custom middleware that define a specification version >= 2.0 will also receive an instance
+ * of this class as part of the parameters of their create-middleware function.
+ *
+ * The set of functions that can be accessed by a custom middleware depends on the specification
+ * version defined for the extension.
+ *
+ * @public
+ * @class
+ * @alias @ui5/server/middleware/MiddlewareUtil
+ * @hideconstructor
+ */
+class MiddlewareUtil {
+ /**
+ *
+ * @param {object} parameters
+ * @param {@ui5/project/graph/ProjectGraph} parameters.graph Relevant ProjectGraph
+ * @param {@ui5/project/specifications/Project} parameters.project Project that is being served
+ * @public
+ */
+ constructor({graph, project}) {
+ if (!graph) {
+ throw new Error(`Missing parameter "graph"`);
+ }
+ if (!project) {
+ throw new Error(`Missing parameter "project"`);
+ }
+ this._graph = graph;
+ this._project = project;
+ }
+
+ /**
+ * Returns the [pathname]{@link https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname}
+ * of a given request. Any escape sequences will be decoded.
+ *
+ * This method is only available to custom middleware extensions defining
+ * Specification Version 2.0 and above.
+ *
+ * @param {object} req Request object
+ * @returns {string} [Pathname]{@link https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname}
+ * of the given request
+ * @public
+ */
+ getPathname(req) {
+ let {pathname} = parseurl(req);
+ pathname = decodeURIComponent(pathname);
+ return pathname;
+ }
+
+ /**
+ * MIME Info
+ *
+ * @example
+ * const mimeInfo = {
+ * "type": "text/html",
+ * "charset": "utf-8",
+ * "contentType": "text/html; charset=utf-8"
+ * };
+ *
+ * @public
+ * @typedef {object} MimeInfo
+ * @property {string} type Detected content-type for the given resource path
+ * @property {string} charset Default charset for the detected content-type
+ * @property {string} contentType Calculated content-type header value
+ * @memberof @ui5/server/middleware/MiddlewareUtil
+ */
+ /**
+ * Returns MIME information derived from a given resource path.
+ *
+ * This method is only available to custom middleware extensions defining
+ * Specification Version 2.0 and above.
+ *
+ * @param {object} resourcePath
+ * @returns {@ui5/server/middleware/MiddlewareUtil.MimeInfo}
+ * @public
+ */
+ getMimeInfo(resourcePath) {
+ const type = mime.lookup(resourcePath) || "application/octet-stream";
+ const charset = mime.charset(type);
+ return {
+ type,
+ charset,
+ contentType: type + (charset ? "; charset=" + charset : "")
+ };
+ }
+ /**
+ * Specification Version-dependent [Project]{@link @ui5/project/specifications/Project} interface.
+ * For details on individual functions, see [Project]{@link @ui5/project/specifications/Project}
+ *
+ * @public
+ * @typedef {object} @ui5/server/middleware/MiddlewareUtil~ProjectInterface
+ * @property {Function} getType Get the project type
+ * @property {Function} getName Get the project name
+ * @property {Function} getVersion Get the project version
+ * @property {Function} getNamespace Get the project namespace
+ * @property {Function} getRootReader Get the project rootReader
+ * @property {Function} getReader Get the project reader, defaulting to "runtime" style instead of "buildtime"
+ * @property {Function} getRootPath Get the local File System path of the project's root directory
+ * @property {Function} getSourcePath Get the local File System path of the project's source directory
+ * @property {Function} getCustomConfiguration Get the project Custom Configuration
+ * @property {Function} isFrameworkProject Check whether the project is a UI5-Framework project
+ * @property {Function} getFrameworkName Get the project's framework name configuration
+ * @property {Function} getFrameworkVersion Get the project's framework version configuration
+ * @property {Function} getFrameworkDependencies Get the project's framework dependencies configuration
+ */
+
+ /**
+ * Retrieve a single project from the dependency graph
+ *
+ *
+ * This method is only available to custom server middleware extensions defining
+ * Specification Version 3.0 and above.
+ *
+ * @param {string|@ui5/fs/Resource} [projectNameOrResource]
+ * Name of the project to retrieve or a Resource instance to retrieve the associated project for.
+ * Defaults to the name of the current root project
+ * @returns {@ui5/server/middleware/MiddlewareUtil~ProjectInterface|undefined}
+ * Specification Version-dependent interface to the Project instance or undefined
+ * if the project name is unknown or the provided resource is not associated with any project.
+ * @public
+ */
+ getProject(projectNameOrResource) {
+ if (projectNameOrResource) {
+ if (typeof projectNameOrResource === "string" || projectNameOrResource instanceof String) {
+ // A project name has been provided
+ return this._graph.getProject(projectNameOrResource);
+ } else {
+ // A Resource instance has been provided
+ return projectNameOrResource.getProject();
+ }
+ }
+ // No parameter has been provided, default to the root project
+ return this._project;
+ }
+
+ /**
+ * Retrieve a list of direct dependencies of a given project from the dependency graph.
+ * Note that this list does not include transitive dependencies.
+ *
+ *
+ * This method is only available to custom server middleware extensions defining
+ * Specification Version 3.0 and above.
+ *
+ * @param {string} [projectName] Name of the project to retrieve.
+ * Defaults to the name of the current root project
+ * @returns {string[]} Names of all direct dependencies
+ * @throws {Error} If the requested project is unknown to the graph
+ * @public
+ */
+ getDependencies(projectName) {
+ return this._graph.getDependencies(projectName || this._project.getName());
+ }
+
+ /**
+ * Specification Version-dependent set of [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory}
+ * functions provided to middleware.
+ * For details on individual functions, see [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory}
+ *
+ * @public
+ * @typedef {object} @ui5/server/middleware/MiddlewareUtil~resourceFactory
+ * @property {Function} createResource Creates a [Resource]{@link @ui5/fs/Resource}.
+ * Accepts the same parameters as the [Resource]{@link @ui5/fs/Resource} constructor.
+ * @property {Function} createReaderCollection Creates a reader collection:
+ * [ReaderCollection]{@link @ui5/fs/ReaderCollection}
+ * @property {Function} createReaderCollectionPrioritized Creates a prioritized reader collection:
+ * [ReaderCollectionPrioritized]{@link @ui5/fs/ReaderCollectionPrioritized}
+ * @property {Function} createFilterReader
+ * Create a [Filter-Reader]{@link @ui5/fs/readers/Filter} with the given reader.
+ * @property {Function} createLinkReader
+ * Create a [Link-Reader]{@link @ui5/fs/readers/Filter} with the given reader.
+ * @property {Function} createFlatReader Create a [Link-Reader]{@link @ui5/fs/readers/Link}
+ * where all requests are prefixed with /resources/.
+ */
+
+ /**
+ * Provides limited access to [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory} functions
+ *
+ *
+ * This attribute is only available to custom server middleware extensions defining
+ * Specification Version 3.0 and above.
+ *
+ * @type {@ui5/server/middleware/MiddlewareUtil~resourceFactory}
+ * @public
+ */
+ resourceFactory = {
+ createResource,
+ createReaderCollection,
+ createReaderCollectionPrioritized,
+ createFilterReader,
+ createLinkReader,
+ createFlatReader,
+ };
+
+ /**
+ * Get an interface to an instance of this class that only provides those functions
+ * that are supported by the given custom middleware extension specification version.
+ *
+ * @param {@ui5/project/specifications/SpecificationVersion} specVersion
+ * SpecVersionComparator instance of the custom server middleware
+ * @returns {object} An object with bound instance methods supported by the given specification version
+ */
+ getInterface(specVersion) {
+ if (specVersion.lt("2.0")) {
+ // Custom middleware defining specVersion <2.0 does not have access to any MiddlewareUtil API
+ return undefined;
+ }
+
+ const baseInterface = {};
+ bindFunctions(this, baseInterface, [
+ "getPathname", "getMimeInfo"
+ ]);
+
+ if (specVersion.gte("3.0")) {
+ // getProject function, returning an interfaced project instance
+ baseInterface.getProject = (projectName) => {
+ const project = this.getProject(projectName);
+ const baseProjectInterface = {};
+ bindFunctions(project, baseProjectInterface, [
+ "getType", "getName", "getVersion", "getNamespace",
+ "getRootReader", "getRootPath", "getSourcePath",
+ "getCustomConfiguration", "isFrameworkProject", "getFrameworkName",
+ "getFrameworkVersion", "getFrameworkDependencies"
+ ]);
+ // Project#getReader defaults to style "buildtime". However ui5-server uses
+ // style "runtime". The main difference is that for some project types (like applications)
+ // the /resources/{{header}}
" + + "{{result.TestName}} ({{result.Failed}} ,{{result.Passed}} ,{{result.All}})
" + + " Rerun" + + "