Skip to content

Commit 8a612a0

Browse files
committed
Fix bugs in wordpress installation and added toggle in php extensions
1 parent 0641e62 commit 8a612a0

2 files changed

Lines changed: 252 additions & 29 deletions

File tree

src/main/services/ProjectManager.js

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,25 +1362,33 @@ class ProjectManager {
13621362
const protocol = url.startsWith('https') ? https : http;
13631363

13641364
// On Windows, SSL certificate verification can fail. Use agent with fallback.
1365+
// Also explicitly use IPv4 (family: 4) because Node.js 20+ prefers IPv6 which fails on some networks for wordpress.org
13651366
const options = url.startsWith('https') ? {
1367+
family: 4,
13661368
agent: new https.Agent({
13671369
rejectUnauthorized: !retryWithoutVerify // First try with verification, then without
13681370
})
1369-
} : {};
1371+
} : { family: 4 };
13701372

13711373
const request = protocol.get(url, options, (response) => {
13721374
// Handle redirects
13731375
if (response.statusCode === 301 || response.statusCode === 302) {
1374-
file.close();
1375-
fs.unlinkSync(dest);
1376-
downloadFile(response.headers.location, dest, retryWithoutVerify);
1376+
file.close(() => {
1377+
try {
1378+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
1379+
} catch (e) {} // ignore unlink errors
1380+
downloadFile(response.headers.location, dest, retryWithoutVerify);
1381+
});
13771382
return;
13781383
}
13791384

13801385
if (response.statusCode !== 200) {
1381-
file.close();
1382-
fs.unlinkSync(dest);
1383-
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
1386+
file.close(() => {
1387+
try {
1388+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
1389+
} catch (e) {}
1390+
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
1391+
});
13841392
return;
13851393
}
13861394

@@ -1407,18 +1415,24 @@ class ProjectManager {
14071415
});
14081416

14091417
request.on('error', (err) => {
1410-
file.close();
1411-
fs.unlink(dest, () => { }); // Delete partial file
1418+
file.close(() => {
1419+
fs.unlink(dest, () => { }); // Delete partial file
1420+
});
1421+
1422+
// Format error message for AggregateError which has empty err.message
1423+
if (!err.message && err.errors && err.errors.length > 0) {
1424+
err.message = err.errors.map(e => e.message || e.code).join(', ');
1425+
}
14121426

14131427
// If SSL certificate error and we haven't retried yet, try without verification
14141428
if (!retryWithoutVerify && (err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
14151429
err.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' ||
1416-
err.message.includes('certificate') ||
1417-
err.message.includes('SSL'))) {
1430+
(err.message && (err.message.includes('certificate') || err.message.includes('SSL'))))) {
14181431
onOutput(' SSL certificate verification failed, retrying...', 'warning');
14191432
downloadFile(url, dest, true);
14201433
return;
14211434
}
1435+
14221436
reject(err);
14231437
});
14241438
};

src/renderer/src/components/PhpIniEditor.jsx

Lines changed: 227 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useState, useEffect } from 'react';
2-
import { X, Save, RotateCcw, FileText, AlertTriangle, Check } from 'lucide-react';
1+
import React, { useState, useEffect, useMemo } from 'react';
2+
import { X, Save, RotateCcw, FileText, AlertTriangle, Check, Search, Code, LayoutGrid, Power } from 'lucide-react';
33
import clsx from 'clsx';
44
import { useModal } from '../context/ModalContext';
55

@@ -12,10 +12,15 @@ function PhpIniEditor({ version, isOpen, onClose }) {
1212
const [error, setError] = useState(null);
1313
const [saved, setSaved] = useState(false);
1414
const [hasChanges, setHasChanges] = useState(false);
15+
16+
const [activeTab, setActiveTab] = useState('extensions');
17+
const [searchQuery, setSearchQuery] = useState('');
1518

1619
useEffect(() => {
1720
if (isOpen && version) {
1821
loadPhpIni();
22+
setActiveTab('extensions');
23+
setSearchQuery('');
1924
}
2025
}, [isOpen, version]);
2126

@@ -41,6 +46,106 @@ function PhpIniEditor({ version, isOpen, onClose }) {
4146
}
4247
};
4348

