@elementor/editor-audits 4.3.0-1063 → 4.3.0-beta2
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/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +325 -195
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +288 -158
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/api/page-context-client.ts +27 -8
- package/src/audits/privacy-policy.ts +1 -0
- package/src/audits/scan-for-cookies.ts +30 -0
- package/src/components/status-section.tsx +1 -1
- package/src/components/violation-cta-button.tsx +22 -0
- package/src/components/violation-row.tsx +19 -4
- package/src/hooks/use-audit-report.ts +6 -0
- package/src/register-audits.ts +2 -0
- package/src/register-promotions.ts +8 -0
- package/src/store/slice.ts +3 -0
- package/src/types.ts +4 -0
- package/src/utils/audit-status-summary.ts +11 -2
- package/src/utils/is-scored-audit.ts +5 -0
- package/src/utils/session-expiration.ts +63 -0
- package/src/utils/severity-counts.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-audits",
|
|
3
|
-
"version": "4.3.0-
|
|
3
|
+
"version": "4.3.0-beta2",
|
|
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.3.0-
|
|
43
|
-
"@elementor/editor-mcp": "4.3.0-
|
|
44
|
-
"@elementor/editor-app-bar": "4.3.0-
|
|
45
|
-
"@elementor/editor-elements": "4.3.0-
|
|
46
|
-
"@elementor/editor-floating-panels": "4.3.0-
|
|
47
|
-
"@elementor/editor-notifications": "4.3.0-
|
|
48
|
-
"@elementor/editor-v1-adapters": "4.3.0-
|
|
49
|
-
"@elementor/events": "4.3.0-
|
|
50
|
-
"@elementor/http-client": "4.3.0-
|
|
42
|
+
"@elementor/editor": "4.3.0-beta2",
|
|
43
|
+
"@elementor/editor-mcp": "4.3.0-beta2",
|
|
44
|
+
"@elementor/editor-app-bar": "4.3.0-beta2",
|
|
45
|
+
"@elementor/editor-elements": "4.3.0-beta2",
|
|
46
|
+
"@elementor/editor-floating-panels": "4.3.0-beta2",
|
|
47
|
+
"@elementor/editor-notifications": "4.3.0-beta2",
|
|
48
|
+
"@elementor/editor-v1-adapters": "4.3.0-beta2",
|
|
49
|
+
"@elementor/events": "4.3.0-beta2",
|
|
50
|
+
"@elementor/http-client": "4.3.0-beta2",
|
|
51
51
|
"@elementor/icons": "~1.77.0",
|
|
52
|
-
"@elementor/locations": "4.3.0-
|
|
53
|
-
"@elementor/session": "4.3.0-
|
|
54
|
-
"@elementor/store": "4.3.0-
|
|
52
|
+
"@elementor/locations": "4.3.0-beta2",
|
|
53
|
+
"@elementor/session": "4.3.0-beta2",
|
|
54
|
+
"@elementor/store": "4.3.0-beta2",
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
36
|
+
return requestPageContext( documentId, attachmentIds, false );
|
|
37
|
+
}
|
|
19
38
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
|
|
3
|
+
import { type Audit } from '../types';
|
|
4
|
+
|
|
5
|
+
export const audit: Audit = {
|
|
6
|
+
id: 'audits/scan-for-cookies',
|
|
7
|
+
title: __( 'Scan for cookies', 'elementor' ),
|
|
8
|
+
description: __(
|
|
9
|
+
"Your site may be setting cookies you haven't disclosed. Some countries require a banner even without a full consent policy. Scan to see what needs disclosure.",
|
|
10
|
+
'elementor'
|
|
11
|
+
),
|
|
12
|
+
fixHint: __( 'Run a Cookiez scan on this page to see which cookies need disclosure.', 'elementor' ),
|
|
13
|
+
categories: [ 'compliance' ],
|
|
14
|
+
severity: 'info',
|
|
15
|
+
weight: 0,
|
|
16
|
+
evaluate: ( ctx ) => {
|
|
17
|
+
const isReady = ctx.pageContext.cookiez_plugin_installed && ctx.pageContext.cookiez_plugin_active;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
status: 'fail',
|
|
21
|
+
violations: [
|
|
22
|
+
{
|
|
23
|
+
auditId: audit.id,
|
|
24
|
+
label: __( 'This page has not been scanned for cookies yet.', 'elementor' ),
|
|
25
|
+
externalUrl: isReady ? ctx.pageContext.cookiez_scan_url : ctx.pageContext.cookiez_plugin_action_url,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -15,7 +15,7 @@ type Props = {
|
|
|
15
15
|
export default function StatusSection( { label, count, color = 'default', defaultExpanded = false, children }: Props ) {
|
|
16
16
|
const [ expanded, setExpanded ] = useState( defaultExpanded );
|
|
17
17
|
|
|
18
|
-
if ( count === 0 ) {
|
|
18
|
+
if ( React.Children.count( children ) === 0 ) {
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -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
|
+
}
|
|
@@ -8,9 +8,11 @@ import { __ } from '@wordpress/i18n';
|
|
|
8
8
|
import { focusViolation } from '../hooks/focus-violation';
|
|
9
9
|
import { type AuditMeta, type AuditViolation } from '../types';
|
|
10
10
|
import { buildAngiePrompt } from '../utils/build-angie-prompt';
|
|
11
|
+
import { isScoredAudit } from '../utils/is-scored-audit';
|
|
11
12
|
import { onKeyboardClick } from '../utils/keyboard-click';
|
|
12
13
|
import FixViolationWithAngie from './fix-violation-with-angie';
|
|
13
14
|
import SeverityIcon from './severity-icons';
|
|
15
|
+
import ViolationCtaButton from './violation-cta-button';
|
|
14
16
|
import ViolationIcon from './violation-icons';
|
|
15
17
|
|
|
16
18
|
type Props = {
|
|
@@ -33,9 +35,11 @@ function StatusIndicator( { audit, violations }: Pick< Props, 'audit' | 'violati
|
|
|
33
35
|
if ( violations ) {
|
|
34
36
|
return (
|
|
35
37
|
<>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
{ isScoredAudit( audit ) && (
|
|
39
|
+
<Typography variant="caption" color="text.secondary" fontWeight="bold">
|
|
40
|
+
{ violations.length }
|
|
41
|
+
</Typography>
|
|
42
|
+
) }
|
|
39
43
|
<SeverityIcon severity={ audit.severity } />
|
|
40
44
|
</>
|
|
41
45
|
);
|
|
@@ -157,7 +161,18 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
|
|
|
157
161
|
{ violation.angieFix && (
|
|
158
162
|
<FixViolationWithAngie prompt={ buildAngiePrompt( rowLabel ) } />
|
|
159
163
|
) }
|
|
160
|
-
|
|
164
|
+
{ violation.ctaLabel && violation.externalUrl ? (
|
|
165
|
+
<ViolationCtaButton
|
|
166
|
+
ctaLabel={ violation.ctaLabel }
|
|
167
|
+
externalUrl={ violation.externalUrl }
|
|
168
|
+
/>
|
|
169
|
+
) : (
|
|
170
|
+
<EyeIcon
|
|
171
|
+
className="violation-hover-icon"
|
|
172
|
+
fontSize="tiny"
|
|
173
|
+
aria-hidden={ true }
|
|
174
|
+
/>
|
|
175
|
+
) }
|
|
161
176
|
</Box>
|
|
162
177
|
);
|
|
163
178
|
} ) }
|
|
@@ -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
|
};
|
package/src/register-audits.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as preferGlobalColors from './audits/prefer-global-colors';
|
|
|
15
15
|
import * as preferGlobalFonts from './audits/prefer-global-fonts';
|
|
16
16
|
import * as privacyPolicy from './audits/privacy-policy';
|
|
17
17
|
import * as robotsNoindex from './audits/robots-noindex';
|
|
18
|
+
import * as scanForCookies from './audits/scan-for-cookies';
|
|
18
19
|
import * as sectionsAndColumns from './audits/sections-and-columns';
|
|
19
20
|
import * as siteIdentity from './audits/site-identity';
|
|
20
21
|
import * as tooManyWidgets from './audits/too-many-widgets';
|
|
@@ -47,6 +48,7 @@ const AUDITS: Audit[] = [
|
|
|
47
48
|
privacyPolicy.audit,
|
|
48
49
|
accessibilityPolicy.audit,
|
|
49
50
|
cookiePolicy.audit,
|
|
51
|
+
scanForCookies.audit,
|
|
50
52
|
];
|
|
51
53
|
|
|
52
54
|
export function registerAllAudits(): void {
|
|
@@ -49,6 +49,14 @@ export const PROMOTIONS: PromotionConfig[] = [
|
|
|
49
49
|
run.result.status === 'fail' ? __( 'Generate cookie policy', 'elementor' ) : null,
|
|
50
50
|
getCtaUrl: firstFailExternalUrl,
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
auditId: 'audits/scan-for-cookies',
|
|
54
|
+
icon: ElementorCookieIcon,
|
|
55
|
+
ctaLabel: __( 'Scan', 'elementor' ),
|
|
56
|
+
formatSubtitle: ( run ) =>
|
|
57
|
+
run.result.status === 'fail' ? __( 'Scan this page for cookies', 'elementor' ) : null,
|
|
58
|
+
getCtaUrl: firstFailExternalUrl,
|
|
59
|
+
},
|
|
52
60
|
{
|
|
53
61
|
auditId: 'audits/images-too-large',
|
|
54
62
|
icon: ShieldHalfFilledIcon,
|
package/src/store/slice.ts
CHANGED
|
@@ -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 = {
|
|
@@ -65,6 +66,9 @@ export type PageContextResponse = {
|
|
|
65
66
|
ally_plugin_url: string;
|
|
66
67
|
cookiez_plugin_active: boolean;
|
|
67
68
|
cookiez_plugin_url: string;
|
|
69
|
+
cookiez_plugin_installed: boolean;
|
|
70
|
+
cookiez_plugin_action_url: string;
|
|
71
|
+
cookiez_scan_url: string;
|
|
68
72
|
image_optimization_plugin_active: boolean;
|
|
69
73
|
image_optimization_plugin_url: string;
|
|
70
74
|
site_identity: {
|
|
@@ -2,6 +2,7 @@ import { type ChipProps } from '@elementor/ui';
|
|
|
2
2
|
import { __ } from '@wordpress/i18n';
|
|
3
3
|
|
|
4
4
|
import { type AuditCategory, type AuditResult, type AuditRun, type PageAuditReport } from '../types';
|
|
5
|
+
import { isScoredAudit } from './is-scored-audit';
|
|
5
6
|
import { sortFailedAuditResults } from './sort-failed-audits';
|
|
6
7
|
|
|
7
8
|
export type AuditStatusGroup = 'fail' | 'pass' | 'skipped';
|
|
@@ -37,7 +38,11 @@ export function partitionAuditResults(
|
|
|
37
38
|
switch ( run.result.status ) {
|
|
38
39
|
case 'fail':
|
|
39
40
|
failed.push( { ...run, result: run.result } );
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
if ( isScoredAudit( run.audit ) ) {
|
|
43
|
+
totalViolations += run.result.violations.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
break;
|
|
42
47
|
case 'pass':
|
|
43
48
|
passed.push( { ...run, result: run.result } );
|
|
@@ -61,7 +66,11 @@ export function auditStatusDisplayCounts( report: PageAuditReport ): Record< Aud
|
|
|
61
66
|
let skipped = 0;
|
|
62
67
|
let totalViolations = 0;
|
|
63
68
|
|
|
64
|
-
for ( const { result } of report.auditResults ) {
|
|
69
|
+
for ( const { audit, result } of report.auditResults ) {
|
|
70
|
+
if ( ! isScoredAudit( audit ) ) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
switch ( result.status ) {
|
|
66
75
|
case 'fail':
|
|
67
76
|
totalViolations += result.violations.length;
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { __, sprintf } from '@wordpress/i18n';
|
|
2
2
|
|
|
3
3
|
import { type AuditCategory, type AuditSeverity, type PageAuditReport } from '../types';
|
|
4
|
+
import { isScoredAudit } from './is-scored-audit';
|
|
4
5
|
|
|
5
6
|
export type SeverityCounts = Record< AuditSeverity, number >;
|
|
6
7
|
|
|
@@ -18,6 +19,10 @@ export function countSeverities( report: PageAuditReport, category?: AuditCatego
|
|
|
18
19
|
continue;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
if ( ! isScoredAudit( audit ) ) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
counts[ audit.severity ] += result.violations.length;
|
|
22
27
|
}
|
|
23
28
|
|