Architecture Overview
Plugin Structure
bp-lock/
├── admin/ # Admin functionality
│ ├── class-bp-lock-admin.php
│ ├── css/ # Admin styles
│ ├── js/ # Admin scripts
│ └── templates/ # Admin templates
├── public/ # Frontend functionality
│ ├── class-bp-lock-public.php
│ ├── css/ # Public styles
│ ├── js/ # Public scripts
│ └── templates/ # Public templates
├── includes/ # Core functionality
│ ├── class-bp-lock.php
│ ├── class-bp-lock-activator.php
│ └── class-bp-lock-deactivator.php
├── docs/ # Documentation
└── bp-lock.php # Main plugin file
- MVC Pattern: Separation of logic, templates, and data
- Singleton Pattern: Main plugin class instance
- Hook-Driven: WordPress action/filter system
- OOP Structure: Class-based architecture
Core Classes
Bp_Lock
Main plugin class that coordinates between admin and public functionality.
class Bp_Lock {
protected $loader;
protected $plugin_name;
protected $version;
public $locked_content_template;
public function __construct() {
$this->version = BPLOCK_PLUGIN_VERSION;
$this->plugin_name = 'bp-lock';
$this->locked_content_template = BPLOCK_PLUGIN_PATH . 'public/templates/bplock-locked-content-template.php';
}
}
Bp_Lock_Admin
Handles all admin-side functionality.
class Bp_Lock_Admin {
private $plugin_name;
private $version;
public $plugin_settings_tabs;
public function bplock_general_settings() {
// Registers settings tabs
}
public function bplock_register_settings_function() {
register_setting('bplock_general_settings', 'bplock_general_settings');
}
}
Bp_Lock_Public
Manages frontend protection logic.
class Bp_Lock_Public {
public function bplock_lock_pages($template) {
// Page protection logic
}
public function bplock_lock_complete_site($template) {
// Full site protection
}
private function bplock_apply_protection($template, $general_settings) {
// Apply protection method
}
}
Hooks & Filters
Action Hooks
bplock_before_apply_protection
Fires before applying protection to a page.
do_action('bplock_before_apply_protection', $lr_form, $general_settings);
// Usage example:
add_action('bplock_before_apply_protection', function($method, $settings) {
// Log protection events
error_log("Protecting page with method: " . $method);
}, 10, 2);
bplock_protecting_pageFires when protecting a specific page.
do_action('bplock_protecting_page', $current_url, $general_settings);
// Usage example:
add_action('bplock_protecting_page', function($url, $settings) {
// Track protected page visits
do_action('my_analytics_track', 'protected_page_visit', $url);
}, 10, 2);
Filter Hooks
bplock_protection_method
Modify the protection method dynamically.
apply_filters('bplock_protection_method', $lr_form, $general_settings);
// Usage example:
add_filter('bplock_protection_method', function($method, $settings) {
// Force redirect for specific conditions
if (is_mobile()) {
return 'page_redirect';
}
return $method;
}, 10, 2);
bplock_is_page_protected
Override protection status for specific pages.
apply_filters('bplock_is_page_protected', $protected, $current_url, $general_settings);
// Usage example:
add_filter('bplock_is_page_protected', function($protected, $url, $settings) {
// Always protect admin-ajax requests
if (strpos($url, 'admin-ajax.php') !== false) {
return false; // Don't protect AJAX
}
return $protected;
}, 10, 3);
bplock_locked_urlsModify the list of protected URLs.
apply_filters('bplock_locked_urls', $locked_urls, $current_url);
// Usage example:
add_filter('bplock_locked_urls', function($urls, $current) {
// Add dynamic URLs
$urls[] = '/user/' . get_current_user_id() . '/private';
return $urls;
}, 10, 2);
bplock_whitelist_urls
Modify whitelist URLs.
apply_filters('bplock_whitelist_urls', $urls, $current_url);
// Usage example:
add_filter('bplock_whitelist_urls', function($urls, $current) {
// Add WooCommerce checkout to whitelist
if (class_exists('WooCommerce')) {
$urls[] = '/checkout';
$urls[] = '/cart';
}
return $urls;
}, 10, 2);
bplock_redirect_urlChange the redirect destination.
apply_filters('bplock_redirect_url', $redirepage_url, $redirepage);
// Usage example:
add_filter('bplock_redirect_url', function($url, $page_id) {
// Redirect to custom login with return URL
return add_query_arg('redirect_to', $_SERVER['REQUEST_URI'], $url);
}, 10, 2);
bplock_protected_templateOverride the template used for protected content.
apply_filters('bplock_protected_template', $template, $lr_form, $general_settings);
// Usage example:
add_filter('bplock_protected_template', function($template, $method, $settings) {
// Use custom template for specific pages
if (is_page('premium')) {
return get_template_directory() . '/custom-locked.php';
}
return $template;
}, 10, 3);
bplock_page_templateFinal template filter for any page.
apply_filters('bplock_page_template', $template, $protected, $general_settings);
// Usage example:
add_filter('bplock_page_template', function($template, $protected, $settings) {
// Log all template decisions
do_action('log_template_decision', $template, $protected);
return $template;
}, 10, 3);
Database Structure
Options Table
bplock_general_settings
Main settings array stored as serialized option.
$settings = array(
// Partial Protection
'bp-components' => array('members', 'groups', 'activity'),
'locked-urls' => "/members\n/private/*\n/premium",
// Full Protection
'bplock-complete-site' => 'on',
'bplock-whitelist-urls' => "/\nwp-login.php\n/api/*",
// Protection Rules
'lr-form' => 'plugin_form', // or 'custom_form', 'page_redirect'
'locked_content' => '<p>This content is protected.</p>',
'custom_form_content' => '[custom_login_form]',
'logout_redirect_page' => 123, // Page ID
);
Legacy Options (Backward Compatibility)
// Old options still checked for compatibility
get_option('bp-components'); // Array of component slugs
get_option('bplock-pages'); // Array of page IDs
get_option('bplock-complete-lock'); // 'on' or false
API Reference
Public Functions
Check Protection Status
/**
* Check if current page should be protected
*
* @return bool
*/
function bplock_is_current_page_protected() {
global $bplock;
$public = new Bp_Lock_Public($bplock->plugin_name, $bplock->version);
// Check protection logic
$settings = get_option('bplock_general_settings', array());
if (is_user_logged_in()) {
return false;
}
// Full protection check
if (isset($settings['bplock-complete-site']) && $settings['bplock-complete-site'] === 'on') {
return !$public->bplock_is_whitelisted_page();
}
// Partial protection check
// ... implement URL checking logic
return false;
}
Get Protected URLs
/**
* Get all protected URLs
*
* @return array
*/
function bplock_get_protected_urls() {
$settings = get_option('bplock_general_settings', array());
$urls = array();
if (isset($settings['locked-urls'])) {
$urls = explode("\n", $settings['locked-urls']);
$urls = array_map('trim', $urls);
$urls = array_filter($urls, function($url) {
return !empty($url) && strpos($url, '#') !== 0;
});
}
return apply_filters('bplock_protected_urls_list', $urls);
}
Check URL Pattern Match
/**
* Check if URL matches pattern
*
* @param string $url URL to check
* @param string $pattern Pattern with wildcards
* @return bool
*/
function bplock_url_matches_pattern($url, $pattern) {
// Handle wildcards
if (strpos($pattern, '*') !== false) {
$regex_pattern = str_replace('*', '.*', preg_quote($pattern, '/'));
return preg_match('/^' . $regex_pattern . '$/i', $url);
}
// Exact match (ignore trailing slashes)
return trim($url, '/') === trim($pattern, '/');
}
Extending the Plugin
Custom Protection Method
// Add custom protection method
add_filter('bplock_protection_method', function($method, $settings) {
// Check for custom condition
if (isset($_GET['special_access'])) {
return 'my_custom_method';
}
return $method;
}, 10, 2);
// Handle custom method
add_filter('bplock_protected_template', function($template, $method, $settings) {
if ($method === 'my_custom_method') {
// Return custom template
return plugin_dir_path(__FILE__) . 'templates/special-access.php';
}
return $template;
}, 10, 3);
Role-Based Protection
// Add role-based protection
add_filter('bplock_is_page_protected', function($protected, $url, $settings) {
// Allow specific roles even when logged in
if (is_user_logged_in()) {
$user = wp_get_current_user();
// Protect from subscribers
if (in_array('subscriber', $user->roles) && strpos($url, '/premium') !== false) {
return true;
}
}
return $protected;
}, 10, 3);
Custom Whitelist Rules
// Dynamic whitelist based on conditions
add_filter('bplock_whitelist_urls', function($urls) {
// Add time-based whitelist
$current_hour = date('H');
if ($current_hour >= 9 && $current_hour <= 17) {
$urls[] = '/business-hours-content';
}
// Add user-specific whitelist
if (isset($_COOKIE['special_token'])) {
$urls[] = '/token-protected-content';
}
return $urls;
});
Integration with Membership Plugins
// Integrate with membership plugin
add_filter('bplock_is_page_protected', function($protected, $url, $settings) {
// Check membership level
if (function_exists('pmpro_hasMembershipLevel')) {
if (pmpro_hasMembershipLevel()) {
return false; // Has membership, allow access
}
}
return $protected;
}, 10, 3);
Code Examples
Example 1: Protect WooCommerce Products
// Protect all WooCommerce products for non-customers
add_filter('bplock_is_page_protected', function($protected, $url) {
if (is_product()) {
// Check if user has purchased before
if (is_user_logged_in()) {
$customer = new WC_Customer(get_current_user_id());
if ($customer->get_order_count() == 0) {
return true; // No orders, protect
}
} else {
return true; // Not logged in, protect
}
}
return $protected;
}, 10, 2);
Example 2: Custom Login Form
// Create custom login form template
add_filter('bplock_protected_template', function($template, $method) {
if ($method === 'custom_form') {
// Check if on specific page type
if (is_singular('course')) {
return plugin_dir_path(__FILE__) . 'templates/course-login.php';
}
}
return $template;
}, 10, 2);
Example 3: Logging Protection Events
// Log all protection events
add_action('bplock_protecting_page', function($url, $settings) {
$log_entry = array(
'timestamp' => current_time('mysql'),
'url' => $url,
'ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'method' => $settings['lr-form'] ?? 'unknown'
);
// Save to custom table or file
error_log(json_encode($log_entry), 3, WP_CONTENT_DIR . '/protection.log');
}, 10, 2);
Example 4: API Endpoint Protection
// Protect REST API endpoints
add_filter('rest_authentication_errors', function($result) {
// Check if Private Community protection should apply
$settings = get_option('bplock_general_settings', array());
if (isset($settings['bplock-complete-site']) && $settings['bplock-complete-site'] === 'on') {
if (!is_user_logged_in()) {
return new WP_Error(
'rest_forbidden',
__('REST API requires authentication.', 'bp-lock'),
array('status' => 401)
);
}
}
return $result;
});
Development Workflow
Setting Up Development Environment
# Clone repository
git clone https://github.com/wbcomdesigns/bp-lock.git
# Install dependencies
composer install
npm install
# Set up local WordPress
wp core download
wp config create --dbname=bplock_dev --dbuser=root
wp core install --url=bplock.test --title="BP Lock Dev"
Code Standards
PHP Standards
- Follow WordPress Coding Standards
- Use PHP 7.4+ features
- Implement proper escaping and sanitization
// Good
$url = esc_url($_POST['url']);
$text = sanitize_text_field($_POST['text']);
echo esc_html($output);
// Bad
$url = $_POST['url'];
echo $output;
JavaScript Standards
- Use ES6+ syntax
- Follow WordPress JavaScript standards
- Implement proper error handling
// Good
const settings = {
protected: true,
method: 'plugin_form'
};
// Use promises or async/await
async function checkProtection() {
try {
const response = await fetch('/wp-json/bplock/v1/check');
return response.json();
} catch (error) {
console.error('Protection check failed:', error);
}
}
Testing
Unit Testing
class BPLockTest extends WP_UnitTestCase {
public function test_url_pattern_matching() {
$this->assertTrue(
bplock_url_matches_pattern('/blog/post-1', '/blog/*')
);
$this->assertFalse(
bplock_url_matches_pattern('/page', '/blog/*')
);
}
}
Integration Testing
public function test_protection_with_full_lock() {
// Set up full protection
update_option('bplock_general_settings', array(
'bplock-complete-site' => 'on',
'bplock-whitelist-urls' => '/'
));
// Test as guest
wp_set_current_user(0);
$this->assertTrue(bplock_is_current_page_protected());
// Test as logged in
wp_set_current_user(1);
$this->assertFalse(bplock_is_current_page_protected());
}
Debugging
Enable Debug Mode
// wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true);
Debug Functions
// Debug protection decisions
add_action('bplock_protecting_page', function($url) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('BP Lock: Protecting URL: ' . $url);
error_log('BP Lock: Stack trace: ' . print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true));
}
});
Performance Optimization
Caching Protection Checks
// Cache protection status
function bplock_is_protected_cached($url) {
$cache_key = 'bplock_protected_' . md5($url);
$cached = wp_cache_get($cache_key, 'bplock');
if ($cached !== false) {
return $cached;
}
$protected = bplock_is_page_protected($url);
wp_cache_set($cache_key, $protected, 'bplock', 300); // 5 minutes
return $protected;
}
Database Query Optimization
// Optimize settings retrieval
function bplock_get_settings() {
static $settings = null;
if ($settings === null) {
$settings = get_option('bplock_general_settings', array());
}
return $settings;
}
Security Best Practices
Input Validation
// Always validate and sanitize input
$url = isset($_POST['url']) ? esc_url_raw($_POST['url']) : '';
$text = isset($_POST['text']) ? sanitize_text_field($_POST['text']) : '';
$html = isset($_POST['html']) ? wp_kses_post($_POST['html']) : '';
Nonce Verification
// Always verify nonces
if (!wp_verify_nonce($_POST['_wpnonce'], 'bplock_action')) {
wp_die(__('Security check failed', 'bp-lock'));
}
Capability Checks
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_die(__('Insufficient permissions', 'bp-lock'));
}
SQL Injection Prevention
// Use prepared statements
global $wpdb;
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
'page',
'publish'
)
);
Troubleshooting
Common Issues
Protection Not Working
- Check if settings are saved correctly
- Verify hooks are properly registered
- Clear all caches
- Check for conflicting plugins
Redirect Loops
- Ensure login page is whitelisted
- Check redirect URL is not protected
- Verify cookies are working
Performance Issues
- Implement caching for protection checks
- Optimize regex patterns
- Reduce number of protected URLs
- Use static variables for repeated checks
Contributing
Pull Request Process
- Fork the repository
- Create feature branch
- Commit changes with clear messages
- Write/update tests
- Update documentation
- Submit pull request
Code Review Checklist
- Follows WordPress coding standards
- Includes proper documentation
- Has unit tests
- Security measures implemented
- Backward compatible
- Performance optimized
