In this guide, we demonstrate how to protect singular paths (for example, a single file like wp-login.php or a single folder like /admin) using a PHP script that updates your .htaccess. We’ll show:
wp-login.php) using the original <Files wp-login.php> ... </Files> approach./admin) with IP-based restrictions using an Apache <If> directive in .htaccess.wp-login.php)Below is the original script you provided, which demonstrates how to protect wp-login.php by editing the <Files wp-login.php> block in .htaccess. It also includes a simple PHP-based login to limit access to the editor page itself.
.htaccess BlockYour .htaccess file should have a section like:
<Files wp-login.php>
Order Deny,Allow
Deny from all
Allow from 1.2.3.4
</Files>
Deny from all blocks everyone.Allow from 1.2.3.4 allows IP 1.2.3.4.wp-login.php.Below is the complete PHP code. Copy/paste it into a file (e.g., manage_wp_login_ips.php) on your server. Adjust $STATIC_PASSWORD_HASH to your password hash, and $htaccessPath to the correct path to .htaccess.
<?php
session_start();
/**************************************************************
* CONFIGURATION
**************************************************************/
// Store the hashed password here (Replace this with your own!)
$STATIC_PASSWORD_HASH = '$2y$10$Lla9zPARA7Dh5siAQZFRE.mWRjr9IgHcaab6lDzm4Kz4qd.kTfwiC';
// Path to the .htaccess file in your public_html folder
$htaccessPath = '/home/store18/public_html/.htaccess'; // <-- Adjust this path
/**************************************************************
* SIMPLE PASSWORD PROTECTION CHECK
**************************************************************/
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
// User is not authenticated, check if they provided a password
if (isset($_POST['password'])) {
// Use password_verify() instead of direct string comparison
if (password_verify($_POST['password'], $STATIC_PASSWORD_HASH)) {
$_SESSION['authenticated'] = true;
header("Location: " . $_SERVER['PHP_SELF']);
exit;
} else {
$loginError = "Invalid password.";
}
}
// Display a simple login form (Bootstrap 5)
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Login Required</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h1 class="h5 text-center mb-4">Protected Access</h1>
<?php if (!empty($loginError)): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($loginError); ?></div>
<?php endif; ?>
<form method="post">
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" id="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<?php
exit;
}
/**************************************************************
* END: SIMPLE PASSWORD PROTECTION
**************************************************************/
/**************************************************************
* HELPER FUNCTIONS TO READ & UPDATE THE .HTACCESS BLOCK
**************************************************************/
/**
* Extract IP lines from the <Files wp-login.php> block.
*
* @param string $content Entire contents of .htaccess
* @return array [ 'startIndex' => int, 'endIndex' => int, 'ips' => string[] ]
*/
function parseWpLoginBlock($content) {
$lines = explode("\n", $content);
$startIndex = -1;
$endIndex = -1;
$ips = [];
// find <Files wp-login.php> start and </Files> end
for ($i = 0; $i < count($lines); $i++) {
$trimmed = trim($lines[$i]);
if (stripos($trimmed, '<Files wp-login.php>') !== false) {
$startIndex = $i;
}
if (stripos($trimmed, '</Files>') !== false && $startIndex !== -1 && $endIndex === -1) {
$endIndex = $i;
break;
}
}
// If valid start/end, parse lines in between
if ($startIndex !== -1 && $endIndex !== -1) {
for ($j = $startIndex; $j <= $endIndex; $j++) {
if (stripos(trim($lines[$j]), 'Allow from') === 0) {
// e.g. "Allow from 79.119.99.83"
$parts = preg_split('/\s+/', trim($lines[$j]));
// $parts[0] = "Allow", $parts[1] = "from", $parts[2] = "x.x.x.x"
if (isset($parts[2])) {
$ips[] = $parts[2];
}
}
}
}
return [
'startIndex' => $startIndex,
'endIndex' => $endIndex,
'ips' => $ips
];
}
/**
* Replace the IPs in the <Files wp-login.php> block with new IPs.
*
* @param string $content Entire .htaccess content
* @param array $newIPs Array of new IP strings
* @return string Updated .htaccess content
*/
function updateWpLoginBlock($content, $newIPs) {
$lines = explode("\n", $content);
$blockInfo = parseWpLoginBlock($content);
$startIndex = $blockInfo['startIndex'];
$endIndex = $blockInfo['endIndex'];
// If we cannot find the block, do nothing
if ($startIndex === -1 || $endIndex === -1) {
return $content;
}
// Rebuild the block lines
$rebuiltBlock = [];
for ($i = $startIndex; $i <= $endIndex; $i++) {
// Keep lines that are not "Allow from ...", skip the old IP lines
$trimmed = trim($lines[$i]);
if (stripos($trimmed, 'Allow from') === 0) {
continue;
}
$rebuiltBlock[] = $lines[$i];
// Right after 'Deny from all', re-inject new "Allow from" lines
if (stripos($trimmed, 'Deny from all') === 0) {
foreach ($newIPs as $ip) {
$rebuiltBlock[] = "Allow from " . $ip;
}
}
}
// Replace lines in $lines
$blockLength = ($endIndex - $startIndex) + 1;
array_splice($lines, $startIndex, $blockLength, $rebuiltBlock);
return implode("\n", $lines);
}
/**************************************************************
* PROCESS FORM SUBMISSION (SAVE NEW IP ADDRESSES)
**************************************************************/
if (isset($_POST['save_ips']) && isset($_POST['ips'])) {
// Clean the posted IPs, remove empty lines, etc.
$newIPs = array_map('trim', $_POST['ips']);
$newIPs = array_filter($newIPs, fn($ip) => $ip !== '');
// Read the .htaccess file content
if (!file_exists($htaccessPath)) {
die("Error: .htaccess not found at $htaccessPath");
}
$htaccessContent = file_get_contents($htaccessPath);
// Update the wp-login.php block
$updatedContent = updateWpLoginBlock($htaccessContent, $newIPs);
// Save changes
file_put_contents($htaccessPath, $updatedContent);
// Redirect or confirm success
header("Location: " . $_SERVER['PHP_SELF'] . "?updated=1");
exit;
}
/**************************************************************
* READ CURRENT IPs AND DISPLAY HTML FORM
**************************************************************/
$htaccessContent = file_get_contents($htaccessPath);
$blockInfo = parseWpLoginBlock($htaccessContent);
$currentIPs = $blockInfo['ips'];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Manage WordPress Login IPs</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4">
<h1 class="mb-4">Manage Allowed IPs for <code>wp-login.php</code></h1>
<?php if (isset($_GET['updated'])): ?>
<div class="alert alert-success">IP addresses have been updated successfully!</div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="save_ips" value="1" />
<table class="table table-bordered bg-white">
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
</tr>
</thead>
<tbody>
<?php
if (!empty($currentIPs)) {
foreach ($currentIPs as $index => $ip) {
?>
<tr>
<td><?php echo ($index + 1); ?></td>
<td>
<input type="text" name="ips[]" value="<?php echo htmlspecialchars($ip); ?>" class="form-control">
</td>
</tr>
<?php
}
} else {
?>
<tr>
<td>1</td>
<td>
<input type="text" name="ips[]" value="" class="form-control" placeholder="e.g. 1.2.3.4">
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<div class="mb-3">
<button type="button" class="btn btn-secondary" onclick="addRow()">+ Add another IP</button>
</div>
<button type="submit" class="btn btn-primary">Save IPs</button>
</form>
</div>
<script>
function addRow() {
const tableBody = document.querySelector('table tbody');
const rowCount = tableBody.rows.length;
const newRow = tableBody.insertRow();
let cellIndex = newRow.insertCell(0);
cellIndex.innerHTML = rowCount + 1;
let cellIP = newRow.insertCell(1);
cellIP.innerHTML = '<input type="text" name="ips[]" value="" class="form-control" placeholder="e.g. 1.2.3.4">';
}
</script>
</body>
</html>
<Files wp-login.php> in .htaccess.That’s how you protect a single file (wp-login.php) with your custom IP-based restrictions.
/admin)Now, if you want to protect an entire folder like /admin, <Files wp-login.php> won’t help, because <Files> only applies to specific filenames. For a folder, you must use different Apache directives.
<If> in .htaccess (Apache 2.4+)If your Apache version supports the <If> directive in .htaccess, you can do something like:
<If "%{REQUEST_URI} =~ m#^/admin/#">
Order Deny,Allow
Deny from all
Allow from 1.2.3.4
</If>
/admin/, deny all except the listed IP(s).Allow from X.X.X.X lines as needed.<If> BlockYour PHP script logic becomes similar to Scenario A, but you search for <If "%{REQUEST_URI} =~ m#^/admin/#"> and </If> lines, and insert or remove Allow from ... lines inside that block. For example, replace the parseWpLoginBlock() logic with something like parseAdminIfBlock(), searching for:
<If "%{REQUEST_URI} =~ m#^/admin/#"></If>And the update logic (updateWpLoginBlock()) becomes updateAdminIfBlock() that re-injects new Allow from lines after Deny from all.
Below is a simplified example (not a full final script) to illustrate:
function parseAdminIfBlock($content) {
$lines = explode("\n", $content);
$startIndex = -1;
$endIndex = -1;
$ips = [];
for ($i = 0; $i < count($lines); $i++) {
$trimmed = trim($lines[$i]);
if (stripos($trimmed, '<If "') !== false
&& stripos($trimmed, 'REQUEST_URI') !== false
&& stripos($trimmed, '/admin/') !== false) {
$startIndex = $i;
}
if (stripos($trimmed, '</If>') !== false && $startIndex !== -1 && $endIndex === -1) {
$endIndex = $i;
break;
}
}
if ($startIndex !== -1 && $endIndex !== -1) {
for ($j = $startIndex; $j <= $endIndex; $j++) {
if (stripos(trim($lines[$j]), 'Allow from') === 0) {
$parts = preg_split('/\s+/', trim($lines[$j]));
if (isset($parts[2])) {
$ips[] = $parts[2];
}
}
}
}
return [
'startIndex' => $startIndex,
'endIndex' => $endIndex,
'ips' => $ips
];
}
function updateAdminIfBlock($content, $newIPs) {
$lines = explode("\n", $content);
$blockInfo = parseAdminIfBlock($content);
$startIndex = $blockInfo['startIndex'];
$endIndex = $blockInfo['endIndex'];
if ($startIndex === -1 || $endIndex === -1) {
return $content;
}
$rebuiltBlock = [];
for ($i = $startIndex; $i <= $endIndex; $i++) {
$trimmed = trim($lines[$i]);
// Skip old "Allow from" lines
if (stripos($trimmed, 'Allow from') === 0) {
continue;
}
$rebuiltBlock[] = $lines[$i];
// Re-inject "Allow from" lines after "Deny from all"
if (stripos($trimmed, 'Deny from all') === 0) {
foreach ($newIPs as $ip) {
$rebuiltBlock[] = "Allow from " . $ip;
}
}
}
// Replace the lines in the main array
$blockLength = ($endIndex - $startIndex) + 1;
array_splice($lines, $startIndex, $blockLength, $rebuiltBlock);
return implode("\n", $lines);
}
Then you’d hook these functions into the same password-protected editor form that Scenario A used, but now referencing <If> blocks for /admin instead of <Files wp-login.php>.
<Directory> blocks in the main Apache config. This is typically done if you control the server (VPS/Dedicated). Example:
<Directory "/var/www/html/admin">
Require ip 1.2.3.4
</Directory><Files wp-login.php> or <FilesMatch> in .htaccess. <If> (Apache 2.4+), <Directory> in the main config, or a different approach.Your PHP script can be adapted in both cases by changing the block it searches for:
<Files wp-login.php> vs. <If "..."> </Files> vs. </If>).All the rest (reading IPs, rewriting lines, injecting Allow from) follows the same pattern as the original script.
This process empowers you to manage IP restrictions for singular paths—either a single file (wp-login.php) or a single folder (/admin)—from your PHP-based interface!