Setting the file. One moment.
Wp Redirect Sources · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page 39.41
PLUGIN READ PLANS
206
function parseRankMathSources
— line 206
This file
Number 39.38
Position 38 of 92
Type JavaScript
Size 24 KB
Lines 461 lib/ wp-redirect-sources.js
JavaScript · 461 lines · 24 KB
13
// Coverage per provider is one of:
14 // extracted rows read completely
15 // confirmed_empty a complete, successful read returned zero rows
16 // unreadable read failed, partial, ambiguous or no channel
17 // unsupported rules live where this pipeline cannot read them (server/CDN, options)
18 // Logs, 404 records, hit counters and caches are never rules.
19
20 // Redirect providers are profile data, not code: a plugin profile entity with a
21 // `redirectSource` block declares where its rules live (see plugins/README.md). A new plugin
22 // whose storage matches a known format needs only a profile; a new storage format needs one
23 // normalizer in FORMATS below.
24 // redirectSource: {
25 // provider: 'redirection', stable id; prefixes every ruleKey, never renamed
26 // format: 'redirection', one of FORMATS
27 // tables: { rules: 'redirection_items', groups: 'redirection_groups' }, bare names
28 // storedIn: 'options', instead of tables: rules sit where reads are denied
29 // pluginSlugs: ['seo-by-rank-math-pro'], optional extra slugs (pro/alias builds)
30 // }
31 const FORMATS = Object. freeze ({
32 redirection: { tableRoles: [ 'rules' , 'groups' ], requiredRoles: [ 'rules' ] },
33 'rank-math' : { tableRoles: [ 'rules' ], requiredRoles: [ 'rules' ] },
34 'csv-export' : { tableRoles: [], requiredRoles: [] },
35 });
36
37 function pluginFolder ( pluginFileId ) {
38 return String (pluginFileId || '' ). split ( '/' )[ 0 ] || null ;
39 }
40
41 // Pure: profiles in, provider definitions out (keyed by provider id).
42 function providersFromProfiles ( profiles = []) {
43 const providers = {};
44 for ( const profile of profiles) {
45 for ( const entity of profile.entities || []) {
46 const def = entity.redirectSource;
47 if ( ! def) continue ;
48 const slugs = new Set ([profile.plugin, ... ((profile.detect && profile.detect.pluginFileIds) || []). map (pluginFolder), ... (def.pluginSlugs || [])]. filter (Boolean));
49 const tables = def.tables || {};
50 providers[def.provider] = {
51 displayName: profile.displayName,
52 profile: profile.plugin,
53 entity: entity.entity,
54 format: def.format,
55 pluginSlugs: [ ... slugs]. sort (),
56 tables: Object. values (tables),
57 ruleTable: tables.rules || null ,
58 groupsTable: tables.groups || null ,
59 storedIn: def.storedIn || null ,
60 };
61 }
62 }
63 return providers;
64 }
65
66 let defaultProviders = null ;
67 function loadDefaultProviders () {
68 if ( ! defaultProviders) {
69 const { loadProfiles , pluginsRoot } = require ( './plugin-knowledge.js' );
70 defaultProviders = Object. freeze ( providersFromProfiles ( loadProfiles ( pluginsRoot ( require ( 'node:path' ). resolve (__dirname, '..' )))));
71 }
72 return defaultProviders;
73 }
74
75 // Profile validation for a redirectSource block (called from plugin-knowledge.js).
76 function validateRedirectSource ( entity , label , errors ) {
77 const def = entity.redirectSource;
78 if (def === undefined ) return ;
79 if ( ! def || typeof def !== 'object' || Array. isArray (def)) { errors. push ( `${ label }: redirectSource must be an object` ); return ; }
80 if ( ! def.provider || ! / ^ [a-z0-9-] +$ / . test (def.provider)) errors. push ( `${ label }: redirectSource.provider must be a lowercase id (it prefixes every ruleKey)` );
81 const format = FORMATS [def.format];
82 if ( ! format) { errors. push ( `${ label }: redirectSource.format must be one of ${ Object . keys ( FORMATS ). join ( ', ' ) }` ); return ; }
83 if (def.storedIn !== undefined && def.storedIn !== 'options' ) errors. push ( `${ label }: redirectSource.storedIn may only be "options"` );
84 const tables = def.tables || {};
85 if (def.tables !== undefined && ( typeof def.tables !== 'object' || Array. isArray (def.tables))) errors. push ( `${ label }: redirectSource.tables must be an object of role -> bare table name` );
86 for ( const [ role , name ] of Object. entries (tables)) {
87 if ( ! format.tableRoles. includes (role)) errors. push ( `${ label }: redirectSource.tables.${ role } is not a table role of format ${ def . format }` );
88 if ( typeof name !== 'string' || ! / ^ [A-Za-z0-9_] +$ / . test (name)) errors. push ( `${ label }: redirectSource.tables.${ role } must be a bare table name` );
89 }
90 if ( ! def.storedIn) {
91 for ( const role of format.requiredRoles) if ( ! tables[role]) errors. push ( `${ label }: redirectSource.tables.${ role } is required for format ${ def . format }` );
92 }
93 if (format.tableRoles. length && ! def.storedIn && entity.channel !== 'db-only' ) errors. push ( `${ label }: a table-backed redirectSource needs channel db-only` );
94 if ((def.storedIn || def.format === 'csv-export' ) && entity.channel !== 'export-file' ) errors. push ( `${ label }: an options- or export-backed redirectSource needs channel export-file` );
95 if (def.pluginSlugs !== undefined && ( ! Array. isArray (def.pluginSlugs) || def.pluginSlugs. some (( slug ) => typeof slug !== 'string' || ! slug))) errors. push ( `${ label }: redirectSource.pluginSlugs must be an array of slugs` );
96 if (entity.capability !== undefined && entity.capability !== 'content.url-redirects' ) errors. push ( `${ label }: a redirectSource entity must carry capability content.url-redirects` );
97 }
98
99 // Resolve a provider table among the discovered physical table names. The prefix is whatever
100 // the site uses; several matches (for example multisite sub-site tables) are ambiguous.
101 function resolveTable ( bareName , physicalTables = [], { prefix = null } = {}) {
102 if (prefix !== null ) {
103 const exact = `${ prefix }${ bareName }` ;
104 return physicalTables. includes (exact) ? { ok: true , physicalTable: exact } : { ok: false , reason: 'table_not_found' };
105 }
106 const matches = physicalTables. filter (( name ) => name. endsWith (bareName) && / ^ [A-Za-z0-9_] *$ / . test (name. slice ( 0 , - bareName. length )));
107 if (matches. length === 1 ) return { ok: true , physicalTable: matches[ 0 ] };
108 if (matches. length === 0 ) return { ok: false , reason: 'table_not_found' };
109 return { ok: false , reason: 'ambiguous_table' , candidates: matches };
110 }
111
112 // Minimal PHP unserialize for the scalar/array shapes plugins store (a, s, i, d, b, N).
113 // Works on bytes because PHP string lengths are byte counts.
114 function phpUnserialize ( input ) {
115 const buf = Buffer. from ( String (input), 'utf8' );
116 let pos = 0 ;
117 function readUntil ( byte ) {
118 const end = buf. indexOf (byte, pos);
119 if (end < 0 ) throw new Error ( 'php-unserialize: truncated' );
120 const value = buf. slice (pos, end). toString ( 'utf8' );
121 pos = end + 1 ;
122 return value;
123 }
124 function parse () {
125 const type = String. fromCharCode (buf[pos]);
126 if (type === 'N' ) { pos += 2 ; return null ; }
127 pos += 2 ;
128 if (type === 'i' ) return Number. parseInt ( readUntil ( 0x3b ), 10 );
129 if (type === 'd' ) return Number. parseFloat ( readUntil ( 0x3b ));
130 if (type === 'b' ) return readUntil ( 0x3b ) === '1' ;
131 if (type === 's' ) {
132 const length = Number. parseInt ( readUntil ( 0x3a ), 10 );
133 if (buf[pos] !== 0x22 ) throw new Error ( 'php-unserialize: expected quote' );
134 const value = buf. slice (pos + 1 , pos + 1 + length). toString ( 'utf8' );
135 pos += 1 + length + 2 ;
136 return value;
137 }
138 if (type === 'a' ) {
139 const count = Number. parseInt ( readUntil ( 0x3a ), 10 );
140 pos += 1 ; // {
141 const entries = [];
142 for ( let i = 0 ; i < count; i += 1 ) entries. push ([ parse (), parse ()]);
143 pos += 1 ; // }
144 const isList = entries. every (([ key ], index ) => key === index);
145 return isList ? entries. map (([, value ]) => value) : Object. fromEntries (entries);
146 }
147 throw new Error ( `php-unserialize: unsupported type ${ type }` );
148 }
149 return parse ();
150 }
151
152 function leadingSlash ( value ) {
153 const v = String (value || '' ). trim ();
154 if ( ! v) return v;
155 if ( / ^ [a-z][a-z0-9+.-] * :/ i . test (v) || v. startsWith ( '/' )) return v;
156 return `/${ v }` ;
157 }
158
159 function toStatus ( value ) {
160 const n = Number. parseInt (value, 10 );
161 return Number. isInteger (n) ? n : null ;
162 }
163
164 function normalizeRedirectionRows ( items , groups , { provider = 'redirection' , providerActive , physicalTable , extractedAt }) {
165 const groupEnabled = new Map ((groups || []). map (( g ) => [ String (g.id), g.status === undefined || g.status === 'enabled' ]));
166 const groupModule = new Map ((groups || []). map (( g ) => [ String (g.id), String (g.module_id || '1' )]));
167 return items. map (( row ) => {
168 const conditions = [];
169 let matchType = String (row.regex) === '1' || row.regex === true ? 'regex' : 'exact' ;
170 const kind = row.match_type || 'url' ;
171 if (kind === 'server' ) {
172 let server = null ;
173 try { server = JSON . parse (row.match_data || '{}' )?.source?.server || null ; } catch { server = null ; }
174 let host = null ;
175 try { host = server ? new URL (server).hostname : null ; } catch { host = null ; }
176 conditions. push (host ? { type: 'host' , value: host } : { type: 'server' });
177 } else if (kind !== 'url' ) {
178 conditions. push ({ type: kind });
179 }
180 if ((row.action_type || 'url' ) !== 'url' ) matchType = 'other' ;
181 // Apache/Nginx module groups are server-config exports, not rules WordPress serves.
182 if (groupModule. get ( String (row.group_id)) && groupModule. get ( String (row.group_id)) !== '1' ) conditions. push ({ type: 'server_module' });
183 const enabled = providerActive && row.status === 'enabled' && groupEnabled. get ( String (row.group_id)) !== false ;
184 return {
185 schemaVersion: 1 ,
186 ruleKey: `${ provider }:${ row . id }` ,
187 provider,
188 storage: { kind: 'table' , name: physicalTable },
189 stableId: String (row.id),
190 enabled,
191 disabledReason: enabled ? null : ( ! providerActive ? 'provider_inactive' : 'rule_or_group_disabled' ),
192 matchType,
193 source: row.url,
194 destination: typeof row.action_data === 'string' && ! row.action_data. startsWith ( 'a:' ) ? row.action_data : null ,
195 statusCode: toStatus (row.action_code),
196 priority: toStatus (row.position),
197 conditions,
198 language: null ,
199 extractedAt,
200 };
201 });
202 }
203
204 const RANK_MATH_COMPARISON = { exact: 'exact' , start: 'prefix' , end: 'suffix' , contains: 'contains' , regex: 'regex' };
205
206 function parseRankMathSources ( value ) {
207 if (Array. isArray (value)) return value;
208 const text = String (value || '' );
209 if (text. startsWith ( '[' ) || text. startsWith ( '{' )) {
210 const parsed = JSON . parse (text);
211 return Array. isArray (parsed) ? parsed : Object. values (parsed);
212 }
213 const parsed = phpUnserialize (text);
214 return Array. isArray (parsed) ? parsed : Object. values (parsed || {});
215 }
216
217 function normalizeRankMathRows ( rows , { provider = 'rank-math' , providerActive , physicalTable , extractedAt }) {
218 const out = [];
219 const errors = [];
220 for ( const row of rows) {
221 let sources;
222 try {
223 sources = parseRankMathSources (row.sources);
224 } catch (error) {
225 errors. push ({ id: String (row.id), reason: `unparseable_sources: ${ error . message }` });
226 continue ;
227 }
228 sources. forEach (( source , index ) => {
229 const enabled = providerActive && row.status === 'active' ;
230 const ignore = String ((source && source.ignore) || '' );
231 out. push ({
232 schemaVersion: 1 ,
233 ruleKey: `${ provider }:${ row . id }:${ index }` ,
234 provider,
235 storage: { kind: 'table' , name: physicalTable },
236 stableId: `${ row . id }:${ index }` ,
237 sourceRuleId: `${ row . id }:${ crypto . createHash ( 'sha256' ). update ( String ( source ?. pattern || '' )). digest ( 'hex' ). slice ( 0 , 16 ) }` ,
238 enabled,
239 disabledReason: enabled ? null : ( ! providerActive ? 'provider_inactive' : `status_${ row . status || 'unknown'}` ),
240 matchType: RANK_MATH_COMPARISON [source && source.comparison] || 'other' ,
241 source: leadingSlash (source && source.pattern),
242 destination: row.url_to ? leadingSlash (row.url_to) : null ,
243 statusCode: toStatus (row.header_code),
244 priority: null ,
245 conditions: ignore. includes ( 'case' ) ? [{ type: 'ignore_case' }] : [],
246 language: null ,
247 extractedAt,
248 });
249 });
250 }
251 return { rules: out, errors };
252 }
253
254 // Minimal CSV (quoted fields, commas, CRLF) for operator-supplied exports.
255 function parseCsv ( text ) {
256 const rows = [];
257 let field = '' ;
258 let row = [];
259 let quoted = false ;
260 for ( let i = 0 ; i < text. length ; i += 1 ) {
261 const ch = text[i];
262 if (quoted) {
263 if (ch === '"' && text[i + 1 ] === '"' ) { field += '"' ; i += 1 ; } else if (ch === '"' ) quoted = false ; else field += ch;
264 } else if (ch === '"' ) quoted = true ;
265 else if (ch === ',' ) { row. push (field); field = '' ; } else if (ch === ' \n ' || ch === ' \r ' ) {
266 if (ch === ' \r ' && text[i + 1 ] === ' \n ' ) i += 1 ;
267 row. push (field); rows. push (row); row = []; field = '' ;
268 } else field += ch;
269 }
270 if (field || row. length ) { row. push (field); rows. push (row); }
271 return rows. filter (( r ) => r. some (( cell ) => cell. trim ()));
272 }
273
274 // Operator export: header row with from/to/status (Yoast: origin/target/type/format).
275 function normalizeExportCsv ( provider , text , { extractedAt , fileRef }) {
276 const [ header , ... rows ] = parseCsv ( String (text || '' ));
277 if ( ! header) return { rules: [], errors: [{ reason: 'empty_export' }] };
278 const col = ( names ) => header. findIndex (( h ) => names. includes (h. trim (). toLowerCase ()));
279 const from = col ([ 'from' , 'origin' , 'source' ]);
280 const to = col ([ 'to' , 'target' , 'destination' ]);
281 const status = col ([ 'status' , 'type' , 'code' ]);
282 const format = col ([ 'format' , 'match' ]);
283 if (from < 0 || to < 0 ) return { rules: [], errors: [{ reason: 'export_missing_from_or_to_column' }] };
284 const rules = rows. map (( cells , index ) => {
285 const matchType = format >= 0 && /regex/ i . test (cells[format] || '' ) ? 'regex' : 'exact' ;
286 return {
287 schemaVersion: 1 ,
288 ruleKey: `${ provider }:export:${ index + 1 }` ,
289 provider,
290 storage: { kind: 'export' , name: fileRef },
291 stableId: `export:${ index + 1 }` ,
292 enabled: true ,
293 disabledReason: null ,
294 matchType,
295 // A pattern is kept verbatim; only exact paths are normalized to site-relative form.
296 source: matchType === 'exact' ? leadingSlash (cells[from]) : cells[from],
297 destination: cells[to] ? (matchType === 'exact' ? leadingSlash (cells[to]) : cells[to]) : null ,
298 statusCode: status >= 0 ? toStatus (cells[status]) : 301 ,
299 priority: null ,
300 conditions: [],
301 language: null ,
302 extractedAt,
303 };
304 });
305 return { rules, errors: [] };
306 }
307
308 // input: {
309 // plugins: [{ slug, active }], detected plugins (from wp-plugin-inventory)
310 // physicalTables: [name], bridge table listing; null when no bridge
311 // tablePrefix: string|null, prefix reported by supported discovery, if any
312 // tables: { <bare>: { rows, complete, error } }, bridge reads by bare table name
313 // exports: { <provider>: { text, fileRef } }, operator-supplied exports
314 // serverRulesSuspected: [{ provider, evidence }], server/CDN rule stores seen elsewhere
315 // }
316 function extractRedirectSources ( input , { providers = loadDefaultProviders () } = {}) {
317 const extractedAt = input.extractedAt || new Date (). toISOString ();
318 const plugins = input.plugins || [];
319 const coverage = [];
320 const rules = [];
321 const bySlug = new Map (plugins. map (( p ) => [p.slug, p]));
322 for ( const [ provider , def ] of Object. entries (providers)) {
323 const plugin = def.pluginSlugs. map (( slug ) => bySlug. get (slug)). find (Boolean);
324 const supplied = input.exports && input.exports[provider];
325 if ( ! plugin && ! supplied) continue ;
326 const providerActive = Boolean (plugin ? plugin.active : true );
327 if (supplied) {
328 const result = normalizeExportCsv (provider, supplied.text, { extractedAt, fileRef: supplied.fileRef || 'operator-export' });
329 rules. push ( ... result.rules. map (( r ) => (providerActive ? r : { ... r, enabled: false , disabledReason: 'provider_inactive' })));
330 coverage. push ({ provider, status: result.errors. length ? 'unreadable' : (result.rules. length ? 'extracted' : 'confirmed_empty' ), channel: 'export' , reason: result.errors[ 0 ]?.reason || null , ruleCount: result.rules. length , providerActive });
331 continue ;
332 }
333 if (def.storedIn === 'options' || ! def.ruleTable) {
334 coverage. push ({ provider, status: 'unsupported' , channel: 'none' , reason: def.storedIn === 'options' ? 'rules_stored_in_options_table; supply the plugin redirect export (CSV)' : 'rules_not_in_a_readable_table; supply the plugin redirect export (CSV)' , ruleCount: 0 , providerActive });
335 continue ;
336 }
337 if ( ! input.physicalTables) {
338 coverage. push ({ provider, status: 'unreadable' , channel: 'none' , reason: 'no_structure_bridge; supply a redirect export' , ruleCount: 0 , providerActive });
339 continue ;
340 }
341 const resolved = resolveTable (def.ruleTable, input.physicalTables, { prefix: input.tablePrefix ?? null });
342 if ( ! resolved.ok) {
343 // A failed schema read also leaves the table unlisted; report that error, not "not found".
344 const readError = (input.tables || {})[def.ruleTable]?.error;
345 coverage. push ({ provider, status: 'unreadable' , channel: 'structure-bridge' , reason: readError || resolved.reason, ruleCount: 0 , providerActive });
346 continue ;
347 }
348 const read = (input.tables || {})[def.ruleTable];
349 if ( ! read || read.error || read.complete !== true || ! Array. isArray (read.rows)) {
350 coverage. push ({ provider, status: 'unreadable' , channel: 'structure-bridge' , reason: read?.error || (read ? 'partial_read' : 'not_read' ), ruleCount: 0 , providerActive, physicalTable: resolved.physicalTable });
351 continue ;
352 }
353 let normalized;
354 let errors = [];
355 if (def.format === 'redirection' ) {
356 const groupsRead = def.groupsTable ? (input.tables || {})[def.groupsTable] : null ;
357 // Group status disables rules, so a declared groups table must be read completely too.
358 if (def.groupsTable && read.rows. length && ( ! groupsRead || groupsRead.complete !== true )) {
359 coverage. push ({ provider, status: 'unreadable' , channel: 'structure-bridge' , reason: 'groups_not_read' , ruleCount: 0 , providerActive, physicalTable: resolved.physicalTable });
360 continue ;
361 }
362 normalized = normalizeRedirectionRows (read.rows, groupsRead ? groupsRead.rows : [], { provider, providerActive, physicalTable: resolved.physicalTable, extractedAt });
363 } else {
364 const result = normalizeRankMathRows (read.rows, { provider, providerActive, physicalTable: resolved.physicalTable, extractedAt });
365 normalized = result.rules;
366 errors = result.errors;
367 }
368 rules. push ( ... normalized);
369 coverage. push ({
370 provider,
371 status: errors. length ? 'unreadable' : (read.rows. length ? 'extracted' : 'confirmed_empty' ),
372 channel: 'structure-bridge' ,
373 reason: errors. length ? `${ errors . length } unparseable row(s)` : null ,
374 ruleCount: normalized. length ,
375 providerActive,
376 physicalTable: resolved.physicalTable,
377 });
378 }
379 for ( const server of input.serverRulesSuspected || []) {
380 coverage. push ({ provider: server.provider, status: 'unsupported' , channel: 'none' , reason: `server/CDN rules outside WordPress (${ server . evidence }); request an export from the host` , ruleCount: 0 , providerActive: true });
381 }
382 for ( const rule of rules) {
383 const capability = input.targetCapabilities?.[rule.ruleKey];
384 if ( ! capability) continue ;
385 if ( typeof capability !== 'object' || Array. isArray (capability) ||
386 typeof capability.productSupported !== 'boolean' ||
387 (capability.automationSupported !== undefined && typeof capability.automationSupported !== 'boolean' ) ||
388 typeof capability.evidence !== 'string' || ! capability.evidence. trim ()) {
389 throw new Error ( `targetCapabilities for ${ rule . ruleKey } requires supported flags and evidence` );
390 }
391 rule.capability = capability;
392 }
393 return { rules, coverage: { schemaVersion: 1 , generatedAt: extractedAt, providers: coverage } };
394 }
395
396
397 // Plugin-coverage rows for content.url-redirects, annotated with what discovery actually
398 // captured. Pure. A row stays `pending` (the Wix side is applied at cutover, not at import);
399 // what changes is that it says how many rules were captured, and its read blocker clears only
400 // when every provider's capture is complete (`extracted` or `confirmed_empty`).
401 const REDIRECT_CAPABILITY = 'content.url-redirects' ;
402 const COMPLETE_CAPTURE = new Set ([ 'extracted' , 'confirmed_empty' ]);
403
404 function annotateRedirectCoverageRows ( rows , captureCoverage , { providers = loadDefaultProviders () } = {}) {
405 const captured = new Map (((captureCoverage && captureCoverage.providers) || []). map (( row ) => [row.provider, row]));
406 return rows. map (( row ) => {
407 if (row.capability !== REDIRECT_CAPABILITY || row.recognized !== true ) return row;
408 const defs = Object. entries (providers). filter (([, def ]) => (row.plugins || []). includes (def.profile));
409 if (defs. length === 0 ) return row;
410 const providerRows = defs. map (([ provider , def ]) => {
411 const hit = captured. get (provider);
412 return hit
413 ? { provider, plugin: def.profile, entity: def.entity, status: hit.status, channel: hit.channel || null , ruleCount: hit.ruleCount || 0 , reason: hit.reason || null }
414 : { provider, plugin: def.profile, entity: def.entity, status: 'not_captured' , channel: null , ruleCount: 0 , reason: 'no capture recorded for this provider' };
415 });
416 const complete = providerRows. every (( p ) => COMPLETE_CAPTURE . has (p.status));
417 const recordCounts = { ... (row.recordCounts || {}) };
418 for ( const p of providerRows) if ( COMPLETE_CAPTURE . has (p.status)) recordCounts[p.entity] = (recordCounts[p.entity] || 0 ) + p.ruleCount;
419 const total = providerRows. reduce (( sum , p ) => sum + p.ruleCount, 0 );
420 const names = defs. map (([, def ]) => def.displayName). join ( ', ' );
421 const failed = providerRows. filter (( p ) => ! COMPLETE_CAPTURE . has (p.status));
422 const capturedEntities = new Set (providerRows. filter (( p ) => COMPLETE_CAPTURE . has (p.status)). map (( p ) => p.entity));
423 // The same entity name can belong to two providers; keep a blocker while any of them failed.
424 const failedEntities = new Set (failed. map (( p ) => p.entity));
425 return {
426 ... row,
427 recordCounts,
428 blocked: (row.blocked || []). filter (( b ) => ! (capturedEntities. has (b.entity) && ! failedEntities. has (b.entity))),
429 redirectCapture: {
430 complete,
431 ruleCount: total,
432 providers: providerRows,
433 artifacts: [ 'state/url-preservation/source-redirects.ndjson' , 'state/url-preservation/source-redirect-coverage.json' ],
434 },
435 userImpact: complete
436 ? `${ total } redirect rule(s) captured from ${ names }. They are installed and checked on Wix at cutover, after the website exists; until then, old links that rely on them are not yet protected.`
437 : `The redirect rules of ${ failed . map (( p ) => p . plugin ). join ( ', ' ) } could not be read or saved (${ failed . map (( p ) => `${ p . status }: ${ p . reason }` ). join ( '; ' ) }). Old links that rely on them will break after cutover unless the rules are supplied.` ,
438 action: complete
439 ? 'None at import. At cutover, run url-redirects.js plan, apply and verify.'
440 : failed. some (( p ) => p.status === 'not_recorded' )
441 ? 'Requires Import Operator: discovery read the redirect rules but could not save them (see the reason). Fix that state file or lock, then re-run discovery or url-redirects.js sources.'
442 : 'Requires Import Operator: re-run discovery with the wix-wp-plugin-v2 bridge connected, or export the plugin \' s redirects as CSV and pass them to url-redirects.js sources --input.' ,
443 };
444 });
445 }
446
447 module . exports = {
448 FORMATS,
449 REDIRECT_CAPABILITY,
450 annotateRedirectCoverageRows,
451 get PROVIDERS () { return loadDefaultProviders (); },
452 providersFromProfiles,
453 loadDefaultProviders,
454 validateRedirectSource,
455 resolveTable,
456 phpUnserialize,
457 normalizeRedirectionRows,
458 normalizeRankMathRows,
459 normalizeExportCsv,
460 extractRedirectSources,
461 };