@elementor/editor-audits 4.4.0-1074 → 4.4.0-1076

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-audits",
3
- "version": "4.4.0-1074",
3
+ "version": "4.4.0-1076",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,19 +39,19 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "4.4.0-1074",
43
- "@elementor/editor-mcp": "4.4.0-1074",
44
- "@elementor/editor-app-bar": "4.4.0-1074",
45
- "@elementor/editor-elements": "4.4.0-1074",
46
- "@elementor/editor-floating-panels": "4.4.0-1074",
47
- "@elementor/editor-notifications": "4.4.0-1074",
48
- "@elementor/editor-v1-adapters": "4.4.0-1074",
49
- "@elementor/events": "4.4.0-1074",
50
- "@elementor/http-client": "4.4.0-1074",
42
+ "@elementor/editor": "4.4.0-1076",
43
+ "@elementor/editor-mcp": "4.4.0-1076",
44
+ "@elementor/editor-app-bar": "4.4.0-1076",
45
+ "@elementor/editor-elements": "4.4.0-1076",
46
+ "@elementor/editor-floating-panels": "4.4.0-1076",
47
+ "@elementor/editor-notifications": "4.4.0-1076",
48
+ "@elementor/editor-v1-adapters": "4.4.0-1076",
49
+ "@elementor/events": "4.4.0-1076",
50
+ "@elementor/http-client": "4.4.0-1076",
51
51
  "@elementor/icons": "~1.77.0",
52
- "@elementor/locations": "4.4.0-1074",
53
- "@elementor/session": "4.4.0-1074",
54
- "@elementor/store": "4.4.0-1074",
52
+ "@elementor/locations": "4.4.0-1076",
53
+ "@elementor/session": "4.4.0-1076",
54
+ "@elementor/store": "4.4.0-1076",
55
55
  "@elementor/ui": "1.37.5"
56
56
  },
57
57
  "peerDependencies": {
@@ -1,19 +1,38 @@
1
1
  import { httpService } from '@elementor/http-client';
2
2
 
3
3
  import { type PageContextResponse } from '../types';
4
+ import { isNonceInvalidError, refreshAuditsNonce } from '../utils/session-expiration';
4
5
  import { getWindowConfig } from '../utils/window-config';
5
6
 
6
7
  export async function fetchPageContext( documentId: number, attachmentIds: number[] ): Promise< PageContextResponse > {
8
+ return requestPageContext( documentId, attachmentIds, true );
9
+ }
10
+
11
+ async function requestPageContext(
12
+ documentId: number,
13
+ attachmentIds: number[],
14
+ allowNonceRetry: boolean
15
+ ): Promise< PageContextResponse > {
7
16
  const { restNamespace, nonce } = getWindowConfig();
8
17
  const url = `${ restNamespace }/audits/page-context`;
9
18
 
10
- const response = await httpService().get< PageContextResponse >( url, {
11
- params: {
12
- document_id: documentId,
13
- attachment_ids: attachmentIds,
14
- },
15
- headers: { 'X-WP-Nonce': nonce },
16
- } );
19
+ try {
20
+ const response = await httpService().get< PageContextResponse >( url, {
21
+ params: {
22
+ document_id: documentId,
23
+ attachment_ids: attachmentIds,
24
+ },
25
+ headers: { 'X-WP-Nonce': nonce },
26
+ } );
27
+
28
+ return response.data;
29
+ } catch ( error ) {
30
+ if ( ! allowNonceRetry || ! isNonceInvalidError( error ) ) {
31
+ throw error;
32
+ }
33
+
34
+ await refreshAuditsNonce();
17
35
 
18
- return response.data;
36
+ return requestPageContext( documentId, attachmentIds, false );
37
+ }
19
38
  }
@@ -28,6 +28,7 @@ export const audit: Audit = {
28
28
  auditId: audit.id,
29
29
  label: __( 'No privacy policy page is set.', 'elementor' ),
30
30
  externalUrl: ctx.pageContext.privacy_settings_url,
31
+ ctaLabel: __( 'Create', 'elementor' ),
31
32
  },
32
33
  ],
33
34
  };
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ import { Button } from '@elementor/ui';
3
+
4
+ type Props = {
5
+ ctaLabel: string;
6
+ externalUrl: string;
7
+ };
8
+
9
+ export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
10
+ const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
11
+ event.stopPropagation();
12
+ event.preventDefault();
13
+
14
+ window.open( externalUrl, '_blank', 'noopener' );
15
+ };
16
+
17
+ return (
18
+ <Button variant="outlined" color="secondary" size="small" onClick={ handleClick }>
19
+ { ctaLabel }
20
+ </Button>
21
+ );
22
+ }
@@ -11,6 +11,7 @@ import { buildAngiePrompt } from '../utils/build-angie-prompt';
11
11
  import { onKeyboardClick } from '../utils/keyboard-click';
12
12
  import FixViolationWithAngie from './fix-violation-with-angie';
13
13
  import SeverityIcon from './severity-icons';
14
+ import ViolationCtaButton from './violation-cta-button';
14
15
  import ViolationIcon from './violation-icons';
15
16
 
