diff --git a/.github/workflows/legacy-release_maven-release-process.yml b/.github/workflows/legacy-release_maven-release-process.yml index 977fb8c99da7..13c707647e44 100644 --- a/.github/workflows/legacy-release_maven-release-process.yml +++ b/.github/workflows/legacy-release_maven-release-process.yml @@ -200,8 +200,6 @@ jobs: with: path: ${{ env.DOCKER_BUILD_CONTEXT }}/context key: docker-context-${{ steps.set-common-vars.outputs.date }}-${{ github.run_id }} - restore-keys: | - docker-context-${{ steps.set-common-vars.outputs.date }} if: success() - name: Create Release diff --git a/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.spec.ts b/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.spec.ts index efe898f677c7..d4f32aa873c9 100644 --- a/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.spec.ts @@ -101,7 +101,7 @@ describe('DotHttpErrorManagerService', () => { }); it('should handle 401 error when user is logout and redirect to login', () => { - loginService.auth.user = null; + loginService.auth.user = undefined as any; jest.spyOn(dotDialogService, 'alert'); jest.spyOn(dotRouterService, 'goToLogin'); @@ -166,7 +166,7 @@ describe('DotHttpErrorManagerService', () => { 'error-key': 'dotcms.api.error.license.required' }); - const responseView: HttpErrorResponse = mockResponseView(403, null, headers); + const responseView: HttpErrorResponse = mockResponseView(403, undefined, headers); service.handle(responseView).subscribe((res) => { result = res; @@ -186,7 +186,7 @@ describe('DotHttpErrorManagerService', () => { it('should handle 400 error on message', () => { jest.spyOn(dotDialogService, 'alert'); - const responseView: HttpErrorResponse = mockResponseView(400, null, null, { + const responseView: HttpErrorResponse = mockResponseView(400, undefined, undefined, { message: 'Error' }); @@ -210,7 +210,7 @@ describe('DotHttpErrorManagerService', () => { const CUSTOM_HEADER = 'Custom Header'; const SERVER_MESSAGE = 'Server Error'; - const responseView: HttpErrorResponse = mockResponseView(400, null, null, { + const responseView: HttpErrorResponse = mockResponseView(400, undefined, undefined, { message: SERVER_MESSAGE, header: CUSTOM_HEADER }); @@ -233,7 +233,7 @@ describe('DotHttpErrorManagerService', () => { it('should handle 400 error on errors[0]', () => { jest.spyOn(dotDialogService, 'alert'); - const responseView: HttpErrorResponse = mockResponseView(400, null, null, [ + const responseView: HttpErrorResponse = mockResponseView(400, undefined, undefined, [ { message: 'Server Error' } ]); @@ -255,7 +255,7 @@ describe('DotHttpErrorManagerService', () => { it('should handle 400 error on error.errors[0]', () => { jest.spyOn(dotDialogService, 'alert'); - const responseView: HttpErrorResponse = mockResponseView(400, null, null, { + const responseView: HttpErrorResponse = mockResponseView(400, undefined, undefined, { errors: [{ message: 'Server Error' }] }); @@ -277,7 +277,7 @@ describe('DotHttpErrorManagerService', () => { it('should handle 400 error on error.error', () => { jest.spyOn(dotDialogService, 'alert'); - const responseView: HttpErrorResponse = mockResponseView(400, null, null, { + const responseView: HttpErrorResponse = mockResponseView(400, undefined, undefined, { error: 'Server Error' }); diff --git a/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.ts b/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.ts index d63bd46c136f..089db84fb653 100644 --- a/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.ts +++ b/core-web/libs/data-access/src/lib/dot-http-error-manager/dot-http-error-manager.service.ts @@ -188,15 +188,97 @@ export class DotHttpErrorManagerService { return false; } + /** + * Extracts a readable error message from an HttpErrorResponse + * + * @param response The HttpErrorResponse to extract the message from + * @returns A string containing the error message or empty string if no message found + */ private getErrorMessage(response?: HttpErrorResponse): string { - let msg: string; - if (Array.isArray(response?.['error']) || Array.isArray(response?.error?.errors)) { - msg = response.error[0]?.message || response.error?.errors[0]?.message; - } else { - const error = response?.['error']; - msg = error?.message || error?.error; + if (!response) { + return ''; + } + + const { error } = response; + let errorMessage = ''; + + // Handle array of errors + if (Array.isArray(error) && error.length > 0) { + errorMessage = this.extractMessageFromErrorObject(error[0]); + } + // Handle error object with nested errors array + else if (error?.errors && Array.isArray(error.errors) && error.errors.length > 0) { + errorMessage = this.extractMessageFromErrorObject(error.errors[0]); + } + // Handle direct error object + else if (error && typeof error === 'object') { + errorMessage = this.extractMessageFromErrorObject(error); + } + // Handle string error + else if (error && typeof error === 'string') { + errorMessage = error; + } + + // Try to get localized message if it's a message key + const localizedMessage = this.dotMessageService.get(errorMessage); + + return localizedMessage !== errorMessage ? localizedMessage : errorMessage; + } + + /** + * Extracts message from an error object and trims it if it contains a colon + * + * @param errorObj The error object to extract message from + * @returns The extracted message or empty string + */ + private extractMessageFromErrorObject(errorObj: unknown): string { + if (!errorObj) { + return ''; } - return msg; + // Handle string directly + if (typeof errorObj === 'string') { + return this.formatErrorMessage(errorObj); + } + + // Handle error object + if (typeof errorObj === 'object' && errorObj !== null) { + const errorRecord = errorObj as Record; + + // Try to extract message from common error properties in priority order + const message = + this.getStringProperty(errorRecord, 'message') || + this.getStringProperty(errorRecord, 'error') || + this.getStringProperty(errorRecord, 'detail') || + this.getStringProperty(errorRecord, 'description') || + ''; + + return this.formatErrorMessage(message); + } + + return ''; + } + + /** + * Safely extracts a string property from an object + * + * @param obj The object to extract from + * @param prop The property name to extract + * @returns The string value or empty string + */ + private getStringProperty(obj: Record, prop: string): string { + const value = obj[prop]; + + return typeof value === 'string' ? value : ''; + } + + /** + * Formats an error message by trimming at first colon if present + * + * @param message The message to format + * @returns The formatted message + */ + private formatErrorMessage(message: string): string { + return message.includes(':') ? message.substring(0, message.indexOf(':')) : message; } } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts index 3a308fad54f7..3e25e8e70f63 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts @@ -209,7 +209,15 @@ export function withEditor() { }; }), $iframeURL: computed>(() => { - const sanitizedURL = sanitizeURL(store.pageParams().url); + /* + Here we need to import pageAPIResponse() to create the computed dependency and have it updated every time a response is received from the PageAPI. + This should change in future UVE improvements. + The url should not depend on the PageAPI response since it does not change (In traditional). + In the future we should have a function that updates the content, independent of the url. + More info: https://github.com/dotCMS/core/issues/31475 + */ + const vanityURL = store.pageAPIResponse().vanityUrl?.url; + const sanitizedURL = sanitizeURL(vanityURL ?? store.pageParams().url); const url = buildIframeURL({ url: sanitizedURL, diff --git a/core-web/libs/sdk/client/package.json b/core-web/libs/sdk/client/package.json index 489b46dfe6fa..bcffe937900b 100644 --- a/core-web/libs/sdk/client/package.json +++ b/core-web/libs/sdk/client/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/client", - "version": "0.0.1-alpha.38", + "version": "0.0.1-beta.2", "description": "Official JavaScript library for interacting with DotCMS REST APIs.", "repository": { "type": "git", diff --git a/core-web/libs/sdk/experiments/package.json b/core-web/libs/sdk/experiments/package.json index f69ac4ca5c4a..d52aea93aa20 100644 --- a/core-web/libs/sdk/experiments/package.json +++ b/core-web/libs/sdk/experiments/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/experiments", - "version": "0.0.1-alpha.38", + "version": "0.0.1-beta.2", "description": "Official JavaScript library to use Experiments with DotCMS.", "repository": { "type": "git", diff --git a/core-web/libs/sdk/experiments/tsconfig.json b/core-web/libs/sdk/experiments/tsconfig.json index 8080348b5d24..ea41cbcc76e9 100644 --- a/core-web/libs/sdk/experiments/tsconfig.json +++ b/core-web/libs/sdk/experiments/tsconfig.json @@ -5,7 +5,8 @@ "allowJs": false, "esModuleInterop": false, "allowSyntheticDefaultImports": true, - "strict": true + "strict": true, + "moduleResolution": "bundler" }, "files": [], "include": ["src"], diff --git a/core-web/libs/sdk/react/package.json b/core-web/libs/sdk/react/package.json index 0100ae4dd309..52d4e937fe61 100644 --- a/core-web/libs/sdk/react/package.json +++ b/core-web/libs/sdk/react/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/react", - "version": "0.0.1-alpha.38", + "version": "0.0.1-beta-2", "peerDependencies": { "react": ">=18", "react-dom": ">=18", @@ -22,6 +22,17 @@ "React", "Components" ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./next": "./src/next.ts" + }, + "typesVersions": { + "*": { + ".": ["./src/index.d.ts"], + "next": ["./src/next.d.ts"] + } + }, "author": "dotcms ", "license": "MIT", "bugs": { diff --git a/core-web/libs/sdk/react/project.json b/core-web/libs/sdk/react/project.json index 0215c1916211..c75a4588e231 100644 --- a/core-web/libs/sdk/react/project.json +++ b/core-web/libs/sdk/react/project.json @@ -16,16 +16,18 @@ "executor": "@nx/rollup:rollup", "outputs": ["{options.outputPath}"], "options": { + "main": "libs/sdk/react/src/index.ts", + "additionalEntryPoints": ["libs/sdk/react/src/next.ts"], + "generateExportsField": true, "outputPath": "dist/libs/sdk/react", "tsConfig": "libs/sdk/react/tsconfig.lib.json", "project": "libs/sdk/react/package.json", "entryFile": "libs/sdk/react/src/index.ts", - "additionalEntryPoints": ["libs/sdk/react/src/next.ts"], "external": ["react/jsx-runtime"], "rollupConfig": "@nrwl/react/plugins/bundle-rollup", "compiler": "babel", - "extractCss": false, "format": ["esm"], + "extractCss": false, "assets": [ { "glob": "libs/sdk/react/README.md", diff --git a/core-web/libs/sdk/uve/package.json b/core-web/libs/sdk/uve/package.json index 49bec22614f7..f1618b85962c 100644 --- a/core-web/libs/sdk/uve/package.json +++ b/core-web/libs/sdk/uve/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/uve", - "version": "0.0.1", + "version": "0.0.1-beta.2", "description": "Official JavaScript library for interacting with Universal Visual Editor (UVE)", "repository": { "type": "git", @@ -13,10 +13,21 @@ "UVE", "Universal Visual Editor" ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./types": "./src/types.ts" + }, + "typesVersions": { + "*": { + ".": ["./src/index.d.ts"], + "types": ["./src/types.d.ts"] + } + }, "author": "dotcms ", "license": "MIT", "bugs": { "url": "https://github.com/dotCMS/core/issues" }, - "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/client/README.md" + "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/uve/README.md" } diff --git a/core-web/libs/sdk/uve/project.json b/core-web/libs/sdk/uve/project.json index 909f4ee9b7fb..f07c09fe84d5 100644 --- a/core-web/libs/sdk/uve/project.json +++ b/core-web/libs/sdk/uve/project.json @@ -24,14 +24,17 @@ "executor": "@nx/rollup:rollup", "outputs": ["{options.outputPath}"], "options": { - "format": ["esm", "cjs"], - "compiler": "tsc", + "main": "libs/sdk/uve/src/index.ts", + "additionalEntryPoints": ["libs/sdk/uve/src/types.ts"], "generateExportsField": true, - "assets": [{ "input": "libs/sdk/uve", "output": ".", "glob": "*.md" }], "outputPath": "dist/libs/sdk/uve", - "main": "libs/sdk/uve/src/public/index.ts", - "additionalEntryPoints": ["libs/sdk/uve/src/public/types.ts"], - "tsConfig": "libs/sdk/uve/tsconfig.lib.json" + "tsConfig": "libs/sdk/uve/tsconfig.lib.json", + "project": "libs/sdk/uve/package.json", + "entryFile": "libs/sdk/uve/src/index.ts", + "compiler": "babel", + "format": ["esm", "cjs"], + "extractCss": false, + "assets": [{ "input": "libs/sdk/uve", "output": ".", "glob": "*.md" }] } } } diff --git a/core-web/libs/sdk/uve/src/index.ts b/core-web/libs/sdk/uve/src/index.ts new file mode 100644 index 000000000000..5c1a90d83cc9 --- /dev/null +++ b/core-web/libs/sdk/uve/src/index.ts @@ -0,0 +1,3 @@ +import { getUVEState } from './lib/utils'; + +export { getUVEState }; diff --git a/core-web/libs/sdk/uve/src/public/index.ts b/core-web/libs/sdk/uve/src/public/index.ts deleted file mode 100644 index ac1855b29745..000000000000 --- a/core-web/libs/sdk/uve/src/public/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getUVEState } from '../lib/utils'; - -export { getUVEState }; diff --git a/core-web/libs/sdk/uve/src/public/types.ts b/core-web/libs/sdk/uve/src/public/types.ts deleted file mode 100644 index cad85c548d1a..000000000000 --- a/core-web/libs/sdk/uve/src/public/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { UVE_MODE, UVEState } from '../lib/types'; - -export { UVE_MODE, UVEState }; diff --git a/core-web/libs/sdk/uve/src/types.ts b/core-web/libs/sdk/uve/src/types.ts new file mode 100644 index 000000000000..bf3100e9583b --- /dev/null +++ b/core-web/libs/sdk/uve/src/types.ts @@ -0,0 +1,3 @@ +import { UVE_MODE, UVEState } from './lib/types'; + +export { UVE_MODE, UVEState }; diff --git a/core-web/tsconfig.base.json b/core-web/tsconfig.base.json index 4721cbd7b516..ca7a5b1d1a93 100644 --- a/core-web/tsconfig.base.json +++ b/core-web/tsconfig.base.json @@ -63,8 +63,8 @@ "@dotcms/utils": ["libs/utils/src"], "@dotcms/utils-testing": ["libs/utils-testing/src/index.ts"], "@dotcms/utils/*": ["libs/utils/src/*"], - "@dotcms/uve": ["libs/sdk/uve/src/public/index.ts"], - "@dotcms/uve/types": ["libs/sdk/uve/src/public/types.ts"], + "@dotcms/uve": ["libs/sdk/uve/src/index.ts"], + "@dotcms/uve/types": ["libs/sdk/uve/src/types.ts"], "@models/*": ["apps/dotcms-ui/src/app/shared/models/*"], "@pipes/*": ["apps/dotcms-ui/src/app/view/pipes/*"], "@portlets/*": ["apps/dotcms-ui/src/app/portlets/*"], diff --git a/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql b/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql index 419a5a60ce07..1802cd6c964d 100644 --- a/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql +++ b/docker/docker-compose-examples/analytics/setup/db/clickhouse/init-scripts/init.sql @@ -4,6 +4,10 @@ CREATE TABLE IF NOT EXISTS clickhouse_test_db.events _timestamp DateTime, api_key String, cluster_id String, + customer_name String, + customer_category String, + environment_name String, + environment_version String, customer_id String, doc_encoding String, doc_host String, @@ -110,3 +114,9 @@ ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS object_detail_page_u ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS object_url; ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS object_forward_to; ALTER TABLE clickhouse_test_db.events DROP COLUMN IF EXISTS comefromvanityurl; + + +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS customer_name String; +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS customer_category String; +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS environment_name String; +ALTER TABLE clickhouse_test_db.events ADD COLUMN IF NOT EXISTS environment_version String; \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java index 457fd13ce30e..077584e631da 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java @@ -66,6 +66,7 @@ public AnalyticsAPIImpl() { @Override public void notify(final SystemTableUpdatedKeyEvent event) { + Logger.info(this, String.format("Received event with key [%s]", event.getKey())); if (event.getKey().contains(ANALYTICS_IDP_URL_KEY)) { analyticsIdpUrl.set(resolveAnalyticsIdpUrl()); } else if (event.getKey().contains(ANALYTICS_ACCESS_TOKEN_RENEW_TIMEOUT_KEY)) { @@ -436,5 +437,4 @@ private CircuitBreakerUrl.Response requestAnalyticsKey(final Analy private Map analyticsKeyHeaders(final AccessToken accessToken) throws AnalyticsException { return CircuitBreakerUrl.authHeaders(AnalyticsHelper.get().formatBearer(accessToken)); } - } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java index 6057c93b9221..b9d25248479a 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/BasicProfileCollector.java @@ -1,13 +1,15 @@ package com.dotcms.analytics.track.collectors; import com.dotcms.enterprise.cluster.ClusterFactory; +import com.dotcms.exception.ExceptionUtil; +import com.dotcms.telemetry.business.MetricsAPI; import com.dotcms.util.FunctionUtils; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.web.WebAPILocator; -import com.dotmarketing.util.PageMode; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import com.liferay.portal.model.User; -import com.liferay.util.StringPool; import javax.servlet.http.HttpServletRequest; import java.time.Instant; @@ -15,12 +17,14 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; -import java.util.Map; import java.util.Objects; /** - * Collects the basic profile information for a collector payload bean. + * Collects the basic profile information for a collector payload bean. It's worth noting that + * ALLALL data collectors will include the information added by this one. + * * @author jsanca + * @since Sep 17th, 2024 */ public class BasicProfileCollector implements Collector { @@ -56,6 +60,8 @@ public CollectorPayloadBean collect(final CollectorContextMap collectorContextMa collectorPayloadBean.put(SESSION_ID, sessionId); collectorPayloadBean.put(SESSION_NEW, sessionNew); + this.setCustomerTelemetryData(collectorPayloadBean); + if (UtilMethods.isSet(collectorContextMap.get(CollectorContextMap.REFERER))) { collectorPayloadBean.put(REFERER, collectorContextMap.get(CollectorContextMap.REFERER).toString()); } @@ -83,13 +89,34 @@ public CollectorPayloadBean collect(final CollectorContextMap collectorContextMa return collectorPayloadBean; } + /** + * Sets the customer Telemetry data as part of the information that will be persisted to the + * Content Analytics database. + * + * @param collectorPayloadBean The {@link CollectorPayloadBean} that will be persisted to the + * Content Analytics database. + */ + private void setCustomerTelemetryData(final CollectorPayloadBean collectorPayloadBean) { + final MetricsAPI metricsAPI = APILocator.getMetricsAPI(); + try { + final MetricsAPI.Client client = metricsAPI.getClient(); + collectorPayloadBean.put(CUSTOMER_NAME, client.getClientName()); + collectorPayloadBean.put(CUSTOMER_CATEGORY, client.getCategory()); + collectorPayloadBean.put(ENVIRONMENT_NAME, client.getEnvironment()); + collectorPayloadBean.put(ENVIRONMENT_VERSION, client.getVersion()); + } catch (final DotDataException e) { + Logger.warnAndDebug(BasicProfileCollector.class, String.format("Failed to retrieve customer Telemetry data: " + + "%s", ExceptionUtil.getErrorMessage(e)), e); + } + } + private void setUserInfo(final HttpServletRequest request, final CollectorPayloadBean collectorPayloadBean) { final User user = WebAPILocator.getUserWebAPI().getUser(request); if (Objects.nonNull(user)) { final HashMap userObject = new HashMap<>(); - userObject.put(ID, user.getUserId().toString()); + userObject.put(ID, user.getUserId()); userObject.put(EMAIL, user.getEmailAddress()); collectorPayloadBean.put(USER_OBJECT, userObject); } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java index f77ed70d7aed..0e783e1cf5bf 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/Collector.java @@ -47,11 +47,9 @@ public interface Collector { String USER_AGENT = "userAgent"; String UTC_TIME = "utc_time"; - String RESPONSE = "action"; String RESPONSE_CODE = "response_code"; - String DETAIL_PAGE_URL = "detail_page_url"; String IS_EXPERIMENT_PAGE = "isexperimentpage"; String IS_TARGET_PAGE = "istargetpage"; @@ -61,6 +59,12 @@ public interface Collector { String EMAIL = "email"; String USER_OBJECT = "user"; + + String CUSTOMER_NAME = "customer_name"; + String CUSTOMER_CATEGORY = "customer_category"; + String ENVIRONMENT_NAME = "environment_name"; + String ENVIRONMENT_VERSION = "environment_version"; + /** * Test if the collector should run * @param collectorContextMap diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/FilesCollector.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/FilesCollector.java index e92872967ee0..a7f4436d20a5 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/FilesCollector.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/FilesCollector.java @@ -8,6 +8,7 @@ import com.dotmarketing.portlets.contentlet.business.ContentletAPI; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.fileassets.business.FileAssetAPI; +import com.dotmarketing.util.PageMode; import com.liferay.util.StringPool; import io.vavr.control.Try; @@ -85,9 +86,8 @@ protected Optional getFileAsset(String uri, Host host, Long language final String actualUri = uri.substring(0, uri.lastIndexOf('.')) + ".scss"; return Optional.ofNullable(this.fileAssetAPI.getFileByPath(actualUri, host, languageId, true)); } else if (uri.startsWith("/dA") || uri.startsWith("/contentAsset") || uri.startsWith("/dotAsset")) { - final String[] split = uri.split(StringPool.FORWARD_SLASH); - final String id = uri.startsWith("/contentAsset") ? split[3] : split[2]; - return getFileAsset(languageId, id); + final FieldNameIdentifier fieldNameIdentifier = getIdentifierAndFieldName(uri); + return getFileAsset(languageId, fieldNameIdentifier); } else { return Optional.ofNullable(this.fileAssetAPI.getFileByPath(uri, host, languageId, true)); } @@ -96,10 +96,32 @@ protected Optional getFileAsset(String uri, Host host, Long language } } - private Optional getFileAsset(final Long languageId, final String id) throws DotDataException, DotSecurityException { + private static FieldNameIdentifier getIdentifierAndFieldName(String uri) { + final String[] split = uri.split(StringPool.FORWARD_SLASH); - return Optional.ofNullable(contentletAPI.findContentletByIdentifier(id, true, languageId, - APILocator.systemUser(), false)); + final int idIndex = uri.startsWith("/contentAsset") ? 3 : 2; + final int fieldNameIndex = uri.startsWith("/contentAsset") ? 4 : 3; + + return new FieldNameIdentifier(split[idIndex], + fieldNameIndex < split.length || !uri.startsWith("/dotAsset")? split[fieldNameIndex] : null); + } + + private Optional getFileAsset(final Long languageId, final FieldNameIdentifier fieldNameIdentifier) + throws DotDataException, DotSecurityException { + + final Contentlet contentletByIdentifier = contentletAPI.findContentletByIdentifier(fieldNameIdentifier.identifier, + PageMode.get().showLive, languageId, + APILocator.systemUser(), false); + + if (Objects.nonNull(fieldNameIdentifier.fieldName)) { + final String binaryFileId = contentletByIdentifier.getStringProperty(fieldNameIdentifier.fieldName); + + return Optional.ofNullable(contentletAPI.findContentletByIdentifier(binaryFileId, + PageMode.get().showLive, languageId, + APILocator.systemUser(), false)); + } else { + return Optional.ofNullable(contentletByIdentifier); + } } @Override @@ -107,4 +129,14 @@ public boolean isAsync() { return true; } + private static class FieldNameIdentifier { + final String fieldName; + final String identifier; + + FieldNameIdentifier(final String identifier, final String fieldName) { + this.fieldName = fieldName; + this.identifier = identifier; + } + } + } diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtils.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtils.java index 262c771f978f..c0893d3aee00 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtils.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtils.java @@ -112,54 +112,69 @@ private static Contentlet fixRecurringDates(Contentlet contentlet, String[] recD * * @return The requested {@link Contentlet} object. */ - public static Contentlet find(final String inodeOrIdentifierIn, final User user, final String tmDate, final boolean EDIT_OR_PREVIEW_MODE, - final long sessionLang) { - final String inodeOrIdentifier = RecurrenceUtil.getBaseEventIdentifier(inodeOrIdentifierIn); - final String[] recDates = RecurrenceUtil.getRecurrenceDates(inodeOrIdentifier); - try { - // by inode - Contentlet contentlet = conAPI.find(inodeOrIdentifier, user, true); - if (contentlet != null) { - return fixRecurringDates(contentlet, recDates); - } - - // time-machine - if (tmDate != null) { - // This should take care of the rendering bits for the time machine - final Date ffdate = new Date(Long.parseLong(tmDate)); - final PageMode pageMode = PageMode.get(); + public static Contentlet find(final String inodeOrIdentifierIn, final User user, + final String tmDate, final boolean EDIT_OR_PREVIEW_MODE, + final long sessionLang) { + final String inodeOrIdentifier = RecurrenceUtil.getBaseEventIdentifier(inodeOrIdentifierIn); + final String[] recDates = RecurrenceUtil.getRecurrenceDates(inodeOrIdentifier); + try { + final PageMode pageMode = PageMode.get(); + // Test by inode + Contentlet contentlet = conAPI.find(inodeOrIdentifier, user, true); + if (contentlet != null) { // Time machine by the identifier extracted from the contentlet we found through the inode + if (null != tmDate) { + final Date ffdate = new Date(Long.parseLong(tmDate)); final Optional futureContent = conAPI.findContentletByIdentifierOrFallback( - inodeOrIdentifier, sessionLang, VariantAPI.DEFAULT_VARIANT.name(), - ffdate, user, pageMode.respectAnonPerms); - if (futureContent.isPresent()) { - return futureContent.get(); - } - // If the content is not found or has expired - // No need to return null we continue to the next step to try to find the content in the live or working version - } + contentlet.getIdentifier(), sessionLang, + VariantAPI.DEFAULT_VARIANT.name(), + ffdate, user, pageMode.respectAnonPerms); + if (futureContent.isPresent()) { + return fixRecurringDates(futureContent.get(), recDates); + } + } + } - final ContentletVersionInfo contentletVersionInfoByFallback = WebAPILocator.getVariantWebAPI() - .getContentletVersionInfoByFallback(sessionLang, inodeOrIdentifier, - EDIT_OR_PREVIEW_MODE ? PageMode.PREVIEW_MODE : PageMode.LIVE, user); - // If content is being viewed in EDIT_OR_PREVIEW_MODE, we need to get the working version. Otherwise, we - // need the live version. That's why we're negating it when calling the API - final String contentletInode = - EDIT_OR_PREVIEW_MODE ? contentletVersionInfoByFallback.getWorkingInode() - : contentletVersionInfoByFallback.getLiveInode(); - - contentlet = conAPI.find(contentletInode, user, true); - return fixRecurringDates(contentlet, recDates); - } catch (final Exception e) { - String msg = e.getMessage(); - msg = (msg.contains("\n")) ? msg.substring(0, msg.indexOf("\n")) : msg; - final String errorMsg = String.format("An error occurred when User '%s' attempted to find Contentlet " + - "with Inode/ID '%s' [lang=%s, tmDate=%s]: %s", - user.getUserId(), inodeOrIdentifier, sessionLang, tmDate, msg); - Logger.warn(ContentUtils.class, errorMsg); - Logger.debug(ContentUtils.class, errorMsg, e); - return null; - } - } + // okay if we failed retrieving the contentlet using inode we still need to test by identifier Cuz this method actually takes an identifier + if (null != tmDate) { + final Date ffdate = new Date(Long.parseLong(tmDate)); + final Optional futureContent = conAPI.findContentletByIdentifierOrFallback( + inodeOrIdentifier, sessionLang, VariantAPI.DEFAULT_VARIANT.name(), + ffdate, user, pageMode.respectAnonPerms); + if (futureContent.isPresent()) { + return fixRecurringDates(futureContent.get(), recDates); + } + } + + // if We ran out of possibilities retrieving the contentlet via time machine we fall back on the current or present live contentlet + // if we already had one found by inode this is the one We should return + if (null != contentlet) { + return fixRecurringDates(contentlet, recDates); + } + // Fallbacks from here on... + //If not we try to get the contentlet by the identifier + final ContentletVersionInfo contentletVersionInfoByFallback = WebAPILocator.getVariantWebAPI() + .getContentletVersionInfoByFallback(sessionLang, inodeOrIdentifier, + EDIT_OR_PREVIEW_MODE ? PageMode.PREVIEW_MODE : PageMode.LIVE, user); + // If content is being viewed in EDIT_OR_PREVIEW_MODE, we need to get the working version. Otherwise, we + // need the live version. That's why we're negating it when calling the API + final String contentletInode = + EDIT_OR_PREVIEW_MODE ? contentletVersionInfoByFallback.getWorkingInode() + : contentletVersionInfoByFallback.getLiveInode(); + + contentlet = conAPI.find(contentletInode, user, true); + return fixRecurringDates(contentlet, recDates); + } catch (final Exception e) { + String msg = e.getMessage(); + msg = (msg.contains("\n")) ? msg.substring(0, msg.indexOf("\n")) : msg; + final String errorMsg = String.format( + "An error occurred when User '%s' attempted to find Contentlet " + + "with Inode/ID '%s' [lang=%s, tmDate=%s]: %s", + user.getUserId(), inodeOrIdentifier, sessionLang, tmDate, msg); + Logger.warn(ContentUtils.class, errorMsg); + Logger.debug(ContentUtils.class, errorMsg, e); + return null; + } + } /** * Returns empty List if no results are found diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java index f2b04c7c9f3f..1f21bf63c865 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java @@ -26,6 +26,7 @@ import com.dotcms.util.ConversionUtils; import com.dotcms.util.HttpRequestDataUtil; import com.dotcms.util.PaginationUtil; +import com.dotcms.util.TimeMachineUtil; import com.dotcms.util.pagination.ContentTypesPaginator; import com.dotcms.util.pagination.OrderDirection; import com.dotcms.vanityurl.business.VanityUrlAPI; @@ -61,12 +62,7 @@ import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.templates.model.Template; import com.dotmarketing.portlets.workflows.model.WorkflowAction; -import com.dotmarketing.util.DateUtil; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.PageMode; -import com.dotmarketing.util.StringUtils; -import com.dotmarketing.util.UtilMethods; -import com.dotmarketing.util.WebKeys; +import com.dotmarketing.util.*; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -142,6 +138,7 @@ public class PageResource { public static final String TM_LANG = "tm_lang"; public static final String TM_HOST = "tm_host"; public static final String DOT_CACHE = "dotcache"; + public static final String IS_PAGE_RESOURCE = "pageResource"; private final PageResourceHelper pageResourceHelper; private final WebResource webResource; @@ -355,19 +352,12 @@ private PageRenderParams optionalRenderParams(final String modeParam, if (null != deviceInode){ builder.deviceInode(deviceInode); } - if (null != timeMachineDateAsISO8601) { - final Date date; - try { - date = Try.of(() -> DateUtil.convertDate(timeMachineDateAsISO8601)).getOrElseThrow( - e -> new IllegalArgumentException( - String.format("Error Parsing date: %s", timeMachineDateAsISO8601), - e)); - } catch (IllegalArgumentException e) { - throw new RuntimeException(e); - } - final Instant instant = date.toInstant(); - builder.timeMachineDate(instant); - } + TimeMachineUtil.parseTimeMachineDate(timeMachineDateAsISO8601).ifPresentOrElse( + builder::timeMachineDate, + () -> Logger.debug(this, () -> String.format( + "Date %s is not older than the grace window. Skipping Time Machine setup.", + timeMachineDateAsISO8601)) + ); return builder.build(); } @@ -492,12 +482,13 @@ private void setUpTimeMachineIfPresent(final PageRenderParams renderParams) { session.setAttribute(TM_LANG, renderParams.languageId()); session.setAttribute(DOT_CACHE, "refresh"); session.setAttribute(TM_HOST, host.get()); - } else { + session.setAttribute(IS_PAGE_RESOURCE, true); + } request.setAttribute(TM_DATE, timeMachineEpochMillis); request.setAttribute(TM_LANG, renderParams.languageId()); request.setAttribute(DOT_CACHE, "refresh"); request.setAttribute(TM_HOST, host.get()); - } + request.setAttribute(IS_PAGE_RESOURCE, true); } } @@ -512,6 +503,8 @@ private void resetTimeMachineIfPresent(final HttpServletRequest request) { session.removeAttribute(TM_LANG); session.removeAttribute(TM_HOST); session.removeAttribute(DOT_CACHE); + // we do not remove the IS_PAGE_RESOURCE attribute + //It'll get removed from the old from the time machine portal } } diff --git a/dotCMS/src/main/java/com/dotcms/timemachine/ajax/TimeMachineAjaxAction.java b/dotCMS/src/main/java/com/dotcms/timemachine/ajax/TimeMachineAjaxAction.java index 06d9c0655b44..4ce0ceceffda 100644 --- a/dotCMS/src/main/java/com/dotcms/timemachine/ajax/TimeMachineAjaxAction.java +++ b/dotCMS/src/main/java/com/dotcms/timemachine/ajax/TimeMachineAjaxAction.java @@ -8,6 +8,7 @@ import com.dotcms.notifications.business.NotificationAPI; import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting; import com.dotcms.rest.WebResource; +import com.dotcms.rest.api.v1.page.PageResource; import com.dotcms.util.I18NMessage; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; @@ -42,6 +43,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; public class TimeMachineAjaxAction extends IndexAjaxAction { @@ -66,6 +68,12 @@ public TimeMachineAjaxAction(final NotificationAPI notificationAPI, ESIndexHelpe @Override public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + final HttpSession session = request.getSession(false); + if(null != session){ + //Clean up attribute so TimeMachineFilter can operate + session.removeAttribute(PageResource.IS_PAGE_RESOURCE); + } + Map map = getURIParams(); String cmd = map.get("cmd"); java.lang.reflect.Method meth = null; diff --git a/dotCMS/src/main/java/com/dotcms/util/TimeMachineUtil.java b/dotCMS/src/main/java/com/dotcms/util/TimeMachineUtil.java index 202f7bd655be..32adf22695fe 100644 --- a/dotCMS/src/main/java/com/dotcms/util/TimeMachineUtil.java +++ b/dotCMS/src/main/java/com/dotcms/util/TimeMachineUtil.java @@ -1,16 +1,24 @@ package com.dotcms.util; import com.dotcms.api.web.HttpServletRequestThreadLocal; - import com.dotcms.rest.api.v1.page.PageResource; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.DateUtil; +import io.vavr.Lazy; +import io.vavr.control.Try; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Objects; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import java.util.Optional; public final class TimeMachineUtil { private TimeMachineUtil(){} - + private static final Lazy FTM_GRACE_WINDOW_LIMIT = + Lazy.of(() -> Config.getIntProperty("FTM_GRACE_WINDOW_LIMIT", 5)); /** * If Time Machine is running return the timestamp of the Time Machine date * Running Time Machine is determined by the presence of the attribute PageResource.TM_DATE in the request or session @@ -30,6 +38,19 @@ public static Optional getTimeMachineDate() { return Optional.ofNullable(timeMachineObject != null ? timeMachineObject.toString() : null); } + /** + * If Time Machine is running return the timestamp of the Time Machine date as a Date + * @return Optional + */ + public static Optional getTimeMachineDateAsDate() { + final Optional timeMachine = getTimeMachineDate(); + if (timeMachine.isPresent()) { + final String tmDate = timeMachine.get(); + return Optional.of(new Date(Long.parseLong(tmDate))); + } + return Optional.empty(); + } + /** * Return true if Time Machine is running, otherwise return false * @return boolean @@ -46,4 +67,40 @@ public static boolean isRunning(){ public static boolean isNotRunning(){ return !isRunning(); } + + /** + * Parses and validates the given date string in ISO 8601 format. + * + * @param dateAsISO8601 The date string in ISO 8601 format. If null, an empty {@link Optional} is returned. + * @return An {@link Optional} containing a valid {@link Instant} if parsing is successful and the date meets the validation criteria. + * Returns an empty {@link Optional} if the date is invalid or does not meet the validation criteria. + * @throws IllegalArgumentException If the date string cannot be parsed. + */ + public static Optional parseTimeMachineDate(final String dateAsISO8601) { + if (Objects.isNull(dateAsISO8601)) { + return Optional.empty(); + } + Instant instant = Try.of(() -> DateUtil.convertDate(dateAsISO8601)) + .map(Date::toInstant) + .getOrElseThrow(e -> + new IllegalArgumentException( + String.format("Error Parsing date: %s", dateAsISO8601), e) + ); + return isOlderThanGraceWindow(instant) ? Optional.of(instant) : Optional.empty(); + } + + + /** + * Determines if the FTM logic should be applied based on the given timeMachineDate. + * It checks if the date is older than the grace window (not too recent), + * using a configurable time limit. + * + * @param timeMachineDate The Time Machine date from the request. + * @return true if the timeMachineDate is older than the grace window, meaning FTM logic should be applied, + * false otherwise (if within the grace window). + */ + public static boolean isOlderThanGraceWindow(final Instant timeMachineDate) { + final Instant graceWindowTime = Instant.now().plus(Duration.ofMinutes(FTM_GRACE_WINDOW_LIMIT.get())); + return timeMachineDate.isAfter(graceWindowTime); + } } diff --git a/dotCMS/src/main/java/com/dotmarketing/filters/CMSUrlUtil.java b/dotCMS/src/main/java/com/dotmarketing/filters/CMSUrlUtil.java index 884019a17d49..15fbedef483c 100644 --- a/dotCMS/src/main/java/com/dotmarketing/filters/CMSUrlUtil.java +++ b/dotCMS/src/main/java/com/dotmarketing/filters/CMSUrlUtil.java @@ -655,4 +655,14 @@ public String getInodeFromUrlPath(final String urlPath) { return urlPath.substring(urlPath.indexOf(FORWARD_SLASH) + 1, urlPath.indexOf(UNDERLINE)); } + /** + * Test the request to see if it is a dotAdmin request + * @param request the request to test + * @return true if the request is a dotAdmin request + */ + public static boolean isDotAdminRequest(HttpServletRequest request) { + final String referer = request.getHeader("referer"); + return referer != null && referer.contains("/dotAdmin"); + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/filters/TimeMachineFilter.java b/dotCMS/src/main/java/com/dotmarketing/filters/TimeMachineFilter.java index be3410806542..044f5689fa6f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/filters/TimeMachineFilter.java +++ b/dotCMS/src/main/java/com/dotmarketing/filters/TimeMachineFilter.java @@ -1,6 +1,9 @@ package com.dotmarketing.filters; +import static com.dotmarketing.filters.CMSUrlUtil.isDotAdminRequest; + +import com.dotcms.rest.api.v1.page.PageResource; import javax.ws.rs.core.MediaType; import org.apache.commons.io.IOUtils; import com.dotmarketing.beans.Host; @@ -45,6 +48,7 @@ * @since May 31, 2012 * */ +@Deprecated(since = "250221", forRemoval = true) public class TimeMachineFilter implements Filter { ServletContext ctx; @@ -67,10 +71,18 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha chain.doFilter(request, response); return; } + + // If there's a session attribute that indicates that the request is a page resource, then skip the filter + // This is a temporary fix to avoid the filter to be executed when the request is a page resource until we retire the old + if(null != req.getSession().getAttribute(PageResource.IS_PAGE_RESOURCE)){ + chain.doFilter(request, response); + return; + } + if(!uri.startsWith("/")) { uri="/"+uri; } - if(uri != null && uri.startsWith("/admin") && req.getSession().getAttribute("tm_date")!=null){ + if(uri.startsWith("/admin") && req.getSession().getAttribute("tm_date") != null){ req.getSession().removeAttribute(TM_DATE_VAR); req.getSession().removeAttribute(TM_LANG_VAR); req.getSession().removeAttribute(TM_HOST_VAR); diff --git a/dotCMS/src/main/java/com/dotmarketing/servlets/BinaryExporterServlet.java b/dotCMS/src/main/java/com/dotmarketing/servlets/BinaryExporterServlet.java index 802cbfebd1d6..c9a5f3e62234 100644 --- a/dotCMS/src/main/java/com/dotmarketing/servlets/BinaryExporterServlet.java +++ b/dotCMS/src/main/java/com/dotmarketing/servlets/BinaryExporterServlet.java @@ -1,5 +1,6 @@ package com.dotmarketing.servlets; +import static com.dotmarketing.filters.CMSUrlUtil.isDotAdminRequest; import static com.dotmarketing.image.focalpoint.FocalPointAPIImpl.TMP; import static com.liferay.util.HttpHeaders.CACHE_CONTROL; import static com.liferay.util.HttpHeaders.EXPIRES; @@ -684,16 +685,6 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws Servl } - /** - * Test the request to see if it is a dotAdmin request - * @param request the request to test - * @return true if the request is a dotAdmin request - */ - public static boolean isDotAdminRequest(HttpServletRequest request) { - final String referer = request.getHeader("referer"); - return referer != null && referer.contains("/dotAdmin"); - } - private Contentlet getContentletByIdentifier(final PageMode pageMode, final String identifier, final long languageId, final User user) throws DotDataException, DotSecurityException { diff --git a/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java b/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java index d6fbc17d1b5e..8364a07a28d0 100644 --- a/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java +++ b/dotCMS/src/main/java/org/apache/velocity/tools/generic/DateTool.java @@ -20,6 +20,7 @@ import java.lang.reflect.Field; +import java.sql.Timestamp; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -828,6 +829,81 @@ public Date toDate(String format, Object obj, } } + //////////// + + /** + * Converts an object to an instance of {@link Timestamp} using the + * format returned by {@link #getFormat()},the {@link Locale} returned + * by {@link #getLocale()}, and the {@link TimeZone} returned by + * {@link #getTimeZone()} if the object is not already an instance + * of Date, Calendar, or Long. + * + * @param obj the date to convert + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + */ + public Timestamp toTimestamp(final Object obj) + { + return new Timestamp(toDate(obj).getTime()); + } + + /** + * Converts an object to an instance of {@link Timestamp} using the + * specified format,the {@link Locale} returned by + * {@link #getLocale()}, and the {@link TimeZone} returned by + * {@link #getTimeZone()} if the object is not already an instance + * of Date, Calendar, or Long. + * + * @param format - the format the date is in + * @param obj - the date to convert + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + * @see #toDate(String format, Object obj, Locale locale) + */ + public Timestamp toTimestamp(final String format, final Object obj) + { + return new Timestamp(toDate(format, obj).getTime()); + } + + /** + * Converts an object to an instance of {@link Timestamp} using the + * specified format and {@link Locale} if the object is not already + * an instance of Date, Calendar, or Long. + * + * @param format - the format the date is in + * @param obj - the date to convert + * @param locale - the {@link Locale} + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + * @see SimpleDateFormat#parse + */ + public Timestamp toTimestamp(final String format, final Object obj, final Locale locale) + { + return new Timestamp(toDate(format, obj, locale).getTime()); + } + + /** + * Converts an object to an instance of {@link Timestamp} using the + * specified format, {@link Locale}, and {@link TimeZone} if the + * object is not already an instance of Date, Calendar, or Long. + * + * @param format - the format the date is in + * @param obj - the date to convert + * @param locale - the {@link Locale} + * @param timezone - the {@link TimeZone} + * @return the object as a {@link Timestamp} or null if no + * conversion is possible + * @see #getDateFormat + * @see SimpleDateFormat#parse + */ + public Timestamp toTimestamp(final String format, final Object obj, + final Locale locale, final TimeZone timezone) + { + return new Timestamp(toDate(format, obj, locale, timezone).getTime()); + } + + /////////// + /** * Converts an object to an instance of {@link Calendar} using the * locale returned by {@link #getLocale()} if necessary. diff --git a/dotCMS/src/test/java/com/dotmarketing/filters/CMSUrlUtilTest.java b/dotCMS/src/test/java/com/dotmarketing/filters/CMSUrlUtilTest.java index 474a8e8b6261..399817e88024 100644 --- a/dotCMS/src/test/java/com/dotmarketing/filters/CMSUrlUtilTest.java +++ b/dotCMS/src/test/java/com/dotmarketing/filters/CMSUrlUtilTest.java @@ -4,9 +4,12 @@ import javax.servlet.http.HttpServletRequest; +import static com.dotmarketing.filters.CMSUrlUtil.isDotAdminRequest; import static com.dotmarketing.filters.Constants.CMS_FILTER_URI_OVERRIDE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -75,4 +78,37 @@ public void test_getIdentifierFromUrlPath() { } + /** + * Given scenario: Test the request comes from dotAdmin + * Expected result: Should return true if the referer is a valid dotAdmin referer + */ + @Test + public void testDotAdminRequestValidReferer() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("referer")).thenReturn("http://localhost:8080/dotAdmin/somepage"); + assertTrue( "Should be true for valid dotAdmin referer", isDotAdminRequest(request)); + } + + /** + * Given scenario: Test the request comes from dotAdmin + * Expected result: Should return true if the referer is a valid dotAdmin referer + */ + @Test + public void testDotAdminRequestWithDifferentDomain() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("referer")).thenReturn("http://otherdomain.com/dotAdmin/somepage"); + assertTrue( "Should be true for valid dotAdmin referer", isDotAdminRequest(request)); + } + + /** + * Given scenario: Test the request comes from dotAdmin + * Expected result: Should return true if the referer is a valid dotAdmin referer + */ + @Test + public void testDotAdminRequestWithoutDotAdmin() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("referer")).thenReturn("http://localhost:8080/anotherPath/somepage"); + assertFalse("Should be false if /dotAdmin is not present", isDotAdminRequest(request)); + } + } diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java index 4bd59f77771e..197f0f26b900 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java @@ -101,7 +101,25 @@ Task240306MigrateLegacyLanguageVariablesTest.class, EmailActionletTest.class, OpenAIGenerateImageActionletTest.class, - RequestMatcherTest.class + RequestMatcherTest.class, + com.dotmarketing.portlets.rules.conditionlet.ConditionletOSGIFTest.class, + com.dotmarketing.portlets.rules.conditionlet.CurrentSessionLanguageConditionletTest.class, + com.dotmarketing.portlets.rules.conditionlet.NumberOfTimesPreviouslyVisitedConditionletTest.class, + com.dotmarketing.portlets.rules.conditionlet.UsersBrowserLanguageConditionletTest.class, + com.dotmarketing.portlets.rules.conditionlet.UsersSiteVisitsConditionletTest.class, + com.dotmarketing.portlets.rules.conditionlet.VisitorOperatingSystemConditionletTest.class, + com.dotmarketing.portlets.rules.conditionlet.VisitedUrlConditionletTest.class, + com.dotmarketing.portlets.rules.business.RulesCacheFTest.class, + com.dotmarketing.portlets.templates.business.TemplateAPITest.class, + com.dotmarketing.portlets.containers.business.ContainerAPIImplTest.class, + com.dotmarketing.portlets.folders.business.FolderAPITest.class, + com.dotmarketing.portlets.containers.business.ContainerAPITest.class, + com.dotmarketing.portlets.containers.business.FileAssetContainerUtilTest.class, + com.dotmarketing.portlets.htmlpages.business.HTMLPageAPITest.class, + com.dotmarketing.portlets.structure.factories.StructureFactoryTest.class, + com.dotmarketing.portlets.structure.factories.FieldFactoryTest.class, + com.dotmarketing.portlets.structure.model.ContentletRelationshipsTest.class, + com.dotmarketing.portlets.structure.transform.ContentletRelationshipsTransformerTest.class, }) public class MainSuite1a { diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java index cc4083fda487..42b4423dccd3 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java @@ -1,21 +1,31 @@ package com.dotcms; +import com.dotcms.ai.workflow.OpenAIAutoTagActionletTest; +import com.dotcms.content.elasticsearch.util.ESMappingUtilHelperTest; import com.dotcms.contenttype.business.DotAssetBaseTypeToContentTypeStrategyImplTest; import com.dotcms.contenttype.test.DotAssetAPITest; import com.dotcms.dotpubsub.PostgresPubSubImplTest; +import com.dotcms.ema.EMAWebInterceptorTest; +import com.dotcms.enterprise.cluster.ClusterFactoryTest; import com.dotcms.junit.MainBaseSuite; import com.dotcms.mock.request.CachedParameterDecoratorTest; import com.dotcms.publisher.bundle.business.BundleFactoryTest; import com.dotcms.publisher.business.PublishAuditAPITest; +import com.dotcms.publisher.util.PushedAssetUtilTest; import com.dotcms.publishing.PublisherFilterImplTest; import com.dotcms.publishing.PushPublishFiltersInitializerTest; +import com.dotcms.rendering.velocity.directive.DotParseTest; import com.dotcms.rendering.velocity.servlet.VelocityServletIntegrationTest; +import com.dotcms.rest.BundleResourceTest; import com.dotcms.rest.api.v1.apps.AppsResourceTest; import com.dotcms.rest.api.v1.folder.FolderResourceTest; import com.dotcms.rest.api.v1.pushpublish.PushPublishFilterResourceTest; import com.dotcms.rest.api.v1.user.UserResourceIntegrationTest; +import com.dotcms.saml.IdentityProviderConfigurationFactoryTest; +import com.dotcms.saml.SamlConfigurationServiceTest; import com.dotcms.security.apps.AppsAPIImplTest; import com.dotcms.security.apps.AppsCacheImplTest; +import com.dotcms.translate.GoogleTranslationServiceIntegrationTest; import com.dotmarketing.image.focalpoint.FocalPointAPITest; import com.dotmarketing.portlets.cmsmaintenance.factories.CMSMaintenanceFactoryTest; import com.dotmarketing.portlets.containers.business.ContainerFactoryImplTest; @@ -26,12 +36,12 @@ import com.dotmarketing.portlets.folders.model.FolderTest; import com.dotmarketing.portlets.templates.business.TemplateFactoryImplTest; import com.dotmarketing.portlets.workflows.actionlet.PushNowActionletTest; +import com.dotmarketing.portlets.workflows.model.TestWorkflowAction; import com.dotmarketing.quartz.job.CleanUpFieldReferencesJobTest; -import com.dotmarketing.startup.runonce.Task05195CreatesDestroyActionAndAssignDestroyDefaultActionsToTheSystemWorkflowTest; -import com.dotmarketing.startup.runonce.Task05210CreateDefaultDotAssetTest; import com.dotmarketing.startup.runonce.Task05225RemoveLoadRecordsToIndexTest; import com.dotmarketing.startup.runonce.Task05305AddPushPublishFilterColumnTest; import com.dotmarketing.startup.runonce.Task05350AddDotSaltClusterColumnTest; +import com.dotmarketing.startup.runonce.Task240131UpdateLanguageVariableContentTypeTest; import com.dotmarketing.util.HashBuilderTest; import com.dotmarketing.util.TestConfig; import com.liferay.portal.language.LanguageUtilTest; @@ -90,76 +100,9 @@ com.dotmarketing.portlets.personas.business.DeleteMultiTreeUsedPersonaTagJobTest.class, com.dotmarketing.portlets.links.business.MenuLinkAPITest.class, com.dotmarketing.portlets.links.factories.LinkFactoryTest.class, - com.dotmarketing.portlets.rules.conditionlet.ConditionletOSGIFTest.class, - com.dotmarketing.portlets.rules.conditionlet.CurrentSessionLanguageConditionletTest.class, - com.dotmarketing.portlets.rules.conditionlet.NumberOfTimesPreviouslyVisitedConditionletTest.class, - com.dotmarketing.portlets.rules.conditionlet.UsersBrowserLanguageConditionletTest.class, - com.dotmarketing.portlets.rules.conditionlet.UsersSiteVisitsConditionletTest.class, - com.dotmarketing.portlets.rules.conditionlet.VisitorOperatingSystemConditionletTest.class, - com.dotmarketing.portlets.rules.conditionlet.VisitedUrlConditionletTest.class, - com.dotmarketing.portlets.rules.business.RulesCacheFTest.class, - com.dotmarketing.portlets.templates.business.TemplateAPITest.class, - com.dotmarketing.portlets.containers.business.ContainerAPIImplTest.class, - com.dotmarketing.portlets.folders.business.FolderAPITest.class, - com.dotmarketing.portlets.containers.business.ContainerAPITest.class, - com.dotmarketing.portlets.containers.business.FileAssetContainerUtilTest.class, - com.dotmarketing.portlets.htmlpages.business.HTMLPageAPITest.class, - com.dotmarketing.portlets.structure.factories.StructureFactoryTest.class, - com.dotmarketing.portlets.structure.factories.FieldFactoryTest.class, - com.dotmarketing.portlets.structure.model.ContentletRelationshipsTest.class, - com.dotmarketing.portlets.structure.transform.ContentletRelationshipsTransformerTest.class, - com.dotmarketing.portlets.categories.business.CategoryAPITest.class, - com.dotmarketing.filters.FiltersTest.class, - com.dotmarketing.business.VersionableAPITest.class, - com.dotmarketing.business.UserAPITest.class, - com.dotmarketing.business.portal.PortletAPIImplTest.class, - com.dotmarketing.business.web.LanguageWebApiTest.class, - com.dotmarketing.business.IdentifierFactoryTest.class, - com.dotmarketing.business.IdentifierAPITest.class, - com.dotmarketing.business.CommitListenerCacheWrapperTest.class, - com.dotmarketing.business.RoleAPITest.class, - com.dotmarketing.business.IdentifierConsistencyIntegrationTest.class, - com.dotmarketing.business.LayoutAPITest.class, - com.dotmarketing.business.PermissionAPIIntegrationTest.class, - com.dotmarketing.business.PermissionAPITest.class, - com.dotmarketing.servlets.BinaryExporterServletTest.class, - com.dotmarketing.servlets.ShortyServletAndTitleImageTest.class, - com.dotmarketing.servlets.ajax.AjaxDirectorServletIntegrationTest.class, com.dotmarketing.factories.MultiTreeAPITest.class, - FocalPointAPITest.class, - com.dotmarketing.tag.business.TagAPITest.class, - OSGIUtilTest.class, - CleanUpFieldReferencesJobTest.class, - CachedParameterDecoratorTest.class, - ContainerFactoryImplTest.class, - TemplateFactoryImplTest.class, - TestConfig.class, - FolderTest.class, - PublishAuditAPITest.class, - BundleFactoryTest.class, - com.dotcms.security.apps.SecretsStoreKeyStoreImplTest.class, - AppsAPIImplTest.class, - AppsResourceTest.class, - AppsCacheImplTest.class, - VelocityServletIntegrationTest.class, - DotAssetAPITest.class, - DotAssetBaseTypeToContentTypeStrategyImplTest.class, - FileAssetAPIImplIntegrationTest.class, - FileAssetFactoryIntegrationTest.class, - UserResourceIntegrationTest.class, - IntegrationResourceLinkTest.class, - HashBuilderTest.class, - LanguageUtilTest.class, - FolderResourceTest.class, - Task05225RemoveLoadRecordsToIndexTest.class, - PublisherFilterImplTest.class, - PushPublishFiltersInitializerTest.class, - PushPublishFilterResourceTest.class, - PushNowActionletTest.class, - Task05305AddPushPublishFilterColumnTest.class, - CMSMaintenanceFactoryTest.class, - Task05350AddDotSaltClusterColumnTest.class, - PostgresPubSubImplTest.class + com.dotmarketing.portlets.categories.business.CategoryAPITest.class, + com.dotmarketing.filters.FiltersTest.class }) public class MainSuite2a { diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java index a2d187519914..c9cf2540dd95 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java @@ -42,6 +42,7 @@ import com.dotcms.contenttype.test.DotAssetAPITest; import com.dotcms.csspreproc.CSSCacheTest; import com.dotcms.csspreproc.CSSPreProcessServletTest; +import com.dotcms.dotpubsub.PostgresPubSubImplTest; import com.dotcms.dotpubsub.RedisPubSubImplTest; import com.dotcms.ema.EMAWebInterceptorTest; import com.dotcms.enterprise.cluster.ClusterFactoryTest; @@ -70,11 +71,16 @@ import com.dotcms.jobs.business.api.JobQueueManagerAPITest; import com.dotcms.junit.MainBaseSuite; import com.dotcms.mail.MailAPIImplTest; +import com.dotcms.mock.request.CachedParameterDecoratorTest; import com.dotcms.publisher.bundle.business.BundleAPITest; +import com.dotcms.publisher.bundle.business.BundleFactoryTest; +import com.dotcms.publisher.business.PublishAuditAPITest; import com.dotcms.publisher.receiver.BundlePublisherTest; import com.dotcms.publisher.util.DependencyManagerTest; import com.dotcms.publisher.util.PushedAssetUtilTest; import com.dotcms.publishing.BundlerUtilTest; +import com.dotcms.publishing.PublisherFilterImplTest; +import com.dotcms.publishing.PushPublishFiltersInitializerTest; import com.dotcms.publishing.manifest.CSVManifestBuilderTest; import com.dotcms.publishing.manifest.CSVManifestReaderTest; import com.dotcms.publishing.manifest.ManifestReaderFactoryTest; @@ -99,6 +105,7 @@ import com.dotcms.rest.api.v1.authentication.ResetPasswordTokenUtilTest; import com.dotcms.rest.api.v1.folder.FolderResourceTest; import com.dotcms.rest.api.v1.menu.MenuResourceTest; +import com.dotcms.rest.api.v1.pushpublish.PushPublishFilterResourceTest; import com.dotcms.rest.api.v1.system.ConfigurationHelperTest; import com.dotcms.rest.api.v1.taillog.TailLogResourceTest; import com.dotcms.rest.api.v1.user.UserResourceIntegrationTest; @@ -127,23 +134,30 @@ import com.dotmarketing.common.db.DBTimeZoneCheckTest; import com.dotmarketing.filters.AutoLoginFilterTest; import com.dotmarketing.filters.CMSUrlUtilIntegrationTest; +import com.dotmarketing.image.focalpoint.FocalPointAPITest; import com.dotmarketing.osgi.GenericBundleActivatorIntegrationTest; import com.dotmarketing.portlets.browser.BrowserUtilTest; import com.dotmarketing.portlets.browser.ajax.BrowserAjaxTest; import com.dotmarketing.portlets.categories.business.CategoryFactoryTest; +import com.dotmarketing.portlets.cmsmaintenance.factories.CMSMaintenanceFactoryTest; +import com.dotmarketing.portlets.containers.business.ContainerFactoryImplTest; import com.dotmarketing.portlets.contentlet.business.ContentletCacheImplTest; import com.dotmarketing.portlets.contentlet.model.ContentletDependenciesTest; import com.dotmarketing.portlets.contentlet.model.IntegrationResourceLinkTest; import com.dotmarketing.portlets.fileassets.business.FileAssetAPIImplIntegrationTest; import com.dotmarketing.portlets.fileassets.business.FileAssetFactoryIntegrationTest; import com.dotmarketing.portlets.folders.business.FolderFactoryImplTest; +import com.dotmarketing.portlets.folders.model.FolderTest; import com.dotmarketing.portlets.htmlpages.business.render.HTMLPageAssetRenderedAPIImplIntegrationTest; import com.dotmarketing.portlets.templates.business.FileAssetTemplateUtilTest; +import com.dotmarketing.portlets.templates.business.TemplateFactoryImplTest; import com.dotmarketing.portlets.workflows.actionlet.MoveContentActionletTest; +import com.dotmarketing.portlets.workflows.actionlet.PushNowActionletTest; import com.dotmarketing.portlets.workflows.actionlet.SaveContentAsDraftActionletIntegrationTest; import com.dotmarketing.portlets.workflows.actionlet.VelocityScriptActionletAbortTest; import com.dotmarketing.portlets.workflows.model.TestWorkflowAction; import com.dotmarketing.quartz.DotStatefulJobTest; +import com.dotmarketing.quartz.job.CleanUpFieldReferencesJobTest; import com.dotmarketing.quartz.job.DropOldContentVersionsJobTest; import com.dotmarketing.quartz.job.IntegrityDataGenerationJobTest; import com.dotmarketing.quartz.job.PopulateContentletAsJSONJobTest; @@ -159,9 +173,11 @@ import com.dotmarketing.util.ITConfigTest; import com.dotmarketing.util.MaintenanceUtilTest; import com.dotmarketing.util.ResourceCollectorUtilTest; +import com.dotmarketing.util.TestConfig; import com.dotmarketing.util.UtilMethodsITest; import com.dotmarketing.util.contentlet.pagination.PaginatedContentletsIntegrationTest; import com.liferay.portal.language.LanguageUtilTest; +import org.apache.felix.framework.OSGIUtilTest; import org.apache.velocity.tools.view.tools.CookieToolTest; import org.junit.runner.RunWith; import org.junit.runners.Suite.SuiteClasses; @@ -376,6 +392,36 @@ AsyncVanitiesCollectorTest.class, HttpServletRequestImpersonatorTest.class, Task250107RemoveEsReadOnlyMonitorJobTest.class, + + com.dotmarketing.business.VersionableAPITest.class, + com.dotmarketing.business.UserAPITest.class, + com.dotmarketing.business.portal.PortletAPIImplTest.class, + com.dotmarketing.business.web.LanguageWebApiTest.class, + com.dotmarketing.business.IdentifierFactoryTest.class, + com.dotmarketing.business.IdentifierAPITest.class, + com.dotmarketing.business.CommitListenerCacheWrapperTest.class, + com.dotmarketing.business.RoleAPITest.class, + com.dotmarketing.business.IdentifierConsistencyIntegrationTest.class, + com.dotmarketing.business.LayoutAPITest.class, + com.dotmarketing.business.PermissionAPIIntegrationTest.class, + com.dotmarketing.business.PermissionAPITest.class, + com.dotmarketing.servlets.BinaryExporterServletTest.class, + com.dotmarketing.servlets.ShortyServletAndTitleImageTest.class, + com.dotmarketing.servlets.ajax.AjaxDirectorServletIntegrationTest.class, + FocalPointAPITest.class, + com.dotmarketing.tag.business.TagAPITest.class, + + OSGIUtilTest.class, + CleanUpFieldReferencesJobTest.class, + CachedParameterDecoratorTest.class, + ContainerFactoryImplTest.class, + TemplateFactoryImplTest.class, + TestConfig.class, + FolderTest.class, + PublishAuditAPITest.class, + BundleFactoryTest.class, + com.dotcms.security.apps.SecretsStoreKeyStoreImplTest.class, + AppsAPIImplTest.class, AppsResourceTest.class, AppsCacheImplTest.class, @@ -419,6 +465,27 @@ Task05195CreatesDestroyActionAndAssignDestroyDefaultActionsToTheSystemWorkflowTest.class, Task05210CreateDefaultDotAssetTest.class, + + + + DotAssetAPITest.class, + DotAssetBaseTypeToContentTypeStrategyImplTest.class, + FileAssetAPIImplIntegrationTest.class, + FileAssetFactoryIntegrationTest.class, + UserResourceIntegrationTest.class, + IntegrationResourceLinkTest.class, + HashBuilderTest.class, + LanguageUtilTest.class, + FolderResourceTest.class, + Task05225RemoveLoadRecordsToIndexTest.class, + PublisherFilterImplTest.class, + PushPublishFiltersInitializerTest.class, + PushPublishFilterResourceTest.class, + PushNowActionletTest.class, + Task05305AddPushPublishFilterColumnTest.class, + CMSMaintenanceFactoryTest.class, + Task05350AddDotSaltClusterColumnTest.class, + PostgresPubSubImplTest.class, DotParseTest.class, TestWorkflowAction.class, SamlConfigurationServiceTest.class, diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java index 16b72e4d2d4d..0149cea7f13b 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/BasicProfileCollectorTest.java @@ -4,6 +4,7 @@ import com.dotcms.LicenseTestUtil; import com.dotcms.analytics.track.matchers.PagesAndUrlMapsRequestMatcher; import com.dotcms.enterprise.cluster.ClusterFactory; +import com.dotcms.telemetry.business.MetricsAPI; import com.dotcms.util.IntegrationTestInitService; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; @@ -17,11 +18,18 @@ import java.net.UnknownHostException; import java.util.Map; +import static com.dotcms.analytics.track.collectors.Collector.CUSTOMER_CATEGORY; +import static com.dotcms.analytics.track.collectors.Collector.CUSTOMER_NAME; +import static com.dotcms.analytics.track.collectors.Collector.ENVIRONMENT_NAME; +import static com.dotcms.analytics.track.collectors.Collector.ENVIRONMENT_VERSION; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; /** + * Verifies that the {@link BasicProfileCollector} is able to collect the basic profile data and + * works as expected. * * @author Jose Castro * @since Oct 9th, 2024 @@ -43,9 +51,13 @@ public static void prepare() throws Exception { /** *
    - *
  • Method to test: {@link }
  • - *
  • Given Scenario:
  • - *
  • Expected Result:
  • + *
  • Method to test: + * {@link BasicProfileCollector#collect(CollectorContextMap, CollectorPayloadBean)}
  • + *
  • Given Scenario: Simulate the collection of Basic Profile data, which is + * data that is ALWAYS collected for any kind of Data Collector, and compare it with an + * expected data map.
  • + *
  • Expected Result: Both the collected data map and the expected data must + * match.
  • *
*/ @Test @@ -54,7 +66,7 @@ public void collectBasicProfileData() throws DotDataException, UnknownHostExcept final String requestId = UUIDUtil.uuid(); final HttpServletRequest request = Util.mockHttpRequestObj(response, "/", requestId, APILocator.getUserAPI().getAnonymousUser()); - final Map expectedDataMap = Map.of( + final Map expectedDataMap = new java.util.HashMap<>(Map.of( Collector.CLUSTER, CLUSTER_ID, Collector.SERVER, SERVER_ID, Collector.PERSONA, "dot:default", @@ -63,7 +75,22 @@ public void collectBasicProfileData() throws DotDataException, UnknownHostExcept Collector.USER_AGENT, Util.USER_AGENT, Collector.SESSION_ID, "DAA3339CD687D9ABD4101CF9EDDD42DB", Collector.REQUEST_ID, requestId - ); + )); + expectedDataMap.putAll(Map.of( + Collector.EVENT_SOURCE, EventSource.DOT_CMS.getName(), + Collector.IS_TARGET_PAGE, false, + Collector.IS_EXPERIMENT_PAGE, false, + Collector.USER_OBJECT, Map.of( + "identifier", "anonymous", + "email", "anonymous@dotcms.anonymoususer"))); + // The values returned when running the Integration Tests are random. So, in this case, + // we'll just verify that the attributes are present, and add any values in here + expectedDataMap.putAll(Map.of( + CUSTOMER_NAME, "", + CUSTOMER_CATEGORY, "", + ENVIRONMENT_NAME, "", + ENVIRONMENT_VERSION, 0 + )); final Collector collector = new BasicProfileCollector(); final CollectorPayloadBean collectedData = Util.getCollectorPayloadBean(request, collector, new PagesAndUrlMapsRequestMatcher(), null); @@ -74,13 +101,25 @@ public void collectBasicProfileData() throws DotDataException, UnknownHostExcept if (collectedData.toMap().containsKey(key)) { final Object expectedValue = expectedDataMap.get(key); final Object collectedValue = collectedData.toMap().get(key); - if (!Collector.UTC_TIME.equalsIgnoreCase(key)) { + if (CUSTOMER_NAME.equalsIgnoreCase(key) || CUSTOMER_CATEGORY.equalsIgnoreCase(key) || + ENVIRONMENT_NAME.equalsIgnoreCase(key) || ENVIRONMENT_VERSION.equalsIgnoreCase(key)) { + assertNotNull(String.format("Collected value '%s' cannot be null", key), collectedValue); + } else if (!Collector.UTC_TIME.equalsIgnoreCase(key)) { assertEquals("Collected value must be equal to expected value for key: " + key, expectedValue, collectedValue); } counter++; } } - assertEquals("Number of returned expected properties doesn't match", counter, expectedDataMap.size()); + final MetricsAPI metricsAPI = APILocator.getMetricsAPI(); + final MetricsAPI.Client client = metricsAPI.getClient(); + // In local envs, the 'category_name' attribute maybe null, and is NOT added to the + // collected data map, so the assertion below would fail. This hack is just to make this + // test run locally without devs having to tweak it + final boolean areAllAttrsPresent = client.getVersion() >= 0 && UtilMethods.isSet(client.getEnvironment()) && + UtilMethods.isSet(client.getCategory()) && UtilMethods.isSet(client.getClientName()); + if (areAllAttrsPresent) { + assertEquals("Number of returned expected properties doesn't match", counter, expectedDataMap.size()); + } } } diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorIntegrationTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorIntegrationTest.java new file mode 100644 index 000000000000..f0efe06fdc99 --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorIntegrationTest.java @@ -0,0 +1,195 @@ +package com.dotcms.analytics.track.collectors; + +import com.dotcms.JUnit4WeldRunner; +import com.dotcms.LicenseTestUtil; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.ImageField; +import com.dotcms.contenttype.model.field.TextField; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.datagen.*; +import com.dotcms.enterprise.cluster.ClusterFactory; +import com.dotcms.util.IntegrationTestInitService; +import com.dotmarketing.beans.Host; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.image.focalpoint.FocalPointAPITest; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.folders.model.Folder; +import com.dotmarketing.portlets.languagesmanager.business.UniqueLanguageDataGen; +import com.dotmarketing.portlets.languagesmanager.model.Language; +import org.apache.commons.io.FileUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.enterprise.context.Dependent; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; + +import static com.dotcms.analytics.track.collectors.Collector.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Dependent +@RunWith(JUnit4WeldRunner.class) +public class FilesCollectorIntegrationTest { + + @BeforeClass + public static void prepare() throws Exception { + // Setting web app environment + IntegrationTestInitService.getInstance().init(); + } + + /** + * Method to test: {@link FilesCollector#collect(CollectorContextMap, CollectorPayloadBean)} + * when: + * - Create a Content TYpe with an Image field, let called it 'contentTypeWithImageField' + * - Create a FileAsset pointing to an Image + * - Create a Contentlet using the 'contentTypeWithImageField' ContentType created in the first step + * - Try to collect the Analytics data pretending that the Image was hit using a + * /dA/[contentTypeWithImageField Content's id]/{contentTypeWithImageField image field variable name} + * + * Should: collect the data using the Image Contentlet not the 'contentTypeWithImageField' Contentlet + */ + @Test + public void registerdAUriInAnalytics() throws IOException, DotDataException, DotSecurityException { + final Host host = new SiteDataGen().nextPersisted(); + final Language language = new UniqueLanguageDataGen().nextPersisted(); + + final FilesCollector filesCollector = new FilesCollector(); + + final CollectorContextMap collectorContextMap = mock(CollectorContextMap.class); + final CollectorPayloadBean collectorPayloadBean = new ConcurrentCollectorPayloadBeanWithBaseMap(new HashMap<>()); + + final Field imageField = new FieldDataGen().type(ImageField.class).next(); + + final Folder imageFolder = new FolderDataGen().site(host).nextPersisted(); + + File tempFile = File.createTempFile("contentWithImageBundleTest", ".jpg"); + URL url = FocalPointAPITest.class.getResource("/images/test.jpg"); + File testImage = new File(url.getFile()); + FileUtils.copyFile(testImage, tempFile); + + final Contentlet imageFileAsset = new FileAssetDataGen(tempFile) + .host(host) + .languageId(language.getId()) + .folder(imageFolder).nextPersisted(); + + final ContentType contentTypeWithImageField = new ContentTypeDataGen().host(host).field(imageField).nextPersisted(); + final Contentlet contentletWithImage = new ContentletDataGen(contentTypeWithImageField) + .host(host) + .setProperty(imageField.variable(), imageFileAsset.getIdentifier()) + .languageId(language.getId()) + .nextPersisted(); + + ContentletDataGen.publish(contentletWithImage); + ContentletDataGen.publish(imageFileAsset); + + final String uri = String.format("/dA/%s/%s", contentletWithImage.getIdentifier(), imageField.variable()); + + when(collectorContextMap.get(CollectorContextMap.URI)).thenReturn(uri); + when(collectorContextMap.get(CollectorContextMap.HOST)).thenReturn(host.getIdentifier()); + when(collectorContextMap.get(CollectorContextMap.CURRENT_HOST)).thenReturn(host); + when(collectorContextMap.get(CollectorContextMap.LANG_ID)).thenReturn(language.getId()); + when(collectorContextMap.get(CollectorContextMap.LANG)).thenReturn(language.getLanguageCode()); + + CollectorPayloadBean collect = filesCollector.collect(collectorContextMap, collectorPayloadBean); + + assertEquals(collectorPayloadBean.get(EVENT_TYPE), EventType.FILE_REQUEST.getType()); + final HashMap fileObjectFromPayload = (HashMap) collectorPayloadBean.get(OBJECT); + + final ContentType imageContentType = imageFileAsset.getContentType(); + + assertEquals( imageFileAsset.getIdentifier(), fileObjectFromPayload.get(ID)); + assertEquals(imageFileAsset.getTitle(), fileObjectFromPayload.get(TITLE)); + assertEquals( uri, fileObjectFromPayload.get(URL)); + assertEquals(imageContentType.id(), fileObjectFromPayload.get(CONTENT_TYPE_ID)); + assertEquals(imageContentType.name(), fileObjectFromPayload.get(CONTENT_TYPE_NAME)); + assertEquals(imageContentType.variable(), fileObjectFromPayload.get(CONTENT_TYPE_VAR_NAME)); + assertEquals(imageContentType.baseType().name(), fileObjectFromPayload.get(BASE_TYPE)); + + } + + /** + * Method to test: {@link FilesCollector#collect(CollectorContextMap, CollectorPayloadBean)} + * when: + * - Create a Content TYpe with an Image field, let called it 'contentTypeWithImageField' + * - Create a FileAsset pointing to an Image + * - Create a Contentlet using the 'contentTypeWithImageField' ContentType created in the first step + * - Try to collect the Analytics data pretending that the Image was hit using a + * /contentAsset/image/[contentTypeWithImageField Content's id]/{contentTypeWithImageField image field variable name} + * + * Should: collect the data using the Image Contentlet not the 'contentTypeWithImageField' Contentlet + */ + @Test + public void registerContentAssetsUriInAnalytics() throws IOException, DotDataException, DotSecurityException { + final Host host = new SiteDataGen().nextPersisted(); + final Language language = new UniqueLanguageDataGen().nextPersisted(); + + final FilesCollector filesCollector = new FilesCollector(); + + final CollectorContextMap collectorContextMap = mock(CollectorContextMap.class); + final CollectorPayloadBean collectorPayloadBean = new ConcurrentCollectorPayloadBeanWithBaseMap(new HashMap<>()); + + final Field fieldTitle = new FieldDataGen().type(TextField.class).name("title").next(); + final Field fieldImage = new FieldDataGen().type(ImageField.class).name("image").next(); + + final ContentType contentType = new ContentTypeDataGen().field(fieldTitle).field(fieldImage).nextPersisted(); + + final Language imageFileLanguage = new UniqueLanguageDataGen().nextPersisted(); + final Folder imageFolder = new FolderDataGen().site(host).nextPersisted(); + + File tempFile = File.createTempFile("contentWithImageBundleTest", ".jpg"); + URL url = FocalPointAPITest.class.getResource("/images/test.jpg"); + File testImage = new File(url.getFile()); + FileUtils.copyFile(testImage, tempFile); + + final Contentlet imageFileAsset = new FileAssetDataGen(tempFile) + .host(host) + .languageId(imageFileLanguage.getId()) + .folder(imageFolder).nextPersisted(); + + final Field imageField = new FieldDataGen().type(ImageField.class).next(); + final ContentType contentTypeWithImageField = new ContentTypeDataGen().host(host).field(imageField).nextPersisted(); + final Contentlet contentletWithImage = new ContentletDataGen(contentTypeWithImageField) + .host(host) + .setProperty(imageField.variable(), imageFileAsset.getIdentifier()) + .languageId(language.getId()) + .nextPersisted(); + + final String uri = String.format("/contentAsset/image/%s/%s", contentletWithImage.getIdentifier(), fieldImage.variable()); + + when(collectorContextMap.get(CollectorContextMap.URI)).thenReturn(uri); + when(collectorContextMap.get(CollectorContextMap.HOST)).thenReturn(host.getIdentifier()); + when(collectorContextMap.get(CollectorContextMap.CURRENT_HOST)).thenReturn(host); + when(collectorContextMap.get(CollectorContextMap.LANG_ID)).thenReturn(language.getId()); + when(collectorContextMap.get(CollectorContextMap.LANG)).thenReturn(language.getLanguageCode()); + + CollectorPayloadBean collect = filesCollector.collect(collectorContextMap, collectorPayloadBean); + + assertEquals(collectorPayloadBean.get(EVENT_TYPE), EventType.FILE_REQUEST.getType()); + final HashMap fileObject = (HashMap) collectorPayloadBean.get(OBJECT); + + final ContentType imageContentType = imageFileAsset.getContentType(); + + assertEquals(imageFileAsset.getIdentifier(), fileObject.get(ID)); + assertEquals(imageFileAsset.getTitle(), fileObject.get(TITLE)); + assertEquals(uri, fileObject.get(URL)); + assertEquals(imageContentType.id(), fileObject.get(CONTENT_TYPE_ID)); + assertEquals(imageContentType.name(), fileObject.get(CONTENT_TYPE_NAME)); + assertEquals(imageContentType.variable(), fileObject.get(CONTENT_TYPE_VAR_NAME)); + assertEquals(imageContentType.baseType().name(), fileObject.get(BASE_TYPE)); + + } + + @Test + public void registerDotAssetsUriInAnalytics() throws IOException, DotDataException, DotSecurityException { + throw new DotRuntimeException("test"); + } +} diff --git a/dotcms-integration/src/test/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtilsTest.java b/dotcms-integration/src/test/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtilsTest.java index e3246d407799..239941725e34 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtilsTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rendering/velocity/viewtools/content/util/ContentUtilsTest.java @@ -1,5 +1,6 @@ package com.dotcms.rendering.velocity.viewtools.content.util; +import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.business.FieldAPI; import com.dotcms.contenttype.model.field.DateField; @@ -13,7 +14,9 @@ import com.dotcms.datagen.ContentletDataGen; import com.dotcms.datagen.SiteDataGen; import com.dotcms.datagen.TestDataUtils; +import com.dotcms.mock.request.MockAttributeRequest; import com.dotcms.mock.request.MockHttpRequestIntegrationTest; +import com.dotcms.mock.request.MockSessionRequest; import com.dotcms.mock.response.MockHttpResponse; import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtilsTest.TestCase.LANGUAGE_TYPE_FILTER; import com.dotcms.rendering.velocity.viewtools.content.util.ContentUtilsTest.TestCase.PUBLISH_TYPE_FILTER; @@ -40,6 +43,9 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.TimeZone; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -1225,4 +1231,105 @@ public void test_add_relationships_on_parent_children_related_contents() Assert.assertTrue(childrenIds.contains(child2.getIdentifier())); Assert.assertTrue(childrenIds.contains(child3.getIdentifier())); } + + /** + * Test method: testFuturePublishDateContentRetrieval + * + * Given: + * - Two versions of the same content (same identifier, different inodes) + * - V1 is published immediately + * - V2 has a publish date set 10 days in the future + * - A query is made with a date after V2's publish date + * + * Scenario: + * - Content is searched by identifier and by inode with a specific future date + * - Content is searched in EDIT mode without specifying a date + * - Content is searched in LIVE mode without specifying a date + * + * Expected Results: + * - When searching with a future date (after the publish date), + * it should return V2 regardless of whether searching by identifier or inode + * - When searching in EDIT mode without a date, it should return V2 (most recent version) + * - When searching in LIVE mode without a date, it should return V1 (currently published version) + */ + @Test + public void testFuturePublishDateContentRetrieval() { + final TimeZone defaultZone = TimeZone.getDefault(); + try { + final TimeZone utc = TimeZone.getTimeZone("UTC"); + TimeZone.setDefault(utc); + + final Host host = new SiteDataGen().nextPersisted(); + final Language language = APILocator.getLanguageAPI().getDefaultLanguage(); + final ContentType blogLikeContentType = TestDataUtils.getBlogLikeContentType(); + final ContentletDataGen blogsDataGen = new ContentletDataGen(blogLikeContentType.id()) + .languageId(language.getId()) + .host(host) + .setProperty("body", TestDataUtils.BLOCK_EDITOR_DUMMY_CONTENT) + .setPolicy(IndexPolicy.WAIT_FOR) + .languageId(language.getId()) + .setProperty(Contentlet.IS_TEST_MODE, true); + + //Ten days from now + final LocalDateTime relativeDate = LocalDateTime.now().plusDays(10).plusHours(1); + final Instant relativeInstant = relativeDate.atZone(utc.toZoneId()).toInstant(); + final Date publishDate = Date.from(relativeInstant); + + //Any date after publish date + final LocalDateTime relativeDate2 = LocalDateTime.now().plusDays(11).plusHours(1); + final Instant relativeInstant2 = relativeDate2.atZone(utc.toZoneId()).toInstant(); + final Date futureDate = Date.from(relativeInstant2); + + + //Let's create two versions of a content they must share the same identifier + blogsDataGen + .setProperty("title", "v1"); + final Contentlet blogV1 = + blogsDataGen.nextPersistedAndPublish(); + + blogsDataGen + .setProperty("identifier", blogV1.getIdentifier()) + .setProperty("title", "v2") + .setProperty("publishDate", publishDate); // Set the publish-date in the future + final Contentlet blogV2 = blogsDataGen.nextPersisted(); + + final MockAttributeRequest mockAttributeRequest = new MockAttributeRequest(new MockHttpRequestIntegrationTest("localhost", "/?mode=LIVE").request()); + HttpServletRequestThreadLocal.INSTANCE.setRequest(mockAttributeRequest); + + //Let's make sure that the identifier is the same + Assert.assertEquals(blogV1.getIdentifier(), blogV2.getIdentifier()); + + //Now, lets for the future version of the content. That's V2 + final String dateAsString = String.valueOf(futureDate.getTime()); + + //Find by identifier and inode + final Contentlet blogV1ByIdentifier = ContentUtils.find(blogV1.getIdentifier(), user, dateAsString, true, language.getId()); + final Contentlet blogV1ByInode = ContentUtils.find(blogV1.getInode(), user, dateAsString, true, language.getId()); + Assert.assertNotNull(blogV1ByIdentifier); + Assert.assertNotNull(blogV1ByInode); + //Here we should always get v2 regardless of the identifier or inode because they both have a future version matching the publish-date + Assert.assertEquals("v2",blogV1ByIdentifier.getTitle()); + Assert.assertEquals("v2",blogV1ByInode.getTitle()); + //Here again we should always get v2 regardless of the identifier or inode because they both have a future version matching the publish-date + final Contentlet blogV2ByIdentifier = ContentUtils.find(blogV2.getIdentifier(), user, dateAsString, true, language.getId()); + final Contentlet blogV2ByInode = ContentUtils.find(blogV2.getInode(), user, dateAsString, true, language.getId()); + Assert.assertNotNull(blogV2ByIdentifier); + Assert.assertNotNull(blogV2ByInode); + Assert.assertEquals("v2",blogV2ByIdentifier.getTitle()); + Assert.assertEquals("v2",blogV2ByInode.getTitle()); + + //If we request Preview or Edit we should continue to get the same unpublished version + final Contentlet blogByIdentifierNoDateEdit = ContentUtils.find(blogV2.getIdentifier(), user, null, true, language.getId()); + Assert.assertNotNull(blogByIdentifierNoDateEdit); + Assert.assertEquals("v2",blogByIdentifierNoDateEdit.getTitle()); + + //Now if we request the live version we should get the v1 + final Contentlet blogByIdentifierNoDateLive = ContentUtils.find(blogV2.getIdentifier(), user, null, false, language.getId()); + Assert.assertNotNull(blogByIdentifierNoDateLive); + Assert.assertEquals("v1",blogByIdentifierNoDateLive.getTitle()); + + } finally { + TimeZone.setDefault(defaultZone); + } + } } diff --git a/dotcms-integration/src/test/java/com/dotcms/util/TimeMachineUtilTest.java b/dotcms-integration/src/test/java/com/dotcms/util/TimeMachineUtilTest.java index 19fe85dc8ad0..07ad55f01858 100644 --- a/dotcms-integration/src/test/java/com/dotcms/util/TimeMachineUtilTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/util/TimeMachineUtilTest.java @@ -1,18 +1,23 @@ package com.dotcms.util; import com.dotcms.api.web.HttpServletRequestThreadLocal; +import com.dotmarketing.util.DateUtil; import org.junit.BeforeClass; import org.junit.Test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import java.text.ParseException; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.Optional; import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class TimeMachineUtilTest { @@ -20,6 +25,7 @@ public class TimeMachineUtilTest { public static void prepare() throws Exception { //Setting web app environment IntegrationTestInitService.getInstance().init(); + mockStatic(DateUtil.class); } /** @@ -95,4 +101,91 @@ public void whenTimeMachineIsNotRunningShouldReturnTrue(){ assertTrue(TimeMachineUtil.isRunning()); } -} + + /** + * Method to Test: {@link TimeMachineUtil#parseTimeMachineDate(String)} + * When: When the input date is null + * Should: Return an empty {@link Optional} + */ + @Test + public void testParseTimeMachineDate_NullDate() { + Optional result = TimeMachineUtil.parseTimeMachineDate(null); + assertFalse(result.isPresent()); + } + + /** + * Method to Test: {@link TimeMachineUtil#parseTimeMachineDate(String)} + * When: When the input date is invalid + * Should: Throw an {@link IllegalArgumentException} + */ + @Test(expected = IllegalArgumentException.class) + public void testParseTimeMachineDate_InvalidDate() { + TimeMachineUtil.parseTimeMachineDate("invalid-date"); + } + + /** + * Method to Test: {@link TimeMachineUtil#parseTimeMachineDate(String)} + * When: When the input date is valid and within the grace window + * Should: Return an empty {@link Optional} + */ + @Test + public void testParseTimeMachineDate_ValidDateWithinGraceWindow() throws ParseException { + Instant now = Instant.now().plus(Duration.ofMinutes(3)); + + // Format the Instant to a string that DateUtil.convertDate can parse + DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; + String dateWithinGraceWindow = formatter.format(now); + + // Convert Instant to Date + Date dateObject = Date.from(now); + when(DateUtil.convertDate(dateWithinGraceWindow)).thenReturn(dateObject); + + Optional result = TimeMachineUtil.parseTimeMachineDate(dateWithinGraceWindow); + assertTrue(result.isEmpty()); + } + + /** + * Method to Test: {@link TimeMachineUtil#parseTimeMachineDate(String)} + * When: When the input date is valid and outside the grace window + * Should: Return a present {@link Optional} with the parsed date + */ + @Test + public void testParseTimeMachineDate_ValidDateOutsideGraceWindow() throws ParseException { + Instant now = Instant.now().plus(Duration.ofMinutes(10)); + + // Format the Instant to a string that DateUtil.convertDate can parse + DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT; + String dateWithinGraceWindow = formatter.format(now); + + Date dateObject = Date.from(now); + when(DateUtil.convertDate(dateWithinGraceWindow)).thenReturn(dateObject); + + Optional result = TimeMachineUtil.parseTimeMachineDate(dateWithinGraceWindow); + assertTrue(result.isPresent()); + assertEquals(now.truncatedTo(ChronoUnit.SECONDS), result.get().truncatedTo(ChronoUnit.SECONDS)); + } + + /** + * Method to Test: {@link TimeMachineUtil#isOlderThanGraceWindow(Instant)} + * When: When the input date is within the grace window + * Should: Return false + */ + @Test + public void testIsOlderThanGraceWindow_shouldReturnTrue() { + Instant now = Instant.now(); + Instant futureDate= now.plus(Duration.ofMinutes(5)); // 5 minutes in the future + assertFalse(TimeMachineUtil.isOlderThanGraceWindow(futureDate)); + } + + /** + * Method to Test: {@link TimeMachineUtil#isOlderThanGraceWindow(Instant)} + * When: When the input date is outside the grace window + * Should: Return true + */ + @Test + public void testIsOlderThanGraceWindow_shouldReturnFalse() { + Instant now = Instant.now(); + Instant futureDate = now.plus(Duration.ofMinutes(6)); // 6 minutes in the future + assertTrue(TimeMachineUtil.isOlderThanGraceWindow(futureDate)); + } +} \ No newline at end of file diff --git a/dotcms-integration/src/test/java/com/dotmarketing/servlets/BinaryExporterServletTest.java b/dotcms-integration/src/test/java/com/dotmarketing/servlets/BinaryExporterServletTest.java index a4eaea2d07ec..b7ff4d1ab7e5 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/servlets/BinaryExporterServletTest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/servlets/BinaryExporterServletTest.java @@ -58,7 +58,6 @@ import java.util.Date; import static com.dotmarketing.business.Role.DOTCMS_BACK_END_USER; -import static com.dotmarketing.servlets.BinaryExporterServlet.isDotAdminRequest; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -408,38 +407,5 @@ private void sendRequest(final HttpServletRequest request, final HttpServletResp } - /** - * Given scenario: Test the request comes from dotAdmin - * Expected result: Should return true if the referer is a valid dotAdmin referer - */ - @Test - public void testDotAdminRequestValidReferer() { - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader("referer")).thenReturn("http://localhost:8080/dotAdmin/somepage"); - assertTrue( "Should be true for valid dotAdmin referer", isDotAdminRequest(request)); - } - - /** - * Given scenario: Test the request comes from dotAdmin - * Expected result: Should return true if the referer is a valid dotAdmin referer - */ - @Test - public void testDotAdminRequestWithDifferentDomain() { - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader("referer")).thenReturn("http://otherdomain.com/dotAdmin/somepage"); - assertTrue( "Should be true for valid dotAdmin referer", isDotAdminRequest(request)); - } - - /** - * Given scenario: Test the request comes from dotAdmin - * Expected result: Should return true if the referer is a valid dotAdmin referer - */ - @Test - public void testDotAdminRequestWithoutDotAdmin() { - HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getHeader("referer")).thenReturn("http://localhost:8080/anotherPath/somepage"); - assertFalse("Should be false if /dotAdmin is not present", isDotAdminRequest(request)); - } - } diff --git a/dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json b/dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json new file mode 100644 index 000000000000..b6d2e5881e75 --- /dev/null +++ b/dotcms-postman/src/main/resources/postman/DateTool.postman_collection.json @@ -0,0 +1,70 @@ +{ + "info": { + "_postman_id": "04e24c3e-7d51-4e2b-be4a-e63094ab158d", + "name": "DateTool", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "781456" + }, + "item": [ + { + "name": "TestToTimeStamp", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "var text = pm.response.text()", + "", + "pm.test(\"Right date\", function () {", + " pm.expect(text).to.be.eql('2025-02-06 00:00:00.0');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "$date.toTimestamp('yyyy-MM-dd','2025-02-06')", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/vtl/dynamic/", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "vtl", + "dynamic", + "" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/scripts/dev-metrics/lead_time_to_change_issues.py b/scripts/dev-metrics/lead_time_to_change_issues.py new file mode 100644 index 000000000000..76e41f6becf7 --- /dev/null +++ b/scripts/dev-metrics/lead_time_to_change_issues.py @@ -0,0 +1,216 @@ +import os +from datetime import datetime, timedelta +import logging +from collections import defaultdict +from statistics import mean +from github_metrics_base import GitHubMetricsBase +import requests + +logger = logging.getLogger(__name__) + +class DeploymentLeadTimeMetrics(GitHubMetricsBase): + def __init__(self, token, owner, repo, team_label, iteration_field_id='120606020'): + super().__init__(token, owner, repo, team_label) + self.iteration_field_id = iteration_field_id + + def get_deployment_label_date(self, issue_number): + """Get the datetime when 'Customer Deployed' label was added to an issue""" + events = self.get_issue_events(issue_number) + + for event in events: + if (event.get('event') == 'labeled' and + event.get('label', {}).get('name') == 'Customer Deployed'): + return datetime.strptime(event['created_at'], '%Y-%m-%dT%H:%M:%SZ') + + return None + + def get_iteration_start_date(self, issue_number): + """Get the datetime when an issue was first included in a sprint""" + try: + field_values = self.get_issue_fields(issue_number) + logger.info(f"Field values: {field_values}") + + if field_values: + # Return the earliest date if there's a value + earliest_field = min(field_values, key=lambda x: datetime.strptime(x['created_at'], '%Y-%m-%dT%H:%M:%SZ')) + return datetime.strptime(earliest_field['created_at'], '%Y-%m-%dT%H:%M:%SZ') + + return None + + except Exception as e: + logger.error(f"Error getting iteration field for issue #{issue_number}: {e}") + return None + + def get_issue_fields(self, issue_number): + """Get the custom fields for an issue, specifically the iteration field""" + try: + logger.debug(f"Fetching fields for issue #{issue_number}") + + # GraphQL query to get iteration field data + query = f""" + query {{ + repository(owner: "{self.owner}", name: "{self.repo}") {{ + issue(number: {issue_number}) {{ + projectItems(first: 10) {{ + nodes {{ + fieldValues(first: 20) {{ + nodes {{ + ... on ProjectV2ItemFieldIterationValue {{ + title + startDate + createdAt + field {{ + ... on ProjectV2IterationField {{ + id + databaseId + }} + }} + }} + }} + }} + }} + }} + }} + }} + }} + """ + + headers = { + 'Authorization': f'Bearer {self.token}', + 'Accept': 'application/vnd.github.v3+json' + } + + response = requests.post( + 'https://api.github.com/graphql', + json={'query': query}, + headers=headers + ) + response.raise_for_status() + data = response.json() + + # Extract the relevant field values + field_values = [] + project_items = data.get('data', {}).get('repository', {}).get('issue', {}).get('projectItems', {}).get('nodes', []) + + for item in project_items: + for field_value in item.get('fieldValues', {}).get('nodes', []): + if field_value and 'field' in field_value: + field_id = str(field_value.get('field', {}).get('databaseId', '')) + if field_id == self.iteration_field_id: + field_values.append({ + 'field_id': field_id, + 'value': field_value.get('title'), + 'created_at': field_value.get('createdAt') + }) + + return field_values + + except Exception as e: + logger.error(f"Error fetching fields for issue #{issue_number}: {e}") + return [] + + def calculate_lead_times(self, start_date=None, end_date=None): + """Calculate deployment lead times for all issues""" + if not start_date: + end_date = datetime.now() + start_date = end_date - timedelta(days=180) # Default to last 6 months + + logger.info(f"Calculating lead times from {start_date.date()} to {end_date.date()}") + + lead_times = [] + page = 1 + + while True: + page_issues = self.get_all_falcon_issues(start_date, end_date, page) + if not page_issues: + break + + logger.info(f"Processing {len(page_issues)} issues from page {page}") + + for issue in page_issues: + issue_number = issue['number'] + + # Get when the issue was added to a sprint + sprint_date = self.get_iteration_start_date(issue_number) + + print(f"Sprint date: {sprint_date}") + + # Get when the issue was marked as deployed + deployed_date = self.get_deployment_label_date(issue_number) + + print(f"Deployed date: {deployed_date}") + + if sprint_date and deployed_date and deployed_date > sprint_date: + # Calculate lead time in days + lead_time = (deployed_date - sprint_date).total_seconds() / 86400 + + lead_times.append({ + 'issue_number': issue_number, + 'title': issue['title'], + 'url': issue['html_url'], + 'sprint_date': sprint_date, + 'deployed_date': deployed_date, + 'lead_time_days': lead_time + }) + + page += 1 + + return lead_times + + def generate_lead_time_report(self, start_date=None, end_date=None): + """Generate a report on deployment lead times""" + lead_times = self.calculate_lead_times(start_date, end_date) + + if not lead_times: + return {"issues": [], "average_lead_time": 0} + + # Calculate average lead time + avg_lead_time = mean([issue['lead_time_days'] for issue in lead_times]) + + return { + "issues": lead_times, + "average_lead_time": avg_lead_time + } + +def main(): + logger.info("Starting GitHub deployment lead time metrics collection...") + + token = os.getenv('GITHUB_TOKEN') + if not token: + raise ValueError("Please set GITHUB_TOKEN environment variable") + + team_label = os.getenv('TEAM_LABEL', 'Team : Falcon') + + metrics = DeploymentLeadTimeMetrics( + token=token, + owner='dotcms', + repo='core', + team_label=team_label + ) + + # Get data for the last 180 days by default + end_date = datetime.now() + start_date = end_date - timedelta(days=30) + + report = metrics.generate_lead_time_report(start_date, end_date) + + # Print results + print(f"\nDeployment Lead Time Report for Team Falcon ({start_date.date()} to {end_date.date()})") + print("=" * 80) + print(f"Average Lead Time: {report['average_lead_time']:.2f} days") + print("\nIssues analyzed:") + print("-" * 80) + + # Sort issues by lead time (ascending) + sorted_issues = sorted(report['issues'], key=lambda x: x['lead_time_days']) + + for issue in sorted_issues: + print(f"#{issue['issue_number']} - {issue['title']}") + print(f"Sprint Date: {issue['sprint_date'].date()}") + print(f"Deployed Date: {issue['deployed_date'].date()}") + print(f"Lead Time: {issue['lead_time_days']:.2f} days") + print(f"URL: {issue['url']}") + print() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/newVTL.feature b/test-karate/src/test/java/graphql/ftm/newVTL.feature index 8612625f35b5..b51764dbbb7c 100644 --- a/test-karate/src/test/java/graphql/ftm/newVTL.feature +++ b/test-karate/src/test/java/graphql/ftm/newVTL.feature @@ -1,4 +1,4 @@ - Feature: Create a Page + Feature: Upload FileAsset Background: * def fileName = __arg.fileName diff --git a/test-karate/src/test/java/graphql/ftm/setup.feature b/test-karate/src/test/java/graphql/ftm/setup.feature index 27f7572eadc8..6709a9b0f369 100644 --- a/test-karate/src/test/java/graphql/ftm/setup.feature +++ b/test-karate/src/test/java/graphql/ftm/setup.feature @@ -38,21 +38,29 @@ Feature: Setting up the Future Time Machine Test * def contentPieceTwoId = contentPieceTwo.map(result => Object.keys(result)[0]) * def contentPieceTwoId = contentPieceTwoId[0] - # Create a couple of new pieces of content + * def createContentPieceThreeResult = callonce read('classpath:graphql/ftm/newContent.feature') { contentTypeId: '#(contentTypeId)', title: 'test 3' } + * def contentPieceThree = createContentPieceThreeResult.response.entity.results + * def contentPieceThreeId = contentPieceThree.map(result => Object.keys(result)[0]) + * def contentPieceThreeId = contentPieceThreeId[0] + * def createBannerContentPieceOneResult = callonce read('classpath:graphql/ftm/newContent.feature') { contentTypeId: '#(bannerContentTypeId)', title: 'banner 1'} * def bannerContentPieceOne = createBannerContentPieceOneResult.response.entity.results * def bannerContentPieceOneId = bannerContentPieceOne.map(result => Object.keys(result)[0]) * def bannerContentPieceOneId = bannerContentPieceOneId[0] # Now lets create a new version for each piece of content - * def formatter = java.time.format.DateTimeFormatter.ofPattern('yyyy-MM-dd') - * def now = java.time.LocalDateTime.now() + * def formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + * def now = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC) * def formattedCurrentDateTime = now.format(formatter) * def futureDateTime = now.plusDays(10) * def formattedFutureDateTime = futureDateTime.format(formatter) + * def futureDateTimeInGraceWindow = now.plusMinutes(4) + * def formattedFutureDateTimeInGraceWindow = futureDateTimeInGraceWindow.format(formatter) + * karate.log('formattedFutureDateTimeInGraceWindow:', formattedFutureDateTimeInGraceWindow) * def newContentPiceOneVersion2 = callonce read('classpath:graphql/ftm/newContentVersion.feature') { contentTypeId: '#(contentTypeId)', identifier: '#(contentPieceOneId)', title: 'test 1 v2 (This ver will be publshed in the future)', publishDate: '#(formattedFutureDateTime)' } * def newContentPiceTwoVersion2 = callonce read('classpath:graphql/ftm/newContentVersion.feature') { contentTypeId: '#(contentTypeId)', identifier: '#(contentPieceTwoId)', title: 'test 2 v2' } + * def newContentPiceThreeVersion2 = callonce read('classpath:graphql/ftm/newContentVersion.feature') { contentTypeId: '#(contentTypeId)', identifier: '#(contentPieceThreeId)', title: 'test 3 v2', publishDate: '#(formattedFutureDateTimeInGraceWindow)' } * def newContentBannerOneVersion2 = callonce read('classpath:graphql/ftm/newContentVersion.feature') { contentTypeId: '#(bannerContentTypeId)', identifier: '#(bannerContentPieceOneId)', title: 'banner 1 v2', publishDate: '#(formattedFutureDateTime)' } # Lets create a new non-published piece of content wiht a publish date in the future @@ -72,7 +80,7 @@ Feature: Setting up the Future Time Machine Test * def pageId = pageId[0] # Now lets add the pieces of content to the page - * def publishPageResult = callonce read('classpath:graphql/ftm/publishPage.feature') { page_id: '#(pageId)', banner_content_ids: ['#(bannerContentPieceOneId)'], content_ids: ['#(contentPieceOneId)', '#(contentPieceTwoId)', '#(nonPublishedPieceId)'], container_id: '#(containerId)' } + * def publishPageResult = callonce read('classpath:graphql/ftm/publishPage.feature') { page_id: '#(pageId)', banner_content_ids: ['#(bannerContentPieceOneId)'], content_ids: ['#(contentPieceOneId)', '#(contentPieceTwoId)', '#(contentPieceThreeId)', '#(nonPublishedPieceId)'], container_id: '#(containerId)' } * karate.log('Page created and Published ::', pageUrl) diff --git a/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature b/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature index f1bee028fbae..9da1ebc13b3a 100644 --- a/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature +++ b/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature @@ -5,6 +5,9 @@ Feature: Test Time Machine functionality * def CONTENTLET_ONE_V1 = 'test 1' * def CONTENTLET_ONE_V2 = 'test 1 v2 (This ver will be publshed in the future)' * def CONTENTLET_TWO_V2 = 'test 2 v2' + * def CONTENTLET_THREE_V1 = 'test 3' + * def CONTENTLET_THREE_V2 = 'test 3 v2' + * def BANNER_CONTENTLET_ONE_V1 = 'banner 1' * def BANNER_CONTENTLET_ONE_V2 = 'banner 1 v2' * def NON_PUBLISHED_CONTENTLET = 'Working version Only! with publish date' @@ -53,7 +56,7 @@ Feature: Test Time Machine functionality * def titles = pageContents.map(x => x.title) # This is the first version of the content, test 1 v2 as the title says it will be published in the future * match titles contains CONTENTLET_ONE_V1 - # This is the second version of the content, This one is already published therefore it should be displayed + # This is the second version of the content, this one is already published therefore it should be displayed * match titles contains CONTENTLET_TWO_V2 # This is the first version of the banner content which is already published therefore it should be displayed * match titles contains BANNER_CONTENTLET_ONE_V1 @@ -74,6 +77,28 @@ Feature: Test Time Machine functionality * match rendered !contains NON_PUBLISHED_CONTENTLET * match rendered !contains BANNER_CONTENTLET_ONE_V2 + @positive @ftm + Scenario: Test Time Machine functionality when a publish date is provided within grace window expect the future content not to be displayed + Given url baseUrl + '/api/v1/page/render/'+pageUrl+'?language_id=1&mode=LIVE&publishDate='+formattedFutureDateTimeInGraceWindow + And headers commonHeaders + When method GET + Then status 200 + * karate.log('request date now:: ', java.time.LocalDateTime.now()) + * def pageContents = extractContentlets (response) + * def titles = pageContents.map(x => x.title) + # This is the first version of the content, this one is already published therefore it should be displayed + * match titles contains CONTENTLET_THREE_V1 + # This is the second version of the content, this one is already published but is within the FTM grace window, + # therefore it should not be displayed + * match titles !contains CONTENTLET_THREE_V2 + + * karate.log('pageContents:', pageContents) + # Check the rendered page. The same items included as contentlets should be displayed here too + * def rendered = response.entity.page.rendered + * karate.log('rendered:', rendered) + * match rendered contains CONTENTLET_THREE_V1 + * match rendered !contains CONTENTLET_THREE_V2 + @positive @ftm Scenario: Test Time Machine functionality when a publish date is provided expect the future content to be displayed