49+
// Parse extensions from php.ini content
50+
const extensions = useMemo(() => {
51+
if (!content) return [];
52+
53+
// List of widely used extensions to always show, even if missing from php.ini
54+
const WIDELY_USED_EXTENSIONS = [
55+
'bz2', 'curl', 'ffi', 'ftp', 'fileinfo', 'gd', 'gettext', 'gmp', 'intl',
56+
'imap', 'ldap', 'mbstring', 'exif', 'mysqli', 'oci8_12c', 'odbc', 'openssl',
57+
'pdo_firebird', 'pdo_mysql', 'pdo_oci', 'pdo_odbc', 'pdo_pgsql', 'pdo_sqlite',
58+
'pgsql', 'shmop', 'snmp', 'soap', 'sockets', 'sodium', 'sqlite3', 'tidy', 'xsl', 'zip',
59+
'redis', 'mongodb', 'imagick', 'memcached', 'xdebug', 'opcache', 'bcmath',
60+
'calendar', 'xmlrpc'
61+
];
62+
63+
const lines = content.split('\n');
64+
const extMap = new Map();
65+
66+
lines.forEach((line, index) => {
67+
const trimmed = line.trim();
68+
// Match extension=... or zend_extension=... with optional leading semicolon
69+
const match = trimmed.match(/^(;?)\s*(zend_extension|extension)\s*=\s*([^\s;]+)/i);
70+
71+
if (match) {
72+
let extName = match[3].replace(/^["']|["']$/g, '');
73+
// Clean up names for display
74+
const displayName = extName
75+
.replace(/^php_/, '')
76+
.replace(/\.dll$/, '')
77+
.replace(/\.so$/, '');
78+
79+
const ext = {
80+
lineIndex: index,
81+
isDisabled: match[1] === ';',
82+
type: match[2].toLowerCase(),
83+
name: extName,
84+
displayName,
85+
existsInFile: true,
86+
};
87+
88+
// If we see the exact same display name, prefer the uncommented one if conflict exists
89+
if (extMap.has(displayName)) {
90+
const existing = extMap.get(displayName);
91+
if (existing.isDisabled && !ext.isDisabled) {
92+
extMap.set(displayName, ext);
93+
}
94+
} else {
95+
extMap.set(displayName, ext);
96+
}
97+
}
98+
});
99+
100+
// Add widely used extensions that completely missing from the file
101+
WIDELY_USED_EXTENSIONS.forEach(extName => {
102+
if (!extMap.has(extName)) {
103+
extMap.set(extName, {
104+
lineIndex: -1, // Indicates it doesn't exist in file yet
105+
isDisabled: true, // Missing means it's not enabled
106+
type: extName === 'opcache' || extName === 'xdebug' ? 'zend_extension' : 'extension',
107+
name: extName, // We don't append .dll/.so strictly here, just the name is usually enough for modern PHP
108+
displayName: extName,
109+
existsInFile: false,
110+
});
111+
}
112+
});
113+
114+
return Array.from(extMap.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
115+
}, [content]);
116+
117+
const filteredExtensions = useMemo(() => {
118+
if (!searchQuery) return extensions;
119+
const query = searchQuery.toLowerCase();
120+
return extensions.filter(ext =>
121+
ext.displayName.toLowerCase().includes(query) ||
122+
ext.name.toLowerCase().includes(query)
123+
);
124+
}, [extensions, searchQuery]);
125+
126+
const toggleExtension = (ext) => {
127+
const lines = content.split('\n');
128+
129+
if (ext.existsInFile) {
130+
// Toggle existing line
131+
const line = lines[ext.lineIndex];
132+
if (line.match(/^\s*;/)) {
133+
// Remove the first semicolon
134+
lines[ext.lineIndex] = line.replace(/^(\s*);\s*/, '$1');
135+
} else {
136+
// Add a semicolon at the start
137+
lines[ext.lineIndex] = line.replace(/^(\s*)/, '$1;');
138+
}
139+
} else {
140+
// Doesn't exist, we must add it.
141+
// Easiest is to append to the end of the file
142+
lines.push(`\n; DevBox Pro automatically added extension`);
143+
lines.push(`${ext.type}=${ext.name}`);
144+
}
145+
146+
setContent(lines.join('\n'));
147+
};
148+
44149
const handleSave = async () => {
45150
setSaving(true);
46151
setError(null);
@@ -115,7 +220,7 @@ function PhpIniEditor({ version, isOpen, onClose }) {
115220

116221
return (
117222
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
118-
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-[900px] max-h-[80vh] flex flex-col overflow-hidden">
223+
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-[900px] max-h-[85vh] flex flex-col overflow-hidden">
119224
{/* Header */}
120225
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
121226
<div className="flex items-center gap-3">
@@ -125,7 +230,7 @@ function PhpIniEditor({ version, isOpen, onClose }) {
125230
PHP {version} Configuration
126231
</h3>
127232
<p className="text-sm text-gray-500 dark:text-gray-400">
128-
Edit php.ini settings for PHP {version}
233+
Manage extensions and php.ini settings
129234
</p>
130235
</div>
131236
</div>
@@ -137,8 +242,36 @@ function PhpIniEditor({ version, isOpen, onClose }) {
137242
</button>
138243
</div>
139244

245+
{/* Tabs */}
246+
<div className="flex items-center px-6 bg-gray-50/50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700">
247+
<button
248+
onClick={() => setActiveTab('extensions')}
249+
className={clsx(
250+
'px-4 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2',
251+
activeTab === 'extensions'
252+
? 'border-primary-500 text-primary-600 dark:text-primary-400'
253+
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600'
254+
)}
255+
>
256+
<LayoutGrid className="w-4 h-4" />
257+
Extensions
258+
</button>
259+
<button
260+
onClick={() => setActiveTab('editor')}
261+
className={clsx(
262+
'px-4 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2',
263+
activeTab === 'editor'
264+
? 'border-primary-500 text-primary-600 dark:text-primary-400'
265+
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600'
266+
)}
267+
>
268+
<Code className="w-4 h-4" />
269+
Raw Editor
270+
</button>
271+
</div>
272+
140273
{/* Content */}
141-
<div className="flex-1 overflow-hidden flex flex-col">
274+
<div className="flex-1 overflow-hidden flex flex-col bg-gray-50/30 dark:bg-gray-900/20">
142275
{loading ? (
143276
<div className="flex-1 flex items-center justify-center">
144277
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
@@ -149,27 +282,101 @@ function PhpIniEditor({ version, isOpen, onClose }) {
149282
{error}
150283
</div>
151284
) : (
152-
<textarea
153-
value={content}
154-
onChange={(e) => setContent(e.target.value)}
155-
className="flex-1 w-full p-4 font-mono text-sm bg-gray-900 text-gray-100 resize-none focus:outline-none"
156-
spellCheck={false}
157-
style={{ minHeight: '400px' }}
158-
/>
285+
<>
286+
{activeTab === 'extensions' && (
287+
<div className="flex-1 flex flex-col overflow-hidden">
288+
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
289+
<div className="relative">
290+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
291+
<input
292+
type="text"
293+
placeholder="Search extensions..."
294+
value={searchQuery}
295+
onChange={(e) => setSearchQuery(e.target.value)}
296+
className="w-full pl-9 pr-4 py-2 bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent"
297+
/>
298+
</div>
299+
</div>
300+
<div className="flex-1 overflow-y-auto p-4">
301+
{filteredExtensions.length === 0 ? (
302+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
303+
No extensions found matching "{searchQuery}"
304+
</div>
305+
) : (
306+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
307+
{filteredExtensions.map((ext) => (
308+
<div
309+
key={`${ext.lineIndex}-${ext.name}`}
310+
className={clsx(
311+
"flex items-center justify-between p-3 rounded-lg border transition-all duration-200",
312+
ext.isDisabled
313+
? "bg-white dark:bg-gray-800/60 border-gray-200 dark:border-gray-700/50"
314+
: "bg-primary-50/50 dark:bg-primary-900/10 border-primary-200 dark:border-primary-800"
315+
)}
316+
>
317+
<div className="min-w-0 pr-3">
318+
<h4 className="font-medium text-sm text-gray-900 dark:text-gray-100 truncate flex items-center gap-2">
319+
{ext.displayName}
320+
{ext.type === 'zend_extension' && (
321+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 font-medium">
322+
Zend
323+
</span>
324+
)}
325+
</h4>
326+
<p className="text-xs text-gray-500 dark:text-gray-500 mt-0.5 truncate font-mono">
327+
{ext.name}
328+
</p>
329+
</div>
330+
<button
331+
onClick={() => toggleExtension(ext)}
332+
className={clsx(
333+
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2",
334+
ext.isDisabled ? "bg-gray-200 dark:bg-gray-700" : "bg-primary-500"
335+
)}
336+
role="switch"
337+
aria-checked={!ext.isDisabled}
338+
>
339+
<span
340+
aria-hidden="true"
341+
className={clsx(
342+
"pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
343+
ext.isDisabled ? "translate-x-0" : "translate-x-4"
344+
)}
345+
/>
346+
</button>
347+
</div>
348+
))}
349+
</div>
350+
)}
351+
</div>
352+
</div>
353+
)}
354+
355+
{activeTab === 'editor' && (
356+
<textarea
357+
value={content}
358+
onChange={(e) => setContent(e.target.value)}
359+
className="flex-1 w-full p-4 font-mono text-sm bg-[#1e1e1e] text-gray-300 resize-none focus:outline-none leading-relaxed"
360+
spellCheck={false}
361+
placeholder="Paste or edit php.ini content here..."
362+
style={{ minHeight: '400px' }}
363+
/>
364+
)}
365+
</>
159366
)}
160367
</div>
161368

162369
{/* Footer */}
163-
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
370+
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
164371
<div className="flex items-center gap-2">
165372
{hasChanges && (
166-
<span className="text-sm text-amber-500 flex items-center gap-1">
373+
<span className="text-sm font-medium text-amber-600 dark:text-amber-400 flex items-center gap-1.5 bg-amber-50 dark:bg-amber-900/20 px-2 py-1 rounded-md">
167374
<AlertTriangle className="w-4 h-4" />
168375
Unsaved changes
169376
</span>
170377
)}
171378
{saved && (
172-
<span className="text-sm text-green-500 flex items-center gap-1">
379+
<span className="text-sm font-medium text-green-600 dark:text-green-400 flex items-center gap-1.5 bg-green-50 dark:bg-green-900/20 px-2 py-1 rounded-md">
173380
<Check className="w-4 h-4" />
174381
Saved successfully
175382
</span>
@@ -179,7 +386,7 @@ function PhpIniEditor({ version, isOpen, onClose }) {
179386
<button
180387
onClick={handleReset}
181388
disabled={loading || saving}
182-
className="btn-secondary"
389+
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors"
183390
>
184391
<RotateCcw className="w-4 h-4" />
185392
Reset to Default
@@ -188,8 +395,10 @@ function PhpIniEditor({ version, isOpen, onClose }) {
188395
onClick={handleSave}
189396
disabled={loading || saving || !hasChanges}
190397
className={clsx(
191-
'btn-primary',
192-
(!hasChanges || saving) && 'opacity-50 cursor-not-allowed'
398+
'px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 flex items-center gap-2 transition-all',
399+
(!hasChanges || saving)
400+
? 'bg-primary-400 cursor-not-allowed'
401+
: 'bg-primary-600 hover:bg-primary-700 shadow-sm'
193402
)}
194403
>
195404
<Save className="w-4 h-4" />

0 commit comments

Comments
 (0)