16
17
  type Props = {
@@ -157,7 +158,18 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
157
158
  { violation.angieFix && (
158
159
  <FixViolationWithAngie prompt={ buildAngiePrompt( rowLabel ) } />
159
160
  ) }
160
- <EyeIcon className="violation-hover-icon" fontSize="tiny" aria-hidden={ true } />
161
+ { violation.ctaLabel && violation.externalUrl ? (
162
+ <ViolationCtaButton
163
+ ctaLabel={ violation.ctaLabel }
164
+ externalUrl={ violation.externalUrl }
165
+ />
166
+ ) : (
167
+ <EyeIcon
168
+ className="violation-hover-icon"
169
+ fontSize="tiny"
170
+ aria-hidden={ true }
171
+ />
172
+ ) }
161
173
  </Box>
162
174
  );
163
175
  } ) }
@@ -5,6 +5,7 @@ import { __useDispatch as useDispatch, __useSelector as useSelector } from '@ele
5
5
  import { runPageAudit } from '../runner';
6
6
  import { type GlobalState, selectError, selectReport, selectStatus, slice } from '../store';
7
7
  import { getPersistedReport, persistReport } from '../utils/report-storage';
8
+ import { SessionExpiredError } from '../utils/session-expiration';
8
9
 
9
10
  export function useAuditReport() {
10
11
  const status = useSelector( ( state: GlobalState ) => selectStatus( state ) );
@@ -35,6 +36,11 @@ export function useAuditReport() {
35
36
  dispatch( slice.actions.runSucceeded( nextReport ) );
36
37
  persistReport( documentIdToRun, nextReport );
37
38
  } catch ( e ) {
39
+ if ( e instanceof SessionExpiredError ) {
40
+ dispatch( slice.actions.runAborted() );
41
+ return;
42
+ }
43
+
38
44
  dispatch( slice.actions.runFailed( e instanceof Error ? e.message : 'Unknown error' ) );
39
45
  }
40
46
  };
@@ -26,6 +26,9 @@ export const slice = __createSlice( {
26
26
  state.status = 'error';
27
27
  state.error = action.payload;
28
28
  },
29
+ runAborted( state ) {
30
+ state.status = state.report ? 'ready' : 'idle';
31
+ },
29
32
  reportRestored( state, action: PayloadAction< PageAuditReport > ) {
30
33
  state.status = 'ready';
31
34
  state.report = action.payload;
package/src/types.ts CHANGED
@@ -38,6 +38,7 @@ export type AuditViolation = {
38
38
  label: string;
39
39
  detail?: string;
40
40
  angieFix?: boolean;
41
+ ctaLabel?: string;
41
42
  };
42
43
 
43
44
  export type PageContextResponse = {
@@ -0,0 +1,63 @@
1
+ import { getWindowConfig } from './window-config';
2
+
3
+ const NONCE_INVALID_CODE = 'rest_cookie_invalid_nonce';
4
+ const DEFAULT_AJAX_URL = '/wp-admin/admin-ajax.php';
5
+
6
+ type AjaxErrorLikeResponse = {
7
+ status?: number;
8
+ data?: { code?: string };
9
+ };
10
+
11
+ type AjaxErrorLike = {
12
+ response?: AjaxErrorLikeResponse;
13
+ };
14
+
15
+ type SessionWindow = Window & {
16
+ elementorCommon?: { ajax?: { config?: { url?: string } } };
17
+ };
18
+
19
+ export class SessionExpiredError extends Error {}
20
+
21
+ let nonceRefreshPromise: Promise< string > | null = null;
22
+
23
+ export function isNonceInvalidError( error: unknown ): boolean {
24
+ const response = ( error as AjaxErrorLike | undefined )?.response;
25
+
26
+ return response?.status === 403 && response?.data?.code === NONCE_INVALID_CODE;
27
+ }
28
+
29
+ export async function refreshAuditsNonce(): Promise< string > {
30
+ if ( nonceRefreshPromise ) {
31
+ return nonceRefreshPromise;
32
+ }
33
+
34
+ nonceRefreshPromise = requestFreshNonce();
35
+
36
+ try {
37
+ return await nonceRefreshPromise;
38
+ } finally {
39
+ nonceRefreshPromise = null;
40
+ }
41
+ }
42
+
43
+ async function requestFreshNonce(): Promise< string > {
44
+ const ajaxUrl = ( window as SessionWindow ).elementorCommon?.ajax?.config?.url ?? DEFAULT_AJAX_URL;
45
+ const url = new URL( ajaxUrl, window.location.origin );
46
+ url.searchParams.set( 'action', 'rest-nonce' );
47
+
48
+ const response = await fetch( url.toString(), { credentials: 'same-origin' } );
49
+
50
+ if ( ! response.ok ) {
51
+ throw new Error( `Failed to refresh audits nonce: HTTP ${ response.status }` );
52
+ }
53
+
54
+ const nonce = await response.text();
55
+
56
+ if ( ! nonce || '0' === nonce ) {
57
+ throw new SessionExpiredError( 'Session expired — received invalid nonce' );
58
+ }
59
+
60
+ window.elementorAudits = { ...getWindowConfig(), nonce };
61
+
62
+ return nonce;
63
+ }