1, 'block_sensitive_files' => 1, 'block_php_in_uploads' => 1, 'disable_server_signature' => 1, 'prevent_clickjacking' => 1, 'prevent_mime_sniffing' => 1, 'xss_protection' => 1, 'disable_trace_track' => 1, 'referrer_policy' => 'strict-origin-when-cross-origin', 'hsts_enabled' => 0, 'hsts_max_age' => 31536000, 'hsts_subdomains' => 0, 'csp_enabled' => 0, 'csp_value' => '', 'permissions_policy' => 0, 'permissions_value' => '', // Performance 'enable_gzip' => 1, 'enable_expires' => 1, 'expires_html' => 3600, 'expires_css_js' => 2592000, 'expires_images' => 31536000, 'etag_control' => 0, // SEO 'www_redirect' => 'off', 'redirect_index_php' => 1, 'force_trailing_slash' => 0, // Custom 'custom_rules' => '', ]; /** * Get saved options or defaults. */ public function getOptions(): array { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite')) ->where($db->quoteName('type') . ' = ' . $db->quote('component')); $db->setQuery($query); $params = new Registry($db->loadResult() ?? '{}'); $htaccess = $params->get('htaccess', null); if ($htaccess) { return array_merge(self::DEFAULTS, (array) json_decode(json_encode($htaccess), true)); } return self::DEFAULTS; } /** * Save options to component params. */ public function saveOptions(array $options): array { try { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('params')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite')) ->where($db->quoteName('type') . ' = ' . $db->quote('component')); $db->setQuery($query); $params = new Registry($db->loadResult() ?? '{}'); $clean = []; foreach (self::DEFAULTS as $key => $default) { $clean[$key] = $options[$key] ?? $default; } $params->set('htaccess', $clean); $db->setQuery( $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite')) ->where($db->quoteName('type') . ' = ' . $db->quote('component')) )->execute(); return ['success' => true, 'message' => 'Options saved.']; } catch (\Throwable $e) { return ['success' => false, 'message' => 'Save failed: ' . $e->getMessage()]; } } /** * Read the current .htaccess file. */ public function readCurrentHtaccess(): string { $path = JPATH_ROOT . '/.htaccess'; return file_exists($path) ? file_get_contents($path) : ''; } /** * Write .htaccess to disk with backup. */ public function saveHtaccess(string $content): array { $path = JPATH_ROOT . '/.htaccess'; $backup = JPATH_ROOT . '/.htaccess.mokosuite.bak'; try { // Backup existing if (file_exists($path)) { copy($path, $backup); } $result = file_put_contents($path, $content); if ($result === false) { // Restore backup if (file_exists($backup)) { copy($backup, $path); } return ['success' => false, 'message' => '.htaccess is not writable.']; } return ['success' => true, 'message' => '.htaccess saved. Backup at .htaccess.mokosuite.bak']; } catch (\Throwable $e) { if (file_exists($backup)) { @copy($backup, $path); } return ['success' => false, 'message' => 'Write failed: ' . $e->getMessage()]; } } /** * Generate .htaccess content from options. */ public function generateHtaccess(array $opts): string { $lines = []; $lines[] = '##'; $lines[] = '## MokoSuite Generated .htaccess'; $lines[] = '## Generated: ' . gmdate('Y-m-d H:i:s') . ' UTC'; $lines[] = '## DO NOT EDIT — regenerate from MokoSuite > .htaccess Maker'; $lines[] = '##'; $lines[] = ''; // --- Security --- if (!empty($opts['disable_directory_listing'])) { $lines[] = '## Disable directory listing'; $lines[] = 'Options -Indexes'; $lines[] = ''; } if (!empty($opts['disable_server_signature'])) { $lines[] = '## Hide server signature'; $lines[] = 'ServerSignature Off'; $lines[] = ''; $lines[] = ' Header unset X-Powered-By'; $lines[] = ' Header unset Server'; $lines[] = ''; $lines[] = ''; } if (!empty($opts['block_sensitive_files'])) { $lines[] = '## Block access to sensitive files'; $lines[] = ''; $lines[] = ' '; $lines[] = ' Require all denied'; $lines[] = ' '; $lines[] = ''; $lines[] = ''; } if (!empty($opts['block_php_in_uploads'])) { $lines[] = '## Block PHP execution in upload directories'; $dirs = ['images', 'media', 'tmp', 'cache', 'logs']; foreach ($dirs as $dir) { $lines[] = ''; $lines[] = ' '; $lines[] = ' '; $lines[] = ' Require all denied'; $lines[] = ' '; $lines[] = ' '; $lines[] = ''; } $lines[] = ''; } if (!empty($opts['disable_trace_track'])) { $lines[] = '## Disable TRACE and TRACK methods'; $lines[] = ''; $lines[] = ' RewriteEngine On'; $lines[] = ' RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)'; $lines[] = ' RewriteRule .* - [F]'; $lines[] = ''; $lines[] = ''; } // Security headers $headers = []; if (!empty($opts['prevent_clickjacking'])) { $headers[] = ' Header always set X-Frame-Options "SAMEORIGIN"'; } if (!empty($opts['prevent_mime_sniffing'])) { $headers[] = ' Header always set X-Content-Type-Options "nosniff"'; } if (!empty($opts['xss_protection'])) { $headers[] = ' Header always set X-XSS-Protection "1; mode=block"'; } $referrer = $opts['referrer_policy'] ?? ''; if (!empty($referrer) && $referrer !== 'off') { $headers[] = ' Header always set Referrer-Policy "' . $referrer . '"'; } if (!empty($opts['hsts_enabled'])) { $maxAge = (int) ($opts['hsts_max_age'] ?? 31536000); $hsts = 'max-age=' . $maxAge; if (!empty($opts['hsts_subdomains'])) { $hsts .= '; includeSubDomains'; } $headers[] = ' Header always set Strict-Transport-Security "' . $hsts . '"'; } if (!empty($opts['csp_enabled']) && !empty($opts['csp_value'])) { $headers[] = ' Header always set Content-Security-Policy "' . str_replace('"', '', $opts['csp_value']) . '"'; } if (!empty($opts['permissions_policy']) && !empty($opts['permissions_value'])) { $headers[] = ' Header always set Permissions-Policy "' . str_replace('"', '', $opts['permissions_value']) . '"'; } if (!empty($headers)) { $lines[] = '## Security headers'; $lines[] = ''; $lines = array_merge($lines, $headers); $lines[] = ''; $lines[] = ''; } // --- Performance --- if (!empty($opts['enable_gzip'])) { $lines[] = '## GZip compression'; $lines[] = ''; $lines[] = ' AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css'; $lines[] = ' AddOutputFilterByType DEFLATE text/javascript application/javascript application/x-javascript'; $lines[] = ' AddOutputFilterByType DEFLATE application/json application/xml application/rss+xml'; $lines[] = ' AddOutputFilterByType DEFLATE image/svg+xml application/font-woff application/font-woff2'; $lines[] = ''; $lines[] = ''; } if (!empty($opts['enable_expires'])) { $html = (int) ($opts['expires_html'] ?? 3600); $cssJs = (int) ($opts['expires_css_js'] ?? 2592000); $images = (int) ($opts['expires_images'] ?? 31536000); $lines[] = '## Browser caching'; $lines[] = ''; $lines[] = ' ExpiresActive On'; $lines[] = ' ExpiresDefault "access plus ' . $html . ' seconds"'; $lines[] = ' ExpiresByType text/html "access plus ' . $html . ' seconds"'; $lines[] = ' ExpiresByType text/css "access plus ' . $cssJs . ' seconds"'; $lines[] = ' ExpiresByType text/javascript "access plus ' . $cssJs . ' seconds"'; $lines[] = ' ExpiresByType application/javascript "access plus ' . $cssJs . ' seconds"'; $lines[] = ' ExpiresByType image/jpeg "access plus ' . $images . ' seconds"'; $lines[] = ' ExpiresByType image/png "access plus ' . $images . ' seconds"'; $lines[] = ' ExpiresByType image/gif "access plus ' . $images . ' seconds"'; $lines[] = ' ExpiresByType image/webp "access plus ' . $images . ' seconds"'; $lines[] = ' ExpiresByType image/svg+xml "access plus ' . $images . ' seconds"'; $lines[] = ' ExpiresByType font/woff2 "access plus ' . $images . ' seconds"'; $lines[] = ''; $lines[] = ''; } if (!empty($opts['etag_control'])) { $lines[] = '## Disable ETags (for load-balanced environments)'; $lines[] = ''; $lines[] = ' Header unset ETag'; $lines[] = ''; $lines[] = 'FileETag None'; $lines[] = ''; } // --- SEO / Redirects --- $wwwRedirect = $opts['www_redirect'] ?? 'off'; if ($wwwRedirect !== 'off' || !empty($opts['redirect_index_php']) || !empty($opts['force_trailing_slash'])) { $lines[] = '## SEO redirects'; $lines[] = ''; $lines[] = ' RewriteEngine On'; if ($wwwRedirect === 'www') { $lines[] = ''; $lines[] = ' ## Force www'; $lines[] = ' RewriteCond %{HTTP_HOST} !^www\. [NC]'; $lines[] = ' RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$1 [R=301,L]'; } elseif ($wwwRedirect === 'non-www') { $lines[] = ''; $lines[] = ' ## Force non-www'; $lines[] = ' RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]'; $lines[] = ' RewriteRule ^(.*)$ https://%1/$1 [R=301,L]'; } if (!empty($opts['redirect_index_php'])) { $lines[] = ''; $lines[] = ' ## Redirect /index.php to root'; $lines[] = ' RewriteCond %{THE_REQUEST} ^[A-Z]{3,}\s/+index\.php\s [NC]'; $lines[] = ' RewriteRule ^index\.php/?(.*)$ /$1 [R=301,L]'; } if (!empty($opts['force_trailing_slash'])) { $lines[] = ''; $lines[] = ' ## Force trailing slash'; $lines[] = ' RewriteCond %{REQUEST_FILENAME} !-f'; $lines[] = ' RewriteCond %{REQUEST_URI} !(.*)/$'; $lines[] = ' RewriteRule ^(.*)$ /$1/ [R=301,L]'; } $lines[] = ''; $lines[] = ''; } // --- Custom rules --- $custom = trim($opts['custom_rules'] ?? ''); if (!empty($custom)) { $lines[] = '## Custom rules'; $lines[] = $custom; $lines[] = ''; } return implode("\n", $lines); } /** * Generate equivalent NginX configuration snippet. */ public function generateNginx(array $opts): string { $lines = []; $lines[] = '## MokoSuite Generated NginX Configuration'; $lines[] = '## Add these directives inside your server { } block'; $lines[] = ''; if (!empty($opts['disable_directory_listing'])) { $lines[] = '# Disable directory listing'; $lines[] = 'autoindex off;'; $lines[] = ''; } if (!empty($opts['disable_server_signature'])) { $lines[] = '# Hide server version'; $lines[] = 'server_tokens off;'; $lines[] = ''; } if (!empty($opts['block_sensitive_files'])) { $lines[] = '# Block sensitive files'; $lines[] = 'location ~* (htaccess\.txt|web\.config\.txt|configuration\.php-dist|README\.txt|LICENSE\.txt)$ {'; $lines[] = ' deny all;'; $lines[] = '}'; $lines[] = ''; } if (!empty($opts['block_php_in_uploads'])) { $lines[] = '# Block PHP in upload directories'; $lines[] = 'location ~* ^/(images|media|tmp|cache|logs)/.*\.php$ {'; $lines[] = ' deny all;'; $lines[] = '}'; $lines[] = ''; } // Headers $hdrs = []; if (!empty($opts['prevent_clickjacking'])) { $hdrs[] = 'add_header X-Frame-Options "SAMEORIGIN" always;'; } if (!empty($opts['prevent_mime_sniffing'])) { $hdrs[] = 'add_header X-Content-Type-Options "nosniff" always;'; } if (!empty($opts['xss_protection'])) { $hdrs[] = 'add_header X-XSS-Protection "1; mode=block" always;'; } $referrer = $opts['referrer_policy'] ?? ''; if (!empty($referrer) && $referrer !== 'off') { $hdrs[] = 'add_header Referrer-Policy "' . $referrer . '" always;'; } if (!empty($opts['hsts_enabled'])) { $maxAge = (int) ($opts['hsts_max_age'] ?? 31536000); $hsts = 'max-age=' . $maxAge; if (!empty($opts['hsts_subdomains'])) { $hsts .= '; includeSubDomains'; } $hdrs[] = 'add_header Strict-Transport-Security "' . $hsts . '" always;'; } if (!empty($hdrs)) { $lines[] = '# Security headers'; $lines = array_merge($lines, $hdrs); $lines[] = ''; } if (!empty($opts['enable_gzip'])) { $lines[] = '# GZip compression'; $lines[] = 'gzip on;'; $lines[] = 'gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;'; $lines[] = 'gzip_min_length 256;'; $lines[] = ''; } if (!empty($opts['enable_expires'])) { $cssJs = (int) ($opts['expires_css_js'] ?? 2592000); $images = (int) ($opts['expires_images'] ?? 31536000); $lines[] = '# Browser caching'; $lines[] = 'location ~* \.(css|js)$ {'; $lines[] = ' expires ' . round($cssJs / 86400) . 'd;'; $lines[] = '}'; $lines[] = 'location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|woff2)$ {'; $lines[] = ' expires ' . round($images / 86400) . 'd;'; $lines[] = '}'; $lines[] = ''; } return implode("\n", $lines); } }