Setting the file. One moment.
Wp Plugin Inventory · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page Script Wp Discovery
scripts/ wp-plugin-inventory.js
JavaScript · 273 lines · 11 KB
(
'node:path'
);
13 const { createProgressLogger , parseProgressArgs } = require ( '../../../lib/progress-log.js' );
14 const {
15 DEFAULT_TIMEOUT_MS ,
16 DEFAULT_RATE_LIMIT_RPM ,
17 DEFAULT_MAX_RETRIES ,
18 configureRateLimit ,
19 buildHeaders ,
20 normalizeBaseUrl ,
21 fetchJson ,
22 } = require ( '../lib/wp-http.js' );
23 const { pluginsRoot , loadProfiles , loadFingerprintAliases } = require ( '../lib/plugin-knowledge.js' );
24 const { detectPlugins } = require ( '../lib/wp-plugin-detect.js' );
25
26 let progress;
27
28 function printUsage () {
29 console. log ( `Usage:
30 node wp-plugin-inventory.js --base-url <url> --out-dir <dir> [auth options]
31
32 Required:
33 --base-url <url> WordPress site base URL
34 --out-dir <dir> Directory to write plugin-inventory.json into
35
36 Authentication options (same as wp-discovery.js):
37 --username <name> WordPress username for Application Password auth
38 --application-password <pw> WordPress Application Password
39 --api-key <token> API key/token for custom auth setups
40 --api-key-header <name> Header name for --api-key
41 --auth-header <'Name: Value'> Add a raw HTTP header. Can be repeated.
42
43 Optional:
44 --no-html-fingerprint Skip the public homepage fetch used for asset fingerprints
45 --timeout-ms <n> Request timeout in ms
46 --rate-limit-rpm <n> Max requests per minute
47 --max-retries <n> Retries on 429/503
48 --progress-log <path> Append progress NDJSON records to this file
49 --help Show this help text
50
51 GET /wp/v2/plugins requires an administrator credential. Without it the run continues and
52 records pluginListAvailable: false — detection then relies on REST namespaces, declared
53 routes, registered types/taxonomies, and public asset paths.
54 ` );
55 }
56
57 function parseArgs ( argv ) {
58 const args = {
59 authHeaders: [],
60 timeoutMs: DEFAULT_TIMEOUT_MS ,
61 rateLimitRpm: DEFAULT_RATE_LIMIT_RPM ,
62 maxRetries: DEFAULT_MAX_RETRIES ,
63 htmlFingerprint: true ,
64 };
65
66 for ( let i = 0 ; i < argv. length ; i += 1 ) {
67 const arg = argv[i];
68 const next = argv[i + 1 ];
69 switch (arg) {
70 case '--help' :
71 case '-h' :
72 args.help = true ;
73 break ;
74 case '--base-url' : args.baseUrl = next; i += 1 ; break ;
75 case '--out-dir' : args.outDir = next; i += 1 ; break ;
76 case '--username' : args.username = next; i += 1 ; break ;
77 case '--application-password' : args.applicationPassword = next; i += 1 ; break ;
78 case '--api-key' : args.apiKey = next; i += 1 ; break ;
79 case '--api-key-header' : args.apiKeyHeader = next; i += 1 ; break ;
80 case '--auth-header' : args.authHeaders. push (next); i += 1 ; break ;
81 case '--timeout-ms' : args.timeoutMs = Number. parseInt (next, 10 ); i += 1 ; break ;
82 case '--rate-limit-rpm' : args.rateLimitRpm = Number. parseInt (next, 10 ); i += 1 ; break ;
83 case '--max-retries' : args.maxRetries = Number. parseInt (next, 10 ); i += 1 ; break ;
84 case '--no-html-fingerprint' : args.htmlFingerprint = false ; break ;
85 default :
86 if (arg. startsWith ( '--' )) throw new Error ( `Unknown argument: ${ arg }` );
87 }
88 }
89
90 args.baseUrl = args.baseUrl || process.env. WP_BASE_URL || process.env. WP_SITE_URL ;
91 args.username = args.username || process.env. WP_USERNAME ;
92 args.applicationPassword = args.applicationPassword || process.env. WP_APPLICATION_PASSWORD ;
93 args.apiKey = args.apiKey || process.env. WP_API_KEY ;
94 args.apiKeyHeader = args.apiKeyHeader || process.env. WP_API_KEY_HEADER ;
95 if (args.authHeaders. length === 0 && process.env. WP_AUTH_HEADER ) {
96 args.authHeaders. push (process.env. WP_AUTH_HEADER );
97 }
98 if ( ! Number. isFinite (args.timeoutMs) || args.timeoutMs < 1000 ) args.timeoutMs = DEFAULT_TIMEOUT_MS ;
99 if ( ! Number. isFinite (args.rateLimitRpm) || args.rateLimitRpm < 1 ) args.rateLimitRpm = DEFAULT_RATE_LIMIT_RPM ;
100 if ( ! Number. isFinite (args.maxRetries) || args.maxRetries < 0 ) args.maxRetries = DEFAULT_MAX_RETRIES ;
101 return args;
102 }
103
104 async function fetchOptional ( baseUrl , routePath , options , label ) {
105 const response = await fetchJson (baseUrl, routePath, options);
106 if (response.ok && response.json !== undefined ) {
107 return { value: response.json, status: response.status, available: true };
108 }
109 return {
110 value: null ,
111 status: response.status,
112 available: false ,
113 reason: `${ label } unavailable: ${ response . status } ${ response . statusText }` ,
114 };
115 }
116
117 // Deliberately a plain fetch rather than the wp-http helper: the homepage is not a
118 // /wp-json route, and this signal is a best-effort extra that must never fail the run.
119 async function fetchHomepageHtml ( baseUrl , timeoutMs ) {
120 const controller = new AbortController ();
121 const timer = setTimeout (() => controller. abort (), Math. min (timeoutMs, 15000 ));
122 try {
123 const response = await fetch ( normalizeBaseUrl (baseUrl), {
124 signal: controller.signal,
125 headers: { accept: 'text/html' },
126 });
127 if ( ! response.ok) return '' ;
128 const text = await response. text ();
129 return text. slice ( 0 , 500000 );
130 } catch {
131 return '' ;
132 } finally {
133 clearTimeout (timer);
134 }
135 }
136
137 async function gatherInventory ({ baseUrl , headers , timeoutMs , htmlFingerprint = true , restIndex = null , logger = null }) {
138 const notes = [];
139 const options = { headers, method: 'GET' , timeoutMs, progress: logger, progressContext: { step: 'plugin-inventory' } };
140
141 let index = restIndex;
142 if ( ! index) {
143 const rootResponse = await fetchJson (baseUrl, '' , options);
144 if ( ! rootResponse.ok || ! rootResponse.json) {
145 throw new Error ( `Failed to fetch WordPress REST index from ${ rootResponse . url }: ${ rootResponse . status } ${ rootResponse . statusText }` );
146 }
147 index = rootResponse.json;
148 }
149
150 const [ pluginList , types , taxonomies , html ] = await Promise . all ([
151 fetchOptional (baseUrl, '/wp/v2/plugins' , options, 'GET /wp/v2/plugins' ),
152 fetchOptional (baseUrl, '/wp/v2/types' , options, 'GET /wp/v2/types' ),
153 fetchOptional (baseUrl, '/wp/v2/taxonomies' , options, 'GET /wp/v2/taxonomies' ),
154 htmlFingerprint ? fetchHomepageHtml (baseUrl, timeoutMs) : Promise . resolve ( '' ),
155 ]);
156 if ( ! pluginList.available) {
157 notes. push ( `${ pluginList . reason }. Plugin detection fell back to namespace, route, type, and asset fingerprints; installed-but-unprofiled plugins cannot be enumerated.` );
158 }
159 if ( ! types.available) notes. push ( `${ types . reason }. Generic custom-post-type derivation is unavailable.` );
160 if ( ! taxonomies.available) notes. push ( `${ taxonomies . reason }. Generic custom-taxonomy derivation is unavailable.` );
161 if (htmlFingerprint && ! html) notes. push ( 'Public homepage HTML could not be read; asset-path plugin fingerprints were skipped.' );
162
163 const knowledgeDir = pluginsRoot (path. resolve (__dirname, '..' ));
164 const profiles = loadProfiles (knowledgeDir);
165 const detection = detectPlugins ({
166 profiles,
167 restIndex: index,
168 pluginList: Array. isArray (pluginList.value) ? pluginList.value : null ,
169 types: types.value,
170 taxonomies: taxonomies.value,
171 htmlSources: html ? [html] : [],
172 fingerprintAliases: loadFingerprintAliases (knowledgeDir),
173 });
174
175 return {
176 restIndex: index,
177 types: types.value,
178 taxonomies: taxonomies.value,
179 // Raw signals are returned so the caller can re-run detection after sampling without
180 // re-fetching: the core-embedded pass needs record properties that do not exist yet.
181 pluginList: Array. isArray (pluginList.value) ? pluginList.value : null ,
182 htmlSources: html ? [html] : [],
183 detection,
184 notes,
185 profileCount: profiles. length ,
186 };
187 }
188
189 function inventoryPayload ({ generatedAt , baseUrl , authenticated , detection , notes , unprofiled = [], profileCount }) {
190 return {
191 generatedAt,
192 baseUrl,
193 authenticated,
194 pluginListAvailable: detection.pluginListAvailable,
195 profilesLoaded: profileCount,
196 notes,
197 detected: detection.detected,
198 unprofiled,
199 installedButUnprofiled: detection.installedButUnprofiled,
200 // Publicly fingerprinted plugins nothing else claimed — names only, no
201 // completeness claim, never route scope.
202 fingerprinted: detection.fingerprinted || [],
203 };
204 }
205
206 async function main () {
207 const parsed = parseProgressArgs (process.argv. slice ( 2 ));
208 progress = createProgressLogger ({
209 script: 'skills/replatform/resources/rp-source-wordpress/scripts/wp-plugin-inventory.js' ,
210 ... parsed.progress,
211 });
212 progress. start ( 'WordPress plugin inventory started' , { phase: 'discovery' });
213
214 const args = parseArgs (parsed.args);
215 if (args.help) {
216 printUsage ();
217 progress. complete ( 'WordPress plugin inventory help shown' , { phase: 'discovery' , step: 'help' });
218 return ;
219 }
220 if ( ! args.baseUrl || ! args.outDir) {
221 printUsage ();
222 progress. error ( 'Missing required plugin inventory arguments' , { phase: 'discovery' });
223 throw new Error ( 'Missing required arguments: --base-url and --out-dir are required.' );
224 }
225
226 configureRateLimit ({ rateLimitRpm: args.rateLimitRpm, maxRetries: args.maxRetries });
227 const headers = buildHeaders (args);
228 const authenticated = Boolean ((args.username && args.applicationPassword) || args.apiKey || args.authHeaders. length > 0 );
229
230 const result = await gatherInventory ({
231 baseUrl: args.baseUrl,
232 headers,
233 timeoutMs: args.timeoutMs,
234 htmlFingerprint: args.htmlFingerprint,
235 logger: progress,
236 });
237
238 const payload = inventoryPayload ({
239 generatedAt: new Date (). toISOString (),
240 baseUrl: normalizeBaseUrl (args.baseUrl),
241 authenticated,
242 detection: result.detection,
243 notes: result.notes,
244 profileCount: result.profileCount,
245 });
246
247 await fs. mkdir (args.outDir, { recursive: true });
248 const outPath = path. join (args.outDir, 'plugin-inventory.json' );
249 await fs. writeFile (outPath, `${ JSON . stringify ( payload , null , 2 ) } \n ` , 'utf8' );
250
251 console. log ( `Detected ${ payload . detected . length } profiled plugin(s); wrote ${ outPath }` );
252 progress. complete ( 'WordPress plugin inventory completed' , {
253 phase: 'discovery' ,
254 artifact: outPath,
255 count: payload.detected. length ,
256 unit: 'plugins' ,
257 });
258 }
259
260 if (require.main === module ) {
261 main (). catch (( error ) => {
262 console. error (error.stack || error.message);
263 if (progress) progress. error (error && error.message ? error.message : 'plugin inventory failed' , { phase: 'discovery' });
264 process.exitCode = 1 ;
265 });
266 }
267
268 module . exports = {
269 parseArgs,
270 gatherInventory,
271 inventoryPayload,
272 fetchHomepageHtml,
273 };