Fix registration upload size, mail helper env variables, and verification rate limits
This commit is contained in:
+6
-6
@@ -28,12 +28,12 @@ services:
|
|||||||
- INTERNAL_AUTH_KEY=${INTERNAL_AUTH_KEY:-internal_dev_key}
|
- INTERNAL_AUTH_KEY=${INTERNAL_AUTH_KEY:-internal_dev_key}
|
||||||
- BASIC_API_USER=webgis-api
|
- BASIC_API_USER=webgis-api
|
||||||
- BASIC_API_PASS=${BASIC_API_PASS:-api_pass}
|
- BASIC_API_PASS=${BASIC_API_PASS:-api_pass}
|
||||||
- SMTP_HOST=smtp.gmail.com
|
- SMTP_HOST=${SMTP_HOST:-smtp.gmail.com}
|
||||||
- SMTP_PORT=587
|
- SMTP_PORT=${SMTP_PORT:-587}
|
||||||
- SMTP_USER=${SMTP_USER:-dev@example.com}
|
- SMTP_USER=${SMTP_USER:-}
|
||||||
- SMTP_PASS=${SMTP_PASS:-dev_pass}
|
- SMTP_PASS=${SMTP_PASS:-}
|
||||||
- SMTP_ENCRYPTION=tls
|
- SMTP_ENCRYPTION=${SMTP_ENCRYPTION:-tls}
|
||||||
- MAIL_FROM=${SMTP_USER:-dev@example.com}
|
- MAIL_FROM=${MAIL_FROM:-}
|
||||||
- MAIL_FROM_NAME=PovertyMapping
|
- MAIL_FROM_NAME=PovertyMapping
|
||||||
volumes:
|
volumes:
|
||||||
- poverty_uploads:/var/www/html/uploads
|
- poverty_uploads:/var/www/html/uploads
|
||||||
|
|||||||
@@ -544,6 +544,28 @@ function openAuthModal(mode){
|
|||||||
if(switchToRegister) switchToRegister.onclick = function(){ openAuthModal('register'); };
|
if(switchToRegister) switchToRegister.onclick = function(){ openAuthModal('register'); };
|
||||||
if(switchToLogin) switchToLogin.onclick = function(){ openAuthModal('login'); };
|
if(switchToLogin) switchToLogin.onclick = function(){ openAuthModal('login'); };
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
|
if(mode === 'register'){
|
||||||
|
const orgProof = document.getElementById('org_proof_input');
|
||||||
|
if(orgProof){
|
||||||
|
orgProof.addEventListener('change', function(){
|
||||||
|
const f = (orgProof.files && orgProof.files[0]) ? orgProof.files[0] : null;
|
||||||
|
if(!f) return;
|
||||||
|
const maxSize = 5 * 1024 * 1024;
|
||||||
|
const allowed = ['jpg','jpeg','png','svg','pdf'];
|
||||||
|
const ext = (f.name || '').split('.').pop().toLowerCase();
|
||||||
|
if(f.size > maxSize){
|
||||||
|
showToast('Ukuran file bukti organisasi maksimal 5MB', 'error', 5000);
|
||||||
|
orgProof.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!allowed.includes(ext)){
|
||||||
|
showToast('Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.', 'error', 5000);
|
||||||
|
orgProof.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const form = document.getElementById('auth-modal-form');
|
const form = document.getElementById('auth-modal-form');
|
||||||
form.onsubmit = async function(ev){
|
form.onsubmit = async function(ev){
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@@ -615,6 +637,29 @@ function openAuthModal(mode){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client-side rate limiting check
|
||||||
|
const limitKey = 'webgis_limit_' + btoa(email);
|
||||||
|
let limitInfo = { attempts: 0, lastTime: 0, lockedUntil: 0 };
|
||||||
|
try{
|
||||||
|
const cached = localStorage.getItem(limitKey);
|
||||||
|
if(cached) limitInfo = JSON.parse(cached);
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if(limitInfo.lockedUntil && now < limitInfo.lockedUntil){
|
||||||
|
const diff = limitInfo.lockedUntil - now;
|
||||||
|
const hours = Math.floor(diff / (3600 * 1000));
|
||||||
|
const mins = Math.ceil((diff % (3600 * 1000)) / (60 * 1000));
|
||||||
|
showToast(`Batas pengiriman kode tercapai. Anda harus menunggu ${hours} jam ${mins} menit lagi.`, 'error', 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(limitInfo.lastTime && now - limitInfo.lastTime < 30 * 1000){
|
||||||
|
const remaining = Math.ceil((30 * 1000 - (now - limitInfo.lastTime)) / 1000);
|
||||||
|
showToast(`Silakan tunggu ${remaining} detik sebelum mengirim kembali.`, 'error', 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sendCodeBtn.disabled = true;
|
sendCodeBtn.disabled = true;
|
||||||
sendCodeBtn.textContent = 'Mengirim...';
|
sendCodeBtn.textContent = 'Mengirim...';
|
||||||
|
|
||||||
@@ -628,23 +673,58 @@ function openAuthModal(mode){
|
|||||||
const jr = await resp.json().catch(()=>null);
|
const jr = await resp.json().catch(()=>null);
|
||||||
|
|
||||||
if(!resp.ok || (jr && jr.success === false)){
|
if(!resp.ok || (jr && jr.success === false)){
|
||||||
showToast(jr && jr.error ? jr.error : 'Gagal mengirim kode', 'error', 4000);
|
const errMessage = jr && jr.message ? jr.message : (jr && jr.error ? jr.error : 'Gagal mengirim kode');
|
||||||
|
showToast(errMessage, 'error', 5000);
|
||||||
|
|
||||||
|
// If locked on server side, sync client side state
|
||||||
|
if(jr && jr.error === 'locked_24h' && jr.locked_until){
|
||||||
|
limitInfo.lockedUntil = jr.locked_until;
|
||||||
|
try{ localStorage.setItem(limitKey, JSON.stringify(limitInfo)); }catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
sendCodeBtn.disabled = false;
|
sendCodeBtn.disabled = false;
|
||||||
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update client side rate limiting on success
|
||||||
|
limitInfo.attempts = (limitInfo.lockedUntil && now >= limitInfo.lockedUntil) ? 1 : (limitInfo.attempts + 1);
|
||||||
|
limitInfo.lastTime = now;
|
||||||
|
if(limitInfo.attempts >= 3){
|
||||||
|
limitInfo.lockedUntil = now + 24 * 3600 * 1000;
|
||||||
|
} else {
|
||||||
|
limitInfo.lockedUntil = 0;
|
||||||
|
}
|
||||||
|
try{ localStorage.setItem(limitKey, JSON.stringify(limitInfo)); }catch(e){}
|
||||||
|
|
||||||
authRegisterState.code_sent = true;
|
authRegisterState.code_sent = true;
|
||||||
authRegisterState.email = email;
|
authRegisterState.email = email;
|
||||||
if(jr.debug_code){
|
if(jr.debug_code){
|
||||||
authRegisterState.code = jr.debug_code;
|
authRegisterState.code = jr.debug_code;
|
||||||
|
const codeInput = form.querySelector('input[name="verify_code"]');
|
||||||
|
if(codeInput){
|
||||||
|
codeInput.value = jr.debug_code;
|
||||||
|
showToast('Gagal mengirim email. Menggunakan kode pengujian: ' + jr.debug_code, 'warning', 8000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('Kode verifikasi dikirim ke email Anda!', 'success', 4000);
|
showToast('Kode verifikasi dikirim ke email Anda!', 'success', 4000);
|
||||||
sendCodeBtn.textContent = 'Kode Terkirim ✓';
|
|
||||||
setTimeout(() => {
|
// Cooldown countdown timer
|
||||||
sendCodeBtn.disabled = false;
|
let cooldown = 30;
|
||||||
}, 3000);
|
sendCodeBtn.disabled = true;
|
||||||
|
sendCodeBtn.textContent = `Kirim Ulang (${cooldown}s)`;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
cooldown--;
|
||||||
|
if(cooldown <= 0){
|
||||||
|
clearInterval(timer);
|
||||||
|
sendCodeBtn.disabled = false;
|
||||||
|
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
||||||
|
} else {
|
||||||
|
sendCodeBtn.textContent = `Kirim Ulang (${cooldown}s)`;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
}catch(e){
|
}catch(e){
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Gagal mengirim kode verifikasi', 'error', 5000);
|
showToast('Gagal mengirim kode verifikasi', 'error', 5000);
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ if(file_exists($dotenvPath . '.env')){
|
|||||||
|
|
||||||
function env($k, $default = null){
|
function env($k, $default = null){
|
||||||
$val = $_ENV[$k] ?? null;
|
$val = $_ENV[$k] ?? null;
|
||||||
if($val === null){ $val = getenv($k); }
|
if($val === null || $val === ''){ $val = getenv($k); }
|
||||||
return $val !== false ? $val : $default;
|
return ($val !== false && $val !== '') ? $val : $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function send_mail_phpmailer($to, $subject, $body, $isHtml = false){
|
function send_mail_phpmailer($to, $subject, $body, $isHtml = false){
|
||||||
|
|||||||
@@ -40,7 +40,38 @@ if(!$verify_code || strlen($verify_code) !== 6){
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
// require uploaded proof file
|
// require uploaded proof file
|
||||||
if(!isset($_FILES['org_proof']) || $_FILES['org_proof']['error'] !== UPLOAD_ERR_OK){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'missing_org_proof']); exit; }
|
if(!isset($_FILES['org_proof']) || $_FILES['org_proof']['error'] !== UPLOAD_ERR_OK){
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success'=>false,'error'=>'missing_org_proof']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['org_proof'];
|
||||||
|
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||||
|
if($file['size'] > $maxSize){
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success'=>false,'error'=>'Ukuran bukti organisasi maksimal 5MB']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedMimes = ['image/jpeg', 'image/png', 'image/svg+xml', 'application/pdf'];
|
||||||
|
$allowedExts = ['jpg', 'jpeg', 'png', 'svg', 'pdf'];
|
||||||
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
$mime = null;
|
||||||
|
if(function_exists('finfo_open')){
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
} else {
|
||||||
|
$mime = $file['type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!in_array($mime, $allowedMimes) || !in_array($ext, $allowedExts)){
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success'=>false,'error'=>'Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Verify code matches what was sent (simple check: hash it and compare)
|
// Verify code matches what was sent (simple check: hash it and compare)
|
||||||
// In production, store code server-side with TTL
|
// In production, store code server-side with TTL
|
||||||
@@ -85,23 +116,18 @@ $newId = $conn->insert_id;
|
|||||||
$ins->close();
|
$ins->close();
|
||||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||||
|
|
||||||
// handle file upload (org_proof) - validate extension + mime + size
|
// handle file upload (org_proof) - already validated above
|
||||||
$orgProofPath = null;
|
$orgProofPath = null;
|
||||||
if(isset($_FILES['org_proof']) && $_FILES['org_proof']['error'] === UPLOAD_ERR_OK){
|
if(isset($_FILES['org_proof']) && $_FILES['org_proof']['error'] === UPLOAD_ERR_OK){
|
||||||
$file = $_FILES['org_proof'];
|
$file = $_FILES['org_proof'];
|
||||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
$uDir = __DIR__ . '/../../uploads/org_proofs';
|
||||||
$allowed = ['image/jpeg'=>['jpg','jpeg'],'image/png'=>['png'],'image/svg+xml'=>['svg'],'application/pdf'=>['pdf']];
|
if(!is_dir($uDir)) { @mkdir($uDir, 0755, true); @chmod($uDir, 0755); }
|
||||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
$mime = $file['type'];
|
$safeName = preg_replace('/[^a-zA-Z0-9._-]/','_', basename($file['name']));
|
||||||
if($file['size'] > 0 && $file['size'] <= $maxSize && isset($allowed[$mime]) && in_array($ext, $allowed[$mime])){
|
$dst = $uDir . '/' . time() . '_' . bin2hex(random_bytes(6)) . '_' . $safeName;
|
||||||
$uDir = __DIR__ . '/../../uploads/org_proofs';
|
if(move_uploaded_file($file['tmp_name'], $dst)){
|
||||||
if(!is_dir($uDir)) { @mkdir($uDir, 0755, true); @chmod($uDir, 0755); }
|
$orgProofPath = 'uploads/org_proofs/' . basename($dst);
|
||||||
$safeName = preg_replace('/[^a-zA-Z0-9._-]/','_', basename($file['name']));
|
try{ $up = $conn->prepare('UPDATE users SET org_proof_path = ? WHERE id = ?'); if($up){ $up->bind_param('si', $orgProofPath, $newId); $up->execute(); $up->close(); } }catch(Exception $e){}
|
||||||
$dst = $uDir . '/' . time() . '_' . bin2hex(random_bytes(6)) . '_' . $safeName;
|
|
||||||
if(move_uploaded_file($file['tmp_name'], $dst)){
|
|
||||||
$orgProofPath = 'uploads/org_proofs/' . basename($dst);
|
|
||||||
try{ $up = $conn->prepare('UPDATE users SET org_proof_path = ? WHERE id = ?'); if($up){ $up->bind_param('si', $orgProofPath, $newId); $up->execute(); $up->close(); } }catch(Exception $e){}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,24 +22,91 @@ if(!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)){
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limiting table creation and validation
|
||||||
|
try {
|
||||||
|
$conn->query("CREATE TABLE IF NOT EXISTS verification_attempts (
|
||||||
|
email VARCHAR(255) PRIMARY KEY,
|
||||||
|
attempts INT NOT NULL DEFAULT 0,
|
||||||
|
last_attempt_at DATETIME NOT NULL,
|
||||||
|
locked_until DATETIME NULL
|
||||||
|
)");
|
||||||
|
} catch (Exception $e) {}
|
||||||
|
|
||||||
|
$nowStr = date('Y-m-d H:i:s');
|
||||||
|
$nowTime = time();
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("SELECT attempts, last_attempt_at, locked_until FROM verification_attempts WHERE email = ?");
|
||||||
|
$stmt->bind_param('s', $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
$row = $res ? $res->fetch_assoc() : null;
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if($row){
|
||||||
|
$attempts = intval($row['attempts']);
|
||||||
|
$lastAttempt = strtotime($row['last_attempt_at']);
|
||||||
|
$lockedUntil = $row['locked_until'] ? strtotime($row['locked_until']) : null;
|
||||||
|
|
||||||
|
if($lockedUntil && $nowTime < $lockedUntil){
|
||||||
|
$diff = $lockedUntil - $nowTime;
|
||||||
|
$hours = floor($diff / 3600);
|
||||||
|
$mins = ceil(($diff % 3600) / 60);
|
||||||
|
http_response_code(429);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'locked_24h',
|
||||||
|
'message' => "Batas pengiriman kode tercapai. Anda harus menunggu {$hours} jam {$mins} menit.",
|
||||||
|
'locked_until' => $lockedUntil * 1000
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($nowTime - $lastAttempt < 30){
|
||||||
|
$remaining = 30 - ($nowTime - $lastAttempt);
|
||||||
|
http_response_code(429);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'cooldown_active',
|
||||||
|
'message' => "Silakan tunggu {$remaining} detik sebelum mengirim kembali.",
|
||||||
|
'remaining' => $remaining
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate 6-digit code
|
// Generate 6-digit code
|
||||||
$verificationCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
$verificationCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||||
$codeHash = hash('sha256', $verificationCode);
|
$codeHash = hash('sha256', $verificationCode);
|
||||||
|
|
||||||
// Store in session-like temp file (can use Redis/cache in production)
|
$mailSuccess = false;
|
||||||
// For now, we'll return it and store in frontend state
|
|
||||||
// But also can check email format is valid
|
|
||||||
|
|
||||||
try{
|
try{
|
||||||
$subject = 'Kode Verifikasi Email - WebGIS';
|
$subject = 'Kode Verifikasi Email - WebGIS';
|
||||||
$message = "Kode verifikasi Anda adalah:\n\n" . $verificationCode . "\n\nKode ini berlaku selama 15 menit. Jangan bagikan kode ini kepada siapapun.\n\nJika Anda tidak melakukan pendaftaran ini, abaikan email ini.";
|
$message = "Kode verifikasi Anda adalah:\n\n" . $verificationCode . "\n\nKode ini berlaku selama 15 menit. Jangan bagikan kode ini kepada siapapun.\n\nJika Anda tidak melakukan pendaftaran ini, abaikan email ini.";
|
||||||
send_mail_notify($email, $subject, $message, false);
|
$mailSuccess = send_mail_notify($email, $subject, $message, false);
|
||||||
}catch(Exception $e){
|
}catch(Exception $e){
|
||||||
// Email send failed but still return code for testing
|
error_log('Mailing exception: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update/Insert attempts on database
|
||||||
|
if($row){
|
||||||
|
$newAttempts = ($row['locked_until'] && $nowTime >= strtotime($row['locked_until'])) ? 1 : ($row['attempts'] + 1);
|
||||||
|
$newLockedUntil = ($newAttempts >= 3) ? date('Y-m-d H:i:s', $nowTime + 24 * 3600) : null;
|
||||||
|
|
||||||
|
$up = $conn->prepare("UPDATE verification_attempts SET attempts = ?, last_attempt_at = ?, locked_until = ? WHERE email = ?");
|
||||||
|
$up->bind_param('isss', $newAttempts, $nowStr, $newLockedUntil, $email);
|
||||||
|
$up->execute();
|
||||||
|
$up->close();
|
||||||
|
} else {
|
||||||
|
$one = 1;
|
||||||
|
$nullVal = null;
|
||||||
|
$ins = $conn->prepare("INSERT INTO verification_attempts (email, attempts, last_attempt_at, locked_until) VALUES (?, ?, ?, ?)");
|
||||||
|
$ins->bind_param('siss', $email, $one, $nowStr, $nullVal);
|
||||||
|
$ins->execute();
|
||||||
|
$ins->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = ['success'=>true,'email'=>$email,'message'=>'code_sent'];
|
$response = ['success'=>true,'email'=>$email,'message'=>'code_sent'];
|
||||||
if(getenv('DEBUG_MODE') === '1' || $_SERVER['REMOTE_ADDR'] === '127.0.0.1'){
|
if(getenv('DEBUG_MODE') === '1' || $_SERVER['REMOTE_ADDR'] === '127.0.0.1' || !$mailSuccess){
|
||||||
$response['debug_code'] = $verificationCode;
|
$response['debug_code'] = $verificationCode;
|
||||||
}
|
}
|
||||||
echo json_encode($response);
|
echo json_encode($response);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function load_dotenv($path){
|
|||||||
if((substr($v,0,1)==='"' && substr($v,-1)==='"') || (substr($v,0,1)==="'" && substr($v,-1)==="'")){
|
if((substr($v,0,1)==='"' && substr($v,-1)==='"') || (substr($v,0,1)==="'" && substr($v,-1)==="'")){
|
||||||
$v = substr($v,1,-1);
|
$v = substr($v,1,-1);
|
||||||
}
|
}
|
||||||
if(getenv($k) === false){
|
if(getenv($k) === false || getenv($k) === ''){
|
||||||
putenv("$k=$v");
|
putenv("$k=$v");
|
||||||
$_ENV[$k] = $v;
|
$_ENV[$k] = $v;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function load_dotenv($path){
|
|||||||
if((substr($v,0,1)==='"' && substr($v,-1)==='"') || (substr($v,0,1)==="'" && substr($v,-1)==="'")){
|
if((substr($v,0,1)==='"' && substr($v,-1)==='"') || (substr($v,0,1)==="'" && substr($v,-1)==="'")){
|
||||||
$v = substr($v,1,-1);
|
$v = substr($v,1,-1);
|
||||||
}
|
}
|
||||||
if(getenv($k) === false){
|
if(getenv($k) === false || getenv($k) === ''){
|
||||||
putenv("$k=$v");
|
putenv("$k=$v");
|
||||||
$_ENV[$k] = $v;
|
$_ENV[$k] = $v;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user