PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 0.7.0 All 126 releases
extendify / src / AutoLaunch / functions / plugins.js

plugins.js in Extendify 3.1.5, at src/AutoLaunch/functions/plugins.js

218 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { recordPluginActivity } from '@shared/api/DataApi';
2 import { digest } from '@shared/api/digest';
3 import { enableAutoUpdate } from '@shared/api/wp';
4 import apiFetch from '@wordpress/api-fetch';
5 import { addQueryArgs } from '@wordpress/url';
6
7 export const getActivePlugins = () =>
8 apiFetch({ path: 'extendify/v1/auto-launch/active-plugins' });
9
10 export const alreadyActive = (activePlugins, pluginSlug) =>
11 activePlugins?.filter((p) => p.includes(pluginSlug))?.length;
12
13 // WP unpacks every install through one shared dir; two at once corrupt both.
14 let installQueue = Promise.resolve();
15
16 // Re-installing is not free: WP unpacks before it finds the folder exists.
17 const installed = new Map();
18
19 const enqueue = (task) => {
20 const result = installQueue.then(task);
21 installQueue = result.catch(() => {});
22 return result;
23 };
24
25 const INSTALL_DRAIN_MS = 60_000;
26
27 // The install POST has no timeout; uncapped, one stall parks the build.
28 const waitForInstalls = () => {
29 let timer;
30 return Promise.race([
31 installQueue,
32 new Promise((resolve) => {
33 timer = setTimeout(resolve, INSTALL_DRAIN_MS);
34 }),
35 ]).finally(() => clearTimeout(timer));
36 };
37
38 const install = async (slug) => {
39 const fn = async () => {
40 const p = await apiFetch({
41 path: '/wp/v2/plugins',
42 method: 'POST',
43 data: { slug },
44 });
45 // Unawaited: no timeout on these, and a stall would hold the queue.
46 recordPluginActivity({ slug, source: 'auto-launch' });
47 enableAutoUpdate(p?.plugin);
48 return p;
49 };
50 try {
51 return await fn();
52 } catch (error) {
53 if (error?.code === 'folder_exists') {
54 // Already on disk — fetch its record so the caller can activate it.
55 return await getPlugin(slug);
56 }
57 try {
58 return await fn();
59 } catch (error) {
60 digest({
61 error,
62 details: { source: 'auto-launch', caller: 'installPlugin' },
63 });
64 return null;
65 }
66 }
67 };
68
69 export const installPlugin = (slug) =>
70 enqueue(async () => {
71 const known = installed.get(slug);
72 if (known) return { plugin: known };
73
74 const plugin = await install(slug);
75 if (plugin?.plugin) installed.set(slug, plugin.plugin);
76 return plugin;
77 });
78
79 export const getPlugin = async (slug) => {
80 const response = await apiFetch({
81 path: addQueryArgs('/wp/v2/plugins', { search: slug }),
82 });
83 return response?.find((p) => p.plugin?.split('/')[0] === slug);
84 };
85
86 export const activatePlugin = async (slug) => {
87 const fn = (s) =>
88 apiFetch({
89 path: `/wp/v2/plugins/${s}`,
90 method: 'POST',
91 data: { status: 'active' },
92 });
93
94 try {
95 await fn(slug);
96 return true;
97 } catch (_) {
98 try {
99 // try once more but get the slug first
100 const { plugin } = await getPlugin(slug);
101 await fn(plugin);
102 return true;
103 } catch (error) {
104 digest({
105 error,
106 details: { source: 'auto-launch', caller: 'activatePlugin' },
107 });
108 return false;
109 }
110 }
111 };
112
113 // Exact match, not substring: woocommerce-payments would mask a missing woocommerce.
114 const notActive = (activePlugins, slugs) =>
115 slugs.filter(
116 (slug) => !activePlugins?.some((path) => path.split('/')[0] === slug),
117 );
118
119 export const ensurePluginsActive = async (slugs) => {
120 // Two callers don't await this; a throw here silently skips every install.
121 const active = await getActivePlugins().catch((error) => {
122 digest({
123 error,
124 details: { source: 'auto-launch', caller: 'ensurePluginsActive' },
125 });
126 return [];
127 });
128 const missing = notActive(active, slugs);
129 if (!missing.length) return { failed: [] };
130
131 const onDisk = window.extSharedData?.installedPluginsSlugs ?? [];
132 const failed = [];
133 for (const slug of missing) {
134 try {
135 const plugin = onDisk.includes(slug) ? null : await installPlugin(slug);
136 const activated = await activatePlugin(plugin?.plugin ?? slug);
137 if (!activated) failed.push(slug);
138 } catch (_) {
139 failed.push(slug);
140 }
141 }
142 return { failed };
143 };
144
145 export const reportInactivePlugins = async (slugs) => {
146 // Falling back to an empty list would name every plugin as inactive.
147 const active = await getActivePlugins().catch(() => null);
148 if (!active) return;
149
150 const inactive = notActive(active, slugs);
151 if (!inactive.length) return;
152
153 digest({
154 error: { message: `Plugins inactive after launch: ${inactive.join(', ')}` },
155 details: {
156 source: 'auto-launch',
157 caller: 'reportInactivePlugins',
158 inactive,
159 },
160 });
161 };
162
163 // Currently this only processes patterns with placeholders
164 // by swapping out the placeholders with the actual code
165 // returns the patterns as blocks with the placeholders replaced
166 export const replacePlaceholderPatterns = async (patterns) => {
167 // Directly replace "blog-section" patterns using their replacement code, skipping the API call
168 patterns = patterns.map((pattern) => {
169 if (
170 pattern.patternTypes.includes('blog-section') &&
171 pattern.patternReplacementCode
172 ) {
173 return {
174 ...pattern,
175 code: pattern.patternReplacementCode,
176 };
177 }
178 return pattern;
179 });
180
181 const hasPlaceholders = patterns.filter((p) => p.patternReplacementCode);
182 if (!hasPlaceholders?.length) return patterns;
183
184 const activePlugins =
185 (await getActivePlugins())?.data?.map((path) => path.split('/')[0]) || [];
186
187 const pluginsActivity = patterns
188 .filter((p) => p.pluginDependency)
189 .map((p) => p.pluginDependency)
190 .filter((p) => !activePlugins.includes(p));
191
192 for (const plugin of pluginsActivity) {
193 recordPluginActivity({
194 slug: plugin,
195 source: 'auto-launch',
196 });
197 }
198
199 try {
200 return await processPlaceholders(patterns);
201 } catch (_e) {
202 // Try one more time (plugins installed may not be fully loaded)
203 return await processPlaceholders(patterns)
204 // If this fails, just return the original patterns
205 .catch(() => patterns);
206 }
207 };
208
209 // This endpoint installs pattern dependencies from PHP, outside the queue.
210 export const processPlaceholders = async (patterns) => {
211 await waitForInstalls();
212 return apiFetch({
213 path: '/extendify/v1/shared/process-placeholders',
214 method: 'POST',
215 data: { patterns },
216 });
217 };
218